handleSelectGroup(dateGroup.groupTitle, dateGroup.assets)}
on:keydown={() => handleSelectGroup(dateGroup.groupTitle, dateGroup.assets)}
>
- {#if $selectedGroup.has(dateGroup.groupTitle)}
+ {#if assetInteraction.selectedGroup.has(dateGroup.groupTitle)}
{:else}
@@ -212,8 +212,8 @@
onClick={(asset) => onClick(dateGroup.assets, dateGroup.groupTitle, asset)}
onSelect={(asset) => assetSelectHandler(asset, dateGroup.assets, dateGroup.groupTitle)}
onMouseEvent={() => assetMouseEventHandler(dateGroup.groupTitle, asset)}
- selected={$selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)}
- selectionCandidate={$assetSelectionCandidates.has(asset)}
+ selected={assetInteraction.selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)}
+ selectionCandidate={assetInteraction.assetSelectionCandidates.has(asset)}
disabled={$assetStore.albumAssets.has(asset.id)}
thumbnailWidth={box.width}
thumbnailHeight={box.height}
diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte
index 5055cdcf4b..cc64c6f02b 100644
--- a/web/src/lib/components/photos-page/asset-grid.svelte
+++ b/web/src/lib/components/photos-page/asset-grid.svelte
@@ -3,7 +3,6 @@
import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
import type { Action } from '$lib/components/asset-viewer/actions/action';
import { AppRoute, AssetAction } from '$lib/constants';
- import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetBucket, AssetStore, type BucketListener, type ViewportXY } from '$lib/stores/assets.store';
import { locale, showDeleteModal } from '$lib/stores/preferences.store';
@@ -37,6 +36,7 @@
import type { UpdatePayload } from 'vite';
import { generateId } from '$lib/utils/generate-id';
import { isTimelineScrolling } from '$lib/stores/timeline.store';
+ import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
interface Props {
isSelectionMode?: boolean;
@@ -46,7 +46,7 @@
additionally, update the page location/url with the asset as the asset-grid is scrolled */
enableRouting: boolean;
assetStore: AssetStore;
- assetInteractionStore: AssetInteractionStore;
+ assetInteraction: AssetInteraction;
removeAction?: AssetAction.UNARCHIVE | AssetAction.ARCHIVE | AssetAction.FAVORITE | AssetAction.UNFAVORITE | null;
withStacked?: boolean;
showArchiveIcon?: boolean;
@@ -64,7 +64,7 @@
singleSelect = false,
enableRouting,
assetStore = $bindable(),
- assetInteractionStore,
+ assetInteraction,
removeAction = null,
withStacked = false,
showArchiveIcon = false,
@@ -78,8 +78,6 @@
}: Props = $props();
let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets, gridScrollTarget } = assetViewingStore;
- const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } =
- assetInteractionStore;
const viewport: ViewportXY = $state({ width: 0, height: 0, x: 0, y: 0 });
const safeViewport: ViewportXY = $state({ width: 0, height: 0, x: 0, y: 0 });
@@ -437,11 +435,11 @@
(assetIds) => $assetStore.removeAssets(assetIds),
idsSelectedAssets,
);
- assetInteractionStore.clearMultiselect();
+ assetInteraction.clearMultiselect();
};
const onDelete = () => {
- const hasTrashedAsset = Array.from($selectedAssets).some((asset) => asset.isTrashed);
+ const hasTrashedAsset = assetInteraction.selectedAssetsArray.some((asset) => asset.isTrashed);
if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) {
isShowDeleteConfirmation = true;
@@ -459,7 +457,7 @@
};
const onStackAssets = async () => {
- const ids = await stackAssets(Array.from($selectedAssets));
+ const ids = await stackAssets(assetInteraction.selectedAssetsArray);
if (ids) {
$assetStore.removeAssets(ids);
onEscape();
@@ -467,7 +465,7 @@
};
const toggleArchive = async () => {
- const ids = await archiveAssets(Array.from($selectedAssets), !isAllArchived);
+ const ids = await archiveAssets(assetInteraction.selectedAssetsArray, !assetInteraction.isAllArchived);
if (ids) {
$assetStore.removeAssets(ids);
deselectAllAssets();
@@ -482,7 +480,7 @@
const handleSelectAsset = (asset: AssetResponseDto) => {
if (!$assetStore.albumAssets.has(asset.id)) {
- assetInteractionStore.selectAsset(asset);
+ assetInteraction.selectAsset(asset);
}
};
@@ -573,7 +571,7 @@
let shiftKeyIsDown = $state(false);
const deselectAllAssets = () => {
- cancelMultiselect(assetInteractionStore);
+ cancelMultiselect(assetInteraction);
};
const onKeyDown = (event: KeyboardEvent) => {
@@ -606,13 +604,13 @@
};
const handleGroupSelect = (group: string, assets: AssetResponseDto[]) => {
- if ($selectedGroup.has(group)) {
- assetInteractionStore.removeGroupFromMultiselectGroup(group);
+ if (assetInteraction.selectedGroup.has(group)) {
+ assetInteraction.removeGroupFromMultiselectGroup(group);
for (const asset of assets) {
- assetInteractionStore.removeAssetFromMultiselectGroup(asset);
+ assetInteraction.removeAssetFromMultiselectGroup(asset);
}
} else {
- assetInteractionStore.addGroupToMultiselectGroup(group);
+ assetInteraction.addGroupToMultiselectGroup(group);
for (const asset of assets) {
handleSelectAsset(asset);
}
@@ -631,26 +629,26 @@
return;
}
- const rangeSelection = $assetSelectionCandidates.size > 0;
- const deselect = $selectedAssets.has(asset);
+ const rangeSelection = assetInteraction.assetSelectionCandidates.size > 0;
+ const deselect = assetInteraction.selectedAssets.has(asset);
// Select/deselect already loaded assets
if (deselect) {
- for (const candidate of $assetSelectionCandidates || []) {
- assetInteractionStore.removeAssetFromMultiselectGroup(candidate);
+ for (const candidate of assetInteraction.assetSelectionCandidates) {
+ assetInteraction.removeAssetFromMultiselectGroup(candidate);
}
- assetInteractionStore.removeAssetFromMultiselectGroup(asset);
+ assetInteraction.removeAssetFromMultiselectGroup(asset);
} else {
- for (const candidate of $assetSelectionCandidates || []) {
+ for (const candidate of assetInteraction.assetSelectionCandidates) {
handleSelectAsset(candidate);
}
handleSelectAsset(asset);
}
- assetInteractionStore.clearAssetSelectionCandidates();
+ assetInteraction.clearAssetSelectionCandidates();
- if ($assetSelectionStart && rangeSelection) {
- let startBucketIndex = $assetStore.getBucketIndexByAssetId($assetSelectionStart.id);
+ if (assetInteraction.assetSelectionStart && rangeSelection) {
+ let startBucketIndex = $assetStore.getBucketIndexByAssetId(assetInteraction.assetSelectionStart.id);
let endBucketIndex = $assetStore.getBucketIndexByAssetId(asset.id);
if (startBucketIndex === null || endBucketIndex === null) {
@@ -667,7 +665,7 @@
await $assetStore.loadBucket(bucket.bucketDate);
for (const asset of bucket.assets) {
if (deselect) {
- assetInteractionStore.removeAssetFromMultiselectGroup(asset);
+ assetInteraction.removeAssetFromMultiselectGroup(asset);
} else {
handleSelectAsset(asset);
}
@@ -682,16 +680,16 @@
const assetsGroupByDate = splitBucketIntoDateGroups(bucket, $locale);
for (const dateGroup of assetsGroupByDate) {
const dateGroupTitle = formatGroupTitle(dateGroup.date);
- if (dateGroup.assets.every((a) => $selectedAssets.has(a))) {
- assetInteractionStore.addGroupToMultiselectGroup(dateGroupTitle);
+ if (dateGroup.assets.every((a) => assetInteraction.selectedAssets.has(a))) {
+ assetInteraction.addGroupToMultiselectGroup(dateGroupTitle);
} else {
- assetInteractionStore.removeGroupFromMultiselectGroup(dateGroupTitle);
+ assetInteraction.removeGroupFromMultiselectGroup(dateGroupTitle);
}
}
}
}
- assetInteractionStore.setAssetSelectionStart(deselect ? null : asset);
+ assetInteraction.setAssetSelectionStart(deselect ? null : asset);
};
const selectAssetCandidates = (endAsset: AssetResponseDto) => {
@@ -699,7 +697,7 @@
return;
}
- const startAsset = $assetSelectionStart;
+ const startAsset = assetInteraction.assetSelectionStart;
if (!startAsset) {
return;
}
@@ -711,11 +709,11 @@
[start, end] = [end, start];
}
- assetInteractionStore.setAssetSelectionCandidates($assetStore.assets.slice(start, end + 1));
+ assetInteraction.setAssetSelectionCandidates($assetStore.assets.slice(start, end + 1));
};
const onSelectStart = (e: Event) => {
- if ($isMultiSelectState && shiftKeyIsDown) {
+ if (assetInteraction.selectionActive && shiftKeyIsDown) {
e.preventDefault();
}
};
@@ -724,12 +722,11 @@
});
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));
+ let idsSelectedAssets = $derived(assetInteraction.selectedAssetsArray.map(({ id }) => id));
$effect(() => {
if (isEmpty) {
- assetInteractionStore.clearMultiselect();
+ assetInteraction.clearMultiselect();
}
});
@@ -760,12 +757,12 @@
{ 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: 'A', ctrl: true }, onShortcut: () => selectAllAssets($assetStore, assetInteraction) },
{ shortcut: { key: 'PageDown' }, preventDefault: false, onShortcut: focusElement },
{ shortcut: { key: 'PageUp' }, preventDefault: false, onShortcut: focusElement },
];
- if ($isMultiSelectState) {
+ if (assetInteraction.selectionActive) {
shortcuts.push(
{ shortcut: { key: 'Delete' }, onShortcut: onDelete },
{ shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete },
@@ -781,13 +778,13 @@
$effect(() => {
if (!lastAssetMouseEvent) {
- assetInteractionStore.clearAssetSelectionCandidates();
+ assetInteraction.clearAssetSelectionCandidates();
}
});
$effect(() => {
if (!shiftKeyIsDown) {
- assetInteractionStore.clearAssetSelectionCandidates();
+ assetInteraction.clearAssetSelectionCandidates();
}
});
@@ -889,7 +886,7 @@
{withStacked}
{showArchiveIcon}
{assetStore}
- {assetInteractionStore}
+ {assetInteraction}
{isSelectionMode}
{singleSelect}
{onScrollTarget}
diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte
index 5d625cef9d..ebc4b49001 100644
--- a/web/src/lib/components/share-page/individual-shared-viewer.svelte
+++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte
@@ -15,11 +15,11 @@
import ControlAppBar from '../shared-components/control-app-bar.svelte';
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
import { cancelMultiselect } from '$lib/utils/asset-utils';
- import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification';
import type { Viewport } from '$lib/stores/assets.store';
import { t } from 'svelte-i18n';
+ import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
interface Props {
sharedLink: SharedLinkResponseDto;
@@ -29,12 +29,10 @@
let { sharedLink = $bindable(), isOwned }: Props = $props();
const viewport: Viewport = $state({ width: 0, height: 0 });
- const assetInteractionStore = createAssetInteractionStore();
- const { selectedAssets } = assetInteractionStore;
+ const assetInteraction = new AssetInteraction();
let innerWidth: number = $state(0);
let assets = $derived(sharedLink.assets);
- let isMultiSelectionMode = $derived($selectedAssets.size > 0);
dragAndDropFilesStore.subscribe((value) => {
if (value.isDragging && value.files.length > 0) {
@@ -73,15 +71,18 @@
};
const handleSelectAll = () => {
- assetInteractionStore.selectAssets(assets);
+ assetInteraction.selectAssets(assets);
};
- {#if isMultiSelectionMode}
- cancelMultiselect(assetInteractionStore)}>
+ {#if assetInteraction.selectionActive}
+ cancelMultiselect(assetInteraction)}
+ >
{#if sharedLink?.allowDownload}
@@ -112,6 +113,6 @@
{/if}
diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
index eda340e7e2..8f8a067a90 100644
--- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
+++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
@@ -5,7 +5,6 @@
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
import { AppRoute, AssetAction } from '$lib/constants';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
- import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
import type { Viewport } from '$lib/stores/assets.store';
import { showDeleteModal } from '$lib/stores/preferences.store';
import { deleteAssets } from '$lib/utils/actions';
@@ -22,10 +21,11 @@
import Portal from '../portal/portal.svelte';
import { handlePromiseError } from '$lib/utils';
import DeleteAssetDialog from '../../photos-page/delete-asset-dialog.svelte';
+ import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
interface Props {
assets: AssetResponseDto[];
- assetInteractionStore: AssetInteractionStore;
+ assetInteraction: AssetInteraction;
disableAssetSelect?: boolean;
showArchiveIcon?: boolean;
viewport: Viewport;
@@ -38,7 +38,7 @@
let {
assets = $bindable(),
- assetInteractionStore = $bindable(),
+ assetInteraction,
disableAssetSelect = false,
showArchiveIcon = false,
viewport,
@@ -51,11 +51,8 @@
let { isViewing: isViewerOpen, asset: viewingAsset, setAsset } = assetViewingStore;
- const { assetSelectionCandidates, assetSelectionStart, selectedAssets, isMultiSelectState } = assetInteractionStore;
-
let showShortcuts = $state(false);
let currentViewAssetIndex = 0;
- let isMultiSelectionMode = $derived($selectedAssets.size > 0);
let shiftKeyIsDown = $state(false);
let lastAssetMouseEvent: AssetResponseDto | null = $state(null);
@@ -66,11 +63,11 @@
};
const selectAllAssets = () => {
- assetInteractionStore.selectAssets(assets);
+ assetInteraction.selectAssets(assets);
};
const deselectAllAssets = () => {
- cancelMultiselect(assetInteractionStore);
+ cancelMultiselect(assetInteraction);
};
const onKeyDown = (event: KeyboardEvent) => {
@@ -91,23 +88,23 @@
if (!asset) {
return;
}
- const deselect = $selectedAssets.has(asset);
+ const deselect = assetInteraction.selectedAssets.has(asset);
// Select/deselect already loaded assets
if (deselect) {
- for (const candidate of $assetSelectionCandidates || []) {
- assetInteractionStore.removeAssetFromMultiselectGroup(candidate);
+ for (const candidate of assetInteraction.assetSelectionCandidates) {
+ assetInteraction.removeAssetFromMultiselectGroup(candidate);
}
- assetInteractionStore.removeAssetFromMultiselectGroup(asset);
+ assetInteraction.removeAssetFromMultiselectGroup(asset);
} else {
- for (const candidate of $assetSelectionCandidates || []) {
- assetInteractionStore.selectAsset(candidate);
+ for (const candidate of assetInteraction.assetSelectionCandidates) {
+ assetInteraction.selectAsset(candidate);
}
- assetInteractionStore.selectAsset(asset);
+ assetInteraction.selectAsset(asset);
}
- assetInteractionStore.clearAssetSelectionCandidates();
- assetInteractionStore.setAssetSelectionStart(deselect ? null : asset);
+ assetInteraction.clearAssetSelectionCandidates();
+ assetInteraction.setAssetSelectionStart(deselect ? null : asset);
};
const handleSelectAssetCandidates = (asset: AssetResponseDto | null) => {
@@ -122,7 +119,7 @@
return;
}
- const startAsset = $assetSelectionStart;
+ const startAsset = assetInteraction.assetSelectionStart;
if (!startAsset) {
return;
}
@@ -134,17 +131,17 @@
[start, end] = [end, start];
}
- assetInteractionStore.setAssetSelectionCandidates(assets.slice(start, end + 1));
+ assetInteraction.setAssetSelectionCandidates(assets.slice(start, end + 1));
};
const onSelectStart = (e: Event) => {
- if ($isMultiSelectState && shiftKeyIsDown) {
+ if (assetInteraction.selectionActive && shiftKeyIsDown) {
e.preventDefault();
}
};
const onDelete = () => {
- const hasTrashedAsset = Array.from($selectedAssets).some((asset) => asset.isTrashed);
+ const hasTrashedAsset = assetInteraction.selectedAssetsArray.some((asset) => asset.isTrashed);
if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) {
isShowDeleteConfirmation = true;
@@ -168,11 +165,11 @@
(assetIds) => (assets = assets.filter((asset) => !assetIds.includes(asset.id))),
idsSelectedAssets,
);
- assetInteractionStore.clearMultiselect();
+ assetInteraction.clearMultiselect();
};
const toggleArchive = async () => {
- const ids = await archiveAssets(Array.from($selectedAssets), !isAllArchived);
+ const ids = await archiveAssets(assetInteraction.selectedAssetsArray, !assetInteraction.isAllArchived);
if (ids) {
assets.filter((asset) => !ids.includes(asset.id));
deselectAllAssets();
@@ -191,7 +188,7 @@
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets() },
];
- if ($isMultiSelectState) {
+ if (assetInteraction.selectionActive) {
shortcuts.push(
{ shortcut: { key: 'Escape' }, onShortcut: deselectAllAssets },
{ shortcut: { key: 'Delete' }, onShortcut: onDelete },
@@ -266,14 +263,13 @@
};
const assetMouseEventHandler = (asset: AssetResponseDto | null) => {
- if ($isMultiSelectState) {
+ if (assetInteraction.selectionActive) {
handleSelectAssetCandidates(asset);
}
};
let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
- let idsSelectedAssets = $derived([...$selectedAssets].map(({ id }) => id));
- let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived));
+ let idsSelectedAssets = $derived(assetInteraction.selectedAssetsArray.map(({ id }) => id));
let geometry = $derived(
(() => {
@@ -297,13 +293,13 @@
$effect(() => {
if (!lastAssetMouseEvent) {
- assetInteractionStore.clearAssetSelectionCandidates();
+ assetInteraction.clearAssetSelectionCandidates();
}
});
$effect(() => {
if (!shiftKeyIsDown) {
- assetInteractionStore.clearAssetSelectionCandidates();
+ assetInteraction.clearAssetSelectionCandidates();
}
});
@@ -318,7 +314,7 @@
{#if isShowDeleteConfirmation}
(isShowDeleteConfirmation = false)}
onConfirm={() => handlePromiseError(trashOrDelete(true))}
/>
@@ -340,7 +336,7 @@
{
- if (isMultiSelectionMode) {
+ if (assetInteraction.selectionActive) {
handleSelectAssets(asset);
return;
}
@@ -351,8 +347,8 @@
onIntersected={() => (i === Math.max(1, assets.length - 7) ? onIntersected?.() : void 0)}
{showArchiveIcon}
{asset}
- selected={$selectedAssets.has(asset)}
- selectionCandidate={$assetSelectionCandidates.has(asset)}
+ selected={assetInteraction.selectedAssets.has(asset)}
+ selectionCandidate={assetInteraction.assetSelectionCandidates.has(asset)}
thumbnailWidth={geometry.boxes[i].width}
thumbnailHeight={geometry.boxes[i].height}
/>
diff --git a/web/src/lib/stores/asset-interaction.store.ts b/web/src/lib/stores/asset-interaction.store.ts
deleted file mode 100644
index f7db5382b0..0000000000
--- a/web/src/lib/stores/asset-interaction.store.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import type { AssetResponseDto } from '@immich/sdk';
-import { derived, readonly, writable } from 'svelte/store';
-
-export type AssetInteractionStore = ReturnType;
-
-export function createAssetInteractionStore() {
- const selectedAssets = writable(new Set());
- const selectedGroup = writable(new Set());
- const isMultiSelectStoreState = derived(selectedAssets, ($selectedAssets) => $selectedAssets.size > 0);
-
- // Candidates for the range selection. This set includes only loaded assets, so it improves highlight
- // performance. From the user's perspective, range is highlighted almost immediately
- const assetSelectionCandidates = writable(new Set());
- // The beginning of the selection range
- const assetSelectionStart = writable(null);
-
- const selectAsset = (asset: AssetResponseDto) => {
- selectedAssets.update(($selectedAssets) => $selectedAssets.add(asset));
- };
-
- const selectAssets = (assets: AssetResponseDto[]) => {
- selectedAssets.update(($selectedAssets) => {
- for (const asset of assets) {
- $selectedAssets.add(asset);
- }
- return $selectedAssets;
- });
- };
-
- const removeAssetFromMultiselectGroup = (asset: AssetResponseDto) => {
- selectedAssets.update(($selectedAssets) => {
- $selectedAssets.delete(asset);
- return $selectedAssets;
- });
- };
-
- const addGroupToMultiselectGroup = (group: string) => {
- selectedGroup.update(($selectedGroup) => $selectedGroup.add(group));
- };
-
- const removeGroupFromMultiselectGroup = (group: string) => {
- selectedGroup.update(($selectedGroup) => {
- $selectedGroup.delete(group);
- return $selectedGroup;
- });
- };
-
- const setAssetSelectionStart = (asset: AssetResponseDto | null) => {
- assetSelectionStart.set(asset);
- };
-
- const setAssetSelectionCandidates = (assets: AssetResponseDto[]) => {
- assetSelectionCandidates.set(new Set(assets));
- };
-
- const clearAssetSelectionCandidates = () => {
- assetSelectionCandidates.set(new Set());
- };
-
- const clearMultiselect = () => {
- // Multi-selection
- selectedAssets.set(new Set());
- selectedGroup.set(new Set());
-
- // Range selection
- assetSelectionCandidates.set(new Set());
- assetSelectionStart.set(null);
- };
-
- return {
- selectAsset,
- selectAssets,
- removeAssetFromMultiselectGroup,
- addGroupToMultiselectGroup,
- removeGroupFromMultiselectGroup,
- setAssetSelectionCandidates,
- clearAssetSelectionCandidates,
- setAssetSelectionStart,
- clearMultiselect,
- isMultiSelectState: readonly(isMultiSelectStoreState),
- selectedAssets: readonly(selectedAssets),
- selectedGroup: readonly(selectedGroup),
- assetSelectionCandidates: readonly(assetSelectionCandidates),
- assetSelectionStart: readonly(assetSelectionStart),
- };
-}
diff --git a/web/src/lib/stores/asset-interaction.svelte.spec.ts b/web/src/lib/stores/asset-interaction.svelte.spec.ts
new file mode 100644
index 0000000000..5d3043b37c
--- /dev/null
+++ b/web/src/lib/stores/asset-interaction.svelte.spec.ts
@@ -0,0 +1,40 @@
+import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
+import { resetSavedUser, user } from '$lib/stores/user.store';
+import { assetFactory } from '@test-data/factories/asset-factory';
+import { userAdminFactory } from '@test-data/factories/user-factory';
+
+describe('AssetInteraction', () => {
+ let assetInteraction: AssetInteraction;
+
+ beforeEach(() => {
+ assetInteraction = new AssetInteraction();
+ });
+
+ it('calculates derived values from selection', () => {
+ assetInteraction.selectAsset(assetFactory.build({ isFavorite: true, isArchived: true, isTrashed: true }));
+ assetInteraction.selectAsset(assetFactory.build({ isFavorite: true, isArchived: false, isTrashed: false }));
+
+ expect(assetInteraction.selectionActive).toBe(true);
+ expect(assetInteraction.isAllTrashed).toBe(false);
+ expect(assetInteraction.isAllArchived).toBe(false);
+ expect(assetInteraction.isAllFavorite).toBe(true);
+ });
+
+ it('updates isAllUserOwned when the active user changes', () => {
+ const [user1, user2] = userAdminFactory.buildList(2);
+ assetInteraction.selectAsset(assetFactory.build({ ownerId: user1.id }));
+
+ const cleanup = $effect.root(() => {
+ expect(assetInteraction.isAllUserOwned).toBe(false);
+
+ user.set(user1);
+ expect(assetInteraction.isAllUserOwned).toBe(true);
+
+ user.set(user2);
+ expect(assetInteraction.isAllUserOwned).toBe(false);
+ });
+
+ cleanup();
+ resetSavedUser();
+ });
+});
diff --git a/web/src/lib/stores/asset-interaction.svelte.ts b/web/src/lib/stores/asset-interaction.svelte.ts
new file mode 100644
index 0000000000..4397c7f71f
--- /dev/null
+++ b/web/src/lib/stores/asset-interaction.svelte.ts
@@ -0,0 +1,66 @@
+import { user } from '$lib/stores/user.store';
+import type { AssetResponseDto, UserAdminResponseDto } from '@immich/sdk';
+import { SvelteSet } from 'svelte/reactivity';
+import { fromStore } from 'svelte/store';
+
+export class AssetInteraction {
+ readonly selectedAssets = new SvelteSet();
+ readonly selectedGroup = new SvelteSet();
+ assetSelectionCandidates = $state(new SvelteSet());
+ assetSelectionStart = $state(null);
+
+ selectionActive = $derived(this.selectedAssets.size > 0);
+ selectedAssetsArray = $derived([...this.selectedAssets]);
+
+ private user = fromStore(user);
+ private userId = $derived(this.user.current?.id);
+
+ isAllTrashed = $derived(this.selectedAssetsArray.every((asset) => asset.isTrashed));
+ isAllArchived = $derived(this.selectedAssetsArray.every((asset) => asset.isArchived));
+ isAllFavorite = $derived(this.selectedAssetsArray.every((asset) => asset.isFavorite));
+ isAllUserOwned = $derived(this.selectedAssetsArray.every((asset) => asset.ownerId === this.userId));
+
+ selectAsset(asset: AssetResponseDto) {
+ this.selectedAssets.add(asset);
+ }
+
+ selectAssets(assets: AssetResponseDto[]) {
+ for (const asset of assets) {
+ this.selectedAssets.add(asset);
+ }
+ }
+
+ removeAssetFromMultiselectGroup(asset: AssetResponseDto) {
+ this.selectedAssets.delete(asset);
+ }
+
+ addGroupToMultiselectGroup(group: string) {
+ this.selectedGroup.add(group);
+ }
+
+ removeGroupFromMultiselectGroup(group: string) {
+ this.selectedGroup.delete(group);
+ }
+
+ setAssetSelectionStart(asset: AssetResponseDto | null) {
+ this.assetSelectionStart = asset;
+ }
+
+ setAssetSelectionCandidates(assets: AssetResponseDto[]) {
+ this.assetSelectionCandidates = new SvelteSet(assets);
+ }
+
+ clearAssetSelectionCandidates() {
+ this.assetSelectionCandidates.clear();
+ }
+
+ clearMultiselect() {
+ // Multi-selection
+ this.selectedAssets.clear();
+ this.selectedGroup.clear();
+
+ // Range selection
+ this.assetSelectionCandidates.clear();
+ this.assetSelectionStart = null;
+ }
+}
diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts
index 37041ecbc4..5b06a66597 100644
--- a/web/src/lib/utils/asset-utils.ts
+++ b/web/src/lib/utils/asset-utils.ts
@@ -2,7 +2,7 @@ import { goto } from '$app/navigation';
import FormatBoldMessage from '$lib/components/i18n/format-bold-message.svelte';
import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification';
import { AppRoute } from '$lib/constants';
-import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
+import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store';
import { downloadManager } from '$lib/stores/download';
@@ -460,7 +460,7 @@ export const keepThisDeleteOthers = async (keepAsset: AssetResponseDto, stack: S
}
};
-export const selectAllAssets = async (assetStore: AssetStore, assetInteractionStore: AssetInteractionStore) => {
+export const selectAllAssets = async (assetStore: AssetStore, assetInteraction: AssetInteraction) => {
if (get(isSelectingAllAssets)) {
// Selection is already ongoing
return;
@@ -474,7 +474,7 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteractionSt
if (!get(isSelectingAllAssets)) {
break; // Cancelled
}
- assetInteractionStore.selectAssets(bucket.assets);
+ assetInteraction.selectAssets(bucket.assets);
// We use setTimeout to allow the UI to update. Otherwise, this may
// cause a long delay between the start of 'select all' and the
@@ -489,9 +489,9 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteractionSt
}
};
-export const cancelMultiselect = (assetInteractionStore: AssetInteractionStore) => {
+export const cancelMultiselect = (assetInteraction: AssetInteraction) => {
isSelectingAllAssets.set(false);
- assetInteractionStore.clearMultiselect();
+ assetInteraction.clearMultiselect();
};
export const toggleArchive = async (asset: AssetResponseDto) => {
diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 5c63d8e1a3..0f6c62a5fa 100644
--- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -35,7 +35,6 @@
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import { AppRoute, AlbumPageViewMode } from '$lib/constants';
import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
- import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore } from '$lib/stores/assets.store';
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
@@ -87,6 +86,7 @@
import { onDestroy } from 'svelte';
import { confirmAlbumDelete } from '$lib/utils/album-utils';
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
+ import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
interface Props {
data: PageData;
@@ -107,11 +107,8 @@
let reactions: ActivityResponseDto[] = $state([]);
let albumOrder: AssetOrder | undefined = $state(data.album.order);
- const assetInteractionStore = createAssetInteractionStore();
- const { isMultiSelectState, selectedAssets } = assetInteractionStore;
-
- const timelineInteractionStore = createAssetInteractionStore();
- const { selectedAssets: timelineSelected } = timelineInteractionStore;
+ const assetInteraction = new AssetInteraction();
+ const timelineInteraction = new AssetInteraction();
afterNavigate(({ from }) => {
let url: string | undefined = from?.url?.pathname;
@@ -234,8 +231,8 @@
if ($showAssetViewer) {
return;
}
- if ($isMultiSelectState) {
- cancelMultiselect(assetInteractionStore);
+ if (assetInteraction.selectionActive) {
+ cancelMultiselect(assetInteraction);
return;
}
await goto(backUrl);
@@ -245,9 +242,8 @@
const refreshAlbum = async () => {
album = await getAlbumInfo({ id: album.id, withoutAssets: true });
};
-
const handleAddAssets = async () => {
- const assetIds = [...$timelineSelected].map((asset) => asset.id);
+ const assetIds = timelineInteraction.selectedAssetsArray.map((asset) => asset.id);
try {
const results = await addAssetsToAlbum({
@@ -263,7 +259,7 @@
await refreshAlbum();
- timelineInteractionStore.clearMultiselect();
+ timelineInteraction.clearMultiselect();
await setModeToView();
} catch (error) {
handleError(error, $t('errors.error_adding_assets_to_album'));
@@ -284,13 +280,13 @@
};
const handleCloseSelectAssets = async () => {
- timelineInteractionStore.clearMultiselect();
+ timelineInteraction.clearMultiselect();
await setModeToView();
};
const handleSelectFromComputer = async () => {
await openFileUploadDialog({ albumId: album.id });
- timelineInteractionStore.clearMultiselect();
+ timelineInteraction.clearMultiselect();
await setModeToView();
};
@@ -359,16 +355,16 @@
}
viewMode = AlbumPageViewMode.VIEW;
- assetInteractionStore.clearMultiselect();
+ assetInteraction.clearMultiselect();
await updateThumbnail(assetId);
};
const updateThumbnailUsingCurrentSelection = async () => {
- if ($selectedAssets.size === 1) {
- const assetId = [...$selectedAssets][0].id;
- assetInteractionStore.clearMultiselect();
- await updateThumbnail(assetId);
+ if (assetInteraction.selectedAssets.size === 1) {
+ const [firstAsset] = assetInteraction.selectedAssets;
+ assetInteraction.clearMultiselect();
+ await updateThumbnail(firstAsset.id);
}
};
@@ -410,9 +406,6 @@
let timelineStore = $derived(new AssetStore({ isArchived: false, withPartners: true }, albumId));
let isOwned = $derived($user.id == album.ownerId);
- let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id));
- let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite));
- let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived));
let showActivityStatus = $derived(
album.albumUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0),
@@ -433,40 +426,50 @@
- {#if $isMultiSelectState}
-
assetInteractionStore.clearMultiselect()}>
+ {#if assetInteraction.selectionActive}
+ assetInteraction.clearMultiselect()}
+ >
-
+
- {#if isAllUserOwned}
- assetStore.triggerUpdate()} />
+ {#if assetInteraction.isAllUserOwned}
+ assetStore.triggerUpdate()}
+ />
{/if}
- {#if isAllUserOwned}
+ {#if assetInteraction.isAllUserOwned}
- {#if $selectedAssets.size === 1}
+ {#if assetInteraction.selectedAssets.size === 1}
updateThumbnailUsingCurrentSelection()}
/>
{/if}
- assetStore.triggerUpdate()} />
+ assetStore.triggerUpdate()}
+ />
{/if}
- {#if $preferences.tags.enabled && isAllUserOwned}
+ {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
{/if}
- {#if isOwned || isAllUserOwned}
+ {#if isOwned || assetInteraction.isAllUserOwned}
{/if}
- {#if isAllUserOwned}
+ {#if assetInteraction.isAllUserOwned}
{/if}
@@ -540,10 +543,10 @@
{#snippet leading()}
- {#if $timelineSelected.size === 0}
+ {#if !timelineInteraction.selectionActive}
{$t('add_to_album')}
{:else}
- {$t('selected_count', { values: { count: $timelineSelected.size } })}
+ {$t('selected_count', { values: { count: timelineInteraction.selectedAssets.size } })}
{/if}
{/snippet}
@@ -556,7 +559,7 @@
>
{$t('select_from_computer')}
-
{/snippet}
@@ -579,7 +582,7 @@
{:else}
@@ -587,7 +590,7 @@
enableRouting={true}
{album}
{assetStore}
- {assetInteractionStore}
+ {assetInteraction}
isShared={album.albumUsers.length > 0}
isSelectionMode={viewMode === AlbumPageViewMode.SELECT_THUMBNAIL}
singleSelect={viewMode === AlbumPageViewMode.SELECT_THUMBNAIL}
diff --git a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 3402dff960..5301364ccb 100644
--- a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -12,12 +12,12 @@
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AssetAction } from '$lib/constants';
- import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { AssetStore } from '$lib/stores/assets.store';
import type { PageData } from './$types';
import { mdiPlus, mdiDotsVertical } from '@mdi/js';
import { t } from 'svelte-i18n';
import { onDestroy } from 'svelte';
+ import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
interface Props {
data: PageData;
@@ -26,26 +26,26 @@
let { data }: Props = $props();
const assetStore = new AssetStore({ isArchived: true });
- const assetInteractionStore = createAssetInteractionStore();
- const { isMultiSelectState, selectedAssets } = assetInteractionStore;
-
- let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite));
+ const assetInteraction = new AssetInteraction();
onDestroy(() => {
assetStore.destroy();
});
-{#if $isMultiSelectState}
- assetInteractionStore.clearMultiselect()}>
+{#if assetInteraction.selectionActive}
+ assetInteraction.clearMultiselect()}
+ >
assetStore.removeAssets(assetIds)} />
-
+
- assetStore.triggerUpdate()} />
+ assetStore.triggerUpdate()} />
assetStore.removeAssets(assetIds)} />
@@ -53,8 +53,8 @@
{/if}
-
-
+
+
{#snippet empty()}
{/snippet}
diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 6635eda6e9..33a03292cd 100644
--- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -14,7 +14,6 @@
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AssetAction } from '$lib/constants';
- import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { AssetStore } from '$lib/stores/assets.store';
import type { PageData } from './$types';
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
@@ -22,6 +21,7 @@
import { onDestroy } from 'svelte';
import { preferences } from '$lib/stores/user.store';
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
+ import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
interface Props {
data: PageData;
@@ -30,10 +30,7 @@
let { data }: Props = $props();
const assetStore = new AssetStore({ isFavorite: true });
- const assetInteractionStore = createAssetInteractionStore();
- const { isMultiSelectState, selectedAssets } = assetInteractionStore;
-
- let isAllArchive = $derived([...$selectedAssets].every((asset) => asset.isArchived));
+ const assetInteraction = new AssetInteraction();
onDestroy(() => {
assetStore.destroy();
@@ -41,11 +38,14 @@
-{#if $isMultiSelectState}
- assetInteractionStore.clearMultiselect()}>
+{#if assetInteraction.selectionActive}
+ assetInteraction.clearMultiselect()}
+ >
assetStore.removeAssets(assetIds)} />
-
+
@@ -54,7 +54,11 @@
- assetStore.removeAssets(assetIds)} />
+ assetStore.removeAssets(assetIds)}
+ />
{#if $preferences.tags.enabled}
{/if}
@@ -63,8 +67,8 @@
{/if}
-
-
+
+
{#snippet empty()}
{/snippet}
diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 065b28c674..5119905652 100644
--- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -3,7 +3,6 @@
import { page } from '$app/stores';
import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte';
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
- import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte';
import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
@@ -17,6 +16,7 @@
import type { PageData } from './$types';
import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte';
import SkipLink from '$lib/components/elements/buttons/skip-link.svelte';
+ import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
interface Props {
data: PageData;
@@ -31,7 +31,7 @@
let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || '');
let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree));
- const assetInteractionStore = createAssetInteractionStore();
+ const assetInteraction = new AssetInteraction();
onMount(async () => {
await foldersStore.fetchUniquePaths();
@@ -80,7 +80,7 @@
{
- assetInteractionStore.clearMultiselect();
assetStore.destroy();
});
- {#if $isMultiSelectState}
-
+ {#if assetInteraction.selectionActive}
+
@@ -50,5 +48,5 @@
{/snippet}
{/if}
-
+
diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 143a19dd5c..6788c678ed 100644
--- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -27,7 +27,6 @@
notificationController,
} from '$lib/components/shared-components/notification/notification';
import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants';
- import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore } from '$lib/stores/assets.store';
import { websocketEvents } from '$lib/stores/websocket';
@@ -58,8 +57,9 @@
import { listNavigation } from '$lib/actions/list-navigation';
import { t } from 'svelte-i18n';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
- import { preferences, user } from '$lib/stores/user.store';
+ import { preferences } from '$lib/stores/user.store';
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
+ import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
interface Props {
data: PageData;
@@ -78,8 +78,7 @@
handlePromiseError(assetStore.updateOptions(assetStoreOptions));
});
- const assetInteractionStore = createAssetInteractionStore();
- const { selectedAssets, isMultiSelectState } = assetInteractionStore;
+ const assetInteraction = new AssetInteraction();
let viewMode: PersonPageViewMode = $state(PersonPageViewMode.VIEW_ASSETS);
let isEditingName = $state(false);
@@ -123,8 +122,8 @@
if ($showAssetViewer || viewMode === PersonPageViewMode.SUGGEST_MERGE) {
return;
}
- if ($isMultiSelectState) {
- assetInteractionStore.clearMultiselect();
+ if (assetInteraction.selectionActive) {
+ assetInteraction.clearMultiselect();
return;
} else {
await goto(previousRoute);
@@ -149,8 +148,8 @@
});
const handleUnmerge = () => {
- $assetStore.removeAssets([...$selectedAssets].map((a) => a.id));
- assetInteractionStore.clearMultiselect();
+ $assetStore.removeAssets(assetInteraction.selectedAssetsArray.map((a) => a.id));
+ assetInteraction.clearMultiselect();
viewMode = PersonPageViewMode.VIEW_ASSETS;
};
@@ -194,7 +193,7 @@
handleError(error, $t('errors.unable_to_set_feature_photo'));
}
- assetInteractionStore.clearMultiselect();
+ assetInteraction.clearMultiselect();
viewMode = PersonPageViewMode.VIEW_ASSETS;
};
@@ -336,15 +335,11 @@
handlePromiseError(updateAssetCount());
}
});
-
- let isAllArchive = $derived([...$selectedAssets].every((asset) => asset.isArchived));
- let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite));
- let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id));
{#if viewMode === PersonPageViewMode.UNASSIGN_ASSETS}
a.id)}
+ assetIds={assetInteraction.selectedAssetsArray.map((a) => a.id)}
personAssets={person}
onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)}
onConfirm={handleUnmerge}
@@ -375,15 +370,18 @@
{/if}
- {#if $isMultiSelectState}
- assetInteractionStore.clearMultiselect()}>
+ {#if assetInteraction.selectionActive}
+ assetInteraction.clearMultiselect()}
+ >
-
+
- assetStore.triggerUpdate()} />
+ assetStore.triggerUpdate()} />
- $assetStore.removeAssets(assetIds)} />
- {#if $preferences.tags.enabled && isAllUserOwned}
+ $assetStore.removeAssets(assetIds)}
+ />
+ {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
{/if}
$assetStore.removeAssets(assetIds)} />
@@ -453,7 +455,7 @@
{
- const selection = [...$selectedAssets];
- isAllOwned = selection.every((asset) => asset.ownerId === $user.id);
- isAllFavorite = selection.every((asset) => asset.isFavorite);
- isAssetStackSelected = selection.length === 1 && !!selection[0].stack;
- const isLivePhoto = selection.length === 1 && !!selection[0].livePhotoVideoId;
+ let selectedAssets = $derived(assetInteraction.selectedAssetsArray);
+ let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack);
+ let isLinkActionAvailable = $derived.by(() => {
+ const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId;
const isLivePhotoCandidate =
- selection.length === 2 &&
- selection.some((asset) => asset.type === AssetTypeEnum.Image) &&
- selection.some((asset) => asset.type === AssetTypeEnum.Video);
- isLinkActionAvailable = isAllOwned && (isLivePhoto || isLivePhotoCandidate);
- });
+ selectedAssets.length === 2 &&
+ selectedAssets.some((asset) => asset.type === AssetTypeEnum.Image) &&
+ selectedAssets.some((asset) => asset.type === AssetTypeEnum.Video);
+ return assetInteraction.isAllUserOwned && (isLivePhoto || isLivePhotoCandidate);
+ });
const handleEscape = () => {
if ($showAssetViewer) {
return;
}
- if ($isMultiSelectState) {
- assetInteractionStore.clearMultiselect();
+ if (assetInteraction.selectionActive) {
+ assetInteraction.clearMultiselect();
return;
}
};
@@ -78,22 +70,22 @@
});
-{#if $isMultiSelectState}
+{#if assetInteraction.selectionActive}
assetInteractionStore.clearMultiselect()}
+ assets={assetInteraction.selectedAssets}
+ clearSelect={() => assetInteraction.clearMultiselect()}
>
-
+
- assetStore.triggerUpdate()} />
+ assetStore.triggerUpdate()} />
- {#if $selectedAssets.size > 1 || isAssetStackSelected}
+ {#if assetInteraction.selectedAssets.size > 1 || isAssetStackSelected}
assetStore.removeAssets(assetIds)}
@@ -103,7 +95,7 @@
{#if isLinkActionAvailable}
@@ -121,11 +113,11 @@
{/if}
-
+
;
-
- let isMultiSelectionMode = $derived($selectedAssets.size > 0);
- let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived));
- let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite));
let searchQuery = $derived($page.url.searchParams.get(QueryParameter.QUERY));
onMount(() => {
@@ -86,8 +81,8 @@
return;
}
- if (isMultiSelectionMode) {
- $selectedAssets = new Set();
+ if (assetInteraction.selectionActive) {
+ assetInteraction.selectedAssets.clear();
return;
}
if (!$preventRaceConditionSearchBar) {
@@ -131,7 +126,7 @@
searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id));
};
const handleSelectAll = () => {
- assetInteractionStore.selectAssets(searchResultAssets);
+ assetInteraction.selectAssets(searchResultAssets);
};
async function onSearchQueryUpdate() {
@@ -231,29 +226,31 @@
function getObjectKeys(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}
- let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id));
- {#if isMultiSelectionMode}
+ {#if assetInteraction.selectionActive}
-
cancelMultiselect(assetInteractionStore)}>
+ cancelMultiselect(assetInteraction)}
+ >
-
+
-
- {#if $preferences.tags.enabled && isAllUserOwned}
+
+ {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
{/if}
@@ -333,7 +330,7 @@
{#if searchResultAssets.length > 0}
{
return Object.fromEntries(tags.map((tag) => [tag.value, tag]));
@@ -198,7 +198,7 @@
{#if tag}
-
+
{#snippet empty()}
{/snippet}
diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 8803ea38c8..7f97d3772b 100644
--- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -15,7 +15,6 @@
notificationController,
} from '$lib/components/shared-components/notification/notification';
import { AppRoute } from '$lib/constants';
- import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { AssetStore } from '$lib/stores/assets.store';
import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
import { handleError } from '$lib/utils/handle-error';
@@ -26,6 +25,7 @@
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
import { t } from 'svelte-i18n';
import { onDestroy } from 'svelte';
+ import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
interface Props {
data: PageData;
@@ -39,8 +39,7 @@
const options = { isTrashed: true };
const assetStore = new AssetStore(options);
- const assetInteractionStore = createAssetInteractionStore();
- const { isMultiSelectState, selectedAssets } = assetInteractionStore;
+ const assetInteraction = new AssetInteraction();
const handleEmptyTrash = async () => {
const isConfirmed = await dialogController.show({
@@ -93,25 +92,28 @@
});
-{#if $isMultiSelectState}
- assetInteractionStore.clearMultiselect()}>
-
+{#if assetInteraction.selectionActive}
+ assetInteraction.clearMultiselect()}
+ >
+
assetStore.removeAssets(assetIds)} />
assetStore.removeAssets(assetIds)} />
{/if}
{#if $featureFlags.loaded && $featureFlags.trash}
-
+
{#snippet buttons()}
-
+
{$t('restore_all')}
- handleEmptyTrash()} disabled={$isMultiSelectState}>
+ handleEmptyTrash()} disabled={assetInteraction.selectionActive}>
{$t('empty_trash')}
@@ -120,7 +122,7 @@
{/snippet}
-
+
{$t('trashed_items_will_be_permanently_deleted_after', { values: { days: $serverConfig.trashDays } })}