mirror of
https://github.com/immich-app/immich.git
synced 2025-12-18 17:23:16 +03:00
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:
@@ -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(
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|
||||||
|
|||||||
@@ -109,5 +109,5 @@ export function updateUnstackedAssetInTimeline(timelineManager: TimelineManager,
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
timelineManager.addAssets(assets);
|
timelineManager.upsertAssets(assets);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,7 +266,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUndoRemoveAssets = async (assets: TimelineAsset[]) => {
|
const handleUndoRemoveAssets = async (assets: TimelineAsset[]) => {
|
||||||
timelineManager.addAssets(assets);
|
timelineManager.upsertAssets(assets);
|
||||||
await refreshAlbum();
|
await refreshAlbum();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -339,7 +339,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUndoDeleteAssets = async (assets: TimelineAsset[]) => {
|
const handleUndoDeleteAssets = async (assets: TimelineAsset[]) => {
|
||||||
timelineManager.addAssets(assets);
|
timelineManager.upsertAssets(assets);
|
||||||
await updateAssetCount();
|
await updateAssetCount();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
timelineManager.updateAssets(updatedAssets);
|
timelineManager.upsertAssets(updatedAssets);
|
||||||
|
|
||||||
handleDeselectAll();
|
handleDeselectAll();
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user