chore(web): migration svelte 5 syntax (#13883)

This commit is contained in:
Alex
2024-11-14 08:43:25 -06:00
committed by GitHub
parent 9203a61709
commit 0b3742cf13
310 changed files with 6435 additions and 4176 deletions

View File

@@ -8,10 +8,14 @@
import { t } from 'svelte-i18n';
import type { OnAddToAlbum } from '$lib/utils/actions';
export let shared = false;
export let onAddToAlbum: OnAddToAlbum = () => {};
interface Props {
shared?: boolean;
onAddToAlbum?: OnAddToAlbum;
}
let showAlbumPicker = false;
let { shared = false, onAddToAlbum = () => {} }: Props = $props();
let showAlbumPicker = $state(false);
const { getAssets } = getAssetControlContext();

View File

@@ -7,15 +7,18 @@
import { archiveAssets } from '$lib/utils/asset-utils';
import { t } from 'svelte-i18n';
export let onArchive: OnArchive;
interface Props {
onArchive: OnArchive;
menuItem?: boolean;
unarchive?: boolean;
}
export let menuItem = false;
export let unarchive = false;
let { onArchive, menuItem = false, unarchive = false }: Props = $props();
$: text = unarchive ? $t('unarchive') : $t('to_archive');
$: icon = unarchive ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline;
let text = $derived(unarchive ? $t('unarchive') : $t('to_archive'));
let icon = $derived(unarchive ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline);
let loading = false;
let loading = $state(false);
const { clearSelect, getOwnedAssets } = getAssetControlContext();
@@ -38,8 +41,8 @@
{#if !menuItem}
{#if loading}
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} onclick={() => {}} />
{:else}
<CircleIconButton title={text} {icon} on:click={handleArchive} />
<CircleIconButton title={text} {icon} onclick={handleArchive} />
{/if}
{/if}

View File

@@ -10,16 +10,16 @@
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
import { t } from 'svelte-i18n';
export let jobs: AssetJobName[] = [
AssetJobName.RegenerateThumbnail,
AssetJobName.RefreshMetadata,
AssetJobName.TranscodeVideo,
];
interface Props {
jobs?: AssetJobName[];
}
let { jobs = [AssetJobName.RegenerateThumbnail, AssetJobName.RefreshMetadata, AssetJobName.TranscodeVideo] }: Props =
$props();
const { clearSelect, getOwnedAssets } = getAssetControlContext();
// svelte-ignore reactive_declaration_non_reactive_property
$: isAllVideos = [...getOwnedAssets()].every((asset) => asset.type === AssetTypeEnum.Video);
let isAllVideos = $derived([...getOwnedAssets()].every((asset) => asset.type === AssetTypeEnum.Video));
const handleRunJob = async (name: AssetJobName) => {
try {

View File

@@ -9,10 +9,14 @@
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
import { mdiCalendarEditOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
export let menuItem = false;
interface Props {
menuItem?: boolean;
}
let { menuItem = false }: Props = $props();
const { clearSelect, getOwnedAssets } = getAssetControlContext();
let isShowChangeDate = false;
let isShowChangeDate = $state(false);
const handleConfirm = async (dateTimeOriginal: string) => {
isShowChangeDate = false;

View File

@@ -9,10 +9,14 @@
import { mdiMapMarkerMultipleOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
export let menuItem = false;
interface Props {
menuItem?: boolean;
}
let { menuItem = false }: Props = $props();
const { clearSelect, getOwnedAssets } = getAssetControlContext();
let isShowChangeLocation = false;
let isShowChangeLocation = $state(false);
async function handleConfirm(point: { lng: number; lat: number }) {
isShowChangeLocation = false;

View File

@@ -5,11 +5,11 @@
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
import { t } from 'svelte-i18n';
let showModal = false;
let showModal = $state(false);
const { getAssets } = getAssetControlContext();
</script>
<CircleIconButton title={$t('share')} icon={mdiShareVariantOutline} on:click={() => (showModal = true)} />
<CircleIconButton title={$t('share')} icon={mdiShareVariantOutline} onclick={() => (showModal = true)} />
{#if showModal}
<CreateSharedLinkModal assetIds={[...getAssets()].map(({ id }) => id)} onClose={() => (showModal = false)} />

View File

@@ -8,16 +8,20 @@
import DeleteAssetDialog from '../delete-asset-dialog.svelte';
import { t } from 'svelte-i18n';
export let onAssetDelete: OnDelete;
export let menuItem = false;
export let force = !$featureFlags.trash;
interface Props {
onAssetDelete: OnDelete;
menuItem?: boolean;
force?: boolean;
}
let { onAssetDelete, menuItem = false, force = !$featureFlags.trash }: Props = $props();
const { clearSelect, getOwnedAssets } = getAssetControlContext();
let isShowConfirmation = false;
let loading = false;
let isShowConfirmation = $state(false);
let loading = $state(false);
$: label = force ? $t('permanently_delete') : $t('delete');
let label = $derived(force ? $t('permanently_delete') : $t('delete'));
const handleTrash = async () => {
if (force) {
@@ -41,9 +45,9 @@
{#if menuItem}
<MenuOption text={label} icon={mdiDeleteOutline} onClick={handleTrash} />
{:else if loading}
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} onclick={() => {}} />
{:else}
<CircleIconButton title={label} icon={mdiDeleteForeverOutline} on:click={handleTrash} />
<CircleIconButton title={label} icon={mdiDeleteForeverOutline} onclick={handleTrash} />
{/if}
{#if isShowConfirmation}

View File

@@ -7,8 +7,12 @@
import { mdiCloudDownloadOutline, mdiFileDownloadOutline, mdiFolderDownloadOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
export let filename = 'immich.zip';
export let menuItem = false;
interface Props {
filename?: string;
menuItem?: boolean;
}
let { filename = 'immich.zip', menuItem = false }: Props = $props();
const { getAssets, clearSelect } = getAssetControlContext();
@@ -24,7 +28,7 @@
await downloadArchive(filename, { assetIds: assets.map((asset) => asset.id) });
};
$: menuItemIcon = getAssets().size === 1 ? mdiFileDownloadOutline : mdiFolderDownloadOutline;
let menuItemIcon = $derived(getAssets().size === 1 ? mdiFileDownloadOutline : mdiFolderDownloadOutline);
</script>
<svelte:window use:shortcut={{ shortcut: { key: 'd', shift: true }, onShortcut: handleDownloadFiles }} />
@@ -32,5 +36,5 @@
{#if menuItem}
<MenuOption text={$t('download')} icon={menuItemIcon} onClick={handleDownloadFiles} />
{:else}
<CircleIconButton title={$t('download')} icon={mdiCloudDownloadOutline} on:click={handleDownloadFiles} />
<CircleIconButton title={$t('download')} icon={mdiCloudDownloadOutline} onclick={handleDownloadFiles} />
{/if}

View File

@@ -12,15 +12,18 @@
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
import { t } from 'svelte-i18n';
export let onFavorite: OnFavorite;
interface Props {
onFavorite: OnFavorite;
menuItem?: boolean;
removeFavorite: boolean;
}
export let menuItem = false;
export let removeFavorite: boolean;
let { onFavorite, menuItem = false, removeFavorite }: Props = $props();
$: text = removeFavorite ? $t('remove_from_favorites') : $t('to_favorite');
$: icon = removeFavorite ? mdiHeartMinusOutline : mdiHeartOutline;
let text = $derived(removeFavorite ? $t('remove_from_favorites') : $t('to_favorite'));
let icon = $derived(removeFavorite ? mdiHeartMinusOutline : mdiHeartOutline);
let loading = false;
let loading = $state(false);
const { clearSelect, getOwnedAssets } = getAssetControlContext();
@@ -65,8 +68,8 @@
{#if !menuItem}
{#if loading}
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} onclick={() => {}} />
{:else}
<CircleIconButton title={text} {icon} on:click={handleFavorite} />
<CircleIconButton title={text} {icon} onclick={handleFavorite} />
{/if}
{/if}

View File

@@ -8,15 +8,19 @@
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
export let onLink: OnLink;
export let onUnlink: OnUnlink;
export let menuItem = false;
export let unlink = false;
interface Props {
onLink: OnLink;
onUnlink: OnUnlink;
menuItem?: boolean;
unlink?: boolean;
}
let loading = false;
let { onLink, onUnlink, menuItem = false, unlink = false }: Props = $props();
$: text = unlink ? $t('unlink_motion_video') : $t('link_motion_video');
$: icon = unlink ? mdiLinkOff : mdiMotionPlayOutline;
let loading = $state(false);
let text = $derived(unlink ? $t('unlink_motion_video') : $t('link_motion_video'));
let icon = $derived(unlink ? mdiLinkOff : mdiMotionPlayOutline);
const { clearSelect, getOwnedAssets } = getAssetControlContext();
@@ -68,8 +72,8 @@
{#if !menuItem}
{#if loading}
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} onclick={() => {}} />
{:else}
<CircleIconButton title={text} {icon} on:click={onClick} />
<CircleIconButton title={text} {icon} onclick={onClick} />
{/if}
{/if}

View File

@@ -11,9 +11,13 @@
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
import { t } from 'svelte-i18n';
export let album: AlbumResponseDto;
export let onRemove: ((assetIds: string[]) => void) | undefined;
export let menuItem = false;
interface Props {
album: AlbumResponseDto;
onRemove: ((assetIds: string[]) => void) | undefined;
menuItem?: boolean;
}
let { album = $bindable(), onRemove, menuItem = false }: Props = $props();
const { getAssets, clearSelect } = getAssetControlContext();
@@ -57,5 +61,5 @@
{#if menuItem}
<MenuOption text={$t('remove_from_album')} icon={mdiImageRemoveOutline} onClick={removeFromAlbum} />
{:else}
<CircleIconButton title={$t('remove_from_album')} icon={mdiDeleteOutline} on:click={removeFromAlbum} />
<CircleIconButton title={$t('remove_from_album')} icon={mdiDeleteOutline} onclick={removeFromAlbum} />
{/if}

View File

@@ -9,7 +9,11 @@
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
import { t } from 'svelte-i18n';
export let sharedLink: SharedLinkResponseDto;
interface Props {
sharedLink: SharedLinkResponseDto;
}
let { sharedLink = $bindable() }: Props = $props();
const { getAssets, clearSelect } = getAssetControlContext();
@@ -55,4 +59,4 @@
};
</script>
<CircleIconButton title={$t('remove_from_shared_link')} on:click={handleRemove} icon={mdiDeleteOutline} />
<CircleIconButton title={$t('remove_from_shared_link')} onclick={handleRemove} icon={mdiDeleteOutline} />

View File

@@ -12,11 +12,15 @@
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
import { t } from 'svelte-i18n';
export let onRestore: OnRestore | undefined;
interface Props {
onRestore: OnRestore | undefined;
}
let { onRestore }: Props = $props();
const { getAssets, clearSelect } = getAssetControlContext();
let loading = false;
let loading = $state(false);
const handleRestore = async () => {
loading = true;
@@ -40,7 +44,7 @@
};
</script>
<Button disabled={loading} size="sm" color="transparent-gray" shadow={false} rounded="lg" on:click={handleRestore}>
<Button disabled={loading} size="sm" color="transparent-gray" shadow={false} rounded="lg" onclick={handleRestore}>
<Icon path={mdiHistory} size="24" />
<span class="ml-2">{$t('restore')}</span>
</Button>

View File

@@ -6,8 +6,12 @@
import { selectAllAssets, cancelMultiselect } from '$lib/utils/asset-utils';
import { t } from 'svelte-i18n';
export let assetStore: AssetStore;
export let assetInteractionStore: AssetInteractionStore;
interface Props {
assetStore: AssetStore;
assetInteractionStore: AssetInteractionStore;
}
let { assetStore, assetInteractionStore }: Props = $props();
const handleSelectAll = async () => {
await selectAllAssets(assetStore, assetInteractionStore);
@@ -19,7 +23,7 @@
</script>
{#if $isSelectingAllAssets}
<CircleIconButton title={$t('unselect_all')} icon={mdiSelectRemove} on:click={handleCancel} />
<CircleIconButton title={$t('unselect_all')} icon={mdiSelectRemove} onclick={handleCancel} />
{:else}
<CircleIconButton title={$t('select_all')} icon={mdiSelectAll} on:click={handleSelectAll} />
<CircleIconButton title={$t('select_all')} icon={mdiSelectAll} onclick={handleSelectAll} />
{/if}

View File

@@ -6,9 +6,13 @@
import type { OnStack, OnUnstack } from '$lib/utils/actions';
import { t } from 'svelte-i18n';
export let unstack = false;
export let onStack: OnStack | undefined;
export let onUnstack: OnUnstack | undefined;
interface Props {
unstack?: boolean;
onStack: OnStack | undefined;
onUnstack: OnUnstack | undefined;
}
let { unstack = false, onStack, onUnstack }: Props = $props();
const { clearSelect, getOwnedAssets } = getAssetControlContext();

View File

@@ -7,13 +7,17 @@
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
import TagAssetForm from '$lib/components/forms/tag-asset-form.svelte';
export let menuItem = false;
interface Props {
menuItem?: boolean;
}
let { menuItem = false }: Props = $props();
const text = $t('tag');
const icon = mdiTagMultipleOutline;
let loading = false;
let isOpen = false;
let loading = $state(false);
let isOpen = $state(false);
const { clearSelect, getOwnedAssets } = getAssetControlContext();
@@ -36,9 +40,9 @@
{#if !menuItem}
{#if loading}
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} onclick={() => {}} />
{:else}
<CircleIconButton title={text} {icon} on:click={handleOpen} />
<CircleIconButton title={text} {icon} onclick={handleOpen} />
{/if}
{/if}

View File

@@ -22,7 +22,7 @@
import { TUNABLES } from '$lib/utils/tunables';
import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk';
import { throttle } from 'lodash-es';
import { onDestroy, onMount } from 'svelte';
import { onDestroy, onMount, type Snippet } from 'svelte';
import Portal from '../shared-components/portal/portal.svelte';
import Scrubber from '../shared-components/scrubber/scrubber.svelte';
import ShowShortcuts from '../shared-components/show-shortcuts.svelte';
@@ -38,80 +38,70 @@
import { generateId } from '$lib/utils/generate-id';
import { isTimelineScrolling } from '$lib/stores/timeline.store';
export let isSelectionMode = false;
export let singleSelect = false;
/** `true` if this asset grid is responds to navigation events; if `true`, then look at the
interface Props {
isSelectionMode?: boolean;
singleSelect?: boolean;
/** `true` if this asset grid is responds to navigation events; if `true`, then look at the
`AssetViewingStore.gridScrollTarget` and load and scroll to the asset specified, and
additionally, update the page location/url with the asset as the asset-grid is scrolled */
export let enableRouting: boolean;
enableRouting: boolean;
assetStore: AssetStore;
assetInteractionStore: AssetInteractionStore;
removeAction?: AssetAction.UNARCHIVE | AssetAction.ARCHIVE | AssetAction.FAVORITE | AssetAction.UNFAVORITE | null;
withStacked?: boolean;
showArchiveIcon?: boolean;
isShared?: boolean;
album?: AlbumResponseDto | null;
isShowDeleteConfirmation?: boolean;
onSelect?: (asset: AssetResponseDto) => void;
onEscape?: () => void;
children?: Snippet;
empty?: Snippet;
}
export let assetStore: AssetStore;
export let assetInteractionStore: AssetInteractionStore;
export let removeAction:
| AssetAction.UNARCHIVE
| AssetAction.ARCHIVE
| AssetAction.FAVORITE
| AssetAction.UNFAVORITE
| null = null;
export let withStacked = false;
export let showArchiveIcon = false;
export let isShared = false;
export let album: AlbumResponseDto | null = null;
export let isShowDeleteConfirmation = false;
export let onSelect: (asset: AssetResponseDto) => void = () => {};
export let onEscape: () => void = () => {};
let {
isSelectionMode = false,
singleSelect = false,
enableRouting,
assetStore = $bindable(),
assetInteractionStore,
removeAction = null,
withStacked = false,
showArchiveIcon = false,
isShared = false,
album = null,
isShowDeleteConfirmation = $bindable(false),
onSelect = () => {},
onEscape = () => {},
children,
empty,
}: Props = $props();
let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets, gridScrollTarget } = assetViewingStore;
const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } =
assetInteractionStore;
const viewport: ViewportXY = { width: 0, height: 0, x: 0, y: 0 };
const safeViewport: ViewportXY = { width: 0, height: 0, x: 0, y: 0 };
const viewport: ViewportXY = $state({ width: 0, height: 0, x: 0, y: 0 });
const safeViewport: ViewportXY = $state({ width: 0, height: 0, x: 0, y: 0 });
const componentId = generateId();
let element: HTMLElement;
let timelineElement: HTMLElement;
let showShortcuts = false;
let showSkeleton = true;
let element: HTMLElement | undefined = $state();
let timelineElement: HTMLElement | undefined = $state();
let showShortcuts = $state(false);
let showSkeleton = $state(true);
let internalScroll = false;
let navigating = false;
let preMeasure: AssetBucket[] = [];
let preMeasure: AssetBucket[] = $state([]);
let lastIntersectedBucketDate: string | undefined;
let scrubBucketPercent = 0;
let scrubBucket: { bucketDate: string | undefined } | undefined;
let scrubOverallPercent: number = 0;
let topSectionHeight = 0;
let topSectionOffset = 0;
let scrubBucketPercent = $state(0);
let scrubBucket: { bucketDate: string | undefined } | undefined = $state();
let scrubOverallPercent: number = $state(0);
let topSectionHeight = $state(0);
let topSectionOffset = $state(0);
// 60 is the bottom spacer element at 60px
let bottomSectionHeight = 60;
let leadout = false;
let leadout = $state(false);
$: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash;
$: isEmpty = $assetStore.initialized && $assetStore.buckets.length === 0;
$: idsSelectedAssets = [...$selectedAssets].map(({ id }) => id);
$: isAllArchived = [...$selectedAssets].every((asset) => asset.isArchived);
$: {
if (isEmpty) {
assetInteractionStore.clearMultiselect();
}
}
$: {
if (element && isViewportOrigin()) {
const rect = element.getBoundingClientRect();
viewport.height = rect.height;
viewport.width = rect.width;
viewport.x = rect.x;
viewport.y = rect.y;
}
if (!isViewportOrigin() && !isEqual(viewport, safeViewport)) {
safeViewport.height = viewport.height;
safeViewport.width = viewport.width;
safeViewport.x = viewport.x;
safeViewport.y = viewport.y;
updateViewport();
}
}
const {
ASSET_GRID: { NAVIGATE_ON_ASSET_IN_VIEW },
BUCKET: {
@@ -141,11 +131,11 @@
if ($gridScrollTarget?.at) {
void $assetStore.scheduleScrollToAssetId($gridScrollTarget, () => {
element.scrollTo({ top: 0 });
element?.scrollTo({ top: 0 });
showSkeleton = false;
});
} else {
element.scrollTo({ top: 0 });
element?.scrollTo({ top: 0 });
showSkeleton = false;
}
};
@@ -185,7 +175,7 @@
{ replaceState: true, forceNavigate: true },
);
} else {
element.scrollTo({ top: 0 });
element?.scrollTo({ top: 0 });
showSkeleton = false;
}
}, 500);
@@ -276,14 +266,24 @@
($assetStore.timelineHeight + bottomSectionHeight + topSectionHeight - safeViewport.height) /
($assetStore.timelineHeight + bottomSectionHeight + topSectionHeight);
const getMaxScroll = () =>
topSectionHeight + bottomSectionHeight + (timelineElement.clientHeight - element.clientHeight);
const getMaxScroll = () => {
if (!element || !timelineElement) {
return 0;
}
return topSectionHeight + bottomSectionHeight + (timelineElement.clientHeight - element.clientHeight);
};
const scrollToBucketAndOffset = (bucket: AssetBucket, bucketScrollPercent: number) => {
const topOffset = getOffset(bucket.bucketDate) + topSectionHeight + topSectionOffset;
const maxScrollPercent = getMaxScrollPercent();
const delta = bucket.bucketHeight * bucketScrollPercent;
const scrollTop = (topOffset + delta) * maxScrollPercent;
if (!element) {
return;
}
element.scrollTop = scrollTop;
};
@@ -297,6 +297,11 @@
const maxScroll = getMaxScroll();
const offset = maxScroll * scrollPercent;
if (!element) {
return;
}
element.scrollTop = offset;
} else {
const bucket = assetStore.buckets.find((b) => b.bucketDate === bucketDate);
@@ -344,6 +349,11 @@
}, 1000);
leadout = false;
if (!element) {
return;
}
if ($assetStore.timelineHeight < safeViewport.height * 2) {
// edge case - scroll limited due to size of content, must adjust - use the overall percent instead
const maxScroll = getMaxScroll();
@@ -409,7 +419,7 @@
: () => void 0;
const onScrollTarget: ScrollTargetListener = ({ bucket, offset }) => {
element.scrollTo({ top: offset });
element?.scrollTo({ top: offset });
if (!bucket.measured) {
preMeasure.push(bucket);
}
@@ -466,37 +476,10 @@
const focusElement = () => {
if (document.activeElement === document.body) {
element.focus();
element?.focus();
}
};
$: shortcutList = (() => {
if ($isSearchEnabled || $showAssetViewer) {
return [];
}
const shortcuts: ShortcutOptions[] = [
{ shortcut: { key: 'Escape' }, onShortcut: onEscape },
{ shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) },
{ shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets($assetStore, assetInteractionStore) },
{ shortcut: { key: 'PageDown' }, preventDefault: false, onShortcut: focusElement },
{ shortcut: { key: 'PageUp' }, preventDefault: false, onShortcut: focusElement },
];
if ($isMultiSelectState) {
shortcuts.push(
{ shortcut: { key: 'Delete' }, onShortcut: onDelete },
{ shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete },
{ shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
{ shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
{ shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
);
}
return shortcuts;
})();
const handleSelectAsset = (asset: AssetResponseDto) => {
if (!$assetStore.albumAssets.has(asset.id)) {
assetInteractionStore.selectAsset(asset);
@@ -585,13 +568,9 @@
}
};
let lastAssetMouseEvent: AssetResponseDto | null = null;
let lastAssetMouseEvent: AssetResponseDto | null = $state(null);
$: if (!lastAssetMouseEvent) {
assetInteractionStore.clearAssetSelectionCandidates();
}
let shiftKeyIsDown = false;
let shiftKeyIsDown = $state(false);
const deselectAllAssets = () => {
cancelMultiselect(assetInteractionStore);
@@ -619,14 +598,6 @@
}
};
$: if (!shiftKeyIsDown) {
assetInteractionStore.clearAssetSelectionCandidates();
}
$: if (shiftKeyIsDown && lastAssetMouseEvent) {
selectAssetCandidates(lastAssetMouseEvent);
}
const handleSelectAssetCandidates = (asset: AssetResponseDto | null) => {
if (asset) {
selectAssetCandidates(asset);
@@ -655,7 +626,7 @@
onSelect(asset);
if (singleSelect) {
if (singleSelect && element) {
element.scrollTop = 0;
return;
}
@@ -723,18 +694,18 @@
assetInteractionStore.setAssetSelectionStart(deselect ? null : asset);
};
const selectAssetCandidates = (asset: AssetResponseDto) => {
const selectAssetCandidates = (endAsset: AssetResponseDto) => {
if (!shiftKeyIsDown) {
return;
}
const rangeStart = $assetSelectionStart;
if (!rangeStart) {
const startAsset = $assetSelectionStart;
if (!startAsset) {
return;
}
let start = $assetStore.assets.indexOf(rangeStart);
let end = $assetStore.assets.indexOf(asset);
let start = $assetStore.assets.findIndex((a) => a.id === startAsset.id);
let end = $assetStore.assets.findIndex((a) => a.id === endAsset.id);
if (start > end) {
[start, end] = [end, start];
@@ -751,9 +722,83 @@
onDestroy(() => {
assetStore.taskManager.removeAllTasksForComponent(componentId);
});
let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
let isEmpty = $derived($assetStore.initialized && $assetStore.buckets.length === 0);
let idsSelectedAssets = $derived([...$selectedAssets].map(({ id }) => id));
let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived));
$effect(() => {
if (isEmpty) {
assetInteractionStore.clearMultiselect();
}
});
$effect(() => {
if (element && isViewportOrigin()) {
const rect = element.getBoundingClientRect();
viewport.height = rect.height;
viewport.width = rect.width;
viewport.x = rect.x;
viewport.y = rect.y;
}
if (!isViewportOrigin() && !isEqual(viewport, safeViewport)) {
safeViewport.height = viewport.height;
safeViewport.width = viewport.width;
safeViewport.x = viewport.x;
safeViewport.y = viewport.y;
updateViewport();
}
});
let shortcutList = $derived(
(() => {
if ($isSearchEnabled || $showAssetViewer) {
return [];
}
const shortcuts: ShortcutOptions[] = [
{ shortcut: { key: 'Escape' }, onShortcut: onEscape },
{ shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) },
{ shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets($assetStore, assetInteractionStore) },
{ shortcut: { key: 'PageDown' }, preventDefault: false, onShortcut: focusElement },
{ shortcut: { key: 'PageUp' }, preventDefault: false, onShortcut: focusElement },
];
if ($isMultiSelectState) {
shortcuts.push(
{ shortcut: { key: 'Delete' }, onShortcut: onDelete },
{ shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete },
{ shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
{ shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
{ shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
);
}
return shortcuts;
})(),
);
$effect(() => {
if (!lastAssetMouseEvent) {
assetInteractionStore.clearAssetSelectionCandidates();
}
});
$effect(() => {
if (!shiftKeyIsDown) {
assetInteractionStore.clearAssetSelectionCandidates();
}
});
$effect(() => {
if (shiftKeyIsDown && lastAssetMouseEvent) {
selectAssetCandidates(lastAssetMouseEvent);
}
});
</script>
<svelte:window on:keydown={onKeyDown} on:keyup={onKeyUp} on:selectstart={onSelectStart} use:shortcuts={shortcutList} />
<svelte:window onkeydown={onKeyDown} onkeyup={onKeyUp} onselectstart={onSelectStart} use:shortcuts={shortcutList} />
{#if isShowDeleteConfirmation}
<DeleteAssetDialog
@@ -789,16 +834,16 @@
tabindex="-1"
use:resizeObserver={({ height, width }) => ((viewport.width = width), (viewport.height = height))}
bind:this={element}
on:scroll={() => ((assetStore.lastScrollTime = Date.now()), handleTimelineScroll())}
onscroll={() => ((assetStore.lastScrollTime = Date.now()), handleTimelineScroll())}
>
<section
use:resizeObserver={({ target, height }) => ((topSectionHeight = height), (topSectionOffset = target.offsetTop))}
class:invisible={showSkeleton}
>
<slot />
{@render children?.()}
{#if isEmpty}
<!-- (optional) empty placeholder -->
<slot name="empty" />
{@render empty?.()}
{/if}
</section>

View File

@@ -1,4 +1,4 @@
<script lang="ts" context="module">
<script lang="ts" module>
import { createContext } from '$lib/utils/context';
import { t } from 'svelte-i18n';
@@ -17,10 +17,16 @@
import type { AssetResponseDto } from '@immich/sdk';
import { mdiClose } from '@mdi/js';
import ControlAppBar from '../shared-components/control-app-bar.svelte';
import type { Snippet } from 'svelte';
export let assets: Set<AssetResponseDto>;
export let clearSelect: () => void;
export let ownerId: string | undefined = undefined;
interface Props {
assets: Set<AssetResponseDto>;
clearSelect: () => void;
ownerId?: string | undefined;
children?: Snippet;
}
let { assets, clearSelect, ownerId = undefined, children }: Props = $props();
setContext({
getAssets: () => assets,
@@ -31,9 +37,13 @@
</script>
<ControlAppBar onClose={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">
<div class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading">
<p class="block sm:hidden">{assets.size}</p>
<p class="hidden sm:block">{$t('selected_count', { values: { count: assets.size } })}</p>
</div>
<slot slot="trailing" />
{#snippet leading()}
<div class="font-medium text-immich-primary dark:text-immich-dark-primary">
<p class="block sm:hidden">{assets.size}</p>
<p class="hidden sm:block">{$t('selected_count', { values: { count: assets.size } })}</p>
</div>
{/snippet}
{#snippet trailing()}
{@render children?.()}
{/snippet}
</ControlAppBar>

View File

@@ -5,11 +5,15 @@
import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte';
export let size: number;
export let onConfirm: () => void;
export let onCancel: () => void;
interface Props {
size: number;
onConfirm: () => void;
onCancel: () => void;
}
let checked = false;
let { size, onConfirm, onCancel }: Props = $props();
let checked = $state(false);
const handleConfirm = () => {
if (checked) {
@@ -25,10 +29,12 @@
onConfirm={handleConfirm}
{onCancel}
>
<svelte:fragment slot="prompt">
{#snippet promptSnippet()}
<p>
<FormatMessage key="permanently_delete_assets_prompt" values={{ count: size }} let:message>
<b>{message}</b>
<FormatMessage key="permanently_delete_assets_prompt" values={{ count: size }}>
{#snippet children({ message })}
<b>{message}</b>
{/snippet}
</FormatMessage>
</p>
<p><b>{$t('cannot_undo_this_action')}</b></p>
@@ -36,5 +42,5 @@
<div class="pt-4 flex justify-center items-center">
<Checkbox id="confirm-deletion-input" label={$t('do_not_show_again')} bind:checked />
</div>
</svelte:fragment>
{/snippet}
</ConfirmDialog>

View File

@@ -1,4 +1,4 @@
<script lang="ts" context="module">
<script lang="ts" module>
const recentTimes: number[] = [];
// TODO: track average time to measure, and use this to populate TUNABLES.ASSETS_STORE.CHECK_INTERVAL_MS
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -20,9 +20,13 @@
import { resizeObserver } from '$lib/actions/resize-observer';
import type { AssetBucket, AssetStore, BucketListener } from '$lib/stores/assets.store';
export let assetStore: AssetStore;
export let bucket: AssetBucket;
export let onMeasured: () => void;
interface Props {
assetStore: AssetStore;
bucket: AssetBucket;
onMeasured: () => void;
}
let { assetStore, bucket, onMeasured }: Props = $props();
async function _measure(element: Element) {
try {

View File

@@ -11,27 +11,29 @@
import { fade } from 'svelte/transition';
import { t } from 'svelte-i18n';
$: shouldRender = $memoryStore?.length > 0;
let shouldRender = $derived($memoryStore?.length > 0);
onMount(async () => {
const localTime = new Date();
$memoryStore = await getMemoryLane({ month: localTime.getMonth() + 1, day: localTime.getDate() });
});
let memoryLaneElement: HTMLElement;
let offsetWidth = 0;
let innerWidth = 0;
let memoryLaneElement: HTMLElement | undefined = $state();
let offsetWidth = $state(0);
let innerWidth = $state(0);
let scrollLeftPosition = 0;
let scrollLeftPosition = $state(0);
const onScroll = () => (scrollLeftPosition = memoryLaneElement?.scrollLeft);
const onScroll = () => {
scrollLeftPosition = memoryLaneElement?.scrollLeft ?? 0;
};
$: canScrollLeft = scrollLeftPosition > 0;
$: canScrollRight = Math.ceil(scrollLeftPosition) < innerWidth - offsetWidth;
let canScrollLeft = $derived(scrollLeftPosition > 0);
let canScrollRight = $derived(Math.ceil(scrollLeftPosition) < innerWidth - offsetWidth);
const scrollBy = 400;
const scrollLeft = () => memoryLaneElement.scrollBy({ left: -scrollBy, behavior: 'smooth' });
const scrollRight = () => memoryLaneElement.scrollBy({ left: scrollBy, behavior: 'smooth' });
const scrollLeft = () => memoryLaneElement?.scrollBy({ left: -scrollBy, behavior: 'smooth' });
const scrollRight = () => memoryLaneElement?.scrollBy({ left: scrollBy, behavior: 'smooth' });
</script>
{#if shouldRender}
@@ -40,7 +42,7 @@
bind:this={memoryLaneElement}
class="relative mt-5 overflow-x-hidden whitespace-nowrap transition-all"
use:resizeObserver={({ width }) => (offsetWidth = width)}
on:scroll={onScroll}
onscroll={onScroll}
>
{#if canScrollLeft || canScrollRight}
<div class="sticky left-0 z-20">
@@ -49,7 +51,7 @@
<button
type="button"
class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100"
on:click={scrollLeft}
onclick={scrollLeft}
>
<Icon path={mdiChevronLeft} size="36" /></button
>
@@ -60,7 +62,7 @@
<button
type="button"
class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100"
on:click={scrollRight}
onclick={scrollRight}
>
<Icon path={mdiChevronRight} size="36" /></button
>

View File

@@ -1,6 +1,10 @@
<script lang="ts">
export let title: string | null = null;
export let height: string | null = null;
interface Props {
title?: string | null;
height?: string | null;
}
let { title = null, height = null }: Props = $props();
</script>
<div class="overflow-clip" style={`height: ${height}`}>