From 4706fbd9a6f37d7e05cc4f9d2315adc6912dfd7b Mon Sep 17 00:00:00 2001 From: Thomas Way Date: Mon, 3 Jul 2023 00:36:29 +0100 Subject: [PATCH] feat(web): select a range of assets The shift key can be held to select a range of assets. Fixes: #2862 --- .../assets/thumbnail/thumbnail.svelte | 32 ++++++-- .../photos-page/asset-date-group.svelte | 38 +++++---- .../components/photos-page/asset-grid.svelte | 79 ++++++++++++++++++- web/src/lib/stores/asset-interaction.store.ts | 20 +++++ web/src/lib/stores/assets.store.ts | 11 ++- 5 files changed, 151 insertions(+), 29 deletions(-) diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte index f0946cfd165472..f70ea33fef938e 100644 --- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte @@ -3,14 +3,15 @@ import { timeToSeconds } from '$lib/utils/time-to-seconds'; import { api, AssetResponseDto, AssetTypeEnum, ThumbnailFormat } from '@api'; import { createEventDispatcher } from 'svelte'; + import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte'; import CheckCircle from 'svelte-material-icons/CheckCircle.svelte'; + import Heart from 'svelte-material-icons/Heart.svelte'; + import ImageBrokenVariant from 'svelte-material-icons/ImageBrokenVariant.svelte'; import MotionPauseOutline from 'svelte-material-icons/MotionPauseOutline.svelte'; import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte'; - import Heart from 'svelte-material-icons/Heart.svelte'; - import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte'; + import { fade } from 'svelte/transition'; import ImageThumbnail from './image-thumbnail.svelte'; import VideoThumbnail from './video-thumbnail.svelte'; - import ImageBrokenVariant from 'svelte-material-icons/ImageBrokenVariant.svelte'; const dispatch = createEventDispatcher(); @@ -21,6 +22,7 @@ export let thumbnailHeight: number | undefined = undefined; export let format: ThumbnailFormat = ThumbnailFormat.Webp; export let selected = false; + export let selectionCandidate = false; export let disabled = false; export let readonly = false; export let publicSharedKey: string | undefined = undefined; @@ -30,7 +32,7 @@ $: dispatch('mouse-event', { isMouseOver: mouseOver, selectedGroupIndex: groupIndex }); - $: [width, height] = (() => { + $: [width, height] = ((): [number, number] => { if (thumbnailSize) { return [thumbnailSize, thumbnailSize]; } @@ -42,12 +44,19 @@ return [235, 235]; })(); - const thumbnailClickedHandler = () => { + const thumbnailClickedHandler = (e: Event) => { if (!disabled) { + e.preventDefault(); dispatch('click', { asset }); } }; + const thumbnailKeyDownHandler = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + thumbnailClickedHandler(e); + } + }; + const onIconClickedHandler = (e: MouseEvent) => { e.stopPropagation(); if (!disabled) { @@ -68,7 +77,7 @@ on:mouseenter={() => (mouseOver = true)} on:mouseleave={() => (mouseOver = false)} on:click={thumbnailClickedHandler} - on:keydown={thumbnailClickedHandler} + on:keydown={thumbnailKeyDownHandler} > {#if intersecting}
@@ -76,9 +85,11 @@ {#if !readonly}
+ {#if selectionCandidate} +
+ {/if} {/if}
diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index fc9eaf2a6dc001..93ddb1a94d888d 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -1,6 +1,7 @@ @@ -171,9 +177,12 @@ class="flex flex-col mt-5" on:mouseenter={() => { isMouseOverGroup = true; - assetMouseEventHandler(dateGroupTitle); + assetMouseEventHandler(dateGroupTitle, null); + }} + on:mouseleave={() => { + isMouseOverGroup = false; + assetMouseEventHandler(dateGroupTitle, null); }} - on:mouseleave={() => (isMouseOverGroup = false)} >

assetClickHandler(asset, assetsInDateGroup, dateGroupTitle)} on:select={() => assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle)} - on:mouse-event={() => assetMouseEventHandler(dateGroupTitle)} - selected={$selectedAssets.has(asset) || $assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1} - disabled={$assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1} + on:mouse-event={() => assetMouseEventHandler(dateGroupTitle, asset)} + selected={$selectedAssets.has(asset) || $assetsInAlbumStoreState.some(({ id }) => id === asset.id)} + selectionCandidate={$assetSelectionCandidates.has(asset)} + disabled={$assetsInAlbumStoreState.some(({ id }) => id === 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 7e7b7979dc0002..7a80533163e6cc 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -1,12 +1,15 @@ + + {#if bucketInfo && viewportHeight && $assetGridState.timelineHeight > viewportHeight} ([]); export const selectedAssets = writable>(new Set()); export const selectedGroup = writable>(new Set()); export const isMultiSelectStoreState = derived(selectedAssets, ($selectedAssets) => $selectedAssets.size > 0); +export const assetSelectionCandidates = writable>(new Set()); function createAssetInteractionStore() { let _assetGridState = new AssetGridState(); @@ -20,6 +21,7 @@ function createAssetInteractionStore() { let _selectedAssets: Set; let _selectedGroup: Set; let _assetsInAlbums: AssetResponseDto[]; + let _assetSelectionCandidates: Set; let savedAssetLength = 0; let assetSortedByDate: AssetResponseDto[] = []; @@ -44,6 +46,10 @@ function createAssetInteractionStore() { _assetsInAlbums = assets; }); + assetSelectionCandidates.subscribe((assets) => { + _assetSelectionCandidates = assets; + }); + // Methods /** @@ -129,14 +135,26 @@ function createAssetInteractionStore() { selectedGroup.set(_selectedGroup); }; + const setAssetSelectionCandidates = (assets: AssetResponseDto[]) => { + _assetSelectionCandidates = new Set(assets); + assetSelectionCandidates.set(_assetSelectionCandidates); + }; + + const clearAssetSelectionCandidates = () => { + _assetSelectionCandidates.clear(); + assetSelectionCandidates.set(_assetSelectionCandidates); + }; + const clearMultiselect = () => { _selectedAssets.clear(); _selectedGroup.clear(); + _assetSelectionCandidates.clear(); _assetsInAlbums = []; selectedAssets.set(_selectedAssets); selectedGroup.set(_selectedGroup); assetsInAlbumStoreState.set(_assetsInAlbums); + assetSelectionCandidates.set(_assetSelectionCandidates); }; return { @@ -148,6 +166,8 @@ function createAssetInteractionStore() { removeAssetFromMultiselectGroup, addGroupToMultiselectGroup, removeGroupFromMultiselectGroup, + setAssetSelectionCandidates, + clearAssetSelectionCandidates, clearMultiselect, }; } diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts index 13475dba6a8a11..f3d142eddfb8d0 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets.store.ts @@ -1,6 +1,5 @@ import { AssetGridState, BucketPosition } from '$lib/models/asset-grid-state'; import { api, AssetCountByTimeBucketResponseDto } from '@api'; -import { flatMap, sumBy } from 'lodash-es'; import { writable } from 'svelte/store'; /** @@ -60,7 +59,7 @@ function createAssetStore() { // Update timeline height based on calculated bucket height assetGridState.update((state) => { - state.timelineHeight = sumBy(state.buckets, (d) => d.bucketHeight); + state.timelineHeight = state.buckets.reduce((acc, b) => acc + b.bucketHeight, 0); return state; }); }; @@ -101,7 +100,7 @@ function createAssetStore() { const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucket); state.buckets[bucketIndex].assets = assets; state.buckets[bucketIndex].position = position; - state.assets = flatMap(state.buckets, (b) => b.assets); + state.assets = state.buckets.flatMap((b) => b.assets); return state; }); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -123,7 +122,7 @@ function createAssetStore() { if (state.buckets[bucketIndex].assets.length === 0) { _removeBucket(state.buckets[bucketIndex].bucketDate); } - state.assets = flatMap(state.buckets, (b) => b.assets); + state.assets = state.buckets.flatMap((b) => b.assets); return state; }); }; @@ -132,7 +131,7 @@ function createAssetStore() { assetGridState.update((state) => { const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucketDate); state.buckets.splice(bucketIndex, 1); - state.assets = flatMap(state.buckets, (b) => b.assets); + state.assets = state.buckets.flatMap((b) => b.assets); return state; }); }; @@ -180,7 +179,7 @@ function createAssetStore() { const assetIndex = state.buckets[bucketIndex].assets.findIndex((a) => a.id === assetId); state.buckets[bucketIndex].assets[assetIndex].isFavorite = isFavorite; - state.assets = flatMap(state.buckets, (b) => b.assets); + state.assets = state.buckets.flatMap((b) => b.assets); return state; }); };