Push up operations to VirtualScrollManager
parent
de84e46f62
commit
12a59f8c68
|
|
@ -174,7 +174,7 @@
|
|||
};
|
||||
|
||||
const scrollAndLoadAsset = async (assetId: string) => {
|
||||
const month = await timelineManager.findMonthForAsset(assetId);
|
||||
const month = await timelineManager.search.getMonthForAsset(assetId);
|
||||
if (!month) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { TimelineAsset, UpdateGeometryOptions } from '$lib/managers/timeline-manager/types';
|
||||
import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
|
||||
import type {
|
||||
AssetOperation,
|
||||
VirtualScrollManager,
|
||||
VisibleWindow,
|
||||
} from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte';
|
||||
|
|
@ -196,6 +197,36 @@ export abstract class ScrollSegment {
|
|||
}
|
||||
this.updateIntersection({ intersecting: actuallyIntersecting || preIntersecting, actuallyIntersecting });
|
||||
}
|
||||
|
||||
runAssetOperation(ids: Set<string>, operation: AssetOperation) {
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const unprocessedIds = new Set<string>(ids);
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const processedIds = new Set<string>();
|
||||
const moveAssets: TimelineAsset[] = [];
|
||||
let changedGeometry = false;
|
||||
for (const assetId of unprocessedIds) {
|
||||
const index = this.viewerAssets.findIndex((viewAsset) => viewAsset.id == assetId);
|
||||
if (index === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const asset = this.viewerAssets[index].asset!;
|
||||
const opResult = operation(asset);
|
||||
let remove = false;
|
||||
if (opResult) {
|
||||
remove = (opResult as { remove: boolean }).remove ?? false;
|
||||
}
|
||||
|
||||
unprocessedIds.delete(assetId);
|
||||
processedIds.add(assetId);
|
||||
if (remove || this.scrollManager.isExcluded(asset)) {
|
||||
this.viewerAssets.splice(index, 1);
|
||||
changedGeometry = true;
|
||||
}
|
||||
}
|
||||
return { moveAssets, processedIds, unprocessedIds, changedGeometry };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
import type { Viewport } from '$lib/managers/timeline-manager/types';
|
||||
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
||||
import { setDifferenceInPlace } from '$lib/managers/timeline-manager/utils.svelte';
|
||||
import type { ScrollSegment, SegmentIdentifier } from '$lib/managers/VirtualScrollManager/ScrollSegment.svelte';
|
||||
import { updateObject } from '$lib/managers/VirtualScrollManager/utils.svelte';
|
||||
|
||||
import { clamp, debounce } from 'lodash-es';
|
||||
|
||||
export type VisibleWindow = {
|
||||
top: number;
|
||||
bottom: number;
|
||||
};
|
||||
export type AssetOperation = (asset: TimelineAsset) => unknown;
|
||||
|
||||
type LayoutOptions = {
|
||||
headerHeight: number;
|
||||
rowHeight: number;
|
||||
gap: number;
|
||||
};
|
||||
|
||||
export type VisibleWindow = {
|
||||
top: number;
|
||||
bottom: number;
|
||||
};
|
||||
|
||||
type ViewportTopSegmentIntersection = {
|
||||
segment: ScrollSegment | null;
|
||||
// Where viewport top intersects segment (0 = segment top, 1 = segment bottom)
|
||||
|
|
@ -278,6 +280,112 @@ export abstract class VirtualScrollManager {
|
|||
|
||||
await segment.load(cancelable);
|
||||
}
|
||||
|
||||
upsertAssets(assets: TimelineAsset[]) {
|
||||
const notExcluded = assets.filter((asset) => !this.isExcluded(asset));
|
||||
const notUpdated = this.#updateAssets(notExcluded);
|
||||
this.addAssetsToSegments(notUpdated);
|
||||
}
|
||||
|
||||
removeAssets(ids: string[]) {
|
||||
this.#runAssetOperation(ids, () => ({ remove: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given operation against every passed in asset id.
|
||||
*
|
||||
* @returns An object with the changed ids, unprocessed ids, and if this resulted
|
||||
* in changes of the timeline geometry.
|
||||
*/
|
||||
updateAssetOperation(ids: string[], operation: AssetOperation) {
|
||||
return this.#runAssetOperation(ids, operation);
|
||||
}
|
||||
|
||||
isExcluded(_: TimelineAsset) {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected addAssetsToSegments(assets: TimelineAsset[]) {
|
||||
if (assets.length === 0) {
|
||||
return;
|
||||
}
|
||||
const context = this.createUpsertContext();
|
||||
const monthCount = this.segments.length;
|
||||
for (const asset of assets) {
|
||||
this.upsertAssetIntoSegment(asset, context);
|
||||
}
|
||||
if (this.segments.length !== monthCount) {
|
||||
this.postCreateSegments();
|
||||
}
|
||||
this.postUpsert(context);
|
||||
this.updateIntersections();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected upsertAssetIntoSegment(asset: TimelineAsset, context: unknown): void {}
|
||||
protected createUpsertContext(): unknown {
|
||||
return undefined;
|
||||
}
|
||||
protected postUpsert(_: unknown): void {}
|
||||
protected postCreateSegments(): void {}
|
||||
|
||||
/**
|
||||
* Looks up the specified asset from the TimelineAsset using its id, and then updates the
|
||||
* existing object to match the rest of the TimelineAsset parameter.
|
||||
|
||||
* @returns list of assets that were updated (not found)
|
||||
*/
|
||||
#updateAssets(updatedAssets: TimelineAsset[]) {
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const lookup = new Map<string, TimelineAsset>();
|
||||
const ids = [];
|
||||
for (const asset of updatedAssets) {
|
||||
ids.push(asset.id);
|
||||
lookup.set(asset.id, asset);
|
||||
}
|
||||
const { unprocessedIds } = this.#runAssetOperation(ids, (asset) => updateObject(asset, lookup.get(asset.id)));
|
||||
const result: TimelineAsset[] = [];
|
||||
for (const id of unprocessedIds) {
|
||||
result.push(lookup.get(id)!);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#runAssetOperation(ids: string[], operation: AssetOperation) {
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const changedMonths = new Set<ScrollSegment>();
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const idsToProcess = new Set(ids);
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const idsProcessed = new Set<string>();
|
||||
const combinedMoveAssets: TimelineAsset[] = [];
|
||||
for (const month of this.segments) {
|
||||
if (idsToProcess.size > 0) {
|
||||
const { moveAssets, processedIds, changedGeometry } = month.runAssetOperation(idsToProcess, operation);
|
||||
if (moveAssets.length > 0) {
|
||||
combinedMoveAssets.push(...moveAssets);
|
||||
}
|
||||
setDifferenceInPlace(idsToProcess, processedIds);
|
||||
for (const id of processedIds) {
|
||||
idsProcessed.add(id);
|
||||
}
|
||||
if (changedGeometry) {
|
||||
changedMonths.add(month);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (combinedMoveAssets.length > 0) {
|
||||
this.addAssetsToSegments(combinedMoveAssets);
|
||||
}
|
||||
const changedGeometry = changedMonths.size > 0;
|
||||
for (const month of changedMonths) {
|
||||
month.updateGeometry({ invalidateHeight: true });
|
||||
}
|
||||
if (changedGeometry) {
|
||||
this.updateIntersections();
|
||||
}
|
||||
return { unprocessedIds: idsToProcess, processedIds: idsProcessed, changedGeometry };
|
||||
}
|
||||
}
|
||||
|
||||
export const isEmptyViewport = (viewport: Viewport) => viewport.width === 0 || viewport.height === 0;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
// 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 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;
|
||||
}
|
||||
|
|
@ -105,15 +105,6 @@ export class TimelineDay {
|
|||
}
|
||||
|
||||
runAssetOperation(ids: Set<string>, operation: AssetOperation) {
|
||||
if (ids.size === 0) {
|
||||
return {
|
||||
moveAssets: [] as TimelineAsset[],
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
processedIds: new Set<string>(),
|
||||
unprocessedIds: ids,
|
||||
changedGeometry: false,
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const unprocessedIds = new Set<string>(ids);
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
|
|
|
|||
|
|
@ -6,14 +6,13 @@ import { TimelineMonth } from '$lib/managers/timeline-manager/TimelineMonth.svel
|
|||
import { TimelineSearchExtension } from '$lib/managers/timeline-manager/TimelineSearchExtension.svelte';
|
||||
import { TimelineWebsocketExtension } from '$lib/managers/timeline-manager/TimelineWebsocketExtension';
|
||||
import type {
|
||||
AssetOperation,
|
||||
Direction,
|
||||
ScrubberMonth,
|
||||
TimelineAsset,
|
||||
TimelineManagerOptions,
|
||||
Viewport,
|
||||
} from '$lib/managers/timeline-manager/types';
|
||||
import { isMismatched, setDifferenceInPlace, updateObject } from '$lib/managers/timeline-manager/utils.svelte';
|
||||
import { isMismatched } from '$lib/managers/timeline-manager/utils.svelte';
|
||||
import { CancellableTask } from '$lib/utils/cancellable-task';
|
||||
import { getSegmentIdentifier } from '$lib/utils/timeline-util';
|
||||
import { AssetOrder, getTimeBuckets } from '@immich/sdk';
|
||||
|
|
@ -121,53 +120,11 @@ export class TimelineManager extends VirtualScrollManager {
|
|||
this.onUpdateViewport(oldViewport, viewport);
|
||||
}
|
||||
|
||||
upsertAssets(assets: TimelineAsset[]) {
|
||||
const notExcluded = assets.filter((asset) => !this.isExcluded(asset));
|
||||
const notUpdated = this.#updateAssets(notExcluded);
|
||||
this.addAssetsToSegments(notUpdated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given operation against every passed in asset id.
|
||||
*
|
||||
* @returns An object with the changed ids, unprocessed ids, and if this resulted
|
||||
* in changes of the timeline geometry.
|
||||
*/
|
||||
updateAssetOperation(ids: string[], operation: AssetOperation) {
|
||||
return this.#runAssetOperation(ids, operation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the specified asset from the TimelineAsset using its id, and then updates the
|
||||
* existing object to match the rest of the TimelineAsset parameter.
|
||||
|
||||
* @returns list of assets that were updated (not found)
|
||||
*/
|
||||
#updateAssets(updatedAssets: TimelineAsset[]) {
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const lookup = new Map<string, TimelineAsset>();
|
||||
const ids = [];
|
||||
for (const asset of updatedAssets) {
|
||||
ids.push(asset.id);
|
||||
lookup.set(asset.id, asset);
|
||||
}
|
||||
const { unprocessedIds } = this.#runAssetOperation(ids, (asset) => updateObject(asset, lookup.get(asset.id)));
|
||||
const result: TimelineAsset[] = [];
|
||||
for (const id of unprocessedIds) {
|
||||
result.push(lookup.get(id)!);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
removeAssets(ids: string[]) {
|
||||
this.#runAssetOperation(ids, () => ({ remove: true }));
|
||||
}
|
||||
|
||||
protected createUpsertContext(): GroupInsertionCache {
|
||||
protected override createUpsertContext(): GroupInsertionCache {
|
||||
return new GroupInsertionCache();
|
||||
}
|
||||
|
||||
protected upsertAssetIntoSegment(asset: TimelineAsset, context: GroupInsertionCache): void {
|
||||
protected override upsertAssetIntoSegment(asset: TimelineAsset, context: GroupInsertionCache): void {
|
||||
let month = this.search.findMonthByDate(asset.localDateTime);
|
||||
|
||||
if (!month) {
|
||||
|
|
@ -178,64 +135,30 @@ export class TimelineManager extends VirtualScrollManager {
|
|||
month.addTimelineAsset(asset, context);
|
||||
}
|
||||
|
||||
protected addAssetsToSegments(assets: TimelineAsset[]) {
|
||||
if (assets.length === 0) {
|
||||
return;
|
||||
}
|
||||
const context = this.createUpsertContext();
|
||||
const monthCount = this.segments.length;
|
||||
for (const asset of assets) {
|
||||
this.upsertAssetIntoSegment(asset, context);
|
||||
}
|
||||
if (this.segments.length !== monthCount) {
|
||||
this.postCreateSegments();
|
||||
}
|
||||
this.postUpsert(context);
|
||||
this.updateIntersections();
|
||||
protected override postCreateSegments(): void {
|
||||
this.segments.sort((a, b) => {
|
||||
return a.yearMonth.year === b.yearMonth.year
|
||||
? b.yearMonth.month - a.yearMonth.month
|
||||
: b.yearMonth.year - a.yearMonth.year;
|
||||
});
|
||||
}
|
||||
|
||||
#runAssetOperation(ids: string[], operation: AssetOperation) {
|
||||
if (ids.length === 0) {
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
return { processedIds: new Set<string>(), unprocessedIds: new Set<string>(), changedGeometry: false };
|
||||
protected override postUpsert(context: GroupInsertionCache): void {
|
||||
for (const group of context.existingDays) {
|
||||
group.sortAssets(this.#options.order);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const changedMonths = new Set<TimelineMonth>();
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const idsToProcess = new Set(ids);
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const idsProcessed = new Set<string>();
|
||||
const combinedMoveAssets: TimelineAsset[] = [];
|
||||
for (const month of this.segments) {
|
||||
if (idsToProcess.size > 0) {
|
||||
const { moveAssets, processedIds, changedGeometry } = month.runAssetOperation(idsToProcess, operation);
|
||||
if (moveAssets.length > 0) {
|
||||
combinedMoveAssets.push(...moveAssets);
|
||||
}
|
||||
setDifferenceInPlace(idsToProcess, processedIds);
|
||||
for (const id of processedIds) {
|
||||
idsProcessed.add(id);
|
||||
}
|
||||
if (changedGeometry) {
|
||||
changedMonths.add(month);
|
||||
}
|
||||
}
|
||||
for (const month of context.monthsWithNewDays) {
|
||||
month.sortDays();
|
||||
}
|
||||
if (combinedMoveAssets.length > 0) {
|
||||
this.addAssetsToSegments(combinedMoveAssets);
|
||||
}
|
||||
const changedGeometry = changedMonths.size > 0;
|
||||
for (const month of changedMonths) {
|
||||
|
||||
for (const month of context.updatedMonths) {
|
||||
month.sortDays();
|
||||
month.updateGeometry({ invalidateHeight: true });
|
||||
}
|
||||
if (changedGeometry) {
|
||||
this.updateIntersections();
|
||||
}
|
||||
return { unprocessedIds: idsToProcess, processedIds: idsProcessed, changedGeometry };
|
||||
}
|
||||
|
||||
isExcluded(asset: TimelineAsset) {
|
||||
override isExcluded(asset: TimelineAsset) {
|
||||
return (
|
||||
isMismatched(this.#options.visibility, asset.visibility) ||
|
||||
isMismatched(this.#options.isFavorite, asset.isFavorite) ||
|
||||
|
|
@ -318,27 +241,4 @@ export class TimelineManager extends VirtualScrollManager {
|
|||
}));
|
||||
this.scrubberTimelineHeight = this.totalViewerHeight;
|
||||
}
|
||||
|
||||
protected postCreateSegments(): void {
|
||||
this.segments.sort((a, b) => {
|
||||
return a.yearMonth.year === b.yearMonth.year
|
||||
? b.yearMonth.month - a.yearMonth.month
|
||||
: b.yearMonth.year - a.yearMonth.year;
|
||||
});
|
||||
}
|
||||
|
||||
protected postUpsert(context: GroupInsertionCache): void {
|
||||
for (const group of context.existingDays) {
|
||||
group.sortAssets(this.#options.order);
|
||||
}
|
||||
|
||||
for (const month of context.monthsWithNewDays) {
|
||||
month.sortDays();
|
||||
}
|
||||
|
||||
for (const month of context.updatedMonths) {
|
||||
month.sortDays();
|
||||
month.updateGeometry({ invalidateHeight: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ 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 type { AssetDescriptor, Direction, TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
|
||||
import { ScrollSegment, type SegmentIdentifier } from '$lib/managers/VirtualScrollManager/ScrollSegment.svelte';
|
||||
import { setDifferenceInPlace } from '$lib/managers/VirtualScrollManager/utils.svelte';
|
||||
import type { AssetOperation } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte';
|
||||
import {
|
||||
formatGroupTitle,
|
||||
formatGroupTitleFull,
|
||||
|
|
@ -68,7 +69,7 @@ export class TimelineMonth extends ScrollSegment {
|
|||
return assets;
|
||||
}
|
||||
|
||||
findAssetAbsolutePosition(assetId: string) {
|
||||
override findAssetAbsolutePosition(assetId: string) {
|
||||
this.#clearDeferredLayout();
|
||||
for (const day of this.days) {
|
||||
const viewerAsset = day.viewerAssets.find((viewAsset) => viewAsset.id === assetId);
|
||||
|
|
@ -206,16 +207,7 @@ export class TimelineMonth extends ScrollSegment {
|
|||
return this.days[0]?.getFirstAsset();
|
||||
}
|
||||
|
||||
runAssetOperation(ids: Set<string>, operation: AssetOperation) {
|
||||
if (ids.size === 0) {
|
||||
return {
|
||||
moveAssets: [] as TimelineAsset[],
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
processedIds: new Set<string>(),
|
||||
unprocessedIds: ids,
|
||||
changedGeometry: false,
|
||||
};
|
||||
}
|
||||
override runAssetOperation(ids: Set<string>, operation: AssetOperation) {
|
||||
const { days } = this;
|
||||
let combinedChangedGeometry = false;
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
|
|
@ -228,6 +220,7 @@ export class TimelineMonth extends ScrollSegment {
|
|||
if (idsToProcess.size > 0) {
|
||||
const group = days[index];
|
||||
const { moveAssets, processedIds, changedGeometry } = group.runAssetOperation(ids, operation);
|
||||
|
||||
if (moveAssets.length > 0) {
|
||||
combinedMoveAssets.push(...moveAssets);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue