176 lines
5.2 KiB
TypeScript
176 lines
5.2 KiB
TypeScript
import { onCreateDay } from '$lib/managers/timeline-manager/internal/TestHooks.svelte';
|
|
import { TimelineMonth } from '$lib/managers/timeline-manager/TimelineMonth.svelte';
|
|
import type { AssetOperation, Direction, TimelineAsset } from '$lib/managers/timeline-manager/types';
|
|
import { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
|
|
import type { CommonLayoutOptions } from '$lib/utils/layout-utils';
|
|
import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils';
|
|
import { plainDateTimeCompare } from '$lib/utils/timeline-util';
|
|
import { AssetOrder } from '@immich/sdk';
|
|
|
|
export class TimelineDay {
|
|
readonly month: TimelineMonth;
|
|
readonly index: number;
|
|
readonly dayTitle: string;
|
|
readonly dayTitleFull: string;
|
|
|
|
readonly day: number;
|
|
viewerAssets: ViewerAsset[] = $state([]);
|
|
|
|
height = $state(0);
|
|
width = $state(0);
|
|
intersecting = $derived.by(() => this.viewerAssets.some((viewAsset) => viewAsset.intersecting));
|
|
|
|
#top: number = $state(0);
|
|
#left: number = $state(0);
|
|
#row = $state(0);
|
|
#col = $state(0);
|
|
#deferredLayout = false;
|
|
|
|
constructor(month: TimelineMonth, index: number, day: number, groupTitle: string, groupTitleFull: string) {
|
|
this.index = index;
|
|
this.month = month;
|
|
this.day = day;
|
|
this.dayTitle = groupTitle;
|
|
this.dayTitleFull = groupTitleFull;
|
|
|
|
if (import.meta.env.DEV) {
|
|
onCreateDay(this);
|
|
}
|
|
}
|
|
|
|
get top() {
|
|
return this.#top;
|
|
}
|
|
|
|
set top(value: number) {
|
|
this.#top = value;
|
|
}
|
|
|
|
get left() {
|
|
return this.#left;
|
|
}
|
|
|
|
set left(value: number) {
|
|
this.#left = value;
|
|
}
|
|
|
|
get row() {
|
|
return this.#row;
|
|
}
|
|
|
|
set row(value: number) {
|
|
this.#row = value;
|
|
}
|
|
|
|
get col() {
|
|
return this.#col;
|
|
}
|
|
|
|
set col(value: number) {
|
|
this.#col = value;
|
|
}
|
|
|
|
get deferredLayout() {
|
|
return this.#deferredLayout;
|
|
}
|
|
|
|
set deferredLayout(value: boolean) {
|
|
this.#deferredLayout = value;
|
|
}
|
|
|
|
sortAssets(sortOrder: AssetOrder = AssetOrder.Desc) {
|
|
const sortFn = plainDateTimeCompare.bind(undefined, sortOrder === AssetOrder.Asc);
|
|
this.viewerAssets.sort((a, b) => sortFn(a.asset.fileCreatedAt, b.asset.fileCreatedAt));
|
|
}
|
|
|
|
getFirstAsset() {
|
|
return this.viewerAssets[0]?.asset;
|
|
}
|
|
|
|
*assetsIterator(options: { startAsset?: TimelineAsset; direction?: Direction } = {}) {
|
|
const isEarlier = (options?.direction ?? 'earlier') === 'earlier';
|
|
let assetIndex = options?.startAsset
|
|
? this.viewerAssets.findIndex((viewerAsset) => viewerAsset.asset.id === options.startAsset!.id)
|
|
: isEarlier
|
|
? 0
|
|
: this.viewerAssets.length - 1;
|
|
|
|
while (assetIndex >= 0 && assetIndex < this.viewerAssets.length) {
|
|
const viewerAsset = this.viewerAssets[assetIndex];
|
|
yield viewerAsset.asset;
|
|
assetIndex += isEarlier ? 1 : -1;
|
|
}
|
|
}
|
|
|
|
getAssets() {
|
|
return this.viewerAssets.map((viewerAsset) => viewerAsset.asset);
|
|
}
|
|
|
|
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
|
|
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!;
|
|
// save old time, pre-mutating operation
|
|
const oldTime = { ...asset.localDateTime };
|
|
const opResult = operation(asset);
|
|
let remove = false;
|
|
if (opResult) {
|
|
remove = (opResult as { remove: boolean }).remove ?? false;
|
|
}
|
|
const newTime = asset.localDateTime;
|
|
if (
|
|
!remove &&
|
|
(oldTime.year !== newTime.year || oldTime.month !== newTime.month || oldTime.day !== newTime.day)
|
|
) {
|
|
remove = true;
|
|
moveAssets.push(asset);
|
|
}
|
|
unprocessedIds.delete(assetId);
|
|
processedIds.add(assetId);
|
|
if (remove || this.month.scrollManager.isExcluded(asset)) {
|
|
this.viewerAssets.splice(index, 1);
|
|
changedGeometry = true;
|
|
}
|
|
}
|
|
return { moveAssets, processedIds, unprocessedIds, changedGeometry };
|
|
}
|
|
|
|
layout(options: CommonLayoutOptions, noDefer: boolean) {
|
|
if (!noDefer && !this.month.intersecting) {
|
|
this.#deferredLayout = true;
|
|
return;
|
|
}
|
|
const assets = this.viewerAssets.map((viewerAsset) => viewerAsset.asset!);
|
|
const geometry = getJustifiedLayoutFromAssets(assets, options);
|
|
this.width = geometry.containerWidth;
|
|
this.height = assets.length === 0 ? 0 : geometry.containerHeight;
|
|
// TODO: lazily get positions instead of loading them all here
|
|
for (let i = 0; i < this.viewerAssets.length; i++) {
|
|
this.viewerAssets[i].position = geometry.getPosition(i);
|
|
}
|
|
}
|
|
|
|
get topAbsolute() {
|
|
return this.month.top + this.#top;
|
|
}
|
|
}
|