chore: merge main into feat/hero_view_transitions

Change-Id: I6e6316f66343b8f3ea9fe33ed3f8f3e56a6a6964
This commit is contained in:
midzelis
2026-05-14 02:30:18 +00:00
379 changed files with 25782 additions and 3260 deletions
+1 -1
View File
@@ -75,7 +75,7 @@
{ {
"label": "Build Immich CLI", "label": "Build Immich CLI",
"type": "shell", "type": "shell",
"command": "pnpm --filter cli build:dev" "command": "pnpm --filter @immich/cli build:dev"
} }
] ]
} }
@@ -16,7 +16,7 @@ services:
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data - ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- pnpm_store_server:/buildcache/pnpm-store - pnpm_store_server:/buildcache/pnpm-store
- ../plugins:/build/corePlugin - ../packages/plugins:/build/corePlugin
immich-web: immich-web:
env_file: !reset [] env_file: !reset []
immich-machine-learning: immich-machine-learning:
+1 -3
View File
@@ -30,9 +30,7 @@ machine-learning/
misc/ misc/
mobile/ mobile/
open-api/typescript-sdk/build/ packages/sdk/build/
!open-api/typescript-sdk/package.json
!open-api/typescript-sdk/package-lock.json
server/upload/ server/upload/
server/src/queries server/src/queries
+2 -2
View File
@@ -24,7 +24,7 @@ mobile/lib/infrastructure/repositories/db.repository.steps.dart linguist-generat
mobile/test/drift/main/generated/** -diff -merge mobile/test/drift/main/generated/** -diff -merge
mobile/test/drift/main/generated/** linguist-generated=true mobile/test/drift/main/generated/** linguist-generated=true
open-api/typescript-sdk/fetch-client.ts -diff -merge packages/sdk/fetch-client.ts -diff -merge
open-api/typescript-sdk/fetch-client.ts linguist-generated=true packages/sdk/fetch-client.ts linguist-generated=true
*.sh text eol=lf *.sh text eol=lf
+1 -1
View File
@@ -1,7 +1,7 @@
cli: cli:
- changed-files: - changed-files:
- any-glob-to-any-file: - any-glob-to-any-file:
- cli/src/** - packages/cli/src/**
documentation: documentation:
- changed-files: - changed-files:
+22 -22
View File
@@ -90,6 +90,11 @@ jobs:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
github_token: ${{ steps.token.outputs.token }}
- name: Create the Keystore - name: Create the Keystore
if: ${{ !github.event.pull_request.head.repo.fork }} if: ${{ !github.event.pull_request.head.repo.fork }}
env: env:
@@ -111,16 +116,8 @@ jobs:
~/.gradle/wrapper ~/.gradle/wrapper
~/.android/sdk ~/.android/sdk
mobile/android/.gradle mobile/android/.gradle
mobile/.dart_tool
key: build-mobile-gradle-${{ runner.os }}-main key: build-mobile-gradle-${{ runner.os }}-main
- name: Setup Flutter SDK
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
with:
channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml
cache: true
- name: Setup Android SDK - name: Setup Android SDK
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1 uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
with: with:
@@ -131,11 +128,10 @@ jobs:
run: flutter pub get run: flutter pub get
- name: Generate translation file - name: Generate translation file
run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart run: mise //mobile:codegen:translation
working-directory: ./mobile
- name: Generate platform APIs - name: Generate platform APIs
run: make pigeon run: mise //mobile:codegen:pigeon
working-directory: ./mobile working-directory: ./mobile
- name: Build Android App Bundle - name: Build Android App Bundle
@@ -175,7 +171,10 @@ jobs:
Download: ${{ env.APK_URL }} Download: ${{ env.APK_URL }}
<img src="https://api.qrserver.com/v1/create-qr-code/?size=240x240&data=${{ env.APK_URL }}" alt="QR code" /> <details>
<summary>QR code</summary>
<img src="https://api.qrserver.com/v1/create-qr-code/?size=240x240&data=${{ env.APK_URL }}" alt="QR code" />
</details>
Installs as a separate app (applicationId `app.alextran.immich.pr${{ github.event.pull_request.number }}`), so it coexists with the Play Store version and any other PR builds. Installs as a separate app (applicationId `app.alextran.immich.pr${{ github.event.pull_request.number }}`), so it coexists with the Play Store version and any other PR builds.
@@ -189,7 +188,6 @@ jobs:
~/.gradle/wrapper ~/.gradle/wrapper
~/.android/sdk ~/.android/sdk
mobile/android/.gradle mobile/android/.gradle
mobile/.dart_tool
key: ${{ steps.cache-gradle-restore.outputs.cache-primary-key }} key: ${{ steps.cache-gradle-restore.outputs.cache-primary-key }}
build-sign-ios: build-sign-ios:
@@ -202,6 +200,12 @@ jobs:
runs-on: macos-15 runs-on: macos-15
steps: steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Select Xcode 26 - name: Select Xcode 26
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
@@ -211,24 +215,20 @@ jobs:
ref: ${{ inputs.ref || github.sha }} ref: ${{ inputs.ref || github.sha }}
persist-credentials: false persist-credentials: false
- name: Setup Flutter SDK - name: Setup Mise
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
channel: 'stable' github_token: ${{ steps.token.outputs.token }}
flutter-version-file: ./mobile/pubspec.yaml
cache: true
- name: Install Flutter dependencies - name: Install Flutter dependencies
working-directory: ./mobile working-directory: ./mobile
run: flutter pub get run: flutter pub get
- name: Generate translation files - name: Generate translation files
run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart run: mise //mobile:codegen:translation
working-directory: ./mobile
- name: Generate platform APIs - name: Generate platform APIs
run: make pigeon run: mise //mobile:codegen:pigeon
working-directory: ./mobile
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0 uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Check for breaking API changes - name: Check for breaking API changes
uses: oasdiff/oasdiff-action/breaking@37bf9ff785c7315df88216660826e71be4cc03da # v0.0.44 uses: oasdiff/oasdiff-action/breaking@26ccb332c67a45ca649de9faf60552ef1b8260d9 # v0.0.46
with: with:
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
revision: open-api/immich-openapi-specs.json revision: open-api/immich-openapi-specs.json
+6 -6
View File
@@ -3,11 +3,11 @@ on:
push: push:
branches: [main] branches: [main]
paths: paths:
- 'cli/**' - 'packages/cli/**'
- '.github/workflows/cli.yml' - '.github/workflows/cli.yml'
pull_request: pull_request:
paths: paths:
- 'cli/**' - 'packages/cli/**'
- '.github/workflows/cli.yml' - '.github/workflows/cli.yml'
release: release:
types: [published] types: [published]
@@ -28,7 +28,7 @@ jobs:
packages: write packages: write
defaults: defaults:
run: run:
working-directory: ./cli working-directory: ./packages/cli
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
@@ -43,7 +43,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -89,7 +89,7 @@ jobs:
- name: Get package version - name: Get package version
id: package-version id: package-version
run: | run: |
version=$(jq -r '.version' cli/package.json) version=$(jq -r '.version' packages/cli/package.json)
echo "version=$version" >> "$GITHUB_OUTPUT" echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Generate docker image tags - name: Generate docker image tags
@@ -107,7 +107,7 @@ jobs:
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with: with:
file: cli/Dockerfile file: packages/cli/Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: ${{ github.event_name == 'release' }} push: ${{ github.event_name == 'release' }}
cache-from: type=gha cache-from: type=gha
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
needs: [get_body, should_run] needs: [get_body, should_run]
if: ${{ needs.should_run.outputs.should_run == 'true' }} if: ${{ needs.should_run.outputs.should_run == 'true' }}
container: container:
image: ghcr.io/immich-app/mdq:main@sha256:32abe582452b12dff55055e1d6bc24508a8f17164f9d1831db7bb70953c014c6 image: ghcr.io/immich-app/mdq:main@sha256:0a8b8867773a0f8368061f47578603f438349f8f1f28b0e16105f481e5c794e0
outputs: outputs:
checked: ${{ steps.get_checkbox.outputs.checked }} checked: ${{ steps.get_checkbox.outputs.checked }}
steps: steps:
+3 -3
View File
@@ -57,7 +57,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -70,7 +70,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
# ️ Command-line programs to run using the OS shell. # ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -83,6 +83,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with: with:
category: '/language:${{matrix.language}}' category: '/language:${{matrix.language}}'
+1 -1
View File
@@ -66,7 +66,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
+1 -1
View File
@@ -131,7 +131,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
+1 -1
View File
@@ -17,6 +17,6 @@ jobs:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
with: with:
repo-token: ${{ steps.token.outputs.token }} repo-token: ${{ steps.token.outputs.token }}
+1 -1
View File
@@ -62,7 +62,7 @@ jobs:
ref: main ref: main
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
+4 -7
View File
@@ -14,9 +14,6 @@ jobs:
contents: read contents: read
id-token: write id-token: write
packages: write packages: write
defaults:
run:
working-directory: ./open-api/typescript-sdk
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
@@ -31,15 +28,15 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
- name: Install deps - name: Install deps
run: pnpm install --frozen-lockfile run: pnpm --filter @immich/sdk install --frozen-lockfile
- name: Build - name: Build
run: pnpm build run: pnpm --filter @immich/sdk build
- name: Publish - name: Publish
run: pnpm publish --provenance --no-git-checks run: pnpm --filter @immich/sdk publish --provenance --no-git-checks
+15 -27
View File
@@ -60,38 +60,30 @@ jobs:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Flutter SDK - name: Setup Mise
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
channel: 'stable' github_token: ${{ steps.token.outputs.token }}
flutter-version-file: ./mobile/pubspec.yaml
- name: Install dependencies - name: Install dependencies
run: dart pub get run: flutter pub get
- name: Install dependencies for UI package - name: Install dependencies for UI package
run: dart pub get run: flutter pub get
working-directory: ./mobile/packages/ui working-directory: ./mobile/packages/ui
- name: Install dependencies for UI Showcase - name: Install dependencies for UI Showcase
run: dart pub get run: flutter pub get
working-directory: ./mobile/packages/ui/showcase working-directory: ./mobile/packages/ui/showcase
- name: Install DCM - name: Generate translation files
uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1 run: mise //mobile:codegen:translation
with:
github-token: ${{ steps.token.outputs.token }}
version: auto
working-directory: ./mobile
- name: Generate translation file
run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
- name: Run Build Runner - name: Run Build Runner
run: make build run: mise //mobile:codegen:dart
- name: Generate platform API - name: Generate platform API
run: make pigeon run: mise //mobile:codegen:pigeon
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
@@ -107,20 +99,16 @@ jobs:
env: env:
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }} CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
run: | run: |
echo "ERROR: Generated files not up to date! Run 'make build' and 'make pigeon' inside the mobile directory" echo "ERROR: Generated files not up to date! Run 'mise //mobile:codegen:dart' and 'mise //mobile:codegen:pigeon'"
echo "Changed files: ${CHANGED_FILES}" echo "Changed files: ${CHANGED_FILES}"
exit 1 exit 1
- name: Run dart analyze - name: Run analyze
run: dart analyze --fatal-infos run: mise //mobile:analyze
- name: Run dart format - name: Run format
run: make format run: mise //mobile:format
# TODO: Re-enable after upgrading custom_lint # TODO: Re-enable after upgrading custom_lint
# - name: Run dart custom_lint # - name: Run dart custom_lint
# run: dart run custom_lint # run: dart run custom_lint
# TODO: Use https://github.com/CQLabs/dcm-action
- name: Run DCM
run: dcm analyze lib --fatal-style --fatal-warnings
+46 -56
View File
@@ -33,14 +33,14 @@ jobs:
web: web:
- 'web/**' - 'web/**'
- 'i18n/**' - 'i18n/**'
- 'open-api/typescript-sdk/**' - 'packages/sdk/**'
- 'pnpm-lock.yaml' - 'pnpm-lock.yaml'
server: server:
- 'server/**' - 'server/**'
- 'pnpm-lock.yaml' - 'pnpm-lock.yaml'
cli: cli:
- 'cli/**' - 'packages/cli/**'
- 'open-api/typescript-sdk/**' - 'packages/sdk/**'
- 'pnpm-lock.yaml' - 'pnpm-lock.yaml'
e2e: e2e:
- 'e2e/**' - 'e2e/**'
@@ -79,7 +79,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -95,7 +95,7 @@ jobs:
contents: read contents: read
defaults: defaults:
run: run:
working-directory: ./cli working-directory: ./packages/cli
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
@@ -110,7 +110,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -126,7 +126,7 @@ jobs:
contents: read contents: read
defaults: defaults:
run: run:
working-directory: ./cli working-directory: ./packages/cli
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
@@ -140,20 +140,15 @@ jobs:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup Mise
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version-file: '.nvmrc' github_token: ${{ steps.token.outputs.token }}
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
- name: Install deps - name: Run setup @immich/sdk
run: mise run //:sdk:install && mise run //:sdk:build
- name: Run pnpm install
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
# Skip linter & formatter in Windows test. # Skip linter & formatter in Windows test.
@@ -190,11 +185,11 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
- name: Run setup typescript-sdk - name: Run setup @immich/sdk
run: mise run //:sdk:install && mise run //:sdk:build run: mise run //:sdk:install && mise run //:sdk:build
- name: Run pnpm install - name: Run pnpm install
@@ -228,7 +223,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -256,15 +251,15 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
- name: Install dependencies - name: Install dependencies
run: pnpm --filter=immich-i18n install --frozen-lockfile run: pnpm -w install --frozen-lockfile
- name: Format - name: Format
run: pnpm --filter=immich-i18n format:fix run: pnpm format:fix
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
@@ -306,7 +301,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -339,7 +334,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -384,20 +379,14 @@ jobs:
cache: 'pnpm' cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml' cache-dependency-path: '**/pnpm-lock.yaml'
- name: Setup typescript-sdk - name: Setup packages
run: pnpm install --frozen-lockfile && pnpm build run: pnpm --filter "@immich/*" install --frozen-lockfile && pnpm --filter "@immich/*" build
working-directory: ./open-api/typescript-sdk
- name: Run setup web - name: Run setup web
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
working-directory: ./web working-directory: ./web
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run setup cli
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./cli
if: ${{ !cancelled() }}
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
@@ -467,9 +456,8 @@ jobs:
cache: 'pnpm' cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml' cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk - name: Run setup @immich/sdk
run: pnpm install --frozen-lockfile && pnpm build run: pnpm --filter @immich/sdk install --frozen-lockfile && pnpm --filter @immich/sdk build
working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Install dependencies - name: Install dependencies
@@ -563,17 +551,22 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Flutter SDK
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
channel: 'stable' github_token: ${{ steps.token.outputs.token }}
flutter-version-file: ./mobile/pubspec.yaml
- name: Generate translation file - name: Install dependencies
run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart run: flutter pub get
working-directory: ./mobile working-directory: ./mobile
- name: Generate translation files
run: mise //mobile:codegen:translation
- name: Run tests - name: Run tests
working-directory: ./mobile run: mise //mobile:test
run: flutter test -j 1
ml-unit-tests: ml-unit-tests:
name: Unit Test ML name: Unit Test ML
needs: pre-job needs: pre-job
@@ -597,7 +590,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -628,7 +621,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -679,18 +672,15 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
- name: Install server dependencies - name: Install server dependencies
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile
- name: Build the app
run: pnpm --filter immich build
- name: Run API generation - name: Run API generation
run: ./bin/generate-open-api.sh run: mise //:open-api
working-directory: open-api working-directory: open-api
- name: Find file changes - name: Find file changes
@@ -699,7 +689,7 @@ jobs:
with: with:
files: | files: |
mobile/openapi mobile/openapi
open-api/typescript-sdk packages/sdk
open-api/immich-openapi-specs.json open-api/immich-openapi-specs.json
- name: Verify files have not changed - name: Verify files have not changed
@@ -744,7 +734,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0 uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -782,7 +772,7 @@ jobs:
exit 1 exit 1
- name: Run SQL generation - name: Run SQL generation
run: pnpm sync:sql run: mise //:sql
env: env:
DB_URL: postgres://postgres:postgres@localhost:5432/immich DB_URL: postgres://postgres:postgres@localhost:5432/immich
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
github-token: ${{ steps.token.outputs.token }} github-token: ${{ steps.token.outputs.token }}
filters: | filters: |
i18n: i18n:
- modified: 'i18n/!(en|package)**\.json' - modified: 'i18n/!(en)**\.json'
skip-force-logic: 'true' skip-force-logic: 'true'
enforce-lock: enforce-lock:
+1 -1
View File
@@ -20,7 +20,7 @@ mobile/openapi/doc
mobile/openapi/.openapi-generator/FILES mobile/openapi/.openapi-generator/FILES
mobile/ios/build mobile/ios/build
open-api/typescript-sdk/build packages/**/build
mobile/android/fastlane/report.xml mobile/android/fastlane/report.xml
mobile/ios/fastlane/report.xml mobile/ios/fastlane/report.xml
View File
+6 -4
View File
@@ -23,15 +23,17 @@
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Immich CLI", "name": "Immich CLI",
"program": "${workspaceFolder}/cli/dist/index.js", "program": "${workspaceFolder}/packages/cli/dist/index.js",
"args": ["upload", "--help"], "args": ["upload", "--help"],
"runtimeArgs": ["--enable-source-maps"], "runtimeArgs": ["--enable-source-maps"],
"console": "integratedTerminal", "console": "integratedTerminal",
"resolveSourceMapLocations": ["${workspaceFolder}/cli/dist/**/*.js.map"], "resolveSourceMapLocations": [
"${workspaceFolder}/packages/cli/dist/**/*.js.map"
],
"sourceMaps": true, "sourceMaps": true,
"outFiles": ["${workspaceFolder}/cli/dist/**/*.js"], "outFiles": ["${workspaceFolder}/packages/cli/dist/**/*.js"],
"skipFiles": ["<node_internals>/**"], "skipFiles": ["<node_internals>/**"],
"preLaunchTask": "Build Immich CLI" "preLaunchTask": "Build @immich/cli"
} }
] ]
} }
+2 -87
View File
@@ -37,105 +37,24 @@ prod-scale:
.PHONY: open-api .PHONY: open-api
open-api: open-api:
cd ./open-api && bash ./bin/generate-open-api.sh @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
open-api-dart:
cd ./open-api && bash ./bin/generate-open-api.sh dart
open-api-typescript:
cd ./open-api && bash ./bin/generate-open-api.sh typescript
sql: sql:
pnpm --filter immich run sync:sql @printf "This command has been removed. Please use:\n\n mise sql # or mise //:sql from another directory\n\n"\n\n >&2 && exit 1
attach-server:
docker exec -it docker_immich-server_1 sh
renovate: renovate:
LOG_LEVEL=debug pnpm exec renovate --platform=local --repository-cache=reset LOG_LEVEL=debug pnpm exec renovate --platform=local --repository-cache=reset
# Directories that need to be created for volumes or build output
VOLUME_DIRS = \
./.pnpm-store \
./web/.svelte-kit \
./web/node_modules \
./web/coverage \
./e2e/node_modules \
./docs/node_modules \
./server/node_modules \
./open-api/typescript-sdk/node_modules \
./.github/node_modules \
./node_modules \
./cli/node_modules
# Include .env file if it exists # Include .env file if it exists
-include docker/.env -include docker/.env
MODULES = e2e server web cli sdk docs .github MODULES = e2e server web cli sdk docs .github
# directory to package name mapping function
# cli = @immich/cli
# docs = documentation
# e2e = immich-e2e
# open-api/typescript-sdk = @immich/sdk
# server = immich
# web = immich-web
map-package = $(subst sdk,@immich/sdk,$(subst cli,@immich/cli,$(subst docs,documentation,$(subst e2e,immich-e2e,$(subst server,immich,$(subst web,immich-web,$1))))))
audit-%:
pnpm --filter $(call map-package,$*) audit fix
install-%:
pnpm --filter $(call map-package,$*) install $(if $(FROZEN),--frozen-lockfile) $(if $(OFFLINE),--offline)
build-cli: build-sdk
build-web: build-sdk
build-%: install-%
pnpm --filter $(call map-package,$*) run build
format-%:
pnpm --filter $(call map-package,$*) run format:fix
lint-%:
pnpm --filter $(call map-package,$*) run lint:fix
check-%:
pnpm --filter $(call map-package,$*) run check
check-web:
pnpm --filter immich-web run check:typescript
pnpm --filter immich-web run check:svelte
test-%:
pnpm --filter $(call map-package,$*) run test
test-e2e: test-e2e:
docker compose -f ./e2e/docker-compose.yml build docker compose -f ./e2e/docker-compose.yml build
pnpm --filter immich-e2e run test pnpm --filter immich-e2e run test
pnpm --filter immich-e2e run test:web pnpm --filter immich-e2e run test:web
test-medium:
docker run \
--rm \
-v ./server/src:/usr/src/app/src \
-v ./server/test:/usr/src/app/test \
-v ./server/vitest.config.medium.mjs:/usr/src/app/vitest.config.medium.mjs \
-v ./server/tsconfig.json:/usr/src/app/tsconfig.json \
-e NODE_ENV=development \
immich-server:latest \
-c "pnpm test:medium -- --run"
test-medium-dev:
docker exec -it immich_server /bin/sh -c "pnpm run test:medium"
install-all:
pnpm -r --filter '!documentation' install
build-all: $(foreach M,$(filter-out e2e docs .github,$(MODULES)),build-$M) ;
check-all:
pnpm -r --filter '!documentation' run "/^(check|check\:svelte|check\:typescript)$/"
lint-all:
pnpm -r --filter '!documentation' run lint:fix
format-all:
pnpm -r --filter '!documentation' run format:fix
audit-all:
pnpm -r --filter '!documentation' audit fix
hygiene-all: audit-all
pnpm -r --filter '!documentation' run "/(format:fix|check|check:svelte|check:typescript|sql)/"
test-all:
pnpm -r --filter '!documentation' run "/^test/"
clean: clean:
find . -name "node_modules" -type d -prune -exec rm -rf {} + find . -name "node_modules" -type d -prune -exec rm -rf {} +
@@ -146,7 +65,3 @@ clean:
find . -name ".pnpm-store" -type d -prune -exec rm -rf '{}' + 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 ./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 command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml down -v --remove-orphans || true
setup-server-dev: install-server
setup-web-dev: install-sdk build-sdk install-web
-14
View File
@@ -1,14 +0,0 @@
FROM node:24.1.0-alpine3.20@sha256:8fe019e0d57dbdce5f5c27c0b63d2775cf34b00e3755a7dea969802d7e0c2b25 AS core
WORKDIR /usr/src/app
COPY package* pnpm* .pnpmfile.cjs ./
COPY ./cli ./cli/
COPY ./open-api/typescript-sdk ./open-api/typescript-sdk/
RUN corepack enable pnpm && \
pnpm install --filter @immich/sdk --filter @immich/cli --frozen-lockfile && \
pnpm --filter @immich/sdk build && \
pnpm --filter @immich/cli build
WORKDIR /import
ENTRYPOINT ["node", "/usr/src/app/cli/dist"]
+30 -30
View File
@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates. # Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" { provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.52.5" version = "4.52.7"
constraints = "4.52.5" constraints = "4.52.7"
hashes = [ hashes = [
"h1:+rfzF+16ZcWZWnTyW/p1HHTzYbPKX8Zt2nIFtR/+f+E=", "h1:+O72J3QYiZtYmYYZM/Eh0f4NNfl1BvjX1eju43qTQsQ=",
"h1:18bXaaOSq8MWKuMxo/4y7EB7/i7G90y5QsKHZRmkoDo=", "h1:0oqjYIPXcXh7XiDiKI085cHDYQQ5mh8kDl9dmBtvtog=",
"h1:4vZVOpKeEQZsF2VrARRZFeL37Ed/gD4rRMtfnvWQres=", "h1:4b4ESb87MGv5bnadgYe7sK5rEkKMZhbkQcwPubQTsR4=",
"h1:BZOsTF83QPKXTAaYqxPKzdl1KRjk/L2qbPpFjM0w28A=", "h1:6mTr3eA1Ddb348lLmJuyvn98z4KF+ejqaUEJ76D1rzQ=",
"h1:CDuC+HXLvc1z6wkCRsSDcc/+QENIHEtssYshiWg3opA=", "h1:9/3YH+9k9HqsvFtbmBf7SO2+xqZeZrXNKzLkjNuhUEA=",
"h1:DE+YFzLnqSe79pI2R4idRGx5QzLdrA7RXvngTkGfZ30=", "h1:Jcq4tBWgyH4/2JsojNBSRaN0mcItVMchO+lynonrlqc=",
"h1:DfaJwH3Ml4yrRbdAY4AcDVy0QTQk5T3A622TXzS/u2E=", "h1:Y4Vv/2RdP0Q+uxqhOxzOdKxuuEMjXPDcU0vPc5bCQzI=",
"h1:EIDXP0W3kgIv2pecrFmqtK/DnlqkyckzBzhxKaXU+4A=", "h1:a0gW8FBKsbP9Fi0HEDoy49WIbEWVHk9+BR4/iwuBdDQ=",
"h1:EV4kYyaOnwGA0bh/3hU6Ezqnt1PFDxopH7i85e48IzY=", "h1:gElv6iqJtg8OKN77gbw+MjrkrQmJHPkkMEi1J+0xkpU=",
"h1:M0iXabfzamU+MPDi0G9XACpbacFKMakmM+Z9HZ8HrsM=", "h1:oslXUugD/NQ+duJgT4BhKQyfGbuFOANknMvR73fiOeM=",
"h1:YWmCbGF/KbsrUzcYVBLscwLizidbp95TDQa0N2qpmVo=", "h1:pPItIWii5oymR+geZB219ROSPuSODPLTlM4S/u8xLvM=",
"h1:cxPcCB5gbrpUO1+IXkQYs1YTY50/0IlApCzGea0cwuQ=", "h1:u67GWw8GwD9NDlDzp9Y5VRnSQGcCrE8rSpkGPaBpDl0=",
"h1:g6DldikTV2HXUu9uoeNY5FuLufgaYWF4ufgZg7wq62s=", "h1:uUUa9dY0XQOycI8pxg16PFFtL0WCTi9uEJz8trTQ7pU=",
"h1:oi/Hrx9pwoQ+Z52CBC+rrowVH387EIj0qvnxQgDeI+0=", "h1:y3rV8KF2q6GEMANNlf5EkKJurlfbKlIKpjGcdxoy7pQ=",
"zh:1a3400cb38863b2585968d1876706bcfc67a148e1318a1d325c6c7704adc999b", "zh:0c904ce31a4c6c4a5b3bf7ff1560e77c0cc7e2450c8553ded8e8c90398e1418b",
"zh:4c5062cb9e9da1676f06ae92b8370186d98976cc4c7030d3cd76df12af54282a", "zh:36183d310c36373fe4cb936b83c595c6fd3b0a94bc7827f28e5789ccbf59752e",
"zh:52110f493b5f0587ef77a1cfd1a67001fd4c617b14c6502d732ab47352bdc2f7", "zh:556a568a6f0235e8f41647de9e4d3a1e7b1d6502df8b19b54ec441f1c653ea10",
"zh:5aa536f9eaeb43823aaf2aa80e7d39b25ef2b383405ed034aa16a28b446a9238", "zh:633ebbd5b0245e75e500ef9be4d9e62288f97e8da3baaa51323892a786d90285",
"zh:5cc39459a1c6be8a918f17054e4fbba573825ed5597dcada588fe99614d98a5b", "zh:6acfe60cf52a65ba8f044f748548d2119e7f4fd7f8ebcb14698960d87c68f529",
"zh:629ae6a7ba298815131da826474d199312d21cec53a4d5ded4fa56a692e6f072",
"zh:719cc7c75dc1d3eb30c22ff5102a017996d9788b948078c7e1c5b3446aeca661",
"zh:8698635a3ca04383c1e93b21d6963346bdae54d27177a48e4b1435b7f731731c",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8a9993f1dcadf1dd6ca43b23348abe374605d29945a2fafc07fb3457644e6a54", "zh:904acc31ebb9d6ef68c792074b30532ee61bf515f19e0a3c75b46f126cca1f13",
"zh:b1b9a1e6bcc24d5863a664a411d2dc906373ae7a2399d2d65548ce7377057852", "zh:a1d0a81246afc8750286d3f6fe7a8fbe6460dd2662407b28dbfbabb612e5fa9d",
"zh:b270184cdeec277218e84b94cb136fead753da717f9b9dc378e51907f3f00bb0", "zh:a41a36fe253fc365fe2b7ffc749624688b2693b4634862fda161179ab100029f",
"zh:dff2bc10071210181726ce270f954995fe42c696e61e2e8f874021fed02521e5", "zh:a7ef269e77ffa8715c8945a2c14322c7ff159ea44c15f62505f3cbb2cae3b32d",
"zh:e8e87b40b6a87dc097b0fdc20d3f725cec0d82abc9cc3755c1f89f8f6e8b0036", "zh:b01aa3bed30610633b762df64332b26f8844a68c3960cebcb30f04918efc67fe",
"zh:ee964a6573d399a5dd22ce328fb38ca1207797a02248f14b2e4913ee390e7803", "zh:b069cc2cd18cae10757df3ae030508eac8d55de7e49eda7a5e3e11f2f7fe6455",
"zh:b2d2c6313729ebb7465dceece374049e2d08bda34473901be9ff46a8836d42b2",
"zh:db0e114edaf4bc2f3d4769958807c83022bfbc619a00bdf4c4bd17faa4ab2d8b",
"zh:ecc0aa8b9044f664fd2aaf8fa992d976578f78478980555b4b8f6148e8d1a5fe",
] ]
} }
@@ -5,7 +5,7 @@ terraform {
required_providers { required_providers {
cloudflare = { cloudflare = {
source = "cloudflare/cloudflare" source = "cloudflare/cloudflare"
version = "4.52.5" version = "4.52.7"
} }
} }
} }
+30 -30
View File
@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates. # Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" { provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.52.5" version = "4.52.7"
constraints = "4.52.5" constraints = "4.52.7"
hashes = [ hashes = [
"h1:+rfzF+16ZcWZWnTyW/p1HHTzYbPKX8Zt2nIFtR/+f+E=", "h1:+O72J3QYiZtYmYYZM/Eh0f4NNfl1BvjX1eju43qTQsQ=",
"h1:18bXaaOSq8MWKuMxo/4y7EB7/i7G90y5QsKHZRmkoDo=", "h1:0oqjYIPXcXh7XiDiKI085cHDYQQ5mh8kDl9dmBtvtog=",
"h1:4vZVOpKeEQZsF2VrARRZFeL37Ed/gD4rRMtfnvWQres=", "h1:4b4ESb87MGv5bnadgYe7sK5rEkKMZhbkQcwPubQTsR4=",
"h1:BZOsTF83QPKXTAaYqxPKzdl1KRjk/L2qbPpFjM0w28A=", "h1:6mTr3eA1Ddb348lLmJuyvn98z4KF+ejqaUEJ76D1rzQ=",
"h1:CDuC+HXLvc1z6wkCRsSDcc/+QENIHEtssYshiWg3opA=", "h1:9/3YH+9k9HqsvFtbmBf7SO2+xqZeZrXNKzLkjNuhUEA=",
"h1:DE+YFzLnqSe79pI2R4idRGx5QzLdrA7RXvngTkGfZ30=", "h1:Jcq4tBWgyH4/2JsojNBSRaN0mcItVMchO+lynonrlqc=",
"h1:DfaJwH3Ml4yrRbdAY4AcDVy0QTQk5T3A622TXzS/u2E=", "h1:Y4Vv/2RdP0Q+uxqhOxzOdKxuuEMjXPDcU0vPc5bCQzI=",
"h1:EIDXP0W3kgIv2pecrFmqtK/DnlqkyckzBzhxKaXU+4A=", "h1:a0gW8FBKsbP9Fi0HEDoy49WIbEWVHk9+BR4/iwuBdDQ=",
"h1:EV4kYyaOnwGA0bh/3hU6Ezqnt1PFDxopH7i85e48IzY=", "h1:gElv6iqJtg8OKN77gbw+MjrkrQmJHPkkMEi1J+0xkpU=",
"h1:M0iXabfzamU+MPDi0G9XACpbacFKMakmM+Z9HZ8HrsM=", "h1:oslXUugD/NQ+duJgT4BhKQyfGbuFOANknMvR73fiOeM=",
"h1:YWmCbGF/KbsrUzcYVBLscwLizidbp95TDQa0N2qpmVo=", "h1:pPItIWii5oymR+geZB219ROSPuSODPLTlM4S/u8xLvM=",
"h1:cxPcCB5gbrpUO1+IXkQYs1YTY50/0IlApCzGea0cwuQ=", "h1:u67GWw8GwD9NDlDzp9Y5VRnSQGcCrE8rSpkGPaBpDl0=",
"h1:g6DldikTV2HXUu9uoeNY5FuLufgaYWF4ufgZg7wq62s=", "h1:uUUa9dY0XQOycI8pxg16PFFtL0WCTi9uEJz8trTQ7pU=",
"h1:oi/Hrx9pwoQ+Z52CBC+rrowVH387EIj0qvnxQgDeI+0=", "h1:y3rV8KF2q6GEMANNlf5EkKJurlfbKlIKpjGcdxoy7pQ=",
"zh:1a3400cb38863b2585968d1876706bcfc67a148e1318a1d325c6c7704adc999b", "zh:0c904ce31a4c6c4a5b3bf7ff1560e77c0cc7e2450c8553ded8e8c90398e1418b",
"zh:4c5062cb9e9da1676f06ae92b8370186d98976cc4c7030d3cd76df12af54282a", "zh:36183d310c36373fe4cb936b83c595c6fd3b0a94bc7827f28e5789ccbf59752e",
"zh:52110f493b5f0587ef77a1cfd1a67001fd4c617b14c6502d732ab47352bdc2f7", "zh:556a568a6f0235e8f41647de9e4d3a1e7b1d6502df8b19b54ec441f1c653ea10",
"zh:5aa536f9eaeb43823aaf2aa80e7d39b25ef2b383405ed034aa16a28b446a9238", "zh:633ebbd5b0245e75e500ef9be4d9e62288f97e8da3baaa51323892a786d90285",
"zh:5cc39459a1c6be8a918f17054e4fbba573825ed5597dcada588fe99614d98a5b", "zh:6acfe60cf52a65ba8f044f748548d2119e7f4fd7f8ebcb14698960d87c68f529",
"zh:629ae6a7ba298815131da826474d199312d21cec53a4d5ded4fa56a692e6f072",
"zh:719cc7c75dc1d3eb30c22ff5102a017996d9788b948078c7e1c5b3446aeca661",
"zh:8698635a3ca04383c1e93b21d6963346bdae54d27177a48e4b1435b7f731731c",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8a9993f1dcadf1dd6ca43b23348abe374605d29945a2fafc07fb3457644e6a54", "zh:904acc31ebb9d6ef68c792074b30532ee61bf515f19e0a3c75b46f126cca1f13",
"zh:b1b9a1e6bcc24d5863a664a411d2dc906373ae7a2399d2d65548ce7377057852", "zh:a1d0a81246afc8750286d3f6fe7a8fbe6460dd2662407b28dbfbabb612e5fa9d",
"zh:b270184cdeec277218e84b94cb136fead753da717f9b9dc378e51907f3f00bb0", "zh:a41a36fe253fc365fe2b7ffc749624688b2693b4634862fda161179ab100029f",
"zh:dff2bc10071210181726ce270f954995fe42c696e61e2e8f874021fed02521e5", "zh:a7ef269e77ffa8715c8945a2c14322c7ff159ea44c15f62505f3cbb2cae3b32d",
"zh:e8e87b40b6a87dc097b0fdc20d3f725cec0d82abc9cc3755c1f89f8f6e8b0036", "zh:b01aa3bed30610633b762df64332b26f8844a68c3960cebcb30f04918efc67fe",
"zh:ee964a6573d399a5dd22ce328fb38ca1207797a02248f14b2e4913ee390e7803", "zh:b069cc2cd18cae10757df3ae030508eac8d55de7e49eda7a5e3e11f2f7fe6455",
"zh:b2d2c6313729ebb7465dceece374049e2d08bda34473901be9ff46a8836d42b2",
"zh:db0e114edaf4bc2f3d4769958807c83022bfbc619a00bdf4c4bd17faa4ab2d8b",
"zh:ecc0aa8b9044f664fd2aaf8fa992d976578f78478980555b4b8f6148e8d1a5fe",
] ]
} }
+1 -1
View File
@@ -5,7 +5,7 @@ terraform {
required_providers { required_providers {
cloudflare = { cloudflare = {
source = "cloudflare/cloudflare" source = "cloudflare/cloudflare"
version = "4.52.5" version = "4.52.7"
} }
} }
} }
+4 -4
View File
@@ -25,10 +25,10 @@ services:
- server_node_modules:/usr/src/app/server/node_modules - server_node_modules:/usr/src/app/server/node_modules
- web_node_modules:/usr/src/app/web/node_modules - web_node_modules:/usr/src/app/web/node_modules
- github_node_modules:/usr/src/app/.github/node_modules - github_node_modules:/usr/src/app/.github/node_modules
- cli_node_modules:/usr/src/app/cli/node_modules - cli_node_modules:/usr/src/app/packages/cli/node_modules
- docs_node_modules:/usr/src/app/docs/node_modules - docs_node_modules:/usr/src/app/docs/node_modules
- e2e_node_modules:/usr/src/app/e2e/node_modules - e2e_node_modules:/usr/src/app/e2e/node_modules
- sdk_node_modules:/usr/src/app/open-api/typescript-sdk/node_modules - sdk_node_modules:/usr/src/app/packages/sdk/node_modules
- app_node_modules:/usr/src/app/node_modules - app_node_modules:/usr/src/app/node_modules
- sveltekit:/usr/src/app/web/.svelte-kit - sveltekit:/usr/src/app/web/.svelte-kit
- coverage:/usr/src/app/web/coverage - coverage:/usr/src/app/web/coverage
@@ -74,7 +74,7 @@ services:
- ${UPLOAD_LOCATION}/photos:/data - ${UPLOAD_LOCATION}/photos:/data
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- pnpm_store_server:/buildcache/pnpm-store - pnpm_store_server:/buildcache/pnpm-store
- ../plugins:/build/corePlugin - ../packages/plugins:/build/corePlugin
env_file: env_file:
- .env - .env
environment: environment:
@@ -157,7 +157,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9 image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
+1 -1
View File
@@ -56,7 +56,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9 image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
+1 -1
View File
@@ -61,7 +61,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9 image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
user: '1000:1000' user: '1000:1000'
security_opt: security_opt:
- no-new-privileges:true - no-new-privileges:true
+1 -1
View File
@@ -49,7 +49,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9 image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
@@ -13,8 +13,11 @@ The `immich-server` docker image comes preinstalled with an administrative CLI (
| `enable-oauth-login` | Enable OAuth login | | `enable-oauth-login` | Enable OAuth login |
| `disable-oauth-login` | Disable OAuth login | | `disable-oauth-login` | Disable OAuth login |
| `list-users` | List Immich users | | `list-users` | List Immich users |
| `grant-admin` | Grant admin privileges to a user (by email) |
| `revoke-admin` | Revoke admin privileges from a user (by email) |
| `version` | Print Immich version | | `version` | Print Immich version |
| `change-media-location` | Change database file paths to align with a new media location | | `change-media-location` | Change database file paths to align with a new media location |
| `schema-check` | Verify database migrations and check for schema drift |
## How to run a command ## How to run a command
@@ -102,6 +105,22 @@ immich-admin list-users
] ]
``` ```
Grant Admin
```
immich-admin grant-admin
? Please enter the user email: user@example.com
Admin access has been granted to user@example.com
```
Revoke Admin
```
immich-admin revoke-admin
? Please enter the user email: user@example.com
Admin access has been revoked from user@example.com
```
Print Immich Version Print Immich Version
``` ```
@@ -126,3 +145,12 @@ immich-admin change-media-location
Database file paths updated successfully! 🎉 Database file paths updated successfully! 🎉
... ...
``` ```
Schema Check
```
immich-admin schema-check
Migrations are up to date
No schema drift detected
```
+1 -1
View File
@@ -10,4 +10,4 @@ OpenAPI is used to generate the client (Typescript, Dart) SDK. `openapi-generato
make open-api make open-api
``` ```
You can find the generated client SDK in the `open-api/typescript-sdk/client` for Typescript SDK and `mobile/openapi` for Dart SDK. You can find the generated client SDK in the `packages/sdk/client` for Typescript SDK and `mobile/openapi` for Dart SDK.
+6 -47
View File
@@ -205,7 +205,7 @@ When the Dev Container starts, it automatically:
1. **Runs post-create script** (`container-server-post-create.sh`): 1. **Runs post-create script** (`container-server-post-create.sh`):
- Adjusts file permissions for the `node` user - Adjusts file permissions for the `node` user
- Installs dependencies: `pnpm install` in all packages - Installs dependencies: `pnpm install` in all packages
- Builds TypeScript SDK: `pnpm run build` in `open-api/typescript-sdk` - Builds TypeScript SDK: `pnpm --filter @immich/sdk build`
2. **Starts development servers** via VS Code tasks: 2. **Starts development servers** via VS Code tasks:
- `Immich API Server (Nest)` - API server with hot-reloading on port 2283 - `Immich API Server (Nest)` - API server with hot-reloading on port 2283
@@ -243,8 +243,8 @@ To connect the mobile app to your Dev Container:
- **Server code** (`/server`): Changes trigger automatic restart - **Server code** (`/server`): Changes trigger automatic restart
- **Web code** (`/web`): Changes trigger hot module replacement - **Web code** (`/web`): Changes trigger hot module replacement
- **Database migrations**: Run `pnpm run sync:sql` in the server directory - **Database migrations**: Run `mise //:sql`
- **API changes**: Regenerate TypeScript SDK with `make open-api` - **API changes**: Regenerate TypeScript SDK with `mise //:open-api`
## Testing ## Testing
@@ -252,20 +252,11 @@ To connect the mobile app to your Dev Container:
The Dev Container supports multiple ways to run tests: The Dev Container supports multiple ways to run tests:
#### Using Make Commands (Recommended) #### Using Mise Commands (Recommended)
```bash ```bash
# Run tests for specific components # Run tests for specific components
make test-server # Server unit tests mise run checklist # in `server/`, `web/`, `packages/cli`
make test-web # Web unit tests
make test-e2e # End-to-end tests
make test-cli # CLI tests
# Run all tests
make test-all # Runs tests for all components
# Medium tests (integration tests)
make test-medium-dev # End-to-end tests
``` ```
#### Using PNPM Directly #### Using PNPM Directly
@@ -289,48 +280,16 @@ pnpm run test # Run API tests
pnpm run test:web # Run web UI tests pnpm run test:web # Run web UI tests
``` ```
### Code Quality Commands
```bash
# Linting
make lint-server # Lint server code
make lint-web # Lint web code
make lint-all # Lint all components
# Formatting
make format-server # Format server code
make format-web # Format web code
make format-all # Format all code
# Type checking
make check-server # Type check server
make check-web # Type check web
make check-all # Check all components
# Complete hygiene check
make hygiene-all # Run lint, format, check, SQL sync, and audit
```
### Additional Make Commands ### Additional Make Commands
```bash ```bash
# Build commands
make build-server # Build server
make build-web # Build web app
make build-all # Build everything
# API generation # API generation
make open-api # Generate OpenAPI specs make open-api # Generate OpenAPI specs
make open-api-typescript # Generate TypeScript SDK make open-api-typescript # Generate TypeScript SDK
make open-api-dart # Generate Dart SDK make open-api-dart # Generate Dart SDK
# Database # Database
make sql # Sync database schema mise sql # Sync database schema
# Dependencies
make install-server # Install server dependencies
make install-web # Install web dependencies
make install-all # Install all dependencies
``` ```
### Debugging ### Debugging
+2 -1
View File
@@ -10,7 +10,8 @@ Our [GitHub Repository](https://github.com/immich-app/immich) is a [monorepo](ht
| :------------------ | :------------------------------------------------------------------- | | :------------------ | :------------------------------------------------------------------- |
| `.github/` | Github templates and action workflows | | `.github/` | Github templates and action workflows |
| `.vscode/` | VSCode debug launch profiles | | `.vscode/` | VSCode debug launch profiles |
| `cli/` | Source code for the work-in-progress CLI rewrite | | `packages/cli` | Source code for the CLI |
| `packages/sdk` | Source code for the generated OpenAPI SDK |
| `docker/` | Docker compose resources for dev, test, production | | `docker/` | Docker compose resources for dev, test, production |
| `design/` | Screenshots and logos for the README | | `design/` | Screenshots and logos for the README |
| `docs/` | Source code for the [https://immich.app](https://immich.app) website | | `docs/` | Source code for the [https://immich.app](https://immich.app) website |
+11 -9
View File
@@ -34,21 +34,23 @@ Run all web checks with `pnpm run check:all`
Run all server checks with `pnpm run check:all` Run all server checks with `pnpm run check:all`
::: :::
:::info Auto Fix :::tip Auto Fix
You can use `pnpm run __:fix` to potentially correct some issues automatically for `pnpm run format` and `lint`. You can use `pnpm run __:fix` to potentially correct some issues automatically for `pnpm run format` and `lint`.
::: :::
## Mobile Checks ## Mobile Checklist
The following commands must be executed from within the mobile app directory of the codebase. - [ ] `mise //mobile:codegen` (auto-generate files using build_runner)
- [ ] `mise //mobile:lint` (static analysis via Dart Analyzer and DCM)
- [ ] `mise //mobile:format` (formatting via Dart Formatter)
- [ ] `mise //mobile:test` (unit tests)
- [ ] `make build` (auto-generate files using build_runner) :::tip
- [ ] `make analyze` (static analysis via Dart Analyzer and DCM) Run all these commands at once with `mise //mobile:checklist`
- [ ] `make format` (formatting via Dart Formatter) :::
- [ ] `make test` (unit tests)
:::info Auto Fix :::tip Auto Fix
You can use `dart fix --apply` and `dcm fix lib` to potentially correct some issues automatically for `make analyze`. You can use `mise //mobile:lint-fix` to potentially correct some issues automatically for `mise //mobile:lint`.
::: :::
## OpenAPI ## OpenAPI
+1 -1
View File
@@ -58,7 +58,7 @@ You can access the web from `http://your-machine-ip:3000` or `http://localhost:3
If you only want to do web development connected to an existing, remote backend, follow these steps: If you only want to do web development connected to an existing, remote backend, follow these steps:
1. Build the Immich SDK - `cd open-api/typescript-sdk && pnpm i && pnpm run build && cd -` 1. Build the Immich SDK - `pnpm --filter @immich/sdk install && pnpm --filter @immich/sdk build`
2. Enter the web directory - `cd web/` 2. Enter the web directory - `cd web/`
3. Install web dependencies - `pnpm i` 3. Install web dependencies - `pnpm i`
4. Start the web development server 4. Start the web development server
+4 -5
View File
@@ -17,15 +17,14 @@ make e2e
Before you can run the tests, you need to run the following commands _once_: Before you can run the tests, you need to run the following commands _once_:
- `pnpm install` (in `e2e/`) - `pnpm install`
- `pnpm run build` (in `cli/`) - `pnpm --filter "@immich/*" build`
- `make open-api` (in the project root `/`) - `mise //:open-api`
Once the test environment is running, the e2e tests can be run via: Once the test environment is running, the e2e tests can be run via:
```bash ```bash
cd e2e/ mise //e2e:test
pnpm test
``` ```
The tests check various things including: The tests check various things including:
+1 -1
View File
@@ -26,7 +26,7 @@ The default configuration looks like this:
}, },
"ffmpeg": { "ffmpeg": {
"accel": "disabled", "accel": "disabled",
"accelDecode": false, "accelDecode": true,
"acceptedAudioCodecs": ["aac", "mp3", "opus"], "acceptedAudioCodecs": ["aac", "mp3", "opus"],
"acceptedContainers": ["mov", "ogg", "webm"], "acceptedContainers": ["mov", "ogg", "webm"],
"acceptedVideoCodecs": ["h264"], "acceptedVideoCodecs": ["h264"],
+66 -2
View File
@@ -52,7 +52,7 @@ Scroll to the bottom of the "**Details**" section and find the `IP Address` list
## Step 4 - Configure Firewall Settings ## Step 4 - Configure Firewall Settings
Once your project completes the build process, your containers will start. In order to be able to access Immich from your browser, you need to configure the firewall settings for your Synology NAS. Once your project completes the build process, your containers will start. In order to be able to access Immich from your browser, you need to configure the firewall settings for your Synology NAS to allow communication between the Immich containers.
Open "**Control Panel**" on your Synology NAS, and select "**Security**". Navigate to "**Firewall**" Open "**Control Panel**" on your Synology NAS, and select "**Security**". Navigate to "**Firewall**"
@@ -74,6 +74,7 @@ Read the [Post Installation](/install/post-install.mdx) steps and [upgrade instr
<details> <details>
<summary>Updating Immich using Container Manager</summary> <summary>Updating Immich using Container Manager</summary>
Check the post installation and upgrade instructions at the links above before proceeding with this section. Check the post installation and upgrade instructions at the links above before proceeding with this section.
## Step 1. Backup ## Step 1. Backup
@@ -110,7 +111,7 @@ Go to **Project**, select **Action** then **Build**. This will download, unpack,
## Step 5. Update firewall rule ## Step 5. Update firewall rule
The default behavior is to automatically start the containers once installed. If `immich_server` runs for a few seconds and then stops, it may be because the firewall rule no longer matches the server IP address. Without a fixed subnet, the default behavior is to automatically start the containers once installed. If `immich_server` runs for a few seconds and then stops, it may be because the firewall rule no longer matches the server IP address.
Go to the **Container** section. Click on `immich_server` and scroll down on **General** to find the IP address. Go to the **Container** section. Click on `immich_server` and scroll down on **General** to find the IP address.
![Container IP](../../static/img/synology-container-ip.png) ![Container IP](../../static/img/synology-container-ip.png)
@@ -123,4 +124,67 @@ In this example, the IP addresses mismatch and the firewall rule needs to be edi
![Edit IP](../../static/img/synology-fw-ipedit.png) ![Edit IP](../../static/img/synology-fw-ipedit.png)
To prevent future firewall issues, you may set a fixed subnet. [See Set Fixed Subnet](#set-fixed-subnet) for instructions.
</details>
<details id="set-fixed-subnet">
<summary>Set Fixed Subnet</summary>
Docker by default assigns dynamic subnets to bridge networks which can change when rebuilding containers and can cause firewall rules to break. To avoid this, define a fixed subnet in your `docker-compose.yml`:
## Step 1. Determine current subnet
Go to the **Container** section. Click on `immich_server` and scroll down on **General** to find the IP address.
![Container IP](../../static/img/synology-container-ip.png)
## Step 2. Add network configuration
Add the following network configuration at the end of your `docker-compose.yml` file:
```yaml
networks:
immich-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
gateway: 172.20.0.1
```
If your docker container is running on a different subnet then update accordingly.
## Step 3. Add network to each service
Add the network to each service (immich-server, immich-machine-learning, redis, database):
```yaml
services:
immich-server:
# other config options
networks:
- immich-network
immich-machine-learning:
# other config options
networks:
- immich-network
redis:
# other config options
networks:
- immich-network
database:
# other config options
networks:
- immich-network
```
Save your changes. Synology will ask if you want to save changes only or rebuild containers. Select rebuild containers.
## Step 4. Update Firewall Rules, if necessary
If your firewall rules were not already set for this subnet, the firewall rules will need to be updated. See [Step 4 - Configure Firewall Settings](#step-4---configure-firewall-settings).
</details> </details>
+2 -2
View File
@@ -4,7 +4,7 @@ services:
e2e-auth-server: e2e-auth-server:
container_name: immich-e2e-auth-server container_name: immich-e2e-auth-server
build: build:
context: ../e2e-auth-server context: ../packages/e2e-auth-server
ports: ports:
- 2286:2286 - 2286:2286
@@ -44,7 +44,7 @@ services:
redis: redis:
container_name: immich-e2e-redis container_name: immich-e2e-redis
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9 image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
@@ -7,7 +7,6 @@ import {
getMyUser, getMyUser,
LoginResponseDto, LoginResponseDto,
SharedLinkType, SharedLinkType,
updateConfig,
} from '@immich/sdk'; } from '@immich/sdk';
import { exiftool } from 'exiftool-vendored'; import { exiftool } from 'exiftool-vendored';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
@@ -24,7 +23,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`; const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`; const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
const facesAssetDir = `${testAssetDir}/metadata/faces`;
const readTags = async (bytes: Buffer, filename: string) => { const readTags = async (bytes: Buffer, filename: string) => {
const filepath = join(tempDir, filename); const filepath = join(tempDir, filename);
@@ -185,78 +183,6 @@ describe('/asset', () => {
}); });
}); });
describe('faces', () => {
const metadataFaceTests = [
{
description: 'without orientation',
filename: 'portrait.jpg',
},
{
description: 'adjusting face regions to orientation',
filename: 'portrait-orientation-6.jpg',
},
];
// should produce same resulting face region coordinates for any orientation
const expectedFaces = [
{
name: 'Marie Curie',
birthDate: null,
isHidden: false,
faces: [
{
imageHeight: 700,
imageWidth: 840,
boundingBoxX1: 261,
boundingBoxX2: 356,
boundingBoxY1: 146,
boundingBoxY2: 284,
sourceType: 'exif',
},
],
},
{
name: 'Pierre Curie',
birthDate: null,
isHidden: false,
faces: [
{
imageHeight: 700,
imageWidth: 840,
boundingBoxX1: 536,
boundingBoxX2: 618,
boundingBoxY1: 83,
boundingBoxY2: 252,
sourceType: 'exif',
},
],
},
];
it.each(metadataFaceTests)('should get the asset faces from $filename $description', async ({ filename }) => {
const config = await utils.getSystemConfig(admin.accessToken);
config.metadata.faces.import = true;
await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
const facesAsset = await utils.createAsset(admin.accessToken, {
assetData: {
filename,
bytes: await readFile(`${facesAssetDir}/${filename}`),
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: facesAsset.id });
const { status, body } = await request(app)
.get(`/assets/${facesAsset.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body.id).toEqual(facesAsset.id);
const sortedPeople = body.people.toSorted((a: any, b: any) => a.name.localeCompare(b.name));
expect(sortedPeople).toMatchObject(expectedFaces);
});
});
it('should work with a shared link', async () => { it('should work with a shared link', async () => {
const sharedLink = await utils.createSharedLink(user1.accessToken, { const sharedLink = await utils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Individual, type: SharedLinkType.Individual,
+12 -1
View File
@@ -441,7 +441,18 @@ describe('/search', () => {
.get('/search/explore') .get('/search/explore')
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual([{ fieldName: 'exifInfo.city', items: [] }]); expect(Array.isArray(body)).toBe(true);
expect(body).toEqual(expect.arrayContaining([{ fieldName: 'exifInfo.city', items: [] }]));
expect(body).toEqual(
expect.arrayContaining([
{
fieldName: 'createdAt',
items: expect.arrayContaining([
expect.objectContaining({ data: expect.objectContaining({ id: assetLast.id }) }),
]),
},
]),
);
}); });
}); });
+1 -1
View File
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
import { immichCli } from 'src/utils'; import { immichCli } from 'src/utils';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
const pkg = JSON.parse(readFileSync('../cli/package.json', 'utf8')); const pkg = JSON.parse(readFileSync('../packages/cli/package.json', 'utf8'));
describe(`immich --version`, () => { describe(`immich --version`, () => {
describe('immich --version', () => { describe('immich --version', () => {
@@ -28,6 +28,7 @@ export function toColumnarFormat(assets: MockTimelineAsset[]): TimeBucketAssetRe
ownerId: [], ownerId: [],
ratio: [], ratio: [],
thumbhash: [], thumbhash: [],
createdAt: [],
fileCreatedAt: [], fileCreatedAt: [],
localOffsetHours: [], localOffsetHours: [],
isFavorite: [], isFavorite: [],
@@ -338,7 +339,6 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons
livePhotoVideoId: asset.livePhotoVideoId, livePhotoVideoId: asset.livePhotoVideoId,
tags: [], tags: [],
people: [], people: [],
unassignedFaces: [],
stack: asset.stack, stack: asset.stack,
isOffline: false, isOffline: false,
hasMetadata: true, hasMetadata: true,
@@ -66,7 +66,6 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => {
livePhotoVideoId: null, livePhotoVideoId: null,
tags: [], tags: [],
people: [], people: [],
unassignedFaces: [],
stack: undefined, stack: undefined,
isOffline: false, isOffline: false,
hasMetadata: true, hasMetadata: true,
+1 -1
View File
@@ -90,7 +90,7 @@ export const tempDir = tmpdir();
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` }); export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
export const asKeyAuth = (key: string) => ({ 'x-api-key': key }); export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
export const immichCli = (args: string[]) => export const immichCli = (args: string[]) =>
executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../cli' }).promise; executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../packages/cli' }).promise;
export const dockerExec = (args: string[]) => export const dockerExec = (args: string[]) =>
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', args.join(' ')]); executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', args.join(' ')]);
export const immichAdmin = (args: string[]) => dockerExec([`immich-admin ${args.join(' ')}`]); export const immichAdmin = (args: string[]) => dockerExec([`immich-admin ${args.join(' ')}`]);
+8 -4
View File
@@ -885,15 +885,13 @@
"cutoff_date_description": "Keep photos from the last…", "cutoff_date_description": "Keep photos from the last…",
"cutoff_day": "{count, plural, one {day} other {days}}", "cutoff_day": "{count, plural, one {day} other {days}}",
"cutoff_year": "{count, plural, one {year} other {years}}", "cutoff_year": "{count, plural, one {year} other {years}}",
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"dark": "Dark", "dark": "Dark",
"dark_theme": "Switch to dark theme", "dark_theme": "Switch to dark theme",
"date": "Date", "date": "Date",
"date_after": "Date after", "date_after": "Date after",
"date_and_time": "Date and Time", "date_and_time": "Date and Time",
"date_before": "Date before", "date_before": "Date before",
"date_format": "E, LLL d, y • h:mm a", "date_of_birth": "Date of birth",
"date_of_birth_saved": "Date of birth saved successfully", "date_of_birth_saved": "Date of birth saved successfully",
"date_range": "Date range", "date_range": "Date range",
"day": "Day", "day": "Day",
@@ -1403,6 +1401,7 @@
"link_to_oauth": "Link to OAuth", "link_to_oauth": "Link to OAuth",
"linked_oauth_account": "Linked OAuth account", "linked_oauth_account": "Linked OAuth account",
"list": "List", "list": "List",
"live": "Live",
"loading": "Loading", "loading": "Loading",
"loading_search_results_failed": "Loading search results failed", "loading_search_results_failed": "Loading search results failed",
"local": "Local", "local": "Local",
@@ -1582,8 +1581,8 @@
"mobile_app_download_onboarding_note": "Download the companion mobile app using the following options", "mobile_app_download_onboarding_note": "Download the companion mobile app using the following options",
"model": "Model", "model": "Model",
"month": "Month", "month": "Month",
"monthly_title_text_date_format": "MMMM y",
"more": "More", "more": "More",
"motion": "Motion",
"move": "Move", "move": "Move",
"move_down": "Move down", "move_down": "Move down",
"move_off_locked_folder": "Move out of locked folder", "move_off_locked_folder": "Move out of locked folder",
@@ -1893,6 +1892,7 @@
"remove_assets_title": "Remove assets?", "remove_assets_title": "Remove assets?",
"remove_custom_date_range": "Remove custom date range", "remove_custom_date_range": "Remove custom date range",
"remove_deleted_assets": "Remove Deleted Assets", "remove_deleted_assets": "Remove Deleted Assets",
"remove_filter": "Remove filter",
"remove_from_album": "Remove from album", "remove_from_album": "Remove from album",
"remove_from_album_action_prompt": "{count} removed from the album", "remove_from_album_action_prompt": "{count} removed from the album",
"remove_from_favorites": "Remove from favorites", "remove_from_favorites": "Remove from favorites",
@@ -2192,6 +2192,7 @@
"show_schema": "Show schema", "show_schema": "Show schema",
"show_search_options": "Show search options", "show_search_options": "Show search options",
"show_shared_links": "Show shared links", "show_shared_links": "Show shared links",
"show_slideshow_metadata_overlay": "Show image info overlay",
"show_slideshow_transition": "Show slideshow transition", "show_slideshow_transition": "Show slideshow transition",
"show_supporter_badge": "Supporter badge", "show_supporter_badge": "Supporter badge",
"show_supporter_badge_description": "Show a supporter badge", "show_supporter_badge_description": "Show a supporter badge",
@@ -2207,6 +2208,9 @@
"skip_to_folders": "Skip to folders", "skip_to_folders": "Skip to folders",
"skip_to_tags": "Skip to tags", "skip_to_tags": "Skip to tags",
"slideshow": "Slideshow", "slideshow": "Slideshow",
"slideshow_metadata_overlay_mode": "Overlay content",
"slideshow_metadata_overlay_mode_description_only": "Description only",
"slideshow_metadata_overlay_mode_full": "Full",
"slideshow_repeat": "Repeat slideshow", "slideshow_repeat": "Repeat slideshow",
"slideshow_repeat_description": "Loop back to beginning when slideshow ends", "slideshow_repeat_description": "Loop back to beginning when slideshow ends",
"slideshow_settings": "Slideshow settings", "slideshow_settings": "Slideshow settings",
-13
View File
@@ -1,13 +0,0 @@
{
"name": "immich-i18n",
"version": "2.7.5",
"private": true,
"scripts": {
"format": "prettier --cache --check .",
"format:fix": "prettier --cache --write --list-different ."
},
"devDependencies": {
"prettier": "^3.7.4",
"prettier-plugin-sort-json": "^4.1.1"
}
}
+3 -6
View File
@@ -64,16 +64,13 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
pnpm version "$NEXT_SERVER" --no-git-tag-version pnpm version "$NEXT_SERVER" --no-git-tag-version
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix server pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix server
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix i18n pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix packages/cli
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix cli
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix web pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix web
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix e2e pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix e2e
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix open-api/typescript-sdk pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix packages/sdk
# copy version to open-api spec # copy version to open-api spec
pnpm install --frozen-lockfile --prefix server mise run //:open-api
pnpm --prefix server run build
( cd ./open-api && bash ./bin/generate-open-api.sh )
uv version --directory machine-learning "$NEXT_SERVER" uv version --directory machine-learning "$NEXT_SERVER"
+35 -11
View File
@@ -2,9 +2,9 @@ experimental_monorepo_root = true
[monorepo] [monorepo]
config_roots = [ config_roots = [
"plugins", "packages/plugins",
"server", "server",
"cli", "packages/cli",
"deployment", "deployment",
"mobile", "mobile",
"e2e", "e2e",
@@ -21,11 +21,12 @@ pnpm = "10.33.1"
terragrunt = "1.0.3" terragrunt = "1.0.3"
opentofu = "1.11.6" opentofu = "1.11.6"
java = "21.0.2" java = "21.0.2"
"npm:oazapfts" = "7.5.0"
[tools."github:CQLabs/homebrew-dcm"] [tools."github:CQLabs/homebrew-dcm"]
version = "1.37.0" version = "1.37.0"
bin = "dcm" bin = "dcm"
postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm" postinstall = "chmod +x \"$MISE_TOOL_INSTALL_PATH/dcm\" || true"
[tools."github:jellyfin/jellyfin-ffmpeg"] [tools."github:jellyfin/jellyfin-ffmpeg"]
version = "7.1.3-6" version = "7.1.3-6"
@@ -40,20 +41,43 @@ macos-arm64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz"
experimental = true experimental = true
pin = true pin = true
[tasks.open-api-typescript]
run = [
"oazapfts --optimistic --argumentStyle=object --useEnumType --allSchemas open-api/immich-openapi-specs.json packages/sdk/src/fetch-client.ts",
{ task = "//:sdk:install" },
{ task = "//:sdk:build" },
]
[tasks.open-api-dart]
dir = "open-api"
run = "bash ./bin/generate-dart-sdk.sh"
[tasks.open-api]
env = { SHARP_IGNORE_GLOBAL_LIBVIPS = true }
run = [
{ task = "//server:install" },
{ task = "//server:build" },
{ task = "//server:sync-open-api" },
{ task = ":open-api-typescript"},
{ task = ":open-api-dart"},
]
[tasks.sql]
dir = "server"
run = "node ./dist/bin/sync-sql.js"
# SDK tasks # SDK tasks
[tasks."sdk:install"] [tasks."sdk:install"]
dir = "open-api/typescript-sdk" dir = "packages/sdk"
run = "pnpm install --filter @immich/sdk --frozen-lockfile" run = "pnpm --filter @immich/sdk install --frozen-lockfile"
[tasks."sdk:build"] [tasks."sdk:build"]
dir = "open-api/typescript-sdk" dir = "packages/sdk"
run = "pnpm run build" run = "pnpm build"
# i18n tasks # i18n tasks
[tasks."i18n:format"] [tasks."i18n:format"]
dir = "i18n" run = "pnpm format"
run = "pnpm run format"
[tasks."i18n:format-fix"] [tasks."i18n:format-fix"]
dir = "i18n" run = "pnpm format:fix"
run = "pnpm run format:fix"
+2
View File
@@ -34,6 +34,7 @@ linter:
unrelated_type_equality_checks: true unrelated_type_equality_checks: true
prefer_const_constructors: true prefer_const_constructors: true
always_use_package_imports: true always_use_package_imports: true
always_put_control_body_on_new_line: true
# Additional information about this file can be found at # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options
@@ -50,6 +51,7 @@ analyzer:
# - custom_lint # - custom_lint
errors: errors:
unawaited_futures: warning unawaited_futures: warning
always_put_control_body_on_new_line: warning
custom_lint: custom_lint:
rules: rules:
@@ -416,12 +416,12 @@ class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, p
} }
} }
} }
fun onAndroidUpload(callback: (Result<Unit>) -> Unit) fun onAndroidUpload(maxMinutesArg: Long?, callback: (Result<Unit>) -> Unit)
{ {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$separatedMessageChannelSuffix" val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(null) { channel.send(listOf(maxMinutesArg)) {
if (it is List<*>) { if (it is List<*>) {
if (it.size > 1) { if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
@@ -107,7 +107,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
* This method acts as a bridge between the native Android background task system and Flutter. * This method acts as a bridge between the native Android background task system and Flutter.
*/ */
override fun onInitialized() { override fun onInitialized() {
flutterApi?.onAndroidUpload { handleHostResult(it) } flutterApi?.onAndroidUpload(maxMinutesArg = 20) { handleHostResult(it) }
} }
// TODO: Move this to a separate NotificationManager class // TODO: Move this to a separate NotificationManager class
+9 -3
View File
@@ -217,7 +217,9 @@ List<TranslationParam> _extractParams(String value) {
final icuType = match.group(2)!; final icuType = match.group(2)!;
final icuContent = match.group(3) ?? ''; final icuContent = match.group(3) ?? '';
if (params.containsKey(name)) continue; if (params.containsKey(name)) {
continue;
}
String type; String type;
if (icuType == 'plural' || icuType == 'number') { if (icuType == 'plural' || icuType == 'number') {
@@ -238,7 +240,9 @@ List<TranslationParam> _extractParams(String value) {
for (var i = 0; i < value.length; i++) { for (var i = 0; i < value.length; i++) {
if (value[i] == '{') { if (value[i] == '{') {
if (depth == 0) icuStart = i; if (depth == 0) {
icuStart = i;
}
depth++; depth++;
} else if (value[i] == '}') { } else if (value[i] == '}') {
depth--; depth--;
@@ -256,7 +260,9 @@ List<TranslationParam> _extractParams(String value) {
for (final match in simpleRegex.allMatches(cleanedValue)) { for (final match in simpleRegex.allMatches(cleanedValue)) {
final name = match.group(1)!; final name = match.group(1)!;
if (params.containsKey(name)) continue; if (params.containsKey(name)) {
continue;
}
String type; String type;
if (_kIntParamNames.contains(name.toLowerCase())) { if (_kIntParamNames.contains(name.toLowerCase())) {
+117 -117
View File
@@ -1003,20 +1003,6 @@
1 1
], ],
"type": "index", "type": "index",
"data": {
"on": 1,
"name": "idx_remote_asset_owner_checksum",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)",
"unique": false,
"columns": []
}
},
{
"id": 12,
"references": [
1
],
"type": "index",
"data": { "data": {
"on": 1, "on": 1,
"name": "UQ_remote_assets_owner_checksum", "name": "UQ_remote_assets_owner_checksum",
@@ -1026,7 +1012,7 @@
} }
}, },
{ {
"id": 13, "id": 12,
"references": [ "references": [
1 1
], ],
@@ -1040,7 +1026,7 @@
} }
}, },
{ {
"id": 14, "id": 13,
"references": [ "references": [
1 1
], ],
@@ -1054,7 +1040,7 @@
} }
}, },
{ {
"id": 15, "id": 14,
"references": [ "references": [
1 1
], ],
@@ -1067,36 +1053,22 @@
"columns": [] "columns": []
} }
}, },
{
"id": 15,
"references": [
1
],
"type": "index",
"data": {
"on": 1,
"name": "idx_remote_asset_owner_visibility_deleted_created",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created\nON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)\n",
"unique": false,
"columns": []
}
},
{ {
"id": 16, "id": 16,
"references": [
1
],
"type": "index",
"data": {
"on": 1,
"name": "idx_remote_asset_local_date_time_day",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))",
"unique": false,
"columns": []
}
},
{
"id": 17,
"references": [
1
],
"type": "index",
"data": {
"on": 1,
"name": "idx_remote_asset_local_date_time_month",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))",
"unique": false,
"columns": []
}
},
{
"id": 18,
"references": [], "references": [],
"type": "table", "type": "table",
"data": { "data": {
@@ -1226,7 +1198,7 @@
} }
}, },
{ {
"id": 19, "id": 17,
"references": [ "references": [
0 0
], ],
@@ -1301,7 +1273,7 @@
} }
}, },
{ {
"id": 20, "id": 18,
"references": [ "references": [
0 0
], ],
@@ -1388,7 +1360,7 @@
} }
}, },
{ {
"id": 21, "id": 19,
"references": [ "references": [
1 1
], ],
@@ -1644,7 +1616,7 @@
} }
}, },
{ {
"id": 22, "id": 20,
"references": [ "references": [
1, 1,
4 4
@@ -1718,7 +1690,7 @@
} }
}, },
{ {
"id": 23, "id": 21,
"references": [ "references": [
4, 4,
0 0
@@ -1806,7 +1778,7 @@
} }
}, },
{ {
"id": 24, "id": 22,
"references": [ "references": [
1 1
], ],
@@ -1902,7 +1874,7 @@
} }
}, },
{ {
"id": 25, "id": 23,
"references": [ "references": [
0 0
], ],
@@ -2066,10 +2038,10 @@
} }
}, },
{ {
"id": 26, "id": 24,
"references": [ "references": [
1, 1,
25 23
], ],
"type": "table", "type": "table",
"data": { "data": {
@@ -2140,7 +2112,7 @@
} }
}, },
{ {
"id": 27, "id": 25,
"references": [ "references": [
0 0
], ],
@@ -2284,10 +2256,10 @@
} }
}, },
{ {
"id": 28, "id": 26,
"references": [ "references": [
1, 1,
27 25
], ],
"type": "table", "type": "table",
"data": { "data": {
@@ -2461,7 +2433,7 @@
} }
}, },
{ {
"id": 29, "id": 27,
"references": [], "references": [],
"type": "table", "type": "table",
"data": { "data": {
@@ -2509,7 +2481,7 @@
} }
}, },
{ {
"id": 30, "id": 28,
"references": [], "references": [],
"type": "table", "type": "table",
"data": { "data": {
@@ -2684,7 +2656,7 @@
} }
}, },
{ {
"id": 31, "id": 29,
"references": [ "references": [
1 1
], ],
@@ -2778,7 +2750,7 @@
} }
}, },
{ {
"id": 32, "id": 30,
"references": [], "references": [],
"type": "table", "type": "table",
"data": { "data": {
@@ -2826,13 +2798,13 @@
} }
}, },
{ {
"id": 33, "id": 31,
"references": [ "references": [
20 18
], ],
"type": "index", "type": "index",
"data": { "data": {
"on": 20, "on": 18,
"name": "idx_partner_shared_with_id", "name": "idx_partner_shared_with_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)", "sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)",
"unique": false, "unique": false,
@@ -2840,19 +2812,47 @@
} }
}, },
{ {
"id": 34, "id": 32,
"references": [ "references": [
21 19
], ],
"type": "index", "type": "index",
"data": { "data": {
"on": 21, "on": 19,
"name": "idx_lat_lng", "name": "idx_lat_lng",
"sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)", "sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)",
"unique": false, "unique": false,
"columns": [] "columns": []
} }
}, },
{
"id": 33,
"references": [
19
],
"type": "index",
"data": {
"on": 19,
"name": "idx_remote_exif_city",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city\nON remote_exif_entity (city) WHERE city IS NOT NULL\n",
"unique": false,
"columns": []
}
},
{
"id": 34,
"references": [
20
],
"type": "index",
"data": {
"on": 20,
"name": "idx_remote_album_asset_album_asset",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)",
"unique": false,
"columns": []
}
},
{ {
"id": 35, "id": 35,
"references": [ "references": [
@@ -2861,20 +2861,6 @@
"type": "index", "type": "index",
"data": { "data": {
"on": 22, "on": 22,
"name": "idx_remote_album_asset_album_asset",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)",
"unique": false,
"columns": []
}
},
{
"id": 36,
"references": [
24
],
"type": "index",
"data": {
"on": 24,
"name": "idx_remote_asset_cloud_id", "name": "idx_remote_asset_cloud_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)", "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)",
"unique": false, "unique": false,
@@ -2882,13 +2868,13 @@
} }
}, },
{ {
"id": 37, "id": 36,
"references": [ "references": [
27 25
], ],
"type": "index", "type": "index",
"data": { "data": {
"on": 27, "on": 25,
"name": "idx_person_owner_id", "name": "idx_person_owner_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)", "sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)",
"unique": false, "unique": false,
@@ -2896,13 +2882,13 @@
} }
}, },
{ {
"id": 38, "id": 37,
"references": [ "references": [
28 26
], ],
"type": "index", "type": "index",
"data": { "data": {
"on": 28, "on": 26,
"name": "idx_asset_face_person_id", "name": "idx_asset_face_person_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)", "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)",
"unique": false, "unique": false,
@@ -2910,13 +2896,13 @@
} }
}, },
{ {
"id": 39, "id": 38,
"references": [ "references": [
28 26
], ],
"type": "index", "type": "index",
"data": { "data": {
"on": 28, "on": 26,
"name": "idx_asset_face_asset_id", "name": "idx_asset_face_asset_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)", "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)",
"unique": false, "unique": false,
@@ -2924,13 +2910,27 @@
} }
}, },
{ {
"id": 40, "id": 39,
"references": [ "references": [
30 26
], ],
"type": "index", "type": "index",
"data": { "data": {
"on": 30, "on": 26,
"name": "idx_asset_face_visible_person",
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person\nON asset_face_entity (person_id, asset_id)\nWHERE is_visible = 1 AND deleted_at IS NULL\n",
"unique": false,
"columns": []
}
},
{
"id": 40,
"references": [
28
],
"type": "index",
"data": {
"on": 28,
"name": "idx_trashed_local_asset_checksum", "name": "idx_trashed_local_asset_checksum",
"sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)", "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)",
"unique": false, "unique": false,
@@ -2940,11 +2940,11 @@
{ {
"id": 41, "id": 41,
"references": [ "references": [
30 28
], ],
"type": "index", "type": "index",
"data": { "data": {
"on": 30, "on": 28,
"name": "idx_trashed_local_asset_album", "name": "idx_trashed_local_asset_album",
"sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)", "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)",
"unique": false, "unique": false,
@@ -2954,11 +2954,11 @@
{ {
"id": 42, "id": 42,
"references": [ "references": [
31 29
], ],
"type": "index", "type": "index",
"data": { "data": {
"on": 31, "on": 29,
"name": "idx_asset_edit_asset_id", "name": "idx_asset_edit_asset_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)", "sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)",
"unique": false, "unique": false,
@@ -3066,15 +3066,6 @@
} }
] ]
}, },
{
"name": "idx_remote_asset_owner_checksum",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)"
}
]
},
{ {
"name": "UQ_remote_assets_owner_checksum", "name": "UQ_remote_assets_owner_checksum",
"sql": [ "sql": [
@@ -3112,20 +3103,11 @@
] ]
}, },
{ {
"name": "idx_remote_asset_local_date_time_day", "name": "idx_remote_asset_owner_visibility_deleted_created",
"sql": [ "sql": [
{ {
"dialect": "sqlite", "dialect": "sqlite",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))" "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)"
}
]
},
{
"name": "idx_remote_asset_local_date_time_month",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))"
} }
] ]
}, },
@@ -3282,6 +3264,15 @@
} }
] ]
}, },
{
"name": "idx_remote_exif_city",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL"
}
]
},
{ {
"name": "idx_remote_album_asset_album_asset", "name": "idx_remote_album_asset_album_asset",
"sql": [ "sql": [
@@ -3327,6 +3318,15 @@
} }
] ]
}, },
{
"name": "idx_asset_face_visible_person",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL"
}
]
},
{ {
"name": "idx_trashed_local_asset_checksum", "name": "idx_trashed_local_asset_checksum",
"sql": [ "sql": [
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -348,7 +348,7 @@ class BackgroundWorkerBgHostApiSetup {
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. /// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
protocol BackgroundWorkerFlutterApiProtocol { protocol BackgroundWorkerFlutterApiProtocol {
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
func onAndroidUpload(completion: @escaping (Result<Void, PigeonError>) -> Void) func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
func cancel(completion: @escaping (Result<Void, PigeonError>) -> Void) func cancel(completion: @escaping (Result<Void, PigeonError>) -> Void)
} }
class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol { class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
@@ -379,10 +379,10 @@ class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
} }
} }
} }
func onAndroidUpload(completion: @escaping (Result<Void, PigeonError>) -> Void) { func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) {
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload\(messageChannelSuffix)" let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload\(messageChannelSuffix)"
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
channel.sendMessage(nil) { response in channel.sendMessage([maxMinutesArg] as [Any?]) { response in
guard let listResponse = response as? [Any?] else { guard let listResponse = response as? [Any?] else {
completion(.failure(createConnectionError(withChannelName: channelName))) completion(.failure(createConnectionError(withChannelName: channelName)))
return return
@@ -61,8 +61,12 @@ class RemoteAlbum {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (other is! RemoteAlbum) return false; if (other is! RemoteAlbum) {
if (identical(this, other)) return true; return false;
}
if (identical(this, other)) {
return true;
}
return id == other.id && return id == other.id &&
name == other.name && name == other.name &&
ownerId == other.ownerId && ownerId == other.ownerId &&
@@ -49,8 +49,12 @@ class LocalAlbum {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (other is! LocalAlbum) return false; if (other is! LocalAlbum) {
if (identical(this, other)) return true; return false;
}
if (identical(this, other)) {
return true;
}
return other.id == id && return other.id == id &&
other.name == name && other.name == name &&
@@ -51,12 +51,18 @@ sealed class BaseAsset {
bool get isAnimatedImage => playbackStyle == AssetPlaybackStyle.imageAnimated; bool get isAnimatedImage => playbackStyle == AssetPlaybackStyle.imageAnimated;
AssetPlaybackStyle get playbackStyle { AssetPlaybackStyle get playbackStyle {
if (isVideo) return AssetPlaybackStyle.video; if (isVideo) {
if (isMotionPhoto) return AssetPlaybackStyle.livePhoto; return AssetPlaybackStyle.video;
}
if (isMotionPhoto) {
return AssetPlaybackStyle.livePhoto;
}
if (isImage && durationMs != null && durationMs! > 0) { if (isImage && durationMs != null && durationMs! > 0) {
return AssetPlaybackStyle.imageAnimated; return AssetPlaybackStyle.imageAnimated;
} }
if (isImage) return AssetPlaybackStyle.image; if (isImage) {
return AssetPlaybackStyle.image;
}
return AssetPlaybackStyle.unknown; return AssetPlaybackStyle.unknown;
} }
@@ -98,7 +104,9 @@ sealed class BaseAsset {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
if (other is BaseAsset) { if (other is BaseAsset) {
return name == other.name && return name == other.name &&
type == other.type && type == other.type &&
@@ -74,8 +74,12 @@ class LocalAsset extends BaseAsset {
// Not checking for remoteId here // Not checking for remoteId here
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (other is! LocalAsset) return false; if (other is! LocalAsset) {
if (identical(this, other)) return true; return false;
}
if (identical(this, other)) {
return true;
}
return super == other && return super == other &&
id == other.id && id == other.id &&
cloudId == other.cloudId && cloudId == other.cloudId &&
@@ -10,6 +10,7 @@ class RemoteAsset extends BaseAsset {
final AssetVisibility visibility; final AssetVisibility visibility;
final String ownerId; final String ownerId;
final String? stackId; final String? stackId;
final DateTime? uploadedAt;
const RemoteAsset({ const RemoteAsset({
required this.id, required this.id,
@@ -20,6 +21,7 @@ class RemoteAsset extends BaseAsset {
required super.type, required super.type,
required super.createdAt, required super.createdAt,
required super.updatedAt, required super.updatedAt,
this.uploadedAt,
super.width, super.width,
super.height, super.height,
super.durationMs, super.durationMs,
@@ -55,6 +57,7 @@ class RemoteAsset extends BaseAsset {
type: $type, type: $type,
createdAt: $createdAt, createdAt: $createdAt,
updatedAt: $updatedAt, updatedAt: $updatedAt,
uploadedAt: ${uploadedAt ?? "<NA>"},
width: ${width ?? "<NA>"}, width: ${width ?? "<NA>"},
height: ${height ?? "<NA>"}, height: ${height ?? "<NA>"},
durationMs: ${durationMs ?? "<NA>"}, durationMs: ${durationMs ?? "<NA>"},
@@ -71,14 +74,19 @@ class RemoteAsset extends BaseAsset {
// Not checking for localId here // Not checking for localId here
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (other is! RemoteAsset) return false; if (other is! RemoteAsset) {
if (identical(this, other)) return true; return false;
}
if (identical(this, other)) {
return true;
}
return super == other && return super == other &&
id == other.id && id == other.id &&
ownerId == other.ownerId && ownerId == other.ownerId &&
thumbHash == other.thumbHash && thumbHash == other.thumbHash &&
visibility == other.visibility && visibility == other.visibility &&
stackId == other.stackId; stackId == other.stackId &&
uploadedAt == other.uploadedAt;
} }
@override @override
@@ -89,7 +97,8 @@ class RemoteAsset extends BaseAsset {
localId.hashCode ^ localId.hashCode ^
thumbHash.hashCode ^ thumbHash.hashCode ^
visibility.hashCode ^ visibility.hashCode ^
stackId.hashCode; stackId.hashCode ^
uploadedAt.hashCode;
RemoteAsset copyWith({ RemoteAsset copyWith({
String? id, String? id,
@@ -100,6 +109,7 @@ class RemoteAsset extends BaseAsset {
AssetType? type, AssetType? type,
DateTime? createdAt, DateTime? createdAt,
DateTime? updatedAt, DateTime? updatedAt,
DateTime? uploadedAt,
int? width, int? width,
int? height, int? height,
int? durationMs, int? durationMs,
@@ -119,6 +129,7 @@ class RemoteAsset extends BaseAsset {
type: type ?? this.type, type: type ?? this.type,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt, updatedAt: updatedAt ?? this.updatedAt,
uploadedAt: uploadedAt ?? this.uploadedAt,
width: width ?? this.width, width: width ?? this.width,
height: height ?? this.height, height: height ?? this.height,
durationMs: durationMs ?? this.durationMs, durationMs: durationMs ?? this.durationMs,
@@ -144,6 +155,7 @@ class RemoteAssetExif extends RemoteAsset {
required super.type, required super.type,
required super.createdAt, required super.createdAt,
required super.updatedAt, required super.updatedAt,
super.uploadedAt,
super.width, super.width,
super.height, super.height,
super.durationMs, super.durationMs,
@@ -158,8 +170,12 @@ class RemoteAssetExif extends RemoteAsset {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (other is! RemoteAssetExif) return false; if (other is! RemoteAssetExif) {
if (identical(this, other)) return true; return false;
}
if (identical(this, other)) {
return true;
}
return super == other && exifInfo == other.exifInfo; return super == other && exifInfo == other.exifInfo;
} }
@@ -176,6 +192,7 @@ class RemoteAssetExif extends RemoteAsset {
AssetType? type, AssetType? type,
DateTime? createdAt, DateTime? createdAt,
DateTime? updatedAt, DateTime? updatedAt,
DateTime? uploadedAt,
int? width, int? width,
int? height, int? height,
int? durationMs, int? durationMs,
@@ -196,6 +213,7 @@ class RemoteAssetExif extends RemoteAsset {
type: type ?? this.type, type: type ?? this.type,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt, updatedAt: updatedAt ?? this.updatedAt,
uploadedAt: uploadedAt ?? this.uploadedAt,
width: width ?? this.width, width: width ?? this.width,
height: height ?? this.height, height: height ?? this.height,
durationMs: durationMs ?? this.durationMs, durationMs: durationMs ?? this.durationMs,
@@ -68,7 +68,9 @@ class AssetFace {
@override @override
bool operator ==(covariant AssetFace other) { bool operator ==(covariant AssetFace other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
return other.id == id && return other.id == id &&
other.assetId == assetId && other.assetId == assetId &&
@@ -1,22 +1,58 @@
import 'package:immich_mobile/domain/models/config/cleanup_config.dart'; import 'package:immich_mobile/domain/models/config/cleanup_config.dart';
import 'package:immich_mobile/domain/models/config/image_config.dart';
import 'package:immich_mobile/domain/models/config/map_config.dart';
import 'package:immich_mobile/domain/models/config/theme_config.dart'; import 'package:immich_mobile/domain/models/config/theme_config.dart';
import 'package:immich_mobile/domain/models/config/timeline_config.dart';
import 'package:immich_mobile/domain/models/config/viewer_config.dart';
class AppConfig { class AppConfig {
final ThemeConfig theme; final ThemeConfig theme;
final CleanupConfig cleanup; final CleanupConfig cleanup;
final MapConfig map;
final TimelineConfig timeline;
final ImageConfig image;
final ViewerConfig viewer;
const AppConfig({this.theme = const .new(), this.cleanup = const .new()}); const AppConfig({
this.theme = const .new(),
this.cleanup = const .new(),
this.map = const .new(),
this.timeline = const .new(),
this.image = const .new(),
this.viewer = const .new(),
});
AppConfig copyWith({ThemeConfig? theme, CleanupConfig? cleanup}) => AppConfig copyWith({
.new(theme: theme ?? this.theme, cleanup: cleanup ?? this.cleanup); ThemeConfig? theme,
CleanupConfig? cleanup,
MapConfig? map,
TimelineConfig? timeline,
ImageConfig? image,
ViewerConfig? viewer,
}) => .new(
theme: theme ?? this.theme,
cleanup: cleanup ?? this.cleanup,
map: map ?? this.map,
timeline: timeline ?? this.timeline,
image: image ?? this.image,
viewer: viewer ?? this.viewer,
);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || (other is AppConfig && other.theme == theme && other.cleanup == cleanup); identical(this, other) ||
(other is AppConfig &&
other.theme == theme &&
other.cleanup == cleanup &&
other.map == map &&
other.timeline == timeline &&
other.image == image &&
other.viewer == viewer);
@override @override
int get hashCode => Object.hash(theme, cleanup); int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer);
@override @override
String toString() => 'AppConfig(theme: $theme, cleanup: $cleanup)'; String toString() =>
'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer)';
} }
@@ -0,0 +1,20 @@
class ImageConfig {
final bool preferRemote;
final bool loadOriginal;
const ImageConfig({this.preferRemote = false, this.loadOriginal = false});
ImageConfig copyWith({bool? preferRemote, bool? loadOriginal}) =>
ImageConfig(preferRemote: preferRemote ?? this.preferRemote, loadOriginal: loadOriginal ?? this.loadOriginal);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is ImageConfig && other.preferRemote == preferRemote && other.loadOriginal == loadOriginal);
@override
int get hashCode => Object.hash(preferRemote, loadOriginal);
@override
String toString() => 'ImageConfig(preferRemoteImage: $preferRemote, loadOriginal: $loadOriginal)';
}
@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
class MapConfig {
final int relativeDays;
final bool favoritesOnly;
final bool includeArchived;
final ThemeMode themeMode;
final bool withPartners;
const MapConfig({
this.relativeDays = 0,
this.favoritesOnly = false,
this.includeArchived = false,
this.themeMode = ThemeMode.system,
this.withPartners = false,
});
MapConfig copyWith({
int? relativeDays,
bool? favoritesOnly,
bool? includeArchived,
ThemeMode? themeMode,
bool? withPartners,
}) => MapConfig(
relativeDays: relativeDays ?? this.relativeDays,
favoritesOnly: favoritesOnly ?? this.favoritesOnly,
includeArchived: includeArchived ?? this.includeArchived,
themeMode: themeMode ?? this.themeMode,
withPartners: withPartners ?? this.withPartners,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is MapConfig &&
other.relativeDays == relativeDays &&
other.favoritesOnly == favoritesOnly &&
other.includeArchived == includeArchived &&
other.themeMode == themeMode &&
other.withPartners == withPartners);
@override
int get hashCode => Object.hash(relativeDays, favoritesOnly, includeArchived, themeMode, withPartners);
@override
String toString() =>
'MapConfig(relativeDays: $relativeDays, favoritesOnly: $favoritesOnly, includeArchived: $includeArchived, themeMode: $themeMode, withPartners: $withPartners)';
}
@@ -0,0 +1,30 @@
import 'package:immich_mobile/domain/models/timeline.model.dart';
class TimelineConfig {
final int tilesPerRow;
final GroupAssetsBy groupAssetsBy;
final bool storageIndicator;
const TimelineConfig({this.tilesPerRow = 4, this.groupAssetsBy = GroupAssetsBy.day, this.storageIndicator = true});
TimelineConfig copyWith({int? tilesPerRow, GroupAssetsBy? groupAssetsBy, bool? storageIndicator}) => TimelineConfig(
tilesPerRow: tilesPerRow ?? this.tilesPerRow,
groupAssetsBy: groupAssetsBy ?? this.groupAssetsBy,
storageIndicator: storageIndicator ?? this.storageIndicator,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is TimelineConfig &&
other.tilesPerRow == tilesPerRow &&
other.groupAssetsBy == groupAssetsBy &&
other.storageIndicator == storageIndicator);
@override
int get hashCode => Object.hash(tilesPerRow, groupAssetsBy, storageIndicator);
@override
String toString() =>
'TimelineConfig(tilesPerRow: $tilesPerRow, groupAssetsBy: $groupAssetsBy, storageIndicator: $storageIndicator)';
}
@@ -0,0 +1,37 @@
class ViewerConfig {
final bool loopVideo;
final bool loadOriginalVideo;
final bool autoPlayVideo;
final bool tapToNavigate;
const ViewerConfig({
this.loopVideo = true,
this.loadOriginalVideo = false,
this.autoPlayVideo = true,
this.tapToNavigate = false,
});
ViewerConfig copyWith({bool? loopVideo, bool? loadOriginalVideo, bool? autoPlayVideo, bool? tapToNavigate}) =>
ViewerConfig(
loopVideo: loopVideo ?? this.loopVideo,
loadOriginalVideo: loadOriginalVideo ?? this.loadOriginalVideo,
autoPlayVideo: autoPlayVideo ?? this.autoPlayVideo,
tapToNavigate: tapToNavigate ?? this.tapToNavigate,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is ViewerConfig &&
other.loopVideo == loopVideo &&
other.loadOriginalVideo == loadOriginalVideo &&
other.autoPlayVideo == autoPlayVideo &&
other.tapToNavigate == tapToNavigate);
@override
int get hashCode => Object.hash(loopVideo, loadOriginalVideo, autoPlayVideo, tapToNavigate);
@override
String toString() =>
'ViewerConfig(loopVideo: $loopVideo, loadOriginalVideo: $loadOriginalVideo, autoPlayVideo: $autoPlayVideo, tapToNavigate: $tapToNavigate)';
}
+3 -1
View File
@@ -69,7 +69,9 @@ class ExifInfo {
@override @override
bool operator ==(covariant ExifInfo other) { bool operator ==(covariant ExifInfo other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
return other.fileSize == fileSize && return other.fileSize == fileSize &&
other.description == description && other.description == description &&
+3 -1
View File
@@ -20,7 +20,9 @@ class LogMessage {
@override @override
bool operator ==(covariant LogMessage other) { bool operator ==(covariant LogMessage other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
return other.message == message && return other.message == message &&
other.level == level && other.level == level &&
+3 -1
View File
@@ -8,7 +8,9 @@ class Marker {
@override @override
bool operator ==(covariant Marker other) { bool operator ==(covariant Marker other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
return other.location == location && other.assetId == assetId; return other.location == location && other.assetId == assetId;
} }
+6 -3
View File
@@ -2,7 +2,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
enum MemoryTypeEnum { enum MemoryTypeEnum {
@@ -36,7 +35,9 @@ class MemoryData {
@override @override
bool operator ==(covariant MemoryData other) { bool operator ==(covariant MemoryData other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
return other.year == year; return other.year == year;
} }
@@ -132,7 +133,9 @@ class DriftMemory {
@override @override
bool operator ==(covariant DriftMemory other) { bool operator ==(covariant DriftMemory other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
final listEquals = const DeepCollectionEquality().equals; final listEquals = const DeepCollectionEquality().equals;
return other.id == id && return other.id == id &&
+37 -3
View File
@@ -7,6 +7,7 @@ import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/config/system_config.dart'; import 'package:immich_mobile/domain/models/config/system_config.dart';
import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
enum MetadataDomain<T extends Object> { enum MetadataDomain<T extends Object> {
appConfig<AppConfig>('config.app'), appConfig<AppConfig>('config.app'),
@@ -23,9 +24,36 @@ enum MetadataKey<T extends Object> {
themeDynamic<bool>(.appConfig, 'theme.dynamic', false), themeDynamic<bool>(.appConfig, 'theme.dynamic', false),
themeColorfulInterface<bool>(.appConfig, 'theme.colorfulInterface', true), 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),
// Timeline
timelineTilesPerRow<int>(.appConfig, 'timeline.tilesPerRow', 4),
timelineGroupAssetsBy<GroupAssetsBy>(
.appConfig,
'timeline.groupAssetsBy',
GroupAssetsBy.day,
_EnumCodec(GroupAssetsBy.values),
),
timelineStorageIndicator<bool>(.appConfig, 'timeline.storageIndicator', true),
// Log // Log
logLevel<LogLevel>(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)), logLevel<LogLevel>(.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 // Cleanup
cleanupKeepFavorites<bool>(.appConfig, 'cleanup.keepFavorites', true), cleanupKeepFavorites<bool>(.appConfig, 'cleanup.keepFavorites', true),
cleanupKeepMediaType<AssetKeepType>( cleanupKeepMediaType<AssetKeepType>(
@@ -115,12 +143,18 @@ final class _ListCodec<T extends Object> extends _MetadataCodec<List<T>> {
List<T>? decode(String raw) { List<T>? decode(String raw) {
try { try {
final decoded = jsonDecode(raw); final decoded = jsonDecode(raw);
if (decoded is! List) return null; if (decoded is! List) {
return null;
}
final result = <T>[]; final result = <T>[];
for (final item in decoded) { for (final item in decoded) {
if (item is! String) return null; if (item is! String) {
return null;
}
final element = _elementCodec.decode(item); final element = _elementCodec.decode(item);
if (element == null) return null; if (element == null) {
return null;
}
result.add(element); result.add(element);
} }
return result; return result;
+6 -2
View File
@@ -69,7 +69,9 @@ class PersonDto {
@override @override
bool operator ==(covariant PersonDto other) { bool operator ==(covariant PersonDto other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
return other.id == id && return other.id == id &&
other.birthDate == birthDate && other.birthDate == birthDate &&
@@ -160,7 +162,9 @@ class DriftPerson {
@override @override
bool operator ==(covariant DriftPerson other) { bool operator ==(covariant DriftPerson other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
return other.id == id && return other.id == id &&
other.createdAt == createdAt && other.createdAt == createdAt &&
@@ -12,7 +12,9 @@ class SearchResult {
@override @override
bool operator ==(covariant SearchResult other) { bool operator ==(covariant SearchResult other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
final listEquals = const DeepCollectionEquality().equals; final listEquals = const DeepCollectionEquality().equals;
return listEquals(other.assets, assets) && other.nextPage == nextPage; return listEquals(other.assets, assets) && other.nextPage == nextPage;
@@ -1,13 +1,6 @@
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
enum Setting<T> { enum Setting<T> {
tilesPerRow<int>(StoreKey.tilesPerRow, 4),
groupAssetsBy<int>(StoreKey.groupAssetsBy, 0),
showStorageIndicator<bool>(StoreKey.storageIndicator, true),
loadOriginal<bool>(StoreKey.loadOriginal, false),
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, false),
autoPlayVideo<bool>(StoreKey.autoPlayVideo, true),
preferRemoteImage<bool>(StoreKey.preferRemoteImage, false),
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false), advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
enableBackup<bool>(StoreKey.enableBackup, false); enableBackup<bool>(StoreKey.enableBackup, false);
+6 -2
View File
@@ -37,7 +37,9 @@ class Stack {
@override @override
bool operator ==(covariant Stack other) { bool operator ==(covariant Stack other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
return other.id == id && return other.id == id &&
other.createdAt == createdAt && other.createdAt == createdAt &&
@@ -61,7 +63,9 @@ class StackResponse {
@override @override
bool operator ==(covariant StackResponse other) { bool operator ==(covariant StackResponse other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
return other.id == id && other.primaryAssetId == primaryAssetId && other.assetIds == assetIds; return other.id == id && other.primaryAssetId == primaryAssetId && other.assetIds == assetIds;
} }
+17 -49
View File
@@ -4,50 +4,18 @@ import 'package:immich_mobile/domain/models/user.model.dart';
/// Defines the data type for each value /// Defines the data type for each value
enum StoreKey<T> { enum StoreKey<T> {
version<int>._(0), version<int>._(0),
assetETag<String>._(1),
currentUser<UserDto>._(2), currentUser<UserDto>._(2),
deviceIdHash<int>._(3),
deviceId<String>._(4), deviceId<String>._(4),
backupFailedSince<DateTime>._(5),
backupRequireWifi<bool>._(6),
backupRequireCharging<bool>._(7), backupRequireCharging<bool>._(7),
backupTriggerDelay<int>._(8), backupTriggerDelay<int>._(8),
serverUrl<String>._(10), serverUrl<String>._(10),
accessToken<String>._(11), accessToken<String>._(11),
serverEndpoint<String>._(12), serverEndpoint<String>._(12),
autoBackup<bool>._(13),
backgroundBackup<bool>._(14),
sslClientCertData<String>._(15),
sslClientPasswd<String>._(16),
// user settings from [AppSettingsEnum] below:
loadPreview<bool>._(100),
loadOriginal<bool>._(101),
tilesPerRow<int>._(103),
dynamicLayout<bool>._(104),
groupAssetsBy<int>._(105),
uploadErrorNotificationGracePeriod<int>._(106),
backgroundBackupTotalProgress<bool>._(107),
backgroundBackupSingleProgress<bool>._(108),
storageIndicator<bool>._(109),
thumbnailCacheSize<int>._(110),
imageCacheSize<int>._(111),
albumThumbnailCacheSize<int>._(112),
selectedAlbumSortOrder<int>._(113), selectedAlbumSortOrder<int>._(113),
advancedTroubleshooting<bool>._(114), advancedTroubleshooting<bool>._(114),
preferRemoteImage<bool>._(116),
loopVideo<bool>._(117),
// map related settings
mapShowFavoriteOnly<bool>._(118),
mapRelativeDate<int>._(119),
selfSignedCert<bool>._(120),
mapIncludeArchived<bool>._(121),
ignoreIcloudAssets<bool>._(122),
selectedAlbumSortReverse<bool>._(123), selectedAlbumSortReverse<bool>._(123),
mapThemeMode<int>._(124),
mapwithPartners<bool>._(125),
enableHapticFeedback<bool>._(126), enableHapticFeedback<bool>._(126),
customHeaders<String>._(127), customHeaders<String>._(127),
syncAlbums<bool>._(131), syncAlbums<bool>._(131),
// Auto endpoint switching // Auto endpoint switching
@@ -56,34 +24,24 @@ enum StoreKey<T> {
localEndpoint<String>._(134), localEndpoint<String>._(134),
externalEndpointList<String>._(135), externalEndpointList<String>._(135),
// Video settings
loadOriginalVideo<bool>._(136),
manageLocalMediaAndroid<bool>._(137), manageLocalMediaAndroid<bool>._(137),
// Read-only Mode settings // Read-only Mode settings
readonlyModeEnabled<bool>._(138), readonlyModeEnabled<bool>._(138),
autoPlayVideo<bool>._(139),
albumGridView<bool>._(140), albumGridView<bool>._(140),
// Image viewer navigation settings
tapToNavigate<bool>._(141),
// Experimental stuff // Experimental stuff
photoManagerCustomFilter<bool>._(1000),
betaPromptShown<bool>._(1001),
betaTimeline<bool>._(1002),
enableBackup<bool>._(1003), enableBackup<bool>._(1003),
useWifiForUploadVideos<bool>._(1004), useWifiForUploadVideos<bool>._(1004),
useWifiForUploadPhotos<bool>._(1005), useWifiForUploadPhotos<bool>._(1005),
needBetaMigration<bool>._(1006),
// TODO: Remove this after patching open-api
shouldResetSync<bool>._(1007),
// Free up space
syncMigrationStatus<String>._(1013), syncMigrationStatus<String>._(1013),
// Legacy keys that have been migrated to the new metadata store // Legacy keys that have been migrated to the new metadata store
legacyLoopVideo<bool>._(117),
legacyLoadOriginalVideo<bool>._(136),
legacyAutoPlayVideo<bool>._(139),
legacyTapToNavigate<bool>._(141),
legacyPreferRemoteImage<bool>._(116),
legacyLoadOriginal<bool>._(101),
legacyPrimaryColor<String>._(128), legacyPrimaryColor<String>._(128),
legacyDynamicTheme<bool>._(129), legacyDynamicTheme<bool>._(129),
legacyColorfulInterface<bool>._(130), legacyColorfulInterface<bool>._(130),
@@ -93,6 +51,14 @@ enum StoreKey<T> {
legacyCleanupKeepAlbumIds<String>._(1010), legacyCleanupKeepAlbumIds<String>._(1010),
legacyCleanupCutoffDaysAgo<int>._(1011), legacyCleanupCutoffDaysAgo<int>._(1011),
legacyCleanupDefaultsInitialized<bool>._(1012), legacyCleanupDefaultsInitialized<bool>._(1012),
legacyTilesPerRow<int>._(103),
legacyGroupAssetsBy<int>._(105),
legacyStorageIndicator<bool>._(109),
legacyMapRelativeDate<int>._(119),
legacyMapShowFavoriteOnly<bool>._(118),
legacyMapIncludeArchived<bool>._(121),
legacyMapThemeMode<int>._(124),
legacyMapwithPartners<bool>._(125),
legacyLogLevel<int>._(115); legacyLogLevel<int>._(115);
const StoreKey._(this.id); const StoreKey._(this.id);
@@ -117,7 +83,9 @@ StoreDto: {
@override @override
bool operator ==(covariant StoreDto<T> other) { bool operator ==(covariant StoreDto<T> other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
return other.key == key && other.value == value; return other.key == key && other.value == value;
} }
+3 -1
View File
@@ -13,7 +13,9 @@ class Tag {
@override @override
bool operator ==(covariant Tag other) { bool operator ==(covariant Tag other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
return other.id == id && other.value == value; return other.id == id && other.value == value;
} }
@@ -2,6 +2,8 @@ enum GroupAssetsBy { day, month, auto, none }
enum HeaderType { none, month, day, monthAndDay } enum HeaderType { none, month, day, monthAndDay }
enum SortAssetsBy { taken, uploaded }
class Bucket { class Bucket {
final int assetCount; final int assetCount;
+6 -2
View File
@@ -125,7 +125,9 @@ profileChangedAt: $profileChangedAt
@override @override
bool operator ==(covariant UserDto other) { bool operator ==(covariant UserDto other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
return other.id == id && return other.id == id &&
((updatedAt == null && other.updatedAt == null) || ((updatedAt == null && other.updatedAt == null) ||
@@ -219,7 +221,9 @@ class PartnerUserDto {
@override @override
bool operator ==(covariant PartnerUserDto other) { bool operator ==(covariant PartnerUserDto other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
return other.id == id && return other.id == id &&
other.email == email && other.email == email &&
@@ -35,7 +35,9 @@ isOnboarded: $isOnboarded,
@override @override
bool operator ==(covariant Onboarding other) { bool operator ==(covariant Onboarding other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
return isOnboarded == other.isOnboarded; return isOnboarded == other.isOnboarded;
} }
@@ -132,7 +134,9 @@ showSupportBadge: $showSupportBadge,
@override @override
bool operator ==(covariant Preferences other) { bool operator ==(covariant Preferences other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
return other.foldersEnabled == foldersEnabled && return other.foldersEnabled == foldersEnabled &&
other.memoriesEnabled == memoriesEnabled && other.memoriesEnabled == memoriesEnabled &&
@@ -199,7 +203,9 @@ licenseKey: $licenseKey,
@override @override
bool operator ==(covariant License other) { bool operator ==(covariant License other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
return activatedAt == other.activatedAt && activationKey == other.activationKey && licenseKey == other.licenseKey; return activatedAt == other.activatedAt && activationKey == other.activationKey && licenseKey == other.licenseKey;
} }
@@ -251,7 +257,9 @@ license: ${license ?? "<NA>"},
@override @override
bool operator ==(covariant UserMetadata other) { bool operator ==(covariant UserMetadata other) {
if (identical(this, other)) return true; if (identical(this, other)) {
return true;
}
return other.userId == userId && return other.userId == userId &&
other.key == key && other.key == key &&
@@ -105,46 +105,58 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
} }
@override @override
Future<void> onAndroidUpload() async { Future<void> onAndroidUpload(int? maxMinutes) async {
_logger.info('Android background processing started'); final hashTimeout = Duration(minutes: _isBackupEnabled ? 3 : 6);
final sw = Stopwatch()..start(); final backupTimeout = maxMinutes != null ? Duration(minutes: maxMinutes - 1) : null;
try { return _backgroundLoop(
if (!await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6))) { hashTimeout: hashTimeout,
_logger.warning("Remote sync did not complete successfully, skipping backup"); backupTimeout: backupTimeout,
return; debugLabel: 'Android background upload',
} );
await _handleBackup();
} catch (error, stack) {
_logger.severe("Failed to complete Android background processing", error, stack);
} finally {
sw.stop();
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
await _cleanup();
}
} }
@override @override
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async { Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
_logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s'); final hashTimeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
final backupTimeout = maxSeconds != null ? Duration(seconds: maxSeconds - 1) : null;
return _backgroundLoop(hashTimeout: hashTimeout, backupTimeout: backupTimeout, debugLabel: 'iOS background upload');
}
Future<void> _backgroundLoop({
required Duration hashTimeout,
required Duration? backupTimeout,
required String debugLabel,
}) async {
_logger.info(
'$debugLabel started hashTimeout: ${hashTimeout.inSeconds}s, backupTimeout: ${backupTimeout?.inMinutes ?? '~'}m',
);
final sw = Stopwatch()..start(); final sw = Stopwatch()..start();
try { try {
final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6); if (!await _syncAssets(hashTimeout: hashTimeout)) {
if (!await _syncAssets(hashTimeout: timeout)) {
_logger.warning("Remote sync did not complete successfully, skipping backup"); _logger.warning("Remote sync did not complete successfully, skipping backup");
return; return;
} }
final backupFuture = _handleBackup(); final backupFuture = _handleBackup();
if (maxSeconds != null) { Timer? cancelTimer;
await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {}); if (backupTimeout != null) {
} else { cancelTimer = Timer(backupTimeout, () {
if (!_cancellationToken.isCompleted) {
_logger.warning("$debugLabel timed out after ${backupTimeout.inMinutes}m, cancelling backup");
_cancellationToken.complete();
}
});
}
try {
await backupFuture; await backupFuture;
} finally {
cancelTimer?.cancel();
} }
} catch (error, stack) { } catch (error, stack) {
_logger.severe("Failed to complete iOS background upload", error, stack); _logger.severe("Failed to complete $debugLabel", error, stack);
} finally { } finally {
sw.stop(); sw.stop();
_logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s"); _logger.info("$debugLabel completed in ${sw.elapsed.inSeconds}s");
await _cleanup(); await _cleanup();
} }
} }
@@ -177,7 +189,9 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
final nativeSyncApi = _ref?.read(nativeSyncApiProvider); final nativeSyncApi = _ref?.read(nativeSyncApiProvider);
_logger.info("Cleaning up background worker"); _logger.info("Cleaning up background worker");
_cancellationToken.complete(); if (!_cancellationToken.isCompleted) {
_cancellationToken.complete();
}
final cleanupFutures = [ final cleanupFutures = [
nativeSyncApi?.cancelHashing(), nativeSyncApi?.cancelHashing(),
workerManagerPatch.dispose().catchError((_) async { workerManagerPatch.dispose().catchError((_) async {
@@ -93,8 +93,7 @@ class LocalSyncService {
if (CurrentPlatform.isIOS) { if (CurrentPlatform.isIOS) {
// On iOS, we need to full sync albums that are marked as cloud as the delta sync // On iOS, we need to full sync albums that are marked as cloud as the delta sync
// does not include changes for cloud albums. If ignoreIcloudAssets is enabled, // does not include changes for cloud albums.
// remove the albums from the local database from the previous sync
final cloudAlbums = deviceAlbums.where((a) => a.isCloud).toLocalAlbums(); final cloudAlbums = deviceAlbums.where((a) => a.isCloud).toLocalAlbums();
for (final album in cloudAlbums) { for (final album in cloudAlbums) {
final dbAlbum = dbAlbums.firstWhereOrNull((a) => a.id == album.id); final dbAlbum = dbAlbums.firstWhereOrNull((a) => a.id == album.id);
@@ -184,7 +184,9 @@ class RemoteAlbumService {
List<RemoteAlbum> albums, { List<RemoteAlbum> albums, {
required AssetDateAggregation aggregation, required AssetDateAggregation aggregation,
}) async { }) async {
if (albums.isEmpty) return []; if (albums.isEmpty) {
return [];
}
final albumIds = albums.map((e) => e.id).toList(); final albumIds = albums.map((e) => e.id).toList();
final sortedIds = await _repository.getSortedAlbumIds(albumIds, aggregation: aggregation); final sortedIds = await _repository.getSortedAlbumIds(albumIds, aggregation: aggregation);
@@ -72,7 +72,9 @@ class StoreService {
/// Stores the [value] for the [key]. Skips write if value hasn't changed. /// Stores the [value] for the [key]. Skips write if value hasn't changed.
Future<void> put<U extends StoreKey<T>, T>(U key, T value) async { Future<void> put<U extends StoreKey<T>, T>(U key, T value) async {
if (_cache[key.id] == value) return; if (_cache[key.id] == value) {
return;
}
await _storeRepository.upsert(key, value); await _storeRepository.upsert(key, value);
_cache[key.id] = value; _cache[key.id] = value;
} }
@@ -24,6 +24,7 @@ enum SyncMigrationTask {
v20260128_ResetExifV1, // EXIF table has incorrect width and height information. v20260128_ResetExifV1, // EXIF table has incorrect width and height information.
v20260128_CopyExifWidthHeightToAsset, // Asset table has incorrect width and height for video ratio calculations. v20260128_CopyExifWidthHeightToAsset, // Asset table has incorrect width and height for video ratio calculations.
v20260128_ResetAssetV1, // Asset v2.5.0 has width and height information that were edited assets. v20260128_ResetAssetV1, // Asset v2.5.0 has width and height information that were edited assets.
v20260597_ResetAssetV1AssetV2, // Assets didn't include the uploadedAt column.
} }
class SyncStreamService { class SyncStreamService {
@@ -132,6 +133,13 @@ class SyncStreamService {
migrations.add(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name); migrations.add(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name);
} }
} }
if (!migrations.contains(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name) &&
semVer > const SemVer(major: 2, minor: 7, patch: 5)) {
_logger.info("Running pre-sync task: v20260597_ResetAssetV1AssetV2");
await _syncApiRepository.deleteSyncAck([SyncEntityType.assetV1, SyncEntityType.assetV2]);
migrations.add(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name);
}
} }
Future<void> _runPostSyncTasks(List<String> migrations) async { Future<void> _runPostSyncTasks(List<String> migrations) async {
@@ -318,7 +326,9 @@ class SyncStreamService {
} }
Future<void> handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) async { Future<void> handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) async {
if (batchData.isEmpty) return; if (batchData.isEmpty) {
return;
}
_logger.info('Processing batch of ${batchData.length} AssetUploadReadyV1 events'); _logger.info('Processing batch of ${batchData.length} AssetUploadReadyV1 events');
@@ -359,7 +369,9 @@ class SyncStreamService {
} }
Future<void> handleWsAssetUploadReadyV2Batch(List<dynamic> batchData) async { Future<void> handleWsAssetUploadReadyV2Batch(List<dynamic> batchData) async {
if (batchData.isEmpty) return; if (batchData.isEmpty) {
return;
}
_logger.info('Processing batch of ${batchData.length} AssetUploadReadyV2 events'); _logger.info('Processing batch of ${batchData.length} AssetUploadReadyV2 events');
@@ -5,10 +5,9 @@ import 'package:collection/collection.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/services/setting.service.dart';
import 'package:immich_mobile/domain/utils/event_stream.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/timeline.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
import 'package:immich_mobile/utils/async_mutex.dart'; import 'package:immich_mobile/utils/async_mutex.dart';
@@ -35,18 +34,21 @@ enum TimelineOrigin {
deepLink, deepLink,
albumActivities, albumActivities,
folder, folder,
recentlyAdded,
} }
class TimelineFactory { class TimelineFactory {
final DriftTimelineRepository _timelineRepository; final DriftTimelineRepository _timelineRepository;
final SettingsService _settingsService; final MetadataRepository _metadataRepository;
const TimelineFactory({required DriftTimelineRepository timelineRepository, required SettingsService settingsService}) const TimelineFactory({
: _timelineRepository = timelineRepository, required DriftTimelineRepository timelineRepository,
_settingsService = settingsService; required MetadataRepository metadataRepository,
}) : _timelineRepository = timelineRepository,
_metadataRepository = metadataRepository;
GroupAssetsBy get groupBy { GroupAssetsBy get groupBy {
final group = GroupAssetsBy.values[_settingsService.get(Setting.groupAssetsBy)]; final group = _metadataRepository.appConfig.timeline.groupAssetsBy;
// We do not support auto grouping in the new timeline yet, fallback to day grouping // We do not support auto grouping in the new timeline yet, fallback to day grouping
return group == GroupAssetsBy.auto ? GroupAssetsBy.day : group; return group == GroupAssetsBy.auto ? GroupAssetsBy.day : group;
} }
@@ -61,6 +63,8 @@ class TimelineFactory {
TimelineService remoteAssets(String userId) => TimelineService(_timelineRepository.remote(userId, groupBy)); TimelineService remoteAssets(String userId) => TimelineService(_timelineRepository.remote(userId, groupBy));
TimelineService recentlyAdded(String userId) => TimelineService(_timelineRepository.recentlyAdded(userId, groupBy));
TimelineService favorite(String userId) => TimelineService(_timelineRepository.favorite(userId, groupBy)); TimelineService favorite(String userId) => TimelineService(_timelineRepository.favorite(userId, groupBy));
TimelineService trash(String userId) => TimelineService(_timelineRepository.trash(userId, groupBy)); TimelineService trash(String userId) => TimelineService(_timelineRepository.trash(userId, groupBy));
+3 -1
View File
@@ -30,7 +30,9 @@ class UserService {
Future<UserDto?> refreshMyUser() async { Future<UserDto?> refreshMyUser() async {
final user = await _userApiRepository.getMyUser(); final user = await _userApiRepository.getMyUser();
if (user == null) return null; if (user == null) {
return null;
}
await _storeService.put(StoreKey.currentUser, user); await _storeService.put(StoreKey.currentUser, user);
return user; return user;
} }
@@ -11,6 +11,7 @@ extension DTOToAsset on api.AssetResponseDto {
checksum: checksum, checksum: checksum,
createdAt: fileCreatedAt, createdAt: fileCreatedAt,
updatedAt: updatedAt, updatedAt: updatedAt,
uploadedAt: createdAt,
ownerId: ownerId, ownerId: ownerId,
visibility: visibility.toAssetVisibility(), visibility: visibility.toAssetVisibility(),
durationMs: duration, durationMs: duration,
@@ -33,6 +34,7 @@ extension DTOToAsset on api.AssetResponseDto {
checksum: checksum, checksum: checksum,
createdAt: fileCreatedAt, createdAt: fileCreatedAt,
updatedAt: updatedAt, updatedAt: updatedAt,
uploadedAt: createdAt,
ownerId: ownerId, ownerId: ownerId,
visibility: visibility.toAssetVisibility(), visibility: visibility.toAssetVisibility(),
durationMs: duration, durationMs: duration,
+6 -2
View File
@@ -94,8 +94,12 @@ class SnapScrollPhysics extends ScrollPhysics {
bool get allowUserScrolling => false; bool get allowUserScrolling => false;
static double target(ScrollMetrics position, double velocity, double snapOffset) { static double target(ScrollMetrics position, double velocity, double snapOffset) {
if (velocity > _minFlingVelocity) return snapOffset; if (velocity > _minFlingVelocity) {
if (velocity < -_minFlingVelocity) return position.pixels < snapOffset ? 0.0 : snapOffset; return snapOffset;
}
if (velocity < -_minFlingVelocity) {
return position.pixels < snapOffset ? 0.0 : snapOffset;
}
return position.pixels < minSnapDistance ? 0.0 : snapOffset; return position.pixels < minSnapDistance ? 0.0 : snapOffset;
} }
} }
@@ -5,6 +5,11 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)') @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)')
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)') @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)')
@TableIndex.sql('''
CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person
ON asset_face_entity (person_id, asset_id)
WHERE is_visible = 1 AND deleted_at IS NULL
''')
class AssetFaceEntity extends Table with DriftDefaultsMixin { class AssetFaceEntity extends Table with DriftDefaultsMixin {
const AssetFaceEntity(); const AssetFaceEntity();
@@ -1350,3 +1350,7 @@ i0.Index get idxAssetFaceAssetId => i0.Index(
'idx_asset_face_asset_id', 'idx_asset_face_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)', 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
); );
i0.Index get idxAssetFaceVisiblePerson => i0.Index(
'idx_asset_face_visible_person',
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
);
@@ -6,6 +6,10 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)') @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)')
@TableIndex.sql('''
CREATE INDEX IF NOT EXISTS idx_remote_exif_city
ON remote_exif_entity (city) WHERE city IS NOT NULL
''')
class RemoteExifEntity extends Table with DriftDefaultsMixin { class RemoteExifEntity extends Table with DriftDefaultsMixin {
const RemoteExifEntity(); const RemoteExifEntity();
@@ -1883,3 +1883,8 @@ class RemoteExifEntityCompanion
.toString(); .toString();
} }
} }
i0.Index get idxRemoteExifCity => i0.Index(
'idx_remote_exif_city',
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
);
@@ -4,7 +4,7 @@ import 'local_asset.entity.dart';
import 'local_album.entity.dart'; import 'local_album.entity.dart';
import 'local_album_asset.entity.dart'; import 'local_album_asset.entity.dart';
mergedAsset: mergedAsset:
SELECT SELECT
rae.id as remote_id, rae.id as remote_id,
(SELECT lae.id FROM local_asset_entity lae WHERE lae.checksum = rae.checksum LIMIT 1) as local_id, (SELECT lae.id FROM local_asset_entity lae WHERE lae.checksum = rae.checksum LIMIT 1) as local_id,
@@ -27,7 +27,8 @@ SELECT
NULL as longitude, NULL as longitude,
NULL as adjustmentTime, NULL as adjustmentTime,
rae.is_edited, rae.is_edited,
0 as playback_style 0 as playback_style,
rae.uploaded_at
FROM FROM
remote_asset_entity rae remote_asset_entity rae
LEFT JOIN LEFT JOIN
@@ -65,7 +66,8 @@ SELECT
lae.longitude, lae.longitude,
lae.adjustment_time, lae.adjustment_time,
0 as is_edited, 0 as is_edited,
lae.playback_style lae.playback_style,
NULL as uploaded_at
FROM FROM
local_asset_entity lae local_asset_entity lae
WHERE NOT EXISTS ( WHERE NOT EXISTS (
+4 -1
View File
@@ -29,7 +29,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
); );
$arrayStartIndex += generatedlimit.amountOfVariables; $arrayStartIndex += generatedlimit.amountOfVariables;
return customSelect( return customSelect(
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_ms, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited, 0 AS playback_style FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_ms, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited, lae.playback_style FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}', 'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_ms, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited, 0 AS playback_style, rae.uploaded_at FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_ms, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited, lae.playback_style, NULL AS uploaded_at FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
variables: [ variables: [
for (var $ in userIds) i0.Variable<String>($), for (var $ in userIds) i0.Variable<String>($),
...generatedlimit.introducedVariables, ...generatedlimit.introducedVariables,
@@ -68,6 +68,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
adjustmentTime: row.readNullable<DateTime>('adjustmentTime'), adjustmentTime: row.readNullable<DateTime>('adjustmentTime'),
isEdited: row.read<bool>('is_edited'), isEdited: row.read<bool>('is_edited'),
playbackStyle: row.read<int>('playback_style'), playbackStyle: row.read<int>('playback_style'),
uploadedAt: row.readNullable<DateTime>('uploaded_at'),
), ),
); );
} }
@@ -141,6 +142,7 @@ class MergedAssetResult {
final DateTime? adjustmentTime; final DateTime? adjustmentTime;
final bool isEdited; final bool isEdited;
final int playbackStyle; final int playbackStyle;
final DateTime? uploadedAt;
MergedAssetResult({ MergedAssetResult({
this.remoteId, this.remoteId,
this.localId, this.localId,
@@ -164,6 +166,7 @@ class MergedAssetResult {
this.adjustmentTime, this.adjustmentTime,
required this.isEdited, required this.isEdited,
required this.playbackStyle, required this.playbackStyle,
this.uploadedAt,
}); });
} }

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