diff --git a/web/src/lib/components/photos-page/actions/select-all-assets.svelte b/web/src/lib/components/photos-page/actions/select-all-assets.svelte index c14ea37882..a3ae241ef6 100644 --- a/web/src/lib/components/photos-page/actions/select-all-assets.svelte +++ b/web/src/lib/components/photos-page/actions/select-all-assets.svelte @@ -1,42 +1,24 @@ -{#if selecting} - -{/if} -{#if !selecting} +{#if $isSelectingAllAssets} + +{:else} {/if} diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 5416737647..53847c5814 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -3,12 +3,12 @@ import { AppRoute, AssetAction } from '$lib/constants'; import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import { BucketPosition, type AssetStore, type Viewport } from '$lib/stores/assets.store'; + import { BucketPosition, isSelectingAllAssets, type AssetStore, type Viewport } from '$lib/stores/assets.store'; import { locale, showDeleteModal } from '$lib/stores/preferences.store'; import { isSearchEnabled } from '$lib/stores/search.store'; import { featureFlags } from '$lib/stores/server-config.store'; import { deleteAssets } from '$lib/utils/actions'; - import { shortcuts, type ShortcutOptions, matchesShortcut } from '$lib/utils/shortcut'; + import { type ShortcutOptions, shortcuts } from '$lib/utils/shortcut'; import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util'; import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk'; import { DateTime } from 'luxon'; @@ -20,6 +20,7 @@ import AssetDateGroup from './asset-date-group.svelte'; import DeleteAssetDialog from './delete-asset-dialog.svelte'; import { handlePromiseError } from '$lib/utils'; + import { selectAllAssets } from '$lib/utils/asset-utils'; export let isSelectionMode = false; export let singleSelect = false; @@ -93,12 +94,14 @@ { shortcut: { key: 'Escape' }, onShortcut: () => dispatch('escape') }, { shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) }, { shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) }, + { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(assetStore, assetInteractionStore) }, ]; if ($isMultiSelectState) { shortcuts.push( { shortcut: { key: 'Delete' }, onShortcut: onDelete }, { shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete }, + { shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() }, ); } @@ -202,12 +205,17 @@ let shiftKeyIsDown = false; + const deselectAllAssets = () => { + $isSelectingAllAssets = false; + assetInteractionStore.clearMultiselect(); + }; + const onKeyDown = (event: KeyboardEvent) => { if ($isSearchEnabled) { return; } - if (matchesShortcut(event, { key: 'Shift', shift: true })) { + if (event.key === 'Shift') { event.preventDefault(); shiftKeyIsDown = true; } @@ -218,7 +226,7 @@ return; } - if (matchesShortcut(event, { key: 'Shift', shift: false })) { + if (event.key === 'Shift') { event.preventDefault(); shiftKeyIsDown = false; } diff --git a/web/src/lib/components/shared-components/control-app-bar.svelte b/web/src/lib/components/shared-components/control-app-bar.svelte index bdb2aed709..095b12d274 100644 --- a/web/src/lib/components/shared-components/control-app-bar.svelte +++ b/web/src/lib/components/shared-components/control-app-bar.svelte @@ -5,7 +5,7 @@ import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import { fly } from 'svelte/transition'; import { mdiClose } from '@mdi/js'; - import { isSelectAllCancelled } from '$lib/stores/assets.store'; + import { isSelectingAllAssets } from '$lib/stores/assets.store'; export let showBackButton = true; export let backIcon = mdiClose; @@ -31,7 +31,7 @@ }; const handleClose = () => { - $isSelectAllCancelled = true; + $isSelectingAllAssets = false; dispatch('close'); }; diff --git a/web/src/lib/stores/asset-interaction.store.ts b/web/src/lib/stores/asset-interaction.store.ts index b02847afd1..9dd0ab9b8c 100644 --- a/web/src/lib/stores/asset-interaction.store.ts +++ b/web/src/lib/stores/asset-interaction.store.ts @@ -3,6 +3,7 @@ import { derived, writable } from 'svelte/store'; export interface AssetInteractionStore { selectAsset: (asset: AssetResponseDto) => void; + selectAssets: (assets: AssetResponseDto[]) => void; removeAssetFromMultiselectGroup: (asset: AssetResponseDto) => void; addGroupToMultiselectGroup: (group: string) => void; removeGroupFromMultiselectGroup: (group: string) => void; @@ -76,6 +77,13 @@ export function createAssetInteractionStore(): AssetInteractionStore { selectedAssets.set(_selectedAssets); }; + const selectAssets = (assets: AssetResponseDto[]) => { + for (const asset of assets) { + _selectedAssets.add(asset); + } + selectedAssets.set(_selectedAssets); + }; + const removeAssetFromMultiselectGroup = (asset: AssetResponseDto) => { _selectedAssets.delete(asset); selectedAssets.set(_selectedAssets); @@ -123,6 +131,7 @@ export function createAssetInteractionStore(): AssetInteractionStore { return { selectAsset, + selectAssets, removeAssetFromMultiselectGroup, addGroupToMultiselectGroup, removeGroupFromMultiselectGroup, diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts index 45b26c1a17..4cc92ab868 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets.store.ts @@ -519,4 +519,4 @@ export class AssetStore { } } -export const isSelectAllCancelled = writable(false); +export const isSelectingAllAssets = writable(false); diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index f1a3d44be5..27c99a4731 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -1,4 +1,6 @@ import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; +import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; +import { BucketPosition, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store'; import { downloadManager } from '$lib/stores/download'; import { downloadRequest, getKey } from '$lib/utils'; import { @@ -13,6 +15,7 @@ import { type UserResponseDto, } from '@immich/sdk'; import { DateTime } from 'luxon'; +import { get } from 'svelte/store'; import { handleError } from './handle-error'; export const addAssetsToAlbum = async (albumId: string, assetIds: Array): Promise => @@ -224,6 +227,35 @@ export const getSelectedAssets = (assets: Set, user: UserRespo return ids; }; +export const selectAllAssets = async (assetStore: AssetStore, assetInteractionStore: AssetInteractionStore) => { + if (get(isSelectingAllAssets)) { + // Selection is already ongoing + return; + } + isSelectingAllAssets.set(true); + + try { + for (const bucket of assetStore.buckets) { + await assetStore.loadBucket(bucket.bucketDate, BucketPosition.Unknown); + + if (!get(isSelectingAllAssets)) { + break; // Cancelled + } + assetInteractionStore.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 + // effective update of the UI, depending on the number of assets + // to select + await delay(0); + } + } catch (error) { + handleError(error, 'Error selecting all assets'); + } finally { + isSelectingAllAssets.set(false); + } +}; + export const delay = async (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); };