Review comments, minus renames
parent
fb5a0089af
commit
14dad831ee
|
|
@ -11,7 +11,7 @@
|
|||
import Skeleton from '$lib/elements/Skeleton.svelte';
|
||||
import type { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
|
||||
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import { getSegmentIdentifier, TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
|
|
@ -19,12 +19,7 @@
|
|||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
import {
|
||||
getSegmentIdentifier,
|
||||
getTimes,
|
||||
type ScrubberListener,
|
||||
type TimelineYearMonth,
|
||||
} from '$lib/utils/timeline-util';
|
||||
import { getTimes, type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||
import { type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
|
|
@ -272,11 +267,6 @@
|
|||
}
|
||||
});
|
||||
|
||||
const getMaxScrollPercent = () => {
|
||||
const totalHeight = timelineManager.timelineHeight + bottomSectionHeight + timelineManager.topSectionHeight;
|
||||
return (totalHeight - timelineManager.viewportHeight) / totalHeight;
|
||||
};
|
||||
|
||||
const getMaxScroll = () => {
|
||||
if (!element || !timelineElement) {
|
||||
return 0;
|
||||
|
|
@ -288,7 +278,7 @@
|
|||
|
||||
const scrollToMonthGroupAndOffset = (monthGroup: MonthGroup, monthGroupScrollPercent: number) => {
|
||||
const topOffset = monthGroup.top;
|
||||
const maxScrollPercent = getMaxScrollPercent();
|
||||
const maxScrollPercent = timelineManager.maxScrollPercent;
|
||||
const delta = monthGroup.height * monthGroupScrollPercent;
|
||||
const scrollToTop = (topOffset + delta) * maxScrollPercent;
|
||||
|
||||
|
|
@ -343,7 +333,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
let maxScrollPercent = getMaxScrollPercent();
|
||||
let maxScrollPercent = timelineManager.maxScrollPercent;
|
||||
let found = false;
|
||||
|
||||
const monthsLength = timelineManager.months.length;
|
||||
|
|
@ -640,7 +630,7 @@
|
|||
{@const display = monthGroup.intersecting}
|
||||
{@const absoluteHeight = monthGroup.top}
|
||||
|
||||
{#if !monthGroup.isLoaded}
|
||||
{#if !monthGroup.loaded}
|
||||
<div
|
||||
style:height={monthGroup.height + 'px'}
|
||||
style:position="absolute"
|
||||
|
|
|
|||
|
|
@ -1,24 +1,22 @@
|
|||
import { updateIntersectionMonthGroup } from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
|
||||
import { updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte';
|
||||
import { CancellableTask } from '$lib/utils/cancellable-task';
|
||||
import { clamp, debounce } from 'lodash-es';
|
||||
|
||||
import type {
|
||||
PhotostreamSegment,
|
||||
SegmentIdentifier,
|
||||
} from '$lib/managers/photostream-manager/PhotostreamSegment.svelte';
|
||||
import { updateIntersectionMonthGroup } from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
|
||||
import { updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte';
|
||||
import type {
|
||||
AssetDescriptor,
|
||||
TimelineAsset,
|
||||
TimelineManagerLayoutOptions,
|
||||
Viewport,
|
||||
} from '$lib/managers/timeline-manager/types';
|
||||
import { CancellableTask, TaskStatus } from '$lib/utils/cancellable-task';
|
||||
import { clamp, debounce } from 'lodash-es';
|
||||
|
||||
export abstract class PhotostreamManager {
|
||||
isInitialized = $state(false);
|
||||
topSectionHeight = $state(0);
|
||||
bottomSectionHeight = $state(60);
|
||||
abstract get months(): PhotostreamSegment[];
|
||||
|
||||
timelineHeight = $derived.by(
|
||||
() => this.months.reduce((accumulator, b) => accumulator + b.height, 0) + this.topSectionHeight,
|
||||
);
|
||||
|
|
@ -71,6 +69,17 @@ export abstract class PhotostreamManager {
|
|||
}
|
||||
}
|
||||
|
||||
abstract get months(): PhotostreamSegment[];
|
||||
|
||||
get maxScrollPercent() {
|
||||
const totalHeight = this.timelineHeight + this.bottomSectionHeight + this.topSectionHeight;
|
||||
return (totalHeight - this.viewportHeight) / totalHeight;
|
||||
}
|
||||
|
||||
get maxScroll() {
|
||||
return this.topSectionHeight + this.bottomSectionHeight + (this.timelineHeight - this.viewportHeight);
|
||||
}
|
||||
|
||||
#setHeaderHeight(value: number) {
|
||||
if (this.#headerHeight == value) {
|
||||
return false;
|
||||
|
|
@ -151,6 +160,10 @@ export abstract class PhotostreamManager {
|
|||
return this.#viewportHeight;
|
||||
}
|
||||
|
||||
get hasEmptyViewport() {
|
||||
return this.viewportWidth === 0 || this.viewportHeight === 0;
|
||||
}
|
||||
|
||||
updateSlidingWindow(scrollTop: number) {
|
||||
if (this.#scrollTop !== scrollTop) {
|
||||
this.#scrollTop = scrollTop;
|
||||
|
|
@ -198,34 +211,12 @@ export abstract class PhotostreamManager {
|
|||
await this.initTask.execute(() => Promise.resolve(undefined), true);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
destroy() {
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
async updateViewport(viewport: Viewport) {
|
||||
if (viewport.height === 0 && viewport.width === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.viewportHeight === viewport.height && this.viewportWidth === viewport.width) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.initTask.executed) {
|
||||
await (this.initTask.loading ? this.initTask.waitUntilCompletion() : this.init());
|
||||
}
|
||||
|
||||
const changedWidth = viewport.width !== this.viewportWidth;
|
||||
this.viewportHeight = viewport.height;
|
||||
this.viewportWidth = viewport.width;
|
||||
this.updateViewportGeometry(changedWidth);
|
||||
}
|
||||
|
||||
protected updateViewportGeometry(changedWidth: boolean) {
|
||||
if (!this.isInitialized) {
|
||||
return;
|
||||
}
|
||||
if (this.viewportWidth === 0 || this.viewportHeight === 0) {
|
||||
if (!this.isInitialized || this.hasEmptyViewport) {
|
||||
return;
|
||||
}
|
||||
for (const month of this.months) {
|
||||
|
|
@ -246,29 +237,18 @@ export abstract class PhotostreamManager {
|
|||
}
|
||||
|
||||
async loadSegment(identifier: SegmentIdentifier, options?: { cancelable: boolean }): Promise<void> {
|
||||
let cancelable = true;
|
||||
if (options) {
|
||||
cancelable = options.cancelable;
|
||||
}
|
||||
const segment = this.getSegmentByIdentifier(identifier);
|
||||
if (!segment) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (segment.loader?.executed) {
|
||||
const { cancelable = true } = options ?? {};
|
||||
const segment = this.months.find((segment) => identifier.matches(segment));
|
||||
if (!segment || segment.loader?.executed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await segment.load(cancelable);
|
||||
if (result === 'LOADED') {
|
||||
if (result === TaskStatus.LOADED) {
|
||||
updateIntersectionMonthGroup(this, segment);
|
||||
}
|
||||
}
|
||||
|
||||
getSegmentByIdentifier(identifier: SegmentIdentifier) {
|
||||
return this.months.find((segment) => identifier.matches(segment));
|
||||
}
|
||||
|
||||
getSegmentForAssetId(assetId: string) {
|
||||
for (const month of this.months) {
|
||||
const asset = month.assets.find((asset) => asset.id === assetId);
|
||||
|
|
@ -285,15 +265,6 @@ export abstract class PhotostreamManager {
|
|||
this.updateIntersections();
|
||||
}
|
||||
|
||||
getMaxScrollPercent() {
|
||||
const totalHeight = this.timelineHeight + this.bottomSectionHeight + this.topSectionHeight;
|
||||
return (totalHeight - this.viewportHeight) / totalHeight;
|
||||
}
|
||||
|
||||
getMaxScroll() {
|
||||
return this.topSectionHeight + this.bottomSectionHeight + (this.timelineHeight - this.viewportHeight);
|
||||
}
|
||||
|
||||
retrieveRange(start: AssetDescriptor, end: AssetDescriptor): Promise<TimelineAsset[]> {
|
||||
const range: TimelineAsset[] = [];
|
||||
let collecting = false;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { CancellableTask } from '$lib/utils/cancellable-task';
|
||||
import type { PhotostreamManager } from '$lib/managers/photostream-manager/PhotostreamManager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
|
||||
import { CancellableTask, TaskStatus } from '$lib/utils/cancellable-task';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
import type { PhotostreamManager } from '$lib/managers/photostream-manager/PhotostreamManager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
|
||||
|
||||
export type SegmentIdentifier = {
|
||||
id(): string;
|
||||
matches(segment: PhotostreamSegment): boolean;
|
||||
};
|
||||
export abstract class PhotostreamSegment {
|
||||
|
|
@ -22,10 +22,10 @@ export abstract class PhotostreamSegment {
|
|||
initialCount = $state(0);
|
||||
percent = $state(0);
|
||||
|
||||
assetsCount = $derived.by(() => (this.isLoaded ? this.viewerAssets.length : this.initialCount));
|
||||
assetsCount = $derived.by(() => (this.loaded ? this.viewerAssets.length : this.initialCount));
|
||||
loader = new CancellableTask(
|
||||
() => this.markLoaded(),
|
||||
() => this.markCanceled,
|
||||
() => (this.loaded = true),
|
||||
() => (this.loaded = false),
|
||||
() => this.handleLoadError,
|
||||
);
|
||||
isHeightActual = $state(false);
|
||||
|
|
@ -34,18 +34,18 @@ export abstract class PhotostreamSegment {
|
|||
|
||||
abstract get identifier(): SegmentIdentifier;
|
||||
|
||||
abstract get id(): string;
|
||||
abstract get viewerAssets(): ViewerAsset[];
|
||||
|
||||
get isLoaded() {
|
||||
abstract findAssetAbsolutePosition(assetId: string): number;
|
||||
|
||||
protected abstract fetch(signal: AbortSignal): Promise<void>;
|
||||
|
||||
get loaded() {
|
||||
return this.#isLoaded;
|
||||
}
|
||||
|
||||
protected markLoaded() {
|
||||
this.#isLoaded = true;
|
||||
}
|
||||
|
||||
protected markCanceled() {
|
||||
this.#isLoaded = false;
|
||||
protected set loaded(newValue: boolean) {
|
||||
this.#isLoaded = newValue;
|
||||
}
|
||||
|
||||
set intersecting(newValue: boolean) {
|
||||
|
|
@ -65,22 +65,11 @@ export abstract class PhotostreamSegment {
|
|||
return this.#intersecting;
|
||||
}
|
||||
|
||||
async load(cancelable: boolean): Promise<'DONE' | 'WAITED' | 'CANCELED' | 'LOADED' | 'ERRORED'> {
|
||||
return await this.loader?.execute(async (signal: AbortSignal) => {
|
||||
await this.fetch(signal);
|
||||
}, cancelable);
|
||||
}
|
||||
|
||||
protected abstract fetch(signal: AbortSignal): Promise<void>;
|
||||
|
||||
get assets(): TimelineAsset[] {
|
||||
return this.#assets;
|
||||
}
|
||||
|
||||
abstract get viewerAssets(): ViewerAsset[];
|
||||
|
||||
set height(height: number) {
|
||||
console.log('height', height);
|
||||
if (this.#height === height) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -131,6 +120,12 @@ export abstract class PhotostreamSegment {
|
|||
return this.#top + this.timelineManager.topSectionHeight;
|
||||
}
|
||||
|
||||
async load(cancelable: boolean): Promise<TaskStatus> {
|
||||
return await this.loader?.execute(async (signal: AbortSignal) => {
|
||||
await this.fetch(signal);
|
||||
}, cancelable);
|
||||
}
|
||||
|
||||
protected handleLoadError(error: unknown) {
|
||||
const _$t = get(t);
|
||||
handleError(error, _$t('errors.failed_to_load_assets'));
|
||||
|
|
@ -146,6 +141,4 @@ export abstract class PhotostreamSegment {
|
|||
this.intersecting = intersecting;
|
||||
this.actuallyIntersecting = actuallyIntersecting;
|
||||
}
|
||||
|
||||
abstract findAssetAbsolutePosition(assetId: string): number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export function updateGeometry(
|
|||
if (invalidateHeight) {
|
||||
month.isHeightActual = false;
|
||||
}
|
||||
if (!month.isLoaded) {
|
||||
if (!month.loaded) {
|
||||
const viewportWidth = timelineManager.viewportWidth;
|
||||
if (!month.isHeightActual) {
|
||||
const unwrappedWidth = (3 / 2) * month.assetsCount * timelineManager.rowHeight * (7 / 10);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { AssetOrder, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
||||
|
||||
import {
|
||||
PhotostreamSegment,
|
||||
type SegmentIdentifier,
|
||||
} from '$lib/managers/photostream-manager/PhotostreamSegment.svelte';
|
||||
import { layoutMonthGroup, updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte';
|
||||
import { loadFromTimeBuckets } from '$lib/managers/timeline-manager/internal/load-support.svelte';
|
||||
import {
|
||||
formatGroupTitle,
|
||||
formatGroupTitleFull,
|
||||
|
|
@ -7,24 +11,16 @@ import {
|
|||
fromTimelinePlainDate,
|
||||
fromTimelinePlainDateTime,
|
||||
fromTimelinePlainYearMonth,
|
||||
getSegmentIdentifier,
|
||||
getTimes,
|
||||
setDifference,
|
||||
type TimelineDateTime,
|
||||
type TimelineYearMonth,
|
||||
} from '$lib/utils/timeline-util';
|
||||
|
||||
import { layoutMonthGroup, updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte';
|
||||
import { loadFromTimeBuckets } from '$lib/managers/timeline-manager/internal/load-support.svelte';
|
||||
|
||||
import {
|
||||
PhotostreamSegment,
|
||||
type SegmentIdentifier,
|
||||
} from '$lib/managers/photostream-manager/PhotostreamSegment.svelte';
|
||||
import { AssetOrder, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import { DayGroup } from './day-group.svelte';
|
||||
import { GroupInsertionCache } from './group-insertion-cache.svelte';
|
||||
import type { TimelineManager } from './timeline-manager.svelte';
|
||||
import { getSegmentIdentifier, type TimelineManager } from './timeline-manager.svelte';
|
||||
import type { AssetDescriptor, AssetOperation, Direction, MoveAsset, TimelineAsset } from './types';
|
||||
import { ViewerAsset } from './viewer-asset.svelte';
|
||||
|
||||
|
|
@ -52,9 +48,7 @@ export class MonthGroup extends PhotostreamSegment {
|
|||
this.#timelineManager = timelineManager;
|
||||
this.#sortOrder = order;
|
||||
this.monthGroupTitle = formatMonthGroupTitle(fromTimelinePlainYearMonth(yearMonth));
|
||||
if (loaded) {
|
||||
this.markLoaded();
|
||||
}
|
||||
this.loaded = loaded;
|
||||
}
|
||||
|
||||
get identifier() {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
||||
import { getMonthGroupByDate } from '$lib/managers/timeline-manager/internal/search-support.svelte';
|
||||
import { AbortError } from '$lib/utils';
|
||||
import { fromISODateTimeUTCToObject, getSegmentIdentifier } from '$lib/utils/timeline-util';
|
||||
import { fromISODateTimeUTCToObject } from '$lib/utils/timeline-util';
|
||||
import { type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
||||
import { timelineAssetFactory, toResponseDto } from '@test-data/factories/asset-factory';
|
||||
import { TimelineManager } from './timeline-manager.svelte';
|
||||
|
|
@ -22,6 +22,14 @@ function deriveLocalDateTimeFromFileCreatedAt(arg: TimelineAsset): TimelineAsset
|
|||
};
|
||||
}
|
||||
|
||||
const initViewport = async (timelineManager: TimelineManager, viewportHeight = 1000, viewportWidth = 1588) => {
|
||||
timelineManager.viewportHeight = viewportHeight;
|
||||
timelineManager.viewportWidth = viewportWidth;
|
||||
await timelineManager.init();
|
||||
timelineManager.updateSlidingWindow(0);
|
||||
timelineManager.updateIntersections();
|
||||
};
|
||||
|
||||
describe('TimelineManager', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
|
|
@ -63,12 +71,12 @@ describe('TimelineManager', () => {
|
|||
]);
|
||||
|
||||
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssetsResponse[timeBucket]));
|
||||
await timelineManager.updateViewport({ width: 1588, height: 1000 });
|
||||
await initViewport(timelineManager);
|
||||
});
|
||||
|
||||
it('should load months in viewport', () => {
|
||||
expect(sdkMock.getTimeBuckets).toBeCalledTimes(1);
|
||||
expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(2);
|
||||
expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('calculates month height', () => {
|
||||
|
|
@ -82,13 +90,13 @@ describe('TimelineManager', () => {
|
|||
expect.arrayContaining([
|
||||
expect.objectContaining({ year: 2024, month: 3, height: 165.5 }),
|
||||
expect.objectContaining({ year: 2024, month: 2, height: 11_996 }),
|
||||
expect.objectContaining({ year: 2024, month: 1, height: 286 }),
|
||||
expect.objectContaining({ year: 2024, month: 1, height: 404.5 }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('calculates timeline height', () => {
|
||||
expect(timelineManager.timelineHeight).toBe(12_447.5);
|
||||
expect(timelineManager.timelineHeight).toBe(12566);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -124,19 +132,19 @@ describe('TimelineManager', () => {
|
|||
}
|
||||
return bucketAssetsResponse[timeBucket];
|
||||
});
|
||||
await timelineManager.updateViewport({ width: 1588, height: 0 });
|
||||
await initViewport(timelineManager);
|
||||
});
|
||||
|
||||
it('loads a month', async () => {
|
||||
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.assets.length).toEqual(0);
|
||||
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
||||
expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
|
||||
expect(sdkMock.getTimeBucket).toBeCalledTimes(2);
|
||||
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.assets.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('ignores invalid months', async () => {
|
||||
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2023, month: 1 }));
|
||||
expect(sdkMock.getTimeBucket).toBeCalledTimes(0);
|
||||
expect(sdkMock.getTimeBucket).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('cancels month loading', async () => {
|
||||
|
|
@ -154,10 +162,10 @@ describe('TimelineManager', () => {
|
|||
timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 })),
|
||||
timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 })),
|
||||
]);
|
||||
expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
|
||||
expect(sdkMock.getTimeBucket).toBeCalledTimes(2);
|
||||
|
||||
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
||||
expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
|
||||
expect(sdkMock.getTimeBucket).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('allows loading a canceled month', async () => {
|
||||
|
|
@ -180,7 +188,8 @@ describe('TimelineManager', () => {
|
|||
timelineManager = new TimelineManager();
|
||||
sdkMock.getTimeBuckets.mockResolvedValue([]);
|
||||
|
||||
await timelineManager.updateViewport({ width: 1588, height: 1000 });
|
||||
timelineManager.viewportHeight = 1000;
|
||||
timelineManager.viewportHeight = 1588;
|
||||
});
|
||||
|
||||
it('is empty initially', () => {
|
||||
|
|
@ -304,7 +313,8 @@ describe('TimelineManager', () => {
|
|||
timelineManager = new TimelineManager();
|
||||
sdkMock.getTimeBuckets.mockResolvedValue([]);
|
||||
|
||||
await timelineManager.updateViewport({ width: 1588, height: 1000 });
|
||||
timelineManager.viewportHeight = 1000;
|
||||
timelineManager.viewportHeight = 1588;
|
||||
});
|
||||
|
||||
it('ignores non-existing assets', () => {
|
||||
|
|
@ -359,7 +369,8 @@ describe('TimelineManager', () => {
|
|||
timelineManager = new TimelineManager();
|
||||
sdkMock.getTimeBuckets.mockResolvedValue([]);
|
||||
|
||||
await timelineManager.updateViewport({ width: 1588, height: 1000 });
|
||||
timelineManager.viewportHeight = 1000;
|
||||
timelineManager.viewportHeight = 1588;
|
||||
});
|
||||
|
||||
it('ignores invalid IDs', () => {
|
||||
|
|
@ -411,7 +422,8 @@ describe('TimelineManager', () => {
|
|||
beforeEach(async () => {
|
||||
timelineManager = new TimelineManager();
|
||||
sdkMock.getTimeBuckets.mockResolvedValue([]);
|
||||
await timelineManager.updateViewport({ width: 0, height: 0 });
|
||||
|
||||
await initViewport(timelineManager, 0, 0);
|
||||
});
|
||||
|
||||
it('empty store returns null', () => {
|
||||
|
|
@ -468,7 +480,8 @@ describe('TimelineManager', () => {
|
|||
{ count: 3, timeBucket: '2024-01-01T00:00:00.000Z' },
|
||||
]);
|
||||
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssetsResponse[timeBucket]));
|
||||
await timelineManager.updateViewport({ width: 1588, height: 1000 });
|
||||
|
||||
initViewport(timelineManager);
|
||||
});
|
||||
|
||||
it('returns null for invalid assetId', async () => {
|
||||
|
|
@ -535,7 +548,7 @@ describe('TimelineManager', () => {
|
|||
timelineManager = new TimelineManager();
|
||||
sdkMock.getTimeBuckets.mockResolvedValue([]);
|
||||
|
||||
await timelineManager.updateViewport({ width: 0, height: 0 });
|
||||
await initViewport(timelineManager, 0, 0);
|
||||
});
|
||||
|
||||
it('returns null for invalid months', () => {
|
||||
|
|
@ -618,7 +631,7 @@ describe('TimelineManager', () => {
|
|||
]);
|
||||
|
||||
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssetsResponse[timeBucket]));
|
||||
await timelineManager.updateViewport({ width: 1588, height: 0 });
|
||||
await initViewport(timelineManager);
|
||||
});
|
||||
|
||||
it('gets all assets once', async () => {
|
||||
|
|
|
|||
|
|
@ -3,12 +3,7 @@ import { AssetOrder, getAssetInfo, getTimeBuckets } from '@immich/sdk';
|
|||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
|
||||
import { CancellableTask } from '$lib/utils/cancellable-task';
|
||||
import {
|
||||
getSegmentIdentifier,
|
||||
toTimelineAsset,
|
||||
type TimelineDateTime,
|
||||
type TimelineYearMonth,
|
||||
} from '$lib/utils/timeline-util';
|
||||
import { toTimelineAsset, type TimelineDateTime, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||
|
|
@ -39,6 +34,17 @@ import type {
|
|||
TimelineManagerOptions,
|
||||
} from './types';
|
||||
|
||||
export const getSegmentIdentifier = (yearMonth: TimelineYearMonth | TimelineDateTime) => ({
|
||||
id: () => {
|
||||
return yearMonth.year + '-' + yearMonth.month;
|
||||
},
|
||||
matches: (segment: MonthGroup) => {
|
||||
return (
|
||||
segment.yearMonth && segment.yearMonth.year === yearMonth.year && segment.yearMonth.month === yearMonth.month
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export class TimelineManager extends PhotostreamManager {
|
||||
albumAssets: Set<string> = new SvelteSet();
|
||||
scrubberMonths: ScrubberMonth[] = $state([]);
|
||||
|
|
|
|||
|
|
@ -513,7 +513,7 @@ export const selectAllAssets = async (timelineManager: TimelineManager, assetInt
|
|||
|
||||
try {
|
||||
for (const monthGroup of timelineManager.months) {
|
||||
await timelineManager.loadSegment(monthGroup.identifier);
|
||||
await monthGroup.load(false);
|
||||
|
||||
if (!get(isSelectingAllAssets)) {
|
||||
assetInteraction.clearMultiselect();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
export enum TaskStatus {
|
||||
DONE,
|
||||
WAITED,
|
||||
CANCELED,
|
||||
LOADED,
|
||||
ERRORED,
|
||||
}
|
||||
export class CancellableTask {
|
||||
cancelToken: AbortController | null = null;
|
||||
cancellable: boolean = true;
|
||||
|
|
@ -32,18 +39,18 @@ export class CancellableTask {
|
|||
|
||||
async waitUntilCompletion() {
|
||||
if (this.executed) {
|
||||
return 'DONE';
|
||||
return TaskStatus.DONE;
|
||||
}
|
||||
// if there is a cancel token, task is currently executing, so wait on the promise. If it
|
||||
// isn't, then the task is in new state, it hasn't been loaded, nor has it been executed.
|
||||
// in either case, we wait on the promise.
|
||||
await this.complete;
|
||||
return 'WAITED';
|
||||
return TaskStatus.WAITED;
|
||||
}
|
||||
|
||||
async execute<F extends (abortSignal: AbortSignal) => Promise<void>>(f: F, cancellable: boolean) {
|
||||
if (this.executed) {
|
||||
return 'DONE';
|
||||
return TaskStatus.DONE;
|
||||
}
|
||||
|
||||
// if promise is pending, wait on previous request instead.
|
||||
|
|
@ -54,7 +61,7 @@ export class CancellableTask {
|
|||
this.cancellable = cancellable;
|
||||
}
|
||||
await this.complete;
|
||||
return 'WAITED';
|
||||
return TaskStatus.WAITED;
|
||||
}
|
||||
this.cancellable = cancellable;
|
||||
const cancelToken = (this.cancelToken = new AbortController());
|
||||
|
|
@ -62,18 +69,18 @@ export class CancellableTask {
|
|||
try {
|
||||
await f(cancelToken.signal);
|
||||
if (cancelToken.signal.aborted) {
|
||||
return 'CANCELED';
|
||||
return TaskStatus.CANCELED;
|
||||
}
|
||||
this.#transitionToExecuted();
|
||||
return 'LOADED';
|
||||
return TaskStatus.LOADED;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((error as any).name === 'AbortError') {
|
||||
// abort error is not treated as an error, but as a cancellation.
|
||||
return 'CANCELED';
|
||||
return TaskStatus.CANCELED;
|
||||
}
|
||||
this.#transitionToErrored(error);
|
||||
return 'ERRORED';
|
||||
return TaskStatus.ERRORED;
|
||||
} finally {
|
||||
this.cancelToken = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getAssetRatio } from '$lib/utils/asset-utils';
|
||||
|
|
@ -243,11 +242,3 @@ export function setDifference<T>(setA: Set<T>, setB: Set<T>): SvelteSet<T> {
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const getSegmentIdentifier = (yearMonth: TimelineYearMonth | TimelineDateTime) => ({
|
||||
matches(segment: MonthGroup) {
|
||||
return (
|
||||
segment.yearMonth && segment.yearMonth.year === yearMonth.year && segment.yearMonth.month === yearMonth.month
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue