refactor(web): move timeline-manager ops back into month-group; clean up API

Consolidates asset operation logic within TimelineManager class and removes the now redundant 
operations-support.svelte.ts file. 

Combines addAsset/updateAsset to be upsertAsset. 


Changes:
- Move `addAssetsToMonthGroups` logic into TimelineManager's `addAssetsToSegments`, `upsertAssetIntoSegment`, `postCreateSegments`, and `postUpsert` methods
- Move `runAssetOperation` from operations-support into TimelineManager's private `#runAssetOperation` method
- Rename public `addAssets`/`updateAssets` methods to unified `upsertAssets` for consistency
- Delete internal/operations-support.svelte.ts
- Update WebsocketSupport to use `upsertAssets` for both add and update operations
- Fix AssetOperation return type to allow undefined/void operations (not just `{ remove: boolean }`)
- Update MonthGroup constructor to accept `loaded` parameter for better initialization control
- Update all test references from `addAssets`/`updateAssets` to `upsertAssets`

This refactoring improves code maintainability by eliminating duplicate logic and consolidating all asset operations within the TimelineManager class where they belong.
This commit is contained in:
midzelis
2025-10-21 19:18:35 +00:00
parent c76324c611
commit 8e97c584cf
15 changed files with 168 additions and 185 deletions

View File

