import React, { useEffect, useRef, useState } from 'react'; import { TabView, TabPanel } from 'primereact/tabview'; import { Accordion, AccordionTab } from 'primereact/accordion'; import { InputText } from 'primereact/inputtext'; import { InputNumber } from 'primereact/inputnumber'; import { InputSwitch } from 'primereact/inputswitch'; import { Dropdown } from 'primereact/dropdown'; import { Checkbox } from 'primereact/checkbox'; import { Tag } from 'primereact/tag'; import { Button } from 'primereact/button'; import { api } from '../api/client'; function normalizeText(value) { return String(value || '').trim().toLowerCase(); } function normalizeSettingKey(value) { return String(value || '').trim().toLowerCase(); } const GENERAL_TOOL_KEYS = new Set([ 'makemkv_command', 'makemkv_registration_key', 'makemkv_min_length_minutes', 'mediainfo_command', 'handbrake_command', 'ffmpeg_command', 'mkvmerge_command', 'ffprobe_command', 'handbrake_restart_delete_incomplete_output', 'script_test_timeout_ms' ]); const HANDBRAKE_PRESET_SETTING_KEYS = new Set([ 'handbrake_preset', 'handbrake_preset_bluray', 'handbrake_preset_dvd' ]); const NOTIFICATION_EVENT_TOGGLE_KEYS = new Set([ 'pushover_notify_metadata_ready', 'pushover_notify_rip_started', 'pushover_notify_encoding_started', 'pushover_notify_job_finished', 'pushover_notify_job_error', 'pushover_notify_job_cancelled', 'pushover_notify_reencode_started', 'pushover_notify_reencode_finished' ]); const PUSHOVER_ENABLED_SETTING_KEY = 'pushover_enabled'; const EXPERT_MODE_SETTING_KEY = 'ui_expert_mode'; const EXTERNAL_STORAGE_PATHS_KEY = 'external_storage_paths'; const ALWAYS_HIDDEN_SETTING_KEYS = new Set([ 'drive_device', 'makemkv_source_index', // auto-derived from drive path — shown as read-only in drive info 'makemkv_rip_mode', 'makemkv_rip_mode_bluray', 'makemkv_rip_mode_dvd', 'makemkv_backup_mode' ]); const EXPERT_ONLY_SETTING_KEYS = new Set([ 'pushover_device', 'pushover_priority', 'pushover_timeout_ms', 'disc_poll_interval_ms', 'hardware_monitoring_interval_ms', 'makemkv_command', 'mediainfo_command', 'handbrake_command', 'mediainfo_extra_args_bluray', 'mediainfo_extra_args_dvd', 'makemkv_analyze_extra_args_bluray', 'makemkv_analyze_extra_args_dvd', 'makemkv_rip_extra_args_bluray', 'makemkv_rip_extra_args_dvd', 'cdparanoia_command', 'script_test_timeout_ms' ]); const EXPERT_ONLY_CATEGORY_NAMES = new Set([ 'logging' ]); function toBoolean(value) { if (typeof value === 'boolean') { return value; } if (typeof value === 'number') { return value !== 0; } const normalized = normalizeText(value); if (!normalized) { return false; } return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on'; } function DriveDevicesEditor({ value, onChange, settingKey }) { const parseDevices = (val) => { try { const parsed = JSON.parse(val || '[]'); if (!Array.isArray(parsed)) return []; const normalized = parsed.map((e) => { if (typeof e === 'string') { const p = e.trim(); if (!p) return null; return { path: p, makemkvIndex: null }; } if (e && typeof e === 'object') { const p = String(e.path || '').trim(); if (!p) return null; const idxRaw = Number(e.makemkvIndex); return { path: p, makemkvIndex: Number.isFinite(idxRaw) && idxRaw >= 0 ? Math.trunc(idxRaw) : null }; } return null; }).filter(Boolean); const used = new Set(); let nextAutoIndex = 0; return normalized.map((entry) => { if (entry.makemkvIndex != null) { used.add(entry.makemkvIndex); if (entry.makemkvIndex >= nextAutoIndex) { nextAutoIndex = entry.makemkvIndex + 1; } return entry; } while (used.has(nextAutoIndex)) { nextAutoIndex += 1; } const resolved = { path: entry.path, makemkvIndex: nextAutoIndex }; used.add(nextAutoIndex); nextAutoIndex += 1; return resolved; }); } catch (_e) { return []; } }; const devices = parseDevices(value); const updateDevices = (newDevices) => { onChange?.(settingKey, JSON.stringify(newDevices)); }; return (
{devices.length === 0 && (

Keine expliziten Laufwerke konfiguriert.

)} {devices.map((dev, idx) => (
{ const next = [...devices]; const p = e.target.value; next[idx] = { ...next[idx], path: p }; updateDevices(next); }} className="drive-device-input" /> { const next = [...devices]; next[idx] = { ...next[idx], makemkvIndex: e.value ?? 0 }; updateDevices(next); }} />
))}
); } function ExternalStoragePathsEditor({ value, onChange, settingKey }) { const parsePaths = (rawValue) => { try { const parsed = JSON.parse(rawValue || '[]'); if (!Array.isArray(parsed)) return []; const normalized = []; for (const entry of parsed) { if (typeof entry === 'string') { normalized.push({ name: '', path: String(entry || '') }); continue; } if (entry && typeof entry === 'object') { normalized.push({ name: String(entry.name || entry.label || ''), path: String(entry.path || '') }); } } return normalized; } catch (_error) { return []; } }; const entries = parsePaths(value); const updatePaths = (nextPaths) => { const normalized = (Array.isArray(nextPaths) ? nextPaths : []) .map((entry) => ({ name: String(entry?.name || ''), path: String(entry?.path || '') })); onChange?.(settingKey, JSON.stringify(normalized)); }; return (
{entries.length === 0 && (

Keine externen Speicherpfade konfiguriert.

)} {entries.map((entry, idx) => (
{ const next = [...entries]; next[idx] = { ...next[idx], name: event.target.value }; updatePaths(next); }} className="external-storage-name-input" /> { const next = [...entries]; next[idx] = { ...next[idx], path: event.target.value }; updatePaths(next); }} className="external-storage-input" />
))}
); } function DetectedDrivesInfo({ expertModeEnabled = false, onNotify = null }) { const [drives, setDrives] = useState(null); const [loading, setLoading] = useState(false); const [unlocking, setUnlocking] = useState(false); const load = () => { setLoading(true); api.getDetectedDrives() .then((res) => setDrives(Array.isArray(res?.drives) ? res.drives : [])) .catch(() => setDrives([])) .finally(() => setLoading(false)); }; useEffect(() => { load(); }, []); const notify = (severity, summary, detail) => { if (typeof onNotify === 'function') { onNotify({ severity, summary, detail }); } }; const handleUnlockOne = async (devicePath) => { setUnlocking(true); try { await api.forceUnlockDrive({ devicePath }); notify('success', 'Laufwerk freigegeben', `${devicePath} wurde entsperrt.`); load(); } catch (error) { notify('error', 'Freigabe fehlgeschlagen', error?.message || 'Unbekannter Fehler'); } finally { setUnlocking(false); } }; const handleUnlockAll = async () => { setUnlocking(true); try { await api.forceUnlockDrive({ all: true }); notify('success', 'Laufwerke freigegeben', 'Alle erkannten Laufwerke wurden entsperrt.'); load(); } catch (error) { notify('error', 'Freigabe fehlgeschlagen', error?.message || 'Unbekannter Fehler'); } finally { setUnlocking(false); } }; return (
Erkannte optische Laufwerke
{expertModeEnabled ? (
{drives === null || loading ? Wird geladen… : drives.length === 0 ? Keine optischen Laufwerke erkannt. : ( {expertModeEnabled ? : null} {drives.map((d) => ( {expertModeEnabled ? ( ) : null} ))}
Gerät Modell MakeMKV IndexFreigabe
{d.path} {d.model || '—'}
)}
); } function shouldHideSettingByExpertMode(settingKey, expertModeEnabled) { const key = normalizeSettingKey(settingKey); if (!key) { return false; } if (ALWAYS_HIDDEN_SETTING_KEYS.has(key)) { return true; } if (key === EXPERT_MODE_SETTING_KEY) { return true; } return !expertModeEnabled && EXPERT_ONLY_SETTING_KEYS.has(key); } function filterSettingsByVisibility(settings, expertModeEnabled) { const list = Array.isArray(settings) ? settings : []; return list.filter((setting) => !shouldHideSettingByExpertMode(setting?.key, expertModeEnabled)); } function shouldHideCategoryByExpertMode(categoryName, expertModeEnabled) { const normalizedCategory = normalizeText(categoryName); if (!normalizedCategory) { return false; } return !expertModeEnabled && EXPERT_ONLY_CATEGORY_NAMES.has(normalizedCategory); } function buildToolSections(settings) { const list = Array.isArray(settings) ? settings : []; const generalBucket = { id: 'general', title: 'General', description: 'Gemeinsame Tool-Settings für alle Medien.', settings: [] }; const blurayBucket = { id: 'bluray', title: 'BluRay', description: 'Profil-spezifische Settings für Blu-ray.', settings: [] }; const dvdBucket = { id: 'dvd', title: 'DVD', description: 'Profil-spezifische Settings für DVD.', settings: [] }; const audiobookBucket = { id: 'audiobook', title: 'Audiobook', description: 'Profil-spezifische Settings für Audiobooks.', settings: [] }; const fallbackBucket = { id: 'other', title: 'Weitere Tool-Settings', description: null, settings: [] }; for (const setting of list) { const key = normalizeSettingKey(setting?.key); if (GENERAL_TOOL_KEYS.has(key)) { generalBucket.settings.push(setting); continue; } if (key.endsWith('_bluray')) { blurayBucket.settings.push(setting); continue; } if (key.endsWith('_dvd')) { dvdBucket.settings.push(setting); continue; } if (key.endsWith('_audiobook')) { audiobookBucket.settings.push(setting); continue; } fallbackBucket.settings.push(setting); } const sections = [ generalBucket, blurayBucket, dvdBucket, audiobookBucket ].filter((item) => item.settings.length > 0); if (fallbackBucket.settings.length > 0) { sections.push(fallbackBucket); } return sections; } // Path keys per medium — _owner keys are rendered inline const BLURAY_PATH_KEYS = [ 'raw_dir_bluray', 'raw_dir_bluray_series', 'movie_dir_bluray', 'series_dir_bluray', 'output_template_bluray', 'output_template_bluray_series_episode', 'output_template_bluray_series_multi_episode' ]; const DVD_PATH_KEYS = [ 'raw_dir_dvd', 'raw_dir_dvd_series', 'movie_dir_dvd', 'series_dir_dvd', 'output_template_dvd', 'output_template_dvd_series_episode', 'output_template_dvd_series_multi_episode' ]; const CD_PATH_KEYS = ['raw_dir_cd', 'movie_dir_cd', 'cd_output_template']; const AUDIOBOOK_PATH_KEYS = ['raw_dir_audiobook', 'movie_dir_audiobook', 'output_template_audiobook', 'output_chapter_template_audiobook', 'audiobook_raw_template']; const DOWNLOAD_PATH_KEYS = ['download_dir']; const LOG_PATH_KEYS = ['log_dir']; const EXTERNAL_STORAGE_PATH_KEYS = [EXTERNAL_STORAGE_PATHS_KEY]; const CONVERTER_PATH_KEYS = ['converter_raw_dir', 'converter_movie_dir', 'converter_audio_dir', 'converter_output_template_video', 'converter_output_template_audio']; const CONVERTER_SCAN_EXTENSION_KEY = 'converter_scan_extensions'; const CONVERTER_SCAN_EXTENSION_OPTIONS = [ 'mkv', 'mp4', 'm2ts', 'iso', 'avi', 'mov', 'flac', 'mp3', 'wav', 'm4a', 'ogg', 'opus' ]; const READONLY_CLI_SETTING_KEYS = new Set([ 'makemkv_command', 'mediainfo_command', 'handbrake_command', 'ffmpeg_command', 'ffprobe_command', 'cdparanoia_command', 'mkvmerge_command' ]); function parseConverterScanExtensions(value) { const tokens = String(value || '') .split(',') .map((item) => item.trim().toLowerCase()) .filter(Boolean); const known = new Set(CONVERTER_SCAN_EXTENSION_OPTIONS); const seen = new Set(); const selected = []; for (const token of tokens) { if (!known.has(token) || seen.has(token)) { continue; } seen.add(token); selected.push(token); } return selected; } function serializeConverterScanExtensions(values) { const list = Array.isArray(values) ? values : []; const known = new Set(CONVERTER_SCAN_EXTENSION_OPTIONS); const seen = new Set(); const normalized = []; for (const item of list) { const ext = String(item || '').trim().toLowerCase(); if (!known.has(ext) || seen.has(ext)) { continue; } seen.add(ext); normalized.push(ext); } return normalized.join(','); } function ConverterScanExtensionsEditor({ value, onChange, settingKey }) { const selected = parseConverterScanExtensions(value); const selectedSet = new Set(selected); const handleToggle = (extension) => { const ext = String(extension || '').trim().toLowerCase(); if (!ext) { return; } let next = selected.filter((item) => item !== ext); if (!selectedSet.has(ext)) { next = [...selected, ext]; } // Setting ist required/minLength: mindestens eine Endung aktiv lassen. if (next.length === 0) { return; } onChange?.(settingKey, serializeConverterScanExtensions(next)); }; return (
{CONVERTER_SCAN_EXTENSION_OPTIONS.map((extension) => ( ))}
); } function buildSectionsForCategory(categoryName, settings) { const list = Array.isArray(settings) ? settings : []; const normalizedCategory = normalizeText(categoryName); if (normalizedCategory === 'tools') { const sections = buildToolSections(list); if (sections.length > 0) { return sections; } } return [ { id: 'all', title: null, description: null, settings: list } ]; } function isHandBrakePresetSetting(setting) { const key = String(setting?.key || '').trim().toLowerCase(); return HANDBRAKE_PRESET_SETTING_KEYS.has(key); } function isNotificationEventToggleSetting(setting) { return setting?.type === 'boolean' && NOTIFICATION_EVENT_TOGGLE_KEYS.has(normalizeSettingKey(setting?.key)); } function isReadonlySetting(setting) { const key = normalizeSettingKey(setting?.key); if (READONLY_CLI_SETTING_KEYS.has(key)) { return true; } try { // API gibt validation als parsed object zurück, nicht validation_json als String const v = setting?.validation; if (v && typeof v === 'object') return v.readonly === true; return JSON.parse(setting?.validation_json || '{}')?.readonly === true; } catch { return false; } } function MakeMKVBetaKeyHint({ disabled = false, onNotify = null, onSettingApplied = null }) { const [loading, setLoading] = useState(true); const [checking, setChecking] = useState(false); const [applying, setApplying] = useState(false); const [betaKey, setBetaKey] = useState(''); const [validUntil, setValidUntil] = useState(''); const [fetchedAt, setFetchedAt] = useState(''); const [appliedKey, setAppliedKey] = useState(''); const [error, setError] = useState(''); const [warning, setWarning] = useState(''); const loadCachedState = async () => { setLoading(true); setError(''); setWarning(''); try { const response = await api.getMakeMKVBetaKey(); setBetaKey(String(response?.betaKey || '').trim()); setValidUntil(String(response?.validUntil || '').trim()); setFetchedAt(String(response?.fetchedAt || '').trim()); setAppliedKey(String(response?.appliedKey || '').trim()); if (response?.stale) { setWarning('Zwischengespeicherter Betakey ist veraltet. Bitte bei Bedarf neu prüfen.'); } else if (!response?.betaKey) { setWarning('Noch kein geprüfter Betakey im Cache. Prüfung startet nur per Button.'); } else if (response?.appliedMatchesCache) { setWarning('Geprüfter Betakey ist bereits übernommen.'); } else { setWarning(''); } } catch (loadError) { setBetaKey(''); setValidUntil(''); setFetchedAt(''); setAppliedKey(''); setError(loadError?.message || 'Betakey konnte nicht geladen werden.'); } finally { setLoading(false); } }; useEffect(() => { void loadCachedState(); }, []); const handleCheck = async () => { setChecking(true); setError(''); setWarning(''); try { const response = await api.checkMakeMKVBetaKey(); const nextKey = String(response?.betaKey || '').trim(); setBetaKey(nextKey); setValidUntil(String(response?.validUntil || '').trim()); setFetchedAt(String(response?.fetchedAt || '').trim()); setWarning(''); if (typeof onNotify === 'function') { onNotify({ severity: 'success', summary: 'MakeMKV Betakey', detail: nextKey ? 'Betakey erfolgreich geprüft.' : 'Betakey-Prüfung abgeschlossen.' }); } } catch (checkError) { setError(checkError?.message || 'Betakey konnte nicht geprüft werden.'); if (typeof onNotify === 'function') { onNotify({ severity: 'error', summary: 'MakeMKV Betakey', detail: checkError?.message || 'Betakey konnte nicht geprüft werden.' }); } } finally { setChecking(false); } }; const handleApply = async () => { if (!betaKey) { return; } setApplying(true); setError(''); try { const response = await api.applyMakeMKVBetaKey({ betaKey }); const nextAppliedKey = String(response?.betaKey || betaKey).trim(); setAppliedKey(nextAppliedKey); setWarning('Geprüfter Betakey ist bereits übernommen.'); if (typeof onSettingApplied === 'function') { onSettingApplied({ key: 'makemkv_registration_key', value: nextAppliedKey }); } if (typeof onNotify === 'function') { onNotify({ severity: 'success', summary: 'MakeMKV Betakey', detail: 'Betakey wurde übernommen und gespeichert.' }); } } catch (applyError) { setError(applyError?.message || 'Betakey konnte nicht übernommen werden.'); if (typeof onNotify === 'function') { onNotify({ severity: 'error', summary: 'MakeMKV Betakey', detail: applyError?.message || 'Betakey konnte nicht übernommen werden.' }); } } finally { setApplying(false); } }; return (
Betakey
{loading ? ( Zwischengespeicherter Betakey wird geladen… ) : null} {!loading && error ? ( {error} ) : null} {!loading && !error && warning ? ( {warning} ) : null} {!loading && !error && fetchedAt ? ( Zuletzt geprüft: {new Date(fetchedAt).toLocaleString('de-DE')} ) : null} {!loading && !error && validUntil ? ( Gültig bis: {new Date(validUntil).toLocaleString('de-DE')} ) : null} {!loading && !error && betaKey ? ( {betaKey} ) : null}
); } function ActivationBytesCacheBox({ entries = [] }) { const list = Array.isArray(entries) ? entries : []; return (

Activation Bytes Cache

Lokal gespeicherte AAX-Activation Bytes. Werden beim ersten Upload automatisch über die Audible-Tools API ermittelt.
{list.length === 0 ? (

Keine Einträge vorhanden.

) : (
{list.map((entry, index) => ( ))}
Checksum Activation Bytes Gespeichert am
{String(entry?.checksum || '-')} {String(entry?.activation_bytes || '-')} {entry?.created_at ? new Date(entry.created_at).toLocaleString('de-DE') : '-'}
)}
); } function SettingField({ setting, value, error, dirty, ownerSetting, ownerValue, ownerError, ownerDirty, onChange, variant = 'default', disabled = false, onNotify = null, onSettingApplied = null }) { const ownerKey = ownerSetting?.key; const pathHasValue = Boolean(String(value ?? '').trim()); const isNotificationToggleBox = variant === 'notification-toggle' && setting?.type === 'boolean'; const readonlySetting = isReadonlySetting(setting); const isDisabled = Boolean(disabled || readonlySetting); return (
{isNotificationToggleBox ? (
onChange?.(setting.key, event.value)} />
) : ( )} {(setting.type === 'string' || setting.type === 'path') && setting.key !== 'drive_devices' && setting.key !== EXTERNAL_STORAGE_PATHS_KEY && setting.key !== CONVERTER_SCAN_EXTENSION_KEY ? ( onChange?.(setting.key, event.target.value)} /> ) : null} {setting.key === 'drive_devices' ? ( ) : null} {setting.key === EXTERNAL_STORAGE_PATHS_KEY ? ( ) : null} {setting.key === CONVERTER_SCAN_EXTENSION_KEY ? ( ) : null} {setting.type === 'number' ? ( onChange?.(setting.key, event.value)} mode="decimal" useGrouping={false} /> ) : null} {setting.type === 'boolean' && !isNotificationToggleBox ? ( onChange?.(setting.key, event.value)} /> ) : null} {setting.type === 'select' ? ( onChange?.(setting.key, event.value)} /> ) : null} {setting.description || ''} {isHandBrakePresetSetting(setting) ? ( Preset-Erklärung:{' '} HandBrake Official Presets ) : null} {normalizeSettingKey(setting?.key) === 'makemkv_registration_key' ? ( ) : null} {error ? ( {error} ) : ( )} {ownerSetting ? (
onChange?.(ownerKey, event.target.value)} /> {ownerError ? ( {ownerError} ) : ( )}
) : null}
); } function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effectivePaths, onNotify, onSettingApplied }) { const list = Array.isArray(settings) ? settings : []; const settingsByKey = new Map(list.map((s) => [s.key, s])); const bluraySettings = list.filter((s) => BLURAY_PATH_KEYS.includes(s.key) || (s.key.endsWith('_owner') && BLURAY_PATH_KEYS.includes(s.key.replace('_owner', '')))); const dvdSettings = list.filter((s) => DVD_PATH_KEYS.includes(s.key) || (s.key.endsWith('_owner') && DVD_PATH_KEYS.includes(s.key.replace('_owner', '')))); const cdSettings = list.filter((s) => CD_PATH_KEYS.includes(s.key) || (s.key.endsWith('_owner') && CD_PATH_KEYS.includes(s.key.replace('_owner', '')))); const audiobookSettings = list.filter((s) => AUDIOBOOK_PATH_KEYS.includes(s.key) || (s.key.endsWith('_owner') && AUDIOBOOK_PATH_KEYS.includes(s.key.replace('_owner', '')))); const downloadSettings = list.filter((s) => DOWNLOAD_PATH_KEYS.includes(s.key) || (s.key.endsWith('_owner') && DOWNLOAD_PATH_KEYS.includes(s.key.replace('_owner', '')))); const logSettings = list.filter((s) => LOG_PATH_KEYS.includes(s.key)); const externalStorageSettings = list.filter((s) => EXTERNAL_STORAGE_PATH_KEYS.includes(s.key)); const converterSettings = list.filter((s) => CONVERTER_PATH_KEYS.includes(s.key)); const defaultRaw = effectivePaths?.defaults?.raw || 'data/output/raw'; const defaultMovies = effectivePaths?.defaults?.movies || 'data/output/movies'; const defaultSeries = effectivePaths?.defaults?.series || 'data/output/series'; const defaultCd = effectivePaths?.defaults?.cd || 'data/output/cd'; const defaultAudiobookRaw = effectivePaths?.defaults?.audiobookRaw || 'data/output/audiobook-raw'; const defaultAudiobookMovies = effectivePaths?.defaults?.audiobookMovies || 'data/output/audiobooks'; const defaultDownloads = effectivePaths?.defaults?.downloads || 'data/downloads'; const defaultConverterRaw = effectivePaths?.defaults?.converterRaw || 'data/output/converter-raw'; const defaultConverterMovies = effectivePaths?.defaults?.converterMovies || 'data/output/converted-movies'; const defaultConverterAudio = effectivePaths?.defaults?.converterAudio || 'data/output/converted-audio'; const ep = effectivePaths || {}; const blurayRaw = ep.bluray?.raw || defaultRaw; const bluraySeriesRaw = ep.bluray?.seriesRaw || blurayRaw; const blurayMovies = ep.bluray?.movies || defaultMovies; const bluraySeries = ep.bluray?.series || defaultSeries; const dvdRaw = ep.dvd?.raw || defaultRaw; const dvdSeriesRaw = ep.dvd?.seriesRaw || dvdRaw; const dvdMovies = ep.dvd?.movies || defaultMovies; const dvdSeries = ep.dvd?.series || defaultSeries; const cdRaw = ep.cd?.raw || defaultCd; const cdMovies = ep.cd?.movies || cdRaw; const audiobookRaw = ep.audiobook?.raw || defaultAudiobookRaw; const audiobookMovies = ep.audiobook?.movies || defaultAudiobookMovies; const downloadPath = ep.downloads?.path || defaultDownloads; const converterRaw = ep.converter?.raw || defaultConverterRaw; const converterMovies = ep.converter?.movies || defaultConverterMovies; const converterAudio = ep.converter?.audio || defaultConverterAudio; const isDefault = (path, def) => path === def; return (
{/* Effektive Pfade Übersicht */}

Effektive Pfade

Zeigt die tatsächlich verwendeten Pfade entsprechend der aktuellen Konfiguration.
Medium RAW / Eingang Encode / Ausgang
Blu-ray {blurayRaw} {isDefault(blurayRaw, defaultRaw) && Standard} {blurayMovies} {isDefault(blurayMovies, defaultMovies) && Standard}
Blu-ray Serie {bluraySeriesRaw} {isDefault(bluraySeriesRaw, blurayRaw) && wie Blu-ray RAW} {bluraySeries} {isDefault(bluraySeries, defaultSeries) && Standard}
DVD {dvdRaw} {isDefault(dvdRaw, defaultRaw) && Standard} {dvdMovies} {isDefault(dvdMovies, defaultMovies) && Standard}
DVD Serie {dvdSeriesRaw} {isDefault(dvdSeriesRaw, dvdRaw) && wie DVD RAW} {dvdSeries} {isDefault(dvdSeries, defaultSeries) && Standard}
CD / Audio {cdRaw} {isDefault(cdRaw, defaultCd) && Standard} {cdMovies} {isDefault(cdMovies, cdRaw) && Standard}
Audiobook {audiobookRaw} {isDefault(audiobookRaw, defaultAudiobookRaw) && Standard} {audiobookMovies} {isDefault(audiobookMovies, defaultAudiobookMovies) && Standard}
Zip-Downloads {downloadPath} {isDefault(downloadPath, defaultDownloads) && Standard} ---
Converter Raw {converterRaw} {isDefault(converterRaw, defaultConverterRaw) && Standard} ---
Converter Video --- {converterMovies} {isDefault(converterMovies, defaultConverterMovies) && Standard}
Converter Audio --- {converterAudio} {isDefault(converterAudio, defaultConverterAudio) && Standard}
{/* Medium-Karten als Accordion */} {[ { title: 'Blu-ray', pathSettings: bluraySettings }, { title: 'DVD', pathSettings: dvdSettings }, { title: 'CD / Audio', pathSettings: cdSettings }, { title: 'Audiobook', pathSettings: audiobookSettings }, { title: 'Converter', pathSettings: converterSettings }, { title: 'Downloads', pathSettings: downloadSettings }, ...(externalStorageSettings.length > 0 ? [{ title: 'Externe Speicher', pathSettings: externalStorageSettings }] : []), ...(logSettings.length > 0 ? [{ title: 'Logs', pathSettings: logSettings }] : []) ] .filter(({ pathSettings }) => pathSettings.filter((s) => !String(s?.key || '').endsWith('_owner')).length > 0 ) .map(({ title, pathSettings }) => { const visibleSettings = pathSettings.filter( (s) => !String(s?.key || '').endsWith('_owner') ); return (
{visibleSettings.map((setting) => { const value = values?.[setting.key]; const error = errors?.[setting.key] || null; const dirty = Boolean(dirtyKeys?.has?.(setting.key)); const ownerKey = `${setting.key}_owner`; const ownerSetting = settingsByKey.get(ownerKey) || null; const ownerValue = values?.[ownerKey]; const ownerError = errors?.[ownerKey] || null; const ownerDirty = Boolean(dirtyKeys?.has?.(ownerKey)); return ( ); })}
); })}
); } export default function DynamicSettingsForm({ categories, values, errors, dirtyKeys, onChange, effectivePaths, activationBytes = [], onNotify, onSettingApplied }) { const safeCategories = Array.isArray(categories) ? categories : []; const expertModeEnabled = toBoolean(values?.[EXPERT_MODE_SETTING_KEY]); const visibleCategories = safeCategories .filter((category) => !shouldHideCategoryByExpertMode(category?.category, expertModeEnabled)) .map((category) => ({ ...category, settings: filterSettingsByVisibility(category?.settings, expertModeEnabled) })) .filter((category) => Array.isArray(category?.settings) && category.settings.length > 0); const [activeIndex, setActiveIndex] = useState(0); const rootRef = useRef(null); useEffect(() => { if (visibleCategories.length === 0) { setActiveIndex(0); return; } if (activeIndex < 0 || activeIndex >= visibleCategories.length) { setActiveIndex(0); } }, [activeIndex, visibleCategories.length]); useEffect(() => { if (typeof window === 'undefined') { return undefined; } const syncToggleHeights = () => { const root = rootRef.current; if (!root) { return; } const grids = root.querySelectorAll('.notification-toggle-grid'); for (const grid of grids) { const cards = Array.from(grid.querySelectorAll('.notification-toggle-box')); if (cards.length === 0) { continue; } for (const card of cards) { card.style.minHeight = '0px'; } const maxHeight = cards.reduce((acc, card) => Math.max(acc, Number(card.offsetHeight || 0)), 0); if (maxHeight <= 0) { continue; } for (const card of cards) { card.style.minHeight = `${maxHeight}px`; } } }; const frameId = window.requestAnimationFrame(syncToggleHeights); window.addEventListener('resize', syncToggleHeights); return () => { window.cancelAnimationFrame(frameId); window.removeEventListener('resize', syncToggleHeights); }; }, [activeIndex, visibleCategories, values]); if (visibleCategories.length === 0) { return

Keine Kategorien vorhanden.

; } return (
setActiveIndex(Number(event.index || 0))} scrollable > {visibleCategories.map((category, categoryIndex) => ( {normalizeText(category?.category) === 'pfade' ? ( ) : (() => { const sections = buildSectionsForCategory(category?.category, category?.settings || []); const grouped = sections.length > 1; const isNotificationCategory = normalizeText(category?.category) === 'benachrichtigungen'; const pushoverEnabled = toBoolean(values?.[PUSHOVER_ENABLED_SETTING_KEY]); const isDriveCategory = normalizeText(category?.category) === 'laufwerk'; const isToolsCategory = normalizeText(category?.category) === 'tools'; return (
{isDriveCategory && } {sections.map((section) => (
{section.title ? (

{section.title}

{section.description ? {section.description} : null}
) : null} {(() => { const ownerKeySet = new Set( (section.settings || []) .filter((s) => String(s.key || '').endsWith('_owner')) .map((s) => s.key) ); const settingsByKey = new Map( (section.settings || []).map((s) => [s.key, s]) ); const baseSettings = (section.settings || []).filter( (s) => !ownerKeySet.has(s.key) ); const notificationToggleSettings = isNotificationCategory ? baseSettings.filter((setting) => isNotificationEventToggleSetting(setting)) : []; const notificationToggleKeys = new Set( notificationToggleSettings.map((setting) => normalizeSettingKey(setting?.key)) ); // Abhängigkeitsbaum aufbauen: depends_on → Kinder const dependentsByParent = new Map(); const rootSettings = []; for (const setting of baseSettings) { if (notificationToggleKeys.has(normalizeSettingKey(setting?.key))) continue; const dep = setting?.depends_on; if (dep) { if (!dependentsByParent.has(dep)) dependentsByParent.set(dep, []); dependentsByParent.get(dep).push(setting); } else { rootSettings.push(setting); } } const renderSetting = (setting, variant = 'default', disabled = false, onChangeFn = onChange) => { const value = values?.[setting.key]; const error = errors?.[setting.key] || null; const dirty = Boolean(dirtyKeys?.has?.(setting.key)); const ownerKey = `${setting.key}_owner`; const ownerSetting = settingsByKey.get(ownerKey) || null; const ownerValue = values?.[ownerKey]; const ownerError = errors?.[ownerKey] || null; const ownerDirty = Boolean(dirtyKeys?.has?.(ownerKey)); return ( ); }; // Alle Nachkommen eines Keys rekursiv auf false setzen const cascadeOff = (key) => { const children = dependentsByParent.get(key) || []; for (const child of children) { onChange?.(child.key, false); cascadeOff(child.key); } }; // Rekursiv: Setting + seine Kinder rendern // Wenn ein Parent auf false geht → alle Kinder werden auf false gesetzt const renderWithChildren = (setting, depth = 0) => { const children = dependentsByParent.get(setting.key) || []; const readonly = depth > 0 && isReadonlySetting(setting); const active = toBoolean(values?.[setting.key]); const effectiveOnChange = children.length > 0 ? (key, value) => { onChange?.(key, value); if (!value) cascadeOff(key); } : onChange; return ( {renderSetting(setting, 'default', readonly, effectiveOnChange)} {children.length > 0 && active && !readonly ? (
{children.map((child) => renderWithChildren(child, depth + 1))}
) : null}
); }; return ( <> {rootSettings.length > 0 ? (
{rootSettings.map((setting) => renderWithChildren(setting, 0))}
) : null} {pushoverEnabled && notificationToggleSettings.length > 0 ? (
{notificationToggleSettings.map((setting) => renderSetting(setting, 'notification-toggle'))}
) : null} ); })()}
))} {isToolsCategory ? : null}
); })()}
))}
); }