nsfwapp/frontend/src/components/ui/RecorderSettings.tsx
2026-01-02 13:13:03 +01:00

288 lines
10 KiB
TypeScript

// RecorderSettings.tsx
'use client'
import { useEffect, useState } from 'react'
import Button from './Button'
import Card from './Card'
import LabeledSwitch from './LabeledSwitch'
import GenerateAssetsTask from './GenerateAssetsTask'
type RecorderSettings = {
recordDir: string
doneDir: string
ffmpegPath?: string
// ✅ neue Optionen
autoAddToDownloadList?: boolean
autoStartAddedDownloads?: boolean
// ✅ Chaturbate Online-Rooms API (Backend pollt, sobald aktiviert)
useChaturbateApi?: boolean
blurPreviews?: boolean
}
const DEFAULTS: RecorderSettings = {
// ✅ relativ zur .exe (Backend löst das auf)
recordDir: 'records',
doneDir: 'records/done',
ffmpegPath: '',
// ✅ defaults für switches
autoAddToDownloadList: true,
autoStartAddedDownloads: true,
useChaturbateApi: false,
blurPreviews: false,
}
type Props = {
onAssetsGenerated?: () => void
}
export default function RecorderSettings({ onAssetsGenerated }: Props) {
const [value, setValue] = useState<RecorderSettings>(DEFAULTS)
const [saving, setSaving] = useState(false)
const [browsing, setBrowsing] = useState<'record' | 'done' | 'ffmpeg' | null>(null)
const [msg, setMsg] = useState<string | null>(null)
const [err, setErr] = useState<string | null>(null)
useEffect(() => {
let alive = true
fetch('/api/settings', { cache: 'no-store' })
.then(async (r) => {
if (!r.ok) throw new Error(await r.text())
return r.json()
})
.then((data: RecorderSettings) => {
if (!alive) return
setValue({
recordDir: (data.recordDir || DEFAULTS.recordDir).toString(),
doneDir: (data.doneDir || DEFAULTS.doneDir).toString(),
ffmpegPath: String(data.ffmpegPath ?? DEFAULTS.ffmpegPath ?? ''),
// ✅ falls backend die Felder noch nicht hat -> defaults nutzen
autoAddToDownloadList: data.autoAddToDownloadList ?? DEFAULTS.autoAddToDownloadList,
autoStartAddedDownloads: data.autoStartAddedDownloads ?? DEFAULTS.autoStartAddedDownloads,
useChaturbateApi: data.useChaturbateApi ?? DEFAULTS.useChaturbateApi,
blurPreviews: data.blurPreviews ?? DEFAULTS.blurPreviews,
})
})
.catch(() => {
// backend evtl. noch alt -> defaults lassen
})
return () => {
alive = false
}
}, [])
async function browse(target: 'record' | 'done' | 'ffmpeg') {
setErr(null)
setMsg(null)
setBrowsing(target)
try {
window.focus()
const res = await fetch(`/api/settings/browse?target=${target}`, { cache: 'no-store' })
if (res.status === 204) return // user cancelled
if (!res.ok) {
const t = await res.text().catch(() => '')
throw new Error(t || `HTTP ${res.status}`)
}
const data = (await res.json()) as { path?: string }
const p = (data.path ?? '').trim()
if (!p) return
setValue((v) => {
if (target === 'record') return { ...v, recordDir: p }
if (target === 'done') return { ...v, doneDir: p }
return { ...v, ffmpegPath: p }
})
} catch (e: any) {
setErr(e?.message ?? String(e))
} finally {
setBrowsing(null)
}
}
async function save() {
setErr(null)
setMsg(null)
const recordDir = value.recordDir.trim()
const doneDir = value.doneDir.trim()
const ffmpegPath = (value.ffmpegPath ?? '').trim()
if (!recordDir || !doneDir) {
setErr('Bitte Aufnahme-Ordner und Ziel-Ordner angeben.')
return
}
// ✅ Switch-Logik: Autostart nur sinnvoll, wenn Auto-Add aktiv ist
const autoAddToDownloadList = !!value.autoAddToDownloadList
const autoStartAddedDownloads = autoAddToDownloadList ? !!value.autoStartAddedDownloads : false
const useChaturbateApi = !!value.useChaturbateApi
const blurPreviews = !!value.blurPreviews
setSaving(true)
try {
const res = await fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recordDir,
doneDir,
ffmpegPath,
autoAddToDownloadList,
autoStartAddedDownloads,
useChaturbateApi,
blurPreviews,
}),
})
if (!res.ok) {
const t = await res.text().catch(() => '')
throw new Error(t || `HTTP ${res.status}`)
}
setMsg('✅ Gespeichert.')
window.dispatchEvent(new CustomEvent('recorder-settings-updated'))
} catch (e: any) {
setErr(e?.message ?? String(e))
} finally {
setSaving(false)
}
}
return (
<Card
header={
<div className="flex items-center justify-between gap-4">
<div>
<div className="text-base font-semibold text-gray-900 dark:text-white">Einstellungen</div>
</div>
<Button variant="primary" onClick={save} disabled={saving}>
Speichern
</Button>
</div>
}
grayBody
>
<div className="space-y-4">
{err && (
<div className="rounded-md bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-500/10 dark:text-red-200">
{err}
</div>
)}
{msg && (
<div className="rounded-md bg-green-50 px-3 py-2 text-sm text-green-700 dark:bg-green-500/10 dark:text-green-200">
{msg}
</div>
)}
{/* Aufnahme-Ordner */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-12 sm:items-center">
<label className="text-sm font-medium text-gray-900 dark:text-gray-200 sm:col-span-3">Aufnahme-Ordner</label>
<div className="sm:col-span-9 flex gap-2">
<input
value={value.recordDir}
onChange={(e) => setValue((v) => ({ ...v, recordDir: e.target.value }))}
placeholder="records (oder absolut: C:\records / /mnt/data/records)"
className="min-w-0 flex-1 rounded-md px-3 py-2 text-sm bg-white text-gray-900
dark:bg-white/10 dark:text-white"
/>
<Button variant="secondary" onClick={() => browse('record')} disabled={saving || browsing !== null}>
Durchsuchen...
</Button>
</div>
</div>
{/* Fertige Downloads */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-12 sm:items-center">
<label className="text-sm font-medium text-gray-900 dark:text-gray-200 sm:col-span-3">
Fertige Downloads nach
</label>
<div className="sm:col-span-9 flex gap-2">
<input
value={value.doneDir}
onChange={(e) => setValue((v) => ({ ...v, doneDir: e.target.value }))}
placeholder="records/done"
className="min-w-0 flex-1 rounded-md px-3 py-2 text-sm bg-white text-gray-900
dark:bg-white/10 dark:text-white"
/>
<Button variant="secondary" onClick={() => browse('done')} disabled={saving || browsing !== null}>
Durchsuchen...
</Button>
</div>
</div>
{/* ffmpeg.exe */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-12 sm:items-center">
<label className="text-sm font-medium text-gray-900 dark:text-gray-200 sm:col-span-3">ffmpeg.exe</label>
<div className="sm:col-span-9 flex gap-2">
<input
value={value.ffmpegPath ?? ''}
onChange={(e) => setValue((v) => ({ ...v, ffmpegPath: e.target.value }))}
placeholder="Leer = automatisch (FFMPEG_PATH / ffmpeg im PATH)"
className="min-w-0 flex-1 rounded-md px-3 py-2 text-sm bg-white text-gray-900
dark:bg-white/10 dark:text-white"
/>
<Button variant="secondary" onClick={() => browse('ffmpeg')} disabled={saving || browsing !== null}>
Durchsuchen...
</Button>
</div>
</div>
{/* Automatisierung */}
<div className="mt-2 border-t border-gray-200 pt-4 dark:border-white/10">
<div className="space-y-3">
<LabeledSwitch
checked={!!value.autoAddToDownloadList}
onChange={(checked) =>
setValue((v) => ({
...v,
autoAddToDownloadList: checked,
// wenn aus, Autostart gleich mit aus
autoStartAddedDownloads: checked ? v.autoStartAddedDownloads : false,
}))
}
label="Automatisch zur Downloadliste hinzufügen"
description="Neue Links/Modelle werden automatisch in die Downloadliste übernommen."
/>
<LabeledSwitch
checked={!!value.autoStartAddedDownloads}
onChange={(checked) => setValue((v) => ({ ...v, autoStartAddedDownloads: checked }))}
disabled={!value.autoAddToDownloadList}
label="Hinzugefügte Downloads automatisch starten"
description="Wenn ein Download hinzugefügt wurde, startet er direkt (sofern möglich)."
/>
<LabeledSwitch
checked={!!value.useChaturbateApi}
onChange={(checked) => setValue((v) => ({ ...v, useChaturbateApi: checked }))}
label="Chaturbate API"
description="Wenn aktiv, pollt das Backend alle paar Sekunden die Online-Rooms API und cached die aktuell online Models."
/>
<LabeledSwitch
checked={!!value.blurPreviews}
onChange={(checked) => setValue((v) => ({ ...v, blurPreviews: checked }))}
label="Vorschaubilder blurren"
description="Weichzeichnet Vorschaubilder/Teaser (praktisch auf mobilen Geräten oder im öffentlichen Umfeld)."
/>
</div>
</div>
{/* Tasks */}
<div className="mt-2 border-t border-gray-200 pt-4 dark:border-white/10">
<div className="mb-2 text-sm font-semibold text-gray-900 dark:text-white">Tasks</div>
<GenerateAssetsTask onFinished={onAssetsGenerated} />
</div>
</div>
</Card>
)
}