mirror of
https://github.com/immich-app/immich.git
synced 2026-03-08 11:07:25 +03:00
chore(web): migration svelte 5 syntax (#13883)
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)} />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -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}`}>
|
||||
|
||||
Reference in New Issue
Block a user