Compare commits

..

20 Commits

Author SHA1 Message Date
renovate[bot]
c6c4c49773 chore(deps): update github-actions 2026-07-21 15:45:59 +00:00
Adam Gastineau
0a7c067b20 chore(mobile): bump maplibre to 0.26.2 in SwiftPM lock (#30108) 2026-07-21 10:44:30 -05:00
Luis Nachtigall
c8de56c422 fix(mobile): prevent duplicate album creation during submission (#30003)
* fix: prevent duplicate album creation during submission

* Nit fix

---------

Co-authored-by: Adam Gastineau <adam@agg.im>
2026-07-21 15:31:23 +00:00
shenlong
0e4572c37b chore: pin photo_manager to 3.9.0 (#30091)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-21 10:06:27 -05:00
Santo Shakil
14a2776453 fix(mobile): add album picker to the partner view bottom sheet (#30099) 2026-07-21 10:05:34 -05:00
renovate[bot]
59bc81423c chore(deps): update github-actions (major) (#30095) 2026-07-21 16:37:38 +02:00
Roberto Carlos Martinez Arriaga
e6fff3b15e feat(web): add maintenance link to command palette (#30016) 2026-07-21 16:27:23 +02:00
renovate[bot]
755df23b22 chore(deps): update ghcr.io/devcontainers/features/docker-in-docker docker tag to v4 (#30094) 2026-07-21 16:05:54 +02:00
Jason Rasmussen
899f547053 fix(web): lens model search (#30088) 2026-07-21 13:54:28 +00:00
Santo Shakil
4938fd4c89 fix(mobile): back up files moved into a watched folder on android (#29872) 2026-07-21 19:09:50 +05:30
Santo Shakil
e81a79224c fix(mobile): stop double close animation when dismissing an asset (#29413) 2026-07-21 19:08:45 +05:30
shenlong
7b0f58b2c1 refactor: gracefully stop during engine startup (#29179)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-21 19:08:13 +05:30
renovate[bot]
ae8398ffe4 chore(deps): update docker.io/valkey/valkey:9 docker digest to 8e8d64b (#30085) 2026-07-21 15:36:01 +02:00
renovate[bot]
aa08dad1f5 chore(deps): update dependency eslint-plugin-unicorn to v72 (#30092)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-21 15:33:46 +02:00
renovate[bot]
ce022233ae chore(deps): update base-image to v202607211135 (#30090) 2026-07-21 13:58:02 +02:00
Rust
1b4d41324e fix(server): file uploads for files with extension only filenames (#30024)
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-21 11:57:28 +00:00
renovate[bot]
8365cdd59e chore(deps): update dependency typescript to v7 (#29903)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-21 13:36:57 +02:00
Mees Frensel
1cec7021e8 fix(web): space between device type and version (#30102) 2026-07-21 11:08:29 +00:00
renovate[bot]
1b0f473457 fix(deps): update typescript-projects (#30087)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-21 11:01:23 +00:00
renovate[bot]
e918658cc1 chore(deps): update ghcr.io/jdx/mise docker tag to v2026.7.11 (#30086) 2026-07-21 12:24:32 +02:00
81 changed files with 1271 additions and 781 deletions

View File

@@ -83,7 +83,7 @@
}
},
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"ghcr.io/devcontainers/features/docker-in-docker:4": {
// https://github.com/devcontainers/features/issues/1466
"moby": false
}

View File

@@ -15,7 +15,7 @@ jobs:
outputs:
uses_template: ${{ steps.check.outputs.uses_template }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
sparse-checkout: .github/pull_request_template.md
sparse-checkout-cone-mode: false

View File

@@ -87,7 +87,7 @@ jobs:
permission-contents: read
permission-pull-requests: write
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ inputs.ref }}
persist-credentials: false
@@ -106,14 +106,14 @@ jobs:
working-directory: ./mobile
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
- uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
- uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: 'zulu'
java-version: '17'
- name: Restore Gradle Cache
id: cache-gradle-restore
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.gradle/caches
@@ -184,7 +184,7 @@ jobs:
- name: Save Gradle Cache
id: cache-gradle-save
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: github.ref == 'refs/heads/main'
with:
path: |
@@ -201,7 +201,7 @@ jobs:
contents: read
# Run on main branch or workflow_dispatch, or on PRs/other branches (build only, no upload)
if: ${{ !github.event.pull_request.head.repo.fork && fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
runs-on: macos-15
runs-on: macos-26
steps:
- id: token
@@ -215,7 +215,7 @@ jobs:
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ inputs.ref || github.sha }}
persist-credentials: false
@@ -241,7 +241,7 @@ jobs:
run: flutter build ios --config-only --no-codesign
- name: Setup Ruby
uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
uses: ruby/setup-ruby@003a5c4d8d6321bd302e38f6f0ec593f77f06600 # v1.319.0
with:
ruby-version: '3.3'
bundler-cache: true

View File

@@ -27,7 +27,7 @@ jobs:
permission-actions: write
- name: Check out code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}

View File

@@ -20,12 +20,12 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Check for breaking API changes
uses: oasdiff/oasdiff-action/breaking@291b6594ce617f22f49af5898ee038ed0b46e8fe # v0.1.5
uses: oasdiff/oasdiff-action/breaking@024f6c399f9a21ada1addb0f9a36ce1bfac995f1 # v0.1.6
with:
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
revision: open-api/immich-openapi-specs.json
@@ -38,7 +38,7 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

View File

@@ -38,7 +38,7 @@ jobs:
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
@@ -71,7 +71,7 @@ jobs:
permission-contents: read
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}

View File

@@ -39,7 +39,7 @@ jobs:
needs: [get_body, should_run]
if: ${{ needs.should_run.outputs.should_run == 'true' }}
container:
image: ghcr.io/immich-app/mdq:main@sha256:e3fe456f346b02c2f479462100763aee3ba8245768dfeac3c4262acb068e5c62
image: ghcr.io/immich-app/mdq:main@sha256:d603f5618c1fc9f2b4d506ffed54ba6bcfec83d56b893b5fa8b3f899a402fa0a
outputs:
checked: ${{ steps.get_checkbox.outputs.checked }}
steps:

View File

@@ -51,14 +51,14 @@ jobs:
permission-contents: read
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -71,7 +71,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@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
uses: github/codeql-action/autobuild@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
# 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
@@ -84,6 +84,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
with:
category: '/language:${{matrix.language}}'

View File

@@ -62,7 +62,7 @@ jobs:
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}

View File

@@ -137,7 +137,7 @@ jobs:
permission-pull-requests: write
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}

View File

@@ -25,7 +25,7 @@ jobs:
permission-pull-requests: write
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}

View File

@@ -14,7 +14,7 @@ jobs:
contents: read
steps:
- name: Checkout pubspec for versionCode
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.release.tag_name }}
persist-credentials: false

View File

@@ -23,7 +23,7 @@ jobs:
permission-pull-requests: write
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.pull_request.head.ref }}
persist-credentials: true

View File

@@ -61,7 +61,7 @@ jobs:
permission-contents: write
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
token: ${{ steps.token.outputs.token }}
persist-credentials: true
@@ -140,7 +140,7 @@ jobs:
permission-actions: read
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
token: ${{ steps.generate-token.outputs.token }}
persist-credentials: false
@@ -152,7 +152,7 @@ jobs:
github-token: ${{ steps.generate-token.outputs.token }}
- name: Create draft release
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
draft: true
prerelease: ${{ needs.bump_version.outputs.rc }}

View File

@@ -23,7 +23,7 @@ jobs:
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}

View File

@@ -58,7 +58,7 @@ jobs:
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}

View File

@@ -85,7 +85,7 @@ jobs:
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
@@ -114,7 +114,7 @@ jobs:
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
@@ -146,7 +146,7 @@ jobs:
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
@@ -178,7 +178,7 @@ jobs:
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
@@ -223,7 +223,7 @@ jobs:
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
@@ -262,7 +262,7 @@ jobs:
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
@@ -291,7 +291,7 @@ jobs:
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
@@ -342,7 +342,7 @@ jobs:
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
@@ -375,7 +375,7 @@ jobs:
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: 'recursive'
@@ -412,7 +412,7 @@ jobs:
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: 'recursive'
@@ -422,7 +422,7 @@ jobs:
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
@@ -490,7 +490,7 @@ jobs:
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: 'recursive'
@@ -500,7 +500,7 @@ jobs:
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
@@ -598,7 +598,7 @@ jobs:
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
@@ -637,7 +637,7 @@ jobs:
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
@@ -669,7 +669,7 @@ jobs:
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
@@ -699,7 +699,7 @@ jobs:
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
@@ -722,7 +722,7 @@ jobs:
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
@@ -781,7 +781,7 @@ jobs:
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}

View File

@@ -154,7 +154,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
healthcheck:
test: redis-cli ping || exit 1

View File

@@ -56,7 +56,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
healthcheck:
test: redis-cli ping || exit 1
restart: always

View File

@@ -61,7 +61,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
user: '1000:1000'
security_opt:
- no-new-privileges:true

View File

@@ -49,7 +49,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
healthcheck:
test: redis-cli ping || exit 1
restart: always

View File

@@ -28,4 +28,4 @@ run = "prettier --write ."
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
[tools]
wrangler = "4.110.0"
wrangler = "4.111.0"

View File

@@ -40,7 +40,8 @@
"@docusaurus/tsconfig": "^3.10.0",
"@docusaurus/types": "^3.10.0",
"prettier": "^3.7.4",
"typescript": "^6.0.0"
"@typescript/native": "npm:typescript@^7.0.2",
"typescript": "npm:@typescript/typescript6@^6.0.2"
},
"browserslist": {
"production": [

View File

@@ -45,7 +45,7 @@ services:
redis:
container_name: immich-e2e-redis
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
healthcheck:
test: redis-cli ping || exit 1

View File

@@ -49,6 +49,7 @@ export default typescriptEslint.config([
'unicorn/isolated-functions': 'off',
'unicorn/prefer-promise-with-resolvers': 'off',
'unicorn/no-declarations-before-early-exit': 'off',
'unicorn/prefer-simple-condition-first': 'off',
curly: 2,
'prettier/prettier': 0,
'unicorn/name-replacements': 'off',

View File

@@ -40,7 +40,7 @@
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^70.0.0",
"eslint-plugin-unicorn": "^72.0.0",
"exiftool-vendored": "^35.0.0",
"globals": "^17.0.0",
"luxon": "^3.4.4",
@@ -51,7 +51,8 @@
"sharp": "^0.34.5",
"socket.io-client": "^4.7.4",
"supertest": "^7.0.0",
"typescript": "^6.0.0",
"@typescript/native": "npm:typescript@^7.0.2",
"typescript": "npm:@typescript/typescript6@^6.0.2",
"typescript-eslint": "^8.28.0",
"utimes": "^5.2.1",
"vitest": "^4.0.0"

View File

@@ -38,7 +38,7 @@ test.describe('asset-viewer', () => {
assets.push(...timeBucket);
}
for (const yearMonth of timelineRestData.buckets.keys()) {
const [year, month] = yearMonth.split('-');
const [year, month] = yearMonth.split('-', 2);
yearMonths.push(`${year}-${Number(month)}`);
}
});

View File

@@ -49,7 +49,7 @@ test.describe('Timeline', () => {
assets.push(...timeBucket);
}
for (const yearMonth of timelineRestData.buckets.keys()) {
const [year, month] = yearMonth.split('-');
const [year, month] = yearMonth.split('-', 2);
yearMonths.push(`${year}-${Number(month)}`);
}
});

View File

@@ -7,7 +7,7 @@ export const sleep = (ms: number) => {
};
export const padYearMonth = (yearMonth: string) => {
const [year, month] = yearMonth.split('-');
const [year, month] = yearMonth.split('-', 2);
return `${year}-${month.padStart(2, '0')}`;
};

View File

@@ -287,43 +287,43 @@ url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536406"
[[tools.pnpm]]
version = "11.11.0"
version = "11.13.1"
backend = "aqua:pnpm/pnpm"
[tools.pnpm."platforms.linux-arm64"]
checksum = "sha256:4871093439f036b3082df6102255ac119f3b6f4f50c51c5711fc7c358b8b22d7"
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-arm64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734816"
checksum = "sha256:b52db99d215ed7dc9563aed815953c62a6c1ffd7cd75803d3a07ad7e4f246aed"
url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-arm64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563983"
provenance = "github-attestations"
[tools.pnpm."platforms.linux-arm64-musl"]
checksum = "sha256:f97316cee0deb3ae3fd64e8a09e1c8c88a76da33373a2b465bd6a4c3e85e0690"
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-arm64-musl.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734819"
checksum = "sha256:2cadd4fc815c591f498a0a84c9e74a836e3e8c1236275f6e1cfd355bae6ae957"
url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-arm64-musl.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563985"
provenance = "github-attestations"
[tools.pnpm."platforms.linux-x64"]
checksum = "sha256:df4699e897012ab14df2cc6eaa942910e830eb7fcaa420a2a1421a9461fd9108"
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-x64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734818"
checksum = "sha256:bd6d4b0e14536207ad76bc838f5980cecd968da15f69aae0b207380cca3f2e98"
url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-x64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563986"
provenance = "github-attestations"
[tools.pnpm."platforms.linux-x64-musl"]
checksum = "sha256:aad186f8a8ae72d827d5efe63e99e801456715fb7f2798949405fa33f20260db"
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-x64-musl.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734821"
checksum = "sha256:ba19690f4ed1b64f1203ade14e9216352b46232d5582468b26a0160e0c9618c5"
url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-x64-musl.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563980"
provenance = "github-attestations"
[tools.pnpm."platforms.macos-arm64"]
checksum = "sha256:ad46ad16c2376c2b78354ac488c1b166e076aa59bfcbcfd567d55957d755a690"
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-darwin-arm64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734814"
checksum = "sha256:765c2bf04e8129cb58c0f946e324262e418370b35a203b50b1f06a0567ef8bc1"
url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-darwin-arm64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563984"
provenance = "github-attestations"
[tools.pnpm."platforms.windows-x64"]
checksum = "sha256:13ad8a9b139c4c829fea33462cf04d061d72f0926b2b76a92ac03c9a36e405f9"
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-win32-x64.zip"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734820"
checksum = "sha256:d8bebbc71df2702961c1d34a5e61196bc0aa3bbde33c33253f6afa3dd4546a6d"
url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-win32-x64.zip"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563981"
provenance = "github-attestations"
[[tools.terragrunt]]

View File

@@ -16,7 +16,7 @@ config_roots = [
[tools]
node = "24.15.0"
pnpm = "11.11.0"
pnpm = "11.13.1"
terragrunt = "1.0.3"
opentofu = "1.11.6"
"npm:oazapfts" = "7.5.0"

View File

@@ -23,6 +23,6 @@ class ImmichApp : Application() {
// as the previous start might have been killed without unlocking.
if (BackgroundEngineLock.connectEngines > 0) return@postDelayed
BackgroundWorkerApiImpl.enqueueBackgroundWorker(this)
}, 5000)
}, 15000)
}
}

View File

@@ -15,6 +15,7 @@ import androidx.work.ListenableWorker
import androidx.work.WorkerParameters
import app.alextran.immich.MainActivity
import app.alextran.immich.R
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
import io.flutter.FlutterInjector
@@ -61,6 +62,11 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
}
override fun startWork(): ListenableFuture<Result> {
if (BackgroundWorkerPreferences(ctx).isLocked() && BackgroundEngineLock.connectEngines > 0) {
Log.i(TAG, "Foreground engine active, skipping background worker")
return Futures.immediateFuture(Result.success())
}
Log.i(TAG, "Starting background upload worker")
if (!loader.initialized()) {
@@ -77,6 +83,10 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
showNotification(notificationConfig.first, notificationConfig.second)
loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
if (isStopped || isComplete) {
return@ensureInitializationCompleteAsync
}
engine = FlutterEngine(ctx)
FlutterEngineCache.getInstance().put(BackgroundWorkerApiImpl.ENGINE_CACHE_KEY, engine!!)
@@ -143,11 +153,17 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
return
}
val api = flutterApi
if (api == null) {
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
complete(Result.failure())
}
return
}
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
if (flutterApi != null) {
flutterApi?.cancel {
complete(Result.failure())
}
api.cancel {
complete(Result.failure())
}
}

