mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
feat(web): improve range selection (#3193)
* Improve range selection * Add comments * Add PR feedback * Remove focus outline from select asset button --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
ea64fdd7b4
commit
93462aafbc
5 changed files with 163 additions and 76 deletions
|
|
@ -84,9 +84,7 @@
|
|||
{#if !readonly && (mouseOver || selected || selectionCandidate)}
|
||||
<button
|
||||
on:click={onIconClickedHandler}
|
||||
on:keydown|preventDefault
|
||||
on:keyup|preventDefault
|
||||
class="absolute p-2"
|
||||
class="absolute p-2 focus:outline-none"
|
||||
class:cursor-not-allowed={disabled}
|
||||
role="checkbox"
|
||||
aria-checked={selected}
|
||||
|
|
|
|||
|
|
@ -9,15 +9,15 @@
|
|||
} from '$lib/stores/asset-interaction.store';
|
||||
import { assetStore } from '$lib/stores/assets.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getAssetRatio } from '$lib/utils/asset-utils';
|
||||
import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util';
|
||||
import type { AssetResponseDto } from '@api';
|
||||
import justifiedLayout from 'justified-layout';
|
||||
import lodash from 'lodash-es';
|
||||
import { DateTime } from 'luxon';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
|
||||
import CircleOutline from 'svelte-material-icons/CircleOutline.svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { DateTime, Interval } from 'luxon';
|
||||
import { getAssetRatio } from '$lib/utils/asset-utils';
|
||||
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
||||
|
||||
export let assets: AssetResponseDto[];
|
||||
|
|
@ -26,13 +26,6 @@
|
|||
export let isAlbumSelectionMode = false;
|
||||
export let viewportWidth: number;
|
||||
|
||||
const groupDateFormat: Intl.DateTimeFormatOptions = {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
};
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let isMouseOverGroup = false;
|
||||
|
|
@ -45,11 +38,7 @@
|
|||
width: number;
|
||||
}
|
||||
|
||||
$: assetsGroupByDate = lodash
|
||||
.chain(assets)
|
||||
.groupBy((a) => new Date(a.fileCreatedAt).toLocaleDateString($locale, groupDateFormat))
|
||||
.sortBy((group) => assets.indexOf(group[0]))
|
||||
.value();
|
||||
$: assetsGroupByDate = splitBucketIntoDateGroups(assets, $locale);
|
||||
|
||||
$: geometry = (() => {
|
||||
const geometry = [];
|
||||
|
|
@ -131,17 +120,7 @@
|
|||
assetsInDateGroup: AssetResponseDto[],
|
||||
dateGroupTitle: string,
|
||||
) => {
|
||||
if ($selectedAssets.has(asset)) {
|
||||
for (const candidate of $assetSelectionCandidates || []) {
|
||||
assetInteractionStore.removeAssetFromMultiselectGroup(candidate);
|
||||
}
|
||||
assetInteractionStore.removeAssetFromMultiselectGroup(asset);
|
||||
} else {
|
||||
for (const candidate of $assetSelectionCandidates || []) {
|
||||
assetInteractionStore.addAssetToMultiselectGroup(candidate);
|
||||
}
|
||||
assetInteractionStore.addAssetToMultiselectGroup(asset);
|
||||
}
|
||||
dispatch('selectAssets', { asset });
|
||||
|
||||
// Check if all assets are selected in a group to toggle the group selection's icon
|
||||
let selectedAssetsInGroupCount = assetsInDateGroup.filter((asset) => $selectedAssets.has(asset)).length;
|
||||
|
|
@ -162,41 +141,6 @@
|
|||
dispatch('selectAssetCandidates', { asset });
|
||||
}
|
||||
};
|
||||
|
||||
const formatGroupTitle = (date: DateTime): string => {
|
||||
const today = DateTime.now().startOf('day');
|
||||
|
||||
// Today
|
||||
if (today.hasSame(date, 'day')) {
|
||||
return 'Today';
|
||||
}
|
||||
|
||||
// Yesterday
|
||||
if (Interval.fromDateTimes(date, today).length('days') == 1) {
|
||||
return 'Yesterday';
|
||||
}
|
||||
|
||||
// Last week
|
||||
if (Interval.fromDateTimes(date, today).length('weeks') < 1) {
|
||||
return date.toLocaleString({ weekday: 'long' });
|
||||
}
|
||||
|
||||
// This year
|
||||
if (today.hasSame(date, 'year')) {
|
||||
return date.toLocaleString({
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
return date.toLocaleString({
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<section
|
||||
|
|
|
|||
|
|
@ -2,14 +2,19 @@
|
|||
import { BucketPosition } from '$lib/models/asset-grid-state';
|
||||
import {
|
||||
assetInteractionStore,
|
||||
assetSelectionCandidates,
|
||||
assetSelectionStart,
|
||||
isMultiSelectStoreState,
|
||||
isViewingAssetStoreState,
|
||||
selectedAssets,
|
||||
viewingAssetStoreState,
|
||||
} from '$lib/stores/asset-interaction.store';
|
||||
import { assetGridState, assetStore, loadingBucketState } from '$lib/stores/assets.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util';
|
||||
import type { UserResponseDto } from '@api';
|
||||
import { api, AssetCountByTimeBucketResponseDto, AssetResponseDto, TimeGroupEnum } from '@api';
|
||||
import { DateTime } from 'luxon';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import AssetViewer from '../asset-viewer/asset-viewer.svelte';
|
||||
import IntersectionObserver from '../asset-viewer/intersection-observer.svelte';
|
||||
|
|
@ -144,12 +149,6 @@
|
|||
selectAssetCandidates(lastAssetMouseEvent);
|
||||
}
|
||||
|
||||
const getLastSelectedAsset = () => {
|
||||
let value;
|
||||
for (value of $selectedAssets);
|
||||
return value;
|
||||
};
|
||||
|
||||
const handleSelectAssetCandidates = (e: CustomEvent) => {
|
||||
const asset = e.detail.asset;
|
||||
if (asset) {
|
||||
|
|
@ -158,18 +157,84 @@
|
|||
lastAssetMouseEvent = asset;
|
||||
};
|
||||
|
||||
const handleSelectAssets = async (e: CustomEvent) => {
|
||||
const asset = e.detail.asset;
|
||||
if (!asset) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rangeSelection = $assetSelectionCandidates.size > 0;
|
||||
const deselect = $selectedAssets.has(asset);
|
||||
|
||||
// Select/deselect already loaded assets
|
||||
if (deselect) {
|
||||
for (const candidate of $assetSelectionCandidates || []) {
|
||||
assetInteractionStore.removeAssetFromMultiselectGroup(candidate);
|
||||
}
|
||||
assetInteractionStore.removeAssetFromMultiselectGroup(asset);
|
||||
} else {
|
||||
for (const candidate of $assetSelectionCandidates || []) {
|
||||
assetInteractionStore.addAssetToMultiselectGroup(candidate);
|
||||
}
|
||||
assetInteractionStore.addAssetToMultiselectGroup(asset);
|
||||
}
|
||||
|
||||
assetInteractionStore.clearAssetSelectionCandidates();
|
||||
|
||||
if ($assetSelectionStart && rangeSelection) {
|
||||
let startBucketIndex = $assetGridState.loadedAssets[$assetSelectionStart.id];
|
||||
let endBucketIndex = $assetGridState.loadedAssets[asset.id];
|
||||
|
||||
if (endBucketIndex < startBucketIndex) {
|
||||
[startBucketIndex, endBucketIndex] = [endBucketIndex, startBucketIndex];
|
||||
}
|
||||
|
||||
// Select/deselect assets in all intermediate buckets
|
||||
for (let bucketIndex = startBucketIndex + 1; bucketIndex < endBucketIndex; bucketIndex++) {
|
||||
const bucket = $assetGridState.buckets[bucketIndex];
|
||||
await assetStore.getAssetsByBucket(bucket.bucketDate, BucketPosition.Unknown);
|
||||
for (const asset of bucket.assets) {
|
||||
if (deselect) {
|
||||
assetInteractionStore.removeAssetFromMultiselectGroup(asset);
|
||||
} else {
|
||||
assetInteractionStore.addAssetToMultiselectGroup(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update date group selection
|
||||
for (let bucketIndex = startBucketIndex; bucketIndex <= endBucketIndex; bucketIndex++) {
|
||||
const bucket = $assetGridState.buckets[bucketIndex];
|
||||
|
||||
// Split bucket into date groups and check each group
|
||||
const assetsGroupByDate = splitBucketIntoDateGroups(bucket.assets, $locale);
|
||||
|
||||
for (const dateGroup of assetsGroupByDate) {
|
||||
const dateGroupTitle = formatGroupTitle(DateTime.fromISO(dateGroup[0].fileCreatedAt).startOf('day'));
|
||||
if (dateGroup.every((a) => $selectedAssets.has(a))) {
|
||||
assetInteractionStore.addGroupToMultiselectGroup(dateGroupTitle);
|
||||
} else {
|
||||
assetInteractionStore.removeGroupFromMultiselectGroup(dateGroupTitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assetInteractionStore.setAssetSelectionStart(deselect ? null : asset);
|
||||
};
|
||||
|
||||
const selectAssetCandidates = (asset: AssetResponseDto) => {
|
||||
if (!shiftKeyIsDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastSelectedAsset = getLastSelectedAsset();
|
||||
if (!lastSelectedAsset) {
|
||||
const rangeStart = $assetSelectionStart;
|
||||
if (!rangeStart) {
|
||||
return;
|
||||
}
|
||||
|
||||
let start = $assetGridState.assets.indexOf(asset);
|
||||
let end = $assetGridState.assets.indexOf(lastSelectedAsset);
|
||||
let start = $assetGridState.assets.indexOf(rangeStart);
|
||||
let end = $assetGridState.assets.indexOf(asset);
|
||||
|
||||
if (start > end) {
|
||||
[start, end] = [end, start];
|
||||
|
|
@ -230,6 +295,7 @@
|
|||
{isAlbumSelectionMode}
|
||||
on:shift={handleScrollTimeline}
|
||||
on:selectAssetCandidates={handleSelectAssetCandidates}
|
||||
on:selectAssets={handleSelectAssets}
|
||||
assets={bucket.assets}
|
||||
bucketDate={bucket.bucketDate}
|
||||
bucketHeight={bucket.bucketHeight}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue