refactor(web): rename MonthGroup to TimelineMonth (#27447)

Rename MonthGroup class to TimelineMonth to better convey that it represents a single month within the timeline. Updates the file, class, and all references across 16 files.

Change-Id: Id50fd6d4b7d0e431571b67c0f81c0e316a6a6964
This commit is contained in:
Min Idzelis
2026-04-03 13:27:12 -04:00
committed by GitHub
parent 207672c481
commit 649d14822a
16 changed files with 295 additions and 274 deletions
+8 -8
View File
@@ -2,7 +2,7 @@
import AssetLayout from '$lib/components/timeline/AssetLayout.svelte'; import AssetLayout from '$lib/components/timeline/AssetLayout.svelte';
import type { AssetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte'; import type { AssetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
import { TimelineDay } from '$lib/managers/timeline-manager/timeline-day.svelte'; import { TimelineDay } from '$lib/managers/timeline-manager/timeline-day.svelte';
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte'; import type { TimelineMonth } from '$lib/managers/timeline-manager/timeline-month.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { assetsSnapshot, filterIsInOrNearViewport } from '$lib/managers/timeline-manager/utils.svelte'; import { assetsSnapshot, filterIsInOrNearViewport } from '$lib/managers/timeline-manager/utils.svelte';
import type { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte'; import type { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte';
@@ -27,7 +27,7 @@
customThumbnailLayout?: Snippet<[TimelineAsset]>; customThumbnailLayout?: Snippet<[TimelineAsset]>;
singleSelect: boolean; singleSelect: boolean;
assetInteraction: AssetMultiSelectManager; assetInteraction: AssetMultiSelectManager;
monthGroup: MonthGroup; timelineMonth: TimelineMonth;
manager: VirtualScrollManager; manager: VirtualScrollManager;
onTimelineDaySelect: (timelineDay: TimelineDay, assets: TimelineAsset[]) => void; onTimelineDaySelect: (timelineDay: TimelineDay, assets: TimelineAsset[]) => void;
}; };
@@ -36,7 +36,7 @@
customThumbnailLayout, customThumbnailLayout,
singleSelect, singleSelect,
assetInteraction, assetInteraction,
monthGroup, timelineMonth,
manager, manager,
onTimelineDaySelect, onTimelineDaySelect,
}: Props = $props(); }: Props = $props();
@@ -44,10 +44,10 @@
let { isUploading } = uploadAssetsStore; let { isUploading } = uploadAssetsStore;
let hoveredTimelineDay = $state<string | null>(null); let hoveredTimelineDay = $state<string | null>(null);
const transitionDuration = $derived(monthGroup.timelineManager.suspendTransitions && !$isUploading ? 0 : 150); const transitionDuration = $derived(timelineMonth.timelineManager.suspendTransitions && !$isUploading ? 0 : 150);
const getTimelineDayFullDate = (timelineDay: TimelineDay): string => { const getTimelineDayFullDate = (timelineDay: TimelineDay): string => {
const { month, year } = timelineDay.monthGroup.yearMonth; const { month, year } = timelineDay.timelineMonth.yearMonth;
const date = fromTimelinePlainDate({ const date = fromTimelinePlainDate({
year, year,
month, month,
@@ -57,13 +57,13 @@
}; };
</script> </script>
{#each filterIsInOrNearViewport(monthGroup.timelineDays) as timelineDay, groupIndex (timelineDay.day)} {#each filterIsInOrNearViewport(timelineMonth.timelineDays) as timelineDay, groupIndex (timelineDay.day)}
{@const isTimelineDaySelected = assetInteraction.selectedGroup.has(timelineDay.groupTitle)} {@const isTimelineDaySelected = assetInteraction.selectedGroup.has(timelineDay.groupTitle)}
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<section <section
class={[ class={[
{ 'transition-all': !monthGroup.timelineManager.suspendTransitions }, { 'transition-all': !timelineMonth.timelineManager.suspendTransitions },
!monthGroup.timelineManager.suspendTransitions && `delay-${transitionDuration}`, !timelineMonth.timelineManager.suspendTransitions && `delay-${transitionDuration}`,
]} ]}
data-group data-group
style:position="absolute" style:position="absolute"
@@ -92,7 +92,7 @@
scrubberWidth = usingMobileDevice ? MOBILE_WIDTH : DESKTOP_WIDTH; scrubberWidth = usingMobileDevice ? MOBILE_WIDTH : DESKTOP_WIDTH;
}); });
const toScrollFromMonthGroupPercentage = ( const toScrollFromTimelineMonthPercentage = (
scrubberMonth: ViewportTopMonth, scrubberMonth: ViewportTopMonth,
scrubberMonthPercent: number, scrubberMonthPercent: number,
scrubOverallPercent: number, scrubOverallPercent: number,
@@ -125,7 +125,7 @@
} }
}; };
const scrollY = $derived( const scrollY = $derived(
toScrollFromMonthGroupPercentage(viewportTopMonth, viewportTopMonthScrollPercent, timelineScrollPercent), toScrollFromTimelineMonthPercentage(viewportTopMonth, viewportTopMonthScrollPercent, timelineScrollPercent),
); );
const timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight); const timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight);
const relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight)); const relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
@@ -281,12 +281,12 @@
const boundingClientRect = bestElement.boundingClientRect; const boundingClientRect = bestElement.boundingClientRect;
const sy = boundingClientRect.y; const sy = boundingClientRect.y;
const relativeY = y - sy; const relativeY = y - sy;
const monthGroupPercentY = relativeY / boundingClientRect.height; const timelineMonthPercentY = relativeY / boundingClientRect.height;
return { return {
isOnPaddingTop: false, isOnPaddingTop: false,
isOnPaddingBottom: false, isOnPaddingBottom: false,
segment, segment,
monthGroupPercentY, timelineMonthPercentY,
}; };
} }
@@ -309,7 +309,7 @@
isOnPaddingTop, isOnPaddingTop,
isOnPaddingBottom, isOnPaddingBottom,
segment: undefined, segment: undefined,
monthGroupPercentY: 0, timelineMonthPercentY: 0,
}; };
}; };
@@ -328,7 +328,7 @@
const upper = rect?.height - (PADDING_TOP + PADDING_BOTTOM); const upper = rect?.height - (PADDING_TOP + PADDING_BOTTOM);
hoverY = clamp(clientY - rect?.top - PADDING_TOP, lower, upper); hoverY = clamp(clientY - rect?.top - PADDING_TOP, lower, upper);
const x = rect!.left + rect!.width / 2; const x = rect!.left + rect!.width / 2;
const { segment, monthGroupPercentY, isOnPaddingTop, isOnPaddingBottom } = getActive(x, clientY); const { segment, timelineMonthPercentY, isOnPaddingTop, isOnPaddingBottom } = getActive(x, clientY);
activeSegment = segment; activeSegment = segment;
isHoverOnPaddingTop = isOnPaddingTop; isHoverOnPaddingTop = isOnPaddingTop;
isHoverOnPaddingBottom = isOnPaddingBottom; isHoverOnPaddingBottom = isOnPaddingBottom;
@@ -336,7 +336,7 @@
const scrubData = { const scrubData = {
scrubberMonth: segmentDate, scrubberMonth: segmentDate,
overallScrollPercent: toTimelineY(hoverY), overallScrollPercent: toTimelineY(hoverY),
scrubberMonthScrollPercent: monthGroupPercentY, scrubberMonthScrollPercent: timelineMonthPercentY,
}; };
if (wasDragging === false && isDragging) { if (wasDragging === false && isDragging) {
void startScrub?.(scrubData); void startScrub?.(scrubData);
+49 -48
View File
@@ -15,7 +15,7 @@
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte'; import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import type { TimelineDay } from '$lib/managers/timeline-manager/timeline-day.svelte'; import type { TimelineDay } from '$lib/managers/timeline-manager/timeline-day.svelte';
import { isIntersecting } from '$lib/managers/timeline-manager/internal/intersection-support.svelte'; import { isIntersecting } from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte'; import type { TimelineMonth } from '$lib/managers/timeline-manager/timeline-month.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset, TimelineManagerOptions, ViewportTopMonth } from '$lib/managers/timeline-manager/types'; import type { TimelineAsset, TimelineManagerOptions, ViewportTopMonth } from '$lib/managers/timeline-manager/types';
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte'; import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
@@ -121,10 +121,11 @@
timelineManager.scrollableElement = scrollableElement; timelineManager.scrollableElement = scrollableElement;
}); });
const getAssetPosition = (assetId: string, monthGroup: MonthGroup) => monthGroup.findAssetAbsolutePosition(assetId); const getAssetPosition = (assetId: string, timelineMonth: TimelineMonth) =>
timelineMonth.findAssetAbsolutePosition(assetId);
const scrollToAssetPosition = (assetId: string, monthGroup: MonthGroup) => { const scrollToAssetPosition = (assetId: string, timelineMonth: TimelineMonth) => {
const position = getAssetPosition(assetId, monthGroup); const position = getAssetPosition(assetId, timelineMonth);
if (!position) { if (!position) {
return; return;
@@ -176,11 +177,11 @@
// the performance benefits of deferred layouts while still supporting deep linking // the performance benefits of deferred layouts while still supporting deep linking
// to assets at the end of the timeline. // to assets at the end of the timeline.
timelineManager.isScrollingOnLoad = true; timelineManager.isScrollingOnLoad = true;
const monthGroup = await timelineManager.findMonthGroupForAsset({ id: assetId }); const timelineMonth = await timelineManager.findTimelineMonthForAsset({ id: assetId });
if (!monthGroup) { if (!timelineMonth) {
return false; return false;
} }
scrollToAssetPosition(assetId, monthGroup); scrollToAssetPosition(assetId, timelineMonth);
return true; return true;
} finally { } finally {
timelineManager.isScrollingOnLoad = false; timelineManager.isScrollingOnLoad = false;
@@ -188,11 +189,11 @@
}; };
const scrollToAsset = (asset: TimelineAsset) => { const scrollToAsset = (asset: TimelineAsset) => {
const monthGroup = timelineManager.getMonthGroupByAssetId(asset.id); const timelineMonth = timelineManager.getTimelineMonthByAssetId(asset.id);
if (!monthGroup) { if (!timelineMonth) {
return false; return false;
} }
scrollToAssetPosition(asset.id, monthGroup); scrollToAssetPosition(asset.id, timelineMonth);
return true; return true;
}; };
@@ -262,10 +263,10 @@
} }
}); });
const scrollToSegmentPercentage = (segmentTop: number, segmentHeight: number, monthGroupScrollPercent: number) => { const scrollToSegmentPercentage = (segmentTop: number, segmentHeight: number, timelineMonthScrollPercent: number) => {
const topOffset = segmentTop; const topOffset = segmentTop;
const maxScrollPercent = timelineManager.maxScrollPercent; const maxScrollPercent = timelineManager.maxScrollPercent;
const delta = segmentHeight * monthGroupScrollPercent; const delta = segmentHeight * timelineMonthScrollPercent;
const scrollToTop = (topOffset + delta) * maxScrollPercent; const scrollToTop = (topOffset + delta) * maxScrollPercent;
timelineManager.scrollTo(scrollToTop); timelineManager.scrollTo(scrollToTop);
@@ -294,13 +295,13 @@
scrubberMonthScrollPercent, scrubberMonthScrollPercent,
); );
} else { } else {
const monthGroup = timelineManager.months.find( const timelineMonth = timelineManager.months.find(
({ yearMonth: { year, month } }) => year === scrubberMonth.year && month === scrubberMonth.month, ({ yearMonth: { year, month } }) => year === scrubberMonth.year && month === scrubberMonth.month,
); );
if (!monthGroup) { if (!timelineMonth) {
return; return;
} }
scrollToSegmentPercentage(monthGroup.top, monthGroup.height, scrubberMonthScrollPercent); scrollToSegmentPercentage(timelineMonth.top, timelineMonth.height, scrubberMonthScrollPercent);
} }
}; };
@@ -325,28 +326,28 @@
const monthsLength = timelineManager.months.length; const monthsLength = timelineManager.months.length;
for (let i = -1; i < monthsLength + 1; i++) { for (let i = -1; i < monthsLength + 1; i++) {
let monthGroup: ViewportTopMonth; let timelineMonth: ViewportTopMonth;
let monthGroupHeight: number; let timelineMonthHeight: number;
if (i === -1) { if (i === -1) {
// lead-in // lead-in
monthGroup = 'lead-in'; timelineMonth = 'lead-in';
monthGroupHeight = timelineManager.topSectionHeight; timelineMonthHeight = timelineManager.topSectionHeight;
} else if (i === monthsLength) { } else if (i === monthsLength) {
// lead-out // lead-out
monthGroup = 'lead-out'; timelineMonth = 'lead-out';
monthGroupHeight = timelineManager.bottomSectionHeight; timelineMonthHeight = timelineManager.bottomSectionHeight;
} else { } else {
monthGroup = timelineManager.months[i].yearMonth; timelineMonth = timelineManager.months[i].yearMonth;
monthGroupHeight = timelineManager.months[i].height; timelineMonthHeight = timelineManager.months[i].height;
} }
let next = top - monthGroupHeight * maxScrollPercent; let next = top - timelineMonthHeight * maxScrollPercent;
// instead of checking for < 0, add a little wiggle room for subpixel resolution // instead of checking for < 0, add a little wiggle room for subpixel resolution
if (next < -1 && monthGroup) { if (next < -1 && timelineMonth) {
viewportTopMonth = monthGroup; viewportTopMonth = timelineMonth;
// allowing next to be at least 1 may cause percent to go negative, so ensure positive percentage // allowing next to be at least 1 may cause percent to go negative, so ensure positive percentage
viewportTopMonthScrollPercent = Math.max(0, top / (monthGroupHeight * maxScrollPercent)); viewportTopMonthScrollPercent = Math.max(0, top / (timelineMonthHeight * maxScrollPercent));
// compensate for lost precision/rounding errors advance to the next bucket, if present // compensate for lost precision/rounding errors advance to the next bucket, if present
if (viewportTopMonthScrollPercent > 0.9999 && i + 1 < monthsLength - 1) { if (viewportTopMonthScrollPercent > 0.9999 && i + 1 < monthsLength - 1) {
@@ -432,16 +433,16 @@
assetInteraction.clearCandidates(); assetInteraction.clearCandidates();
if (assetInteraction.startAsset && rangeSelection) { if (assetInteraction.startAsset && rangeSelection) {
const startBucket = timelineManager.getMonthGroupByAssetId(assetInteraction.startAsset.id); const startBucket = timelineManager.getTimelineMonthByAssetId(assetInteraction.startAsset.id);
const endBucket = timelineManager.getMonthGroupByAssetId(asset.id); const endBucket = timelineManager.getTimelineMonthByAssetId(asset.id);
if (!startBucket || !endBucket) { if (!startBucket || !endBucket) {
return; return;
} }
const monthGroups = timelineManager.months; const timelineMonths = timelineManager.months;
const startBucketIndex = monthGroups.indexOf(startBucket); const startBucketIndex = timelineMonths.indexOf(startBucket);
const endBucketIndex = monthGroups.indexOf(endBucket); const endBucketIndex = timelineMonths.indexOf(endBucket);
if (startBucketIndex === -1 || endBucketIndex === -1) { if (startBucketIndex === -1 || endBucketIndex === -1) {
return; return;
@@ -452,9 +453,9 @@
// Select/deselect assets in range (start,end) // Select/deselect assets in range (start,end)
for (let index = rangeStartIndex + 1; index < rangeEndIndex; index++) { for (let index = rangeStartIndex + 1; index < rangeEndIndex; index++) {
const monthGroup = monthGroups[index]; const timelineMonth = timelineMonths[index];
await timelineManager.loadMonthGroup(monthGroup.yearMonth); await timelineManager.loadTimelineMonth(timelineMonth.yearMonth);
for (const monthAsset of monthGroup.assetsIterator()) { for (const monthAsset of timelineMonth.assetsIterator()) {
if (deselect) { if (deselect) {
assetInteraction.removeAssetFromMultiselectGroup(monthAsset.id); assetInteraction.removeAssetFromMultiselectGroup(monthAsset.id);
} else { } else {
@@ -465,10 +466,10 @@
// Update date group selection in range [start,end] // Update date group selection in range [start,end]
for (let index = rangeStartIndex; index <= rangeEndIndex; index++) { for (let index = rangeStartIndex; index <= rangeEndIndex; index++) {
const monthGroup = monthGroups[index]; const timelineMonth = timelineMonths[index];
// Split month group into day groups and check each group // Split month group into day groups and check each group
for (const timelineDay of monthGroup.timelineDays) { for (const timelineDay of timelineMonth.timelineDays) {
const timelineDayTitle = timelineDay.groupTitle; const timelineDayTitle = timelineDay.groupTitle;
if (timelineDay.getAssets().every((a) => assetInteraction.hasSelectedAsset(a.id))) { if (timelineDay.getAssets().every((a) => assetInteraction.hasSelectedAsset(a.id))) {
assetInteraction.addGroupToMultiselectGroup(timelineDayTitle); assetInteraction.addGroupToMultiselectGroup(timelineDayTitle);
@@ -517,7 +518,7 @@
$effect(() => { $effect(() => {
if (assetViewerManager.asset && assetViewerManager.isViewing) { if (assetViewerManager.asset && assetViewerManager.isViewing) {
const { localDateTime } = getTimes(assetViewerManager.asset.fileCreatedAt, DateTime.local().offset / 60); const { localDateTime } = getTimes(assetViewerManager.asset.fileCreatedAt, DateTime.local().offset / 60);
void timelineManager.loadMonthGroup({ year: localDateTime.year, month: localDateTime.month }); void timelineManager.loadTimelineMonth({ year: localDateTime.year, month: localDateTime.month });
} }
}); });
@@ -643,23 +644,23 @@
{/if} {/if}
</section> </section>
{#each timelineManager.months as monthGroup (monthGroup.viewId)} {#each timelineManager.months as timelineMonth (timelineMonth.viewId)}
{@const isInOrNearViewport = monthGroup.isInOrNearViewport} {@const isInOrNearViewport = timelineMonth.isInOrNearViewport}
{@const absoluteHeight = monthGroup.top} {@const absoluteHeight = timelineMonth.top}
{#if !monthGroup.isLoaded} {#if !timelineMonth.isLoaded}
<div <div
style:height={monthGroup.height + 'px'} style:height={timelineMonth.height + 'px'}
style:position="absolute" style:position="absolute"
style:transform={`translate3d(0,${absoluteHeight}px,0)`} style:transform={`translate3d(0,${absoluteHeight}px,0)`}
style:width="100%" style:width="100%"
> >
<Skeleton {invisible} height={monthGroup.height} title={monthGroup.monthGroupTitle} /> <Skeleton {invisible} height={timelineMonth.height} title={timelineMonth.title} />
</div> </div>
{:else if isInOrNearViewport} {:else if isInOrNearViewport}
<div <div
class="month-group" class="timeline-month"
style:height={monthGroup.height + 'px'} style:height={timelineMonth.height + 'px'}
style:position="absolute" style:position="absolute"
style:transform={`translate3d(0,${absoluteHeight}px,0)`} style:transform={`translate3d(0,${absoluteHeight}px,0)`}
style:width="100%" style:width="100%"
@@ -668,7 +669,7 @@
{assetInteraction} {assetInteraction}
{customThumbnailLayout} {customThumbnailLayout}
{singleSelect} {singleSelect}
{monthGroup} {timelineMonth}
manager={timelineManager} manager={timelineManager}
onTimelineDaySelect={handleGroupSelect} onTimelineDaySelect={handleGroupSelect}
> >
@@ -735,7 +736,7 @@
scrollbar-width: none; scrollbar-width: none;
} }
.month-group { .timeline-month {
contain: layout size paint; contain: layout size paint;
transform-style: flat; transform-style: flat;
backface-visibility: hidden; backface-visibility: hidden;
@@ -1,7 +1,7 @@
import { setDifference, type TimelineDate } from '$lib/utils/timeline-util'; import { setDifference, type TimelineDate } from '$lib/utils/timeline-util';
import { AssetOrder } from '@immich/sdk'; import { AssetOrder } from '@immich/sdk';
import type { MonthGroup } from './month-group.svelte';
import type { TimelineDay } from './timeline-day.svelte'; import type { TimelineDay } from './timeline-day.svelte';
import type { TimelineMonth } from './timeline-month.svelte';
import type { TimelineAsset } from './types'; import type { TimelineAsset } from './types';
export class GroupInsertionCache { export class GroupInsertionCache {
@@ -34,23 +34,23 @@ export class GroupInsertionCache {
get updatedBuckets() { get updatedBuckets() {
// eslint-disable-next-line svelte/prefer-svelte-reactivity // eslint-disable-next-line svelte/prefer-svelte-reactivity
const updated = new Set<MonthGroup>(); const updated = new Set<TimelineMonth>();
for (const group of this.changedTimelineDays) { for (const group of this.changedTimelineDays) {
updated.add(group.monthGroup); updated.add(group.timelineMonth);
} }
return updated; return updated;
} }
get bucketsWithNewTimelineDays() { get bucketsWithNewTimelineDays() {
// eslint-disable-next-line svelte/prefer-svelte-reactivity // eslint-disable-next-line svelte/prefer-svelte-reactivity
const updated = new Set<MonthGroup>(); const updated = new Set<TimelineMonth>();
for (const group of this.newTimelineDays) { for (const group of this.newTimelineDays) {
updated.add(group.monthGroup); updated.add(group.timelineMonth);
} }
return updated; return updated;
} }
sort(monthGroup: MonthGroup, sortOrder: AssetOrder = AssetOrder.Desc) { sort(timelineMonth: TimelineMonth, sortOrder: AssetOrder = AssetOrder.Desc) {
for (const group of this.changedTimelineDays) { for (const group of this.changedTimelineDays) {
group.sortAssets(sortOrder); group.sortAssets(sortOrder);
} }
@@ -58,7 +58,7 @@ export class GroupInsertionCache {
group.sortAssets(sortOrder); group.sortAssets(sortOrder);
} }
if (this.newTimelineDays.size > 0) { if (this.newTimelineDays.size > 0) {
monthGroup.sortTimelineDays(); timelineMonth.sortTimelineDays();
} }
} }
} }
@@ -1,6 +1,6 @@
import { TUNABLES } from '$lib/utils/tunables'; import { TUNABLES } from '$lib/utils/tunables';
import type { MonthGroup } from '../month-group.svelte';
import { TimelineManager } from '../timeline-manager.svelte'; import { TimelineManager } from '../timeline-manager.svelte';
import type { TimelineMonth } from '../timeline-month.svelte';
const { const {
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM }, TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
@@ -40,7 +40,7 @@ function calculateViewportProximity(regionTop: number, regionBottom: number, win
return ViewportProximity.InViewport; return ViewportProximity.InViewport;
} }
export function updateMonthGroupViewportProximity(timelineManager: TimelineManager, month: MonthGroup) { export function updateTimelineMonthViewportProximity(timelineManager: TimelineManager, month: TimelineMonth) {
const proximity = calculateViewportProximity( const proximity = calculateViewportProximity(
month.top, month.top,
month.top + month.height, month.top + month.height,
@@ -1,8 +1,8 @@
import type { MonthGroup } from '../month-group.svelte';
import { TimelineManager } from '../timeline-manager.svelte'; import { TimelineManager } from '../timeline-manager.svelte';
import type { TimelineMonth } from '../timeline-month.svelte';
import type { UpdateGeometryOptions } from '../types'; import type { UpdateGeometryOptions } from '../types';
export function updateGeometry(timelineManager: TimelineManager, month: MonthGroup, options: UpdateGeometryOptions) { export function updateGeometry(timelineManager: TimelineManager, month: TimelineMonth, options: UpdateGeometryOptions) {
const { invalidateHeight, noDefer = false } = options; const { invalidateHeight, noDefer = false } = options;
if (invalidateHeight) { if (invalidateHeight) {
month.isHeightActual = false; month.isHeightActual = false;
@@ -17,10 +17,10 @@ export function updateGeometry(timelineManager: TimelineManager, month: MonthGro
} }
return; return;
} }
layoutMonthGroup(timelineManager, month, noDefer); layoutTimelineMonth(timelineManager, month, noDefer);
} }
export function layoutMonthGroup(timelineManager: TimelineManager, month: MonthGroup, noDefer: boolean = false) { export function layoutTimelineMonth(timelineManager: TimelineManager, month: TimelineMonth, noDefer: boolean = false) {
let cumulativeHeight = 0; let cumulativeHeight = 0;
let cumulativeWidth = 0; let cumulativeWidth = 0;
let currentRowHeight = 0; let currentRowHeight = 0;
@@ -1,21 +1,21 @@
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { toISOYearMonthUTC } from '$lib/utils/timeline-util'; import { toISOYearMonthUTC } from '$lib/utils/timeline-util';
import { getTimeBucket } from '@immich/sdk'; import { getTimeBucket } from '@immich/sdk';
import type { MonthGroup } from '../month-group.svelte';
import { TimelineManager } from '../timeline-manager.svelte'; import { TimelineManager } from '../timeline-manager.svelte';
import type { TimelineMonth } from '../timeline-month.svelte';
import type { TimelineManagerOptions } from '../types'; import type { TimelineManagerOptions } from '../types';
export async function loadFromTimeBuckets( export async function loadFromTimeBuckets(
timelineManager: TimelineManager, timelineManager: TimelineManager,
monthGroup: MonthGroup, timelineMonth: TimelineMonth,
options: TimelineManagerOptions, options: TimelineManagerOptions,
signal: AbortSignal, signal: AbortSignal,
): Promise<void> { ): Promise<void> {
if (monthGroup.getFirstAsset()) { if (timelineMonth.getFirstAsset()) {
return; return;
} }
const timeBucket = toISOYearMonthUTC(monthGroup.yearMonth); const timeBucket = toISOYearMonthUTC(timelineMonth.yearMonth);
const bucketResponse = await getTimeBucket( const bucketResponse = await getTimeBucket(
{ {
...authManager.params, ...authManager.params,
@@ -46,10 +46,10 @@ export async function loadFromTimeBuckets(
} }
} }
const unprocessedAssets = monthGroup.addAssets(bucketResponse, true); const unprocessedAssets = timelineMonth.addAssets(bucketResponse, true);
if (unprocessedAssets.length > 0) { if (unprocessedAssets.length > 0) {
console.error( console.error(
`Warning: getTimeBucket API returning assets not in requested month: ${monthGroup.yearMonth.month}, ${JSON.stringify( `Warning: getTimeBucket API returning assets not in requested month: ${timelineMonth.yearMonth.month}, ${JSON.stringify(
unprocessedAssets.map((unprocessed) => ({ unprocessedAssets.map((unprocessed) => ({
id: unprocessed.id, id: unprocessed.id,
localDateTime: unprocessed.localDateTime, localDateTime: unprocessed.localDateTime,
@@ -1,74 +1,86 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import type { MonthGroup } from '../month-group.svelte'; import type { TimelineMonth } from '../timeline-month.svelte';
import { findClosestGroupForDate } from './search-support.svelte'; import { findClosestTimelineMonthForDate } from './search-support.svelte';
function createMockMonthGroup(year: number, month: number): MonthGroup { function createMockTimelineMonth(year: number, month: number): TimelineMonth {
return { return {
yearMonth: { year, month }, yearMonth: { year, month },
} as MonthGroup; } as TimelineMonth;
} }
describe('findClosestGroupForDate', () => { describe('findClosestTimelineMonthForDate', () => {
it('should return undefined for empty months array', () => { it('should return undefined for empty months array', () => {
const result = findClosestGroupForDate([], { year: 2024, month: 1 }); const result = findClosestTimelineMonthForDate([], { year: 2024, month: 1 });
expect(result).toBeUndefined(); expect(result).toBeUndefined();
}); });
it('should return the only month when there is only one month', () => { it('should return the only month when there is only one month', () => {
const months = [createMockMonthGroup(2024, 6)]; const months = [createMockTimelineMonth(2024, 6)];
const result = findClosestGroupForDate(months, { year: 2025, month: 1 }); const result = findClosestTimelineMonthForDate(months, { year: 2025, month: 1 });
expect(result?.yearMonth).toEqual({ year: 2024, month: 6 }); expect(result?.yearMonth).toEqual({ year: 2024, month: 6 });
}); });
it('should return exact match when available', () => { it('should return exact match when available', () => {
const months = [createMockMonthGroup(2024, 1), createMockMonthGroup(2024, 6), createMockMonthGroup(2024, 12)]; const months = [
const result = findClosestGroupForDate(months, { year: 2024, month: 6 }); createMockTimelineMonth(2024, 1),
createMockTimelineMonth(2024, 6),
createMockTimelineMonth(2024, 12),
];
const result = findClosestTimelineMonthForDate(months, { year: 2024, month: 6 });
expect(result?.yearMonth).toEqual({ year: 2024, month: 6 }); expect(result?.yearMonth).toEqual({ year: 2024, month: 6 });
}); });
it('should find closest month when target is between two months', () => { it('should find closest month when target is between two months', () => {
const months = [createMockMonthGroup(2024, 1), createMockMonthGroup(2024, 6), createMockMonthGroup(2024, 12)]; const months = [
const result = findClosestGroupForDate(months, { year: 2024, month: 4 }); createMockTimelineMonth(2024, 1),
createMockTimelineMonth(2024, 6),
createMockTimelineMonth(2024, 12),
];
const result = findClosestTimelineMonthForDate(months, { year: 2024, month: 4 });
expect(result?.yearMonth).toEqual({ year: 2024, month: 6 }); expect(result?.yearMonth).toEqual({ year: 2024, month: 6 });
}); });
it('should handle year boundaries correctly (2023-12 vs 2024-01)', () => { it('should handle year boundaries correctly (2023-12 vs 2024-01)', () => {
const months = [createMockMonthGroup(2023, 12), createMockMonthGroup(2024, 2)]; const months = [createMockTimelineMonth(2023, 12), createMockTimelineMonth(2024, 2)];
const result = findClosestGroupForDate(months, { year: 2024, month: 1 }); const result = findClosestTimelineMonthForDate(months, { year: 2024, month: 1 });
// 2024-01 is 1 month from 2023-12 and 1 month from 2024-02 // 2024-01 is 1 month from 2023-12 and 1 month from 2024-02
// Should return first encountered with min distance (2023-12) // Should return first encountered with min distance (2023-12)
expect(result?.yearMonth).toEqual({ year: 2023, month: 12 }); expect(result?.yearMonth).toEqual({ year: 2023, month: 12 });
}); });
it('should correctly calculate distance across years', () => { it('should correctly calculate distance across years', () => {
const months = [createMockMonthGroup(2022, 6), createMockMonthGroup(2024, 6)]; const months = [createMockTimelineMonth(2022, 6), createMockTimelineMonth(2024, 6)];
const result = findClosestGroupForDate(months, { year: 2023, month: 6 }); const result = findClosestTimelineMonthForDate(months, { year: 2023, month: 6 });
// Both are exactly 12 months away, should return first encountered // Both are exactly 12 months away, should return first encountered
expect(result?.yearMonth).toEqual({ year: 2022, month: 6 }); expect(result?.yearMonth).toEqual({ year: 2022, month: 6 });
}); });
it('should handle target before all months', () => { it('should handle target before all months', () => {
const months = [createMockMonthGroup(2024, 6), createMockMonthGroup(2024, 12)]; const months = [createMockTimelineMonth(2024, 6), createMockTimelineMonth(2024, 12)];
const result = findClosestGroupForDate(months, { year: 2024, month: 1 }); const result = findClosestTimelineMonthForDate(months, { year: 2024, month: 1 });
expect(result?.yearMonth).toEqual({ year: 2024, month: 6 }); expect(result?.yearMonth).toEqual({ year: 2024, month: 6 });
}); });
it('should handle target after all months', () => { it('should handle target after all months', () => {
const months = [createMockMonthGroup(2024, 1), createMockMonthGroup(2024, 6)]; const months = [createMockTimelineMonth(2024, 1), createMockTimelineMonth(2024, 6)];
const result = findClosestGroupForDate(months, { year: 2025, month: 1 }); const result = findClosestTimelineMonthForDate(months, { year: 2025, month: 1 });
expect(result?.yearMonth).toEqual({ year: 2024, month: 6 }); expect(result?.yearMonth).toEqual({ year: 2024, month: 6 });
}); });
it('should handle multiple years correctly', () => { it('should handle multiple years correctly', () => {
const months = [createMockMonthGroup(2020, 1), createMockMonthGroup(2022, 1), createMockMonthGroup(2024, 1)]; const months = [
const result = findClosestGroupForDate(months, { year: 2023, month: 1 }); createMockTimelineMonth(2020, 1),
createMockTimelineMonth(2022, 1),
createMockTimelineMonth(2024, 1),
];
const result = findClosestTimelineMonthForDate(months, { year: 2023, month: 1 });
// 2023-01 is 12 months from 2022-01 and 12 months from 2024-01 // 2023-01 is 12 months from 2022-01 and 12 months from 2024-01
expect(result?.yearMonth).toEqual({ year: 2022, month: 1 }); expect(result?.yearMonth).toEqual({ year: 2022, month: 1 });
}); });
it('should prefer closer month when one is clearly closer', () => { it('should prefer closer month when one is clearly closer', () => {
const months = [createMockMonthGroup(2024, 1), createMockMonthGroup(2024, 10)]; const months = [createMockTimelineMonth(2024, 1), createMockTimelineMonth(2024, 10)];
const result = findClosestGroupForDate(months, { year: 2024, month: 11 }); const result = findClosestTimelineMonthForDate(months, { year: 2024, month: 11 });
// 2024-11 is 1 month from 2024-10 and 10 months from 2024-01 // 2024-11 is 1 month from 2024-10 and 10 months from 2024-01
expect(result?.yearMonth).toEqual({ year: 2024, month: 10 }); expect(result?.yearMonth).toEqual({ year: 2024, month: 10 });
}); });
@@ -1,8 +1,8 @@
import { plainDateTimeCompare, type TimelineYearMonth } from '$lib/utils/timeline-util'; import { plainDateTimeCompare, type TimelineYearMonth } from '$lib/utils/timeline-util';
import { AssetOrder, type AssetResponseDto } from '@immich/sdk'; import { AssetOrder, type AssetResponseDto } from '@immich/sdk';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import type { MonthGroup } from '../month-group.svelte';
import { TimelineManager } from '../timeline-manager.svelte'; import { TimelineManager } from '../timeline-manager.svelte';
import type { TimelineMonth } from '../timeline-month.svelte';
import type { AssetDescriptor, Direction, TimelineAsset } from '../types'; import type { AssetDescriptor, Direction, TimelineAsset } from '../types';
export async function getAssetWithOffset( export async function getAssetWithOffset(
@@ -11,44 +11,44 @@ export async function getAssetWithOffset(
interval: 'asset' | 'day' | 'month' | 'year' = 'asset', interval: 'asset' | 'day' | 'month' | 'year' = 'asset',
direction: Direction, direction: Direction,
): Promise<TimelineAsset | undefined> { ): Promise<TimelineAsset | undefined> {
const monthGroup = await timelineManager.findMonthGroupForAsset(assetDescriptor); const timelineMonth = await timelineManager.findTimelineMonthForAsset(assetDescriptor);
if (!monthGroup) { if (!timelineMonth) {
return; return;
} }
const asset = monthGroup.findAssetById(assetDescriptor); const asset = timelineMonth.findAssetById(assetDescriptor);
if (!asset) { if (!asset) {
return; return;
} }
switch (interval) { switch (interval) {
case 'asset': { case 'asset': {
return getAssetByAssetOffset(timelineManager, asset, monthGroup, direction); return getAssetByAssetOffset(timelineManager, asset, timelineMonth, direction);
} }
case 'day': { case 'day': {
return getAssetByDayOffset(timelineManager, asset, monthGroup, direction); return getAssetByDayOffset(timelineManager, asset, timelineMonth, direction);
} }
case 'month': { case 'month': {
return getAssetByMonthOffset(timelineManager, monthGroup, direction); return getAssetByMonthOffset(timelineManager, timelineMonth, direction);
} }
case 'year': { case 'year': {
return getAssetByYearOffset(timelineManager, monthGroup, direction); return getAssetByYearOffset(timelineManager, timelineMonth, direction);
} }
} }
} }
export function findMonthGroupForAsset(timelineManager: TimelineManager, id: string) { export function findTimelineMonthForAsset(timelineManager: TimelineManager, id: string) {
for (const month of timelineManager.months) { for (const month of timelineManager.months) {
const asset = month.findAssetById({ id }); const asset = month.findAssetById({ id });
if (asset) { if (asset) {
return { monthGroup: month, asset }; return { timelineMonth: month, asset };
} }
} }
} }
export function getMonthGroupByDate( export function getTimelineMonthByDate(
timelineManager: TimelineManager, timelineManager: TimelineManager,
targetYearMonth: TimelineYearMonth, targetYearMonth: TimelineYearMonth,
): MonthGroup | undefined { ): TimelineMonth | undefined {
return timelineManager.months.find( return timelineManager.months.find(
(month) => month.yearMonth.year === targetYearMonth.year && month.yearMonth.month === targetYearMonth.month, (month) => month.yearMonth.year === targetYearMonth.year && month.yearMonth.month === targetYearMonth.month,
); );
@@ -57,12 +57,12 @@ export function getMonthGroupByDate(
async function getAssetByAssetOffset( async function getAssetByAssetOffset(
timelineManager: TimelineManager, timelineManager: TimelineManager,
asset: TimelineAsset, asset: TimelineAsset,
monthGroup: MonthGroup, timelineMonth: TimelineMonth,
direction: Direction, direction: Direction,
) { ) {
const timelineDay = monthGroup.findTimelineDayForAsset(asset); const timelineDay = timelineMonth.findTimelineDayForAsset(asset);
for await (const targetAsset of timelineManager.assetsIterator({ for await (const targetAsset of timelineManager.assetsIterator({
startMonthGroup: monthGroup, startTimelineMonth: timelineMonth,
startTimelineDay: timelineDay, startTimelineDay: timelineDay,
startAsset: asset, startAsset: asset,
direction, direction,
@@ -76,12 +76,12 @@ async function getAssetByAssetOffset(
async function getAssetByDayOffset( async function getAssetByDayOffset(
timelineManager: TimelineManager, timelineManager: TimelineManager,
asset: TimelineAsset, asset: TimelineAsset,
monthGroup: MonthGroup, timelineMonth: TimelineMonth,
direction: Direction, direction: Direction,
) { ) {
const timelineDay = monthGroup.findTimelineDayForAsset(asset); const timelineDay = timelineMonth.findTimelineDayForAsset(asset);
for await (const targetAsset of timelineManager.assetsIterator({ for await (const targetAsset of timelineManager.assetsIterator({
startMonthGroup: monthGroup, startTimelineMonth: timelineMonth,
startTimelineDay: timelineDay, startTimelineDay: timelineDay,
startAsset: asset, startAsset: asset,
direction, direction,
@@ -92,44 +92,49 @@ async function getAssetByDayOffset(
} }
} }
async function getAssetByMonthOffset(timelineManager: TimelineManager, month: MonthGroup, direction: Direction) { async function getAssetByMonthOffset(timelineManager: TimelineManager, month: TimelineMonth, direction: Direction) {
for (const targetMonth of timelineManager.monthGroupIterator({ startMonthGroup: month, direction })) { for (const targetMonth of timelineManager.timelineMonthIterator({ startTimelineMonth: month, direction })) {
if (targetMonth.yearMonth.month !== month.yearMonth.month) { if (targetMonth.yearMonth.month !== month.yearMonth.month) {
const { value, done } = await timelineManager.assetsIterator({ startMonthGroup: targetMonth, direction }).next(); const { value, done } = await timelineManager
.assetsIterator({ startTimelineMonth: targetMonth, direction })
.next();
return done ? undefined : value; return done ? undefined : value;
} }
} }
} }
async function getAssetByYearOffset(timelineManager: TimelineManager, month: MonthGroup, direction: Direction) { async function getAssetByYearOffset(timelineManager: TimelineManager, month: TimelineMonth, direction: Direction) {
for (const targetMonth of timelineManager.monthGroupIterator({ startMonthGroup: month, direction })) { for (const targetMonth of timelineManager.timelineMonthIterator({ startTimelineMonth: month, direction })) {
if (targetMonth.yearMonth.year !== month.yearMonth.year) { if (targetMonth.yearMonth.year !== month.yearMonth.year) {
const { value, done } = await timelineManager.assetsIterator({ startMonthGroup: targetMonth, direction }).next(); const { value, done } = await timelineManager
.assetsIterator({ startTimelineMonth: targetMonth, direction })
.next();
return done ? undefined : value; return done ? undefined : value;
} }
} }
} }
export async function retrieveRange(timelineManager: TimelineManager, start: AssetDescriptor, end: AssetDescriptor) { export async function retrieveRange(timelineManager: TimelineManager, start: AssetDescriptor, end: AssetDescriptor) {
let { asset: startAsset, monthGroup: startMonthGroup } = findMonthGroupForAsset(timelineManager, start.id) ?? {}; let { asset: startAsset, timelineMonth: startTimelineMonth } =
if (!startMonthGroup || !startAsset) { findTimelineMonthForAsset(timelineManager, start.id) ?? {};
if (!startTimelineMonth || !startAsset) {
return []; return [];
} }
let { asset: endAsset, monthGroup: endMonthGroup } = findMonthGroupForAsset(timelineManager, end.id) ?? {}; let { asset: endAsset, timelineMonth: endTimelineMonth } = findTimelineMonthForAsset(timelineManager, end.id) ?? {};
if (!endMonthGroup || !endAsset) { if (!endTimelineMonth || !endAsset) {
return []; return [];
} }
const assetOrder: AssetOrder = timelineManager.getAssetOrder(); const assetOrder: AssetOrder = timelineManager.getAssetOrder();
if (plainDateTimeCompare(assetOrder === AssetOrder.Desc, startAsset.localDateTime, endAsset.localDateTime) < 0) { if (plainDateTimeCompare(assetOrder === AssetOrder.Desc, startAsset.localDateTime, endAsset.localDateTime) < 0) {
[startAsset, endAsset] = [endAsset, startAsset]; [startAsset, endAsset] = [endAsset, startAsset];
// eslint-disable-next-line no-useless-assignment // eslint-disable-next-line no-useless-assignment
[startMonthGroup, endMonthGroup] = [endMonthGroup, startMonthGroup]; [startTimelineMonth, endTimelineMonth] = [endTimelineMonth, startTimelineMonth];
} }
const range: TimelineAsset[] = []; const range: TimelineAsset[] = [];
const startTimelineDay = startMonthGroup.findTimelineDayForAsset(startAsset); const startTimelineDay = startTimelineMonth.findTimelineDayForAsset(startAsset);
for await (const targetAsset of timelineManager.assetsIterator({ for await (const targetAsset of timelineManager.assetsIterator({
startMonthGroup, startTimelineMonth,
startTimelineDay, startTimelineDay,
startAsset, startAsset,
})) { })) {
@@ -141,7 +146,7 @@ export async function retrieveRange(timelineManager: TimelineManager, start: Ass
return range; return range;
} }
export function findMonthGroupForDate(timelineManager: TimelineManager, targetYearMonth: TimelineYearMonth) { export function findTimelineMonthForDate(timelineManager: TimelineManager, targetYearMonth: TimelineYearMonth) {
for (const month of timelineManager.months) { for (const month of timelineManager.months) {
const { year, month: monthNum } = month.yearMonth; const { year, month: monthNum } = month.yearMonth;
if (monthNum === targetYearMonth.month && year === targetYearMonth.year) { if (monthNum === targetYearMonth.month && year === targetYearMonth.year) {
@@ -150,10 +155,10 @@ export function findMonthGroupForDate(timelineManager: TimelineManager, targetYe
} }
} }
export function findClosestGroupForDate(months: MonthGroup[], targetYearMonth: TimelineYearMonth) { export function findClosestTimelineMonthForDate(months: TimelineMonth[], targetYearMonth: TimelineYearMonth) {
const targetDate = DateTime.fromObject({ year: targetYearMonth.year, month: targetYearMonth.month }); const targetDate = DateTime.fromObject({ year: targetYearMonth.year, month: targetYearMonth.month });
let closestMonth: MonthGroup | undefined; let closestMonth: TimelineMonth | undefined;
let minDifference = Number.MAX_SAFE_INTEGER; let minDifference = Number.MAX_SAFE_INTEGER;
for (const month of months) { for (const month of months) {
@@ -5,12 +5,12 @@ import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils';
import { plainDateTimeCompare } from '$lib/utils/timeline-util'; import { plainDateTimeCompare } from '$lib/utils/timeline-util';
import { SvelteSet } from 'svelte/reactivity'; import { SvelteSet } from 'svelte/reactivity';
import type { MonthGroup } from './month-group.svelte'; import type { TimelineMonth } from './timeline-month.svelte';
import type { Direction, MoveAsset, TimelineAsset } from './types'; import type { Direction, MoveAsset, TimelineAsset } from './types';
import { ViewerAsset } from './viewer-asset.svelte'; import { ViewerAsset } from './viewer-asset.svelte';
export class TimelineDay { export class TimelineDay {
readonly monthGroup: MonthGroup; readonly timelineMonth: TimelineMonth;
readonly index: number; readonly index: number;
readonly groupTitle: string; readonly groupTitle: string;
readonly day: number; readonly day: number;
@@ -26,9 +26,9 @@ export class TimelineDay {
#col = $state(0); #col = $state(0);
#deferredLayout = false; #deferredLayout = false;
constructor(monthGroup: MonthGroup, index: number, day: number, groupTitle: string) { constructor(timelineMonth: TimelineMonth, index: number, day: number, groupTitle: string) {
this.index = index; this.index = index;
this.monthGroup = monthGroup; this.timelineMonth = timelineMonth;
this.day = day; this.day = day;
this.groupTitle = groupTitle; this.groupTitle = groupTitle;
} }
@@ -128,7 +128,7 @@ export class TimelineDay {
} }
unprocessedIds.delete(assetId); unprocessedIds.delete(assetId);
processedIds.add(assetId); processedIds.add(assetId);
if (remove || this.monthGroup.timelineManager.isExcluded(asset)) { if (remove || this.timelineMonth.timelineManager.isExcluded(asset)) {
this.viewerAssets.splice(index, 1); this.viewerAssets.splice(index, 1);
changedGeometry = true; changedGeometry = true;
} }
@@ -137,7 +137,7 @@ export class TimelineDay {
} }
layout(options: CommonLayoutOptions, noDefer: boolean) { layout(options: CommonLayoutOptions, noDefer: boolean) {
if (!noDefer && !this.monthGroup.isInOrNearViewport && !this.monthGroup.timelineManager.isScrollingOnLoad) { if (!noDefer && !this.timelineMonth.isInOrNearViewport && !this.timelineMonth.timelineManager.isScrollingOnLoad) {
this.#deferredLayout = true; this.#deferredLayout = true;
return; return;
} }
@@ -152,6 +152,6 @@ export class TimelineDay {
} }
get absoluteTimelineDayTop() { get absoluteTimelineDayTop() {
return this.monthGroup.top + this.#top; return this.timelineMonth.top + this.#top;
} }
} }
@@ -1,6 +1,6 @@
import { sdkMock } from '$lib/__mocks__/sdk.mock'; import { sdkMock } from '$lib/__mocks__/sdk.mock';
import { eventManager } from '$lib/managers/event-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte';
import { getMonthGroupByDate } from '$lib/managers/timeline-manager/internal/search-support.svelte'; import { getTimelineMonthByDate } from '$lib/managers/timeline-manager/internal/search-support.svelte';
import { AbortError } from '$lib/utils'; import { AbortError } from '$lib/utils';
import { fromISODateTimeUTCToObject } from '$lib/utils/timeline-util'; import { fromISODateTimeUTCToObject } from '$lib/utils/timeline-util';
import { AssetVisibility, type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk'; import { AssetVisibility, type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
@@ -95,7 +95,7 @@ describe('TimelineManager', () => {
}); });
}); });
describe('loadMonthGroup', () => { describe('loadTimelineMonth', () => {
let timelineManager: TimelineManager; let timelineManager: TimelineManager;
const bucketAssets: Record<string, TimelineAsset[]> = { const bucketAssets: Record<string, TimelineAsset[]> = {
'2024-01-03T00:00:00.000Z': timelineAssetFactory.buildList(1).map((asset) => '2024-01-03T00:00:00.000Z': timelineAssetFactory.buildList(1).map((asset) =>
@@ -131,47 +131,47 @@ describe('TimelineManager', () => {
}); });
it('loads a month', async () => { it('loads a month', async () => {
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(0); expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(0);
await timelineManager.loadMonthGroup({ year: 2024, month: 1 }); await timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
expect(sdkMock.getTimeBucket).toBeCalledTimes(1); expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(3); expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(3);
}); });
it('ignores invalid months', async () => { it('ignores invalid months', async () => {
await timelineManager.loadMonthGroup({ year: 2023, month: 1 }); await timelineManager.loadTimelineMonth({ year: 2023, month: 1 });
expect(sdkMock.getTimeBucket).toBeCalledTimes(0); expect(sdkMock.getTimeBucket).toBeCalledTimes(0);
}); });
it('cancels month loading', async () => { it('cancels month loading', async () => {
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })!; const month = getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })!;
void timelineManager.loadMonthGroup({ year: 2024, month: 1 }); void timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
const abortSpy = vi.spyOn(month!.loader!.cancelToken!, 'abort'); const abortSpy = vi.spyOn(month!.loader!.cancelToken!, 'abort');
month?.cancel(); month?.cancel();
expect(abortSpy).toBeCalledTimes(1); expect(abortSpy).toBeCalledTimes(1);
await timelineManager.loadMonthGroup({ year: 2024, month: 1 }); await timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(3); expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(3);
}); });
it('prevents loading months multiple times', async () => { it('prevents loading months multiple times', async () => {
await Promise.all([ await Promise.all([
timelineManager.loadMonthGroup({ year: 2024, month: 1 }), timelineManager.loadTimelineMonth({ year: 2024, month: 1 }),
timelineManager.loadMonthGroup({ year: 2024, month: 1 }), timelineManager.loadTimelineMonth({ year: 2024, month: 1 }),
]); ]);
expect(sdkMock.getTimeBucket).toBeCalledTimes(1); expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
await timelineManager.loadMonthGroup({ year: 2024, month: 1 }); await timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
expect(sdkMock.getTimeBucket).toBeCalledTimes(1); expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
}); });
it('allows loading a canceled month', async () => { it('allows loading a canceled month', async () => {
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })!; const month = getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })!;
const loadPromise = timelineManager.loadMonthGroup({ year: 2024, month: 1 }); const loadPromise = timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
month.cancel(); month.cancel();
await loadPromise; await loadPromise;
expect(month?.getAssets().length).toEqual(0); expect(month?.getAssets().length).toEqual(0);
await timelineManager.loadMonthGroup({ year: 2024, month: 1 }); await timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
expect(month!.getAssets().length).toEqual(3); expect(month!.getAssets().length).toEqual(3);
}); });
}); });
@@ -241,7 +241,7 @@ describe('TimelineManager', () => {
); );
timelineManager.upsertAssets([assetOne, assetTwo, assetThree]); timelineManager.upsertAssets([assetOne, assetTwo, assetThree]);
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 }); const month = getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 });
expect(month).not.toBeNull(); expect(month).not.toBeNull();
expect(month?.getAssets().length).toEqual(3); expect(month?.getAssets().length).toEqual(3);
expect(month?.getAssets()[0].id).toEqual(assetOne.id); expect(month?.getAssets()[0].id).toEqual(assetOne.id);
@@ -346,15 +346,15 @@ describe('TimelineManager', () => {
timelineManager.upsertAssets([asset]); timelineManager.upsertAssets([asset]);
expect(timelineManager.months.length).toEqual(1); expect(timelineManager.months.length).toEqual(1);
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })).not.toBeUndefined(); expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })).not.toBeUndefined();
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(1); expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(1);
timelineManager.upsertAssets([updatedAsset]); timelineManager.upsertAssets([updatedAsset]);
expect(timelineManager.months.length).toEqual(2); expect(timelineManager.months.length).toEqual(2);
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })).not.toBeUndefined(); expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })).not.toBeUndefined();
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(0); expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(0);
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 3 })).not.toBeUndefined(); expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 3 })).not.toBeUndefined();
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 3 })?.getAssets().length).toEqual(1); expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 3 })?.getAssets().length).toEqual(1);
}); });
it('yearMonth is not a shared reference with asset.localDateTime (reference bug)', () => { it('yearMonth is not a shared reference with asset.localDateTime (reference bug)', () => {
@@ -365,7 +365,7 @@ describe('TimelineManager', () => {
); );
timelineManager.upsertAssets([asset]); timelineManager.upsertAssets([asset]);
const januaryMonth = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })!; const januaryMonth = getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })!;
const monthYearMonth = januaryMonth.yearMonth; const monthYearMonth = januaryMonth.yearMonth;
const originalMonth = monthYearMonth.month; const originalMonth = monthYearMonth.month;
@@ -611,8 +611,8 @@ describe('TimelineManager', () => {
}); });
it('returns previous assetId', async () => { it('returns previous assetId', async () => {
await timelineManager.loadMonthGroup({ year: 2024, month: 1 }); await timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 }); const month = getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 });
const a = month!.getAssets()[0]; const a = month!.getAssets()[0];
const b = month!.getAssets()[1]; const b = month!.getAssets()[1];
@@ -621,11 +621,11 @@ describe('TimelineManager', () => {
}); });
it('returns previous assetId spanning multiple months', async () => { it('returns previous assetId spanning multiple months', async () => {
await timelineManager.loadMonthGroup({ year: 2024, month: 2 }); await timelineManager.loadTimelineMonth({ year: 2024, month: 2 });
await timelineManager.loadMonthGroup({ year: 2024, month: 3 }); await timelineManager.loadTimelineMonth({ year: 2024, month: 3 });
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 2 }); const month = getTimelineMonthByDate(timelineManager, { year: 2024, month: 2 });
const previousMonth = getMonthGroupByDate(timelineManager, { year: 2024, month: 3 }); const previousMonth = getTimelineMonthByDate(timelineManager, { year: 2024, month: 3 });
const a = month!.getAssets()[0]; const a = month!.getAssets()[0];
const b = previousMonth!.getAssets()[0]; const b = previousMonth!.getAssets()[0];
const previous = await timelineManager.getLaterAsset(a); const previous = await timelineManager.getLaterAsset(a);
@@ -633,23 +633,23 @@ describe('TimelineManager', () => {
}); });
it('loads previous month', async () => { it('loads previous month', async () => {
await timelineManager.loadMonthGroup({ year: 2024, month: 2 }); await timelineManager.loadTimelineMonth({ year: 2024, month: 2 });
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 2 }); const month = getTimelineMonthByDate(timelineManager, { year: 2024, month: 2 });
const previousMonth = getMonthGroupByDate(timelineManager, { year: 2024, month: 3 }); const previousMonth = getTimelineMonthByDate(timelineManager, { year: 2024, month: 3 });
const a = month!.getFirstAsset(); const a = month!.getFirstAsset();
const b = previousMonth!.getFirstAsset(); const b = previousMonth!.getFirstAsset();
const loadMonthGroupSpy = vi.spyOn(month!.loader!, 'execute'); const loadTimelineMonthSpy = vi.spyOn(month!.loader!, 'execute');
const previousMonthSpy = vi.spyOn(previousMonth!.loader!, 'execute'); const previousMonthSpy = vi.spyOn(previousMonth!.loader!, 'execute');
const previous = await timelineManager.getLaterAsset(a); const previous = await timelineManager.getLaterAsset(a);
expect(previous).toEqual(b); expect(previous).toEqual(b);
expect(loadMonthGroupSpy).toBeCalledTimes(0); expect(loadTimelineMonthSpy).toBeCalledTimes(0);
expect(previousMonthSpy).toBeCalledTimes(0); expect(previousMonthSpy).toBeCalledTimes(0);
}); });
it('skips removed assets', async () => { it('skips removed assets', async () => {
await timelineManager.loadMonthGroup({ year: 2024, month: 1 }); await timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
await timelineManager.loadMonthGroup({ year: 2024, month: 2 }); await timelineManager.loadTimelineMonth({ year: 2024, month: 2 });
await timelineManager.loadMonthGroup({ year: 2024, month: 3 }); await timelineManager.loadTimelineMonth({ year: 2024, month: 3 });
const [assetOne, assetTwo, assetThree] = await getAssets(timelineManager); const [assetOne, assetTwo, assetThree] = await getAssets(timelineManager);
timelineManager.removeAssets([assetTwo.id]); timelineManager.removeAssets([assetTwo.id]);
@@ -657,12 +657,12 @@ describe('TimelineManager', () => {
}); });
it('returns null when no more assets', async () => { it('returns null when no more assets', async () => {
await timelineManager.loadMonthGroup({ year: 2024, month: 3 }); await timelineManager.loadTimelineMonth({ year: 2024, month: 3 });
expect(await timelineManager.getLaterAsset(timelineManager.months[0].getFirstAsset())).toBeUndefined(); expect(await timelineManager.getLaterAsset(timelineManager.months[0].getFirstAsset())).toBeUndefined();
}); });
}); });
describe('getMonthGroupIndexByAssetId', () => { describe('getTimelineMonthIndexByAssetId', () => {
let timelineManager: TimelineManager; let timelineManager: TimelineManager;
beforeEach(async () => { beforeEach(async () => {
@@ -673,8 +673,8 @@ describe('TimelineManager', () => {
}); });
it('returns null for invalid months', () => { it('returns null for invalid months', () => {
expect(getMonthGroupByDate(timelineManager, { year: -1, month: -1 })).toBeUndefined(); expect(getTimelineMonthByDate(timelineManager, { year: -1, month: -1 })).toBeUndefined();
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 3 })).toBeUndefined(); expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 3 })).toBeUndefined();
}); });
it('returns the month index', () => { it('returns the month index', () => {
@@ -690,10 +690,10 @@ describe('TimelineManager', () => {
); );
timelineManager.upsertAssets([assetOne, assetTwo]); timelineManager.upsertAssets([assetOne, assetTwo]);
expect(timelineManager.getMonthGroupByAssetId(assetTwo.id)?.yearMonth.year).toEqual(2024); expect(timelineManager.getTimelineMonthByAssetId(assetTwo.id)?.yearMonth.year).toEqual(2024);
expect(timelineManager.getMonthGroupByAssetId(assetTwo.id)?.yearMonth.month).toEqual(2); expect(timelineManager.getTimelineMonthByAssetId(assetTwo.id)?.yearMonth.month).toEqual(2);
expect(timelineManager.getMonthGroupByAssetId(assetOne.id)?.yearMonth.year).toEqual(2024); expect(timelineManager.getTimelineMonthByAssetId(assetOne.id)?.yearMonth.year).toEqual(2024);
expect(timelineManager.getMonthGroupByAssetId(assetOne.id)?.yearMonth.month).toEqual(1); expect(timelineManager.getTimelineMonthByAssetId(assetOne.id)?.yearMonth.month).toEqual(1);
}); });
it('ignores removed months', () => { it('ignores removed months', () => {
@@ -710,8 +710,8 @@ describe('TimelineManager', () => {
timelineManager.upsertAssets([assetOne, assetTwo]); timelineManager.upsertAssets([assetOne, assetTwo]);
timelineManager.removeAssets([assetTwo.id]); timelineManager.removeAssets([assetTwo.id]);
expect(timelineManager.getMonthGroupByAssetId(assetOne.id)?.yearMonth.year).toEqual(2024); expect(timelineManager.getTimelineMonthByAssetId(assetOne.id)?.yearMonth.year).toEqual(2024);
expect(timelineManager.getMonthGroupByAssetId(assetOne.id)?.yearMonth.month).toEqual(1); expect(timelineManager.getTimelineMonthByAssetId(assetOne.id)?.yearMonth.month).toEqual(1);
}); });
}); });
@@ -2,15 +2,15 @@ import { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/Virtual
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte';
import { GroupInsertionCache } from '$lib/managers/timeline-manager/group-insertion-cache.svelte'; import { GroupInsertionCache } from '$lib/managers/timeline-manager/group-insertion-cache.svelte';
import { updateMonthGroupViewportProximity } from '$lib/managers/timeline-manager/internal/intersection-support.svelte'; import { updateTimelineMonthViewportProximity } from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
import { updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte'; import { updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte';
import { loadFromTimeBuckets } from '$lib/managers/timeline-manager/internal/load-support.svelte'; import { loadFromTimeBuckets } from '$lib/managers/timeline-manager/internal/load-support.svelte';
import { import {
findClosestGroupForDate, findClosestTimelineMonthForDate,
findMonthGroupForAsset as findMonthGroupForAssetUtil, findTimelineMonthForAsset as findTimelineMonthForAssetUtil,
findMonthGroupForDate, findTimelineMonthForDate,
getAssetWithOffset, getAssetWithOffset,
getMonthGroupByDate, getTimelineMonthByDate,
retrieveRange as retrieveRangeUtil, retrieveRange as retrieveRangeUtil,
} from '$lib/managers/timeline-manager/internal/search-support.svelte'; } from '$lib/managers/timeline-manager/internal/search-support.svelte';
import { WebsocketSupport } from '$lib/managers/timeline-manager/internal/websocket-support.svelte'; import { WebsocketSupport } from '$lib/managers/timeline-manager/internal/websocket-support.svelte';
@@ -27,8 +27,8 @@ import { AssetOrder, getAssetInfo, getTimeBuckets, type AssetResponseDto } from
import { clamp, isEqual } from 'lodash-es'; import { clamp, isEqual } from 'lodash-es';
import { SvelteDate, SvelteSet } from 'svelte/reactivity'; import { SvelteDate, SvelteSet } from 'svelte/reactivity';
import { isMismatched, updateObject } from './internal/utils.svelte'; import { isMismatched, updateObject } from './internal/utils.svelte';
import { MonthGroup } from './month-group.svelte';
import { TimelineDay } from './timeline-day.svelte'; import { TimelineDay } from './timeline-day.svelte';
import { TimelineMonth } from './timeline-month.svelte';
import type { import type {
AssetDescriptor, AssetDescriptor,
Direction, Direction,
@@ -40,7 +40,7 @@ import type {
} from './types'; } from './types';
type ViewportTopMonthIntersection = { type ViewportTopMonthIntersection = {
month: MonthGroup | undefined; month: TimelineMonth | undefined;
// Where viewport top intersects month (0 = month top, 1 = month bottom) // Where viewport top intersects month (0 = month top, 1 = month bottom)
viewportTopRatioInMonth: number; viewportTopRatioInMonth: number;
// Where month bottom is in viewport (0 = viewport top, 1 = viewport bottom) // Where month bottom is in viewport (0 = viewport top, 1 = viewport bottom)
@@ -67,7 +67,7 @@ export class TimelineManager extends VirtualScrollManager {
isInitialized = $state(false); isInitialized = $state(false);
isScrollingOnLoad = false; isScrollingOnLoad = false;
months: MonthGroup[] = $state([]); months: TimelineMonth[] = $state([]);
albumAssets: Set<string> = new SvelteSet(); albumAssets: Set<string> = new SvelteSet();
scrubberMonths: ScrubberMonth[] = $state([]); scrubberMonths: ScrubberMonth[] = $state([]);
scrubberTimelineHeight: number = $state(0); scrubberTimelineHeight: number = $state(0);
@@ -137,24 +137,27 @@ export class TimelineManager extends VirtualScrollManager {
} }
async *assetsIterator(options?: { async *assetsIterator(options?: {
startMonthGroup?: MonthGroup; startTimelineMonth?: TimelineMonth;
startTimelineDay?: TimelineDay; startTimelineDay?: TimelineDay;
startAsset?: TimelineAsset; startAsset?: TimelineAsset;
direction?: Direction; direction?: Direction;
}) { }) {
const direction = options?.direction ?? 'earlier'; const direction = options?.direction ?? 'earlier';
let { startTimelineDay, startAsset } = options ?? {}; let { startTimelineDay, startAsset } = options ?? {};
for (const monthGroup of this.monthGroupIterator({ direction, startMonthGroup: options?.startMonthGroup })) { for (const timelineMonth of this.timelineMonthIterator({
await this.loadMonthGroup(monthGroup.yearMonth, { cancelable: false }); direction,
yield* monthGroup.assetsIterator({ startTimelineDay, startAsset, direction }); startTimelineMonth: options?.startTimelineMonth,
})) {
await this.loadTimelineMonth(timelineMonth.yearMonth, { cancelable: false });
yield* timelineMonth.assetsIterator({ startTimelineDay, startAsset, direction });
startTimelineDay = startAsset = undefined; startTimelineDay = startAsset = undefined;
} }
} }
*monthGroupIterator(options?: { direction?: Direction; startMonthGroup?: MonthGroup }) { *timelineMonthIterator(options?: { direction?: Direction; startTimelineMonth?: TimelineMonth }) {
const isEarlier = options?.direction === 'earlier'; const isEarlier = options?.direction === 'earlier';
let startIndex = options?.startMonthGroup let startIndex = options?.startTimelineMonth
? this.months.indexOf(options.startMonthGroup) ? this.months.indexOf(options.startTimelineMonth)
: isEarlier : isEarlier
? 0 ? 0
: this.months.length - 1; : this.months.length - 1;
@@ -181,7 +184,7 @@ export class TimelineManager extends VirtualScrollManager {
this.#websocketSupport = undefined; this.#websocketSupport = undefined;
} }
#calculateMonthBottomViewportRatio(month: MonthGroup | undefined) { #calculateMonthBottomViewportRatio(month: TimelineMonth | undefined) {
if (!month) { if (!month) {
return 0; return 0;
} }
@@ -191,7 +194,7 @@ export class TimelineManager extends VirtualScrollManager {
return clamp(bottomOfMonthInViewport / windowHeight, 0, 1); return clamp(bottomOfMonthInViewport / windowHeight, 0, 1);
} }
#calculateVewportTopRatioInMonth(month: MonthGroup | undefined) { #calculateVewportTopRatioInMonth(month: TimelineMonth | undefined) {
if (!month) { if (!month) {
return 0; return 0;
} }
@@ -209,7 +212,7 @@ export class TimelineManager extends VirtualScrollManager {
this.#updatingViewportProximities = true; this.#updatingViewportProximities = true;
for (const month of this.months) { for (const month of this.months) {
updateMonthGroupViewportProximity(this, month); updateTimelineMonthViewportProximity(this, month);
} }
const month = this.months.find((month) => month.isInViewport); const month = this.months.find((month) => month.isInViewport);
@@ -225,7 +228,7 @@ export class TimelineManager extends VirtualScrollManager {
this.#updatingViewportProximities = false; this.#updatingViewportProximities = false;
} }
clearDeferredLayout(month: MonthGroup) { clearDeferredLayout(month: TimelineMonth) {
const hasDeferred = month.timelineDays.some((group) => group.deferredLayout); const hasDeferred = month.timelineDays.some((group) => group.deferredLayout);
if (hasDeferred) { if (hasDeferred) {
updateGeometry(this, month, { invalidateHeight: true, noDefer: true }); updateGeometry(this, month, { invalidateHeight: true, noDefer: true });
@@ -235,7 +238,7 @@ export class TimelineManager extends VirtualScrollManager {
} }
} }
async #initializeMonthGroups() { async #initializeTimelineMonths() {
const timebuckets = await getTimeBuckets({ const timebuckets = await getTimeBuckets({
...authManager.params, ...authManager.params,
...this.#options, ...this.#options,
@@ -243,7 +246,7 @@ export class TimelineManager extends VirtualScrollManager {
this.months = timebuckets.map((timeBucket) => { this.months = timebuckets.map((timeBucket) => {
const date = new SvelteDate(timeBucket.timeBucket); const date = new SvelteDate(timeBucket.timeBucket);
return new MonthGroup( return new TimelineMonth(
this, this,
{ year: date.getUTCFullYear(), month: date.getUTCMonth() + 1 }, { year: date.getUTCFullYear(), month: date.getUTCMonth() + 1 },
timeBucket.count, timeBucket.count,
@@ -280,7 +283,7 @@ export class TimelineManager extends VirtualScrollManager {
this.albumAssets.clear(); this.albumAssets.clear();
await this.initTask.execute(async () => { await this.initTask.execute(async () => {
this.#options = options; this.#options = options;
await this.#initializeMonthGroups(); await this.#initializeTimelineMonths();
}, true); }, true);
} }
@@ -332,31 +335,31 @@ export class TimelineManager extends VirtualScrollManager {
assetCount: month.assetsCount, assetCount: month.assetsCount,
year: month.yearMonth.year, year: month.yearMonth.year,
month: month.yearMonth.month, month: month.yearMonth.month,
title: month.monthGroupTitle, title: month.title,
height: month.height, height: month.height,
})); }));
this.scrubberTimelineHeight = this.totalViewerHeight; this.scrubberTimelineHeight = this.totalViewerHeight;
} }
async loadMonthGroup(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }): Promise<void> { async loadTimelineMonth(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }): Promise<void> {
let cancelable = true; let cancelable = true;
if (options) { if (options) {
cancelable = options.cancelable; cancelable = options.cancelable;
} }
const monthGroup = getMonthGroupByDate(this, yearMonth); const timelineMonth = getTimelineMonthByDate(this, yearMonth);
if (!monthGroup) { if (!timelineMonth) {
return; return;
} }
if (monthGroup.loader?.executed) { if (timelineMonth.loader?.executed) {
return; return;
} }
const executionStatus = await monthGroup.loader?.execute(async (signal: AbortSignal) => { const executionStatus = await timelineMonth.loader?.execute(async (signal: AbortSignal) => {
await loadFromTimeBuckets(this, monthGroup, this.#options, signal); await loadFromTimeBuckets(this, timelineMonth, this.#options, signal);
}, cancelable); }, cancelable);
if (executionStatus === 'LOADED') { if (executionStatus === 'LOADED') {
updateGeometry(this, monthGroup, { invalidateHeight: false }); updateGeometry(this, timelineMonth, { invalidateHeight: false });
this.updateViewportProximities(); this.updateViewportProximities();
} }
} }
@@ -367,15 +370,15 @@ export class TimelineManager extends VirtualScrollManager {
this.addAssetsUpsertSegments([...notExcluded]); this.addAssetsUpsertSegments([...notExcluded]);
} }
async findMonthGroupForAsset(asset: AssetDescriptor | AssetResponseDto) { async findTimelineMonthForAsset(asset: AssetDescriptor | AssetResponseDto) {
if (!this.isInitialized) { if (!this.isInitialized) {
await this.initTask.waitUntilExecution(); await this.initTask.waitUntilExecution();
} }
const { id } = asset; const { id } = asset;
let { monthGroup } = findMonthGroupForAssetUtil(this, id) ?? {}; let { timelineMonth } = findTimelineMonthForAssetUtil(this, id) ?? {};
if (monthGroup) { if (timelineMonth) {
return monthGroup; return timelineMonth;
} }
const response = isAssetResponseDto(asset) const response = isAssetResponseDto(asset)
@@ -390,20 +393,20 @@ export class TimelineManager extends VirtualScrollManager {
return; return;
} }
monthGroup = await this.#loadMonthGroupAtTime(timelineAsset.localDateTime, { cancelable: false }); timelineMonth = await this.#loadTimelineMonthAtTime(timelineAsset.localDateTime, { cancelable: false });
if (monthGroup?.findAssetById({ id })) { if (timelineMonth?.findAssetById({ id })) {
return monthGroup; return timelineMonth;
} }
} }
async #loadMonthGroupAtTime(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }) { async #loadTimelineMonthAtTime(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }) {
await this.loadMonthGroup(yearMonth, options); await this.loadTimelineMonth(yearMonth, options);
return getMonthGroupByDate(this, yearMonth); return getTimelineMonthByDate(this, yearMonth);
} }
getMonthGroupByAssetId(assetId: string) { getTimelineMonthByAssetId(assetId: string) {
const monthGroupInfo = findMonthGroupForAssetUtil(this, assetId); const timelineMonthInfo = findTimelineMonthForAssetUtil(this, assetId);
return monthGroupInfo?.monthGroup; return timelineMonthInfo?.timelineMonth;
} }
// note: the `index` input is expected to be in the range [0, assetCount). This // note: the `index` input is expected to be in the range [0, assetCount). This
@@ -414,7 +417,7 @@ export class TimelineManager extends VirtualScrollManager {
let accumulatedCount = 0; let accumulatedCount = 0;
let randomMonth: MonthGroup | undefined = undefined; let randomMonth: TimelineMonth | undefined = undefined;
for (const month of this.months) { for (const month of this.months) {
if (randomAssetIndex < accumulatedCount + month.assetsCount) { if (randomAssetIndex < accumulatedCount + month.assetsCount) {
randomMonth = month; randomMonth = month;
@@ -426,7 +429,7 @@ export class TimelineManager extends VirtualScrollManager {
if (!randomMonth) { if (!randomMonth) {
return; return;
} }
await this.loadMonthGroup(randomMonth.yearMonth, { cancelable: false }); await this.loadTimelineMonth(randomMonth.yearMonth, { cancelable: false });
let randomDay: TimelineDay | undefined = undefined; let randomDay: TimelineDay | undefined = undefined;
for (const day of randomMonth.timelineDays) { for (const day of randomMonth.timelineDays) {
@@ -459,10 +462,10 @@ export class TimelineManager extends VirtualScrollManager {
} }
protected upsertSegmentForAsset(asset: TimelineAsset) { protected upsertSegmentForAsset(asset: TimelineAsset) {
let month = getMonthGroupByDate(this, asset.localDateTime); let month = getTimelineMonthByDate(this, asset.localDateTime);
if (!month) { if (!month) {
month = new MonthGroup(this, asset.localDateTime, 1, true, this.#options.order); month = new TimelineMonth(this, asset.localDateTime, 1, true, this.#options.order);
this.months.push(month); this.months.push(month);
} }
return month; return month;
@@ -508,7 +511,7 @@ export class TimelineManager extends VirtualScrollManager {
return { updated: new Set<string>(), notUpdated: ids, changedGeometry: false }; return { updated: new Set<string>(), notUpdated: ids, changedGeometry: false };
} }
// eslint-disable-next-line svelte/prefer-svelte-reactivity // eslint-disable-next-line svelte/prefer-svelte-reactivity
const changedMonthGroups = new Set<MonthGroup>(); const changedTimelineMonths = new Set<TimelineMonth>();
// eslint-disable-next-line svelte/prefer-svelte-reactivity // eslint-disable-next-line svelte/prefer-svelte-reactivity
let notUpdated = new Set(ids); let notUpdated = new Set(ids);
// eslint-disable-next-line svelte/prefer-svelte-reactivity // eslint-disable-next-line svelte/prefer-svelte-reactivity
@@ -523,7 +526,7 @@ export class TimelineManager extends VirtualScrollManager {
assetsToMoveSegments.push(result.moveAssets); assetsToMoveSegments.push(result.moveAssets);
} }
if (result.changedGeometry) { if (result.changedGeometry) {
changedMonthGroups.add(month); changedTimelineMonths.add(month);
} }
notUpdated = setDifference(notUpdated, result.processedIds); notUpdated = setDifference(notUpdated, result.processedIds);
for (const id of result.processedIds) { for (const id of result.processedIds) {
@@ -537,8 +540,8 @@ export class TimelineManager extends VirtualScrollManager {
} }
} }
this.addAssetsUpsertSegments(assetsToAdd); this.addAssetsUpsertSegments(assetsToAdd);
const changedGeometry = changedMonthGroups.size > 0; const changedGeometry = changedTimelineMonths.size > 0;
for (const month of changedMonthGroups) { for (const month of changedTimelineMonths) {
updateGeometry(this, month, { invalidateHeight: true }); updateGeometry(this, month, { invalidateHeight: true });
} }
if (changedGeometry) { if (changedGeometry) {
@@ -573,20 +576,20 @@ export class TimelineManager extends VirtualScrollManager {
} }
async getClosestAssetToDate(dateTime: TimelineDateTime) { async getClosestAssetToDate(dateTime: TimelineDateTime) {
let monthGroup = findMonthGroupForDate(this, dateTime); let timelineMonth = findTimelineMonthForDate(this, dateTime);
if (!monthGroup) { if (!timelineMonth) {
// if exact match not found, find closest // if exact match not found, find closest
monthGroup = findClosestGroupForDate(this.months, dateTime); timelineMonth = findClosestTimelineMonthForDate(this.months, dateTime);
if (!monthGroup) { if (!timelineMonth) {
return; return;
} }
} }
await this.loadMonthGroup(dateTime, { cancelable: false }); await this.loadTimelineMonth(dateTime, { cancelable: false });
const asset = monthGroup.findClosest(dateTime); const asset = timelineMonth.findClosest(dateTime);
if (asset) { if (asset) {
return asset; return asset;
} }
for await (const asset of this.assetsIterator({ startMonthGroup: monthGroup })) { for await (const asset of this.assetsIterator({ startTimelineMonth: timelineMonth })) {
return asset; return asset;
} }
} }
@@ -622,8 +625,8 @@ export class TimelineManager extends VirtualScrollManager {
group.sortAssets(this.#options.order); group.sortAssets(this.#options.order);
} }
for (const monthGroup of context.bucketsWithNewTimelineDays) { for (const timelineMonth of context.bucketsWithNewTimelineDays) {
monthGroup.sortTimelineDays(); timelineMonth.sortTimelineDays();
} }
for (const month of context.updatedBuckets) { for (const month of context.updatedBuckets) {
@@ -4,7 +4,7 @@ import { CancellableTask } from '$lib/utils/cancellable-task';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { import {
formatGroupTitle, formatGroupTitle,
formatMonthGroupTitle, formatTimelineMonthTitle,
fromTimelinePlainDate, fromTimelinePlainDate,
fromTimelinePlainDateTime, fromTimelinePlainDateTime,
fromTimelinePlainYearMonth, fromTimelinePlainYearMonth,
@@ -29,7 +29,7 @@ import type { TimelineManager } from './timeline-manager.svelte';
import type { AssetDescriptor, Direction, MoveAsset, TimelineAsset } from './types'; import type { AssetDescriptor, Direction, MoveAsset, TimelineAsset } from './types';
import { ViewerAsset } from './viewer-asset.svelte'; import { ViewerAsset } from './viewer-asset.svelte';
export class MonthGroup { export class TimelineMonth {
#viewportProximity: ViewportProximity = $state(ViewportProximity.FarFromViewport); #viewportProximity: ViewportProximity = $state(ViewportProximity.FarFromViewport);
isLoaded: boolean = $state(false); isLoaded: boolean = $state(false);
timelineDays: TimelineDay[] = $state([]); timelineDays: TimelineDay[] = $state([]);
@@ -50,7 +50,7 @@ export class MonthGroup {
loader: CancellableTask | undefined; loader: CancellableTask | undefined;
isHeightActual: boolean = $state(false); isHeightActual: boolean = $state(false);
readonly monthGroupTitle: string; readonly title: string;
readonly yearMonth: TimelineYearMonth; readonly yearMonth: TimelineYearMonth;
constructor( constructor(
@@ -65,7 +65,7 @@ export class MonthGroup {
this.#sortOrder = order; this.#sortOrder = order;
this.yearMonth = { year: yearMonth.year, month: yearMonth.month }; this.yearMonth = { year: yearMonth.year, month: yearMonth.month };
this.monthGroupTitle = formatMonthGroupTitle(fromTimelinePlainYearMonth(yearMonth)); this.title = formatTimelineMonthTitle(fromTimelinePlainYearMonth(yearMonth));
this.loader = new CancellableTask( this.loader = new CancellableTask(
() => { () => {
@@ -89,7 +89,7 @@ export class MonthGroup {
} }
this.#viewportProximity = newValue; this.#viewportProximity = newValue;
if (isInOrNearViewportUtil(newValue)) { if (isInOrNearViewportUtil(newValue)) {
void this.timelineManager.loadMonthGroup(this.yearMonth); void this.timelineManager.loadTimelineMonth(this.yearMonth);
} else { } else {
this.cancel(); this.cancel();
} }
@@ -269,9 +269,9 @@ export class MonthGroup {
const index = timelineManager.months.indexOf(this); const index = timelineManager.months.indexOf(this);
const heightDelta = height - this.#height; const heightDelta = height - this.#height;
this.#height = height; this.#height = height;
const prevMonthGroup = timelineManager.months[index - 1]; const previousTimelineMonth = timelineManager.months[index - 1];
if (prevMonthGroup) { if (previousTimelineMonth) {
const newTop = prevMonthGroup.#top + prevMonthGroup.#height; const newTop = previousTimelineMonth.#top + previousTimelineMonth.#height;
if (this.#top !== newTop) { if (this.#top !== newTop) {
this.#top = newTop; this.#top = newTop;
} }
@@ -280,10 +280,10 @@ export class MonthGroup {
return; return;
} }
for (let cursor = index + 1; cursor < timelineManager.months.length; cursor++) { for (let cursor = index + 1; cursor < timelineManager.months.length; cursor++) {
const monthGroup = this.timelineManager.months[cursor]; const timelineMonth = this.timelineManager.months[cursor];
const newTop = monthGroup.#top + heightDelta; const newTop = timelineMonth.#top + heightDelta;
if (monthGroup.#top !== newTop) { if (timelineMonth.#top !== newTop) {
monthGroup.#top = newTop; timelineMonth.#top = newTop;
} }
} }
if (!timelineManager.viewportTopMonthIntersection) { if (!timelineManager.viewportTopMonthIntersection) {
@@ -16,7 +16,7 @@ export class ViewerAsset {
return ViewportProximity.FarFromViewport; return ViewportProximity.FarFromViewport;
} }
const store = this.#group.monthGroup.timelineManager; const store = this.#group.timelineMonth.timelineManager;
const positionTop = this.#group.absoluteTimelineDayTop + this.position.top; const positionTop = this.#group.absoluteTimelineDayTop + this.position.top;
return calculateViewerAssetViewportProximity(store, positionTop, this.position.height); return calculateViewerAssetViewportProximity(store, positionTop, this.position.height);
+5 -5
View File
@@ -396,18 +396,18 @@ export const selectAllAssets = async (timelineManager: TimelineManager, assetInt
assetInteraction.selectAll = true; assetInteraction.selectAll = true;
try { try {
for (const monthGroup of timelineManager.months) { for (const timelineMonth of timelineManager.months) {
if (!monthGroup.isLoaded) { if (!timelineMonth.isLoaded) {
await timelineManager.loadMonthGroup(monthGroup.yearMonth); await timelineManager.loadTimelineMonth(timelineMonth.yearMonth);
} }
if (!assetInteraction.selectAll) { if (!assetInteraction.selectAll) {
assetInteraction.clear(); assetInteraction.clear();
break; // Cancelled break; // Cancelled
} }
assetInteraction.selectAssets([...monthGroup.assetsIterator()]); assetInteraction.selectAssets([...timelineMonth.assetsIterator()]);
for (const dateGroup of monthGroup.timelineDays) { for (const dateGroup of timelineMonth.timelineDays) {
assetInteraction.addGroupToMultiselectGroup(dateGroup.groupTitle); assetInteraction.addGroupToMultiselectGroup(dateGroup.groupTitle);
} }
} }
+1 -1
View File
@@ -100,7 +100,7 @@ export const toISOYearMonthUTC = ({ year, month }: TimelineYearMonth): string =>
return `${yearFull}-${monthFull}-01T00:00:00.000Z`; return `${yearFull}-${monthFull}-01T00:00:00.000Z`;
}; };
export function formatMonthGroupTitle(_date: DateTime): string { export function formatTimelineMonthTitle(_date: DateTime): string {
if (!_date.isValid) { if (!_date.isValid) {
return _date.toString(); return _date.toString();
} }