feat: add bulk asset selection support to large files utility page
parent
95e57a24cb
commit
c01e8f4eb4
|
|
@ -6,10 +6,23 @@
|
|||
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
onViewAsset: (asset: AssetResponseDto) => void;
|
||||
selected: boolean;
|
||||
selectionCandidate: boolean;
|
||||
onSelect: (asset: AssetResponseDto) => void;
|
||||
onClick: (asset: AssetResponseDto) => void;
|
||||
onPreview?: (asset: AssetResponseDto) => void;
|
||||
onMouseEvent?: (asset: AssetResponseDto | null) => void;
|
||||
}
|
||||
|
||||
let { asset, onViewAsset }: Props = $props();
|
||||
let {
|
||||
asset,
|
||||
selected,
|
||||
selectionCandidate,
|
||||
onSelect,
|
||||
onClick,
|
||||
onPreview = undefined,
|
||||
onMouseEvent = undefined,
|
||||
}: Props = $props();
|
||||
|
||||
let assetData = $derived(JSON.stringify(asset, null, 2));
|
||||
|
||||
|
|
@ -22,7 +35,16 @@
|
|||
title={assetData}
|
||||
>
|
||||
<div class="relative w-full h-full overflow-hidden rounded-lg">
|
||||
<Thumbnail asset={toTimelineAsset(asset)} readonly onClick={() => onViewAsset(asset)} thumbnailSize={boxWidth} />
|
||||
<Thumbnail
|
||||
asset={toTimelineAsset(asset)}
|
||||
thumbnailSize={boxWidth}
|
||||
onClick={() => onClick(asset)}
|
||||
onSelect={() => onSelect(asset)}
|
||||
onPreview={onPreview ? () => onPreview?.(asset) : undefined}
|
||||
onMouseEvent={({ isMouseOver }) => onMouseEvent?.(isMouseOver ? asset : null)}
|
||||
{selected}
|
||||
{selectionCandidate}
|
||||
/>
|
||||
|
||||
{#if !!asset.libraryId}
|
||||
<div class="absolute bottom-1 end-3 px-4 py-1 rounded-xl text-xs transition-colors bg-red-500">External</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,21 @@
|
|||
<script lang="ts">
|
||||
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import DeleteAssets from '$lib/components/timeline/actions/DeleteAssetsAction.svelte';
|
||||
import LargeAssetData from '$lib/components/utilities-page/large-assets/large-asset-data.svelte';
|
||||
import Portal from '$lib/elements/Portal.svelte';
|
||||
import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { getAssetBulkActions } from '$lib/services/asset.service';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { getNextAsset, getPreviousAsset } from '$lib/utils/asset-utils';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import { ActionButton, CommandPaletteDefaultProvider, IconButton } from '@immich/ui';
|
||||
import { mdiSelectAll } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
|
|
@ -17,9 +25,12 @@
|
|||
|
||||
let { data }: Props = $props();
|
||||
|
||||
let assets = $derived(data.assets);
|
||||
let assets = $state(data.assets);
|
||||
let asset = $derived(data.asset);
|
||||
|
||||
let shiftKeyIsDown = $state(false);
|
||||
let lastAssetMouseEvent = $state<AssetResponseDto | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (asset) {
|
||||
assetViewerManager.setAsset(asset);
|
||||
|
|
@ -36,29 +47,153 @@
|
|||
return asset;
|
||||
};
|
||||
|
||||
const onAssetsDelete = (assetIds: string[]) => {
|
||||
assets = assets.filter(({ id }) => !assetIds.includes(id));
|
||||
};
|
||||
|
||||
const onAction = (payload: Action) => {
|
||||
if (payload.type == 'trash') {
|
||||
assets = assets.filter((a) => a.id != payload.asset.id);
|
||||
if (payload.type == 'trash' || payload.type == 'delete') {
|
||||
onAssetsDelete([payload.asset.id]);
|
||||
assetViewerManager.showAssetViewer(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAssets = (asset: AssetResponseDto) => {
|
||||
const deselect = assetMultiSelectManager.hasSelectedAsset(asset.id);
|
||||
const timelineAsset = toTimelineAsset(asset);
|
||||
|
||||
if (deselect) {
|
||||
for (const candidate of assetMultiSelectManager.candidates) {
|
||||
assetMultiSelectManager.removeAssetFromMultiselectGroup(candidate.id);
|
||||
}
|
||||
assetMultiSelectManager.removeAssetFromMultiselectGroup(asset.id);
|
||||
} else {
|
||||
for (const candidate of assetMultiSelectManager.candidates) {
|
||||
assetMultiSelectManager.selectAsset(candidate);
|
||||
}
|
||||
assetMultiSelectManager.selectAsset(timelineAsset);
|
||||
}
|
||||
|
||||
assetMultiSelectManager.clearCandidates();
|
||||
assetMultiSelectManager.setAssetSelectionStart(deselect ? null : timelineAsset);
|
||||
};
|
||||
|
||||
const selectAssetCandidates = (endAsset: AssetResponseDto) => {
|
||||
if (!shiftKeyIsDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startAsset = assetMultiSelectManager.startAsset;
|
||||
if (!startAsset) {
|
||||
return;
|
||||
}
|
||||
|
||||
let start = assets.findIndex((a) => a.id === startAsset.id);
|
||||
let end = assets.findIndex((a) => a.id === endAsset.id);
|
||||
|
||||
if (start > end) {
|
||||
[start, end] = [end, start];
|
||||
}
|
||||
|
||||
assetMultiSelectManager.setAssetSelectionCandidates(assets.slice(start, end + 1).map((a) => toTimelineAsset(a)));
|
||||
};
|
||||
|
||||
const handleSelectAssetCandidates = (asset: AssetResponseDto | null) => {
|
||||
if (asset) {
|
||||
selectAssetCandidates(asset);
|
||||
}
|
||||
lastAssetMouseEvent = asset;
|
||||
};
|
||||
|
||||
const onViewAsset = async (asset: AssetResponseDto) => {
|
||||
await navigate({ targetRoute: 'current', assetId: asset.id });
|
||||
};
|
||||
|
||||
const handlePreview = async (asset: AssetResponseDto) => {
|
||||
await navigate({ targetRoute: 'current', assetId: asset.id });
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
assetMultiSelectManager.selectAssets(assets.map((item) => toTimelineAsset(item)));
|
||||
};
|
||||
|
||||
const handleOnClick = (clickedAsset: AssetResponseDto) => {
|
||||
if (assetMultiSelectManager.selectionActive) {
|
||||
handleSelectAssets(clickedAsset);
|
||||
return;
|
||||
}
|
||||
void onViewAsset(clickedAsset);
|
||||
};
|
||||
|
||||
const handleEscape = () => {
|
||||
if (assetMultiSelectManager.selectionActive) {
|
||||
assetMultiSelectManager.clear();
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Shift') {
|
||||
event.preventDefault();
|
||||
shiftKeyIsDown = true;
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
handleEscape();
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyUp = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Shift') {
|
||||
event.preventDefault();
|
||||
shiftKeyIsDown = false;
|
||||
}
|
||||
};
|
||||
|
||||
const assetCursor = $derived({
|
||||
current: assetViewerManager.asset!,
|
||||
nextAsset: getNextAsset(assets, assetViewerManager.asset),
|
||||
previousAsset: getPreviousAsset(assets, assetViewerManager.asset),
|
||||
});
|
||||
|
||||
const onAlbumAddAssets = () => {
|
||||
assetMultiSelectManager.clear();
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
if (!lastAssetMouseEvent) {
|
||||
assetMultiSelectManager.clearCandidates();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!shiftKeyIsDown) {
|
||||
assetMultiSelectManager.clearCandidates();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (shiftKeyIsDown && lastAssetMouseEvent) {
|
||||
selectAssetCandidates(lastAssetMouseEvent);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<UserPageLayout title={data.meta.title} scrollbar={true}>
|
||||
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} />
|
||||
|
||||
<OnEvents {onAssetsDelete} {onAlbumAddAssets} />
|
||||
|
||||
<UserPageLayout title={data.meta.title} scrollbar={true} hideNavbar={assetMultiSelectManager.selectionActive}>
|
||||
<div class="grid gap-2 grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6">
|
||||
{#if assets && data.assets.length > 0}
|
||||
{#if assets && assets.length > 0}
|
||||
{#each assets as asset (asset.id)}
|
||||
<LargeAssetData {asset} {onViewAsset} />
|
||||
<LargeAssetData
|
||||
{asset}
|
||||
selected={assetMultiSelectManager.hasSelectedAsset(asset.id)}
|
||||
selectionCandidate={assetMultiSelectManager.hasSelectionCandidate(asset.id)}
|
||||
onClick={handleOnClick}
|
||||
onSelect={handleSelectAssets}
|
||||
onPreview={assetMultiSelectManager.selectionActive ? handlePreview : undefined}
|
||||
onMouseEvent={handleSelectAssetCandidates}
|
||||
/>
|
||||
{/each}
|
||||
{:else}
|
||||
<p class="text-center text-lg dark:text-white flex place-items-center place-content-center">
|
||||
|
|
@ -68,6 +203,23 @@
|
|||
</div>
|
||||
</UserPageLayout>
|
||||
|
||||
{#if assetMultiSelectManager.selectionActive}
|
||||
<AssetSelectControlBar>
|
||||
{@const Actions = getAssetBulkActions($t)}
|
||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
||||
<ActionButton action={Actions.AddToAlbum} />
|
||||
<IconButton
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
aria-label={$t('select_all')}
|
||||
icon={mdiSelectAll}
|
||||
onclick={handleSelectAll}
|
||||
/>
|
||||
<DeleteAssets onAssetDelete={onAssetsDelete} />
|
||||
</AssetSelectControlBar>
|
||||
{/if}
|
||||
|
||||
{#if assetViewerManager.isViewing}
|
||||
{#await import('$lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
||||
<Portal target="body">
|
||||
|
|
|
|||
Loading…
Reference in New Issue