mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
chore(web): migration svelte 5 syntax (#13883)
This commit is contained in:
parent
9203a61709
commit
0b3742cf13
310 changed files with 6435 additions and 4176 deletions
|
|
@ -7,29 +7,49 @@
|
|||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
export let url: string;
|
||||
export let altText: string | undefined;
|
||||
export let title: string | null = null;
|
||||
export let heightStyle: string | undefined = undefined;
|
||||
export let widthStyle: string;
|
||||
export let base64ThumbHash: string | null = null;
|
||||
export let curve = false;
|
||||
export let shadow = false;
|
||||
export let circle = false;
|
||||
export let hidden = false;
|
||||
export let border = false;
|
||||
export let preload = true;
|
||||
export let hiddenIconClass = 'text-white';
|
||||
export let onComplete: (() => void) | undefined = undefined;
|
||||
interface Props {
|
||||
url: string;
|
||||
altText: string | undefined;
|
||||
title?: string | null;
|
||||
heightStyle?: string | undefined;
|
||||
widthStyle: string;
|
||||
base64ThumbHash?: string | null;
|
||||
curve?: boolean;
|
||||
shadow?: boolean;
|
||||
circle?: boolean;
|
||||
hidden?: boolean;
|
||||
border?: boolean;
|
||||
preload?: boolean;
|
||||
hiddenIconClass?: string;
|
||||
onComplete?: (() => void) | undefined;
|
||||
onClick?: (() => void) | undefined;
|
||||
}
|
||||
|
||||
let {
|
||||
url,
|
||||
altText,
|
||||
title = null,
|
||||
heightStyle = undefined,
|
||||
widthStyle,
|
||||
base64ThumbHash = null,
|
||||
curve = false,
|
||||
shadow = false,
|
||||
circle = false,
|
||||
hidden = false,
|
||||
border = false,
|
||||
preload = true,
|
||||
hiddenIconClass = 'text-white',
|
||||
onComplete = undefined,
|
||||
}: Props = $props();
|
||||
|
||||
let {
|
||||
IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION },
|
||||
} = TUNABLES;
|
||||
|
||||
let loaded = false;
|
||||
let errored = false;
|
||||
let loaded = $state(false);
|
||||
let errored = $state(false);
|
||||
|
||||
let img: HTMLImageElement;
|
||||
let img = $state<HTMLImageElement>();
|
||||
|
||||
const setLoaded = () => {
|
||||
loaded = true;
|
||||
|
|
@ -40,20 +60,22 @@
|
|||
onComplete?.();
|
||||
};
|
||||
onMount(() => {
|
||||
if (img.complete) {
|
||||
if (img?.complete) {
|
||||
setLoaded();
|
||||
}
|
||||
});
|
||||
|
||||
$: optionalClasses = [
|
||||
curve && 'rounded-xl',
|
||||
circle && 'rounded-full',
|
||||
shadow && 'shadow-lg',
|
||||
(circle || !heightStyle) && 'aspect-square',
|
||||
border && 'border-[3px] border-immich-dark-primary/80 hover:border-immich-primary',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
let optionalClasses = $derived(
|
||||
[
|
||||
curve && 'rounded-xl',
|
||||
circle && 'rounded-full',
|
||||
shadow && 'shadow-lg',
|
||||
(circle || !heightStyle) && 'aspect-square',
|
||||
border && 'border-[3px] border-immich-dark-primary/80 hover:border-immich-primary',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if errored}
|
||||
|
|
@ -61,8 +83,8 @@
|
|||
{:else}
|
||||
<img
|
||||
bind:this={img}
|
||||
on:load={setLoaded}
|
||||
on:error={setErrored}
|
||||
onload={setLoaded}
|
||||
onerror={setErrored}
|
||||
loading={preload ? 'eager' : 'lazy'}
|
||||
style:width={widthStyle}
|
||||
style:height={heightStyle}
|
||||
|
|
|
|||
|
|
@ -31,62 +31,89 @@
|
|||
import { TUNABLES } from '$lib/utils/tunables';
|
||||
import { thumbhash } from '$lib/actions/thumbhash';
|
||||
|
||||
export let asset: AssetResponseDto;
|
||||
export let dateGroup: DateGroup | undefined = undefined;
|
||||
export let assetStore: AssetStore | undefined = undefined;
|
||||
export let groupIndex = 0;
|
||||
export let thumbnailSize: number | undefined = undefined;
|
||||
export let thumbnailWidth: number | undefined = undefined;
|
||||
export let thumbnailHeight: number | undefined = undefined;
|
||||
export let selected = false;
|
||||
export let selectionCandidate = false;
|
||||
export let disabled = false;
|
||||
export let readonly = false;
|
||||
export let showArchiveIcon = false;
|
||||
export let showStackedIcon = true;
|
||||
export let disableMouseOver = false;
|
||||
export let intersectionConfig: {
|
||||
root?: HTMLElement;
|
||||
bottom?: string;
|
||||
top?: string;
|
||||
left?: string;
|
||||
priority?: number;
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
dateGroup?: DateGroup | undefined;
|
||||
assetStore?: AssetStore | undefined;
|
||||
groupIndex?: number;
|
||||
thumbnailSize?: number | undefined;
|
||||
thumbnailWidth?: number | undefined;
|
||||
thumbnailHeight?: number | undefined;
|
||||
selected?: boolean;
|
||||
selectionCandidate?: boolean;
|
||||
disabled?: boolean;
|
||||
} = {};
|
||||
readonly?: boolean;
|
||||
showArchiveIcon?: boolean;
|
||||
showStackedIcon?: boolean;
|
||||
disableMouseOver?: boolean;
|
||||
intersectionConfig?: {
|
||||
root?: HTMLElement;
|
||||
bottom?: string;
|
||||
top?: string;
|
||||
left?: string;
|
||||
priority?: number;
|
||||
disabled?: boolean;
|
||||
};
|
||||
retrieveElement?: boolean;
|
||||
onIntersected?: (() => void) | undefined;
|
||||
onClick?: ((asset: AssetResponseDto) => void) | undefined;
|
||||
onRetrieveElement?: ((elment: HTMLElement) => void) | undefined;
|
||||
onSelect?: ((asset: AssetResponseDto) => void) | undefined;
|
||||
onMouseEvent?: ((event: { isMouseOver: boolean; selectedGroupIndex: number }) => void) | undefined;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export let retrieveElement: boolean = false;
|
||||
export let onIntersected: (() => void) | undefined = undefined;
|
||||
export let onClick: ((asset: AssetResponseDto) => void) | undefined = undefined;
|
||||
export let onRetrieveElement: ((elment: HTMLElement) => void) | undefined = undefined;
|
||||
export let onSelect: ((asset: AssetResponseDto) => void) | undefined = undefined;
|
||||
export let onMouseEvent: ((event: { isMouseOver: boolean; selectedGroupIndex: number }) => void) | undefined =
|
||||
undefined;
|
||||
|
||||
let className = '';
|
||||
export { className as class };
|
||||
let {
|
||||
asset,
|
||||
dateGroup = undefined,
|
||||
assetStore = undefined,
|
||||
groupIndex = 0,
|
||||
thumbnailSize = undefined,
|
||||
thumbnailWidth = undefined,
|
||||
thumbnailHeight = undefined,
|
||||
selected = false,
|
||||
selectionCandidate = false,
|
||||
disabled = false,
|
||||
readonly = false,
|
||||
showArchiveIcon = false,
|
||||
showStackedIcon = true,
|
||||
disableMouseOver = false,
|
||||
intersectionConfig = {},
|
||||
retrieveElement = false,
|
||||
onIntersected = undefined,
|
||||
onClick = undefined,
|
||||
onRetrieveElement = undefined,
|
||||
onSelect = undefined,
|
||||
onMouseEvent = undefined,
|
||||
class: className = '',
|
||||
}: Props = $props();
|
||||
|
||||
let {
|
||||
IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION },
|
||||
} = TUNABLES;
|
||||
|
||||
const componentId = generateId();
|
||||
let element: HTMLElement | undefined;
|
||||
let mouseOver = false;
|
||||
let intersecting = false;
|
||||
let lastRetrievedElement: HTMLElement | undefined;
|
||||
let loaded = false;
|
||||
let element: HTMLElement | undefined = $state();
|
||||
let mouseOver = $state(false);
|
||||
let intersecting = $state(false);
|
||||
let lastRetrievedElement: HTMLElement | undefined = $state();
|
||||
let loaded = $state(false);
|
||||
|
||||
$: if (!retrieveElement) {
|
||||
lastRetrievedElement = undefined;
|
||||
}
|
||||
$: if (retrieveElement && element && lastRetrievedElement !== element) {
|
||||
lastRetrievedElement = element;
|
||||
onRetrieveElement?.(element);
|
||||
}
|
||||
$effect(() => {
|
||||
if (!retrieveElement) {
|
||||
lastRetrievedElement = undefined;
|
||||
}
|
||||
});
|
||||
$effect(() => {
|
||||
if (retrieveElement && element && lastRetrievedElement !== element) {
|
||||
lastRetrievedElement = element;
|
||||
onRetrieveElement?.(element);
|
||||
}
|
||||
});
|
||||
|
||||
$: width = thumbnailSize || thumbnailWidth || 235;
|
||||
$: height = thumbnailSize || thumbnailHeight || 235;
|
||||
$: display = intersecting;
|
||||
let width = $derived(thumbnailSize || thumbnailWidth || 235);
|
||||
let height = $derived(thumbnailSize || thumbnailHeight || 235);
|
||||
let display = $derived(intersecting);
|
||||
|
||||
const onIconClickedHandler = (e?: MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
|
|
@ -197,15 +224,15 @@
|
|||
class="group"
|
||||
class:cursor-not-allowed={disabled}
|
||||
class:cursor-pointer={!disabled}
|
||||
on:mouseenter={onMouseEnter}
|
||||
on:mouseleave={onMouseLeave}
|
||||
on:keypress={(evt) => {
|
||||
onmouseenter={onMouseEnter}
|
||||
onmouseleave={onMouseLeave}
|
||||
onkeypress={(evt) => {
|
||||
if (evt.key === 'Enter') {
|
||||
callClickHandlers();
|
||||
}
|
||||
}}
|
||||
tabindex={0}
|
||||
on:click={handleClick}
|
||||
onclick={handleClick}
|
||||
role="link"
|
||||
>
|
||||
{#if mouseOver && !disableMouseOver}
|
||||
|
|
@ -216,7 +243,7 @@
|
|||
style:width="{width}px"
|
||||
style:height="{height}px"
|
||||
href={currentUrlReplaceAssetId(asset.id)}
|
||||
on:click={(evt) => evt.preventDefault()}
|
||||
onclick={(evt) => evt.preventDefault()}
|
||||
tabindex={0}
|
||||
aria-label="Thumbnail URL"
|
||||
>
|
||||
|
|
@ -227,7 +254,7 @@
|
|||
{#if !readonly && (mouseOver || selected || selectionCandidate)}
|
||||
<button
|
||||
type="button"
|
||||
on:click={onIconClickedHandler}
|
||||
onclick={onIconClickedHandler}
|
||||
class="absolute p-2 focus:outline-none"
|
||||
class:cursor-not-allowed={disabled}
|
||||
role="checkbox"
|
||||
|
|
|
|||
|
|
@ -7,31 +7,47 @@
|
|||
import { generateId } from '$lib/utils/generate-id';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
export let assetStore: AssetStore | undefined = undefined;
|
||||
export let url: string;
|
||||
export let durationInSeconds = 0;
|
||||
export let enablePlayback = false;
|
||||
export let playbackOnIconHover = false;
|
||||
export let showTime = true;
|
||||
export let curve = false;
|
||||
export let playIcon = mdiPlayCircleOutline;
|
||||
export let pauseIcon = mdiPauseCircleOutline;
|
||||
interface Props {
|
||||
assetStore?: AssetStore | undefined;
|
||||
url: string;
|
||||
durationInSeconds?: number;
|
||||
enablePlayback?: boolean;
|
||||
playbackOnIconHover?: boolean;
|
||||
showTime?: boolean;
|
||||
curve?: boolean;
|
||||
playIcon?: string;
|
||||
pauseIcon?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
assetStore = undefined,
|
||||
url,
|
||||
durationInSeconds = 0,
|
||||
enablePlayback = $bindable(false),
|
||||
playbackOnIconHover = false,
|
||||
showTime = true,
|
||||
curve = false,
|
||||
playIcon = mdiPlayCircleOutline,
|
||||
pauseIcon = mdiPauseCircleOutline,
|
||||
}: Props = $props();
|
||||
|
||||
const componentId = generateId();
|
||||
let remainingSeconds = durationInSeconds;
|
||||
let loading = true;
|
||||
let error = false;
|
||||
let player: HTMLVideoElement;
|
||||
let remainingSeconds = $state(durationInSeconds);
|
||||
let loading = $state(true);
|
||||
let error = $state(false);
|
||||
let player: HTMLVideoElement | undefined = $state();
|
||||
|
||||
$: if (!enablePlayback) {
|
||||
// Reset remaining time when playback is disabled.
|
||||
remainingSeconds = durationInSeconds;
|
||||
$effect(() => {
|
||||
if (!enablePlayback) {
|
||||
// Reset remaining time when playback is disabled.
|
||||
remainingSeconds = durationInSeconds;
|
||||
|
||||
if (player) {
|
||||
// Cancel video buffering.
|
||||
player.src = '';
|
||||
if (player) {
|
||||
// Cancel video buffering.
|
||||
player.src = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const onMouseEnter = () => {
|
||||
if (assetStore) {
|
||||
assetStore.taskManager.queueScrollSensitiveTask({
|
||||
|
|
@ -78,8 +94,8 @@
|
|||
</span>
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<span class="pr-2 pt-2" on:mouseenter={onMouseEnter} on:mouseleave={onMouseLeave}>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<span class="pr-2 pt-2" onmouseenter={onMouseEnter} onmouseleave={onMouseLeave}>
|
||||
{#if enablePlayback}
|
||||
{#if loading}
|
||||
<LoadingSpinner />
|
||||
|
|
@ -103,15 +119,19 @@
|
|||
autoplay
|
||||
loop
|
||||
src={url}
|
||||
on:play={() => {
|
||||
onplay={() => {
|
||||
loading = false;
|
||||
error = false;
|
||||
}}
|
||||
on:error={() => {
|
||||
onerror={() => {
|
||||
if (!player?.src) {
|
||||
// Do not show error when the URL is empty.
|
||||
return;
|
||||
}
|
||||
error = true;
|
||||
loading = false;
|
||||
}}
|
||||
on:timeupdate={({ currentTarget }) => {
|
||||
ontimeupdate={({ currentTarget }) => {
|
||||
const remaining = currentTarget.duration - currentTarget.currentTime;
|
||||
remainingSeconds = Math.min(
|
||||
Math.ceil(Number.isNaN(remaining) ? Number.POSITIVE_INFINITY : remaining),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue