Compare commits

..

1 Commits

Author SHA1 Message Date
midzelis
8a7e679b74 Eslint custom rules 2025-10-10 01:10:43 +00:00
46 changed files with 3006 additions and 2845 deletions

View File

@@ -55,7 +55,7 @@ jobs:
runs-on: mich
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
ref: ${{ inputs.ref || github.sha }}
persist-credentials: false
@@ -66,7 +66,7 @@ jobs:
working-directory: ./mobile
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: 'zulu'
java-version: '17'

View File

@@ -19,7 +19,7 @@ jobs:
actions: write
steps:
- name: Check out code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false

View File

@@ -29,7 +29,7 @@ jobs:
working-directory: ./cli
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
@@ -37,7 +37,7 @@ jobs:
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './cli/.nvmrc'
registry-url: 'https://registry.npmjs.org'
@@ -65,7 +65,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false

View File

@@ -44,13 +44,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -63,7 +63,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
uses: github/codeql-action/autobuild@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -76,6 +76,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
with:
category: '/language:${{matrix.language}}'

View File

@@ -124,7 +124,7 @@ jobs:
tag-suffix: '-rocm'
platforms: linux/amd64
runner-mapping: '{"linux/amd64": "mich"}'
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@946acac326940f8badf09ccf591d9cb345d6a689 # multi-runner-build-workflow-v0.2.1
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1
permissions:
contents: read
actions: read
@@ -147,7 +147,7 @@ jobs:
name: Build and Push Server
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@946acac326940f8badf09ccf591d9cb345d6a689 # multi-runner-build-workflow-v0.2.1
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1
permissions:
contents: read
actions: read

View File

@@ -31,8 +31,6 @@ jobs:
- 'open-api/immich-openapi-specs.json'
force-filters: |
- '.github/workflows/docs-build.yml'
- '.github/workflows/docs-deploy.yml'
- 'deployment/**'
force-events: 'release'
force-branches: 'main'
@@ -49,7 +47,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
@@ -57,7 +55,7 @@ jobs:
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './docs/.nvmrc'
cache: 'pnpm'

View File

@@ -20,7 +20,7 @@ jobs:
run: echo 'The triggering workflow did not succeed' && exit 1
- name: Get artifact
id: get-artifact
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
@@ -38,7 +38,7 @@ jobs:
return { found: true, id: matchArtifact.id };
- name: Determine deploy parameters
id: parameters
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
with:
@@ -108,13 +108,13 @@ jobs:
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Load parameters
id: parameters
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
PARAM_JSON: ${{ needs.checks.outputs.parameters }}
with:
@@ -125,7 +125,7 @@ jobs:
core.setOutput("shouldDeploy", parameters.shouldDeploy);
- name: Download artifact
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }}
with:

View File

@@ -14,7 +14,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false

View File

@@ -22,7 +22,7 @@ jobs:
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: 'Checkout'
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
ref: ${{ github.event.pull_request.head.ref }}
token: ${{ steps.generate-token.outputs.token }}
@@ -32,7 +32,7 @@ jobs:
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
@@ -48,7 +48,7 @@ jobs:
message: 'chore: fix formatting'
- name: Remove label
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
if: always()
with:
script: |

View File

@@ -11,4 +11,4 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0

View File

@@ -55,20 +55,20 @@ jobs:
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
token: ${{ steps.generate-token.outputs.token }}
persist-credentials: true
ref: main
- name: Install uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
@@ -117,18 +117,18 @@ jobs:
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
token: ${{ steps.generate-token.outputs.token }}
persist-credentials: false
- name: Download APK
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: release-apk-signed
- name: Create draft release
uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
with:
draft: true
tag_name: ${{ env.IMMICH_VERSION }}

View File

