bugfixes
This commit is contained in:
parent
3e32f9480a
commit
a12d9dd1a9
@ -1,7 +1,7 @@
|
|||||||
# Build and Debian package metadata.
|
# Build and Debian package metadata.
|
||||||
PACKAGE_NAME="nsfwapp"
|
PACKAGE_NAME="nsfwapp"
|
||||||
APP_TITLE="NSFW App"
|
APP_TITLE="NSFW App"
|
||||||
VERSION="1.2.1"
|
VERSION="1.3.0"
|
||||||
ARCH="amd64"
|
ARCH="amd64"
|
||||||
SECTION="video"
|
SECTION="video"
|
||||||
PRIORITY="optional"
|
PRIORITY="optional"
|
||||||
|
|||||||
BIN
backend/dist/nsfwapp-linux-amd64
vendored
BIN
backend/dist/nsfwapp-linux-amd64
vendored
Binary file not shown.
BIN
backend/dist/nsfwapp.exe
vendored
BIN
backend/dist/nsfwapp.exe
vendored
Binary file not shown.
BIN
backend/dist/nsfwapp_amd64.deb
vendored
BIN
backend/dist/nsfwapp_amd64.deb
vendored
Binary file not shown.
@ -25,10 +25,12 @@ const (
|
|||||||
|
|
||||||
func normalizePostgresMode(raw string) string {
|
func normalizePostgresMode(raw string) string {
|
||||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||||
|
case postgresModeURL:
|
||||||
|
return postgresModeURL
|
||||||
case postgresModeLocal:
|
case postgresModeLocal:
|
||||||
return postgresModeLocal
|
return postgresModeLocal
|
||||||
default:
|
default:
|
||||||
return postgresModeURL
|
return postgresModeLocal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,12 +78,16 @@ func normalizePostgresSettings(s *RecorderSettings) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.DatabaseType = normalizeDatabaseType(s.DatabaseType)
|
s.DatabaseType = normalizeDatabaseType(s.DatabaseType)
|
||||||
s.PostgresMode = normalizePostgresMode(s.PostgresMode)
|
s.DatabaseURL = strings.TrimSpace(s.DatabaseURL)
|
||||||
|
if strings.TrimSpace(s.PostgresMode) == "" && s.DatabaseURL != "" {
|
||||||
|
s.PostgresMode = postgresModeURL
|
||||||
|
} else {
|
||||||
|
s.PostgresMode = normalizePostgresMode(s.PostgresMode)
|
||||||
|
}
|
||||||
s.PostgresDataDir = normalizePostgresDataDir(s.PostgresDataDir)
|
s.PostgresDataDir = normalizePostgresDataDir(s.PostgresDataDir)
|
||||||
s.PostgresPort = normalizePostgresPort(s.PostgresPort)
|
s.PostgresPort = normalizePostgresPort(s.PostgresPort)
|
||||||
s.PostgresDatabase = normalizePostgresName(s.PostgresDatabase, defaultPostgresDatabase)
|
s.PostgresDatabase = normalizePostgresName(s.PostgresDatabase, defaultPostgresDatabase)
|
||||||
s.PostgresUser = normalizePostgresName(s.PostgresUser, defaultPostgresUser)
|
s.PostgresUser = normalizePostgresName(s.PostgresUser, defaultPostgresUser)
|
||||||
s.DatabaseURL = strings.TrimSpace(s.DatabaseURL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func postgresSettingsKey(s RecorderSettings) string {
|
func postgresSettingsKey(s RecorderSettings) string {
|
||||||
|
|||||||
@ -72,7 +72,7 @@ var (
|
|||||||
settingsMu sync.Mutex
|
settingsMu sync.Mutex
|
||||||
settings = RecorderSettings{
|
settings = RecorderSettings{
|
||||||
DatabaseType: databaseTypePostgres,
|
DatabaseType: databaseTypePostgres,
|
||||||
PostgresMode: postgresModeURL,
|
PostgresMode: postgresModeLocal,
|
||||||
PostgresDataDir: defaultPostgresDataDir,
|
PostgresDataDir: defaultPostgresDataDir,
|
||||||
PostgresPort: defaultPostgresPort,
|
PostgresPort: defaultPostgresPort,
|
||||||
PostgresDatabase: defaultPostgresDatabase,
|
PostgresDatabase: defaultPostgresDatabase,
|
||||||
|
|||||||
@ -286,7 +286,7 @@ function SettingsSection(props: {
|
|||||||
|
|
||||||
const DEFAULTS: RecorderSettings = {
|
const DEFAULTS: RecorderSettings = {
|
||||||
databaseType: 'postgres',
|
databaseType: 'postgres',
|
||||||
postgresMode: 'url',
|
postgresMode: 'local',
|
||||||
postgresDataDir: 'postgres-data',
|
postgresDataDir: 'postgres-data',
|
||||||
postgresPort: 55432,
|
postgresPort: 55432,
|
||||||
postgresDatabase: 'nsfwapp',
|
postgresDatabase: 'nsfwapp',
|
||||||
@ -355,7 +355,7 @@ function shortTaskFilename(name?: string, max = 52) {
|
|||||||
const s = String(name ?? '').trim()
|
const s = String(name ?? '').trim()
|
||||||
if (!s) return ''
|
if (!s) return ''
|
||||||
if (s.length <= max) return s
|
if (s.length <= max) return s
|
||||||
return '…' + s.slice(-(max - 1))
|
return '…' + s.slice(-(max - 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
function regenerateTaskId(file: string) {
|
function regenerateTaskId(file: string) {
|
||||||
@ -390,7 +390,7 @@ function assetsTaskText(file?: string, label?: string, phase?: string) {
|
|||||||
const fn = shortTaskFilename(file, 38)
|
const fn = shortTaskFilename(file, 38)
|
||||||
const phaseText = assetsPhaseLabel(label, phase)
|
const phaseText = assetsPhaseLabel(label, phase)
|
||||||
|
|
||||||
if (fn && phaseText) return `${fn} · ${phaseText}`
|
if (fn && phaseText) return `${fn} · ${phaseText}`
|
||||||
if (fn) return fn
|
if (fn) return fn
|
||||||
return phaseText
|
return phaseText
|
||||||
}
|
}
|
||||||
@ -478,12 +478,12 @@ function mapCheckVideosJobToTask(job: any): TaskItem {
|
|||||||
return {
|
return {
|
||||||
id: checkVideosTaskDomId(String(job?.id || '')),
|
id: checkVideosTaskDomId(String(job?.id || '')),
|
||||||
status,
|
status,
|
||||||
title: 'Downloads prüfen',
|
title: 'Downloads prüfen',
|
||||||
text:
|
text:
|
||||||
status === 'running'
|
status === 'running'
|
||||||
? (String(job?.currentFile || '').trim()
|
? (String(job?.currentFile || '').trim()
|
||||||
? shortTaskFilename(String(job?.currentFile || '').trim(), 52)
|
? shortTaskFilename(String(job?.currentFile || '').trim(), 52)
|
||||||
: 'Prüfe Videos…')
|
: 'Prüfe Videos…')
|
||||||
: String(job?.text || ''),
|
: String(job?.text || ''),
|
||||||
done: Number(job?.done ?? 0),
|
done: Number(job?.done ?? 0),
|
||||||
total: Number(job?.total ?? 0),
|
total: Number(job?.total ?? 0),
|
||||||
@ -524,9 +524,9 @@ function cleanupTaskTitle(rawId: string) {
|
|||||||
const id = String(rawId || '').trim().toLowerCase()
|
const id = String(rawId || '').trim().toLowerCase()
|
||||||
|
|
||||||
if (id.startsWith('cleanup-leftovers-startup')) return 'Startup-Cleanup'
|
if (id.startsWith('cleanup-leftovers-startup')) return 'Startup-Cleanup'
|
||||||
if (id.startsWith('cleanup-leftovers-hourly')) return 'Stündlicher Cleanup'
|
if (id.startsWith('cleanup-leftovers-hourly')) return 'Stündlicher Cleanup'
|
||||||
if (id.startsWith('cleanup-leftovers')) return 'Record-Reste prüfen'
|
if (id.startsWith('cleanup-leftovers')) return 'Record-Reste prüfen'
|
||||||
return 'Aufräumen'
|
return 'Aufräumen'
|
||||||
}
|
}
|
||||||
|
|
||||||
function databaseBackupFilename(contentDisposition: string | null) {
|
function databaseBackupFilename(contentDisposition: string | null) {
|
||||||
@ -550,8 +550,11 @@ function normalizeDatabaseType(_value: unknown): 'postgres' {
|
|||||||
return 'postgres'
|
return 'postgres'
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePostgresMode(value: unknown): 'url' | 'local' {
|
function normalizePostgresMode(value: unknown, databaseUrl?: unknown): 'url' | 'local' {
|
||||||
return String(value ?? '').trim().toLowerCase() === 'local' ? 'local' : 'url'
|
const mode = String(value ?? '').trim().toLowerCase()
|
||||||
|
if (mode === 'url') return 'url'
|
||||||
|
if (mode === 'local') return 'local'
|
||||||
|
return String(databaseUrl ?? '').trim() ? 'url' : 'local'
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePostgresDataDir(value: unknown) {
|
function normalizePostgresDataDir(value: unknown) {
|
||||||
@ -569,7 +572,7 @@ function normalizePostgresName(value: unknown, fallback: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function postgresSourceKey(v: any) {
|
function postgresSourceKey(v: any) {
|
||||||
const mode = normalizePostgresMode(v?.postgresMode)
|
const mode = normalizePostgresMode(v?.postgresMode, v?.databaseUrl)
|
||||||
if (mode === 'local') {
|
if (mode === 'local') {
|
||||||
return [
|
return [
|
||||||
mode,
|
mode,
|
||||||
@ -693,7 +696,7 @@ function RatingThresholdStars({
|
|||||||
role="radio"
|
role="radio"
|
||||||
aria-checked={checked}
|
aria-checked={checked}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
title={`Bis einschließlich ${stars} Stern${stars === 1 ? '' : 'e'} löschen`}
|
title={`Bis einschließlich ${stars} Stern${stars === 1 ? '' : 'e'} löschen`}
|
||||||
onClick={() => onChange(stars)}
|
onClick={() => onChange(stars)}
|
||||||
className={[
|
className={[
|
||||||
'rounded-md p-0.5 transition',
|
'rounded-md p-0.5 transition',
|
||||||
@ -710,7 +713,7 @@ function RatingThresholdStars({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
|
<div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
|
||||||
Löscht bis einschließlich {selected} Stern{selected === 1 ? '' : 'e'}.
|
Löscht bis einschließlich {selected} Stern{selected === 1 ? '' : 'e'}.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -733,7 +736,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
const [appLogOpen, setAppLogOpen] = useState(false)
|
const [appLogOpen, setAppLogOpen] = useState(false)
|
||||||
const [dbModalOpen, setDbModalOpen] = useState(false)
|
const [dbModalOpen, setDbModalOpen] = useState(false)
|
||||||
const [loadedDatabaseType, setLoadedDatabaseType] = useState<'postgres'>('postgres')
|
const [loadedDatabaseType, setLoadedDatabaseType] = useState<'postgres'>('postgres')
|
||||||
const [loadedPostgresMode, setLoadedPostgresMode] = useState<'url' | 'local'>('url')
|
const [loadedPostgresMode, setLoadedPostgresMode] = useState<'url' | 'local'>('local')
|
||||||
const [loadedPostgresDataDir, setLoadedPostgresDataDir] = useState('postgres-data')
|
const [loadedPostgresDataDir, setLoadedPostgresDataDir] = useState('postgres-data')
|
||||||
const [loadedPostgresPort, setLoadedPostgresPort] = useState(55432)
|
const [loadedPostgresPort, setLoadedPostgresPort] = useState(55432)
|
||||||
const [loadedPostgresDatabase, setLoadedPostgresDatabase] = useState('nsfwapp')
|
const [loadedPostgresDatabase, setLoadedPostgresDatabase] = useState('nsfwapp')
|
||||||
@ -748,7 +751,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const saveSucceeded = saveSuccessUntilMs > now
|
const saveSucceeded = saveSuccessUntilMs > now
|
||||||
const saveFailed = Boolean(err) && !saving && !saveSucceeded
|
const saveFailed = Boolean(err) && !saving && !saveSucceeded
|
||||||
const currentPostgresMode = normalizePostgresMode((value as any).postgresMode)
|
const currentPostgresMode = normalizePostgresMode((value as any).postgresMode, (value as any).databaseUrl)
|
||||||
const currentDatabaseUrl = String((value as any).databaseUrl ?? '').trim()
|
const currentDatabaseUrl = String((value as any).databaseUrl ?? '').trim()
|
||||||
const loadedPostgresSourceKey = postgresSourceKey({
|
const loadedPostgresSourceKey = postgresSourceKey({
|
||||||
databaseType: loadedDatabaseType,
|
databaseType: loadedDatabaseType,
|
||||||
@ -809,9 +812,9 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
const saveButton = (() => {
|
const saveButton = (() => {
|
||||||
if (saveUiState === 'saving') {
|
if (saveUiState === 'saving') {
|
||||||
return {
|
return {
|
||||||
text: 'Speichern…',
|
text: 'Speichern…',
|
||||||
color: 'blue' as const,
|
color: 'blue' as const,
|
||||||
icon: null, // Spinner kommt über isLoading
|
icon: null, // Spinner kommt über isLoading
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -910,7 +913,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
.then((data: RecorderSettings) => {
|
.then((data: RecorderSettings) => {
|
||||||
if (!alive) return
|
if (!alive) return
|
||||||
const databaseType = normalizeDatabaseType((data as any).databaseType)
|
const databaseType = normalizeDatabaseType((data as any).databaseType)
|
||||||
const postgresMode = normalizePostgresMode((data as any).postgresMode)
|
const postgresMode = normalizePostgresMode((data as any).postgresMode, (data as any).databaseUrl)
|
||||||
const postgresDataDir = normalizePostgresDataDir((data as any).postgresDataDir)
|
const postgresDataDir = normalizePostgresDataDir((data as any).postgresDataDir)
|
||||||
const postgresPort = normalizePostgresPort((data as any).postgresPort)
|
const postgresPort = normalizePostgresPort((data as any).postgresPort)
|
||||||
const postgresDatabase = normalizePostgresName((data as any).postgresDatabase, 'nsfwapp')
|
const postgresDatabase = normalizePostgresName((data as any).postgresDatabase, 'nsfwapp')
|
||||||
@ -929,7 +932,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
ffmpegPath: String(data.ffmpegPath ?? DEFAULTS.ffmpegPath ?? ''),
|
ffmpegPath: String(data.ffmpegPath ?? DEFAULTS.ffmpegPath ?? ''),
|
||||||
|
|
||||||
|
|
||||||
// ✅ falls backend die Felder noch nicht hat -> defaults nutzen
|
// ✅ falls backend die Felder noch nicht hat -> defaults nutzen
|
||||||
autoAddToDownloadList: data.autoAddToDownloadList ?? DEFAULTS.autoAddToDownloadList,
|
autoAddToDownloadList: data.autoAddToDownloadList ?? DEFAULTS.autoAddToDownloadList,
|
||||||
autoStartAddedDownloads:
|
autoStartAddedDownloads:
|
||||||
(data as any).autoStartAddedDownloads ?? DEFAULTS.autoStartAddedDownloads,
|
(data as any).autoStartAddedDownloads ?? DEFAULTS.autoStartAddedDownloads,
|
||||||
@ -1088,7 +1091,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
const text =
|
const text =
|
||||||
status === 'running' || status === 'queued'
|
status === 'running' || status === 'queued'
|
||||||
? phaseLabel
|
? phaseLabel
|
||||||
? `${shortTaskFilename(file, 38)} · ${phaseLabel}`
|
? `${shortTaskFilename(file, 38)} · ${phaseLabel}`
|
||||||
: shortTaskFilename(file, 52)
|
: shortTaskFilename(file, 52)
|
||||||
: shortTaskFilename(file, 52)
|
: shortTaskFilename(file, 52)
|
||||||
|
|
||||||
@ -1389,7 +1392,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
|
|
||||||
const ok = window.confirm(
|
const ok = window.confirm(
|
||||||
`Datenbank-Backup "${file.name}" wiederherstellen?\n\n` +
|
`Datenbank-Backup "${file.name}" wiederherstellen?\n\n` +
|
||||||
'Das überschreibt die aktuelle Datenbank. Dieser Schritt kann nicht automatisch rueckgaengig gemacht werden.'
|
'Das überschreibt die aktuelle Datenbank. Dieser Schritt kann nicht automatisch rückgängig gemacht werden.'
|
||||||
)
|
)
|
||||||
if (!ok) return
|
if (!ok) return
|
||||||
|
|
||||||
@ -1463,7 +1466,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
: `Datenbank "${databaseUrl}"`
|
: `Datenbank "${databaseUrl}"`
|
||||||
const ok = window.confirm(
|
const ok = window.confirm(
|
||||||
`Aktive PostgreSQL-Datenbank nach ${targetLabel} migrieren?\n\n` +
|
`Aktive PostgreSQL-Datenbank nach ${targetLabel} migrieren?\n\n` +
|
||||||
'Die Ziel-Datenbank wird mit den aktuellen Model-Daten ueberschrieben. Danach nutzt die App das Ziel.'
|
'Die Ziel-Datenbank wird mit den aktuellen Model-Daten überschrieben. Danach nutzt die App das Ziel.'
|
||||||
)
|
)
|
||||||
if (!ok) return
|
if (!ok) return
|
||||||
|
|
||||||
@ -1533,7 +1536,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
const doneDir = value.doneDir.trim()
|
const doneDir = value.doneDir.trim()
|
||||||
const ffmpegPath = (value.ffmpegPath ?? '').trim()
|
const ffmpegPath = (value.ffmpegPath ?? '').trim()
|
||||||
const databaseType = normalizeDatabaseType((value as any).databaseType)
|
const databaseType = normalizeDatabaseType((value as any).databaseType)
|
||||||
const postgresMode = normalizePostgresMode((value as any).postgresMode)
|
const postgresMode = normalizePostgresMode((value as any).postgresMode, (value as any).databaseUrl)
|
||||||
const postgresDataDir = normalizePostgresDataDir((value as any).postgresDataDir)
|
const postgresDataDir = normalizePostgresDataDir((value as any).postgresDataDir)
|
||||||
const postgresPort = normalizePostgresPort((value as any).postgresPort)
|
const postgresPort = normalizePostgresPort((value as any).postgresPort)
|
||||||
const postgresDatabase = normalizePostgresName((value as any).postgresDatabase, 'nsfwapp')
|
const postgresDatabase = normalizePostgresName((value as any).postgresDatabase, 'nsfwapp')
|
||||||
@ -1554,7 +1557,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Switch-Logik: Autostart nur sinnvoll, wenn Auto-Add aktiv ist
|
// ✅ Switch-Logik: Autostart nur sinnvoll, wenn Auto-Add aktiv ist
|
||||||
const autoAddToDownloadList = !!value.autoAddToDownloadList
|
const autoAddToDownloadList = !!value.autoAddToDownloadList
|
||||||
const autoStartAddedDownloads = !!value.autoStartAddedDownloads
|
const autoStartAddedDownloads = !!value.autoStartAddedDownloads
|
||||||
const enableConcurrentDownloadsLimit = !!(value as any).enableConcurrentDownloadsLimit
|
const enableConcurrentDownloadsLimit = !!(value as any).enableConcurrentDownloadsLimit
|
||||||
@ -1646,7 +1649,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
const t = await res.text().catch(() => '')
|
const t = await res.text().catch(() => '')
|
||||||
throw new Error(t || `HTTP ${res.status}`)
|
throw new Error(t || `HTTP ${res.status}`)
|
||||||
}
|
}
|
||||||
// Button-Label: "Gespeichert" für 2.5s
|
// Button-Label: "Gespeichert" für 2.5s
|
||||||
const until = Date.now() + 2500
|
const until = Date.now() + 2500
|
||||||
setSaveSuccessUntilMs(until)
|
setSaveSuccessUntilMs(until)
|
||||||
|
|
||||||
@ -1736,24 +1739,24 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
if ((!mb || mb <= 0) && !deleteLowRated) {
|
if ((!mb || mb <= 0) && !deleteLowRated) {
|
||||||
setErr('Mindestgröße ist 0 und Rating-Löschen ist deaktiviert – es würde nichts gelöscht.')
|
setErr('Mindestgröße ist 0 und Rating-Löschen ist deaktiviert – es würde nichts gelöscht.')
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const ratingLine = deleteLowRated
|
const ratingLine = deleteLowRated
|
||||||
? `• Rating ≤ ${maxStars} Stern${maxStars === 1 ? '' : 'e'}\n`
|
? `• Rating ≤ ${maxStars} Stern${maxStars === 1 ? '' : 'e'}\n`
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
const sizeLine = mb > 0
|
const sizeLine = mb > 0
|
||||||
? `• Dateien < ${mb} MB in "${doneDir}"\n`
|
? `• Dateien < ${mb} MB in "${doneDir}"\n`
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
const ok = window.confirm(
|
const ok = window.confirm(
|
||||||
`Aufräumen:\n` +
|
`Aufräumen:\n` +
|
||||||
sizeLine +
|
sizeLine +
|
||||||
ratingLine +
|
ratingLine +
|
||||||
`• Entfernt defekte Video-Dateien\n` +
|
`• Entfernt defekte Video-Dateien\n` +
|
||||||
`• Löscht Record-Reste und verwaiste Assets\n\n` +
|
`• Löscht Record-Reste und verwaiste Assets\n\n` +
|
||||||
`Fortfahren?`
|
`Fortfahren?`
|
||||||
)
|
)
|
||||||
if (!ok) return null
|
if (!ok) return null
|
||||||
@ -1883,7 +1886,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newPassword !== newPasswordRepeat) {
|
if (newPassword !== newPasswordRepeat) {
|
||||||
setErr('Die neuen Passwörter stimmen nicht überein.')
|
setErr('Die neuen Passwörter stimmen nicht überein.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1908,7 +1911,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
setCurrentPassword('')
|
setCurrentPassword('')
|
||||||
setNewPassword('')
|
setNewPassword('')
|
||||||
setNewPasswordRepeat('')
|
setNewPasswordRepeat('')
|
||||||
setMsg('Passwort wurde geändert.')
|
setMsg('Passwort wurde geändert.')
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setErr(e?.message ?? String(e))
|
setErr(e?.message ?? String(e))
|
||||||
} finally {
|
} finally {
|
||||||
@ -1942,7 +1945,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const ok = window.confirm('2FA wirklich deaktivieren? Dein Login ist danach weniger geschützt.')
|
const ok = window.confirm('2FA wirklich deaktivieren? Dein Login ist danach weniger geschützt.')
|
||||||
if (!ok) return
|
if (!ok) return
|
||||||
|
|
||||||
setSecurityBusy(true)
|
setSecurityBusy(true)
|
||||||
@ -1978,7 +1981,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
function passkeyErrorMessage(e: any) {
|
function passkeyErrorMessage(e: any) {
|
||||||
const raw = String(e?.message ?? e ?? '').trim()
|
const raw = String(e?.message ?? e ?? '').trim()
|
||||||
|
|
||||||
if (!raw) return 'Passkey konnte nicht hinzugefügt werden.'
|
if (!raw) return 'Passkey konnte nicht hinzugefügt werden.'
|
||||||
|
|
||||||
if (raw.includes('passkey registration verify failed')) {
|
if (raw.includes('passkey registration verify failed')) {
|
||||||
return (
|
return (
|
||||||
@ -2021,14 +2024,14 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
setMsg(null)
|
setMsg(null)
|
||||||
setPasskeyFeedback({
|
setPasskeyFeedback({
|
||||||
kind: 'loading',
|
kind: 'loading',
|
||||||
text: 'Starte Passkey-Einrichtung…',
|
text: 'Starte Passkey-Einrichtung…',
|
||||||
})
|
})
|
||||||
setSecurityBusy(true)
|
setSecurityBusy(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setPasskeyFeedback({
|
setPasskeyFeedback({
|
||||||
kind: 'loading',
|
kind: 'loading',
|
||||||
text: 'Hole WebAuthn-Challenge vom Backend…',
|
text: 'Hole WebAuthn-Challenge vom Backend…',
|
||||||
})
|
})
|
||||||
|
|
||||||
const optionsRes = await fetch('/api/auth/passkey/register/options', {
|
const optionsRes = await fetch('/api/auth/passkey/register/options', {
|
||||||
@ -2045,7 +2048,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
|
|
||||||
setPasskeyFeedback({
|
setPasskeyFeedback({
|
||||||
kind: 'loading',
|
kind: 'loading',
|
||||||
text: 'Browser-Passkey-Dialog geöffnet. Bitte Face ID, Fingerabdruck, Windows Hello oder Sicherheitsschlüssel bestätigen.',
|
text: 'Browser-Passkey-Dialog geöffnet. Bitte Face ID, Fingerabdruck, Windows Hello oder Sicherheitsschlüssel bestätigen.',
|
||||||
})
|
})
|
||||||
|
|
||||||
const credential = await startRegistration({
|
const credential = await startRegistration({
|
||||||
@ -2054,7 +2057,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
|
|
||||||
setPasskeyFeedback({
|
setPasskeyFeedback({
|
||||||
kind: 'loading',
|
kind: 'loading',
|
||||||
text: 'Passkey wurde vom Browser erstellt. Verifiziere beim Backend…',
|
text: 'Passkey wurde vom Browser erstellt. Verifiziere beim Backend…',
|
||||||
})
|
})
|
||||||
|
|
||||||
const verifyRes = await fetch('/api/auth/passkey/register/verify', {
|
const verifyRes = await fetch('/api/auth/passkey/register/verify', {
|
||||||
@ -2069,10 +2072,10 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
throw new Error(text || `HTTP ${verifyRes.status}`)
|
throw new Error(text || `HTTP ${verifyRes.status}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
setMsg('Passkey wurde hinzugefügt.')
|
setMsg('Passkey wurde hinzugefügt.')
|
||||||
setPasskeyFeedback({
|
setPasskeyFeedback({
|
||||||
kind: 'success',
|
kind: 'success',
|
||||||
text: 'Passkey wurde erfolgreich hinzugefügt.',
|
text: 'Passkey wurde erfolgreich hinzugefügt.',
|
||||||
})
|
})
|
||||||
await loadAuthStatus()
|
await loadAuthStatus()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@ -2094,14 +2097,14 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
|
|
||||||
const credentialId = String(passkey?.id ?? '').trim()
|
const credentialId = String(passkey?.id ?? '').trim()
|
||||||
if (!credentialId) {
|
if (!credentialId) {
|
||||||
setErr('Passkey kann nicht gelöscht werden: Credential-ID fehlt.')
|
setErr('Passkey kann nicht gelöscht werden: Credential-ID fehlt.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const label = String(passkey?.label || 'diesen Passkey').trim()
|
const label = String(passkey?.label || 'diesen Passkey').trim()
|
||||||
|
|
||||||
const ok = window.confirm(
|
const ok = window.confirm(
|
||||||
`"${label}" wirklich löschen? Anmeldung mit diesem Passkey ist danach nicht mehr möglich.`
|
`"${label}" wirklich löschen? Anmeldung mit diesem Passkey ist danach nicht mehr möglich.`
|
||||||
)
|
)
|
||||||
if (!ok) return
|
if (!ok) return
|
||||||
|
|
||||||
@ -2122,7 +2125,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
throw new Error(text || `HTTP ${res.status}`)
|
throw new Error(text || `HTTP ${res.status}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
setMsg('Passkey wurde gelöscht.')
|
setMsg('Passkey wurde gelöscht.')
|
||||||
await loadAuthStatus()
|
await loadAuthStatus()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setErr(e?.message ?? String(e))
|
setErr(e?.message ?? String(e))
|
||||||
@ -2152,9 +2155,9 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
].join(' ')}
|
].join(' ')}
|
||||||
title={
|
title={
|
||||||
aiServerStatus === null
|
aiServerStatus === null
|
||||||
? 'AI Server Status wird geladen…'
|
? 'AI Server Status wird geladen…'
|
||||||
: aiServerStatus.running
|
: aiServerStatus.running
|
||||||
? `AI Server läuft${aiServerStatus.url ? `: ${aiServerStatus.url}` : ''}`
|
? `AI Server läuft${aiServerStatus.url ? `: ${aiServerStatus.url}` : ''}`
|
||||||
: `AI Server offline${aiServerStatus.error ? `: ${aiServerStatus.error}` : ''}`
|
: `AI Server offline${aiServerStatus.error ? `: ${aiServerStatus.error}` : ''}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -2171,7 +2174,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{aiServerStatus === null
|
{aiServerStatus === null
|
||||||
? 'AI prüft…'
|
? 'AI prüft…'
|
||||||
: aiServerStatus.running
|
: aiServerStatus.running
|
||||||
? 'AI online'
|
? 'AI online'
|
||||||
: 'AI offline'}
|
: 'AI offline'}
|
||||||
@ -2252,15 +2255,15 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
id="tasks"
|
id="tasks"
|
||||||
title="Aufgaben"
|
title="Aufgaben"
|
||||||
description="Hintergrundaufgaben wie z.B. Asset/Preview-Generierung."
|
description="Hintergrundaufgaben wie z.B. Asset/Preview-Generierung."
|
||||||
icon="âš™ï¸"
|
icon="⚙️"
|
||||||
defaultOpen={true}
|
defaultOpen={true}
|
||||||
>
|
>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Task
|
<Task
|
||||||
title="Assets-Generator"
|
title="Assets-Generator"
|
||||||
description="Erzeugt nur fehlende oder unvollständige Assets der aktivierten Phasen. Nicht ausgewählte Phasen werden komplett übersprungen."
|
description="Erzeugt nur fehlende oder unvollständige Assets der aktivierten Phasen. Nicht ausgewählte Phasen werden komplett übersprungen."
|
||||||
startLabel="Start"
|
startLabel="Start"
|
||||||
startingLabel="Starte…"
|
startingLabel="Starte…"
|
||||||
onTrigger={startAssetsTaskFromCurrentSwitches}
|
onTrigger={startAssetsTaskFromCurrentSwitches}
|
||||||
onCreated={(job) => {
|
onCreated={(job) => {
|
||||||
if (!job?.id) return
|
if (!job?.id) return
|
||||||
@ -2277,44 +2280,44 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
checked={!!value.generateAssetsMeta}
|
checked={!!value.generateAssetsMeta}
|
||||||
onChange={(checked) => setValue((v) => ({ ...v, generateAssetsMeta: checked }))}
|
onChange={(checked) => setValue((v) => ({ ...v, generateAssetsMeta: checked }))}
|
||||||
label="Meta"
|
label="Meta"
|
||||||
description="Erzeugt oder ergänzt die meta.json mit Dauer, Auflösung, FPS und weiteren Videoinformationen."
|
description="Erzeugt oder ergänzt die meta.json mit Dauer, Auflösung, FPS und weiteren Videoinformationen."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledSwitch
|
<LabeledSwitch
|
||||||
checked={!!value.generateAssetsThumb}
|
checked={!!value.generateAssetsThumb}
|
||||||
onChange={(checked) => setValue((v) => ({ ...v, generateAssetsThumb: checked }))}
|
onChange={(checked) => setValue((v) => ({ ...v, generateAssetsThumb: checked }))}
|
||||||
label="Vorschaubild"
|
label="Vorschaubild"
|
||||||
description="Erzeugt das statische Vorschaubild preview.jpg für Listen und Kartenansicht."
|
description="Erzeugt das statische Vorschaubild preview.jpg für Listen und Kartenansicht."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledSwitch
|
<LabeledSwitch
|
||||||
checked={!!value.generateAssetsTeaser}
|
checked={!!value.generateAssetsTeaser}
|
||||||
onChange={(checked) => setValue((v) => ({ ...v, generateAssetsTeaser: checked }))}
|
onChange={(checked) => setValue((v) => ({ ...v, generateAssetsTeaser: checked }))}
|
||||||
label="Teaser"
|
label="Teaser"
|
||||||
description="Erzeugt die kurze Video-Vorschau preview.mp4 für Hover- und Preview-Wiedergabe."
|
description="Erzeugt die kurze Video-Vorschau preview.mp4 für Hover- und Preview-Wiedergabe."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledSwitch
|
<LabeledSwitch
|
||||||
checked={!!value.generateAssetsSprites}
|
checked={!!value.generateAssetsSprites}
|
||||||
onChange={(checked) => setValue((v) => ({ ...v, generateAssetsSprites: checked }))}
|
onChange={(checked) => setValue((v) => ({ ...v, generateAssetsSprites: checked }))}
|
||||||
label="Sprites"
|
label="Sprites"
|
||||||
description="Erzeugt das Sprite-Bild preview-sprite.jpg für die Scrubber- und Timeline-Vorschau."
|
description="Erzeugt das Sprite-Bild preview-sprite.jpg für die Scrubber- und Timeline-Vorschau."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledSwitch
|
<LabeledSwitch
|
||||||
checked={!!value.generateAssetsAnalyze}
|
checked={!!value.generateAssetsAnalyze}
|
||||||
onChange={(checked) => setValue((v) => ({ ...v, generateAssetsAnalyze: checked }))}
|
onChange={(checked) => setValue((v) => ({ ...v, generateAssetsAnalyze: checked }))}
|
||||||
label="Analyse"
|
label="Analyse"
|
||||||
description="Führt die Analyse aus und ergänzt Treffer, Segmente und weitere Auswertungsdaten."
|
description="Führt die Analyse aus und ergänzt Treffer, Segmente und weitere Auswertungsdaten."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Task
|
<Task
|
||||||
title="Downloads prüfen"
|
title="Downloads prüfen"
|
||||||
description="Prüft fertige Videos anhand der preview-sprite.jpg auf schwarze Endzellen und schreibt das Ergebnis in die meta.json."
|
description="Prüft fertige Videos anhand der preview-sprite.jpg auf schwarze Endzellen und schreibt das Ergebnis in die meta.json."
|
||||||
startLabel="Prüfen"
|
startLabel="Prüfen"
|
||||||
startingLabel="Starte…"
|
startingLabel="Starte…"
|
||||||
startUrl="/api/tasks/check-videos"
|
startUrl="/api/tasks/check-videos"
|
||||||
onCreated={(job) => {
|
onCreated={(job) => {
|
||||||
if (!job?.id) return
|
if (!job?.id) return
|
||||||
@ -2326,10 +2329,10 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Task
|
<Task
|
||||||
title="Aufräumen"
|
title="Aufräumen"
|
||||||
description='Löscht Dateien im doneDir kleiner als die Mindestgröße, optional Downloads bis zur gewählten Sterne-Bewertung, und entfernt verwaiste Assets.'
|
description='Löscht Dateien im doneDir kleiner als die Mindestgröße, optional Downloads bis zur gewählten Sterne-Bewertung, und entfernt verwaiste Assets.'
|
||||||
startLabel="Aufräumen"
|
startLabel="Aufräumen"
|
||||||
startingLabel="Läuft…"
|
startingLabel="Läuft…"
|
||||||
onTrigger={cleanupSmallDone}
|
onTrigger={cleanupSmallDone}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
onCreated={(job) => {
|
onCreated={(job) => {
|
||||||
@ -2348,7 +2351,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
id="paths"
|
id="paths"
|
||||||
title="Pfad-Einstellungen"
|
title="Pfad-Einstellungen"
|
||||||
description="Aufnahme- und Zielverzeichnisse sowie optionaler ffmpeg-Pfad."
|
description="Aufnahme- und Zielverzeichnisse sowie optionaler ffmpeg-Pfad."
|
||||||
icon="ðŸ“"
|
icon="📁"
|
||||||
defaultOpen={false}
|
defaultOpen={false}
|
||||||
>
|
>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
@ -2492,7 +2495,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
{databaseSettingsDirty ? (
|
{databaseSettingsDirty ? (
|
||||||
<div className="mt-2 text-xs text-amber-700 dark:text-amber-200">
|
<div className="mt-2 text-xs text-amber-700 dark:text-amber-200">
|
||||||
Die Datenbank-Einstellungen wurden geaendert. Bitte erst speichern.
|
Die Datenbank-Einstellungen wurden geändert. Bitte erst speichern.
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@ -2632,14 +2635,14 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
<SettingsSection
|
<SettingsSection
|
||||||
id="automation"
|
id="automation"
|
||||||
title="Automatisierung & Anzeige"
|
title="Automatisierung & Anzeige"
|
||||||
description="Verhalten beim Hinzufügen/Starten sowie Anzeigeoptionen."
|
description="Verhalten beim Hinzufügen/Starten sowie Anzeigeoptionen."
|
||||||
icon="🤖"
|
icon="🤖"
|
||||||
defaultOpen={false}
|
defaultOpen={false}
|
||||||
>
|
>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div className="text-sm font-semibold text-gray-900 dark:text-white">Automatisierung & Anzeige</div>
|
<div className="text-sm font-semibold text-gray-900 dark:text-white">Automatisierung & Anzeige</div>
|
||||||
<div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
|
<div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
|
||||||
Verhalten beim Hinzufügen/Starten sowie Anzeigeoptionen.
|
Verhalten beim Hinzufügen/Starten sowie Anzeigeoptionen.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -2701,7 +2704,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
label="EnrichQ-Postwork im Hintergrund"
|
label="EnrichQ-Postwork im Hintergrund"
|
||||||
description="Wenn aktiv, werden fehlende Analyse-/Enrich-Assets automatisch im Hintergrund nachgeholt. Wenn deaktiviert, werden keine neuen EnrichQ-Hintergrundjobs gestartet; manuelle Asset-Generierung bleibt möglich."
|
description="Wenn aktiv, werden fehlende Analyse-/Enrich-Assets automatisch im Hintergrund nachgeholt. Wenn deaktiviert, werden keine neuen EnrichQ-Hintergrundjobs gestartet; manuelle Asset-Generierung bleibt möglich."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledSwitch
|
<LabeledSwitch
|
||||||
@ -2713,8 +2716,8 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
autoStartAddedDownloads: checked ? v.autoStartAddedDownloads : false,
|
autoStartAddedDownloads: checked ? v.autoStartAddedDownloads : false,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
label="Clipboard-Links in Start-URL übernehmen"
|
label="Clipboard-Links in Start-URL übernehmen"
|
||||||
description="Übernimmt erkannte Chaturbate- und MyFreeCams-Links aus der Zwischenablage automatisch in das Start-URL-Feld. Wenn deaktiviert, werden Clipboard-Links ignoriert."
|
description="Übernimmt erkannte Chaturbate- und MyFreeCams-Links aus der Zwischenablage automatisch in das Start-URL-Feld. Wenn deaktiviert, werden Clipboard-Links ignoriert."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledSwitch
|
<LabeledSwitch
|
||||||
@ -2726,8 +2729,8 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
autoAddToDownloadList: checked ? true : v.autoAddToDownloadList,
|
autoAddToDownloadList: checked ? true : v.autoAddToDownloadList,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
label="Übernommene Links automatisch starten"
|
label="Übernommene Links automatisch starten"
|
||||||
description="Startet automatisch, sobald ein erkannter Clipboard-Link übernommen wurde oder ein aktivierter Provider ein watched Model als öffentlich online erkennt. Aktiviert automatisch auch die Clipboard-Übernahme."
|
description="Startet automatisch, sobald ein erkannter Clipboard-Link übernommen wurde oder ein aktivierter Provider ein watched Model als öffentlich online erkennt. Aktiviert automatisch auch die Clipboard-Übernahme."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-white/10 dark:bg-white/5">
|
<div className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-white/10 dark:bg-white/5">
|
||||||
@ -2742,7 +2745,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
label="Provider-APIs verwenden"
|
label="Provider-APIs verwenden"
|
||||||
description="Master-Schalter für externe Provider-APIs. Wenn deaktiviert, werden keine automatischen API-Abfragen für Online-Status, Profilbilder oder Model-Daten ausgeführt."
|
description="Master-Schalter für externe Provider-APIs. Wenn deaktiviert, werden keine automatischen API-Abfragen für Online-Status, Profilbilder oder Model-Daten ausgeführt."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -2761,7 +2764,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
label="Chaturbate API"
|
label="Chaturbate API"
|
||||||
description="Aktualisiert Online-Status, Vorschaubilder und Model-Daten über die Chaturbate-API. Wenn Automatisch starten aktiv ist, werden öffentliche watched Models gestartet."
|
description="Aktualisiert Online-Status, Vorschaubilder und Model-Daten über die Chaturbate-API. Wenn Automatisch starten aktiv ist, werden öffentliche watched Models gestartet."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledSwitch
|
<LabeledSwitch
|
||||||
@ -2774,12 +2777,12 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
label="MyFreeCams API"
|
label="MyFreeCams API"
|
||||||
description="Aktualisiert Online-Status, Vorschaubilder und Model-Daten über usernameLookup. Wenn Automatisch starten aktiv ist, werden öffentliche watched Models gestartet. Bei 'servers busy' pausiert die App MyFreeCams-Anfragen automatisch."
|
description="Aktualisiert Online-Status, Vorschaubilder und Model-Daten über usernameLookup. Wenn Automatisch starten aktiv ist, werden öffentliche watched Models gestartet. Bei 'servers busy' pausiert die App MyFreeCams-Anfragen automatisch."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ✅ NEU: Auto-Delete kleine Downloads */}
|
{/* ✅ NEU: Auto-Delete kleine Downloads */}
|
||||||
<div className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-white/10 dark:bg-white/5">
|
<div className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-white/10 dark:bg-white/5">
|
||||||
<LabeledSwitch
|
<LabeledSwitch
|
||||||
checked={!!value.autoDeleteSmallDownloads}
|
checked={!!value.autoDeleteSmallDownloads}
|
||||||
@ -2791,8 +2794,8 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
v.autoDeleteSmallDownloadsBelowMB ?? 50,
|
v.autoDeleteSmallDownloadsBelowMB ?? 50,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
label="Kleine Downloads automatisch löschen"
|
label="Kleine Downloads automatisch löschen"
|
||||||
description="Löscht fertige Downloads automatisch, wenn die Datei kleiner als die eingestellte Mindestgröße ist."
|
description="Löscht fertige Downloads automatisch, wenn die Datei kleiner als die eingestellte Mindestgröße ist."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -2802,8 +2805,8 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="sm:col-span-4">
|
<div className="sm:col-span-4">
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-200">Mindestgröße</div>
|
<div className="text-sm font-medium text-gray-900 dark:text-gray-200">Mindestgröße</div>
|
||||||
<div className="text-xs text-gray-600 dark:text-gray-300">Alles darunter wird gelöscht.</div>
|
<div className="text-xs text-gray-600 dark:text-gray-300">Alles darunter wird gelöscht.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sm:col-span-8">
|
<div className="sm:col-span-8">
|
||||||
@ -2842,8 +2845,8 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
autoDeleteSmallDownloadsKeepFavorites: checked,
|
autoDeleteSmallDownloadsKeepFavorites: checked,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
label="Favoriten nicht automatisch löschen"
|
label="Favoriten nicht automatisch löschen"
|
||||||
description="Schützt Downloads von favorisierten Models vor dem automatischen Löschen, auch wenn die Datei kleiner als die Mindestgröße ist."
|
description="Schützt Downloads von favorisierten Models vor dem automatischen Löschen, auch wenn die Datei kleiner als die Mindestgröße ist."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -2859,8 +2862,8 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
v.autoDeleteLowRatedDownloadsMaxStars ?? 3,
|
v.autoDeleteLowRatedDownloadsMaxStars ?? 3,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
label="Downloads nach Analyse nach Rating löschen"
|
label="Downloads nach Analyse nach Rating löschen"
|
||||||
description="Löscht fertige Downloads automatisch nach der Analyse, wenn das Rating höchstens der gewählten Sternezahl entspricht."
|
description="Löscht fertige Downloads automatisch nach der Analyse, wenn das Rating höchstens der gewählten Sternezahl entspricht."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -2874,7 +2877,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
Sterne-Grenze
|
Sterne-Grenze
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-600 dark:text-gray-300">
|
<div className="text-xs text-gray-600 dark:text-gray-300">
|
||||||
Bis einschließlich dieser Bewertung wird gelöscht.
|
Bis einschließlich dieser Bewertung wird gelöscht.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -2912,7 +2915,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
Object Detection Epochs
|
Object Detection Epochs
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-4xl text-xs text-gray-600 dark:text-gray-300">
|
<div className="max-w-4xl text-xs text-gray-600 dark:text-gray-300">
|
||||||
Anzahl der Trainingsdurchläufe für die Box-Erkennung. Mehr Epochs können die Erkennung verbessern, dauern aber länger. 60 ist ein guter Standard.
|
Anzahl der Trainingsdurchläufe für die Box-Erkennung. Mehr Epochs können die Erkennung verbessern, dauern aber länger. 60 ist ein guter Standard.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -2946,7 +2949,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
checked={!!value.blurPreviews}
|
checked={!!value.blurPreviews}
|
||||||
onChange={(checked) => setValue((v) => ({ ...v, blurPreviews: checked }))}
|
onChange={(checked) => setValue((v) => ({ ...v, blurPreviews: checked }))}
|
||||||
label="Vorschaubilder blurren"
|
label="Vorschaubilder blurren"
|
||||||
description="Weichzeichnet Vorschaubilder/Teaser (praktisch auf mobilen Geräten oder im öffentlichen Umfeld)."
|
description="Weichzeichnet Vorschaubilder/Teaser (praktisch auf mobilen Geräten oder im öffentlichen Umfeld)."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-12 sm:items-center">
|
<div className="grid grid-cols-1 gap-2 sm:grid-cols-12 sm:items-center">
|
||||||
@ -2955,7 +2958,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
Teaser abspielen
|
Teaser abspielen
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-600 dark:text-gray-300">
|
<div className="text-xs text-gray-600 dark:text-gray-300">
|
||||||
Standbild spart Leistung. „Bei Hover (Standard)“: Desktop spielt bei Hover ab, Mobile im Viewport. „Alle“ kann viel CPU ziehen.
|
Standbild spart Leistung. „Bei Hover (Standard)“: Desktop spielt bei Hover ab, Mobile im Viewport. „Alle“ kann viel CPU ziehen.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -3021,7 +3024,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
<div>
|
<div>
|
||||||
<span className="font-medium">Schwelle:</span>{' '}
|
<span className="font-medium">Schwelle:</span>{' '}
|
||||||
Pause unter <span className="tabular-nums">{uiPauseGB}</span> GB
|
Pause unter <span className="tabular-nums">{uiPauseGB}</span> GB
|
||||||
{' · '}Resume ab{' '}
|
{' · '}Resume ab{' '}
|
||||||
<span className="tabular-nums">
|
<span className="tabular-nums">
|
||||||
{uiResumeGB}
|
{uiResumeGB}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
@ -3031,7 +3034,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
<div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
|
<div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
|
||||||
{diskStatus
|
{diskStatus
|
||||||
? `Frei: ${diskStatus.freeBytesHuman}${diskStatus.recordPath ? ` (Pfad: ${diskStatus.recordPath})` : ''}`
|
? `Frei: ${diskStatus.freeBytesHuman}${diskStatus.recordPath ? ` (Pfad: ${diskStatus.recordPath})` : ''}`
|
||||||
: 'Status wird geladen…'}
|
: 'Status wird geladen…'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{diskStatus?.emergency && (
|
{diskStatus?.emergency && (
|
||||||
@ -3073,7 +3076,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
].join(' ')}
|
].join(' ')}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
â–¾
|
▾
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -3086,7 +3089,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
Logdatei: {appLog.path}
|
Logdatei: {appLog.path}
|
||||||
</span>
|
</span>
|
||||||
) : appLogLoading ? (
|
) : appLogLoading ? (
|
||||||
'Logdatei wird geladen…'
|
'Logdatei wird geladen…'
|
||||||
) : (
|
) : (
|
||||||
'Logdatei noch nicht geladen.'
|
'Logdatei noch nicht geladen.'
|
||||||
)}
|
)}
|
||||||
@ -3098,7 +3101,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
disabled={appLogLoading}
|
disabled={appLogLoading}
|
||||||
onClick={() => void loadAppLog()}
|
onClick={() => void loadAppLog()}
|
||||||
>
|
>
|
||||||
{appLogLoading ? 'Lade…' : 'Aktualisieren'}
|
{appLogLoading ? 'Lade…' : 'Aktualisieren'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -3113,7 +3116,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
className="max-h-96 overflow-auto whitespace-pre-wrap rounded-xl bg-gray-950 p-3 font-mono text-[11px] leading-relaxed text-gray-100 ring-1 ring-black/10 dark:ring-white/10"
|
className="max-h-96 overflow-auto whitespace-pre-wrap rounded-xl bg-gray-950 p-3 font-mono text-[11px] leading-relaxed text-gray-100 ring-1 ring-black/10 dark:ring-white/10"
|
||||||
>
|
>
|
||||||
{appLogLoading
|
{appLogLoading
|
||||||
? 'Log wird geladen…'
|
? 'Log wird geladen…'
|
||||||
: appLog?.log?.trim()
|
: appLog?.log?.trim()
|
||||||
? appLog.log
|
? appLog.log
|
||||||
: 'Noch kein Log vorhanden.'}
|
: 'Noch kein Log vorhanden.'}
|
||||||
@ -3129,7 +3132,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
id="security"
|
id="security"
|
||||||
title="Konto-Sicherheit"
|
title="Konto-Sicherheit"
|
||||||
description="Passwort, Zwei-Faktor-Authentifizierung und Passkeys getrennt von den Recorder-Einstellungen verwalten."
|
description="Passwort, Zwei-Faktor-Authentifizierung und Passkeys getrennt von den Recorder-Einstellungen verwalten."
|
||||||
icon="🛡ï¸"
|
icon="🛡️"
|
||||||
defaultOpen={false}
|
defaultOpen={false}
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
@ -3143,10 +3146,10 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-semibold text-gray-900 dark:text-white">
|
<div className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||||
Passwort ändern
|
Passwort ändern
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
|
<div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
|
||||||
Ändert das Login-Passwort. Andere Sessions werden abgemeldet.
|
Ändert das Login-Passwort. Andere Sessions werden abgemeldet.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -3201,7 +3204,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
Zwei-Faktor-Authentifizierung
|
Zwei-Faktor-Authentifizierung
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
|
<div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
|
||||||
Authenticator-App als zweiter Faktor. Einzelne Geräte können bei TOTP nicht getrennt erkannt werden.
|
Authenticator-App als zweiter Faktor. Einzelne Geräte können bei TOTP nicht getrennt erkannt werden.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -3211,7 +3214,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
{authStatus?.totpDevice ? (
|
{authStatus?.totpDevice ? (
|
||||||
<SecurityDeviceRow
|
<SecurityDeviceRow
|
||||||
icon="ðŸ”"
|
icon="🔐"
|
||||||
title={authStatus.totpDevice.label || 'Authenticator-App'}
|
title={authStatus.totpDevice.label || 'Authenticator-App'}
|
||||||
subtitle={
|
subtitle={
|
||||||
authStatus?.totpEnabled
|
authStatus?.totpEnabled
|
||||||
@ -3223,7 +3226,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200">
|
<div className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200">
|
||||||
2FA ist noch nicht eingerichtet. Die Einrichtung erfolgt aktuell beim nächsten Login-Setup.
|
2FA ist noch nicht eingerichtet. Die Einrichtung erfolgt aktuell beim nächsten Login-Setup.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -3275,7 +3278,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
Zum Deaktivieren brauchst du dein aktuelles Passwort
|
Zum Deaktivieren brauchst du dein aktuelles Passwort
|
||||||
{authStatus?.totpEnabled ? ' und einen gültigen 2FA-Code.' : '.'}
|
{authStatus?.totpEnabled ? ' und einen gültigen 2FA-Code.' : '.'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -3300,7 +3303,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
Passkeys
|
Passkeys
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
|
<div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
|
||||||
Anmeldung ohne Passwort über Face ID, Fingerabdruck, Windows Hello oder Sicherheitsschlüssel.
|
Anmeldung ohne Passwort über Face ID, Fingerabdruck, Windows Hello oder Sicherheitsschlüssel.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -3312,20 +3315,20 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
(authStatus?.passkeys ?? []).map((pk, index) => (
|
(authStatus?.passkeys ?? []).map((pk, index) => (
|
||||||
<SecurityDeviceRow
|
<SecurityDeviceRow
|
||||||
key={pk.id || index}
|
key={pk.id || index}
|
||||||
icon="🔑"
|
icon="🔑"
|
||||||
title={pk.label || `Passkey ${index + 1}`}
|
title={pk.label || `Passkey ${index + 1}`}
|
||||||
subtitle={
|
subtitle={
|
||||||
pk.backupEligible
|
pk.backupEligible
|
||||||
? pk.backupState
|
? pk.backupState
|
||||||
? 'Synchronisierter Passkey'
|
? 'Synchronisierter Passkey'
|
||||||
: 'Passkey kann synchronisiert werden'
|
: 'Passkey kann synchronisiert werden'
|
||||||
: 'Gerätegebundener Passkey oder Sicherheitsschlüssel'
|
: 'Gerätegebundener Passkey oder Sicherheitsschlüssel'
|
||||||
}
|
}
|
||||||
meta={[
|
meta={[
|
||||||
`Erstellt: ${formatSecurityDate(pk.createdAt)}`,
|
`Erstellt: ${formatSecurityDate(pk.createdAt)}`,
|
||||||
`Zuletzt genutzt: ${formatSecurityDate(pk.lastUsedAt)}`,
|
`Zuletzt genutzt: ${formatSecurityDate(pk.lastUsedAt)}`,
|
||||||
typeof pk.signCount === 'number' ? `SignCount: ${pk.signCount}` : '',
|
typeof pk.signCount === 'number' ? `SignCount: ${pk.signCount}` : '',
|
||||||
].filter(Boolean).join(' · ')}
|
].filter(Boolean).join(' · ')}
|
||||||
badge="Passkey"
|
badge="Passkey"
|
||||||
actions={
|
actions={
|
||||||
<Button
|
<Button
|
||||||
@ -3335,7 +3338,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
disabled={securityBusy}
|
disabled={securityBusy}
|
||||||
onClick={() => void deletePasskey(pk)}
|
onClick={() => void deletePasskey(pk)}
|
||||||
>
|
>
|
||||||
Löschen
|
Löschen
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -3349,7 +3352,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
|
|
||||||
{!passkeySupported ? (
|
{!passkeySupported ? (
|
||||||
<div className="mt-3 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200">
|
<div className="mt-3 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200">
|
||||||
Dein Browser oder diese Verbindung unterstützt Passkeys aktuell nicht.
|
Dein Browser oder diese Verbindung unterstützt Passkeys aktuell nicht.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
@ -3364,9 +3367,9 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
: 'border-blue-200 bg-blue-50 text-blue-800 dark:border-blue-500/30 dark:bg-blue-500/10 dark:text-blue-200',
|
: 'border-blue-200 bg-blue-50 text-blue-800 dark:border-blue-500/30 dark:bg-blue-500/10 dark:text-blue-200',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
{passkeyFeedback.kind === 'loading' ? 'â³ ' : null}
|
{passkeyFeedback.kind === 'loading' ? '⏳ ' : null}
|
||||||
{passkeyFeedback.kind === 'success' ? '✅ ' : null}
|
{passkeyFeedback.kind === 'success' ? '✅ ' : null}
|
||||||
{passkeyFeedback.kind === 'error' ? 'âš ï¸ ' : null}
|
{passkeyFeedback.kind === 'error' ? '⚠️ ' : null}
|
||||||
{passkeyFeedback.text}
|
{passkeyFeedback.text}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@ -3377,7 +3380,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
disabled={securityBusy}
|
disabled={securityBusy}
|
||||||
onClick={() => void registerSettingsPasskey()}
|
onClick={() => void registerSettingsPasskey()}
|
||||||
>
|
>
|
||||||
{securityBusy ? 'Passkey wird eingerichtet…' : 'Passkey hinzufügen'}
|
{securityBusy ? 'Passkey wird eingerichtet…' : 'Passkey hinzufügen'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user