diff --git a/web/src/lib/components/photos-page/asset-date-group-actions.svelte b/web/src/lib/components/photos-page/asset-date-group-actions.svelte deleted file mode 100644 index cecc49b8fb..0000000000 --- a/web/src/lib/components/photos-page/asset-date-group-actions.svelte +++ /dev/null @@ -1,218 +0,0 @@ - - - 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 9cf50d1354..260a06c478 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -6,7 +6,7 @@ import type { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import { assetSnapshot, assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte'; - import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; + import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte'; import { uploadAssetsStore } from '$lib/stores/upload'; import { navigate } from '$lib/utils/navigation'; @@ -18,6 +18,9 @@ import { flip } from 'svelte/animate'; import { fly, scale } from 'svelte/transition'; + import { onMount } from 'svelte'; + import { DateGroupActionLib } from './date-group-actions-lib.svelte'; + let { isUploading } = uploadAssetsStore; interface Props { @@ -29,11 +32,13 @@ timelineManager: TimelineManager; assetInteraction: AssetInteraction; customLayout?: Snippet<[TimelineAsset]>; + onSelect: (asset: TimelineAsset) => void; - onSelect: ({ title, assets }: { title: string; assets: TimelineAsset[] }) => void; - onSelectAssets: (asset: TimelineAsset) => void; - onSelectAssetCandidates: (asset: TimelineAsset | null) => void; + // onSelect: ({ title, assets }: { title: string; assets: TimelineAsset[] }) => void; + // onSelectAssets: (asset: TimelineAsset) => void; + // onSelectAssetCandidates: (asset: TimelineAsset | null) => void; onScrollCompensation: (compensation: { heightDelta?: number; scrollTop?: number }) => void; + scrollTop: (top: number) => void; onThumbnailClick?: ( asset: TimelineAsset, timelineManager: TimelineManager, @@ -57,12 +62,21 @@ timelineManager, customLayout, onSelect, - onSelectAssets, - onSelectAssetCandidates, onScrollCompensation, + scrollTop, onThumbnailClick, }: Props = $props(); + const actionLib = new DateGroupActionLib(); + + onMount(() => { + actionLib.assetInteraction = assetInteraction; + actionLib.timelineManager = timelineManager; + actionLib.singleSelect = singleSelect; + actionLib.onSelect = onSelect; + actionLib.scrollTop = scrollTop; + }); + let isMouseOverGroup = $state(false); let hoveredDayGroup = $state(); @@ -83,15 +97,14 @@ void navigate({ targetRoute: 'current', assetId: asset.id }); }; - const handleSelectGroup = (title: string, assets: TimelineAsset[]) => onSelect({ title, assets }); - + // called when clicking asset with shift key pressed or with mouse const assetSelectHandler = ( timelineManager: TimelineManager, asset: TimelineAsset, assetsInDayGroup: TimelineAsset[], groupTitle: string, ) => { - onSelectAssets(asset); + void actionLib.onSelectAssets(asset); // Check if all assets are selected in a group to toggle the group selection's icon let selectedAssetsInGroupCount = assetsInDayGroup.filter((asset) => @@ -117,7 +130,7 @@ hoveredDayGroup = groupTitle; if (assetInteraction.selectionActive) { - onSelectAssetCandidates(asset); + actionLib.onSelectAssetCandidates(asset); } }; @@ -143,6 +156,8 @@ }); + + {#each filterIntersecting(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)} {@const absoluteWidth = dayGroup.left} @@ -173,8 +188,10 @@
handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))} - onkeydown={() => handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))} + onclick={() => + actionLib.onDateGroupSelect({ title: dayGroup.groupTitle, assets: assetsSnapshot(dayGroup.getAssets()) })} + onkeydown={() => + actionLib.onDateGroupSelect({ title: dayGroup.groupTitle, assets: assetsSnapshot(dayGroup.getAssets()) })} > {#if assetInteraction.selectedGroup.has(dayGroup.groupTitle)} diff --git a/web/src/lib/components/photos-page/asset-grid-without-scrubber.svelte b/web/src/lib/components/photos-page/asset-grid-without-scrubber.svelte index a7b473c4d1..4e07b92790 100644 --- a/web/src/lib/components/photos-page/asset-grid-without-scrubber.svelte +++ b/web/src/lib/components/photos-page/asset-grid-without-scrubber.svelte @@ -2,7 +2,6 @@ import { afterNavigate, beforeNavigate } from '$app/navigation'; import { page } from '$app/stores'; import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer'; - import AssetDateGroupActions from '$lib/components/photos-page/asset-date-group-actions.svelte'; import AssetGridActions from '$lib/components/photos-page/asset-grid-actions.svelte'; import AssetViewerAndActions from '$lib/components/photos-page/asset-viewer-and-actions.svelte'; import Skeleton from '$lib/components/photos-page/skeleton.svelte'; @@ -75,7 +74,7 @@ album = null, person = null, isShowDeleteConfirmation = $bindable(false), - onSelect = () => {}, + onSelect = (asset: TimelineAsset) => void 0, onEscape = () => {}, children, empty, @@ -278,16 +277,6 @@ let onSelectAssetCandidates = <(asset: TimelineAsset | null) => void>$state(); - - @@ -356,9 +345,8 @@ {isSelectionMode} {singleSelect} {monthGroup} - onSelect={onDateGroupSelect} - {onSelectAssetCandidates} - {onSelectAssets} + {onSelect} + {scrollTop} onScrollCompensation={handleScrollCompensation} {customLayout} {onThumbnailClick} diff --git a/web/src/lib/components/photos-page/date-group-actions-lib.svelte.ts b/web/src/lib/components/photos-page/date-group-actions-lib.svelte.ts new file mode 100644 index 0000000000..1521091267 --- /dev/null +++ b/web/src/lib/components/photos-page/date-group-actions-lib.svelte.ts @@ -0,0 +1,188 @@ +import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; +import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; +import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte'; +import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; +import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte'; +import { searchStore } from '$lib/stores/search.svelte'; + +/** Glue functions that update the assetInteraction (asset selection) in response to AssetDateGroup UI + * + */ +export class DateGroupActionLib { + onSelect: (asset: TimelineAsset) => void = () => void 0; + scrollTop: (top: number) => void = () => void 0; + assetInteraction: AssetInteraction = $state(new AssetInteraction()); + timelineManager: TimelineManager = $state(new TimelineManager()); + singleSelect: boolean = $state(false); + lastAssetMouseEvent: TimelineAsset | null = $state(null); + shiftKeyIsDown = $state(false); + isEmpty = $derived(this.timelineManager.isInitialized && this.timelineManager.months.length === 0); + + constructor() { + $effect(() => { + if (!this.lastAssetMouseEvent || !this.lastAssetMouseEvent) { + this.assetInteraction.clearAssetSelectionCandidates(); + } + if (this.shiftKeyIsDown && this.lastAssetMouseEvent) { + void this.selectAssetCandidates(this.lastAssetMouseEvent); + } + if (this.isEmpty) { + this.assetInteraction.clearMultiselect(); + } + }); + } + + handleSelectAsset(asset: TimelineAsset) { + if (!this.timelineManager.albumAssets.has(asset.id)) { + this.assetInteraction.selectAsset(asset); + } + } + + onKeyDown = (event: KeyboardEvent) => { + if (searchStore.isSearchEnabled) { + return; + } + + if (event.key === 'Shift') { + event.preventDefault(); + this.shiftKeyIsDown = true; + } + }; + + onKeyUp = (event: KeyboardEvent) => { + if (searchStore.isSearchEnabled) { + return; + } + + if (event.key === 'Shift') { + event.preventDefault(); + this.shiftKeyIsDown = false; + } + }; + + onSelectAssetCandidates = (asset: TimelineAsset | null) => { + if (asset) { + void this.selectAssetCandidates(asset); + } + this.lastAssetMouseEvent = asset; + }; + + onDateGroupSelect = ({ title: group, assets }: { title: string; assets: TimelineAsset[] }) => { + if (this.assetInteraction.selectedGroup.has(group)) { + this.assetInteraction.removeGroupFromMultiselectGroup(group); + for (const asset of assets) { + this.assetInteraction.removeAssetFromMultiselectGroup(asset.id); + } + } else { + this.assetInteraction.addGroupToMultiselectGroup(group); + for (const asset of assets) { + this.handleSelectAsset(asset); + } + } + + if (this.timelineManager.assetCount == this.assetInteraction.selectedAssets.length) { + isSelectingAllAssets.set(true); + } else { + isSelectingAllAssets.set(false); + } + }; + + onSelectAssets = async (asset: TimelineAsset) => { + if (!asset) { + return; + } + this.onSelect(asset); + + if (this.singleSelect) { + this.scrollTop(0); + + return; + } + + const rangeSelection = this.assetInteraction.assetSelectionCandidates.length > 0; + const deselect = this.assetInteraction.hasSelectedAsset(asset.id); + + // Select/deselect already loaded assets + if (deselect) { + for (const candidate of this.assetInteraction.assetSelectionCandidates) { + this.assetInteraction.removeAssetFromMultiselectGroup(candidate.id); + } + this.assetInteraction.removeAssetFromMultiselectGroup(asset.id); + } else { + for (const candidate of this.assetInteraction.assetSelectionCandidates) { + this.handleSelectAsset(candidate); + } + this.handleSelectAsset(asset); + } + + this.assetInteraction.clearAssetSelectionCandidates(); + + if (this.assetInteraction.assetSelectionStart && rangeSelection) { + let startBucket = this.timelineManager.getMonthGroupByAssetId(this.assetInteraction.assetSelectionStart.id); + let endBucket = this.timelineManager.getMonthGroupByAssetId(asset.id); + + if (startBucket === null || endBucket === null) { + return; + } + + // Select/deselect assets in range (start,end) + let started = false; + for (const monthGroup of this.timelineManager.months) { + if (monthGroup === endBucket) { + break; + } + if (started) { + await this.timelineManager.loadMonthGroup(monthGroup.yearMonth); + for (const asset of monthGroup.assetsIterator()) { + if (deselect) { + this.assetInteraction.removeAssetFromMultiselectGroup(asset.id); + } else { + this.handleSelectAsset(asset); + } + } + } + if (monthGroup === startBucket) { + started = true; + } + } + + // Update date group selection in range [start,end] + started = false; + for (const monthGroup of this.timelineManager.months) { + if (monthGroup === startBucket) { + started = true; + } + if (started) { + // Split month group into day groups and check each group + for (const dayGroup of monthGroup.dayGroups) { + const dayGroupTitle = dayGroup.groupTitle; + if (dayGroup.getAssets().every((a) => this.assetInteraction.hasSelectedAsset(a.id))) { + this.assetInteraction.addGroupToMultiselectGroup(dayGroupTitle); + } else { + this.assetInteraction.removeGroupFromMultiselectGroup(dayGroupTitle); + } + } + } + if (monthGroup === endBucket) { + break; + } + } + } + + this.assetInteraction.setAssetSelectionStart(deselect ? null : asset); + }; + + selectAssetCandidates = async (endAsset: TimelineAsset) => { + if (!this.shiftKeyIsDown) { + return; + } + + const startAsset = this.assetInteraction.assetSelectionStart; + if (!startAsset) { + return; + } + + const assets = assetsSnapshot(await this.timelineManager.retrieveRange(startAsset, endAsset)); + this.assetInteraction.setAssetSelectionCandidates(assets); + }; +}