mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
Merge 0640f8393f into ffc83eae36
This commit is contained in:
commit
a468ede380
7 changed files with 59 additions and 9 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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={() => {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue