summaryrefslogtreecommitdiff
path: root/apps/web/src/timeline/components/graph-editor/custom-presets-store.ts
blob: 4be2adfde667c423fd515bee8b16672df959d27e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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();
}