mirror of
https://github.com/immich-app/immich.git
synced 2025-12-11 17:23:13 +03:00
Compare commits
2 Commits
push-xqttk
...
refactor/e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9bb469385 | ||
|
|
aa559f0b30 |
@@ -33,7 +33,7 @@
|
|||||||
import { deleteAssets, updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
|
import { deleteAssets, updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
|
||||||
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
import { getTimes, toTimelineAsset, type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
import { getSegmentIdentifier, getTimes, toTimelineAsset, type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||||
import { AssetVisibility, getAssetInfo, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
import { AssetVisibility, getAssetInfo, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||||
import { modalManager } from '@immich/ui';
|
import { modalManager } from '@immich/ui';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
@@ -670,7 +670,7 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (started) {
|
if (started) {
|
||||||
await timelineManager.loadMonthGroup(monthGroup.yearMonth);
|
await timelineManager.loadSegment(monthGroup.identifier);
|
||||||
for (const asset of monthGroup.assetsIterator()) {
|
for (const asset of monthGroup.assetsIterator()) {
|
||||||
if (deselect) {
|
if (deselect) {
|
||||||
assetInteraction.removeAssetFromMultiselectGroup(asset.id);
|
assetInteraction.removeAssetFromMultiselectGroup(asset.id);
|
||||||
@@ -811,7 +811,7 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($showAssetViewer) {
|
if ($showAssetViewer) {
|
||||||
const { localDateTime } = getTimes($viewingAsset.fileCreatedAt, DateTime.local().offset / 60);
|
const { localDateTime } = getTimes($viewingAsset.fileCreatedAt, DateTime.local().offset / 60);
|
||||||
void timelineManager.loadMonthGroup({ year: localDateTime.year, month: localDateTime.month });
|
void timelineManager.loadSegment(getSegmentIdentifier({ year: localDateTime.year, month: localDateTime.month }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,293 @@
|
|||||||
|
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/timeline-manager/PhotostreamSegment.svelte';
|
||||||
|
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
|
||||||
|
import type { TimelineManagerLayoutOptions, TimelineManagerOptions, Viewport } from './types';
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
assetCount = $derived.by(() => this.months.reduce((accumulator, b) => accumulator + b.assetsCount, 0));
|
||||||
|
|
||||||
|
topIntersectingMonthGroup: PhotostreamSegment | undefined = $state();
|
||||||
|
|
||||||
|
visibleWindow = $derived.by(() => ({
|
||||||
|
top: this.#scrollTop,
|
||||||
|
bottom: this.#scrollTop + this.viewportHeight,
|
||||||
|
}));
|
||||||
|
|
||||||
|
protected initTask = new CancellableTask(
|
||||||
|
() => (this.isInitialized = true),
|
||||||
|
() => (this.isInitialized = false),
|
||||||
|
() => void 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
protected options: TimelineManagerOptions = {};
|
||||||
|
|
||||||
|
#viewportHeight = $state(0);
|
||||||
|
#viewportWidth = $state(0);
|
||||||
|
#scrollTop = $state(0);
|
||||||
|
|
||||||
|
#rowHeight = $state(235);
|
||||||
|
#headerHeight = $state(48);
|
||||||
|
#gap = $state(12);
|
||||||
|
|
||||||
|
#scrolling = $state(false);
|
||||||
|
#suspendTransitions = $state(false);
|
||||||
|
#resetScrolling = debounce(() => (this.#scrolling = false), 1000);
|
||||||
|
#resetSuspendTransitions = debounce(() => (this.suspendTransitions = false), 1000);
|
||||||
|
scrollCompensation: {
|
||||||
|
heightDelta: number | undefined;
|
||||||
|
scrollTop: number | undefined;
|
||||||
|
monthGroup: PhotostreamSegment | undefined;
|
||||||
|
} = $state({
|
||||||
|
heightDelta: 0,
|
||||||
|
scrollTop: 0,
|
||||||
|
monthGroup: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
setLayoutOptions({ headerHeight = 48, rowHeight = 235, gap = 12 }: TimelineManagerLayoutOptions) {
|
||||||
|
let changed = false;
|
||||||
|
changed ||= this.#setHeaderHeight(headerHeight);
|
||||||
|
changed ||= this.#setGap(gap);
|
||||||
|
changed ||= this.#setRowHeight(rowHeight);
|
||||||
|
if (changed) {
|
||||||
|
this.refreshLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#setHeaderHeight(value: number) {
|
||||||
|
if (this.#headerHeight == value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.#headerHeight = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get headerHeight() {
|
||||||
|
return this.#headerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setGap(value: number) {
|
||||||
|
if (this.#gap == value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.#gap = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get gap() {
|
||||||
|
return this.#gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setRowHeight(value: number) {
|
||||||
|
if (this.#rowHeight == value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.#rowHeight = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get rowHeight() {
|
||||||
|
return this.#rowHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
set scrolling(value: boolean) {
|
||||||
|
this.#scrolling = value;
|
||||||
|
if (value) {
|
||||||
|
this.suspendTransitions = true;
|
||||||
|
this.#resetScrolling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get scrolling() {
|
||||||
|
return this.#scrolling;
|
||||||
|
}
|
||||||
|
|
||||||
|
set suspendTransitions(value: boolean) {
|
||||||
|
this.#suspendTransitions = value;
|
||||||
|
if (value) {
|
||||||
|
this.#resetSuspendTransitions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get suspendTransitions() {
|
||||||
|
return this.#suspendTransitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
set viewportWidth(value: number) {
|
||||||
|
const changed = value !== this.#viewportWidth;
|
||||||
|
this.#viewportWidth = value;
|
||||||
|
this.suspendTransitions = true;
|
||||||
|
void this.updateViewportGeometry(changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
get viewportWidth() {
|
||||||
|
return this.#viewportWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
set viewportHeight(value: number) {
|
||||||
|
this.#viewportHeight = value;
|
||||||
|
this.#suspendTransitions = true;
|
||||||
|
void this.updateViewportGeometry(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
get viewportHeight() {
|
||||||
|
return this.#viewportHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSlidingWindow(scrollTop: number) {
|
||||||
|
if (this.#scrollTop !== scrollTop) {
|
||||||
|
this.#scrollTop = scrollTop;
|
||||||
|
this.updateIntersections();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearScrollCompensation() {
|
||||||
|
this.scrollCompensation = {
|
||||||
|
heightDelta: undefined,
|
||||||
|
scrollTop: undefined,
|
||||||
|
monthGroup: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateIntersections() {
|
||||||
|
if (!this.isInitialized || this.visibleWindow.bottom === this.visibleWindow.top) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let topIntersectingMonthGroup = undefined;
|
||||||
|
for (const month of this.months) {
|
||||||
|
updateIntersectionMonthGroup(this, month);
|
||||||
|
if (!topIntersectingMonthGroup && month.actuallyIntersecting) {
|
||||||
|
topIntersectingMonthGroup = month;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (topIntersectingMonthGroup !== undefined && this.topIntersectingMonthGroup !== topIntersectingMonthGroup) {
|
||||||
|
this.topIntersectingMonthGroup = topIntersectingMonthGroup;
|
||||||
|
}
|
||||||
|
for (const month of this.months) {
|
||||||
|
if (month === this.topIntersectingMonthGroup) {
|
||||||
|
this.topIntersectingMonthGroup.percent = clamp(
|
||||||
|
(this.visibleWindow.top - this.topIntersectingMonthGroup.top) / this.topIntersectingMonthGroup.height,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
month.percent = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async init(options: TimelineManagerOptions) {
|
||||||
|
this.isInitialized = true;
|
||||||
|
// this.months = [];
|
||||||
|
|
||||||
|
await this.initTask.execute(async () => undefined, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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(this.options));
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const month of this.months) {
|
||||||
|
updateGeometry(this, month, { invalidateHeight: changedWidth });
|
||||||
|
}
|
||||||
|
this.updateIntersections();
|
||||||
|
}
|
||||||
|
|
||||||
|
createLayoutOptions() {
|
||||||
|
const viewportWidth = this.viewportWidth;
|
||||||
|
|
||||||
|
return {
|
||||||
|
spacing: 2,
|
||||||
|
heightTolerance: 0.15,
|
||||||
|
rowHeight: this.#rowHeight,
|
||||||
|
rowWidth: Math.floor(viewportWidth),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await segment.loader?.execute(async (signal: AbortSignal) => {
|
||||||
|
await this.fetchSegment(segment, signal);
|
||||||
|
}, cancelable);
|
||||||
|
if (result === 'LOADED') {
|
||||||
|
updateIntersectionMonthGroup(this, segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSegmentMatcher(identifier: SegmentIdentifier) {
|
||||||
|
return (segment: MonthGroup) => {
|
||||||
|
return identifier;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
getSegmentByIdentifier(identifier: SegmentIdentifier) {
|
||||||
|
return this.months.find((segment) => identifier.matches(segment));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fetchSegment(segment: PhotostreamSegment, signal: AbortSignal): Promise<void>;
|
||||||
|
|
||||||
|
refreshLayout() {
|
||||||
|
for (const month of this.months) {
|
||||||
|
updateGeometry(this, month, { invalidateHeight: true });
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { CancellableTask } 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/timeline-manager/PhotostreamManager.svelte';
|
||||||
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
|
|
||||||
|
export type SegmentIdentifier = {
|
||||||
|
matches(segment: PhotostreamSegment): boolean;
|
||||||
|
};
|
||||||
|
export abstract class PhotostreamSegment {
|
||||||
|
#intersecting = $state(false);
|
||||||
|
actuallyIntersecting = $state(false);
|
||||||
|
#isLoaded = $state(false);
|
||||||
|
|
||||||
|
#height = $state(0);
|
||||||
|
#top = $state(0);
|
||||||
|
|
||||||
|
initialCount = $state(0);
|
||||||
|
percent = $state(0);
|
||||||
|
assetsCount = $derived(this.isLoaded ? this.getAssets().length : this.initialCount);
|
||||||
|
loader = new CancellableTask(
|
||||||
|
() => this.markLoaded(),
|
||||||
|
() => this.markCanceled,
|
||||||
|
() => this.handleLoadError,
|
||||||
|
);
|
||||||
|
isHeightActual = $state(false);
|
||||||
|
|
||||||
|
abstract get timelineManager(): PhotostreamManager;
|
||||||
|
|
||||||
|
abstract get identifier(): SegmentIdentifier;
|
||||||
|
|
||||||
|
get isLoaded() {
|
||||||
|
return this.#isLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected markLoaded() {
|
||||||
|
this.#isLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected markCanceled() {
|
||||||
|
this.#isLoaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set intersecting(newValue: boolean) {
|
||||||
|
const old = this.#intersecting;
|
||||||
|
if (old === newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#intersecting = newValue;
|
||||||
|
if (newValue) {
|
||||||
|
this.load();
|
||||||
|
} else {
|
||||||
|
this.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get intersecting() {
|
||||||
|
return this.#intersecting;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract load(): Promise<void>;
|
||||||
|
|
||||||
|
abstract getAssets(): TimelineAsset[];
|
||||||
|
|
||||||
|
set height(height: number) {
|
||||||
|
if (this.#height === height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { timelineManager: store, percent } = this;
|
||||||
|
const index = store.months.indexOf(this);
|
||||||
|
const heightDelta = height - this.#height;
|
||||||
|
this.#height = height;
|
||||||
|
const prevMonthGroup = store.months[index - 1];
|
||||||
|
if (prevMonthGroup) {
|
||||||
|
const newTop = prevMonthGroup.#top + prevMonthGroup.#height;
|
||||||
|
if (this.#top !== newTop) {
|
||||||
|
this.#top = newTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let cursor = index + 1; cursor < store.months.length; cursor++) {
|
||||||
|
const monthGroup = this.timelineManager.months[cursor];
|
||||||
|
const newTop = monthGroup.#top + heightDelta;
|
||||||
|
if (monthGroup.#top !== newTop) {
|
||||||
|
monthGroup.#top = newTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (store.topIntersectingMonthGroup) {
|
||||||
|
const currentIndex = store.months.indexOf(store.topIntersectingMonthGroup);
|
||||||
|
if (currentIndex > 0) {
|
||||||
|
if (index < currentIndex) {
|
||||||
|
store.scrollCompensation = {
|
||||||
|
heightDelta,
|
||||||
|
scrollTop: undefined,
|
||||||
|
monthGroup: this,
|
||||||
|
};
|
||||||
|
} else if (percent > 0) {
|
||||||
|
const top = this.top + height * percent;
|
||||||
|
store.scrollCompensation = {
|
||||||
|
heightDelta: undefined,
|
||||||
|
scrollTop: top,
|
||||||
|
monthGroup: this,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get height() {
|
||||||
|
return this.#height;
|
||||||
|
}
|
||||||
|
|
||||||
|
get top(): number {
|
||||||
|
return this.#top + this.timelineManager.topSectionHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleLoadError(error: unknown) {
|
||||||
|
const _$t = get(t);
|
||||||
|
handleError(error, _$t('errors.failed_to_load_assets'));
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.loader?.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
layout(_noDefer: boolean) {}
|
||||||
|
|
||||||
|
updateIntersection({ intersecting, actuallyIntersecting }: { intersecting: boolean; actuallyIntersecting: boolean }) {
|
||||||
|
this.intersecting = intersecting;
|
||||||
|
this.actuallyIntersecting = actuallyIntersecting;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,24 @@
|
|||||||
|
import type { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.svelte';
|
||||||
|
import type { PhotostreamSegment } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
|
||||||
import { TUNABLES } from '$lib/utils/tunables';
|
import { TUNABLES } from '$lib/utils/tunables';
|
||||||
import type { MonthGroup } from '../month-group.svelte';
|
|
||||||
import type { TimelineManager } from '../timeline-manager.svelte';
|
import type { TimelineManager } from '../timeline-manager.svelte';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
|
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
|
||||||
} = TUNABLES;
|
} = TUNABLES;
|
||||||
|
|
||||||
export function updateIntersectionMonthGroup(timelineManager: TimelineManager, month: MonthGroup) {
|
export function updateIntersectionMonthGroup(timelineManager: PhotostreamManager, month: PhotostreamSegment) {
|
||||||
const actuallyIntersecting = calculateMonthGroupIntersecting(timelineManager, month, 0, 0);
|
const actuallyIntersecting = calculateSegmentIntersecting(timelineManager, month, 0, 0);
|
||||||
let preIntersecting = false;
|
let preIntersecting = false;
|
||||||
if (!actuallyIntersecting) {
|
if (!actuallyIntersecting) {
|
||||||
preIntersecting = calculateMonthGroupIntersecting(
|
preIntersecting = calculateSegmentIntersecting(
|
||||||
timelineManager,
|
timelineManager,
|
||||||
month,
|
month,
|
||||||
INTERSECTION_EXPAND_TOP,
|
INTERSECTION_EXPAND_TOP,
|
||||||
INTERSECTION_EXPAND_BOTTOM,
|
INTERSECTION_EXPAND_BOTTOM,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
month.intersecting = actuallyIntersecting || preIntersecting;
|
month.updateIntersection({ intersecting: actuallyIntersecting || preIntersecting, actuallyIntersecting });
|
||||||
month.actuallyIntersecting = actuallyIntersecting;
|
|
||||||
if (preIntersecting || actuallyIntersecting) {
|
|
||||||
timelineManager.clearDeferredLayout(month);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,9 +37,9 @@ export function isIntersecting(regionTop: number, regionBottom: number, windowTo
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateMonthGroupIntersecting(
|
export function calculateSegmentIntersecting(
|
||||||
timelineManager: TimelineManager,
|
timelineManager: PhotostreamManager,
|
||||||
monthGroup: MonthGroup,
|
monthGroup: PhotostreamSegment,
|
||||||
expandTop: number,
|
expandTop: number,
|
||||||
expandBottom: number,
|
expandBottom: number,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
import type { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.svelte';
|
||||||
|
import type { PhotostreamSegment } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
|
||||||
import type { MonthGroup } from '../month-group.svelte';
|
import type { MonthGroup } from '../month-group.svelte';
|
||||||
import type { TimelineManager } from '../timeline-manager.svelte';
|
|
||||||
import type { UpdateGeometryOptions } from '../types';
|
import type { UpdateGeometryOptions } from '../types';
|
||||||
|
|
||||||
export function updateGeometry(timelineManager: TimelineManager, month: MonthGroup, options: UpdateGeometryOptions) {
|
export function updateGeometry(
|
||||||
|
timelineManager: PhotostreamManager,
|
||||||
|
month: PhotostreamSegment,
|
||||||
|
options: UpdateGeometryOptions,
|
||||||
|
) {
|
||||||
const { invalidateHeight, noDefer = false } = options;
|
const { invalidateHeight, noDefer = false } = options;
|
||||||
if (invalidateHeight) {
|
if (invalidateHeight) {
|
||||||
month.isHeightActual = false;
|
month.isHeightActual = false;
|
||||||
@@ -17,10 +22,10 @@ export function updateGeometry(timelineManager: TimelineManager, month: MonthGro
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
layoutMonthGroup(timelineManager, month, noDefer);
|
month.layout(noDefer);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function layoutMonthGroup(timelineManager: TimelineManager, month: MonthGroup, noDefer: boolean = false) {
|
export function layoutMonthGroup(timelineManager: PhotostreamManager, month: MonthGroup, noDefer: boolean = false) {
|
||||||
let cumulativeHeight = 0;
|
let cumulativeHeight = 0;
|
||||||
let cumulativeWidth = 0;
|
let cumulativeWidth = 0;
|
||||||
let currentRowHeight = 0;
|
let currentRowHeight = 0;
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ export function addAssetsToMonthGroups(
|
|||||||
let month = getMonthGroupByDate(timelineManager, asset.localDateTime);
|
let month = getMonthGroupByDate(timelineManager, asset.localDateTime);
|
||||||
|
|
||||||
if (!month) {
|
if (!month) {
|
||||||
month = new MonthGroup(timelineManager, asset.localDateTime, 1, options.order);
|
month = new MonthGroup(timelineManager, asset.localDateTime, 1, true, options.order);
|
||||||
month.isLoaded = true;
|
|
||||||
timelineManager.months.push(month);
|
timelineManager.months.push(month);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
import { AssetOrder, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
import { AssetOrder, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
||||||
|
|
||||||
import { CancellableTask } from '$lib/utils/cancellable-task';
|
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
|
||||||
import {
|
import {
|
||||||
formatGroupTitle,
|
formatGroupTitle,
|
||||||
formatMonthGroupTitle,
|
formatMonthGroupTitle,
|
||||||
fromTimelinePlainDate,
|
fromTimelinePlainDate,
|
||||||
fromTimelinePlainDateTime,
|
fromTimelinePlainDateTime,
|
||||||
fromTimelinePlainYearMonth,
|
fromTimelinePlainYearMonth,
|
||||||
|
getSegmentIdentifier,
|
||||||
getTimes,
|
getTimes,
|
||||||
setDifference,
|
setDifference,
|
||||||
type TimelineDateTime,
|
type TimelineDateTime,
|
||||||
type TimelineYearMonth,
|
type TimelineYearMonth,
|
||||||
} from '$lib/utils/timeline-util';
|
} from '$lib/utils/timeline-util';
|
||||||
|
|
||||||
import { t } from 'svelte-i18n';
|
import { layoutMonthGroup, updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte';
|
||||||
import { get } from 'svelte/store';
|
import { PhotostreamSegment, type SegmentIdentifier } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
|
||||||
|
|
||||||
import { SvelteSet } from 'svelte/reactivity';
|
import { SvelteSet } from 'svelte/reactivity';
|
||||||
import { DayGroup } from './day-group.svelte';
|
import { DayGroup } from './day-group.svelte';
|
||||||
import { GroupInsertionCache } from './group-insertion-cache.svelte';
|
import { GroupInsertionCache } from './group-insertion-cache.svelte';
|
||||||
@@ -24,73 +22,52 @@ import type { TimelineManager } from './timeline-manager.svelte';
|
|||||||
import type { AssetDescriptor, AssetOperation, Direction, MoveAsset, TimelineAsset } from './types';
|
import type { AssetDescriptor, AssetOperation, Direction, MoveAsset, TimelineAsset } from './types';
|
||||||
import { ViewerAsset } from './viewer-asset.svelte';
|
import { ViewerAsset } from './viewer-asset.svelte';
|
||||||
|
|
||||||
export class MonthGroup {
|
export class MonthGroup extends PhotostreamSegment {
|
||||||
#intersecting: boolean = $state(false);
|
|
||||||
actuallyIntersecting: boolean = $state(false);
|
|
||||||
isLoaded: boolean = $state(false);
|
|
||||||
dayGroups: DayGroup[] = $state([]);
|
dayGroups: DayGroup[] = $state([]);
|
||||||
readonly timelineManager: TimelineManager;
|
|
||||||
|
|
||||||
#height: number = $state(0);
|
|
||||||
#top: number = $state(0);
|
|
||||||
|
|
||||||
#initialCount: number = 0;
|
|
||||||
#sortOrder: AssetOrder = AssetOrder.Desc;
|
#sortOrder: AssetOrder = AssetOrder.Desc;
|
||||||
percent: number = $state(0);
|
#yearMonth: TimelineYearMonth;
|
||||||
|
#identifier: SegmentIdentifier;
|
||||||
assetsCount: number = $derived(
|
#timelineManager: TimelineManager;
|
||||||
this.isLoaded
|
|
||||||
? this.dayGroups.reduce((accumulator, g) => accumulator + g.viewerAssets.length, 0)
|
|
||||||
: this.#initialCount,
|
|
||||||
);
|
|
||||||
loader: CancellableTask | undefined;
|
|
||||||
isHeightActual: boolean = $state(false);
|
|
||||||
|
|
||||||
readonly monthGroupTitle: string;
|
readonly monthGroupTitle: string;
|
||||||
readonly yearMonth: TimelineYearMonth;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
store: TimelineManager,
|
timelineManager: TimelineManager,
|
||||||
yearMonth: TimelineYearMonth,
|
yearMonth: TimelineYearMonth,
|
||||||
initialCount: number,
|
initialCount: number,
|
||||||
|
loaded: boolean,
|
||||||
order: AssetOrder = AssetOrder.Desc,
|
order: AssetOrder = AssetOrder.Desc,
|
||||||
) {
|
) {
|
||||||
this.timelineManager = store;
|
super();
|
||||||
this.#initialCount = initialCount;
|
this.initialCount = initialCount;
|
||||||
|
this.#yearMonth = yearMonth;
|
||||||
|
this.#identifier = getSegmentIdentifier(yearMonth);
|
||||||
|
this.#timelineManager = timelineManager;
|
||||||
this.#sortOrder = order;
|
this.#sortOrder = order;
|
||||||
|
|
||||||
this.yearMonth = yearMonth;
|
|
||||||
this.monthGroupTitle = formatMonthGroupTitle(fromTimelinePlainYearMonth(yearMonth));
|
this.monthGroupTitle = formatMonthGroupTitle(fromTimelinePlainYearMonth(yearMonth));
|
||||||
|
if (loaded) {
|
||||||
this.loader = new CancellableTask(
|
this.markLoaded();
|
||||||
() => {
|
|
||||||
this.isLoaded = true;
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.dayGroups = [];
|
|
||||||
this.isLoaded = false;
|
|
||||||
},
|
|
||||||
this.#handleLoadError,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
set intersecting(newValue: boolean) {
|
|
||||||
const old = this.#intersecting;
|
|
||||||
if (old === newValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.#intersecting = newValue;
|
|
||||||
if (newValue) {
|
|
||||||
void this.timelineManager.loadMonthGroup(this.yearMonth);
|
|
||||||
} else {
|
|
||||||
this.cancel();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get intersecting() {
|
get identifier() {
|
||||||
return this.#intersecting;
|
return this.#identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get timelineManager() {
|
||||||
|
return this.#timelineManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
get yearMonth() {
|
||||||
|
return this.#yearMonth;
|
||||||
|
}
|
||||||
|
|
||||||
|
load(): Promise<void> {
|
||||||
|
return this.timelineManager.loadSegment(this.#identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
get lastDayGroup() {
|
get lastDayGroup() {
|
||||||
return this.dayGroups.at(-1);
|
return this.dayGroups.at(-1);
|
||||||
}
|
}
|
||||||
@@ -247,61 +224,6 @@ export class MonthGroup {
|
|||||||
return year + '-' + month;
|
return year + '-' + month;
|
||||||
}
|
}
|
||||||
|
|
||||||
set height(height: number) {
|
|
||||||
if (this.#height === height) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { timelineManager: store, percent } = this;
|
|
||||||
const index = store.months.indexOf(this);
|
|
||||||
const heightDelta = height - this.#height;
|
|
||||||
this.#height = height;
|
|
||||||
const prevMonthGroup = store.months[index - 1];
|
|
||||||
if (prevMonthGroup) {
|
|
||||||
const newTop = prevMonthGroup.#top + prevMonthGroup.#height;
|
|
||||||
if (this.#top !== newTop) {
|
|
||||||
this.#top = newTop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let cursor = index + 1; cursor < store.months.length; cursor++) {
|
|
||||||
const monthGroup = this.timelineManager.months[cursor];
|
|
||||||
const newTop = monthGroup.#top + heightDelta;
|
|
||||||
if (monthGroup.#top !== newTop) {
|
|
||||||
monthGroup.#top = newTop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (store.topIntersectingMonthGroup) {
|
|
||||||
const currentIndex = store.months.indexOf(store.topIntersectingMonthGroup);
|
|
||||||
if (currentIndex > 0) {
|
|
||||||
if (index < currentIndex) {
|
|
||||||
store.scrollCompensation = {
|
|
||||||
heightDelta,
|
|
||||||
scrollTop: undefined,
|
|
||||||
monthGroup: this,
|
|
||||||
};
|
|
||||||
} else if (percent > 0) {
|
|
||||||
const top = this.top + height * percent;
|
|
||||||
store.scrollCompensation = {
|
|
||||||
heightDelta: undefined,
|
|
||||||
scrollTop: top,
|
|
||||||
monthGroup: this,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get height() {
|
|
||||||
return this.#height;
|
|
||||||
}
|
|
||||||
|
|
||||||
get top(): number {
|
|
||||||
return this.#top + this.timelineManager.topSectionHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
#handleLoadError(error: unknown) {
|
|
||||||
const _$t = get(t);
|
|
||||||
handleError(error, _$t('errors.failed_to_load_assets'));
|
|
||||||
}
|
|
||||||
|
|
||||||
findDayGroupForAsset(asset: TimelineAsset) {
|
findDayGroupForAsset(asset: TimelineAsset) {
|
||||||
for (const group of this.dayGroups) {
|
for (const group of this.dayGroups) {
|
||||||
@@ -316,7 +238,7 @@ export class MonthGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
findAssetAbsolutePosition(assetId: string) {
|
findAssetAbsolutePosition(assetId: string) {
|
||||||
this.timelineManager.clearDeferredLayout(this);
|
this.#clearDeferredLayout();
|
||||||
for (const group of this.dayGroups) {
|
for (const group of this.dayGroups) {
|
||||||
const viewerAsset = group.viewerAssets.find((viewAsset) => viewAsset.id === assetId);
|
const viewerAsset = group.viewerAssets.find((viewAsset) => viewAsset.id === assetId);
|
||||||
if (viewerAsset) {
|
if (viewerAsset) {
|
||||||
@@ -374,4 +296,26 @@ export class MonthGroup {
|
|||||||
cancel() {
|
cancel() {
|
||||||
this.loader?.cancel();
|
this.loader?.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
layout(noDefer: boolean) {
|
||||||
|
layoutMonthGroup(this.timelineManager, this, noDefer);
|
||||||
|
}
|
||||||
|
|
||||||
|
#clearDeferredLayout() {
|
||||||
|
const hasDeferred = this.dayGroups.some((group) => group.deferredLayout);
|
||||||
|
if (hasDeferred) {
|
||||||
|
updateGeometry(this.timelineManager, this, { invalidateHeight: true, noDefer: true });
|
||||||
|
for (const group of this.dayGroups) {
|
||||||
|
group.deferredLayout = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateIntersection({ intersecting, actuallyIntersecting }: { intersecting: boolean; actuallyIntersecting: boolean }) {
|
||||||
|
this.intersecting = intersecting;
|
||||||
|
this.actuallyIntersecting = actuallyIntersecting;
|
||||||
|
if (intersecting) {
|
||||||
|
this.#clearDeferredLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
||||||
import { getMonthGroupByDate } from '$lib/managers/timeline-manager/internal/search-support.svelte';
|
import { getMonthGroupByDate } from '$lib/managers/timeline-manager/internal/search-support.svelte';
|
||||||
import { AbortError } from '$lib/utils';
|
import { AbortError } from '$lib/utils';
|
||||||
import { fromISODateTimeUTCToObject } from '$lib/utils/timeline-util';
|
import { fromISODateTimeUTCToObject, getSegmentIdentifier } from '$lib/utils/timeline-util';
|
||||||
import { type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
import { type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
||||||
import { timelineAssetFactory, toResponseDto } from '@test-data/factories/asset-factory';
|
import { timelineAssetFactory, toResponseDto } from '@test-data/factories/asset-factory';
|
||||||
import { TimelineManager } from './timeline-manager.svelte';
|
import { TimelineManager } from './timeline-manager.svelte';
|
||||||
@@ -92,7 +92,7 @@ describe('TimelineManager', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('loadMonthGroup', () => {
|
describe('loadSegment', () => {
|
||||||
let timelineManager: TimelineManager;
|
let timelineManager: TimelineManager;
|
||||||
const bucketAssets: Record<string, TimelineAsset[]> = {
|
const bucketAssets: Record<string, TimelineAsset[]> = {
|
||||||
'2024-01-03T00:00:00.000Z': timelineAssetFactory.buildList(1).map((asset) =>
|
'2024-01-03T00:00:00.000Z': timelineAssetFactory.buildList(1).map((asset) =>
|
||||||
@@ -129,46 +129,46 @@ describe('TimelineManager', () => {
|
|||||||
|
|
||||||
it('loads a month', async () => {
|
it('loads a month', async () => {
|
||||||
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(0);
|
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(0);
|
||||||
await timelineManager.loadMonthGroup({ year: 2024, month: 1 });
|
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
||||||
expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
|
expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
|
||||||
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(3);
|
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ignores invalid months', async () => {
|
it('ignores invalid months', async () => {
|
||||||
await timelineManager.loadMonthGroup({ year: 2023, month: 1 });
|
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2023, month: 1 }));
|
||||||
expect(sdkMock.getTimeBucket).toBeCalledTimes(0);
|
expect(sdkMock.getTimeBucket).toBeCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cancels month loading', async () => {
|
it('cancels month loading', async () => {
|
||||||
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })!;
|
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })!;
|
||||||
void timelineManager.loadMonthGroup({ year: 2024, month: 1 });
|
void timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
||||||
const abortSpy = vi.spyOn(month!.loader!.cancelToken!, 'abort');
|
const abortSpy = vi.spyOn(month!.loader!.cancelToken!, 'abort');
|
||||||
month?.cancel();
|
month?.cancel();
|
||||||
expect(abortSpy).toBeCalledTimes(1);
|
expect(abortSpy).toBeCalledTimes(1);
|
||||||
await timelineManager.loadMonthGroup({ year: 2024, month: 1 });
|
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
||||||
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(3);
|
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('prevents loading months multiple times', async () => {
|
it('prevents loading months multiple times', async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
timelineManager.loadMonthGroup({ year: 2024, month: 1 }),
|
timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 })),
|
||||||
timelineManager.loadMonthGroup({ year: 2024, month: 1 }),
|
timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 })),
|
||||||
]);
|
]);
|
||||||
expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
|
expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
|
||||||
|
|
||||||
await timelineManager.loadMonthGroup({ year: 2024, month: 1 });
|
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
||||||
expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
|
expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows loading a canceled month', async () => {
|
it('allows loading a canceled month', async () => {
|
||||||
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })!;
|
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })!;
|
||||||
const loadPromise = timelineManager.loadMonthGroup({ year: 2024, month: 1 });
|
const loadPromise = timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
||||||
|
|
||||||
month.cancel();
|
month.cancel();
|
||||||
await loadPromise;
|
await loadPromise;
|
||||||
expect(month?.getAssets().length).toEqual(0);
|
expect(month?.getAssets().length).toEqual(0);
|
||||||
|
|
||||||
await timelineManager.loadMonthGroup({ year: 2024, month: 1 });
|
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
||||||
expect(month!.getAssets().length).toEqual(3);
|
expect(month!.getAssets().length).toEqual(3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -477,7 +477,7 @@ describe('TimelineManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns previous assetId', async () => {
|
it('returns previous assetId', async () => {
|
||||||
await timelineManager.loadMonthGroup({ year: 2024, month: 1 });
|
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
||||||
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 });
|
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 });
|
||||||
|
|
||||||
const a = month!.getAssets()[0];
|
const a = month!.getAssets()[0];
|
||||||
@@ -487,8 +487,8 @@ describe('TimelineManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns previous assetId spanning multiple months', async () => {
|
it('returns previous assetId spanning multiple months', async () => {
|
||||||
await timelineManager.loadMonthGroup({ year: 2024, month: 2 });
|
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 2 }));
|
||||||
await timelineManager.loadMonthGroup({ year: 2024, month: 3 });
|
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 3 }));
|
||||||
|
|
||||||
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 2 });
|
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 2 });
|
||||||
const previousMonth = getMonthGroupByDate(timelineManager, { year: 2024, month: 3 });
|
const previousMonth = getMonthGroupByDate(timelineManager, { year: 2024, month: 3 });
|
||||||
@@ -499,23 +499,23 @@ describe('TimelineManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('loads previous month', async () => {
|
it('loads previous month', async () => {
|
||||||
await timelineManager.loadMonthGroup({ year: 2024, month: 2 });
|
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 2 }));
|
||||||
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 2 });
|
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 2 });
|
||||||
const previousMonth = getMonthGroupByDate(timelineManager, { year: 2024, month: 3 });
|
const previousMonth = getMonthGroupByDate(timelineManager, { year: 2024, month: 3 });
|
||||||
const a = month!.getFirstAsset();
|
const a = month!.getFirstAsset();
|
||||||
const b = previousMonth!.getFirstAsset();
|
const b = previousMonth!.getFirstAsset();
|
||||||
const loadMonthGroupSpy = vi.spyOn(month!.loader!, 'execute');
|
const loadSegmentSpy = vi.spyOn(month!.loader!, 'execute');
|
||||||
const previousMonthSpy = vi.spyOn(previousMonth!.loader!, 'execute');
|
const previousMonthSpy = vi.spyOn(previousMonth!.loader!, 'execute');
|
||||||
const previous = await timelineManager.getLaterAsset(a);
|
const previous = await timelineManager.getLaterAsset(a);
|
||||||
expect(previous).toEqual(b);
|
expect(previous).toEqual(b);
|
||||||
expect(loadMonthGroupSpy).toBeCalledTimes(0);
|
expect(loadSegmentSpy).toBeCalledTimes(0);
|
||||||
expect(previousMonthSpy).toBeCalledTimes(0);
|
expect(previousMonthSpy).toBeCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('skips removed assets', async () => {
|
it('skips removed assets', async () => {
|
||||||
await timelineManager.loadMonthGroup({ year: 2024, month: 1 });
|
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 1 }));
|
||||||
await timelineManager.loadMonthGroup({ year: 2024, month: 2 });
|
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 2 }));
|
||||||
await timelineManager.loadMonthGroup({ year: 2024, month: 3 });
|
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 3 }));
|
||||||
|
|
||||||
const [assetOne, assetTwo, assetThree] = await getAssets(timelineManager);
|
const [assetOne, assetTwo, assetThree] = await getAssets(timelineManager);
|
||||||
timelineManager.removeAssets([assetTwo.id]);
|
timelineManager.removeAssets([assetTwo.id]);
|
||||||
@@ -523,7 +523,7 @@ describe('TimelineManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns null when no more assets', async () => {
|
it('returns null when no more assets', async () => {
|
||||||
await timelineManager.loadMonthGroup({ year: 2024, month: 3 });
|
await timelineManager.loadSegment(getSegmentIdentifier({ year: 2024, month: 3 }));
|
||||||
expect(await timelineManager.getLaterAsset(timelineManager.months[0].getFirstAsset())).toBeUndefined();
|
expect(await timelineManager.getLaterAsset(timelineManager.months[0].getFirstAsset())).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,12 +3,16 @@ import { AssetOrder, getAssetInfo, getTimeBuckets } from '@immich/sdk';
|
|||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
|
|
||||||
import { CancellableTask } from '$lib/utils/cancellable-task';
|
import { CancellableTask } from '$lib/utils/cancellable-task';
|
||||||
import { toTimelineAsset, type TimelineDateTime, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
import {
|
||||||
|
getSegmentIdentifier,
|
||||||
|
toTimelineAsset,
|
||||||
|
type TimelineDateTime,
|
||||||
|
type TimelineYearMonth,
|
||||||
|
} from '$lib/utils/timeline-util';
|
||||||
|
|
||||||
import { clamp, debounce, isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity';
|
import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||||
|
|
||||||
import { updateIntersectionMonthGroup } from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
|
|
||||||
import { updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte';
|
import { updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte';
|
||||||
import { loadFromTimeBuckets } from '$lib/managers/timeline-manager/internal/load-support.svelte';
|
import { loadFromTimeBuckets } from '$lib/managers/timeline-manager/internal/load-support.svelte';
|
||||||
import {
|
import {
|
||||||
@@ -23,6 +27,8 @@ import {
|
|||||||
retrieveRange as retrieveRangeUtil,
|
retrieveRange as retrieveRangeUtil,
|
||||||
} from '$lib/managers/timeline-manager/internal/search-support.svelte';
|
} from '$lib/managers/timeline-manager/internal/search-support.svelte';
|
||||||
import { WebsocketSupport } from '$lib/managers/timeline-manager/internal/websocket-support.svelte';
|
import { WebsocketSupport } from '$lib/managers/timeline-manager/internal/websocket-support.svelte';
|
||||||
|
import { PhotostreamManager } from '$lib/managers/timeline-manager/PhotostreamManager.svelte';
|
||||||
|
import { PhotostreamSegment } from '$lib/managers/timeline-manager/PhotostreamSegment.svelte';
|
||||||
import { DayGroup } from './day-group.svelte';
|
import { DayGroup } from './day-group.svelte';
|
||||||
import { isMismatched, updateObject } from './internal/utils.svelte';
|
import { isMismatched, updateObject } from './internal/utils.svelte';
|
||||||
import { MonthGroup } from './month-group.svelte';
|
import { MonthGroup } from './month-group.svelte';
|
||||||
@@ -32,29 +38,14 @@ import type {
|
|||||||
Direction,
|
Direction,
|
||||||
ScrubberMonth,
|
ScrubberMonth,
|
||||||
TimelineAsset,
|
TimelineAsset,
|
||||||
TimelineManagerLayoutOptions,
|
|
||||||
TimelineManagerOptions,
|
TimelineManagerOptions,
|
||||||
Viewport,
|
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export class TimelineManager {
|
export class TimelineManager extends PhotostreamManager {
|
||||||
isInitialized = $state(false);
|
|
||||||
months: MonthGroup[] = $state([]);
|
|
||||||
topSectionHeight = $state(0);
|
|
||||||
timelineHeight = $derived(this.months.reduce((accumulator, b) => accumulator + b.height, 0) + this.topSectionHeight);
|
|
||||||
assetCount = $derived(this.months.reduce((accumulator, b) => accumulator + b.assetsCount, 0));
|
|
||||||
|
|
||||||
albumAssets: Set<string> = new SvelteSet();
|
albumAssets: Set<string> = new SvelteSet();
|
||||||
|
|
||||||
scrubberMonths: ScrubberMonth[] = $state([]);
|
scrubberMonths: ScrubberMonth[] = $state([]);
|
||||||
scrubberTimelineHeight: number = $state(0);
|
scrubberTimelineHeight: number = $state(0);
|
||||||
|
#months: MonthGroup[] = $state([]);
|
||||||
topIntersectingMonthGroup: MonthGroup | undefined = $state();
|
|
||||||
|
|
||||||
visibleWindow = $derived.by(() => ({
|
|
||||||
top: this.#scrollTop,
|
|
||||||
bottom: this.#scrollTop + this.viewportHeight,
|
|
||||||
}));
|
|
||||||
|
|
||||||
initTask = new CancellableTask(
|
initTask = new CancellableTask(
|
||||||
() => {
|
() => {
|
||||||
@@ -72,121 +63,12 @@ export class TimelineManager {
|
|||||||
);
|
);
|
||||||
|
|
||||||
static #INIT_OPTIONS = {};
|
static #INIT_OPTIONS = {};
|
||||||
#viewportHeight = $state(0);
|
|
||||||
#viewportWidth = $state(0);
|
|
||||||
#scrollTop = $state(0);
|
|
||||||
#websocketSupport: WebsocketSupport | undefined;
|
#websocketSupport: WebsocketSupport | undefined;
|
||||||
|
|
||||||
#rowHeight = $state(235);
|
|
||||||
#headerHeight = $state(48);
|
|
||||||
#gap = $state(12);
|
|
||||||
|
|
||||||
#options: TimelineManagerOptions = TimelineManager.#INIT_OPTIONS;
|
#options: TimelineManagerOptions = TimelineManager.#INIT_OPTIONS;
|
||||||
|
|
||||||
#scrolling = $state(false);
|
get months() {
|
||||||
#suspendTransitions = $state(false);
|
return this.#months;
|
||||||
#resetScrolling = debounce(() => (this.#scrolling = false), 1000);
|
|
||||||
#resetSuspendTransitions = debounce(() => (this.suspendTransitions = false), 1000);
|
|
||||||
scrollCompensation: {
|
|
||||||
heightDelta: number | undefined;
|
|
||||||
scrollTop: number | undefined;
|
|
||||||
monthGroup: MonthGroup | undefined;
|
|
||||||
} = $state({
|
|
||||||
heightDelta: 0,
|
|
||||||
scrollTop: 0,
|
|
||||||
monthGroup: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
setLayoutOptions({ headerHeight = 48, rowHeight = 235, gap = 12 }: TimelineManagerLayoutOptions) {
|
|
||||||
let changed = false;
|
|
||||||
changed ||= this.#setHeaderHeight(headerHeight);
|
|
||||||
changed ||= this.#setGap(gap);
|
|
||||||
changed ||= this.#setRowHeight(rowHeight);
|
|
||||||
if (changed) {
|
|
||||||
this.refreshLayout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#setHeaderHeight(value: number) {
|
|
||||||
if (this.#headerHeight == value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.#headerHeight = value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
get headerHeight() {
|
|
||||||
return this.#headerHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
#setGap(value: number) {
|
|
||||||
if (this.#gap == value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.#gap = value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
get gap() {
|
|
||||||
return this.#gap;
|
|
||||||
}
|
|
||||||
|
|
||||||
#setRowHeight(value: number) {
|
|
||||||
if (this.#rowHeight == value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.#rowHeight = value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
get rowHeight() {
|
|
||||||
return this.#rowHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
set scrolling(value: boolean) {
|
|
||||||
this.#scrolling = value;
|
|
||||||
if (value) {
|
|
||||||
this.suspendTransitions = true;
|
|
||||||
this.#resetScrolling();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get scrolling() {
|
|
||||||
return this.#scrolling;
|
|
||||||
}
|
|
||||||
|
|
||||||
set suspendTransitions(value: boolean) {
|
|
||||||
this.#suspendTransitions = value;
|
|
||||||
if (value) {
|
|
||||||
this.#resetSuspendTransitions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get suspendTransitions() {
|
|
||||||
return this.#suspendTransitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
set viewportWidth(value: number) {
|
|
||||||
const changed = value !== this.#viewportWidth;
|
|
||||||
this.#viewportWidth = value;
|
|
||||||
this.suspendTransitions = true;
|
|
||||||
void this.#updateViewportGeometry(changed);
|
|
||||||
}
|
|
||||||
|
|
||||||
get viewportWidth() {
|
|
||||||
return this.#viewportWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
set viewportHeight(value: number) {
|
|
||||||
this.#viewportHeight = value;
|
|
||||||
this.#suspendTransitions = true;
|
|
||||||
void this.#updateViewportGeometry(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
get viewportHeight() {
|
|
||||||
return this.#viewportHeight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async *assetsIterator(options?: {
|
async *assetsIterator(options?: {
|
||||||
@@ -198,7 +80,7 @@ export class TimelineManager {
|
|||||||
const direction = options?.direction ?? 'earlier';
|
const direction = options?.direction ?? 'earlier';
|
||||||
let { startDayGroup, startAsset } = options ?? {};
|
let { startDayGroup, startAsset } = options ?? {};
|
||||||
for (const monthGroup of this.monthGroupIterator({ direction, startMonthGroup: options?.startMonthGroup })) {
|
for (const monthGroup of this.monthGroupIterator({ direction, startMonthGroup: options?.startMonthGroup })) {
|
||||||
await this.loadMonthGroup(monthGroup.yearMonth, { cancelable: false });
|
await this.loadSegment(monthGroup.identifier, { cancelable: false });
|
||||||
yield* monthGroup.assetsIterator({ startDayGroup, startAsset, direction });
|
yield* monthGroup.assetsIterator({ startDayGroup, startAsset, direction });
|
||||||
startDayGroup = startAsset = undefined;
|
startDayGroup = startAsset = undefined;
|
||||||
}
|
}
|
||||||
@@ -234,75 +116,24 @@ export class TimelineManager {
|
|||||||
this.#websocketSupport = undefined;
|
this.#websocketSupport = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSlidingWindow(scrollTop: number) {
|
|
||||||
if (this.#scrollTop !== scrollTop) {
|
|
||||||
this.#scrollTop = scrollTop;
|
|
||||||
this.updateIntersections();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearScrollCompensation() {
|
|
||||||
this.scrollCompensation = {
|
|
||||||
heightDelta: undefined,
|
|
||||||
scrollTop: undefined,
|
|
||||||
monthGroup: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
updateIntersections() {
|
|
||||||
if (!this.isInitialized || this.visibleWindow.bottom === this.visibleWindow.top) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let topIntersectingMonthGroup = undefined;
|
|
||||||
for (const month of this.months) {
|
|
||||||
updateIntersectionMonthGroup(this, month);
|
|
||||||
if (!topIntersectingMonthGroup && month.actuallyIntersecting) {
|
|
||||||
topIntersectingMonthGroup = month;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (topIntersectingMonthGroup !== undefined && this.topIntersectingMonthGroup !== topIntersectingMonthGroup) {
|
|
||||||
this.topIntersectingMonthGroup = topIntersectingMonthGroup;
|
|
||||||
}
|
|
||||||
for (const month of this.months) {
|
|
||||||
if (month === this.topIntersectingMonthGroup) {
|
|
||||||
this.topIntersectingMonthGroup.percent = clamp(
|
|
||||||
(this.visibleWindow.top - this.topIntersectingMonthGroup.top) / this.topIntersectingMonthGroup.height,
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
month.percent = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearDeferredLayout(month: MonthGroup) {
|
|
||||||
const hasDeferred = month.dayGroups.some((group) => group.deferredLayout);
|
|
||||||
if (hasDeferred) {
|
|
||||||
updateGeometry(this, month, { invalidateHeight: true, noDefer: true });
|
|
||||||
for (const group of month.dayGroups) {
|
|
||||||
group.deferredLayout = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async #initializeMonthGroups() {
|
async #initializeMonthGroups() {
|
||||||
const timebuckets = await getTimeBuckets({
|
const timebuckets = await getTimeBuckets({
|
||||||
...authManager.params,
|
...authManager.params,
|
||||||
...this.#options,
|
...this.#options,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.months = timebuckets.map((timeBucket) => {
|
this.#months = timebuckets.map((timeBucket) => {
|
||||||
const date = new SvelteDate(timeBucket.timeBucket);
|
const date = new SvelteDate(timeBucket.timeBucket);
|
||||||
return new MonthGroup(
|
return new MonthGroup(
|
||||||
this,
|
this,
|
||||||
{ year: date.getUTCFullYear(), month: date.getUTCMonth() + 1 },
|
{ year: date.getUTCFullYear(), month: date.getUTCMonth() + 1 },
|
||||||
timeBucket.count,
|
timeBucket.count,
|
||||||
|
false,
|
||||||
this.#options.order,
|
this.#options.order,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this.albumAssets.clear();
|
this.albumAssets.clear();
|
||||||
this.#updateViewportGeometry(false);
|
this.updateViewportGeometry(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateOptions(options: TimelineManagerOptions) {
|
async updateOptions(options: TimelineManagerOptions) {
|
||||||
@@ -313,13 +144,13 @@ export class TimelineManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.initTask.reset();
|
await this.initTask.reset();
|
||||||
await this.#init(options);
|
await this.init(options);
|
||||||
this.#updateViewportGeometry(false);
|
this.updateViewportGeometry(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async #init(options: TimelineManagerOptions) {
|
async init(options: TimelineManagerOptions) {
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
this.months = [];
|
this.#months = [];
|
||||||
this.albumAssets.clear();
|
this.albumAssets.clear();
|
||||||
await this.initTask.execute(async () => {
|
await this.initTask.execute(async () => {
|
||||||
this.#options = options;
|
this.#options = options;
|
||||||
@@ -332,36 +163,8 @@ export class TimelineManager {
|
|||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateViewport(viewport: Viewport) {
|
updateViewportGeometry(changedWidth: boolean) {
|
||||||
if (viewport.height === 0 && viewport.width === 0) {
|
super.updateViewportGeometry(changedWidth);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.viewportHeight === viewport.height && this.viewportWidth === viewport.width) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.initTask.executed) {
|
|
||||||
await (this.initTask.loading ? this.initTask.waitUntilCompletion() : this.#init(this.#options));
|
|
||||||
}
|
|
||||||
|
|
||||||
const changedWidth = viewport.width !== this.viewportWidth;
|
|
||||||
this.viewportHeight = viewport.height;
|
|
||||||
this.viewportWidth = viewport.width;
|
|
||||||
this.#updateViewportGeometry(changedWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
#updateViewportGeometry(changedWidth: boolean) {
|
|
||||||
if (!this.isInitialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.viewportWidth === 0 || this.viewportHeight === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const month of this.months) {
|
|
||||||
updateGeometry(this, month, { invalidateHeight: changedWidth });
|
|
||||||
}
|
|
||||||
this.updateIntersections();
|
|
||||||
this.#createScrubberMonths();
|
this.#createScrubberMonths();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,37 +179,8 @@ export class TimelineManager {
|
|||||||
this.scrubberTimelineHeight = this.timelineHeight;
|
this.scrubberTimelineHeight = this.timelineHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
createLayoutOptions() {
|
protected fetchSegment(segment: PhotostreamSegment, signal: AbortSignal): Promise<void> {
|
||||||
const viewportWidth = this.viewportWidth;
|
return loadFromTimeBuckets(this, segment as MonthGroup, this.#options, signal);
|
||||||
|
|
||||||
return {
|
|
||||||
spacing: 2,
|
|
||||||
heightTolerance: 0.15,
|
|
||||||
rowHeight: this.#rowHeight,
|
|
||||||
rowWidth: Math.floor(viewportWidth),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadMonthGroup(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }): Promise<void> {
|
|
||||||
let cancelable = true;
|
|
||||||
if (options) {
|
|
||||||
cancelable = options.cancelable;
|
|
||||||
}
|
|
||||||
const monthGroup = getMonthGroupByDate(this, yearMonth);
|
|
||||||
if (!monthGroup) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (monthGroup.loader?.executed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await monthGroup.loader?.execute(async (signal: AbortSignal) => {
|
|
||||||
await loadFromTimeBuckets(this, monthGroup, this.#options, signal);
|
|
||||||
}, cancelable);
|
|
||||||
if (result === 'LOADED') {
|
|
||||||
updateIntersectionMonthGroup(this, monthGroup);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addAssets(assets: TimelineAsset[]) {
|
addAssets(assets: TimelineAsset[]) {
|
||||||
@@ -442,7 +216,7 @@ export class TimelineManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async #loadMonthGroupAtTime(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }) {
|
async #loadMonthGroupAtTime(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }) {
|
||||||
await this.loadMonthGroup(yearMonth, options);
|
await this.loadSegment(getSegmentIdentifier(yearMonth), options);
|
||||||
return getMonthGroupByDate(this, yearMonth);
|
return getMonthGroupByDate(this, yearMonth);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,7 +228,7 @@ export class TimelineManager {
|
|||||||
async getRandomMonthGroup() {
|
async getRandomMonthGroup() {
|
||||||
const random = Math.floor(Math.random() * this.months.length);
|
const random = Math.floor(Math.random() * this.months.length);
|
||||||
const month = this.months[random];
|
const month = this.months[random];
|
||||||
await this.loadMonthGroup(month.yearMonth, { cancelable: false });
|
await this.loadSegment(getSegmentIdentifier(month.yearMonth), { cancelable: false });
|
||||||
return month;
|
return month;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,7 +301,7 @@ export class TimelineManager {
|
|||||||
if (!monthGroup) {
|
if (!monthGroup) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.loadMonthGroup(dateTime, { cancelable: false });
|
await this.loadSegment(getSegmentIdentifier(dateTime), { cancelable: false });
|
||||||
const asset = monthGroup.findClosest(dateTime);
|
const asset = monthGroup.findClosest(dateTime);
|
||||||
if (asset) {
|
if (asset) {
|
||||||
return asset;
|
return asset;
|
||||||
|
|||||||
@@ -513,7 +513,7 @@ export const selectAllAssets = async (timelineManager: TimelineManager, assetInt
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
for (const monthGroup of timelineManager.months) {
|
for (const monthGroup of timelineManager.months) {
|
||||||
await timelineManager.loadMonthGroup(monthGroup.yearMonth);
|
await timelineManager.loadSegment(monthGroup.identifier);
|
||||||
|
|
||||||
if (!get(isSelectingAllAssets)) {
|
if (!get(isSelectingAllAssets)) {
|
||||||
assetInteraction.clearMultiselect();
|
assetInteraction.clearMultiselect();
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ export type ScrubberListener = (
|
|||||||
scrubberMonthScrollPercent: number,
|
scrubberMonthScrollPercent: number,
|
||||||
) => void | Promise<void>;
|
) => void | Promise<void>;
|
||||||
|
|
||||||
|
export const getSegmentIdentifier = (yearMonth: TimelineYearMonth | TimelineDateTime) => ({
|
||||||
|
matches(segment: any) {
|
||||||
|
return segment.yearMonth && segment.yearMonth.year === yearMonth.year && segment.yearMonth.month === yearMonth.month;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// used for AssetResponseDto.dateTimeOriginal, amongst others
|
// used for AssetResponseDto.dateTimeOriginal, amongst others
|
||||||
export const fromISODateTime = (isoDateTime: string, timeZone: string): DateTime<true> =>
|
export const fromISODateTime = (isoDateTime: string, timeZone: string): DateTime<true> =>
|
||||||
DateTime.fromISO(isoDateTime, { zone: timeZone, locale: get(locale) }) as DateTime<true>;
|
DateTime.fromISO(isoDateTime, { zone: timeZone, locale: get(locale) }) as DateTime<true>;
|
||||||
|
|||||||
Reference in New Issue
Block a user