Features/shadcn #1
+95
-31
@@ -3,7 +3,7 @@
|
|||||||
import Card from "$lib/components/Card.svelte";
|
import Card from "$lib/components/Card.svelte";
|
||||||
import ProcessedZipArchiveEditor from "$lib/components/ProcessedZipArchiveEditor.svelte";
|
import ProcessedZipArchiveEditor from "$lib/components/ProcessedZipArchiveEditor.svelte";
|
||||||
import ZipDropzone from "$lib/components/ZipDropzone.svelte";
|
import ZipDropzone from "$lib/components/ZipDropzone.svelte";
|
||||||
import {extractZipArchives, type ProcessedZipArchive} from "$lib/zip-processing";
|
import {extractZipArchives, mergeProcessedZipArchive, type ProcessedZipArchive} from "$lib/zip-processing";
|
||||||
|
|
||||||
const THUMBNAIL_WIDTH_STORAGE_KEY = "thumbnailWidth";
|
const THUMBNAIL_WIDTH_STORAGE_KEY = "thumbnailWidth";
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
let pendingZipCount = $state(0);
|
let pendingZipCount = $state(0);
|
||||||
let thumbnailWidth = $state(200);
|
let thumbnailWidth = $state(200);
|
||||||
let thumbnailWidthHydrated = $state(false);
|
let thumbnailWidthHydrated = $state(false);
|
||||||
|
let archiveGeneration = 0;
|
||||||
|
|
||||||
const clampThumbnailWidth = (value: number) => Math.min(400, Math.max(100, value));
|
const clampThumbnailWidth = (value: number) => Math.min(400, Math.max(100, value));
|
||||||
|
|
||||||
@@ -37,24 +38,49 @@
|
|||||||
localStorage.setItem(THUMBNAIL_WIDTH_STORAGE_KEY, String(thumbnailWidth));
|
localStorage.setItem(THUMBNAIL_WIDTH_STORAGE_KEY, String(thumbnailWidth));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const toArrayBuffer = (bytes: Uint8Array) => {
|
||||||
|
const copy = new Uint8Array(bytes.byteLength);
|
||||||
|
copy.set(bytes);
|
||||||
|
return copy.buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadPdfBytes = (bytes: Uint8Array, fileName: string) => {
|
||||||
|
const pdfBlob = new Blob([toArrayBuffer(bytes)], {type: "application/pdf"});
|
||||||
|
const objectUrl = URL.createObjectURL(pdfBlob);
|
||||||
|
const downloadLink = document.createElement("a");
|
||||||
|
|
||||||
|
downloadLink.href = objectUrl;
|
||||||
|
downloadLink.download = fileName.toLowerCase().endsWith(".pdf") ? fileName : `${fileName}.pdf`;
|
||||||
|
downloadLink.rel = "noopener";
|
||||||
|
downloadLink.click();
|
||||||
|
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
|
||||||
|
};
|
||||||
|
|
||||||
const handleFilesSelected = (files: File[]) => {
|
const handleFilesSelected = (files: File[]) => {
|
||||||
selectedZipFiles = [...selectedZipFiles, ...files];
|
selectedZipFiles = [...selectedZipFiles, ...files];
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
void processZipFile(file);
|
void processZipFile(file, archiveGeneration);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const processZipFile = async (file: File) => {
|
const processZipFile = async (file: File, generation: number) => {
|
||||||
pendingZipCount += 1;
|
pendingZipCount += 1;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const processedZipArchives = await extractZipArchives(file);
|
const processedZipArchives = await extractZipArchives(file);
|
||||||
|
|
||||||
|
if (generation !== archiveGeneration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
processedZipFiles = [...processedZipFiles, ...processedZipArchives];
|
processedZipFiles = [...processedZipFiles, ...processedZipArchives];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to process ${file.name}`, error);
|
console.error(`Failed to process ${file.name}`, error);
|
||||||
} finally {
|
} finally {
|
||||||
pendingZipCount -= 1;
|
if (generation === archiveGeneration) {
|
||||||
|
pendingZipCount -= 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,12 +89,77 @@
|
|||||||
existingIndex === index ? archive : existingArchive,
|
existingIndex === index ? archive : existingArchive,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportAllZipArchivesAsPdf = async () => {
|
||||||
|
for (const archive of processedZipFiles) {
|
||||||
|
try {
|
||||||
|
const mergedBytes = await mergeProcessedZipArchive(archive);
|
||||||
|
downloadPdfBytes(mergedBytes, archive.name);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to export archive ${archive.name}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAllZipArchives = () => {
|
||||||
|
archiveGeneration += 1;
|
||||||
|
selectedZipFiles = [];
|
||||||
|
processedZipFiles = [];
|
||||||
|
pendingZipCount = 0;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet content()}
|
{#snippet content()}
|
||||||
{#if selectedZipFiles.length === 0}
|
{#if selectedZipFiles.length === 0}
|
||||||
<ZipDropzone onFilesSelected={handleFilesSelected}/>
|
<ZipDropzone onFilesSelected={handleFilesSelected}/>
|
||||||
{:else}
|
{:else}
|
||||||
|
<details class="fixed bottom-4 right-4 z-20">
|
||||||
|
<summary
|
||||||
|
class="flex h-12 w-12 cursor-pointer list-none items-center justify-center rounded-full border border-primary-200 bg-white text-lg font-semibold text-primary-800 shadow-lg transition hover:border-primary-300 hover:bg-primary-50">
|
||||||
|
<span class="sr-only">Settings</span>
|
||||||
|
<span>⚙️</span>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<div class="absolute bottom-14 right-0 w-72 rounded-xl border border-primary-100 bg-white p-4 shadow-lg">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
class="flex-1 rounded-container bg-primary-900 px-3 py-2 text-[13px] font-bold leading-5 text-white shadow-sm transition hover:bg-primary-800 disabled:cursor-not-allowed disabled:bg-primary-300"
|
||||||
|
type="button"
|
||||||
|
disabled={processedZipFiles.length === 0}
|
||||||
|
onclick={exportAllZipArchivesAsPdf}
|
||||||
|
>
|
||||||
|
Alle ZIP-Archive als PDF exportieren
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="flex-1 rounded-container bg-red-600 px-3 py-2 text-[13px] font-bold leading-5 text-white shadow-sm transition hover:bg-red-500 disabled:cursor-not-allowed disabled:bg-red-300"
|
||||||
|
type="button"
|
||||||
|
disabled={selectedZipFiles.length === 0}
|
||||||
|
onclick={deleteAllZipArchives}
|
||||||
|
>
|
||||||
|
Alle ZIP-Archive löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="flex flex-col gap-2">
|
||||||
|
<div class="flex items-center justify-between gap-3 text-sm font-medium text-primary-900">
|
||||||
|
<span>Thumbnail Breite</span>
|
||||||
|
<span class="tabular-nums text-primary-700">{thumbnailWidth}px</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="w-full accent-primary-700"
|
||||||
|
type="range"
|
||||||
|
min="100"
|
||||||
|
max="400"
|
||||||
|
step="1"
|
||||||
|
bind:value={thumbnailWidth}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
<div class="flex min-h-[calc(-180px+100vh)] w-full grow flex-col justify-center gap-4 px-4 py-6">
|
<div class="flex min-h-[calc(-180px+100vh)] w-full grow flex-col justify-center gap-4 px-4 py-6">
|
||||||
<div class="mx-auto flex w-full flex-col gap-3">
|
<div class="mx-auto flex w-full flex-col gap-3">
|
||||||
{#if pendingZipCount > 0}
|
{#if pendingZipCount > 0}
|
||||||
@@ -96,33 +187,6 @@
|
|||||||
<section class="px-4">
|
<section class="px-4">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3 py-6">
|
<div class="flex flex-wrap items-center justify-between gap-3 py-6">
|
||||||
<h1 class="h1 text-[20px] font-bold text-primary-900">beA-Edit</h1>
|
<h1 class="h1 text-[20px] font-bold text-primary-900">beA-Edit</h1>
|
||||||
|
|
||||||
<details class="relative">
|
|
||||||
<summary
|
|
||||||
class="flex cursor-pointer list-none items-center rounded-md border border-primary-200 bg-white px-3 py-2 text-sm font-semibold text-primary-800 shadow-sm transition hover:border-primary-300 hover:bg-primary-50">
|
|
||||||
<span class="sr-only">Settings</span>
|
|
||||||
<span>⚙️</span>
|
|
||||||
</summary>
|
|
||||||
|
|
||||||
<div class="absolute right-0 z-10 mt-2 w-64 rounded-xl border border-primary-100 bg-white p-4 shadow-lg">
|
|
||||||
<label class="flex flex-col gap-2">
|
|
||||||
<div class="flex items-center justify-between gap-3 text-sm font-medium text-primary-900">
|
|
||||||
<span>Thumbnail Breite</span>
|
|
||||||
<span class="tabular-nums text-primary-700">{thumbnailWidth}px</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
class="w-full accent-primary-700"
|
|
||||||
type="range"
|
|
||||||
min="100"
|
|
||||||
max="400"
|
|
||||||
step="1"
|
|
||||||
bind:value={thumbnailWidth}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card {content} class="h-full"/>
|
<Card {content} class="h-full"/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user