"use server"; import { revalidatePath } from "next/cache"; import { setAppSetting, type AppSettings, type WhisperJavSettings, APP_SETTINGS_DEFAULTS } from "@/lib/db/appSettings"; export async function setBoolSetting( key: "fadeTransitions" | "purgeFilesOnDelete" | "useRecycleBin", value: boolean, ) { setAppSetting(key, value); revalidatePath("/"); } export async function setTranscodeMode(value: "off" | "always" | "auto-predicate" | "auto-runtime") { if (value !== "off" && value !== "always" && value !== "auto-predicate" && value !== "auto-runtime") return; setAppSetting("transcodeMode", value); revalidatePath("/"); } export async function setNumberSetting( key: "fadeDurationMs" | "trashRetentionDays" | "gridColumns" | "gridColumnsPortrait" | "supersededRetentionDays" | "coverPageSize", value: number, ) { if (!Number.isFinite(value)) return; if (key === "gridColumns" && (value < 2 || value > 4)) return; if (key === "gridColumnsPortrait" && (value < 4 || value > 10)) return; if (key === "trashRetentionDays" && value < 0) return; if (key === "supersededRetentionDays" && value < 0) return; if (key === "coverPageSize" && (value < 25 || value > 500)) return; setAppSetting(key, value); revalidatePath("/"); } const HEX_RE = /^#[0-9a-fA-F]{6}$/; export async function setColorSetting( key: "accentPrimary" | "accentSecondary", value: string, ) { const normalized = value === "" ? "" : value.toLowerCase(); if (normalized !== "" && !HEX_RE.test(normalized)) return; setAppSetting(key, normalized); revalidatePath("/"); } export async function setPaginationMode(value: "url" | "scroll") { if (value !== "url" && value !== "scroll") return; setAppSetting("paginationMode", value); revalidatePath("/"); } export async function setSettingsLayout(value: "sidebar" | "three-column") { if (value !== "sidebar" && value !== "three-column") return; setAppSetting("settingsLayout", value); revalidatePath("/"); } export async function setVideoLibraryPath(value: string) { setAppSetting("videoLibraryPath", value.trim()); revalidatePath("/"); } export async function setPartSuffixPatterns(values: string[]) { // Trim, drop blanks, preserve order. Validation of token grammar // (e.g. `{N}`, `{L}`) happens client-side; storage accepts whatever // the user typed so a malformed pattern doesn't silently disappear. const cleaned = (values ?? []).map((v) => (v ?? "").trim()).filter(Boolean); setAppSetting("partSuffixPatterns", cleaned); // Reclassify on the next video scan; trigger a rescan so the change // takes effect without a manual refresh. try { const { rescanVideoIndex } = await import("@/lib/video"); await rescanVideoIndex(); } catch (e) { console.error("[settings] failed to rescan video index after pattern change:", e); } revalidatePath("/"); } export async function setWhisperJavSettings(values: Partial) { const sanitized: WhisperJavSettings = { ...APP_SETTINGS_DEFAULTS.whisperjav, ...values, cliPath: typeof values.cliPath === "string" ? values.cliPath.trim() : APP_SETTINGS_DEFAULTS.whisperjav.cliPath, }; // Validate enum members so a bad client payload can't poison the row. const QUALITIES: WhisperJavSettings["quality"][] = ["fast", "balanced", "qwen"]; const SOURCE_LANGS: WhisperJavSettings["sourceLanguage"][] = ["japanese", "korean", "chinese", "english"]; const OUTPUT_MODES: WhisperJavSettings["outputMode"][] = ["native", "direct-to-english"]; const SENSITIVITIES: WhisperJavSettings["sensitivity"][] = ["conservative", "balanced", "aggressive"]; const LOCATIONS: WhisperJavSettings["outputLocation"][] = ["beside-video", "data-folder"]; if (!QUALITIES.includes(sanitized.quality)) sanitized.quality = "balanced"; if (!SOURCE_LANGS.includes(sanitized.sourceLanguage)) sanitized.sourceLanguage = "japanese"; if (!OUTPUT_MODES.includes(sanitized.outputMode)) sanitized.outputMode = "native"; if (!SENSITIVITIES.includes(sanitized.sensitivity)) sanitized.sensitivity = "balanced"; if (!LOCATIONS.includes(sanitized.outputLocation)) sanitized.outputLocation = "beside-video"; sanitized.noSignature = sanitized.noSignature !== false; const retention = Number(sanitized.retentionDays); sanitized.retentionDays = Number.isFinite(retention) && retention >= 0 ? Math.floor(retention) : 30; setAppSetting("whisperjav", sanitized); revalidatePath("/"); } export async function setSubtitleCacheLimitMb(value: number) { if (!Number.isFinite(value) || value < 0) return; setAppSetting("subtitleCacheLimitMb", Math.floor(value)); revalidatePath("/"); } export async function setSubtitleExtraPaths(values: string[]) { const seen = new Set(); const cleaned: string[] = []; for (const v of values) { const t = (v ?? "").trim(); if (!t) continue; const key = t.toLowerCase(); if (seen.has(key)) continue; seen.add(key); cleaned.push(t); } setAppSetting("subtitleExtraPaths", cleaned); revalidatePath("/"); } export async function setVideoExtraPaths(values: string[]) { // Trim, drop blanks, dedupe (case-insensitive on Windows-friendly compare). const seen = new Set(); const cleaned: string[] = []; for (const v of values) { const t = (v ?? "").trim(); if (!t) continue; const key = t.toLowerCase(); if (seen.has(key)) continue; seen.add(key); cleaned.push(t); } setAppSetting("videoExtraPaths", cleaned); revalidatePath("/"); } export type WritableBoolKey = Parameters[0]; export type WritableNumberKey = Parameters[0]; export type WritableColorKey = Parameters[0]; export type WritableSettings = Pick;