diff --git a/apps/client/src/print.tsx b/apps/client/src/print.tsx index ed3855635..1df7f9c5f 100644 --- a/apps/client/src/print.tsx +++ b/apps/client/src/print.tsx @@ -9,10 +9,17 @@ import { CustomNoteList, useNoteViewType } from "./widgets/collections/NoteList" interface RendererProps { note: FNote; - onReady: () => void; + onReady: (data: PrintReport) => void; onProgressChanged?: (progress: number) => void; } +export type PrintReport = { + type: "single-note"; +} | { + type: "collection"; + ignoredNoteIds: string[]; +}; + async function main() { const notePath = window.location.hash.substring(1); const noteId = notePath.split("/").at(-1); @@ -35,9 +42,11 @@ function App({ note, noteId }: { note: FNote | null | undefined, noteId: string window.dispatchEvent(new CustomEvent("note-load-progress", { detail: { progress } })); } }, []); - const onReady = useCallback(() => { + const onReady = useCallback((detail: PrintReport) => { if (sentReadyEvent.current) return; - window.dispatchEvent(new Event("note-ready")); + window.dispatchEvent(new CustomEvent("note-ready", { + detail + })); window._noteReady = true; sentReadyEvent.current = true; }, []); @@ -92,7 +101,7 @@ function SingleNoteRenderer({ note, onReady }: RendererProps) { await loadCustomCss(note); } - load().then(() => requestAnimationFrame(onReady)); + load().then(() => requestAnimationFrame(() => onReady({}))); }, [ note ]); return <> @@ -111,9 +120,9 @@ function CollectionRenderer({ note, onReady, onProgressChanged }: RendererProps) ntxId="print" highlightedTokens={null} media="print" - onReady={async () => { + onReady={async (data: PrintReport) => { await loadCustomCss(note); - onReady(); + onReady(data); }} onProgressChanged={onProgressChanged} />; diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 9cfc8b2e1..535c5e04b 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1770,7 +1770,10 @@ "note_detail": { "could_not_find_typewidget": "Could not find typeWidget for type '{{type}}'", "printing": "Printing in progress...", - "printing_pdf": "Exporting to PDF in progress..." + "printing_pdf": "Exporting to PDF in progress...", + "print_report_title": "Print report", + "print_report_collection_content_one": "{{count}} note in the collection could not be printed because they are not supported or they are protected.", + "print_report_collection_content_other": "{{count}} notes in the collection could not be printed because they are not supported or they are protected." }, "note_title": { "placeholder": "type note's title here...", diff --git a/apps/client/src/widgets/NoteDetail.tsx b/apps/client/src/widgets/NoteDetail.tsx index 894bb4ac5..57b840999 100644 --- a/apps/client/src/widgets/NoteDetail.tsx +++ b/apps/client/src/widgets/NoteDetail.tsx @@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from "preact/hooks"; import NoteContext from "../components/note_context"; import FNote from "../entities/fnote"; +import type { PrintReport } from "../print"; import attributes from "../services/attributes"; import { t } from "../services/i18n"; import protected_session_holder from "../services/protected_session_holder"; @@ -179,8 +180,21 @@ export default function NoteDetail() { showToast("printing", e.detail.progress); }); - iframe.contentWindow.addEventListener("note-ready", () => { + iframe.contentWindow.addEventListener("note-ready", (e) => { toast.closePersistent("printing"); + + if ("detail" in e) { + const printReport = e.detail as PrintReport; + if (printReport.type === "collection" && printReport.ignoredNoteIds.length > 0) { + toast.showPersistent({ + id: "print-report", + icon: "bx bx-collection", + title: t("note_detail.print_report_title"), + message: t("note_detail.print_report_collection_content", { count: printReport.ignoredNoteIds.length }) + }); + } + } + iframe.contentWindow?.print(); document.body.removeChild(iframe); }); diff --git a/apps/client/src/widgets/collections/NoteList.tsx b/apps/client/src/widgets/collections/NoteList.tsx index 3188a77ad..5626cc33c 100644 --- a/apps/client/src/widgets/collections/NoteList.tsx +++ b/apps/client/src/widgets/collections/NoteList.tsx @@ -1,14 +1,17 @@ -import { allViewTypes, ViewModeMedia, ViewModeProps, ViewTypeOptions } from "./interface"; -import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent } from "../react/hooks"; -import FNote from "../../entities/fnote"; import "./NoteList.css"; -import { useEffect, useRef, useState } from "preact/hooks"; -import ViewModeStorage from "./view_mode_storage"; -import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } from "../../services/ws"; + import { WebSocketMessage } from "@triliumnext/commons"; -import froca from "../../services/froca"; -import { lazy, Suspense } from "preact/compat"; import { VNode } from "preact"; +import { lazy, Suspense } from "preact/compat"; +import { useEffect, useRef, useState } from "preact/hooks"; + +import FNote from "../../entities/fnote"; +import type { PrintReport } from "../../print"; +import froca from "../../services/froca"; +import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } from "../../services/ws"; +import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent } from "../react/hooks"; +import { allViewTypes, ViewModeMedia, ViewModeProps, ViewTypeOptions } from "./interface"; +import ViewModeStorage from "./view_mode_storage"; interface NoteListProps { note: FNote | null | undefined; notePath: string | null | undefined; @@ -19,7 +22,7 @@ interface NoteListProps { ntxId: string | null | undefined; media: ViewModeMedia; viewType: ViewTypeOptions | undefined; - onReady?: () => void; + onReady?: (data: PrintReport) => void; onProgressChanged?(progress: number): void; } @@ -48,7 +51,7 @@ const ViewComponents: Record import("./presentation/index.js")) } -} +}; export default function NoteList(props: Pick) { const { note, noteContext, notePath, ntxId, viewScope } = useNoteContext(); @@ -57,13 +60,13 @@ export default function NoteList(props: Pick { setEnabled(noteContext?.hasNoteList()); - }, [ note, noteContext, viewType, viewScope?.viewMode, noteType ]) - return + }, [ note, noteContext, viewType, viewScope?.viewMode, noteType ]); + return ; } export function SearchNoteList(props: Omit) { const viewType = useNoteViewType(props.note); - return + return ; } export function CustomNoteList({ note, viewType, isEnabled: shouldEnable, notePath, highlightedTokens, displayOnlyCollections, ntxId, onReady, onProgressChanged, ...restProps }: NoteListProps) { @@ -112,7 +115,7 @@ export function CustomNoteList({ note, viewType, isEnabled: shouldEnable, notePa onProgressChanged: onProgressChanged ?? (() => {}), ...restProps - } + }; } const ComponentToRender = viewType && props && isEnabled && ( @@ -140,9 +143,9 @@ export function useNoteViewType(note?: FNote | null): ViewTypeOptions | undefine } else if (!(allViewTypes as readonly string[]).includes(viewType || "")) { // when not explicitly set, decide based on the note type return note.type === "search" ? "list" : "grid"; - } else { - return viewType as ViewTypeOptions; } + return viewType as ViewTypeOptions; + } export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined, ntxId: string | null | undefined) { @@ -161,26 +164,26 @@ export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOpt async function getNoteIds(note: FNote) { if (directChildrenOnly) { return await note.getChildNoteIdsWithArchiveFiltering(includeArchived); - } else { - return await note.getSubtreeNoteIds(includeArchived); } + return await note.getSubtreeNoteIds(includeArchived); + } // Refresh on note switch. useEffect(() => { - refreshNoteIds() + refreshNoteIds(); }, [ note, includeArchived, directChildrenOnly ]); // Refresh on alterations to the note subtree. useTriliumEvent("entitiesReloaded", ({ loadResults }) => { if (note && loadResults.getBranchRows().some(branch => - branch.parentNoteId === note.noteId + branch.parentNoteId === note.noteId || noteIds.includes(branch.parentNoteId ?? "")) || loadResults.getAttributeRows().some(attr => attr.name === "archived" && attr.noteId && noteIds.includes(attr.noteId)) ) { refreshNoteIds(); } - }) + }); // Refresh on search. useTriliumEvent("searchRefreshed", ({ ntxId: eventNtxId }) => { @@ -201,13 +204,13 @@ export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOpt ...noteIds, ...await getNoteIds(importedNote), importedNoteId - ]) + ]); } } subscribeToMessages(onImport); return () => unsubscribeFromMessage(onImport); - }, [ note, noteIds, setNoteIds ]) + }, [ note, noteIds, setNoteIds ]); return noteIds; } diff --git a/apps/client/src/widgets/collections/interface.ts b/apps/client/src/widgets/collections/interface.ts index 118599260..4a965588d 100644 --- a/apps/client/src/widgets/collections/interface.ts +++ b/apps/client/src/widgets/collections/interface.ts @@ -1,4 +1,5 @@ import FNote from "../../entities/fnote"; +import type { PrintReport } from "../../print"; export const allViewTypes = ["list", "grid", "calendar", "table", "geoMap", "board", "presentation"] as const; export type ViewTypeOptions = typeof allViewTypes[number]; @@ -18,6 +19,6 @@ export interface ViewModeProps { viewConfig: T | undefined; saveConfig(newConfig: T): void; media: ViewModeMedia; - onReady(): void; + onReady(data: PrintReport): void; onProgressChanged?: ProgressChangedFn; } diff --git a/apps/client/src/widgets/collections/legacy/ListPrintView.tsx b/apps/client/src/widgets/collections/legacy/ListPrintView.tsx index b6d642d1b..c2a284e7d 100644 --- a/apps/client/src/widgets/collections/legacy/ListPrintView.tsx +++ b/apps/client/src/widgets/collections/legacy/ListPrintView.tsx @@ -1,6 +1,7 @@ import { useEffect, useLayoutEffect, useState } from "preact/hooks"; import type FNote from "../../../entities/fnote"; +import type { PrintReport } from "../../../print"; import content_renderer from "../../../services/content_renderer"; import froca from "../../../services/froca"; import type { ViewModeProps } from "../interface"; @@ -13,13 +14,17 @@ interface NotesWithContent { export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady, onProgressChanged }: ViewModeProps<{}>) { const noteIds = useFilteredNoteIds(note, unfilteredNoteIds); - const [ notesWithContent, setNotesWithContent ] = useState(); + const [ state, setState ] = useState<{ + notesWithContent?: NotesWithContent[], + data?: PrintReport + }>({}); useLayoutEffect(() => { const noteIdsSet = new Set(); froca.getNotes(noteIds).then(async (notes) => { const noteIdsWithChildren = await note.getSubtreeNoteIds(true); + const ignoredNoteIds: string[] = []; const notesWithContent: NotesWithContent[] = []; async function processNote(note: FNote, depth: number) { @@ -35,6 +40,8 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady, onPro rewriteHeadings(contentEl, depth); noteIdsSet.add(note.noteId); notesWithContent.push({ note, contentEl }); + } else { + ignoredNoteIds.push(note.noteId); } if (onProgressChanged) { @@ -58,22 +65,28 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady, onPro rewriteLinks(contentEl, noteIdsSet); } - setNotesWithContent(notesWithContent); + setState({ + notesWithContent, + data: { + type: "collection", + ignoredNoteIds + } + }); }); }, [noteIds]); useEffect(() => { - if (notesWithContent && onReady) { - onReady(); + if (onReady && state?.data) { + onReady(state.data); } - }, [ notesWithContent, onReady ]); + }, [ state, onReady ]); return (
diff --git a/apps/server/src/services/window.ts b/apps/server/src/services/window.ts index 4431226ab..a959d7d99 100644 --- a/apps/server/src/services/window.ts +++ b/apps/server/src/services/window.ts @@ -1,17 +1,18 @@ +import type { App, BrowserWindow, BrowserWindowConstructorOptions, IpcMainEvent,WebContents } from "electron"; +import electron, { ipcMain } from "electron"; import fs from "fs/promises"; +import { t } from "i18next"; import path from "path"; import url from "url"; -import port from "./port.js"; -import optionService from "./options.js"; -import log from "./log.js"; -import sqlInit from "./sql_init.js"; + import cls from "./cls.js"; import keyboardActionsService from "./keyboard_actions.js"; -import electron, { ipcMain } from "electron"; -import type { App, BrowserWindowConstructorOptions, BrowserWindow, WebContents, IpcMainEvent } from "electron"; -import { formatDownloadTitle, isDev, isMac, isWindows } from "./utils.js"; -import { t } from "i18next"; +import log from "./log.js"; +import optionService from "./options.js"; +import port from "./port.js"; import { RESOURCE_DIR } from "./resource_dir.js"; +import sqlInit from "./sql_init.js"; +import { formatDownloadTitle, isDev, isMac, isWindows } from "./utils.js"; // Prevent the window being garbage collected let mainWindow: BrowserWindow | null; @@ -160,8 +161,8 @@ async function getBrowserWindowForPrinting(e: IpcMainEvent, notePath: string, ac await browserWindow.loadURL(`http://127.0.0.1:${port}/?print#${notePath}`); await browserWindow.webContents.executeJavaScript(` new Promise(resolve => { - if (window._noteReady) return resolve(); - window.addEventListener("note-ready", () => resolve()); + if (window._noteReady) return resolve(window._noteReady); + window.addEventListener("note-ready", (data) => resolve(data)); }); `); ipcMain.off("print-progress", progressCallback); @@ -289,9 +290,9 @@ async function configureWebContents(webContents: WebContents, spellcheckEnabled: function getIcon() { if (process.env.NODE_ENV === "development") { return path.join(__dirname, "../../../desktop/electron-forge/app-icon/png/256x256-dev.png"); - } else { - return path.join(RESOURCE_DIR, "../public/assets/icon.png"); - } + } + return path.join(RESOURCE_DIR, "../public/assets/icon.png"); + } async function createSetupWindow() {