@@ -110,13 +110,9 @@
case AssetAction.ARCHIVE: case AssetAction.ARCHIVE:
case AssetAction.UNARCHIVE: case AssetAction.UNARCHIVE:
case AssetAction.FAVORITE: case AssetAction.FAVORITE:
case AssetAction.UNFAVORITE: { case AssetAction.UNFAVORITE:
timelineManager.updateAssets([action.asset]);
break;
}
case AssetAction.ADD: { case AssetAction.ADD: {
timelineManager.addAssets([action.asset]); timelineManager.upsertAssets([action.asset]);
break; break;
} }
@@ -135,7 +131,7 @@
break; break;
} }
case AssetAction.REMOVE_ASSET_FROM_STACK: { case AssetAction.REMOVE_ASSET_FROM_STACK: {
timelineManager.addAssets([toTimelineAsset(action.asset)]); timelineManager.upsertAssets([toTimelineAsset(action.asset)]);
if (action.stack) { if (action.stack) {
//Have to unstack then restack assets in timeline in order to update the stack count in the timeline. //Have to unstack then restack assets in timeline in order to update the stack count in the timeline.
updateUnstackedAssetInTimeline( updateUnstackedAssetInTimeline(

View File

@@ -46,7 +46,7 @@
!(isTrashEnabled && !force), !(isTrashEnabled && !force),
(assetIds) => timelineManager.removeAssets(assetIds), (assetIds) => timelineManager.removeAssets(assetIds),
assetInteraction.selectedAssets, assetInteraction.selectedAssets,
!isTrashEnabled || force ? undefined : (assets) => timelineManager.addAssets(assets), !isTrashEnabled || force ? undefined : (assets) => timelineManager.upsertAssets(assets),
); );
assetInteraction.clearMultiselect(); assetInteraction.clearMultiselect();
}; };

View File

@@ -122,7 +122,11 @@ export class DayGroup {
const asset = this.viewerAssets[index].asset!; const asset = this.viewerAssets[index].asset!;
const oldTime = { ...asset.localDateTime }; const oldTime = { ...asset.localDateTime };
let { remove } = operation(asset); const opResult = operation(asset);
let remove = false;
if (opResult) {
remove = (opResult as { remove: boolean }).remove ?? false;
}
const newTime = asset.localDateTime; const newTime = asset.localDateTime;
if (oldTime.year !== newTime.year || oldTime.month !== newTime.month || oldTime.day !== newTime.day) { if (oldTime.year !== newTime.year || oldTime.month !== newTime.month || oldTime.day !== newTime.day) {
const { year, month, day } = newTime; const { year, month, day } = newTime;

View File

@@ -1,104 +0,0 @@
import { setDifference, type TimelineDate } from '$lib/utils/timeline-util';
import { AssetOrder } from '@immich/sdk';
import { SvelteSet } from 'svelte/reactivity';
import { GroupInsertionCache } from '../group-insertion-cache.svelte';
import { MonthGroup } from '../month-group.svelte';
import type { TimelineManager } from '../timeline-manager.svelte';
import type { AssetOperation, TimelineAsset } from '../types';
import { updateGeometry } from './layout-support.svelte';
import { getMonthGroupByDate } from './search-support.svelte';
export function addAssetsToMonthGroups(
timelineManager: TimelineManager,
assets: TimelineAsset[],
options: { order: AssetOrder },
) {
if (assets.length === 0) {
return;
}
const addContext = new GroupInsertionCache();
const updatedMonthGroups = new SvelteSet<MonthGroup>();
const monthCount = timelineManager.months.length;
for (const asset of assets) {
let month = getMonthGroupByDate(timelineManager, asset.localDateTime);
if (!month) {
month = new MonthGroup(timelineManager, asset.localDateTime, 1, options.order);
month.isLoaded = true;
timelineManager.months.push(month);
}
month.addTimelineAsset(asset, addContext);
updatedMonthGroups.add(month);
}
if (timelineManager.months.length !== monthCount) {
timelineManager.months.sort((a, b) => {
return a.yearMonth.year === b.yearMonth.year
? b.yearMonth.month - a.yearMonth.month
: b.yearMonth.year - a.yearMonth.year;
});
}
for (const group of addContext.existingDayGroups) {
group.sortAssets(options.order);
}
for (const monthGroup of addContext.bucketsWithNewDayGroups) {
monthGroup.sortDayGroups();
}
for (const month of addContext.updatedBuckets) {
month.sortDayGroups();
updateGeometry(timelineManager, month, { invalidateHeight: true });
}
timelineManager.updateIntersections();
}
export function runAssetOperation(
timelineManager: TimelineManager,
ids: Set<string>,
operation: AssetOperation,
options: { order: AssetOrder },
) {
if (ids.size === 0) {
return { processedIds: new SvelteSet(), unprocessedIds: ids, changedGeometry: false };
}
const changedMonthGroups = new SvelteSet<MonthGroup>();
let idsToProcess = new SvelteSet(ids);
const idsProcessed = new SvelteSet<string>();
const combinedMoveAssets: { asset: TimelineAsset; date: TimelineDate }[][] = [];
for (const month of timelineManager.months) {
if (idsToProcess.size > 0) {
const { moveAssets, processedIds, changedGeometry } = month.runAssetOperation(idsToProcess, operation);
if (moveAssets.length > 0) {
combinedMoveAssets.push(moveAssets);
}
idsToProcess = setDifference(idsToProcess, processedIds);
for (const id of processedIds) {
idsProcessed.add(id);
}
if (changedGeometry) {
changedMonthGroups.add(month);
}
}
}
if (combinedMoveAssets.length > 0) {
addAssetsToMonthGroups(
timelineManager,
combinedMoveAssets.flat().map((a) => a.asset),
options,
);
}
const changedGeometry = changedMonthGroups.size > 0;
for (const month of changedMonthGroups) {
updateGeometry(timelineManager, month, { invalidateHeight: true });
}
if (changedGeometry) {
timelineManager.updateIntersections();
}
return { unprocessedIds: idsToProcess, processedIds: idsProcessed, changedGeometry };
}

View File

@@ -13,10 +13,10 @@ export class WebsocketSupport {
#processPendingChanges = throttle(() => { #processPendingChanges = throttle(() => {
const { add, update, remove } = this.#getPendingChangeBatches(); const { add, update, remove } = this.#getPendingChangeBatches();
if (add.length > 0) { if (add.length > 0) {
this.#timelineManager.addAssets(add); this.#timelineManager.upsertAssets(add);
} }
if (update.length > 0) { if (update.length > 0) {
this.#timelineManager.updateAssets(update); this.#timelineManager.upsertAssets(update);
} }
if (remove.length > 0) { if (remove.length > 0) {
this.#timelineManager.removeAssets(remove); this.#timelineManager.removeAssets(remove);

View File

@@ -50,12 +50,13 @@ export class MonthGroup {
readonly yearMonth: TimelineYearMonth; 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; this.timelineManager = timelineManager;
this.#initialCount = initialCount; this.#initialCount = initialCount;
this.#sortOrder = order; this.#sortOrder = order;
@@ -72,6 +73,9 @@ export class MonthGroup {
}, },
this.#handleLoadError, this.#handleLoadError,
); );
if (loaded) {
this.isLoaded = true;
}
} }
set intersecting(newValue: boolean) { set intersecting(newValue: boolean) {

View File

@@ -175,7 +175,7 @@ describe('TimelineManager', () => {
}); });
}); });
describe('addAssets', () => { describe('upsertAssets', () => {
let timelineManager: TimelineManager; let timelineManager: TimelineManager;
beforeEach(async () => { beforeEach(async () => {
@@ -196,7 +196,7 @@ describe('TimelineManager', () => {
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'), fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'),
}), }),
); );
timelineManager.addAssets([asset]); timelineManager.upsertAssets([asset]);
expect(timelineManager.months.length).toEqual(1); expect(timelineManager.months.length).toEqual(1);
expect(timelineManager.assetCount).toEqual(1); expect(timelineManager.assetCount).toEqual(1);
@@ -212,8 +212,8 @@ describe('TimelineManager', () => {
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'), fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'),
}) })
.map((asset) => deriveLocalDateTimeFromFileCreatedAt(asset)); .map((asset) => deriveLocalDateTimeFromFileCreatedAt(asset));
timelineManager.addAssets([assetOne]); timelineManager.upsertAssets([assetOne]);
timelineManager.addAssets([assetTwo]); timelineManager.upsertAssets([assetTwo]);
expect(timelineManager.months.length).toEqual(1); expect(timelineManager.months.length).toEqual(1);
expect(timelineManager.assetCount).toEqual(2); expect(timelineManager.assetCount).toEqual(2);
@@ -238,7 +238,7 @@ describe('TimelineManager', () => {
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-16T12:00:00.000Z'), fileCreatedAt: fromISODateTimeUTCToObject('2024-01-16T12:00:00.000Z'),
}), }),
); );
timelineManager.addAssets([assetOne, assetTwo, assetThree]); timelineManager.upsertAssets([assetOne, assetTwo, assetThree]);
const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 }); const month = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 });
expect(month).not.toBeNull(); expect(month).not.toBeNull();
@@ -264,7 +264,7 @@ describe('TimelineManager', () => {
fileCreatedAt: fromISODateTimeUTCToObject('2023-01-20T12:00:00.000Z'), fileCreatedAt: fromISODateTimeUTCToObject('2023-01-20T12:00:00.000Z'),
}), }),
); );
timelineManager.addAssets([assetOne, assetTwo, assetThree]); timelineManager.upsertAssets([assetOne, assetTwo, assetThree]);
expect(timelineManager.months.length).toEqual(3); expect(timelineManager.months.length).toEqual(3);
expect(timelineManager.months[0].yearMonth.year).toEqual(2024); expect(timelineManager.months[0].yearMonth.year).toEqual(2024);
@@ -278,11 +278,11 @@ describe('TimelineManager', () => {
}); });
it('updates existing asset', () => { it('updates existing asset', () => {
const updateAssetsSpy = vi.spyOn(timelineManager, 'updateAssets'); const updateAssetsSpy = vi.spyOn(timelineManager, 'upsertAssets');
const asset = deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build()); const asset = deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build());
timelineManager.addAssets([asset]); timelineManager.upsertAssets([asset]);
timelineManager.addAssets([asset]); timelineManager.upsertAssets([asset]);
expect(updateAssetsSpy).toBeCalledWith([asset]); expect(updateAssetsSpy).toBeCalledWith([asset]);
expect(timelineManager.assetCount).toEqual(1); expect(timelineManager.assetCount).toEqual(1);
}); });
@@ -294,12 +294,12 @@ describe('TimelineManager', () => {
const timelineManager = new TimelineManager(); const timelineManager = new TimelineManager();
await timelineManager.updateOptions({ isTrashed: true }); await timelineManager.updateOptions({ isTrashed: true });
timelineManager.addAssets([asset, trashedAsset]); timelineManager.upsertAssets([asset, trashedAsset]);
expect(await getAssets(timelineManager)).toEqual([trashedAsset]); expect(await getAssets(timelineManager)).toEqual([trashedAsset]);
}); });
}); });
describe('updateAssets', () => { describe('upsertAssets', () => {
let timelineManager: TimelineManager; let timelineManager: TimelineManager;
beforeEach(async () => { beforeEach(async () => {
@@ -309,22 +309,15 @@ describe('TimelineManager', () => {
await timelineManager.updateViewport({ width: 1588, height: 1000 }); await timelineManager.updateViewport({ width: 1588, height: 1000 });
}); });
it('ignores non-existing assets', () => { it('upserts an asset', () => {
timelineManager.updateAssets([deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build())]);
expect(timelineManager.months.length).toEqual(0);
expect(timelineManager.assetCount).toEqual(0);
});
it('updates an asset', () => {
const asset = deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build({ isFavorite: false })); const asset = deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build({ isFavorite: false }));
const updatedAsset = { ...asset, isFavorite: true }; const updatedAsset = { ...asset, isFavorite: true };
timelineManager.addAssets([asset]); timelineManager.upsertAssets([asset]);
expect(timelineManager.assetCount).toEqual(1); expect(timelineManager.assetCount).toEqual(1);
expect(timelineManager.months[0].getFirstAsset().isFavorite).toEqual(false); expect(timelineManager.months[0].getFirstAsset().isFavorite).toEqual(false);
timelineManager.updateAssets([updatedAsset]); timelineManager.upsertAssets([updatedAsset]);
expect(timelineManager.assetCount).toEqual(1); expect(timelineManager.assetCount).toEqual(1);
expect(timelineManager.months[0].getFirstAsset().isFavorite).toEqual(true); expect(timelineManager.months[0].getFirstAsset().isFavorite).toEqual(true);
}); });
@@ -340,12 +333,12 @@ describe('TimelineManager', () => {
fileCreatedAt: fromISODateTimeUTCToObject('2024-03-20T12:00:00.000Z'), fileCreatedAt: fromISODateTimeUTCToObject('2024-03-20T12:00:00.000Z'),
}); });
timelineManager.addAssets([asset]); timelineManager.upsertAssets([asset]);
expect(timelineManager.months.length).toEqual(1); expect(timelineManager.months.length).toEqual(1);
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })).not.toBeUndefined(); expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })).not.toBeUndefined();
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(1); expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(1);
timelineManager.updateAssets([updatedAsset]); timelineManager.upsertAssets([updatedAsset]);
expect(timelineManager.months.length).toEqual(2); expect(timelineManager.months.length).toEqual(2);
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })).not.toBeUndefined(); expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })).not.toBeUndefined();
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(0); expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(0);
@@ -365,7 +358,7 @@ describe('TimelineManager', () => {
}); });
it('ignores invalid IDs', () => { it('ignores invalid IDs', () => {
timelineManager.addAssets( timelineManager.upsertAssets(
timelineAssetFactory timelineAssetFactory
.buildList(2, { .buildList(2, {
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'), fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'),
@@ -385,7 +378,7 @@ describe('TimelineManager', () => {
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'), fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'),
}) })
.map((asset) => deriveLocalDateTimeFromFileCreatedAt(asset)); .map((asset) => deriveLocalDateTimeFromFileCreatedAt(asset));
timelineManager.addAssets([assetOne, assetTwo]); timelineManager.upsertAssets([assetOne, assetTwo]);
timelineManager.removeAssets([assetOne.id]); timelineManager.removeAssets([assetOne.id]);
expect(timelineManager.assetCount).toEqual(1); expect(timelineManager.assetCount).toEqual(1);
@@ -399,7 +392,7 @@ describe('TimelineManager', () => {
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'), fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'),
}) })
.map((asset) => deriveLocalDateTimeFromFileCreatedAt(asset)); .map((asset) => deriveLocalDateTimeFromFileCreatedAt(asset));
timelineManager.addAssets(assets); timelineManager.upsertAssets(assets);
timelineManager.removeAssets(assets.map((asset) => asset.id)); timelineManager.removeAssets(assets.map((asset) => asset.id));
expect(timelineManager.assetCount).toEqual(0); expect(timelineManager.assetCount).toEqual(0);
@@ -431,7 +424,7 @@ describe('TimelineManager', () => {
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-15T12:00:00.000Z'), fileCreatedAt: fromISODateTimeUTCToObject('2024-01-15T12:00:00.000Z'),
}), }),
); );
timelineManager.addAssets([assetOne, assetTwo]); timelineManager.upsertAssets([assetOne, assetTwo]);
expect(timelineManager.getFirstAsset()).toEqual(assetOne); expect(timelineManager.getFirstAsset()).toEqual(assetOne);
}); });
}); });
@@ -556,7 +549,7 @@ describe('TimelineManager', () => {
fileCreatedAt: fromISODateTimeUTCToObject('2024-02-15T12:00:00.000Z'), fileCreatedAt: fromISODateTimeUTCToObject('2024-02-15T12:00:00.000Z'),
}), }),
); );
timelineManager.addAssets([assetOne, assetTwo]); timelineManager.upsertAssets([assetOne, assetTwo]);
expect(timelineManager.getMonthGroupByAssetId(assetTwo.id)?.yearMonth.year).toEqual(2024); expect(timelineManager.getMonthGroupByAssetId(assetTwo.id)?.yearMonth.year).toEqual(2024);
expect(timelineManager.getMonthGroupByAssetId(assetTwo.id)?.yearMonth.month).toEqual(2); expect(timelineManager.getMonthGroupByAssetId(assetTwo.id)?.yearMonth.month).toEqual(2);
@@ -575,7 +568,7 @@ describe('TimelineManager', () => {
fileCreatedAt: fromISODateTimeUTCToObject('2024-02-15T12:00:00.000Z'), fileCreatedAt: fromISODateTimeUTCToObject('2024-02-15T12:00:00.000Z'),
}), }),
); );
timelineManager.addAssets([assetOne, assetTwo]); timelineManager.upsertAssets([assetOne, assetTwo]);
timelineManager.removeAssets([assetTwo.id]); timelineManager.removeAssets([assetTwo.id]);
expect(timelineManager.getMonthGroupByAssetId(assetOne.id)?.yearMonth.year).toEqual(2024); expect(timelineManager.getMonthGroupByAssetId(assetOne.id)?.yearMonth.year).toEqual(2024);

