diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0a78ab795..84b83690e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -739,7 +739,7 @@ importers: version: 0.4.3 '@immich/sdk': specifier: file:../open-api/typescript-sdk - version: link:../open-api/typescript-sdk + version: file:open-api/typescript-sdk '@immich/ui': specifier: ^0.61.4 version: 0.61.4(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0) @@ -781,7 +781,7 @@ importers: version: 2.6.0 fabric: specifier: ^6.5.4 - version: 6.9.1 + version: 6.9.1(encoding@0.1.13) geo-coordinates-parser: specifier: ^1.7.4 version: 1.7.4 @@ -809,6 +809,9 @@ importers: maplibre-gl: specifier: ^5.6.2 version: 5.16.0 + media-chrome: + specifier: ^4.17.2 + version: 4.17.2(react@19.2.3) pmtiles: specifier: ^4.3.0 version: 4.3.2 @@ -881,7 +884,7 @@ importers: version: 6.9.1 '@testing-library/svelte': specifier: ^5.2.8 - version: 5.3.1(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 5.3.1(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@testing-library/user-event': specifier: ^14.5.2 version: 14.6.1(@testing-library/dom@10.4.1) @@ -905,7 +908,7 @@ importers: version: 1.5.6 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) dotenv: specifier: ^17.0.0 version: 17.2.3 @@ -968,7 +971,7 @@ importers: version: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) packages: @@ -3126,6 +3129,9 @@ packages: '@immich/justified-layout-wasm@0.4.3': resolution: {integrity: sha512-fpcQ7zPhP3Cp1bEXhONVYSUeIANa2uzaQFGKufUZQo5FO7aFT77szTVChhlCy4XaVy5R4ZvgSkA/1TJmeORz7Q==} + '@immich/sdk@file:open-api/typescript-sdk': + resolution: {directory: open-api/typescript-sdk, type: directory} + '@immich/svelte-markdown-preprocess@0.2.1': resolution: {integrity: sha512-mbr/g75lO8Zh+ELCuYrZP0XB4gf2UbK8rJcGYMYxFJJzMMunV+sm9FqtV1dbwW2dpXzCZGz1XPCEZ6oo526TbA==} peerDependencies: @@ -6229,6 +6235,11 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + ce-la-react@0.3.2: + resolution: {integrity: sha512-QJ6k4lOD/btI08xG8jBPxRCGXvCnusGGkTsiXk0u3NqUu/W+BXRnFD4PYjwtqh8AWmGa5LDbGk0fLQsqr0nSMA==} + peerDependencies: + react: '>=17.0.0' + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -9153,6 +9164,9 @@ packages: mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + media-chrome@4.17.2: + resolution: {integrity: sha512-o/IgiHx0tdSVwRxxqF5H12FK31A/A8T71sv3KdAvh7b6XeBS9dXwqvIFwlR9kdEuqg3n7xpmRIuL83rmYq8FTg==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -15755,6 +15769,10 @@ snapshots: '@immich/justified-layout-wasm@0.4.3': {} + '@immich/sdk@file:open-api/typescript-sdk': + dependencies: + '@oazapfts/runtime': 1.1.0 + '@immich/svelte-markdown-preprocess@0.2.1(svelte@5.48.0)': dependencies: front-matter: 4.0.2 @@ -17804,14 +17822,14 @@ snapshots: dependencies: svelte: 5.48.0 - '@testing-library/svelte@5.3.1(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@testing-library/svelte@5.3.1(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/svelte-core': 1.0.0(svelte@5.48.0) svelte: 5.48.0 optionalDependencies: vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: @@ -18537,7 +18555,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -18552,7 +18570,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -19296,6 +19314,10 @@ snapshots: ccount@2.0.1: {} + ce-la-react@0.3.2(react@19.2.3): + dependencies: + react: 19.2.3 + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -20904,10 +20926,10 @@ snapshots: extend@3.0.2: {} - fabric@6.9.1: + fabric@6.9.1(encoding@0.1.13): optionalDependencies: - canvas: 2.11.2 - jsdom: 20.0.3(canvas@2.11.2) + canvas: 2.11.2(encoding@0.1.13) + jsdom: 20.0.3(canvas@2.11.2(encoding@0.1.13)) transitivePeerDependencies: - bufferutil - encoding @@ -22059,7 +22081,7 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@20.0.3(canvas@2.11.2): + jsdom@20.0.3(canvas@2.11.2(encoding@0.1.13)): dependencies: abab: 2.0.6 acorn: 8.15.0 @@ -22088,7 +22110,7 @@ snapshots: ws: 8.19.0 xml-name-validator: 4.0.0 optionalDependencies: - canvas: 2.11.2 + canvas: 2.11.2(encoding@0.1.13) transitivePeerDependencies: - bufferutil - supports-color @@ -22781,6 +22803,12 @@ snapshots: mdn-data@2.0.30: {} + media-chrome@4.17.2(react@19.2.3): + dependencies: + ce-la-react: 0.3.2(react@19.2.3) + transitivePeerDependencies: + - react + media-typer@0.3.0: {} media-typer@1.1.0: {} @@ -26668,7 +26696,7 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -26697,7 +26725,7 @@ snapshots: '@types/debug': 4.1.12 '@types/node': 25.0.9 happy-dom: 20.3.0 - jsdom: 26.1.0(canvas@2.11.2) + jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13)) transitivePeerDependencies: - jiti - less diff --git a/web/package.json b/web/package.json index 7af0474ba1..b8a5f1d494 100644 --- a/web/package.json +++ b/web/package.json @@ -50,6 +50,7 @@ "lodash-es": "^4.17.21", "luxon": "^3.4.4", "maplibre-gl": "^5.6.2", + "media-chrome": "^4.17.2", "pmtiles": "^4.3.0", "qrcode": "^1.5.4", "simple-icons": "^15.15.0", diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 848870b654..5bb31be7fd 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -513,6 +513,7 @@ cacheKey={asset.thumbhash} projectionType={asset.exifInfo?.projectionType} loopVideo={$slideshowState !== SlideshowState.PlaySlideshow} + showFullscreen={false} onPreviousAsset={() => navigateAsset('previous')} onNextAsset={() => navigateAsset('next')} onVideoEnded={() => (assetViewerManager.isPlayingMotionPhoto = false)} diff --git a/web/src/lib/components/asset-viewer/video-native-viewer.svelte b/web/src/lib/components/asset-viewer/video-native-viewer.svelte index 78fdc3a1ba..287d833e0d 100644 --- a/web/src/lib/components/asset-viewer/video-native-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-native-viewer.svelte @@ -4,24 +4,41 @@ import { assetViewerFadeDuration } from '$lib/constants'; import { castManager } from '$lib/managers/cast-manager.svelte'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; - import { - autoPlayVideo, - loopVideo as loopVideoPreference, - videoViewerMuted, - videoViewerVolume, - } from '$lib/stores/preferences.store'; + import { autoPlayVideo, loopVideo as loopVideoPreference } from '$lib/stores/preferences.store'; import { getAssetMediaUrl, getAssetPlaybackUrl } from '$lib/utils'; - import { AssetMediaSize } from '@immich/sdk'; - import { LoadingSpinner } from '@immich/ui'; + import { timeToSeconds } from '$lib/utils/date-time'; + import { AssetMediaSize, type AssetResponseDto } from '@immich/sdk'; + import { Icon, LoadingSpinner } from '@immich/ui'; + import { + mdiFullscreen, + mdiFullscreenExit, + mdiPause, + mdiPlay, + mdiVolumeHigh, + mdiVolumeLow, + mdiVolumeMedium, + mdiVolumeMute, + } from '@mdi/js'; + import 'media-chrome/media-control-bar'; + import 'media-chrome/media-controller'; + import 'media-chrome/media-fullscreen-button'; + import 'media-chrome/media-mute-button'; + import 'media-chrome/media-play-button'; + import 'media-chrome/media-playback-rate-button'; + import 'media-chrome/media-time-display'; + import 'media-chrome/media-time-range'; + import 'media-chrome/media-volume-range'; import { onDestroy, onMount } from 'svelte'; import { useSwipe, type SwipeCustomEvent } from 'svelte-gestures'; import { fade } from 'svelte/transition'; interface Props { + asset: AssetResponseDto; assetId: string; loopVideo: boolean; cacheKey: string | null; playOriginalVideo: boolean; + showFullscreen?: boolean; onPreviousAsset?: () => void; onNextAsset?: () => void; onVideoEnded?: () => void; @@ -30,10 +47,12 @@ } let { + asset, assetId, loopVideo, cacheKey, playOriginalVideo, + showFullscreen = true, onPreviousAsset = () => {}, onNextAsset = () => {}, onVideoEnded = () => {}, @@ -48,7 +67,6 @@ ? getAssetMediaUrl({ id: assetId, size: AssetMediaSize.Original, cacheKey }) : getAssetPlaybackUrl({ id: assetId, cacheKey }), ); - let isScrubbing = $state(false); let showVideo = $state(false); onMount(() => { @@ -71,7 +89,7 @@ const handleCanPlay = async (video: HTMLVideoElement) => { try { - if (!video.paused && !isScrubbing) { + if (!video.paused) { await video.play(); onVideoStarted(); } @@ -136,30 +154,56 @@ /> {:else} - + + +
+ +
+
+ + + + + + + + +
+ + + + + + +
+ +
+
+ + {#if showFullscreen} + + + + + {/if} +
+
+
{#if isLoading}
@@ -168,8 +212,86 @@ {/if} {#if isFaceEditMode.value} - + {/if} {/if}
{/if} + + diff --git a/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte b/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte index 57d8acd78a..9698384ad2 100644 --- a/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte @@ -11,6 +11,7 @@ cacheKey: string | null; loopVideo: boolean; playOriginalVideo: boolean; + showFullscreen?: boolean; onClose?: () => void; onPreviousAsset?: () => void; onNextAsset?: () => void; @@ -25,6 +26,7 @@ cacheKey, loopVideo, playOriginalVideo, + showFullscreen = true, onPreviousAsset, onClose, onNextAsset, @@ -41,8 +43,10 @@ {/if} diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index 304b5b278e..24fa8c83a2 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -28,7 +28,7 @@ import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { memoryStore, type MemoryAsset } from '$lib/stores/memory.store.svelte'; - import { locale, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store'; + import { locale } from '$lib/stores/preferences.store'; import { preferences } from '$lib/stores/user.store'; import { getAssetMediaUrl, handlePromiseError, memoryLaneTitle } from '$lib/utils'; import { cancelMultiselect } from '$lib/utils/asset-utils'; @@ -303,7 +303,6 @@ $effect(() => { if (videoPlayer) { - videoPlayer.muted = $videoViewerMuted; initPlayer(); } }); @@ -407,9 +406,9 @@ shape="round" variant="ghost" color="secondary" - aria-label={$videoViewerMuted ? $t('unmute_memories') : $t('mute_memories')} - icon={$videoViewerMuted ? mdiVolumeOff : mdiVolumeHigh} - onclick={() => ($videoViewerMuted = !$videoViewerMuted)} + aria-label={videoPlayer?.muted ? $t('unmute_memories') : $t('mute_memories')} + icon={videoPlayer?.muted ? mdiVolumeOff : mdiVolumeHigh} + onclick={() => (videoPlayer ? (videoPlayer.muted = !videoPlayer.muted) : {})} /> {/if} @@ -483,12 +482,7 @@
{#key current.asset.id} {#if current.asset.isVideo} - + {:else} {/if} diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts index 9404d2bbdf..cf66bb488f 100644 --- a/web/src/lib/stores/preferences.store.ts +++ b/web/src/lib/stores/preferences.store.ts @@ -56,9 +56,6 @@ const persistedObject = (key: string, defaults: T) => export const mapSettings = persistedObject('map-settings', defaultMapSettings); -export const videoViewerVolume = persisted('video-viewer-volume', 1, {}); -export const videoViewerMuted = persisted('video-viewer-muted', false, {}); - export interface AlbumViewSettings { view: string; filter: string;