Ask what a single note's export should contain, and fix drawings never being in one
Two questions instead of one, because the answers depend on each other: Markdown holds text only, a PNG holds the drawing only, PDF holds both. Listing every format flat would have offered combinations that cannot exist. A note without a drawing skips the first step rather than showing it mostly greyed out. While testing that, the drawings turned out never to have made it into any export at all — not the ZIP, not the PDF. strokeDocOf() *builds* a stroke document from a bare array, and it was being handed one that was already a document, so the strokes ended up nested one level too deep and every length check read undefined. Nothing complained; the export simply came out without the drawing. It went unnoticed because the notes the earlier suite exported had none. Now uses docStrokes(), and the suite draws on a note before exporting it, checks the PNG's magic bytes, and checks it is 159 KB rather than an empty white page — a check on the file merely existing would have passed against the broken version too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2611d87f8c
commit
8c36c6121b
5 files changed
+284
-57
No files matched your search
@@ -1,18 +1,31 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { exportNoteAsMarkdown, exportNotesAsPdf, exportNotesAsZip } from "../export/exportNotes";
|
||||
import {
|
||||
exportDrawingAsPng,
|
||||
exportNoteAsMarkdown,
|
||||
exportNotesAsPdf,
|
||||
exportNotesAsZip,
|
||||
noteHasDrawing,
|
||||
} from "../export/exportNotes";
|
||||
import { I } from "./icons.jsx";
|
||||
import { FONT_SERIF, FONT_UI } from "./theme";
|
||||
|
||||
/**
|
||||
* Export as Markdown or PDF — used for a single note, a notebook, or everything.
|
||||
* Export as Markdown, PDF or PNG.
|
||||
*
|
||||
* The formats are deliberately both offered rather than one being chosen for
|
||||
* you: Markdown keeps the notes editable somewhere else, PDF keeps them looking
|
||||
* the way they look here. Drawings only survive in PDF and as a PNG beside the
|
||||
* Markdown, which the dialog says out loud rather than leaving to be discovered.
|
||||
* A single note is asked two questions — what should come along, then in what
|
||||
* form — because the two are not independent: Markdown is text only, a PNG is
|
||||
* the drawing only, and PDF is the one format that holds both. Offering all
|
||||
* combinations flat would list options that cannot exist.
|
||||
*
|
||||
* A selection of several notes skips the first question; drawings there always
|
||||
* follow the format's own rule.
|
||||
*/
|
||||
export function ExportDialog({ notes, notebooks, label, dark, onClose }) {
|
||||
const single = notes.length === 1;
|
||||
|
||||
const [parts, setParts] = useState(single ? null : "both");
|
||||
const [hasDrawing, setHasDrawing] = useState(null); // null = still looking
|
||||
const [busy, setBusy] = useState(null);
|
||||
const [progress, setProgress] = useState(null);
|
||||
const [err, setErr] = useState("");
|
||||
@@ -33,33 +46,44 @@ export function ExportDialog({ notes, notebooks, label, dark, onClose }) {
|
||||
[dark]
|
||||
);
|
||||
|
||||
// A note without a drawing has nothing to choose, so the first step is skipped
|
||||
// rather than shown with two thirds of it disabled.
|
||||
useEffect(() => {
|
||||
if (!single) return;
|
||||
let cancelled = false;
|
||||
noteHasDrawing(notes[0]).then((yes) => {
|
||||
if (cancelled) return;
|
||||
setHasDrawing(yes);
|
||||
if (!yes) setParts("text");
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [single, notes]);
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e) => e.key === "Escape" && !busy && onClose();
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [busy, onClose]);
|
||||
|
||||
const single = notes.length === 1;
|
||||
|
||||
const run = async (kind) => {
|
||||
setErr("");
|
||||
setDone("");
|
||||
setBusy(kind);
|
||||
setProgress(null);
|
||||
try {
|
||||
if (kind === "markdown") {
|
||||
if (kind === "png") {
|
||||
await exportDrawingAsPng(notes[0]);
|
||||
setDone("Zeichnung wurde als PNG heruntergeladen.");
|
||||
} else if (kind === "markdown") {
|
||||
if (single) await exportNoteAsMarkdown(notes[0]);
|
||||
else await exportNotesAsZip({ label, notes, notebooks, onProgress: setProgress });
|
||||
setDone(single ? "Datei wurde heruntergeladen." : "Archiv wurde heruntergeladen.");
|
||||
} else {
|
||||
await exportNotesAsPdf(notes, label, setProgress);
|
||||
await exportNotesAsPdf(notes, label, setProgress, parts || "both");
|
||||
setDone("Druckansicht war geöffnet — wähle dort „Als PDF sichern“.");
|
||||
}
|
||||
setDone(
|
||||
kind === "markdown"
|
||||
? single
|
||||
? "Datei wurde heruntergeladen."
|
||||
: "Archiv wurde heruntergeladen."
|
||||
: "Druckansicht war geöffnet — wähle dort „Als PDF sichern“."
|
||||
);
|
||||
} catch (e) {
|
||||
setErr(e?.message || "Export fehlgeschlagen");
|
||||
}
|
||||
@@ -67,29 +91,30 @@ export function ExportDialog({ notes, notebooks, label, dark, onClose }) {
|
||||
setProgress(null);
|
||||
};
|
||||
|
||||
const option = (kind, title, lines) => (
|
||||
const card = (onClick, active, title, lines, disabled = false) => (
|
||||
<button
|
||||
onClick={() => run(kind)}
|
||||
disabled={!!busy}
|
||||
key={title}
|
||||
onClick={onClick}
|
||||
disabled={disabled || !!busy}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
gap: 4,
|
||||
width: "100%",
|
||||
padding: "14px 16px",
|
||||
padding: "13px 16px",
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${T.border}`,
|
||||
background: T.surface,
|
||||
cursor: busy ? "default" : "pointer",
|
||||
border: `1px solid ${active ? T.accent : T.border}`,
|
||||
background: active ? T.accent + "14" : T.surface,
|
||||
cursor: disabled || busy ? "default" : "pointer",
|
||||
textAlign: "left",
|
||||
opacity: busy && busy !== kind ? 0.45 : 1,
|
||||
opacity: disabled ? 0.4 : busy && busy !== active ? 0.5 : 1,
|
||||
marginBottom: 10,
|
||||
fontFamily: FONT_UI,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: T.text }}>{busy === kind ? "Läuft…" : title}</span>
|
||||
{lines.map((l, i) => (
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: T.text }}>{title}</span>
|
||||
{lines.filter(Boolean).map((l, i) => (
|
||||
<span key={i} style={{ fontSize: 12, color: T.muted, lineHeight: 1.5 }}>
|
||||
{l}
|
||||
</span>
|
||||
@@ -97,6 +122,84 @@ export function ExportDialog({ notes, notebooks, label, dark, onClose }) {
|
||||
</button>
|
||||
);
|
||||
|
||||
/* ── step one: what should come along ── */
|
||||
const stepParts = () => (
|
||||
<>
|
||||
<div style={{ fontSize: 12, color: T.muted, marginBottom: 10 }}>Was soll mit?</div>
|
||||
{card(() => setParts("both"), parts === "both", "Notiz mit Zeichnung", [
|
||||
"Text und Zeichnung in einem Dokument — nur als PDF möglich.",
|
||||
])}
|
||||
{card(() => setParts("text"), parts === "text", "Nur den Notiztext", [
|
||||
"Ohne die Zeichnung. Als PDF oder als Markdown.",
|
||||
])}
|
||||
{card(() => setParts("drawing"), parts === "drawing", "Nur die Zeichnung", [
|
||||
"Als PNG-Bild oder als PDF-Seite.",
|
||||
])}
|
||||
</>
|
||||
);
|
||||
|
||||
/* ── step two: in what form ── */
|
||||
const stepFormat = () => {
|
||||
const options = [];
|
||||
if (parts !== "drawing") {
|
||||
options.push(
|
||||
card(
|
||||
() => run("markdown"),
|
||||
busy === "markdown",
|
||||
single ? "Markdown (.md)" : "Markdown-Archiv (ZIP)",
|
||||
[
|
||||
single
|
||||
? "Bleibt anderswo bearbeitbar. Tabellen und Aufgabenhaken kommen mit."
|
||||
: "Je Notiz eine .md-Datei, dazu Bilder und ein Inhaltsverzeichnis.",
|
||||
parts === "both" ? "Die Zeichnung liegt als PNG daneben — Markdown kann sie nicht halten." : null,
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
if (parts === "drawing") {
|
||||
options.push(
|
||||
card(() => run("png"), busy === "png", "PNG-Bild (.png)", [
|
||||
"Die Zeichnung in voller Auflösung, auf weißem Grund.",
|
||||
])
|
||||
);
|
||||
}
|
||||
options.push(
|
||||
card(() => run("pdf"), busy === "pdf", "PDF", [
|
||||
"Öffnet den Druckdialog — dort „Als PDF sichern“ wählen.",
|
||||
parts === "both" ? "Die Zeichnung steht mit im Dokument." : null,
|
||||
])
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{single && hasDrawing && (
|
||||
<button
|
||||
onClick={() => setParts(null)}
|
||||
disabled={!!busy}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
padding: 0,
|
||||
marginBottom: 12,
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
cursor: busy ? "default" : "pointer",
|
||||
color: T.accent,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{I.back} Zurück
|
||||
</button>
|
||||
)}
|
||||
<div style={{ fontSize: 12, color: T.muted, marginBottom: 10 }}>In welcher Form?</div>
|
||||
{options}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const subtitle = single ? label : `${label} · ${notes.length} Notiz${notes.length === 1 ? "" : "en"}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -134,12 +237,21 @@ export function ExportDialog({ notes, notebooks, label, dark, onClose }) {
|
||||
borderBottom: `1px solid ${T.border}`,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<h3 style={{ margin: 0, fontSize: 17, fontWeight: 600, color: T.text, fontFamily: FONT_SERIF }}>
|
||||
Exportieren
|
||||
</h3>
|
||||
<div style={{ fontSize: 12, color: T.muted, marginTop: 2 }}>
|
||||
{single ? label : `${label} · ${notes.length} Notizen`}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: T.muted,
|
||||
marginTop: 2,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{subtitle}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@@ -155,6 +267,7 @@ export function ExportDialog({ notes, notebooks, label, dark, onClose }) {
|
||||
background: "transparent",
|
||||
cursor: busy ? "default" : "pointer",
|
||||
color: T.muted,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{I.x}
|
||||
@@ -162,19 +275,16 @@ export function ExportDialog({ notes, notebooks, label, dark, onClose }) {
|
||||
</div>
|
||||
|
||||
<div style={{ padding: "16px 22px 8px" }}>
|
||||
{option("markdown", single ? "Als Markdown" : "Als Markdown-Archiv (ZIP)", [
|
||||
single
|
||||
? "Eine .md-Datei — Bilder und Zeichnung kommen als ZIP mit, falls vorhanden."
|
||||
: "Je Notiz eine .md-Datei, dazu Bilder, Zeichnungen und ein Inhaltsverzeichnis.",
|
||||
"Bleibt anderswo bearbeitbar. Tabellen und Aufgabenhaken bleiben erhalten.",
|
||||
])}
|
||||
{option("pdf", "Als PDF", [
|
||||
"Öffnet den Druckdialog — dort „Als PDF sichern“ wählen.",
|
||||
"Zeichnungen sind hier mit im Dokument.",
|
||||
])}
|
||||
{single && hasDrawing === null ? (
|
||||
<div style={{ fontSize: 13, color: T.muted, padding: "12px 0" }}>Notiz wird gelesen…</div>
|
||||
) : parts === null ? (
|
||||
stepParts()
|
||||
) : (
|
||||
stepFormat()
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(progress || err || done) && (
|
||||
{parts !== null && (progress || err || done) && (
|
||||
<div style={{ padding: "0 22px 8px" }}>
|
||||
{progress && (
|
||||
<div style={{ fontSize: 12, color: T.muted }}>
|
||||
|
||||
Reference in new issue
Block a user