View File

@@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git",
"state" : {
"revision" : "60d9bb85c94ce6e7fc4406cd32529fd12bdb7809",
"version" : "6.14.0"
"revision" : "84a79bc375a301169390ac110c868f06c857b83f",
"version" : "6.27.0"
}
},
{

View File

@@ -210,17 +210,19 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
await _deleteAssets(deletes);
await _upsertAssets(updates);
// Drop every existing album link for each changed asset before re-adding the
// ones the native side reports. A moved asset only reports its new album here,
// so leaving the old link around makes the per-album delete sweep wipe the
// asset entirely (it is still linked to a bucket it no longer lives in).
await _db.batch((batch) async {
for (final assetId in assetAlbums.keys) {
batch.deleteWhere(_db.localAlbumAssetEntity, (f) => f.assetId.equals(assetId));
}
});
// The ugly casting below is required for now because the generated code
// casts the returned values from the platform during decoding them
// and iterating over them causes the type to be List<Object?> instead of
// List<String>
await _db.batch((batch) async {
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
for (final albumId in albumIds.cast<String?>().nonNulls) {
batch.deleteWhere(_db.localAlbumAssetEntity, (f) => f.albumId.equals(albumId) & f.assetId.equals(assetId));
}
});
});
await _db.batch((batch) async {
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
batch.insertAll(

View File

@@ -26,6 +26,7 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
FocusNode albumTitleTextFieldFocusNode = FocusNode();
FocusNode albumDescriptionTextFieldFocusNode = FocusNode();
bool isAlbumTitleTextFieldFocus = false;
bool isCreatingAlbum = false;
Set<BaseAsset> selectedAssets = {};
@override
@@ -48,7 +49,7 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
super.dispose();
}
bool get _canCreateAlbum => albumTitleController.text.trim().isNotEmpty;
bool get _canCreateAlbum => albumTitleController.text.trim().isNotEmpty && !isCreatingAlbum;
String _getEffectiveTitle() {
return albumTitleController.text.isNotEmpty
@@ -167,7 +168,12 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
}
Future<void> createAlbum() async {
if (isCreatingAlbum) {
return;
}
onBackgroundTapped();
setState(() => isCreatingAlbum = true);
final title = _getEffectiveTitle().trim();
@@ -187,6 +193,10 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
if (context.mounted) {
ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.t());
}
} finally {
if (mounted) {
setState(() => isCreatingAlbum = false);
}
}
}

