From 9656bc77a8e0b4d68f6ddc3e0319cd0070c1c3a6 Mon Sep 17 00:00:00 2001 From: midzelis Date: Sun, 21 Sep 2025 19:41:01 +0000 Subject: [PATCH] refactor(web): Extract asset grid layout component from TimelineDateGroup and split into AssetLayout and Month components --- .../components/timeline/AssetLayout.svelte | 68 +++++ web/src/lib/components/timeline/Month.svelte | 120 +++++++++ .../lib/components/timeline/Timeline.svelte | 103 ++++++-- .../timeline/TimelineDateGroup.svelte | 246 ------------------ .../(user)/utilities/geolocation/+page.svelte | 2 +- 5 files changed, 275 insertions(+), 264 deletions(-) create mode 100644 web/src/lib/components/timeline/AssetLayout.svelte create mode 100644 web/src/lib/components/timeline/Month.svelte delete mode 100644 web/src/lib/components/timeline/TimelineDateGroup.svelte diff --git a/web/src/lib/components/timeline/AssetLayout.svelte b/web/src/lib/components/timeline/AssetLayout.svelte new file mode 100644 index 0000000000..feb0f9d691 --- /dev/null +++ b/web/src/lib/components/timeline/AssetLayout.svelte @@ -0,0 +1,68 @@ + + + +
+ {#each filterIntersecting(viewerAssets) as viewerAsset (viewerAsset.id)} + {@const position = viewerAsset.position!} + {@const asset = viewerAsset.asset!} + + +
+ {@render thumbnail({ asset, position })} + {@render customThumbnailLayout?.(asset)} +
+ {/each} +
+ + diff --git a/web/src/lib/components/timeline/Month.svelte b/web/src/lib/components/timeline/Month.svelte new file mode 100644 index 0000000000..774e266317 --- /dev/null +++ b/web/src/lib/components/timeline/Month.svelte @@ -0,0 +1,120 @@ + + +{#each filterIntersecting(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)} + {@const absoluteWidth = dayGroup.left} + {@const isDayGroupSelected = assetInteraction.selectedGroup.has(dayGroup.groupTitle)} + +
{ + isMouseOverGroup = true; + hoveredDayGroup = dayGroup.groupTitle; + }} + onmouseleave={() => { + isMouseOverGroup = false; + hoveredDayGroup = null; + }} + > + +
+ {#if !singleSelect} +
onDayGroupSelect(dayGroup, assetsSnapshot(dayGroup.getAssets()))} + onkeydown={() => onDayGroupSelect(dayGroup, assetsSnapshot(dayGroup.getAssets()))} + > + {#if isDayGroupSelected} + + {:else} + + {/if} +
+ {/if} + + + {dayGroup.groupTitle} + +
+ + + {#snippet thumbnail({ asset, position })} + {@render thumbnailWithGroup({ asset, position, dayGroup, groupIndex })} + {/snippet} + +
+{/each} + + diff --git a/web/src/lib/components/timeline/Timeline.svelte b/web/src/lib/components/timeline/Timeline.svelte index 044a45e725..714b7bb4b6 100644 --- a/web/src/lib/components/timeline/Timeline.svelte +++ b/web/src/lib/components/timeline/Timeline.svelte @@ -2,6 +2,8 @@ import { afterNavigate, beforeNavigate } from '$app/navigation'; import { page } from '$app/state'; import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer'; + import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; + import Month from '$lib/components/timeline/Month.svelte'; import Scrubber from '$lib/components/timeline/Scrubber.svelte'; import TimelineAssetViewer from '$lib/components/timeline/TimelineAssetViewer.svelte'; import TimelineKeyboardActions from '$lib/components/timeline/actions/TimelineKeyboardActions.svelte'; @@ -19,13 +21,12 @@ import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte'; import { mobileDevice } from '$lib/stores/mobile-device.svelte'; - import { isAssetViewerRoute } from '$lib/utils/navigation'; + import { isAssetViewerRoute, navigate } from '$lib/utils/navigation'; import { getTimes, type ScrubberListener } from '$lib/utils/timeline-util'; import { type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk'; import { DateTime } from 'luxon'; import { onDestroy, onMount, type Snippet } from 'svelte'; import type { UpdatePayload } from 'vite'; - import TimelineDateGroup from './TimelineDateGroup.svelte'; interface Props { isSelectionMode?: boolean; @@ -54,7 +55,7 @@ onEscape?: () => void; children?: Snippet; empty?: Snippet; - customLayout?: Snippet<[TimelineAsset]>; + customThumbnailLayout?: Snippet<[TimelineAsset]>; onThumbnailClick?: ( asset: TimelineAsset, timelineManager: TimelineManager, @@ -86,7 +87,7 @@ onEscape = () => {}, children, empty, - customLayout, + customThumbnailLayout, onThumbnailClick, }: Props = $props(); @@ -392,7 +393,8 @@ lastAssetMouseEvent = asset; }; - const handleGroupSelect = (timelineManager: TimelineManager, group: string, assets: TimelineAsset[]) => { + const handleGroupSelect = (dayGroup: DayGroup, assets: TimelineAsset[]) => { + const group = dayGroup.groupTitle; if (assetInteraction.selectedGroup.has(group)) { assetInteraction.removeGroupFromMultiselectGroup(group); for (const asset of assets) { @@ -412,7 +414,7 @@ } }; - const handleSelectAssets = async (asset: TimelineAsset) => { + const onSelectAssets = async (asset: TimelineAsset) => { if (!asset) { return; } @@ -534,6 +536,46 @@ void timelineManager.loadMonthGroup({ year: localDateTime.year, month: localDateTime.month }); } }); + + const assetSelectHandler = ( + timelineManager: TimelineManager, + asset: TimelineAsset, + assetsInDayGroup: TimelineAsset[], + groupTitle: string, + ) => { + void onSelectAssets(asset); + + // Check if all assets are selected in a group to toggle the group selection's icon + let selectedAssetsInGroupCount = assetsInDayGroup.filter((asset) => + assetInteraction.hasSelectedAsset(asset.id), + ).length; + + // if all assets are selected in a group, add the group to selected group + if (selectedAssetsInGroupCount == assetsInDayGroup.length) { + assetInteraction.addGroupToMultiselectGroup(groupTitle); + } else { + assetInteraction.removeGroupFromMultiselectGroup(groupTitle); + } + + if (timelineManager.assetCount == assetInteraction.selectedAssets.length) { + isSelectingAllAssets.set(true); + } else { + isSelectingAllAssets.set(false); + } + }; + + const _onClick = ( + timelineManager: TimelineManager, + assets: TimelineAsset[], + groupTitle: string, + asset: TimelineAsset, + ) => { + if (isSelectionMode || assetInteraction.selectionActive) { + assetSelectHandler(timelineManager, asset, assets, groupTitle); + return; + } + void navigate({ targetRoute: 'current', assetId: asset.id }); + }; @@ -643,20 +685,47 @@ style:transform={`translate3d(0,${absoluteHeight}px,0)`} style:width="100%" > - handleGroupSelect(timelineManager, title, assets)} - onSelectAssetCandidates={handleSelectAssetCandidates} - onSelectAssets={handleSelectAssets} - {customLayout} - {onThumbnailClick} - /> + manager={timelineManager} + onDayGroupSelect={handleGroupSelect} + > + {#snippet thumbnail({ asset, position, dayGroup, groupIndex })} + {@const isAssetSelectionCandidate = assetInteraction.hasSelectionCandidate(asset.id)} + {@const isAssetSelected = + assetInteraction.hasSelectedAsset(asset.id) || timelineManager.albumAssets.has(asset.id)} + {@const isAssetDisabled = timelineManager.albumAssets.has(asset.id)} + { + if (typeof onThumbnailClick === 'function') { + onThumbnailClick(asset, timelineManager, dayGroup, _onClick); + } else { + _onClick(timelineManager, dayGroup.getAssets(), dayGroup.groupTitle, asset); + } + }} + onSelect={() => { + if (isSelectionMode || assetInteraction.selectionActive) { + assetSelectHandler(timelineManager, asset, dayGroup.getAssets(), dayGroup.groupTitle); + return; + } + void onSelectAssets(asset); + }} + onMouseEvent={() => handleSelectAssetCandidates(asset)} + selected={isAssetSelected} + selectionCandidate={isAssetSelectionCandidate} + disabled={isAssetDisabled} + thumbnailWidth={position.width} + thumbnailHeight={position.height} + /> + {/snippet} + {/if} {/each} diff --git a/web/src/lib/components/timeline/TimelineDateGroup.svelte b/web/src/lib/components/timeline/TimelineDateGroup.svelte deleted file mode 100644 index cd0dc9a212..0000000000 --- a/web/src/lib/components/timeline/TimelineDateGroup.svelte +++ /dev/null @@ -1,246 +0,0 @@ - - -{#each filterIntersecting(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)} - {@const absoluteWidth = dayGroup.left} - - -
{ - isMouseOverGroup = true; - assetMouseEventHandler(dayGroup.groupTitle, null); - }} - onmouseleave={() => { - isMouseOverGroup = false; - assetMouseEventHandler(dayGroup.groupTitle, null); - }} - > - -
- {#if !singleSelect} -
handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))} - onkeydown={() => handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))} - > - {#if assetInteraction.selectedGroup.has(dayGroup.groupTitle)} - - {:else} - - {/if} -
- {/if} - - - {dayGroup.groupTitle} - -
- - -
- {#each filterIntersecting(dayGroup.viewerAssets) as viewerAsset (viewerAsset.id)} - {@const position = viewerAsset.position!} - {@const asset = viewerAsset.asset!} - - - -
- { - if (typeof onThumbnailClick === 'function') { - onThumbnailClick(asset, timelineManager, dayGroup, _onClick); - } else { - _onClick(timelineManager, dayGroup.getAssets(), dayGroup.groupTitle, asset); - } - }} - onSelect={(asset) => assetSelectHandler(timelineManager, asset, dayGroup.getAssets(), dayGroup.groupTitle)} - onMouseEvent={() => assetMouseEventHandler(dayGroup.groupTitle, assetSnapshot(asset))} - selected={assetInteraction.hasSelectedAsset(asset.id) || - dayGroup.monthGroup.timelineManager.albumAssets.has(asset.id)} - selectionCandidate={assetInteraction.hasSelectionCandidate(asset.id)} - disabled={dayGroup.monthGroup.timelineManager.albumAssets.has(asset.id)} - thumbnailWidth={position.width} - thumbnailHeight={position.height} - /> - {#if customLayout} - {@render customLayout(asset)} - {/if} -
- - {/each} -
-
-{/each} - - diff --git a/web/src/routes/(user)/utilities/geolocation/+page.svelte b/web/src/routes/(user)/utilities/geolocation/+page.svelte index 732e0625ab..e5b5f20d99 100644 --- a/web/src/routes/(user)/utilities/geolocation/+page.svelte +++ b/web/src/routes/(user)/utilities/geolocation/+page.svelte @@ -196,7 +196,7 @@ withStacked onThumbnailClick={handleThumbnailClick} > - {#snippet customLayout(asset: TimelineAsset)} + {#snippet customThumbnailLayout(asset: TimelineAsset)} {#if hasGps(asset)}
{asset.city || $t('gps')}