OpenCut/apps/web/src/timeline/components/graph-editor/custom-presets-store.ts

102 lines
2.5 KiB
TypeScript

"use client";
import { useSyncExternalStore } from "react";
import { generateUUID } from "@/utils/id";
import type { NormalizedCubicBezier } from "@/model/decorations/animations";
import type { EasingPreset } from "./easing-presets";
const STORAGE_KEY = "graph-editor-presets";
let cachedPresets: EasingPreset[] | null = null;
const listeners = new Set<() => void>();
function isValidPresetArray(value: unknown): value is EasingPreset[] {
return (
Array.isArray(value) &&
value.every(
(item) =>
typeof item === "object" &&
item !== null &&
typeof item.id === "string" &&
typeof item.label === "string" &&
Array.isArray(item.value) &&
item.value.length === 4 &&
item.value.every((number: unknown) => typeof number === "number"),
)
);
}
function readFromStorage(): EasingPreset[] {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
const parsed: unknown = JSON.parse(raw);
return isValidPresetArray(parsed) ? parsed : [];
} catch {
// Silently recover — corrupted localStorage shouldn't crash the editor
return [];
}
}
function writeToStorage({ presets }: { presets: EasingPreset[] }): void {
localStorage.setItem(STORAGE_KEY, JSON.stringify(presets));
}
function getSnapshot(): EasingPreset[] {
cachedPresets ??= readFromStorage();
return cachedPresets;
}
function getServerSnapshot(): EasingPreset[] {
return [];
}
function notify(): void {
cachedPresets = null;
for (const listener of listeners) {
listener();
}
}
function onStorageChange(event: StorageEvent): void {
if (event.key === STORAGE_KEY) notify();
}
function subscribe(listener: () => void): () => void {
if (listeners.size === 0 && typeof window !== "undefined") {
window.addEventListener("storage", onStorageChange);
}
listeners.add(listener);
return () => {
listeners.delete(listener);
if (listeners.size === 0 && typeof window !== "undefined") {
window.removeEventListener("storage", onStorageChange);
}
};
}
export function useCustomPresets(): EasingPreset[] {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
export function savePreset({ value }: { value: NormalizedCubicBezier }): void {
const current = getSnapshot();
writeToStorage({
presets: [
...current,
{
id: generateUUID(),
label: `Custom ${current.length + 1}`,
value,
isCustom: true,
},
],
});
notify();
}
export function removePreset({ id }: { id: string }): void {
writeToStorage({ presets: getSnapshot().filter((preset) => preset.id !== id) });
notify();
}