This commit is contained in:
hraabis 2026-08-15 13:40:06 +02:00 committed by GitHub
commit a468ede380
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 59 additions and 9 deletions

View file

@ -2024,6 +2024,8 @@
"skip_to_folders": "Skip to folders",
"skip_to_tags": "Skip to tags",
"slideshow": "Slideshow",
"slideshow_animate": "Animate slideshow",
"slideshow_animate_zoom_strength": "Zoom strength percentage",
"slideshow_body": "Sit back and watch your photos play in a full-screen slideshow.",
"slideshow_metadata_overlay_mode": "Overlay content",
"slideshow_metadata_overlay_mode_description_only": "Description only",

View file

@ -57,7 +57,7 @@
import Thumbhash from '$lib/components/Thumbhash.svelte';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { getAssetUrls } from '$lib/utils';
import { AdaptiveImageLoader, type QualityList } from '$lib/utils/adaptive-image-loader.svelte';
import { AdaptiveImageLoader, type ImageQuality, type QualityList } from '$lib/utils/adaptive-image-loader.svelte';
import { scaleToCover, scaleToFit, type Size } from '$lib/utils/container-utils';
import { getAltText } from '$lib/utils/thumbnail-util';
import { toTimelineAsset } from '$lib/utils/timeline-util';
@ -71,7 +71,7 @@
objectFit?: 'contain' | 'cover';
container: Size;
onUrlChange?: (url: string) => void;
onImageReady?: () => void;
onImageReady?: (quality: ImageQuality) => void;
onError?: () => void;
ref?: HTMLDivElement;
imgRef?: HTMLImageElement;

View file

