0.13.1-4 release snapshot
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user