diff --git a/src/App.tsx b/src/App.tsx index eefb22a..fff6df6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,173 +1,12 @@ -import { useCallback, useEffect, useState } from "react"; -import type { Editor as TiptapEditor } from "@tiptap/react"; -import { Sidebar } from "./components/Sidebar"; -import { Dock } from "./components/Dock"; -import { Icon } from "./components/Icon"; -import { Settings } from "./components/Settings"; -import { Editor } from "./editor/Editor"; -import { FloatingToolbar } from "./editor/FloatingToolbar"; +import { Library } from "./components/Library"; +import { EditorView } from "./components/EditorView"; import { useBook } from "./store/useBook"; -import type { Book } from "./model/book"; -import { chooseSavePath, openBook, writeBook } from "./project"; -import { exportEpub, exportPdf } from "./export/exporters"; -import { isDesktop } from "./ipc"; - -const DESKTOP_ONLY = - 'This runs in the desktop app only — open the window from "pnpm tauri dev" (you are viewing the browser preview).'; function App() { - const [dock, setDock] = useState(true); - const [editor, setEditor] = useState(null); - const [settingsOpen, setSettingsOpen] = useState(false); - const [exportOpen, setExportOpen] = useState(false); - const [notice, setNotice] = useState(null); - const title = useBook((s) => s.book.metadata.title); - const author = useBook((s) => s.book.metadata.author); - const chapters = useBook((s) => s.book.chapters); - const activeChapterId = useBook((s) => s.activeChapterId); - const dirty = useBook((s) => s.dirty); - const setChapterContent = useBook((s) => s.setChapterContent); - const setChapterTitle = useBook((s) => s.setChapterTitle); - const replaceBook = useBook((s) => s.replaceBook); - const markSaved = useBook((s) => s.markSaved); + const book = useBook((s) => s.book); + const openBook = useBook((s) => s.openBook); - const idx = chapters.findIndex((c) => c.id === activeChapterId); - const chapter = chapters[idx]; - - const handleSave = useCallback(async () => { - if (!isDesktop) { - setNotice(DESKTOP_ONLY); - return; - } - try { - const { book, path } = useBook.getState(); - const target = path ?? (await chooseSavePath(book)); - if (!target) return; - await writeBook(book, target); - markSaved(target); - setNotice("Saved"); - } catch (e) { - setNotice(`Save failed: ${e}`); - } - }, [markSaved]); - - const handleOpen = useCallback(async () => { - if (!isDesktop) { - setNotice(DESKTOP_ONLY); - return; - } - try { - const result = await openBook(); - if (result) replaceBook(result.book, result.path); - } catch (e) { - setNotice(`Open failed: ${e}`); - } - }, [replaceBook]); - - const runExport = async (label: string, fn: (book: Book) => Promise) => { - setExportOpen(false); - if (!isDesktop) { - setNotice(DESKTOP_ONLY); - return; - } - try { - await fn(useBook.getState().book); - } catch (e) { - setNotice(`${label} export failed: ${e}`); - } - }; - - useEffect(() => { - const onKey = (e: KeyboardEvent) => { - const mod = e.metaKey || e.ctrlKey; - if (mod && e.key.toLowerCase() === "s") { - e.preventDefault(); - handleSave(); - } - if (mod && e.key.toLowerCase() === "o") { - e.preventDefault(); - handleOpen(); - } - }; - window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); - }, [handleSave, handleOpen]); - - useEffect(() => { - if (!notice) return; - const timer = setTimeout(() => setNotice(null), 4000); - return () => clearTimeout(timer); - }, [notice]); - - return ( -
-
- -
- - -
- - {exportOpen && ( - <> -
setExportOpen(false)} /> -
- - -
- - )} -
- -
-
- -
- -
-
-
-
Chapter {idx + 1}
- setChapterTitle(chapter.id, e.target.value)} - /> -
- setChapterContent(chapter.id, content)} - onReady={setEditor} - /> -
- -
- {dock && } -
- - {settingsOpen && setSettingsOpen(false)} />} - {notice && ( -
setNotice(null)}> - {notice} -
- )} -
- ); + return book ? : ; } export default App; diff --git a/src/components/EditorView.tsx b/src/components/EditorView.tsx new file mode 100644 index 0000000..3c38e9e --- /dev/null +++ b/src/components/EditorView.tsx @@ -0,0 +1,142 @@ +import { useCallback, useEffect, useState } from "react"; +import type { Editor as TiptapEditor } from "@tiptap/react"; +import { Sidebar } from "./Sidebar"; +import { Dock } from "./Dock"; +import { Icon } from "./Icon"; +import { Settings } from "./Settings"; +import { Editor } from "../editor/Editor"; +import { FloatingToolbar } from "../editor/FloatingToolbar"; +import { useBook } from "../store/useBook"; +import type { Book } from "../model/book"; +import { saveBook } from "../library"; +import { isDesktop } from "../ipc"; +import { exportEpub, exportPdf } from "../export/exporters"; + +const DESKTOP_ONLY = + 'Export runs in the desktop app only — open the window from "pnpm tauri dev" (you are viewing the browser preview).'; + +export function EditorView() { + const book = useBook((s) => s.book); + const activeChapterId = useBook((s) => s.activeChapterId); + const dirty = useBook((s) => s.dirty); + const setChapterContent = useBook((s) => s.setChapterContent); + const setChapterTitle = useBook((s) => s.setChapterTitle); + const markSaved = useBook((s) => s.markSaved); + + const [editor, setEditor] = useState(null); + const [dock, setDock] = useState(true); + const [settingsOpen, setSettingsOpen] = useState(false); + const [exportOpen, setExportOpen] = useState(false); + const [notice, setNotice] = useState(null); + + const saveNow = useCallback(() => { + const current = useBook.getState().book; + if (current) saveBook(current).then(markSaved).catch((e) => setNotice(`Save failed: ${e}`)); + }, [markSaved]); + + useEffect(() => { + if (!book || !dirty || !isDesktop) return; + const timer = setTimeout(saveNow, 800); + return () => clearTimeout(timer); + }, [book, dirty, saveNow]); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "s") { + e.preventDefault(); + saveNow(); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [saveNow]); + + useEffect(() => { + if (!notice) return; + const timer = setTimeout(() => setNotice(null), 4000); + return () => clearTimeout(timer); + }, [notice]); + + const runExport = async (label: string, fn: (book: Book) => Promise) => { + setExportOpen(false); + if (!isDesktop) { + setNotice(DESKTOP_ONLY); + return; + } + const current = useBook.getState().book; + if (!current) return; + try { + await fn(current); + } catch (e) { + setNotice(`${label} export failed: ${e}`); + } + }; + + if (!book) return null; + const idx = book.chapters.findIndex((c) => c.id === activeChapterId); + const chapter = book.chapters[idx] ?? book.chapters[0]; + + return ( +
+
+ +
+
+ + {exportOpen && ( + <> +
setExportOpen(false)} /> +
+ + +
+ + )} +
+ +
+
+ +
+ +
+
+
+
Chapter {idx + 1}
+ setChapterTitle(chapter.id, e.target.value)} + /> +
+ setChapterContent(chapter.id, content)} + onReady={setEditor} + /> +
+ +
+ {dock && } +
+ + {settingsOpen && setSettingsOpen(false)} onSave={saveNow} />} + {notice && ( +
setNotice(null)}> + {notice} +
+ )} +
+ ); +}