import { useCallback, useEffect, useMemo, useState } from "react"; import * as sync from "./syncEngine"; import { useNoteData } from "./hooks/useNoteData"; import { useSearchIndex } from "./hooks/useSearchIndex"; import { useResponsive } from "./hooks/useResponsive"; import { usePwaInstall } from "./hooks/usePwaInstall"; import { useNotebookDnd } from "./hooks/useNotebookDnd"; import { useNavigationHistory } from "./hooks/useNavigationHistory"; import { CHAPTER_COLORS, DEFAULT_FOLDERS, coverBackground } from "./ui/constants.jsx"; import { I } from "./ui/icons.jsx"; import { makeTheme, FONT_UI } from "./ui/theme"; import { ConfirmDialog } from "./ui/ConfirmDialog.jsx"; import { NameDialog } from "./ui/NameDialog.jsx"; import { MoveNoteDialog } from "./ui/MoveNoteDialog.jsx"; import { ContextMenu } from "./ui/ContextMenu.jsx"; import { EditNotebookModal, CreateNotebookModal } from "./ui/NotebookModals.jsx"; import { AccountMenu } from "./ui/AccountMenu.jsx"; import { AuthScreen } from "./ui/AuthScreen.jsx"; import { AdminPanel } from "./ui/AdminPanel.jsx"; import { ExportDialog } from "./ui/ExportDialog.jsx"; import { NotebookPicker, rememberNotebook } from "./ui/NotebookPicker.jsx"; import { ImportDialog } from "./ui/ImportDialog.jsx"; import { Sidebar } from "./ui/Sidebar.jsx"; import { Dashboard } from "./features/notebooks/Dashboard.jsx"; import { NoteList } from "./features/notes/NoteList.jsx"; import { DEFAULT_SORT, canReorder, sortModesFor, sortNotes, separatorsFor, withNewSeparator, } from "./features/notes/noteSort"; import { EditorPane } from "./features/notes/EditorPane.jsx"; import { HistoryModal } from "./features/notes/HistoryModal.jsx"; import { DrawingLayer } from "./features/drawing/DrawingLayer.jsx"; import { CANON_W, CANON_H, PREVIEW_DRAWING_W, drawnSheets, sheetStrokes, } from "./features/drawing/strokes.js"; /** Views that are not a notebook and therefore cannot hold a new note. */ const VIRTUAL_FOLDERS = ["all", "starred", "trash", "dashboard"]; const EMPTY_BODY = { content: "", drawingData: null, drawingStrokes: null }; /** * Root of the application. * * Holds the things that describe *where the user is* — the open notebook, the * open note, the search box, whichever dialog is up — and hands the data itself * to `useNoteData`. Rendering is delegated to the three panes below. */ export default function App() { const [dark, setDark] = useState(true); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [syncStatus, setSyncStatus] = useState("synced"); const [activeFolder, setActiveFolder] = useState("dashboard"); const [activeNote, setActiveNote] = useState(null); /* Which chapter of the open notebook is being looked into; null is the notebook itself. A chapter is a folder inside a notebook, so this is a second step of "where the user is", not a second notebook. */ const [activeChapter, setActiveChapter] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [showNewNb, setShowNewNb] = useState(false); const [editingNb, setEditingNb] = useState(null); const [showAccount, setShowAccount] = useState(false); const [showAdmin, setShowAdmin] = useState(false); // { notes, label } — set while the export dialog is up. const [exporting, setExporting] = useState(null); // { title, run } — set while the app is asking which notebook to use. const [pickingNotebook, setPickingNotebook] = useState(null); // Set while files are being imported; holds the progress and then the result. const [importing, setImporting] = useState(null); /* Sort order for the views that are not notebooks. Those have nowhere to store a choice — "All Notes" is not a thing that can carry a setting — so it lasts for the session and no longer. */ const [virtualSort, setVirtualSort] = useState({}); // Which separator is being renamed. Held here because the context menu that // starts it lives here too. const [renamingSeparator, setRenamingSeparator] = useState(null); const [historyFor, setHistoryFor] = useState(null); const [ctxMenu, setCtxMenu] = useState(null); // { title, value, onSave } while a chapter is being named or renamed. const [namingChapter, setNamingChapter] = useState(null); // The note whose destination is being chosen, if any. const [movingNote, setMovingNote] = useState(null); const [confirmDialog, setConfirmDialog] = useState(null); // Bumped on a version restore to force a fresh editor on the restored body. const [restoreEpoch, setRestoreEpoch] = useState(0); const T = useMemo(() => makeTheme(dark), [dark]); const { isMobile, sidebarOpen, setSidebarOpen, mobileView, setMobileView } = useResponsive(); const { canInstall, reason: installReason, promptInstall } = usePwaInstall(); // Presentation only, and it lives on the account so it follows to the phone. const noteFont = user?.preferences?.noteFont || "serif"; /* How a chapter opens: a further column beside the list, or unfolded under its own row. A phone has room for neither, so there it fills the screen — which is what "column" already did before there was a choice. */ const chapterView = user?.preferences?.chapterView || "column"; const inlineChapters = chapterView === "inline" && !isMobile; const columnChapters = chapterView === "column" && !isMobile; const data = useNoteData(user, activeNote); const { notes, notebooks, noteContents, backlinks } = data; const search = useSearchIndex(user, notes, searchQuery); const dnd = useNotebookDnd(data.reorderNotebooks); /* Notes reorder through their own list, which is the only place that knows whether a hand-made arrangement is even in effect. Notebooks keep the HTML5 hook: they are dragged with a mouse, in the sidebar and on the dashboard. */ const rearrangeNotes = useCallback( (dragId, targetId) => data.rearrange(activeFolder, dragId, targetId, activeChapter), [data, activeFolder, activeChapter] ); useEffect(() => sync.onSyncStatus(setSyncStatus), []); /* ── Session ── */ useEffect(() => { (async () => { try { const u = await sync.restoreSession(); if (u) { setUser(u); await data.reload(); try { await sync.fullSync(); await data.reload(); } catch (e) { if (e.message === "AUTH") setUser(null); } } } catch { /* a broken session must not block the app from starting */ } setLoading(false); })(); }, []); const onLogin = async (u) => { setUser(u); try { await sync.fullSync(); } catch { /* first sync can fail offline; local data still loads */ } await data.reload(); }; const onLogout = async () => { await sync.logout(); setUser(null); data.reset(); setActiveNote(null); setActiveFolder("dashboard"); }; /* ── Navigation ── */ const openFolder = useCallback( (id) => { setActiveFolder(id); setActiveChapter(null); setActiveNote(null); if (isMobile) { setSidebarOpen(false); setMobileView("list"); } }, [isMobile, setSidebarOpen, setMobileView] ); const openNote = useCallback( (id) => { setActiveNote(id); if (isMobile) setMobileView("editor"); }, [isMobile, setMobileView] ); const closeNote = useCallback(() => { setActiveNote(null); if (isMobile) setMobileView("list"); }, [isMobile, setMobileView]); // Adopts a location the user reached with the browser's back button. const applyNavState = useCallback( (s) => { setActiveFolder(s.folder); setActiveNote(s.note); setMobileView(s.view); }, [setMobileView] ); useNavigationHistory({ folder: activeFolder, note: activeNote, view: mobileView }, isMobile, applyNavState); /* ── Derived note lists ── */ const activeNotes = useMemo(() => notes.filter((n) => !n.trashed), [notes]); /* Chapters of the notebook in hand, with how many notes each holds — the count is what makes a chapter worth opening rather than a guess. */ const folderChapters = useMemo(() => { const list = data.chapters.filter((c) => c.notebookId === activeFolder); return list.map((c) => ({ ...c, noteCount: notes.filter((n) => !n.trashed && n.chapterId === c.id).length, })); }, [data.chapters, activeFolder, notes]); const openChapter = folderChapters.find((c) => c.id === activeChapter) || null; const trashedNotes = useMemo(() => notes.filter((n) => n.trashed), [notes]); const isTrashView = activeFolder === "trash"; const activeNotebook = notebooks.find((nb) => nb.id === activeFolder) || null; /* A real notebook remembers its order on the server; a view keeps one for the session. Either way the note list is told a single mode. */ const rawSortMode = activeNotebook ? activeNotebook.sortMode || DEFAULT_SORT : virtualSort[activeFolder] || DEFAULT_SORT; // A view has nowhere to keep a hand-made arrangement, so it cannot be in one — // even if an older version of the app left "manual" behind on a notebook that // has since been looked at through "All Notes". const sortMode = !activeNotebook && rawSortMode === "manual" ? DEFAULT_SORT : rawSortMode; const setSortMode = useCallback( (mode) => { if (activeNotebook) void data.updateNotebook(activeNotebook.id, { sortMode: mode }); else setVirtualSort((p) => ({ ...p, [activeFolder]: mode })); }, [activeNotebook, activeFolder, data] ); /* ── Separators ── * Lines the user draws between notes. They belong to the notebook's own * arrangement, so they are stored on it and only ever shown in manual order. */ const allSeparators = activeNotebook?.separators || []; /* Only the ones drawn in the list being looked at — the notebook itself, or the chapter that is open. */ const separators = useMemo( () => separatorsFor(allSeparators, activeChapter), [allSeparators, activeChapter] ); const writeSeparators = (next) => activeNotebook && void data.updateNotebook(activeNotebook.id, { separators: next }); /* Written back whole: the notebook stores one list, and the ones belonging to the other chapters have to survive a change made in this one. */ const writeVisible = (visible) => writeSeparators([...allSeparators.filter((s) => (s.chapterId ?? null) !== activeChapter), ...visible]); const addSeparator = () => writeVisible(withNewSeparator(separators, `sep-${Date.now().toString(36)}`, "", activeChapter)); const renameSeparator = (id, label) => writeSeparators(allSeparators.map((s) => (s.id === id ? { ...s, label } : s))); const removeSeparator = (id) => writeSeparators(allSeparators.filter((s) => s.id !== id)); const separatorContextMenu = (e, separator) => { e.preventDefault(); e.stopPropagation(); setCtxMenu({ x: e.clientX, y: e.clientY, items: [ { label: "Rename", icon: I.edit, action: () => setRenamingSeparator(separator.id) }, { divider: true }, { label: "Remove separator", icon: I.trash, danger: true, action: () => confirm({ title: "Remove separator", message: separator.label ? `Remove the separator "${separator.label}"? The notes stay.` : "Remove this separator? The notes stay.", confirmLabel: "Remove", onConfirm: () => removeSeparator(separator.id), }), }, ], }); }; const isSearching = searchQuery.trim().length > 0; const filteredNotes = useMemo(() => { let list; if (activeFolder === "trash") list = trashedNotes; else if (activeFolder === "starred") list = activeNotes.filter((n) => n.starred); else if (activeFolder === "all" || activeFolder === "dashboard") list = activeNotes; else if (activeChapter && !inlineChapters && !columnChapters) list = activeNotes.filter((n) => n.chapterId === activeChapter); /* In the notebook itself, a note filed into a chapter is *in* that chapter and not loose here — otherwise it would appear twice and moving it would look like it had done nothing. */ else list = activeNotes.filter((n) => n.folderId === activeFolder && !n.chapterId); if (searchQuery.trim()) { // A search reads every note's text, so results keep the index's relevance // order instead of being re-sorted by date. if (search.results) { const rank = new Map(search.results.map((h, i) => [h.id, i])); return list.filter((n) => rank.has(n.id)).sort((a, b) => rank.get(a.id) - rank.get(b.id)); } // Until the index answers, fall back to matching titles only. const q = searchQuery.toLowerCase(); return list.filter((n) => n.title.toLowerCase().includes(q)); } return sortNotes(list, sortMode); }, [ activeFolder, activeChapter, inlineChapters, columnChapters, searchQuery, search.results, activeNotes, trashedNotes, sortMode, ]); /* The notes of the chapter being looked into, in the same order they would have in a list of their own. */ const chapterNotes = useMemo( () => (activeChapter ? sortNotes(activeNotes.filter((n) => n.chapterId === activeChapter), sortMode) : []), [activeChapter, activeNotes, sortMode] ); const noteMeta = activeNote ? notes.find((n) => n.id === activeNote) : null; // The editor opens as soon as the metadata is known; the body fills in when // it arrives, rather than blocking on a fetch. const openNoteData = noteMeta ? { ...noteMeta, ...EMPTY_BODY, ...noteContents[activeNote] } : null; const folderName = useMemo(() => { const builtIn = DEFAULT_FOLDERS.find((f) => f.id === activeFolder); if (builtIn) return builtIn.name; return notebooks.find((n) => n.id === activeFolder)?.name || "Notes"; }, [activeFolder, notebooks]); /* ── Note links ── */ // Candidates for the @-menu. Notes rank above notebooks, trashed notes never // appear, and the open note cannot link to itself. const searchLinkTargets = useCallback( (query) => { const q = (query || "").toLowerCase().trim(); const match = (name) => !q || name.toLowerCase().includes(q); return [ ...notes .filter((n) => !n.trashed && n.id !== activeNote && match(n.title || "Untitled")) .slice(0, 8) .map((n) => ({ id: n.id, label: n.title || "Untitled", kind: "note" })), ...notebooks .filter((nb) => match(nb.name)) .slice(0, 4) .map((nb) => ({ id: nb.id, label: nb.name, kind: "notebook" })), ]; }, [notes, notebooks, activeNote] ); // A link stores the label it had when inserted; this keeps it showing the // target's current title, and returns null once the target is gone. const resolveLinkLabel = useCallback( (id) => { const n = notes.find((x) => x.id === id && !x.trashed); if (n) return n.title || "Untitled"; return notebooks.find((x) => x.id === id)?.name ?? null; }, [notes, notebooks] ); const openLinkTarget = useCallback( (id, kind) => { if (kind === "notebook") return openFolder(id); const target = notes.find((n) => n.id === id); if (!target) return; if (target.folderId) setActiveFolder(target.folderId); openNote(id); }, [notes, openFolder, openNote] ); const linkProps = useMemo( () => ({ openLinkTarget, searchLinkTargets, resolveLinkLabel }), [openLinkTarget, searchLinkTargets, resolveLinkLabel] ); /* ── Actions ── */ /** * Runs something that needs a notebook to put a note in. * * "All Notes", "Starred" and the dashboard are views, not places. The app used * to answer that silently with the first notebook, so a note or an imported * file landed somewhere the user had not chosen and could only find by knowing * that rule. Now it asks — once, remembering the answer as the default. */ const withNotebook = useCallback( (title, run) => { if (!VIRTUAL_FOLDERS.includes(activeFolder)) { rememberNotebook(activeFolder); return run(activeFolder); } if (notebooks.length === 0) return; // Nothing to ask about when there is only one place it could go. if (notebooks.length === 1) return run(notebooks[0].id); setPickingNotebook({ title, run }); }, [activeFolder, notebooks] ); /** * Opens a file chooser. * * The input is created and thrown away per use rather than living in the * markup: a hidden input that persists keeps its previous selection, and * choosing the same file twice in a row would then fire no change event. */ const chooseFiles = (accept, multiple, onChosen) => { const input = document.createElement("input"); input.type = "file"; input.accept = accept; input.multiple = multiple; input.onchange = () => { const files = [...(input.files || [])]; if (files.length) onChosen(files); }; input.click(); }; const reportFailure = (message) => setConfirmDialog({ title: "Import failed", message, confirmLabel: "OK", onConfirm: () => setConfirmDialog(null), }); const importPdf = () => withNotebook("Upload PDF", (folderId) => chooseFiles("application/pdf,.pdf", false, async ([file]) => { try { const note = await data.importPdf(folderId, file); if (VIRTUAL_FOLDERS.includes(activeFolder)) setActiveFolder(folderId); openNote(note.id); } catch (e) { reportFailure(e?.message || "The file could not be imported."); } }) ); /** Markdown files, or a ZIP written by this app's export. */ const importMarkdown = () => withNotebook("Import notes", (folderId) => chooseFiles(".md,.markdown,.txt,.zip,text/markdown,application/zip", true, (files) => { if (VIRTUAL_FOLDERS.includes(activeFolder)) setActiveFolder(folderId); setImporting({ files, folderId }); }) ); const createNote = () => withNotebook("New note", async (folderId) => { /* A note made while a chapter is open belongs to that chapter — but only if it is a chapter of the notebook being written into, which it is not when the notebook had to be asked for. */ const chapterId = folderId === activeFolder ? activeChapter : null; const note = await data.createNote(folderId, chapterId); if (VIRTUAL_FOLDERS.includes(activeFolder)) setActiveFolder(folderId); openNote(note.id); }); /** Runs a destructive action behind a confirmation prompt. */ const confirm = (dialog) => setConfirmDialog({ ...dialog, onConfirm: async () => { await dialog.onConfirm(); setConfirmDialog(null); }, }); const trashNote = (id) => confirm({ title: "Move to Trash", message: `Move "${notes.find((n) => n.id === id)?.title}" to trash?`, confirmLabel: "Move to Trash", onConfirm: async () => { await data.trashNote(id); if (activeNote === id) closeNote(); }, }); const restoreNote = async (id) => { await data.restoreNote(id); if (activeNote === id) closeNote(); }; const permanentDeleteNote = (id) => confirm({ title: "Delete Permanently", message: `Permanently delete "${notes.find((n) => n.id === id)?.title}"? This cannot be undone.`, onConfirm: async () => { await data.permanentDeleteNote(id); if (activeNote === id) closeNote(); }, }); const emptyTrash = () => confirm({ title: "Empty Trash", message: `Permanently delete all ${trashedNotes.length} trashed notes?`, onConfirm: async () => { await data.emptyTrash(); setActiveNote(null); }, }); const createNotebook = async (fields) => { const nb = await data.createNotebook(fields); setShowNewNb(false); setActiveFolder(nb.id); }; const updateNotebook = async (id, updates) => { await data.updateNotebook(id, updates); setEditingNb(null); }; const deleteNotebook = (id) => { const nb = notebooks.find((n) => n.id === id); const count = activeNotes.filter((n) => n.folderId === id).length; confirm({ title: "Delete Notebook", message: `Delete "${nb?.name}"${count > 0 ? ` and trash its ${count} notes` : ""} permanently?`, onConfirm: async () => { await data.deleteNotebook(id); if (activeFolder === id) setActiveFolder("dashboard"); }, }); }; /* ── Context menus ── */ const notebookContextMenu = (e, nb) => { e.preventDefault(); e.stopPropagation(); setCtxMenu({ x: e.clientX, y: e.clientY, items: [ { label: "Edit", icon: I.edit, action: () => setEditingNb(nb) }, { label: "Export...", icon: I.export, action: () => setExporting({ label: nb.name, notes: activeNotes.filter((n) => n.folderId === nb.id), }), }, { divider: true }, { label: "Delete Notebook", icon: I.trash, danger: true, action: () => deleteNotebook(nb.id) }, ], }); }; /** * "Move to..." — the other notebooks, and the chapters of this one. * * Both in one list because both answer the same question: where should this * note live. A chapter of the notebook it is already in is one step; another * notebook is another, and takes it out of whatever chapter it was in, since * a chapter belongs to the book it is in. */ const moveSubmenu = (note) => { const here = data.chapters.filter((c) => c.notebookId === note.folderId && c.id !== note.chapterId); const targets = [ ...(note.chapterId ? [{ label: "The notebook itself", action: () => data.moveNote(note.id, note.folderId, null) }] : []), ...here.map((c) => ({ label: c.name, icon: I.books, action: () => data.moveNote(note.id, note.folderId, c.id), })), ...(here.length || note.chapterId ? [{ divider: true }] : []), ...notebooks .filter((nb) => nb.id !== note.folderId) .map((nb) => ({ label: nb.name, colorDot: coverBackground(nb.cover, nb.coverPattern), action: () => data.moveNote(note.id, nb.id, null), })), ]; // A lone divider is not a menu. const usable = targets.filter((t) => !t.divider); return usable.length > 0 ? [{ label: "Move to...", icon: I.move, submenu: targets }] : []; }; /* ── Chapters ── * Folders inside a notebook. Named on creation, because a chapter with no name * is a row nobody can tell from the next one — unlike a separator, which is a * line and reads perfectly well unnamed. */ const addChapter = () => { if (!activeNotebook) return; setNamingChapter({ title: "New chapter", value: "", onSave: (name) => data.createChapter(activeNotebook.id, name) }); }; const chapterContextMenu = (e, chapter) => { e.preventDefault(); e.stopPropagation(); setCtxMenu({ x: e.clientX, y: e.clientY, items: [ { label: "Rename", icon: I.edit, action: () => setNamingChapter({ title: "Rename chapter", value: chapter.name, onSave: (name) => data.renameChapter(chapter.id, name), }), }, { label: "Colour", icon: I.edit, submenu: [ { label: "None", colorDot: T.faint, action: () => data.recolourChapter(chapter.id, null) }, ...CHAPTER_COLORS.map((c) => ({ label: c.label, colorDot: c.value, action: () => data.recolourChapter(chapter.id, c.value), })), ], }, { divider: true }, { label: "Remove chapter", icon: I.trash, danger: true, action: () => setConfirmDialog({ title: "Remove chapter?", message: `"${chapter.name}" is removed. Its ${chapter.noteCount} note${ chapter.noteCount === 1 ? "" : "s" } stay — they go back to the notebook.`, confirmLabel: "Remove", danger: true, onConfirm: () => { if (activeChapter === chapter.id) setActiveChapter(null); void data.deleteChapter(chapter.id); }, }), }, ], }); }; const noteContextMenu = (e, note) => { e.preventDefault(); e.stopPropagation(); const items = note.trashed ? [ { label: "Restore", icon: I.restore, action: () => restoreNote(note.id) }, { divider: true }, { label: "Delete Permanently", icon: I.trash, danger: true, action: () => permanentDeleteNote(note.id), }, ] : [ { label: note.starred ? "Unstar" : "Star", icon: note.starred ? I.starFill : I.star, action: () => data.toggleStar(note.id), }, ...moveSubmenu(note), { label: "Export...", icon: I.export, action: () => setExporting({ label: note.title, notes: [note] }), }, { divider: true }, { label: "Move to Trash", icon: I.trash, danger: true, action: () => trashNote(note.id) }, ]; setCtxMenu({ x: e.clientX, y: e.clientY, items }); }; /* ── Render ── */ if (loading) return (
Loading...
); if (!user) return ; // On a phone the list and the editor take turns; on a desktop both are shown. /* A chapter's notes, unfolded under its own row. Indented and against a rule, so it reads as "inside this" rather than as more of the same list. */ const renderChapterNotes = (chapter) => (
{chapterNotes.length === 0 && (
Nothing in here yet.
)} {chapterNotes.map((n) => ( ))}
); const showList = !(isMobile && mobileView === "editor" && activeNote); const showEditor = !isMobile || mobileView === "editor"; return (
setDark(!dark)} isMobile={isMobile} open={sidebarOpen} onClose={() => setSidebarOpen(false)} activeFolder={activeFolder} onSelectFolder={openFolder} notebooks={notebooks} activeNotes={activeNotes} trashedCount={trashedNotes.length} onNewNotebook={() => setShowNewNb(true)} onOpenAccount={() => setShowAccount(true)} onNotebookContextMenu={notebookContextMenu} dnd={dnd} canInstall={canInstall} installReason={installReason} onInstall={promptInstall} syncStatus={syncStatus} online={sync.isOnline()} /> {activeFolder === "dashboard" ? ( setSidebarOpen(!sidebarOpen)} onOpenNotebook={openFolder} onOpenNote={(note) => { setActiveFolder(note.folderId); openNote(note.id); }} onNewNotebook={() => setShowNewNb(true)} onNotebookContextMenu={notebookContextMenu} dnd={dnd} /> ) : ( <> {showList && ( setSidebarOpen(!sidebarOpen)} onSelectNote={openNote} onNoteContextMenu={noteContextMenu} onCreateNote={createNote} onImportPdf={importPdf} onImportMarkdown={importMarkdown} onEmptyTrash={emptyTrash} /> )} {/* The chapter's own column, beside the list — the way OneNote lays out its pages. Only where there is room: on a phone the chapter fills the screen instead, with a way back. */} {columnChapters && openChapter && (
{I.books} {openChapter.name}
{chapterNotes.length === 0 && (
Nothing in here yet.
)} {chapterNotes.map((n) => ( ))}
)} {showEditor && ( data.updateNote(activeNote, u)} onToggleStar={() => data.toggleStar(activeNote)} onTrash={() => trashNote(activeNote)} onRestore={() => restoreNote(activeNote)} onPermanentDelete={() => permanentDeleteNote(activeNote)} onOpenHistory={() => setHistoryFor(activeNote)} onExport={() => noteMeta && setExporting({ label: noteMeta.title, notes: [noteMeta] })} onCreateNote={createNote} /> )} )} {showNewNb && ( setShowNewNb(false)} onCreate={createNotebook} dark={dark} /> )} {editingNb && ( setEditingNb(null)} onSave={(u) => updateNotebook(editingNb.id, u)} defaultMaxVersions={user?.defaultMaxVersions} dark={dark} /> )} {showAccount && ( setShowAccount(false)} onLogout={() => { setShowAccount(false); onLogout(); }} onUserUpdate={setUser} onOpenAdmin={() => { setShowAccount(false); setShowAdmin(true); }} onExportAll={() => { setShowAccount(false); setExporting({ label: "All Notes", notes: activeNotes }); }} dark={dark} /> )} {pickingNotebook && ( setPickingNotebook(null)} onPick={(id) => { const { run } = pickingNotebook; setPickingNotebook(null); run(id); }} /> )} {importing && ( data.reload()} onClose={() => setImporting(null)} /> )} {exporting && ( setExporting(null)} /> )} {showAdmin && user?.isAdmin && ( setShowAdmin(false)} dark={dark} /> )} {historyFor && ( n.id === historyFor)?.title} dark={dark} onClose={() => setHistoryFor(null)} onRestored={(content) => { data.setNoteContent(historyFor, content); setRestoreEpoch((e) => e + 1); }} /* The first drawn page stands for the drawing in a thumbnail this small. Asking for page one would show a blank sheet for a note whose ink starts on page three. */ renderDrawing={(doc) => ( )} /> )} {movingNote && ( data.moveNote(id, folderId, chapterId)} onClose={() => setMovingNote(null)} dark={dark} /> )} {namingChapter && ( setNamingChapter(null)} dark={dark} /> )} {confirmDialog && ( setConfirmDialog(null)} dark={dark} /> )} {ctxMenu && ( setCtxMenu(null)} dark={dark} /> )}
); }