Files
Artificial-Sweetener-Simple…/web/src/orderedMediaPreviewTransaction.ts
T

117 lines
3.6 KiB
TypeScript

// SimpleSyrup - workflow-focused ComfyUI extensions for image generation
// Copyright (C) 2026 Artificial Sweetener and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
import type { NativePreviewSlot } from "./orderedMediaPreviewAffordances";
export interface OrderedMediaPreviewItem {
readonly sourceUrl: string;
readonly image: HTMLImageElement;
}
export interface OrderedMediaPreviewSurface {
readonly items: OrderedMediaPreviewItem[];
readonly slots: Array<() => NativePreviewSlot>;
readonly apply: (items: OrderedMediaPreviewItem[]) => void;
readonly release: () => void;
}
export interface OrderedMediaPreviewTransactionOptions {
readonly captureSurface: () => OrderedMediaPreviewSurface | null;
readonly authoritativeReady: () => boolean;
readonly stateChanged: () => void;
}
/** Reorder Comfy's loaded preview media without creating another drawing layer. */
export class OrderedMediaPreviewTransaction {
private surface: OrderedMediaPreviewSurface | null = null;
private items: OrderedMediaPreviewItem[] | null = null;
private handoffFrame: number | null = null;
constructor(private readonly options: OrderedMediaPreviewTransactionOptions) {}
/** Move an already-loaded native item without rebuilding its gallery. */
move(index: number, destination: number): boolean {
if (!this.begin()) return false;
const items = this.items;
if (
!items ||
index < 0 ||
index >= items.length ||
destination < 0 ||
destination >= items.length ||
index === destination
) {
return false;
}
const [moved] = items.splice(index, 1);
if (!moved) return false;
items.splice(destination, 0, moved);
this.apply();
return true;
}
/** Remove an already-loaded item and let the native gallery compact itself. */
remove(index: number): boolean {
if (!this.begin()) return false;
const items = this.items;
if (!items || index < 0 || index >= items.length) return false;
items.splice(index, 1);
this.apply();
return true;
}
/** Return live native-cell geometry while an optimistic transaction is active. */
activeSlots(): NativePreviewSlot[] | null {
if (!this.surface || !this.items) return null;
return this.surface.slots.slice(0, this.items.length).map((slot) => slot());
}
/** End the transaction after Comfy's authoritative native preview is ready. */
authoritativePublished(): void {
if (!this.surface || this.handoffFrame !== null) return;
this.scheduleHandoff();
}
/** Release native surface changes and stop any pending handoff check. */
dispose(): void {
if (this.handoffFrame !== null) cancelAnimationFrame(this.handoffFrame);
this.handoffFrame = null;
this.finish();
}
private begin(): boolean {
if (this.surface && this.items) return true;
const surface = this.options.captureSurface();
if (!surface || surface.items.length === 0) return false;
this.surface = surface;
this.items = [...surface.items];
return true;
}
private apply(): void {
if (!this.surface || !this.items) return;
this.surface.apply(this.items);
this.options.stateChanged();
}
private scheduleHandoff(): void {
this.handoffFrame = requestAnimationFrame(() => {
this.handoffFrame = null;
if (!this.surface) return;
if (this.options.authoritativeReady()) {
this.finish();
this.options.stateChanged();
return;
}
this.scheduleHandoff();
});
}
private finish(): void {
this.surface?.release();
this.surface = null;
this.items = null;
}
}