refactoring, numbering plan
This commit is contained in:
1 parent
b5279434b2
commit
3b04039aa9
47 files changed
+3485
-953
No files matched your search
@@ -0,0 +1,269 @@
|
||||
<script lang="ts">
|
||||
import { Download, Eye, GripVertical, RotateCcw, Trash2 } from '@lucide/svelte';
|
||||
import Button from '$lib/components/ui/button.svelte';
|
||||
import {
|
||||
batchStatusClass,
|
||||
batchStatusLabel,
|
||||
downloadBatchZip,
|
||||
formatBytes,
|
||||
shouldWarnAboutBatchMemory,
|
||||
type BatchItem
|
||||
} from '$lib/batch';
|
||||
|
||||
type Props = {
|
||||
items: BatchItem[];
|
||||
/** Resolved stamp label per row; frozen once the row was created. */
|
||||
labels: Record<string, string>;
|
||||
/** Renamed file per row, or `null` when renaming is switched off. */
|
||||
fileNames: Record<string, string | null>;
|
||||
selectedId: string | null;
|
||||
/** Base file name of the bulk ZIP, e.g. `anlagen` → `anlagen_2026-10-06.zip`. */
|
||||
zipBaseName: string;
|
||||
/** Reordering and preview selection are blocked while a run is active. */
|
||||
disabled?: boolean;
|
||||
onSelect: (id: string) => void;
|
||||
onMove: (from: number, to: number) => void;
|
||||
onDownload: (item: BatchItem) => void;
|
||||
onRemove: (id: string) => void;
|
||||
onRetry: (id: string) => void;
|
||||
};
|
||||
|
||||
let {
|
||||
items,
|
||||
labels,
|
||||
fileNames,
|
||||
selectedId,
|
||||
zipBaseName,
|
||||
disabled = false,
|
||||
onSelect,
|
||||
onMove,
|
||||
onDownload,
|
||||
onRemove,
|
||||
onRetry
|
||||
}: Props = $props();
|
||||
|
||||
let draggingId = $state<string | null>(null);
|
||||
let dragOverId = $state<string | null>(null);
|
||||
|
||||
const doneItems = $derived(items.filter((item) => item.status === 'done'));
|
||||
const totalResultBytes = $derived(
|
||||
doneItems.reduce((sum, item) => sum + (item.resultBytes?.byteLength ?? 0), 0)
|
||||
);
|
||||
const processingIndex = $derived(items.findIndex((item) => item.status === 'processing'));
|
||||
const showMemoryWarning = $derived(shouldWarnAboutBatchMemory(items));
|
||||
|
||||
const downloadZip = () => downloadBatchZip(doneItems, zipBaseName);
|
||||
|
||||
const moveUp = (index: number) => {
|
||||
if (disabled || index === 0) return;
|
||||
onMove(index, index - 1);
|
||||
};
|
||||
|
||||
const moveDown = (index: number) => {
|
||||
if (disabled || index === items.length - 1) return;
|
||||
onMove(index, index + 1);
|
||||
};
|
||||
|
||||
const handleDragStart = (event: DragEvent, item: BatchItem) => {
|
||||
if (disabled) return;
|
||||
draggingId = item.id;
|
||||
event.dataTransfer?.setData('text/plain', item.id);
|
||||
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
|
||||
const handleDragOver = (event: DragEvent, item: BatchItem) => {
|
||||
if (disabled || !draggingId || draggingId === item.id) return;
|
||||
event.preventDefault();
|
||||
dragOverId = item.id;
|
||||
};
|
||||
|
||||
const handleDrop = (event: DragEvent, item: BatchItem) => {
|
||||
event.preventDefault();
|
||||
const sourceId = draggingId;
|
||||
draggingId = null;
|
||||
dragOverId = null;
|
||||
if (disabled || !sourceId || sourceId === item.id) return;
|
||||
|
||||
const from = items.findIndex((entry) => entry.id === sourceId);
|
||||
const to = items.findIndex((entry) => entry.id === item.id);
|
||||
if (from === -1 || to === -1) return;
|
||||
onMove(from, to);
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
draggingId = null;
|
||||
dragOverId = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="space-y-3 rounded-2xl border border-border bg-card p-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 class="text-lg font-semibold text-foreground">Anlagen ({items.length})</h2>
|
||||
{#if doneItems.length > 0}
|
||||
<p class="text-sm text-primary">
|
||||
Ergebnisse gesamt: <span class="font-semibold">{formatBytes(totalResultBytes)}</span>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if processingIndex !== -1}
|
||||
<p class="text-sm text-primary" role="status">
|
||||
Datei {processingIndex + 1} von {items.length} wird verarbeitet…
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if showMemoryWarning}
|
||||
<div class="rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">
|
||||
Bei sehr vielen oder sehr großen Dateien kann der Arbeitsspeicher knapp werden. Laden Sie
|
||||
fertige Ergebnisse früh herunter und entfernen Sie sie aus der Liste.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<ul class="divide-y divide-border">
|
||||
{#each items as item, index (item.id)}
|
||||
{@const label = labels[item.id] ?? ''}
|
||||
{@const renamed = fileNames[item.id] ?? null}
|
||||
<li
|
||||
class="flex flex-wrap items-center gap-3 py-3 {dragOverId === item.id
|
||||
? 'bg-accent/40'
|
||||
: ''} {draggingId === item.id ? 'opacity-60' : ''}"
|
||||
data-exhibit-item={item.file.name}
|
||||
data-exhibit-index={index + 1}
|
||||
data-exhibit-label={label}
|
||||
draggable={!disabled}
|
||||
ondragstart={(event) => handleDragStart(event, item)}
|
||||
ondragover={(event) => handleDragOver(event, item)}
|
||||
ondrop={(event) => handleDrop(event, item)}
|
||||
ondragend={handleDragEnd}
|
||||
>
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
<span
|
||||
class="cursor-grab p-1 text-muted-foreground {disabled ? 'opacity-40' : ''}"
|
||||
aria-hidden="true"
|
||||
title="Ziehen zum Verschieben"
|
||||
>
|
||||
<GripVertical class="h-4 w-4" />
|
||||
</span>
|
||||
<span
|
||||
class="flex h-6 w-6 items-center justify-center rounded-full bg-accent text-xs font-bold text-primary"
|
||||
title="Position in der Liste"
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
<div class="flex flex-col">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-0.5 text-primary transition hover:bg-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-40"
|
||||
aria-label="{item.file.name} nach oben verschieben"
|
||||
title="Nach oben"
|
||||
disabled={disabled || index === 0}
|
||||
onclick={() => moveUp(index)}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
class="h-3 w-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
aria-hidden="true"><path d="M6 15l6-6 6 6" /></svg
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-0.5 text-primary transition hover:bg-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-40"
|
||||
aria-label="{item.file.name} nach unten verschieben"
|
||||
title="Nach unten"
|
||||
disabled={disabled || index === items.length - 1}
|
||||
onclick={() => moveDown(index)}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
class="h-3 w-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
aria-hidden="true"><path d="M6 9l6 6 6-6" /></svg
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-semibold text-foreground">
|
||||
{label}
|
||||
</p>
|
||||
<p class="truncate text-xs text-primary">{item.file.name}</p>
|
||||
{#if renamed}
|
||||
<p class="truncate text-xs text-muted-foreground" data-exhibit-filename>
|
||||
→ {renamed}
|
||||
</p>
|
||||
{/if}
|
||||
{#if item.status === 'error' && item.errorMessage}
|
||||
<p class="mt-0.5 text-xs text-red-700" role="alert">{item.errorMessage}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="shrink-0 rounded-full px-2.5 py-0.5 text-xs font-semibold {batchStatusClass(
|
||||
item.status
|
||||
)}"
|
||||
>
|
||||
{batchStatusLabel(item.status)}
|
||||
</span>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="focus-visible:ring-primary-400 rounded-lg p-2 text-primary transition hover:bg-accent focus:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-40 aria-pressed:bg-accent"
|
||||
aria-label="Vorschau für {item.file.name}"
|
||||
title="Vorschau für {item.file.name}"
|
||||
aria-pressed={selectedId === item.id}
|
||||
{disabled}
|
||||
onclick={() => onSelect(item.id)}
|
||||
>
|
||||
<Eye class="h-4 w-4" />
|
||||
</button>
|
||||
{#if item.status === 'done' && item.resultBytes && item.resultName}
|
||||
<button
|
||||
type="button"
|
||||
class="focus-visible:ring-primary-400 rounded-lg p-2 text-primary transition hover:bg-accent focus:outline-none focus-visible:ring-2"
|
||||
aria-label="{item.file.name} herunterladen"
|
||||
title="{item.file.name} herunterladen"
|
||||
onclick={() => onDownload(item)}
|
||||
>
|
||||
<Download class="h-4 w-4" />
|
||||
</button>
|
||||
{/if}
|
||||
{#if item.status === 'error'}
|
||||
<button
|
||||
type="button"
|
||||
class="focus-visible:ring-primary-400 rounded-lg p-2 text-primary transition hover:bg-accent focus:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
aria-label="{item.file.name} erneut versuchen"
|
||||
title="{item.file.name} erneut versuchen"
|
||||
{disabled}
|
||||
onclick={() => onRetry(item.id)}
|
||||
>
|
||||
<RotateCcw class="h-4 w-4" />
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="focus-visible:ring-primary-400 rounded-lg p-2 text-primary transition hover:bg-accent focus:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
aria-label="{item.file.name} entfernen"
|
||||
title="{item.file.name} entfernen"
|
||||
disabled={item.status === 'processing'}
|
||||
onclick={() => onRemove(item.id)}
|
||||
>
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{#if doneItems.length >= 2}
|
||||
<div class="flex justify-end pt-1">
|
||||
<Button onclick={downloadZip} icon={Download}>Alle herunterladen</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in new issue
Block a user