mirror of
https://github.com/zadam/trilium.git
synced 2025-11-04 20:36:13 +01:00
Better PDF export mechanism (part I) (#7399)
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import server from "../services/server.js";
|
||||
import noteAttributeCache from "../services/note_attribute_cache.js";
|
||||
import ws from "../services/ws.js";
|
||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||
import cssClassManager from "../services/css_class_manager.js";
|
||||
import type { Froca } from "../services/froca-interface.js";
|
||||
@@ -586,7 +585,7 @@ export default class FNote {
|
||||
let childBranches = this.getChildBranches();
|
||||
|
||||
if (!childBranches) {
|
||||
ws.logError(`No children for '${this.noteId}'. This shouldn't happen.`);
|
||||
console.error(`No children for '${this.noteId}'. This shouldn't happen.`);
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ export default class DesktopLayout {
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(<SqlTableSchemas />)
|
||||
.child(new NoteDetailWidget())
|
||||
.child(<NoteList />)
|
||||
.child(<NoteList media="screen" />)
|
||||
.child(<SearchResult />)
|
||||
.child(<SqlResults />)
|
||||
.child(<ScrollPadding />)
|
||||
|
||||
@@ -66,6 +66,6 @@ export function applyModals(rootContainer: RootContainer) {
|
||||
.child(<PopupEditorFormattingToolbar />)
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(new NoteDetailWidget())
|
||||
.child(<NoteList displayOnlyCollections />))
|
||||
.child(<NoteList media="screen" displayOnlyCollections />))
|
||||
.child(<CallToActionDialog />);
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ export default class MobileLayout {
|
||||
.filling()
|
||||
.contentSized()
|
||||
.child(new NoteDetailWidget())
|
||||
.child(<NoteList />)
|
||||
.child(<NoteList media="screen" />)
|
||||
.child(<FilePropertiesWrapper />)
|
||||
)
|
||||
.child(<MobileEditorToolbar />)
|
||||
|
||||
155
apps/client/src/print.css
Normal file
155
apps/client/src/print.css
Normal file
@@ -0,0 +1,155 @@
|
||||
:root {
|
||||
--print-font-size: 11pt;
|
||||
--ck-content-color-image-caption-background: transparent !important;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: black;
|
||||
}
|
||||
|
||||
@page {
|
||||
margin: 2cm;
|
||||
}
|
||||
|
||||
.note-list-widget.full-height,
|
||||
.note-list-widget.full-height .note-list-widget-content {
|
||||
height: unset !important;
|
||||
}
|
||||
|
||||
.component {
|
||||
contain: none !important;
|
||||
}
|
||||
|
||||
body[data-note-type="text"] .ck-content {
|
||||
font-size: var(--print-font-size);
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.ck-content figcaption {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.ck-content a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.ck-content a:not([href^="#root/"]) {
|
||||
text-decoration: underline;
|
||||
color: #374a75;
|
||||
}
|
||||
|
||||
.ck-content .todo-list__label * {
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
@supports selector(.todo-list__label__description:has(*)) and (height: 1lh) {
|
||||
.ck-content .todo-list__label__description {
|
||||
/* The percentage of the line height that the check box occupies */
|
||||
--box-ratio: 0.75;
|
||||
/* The size of the gap between the check box and the caption */
|
||||
--box-text-gap: 0.25em;
|
||||
|
||||
--box-size: calc(1lh * var(--box-ratio));
|
||||
--box-vert-offset: calc((1lh - var(--box-size)) / 2);
|
||||
|
||||
display: inline-block;
|
||||
padding-inline-start: calc(var(--box-size) + var(--box-text-gap));
|
||||
/* Source: https://pictogrammers.com/library/mdi/icon/checkbox-blank-outline/ */
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3e%3cpath d='M19%2c3H5C3.89%2c3 3%2c3.89 3%2c5V19A2%2c2 0 0%2c0 5%2c21H19A2%2c2 0 0%2c0 21%2c19V5C21%2c3.89 20.1%2c3 19%2c3M19%2c5V19H5V5H19Z' /%3e%3c/svg%3e");
|
||||
background-position: 0 var(--box-vert-offset);
|
||||
background-size: var(--box-size);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.ck-content .todo-list__label:has(input[type="checkbox"]:checked) .todo-list__label__description {
|
||||
/* Source: https://pictogrammers.com/library/mdi/icon/checkbox-outline/ */
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3e%3cpath d='M19%2c3H5A2%2c2 0 0%2c0 3%2c5V19A2%2c2 0 0%2c0 5%2c21H19A2%2c2 0 0%2c0 21%2c19V5A2%2c2 0 0%2c0 19%2c3M19%2c5V19H5V5H19M10%2c17L6%2c13L7.41%2c11.58L10%2c14.17L16.59%2c7.58L18%2c9' /%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.ck-content .todo-list__label input[type="checkbox"] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* #region Footnotes */
|
||||
.footnote-reference a,
|
||||
.footnote-back-link a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
li.footnote-item {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.ck-content .footnote-back-link {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.ck-content .footnote-content {
|
||||
display: inline-block;
|
||||
width: unset;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region Widows and orphans */
|
||||
p,
|
||||
blockquote {
|
||||
widows: 4;
|
||||
orphans: 4;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
widows: 6;
|
||||
orphans: 6;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
page-break-after: avoid;
|
||||
break-after: avoid;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region Tables */
|
||||
.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;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region Page breaks */
|
||||
.page-break {
|
||||
page-break-after: always;
|
||||
break-after: always;
|
||||
}
|
||||
|
||||
.page-break > *,
|
||||
.page-break::after {
|
||||
display: none !important;
|
||||
}
|
||||
/* #endregion */
|
||||
92
apps/client/src/print.tsx
Normal file
92
apps/client/src/print.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import FNote from "./entities/fnote";
|
||||
import { render } from "preact";
|
||||
import { CustomNoteList } from "./widgets/collections/NoteList";
|
||||
import { useCallback, useLayoutEffect, useRef } from "preact/hooks";
|
||||
import content_renderer from "./services/content_renderer";
|
||||
|
||||
interface RendererProps {
|
||||
note: FNote;
|
||||
onReady: () => void;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const notePath = window.location.hash.substring(1);
|
||||
const noteId = notePath.split("/").at(-1);
|
||||
if (!noteId) return;
|
||||
|
||||
await import("./print.css");
|
||||
const froca = (await import("./services/froca")).default;
|
||||
const note = await froca.getNote(noteId);
|
||||
|
||||
render(<App note={note} noteId={noteId} />, document.body);
|
||||
}
|
||||
|
||||
function App({ note, noteId }: { note: FNote | null | undefined, noteId: string }) {
|
||||
const sentReadyEvent = useRef(false);
|
||||
const onReady = useCallback(() => {
|
||||
if (sentReadyEvent.current) return;
|
||||
window.dispatchEvent(new Event("note-ready"));
|
||||
window._noteReady = true;
|
||||
sentReadyEvent.current = true;
|
||||
}, []);
|
||||
const props: RendererProps | undefined | null = note && { note, onReady };
|
||||
|
||||
if (!note || !props) return <Error404 noteId={noteId} />
|
||||
|
||||
useLayoutEffect(() => {
|
||||
document.body.dataset.noteType = note.type;
|
||||
}, [ note ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{note.type === "book"
|
||||
? <CollectionRenderer {...props} />
|
||||
: <SingleNoteRenderer {...props} />
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SingleNoteRenderer({ note, onReady }: RendererProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
async function load() {
|
||||
if (note.type === "text") {
|
||||
await import("@triliumnext/ckeditor5/src/theme/ck-content.css");
|
||||
}
|
||||
const { $renderedContent } = await content_renderer.getRenderedContent(note, { noChildrenList: true });
|
||||
containerRef.current?.replaceChildren(...$renderedContent);
|
||||
}
|
||||
|
||||
load().then(() => requestAnimationFrame(onReady))
|
||||
}, [ note ]);
|
||||
|
||||
return <>
|
||||
<h1>{note.title}</h1>
|
||||
<main ref={containerRef} />
|
||||
</>;
|
||||
}
|
||||
|
||||
function CollectionRenderer({ note, onReady }: RendererProps) {
|
||||
return <CustomNoteList
|
||||
isEnabled
|
||||
note={note}
|
||||
notePath={note.getBestNotePath().join("/")}
|
||||
ntxId="print"
|
||||
highlightedTokens={null}
|
||||
media="print"
|
||||
onReady={onReady}
|
||||
/>;
|
||||
}
|
||||
|
||||
function Error404({ noteId }: { noteId: string }) {
|
||||
return (
|
||||
<main>
|
||||
<p>The note you are trying to print could not be found.</p>
|
||||
<small>{noteId}</small>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -40,20 +40,23 @@ class FrocaImpl implements Froca {
|
||||
|
||||
constructor() {
|
||||
this.initializedPromise = this.loadInitialTree();
|
||||
this.#clear();
|
||||
}
|
||||
|
||||
async loadInitialTree() {
|
||||
const resp = await server.get<SubtreeResponse>("tree");
|
||||
|
||||
// clear the cache only directly before adding new content which is important for e.g., switching to protected session
|
||||
this.#clear();
|
||||
this.addResp(resp);
|
||||
}
|
||||
|
||||
#clear() {
|
||||
this.notes = {};
|
||||
this.branches = {};
|
||||
this.attributes = {};
|
||||
this.attachments = {};
|
||||
this.blobPromises = {};
|
||||
|
||||
this.addResp(resp);
|
||||
}
|
||||
|
||||
async loadSubTree(subTreeNoteId: string) {
|
||||
|
||||
@@ -76,7 +76,7 @@ export async function ensureMimeTypesForHighlighting(mimeTypeHint?: string) {
|
||||
|
||||
// Load theme.
|
||||
const currentThemeName = String(options.get("codeBlockTheme"));
|
||||
loadHighlightingTheme(currentThemeName);
|
||||
await loadHighlightingTheme(currentThemeName);
|
||||
|
||||
// Load mime types.
|
||||
let mimeTypes: MimeType[];
|
||||
@@ -98,17 +98,16 @@ export async function ensureMimeTypesForHighlighting(mimeTypeHint?: string) {
|
||||
highlightingLoaded = true;
|
||||
}
|
||||
|
||||
export function loadHighlightingTheme(themeName: string) {
|
||||
export async function loadHighlightingTheme(themeName: string) {
|
||||
const themePrefix = "default:";
|
||||
let theme: Theme | null = null;
|
||||
if (themeName.includes(themePrefix)) {
|
||||
if (glob.device === "print") {
|
||||
theme = Themes.vs;
|
||||
} else if (themeName.includes(themePrefix)) {
|
||||
theme = Themes[themeName.substring(themePrefix.length)];
|
||||
}
|
||||
if (!theme) {
|
||||
theme = Themes.default;
|
||||
}
|
||||
|
||||
loadTheme(theme);
|
||||
await loadTheme(theme ?? Themes.default);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -304,6 +304,8 @@ async function sendPing() {
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (glob.device === "print") return;
|
||||
|
||||
ws = connectWebSocket();
|
||||
|
||||
lastPingTs = Date.now();
|
||||
|
||||
@@ -1,322 +0,0 @@
|
||||
: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);
|
||||
|
||||
--print-font-size: 11pt;
|
||||
}
|
||||
|
||||
@page {
|
||||
margin: 2cm;
|
||||
}
|
||||
|
||||
.ck-content {
|
||||
font-size: var(--print-font-size);
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.note-detail-readonly-text {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.no-print,
|
||||
.no-print *,
|
||||
.tab-row-container,
|
||||
.tab-row-widget,
|
||||
.title-bar-buttons,
|
||||
#launcher-pane,
|
||||
#left-pane,
|
||||
#center-pane > *:not(.split-note-container-widget),
|
||||
#right-pane,
|
||||
.title-row .note-icon-widget,
|
||||
.title-row .icon-action,
|
||||
.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;
|
||||
}
|
||||
|
||||
.ck.ck-editor__editable_inline {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.include-note .include-note-content {
|
||||
max-height: unset !important;
|
||||
overflow: unset !important;
|
||||
}
|
||||
|
||||
/* TODO: This will break once we translate the language */
|
||||
.ck-content pre[data-language="Auto-detected"]:after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* Code note specific fixes.
|
||||
*/
|
||||
.note-detail-code pre {
|
||||
border: unset !important;
|
||||
border-radius: unset !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* Links
|
||||
*/
|
||||
|
||||
.note-detail-printable a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.note-detail-printable a:not([href^="#root/"]) {
|
||||
text-decoration: underline;
|
||||
color: #374a75;
|
||||
}
|
||||
|
||||
.note-detail-printable a::after {
|
||||
/* Hide the external link trailing arrow */
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO list check boxes
|
||||
*/
|
||||
|
||||
.note-detail-printable .todo-list__label * {
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
@supports selector(.todo-list__label__description:has(*)) and (height: 1lh) {
|
||||
.note-detail-printable .todo-list__label__description {
|
||||
/* The percentage of the line height that the check box occupies */
|
||||
--box-ratio: 0.75;
|
||||
/* The size of the gap between the check box and the caption */
|
||||
--box-text-gap: 0.25em;
|
||||
|
||||
--box-size: calc(1lh * var(--box-ratio));
|
||||
--box-vert-offset: calc((1lh - var(--box-size)) / 2);
|
||||
|
||||
display: inline-block;
|
||||
padding-inline-start: calc(var(--box-size) + var(--box-text-gap));
|
||||
/* Source: https://pictogrammers.com/library/mdi/icon/checkbox-blank-outline/ */
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3e%3cpath d='M19%2c3H5C3.89%2c3 3%2c3.89 3%2c5V19A2%2c2 0 0%2c0 5%2c21H19A2%2c2 0 0%2c0 21%2c19V5C21%2c3.89 20.1%2c3 19%2c3M19%2c5V19H5V5H19Z' /%3e%3c/svg%3e");
|
||||
background-position: 0 var(--box-vert-offset);
|
||||
background-size: var(--box-size);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.note-detail-printable .todo-list__label:has(input[type="checkbox"]:checked) .todo-list__label__description {
|
||||
/* Source: https://pictogrammers.com/library/mdi/icon/checkbox-outline/ */
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3e%3cpath d='M19%2c3H5A2%2c2 0 0%2c0 3%2c5V19A2%2c2 0 0%2c0 5%2c21H19A2%2c2 0 0%2c0 21%2c19V5A2%2c2 0 0%2c0 19%2c3M19%2c5V19H5V5H19M10%2c17L6%2c13L7.41%2c11.58L10%2c14.17L16.59%2c7.58L18%2c9' /%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.note-detail-printable .todo-list__label input[type="checkbox"] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Blockquotes
|
||||
*/
|
||||
|
||||
.note-detail-printable blockquote {
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
/*
|
||||
* Figures
|
||||
*/
|
||||
|
||||
.note-detail-printable figcaption {
|
||||
--accented-background-color: transparent;
|
||||
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/*
|
||||
* Footnotes
|
||||
*/
|
||||
|
||||
.note-detail-printable .footnote-reference a,
|
||||
.footnote-back-link a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Make the "^" link cover the whole area of the footnote item */
|
||||
|
||||
.footnote-section {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.note-detail-printable li.footnote-item {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.note-detail-printable .footnote-back-link,
|
||||
.note-detail-printable .footnote-back-link *,
|
||||
.note-detail-printable .footnote-back-link a {
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-detail-printable .footnote-back-link a {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.note-detail-printable .footnote-content {
|
||||
display: inline-block;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
/*
|
||||
* Widows and orphans
|
||||
*/
|
||||
p,
|
||||
blockquote {
|
||||
widows: 4;
|
||||
orphans: 4;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
widows: 6;
|
||||
orphans: 6;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
page-break-after: avoid;
|
||||
break-after: avoid;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1722,7 +1722,9 @@
|
||||
"window-on-top": "Keep Window on Top"
|
||||
},
|
||||
"note_detail": {
|
||||
"could_not_find_typewidget": "Could not find typeWidget for type '{{type}}'"
|
||||
"could_not_find_typewidget": "Could not find typeWidget for type '{{type}}'",
|
||||
"printing": "Printing in progress...",
|
||||
"printing_pdf": "Exporting to PDF in progress..."
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "type note's title here..."
|
||||
|
||||
5
apps/client/src/types.d.ts
vendored
5
apps/client/src/types.d.ts
vendored
@@ -16,7 +16,7 @@ interface ElectronProcess {
|
||||
interface CustomGlobals {
|
||||
isDesktop: typeof utils.isDesktop;
|
||||
isMobile: typeof utils.isMobile;
|
||||
device: "mobile" | "desktop";
|
||||
device: "mobile" | "desktop" | "print";
|
||||
getComponentByEl: typeof appContext.getComponentByEl;
|
||||
getHeaders: typeof server.getHeaders;
|
||||
getReferenceLinkTitle: (href: string) => Promise<string>;
|
||||
@@ -59,6 +59,9 @@ declare global {
|
||||
process?: ElectronProcess;
|
||||
glob?: CustomGlobals;
|
||||
|
||||
/** On the printing endpoint, set to true when the note has fully loaded and is ready to be printed/exported as PDF. */
|
||||
_noteReady?: boolean;
|
||||
|
||||
EXCALIDRAW_ASSET_PATH?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { allViewTypes, ViewModeProps, ViewTypeOptions } from "./interface";
|
||||
import { allViewTypes, ViewModeMedia, ViewModeProps, ViewTypeOptions } from "./interface";
|
||||
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useTriliumEvent } from "../react/hooks";
|
||||
import FNote from "../../entities/fnote";
|
||||
import "./NoteList.css";
|
||||
@@ -22,9 +22,11 @@ interface NoteListProps {
|
||||
displayOnlyCollections?: boolean;
|
||||
isEnabled: boolean;
|
||||
ntxId: string | null | undefined;
|
||||
media: ViewModeMedia;
|
||||
onReady?: () => void;
|
||||
}
|
||||
|
||||
export default function NoteList<T extends object>(props: Pick<NoteListProps, "displayOnlyCollections">) {
|
||||
export default function NoteList<T extends object>(props: Pick<NoteListProps, "displayOnlyCollections" | "media" | "onReady">) {
|
||||
const { note, noteContext, notePath, ntxId } = useNoteContext();
|
||||
const isEnabled = noteContext?.hasNoteList();
|
||||
return <CustomNoteList note={note} isEnabled={!!isEnabled} notePath={notePath} ntxId={ntxId} {...props} />
|
||||
@@ -34,7 +36,7 @@ export function SearchNoteList<T extends object>(props: Omit<NoteListProps, "isE
|
||||
return <CustomNoteList {...props} isEnabled={true} />
|
||||
}
|
||||
|
||||
function CustomNoteList<T extends object>({ note, isEnabled: shouldEnable, notePath, highlightedTokens, displayOnlyCollections, ntxId }: NoteListProps) {
|
||||
export function CustomNoteList<T extends object>({ note, isEnabled: shouldEnable, notePath, highlightedTokens, displayOnlyCollections, ntxId, onReady, ...restProps }: NoteListProps) {
|
||||
const widgetRef = useRef<HTMLDivElement>(null);
|
||||
const viewType = useNoteViewType(note);
|
||||
const noteIds = useNoteIds(note, viewType, ntxId);
|
||||
@@ -76,7 +78,9 @@ function CustomNoteList<T extends object>({ note, isEnabled: shouldEnable, noteP
|
||||
note, noteIds, notePath,
|
||||
highlightedTokens,
|
||||
viewConfig: viewModeConfig[0],
|
||||
saveConfig: viewModeConfig[1]
|
||||
saveConfig: viewModeConfig[1],
|
||||
onReady: onReady ?? (() => {}),
|
||||
...restProps
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +127,7 @@ function useNoteViewType(note?: FNote | null): ViewTypeOptions | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined, ntxId: string | null | undefined) {
|
||||
export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined, ntxId: string | null | undefined) {
|
||||
const [ noteIds, setNoteIds ] = useState<string[]>([]);
|
||||
const [ includeArchived ] = useNoteLabelBoolean(note, "includeArchived");
|
||||
|
||||
@@ -187,7 +191,7 @@ function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOptions |
|
||||
return noteIds;
|
||||
}
|
||||
|
||||
function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined) {
|
||||
export function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined) {
|
||||
const [ viewConfig, setViewConfig ] = useState<[T | undefined, (data: T) => void]>();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -3,6 +3,8 @@ import FNote from "../../entities/fnote";
|
||||
export const allViewTypes = ["list", "grid", "calendar", "table", "geoMap", "board", "presentation"] as const;
|
||||
export type ViewTypeOptions = typeof allViewTypes[number];
|
||||
|
||||
export type ViewModeMedia = "screen" | "print";
|
||||
|
||||
export interface ViewModeProps<T extends object> {
|
||||
note: FNote;
|
||||
notePath: string;
|
||||
@@ -13,4 +15,6 @@ export interface ViewModeProps<T extends object> {
|
||||
highlightedTokens: string[] | null | undefined;
|
||||
viewConfig: T | undefined;
|
||||
saveConfig(newConfig: T): void;
|
||||
media: ViewModeMedia;
|
||||
onReady(): void;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ViewModeProps } from "../interface";
|
||||
import { ViewModeMedia, ViewModeProps } from "../interface";
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||
import Reveal from "reveal.js";
|
||||
import slideBaseStylesheet from "reveal.js/dist/reveal.css?raw";
|
||||
@@ -14,11 +14,11 @@ import { t } from "../../../services/i18n";
|
||||
import { DEFAULT_THEME, loadPresentationTheme } from "./themes";
|
||||
import FNote from "../../../entities/fnote";
|
||||
|
||||
export default function PresentationView({ note, noteIds }: ViewModeProps<{}>) {
|
||||
export default function PresentationView({ note, noteIds, media, onReady }: ViewModeProps<{}>) {
|
||||
const [ presentation, setPresentation ] = useState<PresentationModel>();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [ api, setApi ] = useState<Reveal.Api>();
|
||||
const stylesheets = usePresentationStylesheets(note);
|
||||
const stylesheets = usePresentationStylesheets(note, media);
|
||||
|
||||
function refresh() {
|
||||
buildPresentationModel(note).then(setPresentation);
|
||||
@@ -33,31 +33,59 @@ export default function PresentationView({ note, noteIds }: ViewModeProps<{}>) {
|
||||
|
||||
useLayoutEffect(refresh, [ note, noteIds ]);
|
||||
|
||||
return presentation && stylesheets && (
|
||||
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 = (
|
||||
<>
|
||||
<ShadowDom
|
||||
className="presentation-container"
|
||||
containerRef={containerRef}
|
||||
>
|
||||
{stylesheets.map(stylesheet => <style>{stylesheet}</style>)}
|
||||
<Presentation presentation={presentation} setApi={setApi} />
|
||||
</ShadowDom>
|
||||
<ButtonOverlay containerRef={containerRef} api={api} />
|
||||
{stylesheets.map(stylesheet => <style>{stylesheet}</style>)}
|
||||
<Presentation presentation={presentation} setApi={setApi} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
||||
if (media === "screen") {
|
||||
return (
|
||||
<>
|
||||
<ShadowDom
|
||||
className="presentation-container"
|
||||
containerRef={containerRef}
|
||||
>{content}</ShadowDom>
|
||||
<ButtonOverlay containerRef={containerRef} api={api} />
|
||||
</>
|
||||
)
|
||||
} else if (media === "print") {
|
||||
// Printing needs a query parameter that is read by Reveal.js.
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("print-pdf", "");
|
||||
window.history.replaceState({}, '', url);
|
||||
|
||||
// Shadow DOM doesn't work well with Reveal.js's PDF printing mechanism.
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
function usePresentationStylesheets(note: FNote) {
|
||||
function usePresentationStylesheets(note: FNote, media: ViewModeMedia) {
|
||||
const [ themeName ] = useNoteLabelWithDefault(note, "presentation:theme", DEFAULT_THEME);
|
||||
const [ stylesheets, setStylesheets ] = useState<string[]>();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
loadPresentationTheme(themeName).then((themeStylesheet) => {
|
||||
setStylesheets([
|
||||
let stylesheets = [
|
||||
slideBaseStylesheet,
|
||||
themeStylesheet,
|
||||
slideCustomStylesheet
|
||||
].map(stylesheet => stylesheet.replace(/:root/g, ":host")));
|
||||
];
|
||||
if (media === "screen") {
|
||||
// We are rendering in the shadow DOM, so the global variables are not set correctly.
|
||||
stylesheets = stylesheets.map(stylesheet => stylesheet.replace(/:root/g, ":host"));
|
||||
}
|
||||
setStylesheets(stylesheets);
|
||||
});
|
||||
}, [ themeName ]);
|
||||
|
||||
@@ -128,6 +156,7 @@ function Presentation({ presentation, setApi } : { presentation: PresentationMod
|
||||
const api = new Reveal(containerRef.current, {
|
||||
transition: "slide",
|
||||
embedded: true,
|
||||
pdfMaxPagesPerSlide: 1,
|
||||
keyboardCondition(event) {
|
||||
// Full-screen requests sometimes fail, we rely on the UI button instead.
|
||||
if (event.key === "f") {
|
||||
|
||||
@@ -28,11 +28,12 @@ import ContentWidgetTypeWidget from "./type_widgets/content_widget.js";
|
||||
import AttachmentListTypeWidget from "./type_widgets/attachment_list.js";
|
||||
import AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.js";
|
||||
import MindMapWidget from "./type_widgets/mind_map.js";
|
||||
import utils from "../services/utils.js";
|
||||
import utils, { isElectron } from "../services/utils.js";
|
||||
import type { NoteType } from "../entities/fnote.js";
|
||||
import type TypeWidget from "./type_widgets/type_widget.js";
|
||||
import { MermaidTypeWidget } from "./type_widgets/mermaid.js";
|
||||
import AiChatTypeWidget from "./type_widgets/ai_chat.js";
|
||||
import toast from "../services/toast.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="note-detail">
|
||||
@@ -140,6 +141,13 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.contentSized();
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const { ipcRenderer } = utils.dynamicRequire("electron");
|
||||
ipcRenderer.on("print-done", () => {
|
||||
toast.closePersistent("printing");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
@@ -297,18 +305,53 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
// Trigger in timeout to dismiss the menu while printing.
|
||||
setTimeout(window.print, 0);
|
||||
toast.showPersistent({
|
||||
icon: "bx bx-loader-circle bx-spin",
|
||||
message: t("note_detail.printing"),
|
||||
id: "printing"
|
||||
});
|
||||
|
||||
if (isElectron()) {
|
||||
const { ipcRenderer } = utils.dynamicRequire("electron");
|
||||
ipcRenderer.send("print-note", {
|
||||
notePath: this.notePath
|
||||
});
|
||||
} else {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = `?print#${this.notePath}`;
|
||||
iframe.className = "print-iframe";
|
||||
document.body.appendChild(iframe);
|
||||
iframe.onload = () => {
|
||||
if (!iframe.contentWindow) {
|
||||
toast.closePersistent("printing");
|
||||
document.body.removeChild(iframe);
|
||||
return;
|
||||
}
|
||||
|
||||
iframe.contentWindow.addEventListener("note-ready", () => {
|
||||
toast.closePersistent("printing");
|
||||
iframe.contentWindow?.print();
|
||||
document.body.removeChild(iframe);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async exportAsPdfEvent() {
|
||||
if (!this.noteContext?.isActive() || !this.note) {
|
||||
if (!this.noteContext?.isActive() || !this.note || !this.notePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
toast.showPersistent({
|
||||
icon: "bx bx-loader-circle bx-spin",
|
||||
message: t("note_detail.printing_pdf"),
|
||||
id: "printing"
|
||||
});
|
||||
|
||||
const { ipcRenderer } = utils.dynamicRequire("electron");
|
||||
ipcRenderer.send("export-as-pdf", {
|
||||
title: this.note.title,
|
||||
notePath: this.notePath,
|
||||
pageSize: this.note.getAttributeValue("label", "printPageSize") ?? "Letter",
|
||||
landscape: this.note.hasAttribute("label", "printLandscape")
|
||||
});
|
||||
|
||||
@@ -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"].includes(note.type) || (note.type === "book" && note.getLabelValue("viewType") === "presentation");
|
||||
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 (
|
||||
<Dropdown
|
||||
@@ -74,7 +74,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
|
||||
<CommandItem icon="bx bx-export" text={t("note_actions.export_note")}
|
||||
disabled={isInOptions || note.noteId === "_backendLog"}
|
||||
command={() => noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", {
|
||||
notePath: noteContext.notePath,
|
||||
notePath: noteContext.notePath,
|
||||
defaultType: "single"
|
||||
})} />
|
||||
<FormDropdownDivider />
|
||||
@@ -133,4 +133,4 @@ function ConvertToAttachment({ note }: { note: FNote }) {
|
||||
}}
|
||||
>{t("note_actions.convert_into_attachment")}</FormListItem>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ export default function SearchResult() {
|
||||
|
||||
{state === SearchResultState.GOT_RESULTS && (
|
||||
<SearchNoteList
|
||||
media="screen"
|
||||
note={note}
|
||||
notePath={notePath}
|
||||
highlightedTokens={highlightedTokens}
|
||||
|
||||
@@ -76,7 +76,8 @@ export default defineConfig(() => ({
|
||||
setup: join(__dirname, "src", "setup.ts"),
|
||||
share: join(__dirname, "src", "share.ts"),
|
||||
set_password: join(__dirname, "src", "set_password.ts"),
|
||||
runtime: join(__dirname, "src", "runtime.ts")
|
||||
runtime: join(__dirname, "src", "runtime.ts"),
|
||||
print: join(__dirname, "src", "print.tsx")
|
||||
},
|
||||
output: {
|
||||
entryFileNames: "src/[name].js",
|
||||
|
||||
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
File diff suppressed because one or more lines are too long
@@ -302,7 +302,9 @@
|
||||
<td><code>color</code>
|
||||
</td>
|
||||
<td>defines color of the note in note tree, links etc. Use any valid CSS color
|
||||
value like 'red' or #a13d5f</td>
|
||||
value like 'red' or #a13d5f
|
||||
<br>Note: this color may be automatically adjusted when displayed to ensure
|
||||
sufficient contrast with the background.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>keyboardShortcut</code>
|
||||
|
||||
|
Before Width: | Height: | Size: 150 B After Width: | Height: | Size: 150 B |
@@ -1,42 +0,0 @@
|
||||
<p>
|
||||
<img src="Export as PDF_image.png">
|
||||
</p>
|
||||
<p>Screenshot of the note contextual menu indicating the “Export as PDF”
|
||||
option.</p>
|
||||
<p>On the desktop application of Trilium it is possible to export a note
|
||||
as PDF. On the server or PWA (mobile), the option is not available due
|
||||
to technical constraints and it will be hidden.</p>
|
||||
<p>To print a note, select the
|
||||
<img src="1_Export as PDF_image.png">button to the right of the note and select <em>Export as PDF</em>.</p>
|
||||
<p>Afterwards you will be prompted to select where to save the PDF file.</p>
|
||||
<h2>Automatic opening of the file</h2>
|
||||
<p>When the PDF is exported, it is automatically opened with the system default
|
||||
application for easy preview.</p>
|
||||
<p>Note that if you are using Linux with the GNOME desktop environment, sometimes
|
||||
the default application might seem incorrect (such as opening in GIMP).
|
||||
This is because it uses Gnome's “Recommended applications” list.</p>
|
||||
<p>To solve this, you can change the recommended application for PDFs via
|
||||
this command line. First, list the available applications via <code>gio mime application/pdf</code> and
|
||||
then set the desired one. For example to use GNOME's Evince:</p><pre><code class="language-text-x-trilium-auto">gio mime application/pdf</code></pre>
|
||||
<h2>Reporting issues with the rendering</h2>
|
||||
<p>Should you encounter any visual issues in the resulting PDF file (e.g.
|
||||
a table does not fit properly, there is cut off text, etc.) feel free to
|
||||
<a
|
||||
href="#root/_help_wy8So3yZZlH9">report the issue</a>. In this case, it's best to offer a sample note (click
|
||||
on the
|
||||
<img src="1_Export as PDF_image.png">button, select Export note → This note and all of its descendants → HTML
|
||||
in ZIP archive). Make sure not to accidentally leak any personal information.</p>
|
||||
<h2>Landscape mode</h2>
|
||||
<p>When exporting to PDF, there are no customizable settings such as page
|
||||
orientation, size, etc. However, it is possible to specify a given note
|
||||
to be printed as a PDF in landscape mode by adding the <code>#printLandscape</code> attribute
|
||||
to it (see <a class="reference-link" href="#root/_help_zEY4DaJG4YT5">Attributes</a>).</p>
|
||||
<h2>Page size</h2>
|
||||
<p>By default, the resulting PDF will be in Letter format. It is possible
|
||||
to adjust it to another page size via the <code>#printPageSize</code> attribute,
|
||||
with one of the following values: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.</p>
|
||||
<h2>Keyboard shortcut</h2>
|
||||
<p>It's possible to trigger the export to PDF from the keyboard by going
|
||||
to <em>Keyboard shortcuts</em> in <a class="reference-link"
|
||||
href="#root/_help_4TIF1oA4VQRO">Options</a> and assigning a key combination
|
||||
for the <code>exportAsPdf</code> action.</p>
|
||||
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
111
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.html
generated
vendored
Normal file
111
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.html
generated
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
<figure class="image">
|
||||
<img style="aspect-ratio:951/432;" src="Printing & Exporting as PD.png"
|
||||
width="951" height="432">
|
||||
<figcaption>Screenshot of the note contextual menu indicating the “Export as PDF”
|
||||
option.</figcaption>
|
||||
</figure>
|
||||
<h2>Printing</h2>
|
||||
<p>This feature allows printing of notes. It works on both the desktop client,
|
||||
but also on the web.</p>
|
||||
<p>Note that not all note types are printable as of now. We do plan to increase
|
||||
the coverage of supported note types in the future.</p>
|
||||
<p>To print a note, select the
|
||||
<img src="1_Printing & Exporting as PD.png"
|
||||
width="29" height="31">button to the right of the note and select <em>Print note</em>. Depending
|
||||
on the size and type of the note, this can take up to a few seconds. Afterwards
|
||||
you will be redirected to the system/browser printing dialog.</p>
|
||||
<aside
|
||||
class="admonition note">
|
||||
<p>Printing and exporting as PDF are not perfect. Due to technical limitations,
|
||||
and sometimes even browser glitches the text might appear cut off in some
|
||||
circumstances. </p>
|
||||
</aside>
|
||||
<h2>Reporting issues with the rendering</h2>
|
||||
<p>Should you encounter any visual issues in the resulting PDF file (e.g.
|
||||
a table does not fit properly, there is cut off text, etc.) feel free to
|
||||
<a
|
||||
href="#root/_help_wy8So3yZZlH9">report the issue</a>. In this case, it's best to offer a sample note (click
|
||||
on the
|
||||
<img src="1_Printing & Exporting as PD.png" width="29" height="31">button, select Export note → This note and all of its descendants → HTML
|
||||
in ZIP archive). Make sure not to accidentally leak any personal information.</p>
|
||||
<p>Consider adjusting font sizes and using <a href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/_help_CohkqWQC1iBv">page breaks</a> to
|
||||
work around the layout.</p>
|
||||
<h2>Exporting as PDF</h2>
|
||||
<p>On the desktop application of Trilium it is possible to export a note
|
||||
as PDF. On the server or PWA (mobile), the option is not available due
|
||||
to technical constraints and it will be hidden.</p>
|
||||
<p>To print a note, select the
|
||||
<img src="1_Printing & Exporting as PD.png">button to the right of the note and select <em>Export as PDF</em>. Afterwards
|
||||
you will be prompted to select where to save the PDF file.</p>
|
||||
<h3>Automatic opening of the file</h3>
|
||||
<p>When the PDF is exported, it is automatically opened with the system default
|
||||
application for easy preview.</p>
|
||||
<p>Note that if you are using Linux with the GNOME desktop environment, sometimes
|
||||
the default application might seem incorrect (such as opening in GIMP).
|
||||
This is because it uses Gnome's “Recommended applications” list.</p>
|
||||
<p>To solve this, you can change the recommended application for PDFs via
|
||||
this command line. First, list the available applications via <code>gio mime application/pdf</code> and
|
||||
then set the desired one. For example to use GNOME's Evince:</p><pre><code class="language-text-x-trilium-auto">gio mime application/pdf</code></pre>
|
||||
<h3>Customizing exporting as PDF</h3>
|
||||
<p>When exporting to PDF, there are no customizable settings such as page
|
||||
orientation, size. However, there are a few <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/tC7s2alapj8V/_help_zEY4DaJG4YT5">Attributes</a> to
|
||||
adjust some of the settings:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e76a765fe309058890d4f4d3e63bca578">To print in landscape mode instead of portrait (useful for big diagrams
|
||||
or slides), add <code>#printLandscape</code>.</li>
|
||||
<li data-list-item-id="e78123a3a12954d7c9f520f4e75ed375d">By default, the resulting PDF will be in Letter format. It is possible
|
||||
to adjust it to another page size via the <code>#printPageSize</code> attribute,
|
||||
with one of the following values: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.</li>
|
||||
</ul>
|
||||
<aside class="admonition note">
|
||||
<p>These options have no effect when used with the printing feature, since
|
||||
the user-defined settings are used instead.</p>
|
||||
</aside>
|
||||
<h2>Keyboard shortcut</h2>
|
||||
<p>It's possible to trigger both printing and export as PDF from the keyboard
|
||||
by going to <em>Keyboard shortcuts</em> in <a class="reference-link"
|
||||
href="#root/_help_4TIF1oA4VQRO">Options</a> and assigning a key combination
|
||||
for:</p>
|
||||
<ul>
|
||||
<li class="ck-list-marker-italic" data-list-item-id="e52b4441f2a3af7773585b19c8f796c8d"><em>Print Active Note</em>
|
||||
</li>
|
||||
<li class="ck-list-marker-italic" data-list-item-id="e40e1f6f480c7857100f64c2f63e062fb"><em>Export Active Note as PDF</em>
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Constraints & limitations</h2>
|
||||
<p>Not all <a class="reference-link" href="#root/pOsGYCXsbNQG/_help_KSZ04uQ2D1St">Note Types</a> are
|
||||
supported when printing, in which case the <em>Print</em> and <em>Export as PDF</em> options
|
||||
will be disabled.</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e448a833da1bb1643181c72779382d1b6">For <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6f9hih2hXXZk">Code</a> notes:
|
||||
<ul>
|
||||
<li data-list-item-id="e6b699104a21930de7c2618123d0d5750">Line numbers are not printed.</li>
|
||||
<li data-list-item-id="e342cba939534f0989c9620520c7b87a3">Syntax highlighting is enabled, however a default theme (Visual Studio)
|
||||
is enforced.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li data-list-item-id="ea23948a21d6ad2a01d70dfbdfa9bd62f">For <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_GTwFsgaA0lCt">Collections</a>:
|
||||
<ul>
|
||||
<li data-list-item-id="e387ee52424d8b78fd61c3a1e0d395d3e">Only <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/GTwFsgaA0lCt/_help_zP3PMqaG71Ct">Presentation View</a> is
|
||||
currently supported.</li>
|
||||
<li data-list-item-id="e94fa0cced6a7668c96cf7513557f2906">We plan to add support for all the collection types at some point.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li data-list-item-id="e8b17c931e05f9d579767536088a108da">Using <a class="reference-link" href="#root/pOsGYCXsbNQG/pKK96zzmvBGf/_help_AlhDUqhENtH7">Custom app-wide CSS</a> for
|
||||
printing is not longer supported, due to a more stable but isolated mechanism.
|
||||
<ul>
|
||||
<li data-list-item-id="eae91219e277cc2366d86d47291ed9cec">We plan to introduce a new mechanism specifically for a print CSS.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Under the hood</h2>
|
||||
<p>Both printing and exporting as PDF use the same mechanism: a note is rendered
|
||||
individually in a separate webpage that is then sent to the browser or
|
||||
the Electron application either for printing or exporting as PDF.</p>
|
||||
<p>The webpage that renders a single note can actually be accessed in a web
|
||||
browser. For example <code>http://localhost:8080/#root/WWRGzqHUfRln/RRZsE9Al8AIZ?ntxId=0o4fzk</code> becomes <code>http://localhost:8080/?print#root/WWRGzqHUfRln/RRZsE9Al8AIZ</code>.</p>
|
||||
<p>Accessing the print note in a web browser allows for easy debugging to
|
||||
understand why a particular note doesn't render well. The mechanism for
|
||||
rendering is similar to the one used in <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/_help_0ESUbbAxVnoK">Note List</a>.</p>
|
||||
@@ -6,33 +6,31 @@
|
||||
within Trilium.</p>
|
||||
<h2>How it works</h2>
|
||||
<ul>
|
||||
<li data-list-item-id="e51cbe078fb06e2bfdfb1f2bf6fd82225">Each slide is a child note of the collection.</li>
|
||||
<li data-list-item-id="efffc6f15623109770c57338c61b4ccb6">The order of the child notes determines the order of the slides.</li>
|
||||
<li
|
||||
data-list-item-id="e1a795af0f85ba888f84586be6ed2de2a">Unlike traditional presentation software, slides can be laid out both
|
||||
<li>Each slide is a child note of the collection.</li>
|
||||
<li>The order of the child notes determines the order of the slides.</li>
|
||||
<li>Unlike traditional presentation software, slides can be laid out both
|
||||
horizontally and vertically (see belwo for more information).</li>
|
||||
<li data-list-item-id="e9a2a74d8e19974766f65416100e8f877">Direct children will be laid out horizontally and the children of those
|
||||
will be laid out vertically. Children deeper than two levels of nesting
|
||||
are ignored.</li>
|
||||
<li>Direct children will be laid out horizontally and the children of those
|
||||
will be laid out vertically. Children deeper than two levels of nesting
|
||||
are ignored.</li>
|
||||
</ul>
|
||||
<h2>Interaction and navigation</h2>
|
||||
<p>In the floating buttons section (top-right):</p>
|
||||
<ul>
|
||||
<li data-list-item-id="ee6dd604137e6918dcfac24fe271b05bf">Edit button to go to the corresponding note of the current slide.</li>
|
||||
<li
|
||||
data-list-item-id="e4805f237077e9dc8ddbed2cb0b56e585">Press Overview button (or the <kbd>O</kbd> key) to show a birds-eye view
|
||||
<li>Edit button to go to the corresponding note of the current slide.</li>
|
||||
<li>Press Overview button (or the <kbd>O</kbd> key) to show a birds-eye view
|
||||
of the slides. Press the button again to disable it.</li>
|
||||
<li data-list-item-id="ee714da289257895faf87a26f6849e050">Press the “Start presentation” button to show the presentation in full-screen.</li>
|
||||
<li>Press the “Start presentation” button to show the presentation in full-screen.</li>
|
||||
</ul>
|
||||
<p>The following keyboard shortcuts are supported:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e5a34fbaa9c98cd91ffac8301e153a083">Press <kbd>←</kbd> and <kbd>→</kbd> (or <kbd>H</kbd> and <kbd>L</kbd>) to go
|
||||
<li>Press <kbd>←</kbd> and <kbd>→</kbd> (or <kbd>H</kbd> and <kbd>L</kbd>) to go
|
||||
to the slide on the left or on the right (horizontal).</li>
|
||||
<li data-list-item-id="e39394b060a9b767d04c466440106cbf0">Press <kbd>↑</kbd> and <kbd>↓</kbd> (or <kbd>K</kbd> and <kbd>J</kbd>)
|
||||
<li>Press <kbd>↑</kbd> and <kbd>↓</kbd> (or <kbd>K</kbd> and <kbd>J</kbd>)
|
||||
to go to the upward or downward slide (vertical).</li>
|
||||
<li data-list-item-id="e17441e9598f687e89a161a8afe2f703d">Press <kbd>Space</kbd> and <kbd>Shift</kbd> + <kbd>Space</kbd> or to go
|
||||
<li>Press <kbd>Space</kbd> and <kbd>Shift</kbd> + <kbd>Space</kbd> or to go
|
||||
to the next/previous slide in order.</li>
|
||||
<li data-list-item-id="e4212130fc1fdc5de980e2e40feae68c2">And a few more, press <kbd>?</kbd> to display a popup with all the supported
|
||||
<li>And a few more, press <kbd>?</kbd> to display a popup with all the supported
|
||||
keyboard combinations.</li>
|
||||
</ul>
|
||||
<h2>Vertical slides and nesting</h2>
|
||||
@@ -42,15 +40,15 @@
|
||||
<p>This horizontal/vertical organization affects transitions (especially
|
||||
on the “slide” transition), however it is most noticeable in navigation.</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e9245eba99da45713930c1714202add31">Pressing <kbd>←</kbd> and <kbd>→</kbd> will navigate through slides horizontally,
|
||||
<li>Pressing <kbd>←</kbd> and <kbd>→</kbd> will navigate through slides horizontally,
|
||||
thus skipping vertical notes under the current slide. This is useful to
|
||||
skip entire chapters/related slides. </li>
|
||||
<li data-list-item-id="ef9aedf69e5e2a599e9fbd0de1b89b4ad">Pressing <kbd>↑</kbd> and <kbd>↓</kbd> will navigate through the vertical
|
||||
skip entire chapters/related slides.</li>
|
||||
<li>Pressing <kbd>↑</kbd> and <kbd>↓</kbd> will navigate through the vertical
|
||||
slides at the current level.</li>
|
||||
<li data-list-item-id="e436fc36f74a22fdefe31e498684e23b3">Pressing <kbd>Space</kbd> and <kbd>Shift</kbd> + <kbd>Space</kbd> will go to
|
||||
<li>Pressing <kbd>Space</kbd> and <kbd>Shift</kbd> + <kbd>Space</kbd> will go to
|
||||
the next/previous slide in order, regardless of the direction. This is
|
||||
generally the key combination to use when presenting.</li>
|
||||
<li data-list-item-id="e9c5dcf5efec250876bd2c527082e76d7">The arrows on the bottom-right of the slide will also reflect this navigation
|
||||
<li>The arrows on the bottom-right of the slide will also reflect this navigation
|
||||
scheme.</li>
|
||||
</ul>
|
||||
<figure class="image image-style-align-right image_resized" style="width:55.57%;">
|
||||
@@ -62,19 +60,19 @@
|
||||
slides.</p>
|
||||
<p>In the following example, the note structure is as follows:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e4d5d440ec56a9c81b7c8323ab142478d">Presentation collection
|
||||
<li>Presentation collection
|
||||
<ul>
|
||||
<li data-list-item-id="e255021a351d18e2792c15ab2b80c0a57">Trilium Notes (demo page)</li>
|
||||
<li data-list-item-id="ef6f95ec54572aa247a13e8104a2db0c3">“Introduction” slide
|
||||
<li>Trilium Notes (demo page)</li>
|
||||
<li>“Introduction” slide
|
||||
<ul>
|
||||
<li data-list-item-id="eccb526d7f2dd67ac686f8e963e660a77">“The challenge of personal knowledge management”</li>
|
||||
<li data-list-item-id="e0aca477122bb0b00ab9b3bc163436b5f">“Note-taking structures”</li>
|
||||
<li>“The challenge of personal knowledge management”</li>
|
||||
<li>“Note-taking structures”</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li data-list-item-id="e3bbbe33c8d7a18cb17cd0de29b6eff05">“Demo & Feature highlights” slide
|
||||
<li>“Demo & Feature highlights” slide
|
||||
<ul>
|
||||
<li data-list-item-id="ebf5e3fe8a8b4400f21d5cc99b8198898">“Really fast installation process”</li>
|
||||
<li data-list-item-id="e0b6885a0bf7f76fa2ce7801f004a2d42">Video slide</li>
|
||||
<li>“Really fast installation process”</li>
|
||||
<li>Video slide</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -83,56 +81,54 @@
|
||||
<h2>Customization</h2>
|
||||
<p>At collection level, it's possible to adjust:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="edc0a7c71ee836a225a8793e3cb1e29e8">The theme of the entire presentation to one of the predefined themes by
|
||||
going to the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_BlN9DFI679QC">Ribbon</a> and
|
||||
<li>The theme of the entire presentation to one of the predefined themes by
|
||||
going to the <a class="reference-link" href="#root/_help_BlN9DFI679QC">Ribbon</a> and
|
||||
looking for the <em>Collection Properties</em> tab.</li>
|
||||
<li data-list-item-id="ed04e1bd7a997de717d8b8b8b90f19e7f">It's currently not possible to create custom themes, although it is planned.</li>
|
||||
<li
|
||||
data-list-item-id="edb37c7902c9e464de4555ec3ede05403">Note that it is note possible to alter the CSS via <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/pKK96zzmvBGf/_help_AlhDUqhENtH7">Custom app-wide CSS</a> because
|
||||
the slides are rendered isolated (in a shadow DOM).</li>
|
||||
<li>It's currently not possible to create custom themes, although it is planned.</li>
|
||||
<li>Note that it is note possible to alter the CSS via <a class="reference-link"
|
||||
href="#root/_help_AlhDUqhENtH7">Custom app-wide CSS</a> because the
|
||||
slides are rendered isolated (in a shadow DOM).</li>
|
||||
</ul>
|
||||
<p>At slide level:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="eb9c23ec94dcd00a3a8539d3cd633d7df">It's possible to adjust the background color of a slide by using the
|
||||
<li>It's possible to adjust the background color of a slide by using the
|
||||
<a
|
||||
href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_OFXdgB2nNk1F">predefined promoted attribute</a>for the color or manually setting <code>#slide:background</code> to
|
||||
href="#root/_help_OFXdgB2nNk1F">predefined promoted attribute</a>for the color or manually setting <code>#slide:background</code> to
|
||||
a hex color.</li>
|
||||
<li data-list-item-id="e70af66a7d3468b7fa86badb1e2c93cc9">More complex backgrounds can be achieved via gradients. There's no UI
|
||||
<li>More complex backgrounds can be achieved via gradients. There's no UI
|
||||
for it; it has to be set via <code>#slide:background</code> to a CSS gradient
|
||||
definition such as: <code>linear-gradient(to bottom, #283b95, #17b2c3)</code>.</li>
|
||||
</ul>
|
||||
<h2>Tips and tricks</h2>
|
||||
<ul>
|
||||
<li data-list-item-id="ec501025735d0063969f2a48eedb651dc">Text notes generally respect the formatting (bold, italic, foreground
|
||||
<li>Text notes generally respect the formatting (bold, italic, foreground
|
||||
and background colors) and font size. Code blocks and tables also work.</li>
|
||||
<li
|
||||
data-list-item-id="e8acd457a2660726905aee30a9325a620">Try using more than just text notes, the presentation uses the same mechanism
|
||||
as <a href="#root/pOsGYCXsbNQG/tC7s2alapj8V/_help_R9pX4DGra2Vt">shared notes</a> and
|
||||
<li>Try using more than just text notes, the presentation uses the same mechanism
|
||||
as <a href="#root/_help_R9pX4DGra2Vt">shared notes</a> and <a class="reference-link"
|
||||
href="#root/_help_0ESUbbAxVnoK">Note List</a> so it should be able
|
||||
to display <a class="reference-link" href="#root/_help_s1aBHPd79XYj">Mermaid Diagrams</a>,
|
||||
<a
|
||||
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/_help_0ESUbbAxVnoK">Note List</a> so it should be able to display <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_s1aBHPd79XYj">Mermaid Diagrams</a>,
|
||||
<a
|
||||
class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_grjYqerjn243">Canvas</a> and <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_gBbsAeiuUxI5">Mind Map</a> in
|
||||
full-screen (without the interactivity).
|
||||
<ul>
|
||||
<li data-list-item-id="e91cdf4823552f771ed802de7fd6330e4">Consider using a transparent background for <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_grjYqerjn243">Canvas</a>, if
|
||||
the slides have a custom background (go to the hamburger menu in the Canvas,
|
||||
press the button select a custom color and write <code>transparent</code>).</li>
|
||||
<li
|
||||
data-list-item-id="ebed408174c89a15dc9b0ee74d36e2e70">
|
||||
<p>For <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_s1aBHPd79XYj">Mermaid Diagrams</a>,
|
||||
some of them have a predefined background which can be changed via the
|
||||
frontmatter. For example, for XY-charts:</p><pre><code class="language-text-x-trilium-auto">---
|
||||
class="reference-link" href="#root/_help_grjYqerjn243">Canvas</a> and <a class="reference-link" href="#root/_help_gBbsAeiuUxI5">Mind Map</a> in
|
||||
full-screen (without the interactivity).
|
||||
<ul>
|
||||
<li>
|
||||
<p>Consider using a transparent background for <a class="reference-link"
|
||||
href="#root/_help_grjYqerjn243">Canvas</a>, if the slides have a custom
|
||||
background (go to the hamburger menu in the Canvas, press the button select
|
||||
a custom color and write <code>transparent</code>).</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>For <a class="reference-link" href="#root/_help_s1aBHPd79XYj">Mermaid Diagrams</a>,
|
||||
some of them have a predefined background which can be changed via the
|
||||
frontmatter. For example, for XY-charts:</p><pre><code class="language-text-x-trilium-auto">---
|
||||
config:
|
||||
themeVariables:
|
||||
xyChart:
|
||||
backgroundColor: transparent
|
||||
---</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Under the hood</h2>
|
||||
<p>The Presentation view uses <a href="https://revealjs.com/">Reveal.js</a> to
|
||||
|
||||
@@ -1,38 +1,37 @@
|
||||
<p>It is possible to provide a CSS file to be used regardless of the theme
|
||||
set by the user.</p>
|
||||
<figure class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="Custom app-wide CSS_image.png">
|
||||
</td>
|
||||
<td>Start by creating a new note and changing the note type to CSS</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="2_Custom app-wide CSS_image.png">
|
||||
</td>
|
||||
<td>In the ribbon, press the “Owned Attributes” section and type <code>#appCss</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="3_Custom app-wide CSS_image.png">
|
||||
</td>
|
||||
<td>Type the desired CSS.
|
||||
<br>
|
||||
<br>Generally it's a good idea to append <code>!important</code> for the styles
|
||||
that are being changed, in order to prevent other</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="Custom app-wide CSS_image.png">
|
||||
</td>
|
||||
<td>Start by creating a new note and changing the note type to CSS</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="2_Custom app-wide CSS_image.png">
|
||||
</td>
|
||||
<td>In the ribbon, press the “Owned Attributes” section and type <code>#appCss</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="3_Custom app-wide CSS_image.png">
|
||||
</td>
|
||||
<td>Type the desired CSS.
|
||||
<br>
|
||||
<br>Generally it's a good idea to append <code>!important</code> for the styles
|
||||
that are being changed, in order to prevent other</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Seeing the changes</h2>
|
||||
<p>Adding a new <em>app CSS note</em> or modifying an existing one does not
|
||||
immediately apply changes. To see the changes, press Ctrl+Shift+R to refresh
|
||||
@@ -54,10 +53,9 @@
|
||||
workspaces.</p>
|
||||
<p>To do so:</p>
|
||||
<ol>
|
||||
<li data-list-item-id="eaca1b6777262e20c38dae5e2c2ef8496">In the note with <code>#workspace</code>, add an inheritable attribute <code>#cssClass(inheritable)</code> with
|
||||
<li>In the note with <code>#workspace</code>, add an inheritable attribute <code>#cssClass(inheritable)</code> with
|
||||
a value that uniquely identifies the workspace (say <code>my-workspace</code>).</li>
|
||||
<li
|
||||
data-list-item-id="e01663cf2128c10a0cd0cab1bb27fd44d">Anywhere in the note structure, create a CSS note with <code>#appCss</code>.</li>
|
||||
<li>Anywhere in the note structure, create a CSS note with <code>#appCss</code>.</li>
|
||||
</ol>
|
||||
<h4>Change the color of the icons in the <a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a></h4><pre><code class="language-text-x-trilium-auto">.fancytree-node.my-workspace.fancytree-custom-icon {
|
||||
color: #ff0000;
|
||||
@@ -73,8 +71,8 @@
|
||||
width="641" height="630">
|
||||
</figure>
|
||||
<ol>
|
||||
<li data-list-item-id="e6e7ec9751bdc6f7846d10bf42c42c9b1">Insert an image in any note and take the URL of the image.</li>
|
||||
<li data-list-item-id="edc7f77ed718593d91bda3b2983b81bed">Use the following CSS, adjusting the <code>background-image</code> and <code>width</code> and <code>height</code> to
|
||||
<li>Insert an image in any note and take the URL of the image.</li>
|
||||
<li>Use the following CSS, adjusting the <code>background-image</code> and <code>width</code> and <code>height</code> to
|
||||
the desired values.</li>
|
||||
</ol><pre><code class="language-text-x-trilium-auto">.note-split.my-workspace .scrolling-container:after {
|
||||
position: fixed;
|
||||
@@ -94,5 +92,5 @@
|
||||
<p>Some parts of the application can't be styled directly via custom CSS
|
||||
because they are rendered in an isolated mode (shadow DOM), more specifically:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e3ce2c089fe536bc42856bb1b5edc8c8e">The slides in a <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/GTwFsgaA0lCt/_help_zP3PMqaG71Ct">Presentation View</a>.</li>
|
||||
<li>The slides in a <a class="reference-link" href="#root/_help_zP3PMqaG71Ct">Presentation View</a>.</li>
|
||||
</ul>
|
||||
@@ -373,7 +373,8 @@
|
||||
"export_filter": "PDF Document (*.pdf)",
|
||||
"unable-to-export-message": "The current note could not be exported as a PDF.",
|
||||
"unable-to-export-title": "Unable to export as PDF",
|
||||
"unable-to-save-message": "The selected file could not be written to. Try again or select another destination."
|
||||
"unable-to-save-message": "The selected file could not be written to. Try again or select another destination.",
|
||||
"unable-to-print": "Unable to print the note"
|
||||
},
|
||||
"tray": {
|
||||
"tooltip": "Trilium Notes",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
window.glob = {
|
||||
device: "<%= device %>",
|
||||
baseApiUrl: 'api/',
|
||||
baseApiUrl: "<%= baseApiUrl %>",
|
||||
activeDialog: null,
|
||||
maxEntityChangeIdAtLoad: <%= maxEntityChangeIdAtLoad %>,
|
||||
maxEntityChangeSyncIdAtLoad: <%= maxEntityChangeSyncIdAtLoad %>,
|
||||
|
||||
32
apps/server/src/assets/views/print.ejs
Normal file
32
apps/server/src/assets/views/print.ejs
Normal file
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
|
||||
<link rel="manifest" crossorigin="use-credentials" href="../manifest.webmanifest">
|
||||
<title>Trilium Notes</title>
|
||||
<script src="../<%= appPath %>/runtime.js" crossorigin type="module"></script>
|
||||
</head>
|
||||
<body
|
||||
id="trilium-print"
|
||||
lang="<%= currentLocale.id %>" dir="<%= currentLocale.rtl ? 'rtl' : 'ltr' %>"
|
||||
>
|
||||
<noscript><%= t("javascript-required") %></noscript>
|
||||
|
||||
<script>
|
||||
// hide body to reduce flickering on the startup. This is done through JS and not CSS to not hide <noscript>
|
||||
document.getElementsByTagName("body")[0].style.display = "none";
|
||||
</script>
|
||||
|
||||
<%- include("./partials/windowGlobal.ejs", locals) %>
|
||||
|
||||
<!-- Required for correct loading of scripts in Electron -->
|
||||
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
|
||||
|
||||
<script src="../<%= appPath %>/print.js" crossorigin type="module"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -16,9 +16,11 @@ import type { Request, Response } from "express";
|
||||
import type BNote from "../becca/entities/bnote.js";
|
||||
import { getCurrentLocale } from "../services/i18n.js";
|
||||
|
||||
type View = "desktop" | "mobile" | "print";
|
||||
|
||||
function index(req: Request, res: Response) {
|
||||
const options = optionService.getOptionMap();
|
||||
const view = getView(req);
|
||||
const options = optionService.getOptionMap();
|
||||
|
||||
//'overwrite' set to false (default) => the existing token will be re-used and validated
|
||||
//'validateOnReuse' set to false => if validation fails, generate a new token instead of throwing an error
|
||||
@@ -57,13 +59,19 @@ function index(req: Request, res: Response) {
|
||||
isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(),
|
||||
maxContentWidth: Math.max(640, parseInt(options.maxContentWidth)),
|
||||
triliumVersion: packageJson.version,
|
||||
assetPath: assetPath,
|
||||
appPath: appPath,
|
||||
assetPath,
|
||||
appPath,
|
||||
baseApiUrl: 'api/',
|
||||
currentLocale: getCurrentLocale()
|
||||
});
|
||||
}
|
||||
|
||||
function getView(req: Request): "desktop" | "mobile" {
|
||||
function getView(req: Request): View {
|
||||
// Special override for printing.
|
||||
if ("print" in req.query) {
|
||||
return "print";
|
||||
}
|
||||
|
||||
// Electron always uses the desktop view.
|
||||
if (isElectron) {
|
||||
return "desktop";
|
||||
|
||||
@@ -378,8 +378,6 @@ function register(app: express.Application) {
|
||||
asyncApiRoute(PST, "/api/llm/chat/:chatNoteId/messages", llmRoute.sendMessage);
|
||||
asyncApiRoute(PST, "/api/llm/chat/:chatNoteId/messages/stream", llmRoute.streamMessage);
|
||||
|
||||
|
||||
|
||||
// LLM provider endpoints - moved under /api/llm/providers hierarchy
|
||||
asyncApiRoute(GET, "/api/llm/providers/ollama/models", ollamaRoute.listModels);
|
||||
asyncApiRoute(GET, "/api/llm/providers/openai/models", openaiRoute.listModels);
|
||||
|
||||
@@ -8,10 +8,12 @@ import sqlInit from "./sql_init.js";
|
||||
import cls from "./cls.js";
|
||||
import keyboardActionsService from "./keyboard_actions.js";
|
||||
import electron from "electron";
|
||||
import type { App, BrowserWindowConstructorOptions, BrowserWindow, WebContents } from "electron";
|
||||
import type { App, BrowserWindowConstructorOptions, BrowserWindow, WebContents, IpcMainEvent } from "electron";
|
||||
import { formatDownloadTitle, isDev, isMac, isWindows } from "./utils.js";
|
||||
import { t } from "i18next";
|
||||
import { RESOURCE_DIR } from "./resource_dir.js";
|
||||
import { PerformanceObserverEntryList } from "perf_hooks";
|
||||
import options from "./options.js";
|
||||
|
||||
// Prevent the window being garbage collected
|
||||
let mainWindow: BrowserWindow | null;
|
||||
@@ -67,61 +69,102 @@ electron.ipcMain.on("create-extra-window", (event, arg) => {
|
||||
createExtraWindow(arg.extraWindowHash);
|
||||
});
|
||||
|
||||
interface PrintOpts {
|
||||
notePath: string;
|
||||
printToPdf: boolean;
|
||||
}
|
||||
|
||||
interface ExportAsPdfOpts {
|
||||
notePath: string;
|
||||
title: string;
|
||||
landscape: boolean;
|
||||
pageSize: "A0" | "A1" | "A2" | "A3" | "A4" | "A5" | "A6" | "Legal" | "Letter" | "Tabloid" | "Ledger";
|
||||
}
|
||||
|
||||
electron.ipcMain.on("export-as-pdf", async (e, opts: ExportAsPdfOpts) => {
|
||||
const browserWindow = electron.BrowserWindow.fromWebContents(e.sender);
|
||||
if (!browserWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = electron.dialog.showSaveDialogSync(browserWindow, {
|
||||
defaultPath: formatDownloadTitle(opts.title, "file", "application/pdf"),
|
||||
filters: [
|
||||
{
|
||||
name: t("pdf.export_filter"),
|
||||
extensions: ["pdf"]
|
||||
}
|
||||
]
|
||||
electron.ipcMain.on("print-note", async (e, { notePath }: PrintOpts) => {
|
||||
const browserWindow = await getBrowserWindowForPrinting(e, notePath);
|
||||
browserWindow.webContents.print({}, (success, failureReason) => {
|
||||
if (!success) {
|
||||
electron.dialog.showErrorBox(t("pdf.unable-to-print"), failureReason);
|
||||
}
|
||||
e.sender.send("print-done");
|
||||
browserWindow.destroy();
|
||||
});
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
let buffer: Buffer;
|
||||
try {
|
||||
buffer = await browserWindow.webContents.printToPDF({
|
||||
landscape: opts.landscape,
|
||||
pageSize: opts.pageSize,
|
||||
generateDocumentOutline: true,
|
||||
generateTaggedPDF: true,
|
||||
printBackground: true,
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: `<div></div>`,
|
||||
footerTemplate: `
|
||||
<div class="pageNumber" style="width: 100%; text-align: center; font-size: 10pt;">
|
||||
</div>
|
||||
`
|
||||
});
|
||||
} catch (e) {
|
||||
electron.dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-export-message"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.writeFile(filePath, buffer);
|
||||
} catch (e) {
|
||||
electron.dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-save-message"));
|
||||
return;
|
||||
}
|
||||
|
||||
electron.shell.openPath(filePath);
|
||||
});
|
||||
|
||||
electron.ipcMain.on("export-as-pdf", async (e, { title, notePath, landscape, pageSize }: ExportAsPdfOpts) => {
|
||||
const browserWindow = await getBrowserWindowForPrinting(e, notePath);
|
||||
|
||||
async function print() {
|
||||
const filePath = electron.dialog.showSaveDialogSync(browserWindow, {
|
||||
defaultPath: formatDownloadTitle(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,
|
||||
pageSize,
|
||||
generateDocumentOutline: true,
|
||||
generateTaggedPDF: true,
|
||||
printBackground: true,
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: `<div></div>`,
|
||||
footerTemplate: `
|
||||
<div class="pageNumber" style="width: 100%; text-align: center; font-size: 10pt;">
|
||||
</div>
|
||||
`
|
||||
});
|
||||
} catch (e) {
|
||||
electron.dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-export-message"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.writeFile(filePath, buffer);
|
||||
} catch (e) {
|
||||
electron.dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-save-message"));
|
||||
return;
|
||||
}
|
||||
|
||||
electron.shell.openPath(filePath);
|
||||
}
|
||||
|
||||
try {
|
||||
await print();
|
||||
} finally {
|
||||
e.sender.send("print-done");
|
||||
browserWindow.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
async function getBrowserWindowForPrinting(e: IpcMainEvent, notePath: string) {
|
||||
const browserWindow = new electron.BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
offscreen: true,
|
||||
session: e.sender.session
|
||||
},
|
||||
});
|
||||
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());
|
||||
});
|
||||
`);
|
||||
return browserWindow;
|
||||
}
|
||||
|
||||
async function createMainWindow(app: App) {
|
||||
if ("setUserTasks" in app) {
|
||||
app.setUserTasks([
|
||||
|
||||
Reference in New Issue
Block a user