diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index c214ba564e..9495d03bb9 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -96,7 +96,7 @@ jobs: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: ref: ${{ inputs.ref || github.sha }} persist-credentials: false @@ -194,7 +194,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: ref: ${{ inputs.ref || github.sha }} persist-credentials: false diff --git a/.github/workflows/cache-cleanup.yml b/.github/workflows/cache-cleanup.yml index 7d49d94791..a75770ec49 100644 --- a/.github/workflows/cache-cleanup.yml +++ b/.github/workflows/cache-cleanup.yml @@ -25,7 +25,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Check out code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index fc2c9f6853..b9dbb40d41 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -35,7 +35,7 @@ jobs: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -78,7 +78,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 34228843ad..e75d6d2e90 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -50,14 +50,14 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 + uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -70,7 +70,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 + uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 # â„šī¸ 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 @@ -83,6 +83,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 + uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 823aa98fc8..24cb804e77 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -60,7 +60,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 5d01646fef..3a0e918812 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -125,7 +125,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} diff --git a/.github/workflows/docs-destroy.yml b/.github/workflows/docs-destroy.yml index 3ad3f3558e..643c35b1af 100644 --- a/.github/workflows/docs-destroy.yml +++ b/.github/workflows/docs-destroy.yml @@ -23,7 +23,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} diff --git a/.github/workflows/fix-format.yml b/.github/workflows/fix-format.yml index f7f34b929c..fd497a9fb8 100644 --- a/.github/workflows/fix-format.yml +++ b/.github/workflows/fix-format.yml @@ -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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: ref: ${{ github.event.pull_request.head.ref }} token: ${{ steps.generate-token.outputs.token }} diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 6e7ee8f608..45c0e759c4 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -55,14 +55,14 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: token: ${{ steps.generate-token.outputs.token }} persist-credentials: true ref: main - name: Install uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 + uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 @@ -132,7 +132,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: token: ${{ steps.generate-token.outputs.token }} persist-credentials: false @@ -144,7 +144,7 @@ jobs: github-token: ${{ steps.generate-token.outputs.token }} - name: Create draft release - uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 + uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 with: draft: true tag_name: ${{ env.IMMICH_VERSION }} diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 524f6bc77c..a7dc479b26 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -23,14 +23,14 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: token: ${{ steps.generate-token.outputs.token }} persist-credentials: true ref: main - name: Install uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 + uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bb4ea9e245..69a6a9e33b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,7 +58,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: token: ${{ steps.generate-token.outputs.token }} persist-credentials: false @@ -80,11 +80,12 @@ jobs: github-token: ${{ steps.generate-token.outputs.token }} - name: Create draft release - uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 + uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 with: tag_name: ${{ steps.version.outputs.result }} token: ${{ steps.generate-token.outputs.token }} body_path: ${{ steps.changelog.outputs.path }} + draft: true files: | docker/docker-compose.yml docker/example.env diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index 12bdbc55bf..dc8cc34cb2 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -22,7 +22,7 @@ jobs: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 99ee773af4..2b72ceb40a 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -55,7 +55,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 40ffc48de4..cf3255ca4b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,7 +69,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -114,7 +114,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -161,7 +161,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -203,7 +203,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -247,7 +247,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -285,7 +285,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -333,7 +333,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -379,7 +379,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false submodules: 'recursive' @@ -418,7 +418,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false submodules: 'recursive' @@ -473,7 +473,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false submodules: 'recursive' @@ -534,7 +534,7 @@ jobs: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -566,12 +566,12 @@ jobs: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Install uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 + uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 # TODO: add caching when supported (https://github.com/actions/setup-python/pull/818) # with: @@ -610,7 +610,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -639,7 +639,7 @@ jobs: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -661,7 +661,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -723,7 +723,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false token: ${{ steps.token.outputs.token }} diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml index 1f0a7608d1..e37497b9bb 100644 --- a/.github/workflows/weblate-lock.yml +++ b/.github/workflows/weblate-lock.yml @@ -36,8 +36,7 @@ jobs: github-token: ${{ steps.token.outputs.token }} filters: | i18n: - - 'i18n/!(en)**\.json' - exclude-branches: 'chore/translations' + - modified: 'i18n/!(en)**\.json' skip-force-logic: 'true' enforce-lock: diff --git a/README.md b/README.md index b540408475..7e06d9de4b 100644 --- a/README.md +++ b/README.md @@ -118,16 +118,16 @@ Read more about translations [here](https://docs.immich.app/developer/translatio ## Star history - + - - - Star History Chart + + + Star History Chart ## Contributors - + diff --git a/e2e/src/web/specs/timeline/timeline.parallel-e2e-spec.ts b/e2e/src/web/specs/timeline/timeline.parallel-e2e-spec.ts index e9f29f3413..49a8f38312 100644 --- a/e2e/src/web/specs/timeline/timeline.parallel-e2e-spec.ts +++ b/e2e/src/web/specs/timeline/timeline.parallel-e2e-spec.ts @@ -607,8 +607,7 @@ test.describe('Timeline', () => { visibility: 'timeline', ids: [assetToArchive.id], }); - console.log('Skipping assertion - TODO - fix bug with not removing asset from timeline-manager after unarchive'); - // await expect(thumbnail.withAssetId(page, assetToArchive.id)).toHaveCount(0); + await expect(thumbnailUtils.withAssetId(page, assetToArchive.id)).toHaveCount(0); await page.getByText('Photos', { exact: true }).click(); await thumbnailUtils.expectInViewport(page, assetToArchive.id); }); diff --git a/i18n/en.json b/i18n/en.json index 14af1af85f..9a847a51d4 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1121,6 +1121,7 @@ "folders_feature_description": "Browsing the folder view for the photos and videos on the file system", "forgot_pin_code_question": "Forgot your PIN?", "forward": "Forward", + "full_path": "Full path: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "This feature loads external resources from Google in order to work.", "general": "General", @@ -1157,6 +1158,7 @@ "hide_named_person": "Hide person {name}", "hide_password": "Hide password", "hide_person": "Hide person", + "hide_text_recognition": "Hide text recognition", "hide_unnamed_people": "Hide unnamed people", "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", @@ -1966,6 +1968,7 @@ "show_slideshow_transition": "Show slideshow transition", "show_supporter_badge": "Supporter badge", "show_supporter_badge_description": "Show a supporter badge", + "show_text_recognition": "Show text recognition", "show_text_search_menu": "Show text search menu", "shuffle": "Shuffle", "sidebar": "Sidebar", @@ -2048,6 +2051,7 @@ "tags": "Tags", "tap_to_run_job": "Tap to run job", "template": "Template", + "text_recognition": "Text recognition", "theme": "Theme", "theme_selection": "Theme selection", "theme_selection_description": "Automatically set the theme to light or dark based on your browser's system preference", diff --git a/web/src/lib/actions/zoom-image.ts b/web/src/lib/actions/zoom-image.ts index 29074fc7b0..e67d3e1928 100644 --- a/web/src/lib/actions/zoom-image.ts +++ b/web/src/lib/actions/zoom-image.ts @@ -2,7 +2,7 @@ import { photoZoomState } from '$lib/stores/zoom-image.store'; import { useZoomImageWheel } from '@zoom-image/svelte'; import { get } from 'svelte/store'; -export const zoomImageAction = (node: HTMLElement) => { +export const zoomImageAction = (node: HTMLElement, options?: { disabled?: boolean }) => { const { createZoomImage, zoomImageState, setZoomImageState } = useZoomImageWheel(); createZoomImage(node, { @@ -14,9 +14,32 @@ export const zoomImageAction = (node: HTMLElement) => { setZoomImageState(state); } + // Store original event handlers so we can prevent them when disabled + const wheelHandler = (event: WheelEvent) => { + if (options?.disabled) { + event.stopImmediatePropagation(); + } + }; + + const pointerDownHandler = (event: PointerEvent) => { + if (options?.disabled) { + event.stopImmediatePropagation(); + } + }; + + // Add handlers at capture phase with higher priority + node.addEventListener('wheel', wheelHandler, { capture: true }); + node.addEventListener('pointerdown', pointerDownHandler, { capture: true }); + const unsubscribes = [photoZoomState.subscribe(setZoomImageState), zoomImageState.subscribe(photoZoomState.set)]; + return { + update(newOptions?: { disabled?: boolean }) { + options = newOptions; + }, destroy() { + node.removeEventListener('wheel', wheelHandler, { capture: true }); + node.removeEventListener('pointerdown', pointerDownHandler, { capture: true }); for (const unsubscribe of unsubscribes) { unsubscribe(); } diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index 7daade6379..0dad2793bf 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -25,12 +25,12 @@ import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import { AppRoute } from '$lib/constants'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; + import { handleReplaceAsset } from '$lib/services/asset.service'; import { photoViewerImgElement } from '$lib/stores/assets-store.svelte'; import { user } from '$lib/stores/user.store'; import { photoZoomState } from '$lib/stores/zoom-image.store'; import { getAssetJobName, getSharedLink } from '$lib/utils'; import { canCopyImageToClipboard } from '$lib/utils/asset-utils'; - import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { toTimelineAsset } from '$lib/utils/timeline-util'; import { AssetJobName, @@ -227,7 +227,7 @@ openFileUploadDialog({ multiple: false, assetId: asset.id })} + onClick={() => handleReplaceAsset(asset.id)} text={$t('replace_with_upload')} /> {#if !asset.isArchived && !asset.isTrashed} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 8ff7a00710..0af27e8373 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -1,16 +1,19 @@ + +
{/if} + {#if $slideshowState === SlideshowState.None && isShared && ((album && album.isActivityEnabled) || activityManager.commentCount > 0) && !activityManager.isLoading}
{/if} + + {#if $slideshowState === SlideshowState.None && asset.type === AssetTypeEnum.Image && !isShowEditor && ocrManager.hasOcrData} +
+ +
+ {/if} {/key} {/if} diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index a9c447e498..2ee4496830 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -503,7 +503,7 @@ {/if} {#if albums.length > 0} -
+

{$t('appears_in')}

{#each albums as album (album.id)} diff --git a/web/src/lib/components/asset-viewer/ocr-bounding-box.svelte b/web/src/lib/components/asset-viewer/ocr-bounding-box.svelte new file mode 100644 index 0000000000..e64b674ac1 --- /dev/null +++ b/web/src/lib/components/asset-viewer/ocr-bounding-box.svelte @@ -0,0 +1,36 @@ + + +
+ +
+ + +
+ {ocrBox.text} +
+
diff --git a/web/src/lib/components/asset-viewer/ocr-button.svelte b/web/src/lib/components/asset-viewer/ocr-button.svelte new file mode 100644 index 0000000000..9f8966e64a --- /dev/null +++ b/web/src/lib/components/asset-viewer/ocr-button.svelte @@ -0,0 +1,17 @@ + + + ocrManager.toggleOcrBoundingBox()} +/> diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index e37773fca5..261f194d34 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -2,12 +2,14 @@ import { shortcuts } from '$lib/actions/shortcut'; import { zoomImageAction } from '$lib/actions/zoom-image'; import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte'; + import OcrBoundingBox from '$lib/components/asset-viewer/ocr-bounding-box.svelte'; import BrokenAsset from '$lib/components/assets/broken-asset.svelte'; import { assetViewerFadeDuration } from '$lib/constants'; import { castManager } from '$lib/managers/cast-manager.svelte'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import { photoViewerImgElement } from '$lib/stores/assets-store.svelte'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; + import { ocrManager } from '$lib/stores/ocr.svelte'; import { boundingBoxesArray } from '$lib/stores/people.store'; import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store'; import { SlideshowLook, SlideshowState, slideshowLookCssMapping, slideshowStore } from '$lib/stores/slideshow.store'; @@ -15,6 +17,7 @@ import { getAssetOriginalUrl, getAssetThumbnailUrl, handlePromiseError } from '$lib/utils'; import { canCopyImageToClipboard, copyImageToClipboard, isWebCompatibleImage } from '$lib/utils/asset-utils'; import { handleError } from '$lib/utils/handle-error'; + import { getOcrBoundingBoxes } from '$lib/utils/ocr-utils'; import { getBoundingBox } from '$lib/utils/people-utils'; import { cancelImageUrl } from '$lib/utils/sw-messaging'; import { getAltText } from '$lib/utils/thumbnail-util'; @@ -71,6 +74,14 @@ $boundingBoxesArray = []; }); + let ocrBoxes = $derived( + ocrManager.showOverlay && $photoViewerImgElement + ? getOcrBoundingBoxes(ocrManager.data, $photoZoomState, $photoViewerImgElement) + : [], + ); + + let isOcrActive = $derived(ocrManager.showOverlay); + const preload = (targetSize: AssetMediaSize | 'original', preloadAssets?: TimelineAsset[]) => { for (const preloadAsset of preloadAssets || []) { if (preloadAsset.isImage) { @@ -130,9 +141,15 @@ if ($photoZoomState.currentZoom > 1) { return; } + + if (ocrManager.showOverlay) { + return; + } + if (onNextAsset && event.detail.direction === 'left') { onNextAsset(); } + if (onPreviousAsset && event.detail.direction === 'right') { onPreviousAsset(); } @@ -235,7 +252,7 @@ {:else if !imageError}
{/each} + + {#each ocrBoxes as ocrBox (ocrBox.id)} + + {/each} {#if isFaceEditMode.value} diff --git a/web/src/lib/components/timeline/TimelineAssetViewer.svelte b/web/src/lib/components/timeline/TimelineAssetViewer.svelte index ccdd8bd5b4..a121bd1938 100644 --- a/web/src/lib/components/timeline/TimelineAssetViewer.svelte +++ b/web/src/lib/components/timeline/TimelineAssetViewer.svelte @@ -110,13 +110,9 @@ case AssetAction.ARCHIVE: case AssetAction.UNARCHIVE: case AssetAction.FAVORITE: - case AssetAction.UNFAVORITE: { - timelineManager.updateAssets([action.asset]); - break; - } - + case AssetAction.UNFAVORITE: case AssetAction.ADD: { - timelineManager.addAssets([action.asset]); + timelineManager.upsertAssets([action.asset]); break; } @@ -135,7 +131,7 @@ break; } case AssetAction.REMOVE_ASSET_FROM_STACK: { - timelineManager.addAssets([toTimelineAsset(action.asset)]); + timelineManager.upsertAssets([toTimelineAsset(action.asset)]); if (action.stack) { //Have to unstack then restack assets in timeline in order to update the stack count in the timeline. updateUnstackedAssetInTimeline( diff --git a/web/src/lib/components/timeline/TimelineDateGroup.svelte b/web/src/lib/components/timeline/TimelineDateGroup.svelte index cd0dc9a212..c662c16e72 100644 --- a/web/src/lib/components/timeline/TimelineDateGroup.svelte +++ b/web/src/lib/components/timeline/TimelineDateGroup.svelte @@ -2,7 +2,7 @@ import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; import type { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte'; import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte'; - import type { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; + import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import { assetSnapshot, assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte'; import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; diff --git a/web/src/lib/components/timeline/actions/ArchiveAction.svelte b/web/src/lib/components/timeline/actions/ArchiveAction.svelte index 05ef9c99ff..e11da0b2f0 100644 --- a/web/src/lib/components/timeline/actions/ArchiveAction.svelte +++ b/web/src/lib/components/timeline/actions/ArchiveAction.svelte @@ -24,12 +24,12 @@ const { clearSelect, getOwnedAssets } = getAssetControlContext(); const handleArchive = async () => { - const isArchived = unarchive ? AssetVisibility.Timeline : AssetVisibility.Archive; - const assets = [...getOwnedAssets()].filter((asset) => asset.visibility !== isArchived); + const visibility = unarchive ? AssetVisibility.Timeline : AssetVisibility.Archive; + const assets = [...getOwnedAssets()].filter((asset) => asset.visibility !== visibility); loading = true; - const ids = await archiveAssets(assets, isArchived as AssetVisibility); + const ids = await archiveAssets(assets, visibility as AssetVisibility); if (ids) { - onArchive?.(ids, isArchived ? AssetVisibility.Archive : AssetVisibility.Timeline); + onArchive?.(ids, visibility); clearSelect(); } loading = false; diff --git a/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte b/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte index 74b058a74f..d5b1d2ecf6 100644 --- a/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte +++ b/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte @@ -46,7 +46,7 @@ !(isTrashEnabled && !force), (assetIds) => timelineManager.removeAssets(assetIds), assetInteraction.selectedAssets, - !isTrashEnabled || force ? undefined : (assets) => timelineManager.addAssets(assets), + !isTrashEnabled || force ? undefined : (assets) => timelineManager.upsertAssets(assets), ); assetInteraction.clearMultiselect(); }; diff --git a/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte b/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte index 8a8395d792..c0f5428c81 100644 --- a/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte +++ b/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte @@ -12,6 +12,7 @@ mdiClock, mdiFile, mdiFitToScreen, + mdiFolderOutline, mdiHeart, mdiImageMultipleOutline, mdiImageOutline, @@ -51,6 +52,7 @@ fileName: isDifferent((a) => a.originalFileName), fileSize: isDifferent((a) => getFileSize(a)), resolution: isDifferent((a) => getAssetResolution(a)), + originalPath: isDifferent((a) => a.originalPath ?? $t('unknown')), date: isDifferent((a) => { const tz = a.exifInfo?.timeZone; const dt = @@ -79,6 +81,24 @@ (a) => [a.exifInfo?.city, a.exifInfo?.state, a.exifInfo?.country].filter(Boolean).join(', ') || 'unknown', ), }); + + const getBasePath = (fullpath: string, fileName: string): string => { + if (fileName && fullpath.endsWith(fileName)) { + return fullpath.slice(0, -(fileName.length + 1)); + } + return fullpath; + }; + + function truncateMiddle(path: string, maxLength: number = 50): string { + if (path.length <= maxLength) { + return path; + } + + const start = Math.floor(maxLength / 2) - 2; + const end = Math.floor(maxLength / 2) - 2; + + return path.slice(0, Math.max(0, start)) + '...' + path.slice(Math.max(0, path.length - end)); + }
@@ -152,6 +172,14 @@ {asset.originalFileName} + + {truncateMiddle(getBasePath(asset.originalPath, asset.originalFileName)) || $t('unknown')} + + {getFileSize(asset)} diff --git a/web/src/lib/managers/event-manager.svelte.ts b/web/src/lib/managers/event-manager.svelte.ts index 62fc9df8da..a2a4c27850 100644 --- a/web/src/lib/managers/event-manager.svelte.ts +++ b/web/src/lib/managers/event-manager.svelte.ts @@ -16,6 +16,8 @@ export type Events = { LanguageChange: [{ name: string; code: string; rtl?: boolean }]; ThemeChange: [ThemeSetting]; + AssetReplace: [{ oldAssetId: string; newAssetId: string }]; + AlbumDelete: [AlbumResponseDto]; SharedLinkCreate: [SharedLinkResponseDto]; diff --git a/web/src/lib/managers/timeline-manager/internal/intersection-support.svelte.ts b/web/src/lib/managers/timeline-manager/internal/intersection-support.svelte.ts index bdf2b17cbe..3c6f2d8256 100644 --- a/web/src/lib/managers/timeline-manager/internal/intersection-support.svelte.ts +++ b/web/src/lib/managers/timeline-manager/internal/intersection-support.svelte.ts @@ -1,6 +1,6 @@ import { TUNABLES } from '$lib/utils/tunables'; import type { MonthGroup } from '../month-group.svelte'; -import type { TimelineManager } from '../timeline-manager.svelte'; +import { TimelineManager } from '../timeline-manager.svelte'; const { TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM }, diff --git a/web/src/lib/managers/timeline-manager/internal/layout-support.svelte.ts b/web/src/lib/managers/timeline-manager/internal/layout-support.svelte.ts index 0f6ca112d1..71dc168971 100644 --- a/web/src/lib/managers/timeline-manager/internal/layout-support.svelte.ts +++ b/web/src/lib/managers/timeline-manager/internal/layout-support.svelte.ts @@ -1,5 +1,5 @@ import type { MonthGroup } from '../month-group.svelte'; -import type { TimelineManager } from '../timeline-manager.svelte'; +import { TimelineManager } from '../timeline-manager.svelte'; import type { UpdateGeometryOptions } from '../types'; export function updateGeometry(timelineManager: TimelineManager, month: MonthGroup, options: UpdateGeometryOptions) { diff --git a/web/src/lib/managers/timeline-manager/internal/load-support.svelte.ts b/web/src/lib/managers/timeline-manager/internal/load-support.svelte.ts index 0d966c9cee..ec50e3d75e 100644 --- a/web/src/lib/managers/timeline-manager/internal/load-support.svelte.ts +++ b/web/src/lib/managers/timeline-manager/internal/load-support.svelte.ts @@ -2,7 +2,7 @@ import { authManager } from '$lib/managers/auth-manager.svelte'; import { toISOYearMonthUTC } from '$lib/utils/timeline-util'; import { getTimeBucket } from '@immich/sdk'; import type { MonthGroup } from '../month-group.svelte'; -import type { TimelineManager } from '../timeline-manager.svelte'; +import { TimelineManager } from '../timeline-manager.svelte'; import type { TimelineManagerOptions } from '../types'; export async function loadFromTimeBuckets( diff --git a/web/src/lib/managers/timeline-manager/internal/search-support.svelte.ts b/web/src/lib/managers/timeline-manager/internal/search-support.svelte.ts index 52a37b52d0..f889456c20 100644 --- a/web/src/lib/managers/timeline-manager/internal/search-support.svelte.ts +++ b/web/src/lib/managers/timeline-manager/internal/search-support.svelte.ts @@ -2,7 +2,7 @@ import { plainDateTimeCompare, type TimelineYearMonth } from '$lib/utils/timelin import { AssetOrder } from '@immich/sdk'; import { DateTime } from 'luxon'; import type { MonthGroup } from '../month-group.svelte'; -import type { TimelineManager } from '../timeline-manager.svelte'; +import { TimelineManager } from '../timeline-manager.svelte'; import type { AssetDescriptor, Direction, TimelineAsset } from '../types'; export async function getAssetWithOffset( diff --git a/web/src/lib/managers/timeline-manager/internal/websocket-support.svelte.ts b/web/src/lib/managers/timeline-manager/internal/websocket-support.svelte.ts index 4ba237c50c..bff2f15cb9 100644 --- a/web/src/lib/managers/timeline-manager/internal/websocket-support.svelte.ts +++ b/web/src/lib/managers/timeline-manager/internal/websocket-support.svelte.ts @@ -13,10 +13,10 @@ export class WebsocketSupport { #processPendingChanges = throttle(() => { const { add, update, remove } = this.#getPendingChangeBatches(); if (add.length > 0) { - this.#timelineManager.addAssets(add); + this.#timelineManager.upsertAssets(add); } if (update.length > 0) { - this.#timelineManager.updateAssets(update); + this.#timelineManager.upsertAssets(update); } if (remove.length > 0) { this.#timelineManager.removeAssets(remove); diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts index e6eddef9b6..62053f7a0d 100644 --- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts +++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts @@ -2,7 +2,7 @@ import { sdkMock } from '$lib/__mocks__/sdk.mock'; import { getMonthGroupByDate } from '$lib/managers/timeline-manager/internal/search-support.svelte'; import { AbortError } from '$lib/utils'; import { fromISODateTimeUTCToObject } from '$lib/utils/timeline-util'; -import { type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk'; +import { AssetVisibility, type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk'; import { timelineAssetFactory, toResponseDto } from '@test-data/factories/asset-factory'; import { tick } from 'svelte'; import { TimelineManager } from './timeline-manager.svelte'; @@ -175,7 +175,7 @@ describe('TimelineManager', () => { }); }); - describe('addAssets', () => { + describe('upsertAssets', () => { let timelineManager: TimelineManager; beforeEach(async () => { @@ -196,7 +196,7 @@ describe('TimelineManager', () => { fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'), }), ); - timelineManager.addAssets([asset]); + timelineManager.upsertAssets([asset]); expect(timelineManager.months.length).toEqual(1); expect(timelineManager.assetCount).toEqual(1); @@ -212,8 +212,8 @@ describe('TimelineManager', () => { fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'), }) .map((asset) => deriveLocalDateTimeFromFileCreatedAt(asset)); - timelineManager.addAssets([assetOne]); - timelineManager.addAssets([assetTwo]); + timelineManager.upsertAssets([assetOne]); + timelineManager.upsertAssets([assetTwo]); expect(timelineManager.months.length).toEqual(1); expect(timelineManager.assetCount).toEqual(2); @@ -238,7 +238,7 @@ describe('TimelineManager', () => { fileCreatedAt: fromISODateTimeUTCToObject('2024-01-16T12:00:00.000Z'), }), ); - timelineManager.addAssets([assetOne, assetTwo, assetThree]); + timelineManager.upsertAssets([assetOne, assetTwo, assetThree]); const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 }); expect(month).not.toBeNull(); @@ -264,7 +264,7 @@ describe('TimelineManager', () => { fileCreatedAt: fromISODateTimeUTCToObject('2023-01-20T12:00:00.000Z'), }), ); - timelineManager.addAssets([assetOne, assetTwo, assetThree]); + timelineManager.upsertAssets([assetOne, assetTwo, assetThree]); expect(timelineManager.months.length).toEqual(3); expect(timelineManager.months[0].yearMonth.year).toEqual(2024); @@ -278,12 +278,10 @@ describe('TimelineManager', () => { }); it('updates existing asset', () => { - const updateAssetsSpy = vi.spyOn(timelineManager, 'updateAssets'); const asset = deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build()); - timelineManager.addAssets([asset]); + timelineManager.upsertAssets([asset]); - timelineManager.addAssets([asset]); - expect(updateAssetsSpy).toBeCalledWith([asset]); + timelineManager.upsertAssets([asset]); expect(timelineManager.assetCount).toEqual(1); }); @@ -294,12 +292,12 @@ describe('TimelineManager', () => { const timelineManager = new TimelineManager(); await timelineManager.updateOptions({ isTrashed: true }); - timelineManager.addAssets([asset, trashedAsset]); + timelineManager.upsertAssets([asset, trashedAsset]); expect(await getAssets(timelineManager)).toEqual([trashedAsset]); }); }); - describe('updateAssets', () => { + describe('upsertAssets - updating existing', () => { let timelineManager: TimelineManager; beforeEach(async () => { @@ -309,22 +307,15 @@ describe('TimelineManager', () => { await timelineManager.updateViewport({ width: 1588, height: 1000 }); }); - it('ignores non-existing assets', () => { - timelineManager.updateAssets([deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build())]); - - expect(timelineManager.months.length).toEqual(0); - expect(timelineManager.assetCount).toEqual(0); - }); - it('updates an asset', () => { const asset = deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build({ isFavorite: false })); const updatedAsset = { ...asset, isFavorite: true }; - timelineManager.addAssets([asset]); + timelineManager.upsertAssets([asset]); expect(timelineManager.assetCount).toEqual(1); expect(timelineManager.months[0].getFirstAsset().isFavorite).toEqual(false); - timelineManager.updateAssets([updatedAsset]); + timelineManager.upsertAssets([updatedAsset]); expect(timelineManager.assetCount).toEqual(1); expect(timelineManager.months[0].getFirstAsset().isFavorite).toEqual(true); }); @@ -340,18 +331,80 @@ describe('TimelineManager', () => { fileCreatedAt: fromISODateTimeUTCToObject('2024-03-20T12:00:00.000Z'), }); - timelineManager.addAssets([asset]); + timelineManager.upsertAssets([asset]); expect(timelineManager.months.length).toEqual(1); expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })).not.toBeUndefined(); expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(1); - timelineManager.updateAssets([updatedAsset]); + timelineManager.upsertAssets([updatedAsset]); expect(timelineManager.months.length).toEqual(2); expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })).not.toBeUndefined(); expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(0); expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 3 })).not.toBeUndefined(); expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 3 })?.getAssets().length).toEqual(1); }); + it('asset is removed during upsert when TimelineManager if visibility changes', async () => { + await timelineManager.updateOptions({ + visibility: AssetVisibility.Archive, + }); + const fixture = deriveLocalDateTimeFromFileCreatedAt( + timelineAssetFactory.build({ + visibility: AssetVisibility.Archive, + }), + ); + + timelineManager.upsertAssets([fixture]); + expect(timelineManager.assetCount).toEqual(1); + + const updated = Object.freeze({ ...fixture, visibility: AssetVisibility.Timeline }); + timelineManager.upsertAssets([updated]); + expect(timelineManager.assetCount).toEqual(0); + + timelineManager.upsertAssets([{ ...fixture, visibility: AssetVisibility.Archive }]); + expect(timelineManager.assetCount).toEqual(1); + }); + + it('asset is removed during upsert when TimelineManager if isFavorite changes', async () => { + await timelineManager.updateOptions({ + isFavorite: true, + }); + const fixture = deriveLocalDateTimeFromFileCreatedAt( + timelineAssetFactory.build({ + isFavorite: true, + }), + ); + + timelineManager.upsertAssets([fixture]); + expect(timelineManager.assetCount).toEqual(1); + + const updated = Object.freeze({ ...fixture, isFavorite: false }); + timelineManager.upsertAssets([updated]); + expect(timelineManager.assetCount).toEqual(0); + + timelineManager.upsertAssets([{ ...fixture, isFavorite: true }]); + expect(timelineManager.assetCount).toEqual(1); + }); + + it('asset is removed during upsert when TimelineManager if isTrashed changes', async () => { + await timelineManager.updateOptions({ + isTrashed: true, + }); + const fixture = deriveLocalDateTimeFromFileCreatedAt( + timelineAssetFactory.build({ + isTrashed: true, + }), + ); + + timelineManager.upsertAssets([fixture]); + expect(timelineManager.assetCount).toEqual(1); + + const updated = Object.freeze({ ...fixture, isTrashed: false }); + timelineManager.upsertAssets([updated]); + expect(timelineManager.assetCount).toEqual(0); + + timelineManager.upsertAssets([{ ...fixture, isTrashed: true }]); + expect(timelineManager.assetCount).toEqual(1); + }); }); describe('removeAssets', () => { @@ -365,7 +418,7 @@ describe('TimelineManager', () => { }); it('ignores invalid IDs', () => { - timelineManager.addAssets( + timelineManager.upsertAssets( timelineAssetFactory .buildList(2, { fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'), @@ -385,7 +438,7 @@ describe('TimelineManager', () => { fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'), }) .map((asset) => deriveLocalDateTimeFromFileCreatedAt(asset)); - timelineManager.addAssets([assetOne, assetTwo]); + timelineManager.upsertAssets([assetOne, assetTwo]); timelineManager.removeAssets([assetOne.id]); expect(timelineManager.assetCount).toEqual(1); @@ -399,7 +452,7 @@ describe('TimelineManager', () => { fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'), }) .map((asset) => deriveLocalDateTimeFromFileCreatedAt(asset)); - timelineManager.addAssets(assets); + timelineManager.upsertAssets(assets); timelineManager.removeAssets(assets.map((asset) => asset.id)); expect(timelineManager.assetCount).toEqual(0); @@ -431,7 +484,7 @@ describe('TimelineManager', () => { fileCreatedAt: fromISODateTimeUTCToObject('2024-01-15T12:00:00.000Z'), }), ); - timelineManager.addAssets([assetOne, assetTwo]); + timelineManager.upsertAssets([assetOne, assetTwo]); expect(timelineManager.getFirstAsset()).toEqual(assetOne); }); }); @@ -556,7 +609,7 @@ describe('TimelineManager', () => { fileCreatedAt: fromISODateTimeUTCToObject('2024-02-15T12:00:00.000Z'), }), ); - timelineManager.addAssets([assetOne, assetTwo]); + timelineManager.upsertAssets([assetOne, assetTwo]); expect(timelineManager.getMonthGroupByAssetId(assetTwo.id)?.yearMonth.year).toEqual(2024); expect(timelineManager.getMonthGroupByAssetId(assetTwo.id)?.yearMonth.month).toEqual(2); @@ -575,7 +628,7 @@ describe('TimelineManager', () => { fileCreatedAt: fromISODateTimeUTCToObject('2024-02-15T12:00:00.000Z'), }), ); - timelineManager.addAssets([assetOne, assetTwo]); + timelineManager.upsertAssets([assetOne, assetTwo]); timelineManager.removeAssets([assetTwo.id]); expect(timelineManager.getMonthGroupByAssetId(assetOne.id)?.yearMonth.year).toEqual(2024); diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts index 24523ce9e7..e3327663b4 100644 --- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts +++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts @@ -320,10 +320,10 @@ export class TimelineManager extends VirtualScrollManager { } } - addAssets(assets: TimelineAsset[]) { - const assetsToUpdate = assets.filter((asset) => !this.isExcluded(asset)); - const notUpdated = this.updateAssets(assetsToUpdate); - addAssetsToMonthGroups(this, [...notUpdated], { order: this.#options.order ?? AssetOrder.Desc }); + upsertAssets(assets: TimelineAsset[]) { + const notUpdated = this.#updateAssets(assets); + const notExcluded = notUpdated.filter((asset) => !this.isExcluded(asset)); + addAssetsToMonthGroups(this, [...notExcluded], { order: this.#options.order ?? AssetOrder.Desc }); } async findMonthGroupForAsset(id: string) { @@ -404,7 +404,7 @@ export class TimelineManager extends VirtualScrollManager { runAssetOperation(this, new SvelteSet(ids), operation, { order: this.#options.order ?? AssetOrder.Desc }); } - updateAssets(assets: TimelineAsset[]) { + #updateAssets(assets: TimelineAsset[]) { const lookup = new SvelteMap(assets.map((asset) => [asset.id, asset])); const { unprocessedIds } = runAssetOperation( this, diff --git a/web/src/lib/modals/NavigateToDateModal.svelte b/web/src/lib/modals/NavigateToDateModal.svelte index 4b83c66bc6..365cbdb21c 100644 --- a/web/src/lib/modals/NavigateToDateModal.svelte +++ b/web/src/lib/modals/NavigateToDateModal.svelte @@ -1,6 +1,6 @@