- Extracts the asset grid rendering logic from `MonthSegment` into a
dedicated `AssetLayout` component
- Simplifies `MonthSegment` by delegating layout responsibilities
while maintaining all existing functionality
- Renames `customLayout` prop to `customThumbnailLayout` for clarity
across Timeline components
## Changes
- Created new `AssetLayout.svelte` component that handles:
- Asset grid rendering with proper positioning
- Animation transitions
- Filtering of intersecting viewer assets
- Updated `MonthSegment.svelte` to use `AssetLayout` via composition
pattern
- Renamed `customLayout` to `customThumbnailLayout` in Timeline and
related components
- Moved thumbnail click and selection logic to Timeline parent
component using snippets
196 lines
5.7 KiB
TypeScript
196 lines
5.7 KiB
TypeScript
import { CancellableTask } from '$lib/utils/cancellable-task';
|
|
import { handleError } from '$lib/utils/handle-error';
|
|
import { t } from 'svelte-i18n';
|
|
import { get } from 'svelte/store';
|
|
|
|
import type { PhotostreamManager } from '$lib/managers/photostream-manager/PhotostreamManager.svelte';
|
|
import type { AssetOperation, MoveAsset, TimelineAsset } from '$lib/managers/timeline-manager/types';
|
|
import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
|
|
|
|
export type SegmentIdentifier = {
|
|
matches(segment: PhotostreamSegment): boolean;
|
|
};
|
|
export abstract class PhotostreamSegment {
|
|
#intersecting = $state(false);
|
|
actuallyIntersecting = $state(false);
|
|
#isLoaded = $state(false);
|
|
|
|
#height = $state(0);
|
|
#top = $state(0);
|
|
#assets = $derived.by(() => this.viewerAssets.map((viewerAsset) => viewerAsset.asset));
|
|
|
|
initialCount = $state(0);
|
|
percent = $state(0);
|
|
|
|
assetsCount = $derived.by(() => (this.isLoaded ? this.viewerAssets.length : this.initialCount));
|
|
loader = new CancellableTask(
|
|
() => this.markLoaded(),
|
|
() => this.markCanceled,
|
|
() => this.handleLoadError,
|
|
);
|
|
isHeightActual = $state(false);
|
|
|
|
abstract get timelineManager(): PhotostreamManager;
|
|
|
|
abstract get identifier(): SegmentIdentifier;
|
|
|
|
abstract get id(): string;
|
|
|
|
get isLoaded() {
|
|
return this.#isLoaded;
|
|
}
|
|
|
|
protected markLoaded() {
|
|
this.#isLoaded = true;
|
|
}
|
|
|
|
protected markCanceled() {
|
|
this.#isLoaded = false;
|
|
}
|
|
|
|
set intersecting(newValue: boolean) {
|
|
const old = this.#intersecting;
|
|
if (old === newValue) {
|
|
return;
|
|
}
|
|
this.#intersecting = newValue;
|
|
if (newValue) {
|
|
void this.load(true);
|
|
} else {
|
|
this.cancel();
|
|
}
|
|
}
|
|
|
|
get intersecting() {
|
|
return this.#intersecting;
|
|
}
|
|
|
|
async load(cancelable: boolean): Promise<'DONE' | 'WAITED' | 'CANCELED' | 'LOADED' | 'ERRORED'> {
|
|
return await this.loader?.execute(async (signal: AbortSignal) => {
|
|
await this.fetch(signal);
|
|
}, cancelable);
|
|
}
|
|
|
|
protected abstract fetch(signal: AbortSignal): Promise<void>;
|
|
|
|
get assets(): TimelineAsset[] {
|
|
return this.#assets;
|
|
}
|
|
|
|
abstract get viewerAssets(): ViewerAsset[];
|
|
|
|
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;
|
|
}
|
|
|
|
protected handleLoadError(error: unknown) {
|
|
const _$t = get(t);
|
|
handleError(error, _$t('errors.failed_to_load_assets'));
|
|
}
|
|
|
|
cancel() {
|
|
this.loader?.cancel();
|
|
}
|
|
|
|
layout(_: boolean) {}
|
|
|
|
updateIntersection({ intersecting, actuallyIntersecting }: { intersecting: boolean; actuallyIntersecting: boolean }) {
|
|
this.intersecting = intersecting;
|
|
this.actuallyIntersecting = actuallyIntersecting;
|
|
}
|
|
|
|
abstract findAssetAbsolutePosition(assetId: string): number;
|
|
|
|
runAssetOperation(ids: Set<string>, operation: AssetOperation) {
|
|
if (ids.size === 0) {
|
|
return {
|
|
moveAssets: [] as MoveAsset[],
|
|
// 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: MoveAsset[] = [];
|
|
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 oldTime = { ...asset.localDateTime };
|
|
const opResult = operation(asset);
|
|
let remove = false;
|
|
if (opResult) {
|
|
remove = (opResult as { remove: boolean }).remove ?? false;
|
|
}
|
|
const newTime = asset.localDateTime;
|
|
if (oldTime.year !== newTime.year || oldTime.month !== newTime.month || oldTime.day !== newTime.day) {
|
|
const { year, month, day } = newTime;
|
|
remove = true;
|
|
moveAssets.push({ asset, date: { year, month, day } });
|
|
}
|
|
unprocessedIds.delete(assetId);
|
|
processedIds.add(assetId);
|
|
if (remove || this.timelineManager.isExcluded(asset)) {
|
|
this.viewerAssets.splice(index, 1);
|
|
changedGeometry = true;
|
|
}
|
|
}
|
|
return { moveAssets, processedIds, unprocessedIds, changedGeometry };
|
|
}
|
|
}
|