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',
'playlist_tmdb_runtime_subtract_percent',
'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',
'handbrake_review_audio_languages_bluray',
'handbrake_review_subtitle_languages_bluray',
'handbrake_review_audio_languages_dvd',
'handbrake_review_subtitle_languages_dvd'
]);
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 ? (
) : null}
{drives === null || loading
?
Wird geladen…
: drives.length === 0
?
Keine optischen Laufwerke erkannt.
: (
| Gerät |
Modell |
MakeMKV Index |
{expertModeEnabled ? Freigabe | : null}
{drives.map((d) => (
{d.path} |
{d.model || '—'} |
|
{expertModeEnabled ? (
|
) : null}
))}
)}
);
}
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.
) : (
| Checksum |
Activation Bytes |
Gespeichert am |
{list.map((entry, index) => (
| {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}
);
})()}
))}
);
}