Files
2026-05-26 22:46:00 +02:00

96 lines
3.8 KiB
TypeScript

"use client";
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
import type { AppSettings } from "@/lib/db/appSettings";
import { setBoolSetting, setNumberSetting, setColorSetting, setPaginationMode } from "@/app/actions/settings";
type BoolKey = "fadeTransitions" | "purgeFilesOnDelete" | "useRecycleBin";
type NumKey = "fadeDurationMs" | "trashRetentionDays" | "gridColumns" | "gridColumnsPortrait" | "supersededRetentionDays" | "coverPageSize";
type ColorKey = "accentPrimary" | "accentSecondary";
type PaginationModeKey = "paginationMode";
interface Ctx {
settings: AppSettings;
set(key: BoolKey, value: boolean): void;
set(key: NumKey, value: number): void;
set(key: ColorKey, value: string): void;
set(key: PaginationModeKey, value: AppSettings["paginationMode"]): void;
}
const SettingsCtx = createContext<Ctx | null>(null);
const NUM_KEYS = new Set<string>(["fadeDurationMs", "trashRetentionDays", "gridColumns", "gridColumnsPortrait", "supersededRetentionDays", "coverPageSize"]);
const COLOR_KEYS = new Set<string>(["accentPrimary", "accentSecondary"]);
const PAGINATION_MODE_KEYS = new Set<string>(["paginationMode"]);
const ACCENT_VARS: Record<ColorKey, [string, string]> = {
accentPrimary: ["--color-cyan", "--color-cyan-glow"],
accentSecondary: ["--color-violet", "--color-violet-glow"],
};
export function SettingsProvider({
initial,
children,
}: {
initial: AppSettings;
children: React.ReactNode;
}) {
const [settings, setSettings] = useState<AppSettings>(initial);
// The server is the source of truth, but the parent layout passes the latest
// server values on every render. Keep our state in sync after server mutations.
useEffect(() => {
setSettings(initial);
}, [initial]);
useEffect(() => {
document.documentElement.dataset.fade = settings.fadeTransitions ? "on" : "off";
document.documentElement.style.setProperty("--fade-duration", `${settings.fadeDurationMs}ms`);
}, [settings.fadeTransitions, settings.fadeDurationMs]);
useEffect(() => {
const n = Math.max(2, Math.min(4, settings.gridColumns || 3));
document.documentElement.style.setProperty("--grid-cols", String(n));
}, [settings.gridColumns]);
useEffect(() => {
const n = Math.max(4, Math.min(10, settings.gridColumnsPortrait || 6));
document.documentElement.style.setProperty("--grid-cols-portrait", String(n));
}, [settings.gridColumnsPortrait]);
useEffect(() => {
const root = document.documentElement;
for (const [key, [base, glow]] of Object.entries(ACCENT_VARS) as [ColorKey, [string, string]][]) {
const value = settings[key];
if (value) {
root.style.setProperty(base, value);
root.style.setProperty(glow, value);
} else {
root.style.removeProperty(base);
root.style.removeProperty(glow);
}
}
}, [settings.accentPrimary, settings.accentSecondary]);
const set = useCallback((key: BoolKey | NumKey | ColorKey | PaginationModeKey, value: boolean | number | string) => {
setSettings((cur) => ({ ...cur, [key]: value }));
if (NUM_KEYS.has(key)) {
void setNumberSetting(key as NumKey, value as number);
} else if (COLOR_KEYS.has(key)) {
void setColorSetting(key as ColorKey, value as string);
} else if (PAGINATION_MODE_KEYS.has(key)) {
void setPaginationMode(value as AppSettings["paginationMode"]);
} else {
void setBoolSetting(key as BoolKey, value as boolean);
}
}, []) as Ctx["set"];
const value = useMemo<Ctx>(() => ({ settings, set }), [settings, set]);
return <SettingsCtx.Provider value={value}>{children}</SettingsCtx.Provider>;
}
export function useSettings() {
const ctx = useContext(SettingsCtx);
if (!ctx) throw new Error("useSettings must be used within SettingsProvider");
return ctx;
}