This repository has been archived on 2026-07-21. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
cloud-reader/frontend/src/hooks/useReadingSettings.ts
T
2026-06-04 06:42:27 -05:00

161 lines
4.7 KiB
TypeScript

/**
* useReadingSettings — fetch and manage user reading preferences.
* Preview applies instantly to the reader; API saves are debounced for sliders.
*/
import { useCallback, useEffect, useRef, useState } from "react";
import {
getReadingSettings,
updateReadingSettings,
} from "../api/reader";
import type { ReadingSettings } from "../types/reader";
const THEME_COLORS: Record<
ReadingSettings["theme"],
Pick<ReadingSettings, "background_color" | "text_color">
> = {
sepia: { background_color: "#f5f0eb", text_color: "#1a1a1a" },
dark: { background_color: "#1a1a2e", text_color: "#e0e0e0" },
light: { background_color: "#ffffff", text_color: "#1a1a1a" },
paper: { background_color: "#e8e0d4", text_color: "#2c2c2c" },
};
const DEFAULT_SETTINGS: ReadingSettings = {
font_family: "serif",
font_size: 18,
line_height: 1.6,
margin_width: 16,
background_color: "#f5f0eb",
text_color: "#1a1a1a",
brightness: 100,
orientation_lock: "auto",
theme: "sepia",
created_at: "",
updated_at: "",
};
const SAVE_DEBOUNCE_MS = 600;
function applyReadingCssVariables(settings: ReadingSettings): void {
const root = document.documentElement;
root.style.setProperty("--reader-bg", settings.background_color);
root.style.setProperty("--reader-text", settings.text_color);
root.style.setProperty("--reader-margin", `${settings.margin_width}px`);
root.style.setProperty("--reader-brightness", `${settings.brightness}%`);
}
function mergeSettings(
prev: ReadingSettings,
partial: Partial<ReadingSettings>,
): ReadingSettings {
const next = { ...prev, ...partial };
if (partial.theme && THEME_COLORS[partial.theme]) {
Object.assign(next, THEME_COLORS[partial.theme]);
}
return next;
}
export type SettingsPersistMode = "debounced" | "immediate";
export interface UseReadingSettingsReturn {
settings: ReadingSettings;
isLoading: boolean;
error: string | null;
updateSettings: (
partial: Partial<ReadingSettings>,
persist?: SettingsPersistMode,
) => void;
flushSettings: () => Promise<void>;
}
export function useReadingSettings(): UseReadingSettingsReturn {
const [settings, setSettings] = useState<ReadingSettings>(DEFAULT_SETTINGS);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const pendingPatch = useRef<Partial<ReadingSettings>>({});
const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const saving = useRef(false);
const runPersist = useCallback(async () => {
const payload = { ...pendingPatch.current };
pendingPatch.current = {};
if (Object.keys(payload).length === 0 || saving.current) return;
saving.current = true;
try {
const updated = await updateReadingSettings(payload);
setSettings(updated);
applyReadingCssVariables(updated);
setError(null);
} catch (err: unknown) {
setError(
err instanceof Error ? err.message : "Failed to update reading settings",
);
} finally {
saving.current = false;
}
}, []);
const schedulePersist = useCallback(
(partial: Partial<ReadingSettings>, immediate: boolean) => {
Object.assign(pendingPatch.current, partial);
if (saveTimer.current) clearTimeout(saveTimer.current);
if (immediate) {
void runPersist();
return;
}
saveTimer.current = setTimeout(() => {
void runPersist();
}, SAVE_DEBOUNCE_MS);
},
[runPersist],
);
useEffect(() => {
let cancelled = false;
setIsLoading(true);
getReadingSettings()
.then((data) => {
if (!cancelled) {
setSettings(data);
applyReadingCssVariables(data);
}
})
.catch((err: unknown) => {
if (!cancelled) {
setError(
err instanceof Error ? err.message : "Failed to load reading settings",
);
applyReadingCssVariables(DEFAULT_SETTINGS);
}
})
.finally(() => {
if (!cancelled) setIsLoading(false);
});
return () => {
cancelled = true;
if (saveTimer.current) clearTimeout(saveTimer.current);
};
}, []);
const updateSettings = useCallback(
(partial: Partial<ReadingSettings>, persist: SettingsPersistMode = "debounced") => {
setSettings((prev) => {
const next = mergeSettings(prev, partial);
applyReadingCssVariables(next);
return next;
});
schedulePersist(partial, persist === "immediate");
},
[schedulePersist],
);
const flushSettings = useCallback(async () => {
if (saveTimer.current) clearTimeout(saveTimer.current);
await runPersist();
}, [runPersist]);
return { settings, isLoading, error, updateSettings, flushSettings };
}