diff --git a/bin/copy-dist.ts b/bin/copy-dist.ts index bdb4e311e..6ffc2cf98 100644 --- a/bin/copy-dist.ts +++ b/bin/copy-dist.ts @@ -81,7 +81,6 @@ const copy = async () => { "node_modules/mermaid/dist/", "node_modules/jquery/dist/", "node_modules/jquery-hotkeys/", - "node_modules/print-this/", "node_modules/split.js/dist/", "node_modules/panzoom/dist/", "node_modules/i18next/", diff --git a/package-lock.json b/package-lock.json index da8a3c313..49e028734 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,7 +78,6 @@ "normalize-strings": "1.1.1", "normalize.css": "8.0.1", "panzoom": "9.4.3", - "print-this": "2.0.0", "rand-token": "1.0.1", "react": "18.3.1", "react-dom": "18.3.1", @@ -12681,15 +12680,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/opencollective-postinstall": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", - "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", - "license": "MIT", - "bin": { - "opencollective-postinstall": "index.js" - } - }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -13495,17 +13485,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/print-this": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/print-this/-/print-this-2.0.0.tgz", - "integrity": "sha512-/v1/tXs4BQGpEF7OYKe05h4xiQR09Q4HgASL28pngx6aedCQaB1OlHs8t9RDVgUayXHDWHG9V5EBjPlXb46k4w==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "jquery": ">=1.11", - "opencollective-postinstall": "^2.0.2" - } - }, "node_modules/proc-log": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", diff --git a/package.json b/package.json index c84735495..4216a3d26 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,6 @@ "normalize-strings": "1.1.1", "normalize.css": "8.0.1", "panzoom": "9.4.3", - "print-this": "2.0.0", "rand-token": "1.0.1", "react": "18.3.1", "react-dom": "18.3.1", diff --git a/src/public/app/components/app_context.ts b/src/public/app/components/app_context.ts index 072977695..dbfe78cd0 100644 --- a/src/public/app/components/app_context.ts +++ b/src/public/app/components/app_context.ts @@ -81,6 +81,10 @@ export type CommandMappings = { showOptions: CommandData & { section: string; }; + showExportDialog: CommandData & { + notePath: string; + defaultType: "single"; + }; showDeleteNotesDialog: CommandData & { branchIdsToDelete: string[]; callback: (value: ResolveOptions) => void; diff --git a/src/public/app/services/library_loader.ts b/src/public/app/services/library_loader.ts index f8b363302..0cb836f39 100644 --- a/src/public/app/services/library_loader.ts +++ b/src/public/app/services/library_loader.ts @@ -51,10 +51,6 @@ const RELATION_MAP: Library = { css: ["stylesheets/relation_map.css"] }; -const PRINT_THIS: Library = { - js: ["node_modules/print-this/printThis.js"] -}; - const CALENDAR_WIDGET: Library = { css: ["stylesheets/calendar.css"] }; @@ -193,7 +189,6 @@ export default { CODE_MIRROR, ESLINT, RELATION_MAP, - PRINT_THIS, CALENDAR_WIDGET, KATEX, WHEEL_ZOOM, diff --git a/src/public/app/widgets/buttons/note_actions.js b/src/public/app/widgets/buttons/note_actions.ts similarity index 75% rename from src/public/app/widgets/buttons/note_actions.js rename to src/public/app/widgets/buttons/note_actions.ts index c5c1587ad..dc332b541 100644 --- a/src/public/app/widgets/buttons/note_actions.js +++ b/src/public/app/widgets/buttons/note_actions.ts @@ -5,8 +5,15 @@ import dialogService from "../../services/dialog.js"; import server from "../../services/server.js"; import toastService from "../../services/toast.js"; import ws from "../../services/ws.js"; -import appContext from "../../components/app_context.js"; +import appContext, { type EventData } from "../../components/app_context.js"; import { t } from "../../services/i18n.js"; +import type FNote from "../../entities/fnote.js"; +import type { FAttachmentRow } from "../../entities/fattachment.js"; + +// TODO: Deduplicate with server +interface ConvertToAttachmentResponse { + attachment: FAttachmentRow; +} const TPL = ` `; export default class NoteActionsWidget extends NoteContextAwareWidget { + + private $convertNoteIntoAttachmentButton!: JQuery; + private $findInTextButton!: JQuery; + private $printActiveNoteButton!: JQuery; + private $exportAsPdfButton!: JQuery; + private $showSourceButton!: JQuery; + private $showAttachmentsButton!: JQuery; + private $renderNoteButton!: JQuery; + private $saveRevisionButton!: JQuery; + private $exportNoteButton!: JQuery; + private $importNoteButton!: JQuery; + private $openNoteExternallyButton!: JQuery; + private $openNoteCustomButton!: JQuery; + private $deleteNoteButton!: JQuery; + isEnabled() { return this.note?.type !== "launcher"; } doRender() { this.$widget = $(TPL); - this.$widget.on("show.bs.dropdown", () => this.refreshVisibility(this.note)); + this.$widget.on("show.bs.dropdown", () => { + if (this.note) { + this.refreshVisibility(this.note); + } + }); this.$convertNoteIntoAttachmentButton = this.$widget.find("[data-trigger-command='convertNoteIntoAttachment']"); this.$findInTextButton = this.$widget.find(".find-in-text-button"); this.$printActiveNoteButton = this.$widget.find(".print-active-note-button"); + this.$exportAsPdfButton = this.$widget.find(".export-as-pdf-button"); this.$showSourceButton = this.$widget.find(".show-source-button"); this.$showAttachmentsButton = this.$widget.find(".show-attachments-button"); this.$renderNoteButton = this.$widget.find(".render-note-button"); @@ -118,7 +149,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget { this.$exportNoteButton = this.$widget.find(".export-note-button"); this.$exportNoteButton.on("click", () => { - if (this.$exportNoteButton.hasClass("disabled")) { + if (this.$exportNoteButton.hasClass("disabled") || !this.noteContext?.notePath) { return; } @@ -129,7 +160,11 @@ export default class NoteActionsWidget extends NoteContextAwareWidget { }); this.$importNoteButton = this.$widget.find(".import-files-button"); - this.$importNoteButton.on("click", () => this.triggerCommand("showImportDialog", { noteId: this.noteId })); + this.$importNoteButton.on("click", () => { + if (this.noteId) { + this.triggerCommand("showImportDialog", { noteId: this.noteId }); + } + }); this.$widget.on("click", ".dropdown-item", () => this.$widget.find("[data-bs-toggle='dropdown']").dropdown("toggle")); @@ -138,7 +173,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget { this.$deleteNoteButton = this.$widget.find(".delete-note-button"); this.$deleteNoteButton.on("click", () => { - if (this.note.noteId === "root") { + if (!this.note || this.note.noteId === "root") { return; } @@ -146,7 +181,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget { }); } - async refreshVisibility(note) { + async refreshVisibility(note: FNote) { const isInOptions = note.noteId.startsWith("_options"); this.$convertNoteIntoAttachmentButton.toggle(note.isEligibleForConversionToAttachment()); @@ -156,7 +191,10 @@ export default class NoteActionsWidget extends NoteContextAwareWidget { this.toggleDisabled(this.$showAttachmentsButton, !isInOptions); this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "geoMap"].includes(note.type)); - this.toggleDisabled(this.$printActiveNoteButton, ["text", "code"].includes(note.type)); + const canPrint = ["text", "code"].includes(note.type); + this.toggleDisabled(this.$printActiveNoteButton, canPrint); + this.toggleDisabled(this.$exportAsPdfButton, canPrint); + this.$exportAsPdfButton.toggleClass("hidden-ext", !utils.isElectron()); this.$renderNoteButton.toggle(note.type === "render"); @@ -177,11 +215,11 @@ export default class NoteActionsWidget extends NoteContextAwareWidget { } async convertNoteIntoAttachmentCommand() { - if (!(await dialogService.confirm(t("note_actions.convert_into_attachment_prompt", { title: this.note.title })))) { + if (!this.note || !(await dialogService.confirm(t("note_actions.convert_into_attachment_prompt", { title: this.note.title })))) { return; } - const { attachment: newAttachment } = await server.post(`notes/${this.noteId}/convert-to-attachment`); + const { attachment: newAttachment } = await server.post(`notes/${this.noteId}/convert-to-attachment`); if (!newAttachment) { toastService.showMessage(t("note_actions.convert_into_attachment_failed", { title: this.note.title })); @@ -198,7 +236,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget { }); } - toggleDisabled($el, enable) { + toggleDisabled($el: JQuery, enable: boolean) { if (enable) { $el.removeAttr("disabled"); } else { @@ -206,7 +244,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget { } } - entitiesReloadedEvent({ loadResults }) { + entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { if (loadResults.isNoteReloaded(this.noteId)) { this.refresh(); } diff --git a/src/public/app/widgets/note_detail.js b/src/public/app/widgets/note_detail.js index 3fad9f138..21f84dbc1 100644 --- a/src/public/app/widgets/note_detail.js +++ b/src/public/app/widgets/note_detail.js @@ -32,6 +32,7 @@ import AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.js"; import MindMapWidget from "./type_widgets/mind_map.js"; import { getStylesheetUrl, isSyntaxHighlightEnabled } from "../services/syntax_highlight.js"; import GeoMapTypeWidget from "./type_widgets/geo_map.js"; +import utils from "../services/utils.js"; const TPL = `
@@ -249,45 +250,18 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { return; } - await libraryLoader.requireLibrary(libraryLoader.PRINT_THIS); + window.print(); + } - let $promotedAttributes = $(""); - - if (this.note.getPromotedDefinitionAttributes().length > 0) { - $promotedAttributes = (await attributeRenderer.renderNormalAttributes(this.note)).$renderedAttributes; + async exportAsPdfEvent() { + if (!this.noteContext.isActive()) { + return; } - const { assetPath } = window.glob; - const cssToLoad = [ - `${assetPath}/node_modules/codemirror/lib/codemirror.css`, - `${assetPath}/libraries/ckeditor/ckeditor-content.css`, - `${assetPath}/node_modules/bootstrap/dist/css/bootstrap.min.css`, - `${assetPath}/node_modules/katex/dist/katex.min.css`, - `${assetPath}/stylesheets/print.css`, - `${assetPath}/stylesheets/relation_map.css`, - `${assetPath}/stylesheets/ckeditor-theme.css` - ]; - - if (isSyntaxHighlightEnabled()) { - cssToLoad.push(getStylesheetUrl("default:vs")); - } - - this.$widget.find(".note-detail-printable:visible").printThis({ - header: $("
").append($("

").text(this.note.title)).append($promotedAttributes).prop("outerHTML"), - - footer: ` - - - - -`, - importCSS: false, - loadCSS: cssToLoad, - debug: true + const { ipcRenderer } = utils.dynamicRequire("electron"); + ipcRenderer.send("export-as-pdf", { + title: this.note.title, + landscape: this.note.hasAttribute("label", "printLandscape") }); } diff --git a/src/public/stylesheets/print.css b/src/public/stylesheets/print.css index d40396259..34521d733 100644 --- a/src/public/stylesheets/print.css +++ b/src/public/stylesheets/print.css @@ -1,40 +1,162 @@ -@media print { - html body { - /* https://github.com/zadam/trilium/issues/3202 */ - color: black; - } - - .no-print, - .no-print * { - display: none !important; - } - - .relation-map-wrapper { - height: 100vh !important; - } - - .table thead th, - .table td, - .table th { - /* Fix center vertical alignment of table cells */ - vertical-align: middle; - } - - pre { - box-shadow: unset !important; - border: 0.75pt solid gray !important; - border-radius: 2pt !important; - } - - span[style] { - print-color-adjust: exact; - -webkit-print-color-adjust: exact; - } - - /* Fix visibility of checkbox checkmarks - see https://github.com/TriliumNext/Notes/issues/901 */ - .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input[checked]::after { - /* fallback to default ck-editor green */ - border-color: hsl(126, 64%, 41%); - } +:root { + --main-background-color: white; + --root-background: var(--main-background-color); + --launcher-pane-background-color: var(--main-background-color); + --main-text-color: black; + --input-text-color: var(--main-text-color); } + +.no-print, +.no-print *, +.tab-row-container, +.tab-row-widget, +#launcher-pane, +#left-pane, +#right-pane, +.title-row .note-icon-widget, +.title-row .button-widget, +.ribbon-container, +.promoted-attributes-widget, +.scroll-padding-widget, +.note-list-widget, +.spacer { + display: none !important; +} + +body.mobile #mobile-sidebar-wrapper, +body.mobile .classic-toolbar-widget, +body.mobile .action-button { + display: none !important; +} + +body.mobile #detail-container { + max-height: unset; +} + +body.mobile .note-title-widget { + padding: 0 !important; +} + +body, +#root-widget, +#rest-pane > div.component:first-child, +.note-detail-printable, +.note-detail-editable-text-editor { + height: unset !important; + overflow: auto; +} + +.note-title-widget input, +.note-detail-editable-text, +.note-detail-editable-text-editor { + padding: 0 !important; +} + +html, +body { + width: unset !important; + height: unset !important; + overflow: visible; + position: unset; + /* https://github.com/zadam/trilium/issues/3202 */ + color: black; +} + +#root-widget, +#horizontal-main-container, +#rest-pane, +#vertical-main-container, +#center-pane, +.split-note-container-widget, +.note-split:not(.hidden-ext), +body.mobile #mobile-rest-container { + display: block !important; + overflow: auto; + border-radius: 0 !important; +} + +#center-pane, +#rest-pane, +.note-split, +body.mobile #detail-container { + width: unset !important; + max-width: unset !important; +} + +.component { + contain: none !important; +} + +/* Respect page breaks */ +.page-break { + page-break-after: always; + break-after: always; +} + +.page-break > * { + display: none !important; +} + +.relation-map-wrapper { + height: 100vh !important; +} + +.table thead th, +.table td, +.table th { + /* Fix center vertical alignment of table cells */ + vertical-align: middle; +} + +pre { + box-shadow: unset !important; + border: 0.75pt solid gray !important; + border-radius: 2pt !important; +} + +th, +span[style] { + print-color-adjust: exact; + -webkit-print-color-adjust: exact; +} + +/* + * Text note specific fixes + */ +.ck-widget { + outline: none !important; +} + +.ck-placeholder, +.ck-widget__type-around, +.ck-widget__selection-handle { + display: none !important; +} + +.ck-widget.table td.ck-editor__nested-editable.ck-editor__nested-editable_focused, +.ck-widget.table td.ck-editor__nested-editable:focus, +.ck-widget.table th.ck-editor__nested-editable.ck-editor__nested-editable_focused, +.ck-widget.table th.ck-editor__nested-editable:focus { + background: unset !important; + outline: unset !important; +} + +/* Fix visibility of checkbox checkmarks + see https://github.com/TriliumNext/Notes/issues/901 */ +.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input[checked]::after { + /* fallback to default ck-editor green */ + border-color: hsl(126, 64%, 41%); +} + +.include-note .include-note-content { + max-height: unset !important; + overflow: unset !important; +} + +/* + * Code note specific fixes. + */ +.note-detail-code pre { + border: unset !important; + border-radius: unset !important; +} \ No newline at end of file diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css index 24cfee62f..8f883d8ff 100644 --- a/src/public/stylesheets/style.css +++ b/src/public/stylesheets/style.css @@ -1604,4 +1604,4 @@ body.electron.platform-darwin:not(.native-titlebar) .tab-row-container { border-color: var(--hover-item-border-color); background: var(--hover-item-background-color); color: var(--hover-item-text-color); -} +} \ No newline at end of file diff --git a/src/public/stylesheets/theme-next/notes/text.css b/src/public/stylesheets/theme-next/notes/text.css index 4100564d1..56a12e1c4 100644 --- a/src/public/stylesheets/theme-next/notes/text.css +++ b/src/public/stylesheets/theme-next/notes/text.css @@ -104,11 +104,13 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child { opacity: 1; } -.ck-content p code { - border: 1px solid var(--card-border-color); - box-shadow: var(--card-box-shadow); - border-radius: 6px; - background-color: var(--card-background-color); +@media (screen) { + .ck-content p code { + border: 1px solid var(--card-border-color); + box-shadow: var(--card-box-shadow); + border-radius: 6px; + background-color: var(--card-background-color); + } } .note-detail-printable:not(.word-wrap) pre code { diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index d58f0fd56..c0d7d0eb1 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -672,7 +672,8 @@ "save_revision": "Save revision", "convert_into_attachment_failed": "Converting note '{{title}}' failed.", "convert_into_attachment_successful": "Note '{{title}}' has been converted to attachment.", - "convert_into_attachment_prompt": "Are you sure you want to convert note '{{title}}' into an attachment of the parent note?" + "convert_into_attachment_prompt": "Are you sure you want to convert note '{{title}}' into an attachment of the parent note?", + "print_pdf": "Export as PDF..." }, "onclick_button": { "no_click_handler": "Button widget '{{componentId}}' has no defined click handler" diff --git a/src/public/translations/ro/translation.json b/src/public/translations/ro/translation.json index 87be0aa79..280a00302 100644 --- a/src/public/translations/ro/translation.json +++ b/src/public/translations/ro/translation.json @@ -824,7 +824,8 @@ "search_in_note": "Caută în notiță", "convert_into_attachment_failed": "Nu s-a putut converti notița „{{title}}”.", "convert_into_attachment_successful": "Notița „{{title}}” a fost convertită în atașament.", - "convert_into_attachment_prompt": "Doriți convertirea notiței „{{title}}” într-un atașament al notiței părinte?" + "convert_into_attachment_prompt": "Doriți convertirea notiței „{{title}}” într-un atașament al notiței părinte?", + "print_pdf": "Exportare ca PDF..." }, "note_erasure_timeout": { "deleted_notes_erased": "Notițele șterse au fost eliminate permanent.", diff --git a/src/routes/assets.ts b/src/routes/assets.ts index c20c468d0..959de788b 100644 --- a/src/routes/assets.ts +++ b/src/routes/assets.ts @@ -66,8 +66,6 @@ async function register(app: express.Application) { app.use(`/${assetPath}/node_modules/jquery-hotkeys/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/jquery-hotkeys/"))); - app.use(`/${assetPath}/node_modules/print-this/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/print-this/"))); - app.use(`/${assetPath}/node_modules/split.js/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/split.js/dist/"))); app.use(`/${assetPath}/node_modules/panzoom/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/panzoom/dist/"))); diff --git a/src/services/keyboard_actions.ts b/src/services/keyboard_actions.ts index 95ee3e7b8..8d96a915f 100644 --- a/src/services/keyboard_actions.ts +++ b/src/services/keyboard_actions.ts @@ -503,6 +503,12 @@ function getDefaultKeyboardActions() { description: t("keyboard_actions.print-active-note"), scope: "window" }, + { + actionName: "exportAsPdf", + defaultShortcuts: [], + description: t("keyboard_actions.export-as-pdf"), + scope: "window" + }, { actionName: "openNoteExternally", defaultShortcuts: [], diff --git a/src/services/keyboard_actions_interface.ts b/src/services/keyboard_actions_interface.ts index f0f032e6a..68d708309 100644 --- a/src/services/keyboard_actions_interface.ts +++ b/src/services/keyboard_actions_interface.ts @@ -75,6 +75,7 @@ const enum KeyboardActionNamesEnum { toggleRibbonTabSimilarNotes, toggleRightPane, printActiveNote, + exportAsPdf, openNoteExternally, renderActiveNote, runActiveNote, diff --git a/src/services/window.ts b/src/services/window.ts index 84c8f5a46..939ed0e90 100644 --- a/src/services/window.ts +++ b/src/services/window.ts @@ -1,3 +1,4 @@ +import fs from "fs/promises"; import path from "path"; import url from "url"; import port from "./port.js"; @@ -7,12 +8,13 @@ import sqlInit from "./sql_init.js"; import cls from "./cls.js"; import keyboardActionsService from "./keyboard_actions.js"; import remoteMain from "@electron/remote/main/index.js"; -import type { App, BrowserWindow, BrowserWindowConstructorOptions, WebContents } from "electron"; -import { ipcMain } from "electron"; -import { isDev, isMac, isWindows } from "./utils.js"; +import { BrowserWindow, shell, type App, type BrowserWindowConstructorOptions, type WebContents } from "electron"; +import { dialog, ipcMain } from "electron"; +import { formatDownloadTitle, isDev, isMac, isWindows } from "./utils.js"; import { fileURLToPath } from "url"; import { dirname } from "path"; +import { t } from "i18next"; // Prevent the window being garbage collected let mainWindow: BrowserWindow | null; @@ -46,6 +48,50 @@ ipcMain.on("create-extra-window", (event, arg) => { createExtraWindow(arg.extraWindowHash); }); +interface ExportAsPdfOpts { + title: string; + landscape: boolean; +} + +ipcMain.on("export-as-pdf", async (e, opts: ExportAsPdfOpts) => { + const browserWindow = BrowserWindow.fromWebContents(e.sender); + if (!browserWindow) { + return; + } + + const filePath = dialog.showSaveDialogSync(browserWindow, { + defaultPath: formatDownloadTitle(opts.title, "file", "application/pdf"), + filters: [ + { + name: t("pdf.export_filter"), + extensions: [ "pdf" ] + } + ] + }); + if (!filePath) { + return; + } + + let buffer: Buffer; + try { + buffer = await browserWindow.webContents.printToPDF({ + landscape: opts.landscape + }); + } catch (e) { + dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-export-message")); + return; + } + + try { + await fs.writeFile(filePath, buffer); + } catch (e) { + dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-save-message")); + return; + } + + shell.openPath(filePath); +}); + async function createMainWindow(app: App) { if ("setUserTasks" in app) { app.setUserTasks([ diff --git a/src/views/desktop.ejs b/src/views/desktop.ejs index aa325834d..da74e65c8 100644 --- a/src/views/desktop.ejs +++ b/src/views/desktop.ejs @@ -62,6 +62,7 @@ <% } %> +