feat(web): Video memories on web (#16500)

* Video memories on web

* switched mixed up strings
This commit is contained in:
Yaros 2025-03-03 16:54:26 +01:00 committed by GitHub
parent 8b24c31d20
commit 7bbc1d9f68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 65 additions and 10 deletions

View File

@ -890,6 +890,7 @@
"month": "Month", "month": "Month",
"more": "More", "more": "More",
"moved_to_trash": "Moved to trash", "moved_to_trash": "Moved to trash",
"mute_memories": "Mute Memories",
"my_albums": "My albums", "my_albums": "My albums",
"name": "Name", "name": "Name",
"name_or_nickname": "Name or nickname", "name_or_nickname": "Name or nickname",
@ -1304,6 +1305,7 @@
"unnamed_album": "Unnamed Album", "unnamed_album": "Unnamed Album",
"unnamed_album_delete_confirmation": "Are you sure you want to delete this album?", "unnamed_album_delete_confirmation": "Are you sure you want to delete this album?",
"unnamed_share": "Unnamed Share", "unnamed_share": "Unnamed Share",
"unmute_memories": "Unmute Memories",
"unsaved_change": "Unsaved change", "unsaved_change": "Unsaved change",
"unselect_all": "Unselect all", "unselect_all": "Unselect all",
"unselect_all_duplicates": "Unselect all duplicates", "unselect_all_duplicates": "Unselect all duplicates",

View File

@ -28,13 +28,14 @@
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { type Viewport } from '$lib/stores/assets.store'; import { type Viewport } from '$lib/stores/assets.store';
import { loadMemories, memoryStore } from '$lib/stores/memory.store'; import { loadMemories, memoryStore } from '$lib/stores/memory.store';
import { locale } from '$lib/stores/preferences.store'; import { locale, videoViewerMuted } from '$lib/stores/preferences.store';
import { preferences } from '$lib/stores/user.store'; import { preferences } from '$lib/stores/user.store';
import { getAssetThumbnailUrl, handlePromiseError, memoryLaneTitle } from '$lib/utils'; import { getAssetPlaybackUrl, getAssetThumbnailUrl, handlePromiseError, memoryLaneTitle } from '$lib/utils';
import { cancelMultiselect } from '$lib/utils/asset-utils'; import { cancelMultiselect } from '$lib/utils/asset-utils';
import { fromLocalDateTime } from '$lib/utils/timeline-util'; import { fromLocalDateTime } from '$lib/utils/timeline-util';
import { import {
AssetMediaSize, AssetMediaSize,
AssetTypeEnum,
deleteMemory, deleteMemory,
removeMemoryAssets, removeMemoryAssets,
updateMemory, updateMemory,
@ -57,6 +58,8 @@
mdiPlay, mdiPlay,
mdiPlus, mdiPlus,
mdiSelectAll, mdiSelectAll,
mdiVolumeOff,
mdiVolumeHigh,
} from '@mdi/js'; } from '@mdi/js';
import type { NavigationTarget } from '@sveltejs/kit'; import type { NavigationTarget } from '@sveltejs/kit';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
@ -91,9 +94,10 @@
const { isViewing } = assetViewingStore; const { isViewing } = assetViewingStore;
const viewport: Viewport = $state({ width: 0, height: 0 }); const viewport: Viewport = $state({ width: 0, height: 0 });
const assetInteraction = new AssetInteraction(); const assetInteraction = new AssetInteraction();
const progressBarController = tweened<number>(0, { let progressBarController = tweened<number>(0, {
duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0), duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0),
}); });
let videoPlayer: HTMLVideoElement | undefined = $state();
const memories = storeDerived(memoryStore, (memories) => { const memories = storeDerived(memoryStore, (memories) => {
memories = memories ?? []; memories = memories ?? [];
const memoryAssets: MemoryAsset[] = []; const memoryAssets: MemoryAsset[] = [];
@ -139,8 +143,24 @@
return; return;
} }
// Adjust the progress bar duration to the video length
setProgressDuration(asset);
await goto(asHref(asset)); await goto(asHref(asset));
}; };
const setProgressDuration = (asset: AssetResponseDto) => {
if (asset.type === AssetTypeEnum.Video) {
const timeParts = asset.duration.split(':').map(Number);
const durationInMilliseconds = (timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2]) * 1000;
progressBarController = tweened<number>(0, {
duration: (from: number, to: number) => (to ? durationInMilliseconds * (to - from) : 0),
});
} else {
progressBarController = tweened<number>(0, {
duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0),
});
}
};
const handleNextAsset = () => handleNavigate(current?.next?.asset); const handleNextAsset = () => handleNavigate(current?.next?.asset);
const handlePreviousAsset = () => handleNavigate(current?.previous?.asset); const handlePreviousAsset = () => handleNavigate(current?.previous?.asset);
const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]); const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]);
@ -151,18 +171,21 @@
switch (action) { switch (action) {
case 'play': { case 'play': {
paused = false; paused = false;
await videoPlayer?.play();
await progressBarController.set(1); await progressBarController.set(1);
break; break;
} }
case 'pause': { case 'pause': {
paused = true; paused = true;
videoPlayer?.pause();
await progressBarController.set($progressBarController); await progressBarController.set($progressBarController);
break; break;
} }
case 'reset': { case 'reset': {
paused = false; paused = false;
videoPlayer?.pause();
resetPromise = progressBarController.set(0); resetPromise = progressBarController.set(0);
break; break;
} }
@ -203,6 +226,9 @@
} }
current = loadFromParams($memories, $page); current = loadFromParams($memories, $page);
// Adjust the progress bar duration to the video length
setProgressDuration(current.asset);
}; };
const handleDeleteMemoryAsset = async (current?: MemoryAsset) => { const handleDeleteMemoryAsset = async (current?: MemoryAsset) => {
@ -290,6 +316,12 @@
$effect(() => { $effect(() => {
handlePromiseError(handleAction(galleryInView ? 'pause' : 'play')); handlePromiseError(handleAction(galleryInView ? 'pause' : 'play'));
}); });
$effect(() => {
if (videoPlayer) {
videoPlayer.muted = $videoViewerMuted;
}
});
</script> </script>
<svelte:window <svelte:window
@ -373,6 +405,11 @@
{(current.assetIndex + 1).toLocaleString($locale)}/{current.memory.assets.length.toLocaleString($locale)} {(current.assetIndex + 1).toLocaleString($locale)}/{current.memory.assets.length.toLocaleString($locale)}
</p> </p>
</div> </div>
<CircleIconButton
title={$videoViewerMuted ? $t('unmute_memories') : $t('mute_memories')}
icon={$videoViewerMuted ? mdiVolumeOff : mdiVolumeHigh}
onclick={() => ($videoViewerMuted = !$videoViewerMuted)}
/>
</div> </div>
</ControlAppBar> </ControlAppBar>
@ -436,13 +473,29 @@
> >
<div class="relative h-full w-full rounded-2xl bg-black"> <div class="relative h-full w-full rounded-2xl bg-black">
{#key current.asset.id} {#key current.asset.id}
<img <div transition:fade class="h-full w-full">
transition:fade {#if current.asset.type == AssetTypeEnum.Video}
class="h-full w-full rounded-2xl object-contain transition-all" <video
src={getAssetThumbnailUrl({ id: current.asset.id, size: AssetMediaSize.Preview })} bind:this={videoPlayer}
alt={current.asset.exifInfo?.description} autoplay
draggable="false" playsinline
/> class="h-full w-full rounded-2xl object-contain transition-all"
src={getAssetPlaybackUrl({ id: current.asset.id })}
poster={getAssetThumbnailUrl({ id: current.asset.id, size: AssetMediaSize.Preview })}
draggable="false"
muted={$videoViewerMuted}
transition:fade
></video>
{:else}
<img
class="h-full w-full rounded-2xl object-contain transition-all"
src={getAssetThumbnailUrl({ id: current.asset.id, size: AssetMediaSize.Preview })}
alt={current.asset.exifInfo?.description}
draggable="false"
transition:fade
/>
{/if}
</div>
{/key} {/key}
<div <div