View File

@@ -174,6 +174,12 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
return;
}
// The viewer is closing; don't flip the current asset now. Flipping it swaps
// the grid tile hero keys mid pop and animates the close on two tiles (#23779).
if (!mounted || !(ModalRoute.of(context)?.isActive ?? true)) {
return;
}
AssetViewer._setAsset(ref, asset);
_preloader.preload(index, context.sizeData);
_handleCasting();

View File

@@ -1,23 +1,76 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class PartnerDetailBottomSheet extends ConsumerWidget {
class PartnerDetailBottomSheet extends ConsumerStatefulWidget {
const PartnerDetailBottomSheet({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return const BaseBottomSheet(
ConsumerState<PartnerDetailBottomSheet> createState() => _PartnerDetailBottomSheetState();
}
class _PartnerDetailBottomSheetState extends ConsumerState<PartnerDetailBottomSheet> {
late final DraggableScrollableController sheetController;
@override
void initState() {
super.initState();
sheetController = DraggableScrollableController();
}
@override
void dispose() {
sheetController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
Future<void> addToAlbum(RemoteAlbum album) async {
final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album);
if (!context.mounted) {
return;
}
if (!result.success) {
ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error);
return;
}
ImmichToast.show(
context: context,
msg: result.count == 0
? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name})
: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
);
}
Future<void> onKeyboardExpand() {
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
}
return BaseBottomSheet(
controller: sheetController,
initialChildSize: 0.25,
maxChildSize: 0.4,
maxChildSize: 0.85,
shouldCloseOnMinExtent: false,
actions: [
actions: const [
ShareActionButton(source: ActionSource.timeline),
DownloadActionButton(source: ActionSource.timeline),
],
slivers: [
const AddToAlbumHeader(),
AlbumSelector(onAlbumSelected: addToAlbum, onKeyboardExpanded: onKeyboardExpand),
],
);
}
}

View File

@@ -290,10 +290,13 @@ class BackgroundUploadService {
return null;
}
final fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
// Some apps (e.g. DJI/Fusion) return names without an extension; fall back to the asset name for those.
final extension = p.extension(file.path).isNotEmpty ? p.extension(file.path) : p.extension(asset.name);
final originalFileName = p.setExtension(fileName, extension);
String fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
final hasExtension = p.extension(fileName).isNotEmpty;
if (!hasExtension) {
fileName = p.setExtension(fileName, p.extension(asset.name));
}
final originalFileName = entity.isLivePhoto ? p.setExtension(fileName, p.extension(file.path)) : fileName;
String metadata = UploadTaskMetadata(
localAssetId: asset.id,

View File

@@ -309,10 +309,17 @@ class ForegroundUploadService {
return;
}
final fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
// Some apps (e.g. DJI/Fusion) return names without an extension; fall back to the asset name for those.
final extension = p.extension(file.path).isNotEmpty ? p.extension(file.path) : p.extension(asset.name);
final originalFileName = p.setExtension(fileName, extension);
String fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
/// Handle special file name from DJI or Fusion app
/// If the file name has no extension, likely due to special renaming template by specific apps
/// we append the original extension from the asset name
final hasExtension = p.extension(fileName).isNotEmpty;
if (!hasExtension) {
fileName = p.setExtension(fileName, p.extension(asset.name));
}
final originalFileName = entity.isLivePhoto ? p.setExtension(fileName, p.extension(file.path)) : fileName;
final deviceId = Store.get(StoreKey.deviceId);
final fields = {

View File

@@ -58,7 +58,7 @@ dependencies:
path_provider: ^2.1.5
path_provider_foundation: ^2.6.0
permission_handler: ^11.4.0
photo_manager: ^3.9.0
photo_manager: 3.9.0
pinput: ^5.0.2
punycode: ^1.0.0
scroll_date_picker: ^3.8.0

View File

@@ -2,6 +2,7 @@ import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
@@ -35,4 +36,75 @@ void main() {
expect(albums[3].id, '2'); // excluded
});
});
group('processDelta', () {
// Regression for #22844: an asset moved (not copied) into a backup album on
// Android was dropped. The delta reports only the asset's new album, and the
// stale link to its old album made the per-album delete sweep wipe the asset.
test('keeps an asset moved to another album that still holds other assets', () async {
final localAlbumRepo = mediumFactory.getRepository<DriftLocalAlbumRepository>();
final moved = _localAsset('moved');
final other = _localAsset('other');
await localAlbumRepo.upsert(
mediumFactory.localAlbum(id: 'src', backupSelection: BackupSelection.none),
toUpsert: [moved, other],
);
final anchor = _localAsset('anchor');
await localAlbumRepo.upsert(
mediumFactory.localAlbum(id: 'dst', backupSelection: BackupSelection.selected),
toUpsert: [anchor],
);
// Delta reports the moved asset now living only in the destination album.
await localAlbumRepo.processDelta(
updates: [moved],
deletes: [],
assetAlbums: {
'moved': ['dst'],
},
);
// Per-album delete sweep, as sync() runs on Android. src now physically
// holds only `other`; dst holds `anchor` and the moved asset.
await localAlbumRepo.syncDeletes('src', ['other']);
await localAlbumRepo.syncDeletes('dst', ['anchor', 'moved']);
final dstIds = (await localAlbumRepo.getAssets('dst')).map((a) => a.id).toSet();
final srcIds = await localAlbumRepo.getAssetIds('src');
expect(dstIds, contains('moved')); // survived and linked to the backed-up album
expect(srcIds, isNot(contains('moved'))); // stale source link cleared
expect(srcIds, contains('other')); // untouched asset stays put
});
test('replaces album membership with exactly what the delta reports', () async {
final localAlbumRepo = mediumFactory.getRepository<DriftLocalAlbumRepository>();
final moved = _localAsset('moved');
await localAlbumRepo.upsert(mediumFactory.localAlbum(id: 'src'), toUpsert: [moved]);
await localAlbumRepo.upsert(mediumFactory.localAlbum(id: 'dst'));
await localAlbumRepo.processDelta(
updates: [moved],
deletes: [],
assetAlbums: {
'moved': ['dst'],
},
);
expect(await localAlbumRepo.getAssetIds('src'), isEmpty);
expect(await localAlbumRepo.getAssetIds('dst'), ['moved']);
});
});
}
LocalAsset _localAsset(String id) => LocalAsset(
id: id,
name: '$id.jpg',
type: AssetType.image,
createdAt: DateTime(2024),
updatedAt: DateTime(2024),
playbackStyle: AssetPlaybackStyle.image,
isEdited: false,
);

