refactor(web): asset-viewer code improvements

Change-Id: Ic8e750f4d6429a374c3a3f98542db8d36a6a6964
This commit is contained in:
midzelis
2026-04-05 21:50:56 +00:00
parent 4f33aed350
commit 4957bb15d3
4 changed files with 70 additions and 72 deletions
@@ -108,6 +108,10 @@
let sharedLink = getSharedLink(); let sharedLink = getSharedLink();
let fullscreenElement = $state<Element>(); let fullscreenElement = $state<Element>();
let slideShowPlaying = $derived($slideshowState === SlideshowState.PlaySlideshow);
let slideShowAscending = $derived($slideshowNavigation === SlideshowNavigation.AscendingOrder);
let slideShowShuffle = $derived($slideshowNavigation === SlideshowNavigation.Shuffle);
let playOriginalVideo = $state($alwaysLoadOriginalVideo); let playOriginalVideo = $state($alwaysLoadOriginalVideo);
let slideshowStartAssetId = $state<string>(); let slideshowStartAssetId = $state<string>();
@@ -141,12 +145,6 @@
} }
}; };
const onAssetUpdate = (updatedAsset: AssetResponseDto) => {
if (asset.id === updatedAsset.id) {
cursor = { ...cursor, current: updatedAsset };
}
};
let detailPanelTransitionName = $state<string | undefined>(); let detailPanelTransitionName = $state<string | undefined>();
let navigationBarTransitionName = $state<string | undefined>(); let navigationBarTransitionName = $state<string | undefined>();
let previousButtonTransitionName = $state<string | undefined>(); let previousButtonTransitionName = $state<string | undefined>();
@@ -230,68 +228,65 @@
assetViewerManager.closeEditor(); assetViewerManager.closeEditor();
}; };
const completeNavigation = async (order: 'previous' | 'next') => {
preloadManager.cancelBeforeNavigation(order);
let hasNext: boolean;
if (slideShowPlaying && slideShowShuffle) {
let next = order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next();
if (!next) {
const asset = await onRandom?.();
if (asset) {
slideshowHistory.queue(asset);
next = true;
}
}
hasNext = next;
} else {
const target = order === 'previous' ? previousAsset : nextAsset;
hasNext = await navigateToAsset(target);
}
if (!slideShowPlaying) {
return;
}
if (hasNext) {
$restartSlideshowProgress = true;
return;
}
if ($slideshowRepeat && slideshowStartAssetId) {
await assetViewerManager.setAssetId(slideshowStartAssetId);
$restartSlideshowProgress = true;
return;
}
await handleStopSlideshow();
};
const tracker = new InvocationTracker(); const tracker = new InvocationTracker();
let navigating = $state(false); let navigating = $state(false);
const navigateAsset = (order?: 'previous' | 'next') => { const navigateAsset = (order?: 'previous' | 'next') => {
if (!order) { if (!order) {
if ($slideshowState === SlideshowState.PlaySlideshow) { if (slideShowPlaying) {
order = $slideshowNavigation === SlideshowNavigation.AscendingOrder ? 'previous' : 'next'; order = slideShowAscending ? 'previous' : 'next';
} else { } else {
return; return;
} }
} }
preloadManager.cancelBeforeNavigation(order);
if (tracker.isActive()) { if (tracker.isActive()) {
return; return;
} }
navigating = true; navigating = true;
const navigation = tracker.invoke(async () => { void tracker
const isShuffle = .invoke(() => completeNavigation(order), $t('error_while_navigating'))
$slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle; .finally(() => (navigating = false));
let hasNext: boolean;
if (isShuffle) {
hasNext = order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next();
if (!hasNext) {
const asset = await onRandom?.();
if (asset) {
slideshowHistory.queue(asset);
hasNext = true;
}
}
} else {
hasNext =
order === 'previous' ? await navigateToAsset(cursor.previousAsset) : await navigateToAsset(cursor.nextAsset);
}
if ($slideshowState !== SlideshowState.PlaySlideshow) {
return;
}
if (hasNext) {
$restartSlideshowProgress = true;
return;
}
if ($slideshowRepeat && slideshowStartAssetId) {
await assetViewerManager.setAssetId(slideshowStartAssetId);
$restartSlideshowProgress = true;
return;
}
await handleStopSlideshow();
}, $t('error_while_navigating'));
void navigation.finally(() => (navigating = false));
}; };
/**
* Slide show mode
*/
let assetViewerHtmlElement = $state<HTMLElement>(); let assetViewerHtmlElement = $state<HTMLElement>();
const slideshowHistory = new SlideshowHistory((asset) => { const slideshowHistory = new SlideshowHistory((asset) => {
@@ -316,9 +311,11 @@
const handleStopSlideshow = async () => { const handleStopSlideshow = async () => {
try { try {
if (document.fullscreenElement) { if (!document.fullscreenElement) {
await document.exitFullscreen(); return;
} }
document.body.style.cursor = '';
await document.exitFullscreen();
} catch (error) { } catch (error) {
handleError(error, $t('errors.unable_to_exit_fullscreen')); handleError(error, $t('errors.unable_to_exit_fullscreen'));
} finally { } finally {
@@ -421,14 +418,21 @@
return; return;
} }
if (lastCursor) { if (lastCursor) {
previewStackedAsset = undefined;
ocrManager.showOverlay = false;
preloadManager.updateAfterNavigation(lastCursor, cursor, sharedLink); preloadManager.updateAfterNavigation(lastCursor, cursor, sharedLink);
} } else {
if (!lastCursor) {
preloadManager.initializePreloads(cursor, sharedLink); preloadManager.initializePreloads(cursor, sharedLink);
} }
lastCursor = cursor; lastCursor = cursor;
}); });
const onAssetUpdate = (update: AssetResponseDto) => {
if (asset.id === update.id) {
cursor = { ...cursor, current: update };
}
};
const viewerKind = $derived.by(() => { const viewerKind = $derived.by(() => {
if (previewStackedAsset) { if (previewStackedAsset) {
return previewStackedAsset.type === AssetTypeEnum.Image ? 'PhotoViewer' : 'StackVideoViewer'; return previewStackedAsset.type === AssetTypeEnum.Image ? 'PhotoViewer' : 'StackVideoViewer';
@@ -504,7 +508,6 @@
use:focusTrap use:focusTrap
bind:this={assetViewerHtmlElement} bind:this={assetViewerHtmlElement}
> >
<!-- Top navigation bar -->
{#if $slideshowState === SlideshowState.None && !assetViewerManager.isShowEditor} {#if $slideshowState === SlideshowState.None && !assetViewerManager.isShowEditor}
<div <div
class="col-span-4 col-start-1 row-span-1 row-start-1 transition-transform" class="col-span-4 col-start-1 row-span-1 row-start-1 transition-transform"
@@ -551,11 +554,11 @@
</div> </div>
{/if} {/if}
<!-- Asset Viewer -->
<div data-viewer-content class="z-[-1] relative col-start-1 col-span-4 row-start-1 row-span-full"> <div data-viewer-content class="z-[-1] relative col-start-1 col-span-4 row-start-1 row-span-full">
{#if viewerKind === 'StackVideoViewer'} {#if viewerKind === 'StackVideoViewer'}
<VideoViewer <VideoViewer
asset={previewStackedAsset!} asset={previewStackedAsset!}
assetId={previewStackedAsset!.id}
cacheKey={previewStackedAsset!.thumbhash} cacheKey={previewStackedAsset!.thumbhash}
projectionType={previewStackedAsset!.exifInfo?.projectionType} projectionType={previewStackedAsset!.exifInfo?.projectionType}
loopVideo={true} loopVideo={true}
@@ -647,7 +650,7 @@
</div> </div>
{/if} {/if}
{#if stack && withStacked && !assetViewerManager.isShowEditor} {#if stack && withStacked && $slideshowState === SlideshowState.None && !assetViewerManager.isShowEditor}
{@const stackedAssets = stack.assets} {@const stackedAssets = stack.assets}
<div id="stack-slideshow" class="absolute bottom-0 w-full col-span-4 col-start-1 pointer-events-none"> <div id="stack-slideshow" class="absolute bottom-0 w-full col-span-4 col-start-1 pointer-events-none">
<div class="relative flex flex-row no-wrap overflow-x-auto overflow-y-hidden horizontal-scrollbar"> <div class="relative flex flex-row no-wrap overflow-x-auto overflow-y-hidden horizontal-scrollbar">
@@ -35,6 +35,9 @@
let { cursor, element = $bindable(), sharedLink, onError, onSwipe }: Props = $props(); let { cursor, element = $bindable(), sharedLink, onError, onSwipe }: Props = $props();
const { slideshowState, slideshowLook } = slideshowStore; const { slideshowState, slideshowLook } = slideshowStore;
const objectFit = $derived(
$slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.Cover ? 'cover' : 'contain',
);
const asset = $derived(cursor.current); const asset = $derived(cursor.current);
let visibleImageReady: boolean = $state(false); let visibleImageReady: boolean = $state(false);
@@ -226,7 +229,7 @@
{asset} {asset}
{sharedLink} {sharedLink}
{container} {container}
objectFit={$slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.Cover ? 'cover' : 'contain'} {objectFit}
{onUrlChange} {onUrlChange}
onImageReady={() => { onImageReady={() => {
visibleImageReady = true; visibleImageReady = true;
@@ -58,7 +58,6 @@
}); });
$effect(() => { $effect(() => {
// reactive on `assetFileUrl` changes
if (assetFileUrl) { if (assetFileUrl) {
hasFocused = false; hasFocused = false;
videoPlayer?.load(); videoPlayer?.load();
@@ -9,8 +9,8 @@
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
import SkipLink from '$lib/elements/SkipLink.svelte'; import SkipLink from '$lib/elements/SkipLink.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { viewTransitionManager } from '$lib/managers/ViewTransitionManager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { viewTransitionManager } from '$lib/managers/ViewTransitionManager.svelte';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
import { getGlobalActions } from '$lib/services/app.service'; import { getGlobalActions } from '$lib/services/app.service';
import { mediaQueryManager } from '$lib/stores/media-query-manager.svelte'; import { mediaQueryManager } from '$lib/stores/media-query-manager.svelte';
@@ -37,16 +37,11 @@
let shouldShowNotificationPanel = $state(false); let shouldShowNotificationPanel = $state(false);
let innerWidth: number = $state(0); let innerWidth: number = $state(0);
const hasUnreadNotifications = $derived(notificationManager.notifications.length > 0); const hasUnreadNotifications = $derived(notificationManager.notifications.length > 0);
const { Cast } = $derived(getGlobalActions($t));
onMount(async () => {
try {
await notificationManager.refresh();
} catch (error) {
console.error('Failed to load notifications on mount', error);
}
});
onMount(() => { onMount(() => {
void notificationManager.refresh().catch((error) => console.error('Failed to load notifications on mount', error));
return viewTransitionManager.on({ return viewTransitionManager.on({
PrepareOldSnapshot: (types) => { PrepareOldSnapshot: (types) => {
if (types.includes('viewer')) { if (types.includes('viewer')) {
@@ -61,8 +56,6 @@
}, },
}); });
}); });
const { Cast } = $derived(getGlobalActions($t));
</script> </script>
<svelte:window bind:innerWidth /> <svelte:window bind:innerWidth />