Compare commits

...

23 Commits

Author SHA1 Message Date
Daniel Dietzler 8c864a0bec fix: mise on windows 2026-05-11 20:13:55 +02:00
Jason Rasmussen fb0a54d548 chore: mise on windows (#28351)
* chore: mise on windows

* chore: bump use-mise
2026-05-11 20:04:38 +02:00
renovate[bot] 7013cc0904 fix(deps): update dependency @opentelemetry/exporter-prometheus to ^0.217.0 [security] (#28353)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 18:03:30 +00:00
renovate[bot] dcaf7b4a65 fix(deps): update dependency @opentelemetry/sdk-node to ^0.217.0 [security] (#28354)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 19:48:25 +02:00
shenlong 12f7b2a005 chore: add always_put_control_body_on_new_line lint (#28352)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-11 13:47:24 -04:00
Jason Rasmussen 7837d40f57 chore: move sdk to packages (#28350) 2026-05-11 13:37:10 -04:00
bo0tzz b4f719653f fix: indentation and typo (#28349)
* fix: indentation and typo

* neline
2026-05-11 09:17:39 -05:00
bo0tzz f370b4bac6 chore: fold apk comment qr in <details> (#28348) 2026-05-11 09:40:19 -04:00
Mert d788169bf3 chore(server)!: remove libopus enum (#28325) 2026-05-11 08:02:57 -04:00
Mert eea820fa2f chore(server): enable hw decoding by default (#28324) 2026-05-11 08:02:26 -04:00
Timon 271f1cb868 feat(web): Add metadata overlay to slideshow (#24627)
* feat: add slideshow metadata overlay and settings

* Introduced a new SlideshowMetadataOverlay component to display image information during slideshows.
* Updated slideshow settings modal to include options for showing the metadata overlay and selecting its display mode (Description Only or Full).
* Added corresponding translations and store management for the new overlay features.

* remove noisy log

* constant opacity

* 2nd pass

* more

* use text components

* lint
2026-05-11 11:49:12 +02:00
Mert 8c8dc9d32f chore(ml)!: remove deprecated envs (#28326)
remove deprecated envs
2026-05-09 22:40:05 +00:00
Alex fd18e55f7c chore: token extraction for build mobile (#28320)
Co-authored-by: bo0tzz <git@bo0tzz.me>
2026-05-09 15:08:07 +00:00
shenlong faab9e620d refactor: medium repository context helpers (#28311)
* refactor: medium repository context helpers

* test: add regress test for 26723

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-09 08:19:31 -05:00
Matthew Momjian 5ba3efafd8 fix(deployment): remove unneeded volume (#28307)
remove unneeded volume
2026-05-09 09:07:54 -04:00
Alex 8b3c9bf9c3 feat(ci): publish PR Android APK to comment (#28283)
* feat(ci): publish PR Android APK to R2 with installable links

Adds a universal debug APK to PR builds and uploads it to a public
R2 bucket alongside the existing GitHub Actions artifact. Posts a
sticky PR comment with tap-to-install links and a QR code so testers
can install directly on their device without unzipping artifacts.

Required setup:
- Secrets: R2_APK_ACCESS_KEY_ID, R2_APK_SECRET_ACCESS_KEY,
  R2_APK_ACCOUNT_ID, R2_APK_BUCKET
- Optional repo variable: APK_PUBLIC_HOST (defaults to apk.immich.app)
- R2 bucket configured with a public custom domain matching APK_PUBLIC_HOST

* chore(ci): drop R2 upload, link directly to GitHub artifact

Surfaces the existing release-apk-signed artifact in a sticky PR
comment with a QR code. Avoids new infra and secrets — the trade-off
is GitHub login and a zip wrapper instead of tap-to-install.

* feat(ci): build PR APK as release and publish to GitHub Release

PR builds now produce a release APK signed with the release keystore.
The universal APK is published as a GitHub Release asset under tag
'pr-<num>' (prerelease), giving testers a direct, unzipped, tap-to-
install URL plus a QR code in the PR comment. The release-apk-signed
artifact is unchanged.

* chore(ci): drop GitHub Release, publish universal APK as own artifact

Reverts the prerelease publish. Uploads the universal release APK as
a separate single-file artifact so its download URL gives a zip
containing only that APK — no extra files to dig through. The QR in
the PR comment points at this universal-only artifact.

* chore(ci): build only universal APK for PR, drop split artifact

PR builds skip the arm64-only split — release-apk-signed now contains
just the universal app-release.apk, so the download zip is a single
file. Removes the redundant separate universal artifact and points
the PR comment QR at the main artifact URL.

* feat(mobile): suffix PR APK applicationId so it installs alongside production

Each PR build now becomes app.alextran.immich.pr<num> via PR_NUMBER env
read in build.gradle, so testers can install multiple PR builds and the
Play Store version on the same device without uninstalling. Also tags
the version with -pr<num> for visibility.

* feat(ci): allow PR APK build to run on forks

Forks can now run the Android build job. Steps that need repo secrets
(create-workflow-token, Create Keystore) are skipped when the PR is
from a fork, the checkout falls back to GITHUB_TOKEN, and build.gradle
falls back to debug signing if the release keystore isn't materialised.
The PR comment still requires write access, so it's gated to non-fork
PRs — fork APKs are reachable from the workflow run's artifact tab.
2026-05-09 07:46:40 -05:00
Yaros 41f285aa3e feat(mobile): increased tap area on video player overlay (#27269)
* fix(mobile): improved tap area on video player

* fix: back button padding

* chore: use sizedbox.square & button padding

* chore: fixed padding
2026-05-09 10:47:41 +07:00
Sandro fdac6c8bc4 fix(docs): missing colon in config file doc (#28313)
Fix missing colon
2026-05-09 09:44:41 +07:00
Thorsten Winkler d7f05d2510 fix(mobile): Deduplicate assets in person view timeline (#26723)
fix(mobile): deduplicate assets in person view timeline

Previously, assets with multiple face records for the same person (e.g.,
manual Digikam imports and Immich ML detections) appeared multiple times
in the person timeline. This was caused by an inner join on the
assetFaceEntity without proper deduplication.

This commit refactors the timeline queries to use a subquery approach
instead of joins and grouping. This ensures:

- _getPersonBucketAssets: Only unique assets are fetched, even if
  multiple face records exist for a single asset.
- _watchPersonBucket: Asset counts in timeline headers are accurate
  and represent unique assets.
- Performance: Database overhead is reduced by avoiding complex joins
  and explicit groupBy operations on large result sets.

Signed-off-by: thowdev <12428285+thowdev@users.noreply.github.com>
2026-05-09 01:53:18 +00:00
stfn 3100bd5eed fix(mobile): avoid duplicate assets in album view (#28152)
fix(mobile): avoid duplicate assets in remote album timeline

Co-authored-by: Stefan Friedli <stefan@stefanfriedli.ch>
2026-05-09 08:24:54 +07:00
Jason Rasmussen 8a024e2b50 chore: faster web linting (#28303) 2026-05-08 16:55:14 -04:00
Jason Rasmussen 25a6a38b30 chore: use mise (#28298) 2026-05-08 15:21:33 -04:00
Santo Shakil 7c6750941e fix(mobile): mounted check before setState in album sync action (#28300)
_manualSyncAlbums fires a setState 1s after sync via Future.delayed
with no mounted check. if the widget is gone by then, setState throws
null check and the global error logger logs it severe.
2026-05-08 18:55:03 +00:00
196 changed files with 1853 additions and 1154 deletions
+1 -3
View File
@@ -30,9 +30,7 @@ machine-learning/
misc/
mobile/
open-api/typescript-sdk/build/
!open-api/typescript-sdk/package.json
!open-api/typescript-sdk/package-lock.json
packages/sdk/build/
server/upload/
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/** linguist-generated=true
open-api/typescript-sdk/fetch-client.ts -diff -merge
open-api/typescript-sdk/fetch-client.ts linguist-generated=true
packages/sdk/fetch-client.ts -diff -merge
packages/sdk/fetch-client.ts linguist-generated=true
*.sh text eol=lf
+28 -4
View File
@@ -73,8 +73,8 @@ jobs:
needs: pre-job
permissions:
contents: read
# Skip when PR from a fork
if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
pull-requests: write
if: ${{ github.actor != 'dependabot[bot]' && fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
runs-on: mich
steps:
@@ -86,11 +86,12 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.ref || github.sha }}
ref: ${{ inputs.ref }}
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Create the Keystore
if: ${{ !github.event.pull_request.head.repo.fork }}
env:
KEY_JKS: ${{ secrets.KEY_JKS }}
working-directory: ./mobile
@@ -144,20 +145,43 @@ jobs:
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
if [[ $IS_MAIN == 'true' ]]; then
flutter build apk --release
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
else
flutter build apk --debug --split-per-abi --target-platform android-arm64
flutter build apk --release
fi
- name: Publish Android Artifact
id: upload-apk
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-apk-signed
path: mobile/build/app/outputs/flutter-apk/*.apk
- name: Comment APK download link on PR
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }}
uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
APK_URL: ${{ steps.upload-apk.outputs.artifact-url }}
with:
github-token: ${{ steps.token.outputs.token }}
message-id: 'mobile-android-apk'
message: |
📱 **Android release APK (universal)** — `${{ env.HEAD_SHA }}`
Download: ${{ env.APK_URL }}
<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.
- name: Save Gradle Cache
id: cache-gradle-save
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+7 -17
View File
@@ -36,30 +36,20 @@ jobs:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
node-version-file: './cli/.nvmrc'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
github_token: ${{ steps.token.outputs.token }}
- name: Setup typescript-sdk
run: pnpm install && pnpm run build
working-directory: ./open-api/typescript-sdk
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm publish --provenance --no-git-checks
- name: Publish
if: ${{ github.event_name == 'release' }}
run: mise run ci-publish
docker:
name: Docker
+3 -9
View File
@@ -64,17 +64,11 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
node-version-file: './docs/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
github_token: ${{ steps.token.outputs.token }}
- name: Run install
run: pnpm install
+1 -1
View File
@@ -131,7 +131,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- 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:
github_token: ${{ steps.token.outputs.token }}
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- 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:
github_token: ${{ steps.token.outputs.token }}
+7 -13
View File
@@ -14,29 +14,23 @@ jobs:
contents: write
pull-requests: write
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
- 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: 'Checkout'
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.ref }}
token: ${{ steps.generate-token.outputs.token }}
persist-credentials: true
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a # v6.0.4
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
github_token: ${{ steps.token.outputs.token }}
- name: Fix formatting
run: pnpm --recursive install && pnpm run --recursive --if-present --parallel format:fix
+10 -15
View File
@@ -48,33 +48,28 @@ jobs:
version: ${{ steps.output.outputs.version }}
permissions: {} # No job-level permissions are needed because it uses the app-token
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
- 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: Checkout
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
token: ${{ steps.generate-token.outputs.token }}
token: ${{ steps.token.outputs.token }}
persist-credentials: true
ref: main
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
github_token: ${{ steps.token.outputs.token }}
# TODO move to mise
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- name: Setup pnpm
uses: pnpm/action-setup@26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a # v6.0.4
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Bump version
env:
SERVER_BUMP: ${{ inputs.serverBump }}
+11 -16
View File
@@ -14,9 +14,6 @@ jobs:
contents: read
id-token: write
packages: write
defaults:
run:
working-directory: ./open-api/typescript-sdk
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
@@ -24,24 +21,22 @@ jobs:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
node-version-file: './open-api/typescript-sdk/.nvmrc'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
github_token: ${{ steps.token.outputs.token }}
- name: Install deps
run: pnpm install --frozen-lockfile
run: pnpm --filter @immich/sdk install --frozen-lockfile
- name: Build
run: pnpm build
run: pnpm --filter @immich/sdk build
- name: Publish
run: pnpm publish --provenance --no-git-checks
run: pnpm --filter @immich/sdk publish --provenance --no-git-checks
+141 -181
View File
@@ -33,14 +33,14 @@ jobs:
web:
- 'web/**'
- 'i18n/**'
- 'open-api/typescript-sdk/**'
- 'packages/sdk/**'
- 'pnpm-lock.yaml'
server:
- 'server/**'
- 'pnpm-lock.yaml'
cli:
- 'cli/**'
- 'open-api/typescript-sdk/**'
- 'packages/sdk/**'
- 'pnpm-lock.yaml'
e2e:
- 'e2e/**'
@@ -78,28 +78,14 @@ jobs:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run package manager install
run: pnpm install
- name: Run linter
run: pnpm lint
if: ${{ !cancelled() }}
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
- name: Run tsc
run: pnpm check
if: ${{ !cancelled() }}
- name: Run small tests & coverage
run: pnpm test
if: ${{ !cancelled() }}
github_token: ${{ steps.token.outputs.token }}
- name: Run ci-unit
run: mise run ci-unit
cli-unit-tests:
name: Unit Test CLI
needs: pre-job
@@ -122,31 +108,15 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
node-version-file: './cli/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Setup typescript-sdk
run: pnpm install && pnpm run build
working-directory: ./open-api/typescript-sdk
- name: Install deps
run: pnpm install
- name: Run linter
run: pnpm lint
if: ${{ !cancelled() }}
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
- name: Run tsc
run: pnpm check
if: ${{ !cancelled() }}
- name: Run unit tests & coverage
run: pnpm test
if: ${{ !cancelled() }}
github_token: ${{ steps.token.outputs.token }}
- name: Run ci-unit
run: mise run ci-unit
cli-unit-tests-win:
name: Unit Test CLI (Windows)
needs: pre-job
@@ -169,26 +139,28 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
node-version-file: './cli/.nvmrc'
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
github_token: ${{ steps.token.outputs.token }}
- name: Run setup @immich/sdk
run: mise run //:sdk:install && mise run //:sdk:build
- name: Run pnpm install
run: pnpm install --frozen-lockfile
# Skip linter & formatter in Windows test.
- name: Run tsc
run: pnpm check
if: ${{ !cancelled() }}
- name: Run unit tests & coverage
run: pnpm test
if: ${{ !cancelled() }}
web-lint:
name: Lint Web
needs: pre-job
@@ -211,28 +183,22 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
node-version-file: './web/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
github_token: ${{ steps.token.outputs.token }}
- name: Run setup @immich/sdk
run: mise run //:sdk:install && mise run //:sdk:build
- name: Run pnpm install
run: pnpm rebuild && pnpm install --frozen-lockfile
run: pnpm install --frozen-lockfile
- name: Run linter
run: pnpm lint
if: ${{ !cancelled() }}
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
- name: Run svelte checks
run: pnpm check:svelte
if: ${{ !cancelled() }}
web-unit-tests:
name: Test Web
needs: pre-job
@@ -255,25 +221,15 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
node-version-file: './web/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
- name: Run npm install
run: pnpm install --frozen-lockfile
- name: Run tsc
run: pnpm check:typescript
if: ${{ !cancelled() }}
- name: Run unit tests & coverage
run: pnpm test
if: ${{ !cancelled() }}
github_token: ${{ steps.token.outputs.token }}
- name: Run ci-unit
run: mise run ci-unit
i18n-tests:
name: Test i18n
needs: pre-job
@@ -293,24 +249,25 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
node-version-file: './web/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
github_token: ${{ steps.token.outputs.token }}
- name: Install dependencies
run: pnpm --filter=immich-i18n install --frozen-lockfile
- name: Format
run: pnpm --filter=immich-i18n format:fix
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files
with:
files: |
i18n/**
- name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
@@ -319,6 +276,7 @@ jobs:
echo "ERROR: i18n files not up to date!"
echo "Changed files: ${CHANGED_FILES}"
exit 1
e2e-tests-lint:
name: End-to-End Lint
needs: pre-job
@@ -341,30 +299,16 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
node-version-file: './e2e/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
- name: Run linter
run: pnpm lint
if: ${{ !cancelled() }}
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
- name: Run tsc
run: pnpm check
github_token: ${{ steps.token.outputs.token }}
- name: Run ci-unit
run: mise run ci-unit
if: ${{ !cancelled() }}
server-medium-tests:
name: Medium Tests (Server)
needs: pre-job
@@ -388,23 +332,16 @@ jobs:
persist-credentials: false
submodules: 'recursive'
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- 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:
github_token: ${{ steps.token.outputs.token }}
- name: Run pnpm install
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
- name: Run medium tests
run: pnpm test:medium
- name: Run ci-medium
run: mise run ci-medium
if: ${{ !cancelled() }}
e2e-tests-server-cli:
name: End-to-End Tests (Server & CLI)
needs: pre-job
@@ -431,52 +368,62 @@ jobs:
persist-credentials: false
submodules: 'recursive'
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: './e2e/.nvmrc'
node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
- name: Setup @immich/sdk
run: pnpm --filter @immich/sdk install --frozen-lockfile && pnpm --filter @immich/sdk build
- name: Run setup web
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
working-directory: ./web
if: ${{ !cancelled() }}
- name: Run setup cli
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./cli
if: ${{ !cancelled() }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
- name: Start Docker Compose
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
if: ${{ !cancelled() }}
- name: Run e2e tests (api & cli)
env:
VITEST_DISABLE_DOCKER_SETUP: true
run: pnpm test
if: ${{ !cancelled() }}
- name: Run e2e tests (maintenance)
env:
VITEST_DISABLE_DOCKER_SETUP: true
run: pnpm test:maintenance
if: ${{ !cancelled() }}
- name: Capture Docker logs
if: always()
run: docker compose logs --no-color > docker-compose-logs.txt
working-directory: ./e2e
- name: Archive Docker logs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: e2e-server-docker-logs-${{ matrix.runner }}
path: e2e/docker-compose-logs.txt
e2e-tests-web:
name: End-to-End Tests (Web)
needs: pre-job
@@ -503,70 +450,84 @@ jobs:
persist-credentials: false
submodules: 'recursive'
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: './e2e/.nvmrc'
node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
- name: Run setup @immich/sdk
run: pnpm --filter @immich/sdk install --frozen-lockfile && pnpm --filter @immich/sdk build
if: ${{ !cancelled() }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
- name: Install Playwright Browsers
run: pnpm exec playwright install chromium --only-shell
if: ${{ !cancelled() }}
- name: Docker build
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
if: ${{ !cancelled() }}
- name: Run e2e tests (web)
env:
PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web
if: ${{ !cancelled() }}
- name: Archive e2e test (web) results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success() || failure()
with:
name: e2e-web-test-results-${{ matrix.runner }}
path: e2e/playwright-report/
- name: Run ui tests (web)
env:
PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web:ui
if: ${{ !cancelled() }}
- name: Archive ui test (web) results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success() || failure()
with:
name: e2e-ui-test-results-${{ matrix.runner }}
path: e2e/playwright-report/
- name: Run maintenance tests
env:
PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web:maintenance
if: ${{ !cancelled() }}
- name: Archive maintenance tests (web) results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success() || failure()
with:
name: e2e-maintenance-isolated-test-results-${{ matrix.runner }}
path: e2e/playwright-report/
- name: Capture Docker logs
if: always()
run: docker compose logs --no-color > docker-compose-logs.txt
working-directory: ./e2e
- name: Archive Docker logs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: e2e-web-docker-logs-${{ matrix.runner }}
path: e2e/docker-compose-logs.txt
success-check-e2e:
name: End-to-End Tests Success
needs: [e2e-tests-server-cli, e2e-tests-web]
@@ -613,9 +574,6 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
env:
MISE_CEILING_PATHS: ${{ github.workspace }}
MISE_TRUSTED_CONFIG_PATHS: ${{ github.workspace }}/machine-learning/mise.toml
defaults:
run:
working-directory: ./machine-learning
@@ -632,21 +590,13 @@ jobs:
token: ${{ steps.token.outputs.token }}
- 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:
github_token: ${{ steps.token.outputs.token }}
working_directory: ./machine-learning
- name: Install dependencies
run: mise run install --extra cpu
- name: Lint code
run: mise run lint --output-format=github
- name: Format code
run: mise run format
- name: Run type checking
run: mise run check
- name: Run tests and coverage
run: mise run test
- name: Run ci-unit
run: mise run ci-unit
github-files-formatting:
name: .github Files Formatting
needs: pre-job
@@ -669,19 +619,19 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
node-version-file: './.github/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
github_token: ${{ steps.token.outputs.token }}
- name: Run pnpm install
run: pnpm install --frozen-lockfile
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
shellcheck:
name: ShellCheck
runs-on: ubuntu-latest
@@ -720,29 +670,31 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
github_token: ${{ steps.token.outputs.token }}
- name: Install server dependencies
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile
- name: Build the app
run: pnpm --filter immich build
- name: Run API generation
run: ./bin/generate-open-api.sh
working-directory: open-api
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files
with:
files: |
mobile/openapi
open-api/typescript-sdk
packages/sdk
open-api/immich-openapi-specs.json
- name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
@@ -751,6 +703,7 @@ jobs:
echo "ERROR: Generated files not up to date!"
echo "Changed files: ${CHANGED_FILES}"
exit 1
sql-schema-up-to-date:
name: SQL Schema Checks
runs-on: ubuntu-latest
@@ -782,31 +735,35 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
github_token: ${{ steps.token.outputs.token }}
- name: Install server dependencies
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
- name: Build the app
run: pnpm build
- name: Run existing migrations
run: pnpm migrations:run
- name: Test npm run schema:reset command works
run: pnpm schema:reset
- name: Generate new migrations
continue-on-error: true
run: pnpm migrations:generate src/TestMigration
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files
with:
files: |
server/src
- name: Verify migration files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
@@ -816,16 +773,19 @@ jobs:
echo "Changed files: ${CHANGED_FILES}"
cat ./src/*-TestMigration.ts
exit 1
- name: Run SQL generation
run: pnpm sync:sql
env:
DB_URL: postgres://postgres:postgres@localhost:5432/immich
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-sql-files
with:
files: |
server/src/queries
- name: Verify SQL files have not changed
if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
env:
+1 -1
View File
@@ -20,7 +20,7 @@ mobile/openapi/doc
mobile/openapi/.openapi-generator/FILES
mobile/ios/build
open-api/typescript-sdk/build
packages/**/build
mobile/android/fastlane/report.xml
mobile/ios/fastlane/report.xml
View File
+2 -2
View File
@@ -63,7 +63,7 @@ VOLUME_DIRS = \
./e2e/node_modules \
./docs/node_modules \
./server/node_modules \
./open-api/typescript-sdk/node_modules \
./packages/sdk/node_modules \
./.github/node_modules \
./node_modules \
./cli/node_modules
@@ -77,7 +77,7 @@ MODULES = e2e server web cli sdk docs .github
# cli = @immich/cli
# docs = documentation
# e2e = immich-e2e
# open-api/typescript-sdk = @immich/sdk
# packages/sdk = @immich/sdk
# server = immich
# web = immich-web
map-package = $(subst sdk,@immich/sdk,$(subst cli,@immich/cli,$(subst docs,documentation,$(subst e2e,immich-e2e,$(subst server,immich,$(subst web,immich-web,$1))))))
-1
View File
@@ -1 +0,0 @@
24.15.0
+1 -1
View File
@@ -3,7 +3,7 @@ FROM node:24.1.0-alpine3.20@sha256:8fe019e0d57dbdce5f5c27c0b63d2775cf34b00e3755a
WORKDIR /usr/src/app
COPY package* pnpm* .pnpmfile.cjs ./
COPY ./cli ./cli/
COPY ./open-api/typescript-sdk ./open-api/typescript-sdk/
COPY ./packages ./packages/
RUN corepack enable pnpm && \
pnpm install --filter @immich/sdk --filter @immich/cli --frozen-lockfile && \
pnpm --filter @immich/sdk build && \
+24 -1
View File
@@ -7,7 +7,7 @@ run = "vite build"
[tasks.test]
env._.path = "./node_modules/.bin"
run = "vite"
run = "vitest"
[tasks.lint]
env._.path = "./node_modules/.bin"
@@ -27,3 +27,26 @@ run = "prettier --write ."
[tasks.check]
env._.path = "./node_modules/.bin"
run = "tsc --noEmit"
[tasks.ci-publish]
depends = ["//:sdk:install", "//:sdk:build"]
run = [
{ task = ":install" },
{ task = ":build" },
"pnpm publish --provenance --no-git-checks",
]
[tasks.ci-unit]
depends = ["//:sdk:install", "//:sdk:build"]
run = [
{ task = ":install" },
{ task = ":format" },
{ task = ":lint" },
{ task = ":check" },
{ task = ":test --run" },
]
[tasks.checklist]
run = [
{ task = ":ci-unit" },
]
-3
View File
@@ -66,8 +66,5 @@
"fastq": "^1.17.1",
"lodash-es": "^4.17.21",
"micromatch": "^4.0.8"
},
"volta": {
"node": "24.15.0"
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ services:
- cli_node_modules:/usr/src/app/cli/node_modules
- docs_node_modules:/usr/src/app/docs/node_modules
- e2e_node_modules:/usr/src/app/e2e/node_modules
- sdk_node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
- sdk_node_modules:/usr/src/app/packages/sdk/node_modules
- app_node_modules:/usr/src/app/node_modules
- sveltekit:/usr/src/app/web/.svelte-kit
- coverage:/usr/src/app/web/coverage
-3
View File
@@ -95,6 +95,3 @@ services:
restart: always
healthcheck:
disable: false
volumes:
model-cache:
-1
View File
@@ -1 +0,0 @@
24.15.0
+1 -1
View File
@@ -10,4 +10,4 @@ OpenAPI is used to generate the client (Typescript, Dart) SDK. `openapi-generato
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.
+1 -1
View File
@@ -205,7 +205,7 @@ When the Dev Container starts, it automatically:
1. **Runs post-create script** (`container-server-post-create.sh`):
- Adjusts file permissions for the `node` user
- 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:
- `Immich API Server (Nest)` - API server with hot-reloading on port 2283
+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:
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/`
3. Install web dependencies - `pnpm i`
4. Start the web development server
+2 -2
View File
@@ -26,7 +26,7 @@ The default configuration looks like this:
},
"ffmpeg": {
"accel": "disabled",
"accelDecode": false,
"accelDecode": true,
"acceptedAudioCodecs": ["aac", "mp3", "opus"],
"acceptedContainers": ["mov", "ogg", "webm"],
"acceptedVideoCodecs": ["h264"],
@@ -264,4 +264,4 @@ volumes:
- ./configuration.yml:${IMMICH_CONFIG_FILE}
```
::
:::
-3
View File
@@ -56,8 +56,5 @@
},
"engines": {
"node": ">=20"
},
"volta": {
"node": "24.15.0"
}
}
-1
View File
@@ -1 +0,0 @@
24.15.0
+15
View File
@@ -27,3 +27,18 @@ run = { task = "lint --fix" }
[tasks.check]
env._.path = "./node_modules/.bin"
run = "tsc --noEmit"
[tasks.ci-setup]
depends = ["//:sdk:install", "//:sdk:build", "//cli:install", "//cli:build"]
run = { task = ":install" }
[tasks.ci-unit]
depends = ["//:sdk:install", "//:sdk:build"]
run = [
{ task = ":install" },
{ task = ":format" },
{ task = ":lint" },
{ task = ":check" },
]
-3
View File
@@ -56,8 +56,5 @@
"utimes": "^5.2.1",
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.0.0"
},
"volta": {
"node": "24.15.0"
}
}
+4
View File
@@ -2192,6 +2192,7 @@
"show_schema": "Show schema",
"show_search_options": "Show search options",
"show_shared_links": "Show shared links",
"show_slideshow_metadata_overlay": "Show image info overlay",
"show_slideshow_transition": "Show slideshow transition",
"show_supporter_badge": "Supporter badge",
"show_supporter_badge_description": "Show a supporter badge",
@@ -2207,6 +2208,9 @@
"skip_to_folders": "Skip to folders",
"skip_to_tags": "Skip to tags",
"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_description": "Loop back to beginning when slideshow ends",
"slideshow_settings": "Slideshow settings",
-13
View File
@@ -32,25 +32,12 @@ class OcrSettings(BaseModel):
class PreloadModelData(BaseModel):
clip_fallback: str | None = os.getenv("MACHINE_LEARNING_PRELOAD__CLIP", None)
facial_recognition_fallback: str | None = os.getenv("MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION", None)
if clip_fallback is not None:
os.environ["MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL"] = clip_fallback
os.environ["MACHINE_LEARNING_PRELOAD__CLIP__VISUAL"] = clip_fallback
del os.environ["MACHINE_LEARNING_PRELOAD__CLIP"]
if facial_recognition_fallback is not None:
os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION"] = facial_recognition_fallback
os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION"] = facial_recognition_fallback
del os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION"]
clip: ClipSettings = ClipSettings()
facial_recognition: FacialRecognitionSettings = FacialRecognitionSettings()
ocr: OcrSettings = OcrSettings()
class MaxBatchSize(BaseModel):
ocr_fallback: str | None = os.getenv("MACHINE_LEARNING_MAX_BATCH_SIZE__TEXT_RECOGNITION", None)
if ocr_fallback is not None:
os.environ["MACHINE_LEARNING_MAX_BATCH_SIZE__OCR"] = ocr_fallback
facial_recognition: int | None = None
ocr: int | None = None
-14
View File
@@ -117,20 +117,6 @@ async def preload_models(preload: PreloadModelData) -> None:
ModelTask.OCR,
)
if preload.clip_fallback is not None:
log.warning(
"Deprecated env variable: 'MACHINE_LEARNING_PRELOAD__CLIP'. "
"Use 'MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL' and "
"'MACHINE_LEARNING_PRELOAD__CLIP__VISUAL' instead."
)
if preload.facial_recognition_fallback is not None:
log.warning(
"Deprecated env variable: 'MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION'. "
"Use 'MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION' and "
"'MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION' instead."
)
def update_state() -> Iterator[None]:
global active_requests, last_called
+9 -1
View File
@@ -17,6 +17,15 @@ run = "uv run ruff format immich_ml"
[tasks.check]
run = "uv run mypy --strict immich_ml/"
[tasks.ci-unit]
run = [
{ task = ":install --extra cpu" },
{ task = ":format" },
{ task = ":lint --output-format=github" },
{ task = ":check" },
{ task = ":test" },
]
[tasks.checklist]
run = [
{ task = ":install" },
@@ -25,4 +34,3 @@ run = [
{ task = ":check" },
{ task = ":test" },
]
+1 -1
View File
@@ -68,7 +68,7 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
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 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
pnpm install --frozen-lockfile --prefix server
+3 -3
View File
@@ -25,7 +25,7 @@ java = "21.0.2"
[tools."github:CQLabs/homebrew-dcm"]
version = "1.37.0"
bin = "dcm"
postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm"
postinstall = "chmod +x {{ get_env(name='MISE_TOOL_INSTALL_PATH',default='') }}/dcm"
[tools."github:jellyfin/jellyfin-ffmpeg"]
version = "7.1.3-6"
@@ -42,11 +42,11 @@ pin = true
# SDK tasks
[tasks."sdk:install"]
dir = "open-api/typescript-sdk"
dir = "packages/sdk"
run = "pnpm install --filter @immich/sdk --frozen-lockfile"
[tasks."sdk:build"]
dir = "open-api/typescript-sdk"
dir = "packages/sdk"
run = "pnpm run build"
# i18n tasks
+2
View File
@@ -34,6 +34,7 @@ linter:
unrelated_type_equality_checks: true
prefer_const_constructors: true
always_use_package_imports: true
always_put_control_body_on_new_line: true
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
@@ -50,6 +51,7 @@ analyzer:
# - custom_lint
errors:
unawaited_futures: warning
always_put_control_body_on_new_line: warning
custom_lint:
rules:
+8 -1
View File
@@ -64,8 +64,15 @@ android {
}
release {
signingConfig signingConfigs.release
def hasKeystore = file("../key.jks").exists() && file("../key.jks").length() > 0
signingConfig hasKeystore ? signingConfigs.release : signingConfigs.debug
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
def prNumber = System.getenv("PR_NUMBER")
if (prNumber) {
applicationIdSuffix ".pr${prNumber}"
versionNameSuffix "-pr${prNumber}"
}
}
}
namespace 'app.alextran.immich'
+9 -3
View File
@@ -217,7 +217,9 @@ List<TranslationParam> _extractParams(String value) {
final icuType = match.group(2)!;
final icuContent = match.group(3) ?? '';
if (params.containsKey(name)) continue;
if (params.containsKey(name)) {
continue;
}
String type;
if (icuType == 'plural' || icuType == 'number') {
@@ -238,7 +240,9 @@ List<TranslationParam> _extractParams(String value) {
for (var i = 0; i < value.length; i++) {
if (value[i] == '{') {
if (depth == 0) icuStart = i;
if (depth == 0) {
icuStart = i;
}
depth++;
} else if (value[i] == '}') {
depth--;
@@ -256,7 +260,9 @@ List<TranslationParam> _extractParams(String value) {
for (final match in simpleRegex.allMatches(cleanedValue)) {
final name = match.group(1)!;
if (params.containsKey(name)) continue;
if (params.containsKey(name)) {
continue;
}
String type;
if (_kIntParamNames.contains(name.toLowerCase())) {
@@ -61,8 +61,12 @@ class RemoteAlbum {
@override
bool operator ==(Object other) {
if (other is! RemoteAlbum) return false;
if (identical(this, other)) return true;
if (other is! RemoteAlbum) {
return false;
}
if (identical(this, other)) {
return true;
}
return id == other.id &&
name == other.name &&
ownerId == other.ownerId &&
@@ -49,8 +49,12 @@ class LocalAlbum {
@override
bool operator ==(Object other) {
if (other is! LocalAlbum) return false;
if (identical(this, other)) return true;
if (other is! LocalAlbum) {
return false;
}
if (identical(this, other)) {
return true;
}
return other.id == id &&
other.name == name &&
@@ -51,12 +51,18 @@ sealed class BaseAsset {
bool get isAnimatedImage => playbackStyle == AssetPlaybackStyle.imageAnimated;
AssetPlaybackStyle get playbackStyle {
if (isVideo) return AssetPlaybackStyle.video;
if (isMotionPhoto) return AssetPlaybackStyle.livePhoto;
if (isVideo) {
return AssetPlaybackStyle.video;
}
if (isMotionPhoto) {
return AssetPlaybackStyle.livePhoto;
}
if (isImage && durationMs != null && durationMs! > 0) {
return AssetPlaybackStyle.imageAnimated;
}
if (isImage) return AssetPlaybackStyle.image;
if (isImage) {
return AssetPlaybackStyle.image;
}
return AssetPlaybackStyle.unknown;
}
@@ -98,7 +104,9 @@ sealed class BaseAsset {
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
if (other is BaseAsset) {
return name == other.name &&
type == other.type &&
@@ -74,8 +74,12 @@ class LocalAsset extends BaseAsset {
// Not checking for remoteId here
@override
bool operator ==(Object other) {
if (other is! LocalAsset) return false;
if (identical(this, other)) return true;
if (other is! LocalAsset) {
return false;
}
if (identical(this, other)) {
return true;
}
return super == other &&
id == other.id &&
cloudId == other.cloudId &&
@@ -71,8 +71,12 @@ class RemoteAsset extends BaseAsset {
// Not checking for localId here
@override
bool operator ==(Object other) {
if (other is! RemoteAsset) return false;
if (identical(this, other)) return true;
if (other is! RemoteAsset) {
return false;
}
if (identical(this, other)) {
return true;
}
return super == other &&
id == other.id &&
ownerId == other.ownerId &&
@@ -158,8 +162,12 @@ class RemoteAssetExif extends RemoteAsset {
@override
bool operator ==(Object other) {
if (other is! RemoteAssetExif) return false;
if (identical(this, other)) return true;
if (other is! RemoteAssetExif) {
return false;
}
if (identical(this, other)) {
return true;
}
return super == other && exifInfo == other.exifInfo;
}
@@ -68,7 +68,9 @@ class AssetFace {
@override
bool operator ==(covariant AssetFace other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.id == id &&
other.assetId == assetId &&
+3 -1
View File
@@ -69,7 +69,9 @@ class ExifInfo {
@override
bool operator ==(covariant ExifInfo other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.fileSize == fileSize &&
other.description == description &&
+3 -1
View File
@@ -20,7 +20,9 @@ class LogMessage {
@override
bool operator ==(covariant LogMessage other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.message == message &&
other.level == level &&
+3 -1
View File
@@ -8,7 +8,9 @@ class Marker {
@override
bool operator ==(covariant Marker other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.location == location && other.assetId == assetId;
}
+6 -3
View File
@@ -2,7 +2,6 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
enum MemoryTypeEnum {
@@ -36,7 +35,9 @@ class MemoryData {
@override
bool operator ==(covariant MemoryData other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.year == year;
}
@@ -132,7 +133,9 @@ class DriftMemory {
@override
bool operator ==(covariant DriftMemory other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
final listEquals = const DeepCollectionEquality().equals;
return other.id == id &&
+9 -3
View File
@@ -115,12 +115,18 @@ final class _ListCodec<T extends Object> extends _MetadataCodec<List<T>> {
List<T>? decode(String raw) {
try {
final decoded = jsonDecode(raw);
if (decoded is! List) return null;
if (decoded is! List) {
return null;
}
final result = <T>[];
for (final item in decoded) {
if (item is! String) return null;
if (item is! String) {
return null;
}
final element = _elementCodec.decode(item);
if (element == null) return null;
if (element == null) {
return null;
}
result.add(element);
}
return result;
+6 -2
View File
@@ -69,7 +69,9 @@ class PersonDto {
@override
bool operator ==(covariant PersonDto other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.id == id &&
other.birthDate == birthDate &&
@@ -160,7 +162,9 @@ class DriftPerson {
@override
bool operator ==(covariant DriftPerson other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.id == id &&
other.createdAt == createdAt &&
@@ -12,7 +12,9 @@ class SearchResult {
@override
bool operator ==(covariant SearchResult other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
final listEquals = const DeepCollectionEquality().equals;
return listEquals(other.assets, assets) && other.nextPage == nextPage;
+6 -2
View File
@@ -37,7 +37,9 @@ class Stack {
@override
bool operator ==(covariant Stack other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.id == id &&
other.createdAt == createdAt &&
@@ -61,7 +63,9 @@ class StackResponse {
@override
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;
}
+3 -1
View File
@@ -117,7 +117,9 @@ StoreDto: {
@override
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;
}
+3 -1
View File
@@ -13,7 +13,9 @@ class Tag {
@override
bool operator ==(covariant Tag other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.id == id && other.value == value;
}
+6 -2
View File
@@ -125,7 +125,9 @@ profileChangedAt: $profileChangedAt
@override
bool operator ==(covariant UserDto other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.id == id &&
((updatedAt == null && other.updatedAt == null) ||
@@ -219,7 +221,9 @@ class PartnerUserDto {
@override
bool operator ==(covariant PartnerUserDto other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.id == id &&
other.email == email &&
@@ -35,7 +35,9 @@ isOnboarded: $isOnboarded,
@override
bool operator ==(covariant Onboarding other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return isOnboarded == other.isOnboarded;
}
@@ -132,7 +134,9 @@ showSupportBadge: $showSupportBadge,
@override
bool operator ==(covariant Preferences other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.foldersEnabled == foldersEnabled &&
other.memoriesEnabled == memoriesEnabled &&
@@ -199,7 +203,9 @@ licenseKey: $licenseKey,
@override
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;
}
@@ -251,7 +257,9 @@ license: ${license ?? "<NA>"},
@override
bool operator ==(covariant UserMetadata other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.userId == userId &&
other.key == key &&
@@ -184,7 +184,9 @@ class RemoteAlbumService {
List<RemoteAlbum> albums, {
required AssetDateAggregation aggregation,
}) async {
if (albums.isEmpty) return [];
if (albums.isEmpty) {
return [];
}
final albumIds = albums.map((e) => e.id).toList();
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.
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);
_cache[key.id] = value;
}
@@ -318,7 +318,9 @@ class SyncStreamService {
}
Future<void> handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) async {
if (batchData.isEmpty) return;
if (batchData.isEmpty) {
return;
}
_logger.info('Processing batch of ${batchData.length} AssetUploadReadyV1 events');
@@ -359,7 +361,9 @@ class SyncStreamService {
}
Future<void> handleWsAssetUploadReadyV2Batch(List<dynamic> batchData) async {
if (batchData.isEmpty) return;
if (batchData.isEmpty) {
return;
}
_logger.info('Processing batch of ${batchData.length} AssetUploadReadyV2 events');
+3 -1
View File
@@ -30,7 +30,9 @@ class UserService {
Future<UserDto?> refreshMyUser() async {
final user = await _userApiRepository.getMyUser();
if (user == null) return null;
if (user == null) {
return null;
}
await _storeService.put(StoreKey.currentUser, user);
return user;
}
+6 -2
View File
@@ -94,8 +94,12 @@ class SnapScrollPhysics extends ScrollPhysics {
bool get allowUserScrolling => false;
static double target(ScrollMetrics position, double velocity, double snapOffset) {
if (velocity > _minFlingVelocity) return snapOffset;
if (velocity < -_minFlingVelocity) return position.pixels < snapOffset ? 0.0 : snapOffset;
if (velocity > _minFlingVelocity) {
return snapOffset;
}
if (velocity < -_minFlingVelocity) {
return position.pixels < snapOffset ? 0.0 : snapOffset;
}
return position.pixels < minSnapDistance ? 0.0 : snapOffset;
}
}
@@ -2,15 +2,15 @@ import 'dart:async';
import 'dart:ffi';
import 'dart:ui' as ui;
import 'package:ffi/ffi.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:ffi/ffi.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
part 'local_image_request.dart';
part 'thumbhash_image_request.dart';
part 'remote_image_request.dart';
part 'thumbhash_image_request.dart';
abstract class ImageRequest {
static int _nextRequestId = 0;
@@ -74,7 +74,9 @@ abstract class ImageRequest {
Future<ui.FrameInfo?> _fromEncodedPlatformImage(int address, int length) async {
final result = await _codecFromEncodedPlatformImage(address, length);
if (result == null) return null;
if (result == null) {
return null;
}
final (codec, descriptor) = result;
if (_isCancelled) {
@@ -46,7 +46,9 @@ class LocalImageRequest extends ImageRequest {
isVideo: assetType == AssetType.video,
preferEncoded: true,
);
if (info == null) return null;
if (info == null) {
return null;
}
final (codec, _) = await _codecFromEncodedPlatformImage(info['pointer']!, info['length']!) ?? (null, null);
return codec;
@@ -29,7 +29,9 @@ class RemoteImageRequest extends ImageRequest {
}
final info = await remoteImageApi.requestImage(uri, requestId: requestId, preferEncoded: true);
if (info == null) return null;
if (info == null) {
return null;
}
final (codec, _) = await _codecFromEncodedPlatformImage(info['pointer']!, info['length']!) ?? (null, null);
return codec;
@@ -5,7 +5,9 @@ class ApiRepository {
Future<T> checkNull<T>(Future<T?> future) async {
final response = await future;
if (response == null) throw const NoResponseDtoError();
if (response == null) {
throw const NoResponseDtoError();
}
return response;
}
}
@@ -48,7 +48,9 @@ class MetadataRepository extends DriftDatabaseRepository {
T _read<T extends Object>(MetadataKey<T> key) => (_cache[key] as T?) ?? key.defaultValue;
Future<void> write<T extends Object, U extends T>(MetadataKey<T> key, U value) async {
if (_read(key) == value) return;
if (_read(key) == value) {
return;
}
await _db
.into(_db.metadataEntity)
@@ -79,13 +81,17 @@ class MetadataRepository extends DriftDatabaseRepository {
final keyMap = MetadataKey.asKeyMap();
for (final row in rows) {
final key = keyMap[row.key];
if (key == null) continue;
if (key == null) {
continue;
}
_updateCache(key, key.decode(row.value));
}
}
void _updateCache<T extends Object>(MetadataKey<T> key, T value) {
if (_cache[key] == value) return;
if (_cache[key] == value) {
return;
}
_cache[key] = value;
key.domain.rebuild(this);
}
@@ -360,7 +360,9 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
}
Future<List<String>> getSortedAlbumIds(List<String> albumIds, {required AssetDateAggregation aggregation}) async {
if (albumIds.isEmpty) return [];
if (albumIds.isEmpty) {
return [];
}
final jsonIds = jsonEncode(albumIds);
final sqlAgg = aggregation == AssetDateAggregation.start ? 'MIN' : 'MAX';
@@ -244,17 +244,21 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final isAscending = albumData.order == AlbumAssetOrder.asc;
final query = _db.remoteAssetEntity.select().addColumns([_db.localAssetEntity.id]).join([
// Correlated subquery picks the first matching local asset by checksum,
// avoiding fan-out when the same photo exists in multiple device albums (#23273).
final localId = subqueryExpression<String>(
_db.localAssetEntity.selectOnly()
..addColumns([_db.localAssetEntity.id])
..where(_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum))
..limit(1),
);
final query = _db.remoteAssetEntity.select().addColumns([localId]).join([
innerJoin(
_db.remoteAlbumAssetEntity,
_db.remoteAlbumAssetEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
),
leftOuterJoin(
_db.localAssetEntity,
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
useColumns: false,
),
])..where(_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAlbumAssetEntity.albumId.equals(albumId));
if (isAscending) {
@@ -265,9 +269,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
query.limit(count, offset: offset);
return query
.map((row) => row.readTable(_db.remoteAssetEntity).toDto(localId: row.read(_db.localAssetEntity.id)))
.get();
return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto(localId: row.read(localId))).get();
}
TimelineQuery fromAssets(List<BaseAsset> assets, TimelineOrigin origin) => (
@@ -422,23 +424,22 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
}
Stream<List<Bucket>> _watchPersonBucket(String userId, String personId, {GroupAssetsBy groupBy = GroupAssetsBy.day}) {
final idQuery = _db.assetFaceEntity.selectOnly()
..addColumns([_db.assetFaceEntity.assetId])
..where(
_db.assetFaceEntity.personId.equals(personId) &
_db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull(),
);
if (groupBy == GroupAssetsBy.none) {
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([_db.remoteAssetEntity.id.count()])
..join([
innerJoin(
_db.assetFaceEntity,
_db.assetFaceEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
),
])
..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.id.isInQuery(idQuery) &
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
_db.assetFaceEntity.personId.equals(personId) &
_db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull(),
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline),
);
return query.map((row) {
@@ -452,20 +453,11 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..join([
innerJoin(
_db.assetFaceEntity,
_db.assetFaceEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
),
])
..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.id.isInQuery(idQuery) &
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
_db.assetFaceEntity.personId.equals(personId) &
_db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull(),
_db.remoteAssetEntity.deletedAt.isNull(),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
@@ -483,26 +475,26 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
required int offset,
required int count,
}) {
final query =
_db.remoteAssetEntity.select().join([
innerJoin(
_db.assetFaceEntity,
_db.assetFaceEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
),
])
..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
_db.assetFaceEntity.personId.equals(personId) &
_db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull(),
)
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
..limit(count, offset: offset);
final idQuery = _db.assetFaceEntity.selectOnly()
..addColumns([_db.assetFaceEntity.assetId])
..where(
_db.assetFaceEntity.personId.equals(personId) &
_db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull(),
);
return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get();
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
row.id.isInQuery(idQuery) &
row.deletedAt.isNull() &
row.ownerId.equals(userId) &
row.visibility.equalsValue(AssetVisibility.timeline),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
TimelineQuery map(List<String> userIds, TimelineMapOptions options, GroupAssetsBy groupBy) => (
@@ -12,7 +12,9 @@ class DriftAuthUserRepository extends DriftDatabaseRepository {
Future<UserDto?> get(String id) async {
final user = await _db.managers.authUserEntity.filter((user) => user.id.equals(id)).getSingleOrNull();
if (user == null) return null;
if (user == null) {
return null;
}
final query = _db.userMetadataEntity.select()..where((e) => e.userId.equals(id));
final metadata = await query.map((row) => row.toDto()).get();
@@ -12,7 +12,9 @@ class UserApiRepository extends ApiRepository {
Future<UserDto?> getMyUser() async {
final (adminDto, preferenceDto) = await (_api.getMyUser(), _api.getMyPreferences()).wait;
if (adminDto == null) return null;
if (adminDto == null) {
return null;
}
return UserConverter.fromAdminDto(adminDto, preferenceDto);
}
@@ -44,7 +44,9 @@ class Activity {
@override
bool operator ==(covariant Activity other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.id == id &&
other.assetId == assetId &&
+3 -1
View File
@@ -44,7 +44,9 @@ class AuthState {
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other is AuthState &&
other.deviceId == deviceId &&
@@ -16,7 +16,9 @@ class AuxilaryEndpoint {
@override
bool operator ==(covariant AuxilaryEndpoint other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.url == url && other.status == status;
}
@@ -53,7 +55,9 @@ class AuxCheckStatus {
@override
bool operator ==(covariant AuxCheckStatus other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.name == name;
}
@@ -19,7 +19,9 @@ class BiometricStatus {
@override
bool operator ==(covariant BiometricStatus other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
final listEquals = const DeepCollectionEquality().equals;
return listEquals(other.availableBiometrics, availableBiometrics) && other.canAuthenticate == canAuthenticate;
@@ -67,7 +67,9 @@ class CastManagerState {
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other is CastManagerState &&
other.isCasting == isCasting &&
@@ -41,7 +41,9 @@ class DownloadInfo {
@override
bool operator ==(covariant DownloadInfo other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.fileName == fileName && other.progress == progress && other.status == status;
}
@@ -71,7 +73,9 @@ class DownloadState {
@override
bool operator ==(covariant DownloadState other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
final mapEquals = const DeepCollectionEquality().equals;
return other.downloadStatus == downloadStatus &&
@@ -32,7 +32,9 @@ class LivePhotosMetadata {
@override
bool operator ==(covariant LivePhotosMetadata other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.part == part && other.id == id;
}
+3 -1
View File
@@ -17,7 +17,9 @@ class MapMarker {
@override
bool operator ==(covariant MapMarker other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.latLng == latLng && other.assetRemoteId == assetRemoteId;
}
+3 -1
View File
@@ -51,7 +51,9 @@ class MapState {
@override
bool operator ==(covariant MapState other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.themeMode == themeMode &&
other.showFavoriteOnly == showFavoriteOnly &&
@@ -42,7 +42,9 @@ class SearchCuratedContent {
@override
bool operator ==(covariant SearchCuratedContent other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.label == label && other.subtitle == subtitle && other.id == id;
}
@@ -36,7 +36,9 @@ class SearchLocationFilter {
@override
bool operator ==(covariant SearchLocationFilter other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.country == country && other.state == state && other.city == city;
}
@@ -75,7 +77,9 @@ class SearchCameraFilter {
@override
bool operator ==(covariant SearchCameraFilter other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.make == make && other.model == model;
}
@@ -117,7 +121,9 @@ class SearchDateFilter {
@override
bool operator ==(covariant SearchDateFilter other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.takenBefore == takenBefore && other.takenAfter == takenAfter;
}
@@ -152,7 +158,9 @@ class SearchRatingFilter {
@override
bool operator ==(covariant SearchRatingFilter other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.rating == rating;
}
@@ -198,7 +206,9 @@ class SearchDisplayFilters {
@override
bool operator ==(covariant SearchDisplayFilters other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.isNotInAlbum == isNotInAlbum && other.isArchive == isArchive && other.isFavorite == isFavorite;
}
@@ -305,7 +315,9 @@ class SearchFilter {
@override
bool operator ==(covariant SearchFilter other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.context == context &&
other.filename == filename &&
@@ -38,7 +38,9 @@ class ServerConfig {
@override
bool operator ==(covariant ServerConfig other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.trashDays == trashDays &&
other.oauthButtonText == oauthButtonText &&
@@ -35,7 +35,9 @@ class ServerDiskInfo {
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other is ServerDiskInfo &&
other.diskAvailable == diskAvailable &&
@@ -50,7 +50,9 @@ class ServerFeatures {
@override
bool operator ==(covariant ServerFeatures other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.trash == trash &&
other.map == map &&
@@ -60,7 +60,9 @@ class ServerInfo {
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other is ServerInfo &&
other.serverVersion == serverVersion &&
@@ -88,7 +88,9 @@ class ShareIntentAttachment {
@override
bool operator ==(covariant ShareIntentAttachment other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.path == path && other.type == type;
}
@@ -418,7 +418,9 @@ class _PreparingStatusState extends ConsumerState {
}
void _startPollingIfNeeded() {
if (_pollingTimer != null) return;
if (_pollingTimer != null) {
return;
}
_pollingTimer = Timer.periodic(const Duration(seconds: 3), (timer) async {
final currentUser = ref.read(currentUserProvider);
@@ -83,7 +83,9 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
final albumCount = albums.length;
// Filter albums based on search query
final filteredAlbums = albums.where((album) {
if (_searchQuery.isEmpty) return true;
if (_searchQuery.isEmpty) {
return true;
}
return album.name.toLowerCase().contains(_searchQuery.toLowerCase());
}).toList();
@@ -40,7 +40,9 @@ class _DriftUploadDetailPageState extends ConsumerState<DriftUploadDetailPage> {
}
for (final item in uploadingItems) {
if (_taskSlotAssignments.containsKey(item.taskId)) continue;
if (_taskSlotAssignments.containsKey(item.taskId)) {
continue;
}
for (int i = 0; i < _maxSlots; i++) {
if (slots[i] == null) {
@@ -93,7 +93,9 @@ class HeaderSettingsPage extends HookConsumerWidget {
final key = header.key.trim();
final value = header.value.trim();
if (key.isEmpty || value.isEmpty) continue;
if (key.isEmpty || value.isEmpty) {
continue;
}
headersMap[key] = value;
}
@@ -31,7 +31,9 @@ RecursiveFolder? _findFolderInStructure(RootFolder rootFolder, RecursiveFolder t
if (folder.subfolders.isNotEmpty) {
final found = _findFolderInStructure(folder, targetFolder);
if (found != null) return found;
if (found != null) {
return found;
}
}
}
return null;
@@ -113,7 +115,9 @@ class FolderContent extends HookConsumerWidget {
// Initial asset fetch
useEffect(() {
if (folder == null) return;
if (folder == null) {
return;
}
ref.read(folderRenderListProvider(folder!).notifier).fetchAssets(sortOrder);
return null;
}, [folder]);
@@ -20,7 +20,9 @@ class SharedLinkPage extends HookConsumerWidget {
useEffect(() {
ref.read(sharedLinksStateProvider.notifier).fetchLinks();
return () {
if (!context.mounted) return;
if (!context.mounted) {
return;
}
ref.invalidate(sharedLinksStateProvider);
};
}, []);
@@ -191,8 +191,12 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection
}
String _getAssetTypeTitle(BaseAsset asset) {
if (asset is LocalAsset) return 'Local Asset';
if (asset is RemoteAsset) return 'Remote Asset';
if (asset is LocalAsset) {
return 'Local Asset';
}
if (asset is RemoteAsset) {
return 'Remote Asset';
}
return 'Base Asset';
}
}
@@ -245,7 +245,9 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> {
}
Future<void> _handleSave() async {
if (formKey.currentState?.validate() != true) return;
if (formKey.currentState?.validate() != true) {
return;
}
try {
final newTitle = titleController.text.trim();
@@ -95,7 +95,9 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
return PopScope(
canPop: !hasUnsavedEdits,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
if (didPop) {
return;
}
final shouldDiscard = await _showDiscardChangesDialog() ?? false;
if (shouldDiscard && mounted) {
Navigator.of(context).pop();
@@ -179,7 +179,9 @@ class EditorState {
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other is EditorState &&
other.isApplyingEdits == isApplyingEdits &&
@@ -58,7 +58,9 @@ class _ProfilePictureCropPageState extends ConsumerState<ProfilePictureCropPage>
}
Future<void> _handleDone() async {
if (_isLoading) return;
if (_isLoading) {
return;
}
setState(() {
_isLoading = true;
@@ -72,7 +74,9 @@ class _ProfilePictureCropPageState extends ConsumerState<ProfilePictureCropPage>
.read(uploadProfileImageProvider.notifier)
.upload(xFile, fileName: 'profile-picture.png');
if (!context.mounted) return;
if (!context.mounted) {
return;
}
if (success) {
final profileImagePath = ref.read(uploadProfileImageProvider).profileImagePath;
@@ -102,7 +106,9 @@ class _ProfilePictureCropPageState extends ConsumerState<ProfilePictureCropPage>
);
}
} catch (e) {
if (!context.mounted) return;
if (!context.mounted) {
return;
}
ImmichToast.show(
context: context,
@@ -708,7 +708,9 @@ class _SearchResultGrid extends ConsumerWidget {
bool _onScrollUpdateNotification(ScrollNotification notification) {
final metrics = notification.metrics;
if (metrics.axis != Axis.vertical) return false;
if (metrics.axis != Axis.vertical) {
return false;
}
final isBottomSheet = notification.context?.findAncestorWidgetOfExactType<DraggableScrollableSheet>() != null;
final remaining = metrics.maxScrollExtent - metrics.pixels;
@@ -735,7 +737,9 @@ class _SearchResultGrid extends ConsumerWidget {
final hasMore = ref.watch(paginatedSearchProvider.select((s) => s.nextPage != null));
if (hasMore) return null;
if (hasMore) {
return null;
}
return SliverToBoxAdapter(
child: Padding(
@@ -44,7 +44,9 @@ class PaginatedSearchNotifier extends StateNotifier<SearchState> {
Stream<int> get assetCount => _assetCountController.stream;
Future<void> search(SearchFilter filter) async {
if (state.nextPage == null || state.isLoading) return;
if (state.nextPage == null || state.isLoading) {
return;
}
state = SearchState(assets: state.assets, nextPage: state.nextPage, isLoading: true);
@@ -50,7 +50,9 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
List<Widget> _buildMenuChildren() {
final asset = ref.read(assetViewerProvider).currentAsset;
if (asset == null) return [];
if (asset == null) {
return [];
}
final user = ref.read(currentUserProvider);
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;

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