rotate + hover on start-page
Build and Push Docker Image / build (push) Successful in 1m26s

This commit is contained in:
rehlert committed 2026-08-29 12:04:29 +02:00
1 parent ea21fc000f
commit 09cc5c06fb
7 files changed
+402 -58

No files matched your search

+4 -27
View File
@@ -9,6 +9,7 @@
Scissors,
Merge,
Stamp,
RotateCw,
FileText
} from '@lucide/svelte';
import { goto } from '$app/navigation';
@@ -25,11 +26,10 @@
minimize: Minimize2,
scissors: Scissors,
merge: Merge,
stamp: Stamp
stamp: Stamp,
'rotate-cw': RotateCw
};
let isDraggingFiles = $state(false);
const isZipFile = (file: File) =>
file.name.toLowerCase().endsWith('.zip') ||
file.type === 'application/zip' ||
@@ -46,17 +46,11 @@
const handleWindowDragOver = (event: DragEvent) => {
if (!event.dataTransfer?.types.includes('Files')) return;
event.preventDefault();
isDraggingFiles = true;
};
const handleWindowDragLeave = (event: DragEvent) => {
if (event.relatedTarget === null) isDraggingFiles = false;
};
const handleWindowDrop = (event: DragEvent) => {
if (!event.dataTransfer?.files.length) return;
event.preventDefault();
isDraggingFiles = false;
openBeaFiles(Array.from(event.dataTransfer.files));
};
</script>
@@ -65,24 +59,7 @@
<title>beA-Edit — Werkzeugkasten für das besondere elektronische Anwaltspostfach</title>
</svelte:head>
<svelte:window
ondragover={handleWindowDragOver}
ondragleave={handleWindowDragLeave}
ondrop={handleWindowDrop}
/>
{#if isDraggingFiles}
<div
class="pointer-events-none fixed inset-3 z-50 grid place-items-center rounded-3xl border-4 border-dashed border-primary bg-background/95 text-center shadow-2xl"
aria-hidden="true"
>
<div class="px-4">
<Archive class="mx-auto h-12 w-12" />
<p class="mt-4 text-xl font-bold">beA-ZIP hier ablegen</p>
<p class="mt-1 text-sm">Archiv aus dem besonderen elektronischen Anwaltspostfach</p>
</div>
</div>
{/if}
<svelte:window ondragover={handleWindowDragOver} ondrop={handleWindowDrop} />
<div class="flex flex-col gap-10 px-4 py-10">
<!-- Hero -->
+219
View File
@@ -0,0 +1,219 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import { degrees, PDFDocument } from 'pdf-lib';
import { RotateCcw, RotateCw } from '@lucide/svelte';
import FileDropzone from '$lib/components/FileDropzone.svelte';
import PdfPageGrid from '$lib/components/PdfPageGrid.svelte';
import Button from '$lib/components/ui/button.svelte';
import { downloadBlob } from '$lib/download';
import { loadPdfDocument, type PdfDocumentHandle } from '$lib/pdf-thumbnails';
let pdfFile: File | null = $state(null);
let pdfHandle: PdfDocumentHandle | null = $state(null);
let rotations = $state(new Map<number, number>());
let isProcessing = $state(false);
let error = $state<string | null>(null);
let loadingError = $state<string | null>(null);
const rotatedCount = $derived(rotations.size);
const hasRotations = $derived(rotatedCount > 0);
const isEncryptedPdfError = (loadError: unknown) => {
const message =
loadError instanceof Error ? `${loadError.name} ${loadError.message}` : String(loadError);
return /password|encrypted|encryption|verschl/i.test(message);
};
const loadErrorMessage = (loadError: unknown) =>
isEncryptedPdfError(loadError)
? 'Dieses PDF ist passwortgeschützt. Entfernen Sie zuerst unter „Passwort entfernen“ den Schutz.'
: 'PDF konnte nicht geladen werden. Prüfen Sie, ob die Datei gültig ist.';
const handleFileSelected = async (files: File[]) => {
const file = files[0];
if (!file) return;
pdfHandle?.destroy();
pdfHandle = null;
pdfFile = file;
rotations = new Map();
loadingError = null;
error = null;
try {
const bytes = new Uint8Array(await file.arrayBuffer());
pdfHandle = await loadPdfDocument(bytes);
} catch (err) {
console.error('Failed to load PDF:', err);
loadingError = loadErrorMessage(err);
pdfHandle = null;
}
};
const rotatePage = (pageNumber: number, delta: number) => {
const next = new Map(rotations);
const current = next.get(pageNumber) ?? 0;
const value = (((current + delta) % 360) + 360) % 360;
if (value === 0) {
next.delete(pageNumber);
} else {
next.set(pageNumber, value);
}
rotations = next;
};
const rotateAll = (delta: number) => {
if (!pdfHandle) return;
for (let pageNumber = 1; pageNumber <= pdfHandle.pageCount; pageNumber += 1) {
rotatePage(pageNumber, delta);
}
};
const resetRotations = () => {
rotations = new Map();
};
const previewClass = (pageNumber: number) => {
const delta = rotations.get(pageNumber) ?? 0;
switch (delta) {
case 90:
return 'rotate-90';
case 180:
return 'rotate-180';
case 270:
return 'rotate-[270deg]';
default:
return '';
}
};
const rotateAndDownload = async () => {
if (!pdfFile || !pdfHandle || !hasRotations) return;
isProcessing = true;
error = null;
try {
const bytes = new Uint8Array(await pdfFile.arrayBuffer());
const pdfDoc = await PDFDocument.load(bytes);
for (const [index, page] of pdfDoc.getPages().entries()) {
const delta = rotations.get(index + 1);
if (!delta) continue;
const current = page.getRotation().angle;
page.setRotation(degrees((((current + delta) % 360) + 360) % 360));
}
const resultBytes = await pdfDoc.save();
const baseName = pdfFile.name.replace(/\.pdf$/i, '');
downloadBlob(resultBytes, `${baseName}_gedreht.pdf`);
} catch (err) {
console.error('Failed to rotate pages:', err);
error = isEncryptedPdfError(err)
? 'Dieses PDF ist passwortgeschützt. Entfernen Sie zuerst unter „Passwort entfernen“ den Schutz.'
: 'Fehler beim Drehen der Seiten.';
} finally {
isProcessing = false;
}
};
onDestroy(() => {
pdfHandle?.destroy();
});
</script>
<div class="mx-auto max-w-4xl space-y-6">
<div>
<h1 class="text-2xl font-bold text-primary-900">Seiten drehen</h1>
<p class="mt-2 text-sm text-primary-700">
Drehen Sie einzelne oder alle Seiten eines PDFs — verlustfrei und ohne Qualitätsverlust.
</p>
</div>
{#if !pdfHandle}
<FileDropzone onFilesSelected={handleFileSelected} />
{#if loadingError}
<div class="mt-4 rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800">
{loadingError}
</div>
{/if}
{:else}
<div class="space-y-4">
<FileDropzone
onFilesSelected={handleFileSelected}
label="Anderes PDF ablegen"
sublabel={pdfFile?.name ?? ''}
class="py-4"
/>
<div class="flex flex-wrap items-center justify-between gap-3">
<h2 class="text-lg font-semibold text-primary-900">
{pdfFile?.name} — {pdfHandle.pageCount} Seite{pdfHandle.pageCount === 1 ? '' : 'n'}
</h2>
<div class="flex flex-wrap gap-2">
<Button variant="tertiary" class="min-w-0 px-3" onclick={() => rotateAll(90)}>
Alle 90° rechts
</Button>
<Button variant="tertiary" class="min-w-0 px-3" onclick={() => rotateAll(270)}>
Alle 90° links
</Button>
<Button variant="tertiary" class="min-w-0 px-3" onclick={() => rotateAll(180)}>
Alle 180°
</Button>
<Button
variant="tertiary"
class="min-w-0 px-3"
onclick={resetRotations}
disabled={!hasRotations}
>
Zurücksetzen
</Button>
</div>
</div>
<PdfPageGrid thumbnails={pdfHandle.thumbnails} thumbnailWidth={160} cardClass={previewClass}>
{#snippet pageActions(pageNumber)}
{#if (rotations.get(pageNumber) ?? 0) !== 0}
<span
class="absolute top-1 right-1 rounded-full bg-primary px-1.5 py-0.5 text-[10px] font-bold text-white"
>
{rotations.get(pageNumber)}°
</span>
{/if}
<div class="flex gap-1">
<button
type="button"
class="flex h-7 w-7 items-center justify-center rounded-lg border border-primary-200 bg-white text-primary transition hover:bg-primary-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400"
aria-label="Seite {pageNumber} um 90° nach links drehen"
onclick={() => rotatePage(pageNumber, 270)}
>
<RotateCcw class="h-4 w-4" />
</button>
<button
type="button"
class="flex h-7 w-7 items-center justify-center rounded-lg border border-primary-200 bg-white text-primary transition hover:bg-primary-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400"
aria-label="Seite {pageNumber} um 90° nach rechts drehen"
onclick={() => rotatePage(pageNumber, 90)}
>
<RotateCw class="h-4 w-4" />
</button>
</div>
{/snippet}
</PdfPageGrid>
{#if error}
<div class="rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800">
{error}
</div>
{/if}
<div class="flex items-center justify-between">
<p class="text-sm text-primary-700">
{rotatedCount} von {pdfHandle.pageCount} Seite{pdfHandle.pageCount === 1 ? '' : 'n'}
gedreht
</p>
<Button onclick={rotateAndDownload} disabled={isProcessing || !hasRotations}>
{isProcessing ? 'Wird verarbeitet...' : 'Drehen & herunterladen'}
</Button>
</div>
</div>
{/if}
</div>