mirror of
https://github.com/immich-app/immich.git
synced 2026-05-21 15:16:31 -04:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c864a0bec | |||
| fb0a54d548 | |||
| 7013cc0904 | |||
| dcaf7b4a65 | |||
| 12f7b2a005 | |||
| 7837d40f57 | |||
| b4f719653f | |||
| f370b4bac6 | |||
| d788169bf3 | |||
| eea820fa2f | |||
| 271f1cb868 | |||
| 8c8dc9d32f | |||
| fd18e55f7c | |||
| faab9e620d | |||
| 5ba3efafd8 | |||
| 8b3c9bf9c3 | |||
| 41f285aa3e | |||
| fdac6c8bc4 | |||
| d7f05d2510 | |||
| 3100bd5eed | |||
| 8a024e2b50 | |||
| 25a6a38b30 | |||
| 7c6750941e |
+1
-3
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
24.15.0
|
||||
+1
-1
@@ -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
@@ -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" },
|
||||
]
|
||||
|
||||
@@ -66,8 +66,5 @@
|
||||
"fastq": "^1.17.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.15.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -95,6 +95,3 @@ services:
|
||||
restart: always
|
||||
healthcheck:
|
||||
disable: false
|
||||
|
||||
volumes:
|
||||
model-cache:
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
24.15.0
|
||||
+1
-1
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
```
|
||||
|
||||
::
|
||||
:::
|
||||
|
||||
@@ -56,8 +56,5 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.15.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
24.15.0
|
||||
@@ -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" },
|
||||
]
|
||||
|
||||
@@ -56,8 +56,5 @@
|
||||
"utimes": "^5.2.1",
|
||||
"vite-tsconfig-paths": "^6.1.1",
|
||||
"vitest": "^4.0.0"
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.15.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" },
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user