201 lines
6.5 KiB
React
201 lines
6.5 KiB
React
import { useEffect, useRef, useState } from 'react';
|
|
import { FileUpload } from 'primereact/fileupload';
|
|
import { Button } from 'primereact/button';
|
|
import { Tag } from 'primereact/tag';
|
|
import { ProgressBar } from 'primereact/progressbar';
|
|
import { Toast } from 'primereact/toast';
|
|
|
|
function formatBytes(value) {
|
|
const parsed = Number(value);
|
|
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
return 'n/a';
|
|
}
|
|
if (parsed === 0) {
|
|
return '0 B';
|
|
}
|
|
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
let unitIndex = 0;
|
|
let current = parsed;
|
|
while (current >= 1024 && unitIndex < units.length - 1) {
|
|
current /= 1024;
|
|
unitIndex += 1;
|
|
}
|
|
const digits = unitIndex <= 1 ? 0 : 2;
|
|
return `${current.toFixed(digits)} ${units[unitIndex]}`;
|
|
}
|
|
|
|
function normalizeJobId(value) {
|
|
const parsed = Number(value);
|
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
return null;
|
|
}
|
|
return Math.trunc(parsed);
|
|
}
|
|
|
|
export default function AudiobookUploadPanel({
|
|
audiobookUpload,
|
|
onAudiobookUpload,
|
|
onUploaded = null
|
|
}) {
|
|
const toastRef = useRef(null);
|
|
const fileUploadRef = useRef(null);
|
|
const [uploadFile, setUploadFile] = useState(null);
|
|
const [statusVisible, setStatusVisible] = useState(false);
|
|
|
|
const phase = String(audiobookUpload?.phase || 'idle').trim().toLowerCase();
|
|
const uploadBusy = phase === 'uploading' || phase === 'processing';
|
|
const progress = Number.isFinite(Number(audiobookUpload?.progressPercent))
|
|
? Math.max(0, Math.min(100, Number(audiobookUpload.progressPercent)))
|
|
: 0;
|
|
const loadedBytes = Number(audiobookUpload?.loadedBytes || 0);
|
|
const totalBytes = Number(audiobookUpload?.totalBytes || 0);
|
|
const fileName = String(audiobookUpload?.fileName || '').trim()
|
|
|| String(uploadFile?.name || '').trim()
|
|
|| null;
|
|
const statusTone = phase === 'error'
|
|
? 'danger'
|
|
: phase === 'completed'
|
|
? 'success'
|
|
: phase === 'processing'
|
|
? 'info'
|
|
: phase === 'uploading'
|
|
? 'warning'
|
|
: 'secondary';
|
|
const statusLabel = phase === 'uploading'
|
|
? 'Upload laeuft'
|
|
: phase === 'processing'
|
|
? 'Server verarbeitet'
|
|
: phase === 'completed'
|
|
? 'Bereit'
|
|
: phase === 'error'
|
|
? 'Fehler'
|
|
: 'Inaktiv';
|
|
|
|
useEffect(() => {
|
|
if (phase === 'idle') {
|
|
setStatusVisible(false);
|
|
return;
|
|
}
|
|
setStatusVisible(true);
|
|
if (phase === 'completed') {
|
|
const timer = setTimeout(() => setStatusVisible(false), 5000);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
return undefined;
|
|
}, [phase]);
|
|
|
|
const handleUpload = async () => {
|
|
if (!uploadFile) {
|
|
toastRef.current?.show({
|
|
severity: 'warn',
|
|
summary: 'Keine Datei',
|
|
detail: 'Bitte zuerst eine AAX-Datei auswaehlen.',
|
|
life: 2600
|
|
});
|
|
return;
|
|
}
|
|
try {
|
|
const response = await onAudiobookUpload?.(uploadFile, { startImmediately: false });
|
|
const uploadedJobId = normalizeJobId(response?.result?.jobId);
|
|
if (uploadedJobId) {
|
|
toastRef.current?.show({
|
|
severity: 'success',
|
|
summary: 'Audiobook importiert',
|
|
detail: `Job #${uploadedJobId} ist bereit.`,
|
|
life: 3200
|
|
});
|
|
} else {
|
|
toastRef.current?.show({
|
|
severity: 'success',
|
|
summary: 'Audiobook importiert',
|
|
detail: 'Upload abgeschlossen.',
|
|
life: 2600
|
|
});
|
|
}
|
|
setUploadFile(null);
|
|
fileUploadRef.current?.clear?.();
|
|
onUploaded?.(uploadedJobId, response);
|
|
} catch (error) {
|
|
toastRef.current?.show({
|
|
severity: 'error',
|
|
summary: 'Upload fehlgeschlagen',
|
|
detail: error?.message || 'Bitte Logs pruefen.',
|
|
life: 4200
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="audiobook-upload-panel">
|
|
<Toast ref={toastRef} position="top-right" />
|
|
<FileUpload
|
|
ref={fileUploadRef}
|
|
accept=".aax"
|
|
maxFileSize={10737418240}
|
|
customUpload
|
|
uploadHandler={() => void handleUpload()}
|
|
disabled={uploadBusy}
|
|
onSelect={(event) => setUploadFile(event.files[0] || null)}
|
|
onClear={() => setUploadFile(null)}
|
|
onRemove={() => setUploadFile(null)}
|
|
chooseOptions={{ icon: 'pi pi-images', iconOnly: true, className: 'p-button-rounded p-button-outlined' }}
|
|
uploadOptions={{ icon: 'pi pi-cloud-upload', iconOnly: true, className: 'p-button-rounded p-button-outlined p-button-success' }}
|
|
cancelOptions={{ icon: 'pi pi-times', iconOnly: true, className: 'p-button-rounded p-button-outlined p-button-danger' }}
|
|
itemTemplate={(file, options) => (
|
|
<div className="aax-file-item">
|
|
<i className="pi pi-headphones aax-file-icon" />
|
|
<div className="aax-file-info">
|
|
<span className="aax-file-name" title={file.name}>{file.name}</span>
|
|
<small>{options.formatSize}</small>
|
|
</div>
|
|
<Button
|
|
icon="pi pi-times"
|
|
text
|
|
rounded
|
|
severity="danger"
|
|
size="small"
|
|
onClick={options.onRemove}
|
|
disabled={uploadBusy}
|
|
/>
|
|
</div>
|
|
)}
|
|
emptyTemplate={() => (
|
|
<div className="aax-drop-zone">
|
|
<i className="pi pi-headphones aax-drop-icon" />
|
|
<p>AAX-Datei hier ablegen</p>
|
|
<small>oder oben "Auswaehlen" klicken</small>
|
|
</div>
|
|
)}
|
|
/>
|
|
|
|
{statusVisible ? (
|
|
<div className={`audiobook-upload-status tone-${statusTone}`}>
|
|
<div className="audiobook-upload-status-head">
|
|
<strong>{statusLabel}</strong>
|
|
<Tag value={statusLabel} severity={statusTone} />
|
|
</div>
|
|
{audiobookUpload?.statusText ? <small>{audiobookUpload.statusText}</small> : null}
|
|
{fileName ? (
|
|
<small className="audiobook-upload-file" title={fileName}>
|
|
Datei: {fileName}
|
|
</small>
|
|
) : null}
|
|
<div
|
|
className="ripper-job-row-progress audiobook-upload-progress"
|
|
aria-label={`Audiobook Upload ${Math.round(progress)} Prozent`}
|
|
>
|
|
<ProgressBar value={progress} showValue={false} />
|
|
<small>
|
|
{phase === 'processing'
|
|
? '100% | Upload fertig, Job wird vorbereitet ...'
|
|
: totalBytes > 0
|
|
? `${Math.round(progress)}% | ${formatBytes(loadedBytes)} / ${formatBytes(totalBytes)}`
|
|
: `${Math.round(progress)}%`}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|