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

View File

@@ -2,7 +2,7 @@
import AssetLayout from '$lib/components/timeline/AssetLayout.svelte';
import type { AssetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.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 { assetsSnapshot, filterIsInOrNearViewport } from '$lib/managers/timeline-manager/utils.svelte';
import type { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte';
@@ -27,7 +27,7 @@
customThumbnailLayout?: Snippet<[TimelineAsset]>;
singleSelect: boolean;
assetInteraction: AssetMultiSelectManager;
monthGroup: MonthGroup;
timelineMonth: TimelineMonth;
manager: VirtualScrollManager;
onTimelineDaySelect: (timelineDay: TimelineDay, assets: TimelineAsset[]) => void;
};
@@ -36,7 +36,7 @@
customThumbnailLayout,
singleSelect,
assetInteraction,
monthGroup,
timelineMonth,
manager,
onTimelineDaySelect,
}: Props = $props();
@@ -44,10 +44,10 @@
let { isUploading } = uploadAssetsStore;
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 { month, year } = timelineDay.monthGroup.yearMonth;
const { month, year } = timelineDay.timelineMonth.yearMonth;
const date = fromTimelinePlainDate({
year,
month,
@@ -57,13 +57,13 @@
};
</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)}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<section
class={[
{ 'transition-all': !monthGroup.timelineManager.suspendTransitions },
!monthGroup.timelineManager.suspendTransitions && `delay-${transitionDuration}`,
{ 'transition-all': !timelineMonth.timelineManager.suspendTransitions },
!timelineMonth.timelineManager.suspendTransitions && `delay-${transitionDuration}`,
]}
data-group
style:position="absolute"

View File

