diff --git a/web/src/lib/components/timeline/AssetLayout.svelte b/web/src/lib/components/timeline/AssetLayout.svelte new file mode 100644 index 0000000000..fb510d5133 --- /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/MonthSegment.svelte b/web/src/lib/components/timeline/MonthSegment.svelte index 0a58d3f191..ce26314440 100644 --- a/web/src/lib/components/timeline/MonthSegment.svelte +++ b/web/src/lib/components/timeline/MonthSegment.svelte @@ -1,140 +1,49 @@ {#each filterIntersecting(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)} {@const absoluteWidth = dayGroup.left} - + {@const isDayGroupSelected = assetInteraction.selectedGroup.has(dayGroup.groupTitle)}
{ isMouseOverGroup = true; - assetMouseEventHandler(dayGroup.groupTitle, null); + hoveredDayGroup = dayGroup.groupTitle; }} onmouseleave={() => { isMouseOverGroup = false; - assetMouseEventHandler(dayGroup.groupTitle, null); + hoveredDayGroup = null; }} > @@ -163,10 +72,10 @@ class="hover:cursor-pointer transition-all duration-200 ease-out overflow-hidden w-0" class:w-8={(hoveredDayGroup === dayGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dayGroup.groupTitle)} - onclick={() => handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))} - onkeydown={() => handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))} + onclick={() => onDayGroupSelect(dayGroup, assetsSnapshot(dayGroup.getAssets()))} + onkeydown={() => onDayGroupSelect(dayGroup, assetsSnapshot(dayGroup.getAssets()))} > - {#if assetInteraction.selectedGroup.has(dayGroup.groupTitle)} + {#if isDayGroupSelected} {:else} @@ -179,57 +88,17 @@ - -
- {#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} -
+ {#snippet thumbnail({ asset, position })} + {@render thumbnailWithGroup({ asset, position, dayGroup, groupIndex })} + {/snippet} +
{/each} @@ -237,7 +106,4 @@ section { contain: layout paint style; } - [data-image-grid] { - user-select: none; - } diff --git a/web/src/lib/components/timeline/Timeline.svelte b/web/src/lib/components/timeline/Timeline.svelte index 6aadb4adaf..9f24ab9d30 100644 --- a/web/src/lib/components/timeline/Timeline.svelte +++ b/web/src/lib/components/timeline/Timeline.svelte @@ -2,6 +2,7 @@ import { afterNavigate, beforeNavigate } from '$app/navigation'; import { page } from '$app/stores'; import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer'; + import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; import MonthSegment from '$lib/components/timeline/MonthSegment.svelte'; import Scrubber from '$lib/components/timeline/Scrubber.svelte'; import TimelineAssetViewer from '$lib/components/timeline/TimelineAssetViewer.svelte'; @@ -57,7 +58,7 @@ onEscape?: () => void; children?: Snippet; empty?: Snippet; - customLayout?: Snippet<[TimelineAsset]>; + customThumbnailLayout?: Snippet<[TimelineAsset]>; onThumbnailClick?: ( asset: TimelineAsset, timelineManager: TimelineManager, @@ -88,7 +89,7 @@ onEscape = () => {}, children, empty, - customLayout, + customThumbnailLayout, onThumbnailClick, }: Props = $props(); @@ -419,7 +420,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) { @@ -439,7 +441,7 @@ } }; - const handleSelectAssets = async (asset: TimelineAsset) => { + const onSelectAssets = async (asset: TimelineAsset) => { if (!asset) { return; } @@ -561,6 +563,46 @@ void timelineManager.loadSegment(getSegmentIdentifier({ 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 }); + }; @@ -661,20 +703,46 @@ style:width="100%" > handleGroupSelect(timelineManager, title, assets)} - onSelectAssetCandidates={handleSelectAssetCandidates} - onSelectAssets={handleSelectAssets} - onScrollCompensation={handleScrollCompensation} - {customLayout} - {onThumbnailClick} - /> + {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/routes/(user)/utilities/geolocation/+page.svelte b/web/src/routes/(user)/utilities/geolocation/+page.svelte index 5441257df4..3d6f953a58 100644 --- a/web/src/routes/(user)/utilities/geolocation/+page.svelte +++ b/web/src/routes/(user)/utilities/geolocation/+page.svelte @@ -195,7 +195,7 @@ withStacked onThumbnailClick={handleThumbnailClick} > - {#snippet customLayout(asset: TimelineAsset)} + {#snippet customThumbnailLayout(asset: TimelineAsset)} {#if hasGps(asset)}
{asset.city || $t('gps')}