diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index bca3b2024d6bc..72723670e60d1 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -17,6 +17,8 @@ import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; + import { cancelMultiselect } from '$lib/utils/asset-utils'; + import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { AppRoute, QueryParameter } from '$lib/constants'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { type Viewport } from '$lib/stores/assets.store'; @@ -44,7 +46,6 @@ import { tweened } from 'svelte/motion'; import { derived as storeDerived } from 'svelte/store'; import { fade } from 'svelte/transition'; - import { SvelteSet } from 'svelte/reactivity'; type MemoryIndex = { memoryIndex: number; @@ -64,13 +65,14 @@ let memoryWrapper: HTMLElement | undefined = $state(); let galleryInView = $state(false); let paused = $state(false); - let selectedAssets: SvelteSet = $state(new SvelteSet()); let current: MemoryAsset | undefined = $state(undefined); // let memories: MemoryAsset[] = []; let resetPromise = $state(Promise.resolve()); const { isViewing } = assetViewingStore; const viewport: Viewport = $state({ width: 0, height: 0 }); + const assetInteractionStore = createAssetInteractionStore(); + const { selectedAssets } = assetInteractionStore; const progressBarController = tweened(0, { duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0), }); @@ -126,7 +128,7 @@ const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]); const handlePreviousMemory = () => handleNavigate(current?.previousMemory?.assets[0]); const handleEscape = async () => goto(AppRoute.PHOTOS); - const handleSelectAll = () => (selectedAssets = new SvelteSet(current?.memory.assets || [])); + const handleSelectAll = () => assetInteractionStore.selectAssets(current?.memory.assets || []); const handleAction = async (action: 'reset' | 'pause' | 'play') => { switch (action) { case 'play': { @@ -207,13 +209,10 @@ current = loadFromParams($memories, target); }); - $effect(() => { - selectedAssets = galleryInView ? selectedAssets : new SvelteSet(); - }); - let isMultiSelectionMode = $derived(selectedAssets.size > 0); - let isAllArchived = $derived([...selectedAssets].every((asset) => asset.isArchived)); - let isAllFavorite = $derived([...selectedAssets].every((asset) => asset.isFavorite)); + let isMultiSelectionMode = $derived($selectedAssets.size > 0); + let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived)); + let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite)); $effect(() => { handlePromiseError(handleProgress($progressBarController)); @@ -238,7 +237,7 @@ {#if isMultiSelectionMode}
- (selectedAssets = new SvelteSet())}> + cancelMultiselect(assetInteractionStore)}> @@ -485,7 +484,7 @@ onPrevious={handlePreviousAsset} assets={current.memory.assets} {viewport} - bind:selectedAssets + {assetInteractionStore} />
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 245a90f9f3195..5d625cef9d711 100644 --- a/web/src/lib/components/share-page/individual-shared-viewer.svelte +++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte @@ -6,7 +6,7 @@ import { downloadArchive } from '$lib/utils/asset-utils'; import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader'; import { handleError } from '$lib/utils/handle-error'; - import { addSharedLinkAssets, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk'; + import { addSharedLinkAssets, type SharedLinkResponseDto } from '@immich/sdk'; import { mdiArrowLeft, mdiFileImagePlusOutline, mdiFolderDownloadOutline, mdiSelectAll } from '@mdi/js'; import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import DownloadAction from '../photos-page/actions/download-action.svelte'; @@ -14,6 +14,8 @@ import AssetSelectControlBar from '../photos-page/asset-select-control-bar.svelte'; 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'; @@ -27,11 +29,12 @@ let { sharedLink = $bindable(), isOwned }: Props = $props(); const viewport: Viewport = $state({ width: 0, height: 0 }); - let selectedAssets: Set = $state(new Set()); + const assetInteractionStore = createAssetInteractionStore(); + const { selectedAssets } = assetInteractionStore; let innerWidth: number = $state(0); let assets = $derived(sharedLink.assets); - let isMultiSelectionMode = $derived(selectedAssets.size > 0); + let isMultiSelectionMode = $derived($selectedAssets.size > 0); dragAndDropFilesStore.subscribe((value) => { if (value.isDragging && value.files.length > 0) { @@ -70,7 +73,7 @@ }; const handleSelectAll = () => { - selectedAssets = new Set(assets); + assetInteractionStore.selectAssets(assets); }; @@ -78,7 +81,7 @@
{#if isMultiSelectionMode} - (selectedAssets = new Set())}> + cancelMultiselect(assetInteractionStore)}> {#if sharedLink?.allowDownload} @@ -109,6 +112,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 b6bcdabdff877..eda340e7e2c92 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 @@ -1,50 +1,63 @@ + + +{#if isShowDeleteConfirmation} + (isShowDeleteConfirmation = false)} + onConfirm={() => handlePromiseError(trashOrDelete(true))} + /> +{/if} + +{#if showShortcuts} + (showShortcuts = !showShortcuts)} /> +{/if} + {#if assets.length > 0}
{#each assets as asset, i (i)} @@ -159,19 +338,21 @@ title={showAssetName ? asset.originalFileName : ''} > { if (isMultiSelectionMode) { - selectAssetHandler(asset); + handleSelectAssets(asset); return; } void viewAssetHandler(asset); }} - onSelect={(asset) => selectAssetHandler(asset)} + onSelect={(asset) => handleSelectAssets(asset)} + onMouseEvent={() => assetMouseEventHandler(asset)} onIntersected={() => (i === Math.max(1, assets.length - 7) ? onIntersected?.() : void 0)} - selected={selectedAssets.has(asset)} {showArchiveIcon} + {asset} + selected={$selectedAssets.has(asset)} + selectionCandidate={$assetSelectionCandidates.has(asset)} thumbnailWidth={geometry.boxes[i].width} thumbnailHeight={geometry.boxes[i].height} /> 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 255a4373caf65..728387753c0a5 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,6 +3,7 @@ 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'; @@ -10,7 +11,6 @@ import type { Viewport } from '$lib/stores/assets.store'; import { foldersStore } from '$lib/stores/folders.store'; import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils'; - import { type AssetResponseDto } from '@immich/sdk'; import { mdiFolder, mdiFolderHome, mdiFolderOutline } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; @@ -24,7 +24,6 @@ let { data }: Props = $props(); - let selectedAssets: Set = $state(new Set()); const viewport: Viewport = $state({ width: 0, height: 0 }); let pathSegments = $derived(data.path ? data.path.split('/') : []); @@ -32,6 +31,8 @@ let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || ''); let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree)); + const assetInteractionStore = createAssetInteractionStore(); + onMount(async () => { await foldersStore.fetchUniquePaths(); }); @@ -79,7 +80,7 @@
= $state(new Set()); + + const assetInteractionStore = createAssetInteractionStore(); + const { selectedAssets } = assetInteractionStore; type SearchTerms = MetadataSearchDto & Pick; - let isMultiSelectionMode = $derived(selectedAssets.size > 0); - let isAllArchived = $derived([...selectedAssets].every((asset) => asset.isArchived)); - let isAllFavorite = $derived([...selectedAssets].every((asset) => asset.isFavorite)); + 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(() => { @@ -81,7 +85,7 @@ } if (isMultiSelectionMode) { - selectedAssets = new Set(); + $selectedAssets = new Set(); return; } if (!$preventRaceConditionSearchBar) { @@ -125,7 +129,7 @@ searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id)); }; const handleSelectAll = () => { - selectedAssets = new Set(searchResultAssets); + assetInteractionStore.selectAssets(searchResultAssets); }; async function onSearchQueryUpdate() { @@ -216,8 +220,10 @@ const triggerAssetUpdate = () => (searchResultAssets = searchResultAssets); const onAddToAlbum = (assetIds: string[]) => { - const assetIdSet = new Set(assetIds); - searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id)); + if (terms.isNotInAlbum.toString() == 'true') { + const assetIdSet = new Set(assetIds); + searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id)); + } }; function getObjectKeys(obj: T): (keyof T)[] { @@ -230,7 +236,7 @@
{#if isMultiSelectionMode}
- (selectedAssets = new Set())}> + cancelMultiselect(assetInteractionStore)}> @@ -321,7 +327,7 @@ {#if searchResultAssets.length > 0}