bugfixes
This commit is contained in:
parent
3e32f9480a
commit
a12d9dd1a9
@ -1,7 +1,7 @@
|
||||
# Build and Debian package metadata.
|
||||
PACKAGE_NAME="nsfwapp"
|
||||
APP_TITLE="NSFW App"
|
||||
VERSION="1.2.1"
|
||||
VERSION="1.3.0"
|
||||
ARCH="amd64"
|
||||
SECTION="video"
|
||||
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 {
|
||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||
case postgresModeURL:
|
||||
return postgresModeURL
|
||||
case postgresModeLocal:
|
||||
return postgresModeLocal
|
||||
default:
|
||||
return postgresModeURL
|
||||
return postgresModeLocal
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,12 +78,16 @@ func normalizePostgresSettings(s *RecorderSettings) {
|
||||
return
|
||||
}
|
||||
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.PostgresPort = normalizePostgresPort(s.PostgresPort)
|
||||
s.PostgresDatabase = normalizePostgresName(s.PostgresDatabase, defaultPostgresDatabase)
|
||||
s.PostgresUser = normalizePostgresName(s.PostgresUser, defaultPostgresUser)
|
||||
s.DatabaseURL = strings.TrimSpace(s.DatabaseURL)
|
||||
}
|
||||
|
||||
func postgresSettingsKey(s RecorderSettings) string {
|
||||
|
||||
@ -72,7 +72,7 @@ var (
|
||||
settingsMu sync.Mutex
|
||||
settings = RecorderSettings{
|
||||
DatabaseType: databaseTypePostgres,
|
||||
PostgresMode: postgresModeURL,
|
||||
PostgresMode: postgresModeLocal,
|
||||
PostgresDataDir: defaultPostgresDataDir,
|
||||
PostgresPort: defaultPostgresPort,
|
||||
PostgresDatabase: defaultPostgresDatabase,
|
||||
|
||||
@ -286,7 +286,7 @@ function SettingsSection(props: {
|
||||
|
||||
const DEFAULTS: RecorderSettings = {
|
||||
databaseType: 'postgres',
|
||||
postgresMode: 'url',
|
||||
postgresMode: 'local',
|
||||
postgresDataDir: 'postgres-data',
|
||||
postgresPort: 55432,
|
||||
postgresDatabase: 'nsfwapp',
|
||||
@ -355,7 +355,7 @@ function shortTaskFilename(name?: string, max = 52) {
|
||||
const s = String(name ?? '').trim()
|
||||
if (!s) return ''
|
||||
if (s.length <= max) return s
|
||||
return '…' + s.slice(-(max - 1))
|
||||
return '…' + s.slice(-(max - 1))
|
||||
}
|
||||
|
||||
function regenerateTaskId(file: string) {
|
||||
@ -390,7 +390,7 @@ function assetsTaskText(file?: string, label?: string, phase?: string) {
|
||||
const fn = shortTaskFilename(file, 38)
|
||||
const phaseText = assetsPhaseLabel(label, phase)
|
||||
|
||||
if (fn && phaseText) return `${fn} · ${phaseText}`
|
||||
if (fn && phaseText) return `${fn} · ${phaseText}`
|
||||
if (fn) return fn
|
||||
return phaseText
|
||||
}
|
||||
@ -478,12 +478,12 @@ function mapCheckVideosJobToTask(job: any): TaskItem {
|
||||
return {
|
||||
id: checkVideosTaskDomId(String(job?.id || '')),
|
||||
status,
|
||||
title: 'Downloads prüfen',
|
||||
title: 'Downloads prüfen',
|
||||
text:
|
||||
status === 'running'
|
||||
? (String(job?.currentFile || '').trim()
|
||||
? shortTaskFilename(String(job?.currentFile || '').trim(), 52)
|
||||
: 'Prüfe Videos…')
|
||||
: 'Prüfe Videos…')
|
||||
: String(job?.text || ''),
|
||||
done: Number(job?.done ?? 0),
|
||||
total: Number(job?.total ?? 0),
|
||||
@ -524,9 +524,9 @@ function cleanupTaskTitle(rawId: string) {
|
||||
const id = String(rawId || '').trim().toLowerCase()
|
||||
|
||||
if (id.startsWith('cleanup-leftovers-startup')) return 'Startup-Cleanup'
|
||||
if (id.startsWith('cleanup-leftovers-hourly')) return 'Stündlicher Cleanup'
|
||||
if (id.startsWith('cleanup-leftovers')) return 'Record-Reste prüfen'
|
||||
return 'Aufräumen'
|
||||
if (id.startsWith('cleanup-leftovers-hourly')) return 'Stündlicher Cleanup'
|
||||
if (id.startsWith('cleanup-leftovers')) return 'Record-Reste prüfen'
|
||||
return 'Aufräumen'
|
||||
}
|
||||
|
||||
function databaseBackupFilename(contentDisposition: string | null) {
|
||||
@ -550,8 +550,11 @@ function normalizeDatabaseType(_value: unknown): 'postgres' {
|
||||
return 'postgres'
|
||||
}
|
||||
|
||||
function normalizePostgresMode(value: unknown): 'url' | 'local' {
|
||||
return String(value ?? '').trim().toLowerCase() === 'local' ? 'local' : 'url'
|
||||
function normalizePostgresMode(value: unknown, databaseUrl?: unknown): 'url' | 'local' {
|
||||
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) {
|
||||
@ -569,7 +572,7 @@ function normalizePostgresName(value: unknown, fallback: string) {
|
||||
}
|
||||
|
||||
function postgresSourceKey(v: any) {
|
||||
const mode = normalizePostgresMode(v?.postgresMode)
|
||||
const mode = normalizePostgresMode(v?.postgresMode, v?.databaseUrl)
|
||||
if (mode === 'local') {
|
||||
return [
|
||||
mode,
|
||||
@ -693,7 +696,7 @@ function RatingThresholdStars({
|
||||
role="radio"
|
||||
aria-checked={checked}
|
||||
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)}
|
||||
className={[
|
||||
'rounded-md p-0.5 transition',
|
||||
@ -710,7 +713,7 @@ function RatingThresholdStars({
|
||||
</div>
|
||||
|
||||
<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>
|
||||
)
|
||||
@ -733,7 +736,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
const [appLogOpen, setAppLogOpen] = useState(false)
|
||||
const [dbModalOpen, setDbModalOpen] = useState(false)
|
||||
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 [loadedPostgresPort, setLoadedPostgresPort] = useState(55432)
|
||||
const [loadedPostgresDatabase, setLoadedPostgresDatabase] = useState('nsfwapp')
|
||||
@ -748,7 +751,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
const now = Date.now()
|
||||
const saveSucceeded = saveSuccessUntilMs > now
|
||||
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 loadedPostgresSourceKey = postgresSourceKey({
|
||||
databaseType: loadedDatabaseType,
|
||||
@ -809,9 +812,9 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
const saveButton = (() => {
|
||||
if (saveUiState === 'saving') {
|
||||
return {
|
||||
text: 'Speichern…',
|
||||
text: 'Speichern…',
|
||||
color: 'blue' as const,
|
||||
icon: null, // Spinner kommt über isLoading
|
||||
icon: null, // Spinner kommt über isLoading
|
||||
isLoading: true,
|
||||
}
|
||||
}
|
||||
@ -910,7 +913,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
.then((data: RecorderSettings) => {
|
||||
if (!alive) return
|
||||
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 postgresPort = normalizePostgresPort((data as any).postgresPort)
|
||||
const postgresDatabase = normalizePostgresName((data as any).postgresDatabase, 'nsfwapp')
|
||||
@ -929,7 +932,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
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,
|
||||
autoStartAddedDownloads:
|
||||
(data as any).autoStartAddedDownloads ?? DEFAULTS.autoStartAddedDownloads,
|
||||
@ -1088,7 +1091,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
const text =
|
||||
status === 'running' || status === 'queued'
|
||||
? phaseLabel
|
||||
? `${shortTaskFilename(file, 38)} · ${phaseLabel}`
|
||||
? `${shortTaskFilename(file, 38)} · ${phaseLabel}`
|
||||
: shortTaskFilename(file, 52)
|
||||
: shortTaskFilename(file, 52)
|
||||
|
||||
@ -1389,7 +1392,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
|
||||
const ok = window.confirm(
|
||||
`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
|
||||
|
||||
@ -1463,7 +1466,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
: `Datenbank "${databaseUrl}"`
|
||||
const ok = window.confirm(
|
||||
`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
|
||||
|
||||
@ -1533,7 +1536,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
const doneDir = value.doneDir.trim()
|
||||
const ffmpegPath = (value.ffmpegPath ?? '').trim()
|
||||
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 postgresPort = normalizePostgresPort((value as any).postgresPort)
|
||||
const postgresDatabase = normalizePostgresName((value as any).postgresDatabase, 'nsfwapp')
|
||||
@ -1554,7 +1557,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
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 autoStartAddedDownloads = !!value.autoStartAddedDownloads
|
||||
const enableConcurrentDownloadsLimit = !!(value as any).enableConcurrentDownloadsLimit
|
||||
@ -1646,7 +1649,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
const t = await res.text().catch(() => '')
|
||||
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
|
||||
setSaveSuccessUntilMs(until)
|
||||
|
||||
@ -1736,24 +1739,24 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
return null
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
const ratingLine = deleteLowRated
|
||||
? `• Rating ≤ ${maxStars} Stern${maxStars === 1 ? '' : 'e'}\n`
|
||||
? `• Rating ≤ ${maxStars} Stern${maxStars === 1 ? '' : 'e'}\n`
|
||||
: ''
|
||||
|
||||
const sizeLine = mb > 0
|
||||
? `• Dateien < ${mb} MB in "${doneDir}"\n`
|
||||
? `• Dateien < ${mb} MB in "${doneDir}"\n`
|
||||
: ''
|
||||
|
||||
const ok = window.confirm(
|
||||
`Aufräumen:\n` +
|
||||
`Aufräumen:\n` +
|
||||
sizeLine +
|
||||
ratingLine +
|
||||
`• Entfernt defekte Video-Dateien\n` +
|
||||
`• Löscht Record-Reste und verwaiste Assets\n\n` +
|
||||
`• Entfernt defekte Video-Dateien\n` +
|
||||
`• Löscht Record-Reste und verwaiste Assets\n\n` +
|
||||
`Fortfahren?`
|
||||
)
|
||||
if (!ok) return null
|
||||
@ -1883,7 +1886,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
}
|
||||
|
||||
if (newPassword !== newPasswordRepeat) {
|
||||
setErr('Die neuen Passwörter stimmen nicht überein.')
|
||||
setErr('Die neuen Passwörter stimmen nicht überein.')
|
||||
return
|
||||
}
|
||||
|
||||
@ -1908,7 +1911,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
setCurrentPassword('')
|
||||
setNewPassword('')
|
||||
setNewPasswordRepeat('')
|
||||
setMsg('Passwort wurde geändert.')
|
||||
setMsg('Passwort wurde geändert.')
|
||||
} catch (e: any) {
|
||||
setErr(e?.message ?? String(e))
|
||||
} finally {
|
||||
@ -1942,7 +1945,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
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
|
||||
|
||||
setSecurityBusy(true)
|
||||
@ -1978,7 +1981,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
function passkeyErrorMessage(e: any) {
|
||||
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')) {
|
||||
return (
|
||||
@ -2021,14 +2024,14 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
setMsg(null)
|
||||
setPasskeyFeedback({
|
||||
kind: 'loading',
|
||||
text: 'Starte Passkey-Einrichtung…',
|
||||
text: 'Starte Passkey-Einrichtung…',
|
||||
})
|
||||
setSecurityBusy(true)
|
||||
|
||||
try {
|
||||
setPasskeyFeedback({
|
||||
kind: 'loading',
|
||||
text: 'Hole WebAuthn-Challenge vom Backend…',
|
||||
text: 'Hole WebAuthn-Challenge vom Backend…',
|
||||
})
|
||||
|
||||
const optionsRes = await fetch('/api/auth/passkey/register/options', {
|
||||
@ -2045,7 +2048,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
|
||||
setPasskeyFeedback({
|
||||
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({
|
||||
@ -2054,7 +2057,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
|
||||
setPasskeyFeedback({
|
||||
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', {
|
||||
@ -2069,10 +2072,10 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
throw new Error(text || `HTTP ${verifyRes.status}`)
|
||||
}
|
||||
|
||||
setMsg('Passkey wurde hinzugefügt.')
|
||||
setMsg('Passkey wurde hinzugefügt.')
|
||||
setPasskeyFeedback({
|
||||
kind: 'success',
|
||||
text: 'Passkey wurde erfolgreich hinzugefügt.',
|
||||
text: 'Passkey wurde erfolgreich hinzugefügt.',
|
||||
})
|
||||
await loadAuthStatus()
|
||||
} catch (e: any) {
|
||||
@ -2094,14 +2097,14 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
|
||||
const credentialId = String(passkey?.id ?? '').trim()
|
||||
if (!credentialId) {
|
||||
setErr('Passkey kann nicht gelöscht werden: Credential-ID fehlt.')
|
||||
setErr('Passkey kann nicht gelöscht werden: Credential-ID fehlt.')
|
||||
return
|
||||
}
|
||||
|
||||
const label = String(passkey?.label || 'diesen Passkey').trim()
|
||||
|
||||
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
|
||||
|
||||
@ -2122,7 +2125,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
throw new Error(text || `HTTP ${res.status}`)
|
||||
}
|
||||
|
||||
setMsg('Passkey wurde gelöscht.')
|
||||
setMsg('Passkey wurde gelöscht.')
|
||||
await loadAuthStatus()
|
||||
} catch (e: any) {
|
||||
setErr(e?.message ?? String(e))
|
||||
@ -2152,9 +2155,9 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
].join(' ')}
|
||||
title={
|
||||
aiServerStatus === null
|
||||
? 'AI Server Status wird geladen…'
|
||||
? 'AI Server Status wird geladen…'
|
||||
: 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}` : ''}`
|
||||
}
|
||||
>
|
||||
@ -2171,7 +2174,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
/>
|
||||
|
||||
{aiServerStatus === null
|
||||
? 'AI prüft…'
|
||||
? 'AI prüft…'
|
||||
: aiServerStatus.running
|
||||
? 'AI online'
|
||||
: 'AI offline'}
|
||||
@ -2252,15 +2255,15 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
id="tasks"
|
||||
title="Aufgaben"
|
||||
description="Hintergrundaufgaben wie z.B. Asset/Preview-Generierung."
|
||||
icon="âš™ï¸"
|
||||
icon="⚙️"
|
||||
defaultOpen={true}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<Task
|
||||
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"
|
||||
startingLabel="Starte…"
|
||||
startingLabel="Starte…"
|
||||
onTrigger={startAssetsTaskFromCurrentSwitches}
|
||||
onCreated={(job) => {
|
||||
if (!job?.id) return
|
||||
@ -2277,44 +2280,44 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
checked={!!value.generateAssetsMeta}
|
||||
onChange={(checked) => setValue((v) => ({ ...v, generateAssetsMeta: checked }))}
|
||||
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
|
||||
checked={!!value.generateAssetsThumb}
|
||||
onChange={(checked) => setValue((v) => ({ ...v, generateAssetsThumb: checked }))}
|
||||
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
|
||||
checked={!!value.generateAssetsTeaser}
|
||||
onChange={(checked) => setValue((v) => ({ ...v, generateAssetsTeaser: checked }))}
|
||||
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
|
||||
checked={!!value.generateAssetsSprites}
|
||||
onChange={(checked) => setValue((v) => ({ ...v, generateAssetsSprites: checked }))}
|
||||
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
|
||||
checked={!!value.generateAssetsAnalyze}
|
||||
onChange={(checked) => setValue((v) => ({ ...v, generateAssetsAnalyze: checked }))}
|
||||
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>
|
||||
|
||||
<Task
|
||||
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."
|
||||
startLabel="Prüfen"
|
||||
startingLabel="Starte…"
|
||||
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."
|
||||
startLabel="Prüfen"
|
||||
startingLabel="Starte…"
|
||||
startUrl="/api/tasks/check-videos"
|
||||
onCreated={(job) => {
|
||||
if (!job?.id) return
|
||||
@ -2326,10 +2329,10 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
/>
|
||||
|
||||
<Task
|
||||
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.'
|
||||
startLabel="Aufräumen"
|
||||
startingLabel="Läuft…"
|
||||
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.'
|
||||
startLabel="Aufräumen"
|
||||
startingLabel="Läuft…"
|
||||
onTrigger={cleanupSmallDone}
|
||||
disabled={saving}
|
||||
onCreated={(job) => {
|
||||
@ -2348,7 +2351,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
id="paths"
|
||||
title="Pfad-Einstellungen"
|
||||
description="Aufnahme- und Zielverzeichnisse sowie optionaler ffmpeg-Pfad."
|
||||
icon="ðŸ“"
|
||||
icon="📁"
|
||||
defaultOpen={false}
|
||||
>
|
||||
<div className="mb-3">
|
||||
@ -2492,7 +2495,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
</div>
|
||||
{databaseSettingsDirty ? (
|
||||
<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>
|
||||
) : null}
|
||||
</div>
|
||||
@ -2632,14 +2635,14 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
<SettingsSection
|
||||
id="automation"
|
||||
title="Automatisierung & Anzeige"
|
||||
description="Verhalten beim Hinzufügen/Starten sowie Anzeigeoptionen."
|
||||
icon="🤖"
|
||||
description="Verhalten beim Hinzufügen/Starten sowie Anzeigeoptionen."
|
||||
icon="🤖"
|
||||
defaultOpen={false}
|
||||
>
|
||||
<div className="mb-3">
|
||||
<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">
|
||||
Verhalten beim Hinzufügen/Starten sowie Anzeigeoptionen.
|
||||
Verhalten beim Hinzufügen/Starten sowie Anzeigeoptionen.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2701,7 +2704,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
}))
|
||||
}
|
||||
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
|
||||
@ -2713,8 +2716,8 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
autoStartAddedDownloads: checked ? v.autoStartAddedDownloads : false,
|
||||
}))
|
||||
}
|
||||
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."
|
||||
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."
|
||||
/>
|
||||
|
||||
<LabeledSwitch
|
||||
@ -2726,8 +2729,8 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
autoAddToDownloadList: checked ? true : v.autoAddToDownloadList,
|
||||
}))
|
||||
}
|
||||
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."
|
||||
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."
|
||||
/>
|
||||
|
||||
<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"
|
||||
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
|
||||
@ -2761,7 +2764,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
}))
|
||||
}
|
||||
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
|
||||
@ -2774,12 +2777,12 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
}))
|
||||
}
|
||||
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>
|
||||
|
||||
{/* ✅ 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">
|
||||
<LabeledSwitch
|
||||
checked={!!value.autoDeleteSmallDownloads}
|
||||
@ -2791,8 +2794,8 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
v.autoDeleteSmallDownloadsBelowMB ?? 50,
|
||||
}))
|
||||
}
|
||||
label="Kleine Downloads automatisch löschen"
|
||||
description="Löscht fertige Downloads automatisch, wenn die Datei kleiner als die eingestellte Mindestgröße ist."
|
||||
label="Kleine Downloads automatisch löschen"
|
||||
description="Löscht fertige Downloads automatisch, wenn die Datei kleiner als die eingestellte Mindestgröße ist."
|
||||
/>
|
||||
|
||||
<div
|
||||
@ -2802,8 +2805,8 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
}
|
||||
>
|
||||
<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-xs text-gray-600 dark:text-gray-300">Alles darunter wird gelöscht.</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>
|
||||
|
||||
<div className="sm:col-span-8">
|
||||
@ -2842,8 +2845,8 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
autoDeleteSmallDownloadsKeepFavorites: checked,
|
||||
}))
|
||||
}
|
||||
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."
|
||||
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."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -2859,8 +2862,8 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
v.autoDeleteLowRatedDownloadsMaxStars ?? 3,
|
||||
}))
|
||||
}
|
||||
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."
|
||||
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."
|
||||
/>
|
||||
|
||||
<div
|
||||
@ -2874,7 +2877,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
Sterne-Grenze
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@ -2912,7 +2915,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
Object Detection Epochs
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@ -2946,7 +2949,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
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)."
|
||||
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">
|
||||
@ -2955,7 +2958,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
Teaser abspielen
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@ -3021,7 +3024,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
<div>
|
||||
<span className="font-medium">Schwelle:</span>{' '}
|
||||
Pause unter <span className="tabular-nums">{uiPauseGB}</span> GB
|
||||
{' · '}Resume ab{' '}
|
||||
{' · '}Resume ab{' '}
|
||||
<span className="tabular-nums">
|
||||
{uiResumeGB}
|
||||
</span>{' '}
|
||||
@ -3031,7 +3034,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
<div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
|
||||
{diskStatus
|
||||
? `Frei: ${diskStatus.freeBytesHuman}${diskStatus.recordPath ? ` (Pfad: ${diskStatus.recordPath})` : ''}`
|
||||
: 'Status wird geladen…'}
|
||||
: 'Status wird geladen…'}
|
||||
</div>
|
||||
|
||||
{diskStatus?.emergency && (
|
||||
@ -3073,7 +3076,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
].join(' ')}
|
||||
aria-hidden="true"
|
||||
>
|
||||
â–¾
|
||||
▾
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@ -3086,7 +3089,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
Logdatei: {appLog.path}
|
||||
</span>
|
||||
) : appLogLoading ? (
|
||||
'Logdatei wird geladen…'
|
||||
'Logdatei wird geladen…'
|
||||
) : (
|
||||
'Logdatei noch nicht geladen.'
|
||||
)}
|
||||
@ -3098,7 +3101,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
disabled={appLogLoading}
|
||||
onClick={() => void loadAppLog()}
|
||||
>
|
||||
{appLogLoading ? 'Lade…' : 'Aktualisieren'}
|
||||
{appLogLoading ? 'Lade…' : 'Aktualisieren'}
|
||||
</Button>
|
||||
</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"
|
||||
>
|
||||
{appLogLoading
|
||||
? 'Log wird geladen…'
|
||||
? 'Log wird geladen…'
|
||||
: appLog?.log?.trim()
|
||||
? appLog.log
|
||||
: 'Noch kein Log vorhanden.'}
|
||||
@ -3129,7 +3132,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
id="security"
|
||||
title="Konto-Sicherheit"
|
||||
description="Passwort, Zwei-Faktor-Authentifizierung und Passkeys getrennt von den Recorder-Einstellungen verwalten."
|
||||
icon="🛡ï¸"
|
||||
icon="🛡️"
|
||||
defaultOpen={false}
|
||||
actions={
|
||||
<>
|
||||
@ -3143,10 +3146,10 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
Passwort ändern
|
||||
Passwort ändern
|
||||
</div>
|
||||
<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>
|
||||
@ -3201,7 +3204,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
Zwei-Faktor-Authentifizierung
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@ -3211,7 +3214,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
<div className="mt-3 space-y-2">
|
||||
{authStatus?.totpDevice ? (
|
||||
<SecurityDeviceRow
|
||||
icon="ðŸ”"
|
||||
icon="🔐"
|
||||
title={authStatus.totpDevice.label || 'Authenticator-App'}
|
||||
subtitle={
|
||||
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">
|
||||
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>
|
||||
@ -3275,7 +3278,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
) : (
|
||||
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Zum Deaktivieren brauchst du dein aktuelles Passwort
|
||||
{authStatus?.totpEnabled ? ' und einen gültigen 2FA-Code.' : '.'}
|
||||
{authStatus?.totpEnabled ? ' und einen gültigen 2FA-Code.' : '.'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -3300,7 +3303,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
Passkeys
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@ -3312,20 +3315,20 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
(authStatus?.passkeys ?? []).map((pk, index) => (
|
||||
<SecurityDeviceRow
|
||||
key={pk.id || index}
|
||||
icon="🔑"
|
||||
icon="🔑"
|
||||
title={pk.label || `Passkey ${index + 1}`}
|
||||
subtitle={
|
||||
pk.backupEligible
|
||||
? pk.backupState
|
||||
? 'Synchronisierter Passkey'
|
||||
: 'Passkey kann synchronisiert werden'
|
||||
: 'Gerätegebundener Passkey oder Sicherheitsschlüssel'
|
||||
: 'Gerätegebundener Passkey oder Sicherheitsschlüssel'
|
||||
}
|
||||
meta={[
|
||||
`Erstellt: ${formatSecurityDate(pk.createdAt)}`,
|
||||
`Zuletzt genutzt: ${formatSecurityDate(pk.lastUsedAt)}`,
|
||||
typeof pk.signCount === 'number' ? `SignCount: ${pk.signCount}` : '',
|
||||
].filter(Boolean).join(' · ')}
|
||||
].filter(Boolean).join(' · ')}
|
||||
badge="Passkey"
|
||||
actions={
|
||||
<Button
|
||||
@ -3335,7 +3338,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
disabled={securityBusy}
|
||||
onClick={() => void deletePasskey(pk)}
|
||||
>
|
||||
Löschen
|
||||
Löschen
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
@ -3349,7 +3352,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
|
||||
{!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">
|
||||
Dein Browser oder diese Verbindung unterstützt Passkeys aktuell nicht.
|
||||
Dein Browser oder diese Verbindung unterstützt Passkeys aktuell nicht.
|
||||
</div>
|
||||
) : (
|
||||
<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',
|
||||
].join(' ')}
|
||||
>
|
||||
{passkeyFeedback.kind === 'loading' ? 'â³ ' : null}
|
||||
{passkeyFeedback.kind === 'success' ? '✅ ' : null}
|
||||
{passkeyFeedback.kind === 'error' ? 'âš ï¸ ' : null}
|
||||
{passkeyFeedback.kind === 'loading' ? '⏳ ' : null}
|
||||
{passkeyFeedback.kind === 'success' ? '✅ ' : null}
|
||||
{passkeyFeedback.kind === 'error' ? '⚠️ ' : null}
|
||||
{passkeyFeedback.text}
|
||||
</div>
|
||||
) : null}
|
||||
@ -3377,7 +3380,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
disabled={securityBusy}
|
||||
onClick={() => void registerSettingsPasskey()}
|
||||
>
|
||||
{securityBusy ? 'Passkey wird eingerichtet…' : 'Passkey hinzufügen'}
|
||||
{securityBusy ? 'Passkey wird eingerichtet…' : 'Passkey hinzufügen'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user