Files
pinkudex/app/actions/settings.ts
T
2026-05-26 22:46:00 +02:00

143 lines
5.7 KiB
TypeScript

"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<WhisperJavSettings>) {
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<string>();
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<string>();
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<typeof setBoolSetting>[0];
export type WritableNumberKey = Parameters<typeof setNumberSetting>[0];
export type WritableColorKey = Parameters<typeof setColorSetting>[0];
export type WritableSettings = Pick<AppSettings, WritableBoolKey | WritableNumberKey | WritableColorKey>;