View File

@@ -136,51 +136,6 @@ void main() {
expect(task, isNotNull);
expect(task!.fields.containsKey('visibility'), isFalse);
});
test('corrects the extension when iOS returns a rendered file for a .dng asset', () async {
final asset = LocalAssetStub.image1;
final mockEntity = MockAssetEntity();
final mockFile = File('/path/to/IMG_6499.jpg');
when(() => mockEntity.isLivePhoto).thenReturn(false);
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile);
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'IMG_6499.dng');
final task = await sut.getUploadTask(asset);
expect(task, isNotNull);
expect(task!.fields['filename'], equals('IMG_6499.jpg'));
});
test('keeps the .dng extension for a genuine RAW original', () async {
final asset = LocalAssetStub.image1;
final mockEntity = MockAssetEntity();
final mockFile = File('/path/to/IMG_5210.dng');
when(() => mockEntity.isLivePhoto).thenReturn(false);
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile);
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'IMG_5210.dng');
final task = await sut.getUploadTask(asset);
expect(task, isNotNull);
expect(task!.fields['filename'], equals('IMG_5210.dng'));
});
test('borrows the extension from the asset name for an extensionless name (DJI/Fusion)', () async {
final asset = LocalAssetStub.image1;
final mockEntity = MockAssetEntity();
final mockFile = File('/path/to/DJI_0001');
when(() => mockEntity.isLivePhoto).thenReturn(false);
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile);
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'DJI_0001');
final task = await sut.getUploadTask(asset);
expect(task, isNotNull);
expect(task!.fields['filename'], equals('DJI_0001.jpg'));
});
});
group('getLivePhotoUploadTask', () {

View File

@@ -81,24 +81,6 @@ void main() {
return captured;
}
List<String> captureOriginalFileNames() {
final captured = <String>[];
when(
() => mockUploadRepository.uploadFile(
file: any(named: 'file'),
originalFileName: any(named: 'originalFileName'),
fields: any(named: 'fields'),
cancelToken: any(named: 'cancelToken'),
onProgress: any(named: 'onProgress'),
logContext: any(named: 'logContext'),
),
).thenAnswer((invocation) async {
captured.add(invocation.namedArguments[#originalFileName] as String);
return UploadResult.success(remoteAssetId: 'remote-${captured.length}');
});
return captured;
}
group('uploadSingleAsset', () {
test('should upload the motion part hidden and keep the still image visible', () async {
final asset = LocalAssetStub.image1;
@@ -142,59 +124,5 @@ void main() {
expect(captured, hasLength(1));
expect(captured[0].containsKey('visibility'), isFalse);
});
test('corrects the extension when iOS returns a rendered file for a .dng asset', () async {
final asset = LocalAssetStub.image1;
final mockEntity = MockAssetEntity();
final stillFile = File('/path/to/IMG_6499.jpg');
when(() => mockEntity.isLivePhoto).thenReturn(false);
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
when(() => mockStorageRepository.isAssetAvailableLocally(asset.id)).thenAnswer((_) async => true);
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => stillFile);
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'IMG_6499.dng');
final names = captureOriginalFileNames();
await sut.uploadSingleAsset(asset, null, callbacks: const UploadCallbacks());
expect(names, equals(['IMG_6499.jpg']));
});
test('keeps the .dng extension for a genuine RAW original', () async {
final asset = LocalAssetStub.image1;
final mockEntity = MockAssetEntity();
final stillFile = File('/path/to/IMG_5210.dng');
when(() => mockEntity.isLivePhoto).thenReturn(false);
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
when(() => mockStorageRepository.isAssetAvailableLocally(asset.id)).thenAnswer((_) async => true);
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => stillFile);
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'IMG_5210.dng');
final names = captureOriginalFileNames();
await sut.uploadSingleAsset(asset, null, callbacks: const UploadCallbacks());
expect(names, equals(['IMG_5210.dng']));
});
test('borrows the extension from the asset name for an extensionless name (DJI/Fusion)', () async {
final asset = LocalAssetStub.image1;
final mockEntity = MockAssetEntity();
final stillFile = File('/path/to/DJI_0001');
when(() => mockEntity.isLivePhoto).thenReturn(false);
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
when(() => mockStorageRepository.isAssetAvailableLocally(asset.id)).thenAnswer((_) async => true);
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => stillFile);
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'DJI_0001');
final names = captureOriginalFileNames();
await sut.uploadSingleAsset(asset, null, callbacks: const UploadCallbacks());
expect(names, equals(['DJI_0001.jpg']));
});
});
}

