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);
+ };
+}