@@ -92,7 +92,7 @@
scrubberWidth = usingMobileDevice ? MOBILE_WIDTH : DESKTOP_WIDTH;
});
const toScrollFromMonthGroupPercentage = (
const toScrollFromTimelineMonthPercentage = (
scrubberMonth: ViewportTopMonth,
scrubberMonthPercent: number,
scrubOverallPercent: number,
@@ -125,7 +125,7 @@
}
};
const scrollY = $derived(
toScrollFromMonthGroupPercentage(viewportTopMonth, viewportTopMonthScrollPercent, timelineScrollPercent),
toScrollFromTimelineMonthPercentage(viewportTopMonth, viewportTopMonthScrollPercent, timelineScrollPercent),
);
const timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight);
const relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
@@ -281,12 +281,12 @@
const boundingClientRect = bestElement.boundingClientRect;
const sy = boundingClientRect.y;
const relativeY = y - sy;
const monthGroupPercentY = relativeY / boundingClientRect.height;
const timelineMonthPercentY = relativeY / boundingClientRect.height;
return {
isOnPaddingTop: false,
isOnPaddingBottom: false,
segment,
monthGroupPercentY,
timelineMonthPercentY,
};
}
@@ -309,7 +309,7 @@
isOnPaddingTop,
isOnPaddingBottom,
segment: undefined,
monthGroupPercentY: 0,
timelineMonthPercentY: 0,
};
};
@@ -328,7 +328,7 @@
const upper = rect?.height - (PADDING_TOP + PADDING_BOTTOM);
hoverY = clamp(clientY - rect?.top - PADDING_TOP, lower, upper);
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;
isHoverOnPaddingTop = isOnPaddingTop;
isHoverOnPaddingBottom = isOnPaddingBottom;
@@ -336,7 +336,7 @@
const scrubData = {
scrubberMonth: segmentDate,
overallScrollPercent: toTimelineY(hoverY),
scrubberMonthScrollPercent: monthGroupPercentY,
scrubberMonthScrollPercent: timelineMonthPercentY,
};
if (wasDragging === false && isDragging) {
void startScrub?.(scrubData);

View File

@@ -15,7 +15,7 @@
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import type { TimelineDay } from '$lib/managers/timeline-manager/timeline-day.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 type { TimelineAsset, TimelineManagerOptions, ViewportTopMonth } from '$lib/managers/timeline-manager/types';
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
@@ -121,10 +121,11 @@
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 position = getAssetPosition(assetId, monthGroup);
const scrollToAssetPosition = (assetId: string, timelineMonth: TimelineMonth) => {
const position = getAssetPosition(assetId, timelineMonth);
if (!position) {
return;
@@ -176,11 +177,11 @@
// the performance benefits of deferred layouts while still supporting deep linking
// to assets at the end of the timeline.
timelineManager.isScrollingOnLoad = true;
const monthGroup = await timelineManager.findMonthGroupForAsset({ id: assetId });
if (!monthGroup) {
const timelineMonth = await timelineManager.findTimelineMonthForAsset({ id: assetId });
if (!timelineMonth) {
return false;
}
scrollToAssetPosition(assetId, monthGroup);
scrollToAssetPosition(assetId, timelineMonth);
return true;
} finally {
timelineManager.isScrollingOnLoad = false;
@@ -188,11 +189,11 @@
};
const scrollToAsset = (asset: TimelineAsset) => {
const monthGroup = timelineManager.getMonthGroupByAssetId(asset.id);
if (!monthGroup) {
const timelineMonth = timelineManager.getTimelineMonthByAssetId(asset.id);
if (!timelineMonth) {
return false;
}
scrollToAssetPosition(asset.id, monthGroup);
scrollToAssetPosition(asset.id, timelineMonth);
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 maxScrollPercent = timelineManager.maxScrollPercent;
const delta = segmentHeight * monthGroupScrollPercent;
const delta = segmentHeight * timelineMonthScrollPercent;
const scrollToTop = (topOffset + delta) * maxScrollPercent;
timelineManager.scrollTo(scrollToTop);
@@ -294,13 +295,13 @@
scrubberMonthScrollPercent,
);
} else {
const monthGroup = timelineManager.months.find(
const timelineMonth = timelineManager.months.find(
({ yearMonth: { year, month } }) => year === scrubberMonth.year && month === scrubberMonth.month,
);
if (!monthGroup) {
if (!timelineMonth) {
return;
}
scrollToSegmentPercentage(monthGroup.top, monthGroup.height, scrubberMonthScrollPercent);
scrollToSegmentPercentage(timelineMonth.top, timelineMonth.height, scrubberMonthScrollPercent);
}
};
@@ -325,28 +326,28 @@
const monthsLength = timelineManager.months.length;
for (let i = -1; i < monthsLength + 1; i++) {
let monthGroup: ViewportTopMonth;
let monthGroupHeight: number;
let timelineMonth: ViewportTopMonth;
let timelineMonthHeight: number;
if (i === -1) {
// lead-in
monthGroup = 'lead-in';
monthGroupHeight = timelineManager.topSectionHeight;
timelineMonth = 'lead-in';
timelineMonthHeight = timelineManager.topSectionHeight;
} else if (i === monthsLength) {
// lead-out
monthGroup = 'lead-out';
monthGroupHeight = timelineManager.bottomSectionHeight;
timelineMonth = 'lead-out';
timelineMonthHeight = timelineManager.bottomSectionHeight;
} else {
monthGroup = timelineManager.months[i].yearMonth;
monthGroupHeight = timelineManager.months[i].height;
timelineMonth = timelineManager.months[i].yearMonth;
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
if (next < -1 && monthGroup) {
viewportTopMonth = monthGroup;
if (next < -1 && timelineMonth) {
viewportTopMonth = timelineMonth;
// 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
if (viewportTopMonthScrollPercent > 0.9999 && i + 1 < monthsLength - 1) {
@@ -432,16 +433,16 @@
assetInteraction.clearCandidates();
if (assetInteraction.startAsset && rangeSelection) {
const startBucket = timelineManager.getMonthGroupByAssetId(assetInteraction.startAsset.id);
const endBucket = timelineManager.getMonthGroupByAssetId(asset.id);
const startBucket = timelineManager.getTimelineMonthByAssetId(assetInteraction.startAsset.id);
const endBucket = timelineManager.getTimelineMonthByAssetId(asset.id);
if (!startBucket || !endBucket) {
return;
}
const monthGroups = timelineManager.months;
const startBucketIndex = monthGroups.indexOf(startBucket);
const endBucketIndex = monthGroups.indexOf(endBucket);
const timelineMonths = timelineManager.months;
const startBucketIndex = timelineMonths.indexOf(startBucket);
const endBucketIndex = timelineMonths.indexOf(endBucket);
if (startBucketIndex === -1 || endBucketIndex === -1) {
return;
@@ -452,9 +453,9 @@
// Select/deselect assets in range (start,end)
for (let index = rangeStartIndex + 1; index < rangeEndIndex; index++) {
const monthGroup = monthGroups[index];
await timelineManager.loadMonthGroup(monthGroup.yearMonth);
for (const monthAsset of monthGroup.assetsIterator()) {
const timelineMonth = timelineMonths[index];
await timelineManager.loadTimelineMonth(timelineMonth.yearMonth);
for (const monthAsset of timelineMonth.assetsIterator()) {
if (deselect) {
assetInteraction.removeAssetFromMultiselectGroup(monthAsset.id);
} else {
@@ -465,10 +466,10 @@
// Update date group selection in range [start,end]
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
for (const timelineDay of monthGroup.timelineDays) {
for (const timelineDay of timelineMonth.timelineDays) {
const timelineDayTitle = timelineDay.groupTitle;
if (timelineDay.getAssets().every((a) => assetInteraction.hasSelectedAsset(a.id))) {
assetInteraction.addGroupToMultiselectGroup(timelineDayTitle);
@@ -517,7 +518,7 @@
$effect(() => {
if (assetViewerManager.asset && assetViewerManager.isViewing) {
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}
</section>
{#each timelineManager.months as monthGroup (monthGroup.viewId)}
{@const isInOrNearViewport = monthGroup.isInOrNearViewport}
{@const absoluteHeight = monthGroup.top}
{#each timelineManager.months as timelineMonth (timelineMonth.viewId)}
{@const isInOrNearViewport = timelineMonth.isInOrNearViewport}
{@const absoluteHeight = timelineMonth.top}
{#if !monthGroup.isLoaded}
{#if !timelineMonth.isLoaded}
<div
style:height={monthGroup.height + 'px'}
style:height={timelineMonth.height + 'px'}
style:position="absolute"
style:transform={`translate3d(0,${absoluteHeight}px,0)`}
style:width="100%"
>
<Skeleton {invisible} height={monthGroup.height} title={monthGroup.monthGroupTitle} />
<Skeleton {invisible} height={timelineMonth.height} title={timelineMonth.title} />
</div>
{:else if isInOrNearViewport}
<div
class="month-group"
style:height={monthGroup.height + 'px'}
class="timeline-month"
style:height={timelineMonth.height + 'px'}
style:position="absolute"
style:transform={`translate3d(0,${absoluteHeight}px,0)`}
style:width="100%"
@@ -668,7 +669,7 @@
{assetInteraction}
{customThumbnailLayout}
{singleSelect}
{monthGroup}
{timelineMonth}
manager={timelineManager}
onTimelineDaySelect={handleGroupSelect}
>
@@ -735,7 +736,7 @@
scrollbar-width: none;
}
.month-group {
.timeline-month {
contain: layout size paint;
transform-style: flat;
backface-visibility: hidden;

View File

@@ -1,7 +1,7 @@
import { setDifference, type TimelineDate } from '$lib/utils/timeline-util';
import { AssetOrder } from '@immich/sdk';
import type { MonthGroup } from './month-group.svelte';
import type { TimelineDay } from './timeline-day.svelte';
import type { TimelineMonth } from './timeline-month.svelte';
import type { TimelineAsset } from './types';
export class GroupInsertionCache {
@@ -34,23 +34,23 @@ export class GroupInsertionCache {
get updatedBuckets() {
// eslint-disable-next-line svelte/prefer-svelte-reactivity
const updated = new Set<MonthGroup>();
const updated = new Set<TimelineMonth>();
for (const group of this.changedTimelineDays) {
updated.add(group.monthGroup);
updated.add(group.timelineMonth);
}
return updated;
}
get bucketsWithNewTimelineDays() {
// eslint-disable-next-line svelte/prefer-svelte-reactivity
const updated = new Set<MonthGroup>();
const updated = new Set<TimelineMonth>();
for (const group of this.newTimelineDays) {
updated.add(group.monthGroup);
updated.add(group.timelineMonth);
}
return updated;
}
sort(monthGroup: MonthGroup, sortOrder: AssetOrder = AssetOrder.Desc) {
sort(timelineMonth: TimelineMonth, sortOrder: AssetOrder = AssetOrder.Desc) {
for (const group of this.changedTimelineDays) {
group.sortAssets(sortOrder);
}
@@ -58,7 +58,7 @@ export class GroupInsertionCache {
group.sortAssets(sortOrder);
}
if (this.newTimelineDays.size > 0) {
monthGroup.sortTimelineDays();
timelineMonth.sortTimelineDays();
}
}
}

View File

@@ -1,6 +1,6 @@
import { TUNABLES } from '$lib/utils/tunables';
import type { MonthGroup } from '../month-group.svelte';
import { TimelineManager } from '../timeline-manager.svelte';
import type { TimelineMonth } from '../timeline-month.svelte';
const {
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
@@ -40,7 +40,7 @@ function calculateViewportProximity(regionTop: number, regionBottom: number, win
return ViewportProximity.InViewport;
}
export function updateMonthGroupViewportProximity(timelineManager: TimelineManager, month: MonthGroup) {
export function updateTimelineMonthViewportProximity(timelineManager: TimelineManager, month: TimelineMonth) {
const proximity = calculateViewportProximity(
month.top,
month.top + month.height,

View File

@@ -1,8 +1,8 @@
import type { MonthGroup } from '../month-group.svelte';
import { TimelineManager } from '../timeline-manager.svelte';
import type { TimelineMonth } from '../timeline-month.svelte';
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;
if (invalidateHeight) {
month.isHeightActual = false;
@@ -17,10 +17,10 @@ export function updateGeometry(timelineManager: TimelineManager, month: MonthGro
}
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 cumulativeWidth = 0;
let currentRowHeight = 0;

View File

@@ -1,21 +1,21 @@
import { authManager } from '$lib/managers/auth-manager.svelte';
import { toISOYearMonthUTC } from '$lib/utils/timeline-util';
import { getTimeBucket } from '@immich/sdk';
import type { MonthGroup } from '../month-group.svelte';
import { TimelineManager } from '../timeline-manager.svelte';
import type { TimelineMonth } from '../timeline-month.svelte';
import type { TimelineManagerOptions } from '../types';
export async function loadFromTimeBuckets(
timelineManager: TimelineManager,
monthGroup: MonthGroup,
timelineMonth: TimelineMonth,
options: TimelineManagerOptions,
signal: AbortSignal,
): Promise<void> {
if (monthGroup.getFirstAsset()) {
if (timelineMonth.getFirstAsset()) {
return;
}
const timeBucket = toISOYearMonthUTC(monthGroup.yearMonth);
const timeBucket = toISOYearMonthUTC(timelineMonth.yearMonth);
const bucketResponse = await getTimeBucket(
{
...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) {
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) => ({
id: unprocessed.id,
localDateTime: unprocessed.localDateTime,

View File

@@ -1,74 +1,86 @@
import { describe, expect, it } from 'vitest';
import type { MonthGroup } from '../month-group.svelte';
import { findClosestGroupForDate } from './search-support.svelte';
import type { TimelineMonth } from '../timeline-month.svelte';
import { findClosestTimelineMonthForDate } from './search-support.svelte';
function createMockMonthGroup(year: number, month: number): MonthGroup {
function createMockTimelineMonth(year: number, month: number): TimelineMonth {
return {
yearMonth: { year, month },
} as MonthGroup;
} as TimelineMonth;
}
describe('findClosestGroupForDate', () => {
describe('findClosestTimelineMonthForDate', () => {
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();
});
it('should return the only month when there is only one month', () => {
const months = [createMockMonthGroup(2024, 6)];
const result = findClosestGroupForDate(months, { year: 2025, month: 1 });
const months = [createMockTimelineMonth(2024, 6)];
const result = findClosestTimelineMonthForDate(months, { year: 2025, month: 1 });
expect(result?.yearMonth).toEqual({ year: 2024, month: 6 });
});
it('should return exact match when available', () => {
const months = [createMockMonthGroup(2024, 1), createMockMonthGroup(2024, 6), createMockMonthGroup(2024, 12)];
const result = findClosestGroupForDate(months, { year: 2024, month: 6 });
const months = [
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 });
});
it('should find closest month when target is between two months', () => {
const months = [createMockMonthGroup(2024, 1), createMockMonthGroup(2024, 6), createMockMonthGroup(2024, 12)];
const result = findClosestGroupForDate(months, { year: 2024, month: 4 });
const months = [
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 });
});
it('should handle year boundaries correctly (2023-12 vs 2024-01)', () => {
const months = [createMockMonthGroup(2023, 12), createMockMonthGroup(2024, 2)];
const result = findClosestGroupForDate(months, { year: 2024, month: 1 });
const months = [createMockTimelineMonth(2023, 12), createMockTimelineMonth(2024, 2)];
const result = findClosestTimelineMonthForDate(months, { year: 2024, month: 1 });
// 2024-01 is 1 month from 2023-12 and 1 month from 2024-02
// Should return first encountered with min distance (2023-12)
expect(result?.yearMonth).toEqual({ year: 2023, month: 12 });
});
it('should correctly calculate distance across years', () => {
const months = [createMockMonthGroup(2022, 6), createMockMonthGroup(2024, 6)];
const result = findClosestGroupForDate(months, { year: 2023, month: 6 });
const months = [createMockTimelineMonth(2022, 6), createMockTimelineMonth(2024, 6)];
const result = findClosestTimelineMonthForDate(months, { year: 2023, month: 6 });
// Both are exactly 12 months away, should return first encountered
expect(result?.yearMonth).toEqual({ year: 2022, month: 6 });
});
it('should handle target before all months', () => {
const months = [createMockMonthGroup(2024, 6), createMockMonthGroup(2024, 12)];
const result = findClosestGroupForDate(months, { year: 2024, month: 1 });
const months = [createMockTimelineMonth(2024, 6), createMockTimelineMonth(2024, 12)];
const result = findClosestTimelineMonthForDate(months, { year: 2024, month: 1 });
expect(result?.yearMonth).toEqual({ year: 2024, month: 6 });
});
it('should handle target after all months', () => {
const months = [createMockMonthGroup(2024, 1), createMockMonthGroup(2024, 6)];
const result = findClosestGroupForDate(months, { year: 2025, month: 1 });
const months = [createMockTimelineMonth(2024, 1), createMockTimelineMonth(2024, 6)];
const result = findClosestTimelineMonthForDate(months, { year: 2025, month: 1 });
expect(result?.yearMonth).toEqual({ year: 2024, month: 6 });
});
it('should handle multiple years correctly', () => {
const months = [createMockMonthGroup(2020, 1), createMockMonthGroup(2022, 1), createMockMonthGroup(2024, 1)];
const result = findClosestGroupForDate(months, { year: 2023, month: 1 });
const months = [
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
expect(result?.yearMonth).toEqual({ year: 2022, month: 1 });
});
it('should prefer closer month when one is clearly closer', () => {
const months = [createMockMonthGroup(2024, 1), createMockMonthGroup(2024, 10)];
const result = findClosestGroupForDate(months, { year: 2024, month: 11 });
const months = [createMockTimelineMonth(2024, 1), createMockTimelineMonth(2024, 10)];
const result = findClosestTimelineMonthForDate(months, { year: 2024, month: 11 });
// 2024-11 is 1 month from 2024-10 and 10 months from 2024-01
expect(result?.yearMonth).toEqual({ year: 2024, month: 10 });
});

View File

@@ -1,8 +1,8 @@
import { plainDateTimeCompare, type TimelineYearMonth } from '$lib/utils/timeline-util';
import { AssetOrder, type AssetResponseDto } from '@immich/sdk';
import { DateTime } from 'luxon';
import type { MonthGroup } from '../month-group.svelte';
import { TimelineManager } from '../timeline-manager.svelte';
import type { TimelineMonth } from '../timeline-month.svelte';
import type { AssetDescriptor, Direction, TimelineAsset } from '../types';
export async function getAssetWithOffset(
@@ -11,44 +11,44 @@ export async function getAssetWithOffset(
interval: 'asset' | 'day' | 'month' | 'year' = 'asset',
direction: Direction,
): Promise<TimelineAsset | undefined> {
const monthGroup = await timelineManager.findMonthGroupForAsset(assetDescriptor);
if (!monthGroup) {
const timelineMonth = await timelineManager.findTimelineMonthForAsset(assetDescriptor);
if (!timelineMonth) {
return;
}
const asset = monthGroup.findAssetById(assetDescriptor);
const asset = timelineMonth.findAssetById(assetDescriptor);
if (!asset) {
return;
}
switch (interval) {
case 'asset': {
return getAssetByAssetOffset(timelineManager, asset, monthGroup, direction);
return getAssetByAssetOffset(timelineManager, asset, timelineMonth, direction);
}
case 'day': {
return getAssetByDayOffset(timelineManager, asset, monthGroup, direction);
return getAssetByDayOffset(timelineManager, asset, timelineMonth, direction);
}
case 'month': {
return getAssetByMonthOffset(timelineManager, monthGroup, direction);
return getAssetByMonthOffset(timelineManager, timelineMonth, direction);
}
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) {
const asset = month.findAssetById({ id });
if (asset) {
return { monthGroup: month, asset };
return { timelineMonth: month, asset };
}
}
}
export function getMonthGroupByDate(
export function getTimelineMonthByDate(
timelineManager: TimelineManager,
targetYearMonth: TimelineYearMonth,
): MonthGroup | undefined {
): TimelineMonth | undefined {
return timelineManager.months.find(
(month) => month.yearMonth.year === targetYearMonth.year && month.yearMonth.month === targetYearMonth.month,
);
@@ -57,12 +57,12 @@ export function getMonthGroupByDate(
async function getAssetByAssetOffset(
timelineManager: TimelineManager,
asset: TimelineAsset,
monthGroup: MonthGroup,
timelineMonth: TimelineMonth,
direction: Direction,
) {
const timelineDay = monthGroup.findTimelineDayForAsset(asset);
const timelineDay = timelineMonth.findTimelineDayForAsset(asset);
for await (const targetAsset of timelineManager.assetsIterator({
startMonthGroup: monthGroup,
startTimelineMonth: timelineMonth,
startTimelineDay: timelineDay,
startAsset: asset,
direction,
@@ -76,12 +76,12 @@ async function getAssetByAssetOffset(
async function getAssetByDayOffset(
timelineManager: TimelineManager,
asset: TimelineAsset,
monthGroup: MonthGroup,
timelineMonth: TimelineMonth,
direction: Direction,
) {
const timelineDay = monthGroup.findTimelineDayForAsset(asset);
const timelineDay = timelineMonth.findTimelineDayForAsset(asset);
for await (const targetAsset of timelineManager.assetsIterator({
startMonthGroup: monthGroup,
startTimelineMonth: timelineMonth,
startTimelineDay: timelineDay,
startAsset: asset,
direction,
@@ -92,44 +92,49 @@ async function getAssetByDayOffset(
}
}
async function getAssetByMonthOffset(timelineManager: TimelineManager, month: MonthGroup, direction: Direction) {
for (const targetMonth of timelineManager.monthGroupIterator({ startMonthGroup: month, direction })) {
async function getAssetByMonthOffset(timelineManager: TimelineManager, month: TimelineMonth, direction: Direction) {
for (const targetMonth of timelineManager.timelineMonthIterator({ startTimelineMonth: month, direction })) {
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;
}
}
}
async function getAssetByYearOffset(timelineManager: TimelineManager, month: MonthGroup, direction: Direction) {
for (const targetMonth of timelineManager.monthGroupIterator({ startMonthGroup: month, direction })) {
async function getAssetByYearOffset(timelineManager: TimelineManager, month: TimelineMonth, direction: Direction) {
for (const targetMonth of timelineManager.timelineMonthIterator({ startTimelineMonth: month, direction })) {
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;
}
}
}
export async function retrieveRange(timelineManager: TimelineManager, start: AssetDescriptor, end: AssetDescriptor) {
let { asset: startAsset, monthGroup: startMonthGroup } = findMonthGroupForAsset(timelineManager, start.id) ?? {};
if (!startMonthGroup || !startAsset) {
let { asset: startAsset, timelineMonth: startTimelineMonth } =
findTimelineMonthForAsset(timelineManager, start.id) ?? {};
if (!startTimelineMonth || !startAsset) {
return [];
}
let { asset: endAsset, monthGroup: endMonthGroup } = findMonthGroupForAsset(timelineManager, end.id) ?? {};
if (!endMonthGroup || !endAsset) {
let { asset: endAsset, timelineMonth: endTimelineMonth } = findTimelineMonthForAsset(timelineManager, end.id) ?? {};
if (!endTimelineMonth || !endAsset) {
return [];
}
const assetOrder: AssetOrder = timelineManager.getAssetOrder();
if (plainDateTimeCompare(assetOrder === AssetOrder.Desc, startAsset.localDateTime, endAsset.localDateTime) < 0) {
[startAsset, endAsset] = [endAsset, startAsset];
// eslint-disable-next-line no-useless-assignment
[startMonthGroup, endMonthGroup] = [endMonthGroup, startMonthGroup];
[startTimelineMonth, endTimelineMonth] = [endTimelineMonth, startTimelineMonth];
}
const range: TimelineAsset[] = [];
const startTimelineDay = startMonthGroup.findTimelineDayForAsset(startAsset);
const startTimelineDay = startTimelineMonth.findTimelineDayForAsset(startAsset);
for await (const targetAsset of timelineManager.assetsIterator({
startMonthGroup,
startTimelineMonth,
startTimelineDay,
startAsset,
})) {
@@ -141,7 +146,7 @@ export async function retrieveRange(timelineManager: TimelineManager, start: Ass
return range;
}
export function findMonthGroupForDate(timelineManager: TimelineManager, targetYearMonth: TimelineYearMonth) {
export function findTimelineMonthForDate(timelineManager: TimelineManager, targetYearMonth: TimelineYearMonth) {
for (const month of timelineManager.months) {
const { year, month: monthNum } = month.yearMonth;
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 });
let closestMonth: MonthGroup | undefined;
let closestMonth: TimelineMonth | undefined;
let minDifference = Number.MAX_SAFE_INTEGER;
for (const month of months) {

View File

@@ -5,12 +5,12 @@ import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils';
import { plainDateTimeCompare } from '$lib/utils/timeline-util';
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 { ViewerAsset } from './viewer-asset.svelte';
export class TimelineDay {
readonly monthGroup: MonthGroup;
readonly timelineMonth: TimelineMonth;
readonly index: number;
readonly groupTitle: string;
readonly day: number;
@@ -26,9 +26,9 @@ export class TimelineDay {
#col = $state(0);
#deferredLayout = false;
constructor(monthGroup: MonthGroup, index: number, day: number, groupTitle: string) {
constructor(timelineMonth: TimelineMonth, index: number, day: number, groupTitle: string) {
this.index = index;
this.monthGroup = monthGroup;
this.timelineMonth = timelineMonth;
this.day = day;
this.groupTitle = groupTitle;
}
@@ -128,7 +128,7 @@ export class TimelineDay {
}
unprocessedIds.delete(assetId);
processedIds.add(assetId);
if (remove || this.monthGroup.timelineManager.isExcluded(asset)) {
if (remove || this.timelineMonth.timelineManager.isExcluded(asset)) {
this.viewerAssets.splice(index, 1);
changedGeometry = true;
}
@@ -137,7 +137,7 @@ export class TimelineDay {
}
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;
return;
}
@@ -152,6 +152,6 @@ export class TimelineDay {
}
get absoluteTimelineDayTop() {
return this.monthGroup.top + this.#top;
return this.timelineMonth.top + this.#top;
}
}

View File

@@ -1,6 +1,6 @@
import { sdkMock } from '$lib/__mocks__/sdk.mock';
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 { fromISODateTimeUTCToObject } from '$lib/utils/timeline-util';
import { AssetVisibility, type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
@@ -95,7 +95,7 @@ describe('TimelineManager', () => {
});
});
describe('loadMonthGroup', () => {
describe('loadTimelineMonth', () => {
let timelineManager: TimelineManager;
const bucketAssets: Record<string, TimelineAsset[]> = {
'2024-01-03T00:00:00.000Z': timelineAssetFactory.buildList(1).map((asset) =>
@@ -131,47 +131,47 @@ describe('TimelineManager', () => {
});
it('loads a month', async () => {
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(0);
await timelineManager.loadMonthGroup({ year: 2024, month: 1 });
expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(0);
await timelineManager.loadTimelineMonth({ year: 2024, month: 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 () => {
await timelineManager.loadMonthGroup({ year: 2023, month: 1 });
await timelineManager.loadTimelineMonth({ year: 2023, month: 1 });
expect(sdkMock.getTimeBucket).toBeCalledTimes(0);
});
it('cancels month loading', async () => {
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })!;
void timelineManager.loadMonthGroup({ year: 2024, month: 1 });
const month = getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })!;
void timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
const abortSpy = vi.spyOn(month!.loader!.cancelToken!, 'abort');
month?.cancel();
expect(abortSpy).toBeCalledTimes(1);
await timelineManager.loadMonthGroup({ year: 2024, month: 1 });
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(3);
await timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(3);
});
it('prevents loading months multiple times', async () => {
await Promise.all([
timelineManager.loadMonthGroup({ year: 2024, month: 1 }),
timelineManager.loadMonthGroup({ year: 2024, month: 1 }),
timelineManager.loadTimelineMonth({ year: 2024, month: 1 }),
timelineManager.loadTimelineMonth({ year: 2024, month: 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);
});
it('allows loading a canceled month', async () => {
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })!;
const loadPromise = timelineManager.loadMonthGroup({ year: 2024, month: 1 });
const month = getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })!;
const loadPromise = timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
month.cancel();
await loadPromise;
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);
});
});
@@ -241,7 +241,7 @@ describe('TimelineManager', () => {
);
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?.getAssets().length).toEqual(3);
expect(month?.getAssets()[0].id).toEqual(assetOne.id);
@@ -346,15 +346,15 @@ describe('TimelineManager', () => {
timelineManager.upsertAssets([asset]);
expect(timelineManager.months.length).toEqual(1);
expect(getMonthGroupByDate(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 })).not.toBeUndefined();
expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(1);
timelineManager.upsertAssets([updatedAsset]);
expect(timelineManager.months.length).toEqual(2);
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })).not.toBeUndefined();
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(0);
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 3 })).not.toBeUndefined();
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 3 })?.getAssets().length).toEqual(1);
expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })).not.toBeUndefined();
expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(0);
expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 3 })).not.toBeUndefined();
expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 3 })?.getAssets().length).toEqual(1);
});
it('yearMonth is not a shared reference with asset.localDateTime (reference bug)', () => {
@@ -365,7 +365,7 @@ describe('TimelineManager', () => {
);
timelineManager.upsertAssets([asset]);
const januaryMonth = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })!;
const januaryMonth = getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })!;
const monthYearMonth = januaryMonth.yearMonth;
const originalMonth = monthYearMonth.month;
@@ -611,8 +611,8 @@ describe('TimelineManager', () => {
});
it('returns previous assetId', async () => {
await timelineManager.loadMonthGroup({ year: 2024, month: 1 });
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 });
await timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
const month = getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 });
const a = month!.getAssets()[0];
const b = month!.getAssets()[1];
@@ -621,11 +621,11 @@ describe('TimelineManager', () => {
});
it('returns previous assetId spanning multiple months', async () => {
await timelineManager.loadMonthGroup({ year: 2024, month: 2 });
await timelineManager.loadMonthGroup({ year: 2024, month: 3 });
await timelineManager.loadTimelineMonth({ year: 2024, month: 2 });
await timelineManager.loadTimelineMonth({ year: 2024, month: 3 });
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 2 });
const previousMonth = getMonthGroupByDate(timelineManager, { year: 2024, month: 3 });
const month = getTimelineMonthByDate(timelineManager, { year: 2024, month: 2 });
const previousMonth = getTimelineMonthByDate(timelineManager, { year: 2024, month: 3 });
const a = month!.getAssets()[0];
const b = previousMonth!.getAssets()[0];
const previous = await timelineManager.getLaterAsset(a);
@@ -633,23 +633,23 @@ describe('TimelineManager', () => {
});
it('loads previous month', async () => {
await timelineManager.loadMonthGroup({ year: 2024, month: 2 });
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 2 });
const previousMonth = getMonthGroupByDate(timelineManager, { year: 2024, month: 3 });
await timelineManager.loadTimelineMonth({ year: 2024, month: 2 });
const month = getTimelineMonthByDate(timelineManager, { year: 2024, month: 2 });
const previousMonth = getTimelineMonthByDate(timelineManager, { year: 2024, month: 3 });
const a = month!.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 previous = await timelineManager.getLaterAsset(a);
expect(previous).toEqual(b);
expect(loadMonthGroupSpy).toBeCalledTimes(0);
expect(loadTimelineMonthSpy).toBeCalledTimes(0);
expect(previousMonthSpy).toBeCalledTimes(0);
});
it('skips removed assets', async () => {
await timelineManager.loadMonthGroup({ year: 2024, month: 1 });
await timelineManager.loadMonthGroup({ year: 2024, month: 2 });
await timelineManager.loadMonthGroup({ year: 2024, month: 3 });
await timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
await timelineManager.loadTimelineMonth({ year: 2024, month: 2 });
await timelineManager.loadTimelineMonth({ year: 2024, month: 3 });
const [assetOne, assetTwo, assetThree] = await getAssets(timelineManager);
timelineManager.removeAssets([assetTwo.id]);
@@ -657,12 +657,12 @@ describe('TimelineManager', () => {
});
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();
});
});
describe('getMonthGroupIndexByAssetId', () => {
describe('getTimelineMonthIndexByAssetId', () => {
let timelineManager: TimelineManager;
beforeEach(async () => {
@@ -673,8 +673,8 @@ describe('TimelineManager', () => {
});
it('returns null for invalid months', () => {
expect(getMonthGroupByDate(timelineManager, { year: -1, month: -1 })).toBeUndefined();
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 3 })).toBeUndefined();
expect(getTimelineMonthByDate(timelineManager, { year: -1, month: -1 })).toBeUndefined();
expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 3 })).toBeUndefined();
});
it('returns the month index', () => {
@@ -690,10 +690,10 @@ describe('TimelineManager', () => {
);
timelineManager.upsertAssets([assetOne, assetTwo]);
expect(timelineManager.getMonthGroupByAssetId(assetTwo.id)?.yearMonth.year).toEqual(2024);
expect(timelineManager.getMonthGroupByAssetId(assetTwo.id)?.yearMonth.month).toEqual(2);
expect(timelineManager.getMonthGroupByAssetId(assetOne.id)?.yearMonth.year).toEqual(2024);
expect(timelineManager.getMonthGroupByAssetId(assetOne.id)?.yearMonth.month).toEqual(1);
expect(timelineManager.getTimelineMonthByAssetId(assetTwo.id)?.yearMonth.year).toEqual(2024);
expect(timelineManager.getTimelineMonthByAssetId(assetTwo.id)?.yearMonth.month).toEqual(2);
expect(timelineManager.getTimelineMonthByAssetId(assetOne.id)?.yearMonth.year).toEqual(2024);
expect(timelineManager.getTimelineMonthByAssetId(assetOne.id)?.yearMonth.month).toEqual(1);
});
it('ignores removed months', () => {
@@ -710,8 +710,8 @@ describe('TimelineManager', () => {
timelineManager.upsertAssets([assetOne, assetTwo]);
timelineManager.removeAssets([assetTwo.id]);
expect(timelineManager.getMonthGroupByAssetId(assetOne.id)?.yearMonth.year).toEqual(2024);
expect(timelineManager.getMonthGroupByAssetId(assetOne.id)?.yearMonth.month).toEqual(1);
expect(timelineManager.getTimelineMonthByAssetId(assetOne.id)?.yearMonth.year).toEqual(2024);
expect(timelineManager.getTimelineMonthByAssetId(assetOne.id)?.yearMonth.month).toEqual(1);
});
});

View File

@@ -2,15 +2,15 @@ import { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/Virtual
import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.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 { loadFromTimeBuckets } from '$lib/managers/timeline-manager/internal/load-support.svelte';
import {
findClosestGroupForDate,
findMonthGroupForAsset as findMonthGroupForAssetUtil,
findMonthGroupForDate,
findClosestTimelineMonthForDate,
findTimelineMonthForAsset as findTimelineMonthForAssetUtil,
findTimelineMonthForDate,
getAssetWithOffset,
getMonthGroupByDate,
getTimelineMonthByDate,
retrieveRange as retrieveRangeUtil,
} from '$lib/managers/timeline-manager/internal/search-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 { SvelteDate, SvelteSet } from 'svelte/reactivity';
import { isMismatched, updateObject } from './internal/utils.svelte';
import { MonthGroup } from './month-group.svelte';
import { TimelineDay } from './timeline-day.svelte';
import { TimelineMonth } from './timeline-month.svelte';
import type {
AssetDescriptor,
Direction,
@@ -40,7 +40,7 @@ import type {
} from './types';
type ViewportTopMonthIntersection = {
month: MonthGroup | undefined;
month: TimelineMonth | undefined;
// Where viewport top intersects month (0 = month top, 1 = month bottom)
viewportTopRatioInMonth: number;
// Where month bottom is in viewport (0 = viewport top, 1 = viewport bottom)
@@ -67,7 +67,7 @@ export class TimelineManager extends VirtualScrollManager {
isInitialized = $state(false);
isScrollingOnLoad = false;
months: MonthGroup[] = $state([]);
months: TimelineMonth[] = $state([]);
albumAssets: Set<string> = new SvelteSet();
scrubberMonths: ScrubberMonth[] = $state([]);
scrubberTimelineHeight: number = $state(0);
@@ -137,24 +137,27 @@ export class TimelineManager extends VirtualScrollManager {
}
async *assetsIterator(options?: {
startMonthGroup?: MonthGroup;
startTimelineMonth?: TimelineMonth;
startTimelineDay?: TimelineDay;
startAsset?: TimelineAsset;
direction?: Direction;
}) {
const direction = options?.direction ?? 'earlier';
let { startTimelineDay, startAsset } = options ?? {};
for (const monthGroup of this.monthGroupIterator({ direction, startMonthGroup: options?.startMonthGroup })) {
await this.loadMonthGroup(monthGroup.yearMonth, { cancelable: false });
yield* monthGroup.assetsIterator({ startTimelineDay, startAsset, direction });
for (const timelineMonth of this.timelineMonthIterator({
direction,
startTimelineMonth: options?.startTimelineMonth,
})) {
await this.loadTimelineMonth(timelineMonth.yearMonth, { cancelable: false });
yield* timelineMonth.assetsIterator({ startTimelineDay, startAsset, direction });
startTimelineDay = startAsset = undefined;
}
}
*monthGroupIterator(options?: { direction?: Direction; startMonthGroup?: MonthGroup }) {
*timelineMonthIterator(options?: { direction?: Direction; startTimelineMonth?: TimelineMonth }) {
const isEarlier = options?.direction === 'earlier';
let startIndex = options?.startMonthGroup
? this.months.indexOf(options.startMonthGroup)
let startIndex = options?.startTimelineMonth
? this.months.indexOf(options.startTimelineMonth)
: isEarlier
? 0
: this.months.length - 1;
@@ -181,7 +184,7 @@ export class TimelineManager extends VirtualScrollManager {
this.#websocketSupport = undefined;
}
#calculateMonthBottomViewportRatio(month: MonthGroup | undefined) {
#calculateMonthBottomViewportRatio(month: TimelineMonth | undefined) {
if (!month) {
return 0;
}
@@ -191,7 +194,7 @@ export class TimelineManager extends VirtualScrollManager {
return clamp(bottomOfMonthInViewport / windowHeight, 0, 1);
}
#calculateVewportTopRatioInMonth(month: MonthGroup | undefined) {
#calculateVewportTopRatioInMonth(month: TimelineMonth | undefined) {
if (!month) {
return 0;
}
@@ -209,7 +212,7 @@ export class TimelineManager extends VirtualScrollManager {
this.#updatingViewportProximities = true;
for (const month of this.months) {
updateMonthGroupViewportProximity(this, month);
updateTimelineMonthViewportProximity(this, month);
}
const month = this.months.find((month) => month.isInViewport);
@@ -225,7 +228,7 @@ export class TimelineManager extends VirtualScrollManager {
this.#updatingViewportProximities = false;
}
clearDeferredLayout(month: MonthGroup) {
clearDeferredLayout(month: TimelineMonth) {
const hasDeferred = month.timelineDays.some((group) => group.deferredLayout);
if (hasDeferred) {
updateGeometry(this, month, { invalidateHeight: true, noDefer: true });
@@ -235,7 +238,7 @@ export class TimelineManager extends VirtualScrollManager {
}
}
async #initializeMonthGroups() {
async #initializeTimelineMonths() {
const timebuckets = await getTimeBuckets({
...authManager.params,
...this.#options,
@@ -243,7 +246,7 @@ export class TimelineManager extends VirtualScrollManager {
this.months = timebuckets.map((timeBucket) => {
const date = new SvelteDate(timeBucket.timeBucket);
return new MonthGroup(
return new TimelineMonth(
this,
{ year: date.getUTCFullYear(), month: date.getUTCMonth() + 1 },
timeBucket.count,
@@ -280,7 +283,7 @@ export class TimelineManager extends VirtualScrollManager {
this.albumAssets.clear();
await this.initTask.execute(async () => {
this.#options = options;
await this.#initializeMonthGroups();
await this.#initializeTimelineMonths();
}, true);
}
@@ -332,31 +335,31 @@ export class TimelineManager extends VirtualScrollManager {
assetCount: month.assetsCount,
year: month.yearMonth.year,
month: month.yearMonth.month,
title: month.monthGroupTitle,
title: month.title,
height: month.height,
}));
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;
if (options) {
cancelable = options.cancelable;
}
const monthGroup = getMonthGroupByDate(this, yearMonth);
if (!monthGroup) {
const timelineMonth = getTimelineMonthByDate(this, yearMonth);
if (!timelineMonth) {
return;
}
if (monthGroup.loader?.executed) {
if (timelineMonth.loader?.executed) {
return;
}
const executionStatus = await monthGroup.loader?.execute(async (signal: AbortSignal) => {
await loadFromTimeBuckets(this, monthGroup, this.#options, signal);
const executionStatus = await timelineMonth.loader?.execute(async (signal: AbortSignal) => {
await loadFromTimeBuckets(this, timelineMonth, this.#options, signal);
}, cancelable);
if (executionStatus === 'LOADED') {
updateGeometry(this, monthGroup, { invalidateHeight: false });
updateGeometry(this, timelineMonth, { invalidateHeight: false });
this.updateViewportProximities();
}
}
@@ -367,15 +370,15 @@ export class TimelineManager extends VirtualScrollManager {
this.addAssetsUpsertSegments([...notExcluded]);
}
async findMonthGroupForAsset(asset: AssetDescriptor | AssetResponseDto) {
async findTimelineMonthForAsset(asset: AssetDescriptor | AssetResponseDto) {
if (!this.isInitialized) {
await this.initTask.waitUntilExecution();
}
const { id } = asset;
let { monthGroup } = findMonthGroupForAssetUtil(this, id) ?? {};
if (monthGroup) {
return monthGroup;
let { timelineMonth } = findTimelineMonthForAssetUtil(this, id) ?? {};
if (timelineMonth) {
return timelineMonth;
}
const response = isAssetResponseDto(asset)
@@ -390,20 +393,20 @@ export class TimelineManager extends VirtualScrollManager {
return;
}
monthGroup = await this.#loadMonthGroupAtTime(timelineAsset.localDateTime, { cancelable: false });
if (monthGroup?.findAssetById({ id })) {
return monthGroup;
timelineMonth = await this.#loadTimelineMonthAtTime(timelineAsset.localDateTime, { cancelable: false });
if (timelineMonth?.findAssetById({ id })) {
return timelineMonth;
}
}
async #loadMonthGroupAtTime(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }) {
await this.loadMonthGroup(yearMonth, options);
return getMonthGroupByDate(this, yearMonth);
async #loadTimelineMonthAtTime(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }) {
await this.loadTimelineMonth(yearMonth, options);
return getTimelineMonthByDate(this, yearMonth);
}
getMonthGroupByAssetId(assetId: string) {
const monthGroupInfo = findMonthGroupForAssetUtil(this, assetId);
return monthGroupInfo?.monthGroup;
getTimelineMonthByAssetId(assetId: string) {
const timelineMonthInfo = findTimelineMonthForAssetUtil(this, assetId);
return timelineMonthInfo?.timelineMonth;
}
// 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 randomMonth: MonthGroup | undefined = undefined;
let randomMonth: TimelineMonth | undefined = undefined;
for (const month of this.months) {
if (randomAssetIndex < accumulatedCount + month.assetsCount) {
randomMonth = month;
@@ -426,7 +429,7 @@ export class TimelineManager extends VirtualScrollManager {
if (!randomMonth) {
return;
}
await this.loadMonthGroup(randomMonth.yearMonth, { cancelable: false });
await this.loadTimelineMonth(randomMonth.yearMonth, { cancelable: false });
let randomDay: TimelineDay | undefined = undefined;
for (const day of randomMonth.timelineDays) {
@@ -459,10 +462,10 @@ export class TimelineManager extends VirtualScrollManager {
}
protected upsertSegmentForAsset(asset: TimelineAsset) {
let month = getMonthGroupByDate(this, asset.localDateTime);
let month = getTimelineMonthByDate(this, asset.localDateTime);
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);
}
return month;
@@ -508,7 +511,7 @@ export class TimelineManager extends VirtualScrollManager {
return { updated: new Set<string>(), notUpdated: ids, changedGeometry: false };
}
// 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
let notUpdated = new Set(ids);
// eslint-disable-next-line svelte/prefer-svelte-reactivity
@@ -523,7 +526,7 @@ export class TimelineManager extends VirtualScrollManager {
assetsToMoveSegments.push(result.moveAssets);
}
if (result.changedGeometry) {
changedMonthGroups.add(month);
changedTimelineMonths.add(month);
}
notUpdated = setDifference(notUpdated, result.processedIds);
for (const id of result.processedIds) {
@@ -537,8 +540,8 @@ export class TimelineManager extends VirtualScrollManager {
}
}
this.addAssetsUpsertSegments(assetsToAdd);
const changedGeometry = changedMonthGroups.size > 0;
for (const month of changedMonthGroups) {
const changedGeometry = changedTimelineMonths.size > 0;
for (const month of changedTimelineMonths) {
updateGeometry(this, month, { invalidateHeight: true });
}
if (changedGeometry) {
@@ -573,20 +576,20 @@ export class TimelineManager extends VirtualScrollManager {
}
async getClosestAssetToDate(dateTime: TimelineDateTime) {
let monthGroup = findMonthGroupForDate(this, dateTime);
if (!monthGroup) {
let timelineMonth = findTimelineMonthForDate(this, dateTime);
if (!timelineMonth) {
// if exact match not found, find closest
monthGroup = findClosestGroupForDate(this.months, dateTime);
if (!monthGroup) {
timelineMonth = findClosestTimelineMonthForDate(this.months, dateTime);
if (!timelineMonth) {
return;
}
}
await this.loadMonthGroup(dateTime, { cancelable: false });
const asset = monthGroup.findClosest(dateTime);
await this.loadTimelineMonth(dateTime, { cancelable: false });
const asset = timelineMonth.findClosest(dateTime);
if (asset) {
return asset;
}
for await (const asset of this.assetsIterator({ startMonthGroup: monthGroup })) {
for await (const asset of this.assetsIterator({ startTimelineMonth: timelineMonth })) {
return asset;
}
}
@@ -622,8 +625,8 @@ export class TimelineManager extends VirtualScrollManager {
group.sortAssets(this.#options.order);
}
for (const monthGroup of context.bucketsWithNewTimelineDays) {
monthGroup.sortTimelineDays();
for (const timelineMonth of context.bucketsWithNewTimelineDays) {
timelineMonth.sortTimelineDays();
}
for (const month of context.updatedBuckets) {

View File

@@ -4,7 +4,7 @@ import { CancellableTask } from '$lib/utils/cancellable-task';
import { handleError } from '$lib/utils/handle-error';
import {
formatGroupTitle,
formatMonthGroupTitle,
formatTimelineMonthTitle,
fromTimelinePlainDate,
fromTimelinePlainDateTime,
fromTimelinePlainYearMonth,
@@ -29,7 +29,7 @@ import type { TimelineManager } from './timeline-manager.svelte';
import type { AssetDescriptor, Direction, MoveAsset, TimelineAsset } from './types';
import { ViewerAsset } from './viewer-asset.svelte';
export class MonthGroup {
export class TimelineMonth {
#viewportProximity: ViewportProximity = $state(ViewportProximity.FarFromViewport);
isLoaded: boolean = $state(false);
timelineDays: TimelineDay[] = $state([]);
@@ -50,7 +50,7 @@ export class MonthGroup {
loader: CancellableTask | undefined;
isHeightActual: boolean = $state(false);
readonly monthGroupTitle: string;
readonly title: string;
readonly yearMonth: TimelineYearMonth;
constructor(
@@ -65,7 +65,7 @@ export class MonthGroup {
this.#sortOrder = order;
this.yearMonth = { year: yearMonth.year, month: yearMonth.month };
this.monthGroupTitle = formatMonthGroupTitle(fromTimelinePlainYearMonth(yearMonth));
this.title = formatTimelineMonthTitle(fromTimelinePlainYearMonth(yearMonth));
this.loader = new CancellableTask(
() => {
@@ -89,7 +89,7 @@ export class MonthGroup {
}
this.#viewportProximity = newValue;
if (isInOrNearViewportUtil(newValue)) {
void this.timelineManager.loadMonthGroup(this.yearMonth);
void this.timelineManager.loadTimelineMonth(this.yearMonth);
} else {
this.cancel();
}
@@ -269,9 +269,9 @@ export class MonthGroup {
const index = timelineManager.months.indexOf(this);
const heightDelta = height - this.#height;
this.#height = height;
const prevMonthGroup = timelineManager.months[index - 1];
if (prevMonthGroup) {
const newTop = prevMonthGroup.#top + prevMonthGroup.#height;
const previousTimelineMonth = timelineManager.months[index - 1];
if (previousTimelineMonth) {
const newTop = previousTimelineMonth.#top + previousTimelineMonth.#height;
if (this.#top !== newTop) {
this.#top = newTop;
}
@@ -280,10 +280,10 @@ export class MonthGroup {
return;
}
for (let cursor = index + 1; cursor < timelineManager.months.length; cursor++) {
const monthGroup = this.timelineManager.months[cursor];
const newTop = monthGroup.#top + heightDelta;
if (monthGroup.#top !== newTop) {
monthGroup.#top = newTop;
const timelineMonth = this.timelineManager.months[cursor];
const newTop = timelineMonth.#top + heightDelta;
if (timelineMonth.#top !== newTop) {
timelineMonth.#top = newTop;
}
}
if (!timelineManager.viewportTopMonthIntersection) {

View File

@@ -16,7 +16,7 @@ export class ViewerAsset {
return ViewportProximity.FarFromViewport;
}
const store = this.#group.monthGroup.timelineManager;
const store = this.#group.timelineMonth.timelineManager;
const positionTop = this.#group.absoluteTimelineDayTop + this.position.top;
return calculateViewerAssetViewportProximity(store, positionTop, this.position.height);

View File

@@ -396,18 +396,18 @@ export const selectAllAssets = async (timelineManager: TimelineManager, assetInt
assetInteraction.selectAll = true;
try {
for (const monthGroup of timelineManager.months) {
if (!monthGroup.isLoaded) {
await timelineManager.loadMonthGroup(monthGroup.yearMonth);
for (const timelineMonth of timelineManager.months) {
if (!timelineMonth.isLoaded) {
await timelineManager.loadTimelineMonth(timelineMonth.yearMonth);
}
if (!assetInteraction.selectAll) {
assetInteraction.clear();
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);
}
}

View File

@@ -100,7 +100,7 @@ export const toISOYearMonthUTC = ({ year, month }: TimelineYearMonth): string =>
return `${yearFull}-${monthFull}-01T00:00:00.000Z`;
};
export function formatMonthGroupTitle(_date: DateTime): string {
export function formatTimelineMonthTitle(_date: DateTime): string {
if (!_date.isValid) {
return _date.toString();
}