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 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 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 navigationBarTransitionName = $state<string | undefined>();
let previousButtonTransitionName = $state<string | undefined>();
@@ -230,68 +228,65 @@
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();
let navigating = $state(false);
const navigateAsset = (order?: 'previous' | 'next') => {
if (!order) {
if ($slideshowState === SlideshowState.PlaySlideshow) {
order = $slideshowNavigation === SlideshowNavigation.AscendingOrder ? 'previous' : 'next';
if (slideShowPlaying) {
order = slideShowAscending ? 'previous' : 'next';
} else {
return;
}
}
preloadManager.cancelBeforeNavigation(order);
if (tracker.isActive()) {
return;
}
navigating = true;
const navigation = tracker.invoke(async () => {
const isShuffle =
$slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle;
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));
void tracker
.invoke(() => completeNavigation(order), $t('error_while_navigating'))
.finally(() => (navigating = false));
};
/**
* Slide show mode
*/
let assetViewerHtmlElement = $state<HTMLElement>();
const slideshowHistory = new SlideshowHistory((asset) => {
@@ -316,9 +311,11 @@
const handleStopSlideshow = async () => {
try {
if (document.fullscreenElement) {
await document.exitFullscreen();
if (!document.fullscreenElement) {
return;
}
document.body.style.cursor = '';
await document.exitFullscreen();
} catch (error) {
handleError(error, $t('errors.unable_to_exit_fullscreen'));
} finally {
@@ -421,14 +418,21 @@
return;
}
if (lastCursor) {
previewStackedAsset = undefined;
ocrManager.showOverlay = false;
preloadManager.updateAfterNavigation(lastCursor, cursor, sharedLink);
}
if (!lastCursor) {
} else {
preloadManager.initializePreloads(cursor, sharedLink);
}
lastCursor = cursor;
});
const onAssetUpdate = (update: AssetResponseDto) => {
if (asset.id === update.id) {
cursor = { ...cursor, current: update };
}
};
const viewerKind = $derived.by(() => {
if (previewStackedAsset) {
return previewStackedAsset.type === AssetTypeEnum.Image ? 'PhotoViewer' : 'StackVideoViewer';
@@ -504,7 +508,6 @@
use:focusTrap
bind:this={assetViewerHtmlElement}
>
<!-- Top navigation bar -->
{#if $slideshowState === SlideshowState.None && !assetViewerManager.isShowEditor}
<div
class="col-span-4 col-start-1 row-span-1 row-start-1 transition-transform"
@@ -551,11 +554,11 @@
</div>
{/if}
<!-- Asset Viewer -->
<div data-viewer-content class="z-[-1] relative col-start-1 col-span-4 row-start-1 row-span-full">
{#if viewerKind === 'StackVideoViewer'}
<VideoViewer
asset={previewStackedAsset!}
assetId={previewStackedAsset!.id}
cacheKey={previewStackedAsset!.thumbhash}
projectionType={previewStackedAsset!.exifInfo?.projectionType}
loopVideo={true}
@@ -647,7 +650,7 @@
</div>
{/if}
{#if stack && withStacked && !assetViewerManager.isShowEditor}
{#if stack && withStacked && $slideshowState === SlideshowState.None && !assetViewerManager.isShowEditor}
{@const stackedAssets = stack.assets}
<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">
@@ -35,6 +35,9 @@
let { cursor, element = $bindable(), sharedLink, onError, onSwipe }: Props = $props();
const { slideshowState, slideshowLook } = slideshowStore;
const objectFit = $derived(
$slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.Cover ? 'cover' : 'contain',
);
const asset = $derived(cursor.current);
let visibleImageReady: boolean = $state(false);
@@ -226,7 +229,7 @@
{asset}
{sharedLink}
{container}
objectFit={$slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.Cover ? 'cover' : 'contain'}
{objectFit}
{onUrlChange}
onImageReady={() => {
visibleImageReady = true;
@@ -58,7 +58,6 @@
});
$effect(() => {
// reactive on `assetFileUrl` changes
if (assetFileUrl) {
hasFocused = false;
videoPlayer?.load();
@@ -9,8 +9,8 @@
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
import SkipLink from '$lib/elements/SkipLink.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 { viewTransitionManager } from '$lib/managers/ViewTransitionManager.svelte';
import { Route } from '$lib/route';
import { getGlobalActions } from '$lib/services/app.service';
import { mediaQueryManager } from '$lib/stores/media-query-manager.svelte';
@@ -37,16 +37,11 @@
let shouldShowNotificationPanel = $state(false);
let innerWidth: number = $state(0);
const hasUnreadNotifications = $derived(notificationManager.notifications.length > 0);
onMount(async () => {
try {
await notificationManager.refresh();
} catch (error) {
console.error('Failed to load notifications on mount', error);
}
});
const { Cast } = $derived(getGlobalActions($t));
onMount(() => {
void notificationManager.refresh().catch((error) => console.error('Failed to load notifications on mount', error));
return viewTransitionManager.on({
PrepareOldSnapshot: (types) => {
if (types.includes('viewer')) {
@@ -61,8 +56,6 @@
},
});
});
const { Cast } = $derived(getGlobalActions($t));
</script>
<svelte:window bind:innerWidth />