View File

@@ -8,7 +8,7 @@
"format": "prettier --cache --check i18n/",
"format:fix": "prettier --cache --write --list-different i18n"
},
"packageManager": "pnpm@11.11.0",
"packageManager": "pnpm@11.13.1",
"engines": {
"pnpm": ">=10.0.0"
},

View File

@@ -46,6 +46,7 @@ export default typescriptEslint.config([
'unicorn/no-process-exit': 'off',
'unicorn/import-style': 'off',
'unicorn/consistent-class-member-order': 'off',
'unicorn/prefer-simple-condition-first': 'off',
curly: 2,
// prefer the typescript-eslint type-aware version
'unicorn/require-array-sort-compare': 'off',

View File

@@ -33,12 +33,13 @@
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^70.0.0",
"eslint-plugin-unicorn": "^72.0.0",
"globals": "^17.0.0",
"mock-fs": "^5.2.0",
"prettier": "^3.7.4",
"prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^6.0.0",
"@typescript/native": "npm:typescript@^7.0.2",
"typescript": "npm:@typescript/typescript6@^6.0.2",
"typescript-eslint": "^8.58.0",
"vite": "^8.0.0",
"vitest": "^4.0.0",

View File

@@ -13,5 +13,5 @@
"oidc-provider": "^9.0.0",
"tsx": "^4.20.6"
},
"packageManager": "pnpm@11.11.0"
"packageManager": "pnpm@11.13.1"
}

View File

