0.16.1-5 Ebcode Fix / presets
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user