feat(client/print): print presentations with waiting for slides to load

This commit is contained in:
Elian Doran
2025-10-19 16:27:13 +03:00
parent 89dac52f49
commit 64576458b7
7 changed files with 58 additions and 10 deletions

View File

@@ -2,6 +2,7 @@ import FNote from "./entities/fnote";
import { render } from "preact"; import { render } from "preact";
import { CustomNoteList } from "./widgets/collections/NoteList"; import { CustomNoteList } from "./widgets/collections/NoteList";
import "./print.css"; import "./print.css";
import { useCallback, useRef } from "preact/hooks";
async function main() { async function main() {
const notePath = window.location.hash.substring(1); const notePath = window.location.hash.substring(1);
@@ -12,10 +13,25 @@ async function main() {
const note = await froca.getNote(noteId); const note = await froca.getNote(noteId);
if (!note) return; if (!note) return;
render(getElementForNote(note), document.body); render(<App note={note} />, document.body);
} }
function getElementForNote(note: FNote) { function App({ note }: { note: FNote }) {
return (
<>
<ContentRenderer note={note} />
</>
);
}
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. // Collections.
if (note.type === "book") { if (note.type === "book") {
return <CustomNoteList return <CustomNoteList
@@ -25,6 +41,7 @@ function getElementForNote(note: FNote) {
ntxId="print" ntxId="print"
highlightedTokens={null} highlightedTokens={null}
media="print" media="print"
onReady={onReady}
/>; />;
} }

View File

@@ -2422,4 +2422,14 @@ footer.webview-footer button {
.revision-diff-removed { .revision-diff-removed {
background: rgba(255, 100, 100, 0.5); background: rgba(255, 100, 100, 0.5);
text-decoration: line-through; text-decoration: line-through;
}
iframe.print-iframe {
position: absolute;
top: 0;
left: -600px;
right: -600px;
bottom: 0;
width: 0;
height: 0;
} }

View File

@@ -23,9 +23,10 @@ interface NoteListProps {
isEnabled: boolean; isEnabled: boolean;
ntxId: string | null | undefined; ntxId: string | null | undefined;
media: ViewModeMedia; media: ViewModeMedia;
onReady: () => void;
} }
export default function NoteList<T extends object>(props: Pick<NoteListProps, "displayOnlyCollections" | "media">) { export default function NoteList<T extends object>(props: Pick<NoteListProps, "displayOnlyCollections" | "media" | "onReady">) {
const { note, noteContext, notePath, ntxId } = useNoteContext(); const { note, noteContext, notePath, ntxId } = useNoteContext();
const isEnabled = noteContext?.hasNoteList(); const isEnabled = noteContext?.hasNoteList();
return <CustomNoteList note={note} isEnabled={!!isEnabled} notePath={notePath} ntxId={ntxId} {...props} /> return <CustomNoteList note={note} isEnabled={!!isEnabled} notePath={notePath} ntxId={ntxId} {...props} />

View File

@@ -16,4 +16,5 @@ export interface ViewModeProps<T extends object> {
viewConfig: T | undefined; viewConfig: T | undefined;
saveConfig(newConfig: T): void; saveConfig(newConfig: T): void;
media: ViewModeMedia; media: ViewModeMedia;
onReady(): void;
} }

View File

@@ -14,7 +14,7 @@ import { t } from "../../../services/i18n";
import { DEFAULT_THEME, loadPresentationTheme } from "./themes"; import { DEFAULT_THEME, loadPresentationTheme } from "./themes";
import FNote from "../../../entities/fnote"; 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<PresentationModel>(); const [ presentation, setPresentation ] = useState<PresentationModel>();
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [ api, setApi ] = useState<Reveal.Api>(); const [ api, setApi ] = useState<Reveal.Api>();
@@ -33,6 +33,14 @@ export default function PresentationView({ note, noteIds, media }: ViewModeProps
useLayoutEffect(refresh, [ note, noteIds ]); 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; if (!presentation || !stylesheets) return;
const content = ( const content = (
<> <>

View File

@@ -297,8 +297,19 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
return; return;
} }
// Trigger in timeout to dismiss the menu while printing. const iframe = document.createElement('iframe');
setTimeout(window.print, 0); 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() { async exportAsPdfEvent() {

View File

@@ -47,11 +47,11 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment(); const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(note.type); const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(note.type);
const isInOptions = note.noteId.startsWith("_options"); 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 isElectron = getIsElectron();
const isMac = getIsMac(); const isMac = getIsMac();
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type); 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 ( return (
<Dropdown <Dropdown
@@ -74,7 +74,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
<CommandItem icon="bx bx-export" text={t("note_actions.export_note")} <CommandItem icon="bx bx-export" text={t("note_actions.export_note")}
disabled={isInOptions || note.noteId === "_backendLog"} disabled={isInOptions || note.noteId === "_backendLog"}
command={() => noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", { command={() => noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", {
notePath: noteContext.notePath, notePath: noteContext.notePath,
defaultType: "single" defaultType: "single"
})} /> })} />
<FormDropdownDivider /> <FormDropdownDivider />
@@ -133,4 +133,4 @@ function ConvertToAttachment({ note }: { note: FNote }) {
}} }}
>{t("note_actions.convert_into_attachment")}</FormListItem> >{t("note_actions.convert_into_attachment")}</FormListItem>
) )
} }