0.16.1-5 Ebcode Fix / presets
Deploy Docs to GitHub Pages / Build Documentation (push) Has been cancelled
Deploy Docs to GitHub Pages / Deploy to GitHub Pages (push) Has been cancelled

This commit is contained in:
2026-04-30 12:37:32 +00:00
parent f9cf3cef09
commit 6fbc5395a1
24 changed files with 1131 additions and 784 deletions
+210 -4
View File
@@ -14,6 +14,16 @@ import CronJobsTab from '../components/CronJobsTab';
import { confirmModal } from '../utils/confirmModal';
const EXPERT_MODE_SETTING_KEY = 'ui_expert_mode';
const USER_PRESET_DEFAULT_SLOTS = Object.freeze([
{ key: 'bluray_movie', label: 'Standard-Userpreset für BluRay Film', mediaType: 'bluray' },
{ key: 'bluray_series', label: 'Standard-Userpreset für BluRay Serie', mediaType: 'bluray' },
{ key: 'dvd_movie', label: 'Standard-Userpreset für DVD Film', mediaType: 'dvd' },
{ key: 'dvd_series', label: 'Standard-Userpreset für DVD Serie', mediaType: 'dvd' }
]);
const EMPTY_USER_PRESET_DEFAULTS = Object.freeze(
USER_PRESET_DEFAULT_SLOTS.reduce((acc, slot) => ({ ...acc, [slot.key]: null }), {})
);
// Validates template/string fields: only letters, digits, space, _, -, and {placeholders} allowed
function validateSettingValue(key, value) {
@@ -45,6 +55,68 @@ function buildValuesMap(categories) {
return next;
}
function normalizePositiveId(value) {
if (value === null || value === undefined) {
return null;
}
if (typeof value === 'string' && value.trim() === '') {
return null;
}
const numeric = Number(value);
if (!Number.isFinite(numeric) || numeric <= 0) {
return null;
}
return Math.trunc(numeric);
}
function normalizeUserPresetDefaultsMap(raw = {}) {
const source = raw && typeof raw === 'object' ? raw : {};
const normalized = { ...EMPTY_USER_PRESET_DEFAULTS };
for (const slot of USER_PRESET_DEFAULT_SLOTS) {
normalized[slot.key] = normalizePositiveId(source[slot.key]);
}
return normalized;
}
function isPresetCompatibleWithSlot(preset, slotMediaType) {
const mediaType = String(preset?.mediaType || '').trim().toLowerCase();
if (mediaType === 'all') {
return true;
}
return mediaType === slotMediaType;
}
function buildUserPresetDefaultOptions(userPresets, slotMediaType, selectedPresetId = null) {
const presets = Array.isArray(userPresets) ? userPresets : [];
const options = [{ label: '(keine Standardzuordnung)', value: null }];
const seen = new Set([null]);
for (const preset of presets) {
const presetId = normalizePositiveId(preset?.id);
if (!presetId || !isPresetCompatibleWithSlot(preset, slotMediaType)) {
continue;
}
seen.add(presetId);
options.push({
label: `#${presetId} - ${preset?.name || 'Preset'}`,
value: presetId
});
}
const normalizedSelectedId = normalizePositiveId(selectedPresetId);
if (normalizedSelectedId && !seen.has(normalizedSelectedId)) {
const existingPreset = presets.find((preset) => normalizePositiveId(preset?.id) === normalizedSelectedId);
options.push({
label: existingPreset
? `#${normalizedSelectedId} - ${existingPreset?.name || 'Preset'} (nicht kompatibel)`
: `#${normalizedSelectedId} - (nicht gefunden)`,
value: normalizedSelectedId
});
}
return options;
}
function isSameValue(a, b) {
if (typeof a === 'number' && typeof b === 'number') {
return Number(a) === Number(b);
@@ -213,6 +285,10 @@ export default function SettingsPage() {
const [userPresets, setUserPresets] = useState([]);
const [userPresetsLoading, setUserPresetsLoading] = useState(false);
const [userPresetSaving, setUserPresetSaving] = useState(false);
const [userPresetDefaults, setUserPresetDefaults] = useState(EMPTY_USER_PRESET_DEFAULTS);
const [userPresetDefaultsDraft, setUserPresetDefaultsDraft] = useState(EMPTY_USER_PRESET_DEFAULTS);
const [userPresetDefaultsLoading, setUserPresetDefaultsLoading] = useState(false);
const [userPresetDefaultsSaving, setUserPresetDefaultsSaving] = useState(false);
const [userPresetEditor, setUserPresetEditor] = useState({
open: false,
id: null,
@@ -235,6 +311,23 @@ export default function SettingsPage() {
),
[handBrakePresetSourceOptions, userPresetEditor.handbrakePreset]
);
const userPresetDefaultsDirty = useMemo(
() => USER_PRESET_DEFAULT_SLOTS.some((slot) => (
normalizePositiveId(userPresetDefaultsDraft?.[slot.key]) !== normalizePositiveId(userPresetDefaults?.[slot.key])
)),
[userPresetDefaultsDraft, userPresetDefaults]
);
const userPresetDefaultOptionsBySlot = useMemo(() => {
const next = {};
for (const slot of USER_PRESET_DEFAULT_SLOTS) {
next[slot.key] = buildUserPresetDefaultOptions(
userPresets,
slot.mediaType,
userPresetDefaultsDraft?.[slot.key]
);
}
return next;
}, [userPresets, userPresetDefaultsDraft]);
const loadScripts = async ({ silent = false } = {}) => {
if (!silent) {
@@ -291,6 +384,26 @@ export default function SettingsPage() {
}
};
const loadUserPresetDefaults = async ({ silent = false } = {}) => {
if (!silent) {
setUserPresetDefaultsLoading(true);
}
try {
const response = await api.getUserPresetDefaults();
const normalized = normalizeUserPresetDefaultsMap(response?.defaults || {});
setUserPresetDefaults(normalized);
setUserPresetDefaultsDraft(normalized);
} catch (error) {
if (!silent) {
toastRef.current?.show({ severity: 'error', summary: 'Preset-Standards', detail: error.message });
}
} finally {
if (!silent) {
setUserPresetDefaultsLoading(false);
}
}
};
const openNewUserPreset = () => {
setUserPresetEditor({ open: true, id: null, name: '', mediaType: 'all', handbrakePreset: '', extraArgs: '', description: '' });
setUserPresetErrors({});
@@ -352,12 +465,59 @@ export default function SettingsPage() {
try {
await api.deleteUserPreset(presetId);
toastRef.current?.show({ severity: 'success', summary: 'Preset', detail: 'Preset gelöscht.' });
await loadUserPresets({ silent: true });
await Promise.all([
loadUserPresets({ silent: true }),
loadUserPresetDefaults({ silent: true })
]);
} catch (error) {
toastRef.current?.show({ severity: 'error', summary: 'Preset löschen', detail: error.message });
}
};
const handleChangeUserPresetDefault = (slotKey, rawValue) => {
const normalized = normalizePositiveId(rawValue);
setUserPresetDefaultsDraft((prev) => ({
...prev,
[slotKey]: normalized
}));
};
const handleResetUserPresetDefaults = () => {
setUserPresetDefaultsDraft({ ...userPresetDefaults });
};
const handleSaveUserPresetDefaults = async () => {
if (!userPresetDefaultsDirty) {
toastRef.current?.show({
severity: 'info',
summary: 'Preset-Standards',
detail: 'Keine Änderungen zum Speichern.'
});
return;
}
setUserPresetDefaultsSaving(true);
try {
const payload = normalizeUserPresetDefaultsMap(userPresetDefaultsDraft);
const response = await api.updateUserPresetDefaults(payload);
const normalized = normalizeUserPresetDefaultsMap(response?.defaults || payload);
setUserPresetDefaults(normalized);
setUserPresetDefaultsDraft(normalized);
toastRef.current?.show({
severity: 'success',
summary: 'Preset-Standards',
detail: 'Standard-Zuordnungen gespeichert.'
});
} catch (error) {
toastRef.current?.show({
severity: 'error',
summary: 'Preset-Standards',
detail: error.message
});
} finally {
setUserPresetDefaultsSaving(false);
}
};
const loadEffectivePaths = async ({ silent = false } = {}) => {
try {
const paths = await api.getEffectivePaths({ forceRefresh: true });
@@ -430,6 +590,7 @@ export default function SettingsPage() {
useEffect(() => {
load();
loadUserPresets();
loadUserPresetDefaults();
}, []);
const dirtyKeys = useMemo(() => {
@@ -1670,12 +1831,57 @@ export default function SettingsPage() {
label="Presets neu laden"
icon="pi pi-refresh"
severity="secondary"
onClick={() => loadUserPresets()}
loading={userPresetsLoading}
disabled={userPresetSaving}
onClick={() => Promise.all([loadUserPresets(), loadUserPresetDefaults()])}
loading={userPresetsLoading || userPresetDefaultsLoading}
disabled={userPresetSaving || userPresetDefaultsSaving}
/>
</div>
<div className="script-list-box" style={{ marginTop: '1rem' }}>
<h4>Standard-Zuordnungen</h4>
<small>
Weise für jeden Medientyp/Fall ein Standard-Userpreset zu. Diese Zuordnungen werden persistent gespeichert.
</small>
<div className="user-preset-default-grid" style={{ marginTop: '0.8rem' }}>
{USER_PRESET_DEFAULT_SLOTS.map((slot) => (
<div key={slot.key} className="user-preset-default-grid-item">
<label htmlFor={`user-preset-default-${slot.key}`} style={{ display: 'block', marginBottom: '0.3rem' }}>
{slot.label}
</label>
<Dropdown
id={`user-preset-default-${slot.key}`}
value={normalizePositiveId(userPresetDefaultsDraft?.[slot.key])}
options={userPresetDefaultOptionsBySlot?.[slot.key] || []}
optionLabel="label"
optionValue="value"
onChange={(event) => handleChangeUserPresetDefault(slot.key, event.value)}
placeholder="Preset auswählen"
showClear
disabled={userPresetDefaultsLoading || userPresetDefaultsSaving}
style={{ width: '100%' }}
/>
</div>
))}
</div>
<div className="actions-row" style={{ marginTop: '0.8rem' }}>
<Button
label="Zuordnungen speichern"
icon="pi pi-save"
onClick={handleSaveUserPresetDefaults}
loading={userPresetDefaultsSaving}
disabled={userPresetDefaultsLoading || !userPresetDefaultsDirty}
/>
<Button
label="Änderungen verwerfen"
icon="pi pi-undo"
severity="secondary"
outlined
onClick={handleResetUserPresetDefaults}
disabled={userPresetDefaultsLoading || userPresetDefaultsSaving || !userPresetDefaultsDirty}
/>
</div>
</div>
<small>
Encode-Presets fassen ein HandBrake-Preset und zusätzliche CLI-Argumente zusammen.
Sie sind medienbezogen (Blu-ray, DVD oder Universell) und können vor dem Encode