"use client"; import { useEffect, useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import { Captions, Folder, FolderOpen, Loader2, Plus, Save, Trash2 } from "lucide-react"; import { setSubtitleExtraPaths, setSubtitleCacheLimitMb } from "@/app/actions/settings"; import { useSettings } from "./SettingsProvider"; /** * Persistent recursive-scan folders for subtitle sidecars. The player * always finds same-folder sidecars; configure these only if subtitles * live in a separate location from videos. */ export function SubtitleLibraryPaths() { const { settings } = useSettings(); const router = useRouter(); const [, start] = useTransition(); const [draftSubExtras, setDraftSubExtras] = useState(settings.subtitleExtraPaths ?? []); const [newSubExtra, setNewSubExtra] = useState(""); const [saving, setSaving] = useState(false); const [picking, setPicking] = useState(null); const [error, setError] = useState(null); useEffect(() => { setDraftSubExtras(settings.subtitleExtraPaths ?? []); }, [settings.subtitleExtraPaths]); async function pickFolder(startPath: string): Promise { try { const r = await fetch("/api/pick-folder", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ start: startPath }), }); const j = await r.json(); if (!r.ok) throw new Error(j.error ?? `picker failed (${r.status})`); return typeof j.path === "string" && j.path ? j.path : null; } catch (e) { setError((e as Error).message); return null; } } const dirty = draftSubExtras.length !== (settings.subtitleExtraPaths ?? []).length || draftSubExtras.some((v, i) => v !== (settings.subtitleExtraPaths ?? [])[i]); function addSubExtra() { const v = newSubExtra.trim(); if (!v || draftSubExtras.includes(v)) return; setDraftSubExtras((cur) => [...cur, v]); setNewSubExtra(""); } function removeSubExtra(idx: number) { setDraftSubExtras((cur) => cur.filter((_, i) => i !== idx)); } function updateSubExtra(idx: number, value: string) { setDraftSubExtras((cur) => cur.map((v, i) => (i === idx ? value : v))); } async function browseExtra(idx: number) { setPicking(`extra-${idx}`); try { const p = await pickFolder(draftSubExtras[idx] ?? ""); if (p) updateSubExtra(idx, p); } finally { setPicking(null); } } async function browseNewExtra() { setPicking("new"); try { const p = await pickFolder(newSubExtra); if (p) { if (!draftSubExtras.includes(p)) { setDraftSubExtras((cur) => [...cur, p]); setNewSubExtra(""); } else { setNewSubExtra(p); } } } finally { setPicking(null); } } async function save() { setSaving(true); setError(null); try { await setSubtitleExtraPaths(draftSubExtras); start(() => router.refresh()); } catch (e) { setError((e as Error).message); } finally { setSaving(false); } } return (
Subtitle Library Folders
Recursively scanned for subtitle sidecars matching the playing video's stem or code (depth 3). The player always finds same-folder sidecars; configure these only if you keep subtitles in a separate location.
{draftSubExtras.length === 0 ? (
No subtitle folders.
) : ( draftSubExtras.map((p, i) => (
updateSubExtra(i, e.target.value)} className="w-full glass rounded-lg pl-9 pr-3 py-1.5 text-sm font-mono outline-none focus:border-[var(--color-cyan)]" />
)) )}
setNewSubExtra(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); addSubExtra(); } }} placeholder="D:\\Subtitles" className="w-full glass rounded-lg pl-9 pr-3 py-1.5 text-sm font-mono outline-none focus:border-[var(--color-cyan)]" />
{error && (
{error}
)}
); } function SubtitleCacheLimit() { const { settings } = useSettings(); const router = useRouter(); const [, start] = useTransition(); const [draft, setDraft] = useState(settings.subtitleCacheLimitMb ?? 100); const [saving, setSaving] = useState(false); useEffect(() => { setDraft(settings.subtitleCacheLimitMb ?? 100); }, [settings.subtitleCacheLimitMb]); const dirty = draft !== (settings.subtitleCacheLimitMb ?? 100); async function save() { setSaving(true); try { await setSubtitleCacheLimitMb(Math.max(0, Math.floor(draft))); start(() => router.refresh()); } finally { setSaving(false); } } return (
Subtitle Cache Size Limit
Soft cap on data/subtitle-cache/ (converted WebVTT files). When exceeded, oldest entries get evicted until size drops below 80% of the cap.{" "} 0 = unlimited.
setDraft(Math.max(0, Number(e.target.value) || 0))} className="w-28 glass rounded-lg px-3 py-1.5 text-sm font-mono outline-none focus:border-[var(--color-cyan)]" /> MB
); }