diff --git a/backend/build_manifest.env b/backend/build_manifest.env index 1a5082b..68f6af6 100644 --- a/backend/build_manifest.env +++ b/backend/build_manifest.env @@ -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" diff --git a/backend/dist/nsfwapp-linux-amd64 b/backend/dist/nsfwapp-linux-amd64 index 829fe6d..e1a033d 100644 Binary files a/backend/dist/nsfwapp-linux-amd64 and b/backend/dist/nsfwapp-linux-amd64 differ diff --git a/backend/dist/nsfwapp.exe b/backend/dist/nsfwapp.exe index 02163f4..68c1079 100644 Binary files a/backend/dist/nsfwapp.exe and b/backend/dist/nsfwapp.exe differ diff --git a/backend/dist/nsfwapp_amd64.deb b/backend/dist/nsfwapp_amd64.deb index c4759e0..1d18395 100644 Binary files a/backend/dist/nsfwapp_amd64.deb and b/backend/dist/nsfwapp_amd64.deb differ diff --git a/backend/postgres_local.go b/backend/postgres_local.go index 11daacd..e106c8f 100644 --- a/backend/postgres_local.go +++ b/backend/postgres_local.go @@ -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 { diff --git a/backend/settings.go b/backend/settings.go index 4c5d344..f184f1c 100644 --- a/backend/settings.go +++ b/backend/settings.go @@ -72,7 +72,7 @@ var ( settingsMu sync.Mutex settings = RecorderSettings{ DatabaseType: databaseTypePostgres, - PostgresMode: postgresModeURL, + PostgresMode: postgresModeLocal, PostgresDataDir: defaultPostgresDataDir, PostgresPort: defaultPostgresPort, PostgresDatabase: defaultPostgresDatabase, diff --git a/frontend/src/components/ui/RecorderSettings.tsx b/frontend/src/components/ui/RecorderSettings.tsx index f30e148..66e96d4 100644 --- a/frontend/src/components/ui/RecorderSettings.tsx +++ b/frontend/src/components/ui/RecorderSettings.tsx @@ -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({
- Löscht bis einschließlich {selected} Stern{selected === 1 ? '' : 'e'}. + Löscht bis einschließlich {selected} Stern{selected === 1 ? '' : 'e'}.
) @@ -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} >
{ 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." /> 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." /> 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." /> 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." /> 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." />
{ if (!job?.id) return @@ -2326,10 +2329,10 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { /> { @@ -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} >
@@ -2492,7 +2495,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
{databaseSettingsDirty ? (
- Die Datenbank-Einstellungen wurden geaendert. Bitte erst speichern. + Die Datenbank-Einstellungen wurden geändert. Bitte erst speichern.
) : null} @@ -2632,14 +2635,14 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
Automatisierung & Anzeige
- Verhalten beim Hinzufügen/Starten sowie Anzeigeoptionen. + Verhalten beim Hinzufügen/Starten sowie Anzeigeoptionen.
@@ -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." />
@@ -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." />
- {/* ✅ NEU: Auto-Delete kleine Downloads */} + {/* ✅ NEU: Auto-Delete kleine Downloads */}
-
Mindestgröße
-
Alles darunter wird gelöscht.
+
Mindestgröße
+
Alles darunter wird gelöscht.
@@ -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." />
@@ -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." />
- Bis einschließlich dieser Bewertung wird gelöscht. + Bis einschließlich dieser Bewertung wird gelöscht.
@@ -2912,7 +2915,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { Object Detection Epochs
- 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.
@@ -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)." />
@@ -2955,7 +2958,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { Teaser abspielen
- 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.
@@ -3021,7 +3024,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
Schwelle:{' '} Pause unter {uiPauseGB} GB - {' · '}Resume ab{' '} + {' · '}Resume ab{' '} {uiResumeGB} {' '} @@ -3031,7 +3034,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
{diskStatus ? `Frei: ${diskStatus.freeBytesHuman}${diskStatus.recordPath ? ` (Pfad: ${diskStatus.recordPath})` : ''}` - : 'Status wird geladen…'} + : 'Status wird geladen…'}
{diskStatus?.emergency && ( @@ -3073,7 +3076,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { ].join(' ')} aria-hidden="true" > - â–¾ + ▾ @@ -3086,7 +3089,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { Logdatei: {appLog.path} ) : 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'}
@@ -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) {
- Passwort ändern + Passwort ändern
- Ändert das Login-Passwort. Andere Sessions werden abgemeldet. + Ändert das Login-Passwort. Andere Sessions werden abgemeldet.
@@ -3201,7 +3204,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { Zwei-Faktor-Authentifizierung
- 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.
@@ -3211,7 +3214,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
{authStatus?.totpDevice ? ( ) : (
- 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.
)}
@@ -3275,7 +3278,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { ) : (
Zum Deaktivieren brauchst du dein aktuelles Passwort - {authStatus?.totpEnabled ? ' und einen gültigen 2FA-Code.' : '.'} + {authStatus?.totpEnabled ? ' und einen gültigen 2FA-Code.' : '.'}
)} @@ -3300,7 +3303,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { Passkeys
- Anmeldung ohne Passwort über Face ID, Fingerabdruck, Windows Hello oder Sicherheitsschlüssel. + Anmeldung ohne Passwort über Face ID, Fingerabdruck, Windows Hello oder Sicherheitsschlüssel.
@@ -3312,20 +3315,20 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { (authStatus?.passkeys ?? []).map((pk, index) => ( void deletePasskey(pk)} > - Löschen + Löschen } /> @@ -3349,7 +3352,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { {!passkeySupported ? (
- Dein Browser oder diese Verbindung unterstützt Passkeys aktuell nicht. + Dein Browser oder diese Verbindung unterstützt Passkeys aktuell nicht.
) : (
@@ -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}
) : 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'}