201 lines
6.0 KiB
Svelte
201 lines
6.0 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import BeaWorkspaceControls from '$lib/components/BeaWorkspaceControls.svelte';
|
|
import ProcessedZipArchiveEditor from '$lib/components/ProcessedZipArchiveEditor.svelte';
|
|
import ZipDropzone from '$lib/components/ZipDropzone.svelte';
|
|
import BeaArchiveProcessing, {
|
|
type ZipProcessingJob
|
|
} from '$lib/components/BeaArchiveProcessing.svelte';
|
|
import {
|
|
extractZipArchives,
|
|
mergeProcessedZipArchive,
|
|
type ProcessedZipArchive
|
|
} from '$lib/zip-processing';
|
|
import { downloadBlob } from '$lib/download';
|
|
import { takeQueuedZipFiles } from '$lib/pending-files';
|
|
import * as Card from '$lib/components/ui/card/index';
|
|
|
|
const THUMBNAIL_WIDTH_STORAGE_KEY = 'thumbnailWidth';
|
|
|
|
let selectedZipFiles: File[] = $state([]);
|
|
let processedZipFiles: ProcessedZipArchive[] = $state([]);
|
|
let zipJobs: ZipProcessingJob[] = $state([]);
|
|
let pendingZipCount = $derived(zipJobs.filter((job) => job.status === 'processing').length);
|
|
let failedZipCount = $derived(zipJobs.filter((job) => job.status === 'error').length);
|
|
let thumbnailWidth = $state(200);
|
|
let thumbnailWidthHydrated = $state(false);
|
|
let archiveGeneration = 0;
|
|
|
|
const clampThumbnailWidth = (value: number) => Math.min(400, Math.max(100, value));
|
|
|
|
onMount(() => {
|
|
const storedValue = localStorage.getItem(THUMBNAIL_WIDTH_STORAGE_KEY);
|
|
|
|
if (storedValue !== null) {
|
|
const parsedValue = Number(storedValue);
|
|
|
|
if (Number.isFinite(parsedValue)) {
|
|
thumbnailWidth = clampThumbnailWidth(parsedValue);
|
|
}
|
|
}
|
|
|
|
thumbnailWidthHydrated = true;
|
|
|
|
const queuedFiles = takeQueuedZipFiles();
|
|
if (queuedFiles.length > 0) handleFilesSelected(queuedFiles);
|
|
});
|
|
|
|
$effect(() => {
|
|
if (!thumbnailWidthHydrated) {
|
|
return;
|
|
}
|
|
|
|
localStorage.setItem(THUMBNAIL_WIDTH_STORAGE_KEY, String(thumbnailWidth));
|
|
});
|
|
|
|
const updateZipJob = (jobId: string, update: Partial<ZipProcessingJob>) => {
|
|
zipJobs = zipJobs.map((job) => (job.id === jobId ? { ...job, ...update } : job));
|
|
};
|
|
|
|
const handleFilesSelected = (files: File[]) => {
|
|
selectedZipFiles = [...selectedZipFiles, ...files];
|
|
|
|
const jobs = files.map((file) => ({
|
|
id: crypto.randomUUID(),
|
|
file,
|
|
status: 'processing' as const
|
|
}));
|
|
zipJobs = [...zipJobs, ...jobs];
|
|
|
|
for (const job of jobs) {
|
|
void processZipFile(job.id, job.file, archiveGeneration);
|
|
}
|
|
};
|
|
|
|
const processZipFile = async (jobId: string, file: File, generation: number) => {
|
|
updateZipJob(jobId, { status: 'processing', error: undefined, archiveCount: undefined });
|
|
|
|
try {
|
|
const processedZipArchives = await extractZipArchives(file);
|
|
|
|
if (generation !== archiveGeneration || !zipJobs.some((job) => job.id === jobId)) {
|
|
return;
|
|
}
|
|
|
|
processedZipFiles = [...processedZipFiles, ...processedZipArchives];
|
|
updateZipJob(jobId, {
|
|
status: 'complete',
|
|
archiveCount: processedZipArchives.length
|
|
});
|
|
} catch (error) {
|
|
console.error(`Failed to process ${file.name}`, error);
|
|
|
|
if (generation === archiveGeneration && zipJobs.some((job) => job.id === jobId)) {
|
|
updateZipJob(jobId, {
|
|
status: 'error',
|
|
error: 'Prüfen Sie, ob die Datei ein gültiges beA-ZIP-Archiv ist.'
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const retryZipFile = (jobId: string) => {
|
|
const job = zipJobs.find((candidate) => candidate.id === jobId);
|
|
if (!job || job.status === 'processing') return;
|
|
|
|
void processZipFile(job.id, job.file, archiveGeneration);
|
|
};
|
|
|
|
const removeZipFile = (jobId: string) => {
|
|
const job = zipJobs.find((candidate) => candidate.id === jobId);
|
|
if (!job || job.status === 'processing') return;
|
|
|
|
zipJobs = zipJobs.filter((candidate) => candidate.id !== jobId);
|
|
selectedZipFiles = selectedZipFiles.filter((file) => file !== job.file);
|
|
};
|
|
|
|
const updateProcessedZipFile = (index: number, archive: ProcessedZipArchive) => {
|
|
processedZipFiles = processedZipFiles.map((existingArchive, existingIndex) =>
|
|
existingIndex === index ? archive : existingArchive
|
|
);
|
|
};
|
|
|
|
const exportAllZipArchivesAsPdf = async () => {
|
|
for (const archive of processedZipFiles) {
|
|
try {
|
|
const mergedBytes = await mergeProcessedZipArchive(archive);
|
|
downloadBlob(mergedBytes, archive.name);
|
|
} catch (error) {
|
|
console.error(`Failed to export archive ${archive.name}`, error);
|
|
}
|
|
}
|
|
};
|
|
|
|
const resetWorkspace = () => {
|
|
archiveGeneration += 1;
|
|
selectedZipFiles = [];
|
|
processedZipFiles = [];
|
|
zipJobs = [];
|
|
};
|
|
|
|
const replaceWithZipFiles = (files: File[]) => {
|
|
resetWorkspace();
|
|
// handleFilesSelected captures the incremented archiveGeneration, so results
|
|
// from the previous workspace can no longer append.
|
|
handleFilesSelected(files);
|
|
};
|
|
</script>
|
|
|
|
{#snippet content()}
|
|
{#if selectedZipFiles.length === 0}
|
|
<Card.Root>
|
|
<Card.Content>
|
|
<ZipDropzone onFilesSelected={handleFilesSelected} />
|
|
</Card.Content>
|
|
</Card.Root>
|
|
{:else if zipJobs.length > 0}
|
|
<div class="flex w-full grow flex-col gap-4 px-4 py-6">
|
|
<BeaWorkspaceControls
|
|
selectedZipCount={selectedZipFiles.length}
|
|
processedArchiveCount={processedZipFiles.length}
|
|
{pendingZipCount}
|
|
{thumbnailWidth}
|
|
onThumbnailWidthChange={(value) => (thumbnailWidth = value)}
|
|
onRequestReplacement={replaceWithZipFiles}
|
|
onAppendFiles={handleFilesSelected}
|
|
onExportAll={exportAllZipArchivesAsPdf}
|
|
onClear={resetWorkspace}
|
|
/>
|
|
<div class="mx-auto flex w-full flex-col gap-3">
|
|
{#if processedZipFiles.length === 0}
|
|
<BeaArchiveProcessing jobs={zipJobs} onRetry={retryZipFile} onRemove={removeZipFile} />
|
|
{:else if pendingZipCount > 0 || failedZipCount > 0}
|
|
<BeaArchiveProcessing
|
|
jobs={zipJobs}
|
|
compact
|
|
onRetry={retryZipFile}
|
|
onRemove={removeZipFile}
|
|
/>
|
|
{/if}
|
|
|
|
{#if processedZipFiles.length > 0}
|
|
<div class="flex flex-col gap-4">
|
|
{#each processedZipFiles as file, index}
|
|
<ProcessedZipArchiveEditor
|
|
archive={file}
|
|
{thumbnailWidth}
|
|
onArchiveChange={(updatedArchive) => updateProcessedZipFile(index, updatedArchive)}
|
|
/>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{/snippet}
|
|
|
|
<div>
|
|
<h1 class="h1 text-[20px] font-bold text-primary-900">beA-Edit</h1>
|
|
{@render content()}
|
|
</div>
|