mirror of
https://github.com/immich-app/immich.git
synced 2026-02-04 19:12:11 -08:00
Relocate/rename internal timeline functions to follow TimelineExtensions pattern
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
import HotModuleReload from '$lib/elements/HotModuleReload.svelte';
|
||||
import Portal from '$lib/elements/Portal.svelte';
|
||||
import Skeleton from '$lib/elements/Skeleton.svelte';
|
||||
import { isIntersecting } from '$lib/managers/VirtualScrollManager/ScrollSegment.svelte';
|
||||
import type { TimelineDay } from '$lib/managers/timeline-manager/TimelineDay.svelte';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/TimelineManager.svelte';
|
||||
import type { TimelineMonth } from '$lib/managers/timeline-manager/TimelineMonth.svelte';
|
||||
@@ -18,6 +19,7 @@
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { isAssetViewerRoute } from '$lib/utils/navigation';
|
||||
import { getSegmentIdentifier, getTimes, type ScrubberListener } from '$lib/utils/timeline-util';
|
||||
import { type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
@@ -128,13 +130,25 @@
|
||||
timelineManager.scrollableElement = scrollableElement;
|
||||
});
|
||||
|
||||
const getAssetPosition = (assetId: string, monthGroup: MonthGroup) => monthGroup.findAssetAbsolutePosition(assetId);
|
||||
const scrollToAssetPosition = (assetId: string, month: TimelineMonth) => {
|
||||
const position = month.findAssetAbsolutePosition(assetId);
|
||||
|
||||
const getAssetHeight = (assetId: string, month: TimelineMonth) => month.findAssetAbsolutePosition(assetId);
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assetIsVisible = (assetTop: number): boolean => {
|
||||
if (!scrollableElement) {
|
||||
return false;
|
||||
// Need to update window positions/intersections because <Portal> may have
|
||||
// gone from invisible to visible.
|
||||
timelineManager.updateVisibleWindow();
|
||||
|
||||
const assetTop = position.top;
|
||||
const assetBottom = position.top + position.height;
|
||||
const visibleTop = timelineManager.visibleWindow.top;
|
||||
const visibleBottom = timelineManager.visibleWindow.bottom;
|
||||
|
||||
// Check if the asset is already at least partially visible in the viewport
|
||||
if (isIntersecting(assetTop, assetBottom, visibleTop, visibleBottom)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTop = scrollableElement?.scrollTop || 0;
|
||||
@@ -159,33 +173,21 @@
|
||||
timelineManager.scrollTo(scrollTarget);
|
||||
};
|
||||
|
||||
const scrollToAssetId = async (assetId: string) => {
|
||||
const month = await timelineManager.search.getMonthForAsset(assetId);
|
||||
const scrollAndLoadAsset = async (assetId: string) => {
|
||||
const month = await timelineManager.findMonthForAsset(assetId);
|
||||
if (!month) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const height = getAssetHeight(assetId, month);
|
||||
|
||||
// If the asset is already visible, then don't scroll.
|
||||
if (assetIsVisible(height)) {
|
||||
// need to update window positions/intersections because since the <Portal>
|
||||
// went from invisible to visible
|
||||
timelineManager.updateVisibleWindow();
|
||||
return true;
|
||||
}
|
||||
|
||||
timelineManager.scrollTo(height);
|
||||
scrollToAssetPosition(assetId, month);
|
||||
return true;
|
||||
};
|
||||
|
||||
const scrollToAsset = (asset: TimelineAsset) => {
|
||||
const month = timelineManager.getSegmentForAssetId(asset.id) as TimelineMonth | undefined;
|
||||
const month = timelineManager.search.findMonthForAsset(asset.id)?.month;
|
||||
if (!month) {
|
||||
return false;
|
||||
}
|
||||
const height = getAssetHeight(asset.id, month);
|
||||
timelineManager.scrollTo(height);
|
||||
scrollToAssetPosition(asset.id, month);
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export abstract class ScrollSegment {
|
||||
|
||||
abstract get viewerAssets(): ViewerAsset[];
|
||||
|
||||
abstract findAssetAbsolutePosition(assetId: string): number;
|
||||
abstract findAssetAbsolutePosition(assetId: string): { top: number; height: number } | undefined;
|
||||
|
||||
protected abstract fetch(signal: AbortSignal): Promise<unknown>;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { onCreateTimelineDay } from '$lib/managers/timeline-manager/internal/TestHooks.svelte';
|
||||
import type { TimelineMonth } from '$lib/managers/timeline-manager/TimelineMonth.svelte';
|
||||
import { onCreateTimelineDay } from '$lib/managers/timeline-manager/TimelineTestHooks.svelte';
|
||||
import type { AssetOperation, Direction, TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
|
||||
import type { CommonLayoutOptions } from '$lib/utils/layout-utils';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { TimelineDay } from '$lib/managers/timeline-manager/TimelineDay.svelte';
|
||||
import type { TimelineMonth } from '$lib/managers/timeline-manager/TimelineMonth.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { setDifference, type TimelineDate } from '$lib/utils/timeline-util';
|
||||
import { setDifference } from '$lib/managers/timeline-manager/utils.svelte';
|
||||
import { type TimelineDate } from '$lib/utils/timeline-util';
|
||||
import { AssetOrder } from '@immich/sdk';
|
||||
|
||||
export class GroupInsertionCache {
|
||||
@@ -1,9 +1,8 @@
|
||||
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
||||
import { getMonthByDate } from '$lib/managers/timeline-manager/internal/search-support.svelte';
|
||||
import { setTestHooks } from '$lib/managers/timeline-manager/internal/TestHooks.svelte';
|
||||
import type { TimelineDay } from '$lib/managers/timeline-manager/TimelineDay.svelte';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/TimelineManager.svelte';
|
||||
import type { TimelineMonth } from '$lib/managers/timeline-manager/TimelineMonth.svelte';
|
||||
import { setTestHooks } from '$lib/managers/timeline-manager/TimelineTestHooks.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { AbortError } from '$lib/utils';
|
||||
import { fromISODateTimeUTCToObject, getSegmentIdentifier } from '$lib/utils/timeline-util';
|
||||
@@ -134,10 +133,10 @@ describe('TimelineManager', () => {
|
||||
});
|
||||
|
||||
it('loads a month', async () => {
|
||||
expect(getMonthByDate(timelineManager, { year: 2024, month: 1 })?.assets.length).toEqual(0);
|
||||
expect(timelineManager.search.findMonthByDate({ year: 2024, month: 1 })?.assets.length).toEqual(0);
|
||||
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
||||
expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
|
||||
expect(getMonthByDate(timelineManager, { year: 2024, month: 1 })?.assets.length).toEqual(3);
|
||||
expect(timelineManager.search.findMonthByDate({ year: 2024, month: 1 })?.assets.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('ignores invalid months', async () => {
|
||||
@@ -146,13 +145,13 @@ describe('TimelineManager', () => {
|
||||
});
|
||||
|
||||
it('cancels month loading', async () => {
|
||||
const month = getMonthByDate(timelineManager, { year: 2024, month: 1 })!;
|
||||
const month = timelineManager.search.findMonthByDate({ year: 2024, month: 1 })!;
|
||||
void timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
||||
const abortSpy = vi.spyOn(month!.loader!.cancelToken!, 'abort');
|
||||
month?.cancel();
|
||||
expect(abortSpy).toBeCalledTimes(1);
|
||||
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
||||
expect(getMonthByDate(timelineManager, { year: 2024, month: 1 })?.assets.length).toEqual(3);
|
||||
expect(timelineManager.search.findMonthByDate({ year: 2024, month: 1 })?.assets.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('prevents loading months multiple times', async () => {
|
||||
@@ -167,7 +166,7 @@ describe('TimelineManager', () => {
|
||||
});
|
||||
|
||||
it('allows loading a canceled month', async () => {
|
||||
const month = getMonthByDate(timelineManager, { year: 2024, month: 1 })!;
|
||||
const month = timelineManager.search.findMonthByDate({ year: 2024, month: 1 })!;
|
||||
const loadPromise = timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
||||
|
||||
month.cancel();
|
||||
@@ -244,7 +243,7 @@ describe('TimelineManager', () => {
|
||||
);
|
||||
timelineManager.upsertAssets([assetOne, assetTwo, assetThree]);
|
||||
|
||||
const month = getMonthByDate(timelineManager, { year: 2024, month: 1 });
|
||||
const month = timelineManager.search.findMonthByDate({ year: 2024, month: 1 });
|
||||
expect(month).not.toBeNull();
|
||||
expect(month?.assets.length).toEqual(3);
|
||||
expect(month?.assets[0].id).toEqual(assetOne.id);
|
||||
@@ -455,15 +454,15 @@ describe('TimelineManager', () => {
|
||||
|
||||
timelineManager.upsertAssets([asset]);
|
||||
expect(timelineManager.segments.length).toEqual(1);
|
||||
expect(getMonthByDate(timelineManager, { year: 2024, month: 1 })).not.toBeUndefined();
|
||||
expect(getMonthByDate(timelineManager, { year: 2024, month: 1 })?.assets.length).toEqual(1);
|
||||
expect(timelineManager.search.findMonthByDate({ year: 2024, month: 1 })).not.toBeUndefined();
|
||||
expect(timelineManager.search.findMonthByDate({ year: 2024, month: 1 })?.assets.length).toEqual(1);
|
||||
|
||||
timelineManager.upsertAssets([updatedAsset]);
|
||||
expect(timelineManager.segments.length).toEqual(2);
|
||||
expect(getMonthByDate(timelineManager, { year: 2024, month: 1 })).not.toBeUndefined();
|
||||
expect(getMonthByDate(timelineManager, { year: 2024, month: 1 })?.assets.length).toEqual(0);
|
||||
expect(getMonthByDate(timelineManager, { year: 2024, month: 3 })).not.toBeUndefined();
|
||||
expect(getMonthByDate(timelineManager, { year: 2024, month: 3 })?.assets.length).toEqual(1);
|
||||
expect(timelineManager.search.findMonthByDate({ year: 2024, month: 1 })).not.toBeUndefined();
|
||||
expect(timelineManager.search.findMonthByDate({ year: 2024, month: 1 })?.assets.length).toEqual(0);
|
||||
expect(timelineManager.search.findMonthByDate({ year: 2024, month: 3 })).not.toBeUndefined();
|
||||
expect(timelineManager.search.findMonthByDate({ year: 2024, month: 3 })?.assets.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -593,7 +592,7 @@ describe('TimelineManager', () => {
|
||||
|
||||
it('returns previous assetId', async () => {
|
||||
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
||||
const month = getMonthByDate(timelineManager, { year: 2024, month: 1 });
|
||||
const month = timelineManager.search.findMonthByDate({ year: 2024, month: 1 });
|
||||
|
||||
const a = month!.assets[0];
|
||||
const b = month!.assets[1];
|
||||
@@ -605,8 +604,8 @@ describe('TimelineManager', () => {
|
||||
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 2 }));
|
||||
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 3 }));
|
||||
|
||||
const month = getMonthByDate(timelineManager, { year: 2024, month: 2 });
|
||||
const previousMonth = getMonthByDate(timelineManager, { year: 2024, month: 3 });
|
||||
const month = timelineManager.search.findMonthByDate({ year: 2024, month: 2 });
|
||||
const previousMonth = timelineManager.search.findMonthByDate({ year: 2024, month: 3 });
|
||||
const a = month!.assets[0];
|
||||
const b = previousMonth!.assets[0];
|
||||
const previous = await timelineManager.getLaterAsset(a);
|
||||
@@ -615,8 +614,8 @@ describe('TimelineManager', () => {
|
||||
|
||||
it('loads previous month', async () => {
|
||||
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 2 }));
|
||||
const month = getMonthByDate(timelineManager, { year: 2024, month: 2 });
|
||||
const previousMonth = getMonthByDate(timelineManager, { year: 2024, month: 3 });
|
||||
const month = timelineManager.search.findMonthByDate({ year: 2024, month: 2 });
|
||||
const previousMonth = timelineManager.search.findMonthByDate({ year: 2024, month: 3 });
|
||||
const a = month!.getFirstAsset();
|
||||
const b = previousMonth!.getFirstAsset();
|
||||
const loadmonthSpy = vi.spyOn(month!.loader!, 'execute');
|
||||
@@ -654,8 +653,8 @@ describe('TimelineManager', () => {
|
||||
});
|
||||
|
||||
it('returns null for invalid months', () => {
|
||||
expect(getMonthByDate(timelineManager, { year: -1, month: -1 })).toBeUndefined();
|
||||
expect(getMonthByDate(timelineManager, { year: 2024, month: 3 })).toBeUndefined();
|
||||
expect(timelineManager.search.findMonthByDate({ year: -1, month: -1 })).toBeUndefined();
|
||||
expect(timelineManager.search.findMonthByDate({ year: 2024, month: 3 })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns the month index', () => {
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import type { TimelineDay } from '$lib/managers/timeline-manager/TimelineDay.svelte';
|
||||
import { GroupInsertionCache } from '$lib/managers/timeline-manager/TimelineInsertionCache.svelte';
|
||||
import { TimelineMonth } from '$lib/managers/timeline-manager/TimelineMonth.svelte';
|
||||
import { GroupInsertionCache } from '$lib/managers/timeline-manager/group-insertion-cache.svelte';
|
||||
import {
|
||||
findClosestGroupForDate,
|
||||
findMonthForAsset as findMonthForAssetUtil,
|
||||
findMonthForDate,
|
||||
getAssetWithOffset,
|
||||
getMonthByDate,
|
||||
retrieveRange as retrieveRangeUtil,
|
||||
} from '$lib/managers/timeline-manager/internal/search-support.svelte';
|
||||
import { isMismatched, updateObject } from '$lib/managers/timeline-manager/internal/utils.svelte';
|
||||
import { WebsocketSupport } from '$lib/managers/timeline-manager/internal/websocket-support.svelte';
|
||||
import { TimelineSearchExtension } from '$lib/managers/timeline-manager/TimelineSearchExtension.svelte';
|
||||
import { TimelineWebsocketExtension } from '$lib/managers/timeline-manager/TimelineWebsocketExtension';
|
||||
import type {
|
||||
AssetDescriptor,
|
||||
AssetOperation,
|
||||
@@ -22,10 +14,10 @@ import type {
|
||||
TimelineManagerOptions,
|
||||
Viewport,
|
||||
} from '$lib/managers/timeline-manager/types';
|
||||
import { isMismatched, setDifferenceInPlace, updateObject } from '$lib/managers/timeline-manager/utils.svelte';
|
||||
import { CancellableTask } from '$lib/utils/cancellable-task';
|
||||
import {
|
||||
getSegmentIdentifier,
|
||||
setDifferenceInPlace,
|
||||
toTimelineAsset,
|
||||
type TimelineDateTime,
|
||||
type TimelineYearMonth,
|
||||
@@ -36,14 +28,10 @@ import { SvelteDate, SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
export class TimelineManager extends VirtualScrollManager {
|
||||
override bottomSectionHeight = $state(60);
|
||||
|
||||
segments: TimelineMonth[] = $state([]);
|
||||
albumAssets: Set<string> = new SvelteSet();
|
||||
scrubberMonths: ScrubberMonth[] = $state([]);
|
||||
scrubberTimelineHeight: number = $state(0);
|
||||
|
||||
limitedScroll = $derived(this.maxScrollPercent < 0.5);
|
||||
initTask = new CancellableTask(
|
||||
readonly search = new TimelineSearchExtension(this);
|
||||
readonly albumAssets: Set<string> = new SvelteSet();
|
||||
readonly limitedScroll = $derived(this.maxScrollPercent < 0.5);
|
||||
readonly initTask = new CancellableTask(
|
||||
() => {
|
||||
this.isInitialized = true;
|
||||
if (this.#options.albumId || this.#options.personId) {
|
||||
@@ -58,10 +46,12 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
() => void 0,
|
||||
);
|
||||
|
||||
static #INIT_OPTIONS = {};
|
||||
#websocketSupport: WebsocketSupport | undefined;
|
||||
#options: TimelineManagerOptions = TimelineManager.#INIT_OPTIONS;
|
||||
segments: TimelineMonth[] = $state([]);
|
||||
scrubberMonths: ScrubberMonth[] = $state([]);
|
||||
scrubberTimelineHeight: number = $state(0);
|
||||
|
||||
#websocketSupport: TimelineWebsocketExtension | undefined;
|
||||
#options: TimelineManagerOptions = {};
|
||||
#scrollableElement: HTMLElement | undefined = $state();
|
||||
|
||||
constructor() {
|
||||
@@ -106,7 +96,7 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
if (options.deferInit) {
|
||||
return;
|
||||
}
|
||||
if (this.#options !== TimelineManager.#INIT_OPTIONS && isEqual(this.#options, options)) {
|
||||
if (isEqual(this.#options, options)) {
|
||||
return;
|
||||
}
|
||||
await this.initTask.reset();
|
||||
@@ -141,7 +131,7 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
if (this.#websocketSupport) {
|
||||
throw new Error('TimelineManager already connected');
|
||||
}
|
||||
this.#websocketSupport = new WebsocketSupport(this);
|
||||
this.#websocketSupport = new TimelineWebsocketExtension(this);
|
||||
this.#websocketSupport.connectWebsocketEvents();
|
||||
}
|
||||
|
||||
@@ -164,7 +154,7 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
await this.initTask.waitUntilCompletion();
|
||||
}
|
||||
|
||||
let { month } = findMonthForAssetUtil(this, id) ?? {};
|
||||
let { month } = this.search.findMonthForAsset(id) ?? {};
|
||||
if (month) {
|
||||
return month;
|
||||
}
|
||||
@@ -187,11 +177,11 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
|
||||
async #loadMonthAtTime(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }) {
|
||||
await this.loadSegment(getSegmentIdentifier(yearMonth), options);
|
||||
return getMonthByDate(this, yearMonth);
|
||||
return this.search.findMonthByDate(yearMonth);
|
||||
}
|
||||
|
||||
getMonthByAssetId(assetId: string) {
|
||||
const monthInfo = findMonthForAssetUtil(this, assetId);
|
||||
const monthInfo = this.search.findMonthForAsset(assetId);
|
||||
return monthInfo?.month;
|
||||
}
|
||||
|
||||
@@ -274,7 +264,7 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
}
|
||||
|
||||
protected upsertAssetIntoSegment(asset: TimelineAsset, context: GroupInsertionCache): void {
|
||||
let month = getMonthByDate(this, asset.localDateTime);
|
||||
let month = this.search.findMonthByDate(asset.localDateTime);
|
||||
|
||||
if (!month) {
|
||||
month = new TimelineMonth(this, asset.localDateTime, 1, true, this.#options.order);
|
||||
@@ -361,20 +351,20 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
assetDescriptor: AssetDescriptor,
|
||||
interval: 'asset' | 'day' | 'month' | 'year' = 'asset',
|
||||
): Promise<TimelineAsset | undefined> {
|
||||
return await getAssetWithOffset(this, assetDescriptor, interval, 'later');
|
||||
return await this.search.getAssetWithOffset(assetDescriptor, interval, 'later');
|
||||
}
|
||||
|
||||
async getEarlierAsset(
|
||||
assetDescriptor: AssetDescriptor,
|
||||
interval: 'asset' | 'day' | 'month' | 'year' = 'asset',
|
||||
): Promise<TimelineAsset | undefined> {
|
||||
return await getAssetWithOffset(this, assetDescriptor, interval, 'earlier');
|
||||
return await this.search.getAssetWithOffset(assetDescriptor, interval, 'earlier');
|
||||
}
|
||||
|
||||
async getClosestAssetToDate(dateTime: TimelineDateTime) {
|
||||
let month = findMonthForDate(this, dateTime);
|
||||
let month = this.search.findMonthForDate(dateTime);
|
||||
if (!month) {
|
||||
month = findClosestGroupForDate(this.segments, dateTime);
|
||||
month = this.search.findClosestGroupForDate(this.segments, dateTime);
|
||||
if (!month) {
|
||||
return;
|
||||
}
|
||||
@@ -390,17 +380,7 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
}
|
||||
|
||||
async retrieveRange(start: AssetDescriptor, end: AssetDescriptor) {
|
||||
return retrieveRangeUtil(this, start, end);
|
||||
}
|
||||
|
||||
clearDeferredLayout(month: TimelineMonth) {
|
||||
const hasDeferred = month.days.some((group) => group.deferredLayout);
|
||||
if (hasDeferred) {
|
||||
month.updateGeometry({ invalidateHeight: true, noDefer: true });
|
||||
for (const group of month.days) {
|
||||
group.deferredLayout = false;
|
||||
}
|
||||
}
|
||||
return this.search.retrieveRange(start, end);
|
||||
}
|
||||
|
||||
async *assetsIterator(options?: {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { GroupInsertionCache } from '$lib/managers/timeline-manager/group-insertion-cache.svelte';
|
||||
import { onCreateTimelineMonth } from '$lib/managers/timeline-manager/internal/TestHooks.svelte';
|
||||
import { TimelineDay } from '$lib/managers/timeline-manager/TimelineDay.svelte';
|
||||
import { GroupInsertionCache } from '$lib/managers/timeline-manager/TimelineInsertionCache.svelte';
|
||||
import type { TimelineManager } from '$lib/managers/timeline-manager/TimelineManager.svelte';
|
||||
import { onCreateTimelineMonth } from '$lib/managers/timeline-manager/TimelineTestHooks.svelte';
|
||||
import type { AssetDescriptor, AssetOperation, Direction, TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { setDifferenceInPlace } from '$lib/managers/timeline-manager/utils.svelte';
|
||||
import { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
|
||||
import { ScrollSegment, type SegmentIdentifier } from '$lib/managers/VirtualScrollManager/ScrollSegment.svelte';
|
||||
import {
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
fromTimelinePlainYearMonth,
|
||||
getSegmentIdentifier,
|
||||
getTimes,
|
||||
setDifferenceInPlace,
|
||||
toISOYearMonthUTC,
|
||||
type TimelineDateTime,
|
||||
type TimelineYearMonth,
|
||||
@@ -70,17 +70,19 @@ export class TimelineMonth extends ScrollSegment {
|
||||
|
||||
findAssetAbsolutePosition(assetId: string) {
|
||||
this.#clearDeferredLayout();
|
||||
for (const group of this.days) {
|
||||
const viewerAsset = group.viewerAssets.find((viewAsset) => viewAsset.id === assetId);
|
||||
for (const day of this.days) {
|
||||
const viewerAsset = day.viewerAssets.find((viewAsset) => viewAsset.id === assetId);
|
||||
if (viewerAsset) {
|
||||
if (!viewerAsset.position) {
|
||||
console.warn('No position for asset');
|
||||
break;
|
||||
return;
|
||||
}
|
||||
return this.top + group.top + viewerAsset.position.top + this.scrollManager.headerHeight;
|
||||
return {
|
||||
top: this.top + day.top + viewerAsset.position.top + this.#timelineManager.headerHeight,
|
||||
height: viewerAsset.position.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
protected async fetch(signal: AbortSignal): Promise<unknown> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/TimelineManager.svelte';
|
||||
import type { TimelineMonth } from '$lib/managers/timeline-manager/TimelineMonth.svelte';
|
||||
import { findClosestGroupForDate } from '$lib/managers/timeline-manager/internal/search-support.svelte';
|
||||
import { TimelineSearchExtension } from '$lib/managers/timeline-manager/TimelineSearchExtension.svelte';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
function createMockMonthGroup(year: number, month: number): TimelineMonth {
|
||||
@@ -9,32 +10,36 @@ function createMockMonthGroup(year: number, month: number): TimelineMonth {
|
||||
}
|
||||
|
||||
describe('findClosestGroupForDate', () => {
|
||||
let search: TimelineSearchExtension;
|
||||
beforeEach(() => {
|
||||
search = new TimelineSearchExtension(new TimelineManager());
|
||||
});
|
||||
it('should return undefined for empty months array', () => {
|
||||
const result = findClosestGroupForDate([], { year: 2024, month: 1 });
|
||||
const result = search.findClosestGroupForDate([], { 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 result = search.findClosestGroupForDate(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 result = search.findClosestGroupForDate(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 result = search.findClosestGroupForDate(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 result = search.findClosestGroupForDate(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 });
|
||||
@@ -42,33 +47,33 @@ describe('findClosestGroupForDate', () => {
|
||||
|
||||
it('should correctly calculate distance across years', () => {
|
||||
const months = [createMockMonthGroup(2022, 6), createMockMonthGroup(2024, 6)];
|
||||
const result = findClosestGroupForDate(months, { year: 2023, month: 6 });
|
||||
const result = search.findClosestGroupForDate(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 result = search.findClosestGroupForDate(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 result = search.findClosestGroupForDate(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 result = search.findClosestGroupForDate(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 result = search.findClosestGroupForDate(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 });
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
import type { TimelineManager } from '$lib/managers/timeline-manager/TimelineManager.svelte';
|
||||
import type { TimelineMonth } from '$lib/managers/timeline-manager/TimelineMonth.svelte';
|
||||
import type { AssetDescriptor, Direction, TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { plainDateTimeCompare, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||
import { AssetOrder } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
export class TimelineSearchExtension {
|
||||
#timelineManager: TimelineManager;
|
||||
constructor(timelineManager: TimelineManager) {
|
||||
this.#timelineManager = timelineManager;
|
||||
}
|
||||
async getAssetWithOffset(
|
||||
assetDescriptor: AssetDescriptor,
|
||||
interval: 'asset' | 'day' | 'month' | 'year' = 'asset',
|
||||
direction: Direction,
|
||||
): Promise<TimelineAsset | undefined> {
|
||||
const { asset, month } = this.findMonthForAsset(assetDescriptor.id) ?? {};
|
||||
if (!month || !asset) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (interval) {
|
||||
case 'asset': {
|
||||
return this.getAssetByAssetOffset(asset, month, direction);
|
||||
}
|
||||
case 'day': {
|
||||
return this.getAssetByDayOffset(asset, month, direction);
|
||||
}
|
||||
case 'month': {
|
||||
return this.getAssetByMonthOffset(month, direction);
|
||||
}
|
||||
case 'year': {
|
||||
return this.getAssetByYearOffset(month, direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findMonthForAsset(id: string) {
|
||||
for (const month of this.#timelineManager.segments) {
|
||||
const asset = month.findAssetById({ id });
|
||||
if (asset) {
|
||||
return { month, asset };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findMonthByDate(targetYearMonth: TimelineYearMonth): TimelineMonth | undefined {
|
||||
return this.#timelineManager.segments.find(
|
||||
(month) => month.yearMonth.year === targetYearMonth.year && month.yearMonth.month === targetYearMonth.month,
|
||||
);
|
||||
}
|
||||
|
||||
async getAssetByAssetOffset(asset: TimelineAsset, month: TimelineMonth, direction: Direction) {
|
||||
const day = month.findDayForAsset(asset);
|
||||
for await (const targetAsset of this.#timelineManager.assetsIterator({
|
||||
startMonth: month,
|
||||
startDay: day,
|
||||
startAsset: asset,
|
||||
direction,
|
||||
})) {
|
||||
if (asset.id !== targetAsset.id) {
|
||||
return targetAsset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getAssetByDayOffset(asset: TimelineAsset, month: TimelineMonth, direction: Direction) {
|
||||
const day = month.findDayForAsset(asset);
|
||||
for await (const targetAsset of this.#timelineManager.assetsIterator({
|
||||
startMonth: month,
|
||||
startDay: day,
|
||||
startAsset: asset,
|
||||
direction,
|
||||
})) {
|
||||
if (targetAsset.localDateTime.day !== asset.localDateTime.day) {
|
||||
return targetAsset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getAssetByMonthOffset(month: TimelineMonth, direction: Direction) {
|
||||
for (const targetMonth of this.#timelineManager.monthIterator({ startMonth: month, direction })) {
|
||||
if (targetMonth.yearMonth.month !== month.yearMonth.month) {
|
||||
const { value, done } = await this.#timelineManager
|
||||
.assetsIterator({ startMonth: targetMonth, direction })
|
||||
.next();
|
||||
return done ? undefined : value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getAssetByYearOffset(month: TimelineMonth, direction: Direction) {
|
||||
for (const targetMonth of this.#timelineManager.monthIterator({ startMonth: month, direction })) {
|
||||
if (targetMonth.yearMonth.year !== month.yearMonth.year) {
|
||||
const { value, done } = await this.#timelineManager
|
||||
.assetsIterator({ startMonth: targetMonth, direction })
|
||||
.next();
|
||||
return done ? undefined : value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async retrieveRange(start: AssetDescriptor, end: AssetDescriptor) {
|
||||
let { asset: startAsset, month: startMonth } = this.findMonthForAsset(start.id) ?? {};
|
||||
if (!startMonth || !startAsset) {
|
||||
return [];
|
||||
}
|
||||
let { asset: endAsset, month: endMonth } = this.findMonthForAsset(end.id) ?? {};
|
||||
if (!endMonth || !endAsset) {
|
||||
return [];
|
||||
}
|
||||
const assetOrder: AssetOrder = this.#timelineManager.getAssetOrder();
|
||||
if (plainDateTimeCompare(assetOrder === AssetOrder.Desc, startAsset.localDateTime, endAsset.localDateTime) < 0) {
|
||||
[startAsset, endAsset] = [endAsset, startAsset];
|
||||
[startMonth, endMonth] = [endMonth, startMonth];
|
||||
}
|
||||
|
||||
const range: TimelineAsset[] = [];
|
||||
const startDay = startMonth.findDayForAsset(startAsset);
|
||||
for await (const targetAsset of this.#timelineManager.assetsIterator({
|
||||
startMonth,
|
||||
startDay,
|
||||
startAsset,
|
||||
})) {
|
||||
range.push(targetAsset);
|
||||
if (targetAsset.id === endAsset.id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return range;
|
||||
}
|
||||
|
||||
findMonthForDate(targetYearMonth: TimelineYearMonth) {
|
||||
for (const month of this.#timelineManager.segments) {
|
||||
const { year, month: monthNum } = month.yearMonth;
|
||||
if (monthNum === targetYearMonth.month && year === targetYearMonth.year) {
|
||||
return month;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findClosestGroupForDate(months: TimelineMonth[], targetYearMonth: TimelineYearMonth) {
|
||||
const targetDate = DateTime.fromObject({ year: targetYearMonth.year, month: targetYearMonth.month });
|
||||
|
||||
let closestMonth: TimelineMonth | undefined;
|
||||
let minDifference = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
for (const month of months) {
|
||||
const monthDate = DateTime.fromObject({ year: month.yearMonth.year, month: month.yearMonth.month });
|
||||
const totalDiff = Math.abs(monthDate.diff(targetDate, 'months').months);
|
||||
|
||||
if (totalDiff < minDifference) {
|
||||
minDifference = totalDiff;
|
||||
closestMonth = month;
|
||||
}
|
||||
}
|
||||
|
||||
return closestMonth;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { TimelineDay } from '$lib/managers/timeline-manager/TimelineDay.svelte';
|
||||
import type { TimelineMonth } from '$lib/managers/timeline-manager/TimelineMonth.svelte';
|
||||
|
||||
let testHooks: TestHooks | undefined = undefined;
|
||||
|
||||
export type TestHooks = {
|
||||
onCreateTimelineMonth(month: TimelineMonth): unknown;
|
||||
onCreateTimelineDay(day: TimelineDay): unknown;
|
||||
};
|
||||
|
||||
let testHooks: TestHooks | undefined = undefined;
|
||||
|
||||
export const setTestHooks = (hooks: TestHooks) => {
|
||||
testHooks = hooks;
|
||||
};
|
||||
@@ -5,12 +5,9 @@ import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { throttle } from 'lodash-es';
|
||||
import type { Unsubscriber } from 'svelte/store';
|
||||
|
||||
export class WebsocketSupport {
|
||||
#pendingChanges: PendingChange[] = [];
|
||||
#unsubscribers: Unsubscriber[] = [];
|
||||
#timelineManager: TimelineManager;
|
||||
|
||||
#processPendingChanges = throttle(() => {
|
||||
export class TimelineWebsocketExtension {
|
||||
readonly #timelineManager: TimelineManager;
|
||||
readonly #processPendingChanges = throttle(() => {
|
||||
const { add, update, remove } = this.#getPendingChangeBatches();
|
||||
if (add.length > 0) {
|
||||
this.#timelineManager.upsertAssets(add);
|
||||
@@ -24,6 +21,9 @@ export class WebsocketSupport {
|
||||
this.#pendingChanges = [];
|
||||
}, 2500);
|
||||
|
||||
#pendingChanges: PendingChange[] = [];
|
||||
#unsubscribers: Unsubscriber[] = [];
|
||||
|
||||
constructor(timeineManager: TimelineManager) {
|
||||
this.#timelineManager = timeineManager;
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import type { TimelineManager } from '$lib/managers/timeline-manager/TimelineManager.svelte';
|
||||
import type { TimelineMonth } from '$lib/managers/timeline-manager/TimelineMonth.svelte';
|
||||
import type { AssetDescriptor, Direction, TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { plainDateTimeCompare, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||
import { AssetOrder } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
export async function getAssetWithOffset(
|
||||
timelineManager: TimelineManager,
|
||||
assetDescriptor: AssetDescriptor,
|
||||
interval: 'asset' | 'day' | 'month' | 'year' = 'asset',
|
||||
direction: Direction,
|
||||
): Promise<TimelineAsset | undefined> {
|
||||
const { asset, month } = findMonthForAsset(timelineManager, assetDescriptor.id) ?? {};
|
||||
if (!month || !asset) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (interval) {
|
||||
case 'asset': {
|
||||
return getAssetByAssetOffset(timelineManager, asset, month, direction);
|
||||
}
|
||||
case 'day': {
|
||||
return getAssetByDayOffset(timelineManager, asset, month, direction);
|
||||
}
|
||||
case 'month': {
|
||||
return getAssetByMonthOffset(timelineManager, month, direction);
|
||||
}
|
||||
case 'year': {
|
||||
return getAssetByYearOffset(timelineManager, month, direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function findMonthForAsset(timelineManager: TimelineManager, id: string) {
|
||||
for (const month of timelineManager.segments) {
|
||||
const asset = month.findAssetById({ id });
|
||||
if (asset) {
|
||||
return { month, asset };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getMonthByDate(
|
||||
timelineManager: TimelineManager,
|
||||
targetYearMonth: TimelineYearMonth,
|
||||
): TimelineMonth | undefined {
|
||||
return timelineManager.segments.find(
|
||||
(month) => month.yearMonth.year === targetYearMonth.year && month.yearMonth.month === targetYearMonth.month,
|
||||
);
|
||||
}
|
||||
|
||||
async function getAssetByAssetOffset(
|
||||
timelineManager: TimelineManager,
|
||||
asset: TimelineAsset,
|
||||
month: TimelineMonth,
|
||||
direction: Direction,
|
||||
) {
|
||||
const day = month.findDayForAsset(asset);
|
||||
for await (const targetAsset of timelineManager.assetsIterator({
|
||||
startMonth: month,
|
||||
startDay: day,
|
||||
startAsset: asset,
|
||||
direction,
|
||||
})) {
|
||||
if (asset.id !== targetAsset.id) {
|
||||
return targetAsset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getAssetByDayOffset(
|
||||
timelineManager: TimelineManager,
|
||||
asset: TimelineAsset,
|
||||
month: TimelineMonth,
|
||||
direction: Direction,
|
||||
) {
|
||||
const day = month.findDayForAsset(asset);
|
||||
for await (const targetAsset of timelineManager.assetsIterator({
|
||||
startMonth: month,
|
||||
startDay: day,
|
||||
startAsset: asset,
|
||||
direction,
|
||||
})) {
|
||||
if (targetAsset.localDateTime.day !== asset.localDateTime.day) {
|
||||
return targetAsset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getAssetByMonthOffset(timelineManager: TimelineManager, month: TimelineMonth, direction: Direction) {
|
||||
for (const targetMonth of timelineManager.monthIterator({ startMonth: month, direction })) {
|
||||
if (targetMonth.yearMonth.month !== month.yearMonth.month) {
|
||||
const { value, done } = await timelineManager.assetsIterator({ startMonth: targetMonth, direction }).next();
|
||||
return done ? undefined : value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getAssetByYearOffset(timelineManager: TimelineManager, month: TimelineMonth, direction: Direction) {
|
||||
for (const targetMonth of timelineManager.monthIterator({ startMonth: month, direction })) {
|
||||
if (targetMonth.yearMonth.year !== month.yearMonth.year) {
|
||||
const { value, done } = await timelineManager.assetsIterator({ startMonth: targetMonth, direction }).next();
|
||||
return done ? undefined : value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function retrieveRange(timelineManager: TimelineManager, start: AssetDescriptor, end: AssetDescriptor) {
|
||||
let { asset: startAsset, month: startMonth } = findMonthForAsset(timelineManager, start.id) ?? {};
|
||||
if (!startMonth || !startAsset) {
|
||||
return [];
|
||||
}
|
||||
let { asset: endAsset, month: endMonth } = findMonthForAsset(timelineManager, end.id) ?? {};
|
||||
if (!endMonth || !endAsset) {
|
||||
return [];
|
||||
}
|
||||
const assetOrder: AssetOrder = timelineManager.getAssetOrder();
|
||||
if (plainDateTimeCompare(assetOrder === AssetOrder.Desc, startAsset.localDateTime, endAsset.localDateTime) < 0) {
|
||||
[startAsset, endAsset] = [endAsset, startAsset];
|
||||
[startMonth, endMonth] = [endMonth, startMonth];
|
||||
}
|
||||
|
||||
const range: TimelineAsset[] = [];
|
||||
const startDay = startMonth.findDayForAsset(startAsset);
|
||||
for await (const targetAsset of timelineManager.assetsIterator({
|
||||
startMonth,
|
||||
startDay,
|
||||
startAsset,
|
||||
})) {
|
||||
range.push(targetAsset);
|
||||
if (targetAsset.id === endAsset.id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return range;
|
||||
}
|
||||
|
||||
export function findMonthForDate(timelineManager: TimelineManager, targetYearMonth: TimelineYearMonth) {
|
||||
for (const month of timelineManager.segments) {
|
||||
const { year, month: monthNum } = month.yearMonth;
|
||||
if (monthNum === targetYearMonth.month && year === targetYearMonth.year) {
|
||||
return month;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function findClosestGroupForDate(months: TimelineMonth[], targetYearMonth: TimelineYearMonth) {
|
||||
const targetDate = DateTime.fromObject({ year: targetYearMonth.year, month: targetYearMonth.month });
|
||||
|
||||
let closestMonth: TimelineMonth | undefined;
|
||||
let minDifference = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
for (const month of months) {
|
||||
const monthDate = DateTime.fromObject({ year: month.yearMonth.year, month: month.yearMonth.month });
|
||||
const totalDiff = Math.abs(monthDate.diff(targetDate, 'months').months);
|
||||
|
||||
if (totalDiff < minDifference) {
|
||||
minDifference = totalDiff;
|
||||
closestMonth = month;
|
||||
}
|
||||
}
|
||||
|
||||
return closestMonth;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function updateObject(target: any, source: any): boolean {
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
let updated = false;
|
||||
for (const key in source) {
|
||||
if (!Object.prototype.hasOwnProperty.call(source, key)) {
|
||||
continue;
|
||||
}
|
||||
if (key === '__proto__' || key === 'constructor') {
|
||||
continue;
|
||||
}
|
||||
const isDate = target[key] instanceof Date;
|
||||
if (typeof target[key] === 'object' && !isDate) {
|
||||
updated = updated || updateObject(target[key], source[key]);
|
||||
} else {
|
||||
if (target[key] !== source[key]) {
|
||||
target[key] = source[key];
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
export function isMismatched<T>(option: T | undefined, value: T): boolean {
|
||||
return option === undefined ? false : option !== value;
|
||||
}
|
||||
@@ -2,3 +2,58 @@ import type { TimelineAsset } from './types';
|
||||
|
||||
export const assetSnapshot = (asset: TimelineAsset): TimelineAsset => $state.snapshot(asset);
|
||||
export const assetsSnapshot = (assets: TimelineAsset[]) => assets.map((asset) => $state.snapshot(asset));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function updateObject(target: any, source: any): boolean {
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
let updated = false;
|
||||
for (const key in source) {
|
||||
if (!Object.prototype.hasOwnProperty.call(source, key)) {
|
||||
continue;
|
||||
}
|
||||
if (key === '__proto__' || key === 'constructor') {
|
||||
continue;
|
||||
}
|
||||
const isDate = target[key] instanceof Date;
|
||||
if (typeof target[key] === 'object' && !isDate) {
|
||||
updated = updated || updateObject(target[key], source[key]);
|
||||
} else {
|
||||
if (target[key] !== source[key]) {
|
||||
target[key] = source[key];
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
export function isMismatched<T>(option: T | undefined, value: T): boolean {
|
||||
return option === undefined ? false : option !== value;
|
||||
}
|
||||
|
||||
export function setDifference<T>(setA: Set<T>, setB: Set<T>): Set<T> {
|
||||
// Check if native Set.prototype.difference is available (ES2025)
|
||||
const setWithDifference = setA as unknown as Set<T> & { difference?: (other: Set<T>) => Set<T> };
|
||||
if (setWithDifference.difference && typeof setWithDifference.difference === 'function') {
|
||||
return setWithDifference.difference(setB);
|
||||
}
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const result = new Set<T>();
|
||||
for (const value of setA) {
|
||||
if (!setB.has(value)) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all elements of setB from setA in-place (mutates setA).
|
||||
*/
|
||||
export function setDifferenceInPlace<T>(setA: Set<T>, setB: Set<T>): Set<T> {
|
||||
for (const value of setB) {
|
||||
setA.delete(value);
|
||||
}
|
||||
return setA;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user