From 64576458b7053c739fdbaab9ef2fb15563e0abd8 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 19 Oct 2025 16:27:13 +0300 Subject: [PATCH] feat(client/print): print presentations with waiting for slides to load --- apps/client/src/print.tsx | 21 +++++++++++++++++-- apps/client/src/stylesheets/style.css | 10 +++++++++ .../src/widgets/collections/NoteList.tsx | 3 ++- .../src/widgets/collections/interface.ts | 1 + .../collections/presentation/index.tsx | 10 ++++++++- apps/client/src/widgets/note_detail.ts | 15 +++++++++++-- .../client/src/widgets/ribbon/NoteActions.tsx | 8 +++---- 7 files changed, 58 insertions(+), 10 deletions(-) diff --git a/apps/client/src/print.tsx b/apps/client/src/print.tsx index 4d74fd90c..0ff7522f9 100644 --- a/apps/client/src/print.tsx +++ b/apps/client/src/print.tsx @@ -2,6 +2,7 @@ import FNote from "./entities/fnote"; import { render } from "preact"; import { CustomNoteList } from "./widgets/collections/NoteList"; import "./print.css"; +import { useCallback, useRef } from "preact/hooks"; async function main() { const notePath = window.location.hash.substring(1); @@ -12,10 +13,25 @@ async function main() { const note = await froca.getNote(noteId); if (!note) return; - render(getElementForNote(note), document.body); + render(, document.body); } -function getElementForNote(note: FNote) { +function App({ note }: { note: FNote }) { + return ( + <> + + + ); +} + +function ContentRenderer({ note }: { note: FNote }) { + const sentReadyEvent = useRef(false); + const onReady = useCallback(() => { + if (sentReadyEvent.current) return; + window.dispatchEvent(new Event("note-ready")); + sentReadyEvent.current = true; + }, []); + // Collections. if (note.type === "book") { return ; } diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index 03d1bd8ff..fd8383130 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -2422,4 +2422,14 @@ footer.webview-footer button { .revision-diff-removed { background: rgba(255, 100, 100, 0.5); text-decoration: line-through; +} + +iframe.print-iframe { + position: absolute; + top: 0; + left: -600px; + right: -600px; + bottom: 0; + width: 0; + height: 0; } \ No newline at end of file diff --git a/apps/client/src/widgets/collections/NoteList.tsx b/apps/client/src/widgets/collections/NoteList.tsx index 11abcc5c2..f9c9ba5d1 100644 --- a/apps/client/src/widgets/collections/NoteList.tsx +++ b/apps/client/src/widgets/collections/NoteList.tsx @@ -23,9 +23,10 @@ interface NoteListProps { isEnabled: boolean; ntxId: string | null | undefined; media: ViewModeMedia; + onReady: () => void; } -export default function NoteList(props: Pick) { +export default function NoteList(props: Pick) { const { note, noteContext, notePath, ntxId } = useNoteContext(); const isEnabled = noteContext?.hasNoteList(); return diff --git a/apps/client/src/widgets/collections/interface.ts b/apps/client/src/widgets/collections/interface.ts index 81a8e4d3d..7bec23a64 100644 --- a/apps/client/src/widgets/collections/interface.ts +++ b/apps/client/src/widgets/collections/interface.ts @@ -16,4 +16,5 @@ export interface ViewModeProps { viewConfig: T | undefined; saveConfig(newConfig: T): void; media: ViewModeMedia; + onReady(): void; } diff --git a/apps/client/src/widgets/collections/presentation/index.tsx b/apps/client/src/widgets/collections/presentation/index.tsx index 44341688a..dfa4574e0 100644 --- a/apps/client/src/widgets/collections/presentation/index.tsx +++ b/apps/client/src/widgets/collections/presentation/index.tsx @@ -14,7 +14,7 @@ import { t } from "../../../services/i18n"; import { DEFAULT_THEME, loadPresentationTheme } from "./themes"; import FNote from "../../../entities/fnote"; -export default function PresentationView({ note, noteIds, media }: ViewModeProps<{}>) { +export default function PresentationView({ note, noteIds, media, onReady }: ViewModeProps<{}>) { const [ presentation, setPresentation ] = useState(); const containerRef = useRef(null); const [ api, setApi ] = useState(); @@ -33,6 +33,14 @@ export default function PresentationView({ note, noteIds, media }: ViewModeProps useLayoutEffect(refresh, [ note, noteIds ]); + useEffect(() => { + // We need to wait for Reveal.js to initialize (by setting api) and for the presentation to become available. + if (api && presentation) { + // Timeout is necessary because it otherwise can cause flakiness by rendering only the first slide. + setTimeout(onReady, 200); + } + }, [ api, presentation ]); + if (!presentation || !stylesheets) return; const content = ( <> diff --git a/apps/client/src/widgets/note_detail.ts b/apps/client/src/widgets/note_detail.ts index 5b798a06a..0ec68060c 100644 --- a/apps/client/src/widgets/note_detail.ts +++ b/apps/client/src/widgets/note_detail.ts @@ -297,8 +297,19 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { return; } - // Trigger in timeout to dismiss the menu while printing. - setTimeout(window.print, 0); + const iframe = document.createElement('iframe'); + iframe.src = `?print#${this.notePath}`; + iframe.className = "print-iframe"; + document.body.appendChild(iframe); + iframe.onload = () => { + console.log("Got ", iframe, iframe.contentWindow); + if (iframe.contentWindow) { + iframe.contentWindow.addEventListener("note-ready", () => { + iframe.contentWindow?.print(); + document.body.removeChild(iframe); + }); + } + }; } async exportAsPdfEvent() { diff --git a/apps/client/src/widgets/ribbon/NoteActions.tsx b/apps/client/src/widgets/ribbon/NoteActions.tsx index f780eab8b..2fed2ea02 100644 --- a/apps/client/src/widgets/ribbon/NoteActions.tsx +++ b/apps/client/src/widgets/ribbon/NoteActions.tsx @@ -47,11 +47,11 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment(); const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(note.type); const isInOptions = note.noteId.startsWith("_options"); - const isPrintable = ["text", "code"].includes(note.type); + const isPrintable = ["text", "code", "book"].includes(note.type); const isElectron = getIsElectron(); const isMac = getIsMac(); const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type); - const isSearchOrBook = ["search", "book"].includes(note.type); + const isSearchOrBook = ["search", "book"].includes(note.type); return ( noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", { - notePath: noteContext.notePath, + notePath: noteContext.notePath, defaultType: "single" })} /> @@ -133,4 +133,4 @@ function ConvertToAttachment({ note }: { note: FNote }) { }} >{t("note_actions.convert_into_attachment")} ) -} \ No newline at end of file +}