0.13.0-1 Series Flow

This commit is contained in:
2026-04-11 19:11:31 +00:00
parent 974b7bfad3
commit 1f8e6d3ce8
10 changed files with 1138 additions and 163 deletions
+18 -1
View File
@@ -739,6 +739,21 @@ function resolveSeriesDiscNumber(job) {
);
}
function compareSeriesChildJobsByDisc(leftJob, rightJob) {
const leftDisc = resolveSeriesDiscNumber(leftJob);
const rightDisc = resolveSeriesDiscNumber(rightJob);
if (leftDisc !== null && rightDisc !== null && leftDisc !== rightDisc) {
return leftDisc - rightDisc;
}
if (leftDisc !== null && rightDisc === null) {
return -1;
}
if (leftDisc === null && rightDisc !== null) {
return 1;
}
return Number(leftJob?.id || 0) - Number(rightJob?.id || 0);
}
function BoolState({ value }) {
const isTrue = Boolean(value);
return isTrue ? (
@@ -1113,7 +1128,9 @@ export default function JobDetailDialog({
const metadataDetails = resolveMetadataDetailsForDisplay(job, omdbInfo);
const seriesDiscNumber = isDvdSeries ? resolveSeriesDiscNumber(job) : null;
const seriesDiscLabel = seriesDiscNumber ? `Disk ${seriesDiscNumber}` : 'Disk unbekannt';
const childJobs = Array.isArray(job?.children) ? job.children : [];
const childJobs = Array.isArray(job?.children)
? [...job.children].sort(compareSeriesChildJobsByDisc)
: [];
const isSeriesContainer = isDvdSeries && String(job?.job_kind || '').trim().toLowerCase() === 'dvd_series_container';
const seriesChildSummary = job?.seriesChildSummary || null;
const seriesBackupSummary = isSeriesContainer ? seriesChildSummary?.backup : null;
@@ -13,6 +13,17 @@ export default function MetadataSelectionDialog({
onSearch,
busy
}) {
const parseDiscNumber = (rawValue) => {
const text = String(rawValue ?? '').trim();
if (!/^\d+$/.test(text)) {
return null;
}
const parsed = Number(text);
if (!Number.isFinite(parsed) || parsed <= 0) {
return null;
}
return Math.trunc(parsed);
};
const [selected, setSelected] = useState(null);
const [query, setQuery] = useState('');
const [manualTitle, setManualTitle] = useState('');
@@ -23,9 +34,18 @@ export default function MetadataSelectionDialog({
const [searchBusy, setSearchBusy] = useState(false);
const [searchError, setSearchError] = useState('');
const [seasonFilter, setSeasonFilter] = useState('');
const [discValidationError, setDiscValidationError] = useState('');
const metadataProvider = String(context?.metadataProvider || 'omdb').trim().toLowerCase() || 'omdb';
const mediaProfile = String(
context?.mediaProfile
|| context?.selectedMetadata?.mediaProfile
|| ''
).trim().toLowerCase();
const providerLabel = metadataProvider === 'tmdb' ? 'TMDb' : 'OMDb';
const supportsExternalIdInput = metadataProvider === 'omdb';
const requiresDiscNumber = metadataProvider === 'tmdb' && (mediaProfile === 'dvd' || !mediaProfile);
const effectiveDiscNumber = parseDiscNumber(manualDiscNumber);
const hasValidDiscNumber = effectiveDiscNumber !== null;
useEffect(() => {
if (!visible) {
@@ -51,8 +71,18 @@ export default function MetadataSelectionDialog({
setSearchBusy(false);
setSearchError('');
setSeasonFilter('');
setDiscValidationError('');
}, [visible, context]);
useEffect(() => {
if (!discValidationError) {
return;
}
if (!requiresDiscNumber || hasValidDiscNumber) {
setDiscValidationError('');
}
}, [discValidationError, requiresDiscNumber, hasValidDiscNumber]);
const rows = useMemo(() => {
const base = Array.isArray(context?.metadataCandidates)
? context.metadataCandidates
@@ -136,10 +166,10 @@ export default function MetadataSelectionDialog({
};
const handleSubmit = async () => {
const parsedDiscNumber = Number(manualDiscNumber);
const effectiveDiscNumber = Number.isFinite(parsedDiscNumber) && parsedDiscNumber > 0
? Math.trunc(parsedDiscNumber)
: null;
if (requiresDiscNumber && !hasValidDiscNumber) {
setDiscValidationError('Disk-Nummer ist Pflicht und muss eine ganze Zahl >= 1 sein.');
return;
}
const payload = selected
? {
@@ -174,11 +204,19 @@ export default function MetadataSelectionDialog({
await onSubmit(payload);
};
const handleHideRequest = () => {
if (requiresDiscNumber && !hasValidDiscNumber) {
setDiscValidationError('Disk-Nummer ist Pflicht und muss eine ganze Zahl >= 1 sein.');
return;
}
onHide?.();
};
return (
<Dialog
header="Metadaten auswählen"
visible={visible}
onHide={onHide}
onHide={handleHideRequest}
style={{ width: '52rem', maxWidth: '95vw' }}
className="metadata-selection-dialog"
breakpoints={{ '1200px': '92vw', '768px': '96vw', '560px': '98vw' }}
@@ -211,16 +249,19 @@ export default function MetadataSelectionDialog({
<InputText
value={manualDiscNumber}
onChange={(event) => setManualDiscNumber(event.target.value)}
placeholder="Disc-Nr (optional)"
placeholder={requiresDiscNumber ? 'Disc-Nr (Pflicht)' : 'Disc-Nr (optional)'}
inputMode="numeric"
disabled={searchBusy || busy}
/>
<small className="metadata-filter-subtitle">
Optionale Disc-Nummer fuer internes Mapping und Output-Templates. Nur Werte groesser 0 werden gespeichert.
{requiresDiscNumber
? 'Pflichtfeld fuer Serien-DVDs. Ohne Disk-Nummer koennen die Metadaten nicht verlassen werden.'
: 'Optionale Disc-Nummer fuer internes Mapping und Output-Templates. Nur Werte groesser 0 werden gespeichert.'}
</small>
</div>
</div>
{searchError ? <small className="error-text">{searchError}</small> : null}
{discValidationError ? <small className="error-text">{discValidationError}</small> : null}
<div className="table-scroll-wrap table-scroll-medium">
<DataTable
@@ -271,13 +312,22 @@ export default function MetadataSelectionDialog({
</div>
<div className="dialog-actions">
<Button label="Abbrechen" severity="secondary" text onClick={onHide} />
<Button
label="Abbrechen"
severity="secondary"
text
onClick={handleHideRequest}
disabled={requiresDiscNumber && !hasValidDiscNumber}
/>
<Button
label="Auswahl übernehmen"
icon="pi pi-play"
onClick={handleSubmit}
loading={busy}
disabled={!selected && !manualTitle.trim() && !manualImdb.trim()}
disabled={
(!selected && !manualTitle.trim() && !manualImdb.trim())
|| (requiresDiscNumber && !hasValidDiscNumber)
}
/>
</div>
</Dialog>