0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-03-11 02:23:09 -05:00

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

View file

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