HandBrake
This commit is contained in:
@@ -266,6 +266,29 @@ export const api = {
|
||||
return request(`/history/${jobId}${suffix}`);
|
||||
},
|
||||
|
||||
// ── User Presets ───────────────────────────────────────────────────────────
|
||||
getUserPresets(mediaType = null) {
|
||||
const suffix = mediaType ? `?media_type=${encodeURIComponent(mediaType)}` : '';
|
||||
return request(`/settings/user-presets${suffix}`);
|
||||
},
|
||||
createUserPreset(payload = {}) {
|
||||
return request('/settings/user-presets', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
},
|
||||
updateUserPreset(id, payload = {}) {
|
||||
return request(`/settings/user-presets/${encodeURIComponent(id)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
},
|
||||
deleteUserPreset(id) {
|
||||
return request(`/settings/user-presets/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
},
|
||||
|
||||
// ── Cron Jobs ──────────────────────────────────────────────────────────────
|
||||
getCronJobs() {
|
||||
return request('/crons');
|
||||
|
||||
@@ -174,7 +174,8 @@ function buildHandBrakeCommandPreview({
|
||||
title,
|
||||
selectedAudioTrackIds,
|
||||
selectedSubtitleTrackIds,
|
||||
commandOutputPath = null
|
||||
commandOutputPath = null,
|
||||
presetOverride = null
|
||||
}) {
|
||||
const inputPath = String(title?.filePath || review?.encodeInputPath || '').trim();
|
||||
const handBrakeCmd = String(
|
||||
@@ -182,8 +183,12 @@ function buildHandBrakeCommandPreview({
|
||||
|| review?.selectors?.handBrakeCommand
|
||||
|| 'HandBrakeCLI'
|
||||
).trim() || 'HandBrakeCLI';
|
||||
const preset = String(review?.selectors?.preset || '').trim();
|
||||
const extraArgs = String(review?.selectors?.extraArgs || '').trim();
|
||||
const preset = presetOverride !== null
|
||||
? String(presetOverride.handbrakePreset || '').trim()
|
||||
: String(review?.selectors?.preset || '').trim();
|
||||
const extraArgs = presetOverride !== null
|
||||
? String(presetOverride.extraArgs || '').trim()
|
||||
: String(review?.selectors?.extraArgs || '').trim();
|
||||
const rawMappedTitleId = Number(review?.handBrakeTitleId);
|
||||
const mappedTitleId = Number.isFinite(rawMappedTitleId) && rawMappedTitleId > 0
|
||||
? Math.trunc(rawMappedTitleId)
|
||||
@@ -697,7 +702,10 @@ export default function MediaInfoReviewPanel({
|
||||
onAddPostEncodeItem = null,
|
||||
onChangePostEncodeItem = null,
|
||||
onRemovePostEncodeItem = null,
|
||||
onReorderPostEncodeItem = null
|
||||
onReorderPostEncodeItem = null,
|
||||
userPresets = [],
|
||||
selectedUserPresetId = null,
|
||||
onUserPresetChange = null
|
||||
}) {
|
||||
if (!review) {
|
||||
return <p>Keine Mediainfo-Daten vorhanden.</p>;
|
||||
@@ -711,6 +719,18 @@ export default function MediaInfoReviewPanel({
|
||||
const playlistRecommendation = review.playlistRecommendation || null;
|
||||
const rawPreset = String(review.selectors?.preset || '').trim();
|
||||
const presetLabel = String(presetDisplayValue || rawPreset).trim() || '(kein Preset)';
|
||||
|
||||
// User preset resolution
|
||||
const normalizedUserPresets = Array.isArray(userPresets) ? userPresets : [];
|
||||
const selectedUserPreset = selectedUserPresetId
|
||||
? normalizedUserPresets.find((p) => Number(p.id) === Number(selectedUserPresetId)) || null
|
||||
: null;
|
||||
const effectivePresetOverride = selectedUserPreset
|
||||
? { handbrakePreset: selectedUserPreset.handbrakePreset || '', extraArgs: selectedUserPreset.extraArgs || '' }
|
||||
: null;
|
||||
const hasUserPresets = normalizedUserPresets.length > 0;
|
||||
const allowUserPresetSelection = hasUserPresets && typeof onUserPresetChange === 'function' && allowEncodeItemSelection;
|
||||
|
||||
const scriptCatalog = (Array.isArray(availableScripts) ? availableScripts : [])
|
||||
.map((item) => ({
|
||||
id: normalizeScriptId(item?.id),
|
||||
@@ -740,10 +760,56 @@ export default function MediaInfoReviewPanel({
|
||||
|
||||
return (
|
||||
<div className="media-review-wrap">
|
||||
{allowUserPresetSelection && (
|
||||
<div className="user-preset-selector" style={{ marginBottom: '0.75rem', padding: '0.75rem', border: '1px solid var(--surface-border, #e0e0e0)', borderRadius: '6px', background: 'var(--surface-ground, #f8f8f8)' }}>
|
||||
<label style={{ display: 'block', marginBottom: '0.4rem', fontWeight: 600 }}>
|
||||
Encode-Preset auswählen
|
||||
</label>
|
||||
<Dropdown
|
||||
value={selectedUserPresetId ? Number(selectedUserPresetId) : null}
|
||||
options={[
|
||||
{ label: '(Einstellungen-Fallback)', value: null },
|
||||
...normalizedUserPresets.map((p) => ({
|
||||
label: `${p.name}${p.mediaType !== 'all' ? ` [${p.mediaType === 'bluray' ? 'Blu-ray' : p.mediaType === 'dvd' ? 'DVD' : 'Sonstiges'}]` : ''}`,
|
||||
value: Number(p.id)
|
||||
}))
|
||||
]}
|
||||
onChange={(e) => onUserPresetChange(e.value)}
|
||||
placeholder="(Einstellungen-Fallback)"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
{selectedUserPreset && (
|
||||
<div style={{ marginTop: '0.4rem', fontSize: '0.8rem', opacity: 0.8 }}>
|
||||
{selectedUserPreset.handbrakePreset
|
||||
? <span><strong>-Z</strong> {selectedUserPreset.handbrakePreset}</span>
|
||||
: <span style={{ opacity: 0.6 }}>(kein Preset-Name)</span>}
|
||||
{selectedUserPreset.extraArgs && (
|
||||
<span style={{ marginLeft: '1rem' }}><strong>Args:</strong> {selectedUserPreset.extraArgs}</span>
|
||||
)}
|
||||
{selectedUserPreset.description && (
|
||||
<span style={{ marginLeft: '1rem', opacity: 0.7 }}>{selectedUserPreset.description}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="media-review-meta">
|
||||
<div><strong>Preset:</strong> {presetLabel}</div>
|
||||
<div><strong>Extra Args:</strong> {review.selectors?.extraArgs || '(keine)'}</div>
|
||||
<div><strong>Preset-Profil:</strong> {review.selectors?.presetProfileSource || '-'}</div>
|
||||
<div>
|
||||
<strong>Preset:</strong>{' '}
|
||||
{effectivePresetOverride
|
||||
? (effectivePresetOverride.handbrakePreset || '(kein Preset)')
|
||||
: presetLabel}
|
||||
{effectivePresetOverride && <span style={{ marginLeft: '0.5rem', fontSize: '0.75rem', opacity: 0.7 }}>(User-Preset: {selectedUserPreset?.name})</span>}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Extra Args:</strong>{' '}
|
||||
{effectivePresetOverride
|
||||
? (effectivePresetOverride.extraArgs || '(keine)')
|
||||
: (review.selectors?.extraArgs || '(keine)')}
|
||||
{effectivePresetOverride && !selectedUserPreset?.extraArgs && <span style={{ marginLeft: '0.5rem', fontSize: '0.75rem', opacity: 0.7 }}>(aus User-Preset)</span>}
|
||||
</div>
|
||||
<div><strong>Preset-Profil:</strong> {effectivePresetOverride ? 'user-preset' : (review.selectors?.presetProfileSource || '-')}</div>
|
||||
<div><strong>MIN_LENGTH_MINUTES:</strong> {review.minLengthMinutes}</div>
|
||||
<div><strong>Encode Input:</strong> {encodeInputTitle?.fileName || '-'}</div>
|
||||
<div><strong>Audio Auswahl:</strong> {review.selectors?.audio?.mode || '-'}</div>
|
||||
@@ -1090,7 +1156,8 @@ export default function MediaInfoReviewPanel({
|
||||
title,
|
||||
selectedAudioTrackIds,
|
||||
selectedSubtitleTrackIds,
|
||||
commandOutputPath
|
||||
commandOutputPath,
|
||||
presetOverride: effectivePresetOverride
|
||||
});
|
||||
return (
|
||||
<div className="handbrake-command-preview">
|
||||
|
||||
@@ -226,6 +226,7 @@ export default function PipelineStatusCard({
|
||||
const reviewConfirmed = Boolean(pipeline?.context?.reviewConfirmed || mediaInfoReview?.reviewConfirmed);
|
||||
const reviewMode = String(mediaInfoReview?.mode || '').trim().toLowerCase();
|
||||
const isPreRipReview = reviewMode === 'pre_rip' || Boolean(mediaInfoReview?.preRip);
|
||||
const jobMediaProfile = String(pipeline?.context?.mediaProfile || '').trim().toLowerCase() || null;
|
||||
const [selectedEncodeTitleId, setSelectedEncodeTitleId] = useState(null);
|
||||
const [selectedPlaylistId, setSelectedPlaylistId] = useState(null);
|
||||
const [trackSelectionByTitle, setTrackSelectionByTitle] = useState({});
|
||||
@@ -236,16 +237,19 @@ export default function PipelineStatusCard({
|
||||
// Unified ordered lists: [{type: 'script'|'chain', id: number}]
|
||||
const [preEncodeItems, setPreEncodeItems] = useState([]);
|
||||
const [postEncodeItems, setPostEncodeItems] = useState([]);
|
||||
const [userPresets, setUserPresets] = useState([]);
|
||||
const [selectedUserPresetId, setSelectedUserPresetId] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
const [settingsResponse, presetsResponse, scriptsResponse, chainsResponse] = await Promise.allSettled([
|
||||
const [settingsResponse, presetsResponse, scriptsResponse, chainsResponse, userPresetsResponse] = await Promise.allSettled([
|
||||
api.getSettings(),
|
||||
api.getHandBrakePresets(),
|
||||
api.getScripts(),
|
||||
api.getScriptChains()
|
||||
api.getScriptChains(),
|
||||
api.getUserPresets()
|
||||
]);
|
||||
if (!cancelled) {
|
||||
const categories = settingsResponse.status === 'fulfilled'
|
||||
@@ -269,6 +273,10 @@ export default function PipelineStatusCard({
|
||||
? (Array.isArray(chainsResponse.value?.chains) ? chainsResponse.value.chains : [])
|
||||
: [];
|
||||
setChainCatalog(chains.map((item) => ({ id: item?.id, name: item?.name })));
|
||||
const allUserPresets = userPresetsResponse.status === 'fulfilled'
|
||||
? (Array.isArray(userPresetsResponse.value?.presets) ? userPresetsResponse.value.presets : [])
|
||||
: [];
|
||||
setUserPresets(allUserPresets);
|
||||
}
|
||||
} catch (_error) {
|
||||
if (!cancelled) {
|
||||
@@ -276,6 +284,7 @@ export default function PipelineStatusCard({
|
||||
setPresetDisplayMap({});
|
||||
setScriptCatalog([]);
|
||||
setChainCatalog([]);
|
||||
setUserPresets([]);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -298,6 +307,7 @@ export default function PipelineStatusCard({
|
||||
...normalizeScriptIdList(mediaInfoReview?.postEncodeScriptIds || []).map((id) => ({ type: 'script', id })),
|
||||
...normChain(mediaInfoReview?.postEncodeChainIds).map((id) => ({ type: 'chain', id }))
|
||||
]);
|
||||
setSelectedUserPresetId(null);
|
||||
}, [mediaInfoReview?.encodeInputTitleId, mediaInfoReview?.generatedAt, retryJobId]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -323,6 +333,14 @@ export default function PipelineStatusCard({
|
||||
const reviewPlaylistDecisionRequired = Boolean(mediaInfoReview?.playlistDecisionRequired);
|
||||
const hasSelectedEncodeTitle = Boolean(normalizeTitleId(selectedEncodeTitleId));
|
||||
const canConfirmReview = !reviewPlaylistDecisionRequired || hasSelectedEncodeTitle;
|
||||
|
||||
// Filter user presets by job media profile ('all' presets always shown)
|
||||
const filteredUserPresets = (Array.isArray(userPresets) ? userPresets : []).filter((p) => {
|
||||
if (!jobMediaProfile) {
|
||||
return true;
|
||||
}
|
||||
return p.mediaType === 'all' || p.mediaType === jobMediaProfile;
|
||||
});
|
||||
const canStartReadyJob = isPreRipReview
|
||||
? Boolean(retryJobId)
|
||||
: Boolean(retryJobId && encodeInputPath);
|
||||
@@ -575,7 +593,8 @@ export default function PipelineStatusCard({
|
||||
selectedPostEncodeScriptIds: selectedPostScriptIds,
|
||||
selectedPreEncodeScriptIds: selectedPreScriptIds,
|
||||
selectedPostEncodeChainIds: selectedPostChainIds,
|
||||
selectedPreEncodeChainIds: selectedPreChainIds
|
||||
selectedPreEncodeChainIds: selectedPreChainIds,
|
||||
selectedUserPresetId: selectedUserPresetId || null
|
||||
});
|
||||
}}
|
||||
loading={busy}
|
||||
@@ -786,6 +805,9 @@ export default function PipelineStatusCard({
|
||||
availableChains={chainCatalog}
|
||||
preEncodeItems={preEncodeItems}
|
||||
postEncodeItems={postEncodeItems}
|
||||
userPresets={filteredUserPresets}
|
||||
selectedUserPresetId={selectedUserPresetId}
|
||||
onUserPresetChange={(presetId) => setSelectedUserPresetId(presetId)}
|
||||
allowEncodeItemSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked}
|
||||
onAddPreEncodeItem={(itemType) => {
|
||||
setPreEncodeItems((prev) => {
|
||||
|
||||
@@ -792,6 +792,7 @@ export default function DashboardPage({
|
||||
selectedPreEncodeScriptIds: startOptions.selectedPreEncodeScriptIds ?? [],
|
||||
selectedPostEncodeChainIds: startOptions.selectedPostEncodeChainIds ?? [],
|
||||
selectedPreEncodeChainIds: startOptions.selectedPreEncodeChainIds ?? [],
|
||||
selectedUserPresetId: startOptions.selectedUserPresetId ?? null,
|
||||
skipPipelineStateUpdate: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,6 +156,21 @@ export default function SettingsPage() {
|
||||
const [chainEditorErrors, setChainEditorErrors] = useState({});
|
||||
const [chainDragSource, setChainDragSource] = useState(null);
|
||||
|
||||
// User presets state
|
||||
const [userPresets, setUserPresets] = useState([]);
|
||||
const [userPresetsLoading, setUserPresetsLoading] = useState(false);
|
||||
const [userPresetSaving, setUserPresetSaving] = useState(false);
|
||||
const [userPresetEditor, setUserPresetEditor] = useState({
|
||||
open: false,
|
||||
id: null,
|
||||
name: '',
|
||||
mediaType: 'all',
|
||||
handbrakePreset: '',
|
||||
extraArgs: '',
|
||||
description: ''
|
||||
});
|
||||
const [userPresetErrors, setUserPresetErrors] = useState({});
|
||||
|
||||
const toastRef = useRef(null);
|
||||
|
||||
const loadScripts = async ({ silent = false } = {}) => {
|
||||
@@ -195,6 +210,91 @@ export default function SettingsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadUserPresets = async ({ silent = false } = {}) => {
|
||||
if (!silent) {
|
||||
setUserPresetsLoading(true);
|
||||
}
|
||||
try {
|
||||
const response = await api.getUserPresets();
|
||||
setUserPresets(Array.isArray(response?.presets) ? response.presets : []);
|
||||
} catch (error) {
|
||||
if (!silent) {
|
||||
toastRef.current?.show({ severity: 'error', summary: 'User-Presets', detail: error.message });
|
||||
}
|
||||
} finally {
|
||||
if (!silent) {
|
||||
setUserPresetsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const openNewUserPreset = () => {
|
||||
setUserPresetEditor({ open: true, id: null, name: '', mediaType: 'all', handbrakePreset: '', extraArgs: '', description: '' });
|
||||
setUserPresetErrors({});
|
||||
};
|
||||
|
||||
const openEditUserPreset = (preset) => {
|
||||
setUserPresetEditor({
|
||||
open: true,
|
||||
id: preset.id,
|
||||
name: preset.name || '',
|
||||
mediaType: preset.mediaType || 'all',
|
||||
handbrakePreset: preset.handbrakePreset || '',
|
||||
extraArgs: preset.extraArgs || '',
|
||||
description: preset.description || ''
|
||||
});
|
||||
setUserPresetErrors({});
|
||||
};
|
||||
|
||||
const closeUserPresetEditor = () => {
|
||||
setUserPresetEditor((prev) => ({ ...prev, open: false }));
|
||||
setUserPresetErrors({});
|
||||
};
|
||||
|
||||
const handleSaveUserPreset = async () => {
|
||||
const errors = {};
|
||||
if (!userPresetEditor.name.trim()) {
|
||||
errors.name = 'Name ist erforderlich.';
|
||||
}
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setUserPresetErrors(errors);
|
||||
return;
|
||||
}
|
||||
setUserPresetSaving(true);
|
||||
try {
|
||||
const payload = {
|
||||
name: userPresetEditor.name.trim(),
|
||||
mediaType: userPresetEditor.mediaType,
|
||||
handbrakePreset: userPresetEditor.handbrakePreset.trim(),
|
||||
extraArgs: userPresetEditor.extraArgs.trim(),
|
||||
description: userPresetEditor.description.trim()
|
||||
};
|
||||
if (userPresetEditor.id) {
|
||||
await api.updateUserPreset(userPresetEditor.id, payload);
|
||||
toastRef.current?.show({ severity: 'success', summary: 'Preset', detail: 'Preset aktualisiert.' });
|
||||
} else {
|
||||
await api.createUserPreset(payload);
|
||||
toastRef.current?.show({ severity: 'success', summary: 'Preset', detail: 'Preset erstellt.' });
|
||||
}
|
||||
closeUserPresetEditor();
|
||||
await loadUserPresets({ silent: true });
|
||||
} catch (error) {
|
||||
toastRef.current?.show({ severity: 'error', summary: 'Preset speichern', detail: error.message });
|
||||
} finally {
|
||||
setUserPresetSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteUserPreset = async (presetId) => {
|
||||
try {
|
||||
await api.deleteUserPreset(presetId);
|
||||
toastRef.current?.show({ severity: 'success', summary: 'Preset', detail: 'Preset gelöscht.' });
|
||||
await loadUserPresets({ silent: true });
|
||||
} catch (error) {
|
||||
toastRef.current?.show({ severity: 'error', summary: 'Preset löschen', detail: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -250,6 +350,7 @@ export default function SettingsPage() {
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
loadUserPresets();
|
||||
}, []);
|
||||
|
||||
const dirtyKeys = useMemo(() => {
|
||||
@@ -1379,6 +1480,173 @@ export default function SettingsPage() {
|
||||
</Dialog>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Encode-Presets">
|
||||
<div className="actions-row">
|
||||
<Button
|
||||
label="Neues Preset"
|
||||
icon="pi pi-plus"
|
||||
onClick={openNewUserPreset}
|
||||
severity="success"
|
||||
outlined
|
||||
disabled={userPresetSaving}
|
||||
/>
|
||||
<Button
|
||||
label="Presets neu laden"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
onClick={() => loadUserPresets()}
|
||||
loading={userPresetsLoading}
|
||||
disabled={userPresetSaving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<small>
|
||||
Encode-Presets fassen ein HandBrake-Preset und zusätzliche CLI-Argumente zusammen.
|
||||
Sie sind medienbezogen (Blu-ray, DVD, Sonstiges oder Universell) und können vor dem Encode
|
||||
in der Mediainfo-Prüfung ausgewählt werden. Kein Preset gewählt = Fallback aus Einstellungen.
|
||||
</small>
|
||||
|
||||
{userPresetsLoading ? (
|
||||
<p style={{ marginTop: '1rem' }}>Lade Presets ...</p>
|
||||
) : userPresets.length === 0 ? (
|
||||
<p style={{ marginTop: '1rem' }}>Keine Presets vorhanden. Lege ein neues Preset an.</p>
|
||||
) : (
|
||||
<div className="script-list" style={{ marginTop: '1rem' }}>
|
||||
{userPresets.map((preset) => (
|
||||
<div key={preset.id} className="script-list-item">
|
||||
<div className="script-list-main">
|
||||
<div className="script-title-line">
|
||||
<strong>#{preset.id} – {preset.name}</strong>
|
||||
<span style={{ marginLeft: '0.5rem', fontSize: '0.8rem', opacity: 0.7 }}>
|
||||
{preset.mediaType === 'bluray' ? 'Blu-ray'
|
||||
: preset.mediaType === 'dvd' ? 'DVD'
|
||||
: preset.mediaType === 'other' ? 'Sonstiges'
|
||||
: 'Universell'}
|
||||
</span>
|
||||
</div>
|
||||
{preset.description && <small style={{ display: 'block', marginTop: '0.2rem', opacity: 0.8 }}>{preset.description}</small>}
|
||||
<div style={{ marginTop: '0.3rem', fontFamily: 'monospace', fontSize: '0.8rem' }}>
|
||||
{preset.handbrakePreset
|
||||
? <span><span style={{ opacity: 0.6 }}>Preset:</span> {preset.handbrakePreset}</span>
|
||||
: <span style={{ opacity: 0.5 }}>(kein Preset-Name)</span>}
|
||||
{preset.extraArgs && (
|
||||
<span style={{ marginLeft: '1rem' }}><span style={{ opacity: 0.6 }}>Args:</span> {preset.extraArgs}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="script-list-actions">
|
||||
<Button
|
||||
icon="pi pi-pencil"
|
||||
severity="secondary"
|
||||
outlined
|
||||
rounded
|
||||
title="Bearbeiten"
|
||||
onClick={() => openEditUserPreset(preset)}
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
outlined
|
||||
rounded
|
||||
title="Löschen"
|
||||
onClick={() => handleDeleteUserPreset(preset.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Dialog
|
||||
header={userPresetEditor.id ? 'Preset bearbeiten' : 'Neues Preset'}
|
||||
visible={userPresetEditor.open}
|
||||
style={{ width: '520px' }}
|
||||
onHide={closeUserPresetEditor}
|
||||
modal
|
||||
>
|
||||
<div className="script-editor-fields" style={{ display: 'flex', flexDirection: 'column', gap: '0.8rem' }}>
|
||||
<div>
|
||||
<label htmlFor="preset-name" style={{ display: 'block', marginBottom: '0.3rem' }}>Name *</label>
|
||||
<InputText
|
||||
id="preset-name"
|
||||
value={userPresetEditor.name}
|
||||
onChange={(e) => setUserPresetEditor((prev) => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="z.B. Blu-ray HQ"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
{userPresetErrors.name && <small className="error-text">{userPresetErrors.name}</small>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="preset-media-type" style={{ display: 'block', marginBottom: '0.3rem' }}>Medientyp</label>
|
||||
<select
|
||||
id="preset-media-type"
|
||||
value={userPresetEditor.mediaType}
|
||||
onChange={(e) => setUserPresetEditor((prev) => ({ ...prev, mediaType: e.target.value }))}
|
||||
style={{ width: '100%', padding: '0.5rem', borderRadius: '4px', border: '1px solid var(--surface-border, #ccc)', background: 'var(--surface-overlay, #fff)', color: 'var(--text-color, #000)' }}
|
||||
>
|
||||
<option value="all">Universell (alle Medien)</option>
|
||||
<option value="bluray">Blu-ray</option>
|
||||
<option value="dvd">DVD</option>
|
||||
<option value="other">Sonstiges</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="preset-hb-preset" style={{ display: 'block', marginBottom: '0.3rem' }}>HandBrake Preset (-Z)</label>
|
||||
<InputText
|
||||
id="preset-hb-preset"
|
||||
value={userPresetEditor.handbrakePreset}
|
||||
onChange={(e) => setUserPresetEditor((prev) => ({ ...prev, handbrakePreset: e.target.value }))}
|
||||
placeholder="z.B. H.264 MKV 1080p30 (leer = kein Preset)"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="preset-extra-args" style={{ display: 'block', marginBottom: '0.3rem' }}>Extra Args</label>
|
||||
<InputText
|
||||
id="preset-extra-args"
|
||||
value={userPresetEditor.extraArgs}
|
||||
onChange={(e) => setUserPresetEditor((prev) => ({ ...prev, extraArgs: e.target.value }))}
|
||||
placeholder="z.B. -q 22 --encoder x264"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="preset-description" style={{ display: 'block', marginBottom: '0.3rem' }}>Beschreibung (optional)</label>
|
||||
<InputTextarea
|
||||
id="preset-description"
|
||||
value={userPresetEditor.description}
|
||||
onChange={(e) => setUserPresetEditor((prev) => ({ ...prev, description: e.target.value }))}
|
||||
rows={3}
|
||||
autoResize
|
||||
placeholder="Kurzbeschreibung für dieses Preset"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="actions-row" style={{ marginTop: '0.5rem' }}>
|
||||
<Button
|
||||
label={userPresetEditor.id ? 'Aktualisieren' : 'Erstellen'}
|
||||
icon="pi pi-save"
|
||||
onClick={handleSaveUserPreset}
|
||||
loading={userPresetSaving}
|
||||
/>
|
||||
<Button
|
||||
label="Abbrechen"
|
||||
icon="pi pi-times"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={closeUserPresetEditor}
|
||||
disabled={userPresetSaving}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Cronjobs">
|
||||
<CronJobsTab />
|
||||
</TabPanel>
|
||||
|
||||
Reference in New Issue
Block a user