@@ -24,7 +24,7 @@ jobs:
permissions:
pull-requests: write
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
github.rest.issues.removeLabel({

View File

@@ -16,7 +16,7 @@ jobs:
run:
working-directory: ./open-api/typescript-sdk
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
@@ -24,7 +24,7 @@ jobs:
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './open-api/typescript-sdk/.nvmrc'
registry-url: 'https://registry.npmjs.org'

View File

@@ -42,7 +42,7 @@ jobs:
working-directory: ./mobile
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false

View File

@@ -56,13 +56,13 @@ jobs:
working-directory: ./server
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
@@ -93,13 +93,13 @@ jobs:
working-directory: ./cli
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './cli/.nvmrc'
cache: 'pnpm'
@@ -133,13 +133,13 @@ jobs:
working-directory: ./cli
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './cli/.nvmrc'
cache: 'pnpm'
@@ -168,13 +168,13 @@ jobs:
working-directory: ./web
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './web/.nvmrc'
cache: 'pnpm'
@@ -205,13 +205,13 @@ jobs:
working-directory: ./web
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './web/.nvmrc'
cache: 'pnpm'
@@ -236,13 +236,13 @@ jobs:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './web/.nvmrc'
cache: 'pnpm'
@@ -277,13 +277,13 @@ jobs:
working-directory: ./e2e
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './e2e/.nvmrc'
cache: 'pnpm'
@@ -316,13 +316,13 @@ jobs:
working-directory: ./server
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
@@ -347,14 +347,14 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
submodules: 'recursive'
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './e2e/.nvmrc'
cache: 'pnpm'
@@ -395,14 +395,14 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
submodules: 'recursive'
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './e2e/.nvmrc'
cache: 'pnpm'
@@ -441,7 +441,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup Flutter SDK
@@ -466,12 +466,12 @@ jobs:
run:
working-directory: ./machine-learning
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
# TODO: add caching when supported (https://github.com/actions/setup-python/pull/818)
# with:
# python-version: 3.11
@@ -503,13 +503,13 @@ jobs:
working-directory: ./.github
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './.github/.nvmrc'
cache: 'pnpm'
@@ -525,7 +525,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Run ShellCheck
@@ -540,13 +540,13 @@ jobs:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
@@ -581,7 +581,7 @@ jobs:
contents: read
services:
postgres:
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3@sha256:dbf18b3ffea4a81434c65b71e20d27203baf903a0275f4341e4c16dfd901fd67
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3@sha256:da52bbead5d818adaa8077c8dcdaad0aaf93038c31ad8348b51f9f0ec1310a4d
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
@@ -595,13 +595,13 @@ jobs:
working-directory: ./server
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'

View File

@@ -22,7 +22,7 @@ For organizations seeking to resell Immich, we have established the following gu
- Do not misrepresent your reseller site or services as being officially affiliated with or endorsed by Immich or our development team.
- For small resellers who wish to contribute financially to Immich's development, we recommend directing your customers to purchase licenses directly from us rather than attempting to broker revenue-sharing arrangements. We ask that you refrain from misrepresenting reseller activities as directly supporting our development work.
- For small resellers who wish to contribute financially to Immich's development, we recommend directing your customers to purchase licenses directy from us rather than attempting to broker revenue-sharing arrangements. We ask that you refrain from misrepresenting reseller activities as directly supporting our development work.
When in doubt or if you have an edge case scenario, we encourage you to contact us directly via email to discuss the use of our trademark. We can provide clear guidance on what is acceptable and what is not. You can reach out at: questions@immich.app

View File

@@ -4,7 +4,7 @@ Immich supports the Google's Cast protocol so that photos and videos can be cast
## Enable Google Cast Support
Google Cast support is disabled by default. The web UI uses Google-provided scripts and must retrieve them from Google servers when the page loads. This is a privacy concern for some and is thus opt-in.
Google Cast support is disabled by default. The web UI uses Google-provided scripts and must retreive them from Google servers when the page loads. This is a privacy concern for some and is thus opt-in.
You can enable Google Cast support through `Account Settings > Features > Cast > Google Cast`

View File

@@ -17,9 +17,9 @@
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "~3.9.0",
"@docusaurus/preset-classic": "~3.9.0",
"@docusaurus/theme-common": "~3.9.0",
"@docusaurus/core": "~3.8.0",
"@docusaurus/preset-classic": "~3.8.0",
"@docusaurus/theme-common": "~3.8.0",
"@mdi/js": "^7.3.67",
"@mdi/react": "^1.6.1",
"@mdx-js/react": "^3.0.0",
@@ -35,7 +35,7 @@
"url": "^0.11.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "~3.9.0",
"@docusaurus/module-type-aliases": "~3.8.0",
"@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/types": "^3.7.0",
"prettier": "^3.2.4",

View File

@@ -43,7 +43,7 @@
"pngjs": "^7.0.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^4.0.0",
"sharp": "^0.34.4",
"sharp": "^0.34.3",
"socket.io-client": "^4.7.4",
"supertest": "^7.0.0",
"typescript": "^5.3.3",

View File

@@ -1,7 +1,7 @@
[tools]
node = "22.20.0"
flutter = "3.35.5"
pnpm = "10.18.0"
pnpm = "10.15.1"
[tools."github:CQLabs/homebrew-dcm"]
version = "1.30.0"

View File

@@ -114,7 +114,7 @@ struct ImmichMemoryProvider: TimelineProvider {
}
}
// If we didn't add any memory images (some failure occurred or no images in memory),
// If we didnt add any memory images (some failure occured or no images in memory),
// default to 12 hours of random photos
if entries.count == 0 {
// this must be a do/catch since we need to

View File

@@ -86,7 +86,7 @@ class StoreService {
_cache.remove(key.id);
}
/// Clears all values from the store (cache and DB)
/// Clears all values from thw store (cache and DB)
Future<void> clear() async {
await _storeRepository.deleteAll();
_cache.clear();

View File

@@ -221,8 +221,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
context.scaffoldMessenger.hideCurrentSnackBar();
// send image to casting if the server has it
if (asset is RemoteAsset) {
ref.read(castProvider.notifier).loadMedia(asset, false);
if (asset.hasRemote) {
final remoteAsset = asset as RemoteAsset;
ref.read(castProvider.notifier).loadMedia(remoteAsset, false);
} else {
// casting cannot show local assets
context.scaffoldMessenger.clearSnackBars();

View File

@@ -3,7 +3,7 @@
"version": "0.0.1",
"description": "Monorepo for Immich",
"private": true,
"packageManager": "pnpm@10.18.0+sha512.e804f889f1cecc40d572db084eec3e4881739f8dec69c0ff10d2d1beff9a4e309383ba27b5b750059d7f4c149535b6cd0d2cb1ed3aeb739239a4284a68f40cfa",
"packageManager": "pnpm@10.15.1+sha512.34e538c329b5553014ca8e8f4535997f96180a1d0f614339357449935350d924e22f8614682191264ec33d1462ac21561aff97f6bb18065351c162c7e8f6de67",
"engines": {
"pnpm": ">=10.0.0"
}

5157
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@ onlyBuiltDependencies:
- '@tailwindcss/oxide'
overrides:
canvas: 2.11.2
sharp: ^0.34.4
sharp: ^0.34.3
packageExtensions:
nestjs-kysely:
dependencies:

View File

@@ -21,11 +21,6 @@
"addLabels": [
"📱mobile"
]
},
{
"matchPackageNames": ["ghcr.io/immich-app/postgres"],
"matchUpdateTypes": ["major"],
"enabled": false
}
],
"ignorePaths": [

View File

@@ -1,4 +1,4 @@
FROM ghcr.io/immich-app/base-server-dev:202510092146@sha256:124ec9659cba4a206924de5e3691f84acde16d75fa2b10b7007542424b696b96 AS builder
FROM ghcr.io/immich-app/base-server-dev:202509210934@sha256:b5ce2d7eaf379d4cf15efd4bab180d8afc8a80d20b36c9800f4091aca6ae267e AS builder
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
CI=1 \
COREPACK_HOME=/tmp \
@@ -48,7 +48,7 @@ RUN --mount=type=cache,id=pnpm-cli,target=/buildcache/pnpm-store \
pnpm --filter @immich/sdk --filter @immich/cli build && \
pnpm --filter @immich/cli --prod --no-optional deploy /output/cli-pruned
FROM ghcr.io/immich-app/base-server-prod:202510092146@sha256:c39b9ad949e7777bce415e6931334aeff7331e04cb7f9df93f9ae44f6ff36b9e
FROM ghcr.io/immich-app/base-server-prod:202509210934@sha256:0c7eacf0ba88ca52e1a267cfc62d20d07792ea2c604818c2cbd37dc7dcefdac9
WORKDIR /usr/src/app
ENV NODE_ENV=production \

View File

@@ -1,5 +1,5 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:202510092146@sha256:124ec9659cba4a206924de5e3691f84acde16d75fa2b10b7007542424b696b96 AS dev
FROM ghcr.io/immich-app/base-server-dev:202509210934@sha256:b5ce2d7eaf379d4cf15efd4bab180d8afc8a80d20b36c9800f4091aca6ae267e AS dev
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
CI=1 \

View File

@@ -47,7 +47,7 @@
"@opentelemetry/exporter-prometheus": "^0.205.0",
"@opentelemetry/instrumentation-http": "^0.205.0",
"@opentelemetry/instrumentation-ioredis": "^0.53.0",
"@opentelemetry/instrumentation-nestjs-core": "^0.52.0",
"@opentelemetry/instrumentation-nestjs-core": "^0.51.0",
"@opentelemetry/instrumentation-pg": "^0.58.0",
"@opentelemetry/resources": "^2.0.1",
"@opentelemetry/sdk-metrics": "^2.0.1",
@@ -67,7 +67,7 @@
"compression": "^1.8.0",
"cookie": "^1.0.2",
"cookie-parser": "^1.4.7",
"cron": "4.3.3",
"cron": "4.3.0",
"exiftool-vendored": "^28.8.0",
"express": "^5.1.0",
"fast-glob": "^3.3.2",
@@ -85,7 +85,7 @@
"multer": "^2.0.2",
"nest-commander": "^3.16.0",
"nestjs-cls": "^5.0.0",
"nestjs-kysely": "3.0.0",
"nestjs-kysely": "^3.0.0",
"nestjs-otel": "^7.0.0",
"nodemailer": "^7.0.0",
"openid-client": "^6.3.3",
@@ -101,7 +101,7 @@
"sanitize-filename": "^1.6.3",
"sanitize-html": "^2.14.0",
"semver": "^7.6.2",
"sharp": "^0.34.4",
"sharp": "^0.34.3",
"sirv": "^3.0.0",
"socket.io": "^4.8.1",
"tailwindcss-preset-email": "^1.4.0",
@@ -164,6 +164,6 @@
"node": "22.20.0"
},
"overrides": {
"sharp": "^0.34.4"
"sharp": "^0.34.3"
}
}

View File

@@ -10,6 +10,7 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
import parser from 'svelte-eslint-parser';
import typescriptEslint from 'typescript-eslint';
import immichEslintRules from './immich.eslint.rules.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -79,6 +80,11 @@ export default typescriptEslint.config(
{
plugins: {
svelte: eslintPluginSvelte,
immich: {
rules: {
'no-unlocalized-strings': immichEslintRules,
},
},
},
languageOptions: {
@@ -99,7 +105,7 @@ export default typescriptEslint.config(
},
},
ignores: ['**/service-worker/**'],
ignores: ['**/service-worker/**', '**/*.svelte.ts'],
rules: {
'@typescript-eslint/no-unused-vars': [
@@ -129,6 +135,7 @@ export default typescriptEslint.config(
'@typescript-eslint/require-await': 'error',
'object-shorthand': ['error', 'always'],
'svelte/no-navigation-without-resolve': 'off',
'immich/no-unlocalized-strings': 'error',
},
},
{

325
web/immich.eslint.rules.js Normal file
View File

@@ -0,0 +1,325 @@
/**
* ESLint rule: no-unlocalized-strings
*
* Ensures all string literals containing alphabetic characters are wrapped in t() function calls.
* String literals without alpha characters (like "123", "---", etc.) are ignored.
*/
export default {
meta: {
type: 'problem',
docs: {
description: 'Require string literals with alphabetic characters to be wrapped in t() function',
category: 'Internationalization',
recommended: true,
},
messages: {
unlocalizedString: "String literal '{{text}}' should be wrapped in t() function for internationalization",
},
schema: [], // no options
},
create(context) {
/**
* Check if current file should be ignored
*/
function shouldIgnoreFile() {
const filename = context.getFilename();
// Ignore test/spec files
if (/\.(spec|test)\.(ts|tsx|js|jsx)$/.test(filename)) {
return true;
}
// Ignore files in __tests__ directories
if (filename.includes('__tests__')) {
return true;
}
// Ignore files in test-data directories
if (filename.includes('test-data')) {
return true;
}
// Ignore config files
if (/\.config\.(ts|js)$/.test(filename)) {
return true;
}
return false;
}
// Skip entire file if it should be ignored
if (shouldIgnoreFile()) {
return {};
}
/**
* Check if a node has an ignore comment
* Supports: i18n-ignore, or standard eslint-disable
*/
function hasIgnoreComment(node) {
const sourceCode = context.getSourceCode();
const comments = sourceCode.getCommentsBefore(node);
// Check for i18n-ignore in comments on the same line or line before
for (const comment of comments) {
if (comment.value.trim().includes('i18n-ignore')) {
return true;
}
}
// Also check for inline comments on the same line
const lineComments = sourceCode.getCommentsAfter(node);
for (const comment of lineComments) {
if (comment.loc.start.line === node.loc.start.line && comment.value.trim().includes('i18n-ignore')) {
return true;
}
}
return false;
}
/**
* Check if a string contains any alphabetic characters
*/
function containsAlphabeticChars(str) {
return /[a-zA-Z]/.test(str);
}
/**
* Check if a node is a direct argument to t() function call
*/
function isInTranslationFunction(node) {
const parent = node.parent;
// Check if parent is a CallExpression with callee named 't' or '$t'
if (parent && parent.type === 'CallExpression') {
const callee = parent.callee;
// Direct function call: t('string') or $t('string')
if (callee.type === 'Identifier' && (callee.name === 't' || callee.name === '$t')) {
return true;
}
// Member expression: this.t('string') or obj.t('string')
if (
callee.type === 'MemberExpression' &&
callee.property.type === 'Identifier' &&
(callee.property.name === 't' || callee.property.name === '$t')
) {
return true;
}
}
return false;
}
/**
* Check if node is in a context where translation is not needed
*/
function isInIgnoredContext(node) {
let current = node.parent;
while (current) {
// Ignore object keys: { key: 'value' } - the 'key' part
if (current.type === 'Property' && current.key === node) {
return true;
}
// Ignore object property values for certain keys
if (current.type === 'Property' && current.value === node) {
const keyName = current.key?.name || current.key?.value;
// List of object keys whose values don't need translation
const nonTranslatableKeys = [
'key',
'keys',
'shortcut',
'hotkey',
'keyCode',
'code',
'id',
'type',
'name',
'ref',
'testId',
'dataTestId',
'className',
'class',
'style',
'href',
'src',
'alt',
'role',
'method',
'action',
'target',
'rel',
'as',
];
if (nonTranslatableKeys.includes(keyName)) {
return true;
}
// Ignore data-* properties
if (typeof keyName === 'string' && keyName.startsWith('data-')) {
return true;
}
}
// Ignore import/export statements
if (
current.type === 'ImportDeclaration' ||
current.type === 'ExportNamedDeclaration' ||
current.type === 'ExportAllDeclaration'
) {
return true;
}
// Ignore dynamic imports: import('module-name')
if (current.type === 'ImportExpression') {
return true;
}
// Ignore type annotations (TypeScript)
if (current.type === 'TSLiteralType' || current.type === 'TSTypeReference') {
return true;
}
// Ignore JSX/Svelte attribute values that are plain strings (not expressions)
// For example: <div class="container"> or <Button variant="primary">
if (current.type === 'JSXAttribute') {
const attrName = current.name?.name;
// List of attributes that typically don't need translation
const nonTranslatableAttrs = [
'class',
'className',
'id',
'style',
'type',
'name',
'value',
'href',
'src',
'alt',
'role',
'data-testid',
'key',
'ref',
'variant',
'color',
'size',
'shape',
'icon',
'position',
'align',
'justify',
'direction',
'wrap',
'gap',
'spacing',
];
// Also ignore any data-* or aria-* attributes except aria-label, aria-description
const isDataAttr = attrName?.startsWith('data-');
const isNonTextAriaAttr =
attrName?.startsWith('aria-') &&
attrName !== 'aria-label' &&
attrName !== 'aria-description' &&
attrName !== 'aria-placeholder';
if (nonTranslatableAttrs.includes(attrName) || isDataAttr || isNonTextAriaAttr) {
return true;
}
}
current = current.parent;
}
return false;
}
return {
Literal(node) {
// Only check string literals
if (typeof node.value !== 'string') {
return;
}
const stringValue = node.value;
// Skip strings without alphabetic characters
if (!containsAlphabeticChars(stringValue)) {
return;
}
// Skip if has ignore comment
if (hasIgnoreComment(node)) {
return;
}
// Skip if already wrapped in t() function
if (isInTranslationFunction(node)) {
return;
}
// Skip if in ignored context (object keys, imports, etc.)
if (isInIgnoredContext(node)) {
return;
}
// Report the violation
context.report({
node,
messageId: 'unlocalizedString',
data: {
text: stringValue.length > 30 ? stringValue.substring(0, 30) + '...' : stringValue,
},
});
},
// Also handle template literals
TemplateLiteral(node) {
// Skip if it has expressions (e.g., `Hello ${name}`)
if (node.expressions.length > 0) {
return;
}
// Get the string value from quasis
const stringValue = node.quasis[0]?.value.cooked || '';
// Skip strings without alphabetic characters
if (!containsAlphabeticChars(stringValue)) {
return;
}
// Skip if has ignore comment
if (hasIgnoreComment(node)) {
return;
}
// Skip if already wrapped in t() function
if (isInTranslationFunction(node)) {
return;
}
// Skip if in ignored context
if (isInIgnoredContext(node)) {
return;
}
// Report the violation
context.report({
node,
messageId: 'unlocalizedString',
data: {
text: stringValue.length > 30 ? stringValue.substring(0, 30) + '...' : stringValue,
},
});
},
};
},
};

View File

@@ -27,7 +27,7 @@
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.9.8",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@immich/ui": "^0.34.0",
"@immich/ui": "^0.29.0",
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.11.5",
@@ -44,7 +44,7 @@
"geo-coordinates-parser": "^1.7.4",
"geojson": "^0.5.0",
"handlebars": "^4.7.8",
"happy-dom": "^20.0.0",
"happy-dom": "^18.0.1",
"intl-messageformat": "^10.7.11",
"justified-layout": "^4.1.0",
"lodash-es": "^4.17.21",
@@ -69,7 +69,7 @@
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/enhanced-img": "^0.8.0",
"@sveltejs/kit": "^2.27.1",
"@sveltejs/vite-plugin-svelte": "6.2.1",
"@sveltejs/vite-plugin-svelte": "6.2.0",
"@tailwindcss/vite": "^4.1.7",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/svelte": "^5.2.8",
@@ -89,13 +89,13 @@
"eslint-plugin-unicorn": "^61.0.2",
"factory.ts": "^1.4.1",
"globals": "^16.0.0",
"happy-dom": "^20.0.0",
"happy-dom": "^18.0.1",
"prettier": "^3.4.2",
"prettier-plugin-organize-imports": "^4.0.0",
"prettier-plugin-sort-json": "^4.1.1",
"prettier-plugin-svelte": "^3.3.3",
"rollup-plugin-visualizer": "^6.0.0",
"svelte": "5.39.11",
"svelte": "5.38.10",
"svelte-check": "^4.1.5",
"svelte-eslint-parser": "^1.3.3",
"tailwindcss": "^4.1.7",

View File

@@ -16,7 +16,7 @@
<Label size="small" color="primary" for={id}>{title}</Label>
<Text size="small" color="muted" {id}>
{#if versionHref}
<Link href={versionHref}>{version}</Link>
<Link external href={versionHref}>{version}</Link>
{:else}
{version}
{/if}

View File

@@ -22,8 +22,8 @@
import {
AssetJobName,
AssetTypeEnum,
getAllAlbums,
getAssetInfo,
getAllAlbums,
getStack,
runAssetJobs,
type AlbumResponseDto,
@@ -303,8 +303,10 @@
const handleStopSlideshow = async () => {
try {
// eslint-disable-next-line tscompat/tscompat
if (document.fullscreenElement) {
document.body.style.cursor = '';
// eslint-disable-next-line tscompat/tscompat
await document.exitFullscreen();
}
} catch (error) {

View File

@@ -100,7 +100,9 @@
};
const onShowSettings = async () => {
// eslint-disable-next-line tscompat/tscompat
if (document.fullscreenElement) {
// eslint-disable-next-line tscompat/tscompat
await document.exitFullscreen();
}
await modalManager.show(SlideshowSettingsModal);
@@ -112,7 +114,11 @@
webkitIsFullScreen?: boolean;
};
if (!document.fullscreenElement && !doc.webkitIsFullScreen) {
if (
// eslint-disable-next-line tscompat/tscompat
!document.fullscreenElement &&
!doc.webkitIsFullScreen
) {
onClose();
}
}

View File

@@ -341,6 +341,7 @@
scrubberMonthScrollPercent: monthGroupPercentY,
});
};
/* eslint-disable tscompat/tscompat */
const getTouch = (event: TouchEvent) => {
if (event.touches.length === 1) {
return event.touches[0];
@@ -385,6 +386,7 @@
isHover = false;
}
};
/* eslint-enable tscompat/tscompat */
onMount(() => {
document.addEventListener('touchmove', onTouchMove, { capture: true, passive: true });
return () => {

View File

@@ -134,12 +134,26 @@
element.scrollTop = top;
}
};
const scrollBy = (y: number) => {
if (element) {
element.scrollBy(0, y);
}
};
const scrollToTop = () => {
scrollTo(0);
};
const getAssetHeight = (assetId: string, monthGroup: MonthGroup) => monthGroup.findAssetAbsolutePosition(assetId);
const getAssetHeight = (assetId: string, monthGroup: MonthGroup) => {
// the following method may trigger any layouts, so need to
// handle any scroll compensation that may have been set
const height = monthGroup!.findAssetAbsolutePosition(assetId);
while (timelineManager.scrollCompensation.monthGroup) {
handleScrollCompensation(timelineManager.scrollCompensation);
timelineManager.clearScrollCompensation();
}
return height;
};
const assetIsVisible = (assetTop: number): boolean => {
if (!element) {
@@ -232,6 +246,19 @@
// note: don't throttle, debounch, or otherwise do this function async - it causes flicker
const updateSlidingWindow = () => timelineManager.updateSlidingWindow(element?.scrollTop || 0);
const handleScrollCompensation = ({ heightDelta, scrollTop }: { heightDelta?: number; scrollTop?: number }) => {
if (heightDelta !== undefined) {
scrollBy(heightDelta);
} else if (scrollTop !== undefined) {
scrollTo(scrollTop);
}
// Yes, updateSlideWindow() is called by the onScroll event triggered as a result of
// the above calls. However, this delay is enough time to set the intersecting property
// of the monthGroup to false, then true, which causes the DOM nodes to be recreated,
// causing bad perf, and also, disrupting focus of those elements.
updateSlidingWindow();
};
const topSectionResizeObserver: OnResizeCallback = ({ height }) => (timelineManager.topSectionHeight = height);
onMount(() => {
@@ -639,6 +666,7 @@
onSelect={({ title, assets }) => handleGroupSelect(timelineManager, title, assets)}
onSelectAssetCandidates={handleSelectAssetCandidates}
onSelectAssets={handleSelectAssets}
onScrollCompensation={handleScrollCompensation}
{customLayout}
{onThumbnailClick}
/>

View File

@@ -33,6 +33,7 @@
onSelect: ({ title, assets }: { title: string; assets: TimelineAsset[] }) => void;
onSelectAssets: (asset: TimelineAsset) => void;
onSelectAssetCandidates: (asset: TimelineAsset | null) => void;
onScrollCompensation: (compensation: { heightDelta?: number; scrollTop?: number }) => void;
onThumbnailClick?: (
asset: TimelineAsset,
timelineManager: TimelineManager,
@@ -58,7 +59,7 @@
onSelect,
onSelectAssets,
onSelectAssetCandidates,
onScrollCompensation,
onThumbnailClick,
}: Props = $props();
@@ -133,6 +134,13 @@
});
return getDateLocaleString(date);
};
$effect.root(() => {
if (timelineManager.scrollCompensation.monthGroup === monthGroup) {
onScrollCompensation(timelineManager.scrollCompensation);
timelineManager.clearScrollCompensation();
}
});
</script>
{#each filterIntersecting(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)}

View File

@@ -28,7 +28,7 @@
};
</script>
<svelte:document use:shortcut={{ shortcut: { key: 't' }, onShortcut: handleTagAssets }} />
<svelte:document use:shortcut={{ shortcut: { key: 't' /* i18n-ignore */ }, onShortcut: handleTagAssets }} />
{#if menuItem}
<MenuOption {text} {icon} onClick={handleTagAssets} />

View File

@@ -55,7 +55,7 @@ export function calculateMonthGroupIntersecting(
}
/**
* Calculate intersection for viewer assets with additional parameters like header height
* Calculate intersection for viewer assets with additional parameters like header height and scroll compensation
*/
export function calculateViewerAssetIntersecting(
timelineManager: TimelineManager,
@@ -64,8 +64,12 @@ export function calculateViewerAssetIntersecting(
expandTop: number = INTERSECTION_EXPAND_TOP,
expandBottom: number = INTERSECTION_EXPAND_BOTTOM,
) {
const topWindow = timelineManager.visibleWindow.top - timelineManager.headerHeight - expandTop;
const bottomWindow = timelineManager.visibleWindow.bottom + timelineManager.headerHeight + expandBottom;
const scrollCompensationHeightDelta = timelineManager.scrollCompensation?.heightDelta ?? 0;
const topWindow =
timelineManager.visibleWindow.top - timelineManager.headerHeight - expandTop + scrollCompensationHeightDelta;
const bottomWindow =
timelineManager.visibleWindow.bottom + timelineManager.headerHeight + expandBottom + scrollCompensationHeightDelta;
const positionBottom = positionTop + positionHeight;

View File

@@ -36,6 +36,7 @@ export class MonthGroup {
#initialCount: number = 0;
#sortOrder: AssetOrder = AssetOrder.Desc;
percent: number = $state(0);
assetsCount: number = $derived(
this.isLoaded
@@ -241,31 +242,42 @@ export class MonthGroup {
if (this.#height === height) {
return;
}
let needsIntersectionUpdate = false;
const timelineManager = this.timelineManager;
const index = timelineManager.months.indexOf(this);
const { timelineManager: store, percent } = this;
const index = store.months.indexOf(this);
const heightDelta = height - this.#height;
this.#height = height;
const prevMonthGroup = timelineManager.months[index - 1];
const prevMonthGroup = store.months[index - 1];
if (prevMonthGroup) {
const newTop = prevMonthGroup.#top + prevMonthGroup.#height;
if (this.#top !== newTop) {
this.#top = newTop;
}
}
if (heightDelta === 0) {
return;
}
for (let cursor = index + 1; cursor < timelineManager.months.length; cursor++) {
for (let cursor = index + 1; cursor < store.months.length; cursor++) {
const monthGroup = this.timelineManager.months[cursor];
const newTop = monthGroup.#top + heightDelta;
if (monthGroup.#top !== newTop) {
monthGroup.#top = newTop;
needsIntersectionUpdate = true;
}
}
if (needsIntersectionUpdate) {
timelineManager.updateIntersections();
if (store.topIntersectingMonthGroup) {
const currentIndex = store.months.indexOf(store.topIntersectingMonthGroup);
if (currentIndex > 0) {
if (index < currentIndex) {
store.scrollCompensation = {
heightDelta,
scrollTop: undefined,
monthGroup: this,
};
} else if (percent > 0) {
const top = this.top + height * percent;
store.scrollCompensation = {
heightDelta: undefined,
scrollTop: top,
monthGroup: this,
};
}
}
}
}

View File

@@ -68,7 +68,7 @@ describe('TimelineManager', () => {
it('should load months in viewport', () => {
expect(sdkMock.getTimeBuckets).toBeCalledTimes(1);
expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(3);
expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(2);
});
it('calculates month height', () => {
@@ -82,13 +82,13 @@ describe('TimelineManager', () => {
expect.arrayContaining([
expect.objectContaining({ year: 2024, month: 3, height: 165.5 }),
expect.objectContaining({ year: 2024, month: 2, height: 11_996 }),
expect.objectContaining({ year: 2024, month: 1, height: 48 }),
expect.objectContaining({ year: 2024, month: 1, height: 286 }),
]),
);
});
it('calculates timeline height', () => {
expect(timelineManager.timelineHeight).toBe(12_209.5);
expect(timelineManager.timelineHeight).toBe(12_447.5);
});
});

View File

@@ -5,7 +5,7 @@ import { authManager } from '$lib/managers/auth-manager.svelte';
import { CancellableTask } from '$lib/utils/cancellable-task';
import { toTimelineAsset, type TimelineDateTime, type TimelineYearMonth } from '$lib/utils/timeline-util';
import { debounce, isEqual } from 'lodash-es';
import { clamp, debounce, isEqual } from 'lodash-es';
import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity';
import { updateIntersectionMonthGroup } from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
@@ -49,6 +49,8 @@ export class TimelineManager {
scrubberMonths: ScrubberMonth[] = $state([]);
scrubberTimelineHeight: number = $state(0);
topIntersectingMonthGroup: MonthGroup | undefined = $state();
visibleWindow = $derived.by(() => ({
top: this.#scrollTop,
bottom: this.#scrollTop + this.viewportHeight,
@@ -85,6 +87,15 @@ export class TimelineManager {
#suspendTransitions = $state(false);
#resetScrolling = debounce(() => (this.#scrolling = false), 1000);
#resetSuspendTransitions = debounce(() => (this.suspendTransitions = false), 1000);
scrollCompensation: {
heightDelta: number | undefined;
scrollTop: number | undefined;
monthGroup: MonthGroup | undefined;
} = $state({
heightDelta: 0,
scrollTop: 0,
monthGroup: undefined,
});
constructor() {}
@@ -230,12 +241,38 @@ export class TimelineManager {
}
}
clearScrollCompensation() {
this.scrollCompensation = {
heightDelta: undefined,
scrollTop: undefined,
monthGroup: undefined,
};
}
updateIntersections() {
if (!this.isInitialized || this.visibleWindow.bottom === this.visibleWindow.top) {
return;
}
let topIntersectingMonthGroup = undefined;
for (const month of this.months) {
updateIntersectionMonthGroup(this, month);
if (!topIntersectingMonthGroup && month.actuallyIntersecting) {
topIntersectingMonthGroup = month;
}
}
if (topIntersectingMonthGroup !== undefined && this.topIntersectingMonthGroup !== topIntersectingMonthGroup) {
this.topIntersectingMonthGroup = topIntersectingMonthGroup;
}
for (const month of this.months) {
if (month === this.topIntersectingMonthGroup) {
this.topIntersectingMonthGroup.percent = clamp(
(this.visibleWindow.top - this.topIntersectingMonthGroup.top) / this.topIntersectingMonthGroup.height,
0,
1,
);
} else {
month.percent = 0;
}
}
}
@@ -364,10 +401,10 @@ export class TimelineManager {
return;
}
const executionStatus = await monthGroup.loader?.execute(async (signal: AbortSignal) => {
const result = await monthGroup.loader?.execute(async (signal: AbortSignal) => {
await loadFromTimeBuckets(this, monthGroup, this.#options, signal);
}, cancelable);
if (executionStatus === 'LOADED') {
if (result === 'LOADED') {
updateIntersectionMonthGroup(this, monthGroup);
}
}

View File

@@ -3,7 +3,6 @@ import { init } from 'svelte-i18n';
beforeAll(async () => {
await init({ fallbackLocale: 'dev' });
Element.prototype.animate = vi.fn().mockImplementation(() => ({ cancel: () => {}, finished: Promise.resolve() }));
});
Object.defineProperty(globalThis, 'matchMedia', {
@@ -17,11 +16,3 @@ Object.defineProperty(globalThis, 'matchMedia', {
dispatchEvent: vi.fn(),
})),
});
vi.mock('$env/dynamic/public', () => {
return {
env: {
PUBLIC_IMMICH_HOSTNAME: '',
},
};
});

View File

@@ -10,8 +10,7 @@ process.env.PUBLIC_IMMICH_PAY_HOST = process.env.PUBLIC_IMMICH_PAY_HOST || 'http
/** @type {import('@sveltejs/kit').Config} */
const config = {
compilerOptions: {
// TODO pending `@immich/ui` to enable it
// runes: true,
runes: true,
},
preprocess: vitePreprocess(),
kit: {