View File

@@ -1,12 +1,9 @@
import { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte'; import { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { GroupInsertionCache } from '$lib/managers/timeline-manager/group-insertion-cache.svelte';
import { updateIntersectionMonthGroup } from '$lib/managers/timeline-manager/internal/intersection-support.svelte'; 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 {
addAssetsToMonthGroups,
runAssetOperation,
} from '$lib/managers/timeline-manager/internal/operations-support.svelte';
import { import {
findClosestGroupForDate, findClosestGroupForDate,
findMonthGroupForAsset as findMonthGroupForAssetUtil, findMonthGroupForAsset as findMonthGroupForAssetUtil,
@@ -17,10 +14,15 @@ import {
} 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 { 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 {
setDifference,
toTimelineAsset,
type TimelineDateTime,
type TimelineYearMonth,
} from '$lib/utils/timeline-util';
import { AssetOrder, getAssetInfo, getTimeBuckets } from '@immich/sdk'; import { AssetOrder, getAssetInfo, getTimeBuckets } from '@immich/sdk';
import { clamp, isEqual } from 'lodash-es'; import { clamp, isEqual } from 'lodash-es';
import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity'; import { SvelteDate, SvelteSet } from 'svelte/reactivity';
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';
@@ -28,6 +30,7 @@ import type {
AssetDescriptor, AssetDescriptor,
AssetOperation, AssetOperation,
Direction, Direction,
MoveAsset,
ScrubberMonth, ScrubberMonth,
TimelineAsset, TimelineAsset,
TimelineManagerOptions, TimelineManagerOptions,
@@ -217,6 +220,7 @@ export class TimelineManager extends VirtualScrollManager {
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,
); );
}); });
@@ -319,10 +323,10 @@ export class TimelineManager extends VirtualScrollManager {
} }
} }
addAssets(assets: TimelineAsset[]) { upsertAssets(assets: TimelineAsset[]) {
const assetsToUpdate = assets.filter((asset) => !this.isExcluded(asset)); const notExcluded = assets.filter((asset) => !this.isExcluded(asset));
const notUpdated = this.updateAssets(assetsToUpdate); const notUpdated = this.#updateAssets(notExcluded);
addAssetsToMonthGroups(this, [...notUpdated], { order: this.#options.order ?? AssetOrder.Desc }); this.addAssetsToSegments([...notUpdated]);
} }
async findMonthGroupForAsset(id: string) { async findMonthGroupForAsset(id: string) {
@@ -400,19 +404,16 @@ export class TimelineManager extends VirtualScrollManager {
} }
updateAssetOperation(ids: string[], operation: AssetOperation) { updateAssetOperation(ids: string[], operation: AssetOperation) {
runAssetOperation(this, new SvelteSet(ids), operation, { order: this.#options.order ?? AssetOrder.Desc }); // eslint-disable-next-line svelte/prefer-svelte-reactivity
return this.#runAssetOperation(new Set(ids), operation);
} }
updateAssets(assets: TimelineAsset[]) { #updateAssets(assets: TimelineAsset[]) {
const lookup = new SvelteMap<string, TimelineAsset>(assets.map((asset) => [asset.id, asset])); // eslint-disable-next-line svelte/prefer-svelte-reactivity
const { unprocessedIds } = runAssetOperation( const lookup = new Map<string, TimelineAsset>(assets.map((asset) => [asset.id, asset]));
this, // eslint-disable-next-line svelte/prefer-svelte-reactivity
new SvelteSet(lookup.keys()), const { unprocessedIds } = this.#runAssetOperation(new Set(lookup.keys()), (asset) =>
(asset) => { updateObject(asset, lookup.get(asset.id)),
updateObject(asset, lookup.get(asset.id));
return { remove: false };
},
{ order: this.#options.order ?? AssetOrder.Desc },
); );
const result: TimelineAsset[] = []; const result: TimelineAsset[] = [];
for (const id of unprocessedIds.values()) { for (const id of unprocessedIds.values()) {
@@ -422,17 +423,83 @@ export class TimelineManager extends VirtualScrollManager {
} }
removeAssets(ids: string[]) { removeAssets(ids: string[]) {
const { unprocessedIds } = runAssetOperation( // eslint-disable-next-line svelte/prefer-svelte-reactivity
this, const { unprocessedIds } = this.#runAssetOperation(new Set(ids), () => ({ remove: true }));
new SvelteSet(ids),
() => {
return { remove: true };
},
{ order: this.#options.order ?? AssetOrder.Desc },
);
return [...unprocessedIds]; return [...unprocessedIds];
} }
protected createUpsertContext(): GroupInsertionCache {
return new GroupInsertionCache();
}
protected upsertAssetIntoSegment(asset: TimelineAsset, context: GroupInsertionCache): void {
let month = getMonthGroupByDate(this, asset.localDateTime);
if (!month) {
month = new MonthGroup(this, asset.localDateTime, 1, true, this.#options.order);
this.months.push(month);
}
month.addTimelineAsset(asset, context);
}
protected addAssetsToSegments(assets: TimelineAsset[]) {
if (assets.length === 0) {
return;
}
const context = this.createUpsertContext();
const monthCount = this.months.length;
for (const asset of assets) {
this.upsertAssetIntoSegment(asset, context);
}
if (this.months.length !== monthCount) {
this.postCreateSegments();
}
this.postUpsert(context);
this.updateIntersections();
}
#runAssetOperation(ids: Set<string>, operation: AssetOperation) {
if (ids.size === 0) {
// eslint-disable-next-line svelte/prefer-svelte-reactivity
return { processedIds: new Set(), unprocessedIds: ids, changedGeometry: false };
}
// eslint-disable-next-line svelte/prefer-svelte-reactivity
const changedMonthGroups = new Set<MonthGroup>();
// eslint-disable-next-line svelte/prefer-svelte-reactivity
let idsToProcess = new Set(ids);
// eslint-disable-next-line svelte/prefer-svelte-reactivity
const idsProcessed = new Set<string>();
const combinedMoveAssets: MoveAsset[][] = [];
for (const month of this.months) {
if (idsToProcess.size > 0) {
const { moveAssets, processedIds, changedGeometry } = month.runAssetOperation(idsToProcess, operation);
if (moveAssets.length > 0) {
combinedMoveAssets.push(moveAssets);
}
idsToProcess = setDifference(idsToProcess, processedIds);
for (const id of processedIds) {
idsProcessed.add(id);
}
if (changedGeometry) {
changedMonthGroups.add(month);
}
}
}
if (combinedMoveAssets.length > 0) {
this.addAssetsToSegments(combinedMoveAssets.flat().map((a) => a.asset));
}
const changedGeometry = changedMonthGroups.size > 0;
for (const month of changedMonthGroups) {
updateGeometry(this, month, { invalidateHeight: true });
}
if (changedGeometry) {
this.updateIntersections();
}
return { unprocessedIds: idsToProcess, processedIds: idsProcessed, changedGeometry };
}
override refreshLayout() { override refreshLayout() {
for (const month of this.months) { for (const month of this.months) {
updateGeometry(this, month, { invalidateHeight: true }); updateGeometry(this, month, { invalidateHeight: true });
@@ -492,4 +559,27 @@ export class TimelineManager extends VirtualScrollManager {
getAssetOrder() { getAssetOrder() {
return this.#options.order ?? AssetOrder.Desc; return this.#options.order ?? AssetOrder.Desc;
} }
protected postCreateSegments(): void {
this.months.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.existingDayGroups) {
group.sortAssets(this.#options.order);
}
for (const monthGroup of context.bucketsWithNewDayGroups) {
monthGroup.sortDayGroups();
}
for (const month of context.updatedBuckets) {
month.sortDayGroups();
updateGeometry(this, month, { invalidateHeight: true });
}
}
} }

View File

@@ -37,7 +37,7 @@ export type TimelineAsset = {
longitude?: number | null; longitude?: number | null;
}; };
export type AssetOperation = (asset: TimelineAsset) => { remove: boolean }; export type AssetOperation = (asset: TimelineAsset) => { remove: boolean } | unknown;
export type MoveAsset = { asset: TimelineAsset; date: TimelineDate }; export type MoveAsset = { asset: TimelineAsset; date: TimelineDate };

View File

@@ -109,5 +109,5 @@ export function updateUnstackedAssetInTimeline(timelineManager: TimelineManager,
}, },
); );
timelineManager.addAssets(assets); timelineManager.upsertAssets(assets);
} }

View File

@@ -266,7 +266,7 @@
}; };
const handleUndoRemoveAssets = async (assets: TimelineAsset[]) => { const handleUndoRemoveAssets = async (assets: TimelineAsset[]) => {
timelineManager.addAssets(assets); timelineManager.upsertAssets(assets);
await refreshAlbum(); await refreshAlbum();
}; };

View File

@@ -94,7 +94,7 @@
<DeleteAssets <DeleteAssets
menuItem menuItem
onAssetDelete={(assetIds) => timelineManager.removeAssets(assetIds)} onAssetDelete={(assetIds) => timelineManager.removeAssets(assetIds)}
onUndoDelete={(assets) => timelineManager.addAssets(assets)} onUndoDelete={(assets) => timelineManager.upsertAssets(assets)}
/> />
</ButtonContextMenu> </ButtonContextMenu>
</AssetSelectControlBar> </AssetSelectControlBar>

View File

@@ -339,7 +339,7 @@
}; };
const handleUndoDeleteAssets = async (assets: TimelineAsset[]) => { const handleUndoDeleteAssets = async (assets: TimelineAsset[]) => {
timelineManager.addAssets(assets); timelineManager.upsertAssets(assets);
await updateAssetCount(); await updateAssetCount();
}; };

View File

@@ -69,12 +69,12 @@
const handleLink: OnLink = ({ still, motion }) => { const handleLink: OnLink = ({ still, motion }) => {
timelineManager.removeAssets([motion.id]); timelineManager.removeAssets([motion.id]);
timelineManager.updateAssets([still]); timelineManager.upsertAssets([still]);
}; };
const handleUnlink: OnUnlink = ({ still, motion }) => { const handleUnlink: OnUnlink = ({ still, motion }) => {
timelineManager.addAssets([motion]); timelineManager.upsertAssets([motion]);
timelineManager.updateAssets([still]); timelineManager.upsertAssets([still]);
}; };
const handleSetVisibility = (assetIds: string[]) => { const handleSetVisibility = (assetIds: string[]) => {
@@ -153,7 +153,7 @@
<DeleteAssets <DeleteAssets
menuItem menuItem
onAssetDelete={(assetIds) => timelineManager.removeAssets(assetIds)} onAssetDelete={(assetIds) => timelineManager.removeAssets(assetIds)}
onUndoDelete={(assets) => timelineManager.addAssets(assets)} onUndoDelete={(assets) => timelineManager.upsertAssets(assets)}
/> />
<SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} /> <SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
<hr /> <hr />

View File

@@ -63,7 +63,7 @@
}), }),
); );
timelineManager.updateAssets(updatedAssets); timelineManager.upsertAssets(updatedAssets);
handleDeselectAll(); handleDeselectAll();
}; };