100 lines
4.1 KiB
TypeScript
100 lines
4.1 KiB
TypeScript
import React from 'react';
|
|
import { Page } from './_util';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useParams } from 'react-router-dom';
|
|
import { getDeviceId } from '../lib/device';
|
|
import { compressPhoto, formatBytes } from '../lib/image';
|
|
import { useUploadQueue } from '../queue/hooks';
|
|
import React from 'react';
|
|
|
|
type Item = { file: File; out?: File; progress: number; done?: boolean; error?: string; id?: number };
|
|
|
|
export default function UploadPage() {
|
|
const { slug } = useParams();
|
|
const [items, setItems] = React.useState<Item[]>([]);
|
|
const queue = useUploadQueue();
|
|
const [progressMap, setProgressMap] = React.useState<Record<number, number>>({});
|
|
|
|
React.useEffect(() => {
|
|
const onProg = (e: any) => {
|
|
const { id, progress } = e.detail || {};
|
|
if (typeof id === 'number') setProgressMap((m) => ({ ...m, [id]: progress }));
|
|
};
|
|
window.addEventListener('queue-progress', onProg);
|
|
return () => window.removeEventListener('queue-progress', onProg);
|
|
}, []);
|
|
|
|
async function onPick(e: React.ChangeEvent<HTMLInputElement>) {
|
|
const files = Array.from(e.target.files ?? []).slice(0, 10);
|
|
const results: Item[] = [];
|
|
for (const f of files) {
|
|
try {
|
|
const out = await compressPhoto(f, { targetBytes: 1_500_000, maxEdge: 2560, qualityStart: 0.85 });
|
|
results.push({ file: f, out, progress: 0 });
|
|
} catch (err: any) {
|
|
results.push({ file: f, progress: 0, error: err?.message || 'Komprimierung fehlgeschlagen' });
|
|
}
|
|
}
|
|
setItems(results);
|
|
}
|
|
|
|
async function startUpload() {
|
|
// Enqueue items for offline-friendly processing
|
|
for (const it of items) {
|
|
await queue.add({ slug: slug!, fileName: it.out?.name ?? it.file.name, blob: it.out ?? it.file });
|
|
}
|
|
setItems([]);
|
|
}
|
|
|
|
return (
|
|
<Page title="Foto aufnehmen/hochladen">
|
|
<Input type="file" accept="image/*" multiple capture="environment" onChange={onPick} />
|
|
<div className="h-3" />
|
|
<Button onClick={startUpload} disabled={items.length === 0}>Hochladen</Button>
|
|
<div className="mt-4 space-y-2">
|
|
{items.map((it, i) => (
|
|
<div key={i} className="rounded border p-2 text-sm">
|
|
<div className="flex items-center justify-between">
|
|
<div className="truncate">{it.file.name}</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{formatBytes(it.out?.size ?? it.file.size)}
|
|
</div>
|
|
</div>
|
|
{it.done && <div className="text-xs text-muted-foreground">Fertig</div>}
|
|
{it.error && <div className="text-xs text-red-500">{it.error}</div>}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="mt-6">
|
|
<div className="mb-2 text-sm font-medium">Warteschlange</div>
|
|
{queue.loading ? (
|
|
<div className="text-sm text-muted-foreground">Lade…</div>
|
|
) : queue.items.length === 0 ? (
|
|
<div className="text-sm text-muted-foreground">Keine offenen Uploads.</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{queue.items.map((q) => (
|
|
<div key={q.id} className="rounded border p-2 text-sm">
|
|
<div className="flex items-center justify-between">
|
|
<div className="truncate">{q.fileName}</div>
|
|
<div className="text-xs text-muted-foreground">{q.status}{q.status==='uploading' && typeof q.id==='number' ? ` • ${progressMap[q.id] ?? 0}%` : ''}</div>
|
|
</div>
|
|
{q.status === 'uploading' && typeof q.id==='number' && (
|
|
<div className="mt-2 h-2 w-full rounded bg-gray-200 dark:bg-gray-700">
|
|
<div className="h-2 rounded bg-emerald-500" style={{ width: `${progressMap[q.id] ?? 0}%` }} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
<div className="flex gap-2">
|
|
<Button variant="secondary" onClick={queue.retryAll}>Erneut versuchen</Button>
|
|
<Button variant="secondary" onClick={queue.clearFinished}>Erledigte entfernen</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Page>
|
|
);
|
|
}
|