summaryrefslogtreecommitdiff
path: root/apps/web/src/timeline/components/graph-editor/custom-presets-store.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
commit3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch)
tree57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/timeline/components/graph-editor/custom-presets-store.ts
parent729d10592f4e802f91c555c309ff3ba4b119f843 (diff)
downloadopencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/timeline/components/graph-editor/custom-presets-store.ts')
-rw-r--r--apps/web/src/timeline/components/graph-editor/custom-presets-store.ts101
1 files changed, 101 insertions, 0 deletions
diff --git a/apps/web/src/timeline/components/graph-editor/custom-presets-store.ts b/apps/web/src/timeline/components/graph-editor/custom-presets-store.ts
new file mode 100644
index 00000000..4be2adfd
--- /dev/null
+++ b/apps/web/src/timeline/components/graph-editor/custom-presets-store.ts
@@ -0,0 +1,101 @@
+"use client";
+
+import { useSyncExternalStore } from "react";
+import { generateUUID } from "@/utils/id";
+import type { NormalizedCubicBezier } from "@/animation/types";
+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();
+}