288 lines
10 KiB
TypeScript
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>
|
|
)
|
|
}
|