0.10.2-19 Frontend

This commit is contained in:
2026-03-17 11:58:04 +00:00
parent 7796587f16
commit 46bd235afc
13 changed files with 227 additions and 100 deletions
+9
View File
@@ -415,6 +415,15 @@ export const api = {
method: 'POST'
});
},
async getPref(key) {
return request(`/settings/prefs/${encodeURIComponent(key)}`);
},
async setPref(key, value) {
return request(`/settings/prefs/${encodeURIComponent(key)}`, {
method: 'PUT',
body: JSON.stringify({ value })
});
},
async updateSetting(key, value) {
const result = await request(`/settings/${encodeURIComponent(key)}`, {
method: 'PUT',
+102 -89
View File
@@ -878,8 +878,8 @@ export default function JobDetailDialog({
) : null}
<h4>Aktionen</h4>
<div className="actions-row">
{queueLocked ? (
{queueLocked ? (
<div className="actions-row">
<Button
label="Aus Queue löschen"
icon="pi pi-times"
@@ -890,103 +890,116 @@ export default function JobDetailDialog({
loading={actionBusy}
disabled={typeof onRemoveFromQueue !== 'function'}
/>
) : (
<>
{!isCd && !isAudiobook ? (
</div>
) : (
<div className="actions-section">
{!isCd ? (
<div className="actions-group">
<div className="actions-group-label"><i className="pi pi-play-circle" /> Kodierung</div>
{canResumeReady ? (
<div className="action-item">
<Button
label="Im Dashboard öffnen"
icon="pi pi-window-maximize"
severity="info"
outlined
size="small"
onClick={() => onResumeReady?.(job)}
loading={actionBusy}
/>
<span className="action-desc">Öffnet den wartenden Job im Dashboard zur Weiterverarbeitung.</span>
</div>
) : null}
{typeof onRestartEncode === 'function' ? (
<div className="action-item">
<Button
label="Encode neu starten"
icon="pi pi-play"
severity="success"
size="small"
onClick={() => onRestartEncode?.(job)}
loading={actionBusy}
disabled={!canRestartEncode}
/>
<span className="action-desc">Startet {isAudiobook ? 'FFmpeg' : 'HandBrake'} mit den zuletzt gespeicherten Einstellungen direkt neu ohne Review.</span>
</div>
) : null}
{!isAudiobook && typeof onRestartReview === 'function' ? (
<div className="action-item">
<Button
label="Review neu starten"
icon="pi pi-refresh"
severity="info"
outlined
size="small"
onClick={() => onRestartReview?.(job)}
loading={actionBusy}
disabled={!canRestartReview}
/>
<span className="action-desc">Zeigt den Review-Screen erneut (Spur- und Preset-Auswahl), um Einstellungen vor dem Encode zu ändern.</span>
</div>
) : null}
{!isAudiobook ? (
<div className="action-item">
<Button
label="RAW neu encodieren"
icon="pi pi-cog"
severity="info"
size="small"
onClick={() => onReencode?.(job)}
loading={reencodeBusy}
disabled={!canReencode || typeof onReencode !== 'function'}
/>
<span className="action-desc">Analysiert die Quelldatei neu (MediaInfo, Playlist-Check) und startet dann den Review. Unterschied zu Review neu starten": der Analyse-Schritt wird komplett wiederholt — sinnvoll wenn sich der RAW-Inhalt geändert hat.</span>
</div>
) : null}
</div>
) : null}
<div className="actions-group">
<div className="actions-group-label"><i className="pi pi-trash" /> Dateien löschen</div>
<div className="action-item">
<Button
label="OMDb neu zuordnen"
icon="pi pi-search"
severity="secondary"
size="small"
onClick={() => onAssignOmdb?.(job)}
loading={omdbAssignBusy}
disabled={running || typeof onAssignOmdb !== 'function'}
/>
) : (
<Button
label="MusicBrainz neu zuordnen"
icon="pi pi-search"
severity="secondary"
size="small"
onClick={() => onAssignCdMetadata?.(job)}
loading={cdMetadataAssignBusy}
disabled={running || typeof onAssignCdMetadata !== 'function'}
/>
)}
{!isCd && canResumeReady ? (
<Button
label="Im Dashboard öffnen"
icon="pi pi-window-maximize"
severity="info"
label="RAW löschen"
icon="pi pi-trash"
severity="warning"
outlined
size="small"
onClick={() => onResumeReady?.(job)}
onClick={() => onDeleteFiles?.(job, 'raw')}
loading={actionBusy}
disabled={!job.rawStatus?.exists || typeof onDeleteFiles !== 'function'}
/>
) : null}
{!isCd && typeof onRestartEncode === 'function' ? (
<span className="action-desc">Löscht die Quelldateien nach dem Rip ({isCd ? 'Audio-Rohdaten' : isAudiobook ? 'AAX/MP3-Quelldatei' : 'MKV-Datei von MakeMKV'}).</span>
</div>
<div className="action-item">
<Button
label="Encode neu starten"
icon="pi pi-play"
severity="success"
size="small"
onClick={() => onRestartEncode?.(job)}
loading={actionBusy}
disabled={!canRestartEncode}
/>
) : null}
{!isCd && !isAudiobook && typeof onRestartReview === 'function' ? (
<Button
label="Review neu starten"
icon="pi pi-refresh"
severity="info"
label={isCd ? 'Audio löschen' : (isAudiobook ? 'Ausgabe löschen' : 'Movie löschen')}
icon="pi pi-trash"
severity="warning"
outlined
size="small"
onClick={() => onRestartReview?.(job)}
onClick={() => onDeleteFiles?.(job, 'movie')}
loading={actionBusy}
disabled={!canRestartReview}
disabled={!job.outputStatus?.exists || typeof onDeleteFiles !== 'function'}
/>
) : null}
{!isCd && !isAudiobook ? (
<span className="action-desc">Löscht {isCd ? 'die fertig gerippten Audiodateien' : isAudiobook ? 'die fertig konvertierte Audiobook-Ausgabe' : 'die fertig encodierte Filmdatei'}.</span>
</div>
<div className="action-item">
<Button
label="RAW neu encodieren"
icon="pi pi-cog"
severity="info"
label="Beides löschen"
icon="pi pi-times"
severity="danger"
size="small"
onClick={() => onReencode?.(job)}
loading={reencodeBusy}
disabled={!canReencode || typeof onReencode !== 'function'}
onClick={() => onDeleteFiles?.(job, 'both')}
loading={actionBusy}
disabled={(!job.rawStatus?.exists && !job.outputStatus?.exists) || typeof onDeleteFiles !== 'function'}
/>
) : null}
<Button
label="RAW löschen"
icon="pi pi-trash"
severity="warning"
outlined
size="small"
onClick={() => onDeleteFiles?.(job, 'raw')}
loading={actionBusy}
disabled={!job.rawStatus?.exists || typeof onDeleteFiles !== 'function'}
/>
<Button
label={isCd ? 'Audio löschen' : (isAudiobook ? 'Ausgabe löschen' : 'Movie löschen')}
icon="pi pi-trash"
severity="warning"
outlined
size="small"
onClick={() => onDeleteFiles?.(job, 'movie')}
loading={actionBusy}
disabled={!job.outputStatus?.exists || typeof onDeleteFiles !== 'function'}
/>
<Button
label="Beides löschen"
icon="pi pi-times"
severity="danger"
size="small"
onClick={() => onDeleteFiles?.(job, 'both')}
loading={actionBusy}
disabled={(!job.rawStatus?.exists && !job.outputStatus?.exists) || typeof onDeleteFiles !== 'function'}
/>
<span className="action-desc">Löscht RAW-Quelldateien und Ausgabe gemeinsam.</span>
</div>
</div>
<div className="action-delete-entry">
<span className="action-desc">Entfernt diesen Eintrag aus der Verlaufsliste — Dateien auf der Festplatte bleiben erhalten.</span>
<Button
label="Historieneintrag löschen"
icon="pi pi-trash"
@@ -997,9 +1010,9 @@ export default function JobDetailDialog({
loading={deleteEntryBusy}
disabled={!canDeleteEntry}
/>
</>
)}
</div>
</div>
</div>
)}
<h4>Log</h4>
{showFinalLog ? (
+13 -2
View File
@@ -354,7 +354,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
const [search, setSearch] = useState('');
const [status, setStatus] = useState('');
const [mediumFilter, setMediumFilter] = useState('');
const [layout, setLayout] = useState('list');
const [layout, setLayout] = useState('grid');
const [sortKey, setSortKey] = useState('!start_time');
const [sortField, setSortField] = useState('start_time');
const [sortOrder, setSortOrder] = useState(-1);
@@ -432,6 +432,14 @@ export default function HistoryPage({ refreshToken = 0 }) {
}
};
useEffect(() => {
api.getPref('history_layout').then((res) => {
if (res?.value === 'list' || res?.value === 'grid') {
setLayout(res.value);
}
}).catch(() => {});
}, []);
useEffect(() => {
const timer = setTimeout(() => {
load();
@@ -1169,7 +1177,10 @@ export default function HistoryPage({ refreshToken = 0 }) {
<Button label="Neu laden" icon="pi pi-refresh" onClick={load} loading={loading} />
<div className="history-dv-layout-toggle">
<DataViewLayoutOptions layout={layout} onChange={(event) => setLayout(event.value)} />
<DataViewLayoutOptions layout={layout} onChange={(event) => {
setLayout(event.value);
api.setPref('history_layout', event.value).catch(() => {});
}} />
</div>
</div>
);
+52
View File
@@ -367,6 +367,58 @@ body {
flex-wrap: wrap;
}
.actions-section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.actions-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.actions-group-label {
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-color-secondary);
margin-bottom: 0.1rem;
}
.action-item {
display: flex;
align-items: center;
gap: 0.75rem;
}
.action-item .p-button {
flex-shrink: 0;
min-width: 11rem;
}
.action-desc {
font-size: 0.8rem;
color: var(--text-color-secondary);
line-height: 1.35;
}
.action-delete-entry {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.75rem;
border-top: 1px solid var(--surface-border);
padding-top: 0.75rem;
margin-top: 0.25rem;
}
.action-delete-entry .action-desc {
text-align: right;
}
.dashboard-drive-state {
margin: 0.5rem 0;
}