@ -23,6 +23,7 @@
import { useSwipe, type SwipeCustomEvent } from 'svelte-gestures';
import { t } from 'svelte-i18n';
import type { AssetCursor } from './AssetViewer.svelte';
import { type ImageQuality } from '$lib/utils/adaptive-image-loader.svelte';
type Props = {
cursor: AssetCursor;
@ -35,7 +36,8 @@
let { cursor, element = $bindable(), sharedLink, onReady, onError, onSwipe }: Props = $props();
const { slideshowState, slideshowLook } = slideshowStore;
const { slideshowState, slideshowLook, slideshowAnimate, slideshowDelay, slideshowAnimateZoomStrength } =
slideshowStore;
const asset = $derived(cursor.current);
let visibleImageReady: boolean = $state(false);
@ -113,6 +115,25 @@
const onPlaySlideshow = () => ($slideshowState = SlideshowState.PlaySlideshow);
const animateSlideshow = () => {
let randomDirection = getRandomInt(0, 9);
if (randomDirection == 0) {
return;
}
let randomScale1 = (getRandomInt(0, 200) / 100) * ($slideshowAnimateZoomStrength / 100) + 1;
let duration = $slideshowDelay * 1000 - 600;
let randomDuration = Math.round(duration * (getRandomInt(50, 100) / 100));
let randomScale2 = (getRandomInt(0, 200) / 100) * ($slideshowAnimateZoomStrength / 100) + 1;
if (randomDirection <= 2) {
assetViewerManager.animatedZoom(randomScale1, randomDuration, true);
} else if (randomDirection > 2) {
assetViewerManager.animatedZoom(randomScale1, 20);
setTimeout(() => {
assetViewerManager.animatedZoom(randomScale2, randomDuration - 40, true);
}, 40);
}
};
// TODO move to action + command palette
const onCopyShortcut = (event: KeyboardEvent) => {
// eslint-disable-next-line unicorn/no-unnecessary-global-this
@ -198,6 +219,12 @@
return result;
});
function getRandomInt(min: number, max: number): number {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max + 1 - min)) + min;
}
</script>
<AssetViewerEvents {onCopy} {onZoom} {onFaceEditModeChange} />
@ -227,8 +254,12 @@
{container}
objectFit={$slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.Cover ? 'cover' : 'contain'}
{onUrlChange}
onImageReady={() => {
onImageReady={(quality: ImageQuality) => {
visibleImageReady = true;
if (quality === 'thumbnail' && $slideshowState === SlideshowState.PlaySlideshow && $slideshowAnimate) {
animateSlideshow();
}
onReady?.();
}}
onError={() => {

View file

@ -1,6 +1,6 @@
import { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
import type { ZoomImageWheelState } from '@zoom-image/core';
import { cubicOut } from 'svelte/easing';
import { cubicOut, quadInOut } from 'svelte/easing';
import { authManager } from '$lib/managers/auth-manager.svelte';
import type { ImageLoaderStatus } from '$lib/utils/adaptive-image-loader.svelte';
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
@ -142,7 +142,7 @@ class AssetViewerManager extends BaseEventManager<Events> {
this.#animationFrameId = null;
}
animatedZoom(targetZoom: number, duration = 300) {
animatedZoom(targetZoom: number, duration = 300, easeInOut = false) {
this.cancelZoomAnimation();
const startZoom = this.#zoomState.currentZoom;
@ -151,7 +151,7 @@ class AssetViewerManager extends BaseEventManager<Events> {
const frame = (currentTime: number) => {
const elapsed = currentTime - startTime;
const linearProgress = Math.min(elapsed / duration, 1);
const easedProgress = cubicOut(linearProgress);
const easedProgress = easeInOut ? quadInOut(linearProgress) : cubicOut(linearProgress);
const interpolatedZoom = startZoom + (targetZoom - startZoom) * easedProgress;
this.zoomState = { ...this.#zoomState, currentZoom: interpolatedZoom };

View file

@ -22,6 +22,8 @@
const {
slideshowDelay,
showProgressBar,
slideshowAnimate,
slideshowAnimateZoomStrength,
slideshowNavigation,
slideshowLook,
slideshowTransition,
@ -41,6 +43,8 @@
// Temporary variables to hold the settings - marked as reactive with $state() but initialized with store values
let tempSlideshowDelay = $state($slideshowDelay);
let tempShowProgressBar = $state($showProgressBar);
let tempSlideshowAnimate = $state($slideshowAnimate);
let tempSlideshowAnimateZoomStrength = $state($slideshowAnimateZoomStrength);
let tempSlideshowNavigation = $state($slideshowNavigation);
let tempSlideshowLook = $state($slideshowLook);
let tempSlideshowTransition = $state($slideshowTransition);
@ -84,6 +88,8 @@
const onSubmit = () => {
$slideshowDelay = tempSlideshowDelay;
$showProgressBar = tempShowProgressBar;
$slideshowAnimate = tempSlideshowAnimate;
$slideshowAnimateZoomStrength = tempSlideshowAnimateZoomStrength;
$slideshowNavigation = tempSlideshowNavigation;
$slideshowLook = tempSlideshowLook;
$slideshowTransition = tempSlideshowTransition;
@ -151,5 +157,12 @@
<NumberInput min={1} bind:value={tempSlideshowDelay} />
<HelperText>{$t('admin.slideshow_duration_description')}</HelperText>
</Field>
<Field label={$t('slideshow_animate')}>
<Switch bind:checked={tempSlideshowAnimate} />
</Field>
<Field label={$t('slideshow_animate_zoom_strength')}>
<NumberInput bind:value={tempSlideshowAnimateZoomStrength} min={1} max={100} disabled={!tempSlideshowAnimate} />
</Field>
</div>
</FormModal>

View file

@ -36,6 +36,8 @@ function createSlideshowStore() {
const slideshowState = writable<SlideshowState>(SlideshowState.None);
const showProgressBar = persisted<boolean>('slideshow-show-progressbar', true);
const slideshowAnimate = persisted<boolean>('slideshow-animate', false);
const slideshowAnimateZoomStrength = persisted<number>('slideshow-animate-zoom-strength', 50, {});
const slideshowDelay = persisted<number>('slideshow-delay', 5, {});
const slideshowTransition = persisted<boolean>('slideshow-transition', true);
const slideshowAutoplay = persisted<boolean>('slideshow-autoplay', true, {});
@ -83,6 +85,8 @@ function createSlideshowStore() {
slideshowRepeat,
slideshowShowMetadataOverlay,
slideshowMetadataOverlayMode,
slideshowAnimate,
slideshowAnimateZoomStrength,
};
}

View file

@ -14,7 +14,7 @@ export type ImageLoaderStatus = {
type ImageLoaderCallbacks = {
onUrlChange?: (url: string) => void;
onImageReady?: () => void;
onImageReady?: (quality: ImageQuality) => void;
onError?: () => void;
};
@ -99,7 +99,7 @@ export class AdaptiveImageLoader {
this.highestLoadedQualityIndex = index;
this.status.quality[quality] = 'success';
this.callbacks?.onUrlChange?.(this.qualityConfigs[quality].url);
this.callbacks?.onImageReady?.();
this.callbacks?.onImageReady?.(quality);
config.onAfterLoad?.(this);
}