@@ -16,6 +16,6 @@
"@immich/sdk": "workspace:*",
"@immich/plugin-sdk": "workspace:*",
"esbuild": "^0.28.0",
"typescript": "^6.0.0"
"typescript": "^7.0.0"
}
}

View File

@@ -27,14 +27,14 @@
"keywords": [],
"author": "",
"license": "GNU Affero General Public License version 3",
"packageManager": "pnpm@11.11.0",
"packageManager": "pnpm@11.13.1",
"devDependencies": {
"@extism/js-pdk": "^1.1.1",
"@immich/sdk": "workspace:*",
"@types/node": "^24.13.3",
"esbuild": "^0.28.0",
"tsc-alias": "^1.8.16",
"typescript": "^6.0.0"
"typescript": "^7.0.0"
},
"peerDependencies": {
"@extism/js-pdk": "^1.1.1"

View File

@@ -32,5 +32,5 @@
"vite": "^8.0.16",
"vitest": "^4.1.8"
},
"packageManager": "pnpm@11.11.0"
"packageManager": "pnpm@11.13.1"
}

View File

@@ -25,6 +25,6 @@
},
"devDependencies": {
"@types/node": "^24.13.3",
"typescript": "^6.0.0"
"typescript": "^7.0.0"
}
}

1409
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
FROM ghcr.io/immich-app/base-server-dev:202607132058@sha256:968709a3668167f11ef8ece5962e72ea8a4231e841f98bd5e0f35da4cc295cc7 AS builder
FROM ghcr.io/immich-app/base-server-dev:202607211135@sha256:83c9ff3f7390111596a2dcd24a746c3f8618d58259dbc7aae36618d153462f18 AS builder
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
CI=1 \
COREPACK_HOME=/tmp \
@@ -56,7 +56,7 @@ FROM builder AS plugins
ARG TARGETPLATFORM
COPY --from=ghcr.io/jdx/mise:2026.7.7@sha256:ead960a5f0d2834a78e4640b8e1579d0fcf08cc52a16810dba33b06abf13282e /usr/local/bin/mise /usr/local/bin/mise
COPY --from=ghcr.io/jdx/mise:2026.7.11@sha256:6599c81b0da6206cbf5151df97a36794d70aaec4574ceb92d13783b42080b055 /usr/local/bin/mise /usr/local/bin/mise
WORKDIR /app
COPY ./mise.toml ./mise.toml
@@ -80,7 +80,7 @@ RUN --mount=type=cache,id=pnpm-packages,target=/buildcache/pnpm-store \
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
mise //:plugins
FROM ghcr.io/immich-app/base-server-prod:202607132058@sha256:85cca651310842265015f26a6710bb7420a54a8fd1ec22d519b7b620f5f93bb1
FROM ghcr.io/immich-app/base-server-prod:202607211135@sha256:ced131da7523544fe975cfd25abd67386e712e39496340b437cd95a08d0a18f3
WORKDIR /usr/src/app
ENV NODE_ENV=production \

View File

@@ -1,8 +1,8 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:202607132058@sha256:968709a3668167f11ef8ece5962e72ea8a4231e841f98bd5e0f35da4cc295cc7 AS dev
FROM ghcr.io/immich-app/base-server-dev:202607211135@sha256:83c9ff3f7390111596a2dcd24a746c3f8618d58259dbc7aae36618d153462f18 AS dev
COPY --from=ghcr.io/jdx/mise:2026.7.7@sha256:ead960a5f0d2834a78e4640b8e1579d0fcf08cc52a16810dba33b06abf13282e /usr/local/bin/mise /usr/local/bin/mise
COPY --from=ghcr.io/jdx/mise:2026.7.11@sha256:6599c81b0da6206cbf5151df97a36794d70aaec4574ceb92d13783b42080b055 /usr/local/bin/mise /usr/local/bin/mise
RUN echo "devdir=/buildcache/node-gyp" >> /usr/local/etc/npmrc && \
echo "store-dir=/buildcache/pnpm-store" >> /usr/local/etc/npmrc && \

View File

