diff --git a/build/Dockerfile b/build/Dockerfile index f1f0950..64c1ea9 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -4,7 +4,10 @@ WORKDIR /app/frontend COPY frontend/package*.json ./ RUN npm ci COPY frontend/ ./ -RUN npm run build +# Erst prüfen, dann bauen: Die Tests decken die Stellen ab, die sich ohne +# Browser prüfen lassen – Reihenfolgen, Zoomstufen, Suche im Text. Sie laufen +# in einer halben Sekunde; ein Abbild mit einer kaputten Sortierung kostet mehr. +RUN npm test && npm run build # ── Stage 2: Backend build ──────────────────────────────────────────────────── FROM golang:1.25-alpine AS backend-builder diff --git a/frontend/src/components/FileInfoDialog.tsx b/frontend/src/components/FileInfoDialog.tsx index f8fe621..eab9a5f 100644 --- a/frontend/src/components/FileInfoDialog.tsx +++ b/frontend/src/components/FileInfoDialog.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { filesApi, type Correspondent, type FileItem, type Tag } from '../api/client' -import { formatBytes } from '../pages/FileBrowser' +import { formatBytes } from '../format' import { useOverlayDismiss } from '../hooks/useOverlayDismiss' import TagChip from './TagChip' diff --git a/frontend/src/components/FileViews.tsx b/frontend/src/components/FileViews.tsx index 808d4a8..71378a0 100644 --- a/frontend/src/components/FileViews.tsx +++ b/frontend/src/components/FileViews.tsx @@ -5,7 +5,7 @@ import { TagRow } from './TagDialog' import TagChip from './TagChip' import { FileTypeIcon, FolderGlyph } from './icons' import FileCover, { FolderCover } from './FileCover' -import { formatBytes } from '../pages/FileBrowser' +import { formatBytes } from '../format' /** * Die beiden Darstellungen einer Ordnerliste: Kacheln und Zeilen. diff --git a/frontend/src/components/PdfViewer.tsx b/frontend/src/components/PdfViewer.tsx index b8b83ad..96c7bb4 100644 --- a/frontend/src/components/PdfViewer.tsx +++ b/frontend/src/components/PdfViewer.tsx @@ -3,6 +3,10 @@ import { filesApi } from '../api/client' import * as pdfjs from 'pdfjs-dist' import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist' import workerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url' +import PdfToolbar from './pdf/PdfToolbar' +import { commonWidth, padOf, pageGap, type PageSize } from './pdf/geometry' +import { countIn, wordAround, type Hit } from './pdf/text' +import { nextFine, nextStep } from './pdf/zoom' import './pdf.css' /** @@ -23,6 +27,10 @@ import './pdf.css' * Was er absichtlich *nicht* kann: Anmerkungen, Formulare, Unterschriften. * Das Original wird in dieser Anwendung nie verändert – ein Betrachter, der * schreiben kann, wäre ein Widerspruch dazu. + * + * Hier steht, was am DOM hängt: laden, zeichnen, rollen, zoomen, färben. Was + * sich ohne Browser ausrechnen lässt, liegt daneben in `pdf/` – Zoomstufen, + * Seitenmaße, Suche im Text – und wird dort auch geprüft. */ // Der Arbeiter kommt gebündelt aus node_modules und wird vom eigenen Server @@ -39,63 +47,24 @@ pdfjs.GlobalWorkerOptions.workerSrc = workerUrl */ const standardFontDataUrl = '/pdfjs/standard_fonts/' -/** Zoomstufen der **Knöpfe**. „Fit" liegt außerhalb und wird gerechnet. */ -const zoomSteps = [0.5, 0.75, 1, 1.25, 1.5, 2, 3] +/** + * Wie viele Seitenmaße gleichzeitig geholt werden. + * + * Vorher lief die Schleife streng nacheinander: Bei einem 200-seitigen Scan + * waren das 200 Runden, bevor überhaupt etwas zu sehen war. Alle auf einmal + * wäre das andere Extrem – PDF.js hält dann 200 Seitenobjekte zugleich. + */ +const sizeBatch = 24 /** - * Zoomschritt für Finger und Mausrad: **fünf Prozentpunkte**, auf runde - * Zahlen gerastet – 65, 70, 75, 80 … + * Wartezeit, bevor eine Eingabe im Suchfeld das ganze Dokument durchsucht. * - * Zuerst war es ein fester Faktor (fünf Prozent *vom aktuellen Wert*). - * Rechnerisch ist das die sauberere Größe: Sie fühlt sich unten wie oben - * gleich an. Abzulesen war sie aber nicht – die Anzeige sprang um drei - * Punkte, dann um vier, dann um sechs. Wer eine Zahl sieht, erwartet ein - * Raster, und das hier ist eines. - * - * Der Preis steht dazu: Bei 400 % ist ein Schritt gut ein Prozent, bei 25 % - * ein Fünftel. Das gleicht die Geste aus, nicht die Rasterung – siehe unten. + * Ohne sie las jeder einzelne Tastendruck den Text **aller** Seiten neu aus. + * Wer „Rechnung" tippt, löste damit acht vollständige Durchläufe aus, von + * denen sieben schon veraltet waren, bevor sie fertig wurden. Ein Sechstel + * einer Sekunde bemerkt beim Tippen niemand. */ -const zoomStepPercent = 5 -const minScale = 0.25 -const maxScale = 4 - -/** - * Innenabstand der Seitenliste. Am Telefon fast keiner: Dort ist die Breite - * das knappe Gut, und ein 16-Pixel-Rand links und rechts kostet zusammen mehr - * als ein Zehntel der Darstellungsgröße. - */ -function padOf(width: number): number { - return width < 560 ? 4 : 16 -} - -/** - * Ab welcher Breite die Leiste als Dreispaltenraster taugt. - * - * Gerechnet, nicht geschätzt: Die Steuerung links braucht 275 Pixel - * (zwei Knöpfe, Seitenzahl, Trenner, Zoom), die Suche in der Mitte 492 – und - * die rechte Spalte ist genauso breit wie die linke, sonst säße die Suche - * nicht mittig. Macht 2 × 275 + 492. - * - * **Vorher stand hier 720, und dazwischen war die Leiste kaputt:** Das Raster - * gab der linken Spalte weniger, als ihr Inhalt braucht, und die Knöpfe - * schrumpften mit. Gemessen bei 980 Pixeln Fensterbreite – erster Knopf 16 - * statt 28 Pixel, bei 760 die ganze Gruppe auf 108 statt 275. Am Ende war von - * der Steuerung nichts mehr zu sehen. - */ -const minGridWidth = 2 * 275 + 492 - -/** - * Breiten rechts vom Suchfeld – und damit das Maß für das Gegengewicht links. - * Beide Seiten gleich breit heißt: Das Feld sitzt in der Mitte der Leiste. - */ -const counterWidth = 54 -const hitButtonsWidth = 60 -/** Zähler + Knöpfe + die beiden Abstände dazwischen. */ -const hitsWidth = counterWidth + hitButtonsWidth + 6 - -/** Abstand zwischen zwei Seiten, in Pixeln. Auch der Wert, mit dem gerechnet - * wird, wenn die sichtbare Seite bestimmt wird. */ -const pageGap = 12 +const searchDelay = 160 interface Props { /** Adresse der Datei – dieselbe, die auch das