mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
refactor: timeline manager renames (#19007)
* refactor: timeline manager renames * refactor(web): improve timeline manager naming consistency - Rename AddContext → GroupInsertionCache for clearer purpose - Rename TimelineDay → DayGroup for better clarity - Rename TimelineMonth → MonthGroup for better clarity - Replace all "bucket" references with "monthGroup" terminology - Update all component props, method names, and variable references - Maintain consistent naming patterns across TypeScript and Svelte files * refactor(web): rename buckets to months in timeline manager - Rename TimelineManager.buckets property to months - Update all store.buckets references to store.months - Use 'month' shorthand for monthGroup arguments (not method names) - Update component templates and test files for consistency - Maintain API-related 'bucket' terminology (bucketHeight, getTimeBucket) * refactor(web): rename assetStore to timelineManager and update types - Rename assetStore variables to timelineManager in all .svelte files - Update parameter names in actions.ts and asset-utils.ts functions - Rename AssetStoreLayoutOptions to TimelineManagerLayoutOptions - Rename AssetStoreOptions to TimelineManagerOptions - Move assets-store.spec.ts to timeline-manager.spec.ts * refactor(web): rename intersectingAssets to viewerAssets and fix property references - Rename intersectingAssets to viewerAssets in DayGroup and MonthGroup classes - Update arrow function parameters to use viewerAsset/viewAsset shorthand - Rename topIntersectingBucket to topIntersectingMonthGroup - Fix dateGroups references to dayGroups in asset-utils.ts and album page - Update template loops and variable names in Svelte components * refactor(web): rename #initializeTimeBuckets to #initializeMonthGroups and bucketDateFormatted to monthGroupTitle * refactor(web): rename monthGroupHeight to height * refactor(web): rename bucketCount to assetsCount, bucketsIterator to monthGroupIterator, and related properties * refactor(web): rename count to assetCount in TimelineManager * refactor(web): rename LiteBucket to ScrubberMonth and update scrubber variables - Rename LiteBucket type to ScrubberMonth - Rename bucketDateFormattted to title in ScrubberMonth type - Rename bucketPercentY to monthGroupPercentY in scrubber component - Rename scrubBucket to scrubberMonth and scrubBucketPercent to scrubberMonthPercent * fix remaining refs to bucket * reset submodule to correct commit * reset submodule to correct commit * refactor(web): extract TimelineManager internals into separate modules - Move search-related functions to internal/search-support.svelte.ts - Extract websocket event handling into WebsocketSupport class - Move utility functions (updateObject, isMismatched) to internal/utils.svelte.ts - Update imports in tests to use new module structure * refactor(web): extract intersection logic from TimelineManager - Create intersection-support.svelte.ts with updateIntersection and calculateIntersecting functions - Remove private intersection methods from TimelineManager - Export findMonthGroupForAsset from search-support for reuse - Update TimelineManager to use the extracted intersection functions * refactor(web): rename a few methods in intersecting * refactor(web): rename a few methods in intersecting * refactor(web): extract layout logic from TimelineManager - Create layout-support.svelte.ts with updateGeometry and layoutMonthGroup functions - Remove private layout methods from TimelineManager - Update TimelineManager to use the extracted layout functions - Remove unused UpdateGeometryOptions import * refactor(web): extract asset operations from TimelineManager - Create operations-support.svelte.ts with addAssetsToMonthGroups and runAssetOperation functions - Remove private asset operation methods from TimelineManager - Update TimelineManager to use extracted operation functions with proper AssetOrder handling - Rename getMonthGroupIndexByAssetId to getMonthGroupByAssetId for consistency - Move utility functions from utils.svelte.ts to internal/utils.svelte.ts - Fix method name references in asset-grid.svelte and tests * refactor(web): extract loading logic from TimelineManager - Create load-support.svelte.ts with loadFromTimeBuckets function - Extract time bucket loading, album asset handling, and error logging - Simplify TimelineManager's loadMonthGroup method to use extracted function * refresh timeline after archive keyboard shortcut * remove debugger * rename * Review comments - remove shadowed var * reduce indents - early return * review comment * refactor: simplify asset filtering in addAssets method Replace for loop with filter operation for better readability * fix: bad merge * refactor(web): simplify timeline layout algorithm - Replace rowSpaceRemaining array with direct cumulative width tracking - Invert logic from tracking remaining space to tracking used space - Fix spelling: cummulative to cumulative - Rename lastRowHeight to currentRowHeight for clarity - Remove confusing lastRow variable and simplify final height calculation - Add explanatory comments for clarity - Rename loop variable assetGroup to dayGroup for consistency * simplify assetsIterator usage * merge/lint --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
6499057b4c
commit
4b4ee5abf3
39 changed files with 2288 additions and 2139 deletions
366
web/src/lib/managers/timeline-manager/month-group.svelte.ts
Normal file
366
web/src/lib/managers/timeline-manager/month-group.svelte.ts
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
import { AssetOrder, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
||||
|
||||
import { CancellableTask } from '$lib/utils/cancellable-task';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import {
|
||||
formatGroupTitle,
|
||||
formatMonthGroupTitle,
|
||||
fromTimelinePlainDate,
|
||||
fromTimelinePlainDateTime,
|
||||
fromTimelinePlainYearMonth,
|
||||
getTimes,
|
||||
type TimelinePlainDateTime,
|
||||
type TimelinePlainYearMonth,
|
||||
} from '$lib/utils/timeline-util';
|
||||
|
||||
import { t } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
import { DayGroup } from './day-group.svelte';
|
||||
import { GroupInsertionCache } from './group-insertion-cache.svelte';
|
||||
import type { TimelineManager } from './timeline-manager.svelte';
|
||||
import type { AssetDescriptor, AssetOperation, Direction, MoveAsset, TimelineAsset } from './types';
|
||||
import { ViewerAsset } from './viewer-asset.svelte';
|
||||
|
||||
export class MonthGroup {
|
||||
#intersecting: boolean = $state(false);
|
||||
actuallyIntersecting: boolean = $state(false);
|
||||
isLoaded: boolean = $state(false);
|
||||
dayGroups: DayGroup[] = $state([]);
|
||||
readonly timelineManager: TimelineManager;
|
||||
|
||||
#height: number = $state(0);
|
||||
#top: number = $state(0);
|
||||
|
||||
#initialCount: number = 0;
|
||||
#sortOrder: AssetOrder = AssetOrder.Desc;
|
||||
percent: number = $state(0);
|
||||
|
||||
assetsCount: number = $derived(
|
||||
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 yearMonth: TimelinePlainYearMonth;
|
||||
|
||||
constructor(
|
||||
store: TimelineManager,
|
||||
yearMonth: TimelinePlainYearMonth,
|
||||
initialCount: number,
|
||||
order: AssetOrder = AssetOrder.Desc,
|
||||
) {
|
||||
this.timelineManager = store;
|
||||
this.#initialCount = initialCount;
|
||||
this.#sortOrder = order;
|
||||
|
||||
this.yearMonth = yearMonth;
|
||||
this.monthGroupTitle = formatMonthGroupTitle(fromTimelinePlainYearMonth(yearMonth));
|
||||
|
||||
this.loader = new CancellableTask(
|
||||
() => {
|
||||
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() {
|
||||
return this.#intersecting;
|
||||
}
|
||||
|
||||
get lastDayGroup() {
|
||||
return this.dayGroups.at(-1);
|
||||
}
|
||||
|
||||
getFirstAsset() {
|
||||
return this.dayGroups[0]?.getFirstAsset();
|
||||
}
|
||||
|
||||
getAssets() {
|
||||
// eslint-disable-next-line unicorn/no-array-reduce
|
||||
return this.dayGroups.reduce((accumulator: TimelineAsset[], g: DayGroup) => accumulator.concat(g.getAssets()), []);
|
||||
}
|
||||
|
||||
sortDayGroups() {
|
||||
if (this.#sortOrder === AssetOrder.Asc) {
|
||||
return this.dayGroups.sort((a, b) => a.day - b.day);
|
||||
}
|
||||
|
||||
return this.dayGroups.sort((a, b) => b.day - a.day);
|
||||
}
|
||||
|
||||
runAssetOperation(ids: Set<string>, operation: AssetOperation) {
|
||||
if (ids.size === 0) {
|
||||
return {
|
||||
moveAssets: [] as MoveAsset[],
|
||||
processedIds: new Set<string>(),
|
||||
unprocessedIds: ids,
|
||||
changedGeometry: false,
|
||||
};
|
||||
}
|
||||
const { dayGroups } = this;
|
||||
let combinedChangedGeometry = false;
|
||||
let idsToProcess = new Set(ids);
|
||||
const idsProcessed = new Set<string>();
|
||||
const combinedMoveAssets: MoveAsset[][] = [];
|
||||
let index = dayGroups.length;
|
||||
while (index--) {
|
||||
if (idsToProcess.size > 0) {
|
||||
const group = dayGroups[index];
|
||||
const { moveAssets, processedIds, changedGeometry } = group.runAssetOperation(ids, operation);
|
||||
if (moveAssets.length > 0) {
|
||||
combinedMoveAssets.push(moveAssets);
|
||||
}
|
||||
idsToProcess = idsToProcess.difference(processedIds);
|
||||
for (const id of processedIds) {
|
||||
idsProcessed.add(id);
|
||||
}
|
||||
combinedChangedGeometry = combinedChangedGeometry || changedGeometry;
|
||||
if (group.viewerAssets.length === 0) {
|
||||
dayGroups.splice(index, 1);
|
||||
combinedChangedGeometry = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
moveAssets: combinedMoveAssets.flat(),
|
||||
unprocessedIds: idsToProcess,
|
||||
processedIds: idsProcessed,
|
||||
changedGeometry: combinedChangedGeometry,
|
||||
};
|
||||
}
|
||||
|
||||
addAssets(bucketAssets: TimeBucketAssetResponseDto) {
|
||||
const addContext = new GroupInsertionCache();
|
||||
for (let i = 0; i < bucketAssets.id.length; i++) {
|
||||
const { localDateTime, fileCreatedAt } = getTimes(
|
||||
bucketAssets.fileCreatedAt[i],
|
||||
bucketAssets.localOffsetHours[i],
|
||||
);
|
||||
|
||||
const timelineAsset: TimelineAsset = {
|
||||
city: bucketAssets.city[i],
|
||||
country: bucketAssets.country[i],
|
||||
duration: bucketAssets.duration[i],
|
||||
id: bucketAssets.id[i],
|
||||
visibility: bucketAssets.visibility[i],
|
||||
isFavorite: bucketAssets.isFavorite[i],
|
||||
isImage: bucketAssets.isImage[i],
|
||||
isTrashed: bucketAssets.isTrashed[i],
|
||||
isVideo: !bucketAssets.isImage[i],
|
||||
livePhotoVideoId: bucketAssets.livePhotoVideoId[i],
|
||||
localDateTime,
|
||||
fileCreatedAt,
|
||||
ownerId: bucketAssets.ownerId[i],
|
||||
projectionType: bucketAssets.projectionType[i],
|
||||
ratio: bucketAssets.ratio[i],
|
||||
stack: bucketAssets.stack?.[i]
|
||||
? {
|
||||
id: bucketAssets.stack[i]![0],
|
||||
primaryAssetId: bucketAssets.id[i],
|
||||
assetCount: Number.parseInt(bucketAssets.stack[i]![1]),
|
||||
}
|
||||
: null,
|
||||
thumbhash: bucketAssets.thumbhash[i],
|
||||
people: null, // People are not included in the bucket assets
|
||||
};
|
||||
this.addTimelineAsset(timelineAsset, addContext);
|
||||
}
|
||||
|
||||
for (const group of addContext.existingDayGroups) {
|
||||
group.sortAssets(this.#sortOrder);
|
||||
}
|
||||
|
||||
if (addContext.newDayGroups.size > 0) {
|
||||
this.sortDayGroups();
|
||||
}
|
||||
|
||||
addContext.sort(this, this.#sortOrder);
|
||||
|
||||
return addContext.unprocessedAssets;
|
||||
}
|
||||
|
||||
addTimelineAsset(timelineAsset: TimelineAsset, addContext: GroupInsertionCache) {
|
||||
const { localDateTime } = timelineAsset;
|
||||
|
||||
const { year, month } = this.yearMonth;
|
||||
if (month !== localDateTime.month || year !== localDateTime.year) {
|
||||
addContext.unprocessedAssets.push(timelineAsset);
|
||||
return;
|
||||
}
|
||||
|
||||
let dayGroup = addContext.getDayGroup(localDateTime) || this.findDayGroupByDay(localDateTime.day);
|
||||
if (dayGroup) {
|
||||
addContext.setDayGroup(dayGroup, localDateTime);
|
||||
} else {
|
||||
const groupTitle = formatGroupTitle(fromTimelinePlainDate(localDateTime));
|
||||
dayGroup = new DayGroup(this, this.dayGroups.length, localDateTime.day, groupTitle);
|
||||
this.dayGroups.push(dayGroup);
|
||||
addContext.setDayGroup(dayGroup, localDateTime);
|
||||
addContext.newDayGroups.add(dayGroup);
|
||||
}
|
||||
|
||||
const viewerAsset = new ViewerAsset(dayGroup, timelineAsset);
|
||||
dayGroup.viewerAssets.push(viewerAsset);
|
||||
addContext.changedDayGroups.add(dayGroup);
|
||||
}
|
||||
|
||||
getRandomDayGroup() {
|
||||
const random = Math.floor(Math.random() * this.dayGroups.length);
|
||||
return this.dayGroups[random];
|
||||
}
|
||||
|
||||
getRandomAsset() {
|
||||
return this.getRandomDayGroup()?.getRandomAsset()?.asset;
|
||||
}
|
||||
|
||||
get viewId() {
|
||||
const { year, month } = this.yearMonth;
|
||||
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) {
|
||||
for (const group of this.dayGroups) {
|
||||
if (group.viewerAssets.some((viewerAsset) => viewerAsset.id === asset.id)) {
|
||||
return group;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findDayGroupByDay(day: number) {
|
||||
return this.dayGroups.find((group) => group.day === day);
|
||||
}
|
||||
|
||||
findAssetAbsolutePosition(assetId: string) {
|
||||
this.timelineManager.clearDeferredLayout(this);
|
||||
for (const group of this.dayGroups) {
|
||||
const viewerAsset = group.viewerAssets.find((viewAsset) => viewAsset.id === assetId);
|
||||
if (viewerAsset) {
|
||||
if (!viewerAsset.position) {
|
||||
console.warn('No position for asset');
|
||||
break;
|
||||
}
|
||||
return this.top + group.top + viewerAsset.position.top + this.timelineManager.headerHeight;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
*assetsIterator(options?: { startDayGroup?: DayGroup; startAsset?: TimelineAsset; direction?: Direction }) {
|
||||
const direction = options?.direction ?? 'earlier';
|
||||
let { startAsset } = options ?? {};
|
||||
const isEarlier = direction === 'earlier';
|
||||
let groupIndex = options?.startDayGroup
|
||||
? this.dayGroups.indexOf(options.startDayGroup)
|
||||
: isEarlier
|
||||
? 0
|
||||
: this.dayGroups.length - 1;
|
||||
|
||||
while (groupIndex >= 0 && groupIndex < this.dayGroups.length) {
|
||||
const group = this.dayGroups[groupIndex];
|
||||
yield* group.assetsIterator({ startAsset, direction });
|
||||
startAsset = undefined;
|
||||
groupIndex += isEarlier ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
findAssetById(assetDescriptor: AssetDescriptor) {
|
||||
return this.assetsIterator().find((asset) => asset.id === assetDescriptor.id);
|
||||
}
|
||||
|
||||
findClosest(target: TimelinePlainDateTime) {
|
||||
const targetDate = fromTimelinePlainDateTime(target);
|
||||
let closest = undefined;
|
||||
let smallestDiff = Infinity;
|
||||
for (const current of this.assetsIterator()) {
|
||||
const currentAssetDate = fromTimelinePlainDateTime(current.localDateTime);
|
||||
const diff = Math.abs(targetDate.diff(currentAssetDate).as('milliseconds'));
|
||||
if (diff < smallestDiff) {
|
||||
smallestDiff = diff;
|
||||
closest = current;
|
||||
}
|
||||
}
|
||||
return closest;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.loader?.cancel();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue