The paper measurably shrank on switching to Draw — 733px to 460px on the same screen. The editor was a flex item at its default `flex: 0 1 auto`, so its width was its widest row's: the toolbar. Swapping a dozen formatting buttons for six drawing ones collapsed it. It now fills the pane, so the width is a property of the window. Each notebook remembers how its notes are ordered, on the server rather than per device. The manual order has to survive being looked away from, so `reindex` renumbers every note in the notebook rather than the filtered view, and dragging is switched off while a search narrows the list. Reordering has its own route: `PUT /notes/:id` would bump `updated_at` and snapshot a version, so a drag would move a note to the top of "last changed" and spend a slot of its history. Markdown and our own ZIP archives come back as notes, with their images re-uploaded as attachments. A drawing returns as the PNG it was exported as — vectors are not recoverable from pixels — and the import says so rather than leaving it to be discovered. Writing the parser turned up a real bug in the exporter: blocks were emitted with no blank line between them, so a list after a list became one list and a quote after an item was read as part of it. Creating or importing from "All Notes" now asks which notebook instead of silently taking the first one. The web-link prompt is a real dialog: a bare domain becomes https, javascript: is refused with a reason. The PDF viewer's hide-drawing control is a labelled button rather than a filled-or-hollow dot, and the export icon points down into a tray like every other download. 186 unit tests, 179 browser checks across eleven suites, including a new `offline` suite that loads the app cold with the network off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
166 lines
5.5 KiB
React
166 lines
5.5 KiB
React
import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
import { importFiles } from "../import/importNotes";
|
|
|
|
/**
|
|
* Runs an import and says plainly what came of it.
|
|
*
|
|
* The report is the point. An import can succeed in the sense that no error was
|
|
* thrown and still not be what the user expected — a drawing arrives as a flat
|
|
* picture, an image the archive did not contain cannot be restored. Those are
|
|
* stated afterwards rather than left to be discovered, because the moment to
|
|
* learn that a drawing is no longer editable is now, while the original export
|
|
* is still on disk.
|
|
*/
|
|
export function ImportDialog({ files, folderId, onDone, onClose, dark }) {
|
|
const [progress, setProgress] = useState({ done: 0, total: 0, label: "" });
|
|
const [result, setResult] = useState(null);
|
|
const [error, setError] = useState("");
|
|
const started = useRef(false);
|
|
|
|
const T = useMemo(
|
|
() => ({
|
|
bg: dark ? "#161b22" : "#fff",
|
|
border: dark ? "#30363d" : "#d0d7de",
|
|
text: dark ? "#e6edf3" : "#1f2328",
|
|
muted: dark ? "#8b949e" : "#656d76",
|
|
accent: dark ? "#388bfd" : "#0969da",
|
|
warning: dark ? "#d29922" : "#9a6700",
|
|
danger: dark ? "#f85149" : "#cf222e",
|
|
track: dark ? "#21262d" : "#e5e7eb",
|
|
}),
|
|
[dark]
|
|
);
|
|
|
|
useEffect(() => {
|
|
// React runs effects twice in development; importing twice would create
|
|
// every note a second time.
|
|
if (started.current) return;
|
|
started.current = true;
|
|
|
|
importFiles(files, folderId, setProgress)
|
|
.then((r) => {
|
|
setResult(r);
|
|
onDone?.(r);
|
|
})
|
|
.catch((e) => setError(e?.message || "Der Import ist fehlgeschlagen."));
|
|
}, [files, folderId, onDone]);
|
|
|
|
const done = result || error;
|
|
const pct = progress.total ? Math.round((progress.done / progress.total) * 100) : 0;
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
position: "fixed",
|
|
inset: 0,
|
|
background: "rgba(0,0,0,0.5)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
zIndex: 2000,
|
|
backdropFilter: "blur(4px)",
|
|
}}
|
|
onClick={done ? onClose : undefined}
|
|
>
|
|
<div
|
|
onClick={(e) => e.stopPropagation()}
|
|
style={{
|
|
background: T.bg,
|
|
borderRadius: 16,
|
|
padding: 24,
|
|
width: 440,
|
|
maxWidth: "92vw",
|
|
border: `1px solid ${T.border}`,
|
|
boxShadow: "0 20px 40px rgba(0,0,0,0.3)",
|
|
fontFamily: "'DM Sans',sans-serif",
|
|
color: T.text,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
fontSize: 17,
|
|
fontWeight: 600,
|
|
marginBottom: 14,
|
|
fontFamily: "'Newsreader',Georgia,serif",
|
|
}}
|
|
>
|
|
{done ? "Import abgeschlossen" : "Notizen werden importiert…"}
|
|
</div>
|
|
|
|
{!done && (
|
|
<>
|
|
<div style={{ height: 6, borderRadius: 3, background: T.track, overflow: "hidden" }}>
|
|
<div
|
|
style={{
|
|
width: `${pct}%`,
|
|
height: "100%",
|
|
background: T.accent,
|
|
transition: "width .2s",
|
|
}}
|
|
/>
|
|
</div>
|
|
<div style={{ fontSize: 12, color: T.muted, marginTop: 8 }}>
|
|
{progress.total ? `${progress.done} von ${progress.total}` : "Archiv wird gelesen…"}
|
|
{progress.label ? ` — ${progress.label}` : ""}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{error && <div style={{ fontSize: 13, color: T.danger }}>{error}</div>}
|
|
|
|
{result && (
|
|
<div style={{ fontSize: 13, lineHeight: 1.7 }}>
|
|
<div>
|
|
<strong>{result.notes.length}</strong>{" "}
|
|
{result.notes.length === 1 ? "Notiz importiert" : "Notizen importiert"}
|
|
</div>
|
|
|
|
{result.flattenedDrawings > 0 && (
|
|
<div style={{ color: T.warning, marginTop: 10, fontSize: 12.5 }}>
|
|
{result.flattenedDrawings === 1
|
|
? "Eine Zeichnung kam als Bild zurück"
|
|
: `${result.flattenedDrawings} Zeichnungen kamen als Bilder zurück`}{" "}
|
|
— im Archiv liegen sie als PNG, und aus Pixeln lassen sich die Striche nicht zurückrechnen.
|
|
Sie sind sichtbar und exportierbar, aber nicht mehr mit dem Stift zu bearbeiten.
|
|
</div>
|
|
)}
|
|
|
|
{result.missingImages > 0 && (
|
|
<div style={{ color: T.warning, marginTop: 10, fontSize: 12.5 }}>
|
|
{result.missingImages} {result.missingImages === 1 ? "Bild lag" : "Bilder lagen"} nicht im
|
|
Archiv. Die Stelle in der Notiz bleibt erhalten, das Bild fehlt.
|
|
</div>
|
|
)}
|
|
|
|
{result.failed.length > 0 && (
|
|
<div style={{ color: T.danger, marginTop: 10, fontSize: 12.5 }}>
|
|
Nicht gelesen: {result.failed.join(", ")}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ display: "flex", justifyContent: "flex-end", marginTop: 20 }}>
|
|
<button
|
|
onClick={onClose}
|
|
disabled={!done}
|
|
style={{
|
|
padding: "8px 20px",
|
|
borderRadius: 10,
|
|
border: "none",
|
|
background: done ? T.accent : T.track,
|
|
color: done ? "#fff" : T.muted,
|
|
cursor: done ? "pointer" : "default",
|
|
fontSize: 13,
|
|
fontWeight: 600,
|
|
}}
|
|
>
|
|
{done ? "Fertig" : "Läuft…"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|