From 8fd4edb20609a9d5d07c64e53a7c295e59d54793 Mon Sep 17 00:00:00 2001
From: Thomas <9749173+uhthomas@users.noreply.github.com>
Date: Mon, 3 Jul 2023 10:56:58 +0100
Subject: [PATCH] feat(web): select a range of assets (#3086)
The shift key can be held to select a range of assets.
Fixes: #2862
---
.../assets/thumbnail/thumbnail.svelte | 38 ++++++---
.../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, 154 insertions(+), 32 deletions(-)
diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte
index f0946cfd16..b49f5bacc1 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,21 +77,23 @@
on:mouseenter={() => (mouseOver = true)}
on:mouseleave={() => (mouseOver = false)}
on:click={thumbnailClickedHandler}
- on:keydown={thumbnailClickedHandler}
+ on:keydown={thumbnailKeyDownHandler}
>
{#if intersecting}
- {#if !readonly}
+ {#if !readonly && (mouseOver || selected || selectionCandidate)}
{/if}
+ {#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 fc9eaf2a6d..6298c19a05 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 7e7b7979dc..7a80533163 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();
@@ -19,6 +20,7 @@ function createAssetInteractionStore() {
let _selectedAssets: Set;
let _selectedGroup: Set;
let _assetsInAlbums: AssetResponseDto[];
+ let _assetSelectionCandidates: Set;
// Subscriber
assetGridState.subscribe((state) => {
@@ -41,6 +43,10 @@ function createAssetInteractionStore() {
_assetsInAlbums = assets;
});
+ assetSelectionCandidates.subscribe((assets) => {
+ _assetSelectionCandidates = assets;
+ });
+
// Methods
/**
@@ -117,14 +123,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 {
@@ -136,6 +154,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 13475dba6a..f3d142eddf 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;
});
};