First visit ends in a sheet, not in an empty box

A fresh print-only install showed "Pick from the library" as the second
way in — on an instance that has no library, because the library belongs
to the AI module. The button opened an empty picker. It is gone when the
module is off (`hasLibrary`), together with the two smaller "from the
library" links.

In its place: "Try it with a sample photo". It fetches
public/samples/lake-at-dusk.jpg — an original illustration drawn for this,
no third-party material — and sends it through the normal upload, so
everything after it is what happens with a real picture.

Checked in the browser, English and German, on a print-only instance:
no library button, sample loads, the print button enables, the PDF
measures 210.00 x 297.00 mm, no failed request.
This commit is contained in:
till committed 2026-10-10 06:34:05 +00:00
1 parent 8e05c4eb53
commit 55c0054ad8
5 files changed
+42 -9

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
# Sample image
`lake-at-dusk.jpg` is an original illustration drawn for Klarbild as vector
shapes and rendered to JPEG. It contains no third-party material and is
covered by the repository's licence like everything else here.
It exists so that the first visit to a fresh instance can end in a printed
sheet: the "Try it with a sample photo" button in the print studio uploads
it through the normal upload path.
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+29 -8
View File
@@ -110,8 +110,8 @@ function shrink(dataUrl: string, maxEdge = 700): Promise<{ url: string; w: numbe
/* ========================================================================== */
export default function PrintApp({ locale, dict, canDeliver = false }:
{ locale?: Locale; dict?: Dict | null; canDeliver?: boolean }) {
export default function PrintApp({ locale, dict, canDeliver = false, hasLibrary = true }:
{ locale?: Locale; dict?: Dict | null; canDeliver?: boolean; hasLibrary?: boolean }) {
const t = useT(dict);
const [cells, setCells] = useState<Cell[]>([]);
const [paperId, setPaperId] = useState('A4');
@@ -335,6 +335,17 @@ export default function PrintApp({ locale, dict, canDeliver = false }:
} catch { notify(t('These settings could not be applied.'), true); }
};
// A photo to try it with, for the first visit — before anyone has picked a
// picture of their own. It goes through the normal upload, so what follows is
// exactly what happens with a real image. (An original illustration made for
// Klarbild; public/samples/README.md says so.)
const addSample = async () => {
try {
const blob = await fetch('/samples/lake-at-dusk.jpg').then((r) => { if (!r.ok) throw new Error(); return r.blob(); });
await addFiles([new File([blob], 'lake-at-dusk.jpg', { type: 'image/jpeg' })]);
} catch { notify(t('The sample photo could not be loaded.')); }
};
const addFromLibrary = (it: any) => {
const [w, h] = String(it.output_px || '').split(/[x×]/).map((n: string) => parseInt(n, 10) || 0);
const id = uid();
@@ -500,7 +511,7 @@ export default function PrintApp({ locale, dict, canDeliver = false }:
<div className="col work-left">
<Card nr="1" title={t('Images')} actions={
<>
<button className="link" onClick={() => setPicker(true)}>{t('From the library')}</button>
{hasLibrary && <button className="link" onClick={() => setPicker(true)}>{t('From the library')}</button>}
{cells.length > 0 && (
<button className="link danger"
onClick={() => { if (confirm(t('Remove {images}?',
@@ -528,10 +539,20 @@ export default function PrintApp({ locale, dict, canDeliver = false }:
already in Klarbild and need not be exported and uploaded
again first. */}
<div className="or"><span>{t('or')}</span></div>
<button type="button" className="drop drop-lib" onClick={() => setPicker(true)}>
<span className="drop-title">{t('Pick from the library')}</span>
<span className="drop-hint">{t('Everything Klarbild has already made — with search, by folder, and several at once')}</span>
</button>
{hasLibrary ? (
<button type="button" className="drop drop-lib" onClick={() => setPicker(true)}>
<span className="drop-title">{t('Pick from the library')}</span>
<span className="drop-hint">{t('Everything Klarbild has already made — with search, by folder, and several at once')}</span>
</button>
) : (
// Print-only instance: there is no library to pick from. The
// second way in is a sample, so the first visit ends in a
// sheet and not in an empty box.
<button type="button" className="drop drop-lib" onClick={addSample}>
<span className="drop-title">{t('Try it with a sample photo')}</span>
<span className="drop-hint">{t('No picture at hand? Take ours, pick a preset below and make the PDF.')}</span>
</button>
)}
{presets.length > 0 && (
<div className="quickstart">
<span className="mono-label">{t('Or start from a preset')}</span>
@@ -559,7 +580,7 @@ export default function PrintApp({ locale, dict, canDeliver = false }:
))}
<div className="cell-add-group">
<button className="cell-add" onClick={() => fileRef.current?.click()}>{t('+ Upload')}</button>
<button className="cell-add" onClick={() => setPicker(true)}>{t('+ From the library')}</button>
{hasLibrary && <button className="cell-add" onClick={() => setPicker(true)}>{t('+ From the library')}</button>}
</div>
</div>
)}
+3
View File
@@ -564,6 +564,7 @@
"No mail sending is set up on this server yet — the address is stored, but nothing arrives yet. An admin sets that up under Admin → Email.": "Auf diesem Server ist noch kein E-Mail-Versand eingerichtet — die Adresse wird zwar gespeichert, es kommt aber noch nichts an. Ein Admin richtet das unter Admin → E-Mail ein.",
"No models yet. Add one from the available ones:": "Noch keine Modelle. Aus verfügbaren hinzufügen:",
"No paired chats yet.": "Noch keine gekoppelten Chats.",
"No picture at hand? Take ours, pick a preset below and make the PDF.": "Gerade kein Bild zur Hand? Nimm unseres, wähl unten eine Vorlage und mach das PDF.",
"No sending set up yet — forgotten password, job notices and mailing share links do not work until then.": "Noch kein Versand eingerichtet — Passwort vergessen, Auftragshinweise und der Mailversand von Teilen-Links funktionieren bis dahin nicht.",
"No token generated yet.": "Noch kein Token erzeugt.",
"No valid image size chosen yet.": "Noch kein gültiges Bildmaß gewählt.",
@@ -849,6 +850,7 @@
"The password is stored encrypted and never shown again.": "Das Passwort wird verschlüsselt gespeichert und nie wieder angezeigt.",
"The problem": "Das Problem",
"The same image once more — for a second format, say": "Dasselbe Bild noch einmal — z. B. für ein zweites Format",
"The sample photo could not be loaded.": "Das Beispielfoto ließ sich nicht laden.",
"The second way out for large sizes: the image is spread across several sheets. The tiles overlap by a glue flap, which is printed on both neighbouring sheets and marked with a dashed line. Every sheet carries its number with row and column.": "Der zweite Weg für große Maße: Das Bild wird auf mehrere Blätter verteilt. Die Kacheln überlappen sich um einen Klebefalz, der auf beiden Nachbarblättern gedruckt und gestrichelt markiert wird. Jedes Blatt trägt seine Nummer samt Reihe und Spalte.",
"The trash is empty": "Der Papierkorb ist leer",
"The trash is switched off — deleting is final.": "Der Papierkorb ist abgeschaltet — Löschen ist endgültig.",
@@ -878,6 +880,7 @@
"Trash · {n}": "Papierkorb · {n}",
"True to size": "Maßhaltig",
"Try again": "Noch einmal versuchen",
"Try it with a sample photo": "Mit einem Beispielfoto ausprobieren",
"US visa 51 × 51 mm (2 × 2 in)": "Visum USA 51 × 51 mm (2 × 2 in)",
"Under <b>“File names”</b> the existing library can be brought onto the unique naming scheme. <b>“Check library”</b> first shows what would change and how many names are currently used twice; <b>“Rename now”</b> carries it out. The image files themselves are left untouched — they sit in storage under their id, not under their name. A second run changes nothing. <b>“Deliver again”</b> then uploads every already delivered image under its new name; the old files stay on the delivery target and you clear them away there yourself.": "Unter <b>„Dateinamen\"</b> lässt sich die vorhandene Bibliothek auf das eindeutige Namensschema bringen. <b>„Bibliothek prüfen\"</b> zeigt zuerst, was sich ändern würde und wie viele Namen aktuell doppelt vergeben sind; <b>„Jetzt umbenennen\"</b> setzt es um. Die Bilddateien selbst bleiben dabei unangetastet — sie liegen im Speicher unter ihrer Kennung, nicht unter ihrem Namen. Ein zweiter Lauf ändert nichts mehr. <b>„Erneut ausliefern\"</b> lädt danach alle schon ausgelieferten Bilder unter dem neuen Namen erneut hoch; die alten Dateien bleiben am Ziel liegen und räumst du dort selbst weg.",
"Under the hood": "Unter der Haube",
+1 -1
View File
@@ -12,7 +12,7 @@ const dict = dictFor(locale);
<p class="page-lead">
{t('No AI: bring images to exact sizes and place several of them at 100% size with crop marks on one sheet — from the biometric passport photo through the school photo set to the 30 × 40 poster.')}
</p>
<PrintApp client:load locale={locale} dict={dict} canDeliver={has('delivery') || has('mirror')} />
<PrintApp client:load locale={locale} dict={dict} canDeliver={has('delivery') || has('mirror')} hasLibrary={has('ai')} />
<style>
.page-title{font-family:var(--font-display);font-size:1.6rem;margin:6px 0 4px;letter-spacing:-.02em;}
.page-lead{color:var(--soft);font-size:13.5px;margin:0 0 16px;max-width:64ch;line-height:1.55;}