Archived
feat: uv config other feats
- add uv configuration for the backend - update frontend to make auth work - add new auth endpoints - add bookmars feat - add reader feat
This commit is contained in:
@@ -1,15 +1,25 @@
|
||||
/**
|
||||
* useReadingSettings — fetch and manage user reading preferences.
|
||||
* Applies settings as CSS custom properties on the document root.
|
||||
* Preview applies instantly to the reader; API saves are debounced for sliders.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
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,
|
||||
@@ -24,22 +34,38 @@ const DEFAULT_SETTINGS: ReadingSettings = {
|
||||
updated_at: "",
|
||||
};
|
||||
|
||||
function applyCssVariables(settings: ReadingSettings): void {
|
||||
const SAVE_DEBOUNCE_MS = 600;
|
||||
|
||||
export 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-font-family", settings.font_family);
|
||||
root.style.setProperty("--reader-font-size", `${settings.font_size}px`);
|
||||
root.style.setProperty("--reader-line-height", String(settings.line_height));
|
||||
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>) => Promise<void>;
|
||||
updateSettings: (
|
||||
partial: Partial<ReadingSettings>,
|
||||
persist?: SettingsPersistMode,
|
||||
) => void;
|
||||
flushSettings: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useReadingSettings(): UseReadingSettingsReturn {
|
||||
@@ -47,7 +73,45 @@ export function useReadingSettings(): UseReadingSettingsReturn {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Fetch settings on mount
|
||||
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);
|
||||
@@ -55,16 +119,15 @@ export function useReadingSettings(): UseReadingSettingsReturn {
|
||||
.then((data) => {
|
||||
if (!cancelled) {
|
||||
setSettings(data);
|
||||
applyCssVariables(data);
|
||||
applyReadingCssVariables(data);
|
||||
}
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (!cancelled) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Failed to load reading settings"
|
||||
err instanceof Error ? err.message : "Failed to load reading settings",
|
||||
);
|
||||
// Apply defaults
|
||||
applyCssVariables(DEFAULT_SETTINGS);
|
||||
applyReadingCssVariables(DEFAULT_SETTINGS);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -72,25 +135,26 @@ export function useReadingSettings(): UseReadingSettingsReturn {
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (saveTimer.current) clearTimeout(saveTimer.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const updateSettings = useCallback(
|
||||
async (partial: Partial<ReadingSettings>) => {
|
||||
try {
|
||||
const updated = await updateReadingSettings(partial);
|
||||
setSettings(updated);
|
||||
applyCssVariables(updated);
|
||||
setError(null);
|
||||
} catch (err: unknown) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Failed to update reading settings"
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
(partial: Partial<ReadingSettings>, persist: SettingsPersistMode = "debounced") => {
|
||||
setSettings((prev) => {
|
||||
const next = mergeSettings(prev, partial);
|
||||
applyReadingCssVariables(next);
|
||||
return next;
|
||||
});
|
||||
schedulePersist(partial, persist === "immediate");
|
||||
},
|
||||
[]
|
||||
[schedulePersist],
|
||||
);
|
||||
|
||||
return { settings, isLoading, error, updateSettings };
|
||||
}
|
||||
const flushSettings = useCallback(async () => {
|
||||
if (saveTimer.current) clearTimeout(saveTimer.current);
|
||||
await runPersist();
|
||||
}, [runPersist]);
|
||||
|
||||
return { settings, isLoading, error, updateSettings, flushSettings };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user