102 lines
2.5 KiB
TypeScript
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();
|
|
}
|