feat: sheet templates, margin profiles and delivering the sheet
Seeds the four sheet setups Till actually prints (Kita set, small school set, full passport sheet, two 10x7,5 on 10x15 paper). A multi-format template applied to a single image clones it into every format, so one click yields the whole set. Margin gets one-click borderless / 5 mm / 10 mm profiles, and the finished PDF can be pushed to Picdrop, the NAS or any extra target.
This commit is contained in:
6 files changed
+125
-15
No files matched your search
+51
-11
@@ -70,6 +70,9 @@ export default function PrintApp() {
|
||||
const [ext, setExt] = useState<'jpg' | 'png'>('jpg');
|
||||
const [center, setCenter] = useState(true);
|
||||
const [footer, setFooter] = useState(true);
|
||||
const [deliverTo, setDeliverTo] = useState<string>(''); // '' = nur herunterladen
|
||||
const [gallery, setGallery] = useState('');
|
||||
const [targets, setTargets] = useState<any[]>([]);
|
||||
|
||||
const [editing, setEditing] = useState<string | null>(null);
|
||||
const [picker, setPicker] = useState(false);
|
||||
@@ -149,7 +152,10 @@ export default function PrintApp() {
|
||||
return () => window.removeEventListener('paste', onPaste);
|
||||
}, [addFiles]);
|
||||
|
||||
useEffect(() => { loadPresets(); }, []);
|
||||
useEffect(() => {
|
||||
loadPresets();
|
||||
fetch('/api/delivery-targets').then((r) => r.json()).then((j) => setTargets(j.targets || [])).catch(() => {});
|
||||
}, []);
|
||||
const loadPresets = () => fetch('/api/print/presets').then((r) => r.json())
|
||||
.then((j) => setPresets(j.presets || [])).catch(() => {});
|
||||
|
||||
@@ -183,6 +189,7 @@ export default function PrintApp() {
|
||||
paper: paperId === 'custom' ? { size: paperCustom } : { id: paperId },
|
||||
landscape: paperLandscape,
|
||||
marginMm, gapMm: gapEff, bleedMm, center, dpi, ext, footer,
|
||||
...(deliverTo ? { deliver: { target: deliverTo === 'picdrop' ? '' : deliverTo, gallery: gallery.trim() || undefined } } : {}),
|
||||
marks: { mode: marks, lengthMm: markLen, offsetMm: markOff },
|
||||
cells: cells.filter((c) => c.src && !c.error && sizeOfCell(c)).map((c) => {
|
||||
const s = sizeOfCell(c)!;
|
||||
@@ -200,7 +207,9 @@ export default function PrintApp() {
|
||||
if (!res.ok) { notify((await res.json().catch(() => ({}))).error || 'Fehlgeschlagen.'); return; }
|
||||
const blob = await res.blob();
|
||||
download(blob, `klarbild-druckbogen-${new Date().toISOString().slice(0, 10)}.pdf`);
|
||||
notify(`PDF erzeugt · ${res.headers.get('X-Klarbild-Pages') || '?'} Bogen. Beim Drucken „Tatsächliche Größe / 100 %" wählen.`);
|
||||
const dmsgRaw = res.headers.get('X-Klarbild-Delivery-Msg');
|
||||
const dmsg = dmsgRaw ? ` ${decodeURIComponent(dmsgRaw)}` : '';
|
||||
notify(`PDF erzeugt · ${res.headers.get('X-Klarbild-Pages') || '?'} Bogen. Beim Drucken „Tatsächliche Größe / 100 %" wählen.${dmsg}`);
|
||||
} catch { notify('Netzwerkfehler.'); } finally { setBusy(null); }
|
||||
};
|
||||
|
||||
@@ -227,7 +236,7 @@ export default function PrintApp() {
|
||||
if (!name?.trim()) return;
|
||||
const config = {
|
||||
paperId, paperCustom, paperLandscape, marginMm, autoGap, gapMm, bleedMm,
|
||||
marks, markLen, markOff, dpi, ext, center, footer,
|
||||
marks, markLen, markOff, dpi, ext, center, footer, deliverTo, gallery,
|
||||
formats: cells.map((c) => ({ sizeId: c.sizeId, customSize: c.customSize, landscape: c.landscape, count: c.count, allowRotate: c.allowRotate })),
|
||||
};
|
||||
await fetch('/api/print/presets', {
|
||||
@@ -243,16 +252,26 @@ export default function PrintApp() {
|
||||
setMarginMm(c.marginMm ?? 5); setAutoGap(c.autoGap !== false); setGapMm(c.gapMm ?? 12);
|
||||
setBleedMm(c.bleedMm ?? 0); setMarks(c.marks ?? 'corner'); setMarkLen(c.markLen ?? 4); setMarkOff(c.markOff ?? 3);
|
||||
setDpi(c.dpi ?? 300); setExt(c.ext === 'png' ? 'png' : 'jpg'); setCenter(c.center !== false); setFooter(c.footer !== false);
|
||||
// Formate auf die vorhandenen Bilder anwenden (der Reihe nach).
|
||||
setDeliverTo(c.deliverTo ?? ''); setGallery(c.gallery ?? '');
|
||||
// Formate anwenden. Der typische Fall ist ein Bild und mehrere Größen
|
||||
// („Kita-Satz": 1× 13×18, 2× 9×13, 8× Passbild) — dann wird das Bild geklont.
|
||||
if (Array.isArray(c.formats) && c.formats.length) {
|
||||
setCells((prev) => prev.map((cell, i) => {
|
||||
const f = c.formats[Math.min(i, c.formats.length - 1)];
|
||||
const next = { ...cell, sizeId: f.sizeId, customSize: f.customSize || '', landscape: !!f.landscape, count: f.count || 1, allowRotate: f.allowRotate !== false };
|
||||
const a = aspectOf(next); if (a) next.crop = coverCrop(next.natW, next.natH, a, cell.crop);
|
||||
return next;
|
||||
}));
|
||||
setCells((prev) => {
|
||||
if (!prev.length) return prev;
|
||||
const apply = (cell: Cell, f: any): Cell => {
|
||||
const next = { ...cell, id: cell.id, sizeId: f.sizeId, customSize: f.customSize || '',
|
||||
landscape: !!f.landscape, count: f.count || 1, allowRotate: f.allowRotate !== false };
|
||||
const a = aspectOf(next); if (a) next.crop = coverCrop(next.natW, next.natH, a, cell.crop);
|
||||
return next;
|
||||
};
|
||||
if (prev.length === 1 && c.formats.length > 1)
|
||||
return c.formats.map((f: any, i: number) => apply({ ...prev[0], id: i === 0 ? prev[0].id : uid() }, f));
|
||||
return prev.map((cell, i) => apply(cell, c.formats[Math.min(i, c.formats.length - 1)]));
|
||||
});
|
||||
}
|
||||
notify('Vorlage geladen.');
|
||||
notify(Array.isArray(c.formats) && c.formats.length > 1
|
||||
? 'Vorlage geladen — das Bild wurde für jedes Format übernommen.'
|
||||
: 'Vorlage geladen.');
|
||||
};
|
||||
|
||||
const deletePreset = async (id: string) => {
|
||||
@@ -368,6 +387,11 @@ export default function PrintApp() {
|
||||
|
||||
<div className="feld">
|
||||
<label>Ränder & Abstände</label>
|
||||
<div className="schalter zart">
|
||||
<button className={marginMm === 0 ? 'an' : ''} onClick={() => setMarginMm(0)}>Randlos</button>
|
||||
<button className={marginMm === 5 ? 'an' : ''} onClick={() => setMarginMm(5)}>Standard 5 mm</button>
|
||||
<button className={marginMm === 10 ? 'an' : ''} onClick={() => setMarginMm(10)}>Sicher 10 mm</button>
|
||||
</div>
|
||||
<div className="reihe zwei">
|
||||
<label className="mini">Rand zum Papier (mm)
|
||||
<input className="input" type="number" min={0} max={40} step={0.5} value={marginMm}
|
||||
@@ -412,6 +436,21 @@ export default function PrintApp() {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="feld">
|
||||
<label>Wohin?</label>
|
||||
<select className="select" value={deliverTo} onChange={(e) => setDeliverTo(e.target.value)}>
|
||||
<option value="">Nur herunterladen</option>
|
||||
<option value="picdrop">Zusätzlich an Picdrop (Standard)</option>
|
||||
<option value="nas">Zusätzlich aufs NAS</option>
|
||||
{targets.map((t) => <option key={t.id} value={t.id}>Zusätzlich an „{t.name}"</option>)}
|
||||
</select>
|
||||
{deliverTo && (
|
||||
<input className="input" style={{ marginTop: 7 }} value={gallery} onChange={(e) => setGallery(e.target.value)}
|
||||
placeholder="Galerie/Ordner (leer = Standard)" />
|
||||
)}
|
||||
<div className="fein">Der fertige Bogen wird als PDF geladen — auf Wunsch zusätzlich ins gewohnte Ziel gelegt.</div>
|
||||
</div>
|
||||
|
||||
<button className="knopf" disabled={!plan?.pages.length || busy === 'pdf'} onClick={makePdf}>
|
||||
{busy === 'pdf' ? <><span className="spin" />PDF wird gebaut …</> : 'Druck-PDF erzeugen'}
|
||||
</button>
|
||||
@@ -722,6 +761,7 @@ function PrintStyles() {
|
||||
.schalter{display:flex;gap:6px;margin-top:8px;}
|
||||
.schalter button{flex:1;background:#fff;border:1px solid var(--line);border-radius:3px;padding:8px;cursor:pointer;font-family:inherit;font-size:13px;color:var(--soft);}
|
||||
.schalter button.an{border-color:var(--accent);background:var(--accent-bg);color:var(--ink);font-weight:600;}
|
||||
.schalter.zart button{font-size:12px;padding:6px 4px;}
|
||||
.schalter.winzig{margin:0;flex:1;}
|
||||
.schalter.winzig button{padding:5px 4px;font-size:11.5px;}
|
||||
.pbtn{background:#fff;border:1px solid var(--line);border-radius:3px;padding:8px 10px;cursor:pointer;font-family:inherit;font-size:12px;color:var(--ink);white-space:nowrap;}
|
||||
|
||||
Reference in new issue
Block a user