@@ -64,6 +64,7 @@ export default typescriptEslint.config([
'unicorn/consistent-boolean-name': 'off',
'unicorn/no-computed-property-existence-check': 'off',
'unicorn/no-non-function-verb-prefix': 'off',
'unicorn/prefer-simple-condition-first': 'off',
// prefer the typescript-eslint type-aware version
'unicorn/require-array-sort-compare': 'off',
'@typescript-eslint/require-array-sort-compare': 'error',

View File

@@ -151,7 +151,7 @@
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^70.0.0",
"eslint-plugin-unicorn": "^72.0.0",
"globals": "^17.0.0",
"mock-fs": "^5.2.0",
"pngjs": "^7.0.0",
@@ -161,7 +161,8 @@
"supertest": "^7.1.0",
"tailwindcss": "^3.4.0",
"testcontainers": "^12.0.0",
"typescript": "^6.0.0",
"@typescript/native": "npm:typescript@^7.0.2",
"typescript": "npm:@typescript/typescript6@^6.0.2",
"typescript-eslint": "^8.28.0",
"unplugin-swc": "^1.4.5",
"vite-tsconfig-paths": "^6.0.0",

View File

@@ -128,7 +128,7 @@ export class AssetMediaController {
this.logger.deprecate(
'Calling the thumbnail endpoint with size=original is deprecated. Use the :id/original endpoint instead',
);
const [_, reqSearch] = req.url.split('?');
const [_, reqSearch] = req.url.split('?', 2);
const redirSearchParams = new URLSearchParams(reqSearch);
redirSearchParams.delete('size');
return res.redirect('original?' + redirSearchParams.toString());
@@ -142,7 +142,7 @@ export class AssetMediaController {
// viewThumbnailRes is a AssetMediaRedirectResponse
// which redirects to the original asset or a specific size to make better use of caching
const { targetSize } = viewThumbnailRes;
const [reqPath, reqSearch] = req.url.split('?');
const [reqPath, reqSearch] = req.url.split('?', 2);
let redirPath: string;
const redirSearchParams = new URLSearchParams(reqSearch);
if (targetSize === 'original') {

View File

@@ -313,7 +313,7 @@ export class MediaRepository {
if (!line) {
return;
}
const [ptsStr, durationStr, flags] = line.split(',');
const [ptsStr, durationStr, flags] = line.split(',', 3);
const pts = Number.parseInt(ptsStr);
const duration = Number.parseInt(durationStr);
if (Number.isNaN(pts) || Number.isNaN(duration) || !flags) {

View File

@@ -269,6 +269,10 @@ describe(AssetMediaService.name, () => {
'random-uuid.jpg',
);
});
it('should accept filenames with just an extension', () => {
expect(sut.getUploadFilename(uploadFile.filename(UploadFieldName.ASSET_DATA, '.jpg'))).toEqual('random-uuid.jpg');
});
});
describe('getUploadFolder', () => {

View File

@@ -1,5 +1,4 @@
import { BadRequestException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { extname } from 'node:path';
import sanitize from 'sanitize-filename';
import { StorageCore } from 'src/cores/storage.core';
import { AuthSharedLink } from 'src/database';
@@ -92,8 +91,7 @@ export class AssetMediaService extends BaseService {
getUploadFilename({ auth, fieldName, file, body }: UploadRequest): string {
requireUploadAccess(auth);
const extension = extname(body.filename || file.originalName);
const extension = getFilenameExtension(body.filename || file.originalName);
const lookup = {
[UploadFieldName.ASSET_DATA]: extension,
[UploadFieldName.SIDECAR_DATA]: '.xmp',

View File

@@ -461,7 +461,7 @@ export class AuthService extends BaseService {
}
private getBearerToken(headers: IncomingHttpHeaders): string | null {
const [type, token] = (headers.authorization || '').split(' ');
const [type, token] = (headers.authorization || '').split(' ', 2);
if (type.toLowerCase() === 'bearer') {
return token;
}

View File

@@ -214,7 +214,6 @@ export class DatabaseBackupService {
bin: `/usr/lib/postgresql/${databaseMajorVersion}/bin/${bin}`,
args,
databaseUsername,
// eslint-disable-next-line unicorn/prefer-minimal-ternary
databasePassword: isUrlConnection ? new URL(databaseConfig.url).password : databaseConfig.password,
databaseVersion,
databaseMajorVersion,

View File

@@ -15,6 +15,7 @@ import { makeStream, newTestService, ServiceMocks } from 'test/utils';
import { vitest } from 'vitest';
async function* mockWalk() {
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject
yield await Promise.resolve(['/data/user1/photo.jpg']);
}

View File

@@ -20,7 +20,7 @@ import { ArgOf } from 'src/repositories/event.repository';
import { BaseService } from 'src/services/base.service';
import { JobOf, StorageAsset } from 'src/types';
import { getAssetFile } from 'src/utils/asset.util';
import { getLivePhotoMotionFilename } from 'src/utils/file';
import { getFilenameExtension, getLivePhotoMotionFilename } from 'src/utils/file';
const storageTokens = {
secondOptions: ['s', 'ss', 'SSS'],
@@ -267,10 +267,10 @@ export class StorageTemplateService extends BaseService {
const { storageLabel, filename } = metadata;
try {
const filenameWithoutExtension = path.basename(filename, path.extname(filename));
const filenameWithoutExtension = path.basename(filename, getFilenameExtension(filename));
const source = asset.originalPath;
let extension = path.extname(source).split('.').pop() as string;
let extension = getFilenameExtension(source).split('.').pop() as string;
const sanitized = sanitize(path.basename(filenameWithoutExtension, `.${extension}`));
extension = extension?.toLowerCase();
const rootPath = StorageCore.getLibraryFolder({ id: asset.ownerId, storageLabel });

View File

@@ -51,7 +51,7 @@ export class SystemConfigService extends BaseService {
@OnEvent({ name: 'ConfigValidate' })
onConfigValidate({ newConfig, oldConfig }: ArgOf<'ConfigValidate'>) {
const { logLevel } = this.configRepository.getEnv();
if (!_.isEqual(toPlainObject(newConfig.logging), oldConfig.logging) && logLevel) {
if (logLevel && !_.isEqual(toPlainObject(newConfig.logging), oldConfig.logging)) {
throw new Error('Logging cannot be changed while the environment variable IMMICH_LOG_LEVEL is set.');
}
}

View File

@@ -9,15 +9,19 @@ import { ImmichReadStream } from 'src/repositories/storage.repository';
import { isConnectionAborted } from 'src/utils/misc';
export function getFileNameWithoutExtension(path: string): string {
return basename(path, extname(path));
return basename(path, getFilenameExtension(path));
}
export function getFilenameExtension(path: string): string {
return extname(path);
export function getFilenameExtension(path: string) {
const extension = extname(path);
if (!extension && path.startsWith('.') && !path.includes('.', 1)) {
return path;
}
return extension;
}
export function getLivePhotoMotionFilename(stillName: string, motionName: string) {
return getFileNameWithoutExtension(stillName) + extname(motionName);
return getFileNameWithoutExtension(stillName) + getFilenameExtension(motionName);
}
export class ImmichFileResponse {

View File

@@ -1,5 +1,5 @@
import { extname } from 'node:path';
import { AssetType } from 'src/enum';
import { getFilenameExtension } from 'src/utils/file';
const raw = {
'.3fr': ['image/3fr', 'image/x-hasselblad-3fr'],
@@ -132,10 +132,12 @@ const sidecar: Record<string, string[]> = {
const types = { ...image, ...video, ...sidecar };
const isType = (filename: string, record: Record<string, string[]>) =>
Object.hasOwn(record, extname(filename).toLowerCase());
const isType = (filename: string, r: Record<string, string[]>) =>
Object.hasOwn(r, getFilenameExtension(filename).toLowerCase());
const lookup = (filename: string) =>
types[getFilenameExtension(filename).toLowerCase()]?.[0] ?? 'application/octet-stream';
const lookup = (filename: string) => types[extname(filename).toLowerCase()]?.[0] ?? 'application/octet-stream';
const toExtension = (mimeType: string) => {
return (
extensionOverrides[mimeType] || Object.entries(types).find(([, mimeTypes]) => mimeTypes.includes(mimeType))?.[0]
@@ -158,7 +160,8 @@ export const mimeTypes = {
isProfile: (filename: string) => isType(filename, profile),
isSidecar: (filename: string) => isType(filename, sidecar),
isVideo: (filename: string) => isType(filename, video),
canBeTransparent: (filename: string) => transparentCapableExtensions.has(extname(filename).toLowerCase()),
canBeTransparent: (filename: string) =>
transparentCapableExtensions.has(getFilenameExtension(filename).toLowerCase()),
isRaw: (filename: string) => isType(filename, raw),
lookup,
/** return an extension (including a leading `.`) for a mime-type */
@@ -169,7 +172,7 @@ export const mimeTypes = {
return AssetType.Image;
}
if (contentType.startsWith('video/') || contentType === 'application/mxf') {
if (contentType === 'application/mxf' || contentType.startsWith('video/')) {
return AssetType.Video;
}

View File

@@ -9,7 +9,7 @@ type Impossible<K extends keyof any> = {
type Exact<T, U extends T = T> = U & Impossible<Exclude<keyof U, keyof T>>;
export const fromAck = (ack: string): SyncAck => {
const [type, updateId, extraId] = ack.split('|');
const [type, updateId, extraId] = ack.split('|', 3);
return { type: type as SyncEntityType, updateId, extraId };
};

View File

@@ -146,6 +146,7 @@ export default typescriptEslint.config(
'unicorn/no-non-function-verb-prefix': 'off',
'unicorn/prefer-minimal-ternary': 'off',
'unicorn/no-empty-file': 'off',
'unicorn/prefer-simple-condition-first': 'off',
// prefer the typescript-eslint type-aware version
'unicorn/require-array-sort-compare': 'off',
'@typescript-eslint/require-array-sort-compare': 'error',

View File

@@ -97,7 +97,7 @@
"eslint-plugin-better-tailwindcss": "^4.5.0",
"eslint-plugin-compat": "^7.0.0",
"eslint-plugin-svelte": "^3.12.4",
"eslint-plugin-unicorn": "^70.0.0",
"eslint-plugin-unicorn": "^72.0.0",
"factory.ts": "^1.4.1",
"globals": "^17.0.0",
"happy-dom": "^20.0.0",
@@ -105,11 +105,12 @@
"prettier-plugin-sort-json": "^4.1.1",
"prettier-plugin-svelte": "^4.0.0",
"rollup-plugin-visualizer": "^7.0.0",
"svelte": "5.56.4",
"svelte": "5.56.5",
"svelte-check": "^4.4.6",
"svelte-eslint-parser": "^1.3.3",
"tailwindcss": "^4.2.4",
"typescript": "^6.0.0",
"@typescript/native": "npm:typescript@^7.0.2",
"typescript": "npm:@typescript/typescript6@^6.0.2",
"typescript-eslint": "^8.45.0",
"vite": "^8.0.0",
"vitest": "^4.0.0"

View File

@@ -25,6 +25,7 @@ import {
mdiThemeLightDark,
mdiToolboxOutline,
mdiTrashCanOutline,
mdiWrench,
} from '@mdi/js';
import type { MessageFormatter } from 'svelte-i18n';
import { goto } from '$app/navigation';
@@ -66,6 +67,12 @@ export const getPagesProvider = ($t: MessageFormatter) => {
icon: mdiServer,
onAction: () => goto(Route.systemStatistics()),
},
{
title: $t('admin.maintenance_settings'),
description: $t('admin.maintenance_settings_description'),
icon: mdiWrench,
onAction: () => goto(Route.systemMaintenance()),
},
].map((route) => ({ ...route, $if: () => authManager.authenticated && authManager.user.isAdmin }));
const userPages: ActionItem[] = [

View File

@@ -17,7 +17,7 @@
continue;
}
const [group] = permission.split('.');
const [group] = permission.split('.', 1);
if (!Object.hasOwn(permissions, group)) {
permissions[group] = [];
}

View File

@@ -50,7 +50,7 @@
}
if (isFirefox && ref) {
ref.decode().then(completeLoad, completeLoad);
ref.decode().then(completeLoad).catch(completeLoad);
return;
}

View File

@@ -35,7 +35,7 @@
}
if (isRotated) {
let [width, height] = ratio.value.split(':');
let [width, height] = ratio.value.split(':', 2);
return `${height}:${width}`;
}
return ratio.value;

View File

@@ -58,7 +58,7 @@
{#if session.deviceType || session.deviceOS}
<span
>{session.deviceOS || $t('unknown')}{session.deviceType || $t('unknown')}{session.appVersion
? `(v${session.appVersion})`
? ` (v${session.appVersion})`
: ''}</span
>
{:else}

View File

@@ -5,7 +5,7 @@ import { describe } from 'vitest';
import FormatMessage from '$lib/elements/FormatMessage.svelte';
import FormatTagB from '$lib/elements/__test__/FormatTagB.svelte';
const getSanitizedHTML = (container: HTMLElement) => container.innerHTML.replaceAll('<!---->', '');
const getSanitizedHTML = (container: HTMLElement) => container.getHTML().replaceAll('<!---->', '');
describe('FormatMessage component', () => {
beforeAll(async () => {

View File

@@ -124,7 +124,7 @@ class AuthManager {
}
for (const cookie of document.cookie.split('; ')) {
const [name] = cookie.split('=');
const [name] = cookie.split('=', 1);
if (name === 'immich_is_authenticated') {
return true;
}

View File

@@ -834,7 +834,7 @@ class TransformManager implements EditToolManager {
return;
}
const [widthRatio, heightRatio] = aspectRatio.split(':');
const [widthRatio, heightRatio] = aspectRatio.split(':', 2);
this.setAspectRatio(`${heightRatio}:${widthRatio}`);
}

View File

@@ -140,7 +140,7 @@
city: emptyStringToNull(filter.location.city),
make: emptyStringToNull(filter.camera.make),
model: emptyStringToNull(filter.camera.model),
lensModel: filter.camera.lensModel,
lensModel: emptyStringToNull(filter.camera.lensModel),
takenAfter: filter.date.takenAfter
? asLocalTimeISO(filter.date.takenAfter.startOf('day') as DateTime<true>)
: undefined,

View File

@@ -281,8 +281,9 @@
};
afterNavigate(({ from, to }) => {
memoryManager.ready().then(
() => {
memoryManager
.ready()
.then(() => {
let target;
if (to?.params?.assetId) {
target = to;
@@ -294,11 +295,10 @@
init(target);
initPlayer();
},
(error) => {
})
.catch((error) => {
console.error(`Error loading memories: ${error}`);
},
);
});
});
$effect(() => {