mirror of
https://github.com/zadam/trilium.git
synced 2026-01-06 23:42:15 +01:00
Compare commits
323 Commits
static-cor
...
renovate/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42f3968cc0 | ||
|
|
7f2cc885fe | ||
|
|
19a365a370 | ||
|
|
9a50da328e | ||
|
|
181e36a7c1 | ||
|
|
178508d245 | ||
|
|
d132d084cf | ||
|
|
494b55d685 | ||
|
|
51513d3779 | ||
|
|
458398f2ca | ||
|
|
7a6cc4f51e | ||
|
|
f4ccce7de5 | ||
|
|
f8b5417d6c | ||
|
|
13ce8cf498 | ||
|
|
6c2afc086c | ||
|
|
93d50712a9 | ||
|
|
ed91a44928 | ||
|
|
cd10e66fbb | ||
|
|
d6aa126fcc | ||
|
|
3308c7bdf4 | ||
|
|
56341a1a73 | ||
|
|
0857e1a536 | ||
|
|
5d6b25a29e | ||
|
|
5bc15a5448 | ||
|
|
51a19d0544 | ||
|
|
fb4e912ed0 | ||
|
|
20c2652013 | ||
|
|
971d6ad9e3 | ||
|
|
757fc7a7fe | ||
|
|
e4d0a4554a | ||
|
|
dfab7dbc4b | ||
|
|
0039f4c155 | ||
|
|
23f7dc63b8 | ||
|
|
e485b75a44 | ||
|
|
dbef57d329 | ||
|
|
c650441655 | ||
|
|
e573a8af77 | ||
|
|
b23252d046 | ||
|
|
2f7448dbd4 | ||
|
|
9bf4aa2968 | ||
|
|
d78a7bad3b | ||
|
|
b812177e78 | ||
|
|
4710a6af41 | ||
|
|
a613980ea4 | ||
|
|
20ae1f844b | ||
|
|
69511134e5 | ||
|
|
75952563e4 | ||
|
|
21cf5e1df7 | ||
|
|
9df5505989 | ||
|
|
1809d59193 | ||
|
|
feaa54d660 | ||
|
|
c94bd41162 | ||
|
|
f1f3e66537 | ||
|
|
80363cdc73 | ||
|
|
02e08fdf12 | ||
|
|
42283b2469 | ||
|
|
d3b598a5b2 | ||
|
|
0dd3a03c6b | ||
|
|
2144888447 | ||
|
|
b2549066dc | ||
|
|
cd1f3aa9a7 | ||
|
|
1674401342 | ||
|
|
7ba8dbbf6e | ||
|
|
ad27d9ed0e | ||
|
|
482d2f9624 | ||
|
|
824ef704d4 | ||
|
|
58b73cfc7d | ||
|
|
0465fea2db | ||
|
|
39b75e3561 | ||
|
|
2933db9b16 | ||
|
|
d94914046b | ||
|
|
9cf384b14b | ||
|
|
614a2f0ccb | ||
|
|
5cecc72384 | ||
|
|
3ad37fb602 | ||
|
|
42b048c2bf | ||
|
|
a01bf3dfa1 | ||
|
|
ad60988553 | ||
|
|
c9ae4e4cc6 | ||
|
|
d2639851d5 | ||
|
|
8dc5f9cfa4 | ||
|
|
d99a408e04 | ||
|
|
5f14861682 | ||
|
|
8f8493f3ec | ||
|
|
62af66b5ae | ||
|
|
e8d1fa7447 | ||
|
|
ee03871405 | ||
|
|
345378d97f | ||
|
|
07a463ee52 | ||
|
|
3157047160 | ||
|
|
a1dda3b578 | ||
|
|
e161ffce57 | ||
|
|
0c1859dc43 | ||
|
|
e4dcc0f768 | ||
|
|
74ab591214 | ||
|
|
7bd7996893 | ||
|
|
505ae4eeb5 | ||
|
|
951d6d3ce3 | ||
|
|
5ff7764699 | ||
|
|
0d74998625 | ||
|
|
29b70a12bd | ||
|
|
d84150e97b | ||
|
|
2b2ef4251f | ||
|
|
2840ea0f38 | ||
|
|
542d485267 | ||
|
|
cdd4fbc81d | ||
|
|
bfdddab0a0 | ||
|
|
44d1d01105 | ||
|
|
120bb09171 | ||
|
|
b7af99c671 | ||
|
|
869e0b3973 | ||
|
|
b68613dee4 | ||
|
|
ce0f32e7d5 | ||
|
|
78bc9b59c2 | ||
|
|
23cf3d2923 | ||
|
|
335136f3a3 | ||
|
|
11dd7aef09 | ||
|
|
2d1769e2f9 | ||
|
|
21e26147b0 | ||
|
|
ba301f8c12 | ||
|
|
3420374649 | ||
|
|
644d3a181f | ||
|
|
4be3011a8a | ||
|
|
5aa0a956dd | ||
|
|
7fdb1bdce8 | ||
|
|
57c6cef2bd | ||
|
|
e5599adca1 | ||
|
|
ab392ffb7f | ||
|
|
7585d4b258 | ||
|
|
ff82d9c38c | ||
|
|
920fde69bb | ||
|
|
053812e5f0 | ||
|
|
c2f59c4b6c | ||
|
|
06980fe9b5 | ||
|
|
3f5616f1fc | ||
|
|
b6af3b70b0 | ||
|
|
d8e4547988 | ||
|
|
34f649155e | ||
|
|
11779fe3e3 | ||
|
|
032cde67b0 | ||
|
|
229636a796 | ||
|
|
da9c9ac346 | ||
|
|
3fecc4c648 | ||
|
|
98cefcf77b | ||
|
|
413ee81ffa | ||
|
|
578ca8785e | ||
|
|
da4112c078 | ||
|
|
704c7c881d | ||
|
|
63b6abdb9d | ||
|
|
2e936a3d5c | ||
|
|
606574e18e | ||
|
|
1021879167 | ||
|
|
dc4aa9c607 | ||
|
|
b2c3d78773 | ||
|
|
8d3a0b5295 | ||
|
|
9879d07bec | ||
|
|
7bfce851e7 | ||
|
|
34e81881ec | ||
|
|
0143d6c60d | ||
|
|
267c2bc907 | ||
|
|
316f27d88c | ||
|
|
452b56f470 | ||
|
|
43aeaa4455 | ||
|
|
08b7a6985e | ||
|
|
4bbd8e28c1 | ||
|
|
fcf4c09389 | ||
|
|
2c323cbe80 | ||
|
|
7ec7b6bd7b | ||
|
|
b2378f2a53 | ||
|
|
8bf8d85bb7 | ||
|
|
676173e895 | ||
|
|
d8649c87e0 | ||
|
|
b9456ca466 | ||
|
|
cfccbb8927 | ||
|
|
a18578362a | ||
|
|
2f9f94dee0 | ||
|
|
c84e45ddee | ||
|
|
ea558d8c9d | ||
|
|
b936a35b63 | ||
|
|
b4ef4c2143 | ||
|
|
0ff4756ef4 | ||
|
|
94204b4739 | ||
|
|
bf3a2b768e | ||
|
|
5fb7badfb4 | ||
|
|
239d56f9a3 | ||
|
|
9163fc23f4 | ||
|
|
d225c28fde | ||
|
|
8a3f02e845 | ||
|
|
d0dc92c891 | ||
|
|
8d660f5a2f | ||
|
|
b41b4e77b2 | ||
|
|
267a37d3bd | ||
|
|
0cf23c7d7c | ||
|
|
a632486229 | ||
|
|
64a518a00b | ||
|
|
2f3a914027 | ||
|
|
7182d32d9c | ||
|
|
18381c5d32 | ||
|
|
79327073b4 | ||
|
|
018f2fd789 | ||
|
|
3889392aed | ||
|
|
bd976a25f1 | ||
|
|
01f05ac6fd | ||
|
|
52292cb5a5 | ||
|
|
c6dd1ba0ca | ||
|
|
84f069087c | ||
|
|
76cfced60f | ||
|
|
5c2aea0a6b | ||
|
|
7a883c62df | ||
|
|
ff97461ff8 | ||
|
|
51b0eb74a5 | ||
|
|
2304407986 | ||
|
|
a1ebdc3004 | ||
|
|
fef30f4bea | ||
|
|
eee8d9ab7c | ||
|
|
4f2678d321 | ||
|
|
c473fba628 | ||
|
|
9a9cd8e6a5 | ||
|
|
f5a89aa81a | ||
|
|
3c1beab725 | ||
|
|
79f03ad3ac | ||
|
|
574138a1fb | ||
|
|
6513e2cfca | ||
|
|
43a749b6a7 | ||
|
|
c1d6b3121a | ||
|
|
0d9c8ae4df | ||
|
|
62d8c089ed | ||
|
|
971a76ce11 | ||
|
|
cb33404122 | ||
|
|
bcf72f4624 | ||
|
|
77ad6950e8 | ||
|
|
e2d29aadca | ||
|
|
64ca04ad07 | ||
|
|
b6506a9331 | ||
|
|
fd7222242a | ||
|
|
e36049cd43 | ||
|
|
257f6c5994 | ||
|
|
9098bfb63a | ||
|
|
118d22c4ec | ||
|
|
758df0d85a | ||
|
|
59bbd902fc | ||
|
|
fffab73061 | ||
|
|
0a9ce84cf2 | ||
|
|
07a1734d4b | ||
|
|
6e41d3591d | ||
|
|
4134e5054a | ||
|
|
bb374a5ce2 | ||
|
|
359f398afa | ||
|
|
84425e86e9 | ||
|
|
ebf725c949 | ||
|
|
fc0ea36cf3 | ||
|
|
7836de3f08 | ||
|
|
406232c478 | ||
|
|
9e0c29496f | ||
|
|
480954ee87 | ||
|
|
94039bd9b1 | ||
|
|
667eaca9f2 | ||
|
|
446822a7ae | ||
|
|
f09a3e06f4 | ||
|
|
7c4a56f5f2 | ||
|
|
08f6a32c34 | ||
|
|
3e255fa647 | ||
|
|
c0a90402ef | ||
|
|
5e42627bce | ||
|
|
41bcf9524a | ||
|
|
914cf10911 | ||
|
|
855d4d139d | ||
|
|
abb7b0f8c8 | ||
|
|
d78ad52662 | ||
|
|
25b4bcd311 | ||
|
|
1d3e971ed7 | ||
|
|
3d1f6c4f91 | ||
|
|
8368969932 | ||
|
|
cb016c4307 | ||
|
|
7c7797d35a | ||
|
|
87ab41c80c | ||
|
|
d2391f94c0 | ||
|
|
050ddb8c55 | ||
|
|
bc23e0984a | ||
|
|
07de353207 | ||
|
|
c02491d2e6 | ||
|
|
a6ede8f905 | ||
|
|
22941a9ce0 | ||
|
|
633a09d414 | ||
|
|
29f0881c5a | ||
|
|
60debca37b | ||
|
|
30ea81d0fb | ||
|
|
b1d92c4fe6 | ||
|
|
70f46de2d8 | ||
|
|
f1b2d0b870 | ||
|
|
8a385972fc | ||
|
|
28dd85c1d1 | ||
|
|
827c8e0e72 | ||
|
|
162c076a14 | ||
|
|
9386465de7 | ||
|
|
acca22f3a1 | ||
|
|
f8d84814e0 | ||
|
|
c46cf41842 | ||
|
|
64ab1c4116 | ||
|
|
a6de1041c7 | ||
|
|
c8d34e65ea | ||
|
|
51db729546 | ||
|
|
d2052ad236 | ||
|
|
9c4301467f | ||
|
|
e7355dc0e4 | ||
|
|
4110fec94f | ||
|
|
d5e601eae9 | ||
|
|
4f044c4a57 | ||
|
|
5821c350e1 | ||
|
|
edba8188fe | ||
|
|
1471a72633 | ||
|
|
56834cb88a | ||
|
|
a0f16f9184 | ||
|
|
de80eb4806 | ||
|
|
48a4b81fbe | ||
|
|
e225794f72 | ||
|
|
4eef30f8b5 | ||
|
|
569b09609d | ||
|
|
39838c25c2 | ||
|
|
49e90c08a9 | ||
|
|
e777b06fb8 | ||
|
|
497ec2ac74 | ||
|
|
c5d282d203 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -51,3 +51,4 @@ upload
|
||||
# docs
|
||||
site/
|
||||
apps/*/coverage
|
||||
scripts/translation/.language*.json
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
"keywords": [],
|
||||
"author": "Elian Doran <contact@eliandoran.me>",
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.26.2",
|
||||
"packageManager": "pnpm@10.27.0",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.14.1",
|
||||
"@redocly/cli": "2.14.3",
|
||||
"archiver": "7.0.1",
|
||||
"fs-extra": "11.3.3",
|
||||
"react": "19.2.3",
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"debounce": "3.0.0",
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.51.0",
|
||||
"globals": "16.5.0",
|
||||
"globals": "17.0.0",
|
||||
"i18next": "25.7.3",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "3.7.1",
|
||||
@@ -56,11 +56,11 @@
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "17.0.1",
|
||||
"mermaid": "11.12.2",
|
||||
"mind-elixir": "5.3.8",
|
||||
"mind-elixir": "5.5.0",
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.28.1",
|
||||
"react-i18next": "16.5.0",
|
||||
"react-i18next": "16.5.1",
|
||||
"react-window": "2.2.3",
|
||||
"reveal.js": "5.2.1",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ColumnComponent } from "tabulator-tables";
|
||||
|
||||
import type { Attribute } from "../services/attribute_parser.js";
|
||||
import froca from "../services/froca.js";
|
||||
import { initLocale,t } from "../services/i18n.js";
|
||||
import { initLocale, t } from "../services/i18n.js";
|
||||
import keyboardActionsService from "../services/keyboard_actions.js";
|
||||
import linkService, { type ViewScope } from "../services/link.js";
|
||||
import type LoadResults from "../services/load_results.js";
|
||||
@@ -382,7 +382,8 @@ export type CommandMappings = {
|
||||
reloadTextEditor: CommandData;
|
||||
chooseNoteType: CommandData & {
|
||||
callback: ChooseNoteTypeCallback
|
||||
}
|
||||
};
|
||||
customDownload: CommandData;
|
||||
};
|
||||
|
||||
type EventMappings = {
|
||||
@@ -473,6 +474,11 @@ type EventMappings = {
|
||||
noteContextRemoved: {
|
||||
ntxIds: string[];
|
||||
};
|
||||
contextDataChanged: {
|
||||
noteContext: NoteContext;
|
||||
key: string;
|
||||
value: unknown;
|
||||
};
|
||||
exportSvg: { ntxId: string | null | undefined; };
|
||||
exportPng: { ntxId: string | null | undefined; };
|
||||
geoMapCreateChildNote: {
|
||||
|
||||
@@ -57,6 +57,18 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a child component from this component's children array.
|
||||
* This is used for cleanup when a widget is unmounted to prevent event listener accumulation.
|
||||
*/
|
||||
removeChild(component: ChildT) {
|
||||
const index = this.children.indexOf(component);
|
||||
if (index !== -1) {
|
||||
this.children.splice(index, 1);
|
||||
component.parent = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null | undefined {
|
||||
try {
|
||||
const callMethodPromise = this.initialized ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) : this.callMethod((this as any)[`${name}Event`], data);
|
||||
|
||||
@@ -12,6 +12,7 @@ import server from "../services/server.js";
|
||||
import treeService from "../services/tree.js";
|
||||
import utils from "../services/utils.js";
|
||||
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
||||
import type { HeadingContext } from "../widgets/sidebar/TableOfContents.js";
|
||||
import appContext, { type EventData, type EventListener } from "./app_context.js";
|
||||
import Component from "./component.js";
|
||||
|
||||
@@ -22,6 +23,31 @@ export interface SetNoteOpts {
|
||||
|
||||
export type GetTextEditorCallback = (editor: CKTextEditor) => void;
|
||||
|
||||
export type SaveState = "saved" | "saving" | "unsaved" | "error";
|
||||
|
||||
export interface NoteContextDataMap {
|
||||
toc: HeadingContext;
|
||||
pdfPages: {
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
scrollToPage(page: number): void;
|
||||
requestThumbnail(page: number): void;
|
||||
};
|
||||
pdfAttachments: {
|
||||
attachments: PdfAttachment[];
|
||||
downloadAttachment(filename: string): void;
|
||||
};
|
||||
pdfLayers: {
|
||||
layers: PdfLayer[];
|
||||
toggleLayer(layerId: string, visible: boolean): void;
|
||||
};
|
||||
saveState: {
|
||||
state: SaveState;
|
||||
};
|
||||
}
|
||||
|
||||
type ContextDataKey = keyof NoteContextDataMap;
|
||||
|
||||
class NoteContext extends Component implements EventListener<"entitiesReloaded"> {
|
||||
ntxId: string | null;
|
||||
hoistedNoteId: string;
|
||||
@@ -32,6 +58,13 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
parentNoteId?: string | null;
|
||||
viewScope?: ViewScope;
|
||||
|
||||
/**
|
||||
* Metadata storage for UI components (e.g., table of contents, PDF page list, code outline).
|
||||
* This allows type widgets to publish data that sidebar/toolbar components can consume.
|
||||
* Data is automatically cleared when navigating to a different note.
|
||||
*/
|
||||
private contextData: Map<string, unknown> = new Map();
|
||||
|
||||
constructor(ntxId: string | null = null, hoistedNoteId: string = "root", mainNtxId: string | null = null) {
|
||||
super();
|
||||
|
||||
@@ -91,6 +124,22 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
this.viewScope = opts.viewScope;
|
||||
({ noteId: this.noteId, parentNoteId: this.parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(resolvedNotePath));
|
||||
|
||||
// Clear context data when switching notes and notify subscribers
|
||||
const oldKeys = Array.from(this.contextData.keys());
|
||||
this.contextData.clear();
|
||||
if (oldKeys.length > 0) {
|
||||
// Notify subscribers asynchronously to avoid blocking navigation
|
||||
window.setTimeout(() => {
|
||||
for (const key of oldKeys) {
|
||||
this.triggerEvent("contextDataChanged", {
|
||||
noteContext: this,
|
||||
key,
|
||||
value: undefined
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
this.saveToRecentNotes(resolvedNotePath);
|
||||
|
||||
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
|
||||
@@ -443,6 +492,52 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set metadata for this note context (e.g., table of contents, PDF pages, code outline).
|
||||
* This data can be consumed by sidebar/toolbar components.
|
||||
*
|
||||
* @param key - Unique identifier for the data type (e.g., "toc", "pdfPages", "codeOutline")
|
||||
* @param value - The data to store (will be cleared when switching notes)
|
||||
*/
|
||||
setContextData<K extends ContextDataKey>(key: K, value: NoteContextDataMap[K]): void {
|
||||
this.contextData.set(key, value);
|
||||
// Trigger event so subscribers can react
|
||||
this.triggerEvent("contextDataChanged", {
|
||||
noteContext: this,
|
||||
key,
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata for this note context.
|
||||
*
|
||||
* @param key - The data key to retrieve
|
||||
* @returns The stored data, or undefined if not found
|
||||
*/
|
||||
getContextData<K extends ContextDataKey>(key: K): NoteContextDataMap[K] | undefined {
|
||||
return this.contextData.get(key) as NoteContextDataMap[K] | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if context data exists for a given key.
|
||||
*/
|
||||
hasContextData(key: ContextDataKey): boolean {
|
||||
return this.contextData.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear specific context data.
|
||||
*/
|
||||
clearContextData(key: ContextDataKey): void {
|
||||
this.contextData.delete(key);
|
||||
this.triggerEvent("contextDataChanged", {
|
||||
noteContext: this,
|
||||
key,
|
||||
value: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function openInCurrentNoteContext(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, notePath: string, viewScope?: ViewScope) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import search from "../services/search.js";
|
||||
import server from "../services/server.js";
|
||||
import utils from "../services/utils.js";
|
||||
import type FAttachment from "./fattachment.js";
|
||||
import type { AttributeType,default as FAttribute } from "./fattribute.js";
|
||||
import type { AttributeType, default as FAttribute } from "./fattribute.js";
|
||||
|
||||
const LABEL = "label";
|
||||
const RELATION = "relation";
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
import { applyModals } from "./layout_commons.js";
|
||||
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||
import { useNoteContext } from "../widgets/react/hooks.jsx";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.js";
|
||||
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||
import type AppContext from "../components/app_context.js";
|
||||
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
||||
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.js";
|
||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||
import NoteTitleWidget from "../widgets/note_title.js";
|
||||
import ContentHeader from "../widgets/containers/content_header.js";
|
||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
||||
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
|
||||
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
|
||||
import NoteTitleWidget from "../widgets/note_title.js";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import { useNoteContext } from "../widgets/react/hooks.jsx";
|
||||
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
||||
import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx";
|
||||
import SearchResult from "../widgets/search_result.jsx";
|
||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
|
||||
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||
import TabRowWidget from "../widgets/tab_row.js";
|
||||
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
|
||||
import type AppContext from "../components/app_context.js";
|
||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
|
||||
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||
import { applyModals } from "./layout_commons.js";
|
||||
|
||||
const MOBILE_CSS = `
|
||||
<style>
|
||||
@@ -194,11 +194,11 @@ export default class MobileLayout {
|
||||
}
|
||||
|
||||
function FilePropertiesWrapper() {
|
||||
const { note } = useNoteContext();
|
||||
const { note, ntxId } = useNoteContext();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{note?.type === "file" && <FilePropertiesTab note={note} />}
|
||||
{note?.type === "file" && <FilePropertiesTab note={note} ntxId={ntxId} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import renderService from "./render.js";
|
||||
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||
import WheelZoom from 'vanilla-js-wheel-zoom';
|
||||
|
||||
import FAttachment from "../entities/fattachment.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
import imageContextMenuService from "../menus/image_context_menu.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import renderText from "./content_renderer_text.js";
|
||||
import renderDoc from "./doc_renderer.js";
|
||||
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
|
||||
import openService from "./open.js";
|
||||
import protectedSessionService from "./protected_session.js";
|
||||
import protectedSessionHolder from "./protected_session_holder.js";
|
||||
import openService from "./open.js";
|
||||
import utils from "./utils.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
import FAttachment from "../entities/fattachment.js";
|
||||
import imageContextMenuService from "../menus/image_context_menu.js";
|
||||
import renderService from "./render.js";
|
||||
import { applySingleBlockSyntaxHighlight } from "./syntax_highlight.js";
|
||||
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
|
||||
import renderDoc from "./doc_renderer.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import WheelZoom from 'vanilla-js-wheel-zoom';
|
||||
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||
import renderText from "./content_renderer_text.js";
|
||||
import utils from "./utils.js";
|
||||
|
||||
let idCounter = 1;
|
||||
|
||||
@@ -152,7 +153,7 @@ function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLE
|
||||
|
||||
const $img = $("<img>")
|
||||
.attr("src", url || "")
|
||||
.attr("id", "attachment-image-" + idCounter++)
|
||||
.attr("id", `attachment-image-${idCounter++}`)
|
||||
.css("max-width", "100%");
|
||||
|
||||
$renderedContent.append($img);
|
||||
@@ -193,7 +194,7 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
|
||||
|
||||
if (type === "pdf") {
|
||||
const $pdfPreview = $('<iframe class="pdf-preview" style="width: 100%; flex-grow: 100;"></iframe>');
|
||||
$pdfPreview.attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open`));
|
||||
$pdfPreview.attr("src", openService.getUrlForDownload(`pdfjs/web/viewer.html?file=../../api/${entityType}/${entityId}/open`));
|
||||
|
||||
$content.append($pdfPreview);
|
||||
} else if (type === "audio") {
|
||||
@@ -217,28 +218,28 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
|
||||
// in attachment list
|
||||
const $downloadButton = $(`
|
||||
<button class="file-download btn btn-primary" type="button">
|
||||
<span class="bx bx-download"></span>
|
||||
<span class="tn-icon bx bx-download"></span>
|
||||
${t("file_properties.download")}
|
||||
</button>
|
||||
`);
|
||||
|
||||
const $openButton = $(`
|
||||
<button class="file-open btn btn-primary" type="button">
|
||||
<span class="bx bx-link-external"></span>
|
||||
<span class="tn-icon bx bx-link-external"></span>
|
||||
${t("file_properties.open")}
|
||||
</button>
|
||||
`);
|
||||
|
||||
$downloadButton.on("click", (e) => {
|
||||
e.stopPropagation();
|
||||
openService.downloadFileNote(entity.noteId)
|
||||
openService.downloadFileNote(entity, null, null);
|
||||
});
|
||||
$openButton.on("click", async (e) => {
|
||||
const iconEl = $openButton.find("> .bx");
|
||||
iconEl.removeClass("bx bx-link-external");
|
||||
iconEl.addClass("bx bx-loader spin");
|
||||
e.stopPropagation();
|
||||
await openService.openNoteExternally(entity.noteId, entity.mime)
|
||||
await openService.openNoteExternally(entity.noteId, entity.mime);
|
||||
iconEl.removeClass("bx bx-loader spin");
|
||||
iconEl.addClass("bx bx-link-external");
|
||||
});
|
||||
@@ -266,7 +267,7 @@ async function renderMermaid(note: FNote | FAttachment, $renderedContent: JQuery
|
||||
|
||||
try {
|
||||
await loadElkIfNeeded(mermaid, content);
|
||||
const { svg } = await mermaid.mermaidAPI.render("in-mermaid-graph-" + idCounter++, content);
|
||||
const { svg } = await mermaid.mermaidAPI.render(`in-mermaid-graph-${idCounter++}`, content);
|
||||
|
||||
$renderedContent.append($(postprocessMermaidSvg(svg)));
|
||||
} catch (e) {
|
||||
|
||||
14
apps/client/src/services/css_class_manager.spec.ts
Normal file
14
apps/client/src/services/css_class_manager.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getReadableTextColor } from "./css_class_manager";
|
||||
|
||||
describe("getReadableTextColor", () => {
|
||||
it("doesn't crash for invalid color", () => {
|
||||
expect(getReadableTextColor("RandomColor")).toBe("#000");
|
||||
});
|
||||
|
||||
it("tolerates different casing", () => {
|
||||
expect(getReadableTextColor("Blue"))
|
||||
.toBe(getReadableTextColor("blue"));
|
||||
});
|
||||
});
|
||||
@@ -1,21 +1,22 @@
|
||||
import clsx from "clsx";
|
||||
import {readCssVar} from "../utils/css-var";
|
||||
import Color, { ColorInstance } from "color";
|
||||
|
||||
import {readCssVar} from "../utils/css-var";
|
||||
|
||||
const registeredClasses = new Set<string>();
|
||||
const colorsWithHue = new Set<string>();
|
||||
|
||||
// Read the color lightness limits defined in the theme as CSS variables
|
||||
|
||||
const lightThemeColorMaxLightness = readCssVar(
|
||||
document.documentElement,
|
||||
"tree-item-light-theme-max-color-lightness"
|
||||
).asNumber(70);
|
||||
document.documentElement,
|
||||
"tree-item-light-theme-max-color-lightness"
|
||||
).asNumber(70);
|
||||
|
||||
const darkThemeColorMinLightness = readCssVar(
|
||||
document.documentElement,
|
||||
"tree-item-dark-theme-min-color-lightness"
|
||||
).asNumber(50);
|
||||
document.documentElement,
|
||||
"tree-item-dark-theme-min-color-lightness"
|
||||
).asNumber(50);
|
||||
|
||||
function createClassForColor(colorString: string | null) {
|
||||
if (!colorString?.trim()) return "";
|
||||
@@ -27,7 +28,7 @@ function createClassForColor(colorString: string | null) {
|
||||
|
||||
if (!registeredClasses.has(className)) {
|
||||
const adjustedColor = adjustColorLightness(color, lightThemeColorMaxLightness!,
|
||||
darkThemeColorMinLightness!);
|
||||
darkThemeColorMinLightness!);
|
||||
const hue = getHue(color);
|
||||
|
||||
$("head").append(`<style>
|
||||
@@ -50,7 +51,7 @@ function createClassForColor(colorString: string | null) {
|
||||
|
||||
function parseColor(color: string) {
|
||||
try {
|
||||
return Color(color);
|
||||
return Color(color.toLowerCase());
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
@@ -84,8 +85,8 @@ function getHue(color: ColorInstance) {
|
||||
}
|
||||
|
||||
export function getReadableTextColor(bgColor: string) {
|
||||
const colorInstance = Color(bgColor);
|
||||
return colorInstance.isLight() ? "#000" : "#fff";
|
||||
const colorInstance = parseColor(bgColor);
|
||||
return !colorInstance || colorInstance?.isLight() ? "#000" : "#fff";
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import utils from "./utils.js";
|
||||
import Component from "../components/component.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
import options from "./options.js";
|
||||
import server from "./server.js";
|
||||
import utils from "./utils.js";
|
||||
|
||||
type ExecFunction = (command: string, cb: (err: string, stdout: string, stderror: string) => void) => void;
|
||||
|
||||
@@ -36,9 +38,14 @@ function download(url: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function downloadFileNote(noteId: string) {
|
||||
const url = `${getFileUrl("notes", noteId)}?${Date.now()}`; // don't use cache
|
||||
export function downloadFileNote(note: FNote, parentComponent: Component | null, ntxId: string | null | undefined) {
|
||||
if (note.type === "file" && note.mime === "application/pdf" && parentComponent) {
|
||||
// Special handling, manages its own downloading process.
|
||||
parentComponent.triggerEvent("customDownload", { ntxId });
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${getFileUrl("notes", note.noteId)}?${Date.now()}`; // don't use cache
|
||||
download(url);
|
||||
}
|
||||
|
||||
@@ -97,7 +104,7 @@ async function openCustom(type: string, entityId: string, mime: string) {
|
||||
// Note that the path separator must be \ instead of /
|
||||
filePath = filePath.replace(/\//g, "\\");
|
||||
}
|
||||
const command = `rundll32.exe shell32.dll,OpenAs_RunDLL ` + filePath;
|
||||
const command = `rundll32.exe shell32.dll,OpenAs_RunDLL ${filePath}`;
|
||||
exec(command, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.error("Open Note custom: ", err);
|
||||
@@ -131,10 +138,10 @@ export function getUrlForDownload(url: string) {
|
||||
if (utils.isElectron()) {
|
||||
// electron needs absolute URL, so we extract current host, port, protocol
|
||||
return `${getHost()}/${url}`;
|
||||
} else {
|
||||
// web server can be deployed on subdomain, so we need to use a relative path
|
||||
return url;
|
||||
}
|
||||
// web server can be deployed on subdomain, so we need to use a relative path
|
||||
return url;
|
||||
|
||||
}
|
||||
|
||||
function canOpenInBrowser(mime: string) {
|
||||
|
||||
@@ -85,13 +85,15 @@ async function remove<T>(url: string, componentId?: string) {
|
||||
return await call<T>("DELETE", url, componentId);
|
||||
}
|
||||
|
||||
async function upload(url: string, fileToUpload: File) {
|
||||
async function upload(url: string, fileToUpload: File, componentId?: string) {
|
||||
const formData = new FormData();
|
||||
formData.append("upload", fileToUpload);
|
||||
|
||||
return await $.ajax({
|
||||
url: window.glob.baseApiUrl + url,
|
||||
headers: await getHeaders(),
|
||||
headers: await getHeaders(componentId ? {
|
||||
"trilium-component-id": componentId
|
||||
} : undefined),
|
||||
data: formData,
|
||||
type: "PUT",
|
||||
timeout: 60 * 60 * 1000,
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
import type { SaveState } from "../components/note_context";
|
||||
import { getErrorMessage } from "./utils";
|
||||
|
||||
type Callback = () => Promise<void> | void;
|
||||
|
||||
export type StateCallback = (state: SaveState) => void;
|
||||
|
||||
export default class SpacedUpdate {
|
||||
private updater: Callback;
|
||||
private lastUpdated: number;
|
||||
private changed: boolean;
|
||||
private updateInterval: number;
|
||||
private changeForbidden?: boolean;
|
||||
private stateCallback?: StateCallback;
|
||||
|
||||
constructor(updater: Callback, updateInterval = 1000) {
|
||||
constructor(updater: Callback, updateInterval = 1000, stateCallback?: StateCallback) {
|
||||
this.updater = updater;
|
||||
this.lastUpdated = Date.now();
|
||||
this.changed = false;
|
||||
this.updateInterval = updateInterval;
|
||||
this.stateCallback = stateCallback;
|
||||
}
|
||||
|
||||
scheduleUpdate() {
|
||||
if (!this.changeForbidden) {
|
||||
this.changed = true;
|
||||
this.stateCallback?.("unsaved");
|
||||
setTimeout(() => this.triggerUpdate());
|
||||
}
|
||||
}
|
||||
@@ -26,10 +34,13 @@ export default class SpacedUpdate {
|
||||
this.changed = false; // optimistic...
|
||||
|
||||
try {
|
||||
this.stateCallback?.("saving");
|
||||
await this.updater();
|
||||
this.stateCallback?.("saved");
|
||||
} catch (e) {
|
||||
this.changed = true;
|
||||
|
||||
this.stateCallback?.("error");
|
||||
logError(getErrorMessage(e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -59,15 +70,22 @@ export default class SpacedUpdate {
|
||||
this.updateInterval = interval;
|
||||
}
|
||||
|
||||
triggerUpdate() {
|
||||
async triggerUpdate() {
|
||||
if (!this.changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Date.now() - this.lastUpdated > this.updateInterval) {
|
||||
this.updater();
|
||||
this.stateCallback?.("saving");
|
||||
try {
|
||||
await this.updater();
|
||||
this.stateCallback?.("saved");
|
||||
this.changed = false;
|
||||
} catch (e) {
|
||||
this.stateCallback?.("error");
|
||||
logError(getErrorMessage(e));
|
||||
}
|
||||
this.lastUpdated = Date.now();
|
||||
this.changed = false;
|
||||
} else {
|
||||
// update isn't triggered but changes are still pending, so we need to schedule another check
|
||||
this.scheduleUpdate();
|
||||
|
||||
@@ -187,13 +187,15 @@ export function formatSize(size: number | null | undefined) {
|
||||
return "";
|
||||
}
|
||||
|
||||
size = Math.max(Math.round(size / 1024), 1);
|
||||
|
||||
if (size < 1024) {
|
||||
return `${size} KiB`;
|
||||
if (size === 0) {
|
||||
return "0 B";
|
||||
}
|
||||
return `${Math.round(size / 102.4) / 10} MiB`;
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KiB", "MiB", "GiB"];
|
||||
const i = Math.floor(Math.log(size) / Math.log(k));
|
||||
|
||||
return `${Math.round((size / Math.pow(k, i)) * 100) / 100} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
function toObject<T, R>(array: T[], fn: (arg0: T) => [key: string, value: R]) {
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
|
||||
:root {
|
||||
color-scheme: var(--theme-style);
|
||||
|
||||
--main-font-family: "Inter", sans-serif;
|
||||
|
||||
--main-font-size: normal;
|
||||
|
||||
@@ -767,7 +767,7 @@ body.mobile .fancytree-node > span {
|
||||
background: var(--left-pane-item-hover-background);
|
||||
}
|
||||
|
||||
#left-pane span.fancytree-node.shared .fancytree-title::after {
|
||||
#left-pane .note-indicator-icon.shared-indicator {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -1259,8 +1259,16 @@ body.layout-horizontal #rest-pane > .classic-toolbar-widget {
|
||||
#center-pane .note-split {
|
||||
padding-top: 2px;
|
||||
background-color: var(--note-split-background-color, var(--main-background-color));
|
||||
transition: border-color 250ms ease-in;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
/* The active split in a multi-split view */
|
||||
#center-pane > .split-note-container-widget:has(> .note-split.visible ~ .note-split.visible) > .note-split.active {
|
||||
border-color: var(--link-selection-outline-color);
|
||||
}
|
||||
|
||||
|
||||
body:not(.background-effects) #center-pane .note-split {
|
||||
animation: note-entrance 100ms linear;
|
||||
}
|
||||
|
||||
@@ -148,29 +148,28 @@ span.fancytree-node.protected > span.fancytree-custom-icon {
|
||||
filter: drop-shadow(2px 2px 2px var(--main-text-color));
|
||||
}
|
||||
|
||||
span.fancytree-node.multiple-parents.shared .fancytree-title::after {
|
||||
/* Note indicator icons (clone, shared) - real DOM elements for tooltip support */
|
||||
.note-indicator-icon {
|
||||
font-family: "boxicons" !important;
|
||||
font-size: smaller;
|
||||
content: " \eb3d \ec03";
|
||||
margin-inline-start: 4px;
|
||||
opacity: 0.8;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
span.fancytree-node.multiple-parents .fancytree-title::after {
|
||||
font-family: "boxicons" !important;
|
||||
font-size: smaller;
|
||||
content: " \eb3d"; /* lookup code for "link-alt" in boxicons.css */
|
||||
.note-indicator-icon.clone-indicator::before {
|
||||
content: "\eb3d"; /* bx-link-alt */
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout span.fancytree-node.multiple-parents .fancytree-title::after {
|
||||
content: " \ed82";
|
||||
.note-indicator-icon.shared-indicator::before {
|
||||
content: "\ec03"; /* bx-share-alt */
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout .note-indicator-icon.clone-indicator::before {
|
||||
content: "\ed82";
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
span.fancytree-node.shared .fancytree-title::after {
|
||||
font-family: "boxicons" !important;
|
||||
font-size: smaller;
|
||||
content: " \ec03"; /* lookup code for "share-alt" in boxicons.css */
|
||||
}
|
||||
|
||||
span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -484,7 +484,6 @@
|
||||
"delete_button": "حذف",
|
||||
"download_button": "تنزيل",
|
||||
"restore_button": "أستعادة",
|
||||
"preview": "معاينة:",
|
||||
"note_revisions": "مراجعات الملاحظة",
|
||||
"diff_on": "عرض الفروقات",
|
||||
"diff_off": "عرض المحتوى",
|
||||
|
||||
@@ -64,8 +64,7 @@
|
||||
"restore_button": "Restaura",
|
||||
"delete_button": "Suprimeix",
|
||||
"download_button": "Descarrega",
|
||||
"mime": "MIME: ",
|
||||
"preview": "Vista prèvia:"
|
||||
"mime": "MIME: "
|
||||
},
|
||||
"sort_child_notes": {
|
||||
"title": "títol",
|
||||
|
||||
@@ -290,7 +290,6 @@
|
||||
"download_button": "下载",
|
||||
"mime": "MIME 类型: ",
|
||||
"file_size": "文件大小:",
|
||||
"preview": "预览:",
|
||||
"preview_not_available": "无法预览此类型的笔记。",
|
||||
"diff_on": "显示差异",
|
||||
"diff_off": "显示内容",
|
||||
@@ -765,7 +764,14 @@
|
||||
"note_icon": {
|
||||
"change_note_icon": "更改笔记图标",
|
||||
"search": "搜索:",
|
||||
"reset-default": "重置为默认图标"
|
||||
"reset-default": "重置为默认图标",
|
||||
"search_placeholder_other": "在 {{count}} 个图标包中搜索 {{number}} 个图标",
|
||||
"search_placeholder_filtered": "在 {{name}} 中搜索 {{number}} 个图标",
|
||||
"filter": "筛选",
|
||||
"filter-none": "所有图标",
|
||||
"filter-default": "默认图标",
|
||||
"icon_tooltip": "{{name}}\n图标包:{{iconPack}}",
|
||||
"no_results": "没有找到图标。"
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "笔记类型",
|
||||
@@ -1445,7 +1451,7 @@
|
||||
"will_be_deleted_in": "此附件将在 {{time}} 后自动删除",
|
||||
"will_be_deleted_soon": "该附件在不久后将被自动删除",
|
||||
"deletion_reason": ",因为该附件未链接在笔记的内容中。为防止被删除,请将附件链接重新添加到内容中或将附件转换为笔记。",
|
||||
"role_and_size": "角色:{{role}},大小:{{size}}",
|
||||
"role_and_size": "角色:{{role}},大小:{{size}},文件类型:{{- mimeType}}",
|
||||
"link_copied": "附件链接已复制到剪贴板。",
|
||||
"unrecognized_role": "无法识别的附件角色 '{{role}}'。"
|
||||
},
|
||||
@@ -1588,7 +1594,11 @@
|
||||
"create-child-note": "创建子笔记",
|
||||
"unhoist": "取消聚焦",
|
||||
"toggle-sidebar": "切换侧边栏",
|
||||
"dropping-not-allowed": "不允许移动笔记到此处。"
|
||||
"dropping-not-allowed": "不允许移动笔记到此处。",
|
||||
"shared-indicator-tooltip": "此笔记已公开分享",
|
||||
"shared-indicator-tooltip-with-url": "此笔记已公开分享至:{{- url}}",
|
||||
"clone-indicator-tooltip": "此笔记有 {{- count}} 个父级: {{- parents}}",
|
||||
"clone-indicator-tooltip-single": "此笔记已克隆(1 个额外的父级:{{- parent}})"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "保持此窗口置顶"
|
||||
@@ -2186,7 +2196,14 @@
|
||||
"execute_sql_description": "这是一篇 SQL 笔记。点击即可执行 SQL 查询。",
|
||||
"shared_copy_to_clipboard": "复制链接到剪贴板",
|
||||
"shared_open_in_browser": "在浏览器中打开链接",
|
||||
"shared_unshare": "取消共享"
|
||||
"shared_unshare": "取消共享",
|
||||
"save_status_saved": "已保存",
|
||||
"save_status_saving": "保存中...",
|
||||
"save_status_unsaved": "未保存",
|
||||
"save_status_error": "保存失败",
|
||||
"save_status_unsaved_tooltip": "还有一些更改尚未保存。它们将稍后自动保存。",
|
||||
"save_status_error_tooltip": "保存笔记时出错。如果可以,请尝试将笔记内容复制到其他位置并重新加载应用程序。",
|
||||
"save_status_saving_tooltip": "更改正在保存。"
|
||||
},
|
||||
"status_bar": {
|
||||
"language_title": "更改内容语言",
|
||||
@@ -2217,5 +2234,12 @@
|
||||
},
|
||||
"attributes_panel": {
|
||||
"title": "笔记属性"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_other": "{{count}} 个附件",
|
||||
"pages_other": "共{{count}}页",
|
||||
"pages_alt": "第{{pageNumber}}页",
|
||||
"pages_loading": "加载中...",
|
||||
"layers_other": "{{count}} 层"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,7 +281,6 @@
|
||||
"download_button": "Herunterladen",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "Dateigröße:",
|
||||
"preview": "Vorschau:",
|
||||
"preview_not_available": "Für diesen Notiztyp ist keine Vorschau verfügbar.",
|
||||
"restore_button": "Wiederherstellen",
|
||||
"delete_button": "Löschen",
|
||||
@@ -692,7 +691,11 @@
|
||||
"convert_into_attachment_successful": "Notiz '{{title}}' wurde als Anhang konvertiert.",
|
||||
"convert_into_attachment_prompt": "Bist du dir sicher, dass du die Notiz '{{title}}' in ein Anhang der übergeordneten Notiz konvertieren möchtest?",
|
||||
"print_pdf": "Export als PDF...",
|
||||
"open_note_on_server": "Öffne Notiz auf dem Server"
|
||||
"open_note_on_server": "Öffne Notiz auf dem Server",
|
||||
"export_as_image": "Als Bild exportieren",
|
||||
"export_as_image_png": "PNG (Raster)",
|
||||
"export_as_image_svg": "SVG (Vektor)",
|
||||
"note_map": "Notizen Karte"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "Das Schaltflächen-Widget „{{componentId}}“ hat keinen definierten Klick-Handler"
|
||||
@@ -750,7 +753,15 @@
|
||||
"note_icon": {
|
||||
"change_note_icon": "Notiz-Icon ändern",
|
||||
"search": "Suche:",
|
||||
"reset-default": "Standard wiederherstellen"
|
||||
"reset-default": "Standard wiederherstellen",
|
||||
"search_placeholder_one": "Suche {{number}} Icons über {{count}} Pakete",
|
||||
"search_placeholder_other": "Suche {{number}} Icons über {{count}} Pakete",
|
||||
"search_placeholder_filtered": "Suche {{number}} Icons in {{name}}",
|
||||
"filter": "Filter",
|
||||
"filter-none": "Alle Icons",
|
||||
"filter-default": "Standard Icons",
|
||||
"icon_tooltip": "{{name}}\nIcon Paket: {{iconPack}}",
|
||||
"no_results": "Keine Icons gefunden."
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "Notiztyp",
|
||||
@@ -810,7 +821,8 @@
|
||||
},
|
||||
"inherited_attribute_list": {
|
||||
"title": "Geerbte Attribute",
|
||||
"no_inherited_attributes": "Keine geerbten Attribute."
|
||||
"no_inherited_attributes": "Keine geerbten Attribute.",
|
||||
"none": "Keine"
|
||||
},
|
||||
"note_info_widget": {
|
||||
"note_id": "Notiz-ID",
|
||||
@@ -821,7 +833,9 @@
|
||||
"note_size_info": "Die Notizgröße bietet eine grobe Schätzung des Speicherbedarfs für diese Notiz. Es berücksichtigt den Inhalt der Notiz und den Inhalt ihrer Notizrevisionen.",
|
||||
"calculate": "berechnen",
|
||||
"subtree_size": "(Teilbaumgröße: {{size}} in {{count}} Notizen)",
|
||||
"title": "Notizinfo"
|
||||
"title": "Notizinfo",
|
||||
"mime": "MIME Typ",
|
||||
"show_similar_notes": "Zeige ähnliche Notizen"
|
||||
},
|
||||
"note_map": {
|
||||
"open_full": "Vollständig erweitern",
|
||||
@@ -884,7 +898,8 @@
|
||||
"search_parameters": "Suchparameter",
|
||||
"unknown_search_option": "Unbekannte Suchoption {{searchOptionName}}",
|
||||
"search_note_saved": "Suchnotiz wurde in {{-notePathTitle}} gespeichert",
|
||||
"actions_executed": "Aktionen wurden ausgeführt."
|
||||
"actions_executed": "Aktionen wurden ausgeführt.",
|
||||
"view_options": "Anzeigeoptionen:"
|
||||
},
|
||||
"similar_notes": {
|
||||
"title": "Ähnliche Notizen",
|
||||
@@ -988,7 +1003,12 @@
|
||||
"editable_text": {
|
||||
"placeholder": "Gebe hier den Inhalt deiner Notiz ein...",
|
||||
"auto-detect-language": "Automatisch erkannt",
|
||||
"keeps-crashing": "Die Bearbeitungskomponente stürzt immer wieder ab. Bitte starten Sie Trilium neu. Wenn das Problem weiterhin besteht, erstellen Sie einen Fehlerbericht."
|
||||
"keeps-crashing": "Die Bearbeitungskomponente stürzt immer wieder ab. Bitte starten Sie Trilium neu. Wenn das Problem weiterhin besteht, erstellen Sie einen Fehlerbericht.",
|
||||
"editor_crashed_title": "Der Text Editor ist abgestürzt",
|
||||
"editor_crashed_content": "Ihr Inhalt wurde erfolgreich wiederhergestellt, aber einzelne Ihrer letzten Änderungen waren möglicherweise noch nicht gespeichert.",
|
||||
"editor_crashed_details_button": "Zeige mehr Details…",
|
||||
"editor_crashed_details_intro": "Falls Sie diesen Fehler mehrmals sehen, melden Sie dies auf GitHub mit den folgenden Informationen.",
|
||||
"editor_crashed_details_title": "Technische Informationen"
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "Öffne eine Notiz, indem du den Titel der Notiz in die Eingabe unten eingibst oder eine Notiz in der Baumstruktur auswählst.",
|
||||
@@ -1503,7 +1523,12 @@
|
||||
},
|
||||
"highlights_list_2": {
|
||||
"title": "Hervorhebungs-Liste",
|
||||
"options": "Optionen"
|
||||
"options": "Optionen",
|
||||
"title_with_count_one": "{{count}} Highlight",
|
||||
"title_with_count_other": "{{count}} Highlights",
|
||||
"modal_title": "Highlight Liste konfigurieren",
|
||||
"menu_configure": "Highlight Liste konfigurieren…",
|
||||
"no_highlights": "Keine Highlights gefunden."
|
||||
},
|
||||
"quick-search": {
|
||||
"placeholder": "Schnellsuche",
|
||||
@@ -1535,10 +1560,21 @@
|
||||
"note_detail": {
|
||||
"could_not_find_typewidget": "Konnte typeWidget für Typ ‚{{type}}‘ nicht finden",
|
||||
"printing": "Druckvorgang läuft…",
|
||||
"printing_pdf": "PDF-Export läuft…"
|
||||
"printing_pdf": "PDF-Export läuft…",
|
||||
"print_report_title": "Druckreport",
|
||||
"print_report_collection_details_button": "Details anzeigen",
|
||||
"print_report_collection_details_ignored_notes": "Ignorierte Notizen"
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "Titel der Notiz hier eingeben…"
|
||||
"placeholder": "Titel der Notiz hier eingeben…",
|
||||
"created_on": "Erstellt am <Value />",
|
||||
"last_modified": "Bearbeitet am <Value />",
|
||||
"note_type_switcher_label": "Ändere von {{type}} zu:",
|
||||
"note_type_switcher_others": "Andere Notizart",
|
||||
"note_type_switcher_templates": "Template",
|
||||
"note_type_switcher_collection": "Sammlung",
|
||||
"edited_notes": "Notizen, bearbeitet an diesem Tag",
|
||||
"promoted_attributes": "Hervorgehobene Attribute"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "Es wurden keine Notizen mit den angegebenen Suchparametern gefunden.",
|
||||
@@ -1567,7 +1603,8 @@
|
||||
},
|
||||
"toc": {
|
||||
"table_of_contents": "Inhaltsverzeichnis",
|
||||
"options": "Optionen"
|
||||
"options": "Optionen",
|
||||
"no_headings": "Keine Überschriften."
|
||||
},
|
||||
"watched_file_update_status": {
|
||||
"file_last_modified": "Datei <code class=\"file-path\"></code> wurde zuletzt geändert am <span class=\"file-last-modified\"></span>.",
|
||||
@@ -2106,5 +2143,10 @@
|
||||
},
|
||||
"popup-editor": {
|
||||
"maximize": "Wechsele zum vollständigen Editor"
|
||||
},
|
||||
"experimental_features": {
|
||||
"title": "Experimentelle Optionen",
|
||||
"disclaimer": "Diese Optionen sind experimentell und können Instabilitäten verursachen. Achtsam zu verwenden.",
|
||||
"new_layout_name": "Neues Layout"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +295,6 @@
|
||||
"download_button": "Download",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "File size:",
|
||||
"preview": "Preview:",
|
||||
"preview_not_available": "Preview isn't available for this note type."
|
||||
},
|
||||
"sort_child_notes": {
|
||||
@@ -1769,7 +1768,11 @@
|
||||
"create-child-note": "Create child note",
|
||||
"unhoist": "Unhoist",
|
||||
"toggle-sidebar": "Toggle sidebar",
|
||||
"dropping-not-allowed": "Dropping notes into this location is not allowed."
|
||||
"dropping-not-allowed": "Dropping notes into this location is not allowed.",
|
||||
"clone-indicator-tooltip": "This note has {{- count}} parents: {{- parents}}",
|
||||
"clone-indicator-tooltip-single": "This note is cloned (1 additional parent: {{- parent}})",
|
||||
"shared-indicator-tooltip": "This note is shared publicly",
|
||||
"shared-indicator-tooltip-with-url": "This note is shared publicly at: {{- url}}"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Keep Window on Top"
|
||||
@@ -2205,7 +2208,14 @@
|
||||
"execute_script": "Run script",
|
||||
"execute_script_description": "This note is a script note. Click to execute the script.",
|
||||
"execute_sql": "Run SQL",
|
||||
"execute_sql_description": "This note is a SQL note. Click to execute the SQL query."
|
||||
"execute_sql_description": "This note is a SQL note. Click to execute the SQL query.",
|
||||
"save_status_saved": "Saved",
|
||||
"save_status_saving": "Saving...",
|
||||
"save_status_unsaved": "Unsaved",
|
||||
"save_status_error": "Save failed",
|
||||
"save_status_saving_tooltip": "Changes are being saved.",
|
||||
"save_status_unsaved_tooltip": "There are unsaved changes. They will be saved automatically in a moment.",
|
||||
"save_status_error_tooltip": "An error occurred while saving the note. If possible, try copying the note content elsewhere and reloading the application."
|
||||
},
|
||||
"status_bar": {
|
||||
"language_title": "Change content language",
|
||||
@@ -2234,5 +2244,15 @@
|
||||
"empty_button": "Hide the panel",
|
||||
"toggle": "Toggle right panel",
|
||||
"custom_widget_go_to_source": "Go to source code"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_one": "{{count}} attachment",
|
||||
"attachments_other": "{{count}} attachments",
|
||||
"layers_one": "{{count}} layer",
|
||||
"layers_other": "{{count}} layers",
|
||||
"pages_one": "{{count}} page",
|
||||
"pages_other": "{{count}} pages",
|
||||
"pages_alt": "Page {{pageNumber}}",
|
||||
"pages_loading": "Loading..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +279,6 @@
|
||||
"download_button": "Descargar",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "Tamaño del archivo:",
|
||||
"preview": "Vista previa:",
|
||||
"preview_not_available": "La vista previa no está disponible para este tipo de notas.",
|
||||
"diff_off": "Mostrar contenido",
|
||||
"diff_on": "Mostrar diferencia",
|
||||
|
||||
@@ -22,6 +22,12 @@
|
||||
"bundle-error": {
|
||||
"title": "Echec du chargement d'un script personnalisé",
|
||||
"message": "Le script de la note avec l'ID \"{{id}}\", intitulé \"{{title}}\" n'a pas pu être exécuté à cause de\n\n{{message}}"
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "Impossible d'obtenir la liste des widgets depuis le serveur"
|
||||
},
|
||||
"widget-render-error": {
|
||||
"title": "Rendu impossible d'un widget React custom"
|
||||
}
|
||||
},
|
||||
"add_link": {
|
||||
@@ -279,7 +285,6 @@
|
||||
"download_button": "Télécharger",
|
||||
"mime": "MIME : ",
|
||||
"file_size": "Taille du fichier :",
|
||||
"preview": "Aperçu :",
|
||||
"preview_not_available": "L'aperçu n'est pas disponible pour ce type de note.",
|
||||
"restore_button": "Restaurer",
|
||||
"delete_button": "Supprimer",
|
||||
@@ -757,7 +762,11 @@
|
||||
"note_icon": {
|
||||
"change_note_icon": "Changer l'icône de note",
|
||||
"search": "Recherche :",
|
||||
"reset-default": "Réinitialiser l'icône par défaut"
|
||||
"reset-default": "Réinitialiser l'icône par défaut",
|
||||
"filter": "Filtre",
|
||||
"filter-none": "Toutes les icônes",
|
||||
"filter-default": "Icônes par défaut",
|
||||
"icon_tooltip": "{{name}}\nPack d'icônes : {{iconPack}}"
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "Type de note",
|
||||
@@ -1541,7 +1550,8 @@
|
||||
"refresh-saved-search-results": "Rafraîchir les résultats de recherche enregistrée",
|
||||
"create-child-note": "Créer une note enfant",
|
||||
"unhoist": "Désactiver le focus",
|
||||
"toggle-sidebar": "Basculer la barre latérale"
|
||||
"toggle-sidebar": "Basculer la barre latérale",
|
||||
"dropping-not-allowed": "Lâcher des notes à cet endroit n'est pas autorisé"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Épingler cette fenêtre au premier plan"
|
||||
@@ -1549,10 +1559,19 @@
|
||||
"note_detail": {
|
||||
"could_not_find_typewidget": "Impossible de trouver typeWidget pour le type '{{type}}'",
|
||||
"printing": "Impression en cours...",
|
||||
"printing_pdf": "Export au format PDF en cours..."
|
||||
"printing_pdf": "Export au format PDF en cours...",
|
||||
"print_report_title": "Imprimer le rapport",
|
||||
"print_report_collection_details_button": "Consulter les détails",
|
||||
"print_report_collection_details_ignored_notes": "Notes ignorées"
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "saisir le titre de la note ici..."
|
||||
"placeholder": "saisir le titre de la note ici...",
|
||||
"created_on": "Créé le <Value />",
|
||||
"last_modified": "Modifié le <Value />",
|
||||
"note_type_switcher_label": "Basculer de {{type}} à :",
|
||||
"note_type_switcher_others": "Autre type de note",
|
||||
"note_type_switcher_templates": "Modèle",
|
||||
"note_type_switcher_collection": "Collection"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "Aucune note n'a été trouvée pour les paramètres de recherche donnés.",
|
||||
@@ -1581,7 +1600,8 @@
|
||||
},
|
||||
"toc": {
|
||||
"table_of_contents": "Table des matières",
|
||||
"options": "Options"
|
||||
"options": "Options",
|
||||
"no_headings": "Pas d'en-tête."
|
||||
},
|
||||
"watched_file_update_status": {
|
||||
"file_last_modified": "Le fichier <code class=\"file-path\"></code> a été modifié pour la dernière fois le <span class=\"file-last-modified\"></span>.",
|
||||
@@ -1682,7 +1702,8 @@
|
||||
"copy-link": "Copier le lien",
|
||||
"paste": "Coller",
|
||||
"paste-as-plain-text": "Coller comme texte brut",
|
||||
"search_online": "Rechercher «{{term}}» avec {{searchEngine}}"
|
||||
"search_online": "Rechercher «{{term}}» avec {{searchEngine}}",
|
||||
"search_in_trilium": "Rechercher \"{{term}}\" dans Trilium"
|
||||
},
|
||||
"image_context_menu": {
|
||||
"copy_reference_to_clipboard": "Copier la référence dans le presse-papiers",
|
||||
@@ -1991,7 +2012,8 @@
|
||||
"add-column": "Ajouter une colonne",
|
||||
"add-column-placeholder": "Entrez le nom de la colonne...",
|
||||
"edit-note-title": "Cliquez pour modifier le titre de la note",
|
||||
"edit-column-title": "Cliquez pour modifier le titre de la colonne"
|
||||
"edit-column-title": "Cliquez pour modifier le titre de la colonne",
|
||||
"column-already-exists": "Cette colonne existe déjà dans le tableau."
|
||||
},
|
||||
"presentation_view": {
|
||||
"edit-slide": "Modifier cette diapositive",
|
||||
@@ -2075,7 +2097,8 @@
|
||||
"button_title": "Exporter le diagramme au format PNG"
|
||||
},
|
||||
"svg": {
|
||||
"export_to_png": "Le diagramme n'a pas pu être exporté au format PNG."
|
||||
"export_to_png": "Le diagramme n'a pas pu être exporté au format PNG.",
|
||||
"export_to_svg": "Le diagramme n'a pas pu être exporté en SVG."
|
||||
},
|
||||
"code_theme": {
|
||||
"title": "Apparence",
|
||||
@@ -2108,6 +2131,10 @@
|
||||
},
|
||||
"read-only-info": {
|
||||
"read-only-note": "Vous consultez actuellement une note en lecture seule.",
|
||||
"auto-read-only-note": "Cette note s'affiche en mode lecture seule pour un chargement plus rapide."
|
||||
"auto-read-only-note": "Cette note s'affiche en mode lecture seule pour un chargement plus rapide.",
|
||||
"edit-note": "Editer la note"
|
||||
},
|
||||
"calendar_view": {
|
||||
"delete_note": "Effacer la note..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
{
|
||||
"about": {
|
||||
"title": "ट्रिलियम नोट्स के बारें में"
|
||||
"title": "ट्रिलियम नोट्स के बारें में",
|
||||
"build_date": "निर्माण की तारीख:"
|
||||
},
|
||||
"toast": {
|
||||
"widget-error": {
|
||||
"title": "एक विजेट को इनिशियलाइज़ करने में विफल रहा"
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "एक कस्टम स्क्रिप्ट लोड करने में विफल रहा"
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "सर्वर से विजेट्स की सूची प्राप्त करने में विफल"
|
||||
},
|
||||
"open-script-note": "स्क्रिप्ट नोट खोलें"
|
||||
},
|
||||
"update_available": {
|
||||
"update_available": "उपलब्ध अद्यतन"
|
||||
},
|
||||
"code_buttons": {
|
||||
"execute_button_title": "स्क्रिप्ट एक्सीक्यूट करें",
|
||||
"trilium_api_docs_button_title": "ट्रिलियम एपीआई डॉक्स खोलें",
|
||||
"save_to_note_button_title": "नोट में सेव करें"
|
||||
},
|
||||
"hide_floating_buttons_button": {
|
||||
"button_title": "बटन छुपाएं"
|
||||
},
|
||||
"show_floating_buttons_button": {
|
||||
"button_title": "बटन दिखाएं"
|
||||
},
|
||||
"add_link": {
|
||||
"note": "नोट"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,22 @@
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Non si è riusciti a caricare uno script personalizzato",
|
||||
"message": "Lo script della nota con ID \"{{id}}\", dal titolo \"{{title}}\" non è stato inizializzato a causa di:\n\n{{message}}"
|
||||
"message": "Impossibile eseguire lo script a causa di:\n\n{{message}}"
|
||||
},
|
||||
"widget-error": {
|
||||
"title": "Impossibile inizializzare un widget",
|
||||
"message-custom": "Il widget personalizzato dalla nota con ID “{{id}}”, intitolato “{{title}}”, non è stato possibile inizializzare a causa di:\n\n{{message}}",
|
||||
"message-unknown": "Un widget sconosciuto non è stato inizializzato a causa di:\n\n{{message}}"
|
||||
}
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "Impossibile ottenere l'elenco dei widget dal server"
|
||||
},
|
||||
"widget-render-error": {
|
||||
"title": "Impossibile eseguire il rendering di un widget React personalizzato"
|
||||
},
|
||||
"widget-missing-parent": "Il widget personalizzato non ha la proprietà obbligatoria '{{property}}' definita.\n\nSe questo script deve essere eseguito senza un elemento dell'interfaccia utente, utilizzare invece '#run=frontendStartup'.",
|
||||
"open-script-note": "Apri script note",
|
||||
"scripting-error": "Errore script personalizzato: {{title}}"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Aggiungi un collegamento",
|
||||
@@ -893,7 +902,6 @@
|
||||
"download_button": "Scarica",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "Dimensione del file:",
|
||||
"preview": "Anteprima:",
|
||||
"preview_not_available": "L'anteprima non è disponibile per questo tipo di nota."
|
||||
},
|
||||
"sort_child_notes": {
|
||||
@@ -1334,7 +1342,16 @@
|
||||
"note_icon": {
|
||||
"change_note_icon": "Cambia icona nota",
|
||||
"search": "Ricerca:",
|
||||
"reset-default": "Ripristina l'icona predefinita"
|
||||
"reset-default": "Ripristina l'icona predefinita",
|
||||
"search_placeholder_one": "Cerca {{number}} icona in {{count}} pacchetto",
|
||||
"search_placeholder_many": "Cerca {{number}} icone in {{count}} pacchetti",
|
||||
"search_placeholder_other": "Cerca {{number}} icone in {{count}} pacchetti",
|
||||
"search_placeholder_filtered": "Cerca {{number}} icone in {{name}}",
|
||||
"filter": "Filtro",
|
||||
"filter-none": "Tutte le icone",
|
||||
"filter-default": "Icone predefinite",
|
||||
"icon_tooltip": "{{name}}\nPacchetto icone: {{iconPack}}",
|
||||
"no_results": "Nessuna icona trovata."
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "Tipo di nota",
|
||||
@@ -1792,7 +1809,7 @@
|
||||
"will_be_deleted_in": "Questo allegato verrà eliminato automaticamente tra {{time}}",
|
||||
"will_be_deleted_soon": "Questo allegato verrà eliminato automaticamente a breve",
|
||||
"deletion_reason": ", perché l'allegato non è collegato al contenuto della nota. Per impedirne l'eliminazione, aggiungi nuovamente il collegamento all'allegato nel contenuto o converti l'allegato in nota.",
|
||||
"role_and_size": "Ruolo: {{role}}, Dimensione: {{size}}",
|
||||
"role_and_size": "Ruolo: {{role}}, dimensione: {{size}}, MIME: {{- mimeType}}",
|
||||
"link_copied": "Link all'allegato copiato negli appunti.",
|
||||
"unrecognized_role": "Ruolo di allegato non riconosciuto '{{role}}'."
|
||||
},
|
||||
@@ -1886,7 +1903,13 @@
|
||||
"note_detail": {
|
||||
"could_not_find_typewidget": "Impossibile trovare typeWidget per il tipo '{{type}}'",
|
||||
"printing": "Stampa in corso...",
|
||||
"printing_pdf": "Esportazione in PDF in corso..."
|
||||
"printing_pdf": "Esportazione in PDF in corso...",
|
||||
"print_report_title": "Stampa rapporto",
|
||||
"print_report_collection_content_one": "{{count}} la note nella raccolta non può essere stampata perché non è supportata o è protetta.",
|
||||
"print_report_collection_content_many": "{{count}} le note nella raccolta non possono essere stampate perché non sono supportate o sono protette.",
|
||||
"print_report_collection_content_other": "{{count}} le note nella raccolta non possono essere stampate perché non sono supportate o sono protette.",
|
||||
"print_report_collection_details_button": "Vedi dettagli",
|
||||
"print_report_collection_details_ignored_notes": "Note ignorate"
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "scrivi qui il titolo della nota...",
|
||||
@@ -1896,7 +1919,8 @@
|
||||
"note_type_switcher_others": "Altro tipo di nota",
|
||||
"note_type_switcher_templates": "Modello",
|
||||
"note_type_switcher_collection": "Collezione",
|
||||
"edited_notes": "Note modificate"
|
||||
"edited_notes": "Note modificate in questo giorno",
|
||||
"promoted_attributes": "Attributi promossi"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "Non sono state trovate note per i parametri di ricerca specificati.",
|
||||
|
||||
@@ -153,7 +153,14 @@
|
||||
"note_icon": {
|
||||
"change_note_icon": "ノートアイコンの変更",
|
||||
"search": "検索:",
|
||||
"reset-default": "アイコンをデフォルトに戻す"
|
||||
"reset-default": "アイコンをデフォルトに戻す",
|
||||
"search_placeholder_other": "{{count}} 個のパックから {{number}} 個のアイコンを検索",
|
||||
"search_placeholder_filtered": "{{name}} で {{number}} 個のアイコンを検索",
|
||||
"filter": "フィルター",
|
||||
"filter-none": "すべてのアイコン",
|
||||
"filter-default": "デフォルトアイコン",
|
||||
"icon_tooltip": "{{name}}\nアイコンパック: {{iconPack}}",
|
||||
"no_results": "アイコンが見つかりません。"
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "ノートタイプ",
|
||||
@@ -647,7 +654,6 @@
|
||||
"revision_deleted": "ノートの変更履歴は削除されました。",
|
||||
"settings": "ノートの変更履歴の設定",
|
||||
"file_size": "ファイルサイズ:",
|
||||
"preview": "プレビュー:",
|
||||
"preview_not_available": "このノートタイプではプレビューは利用できません。",
|
||||
"diff_on": "差分を表示",
|
||||
"diff_off": "内容を表示",
|
||||
@@ -1238,7 +1244,11 @@
|
||||
"saved-search-note-refreshed": "保存した検索ノートが更新されました。",
|
||||
"refresh-saved-search-results": "保存した検索結果を更新",
|
||||
"toggle-sidebar": "サイドバーを切り替え",
|
||||
"dropping-not-allowed": "この場所にノートをドロップすることはできません。"
|
||||
"dropping-not-allowed": "この場所にノートをドロップすることはできません。",
|
||||
"clone-indicator-tooltip": "このノートには {{- count}} 個の親があります: {{- parents}}",
|
||||
"clone-indicator-tooltip-single": "このノートは複製されています (親が 1 件追加: {{- parent}})",
|
||||
"shared-indicator-tooltip": "このノートは公開されています",
|
||||
"shared-indicator-tooltip-with-url": "このノートは以下で公開されています: {{- url}}"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "一括操作",
|
||||
@@ -2129,7 +2139,7 @@
|
||||
"will_be_deleted_in": "この添付ファイルは {{time}} 後に自動的に削除されます",
|
||||
"will_be_deleted_soon": "この添付ファイルはすぐに自動的に削除されます",
|
||||
"deletion_reason": "、添付ファイルがノートのコンテンツにリンクされていないためです。削除されないようにするには、添付ファイルのリンクをコンテンツに再度追加するか、添付ファイルをノートに変換してください。",
|
||||
"role_and_size": "ロール: {{role}},サイズ: {{size}}",
|
||||
"role_and_size": "ロール: {{role}},サイズ: {{size}}, MIME: {{- mimeType}}",
|
||||
"link_copied": "添付ファイルのリンクをクリップボードにコピーしました。",
|
||||
"unrecognized_role": "添付ファイルのロール「{{role}}」は認識されません。"
|
||||
},
|
||||
@@ -2186,7 +2196,14 @@
|
||||
"execute_sql_description": "このノートは SQL ノートです。クリックすると SQL クエリが実行されます。",
|
||||
"shared_copy_to_clipboard": "リンクをクリップボードにコピー",
|
||||
"shared_open_in_browser": "ブラウザでリンクを開く",
|
||||
"shared_unshare": "共有を削除"
|
||||
"shared_unshare": "共有を削除",
|
||||
"save_status_saved": "保存されました",
|
||||
"save_status_saving": "保存中...",
|
||||
"save_status_unsaved": "未保存",
|
||||
"save_status_error": "保存に失敗しました",
|
||||
"save_status_saving_tooltip": "変更を保存しています。",
|
||||
"save_status_unsaved_tooltip": "未保存の変更があります。すぐに自動的に保存されます。",
|
||||
"save_status_error_tooltip": "ノートの保存中にエラーが発生しました。可能であれば、ノートの内容を別の場所にコピーして、アプリケーションを再読み込みしてください。"
|
||||
},
|
||||
"status_bar": {
|
||||
"language_title": "コンテンツの言語を変更",
|
||||
@@ -2217,5 +2234,12 @@
|
||||
},
|
||||
"attributes_panel": {
|
||||
"title": "ノート属性"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_other": "{{count}} 添付ファイル",
|
||||
"layers_other": "{{count}} 層",
|
||||
"pages_other": "{{count}} ページ",
|
||||
"pages_alt": "ページ {{pageNumber}}",
|
||||
"pages_loading": "読み込み中..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,22 @@
|
||||
{}
|
||||
{
|
||||
"about": {
|
||||
"title": "Om Trilium Notes",
|
||||
"app_version": "App versjon:",
|
||||
"db_version": "DB versjon:",
|
||||
"sync_version": "Synk versjon:",
|
||||
"build_date": "Byggdato:",
|
||||
"build_revision": "Bygg versjon:",
|
||||
"data_directory": "Datamappe:",
|
||||
"homepage": "Hjemmeside:"
|
||||
},
|
||||
"experimental_features": {
|
||||
"new_layout_description": "Prøv det nye grensesnittet for et mer moderne utseende og forbedret brukervenlighet. Det må påregnes betydelige endringer i kommende versjoner."
|
||||
},
|
||||
"cpu_arch_warning": {
|
||||
"recommendation": "For den beste brukeropplevelsen, vennligst last ned den tilpassede ARM64-versjonen av TriliumNext fra siden for utgivelser."
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink_one": "{{count}} Tilbakelenke",
|
||||
"backlink_other": "{{count}} Tilbakelenker"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -959,7 +959,6 @@
|
||||
"download_button": "Pobierz",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "Rozmiar pliku:",
|
||||
"preview": "Podgląd:",
|
||||
"preview_not_available": "Podgląd nie jest dostępny dla tego typu notatki."
|
||||
},
|
||||
"sort_child_notes": {
|
||||
|
||||
@@ -274,7 +274,6 @@
|
||||
"download_button": "Descarregar",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "Tamanho do ficheiro:",
|
||||
"preview": "Visualizar:",
|
||||
"preview_not_available": "A visualização não está disponível para este tipo de nota."
|
||||
},
|
||||
"sort_child_notes": {
|
||||
|
||||
@@ -439,7 +439,6 @@
|
||||
"download_button": "Download",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "Tamanho do arquivo:",
|
||||
"preview": "Visualizar:",
|
||||
"preview_not_available": "A visualização não está disponível para este tipo de nota.",
|
||||
"diff_on": "Exibir diferença",
|
||||
"diff_off": "Exibir conteúdo",
|
||||
|
||||
@@ -1103,7 +1103,6 @@
|
||||
"mime": "MIME: ",
|
||||
"no_revisions": "Nu există încă nicio revizie pentru această notiță...",
|
||||
"note_revisions": "Revizii ale notiței",
|
||||
"preview": "Previzualizare:",
|
||||
"preview_not_available": "Nu este disponibilă o previzualizare pentru acest tip de notiță.",
|
||||
"restore_button": "Restaurează",
|
||||
"revision_deleted": "Revizia notiței a fost ștearsă.",
|
||||
|
||||
@@ -387,7 +387,6 @@
|
||||
"revision_deleted": "Версия заметки была удалена.",
|
||||
"download_button": "Скачать",
|
||||
"file_size": "Размер файла:",
|
||||
"preview": "Предпросмотр:",
|
||||
"preview_not_available": "Предпосмотр недоступен для заметки этого типа.",
|
||||
"mime": "MIME: ",
|
||||
"settings": "Настройка версионирования заметок",
|
||||
@@ -1012,7 +1011,16 @@
|
||||
"note_icon": {
|
||||
"search": "Поиск:",
|
||||
"change_note_icon": "Изменить иконку заметки",
|
||||
"reset-default": "Сбросить к значку по умолчанию"
|
||||
"reset-default": "Сбросить к значку по умолчанию",
|
||||
"no_results": "Иконки не найдены.",
|
||||
"icon_tooltip": "{{name}}\nНабор иконок: {{iconPack}}",
|
||||
"filter-default": "Иконки по-умолчанию",
|
||||
"filter-none": "Все иконки",
|
||||
"filter": "Фильтр",
|
||||
"search_placeholder_filtered": "Поиск {{number}} иконок в {{name}}",
|
||||
"search_placeholder_one": "Поиск {{number}} иконки среди {{count}} наборов",
|
||||
"search_placeholder_few": "Поиск {{number}} иконок среди {{count}} наборов",
|
||||
"search_placeholder_many": "Поиск {{number}} иконок среди {{count}} наборов"
|
||||
},
|
||||
"basic_properties": {
|
||||
"editable": "Изменяемое",
|
||||
@@ -2025,7 +2033,7 @@
|
||||
"lost-websocket-connection-message": "Проверьте конфигурацию обратного прокси (например, nginx или Apache), чтобы убедиться, что соединения WebSocket должным образом разрешены и не заблокированы."
|
||||
},
|
||||
"attachment_detail_2": {
|
||||
"role_and_size": "Роль: {{role}}, Размер: {{size}}",
|
||||
"role_and_size": "Роль: {{role}}, размер: {{size}}, MIME: {{- mimeType}}",
|
||||
"unrecognized_role": "Нераспознанная роль вложения '{{role}}'.",
|
||||
"link_copied": "Ссылка на вложение скопирована в буфер обмена.",
|
||||
"will_be_deleted_soon": "Это вложение скоро будет автоматически удалено",
|
||||
|
||||
@@ -271,7 +271,6 @@
|
||||
"download_button": "Preuzmi",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "Veličina datoteke:",
|
||||
"preview": "Pregled:",
|
||||
"preview_not_available": "Pregled nije dostupan za ovaj tip beleške."
|
||||
},
|
||||
"sort_child_notes": {
|
||||
|
||||
@@ -29,8 +29,9 @@
|
||||
"widget-render-error": {
|
||||
"title": "無法渲染自訂 React 元件"
|
||||
},
|
||||
"widget-missing-parent": "自訂元件未定義強制性的 \"{{property}}\" 屬性。",
|
||||
"open-script-note": "打開腳本筆記"
|
||||
"widget-missing-parent": "自訂元件未定義強制性的 \"{{property}}\" 屬性。\n\n若此腳本需在無 UI 的情況下執行,請改用 \"#run=frontendStartup\"。",
|
||||
"open-script-note": "打開腳本筆記",
|
||||
"scripting-error": "自訂腳本錯誤:{{title}}"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "新增連結",
|
||||
@@ -287,7 +288,6 @@
|
||||
"download_button": "下載",
|
||||
"mime": "MIME類型: ",
|
||||
"file_size": "檔案大小:",
|
||||
"preview": "預覽:",
|
||||
"preview_not_available": "無法預覽此類型的筆記。",
|
||||
"restore_button": "還原",
|
||||
"delete_button": "刪除",
|
||||
@@ -762,7 +762,15 @@
|
||||
"note_icon": {
|
||||
"change_note_icon": "更改筆記圖標",
|
||||
"search": "搜尋:",
|
||||
"reset-default": "重置為預設圖標"
|
||||
"reset-default": "重置為預設圖標",
|
||||
"search_placeholder_one": "在 {{count}} 個圖示包中搜尋 {{number}} 個圖示",
|
||||
"search_placeholder_other": "",
|
||||
"search_placeholder_filtered": "在 {{name}} 中搜尋 {{number}} 個圖示",
|
||||
"filter": "篩選",
|
||||
"filter-none": "所有圖示",
|
||||
"filter-default": "預設圖示",
|
||||
"icon_tooltip": "{{name}}\n圖示包:{{iconPack}}",
|
||||
"no_results": "找不到圖示。"
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "筆記類型",
|
||||
@@ -1404,7 +1412,7 @@
|
||||
"will_be_deleted_in": "此附件將在 {{time}} 後自動刪除",
|
||||
"will_be_deleted_soon": "該附件即將被自動刪除",
|
||||
"deletion_reason": ",因為該附件未連結在筆記的內容中。為防止被刪除,請將附件連結重新新增至內容中或將附件轉換為筆記。",
|
||||
"role_and_size": "角色:{{role}},大小:{{size}}",
|
||||
"role_and_size": "角色:{{role}},大小:{{size}},MIME:{{- mimeType}}",
|
||||
"link_copied": "已複製附件連結到剪貼簿。",
|
||||
"unrecognized_role": "無法識別的附件角色 '{{role}}'。"
|
||||
},
|
||||
@@ -1548,7 +1556,11 @@
|
||||
"create-child-note": "建立子筆記",
|
||||
"unhoist": "取消聚焦",
|
||||
"toggle-sidebar": "切換側邊欄",
|
||||
"dropping-not-allowed": "不允許移動筆記至此處。"
|
||||
"dropping-not-allowed": "不允許移動筆記至此處。",
|
||||
"clone-indicator-tooltip": "此筆記有 {{- count}} 個父級:{{- parents}}",
|
||||
"clone-indicator-tooltip-single": "此筆記已克隆(新增 1 個父級:{{- parent}})",
|
||||
"shared-indicator-tooltip": "此筆記已公開分享",
|
||||
"shared-indicator-tooltip-with-url": "此筆記已公開分享至:{{- url}}"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "保持此視窗置頂"
|
||||
@@ -1556,7 +1568,12 @@
|
||||
"note_detail": {
|
||||
"could_not_find_typewidget": "找不到類型為 '{{type}}' 的 typeWidget",
|
||||
"printing": "正在列印…",
|
||||
"printing_pdf": "正在匯出為 PDF…"
|
||||
"printing_pdf": "正在匯出為 PDF…",
|
||||
"print_report_title": "列印報告",
|
||||
"print_report_collection_content_one": "集合中的 {{count}} 篇筆記無法列印,因為它們不被支援或受到保護。",
|
||||
"print_report_collection_content_other": "",
|
||||
"print_report_collection_details_button": "查看詳情",
|
||||
"print_report_collection_details_ignored_notes": "忽略的筆記"
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "請輸入筆記標題...",
|
||||
@@ -1566,7 +1583,8 @@
|
||||
"note_type_switcher_others": "其他筆記類型",
|
||||
"note_type_switcher_templates": "模板",
|
||||
"note_type_switcher_collection": "集合",
|
||||
"edited_notes": "編輯過的筆記"
|
||||
"edited_notes": "今天編輯過的筆記",
|
||||
"promoted_attributes": "升級屬性"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "沒有找到符合搜尋條件的筆記。",
|
||||
@@ -2182,7 +2200,14 @@
|
||||
"read_only_temporarily_disabled_description": "此筆記目前可編輯,但通常為唯讀狀態。當您切換至其他筆記時,本筆記將立即恢復為唯讀模式。\n\n點擊此處重新啟用唯讀模式。",
|
||||
"clipped_note_description": "本筆記原始來源為 {{url}}。\n\n點擊此處前往原網頁。",
|
||||
"execute_script_description": "此筆記為腳本筆記。點擊以執行腳本。",
|
||||
"execute_sql_description": "此筆記為 SQL 筆記。點擊以執行 SQL 查詢。"
|
||||
"execute_sql_description": "此筆記為 SQL 筆記。點擊以執行 SQL 查詢。",
|
||||
"save_status_saved": "已儲存",
|
||||
"save_status_saving": "正在儲存…",
|
||||
"save_status_unsaved": "未儲存",
|
||||
"save_status_error": "儲存失敗",
|
||||
"save_status_saving_tooltip": "正在儲存更動。",
|
||||
"save_status_unsaved_tooltip": "仍有更動尚未儲存。它們將在稍後自動儲存。",
|
||||
"save_status_error_tooltip": "在儲存筆記時發生錯誤。如果可以,請嘗試將筆記內容複製至他處並重新載入應用程式。"
|
||||
},
|
||||
"breadcrumb": {
|
||||
"hoisted_badge": "聚焦",
|
||||
@@ -2219,5 +2244,15 @@
|
||||
},
|
||||
"attributes_panel": {
|
||||
"title": "筆記屬性"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_one": "{{count}} 個附件",
|
||||
"attachments_other": "",
|
||||
"layers_one": "{{count}} 層",
|
||||
"layers_other": "",
|
||||
"pages_one": "共 {{count}} 頁",
|
||||
"pages_other": "",
|
||||
"pages_alt": "第 {{pageNumber}} 頁",
|
||||
"pages_loading": "正在載入…"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,7 +321,6 @@
|
||||
"download_button": "Завантажити",
|
||||
"mime": "МІМЕ: ",
|
||||
"file_size": "Розмір файлу:",
|
||||
"preview": "Попередній перегляд:",
|
||||
"preview_not_available": "Попередній перегляд недоступний для цього типу нотатки.",
|
||||
"diff_on": "Показати різницю",
|
||||
"diff_off": "Показати вміст",
|
||||
|
||||
121
apps/client/src/types-pdfjs.d.ts
vendored
Normal file
121
apps/client/src/types-pdfjs.d.ts
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
type HistoryData = {
|
||||
files: {
|
||||
fingerprint: string;
|
||||
page: number;
|
||||
zoom: string;
|
||||
scrollLeft: number;
|
||||
scrollTop: number;
|
||||
rotation: number;
|
||||
sidebarView: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
interface Window {
|
||||
/**
|
||||
* By default, pdf.js will try to store information about the opened PDFs such as zoom and scroll position in local storage.
|
||||
* The Trilium alternative is to use attachments stored at note level.
|
||||
* This variable represents the direct content used by the pdf.js viewer in its local storage key, but in plain JS object format.
|
||||
* The variable must be set early at startup, before pdf.js fully initializes.
|
||||
*/
|
||||
TRILIUM_VIEW_HISTORY_STORE?: HistoryData;
|
||||
|
||||
/**
|
||||
* If set to true, hides the pdf.js viewer default sidebar containing the outline, page navigation, etc.
|
||||
* This needs to be set early in the main method.
|
||||
*/
|
||||
TRILIUM_HIDE_SIDEBAR?: boolean;
|
||||
|
||||
TRILIUM_NOTE_ID: string;
|
||||
|
||||
TRILIUM_NTX_ID: string | null | undefined;
|
||||
}
|
||||
|
||||
interface PdfOutlineItem {
|
||||
title: string;
|
||||
level: number;
|
||||
dest: unknown;
|
||||
id: string;
|
||||
items: PdfOutlineItem[];
|
||||
}
|
||||
|
||||
interface WithContext {
|
||||
ntxId: string;
|
||||
noteId: string | null | undefined;
|
||||
}
|
||||
|
||||
interface PdfDocumentModifiedMessage extends WithContext {
|
||||
type: "pdfjs-viewer-document-modified";
|
||||
}
|
||||
|
||||
interface PdfDocumentBlobResultMessage extends WithContext {
|
||||
type: "pdfjs-viewer-blob";
|
||||
data: Uint8Array<ArrayBufferLike>;
|
||||
}
|
||||
|
||||
interface PdfSaveViewHistoryMessage extends WithContext {
|
||||
type: "pdfjs-viewer-save-view-history";
|
||||
data: string;
|
||||
}
|
||||
|
||||
interface PdfViewerTocMessage {
|
||||
type: "pdfjs-viewer-toc";
|
||||
data: PdfOutlineItem[];
|
||||
}
|
||||
|
||||
interface PdfViewerActiveHeadingMessage {
|
||||
type: "pdfjs-viewer-active-heading";
|
||||
headingId: string;
|
||||
}
|
||||
|
||||
interface PdfViewerPageInfoMessage {
|
||||
type: "pdfjs-viewer-page-info";
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
}
|
||||
|
||||
interface PdfViewerCurrentPageMessage {
|
||||
type: "pdfjs-viewer-current-page";
|
||||
currentPage: number;
|
||||
}
|
||||
|
||||
interface PdfViewerThumbnailMessage {
|
||||
type: "pdfjs-viewer-thumbnail";
|
||||
pageNumber: number;
|
||||
dataUrl: string;
|
||||
}
|
||||
|
||||
interface PdfAttachment {
|
||||
filename: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface PdfViewerAttachmentsMessage {
|
||||
type: "pdfjs-viewer-attachments";
|
||||
attachments: PdfAttachment[];
|
||||
downloadAttachment?: (fileName: string) => void;
|
||||
}
|
||||
|
||||
interface PdfLayer {
|
||||
id: string;
|
||||
name: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
interface PdfViewerLayersMessage {
|
||||
type: "pdfjs-viewer-layers";
|
||||
layers: PdfLayer[];
|
||||
toggleLayer?: (layerId: string, visible: boolean) => void;
|
||||
}
|
||||
|
||||
type PdfMessageEvent = MessageEvent<
|
||||
PdfDocumentModifiedMessage
|
||||
| PdfSaveViewHistoryMessage
|
||||
| PdfViewerTocMessage
|
||||
| PdfViewerActiveHeadingMessage
|
||||
| PdfViewerPageInfoMessage
|
||||
| PdfViewerCurrentPageMessage
|
||||
| PdfViewerThumbnailMessage
|
||||
| PdfViewerAttachmentsMessage
|
||||
| PdfViewerLayersMessage
|
||||
| PdfDocumentBlobResultMessage
|
||||
>;
|
||||
@@ -1,11 +1,17 @@
|
||||
import clsx from "clsx";
|
||||
|
||||
import { t } from "../../services/i18n";
|
||||
import options from "../../services/options";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { useTriliumOptionBool } from "../react/hooks";
|
||||
import { useState, useCallback } from "preact/hooks";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
|
||||
export default function RightPaneToggle() {
|
||||
const [ rightPaneVisible, setRightPaneVisible ] = useTriliumOptionBool("rightPaneVisible");
|
||||
const [ rightPaneVisible, setRightPaneVisible ] = useState(options.is("rightPaneVisible"));
|
||||
|
||||
useTriliumEvent("toggleRightPane", useCallback(() => {
|
||||
setRightPaneVisible(current => !current);
|
||||
}, []));
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
@@ -15,7 +21,7 @@ export default function RightPaneToggle() {
|
||||
)}
|
||||
text={t("right_pane.toggle")}
|
||||
icon="bx bx-sidebar"
|
||||
onClick={() => setRightPaneVisible(!rightPaneVisible)}
|
||||
triggerCommand="toggleRightPane"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import froca from "../../services/froca";
|
||||
import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } from "../../services/ws";
|
||||
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent } from "../react/hooks";
|
||||
import { allViewTypes, ViewModeMedia, ViewModeProps, ViewTypeOptions } from "./interface";
|
||||
import ViewModeStorage from "./view_mode_storage";
|
||||
import ViewModeStorage, { type ViewModeStorageType } from "./view_mode_storage";
|
||||
|
||||
interface NoteListProps {
|
||||
note: FNote | null | undefined;
|
||||
notePath: string | null | undefined;
|
||||
@@ -215,7 +216,7 @@ export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOpt
|
||||
return noteIds;
|
||||
}
|
||||
|
||||
export function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined) {
|
||||
export function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewModeStorageType | undefined) {
|
||||
const [ viewConfig, setViewConfig ] = useState<{
|
||||
config: T | undefined;
|
||||
storeFn: (data: T) => void;
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
import { DateSelectArg, EventChangeArg, EventMountArg, EventSourceFuncArg, LocaleInput, PluginDef } from "@fullcalendar/core/index.js";
|
||||
import { ViewModeProps } from "../interface";
|
||||
import Calendar from "./calendar";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import "./index.css";
|
||||
import { useNoteLabel, useNoteLabelBoolean, useResizeObserver, useSpacedUpdate, useTriliumEvent, useTriliumOption, useTriliumOptionInt } from "../../react/hooks";
|
||||
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
|
||||
|
||||
import { Calendar as FullCalendar } from "@fullcalendar/core";
|
||||
import { parseStartEndDateFromEvent, parseStartEndTimeFromEvent } from "./utils";
|
||||
import dialog from "../../../services/dialog";
|
||||
import { t } from "../../../services/i18n";
|
||||
import { buildEvents, buildEventsForCalendar } from "./event_builder";
|
||||
import { changeEvent, newEvent } from "./api";
|
||||
import froca from "../../../services/froca";
|
||||
import date_notes from "../../../services/date_notes";
|
||||
import appContext from "../../../components/app_context";
|
||||
import { DateSelectArg, EventChangeArg, EventMountArg, EventSourceFuncArg, LocaleInput, PluginDef } from "@fullcalendar/core/index.js";
|
||||
import { DateClickArg } from "@fullcalendar/interaction";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import Button, { ButtonGroup } from "../../react/Button";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
|
||||
import { RefObject } from "preact";
|
||||
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
|
||||
import { openCalendarContextMenu } from "./context_menu";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext from "../../../components/app_context";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import date_notes from "../../../services/date_notes";
|
||||
import dialog from "../../../services/dialog";
|
||||
import froca from "../../../services/froca";
|
||||
import { t } from "../../../services/i18n";
|
||||
import { isMobile } from "../../../services/utils";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import Button, { ButtonGroup } from "../../react/Button";
|
||||
import { useNoteLabel, useNoteLabelBoolean, useResizeObserver, useSpacedUpdate, useTriliumEvent, useTriliumOption, useTriliumOptionInt } from "../../react/hooks";
|
||||
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
|
||||
import { ViewModeProps } from "../interface";
|
||||
import { changeEvent, newEvent } from "./api";
|
||||
import Calendar from "./calendar";
|
||||
import { openCalendarContextMenu } from "./context_menu";
|
||||
import { buildEvents, buildEventsForCalendar } from "./event_builder";
|
||||
import { parseStartEndDateFromEvent, parseStartEndTimeFromEvent } from "./utils";
|
||||
|
||||
interface CalendarViewData {
|
||||
|
||||
@@ -59,7 +61,7 @@ const CALENDAR_VIEWS = [
|
||||
previousText: t("calendar.month_previous"),
|
||||
nextText: t("calendar.month_next")
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
const SUPPORTED_CALENDAR_VIEW_TYPE = CALENDAR_VIEWS.map(v => v.type);
|
||||
|
||||
@@ -75,6 +77,7 @@ export const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, (() => Promise<{ de
|
||||
ru: () => import("@fullcalendar/core/locales/ru"),
|
||||
ja: () => import("@fullcalendar/core/locales/ja"),
|
||||
pt: () => import("@fullcalendar/core/locales/pt"),
|
||||
pl: () => import("@fullcalendar/core/locales/pl"),
|
||||
"pt_br": () => import("@fullcalendar/core/locales/pt-br"),
|
||||
uk: () => import("@fullcalendar/core/locales/uk"),
|
||||
en: null,
|
||||
@@ -102,9 +105,9 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
|
||||
const eventBuilder = useMemo(() => {
|
||||
if (!isCalendarRoot) {
|
||||
return async () => await buildEvents(noteIds);
|
||||
} else {
|
||||
return async (e: EventSourceFuncArg) => await buildEventsForCalendar(note, e);
|
||||
}
|
||||
}
|
||||
return async (e: EventSourceFuncArg) => await buildEventsForCalendar(note, e);
|
||||
|
||||
}, [isCalendarRoot, noteIds]);
|
||||
|
||||
const plugins = usePlugins(isEditable, isCalendarRoot);
|
||||
@@ -178,7 +181,7 @@ function CalendarHeader({ calendarRef }: { calendarRef: RefObject<FullCalendar>
|
||||
<ActionButton icon="bx bx-chevron-right" text={currentViewData?.nextText ?? ""} frame onClick={() => calendarRef.current?.next()} />
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function usePlugins(isEditable: boolean, isCalendarRoot: boolean) {
|
||||
@@ -293,7 +296,7 @@ function useEventDisplayCustomization(parentNote: FNote) {
|
||||
if (promotedAttributes) {
|
||||
let promotedAttributesHtml = "";
|
||||
for (const [name, value] of promotedAttributes) {
|
||||
promotedAttributesHtml = promotedAttributesHtml + /*html*/`\
|
||||
promotedAttributesHtml = `${promotedAttributesHtml /*html*/}\
|
||||
<div class="promoted-attribute">
|
||||
<span class="promoted-attribute-name">${name}</span>: <span class="promoted-attribute-value">${value}</span>
|
||||
</div>`;
|
||||
|
||||
@@ -142,4 +142,10 @@
|
||||
border: 1px solid var(--main-border-color);
|
||||
background: var(--more-accented-background-color);
|
||||
}
|
||||
|
||||
.note-list.grid-view .note-path {
|
||||
margin-left: 0.5em;
|
||||
vertical-align: middle;
|
||||
opacity: 0.5;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
@@ -7,7 +7,6 @@ import attribute_renderer from "../../../services/attribute_renderer";
|
||||
import content_renderer from "../../../services/content_renderer";
|
||||
import { t } from "../../../services/i18n";
|
||||
import link from "../../../services/link";
|
||||
import tree from "../../../services/tree";
|
||||
import { useImperativeSearchHighlighlighting, useNoteLabel, useNoteLabelBoolean } from "../../react/hooks";
|
||||
import Icon from "../../react/Icon";
|
||||
import NoteLink from "../../react/NoteLink";
|
||||
@@ -103,16 +102,7 @@ function ListNoteCard({ note, parentNote, highlightedTokens, currentLevel, expan
|
||||
}
|
||||
|
||||
function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined }) {
|
||||
const titleRef = useRef<HTMLSpanElement>(null);
|
||||
const [ noteTitle, setNoteTitle ] = useState<string>();
|
||||
const notePath = getNotePath(parentNote, note);
|
||||
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
|
||||
|
||||
useEffect(() => {
|
||||
tree.getNoteTitle(note.noteId, parentNote.noteId).then(setNoteTitle);
|
||||
}, [ note ]);
|
||||
|
||||
useEffect(() => highlightSearch(titleRef.current), [ noteTitle, highlightedTokens ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -123,7 +113,7 @@ function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, pa
|
||||
>
|
||||
<h5 className="note-book-header">
|
||||
<Icon className="note-icon" icon={note.getIcon()} />
|
||||
<span ref={titleRef} className="note-book-title">{noteTitle}</span>
|
||||
<NoteLink className="note-book-title" notePath={notePath} noPreview showNotePath={parentNote.type === "search"} highlightedTokens={highlightedTokens} />
|
||||
<NoteAttributes note={note} />
|
||||
</h5>
|
||||
<NoteContent
|
||||
|
||||
@@ -4,14 +4,16 @@ import { ViewTypeOptions } from "../collections/interface";
|
||||
|
||||
const ATTACHMENT_ROLE = "viewConfig";
|
||||
|
||||
export type ViewModeStorageType = ViewTypeOptions | "pdfHistory";
|
||||
|
||||
export default class ViewModeStorage<T extends object> {
|
||||
|
||||
private note: FNote;
|
||||
private attachmentName: string;
|
||||
|
||||
constructor(note: FNote, viewType: ViewTypeOptions) {
|
||||
constructor(note: FNote, viewType: ViewModeStorageType) {
|
||||
this.note = note;
|
||||
this.attachmentName = viewType + ".json";
|
||||
this.attachmentName = `${viewType}.json`;
|
||||
}
|
||||
|
||||
async store(data: T) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import FlexContainer from "./flex_container.js";
|
||||
import appContext, { type CommandData, type CommandListenerData, type EventData, type EventNames, type NoteSwitchedContext } from "../../components/app_context.js";
|
||||
import type BasicWidget from "../basic_widget.js";
|
||||
import Component from "../../components/component.js";
|
||||
import NoteContext from "../../components/note_context.js";
|
||||
import splitService from "../../services/resizer.js";
|
||||
import { isMobile } from "../../services/utils.js";
|
||||
import NoteContext from "../../components/note_context.js";
|
||||
import type BasicWidget from "../basic_widget.js";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import FlexContainer from "./flex_container.js";
|
||||
|
||||
interface SplitNoteWidget extends BasicWidget {
|
||||
hasBeenAlreadyShown?: boolean;
|
||||
@@ -74,7 +75,7 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
|
||||
|
||||
|
||||
const subContexts = activeContext.getSubContexts();
|
||||
let noteContext: NoteContext | undefined = undefined;
|
||||
let noteContext: NoteContext | undefined;
|
||||
if (isMobile() && subContexts.length > 1) {
|
||||
noteContext = subContexts.find(s => s.ntxId !== ntxId);
|
||||
}
|
||||
@@ -201,6 +202,11 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
|
||||
|
||||
async refresh() {
|
||||
this.toggleExt(true);
|
||||
|
||||
// Mark the active note context.
|
||||
for (const child of this.children as NoteContextAwareWidget[]) {
|
||||
child.$widget.toggleClass("active", !!child.noteContext?.isActive());
|
||||
}
|
||||
}
|
||||
|
||||
toggleInt(show: boolean) {} // not needed
|
||||
@@ -239,16 +245,16 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
|
||||
widget.hasBeenAlreadyShown = true;
|
||||
|
||||
return [widget.handleEvent("noteSwitched", noteSwitchedContext), this.refreshNotShown(noteSwitchedContext)];
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.resolve();
|
||||
|
||||
}
|
||||
|
||||
if (name === "activeContextChanged") {
|
||||
return this.refreshNotShown(data as EventData<"activeContextChanged">);
|
||||
} else {
|
||||
return super.handleEventInChildren(name, data);
|
||||
}
|
||||
return super.handleEventInChildren(name, data);
|
||||
|
||||
}
|
||||
|
||||
refreshNotShown(data: NoteSwitchedContext | EventData<"activeContextChanged">) {
|
||||
|
||||
@@ -88,6 +88,7 @@ export default function PopupEditor() {
|
||||
onHidden={() => setShown(false)}
|
||||
keepInDom // needed for faster loading
|
||||
noFocus // automatic focus breaks block popup
|
||||
stackable
|
||||
>
|
||||
{!isNewLayout && <ReadOnlyNoteInfoBar />}
|
||||
<PromotedAttributes />
|
||||
|
||||
@@ -14,7 +14,7 @@ body.mobile .revisions-dialog {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.modal-body {
|
||||
height: fit-content !important;
|
||||
flex-direction: column;
|
||||
@@ -24,7 +24,7 @@ body.mobile .revisions-dialog {
|
||||
.modal-footer {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
|
||||
.revision-list {
|
||||
height: fit-content !important;
|
||||
max-height: 20vh;
|
||||
@@ -32,7 +32,7 @@ body.mobile .revisions-dialog {
|
||||
padding: 0 1em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
.modal-body > .revision-content-wrapper {
|
||||
flex-grow: 1;
|
||||
max-width: unset !important;
|
||||
@@ -40,24 +40,68 @@ body.mobile .revisions-dialog {
|
||||
margin: 0;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
|
||||
.modal-body > .revision-content-wrapper > div:first-of-type {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.revision-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.revision-title-buttons {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
|
||||
.revision-content {
|
||||
padding: 0.5em;
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.revisions-dialog {
|
||||
.revision-title-buttons {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.revision-list {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.revision-content.type-file {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex-grow: 1;
|
||||
|
||||
.file-preview-table {
|
||||
th,
|
||||
td {
|
||||
padding: 0.25em 0;
|
||||
}
|
||||
}
|
||||
|
||||
.revision-file-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.revision-file-preview-content {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> * {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
import type { RevisionPojo, RevisionItem } from "@triliumnext/commons";
|
||||
import "./revisions.css";
|
||||
|
||||
import type { RevisionItem, RevisionPojo } from "@triliumnext/commons";
|
||||
import clsx from "clsx";
|
||||
import { diffWords } from "diff";
|
||||
import type { CSSProperties } from "preact/compat";
|
||||
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import dialog from "../../services/dialog";
|
||||
import froca from "../../services/froca";
|
||||
import { t } from "../../services/i18n";
|
||||
import { renderMathInElement } from "../../services/math";
|
||||
import open from "../../services/open";
|
||||
import options from "../../services/options";
|
||||
import protected_session_holder from "../../services/protected_session_holder";
|
||||
import server from "../../services/server";
|
||||
import toast from "../../services/toast";
|
||||
import Button from "../react/Button";
|
||||
import FormToggle from "../react/FormToggle";
|
||||
import Modal from "../react/Modal";
|
||||
import FormList, { FormListItem } from "../react/FormList";
|
||||
import utils from "../../services/utils";
|
||||
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
|
||||
import protected_session_holder from "../../services/protected_session_holder";
|
||||
import { renderMathInElement } from "../../services/math";
|
||||
import type { CSSProperties } from "preact/compat";
|
||||
import open from "../../services/open";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import options from "../../services/options";
|
||||
import Button from "../react/Button";
|
||||
import FormList, { FormListItem } from "../react/FormList";
|
||||
import FormToggle from "../react/FormToggle";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import { diffWords } from "diff";
|
||||
import "./revisions.css";
|
||||
import Modal from "../react/Modal";
|
||||
import { RawHtmlBlock } from "../react/RawHtml";
|
||||
import PdfViewer from "../type_widgets/file/PdfViewer";
|
||||
|
||||
export default function RevisionsDialog() {
|
||||
const [ note, setNote ] = useState<FNote>();
|
||||
@@ -47,7 +52,7 @@ export default function RevisionsDialog() {
|
||||
setRevisions(undefined);
|
||||
setNoteContent(undefined);
|
||||
}
|
||||
}, [ note?.noteId, refreshCounter ]);
|
||||
}, [ note, refreshCounter ]);
|
||||
|
||||
if (revisions?.length && !currentRevision) {
|
||||
setCurrentRevision(revisions[0]);
|
||||
@@ -102,38 +107,38 @@ export default function RevisionsDialog() {
|
||||
setRevisions(undefined);
|
||||
}}
|
||||
show={shown}
|
||||
>
|
||||
<RevisionsList
|
||||
revisions={revisions ?? []}
|
||||
onSelect={(revisionId) => {
|
||||
const correspondingRevision = (revisions ?? []).find((r) => r.revisionId === revisionId);
|
||||
if (correspondingRevision) {
|
||||
setCurrentRevision(correspondingRevision);
|
||||
}
|
||||
}}
|
||||
currentRevision={currentRevision}
|
||||
/>
|
||||
>
|
||||
<RevisionsList
|
||||
revisions={revisions ?? []}
|
||||
onSelect={(revisionId) => {
|
||||
const correspondingRevision = (revisions ?? []).find((r) => r.revisionId === revisionId);
|
||||
if (correspondingRevision) {
|
||||
setCurrentRevision(correspondingRevision);
|
||||
}
|
||||
}}
|
||||
currentRevision={currentRevision}
|
||||
/>
|
||||
|
||||
<div className="revision-content-wrapper" style={{
|
||||
flexGrow: "1",
|
||||
marginInlineStart: "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
maxWidth: "calc(100% - 150px)",
|
||||
minWidth: 0
|
||||
}}>
|
||||
<RevisionPreview
|
||||
noteContent={noteContent}
|
||||
revisionItem={currentRevision}
|
||||
showDiff={showDiff}
|
||||
setShown={setShown}
|
||||
onRevisionDeleted={() => {
|
||||
setRefreshCounter(c => c + 1);
|
||||
setCurrentRevision(undefined);
|
||||
}} />
|
||||
</div>
|
||||
<div className="revision-content-wrapper" style={{
|
||||
flexGrow: "1",
|
||||
marginInlineStart: "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
maxWidth: "calc(100% - 150px)",
|
||||
minWidth: 0
|
||||
}}>
|
||||
<RevisionPreview
|
||||
noteContent={noteContent}
|
||||
revisionItem={currentRevision}
|
||||
showDiff={showDiff}
|
||||
setShown={setShown}
|
||||
onRevisionDeleted={() => {
|
||||
setRefreshCounter(c => c + 1);
|
||||
setCurrentRevision(undefined);
|
||||
}} />
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: RevisionItem[], onSelect: (val: string) => void, currentRevision?: RevisionItem }) {
|
||||
@@ -141,6 +146,7 @@ function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: Re
|
||||
<FormList onSelect={onSelect} fullHeight wrapperClassName="revision-list">
|
||||
{revisions.map((item) =>
|
||||
<FormListItem
|
||||
key={item.revisionId}
|
||||
value={item.revisionId}
|
||||
active={currentRevision && item.revisionId === currentRevision.revisionId}
|
||||
>
|
||||
@@ -202,14 +208,17 @@ function RevisionPreview({noteContent, revisionItem, showDiff, setShown, onRevis
|
||||
text={t("revisions.download_button")}
|
||||
onClick={() => {
|
||||
if (revisionItem.revisionId) {
|
||||
open.downloadRevision(revisionItem.noteId, revisionItem.revisionId)}
|
||||
}
|
||||
open.downloadRevision(revisionItem.noteId, revisionItem.revisionId);}
|
||||
}
|
||||
}/>
|
||||
</>
|
||||
}
|
||||
</div>)}
|
||||
</div>
|
||||
<div className="revision-content use-tn-links selectable-text" style={{ overflow: "auto", wordBreak: "break-word" }}>
|
||||
<div
|
||||
className={clsx("revision-content use-tn-links selectable-text", `type-${revisionItem?.type}`)}
|
||||
style={{ overflow: "auto", wordBreak: "break-word" }}
|
||||
>
|
||||
<RevisionContent noteContent={noteContent} revisionItem={revisionItem} fullRevision={fullRevision} showDiff={showDiff}/>
|
||||
</div>
|
||||
</>
|
||||
@@ -230,16 +239,16 @@ const CODE_STYLE: CSSProperties = {
|
||||
|
||||
function RevisionContent({ noteContent, revisionItem, fullRevision, showDiff }: { noteContent?:string, revisionItem?: RevisionItem, fullRevision?: RevisionPojo, showDiff: boolean}) {
|
||||
const content = fullRevision?.content;
|
||||
if (!revisionItem || !content) {
|
||||
if (!revisionItem || !fullRevision) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (showDiff) {
|
||||
return <RevisionContentDiff noteContent={noteContent} itemContent={content} itemType={revisionItem.type}/>
|
||||
return <RevisionContentDiff noteContent={noteContent} itemContent={content} itemType={revisionItem.type}/>;
|
||||
}
|
||||
switch (revisionItem.type) {
|
||||
case "text":
|
||||
return <RevisionContentText content={content} />
|
||||
return <RevisionContentText content={content} />;
|
||||
case "code":
|
||||
return <pre style={CODE_STYLE}>{content}</pre>;
|
||||
case "image":
|
||||
@@ -256,28 +265,11 @@ function RevisionContent({ noteContent, revisionItem, fullRevision, showDiff }:
|
||||
// as a URL to be used in a note. Instead, if they copy and paste it into a note, it will be uploaded as a new note
|
||||
return <img
|
||||
src={`data:${fullRevision.mime};base64,${fullRevision.content}`}
|
||||
style={IMAGE_STYLE} />
|
||||
style={IMAGE_STYLE} />;
|
||||
}
|
||||
}
|
||||
case "file":
|
||||
return <table cellPadding="10">
|
||||
<tr>
|
||||
<th>{t("revisions.mime")}</th>
|
||||
<td>{revisionItem.mime}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t("revisions.file_size")}</th>
|
||||
<td>{revisionItem.contentLength && utils.formatSize(revisionItem.contentLength)}</td>
|
||||
</tr>
|
||||
{fullRevision.content &&
|
||||
<tr>
|
||||
<td colspan={2}>
|
||||
<strong>{t("revisions.preview")}</strong>
|
||||
<pre className="file-preview-content" style={CODE_STYLE}>{fullRevision.content}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>;
|
||||
return <FilePreview fullRevision={fullRevision} revisionItem={revisionItem} />;
|
||||
case "canvas":
|
||||
case "mindMap":
|
||||
case "mermaid": {
|
||||
@@ -287,7 +279,7 @@ function RevisionContent({ noteContent, revisionItem, fullRevision, showDiff }:
|
||||
style={IMAGE_STYLE} />;
|
||||
}
|
||||
default:
|
||||
return <>{t("revisions.preview_not_available")}</>
|
||||
return <>{t("revisions.preview_not_available")}</>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +290,7 @@ function RevisionContentText({ content }: { content: string | Buffer<ArrayBuffer
|
||||
renderMathInElement(contentRef.current, { trust: true });
|
||||
}
|
||||
}, [content]);
|
||||
return <div ref={contentRef} className="ck-content" dangerouslySetInnerHTML={{ __html: content as string }}></div>
|
||||
return <RawHtmlBlock containerRef={contentRef} className="ck-content" html={content as string} />;
|
||||
}
|
||||
|
||||
function RevisionContentDiff({ noteContent, itemContent, itemType }: {
|
||||
@@ -330,9 +322,9 @@ function RevisionContentDiff({ noteContent, itemContent, itemType }: {
|
||||
return `<span class="revision-diff-added">${utils.escapeHtml(part.value)}</span>`;
|
||||
} else if (part.removed) {
|
||||
return `<span class="revision-diff-removed">${utils.escapeHtml(part.value)}</span>`;
|
||||
} else {
|
||||
return utils.escapeHtml(part.value);
|
||||
}
|
||||
return utils.escapeHtml(part.value);
|
||||
|
||||
}).join("");
|
||||
|
||||
if (contentRef.current) {
|
||||
@@ -340,7 +332,7 @@ function RevisionContentDiff({ noteContent, itemContent, itemType }: {
|
||||
}
|
||||
}, [noteContent, itemContent, itemType]);
|
||||
|
||||
return <div ref={contentRef} className="ck-content" style={{ whiteSpace: "pre-wrap" }}></div>;
|
||||
return <div ref={contentRef} className="ck-content" style={{ whiteSpace: "pre-wrap" }} />;
|
||||
}
|
||||
|
||||
function RevisionFooter({ note }: { note?: FNote }) {
|
||||
@@ -348,7 +340,7 @@ function RevisionFooter({ note }: { note?: FNote }) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
let revisionsNumberLimit: number | string = parseInt(note?.getLabelValue("versioningLimit") ?? "");
|
||||
let revisionsNumberLimit: number | string = parseInt(note?.getLabelValue("versioningLimit") ?? "", 10);
|
||||
if (!Number.isInteger(revisionsNumberLimit)) {
|
||||
revisionsNumberLimit = options.getInt("revisionSnapshotNumberLimit") ?? 0;
|
||||
}
|
||||
@@ -370,10 +362,67 @@ function RevisionFooter({ note }: { note?: FNote }) {
|
||||
</>;
|
||||
}
|
||||
|
||||
function FilePreview({ revisionItem, fullRevision }: { revisionItem: RevisionItem, fullRevision: RevisionPojo }) {
|
||||
return (
|
||||
<div className="revision-file-preview">
|
||||
<table className="file-preview-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{t("revisions.mime")}</th>
|
||||
<td>{revisionItem.mime}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t("revisions.file_size")}</th>
|
||||
<td>{revisionItem.contentLength && utils.formatSize(revisionItem.contentLength)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="revision-file-preview-content">
|
||||
<FilePreviewInner revisionItem={revisionItem} fullRevision={fullRevision} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FilePreviewInner({ revisionItem, fullRevision }: { revisionItem: RevisionItem, fullRevision: RevisionPojo }) {
|
||||
if (revisionItem.mime.startsWith("audio/")) {
|
||||
return (
|
||||
<audio
|
||||
src={`api/revisions/${revisionItem.revisionId}/download`}
|
||||
controls
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (revisionItem.mime.startsWith("video/")) {
|
||||
return (
|
||||
<video
|
||||
src={`api/revisions/${revisionItem.revisionId}/download`}
|
||||
controls
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (revisionItem.mime === "application/pdf") {
|
||||
return (
|
||||
<PdfViewer
|
||||
pdfUrl={`../../api/revisions/${revisionItem.revisionId}/download`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (fullRevision.content) {
|
||||
return <pre className="file-preview-content" style={CODE_STYLE}>{fullRevision.content}</pre>;
|
||||
}
|
||||
|
||||
return t("revisions.preview_not_available");
|
||||
}
|
||||
|
||||
async function getNote(noteId?: string | null) {
|
||||
if (noteId) {
|
||||
return await froca.getNote(noteId);
|
||||
} else {
|
||||
return appContext.tabManager.getActiveContextNote();
|
||||
}
|
||||
return appContext.tabManager.getActiveContextNote();
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
contain: none;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from { opacity: var(--default-opacity); }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
.note-badges {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
@@ -16,6 +21,23 @@
|
||||
&.share-badge {--color: var(--badge-share-background-color);}
|
||||
&.clipped-note-badge {--color: var(--badge-clipped-note-background-color);}
|
||||
&.execute-badge {--color: var(--badge-execute-background-color);}
|
||||
&.save-status-badge {
|
||||
--default-opacity: 0.4;
|
||||
opacity: var(--default-opacity);
|
||||
transition: opacity 250ms ease-in;
|
||||
color: var(--main-text-color);
|
||||
|
||||
&.error {
|
||||
color: var(--dropdown-item-icon-destructive-color);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.saved {
|
||||
animation: fadeOut 250ms ease-in 5s forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
min-width: 0;
|
||||
|
||||
.text {
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import "./NoteBadges.css";
|
||||
|
||||
import { clsx } from "clsx";
|
||||
|
||||
import { copyTextWithToast } from "../../services/clipboard_ext";
|
||||
import { t } from "../../services/i18n";
|
||||
import { goToLinkExt } from "../../services/link";
|
||||
import { Badge, BadgeWithDropdown } from "../react/Badge";
|
||||
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
|
||||
import { useGetContextData, useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
|
||||
import { useShareState } from "../ribbon/BasicPropertiesTab";
|
||||
import { useShareInfo } from "../shared_info";
|
||||
|
||||
export default function NoteBadges() {
|
||||
return (
|
||||
<div className="note-badges">
|
||||
<SaveStatusBadge />
|
||||
<ReadOnlyBadge />
|
||||
<ShareBadge />
|
||||
<ClippedNoteBadge />
|
||||
@@ -105,3 +108,42 @@ function ExecuteBadge() {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SaveStatusBadge() {
|
||||
const saveState = useGetContextData("saveState");
|
||||
if (!saveState) return;
|
||||
|
||||
const stateConfig = {
|
||||
saved: {
|
||||
icon: "bx bx-check",
|
||||
title: t("breadcrumb_badges.save_status_saved"),
|
||||
tooltip: undefined
|
||||
},
|
||||
saving: {
|
||||
icon: "bx bx-loader bx-spin",
|
||||
title: t("breadcrumb_badges.save_status_saving"),
|
||||
tooltip: t("breadcrumb_badges.save_status_saving_tooltip")
|
||||
},
|
||||
unsaved: {
|
||||
icon: "bx bx-pencil",
|
||||
title: t("breadcrumb_badges.save_status_unsaved"),
|
||||
tooltip: t("breadcrumb_badges.save_status_unsaved_tooltip")
|
||||
},
|
||||
error: {
|
||||
icon: "bx bxs-error",
|
||||
title: t("breadcrumb_badges.save_status_error"),
|
||||
tooltip: t("breadcrumb_badges.save_status_error_tooltip")
|
||||
}
|
||||
};
|
||||
|
||||
const { icon, title, tooltip } = stateConfig[saveState.state];
|
||||
|
||||
return (
|
||||
<Badge
|
||||
className={clsx("save-status-badge", saveState.state)}
|
||||
icon={icon}
|
||||
text={title}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||
import clsx from "clsx";
|
||||
import { type ComponentChildren, RefObject } from "preact";
|
||||
import { createPortal } from "preact/compat";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import NoteContext from "../../components/note_context";
|
||||
@@ -338,15 +338,19 @@ interface AttributesProps extends StatusBarContext {
|
||||
function AttributesButton({ note, attributesShown, setAttributesShown }: AttributesProps) {
|
||||
const [ count, setCount ] = useState(note.attributes.length);
|
||||
|
||||
const refreshCount = useCallback((note: FNote) => {
|
||||
return note.getAttributes().filter(a => !a.isAutoLink).length;
|
||||
}, []);
|
||||
|
||||
// React to note changes.
|
||||
useEffect(() => {
|
||||
setCount(note.getAttributes().filter(a => !a.isAutoLink).length);
|
||||
}, [ note ]);
|
||||
setCount(refreshCount(note));
|
||||
}, [ note, refreshCount ]);
|
||||
|
||||
// React to changes in count.
|
||||
useTriliumEvent("entitiesReloaded", (({loadResults}) => {
|
||||
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
|
||||
setCount(note.attributes.length);
|
||||
setCount(refreshCount(note));
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -624,6 +624,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
const $span = $(node.span);
|
||||
|
||||
$span.find(".tree-item-button").remove();
|
||||
$span.find(".note-indicator-icon").remove();
|
||||
|
||||
const isHoistedNote = activeNoteContext && activeNoteContext.hoistedNoteId === note.noteId && note.noteId !== "root";
|
||||
|
||||
@@ -664,6 +665,34 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
|
||||
$span.append($unhoistButton);
|
||||
}
|
||||
|
||||
// Add clone indicator with tooltip if note has multiple parents
|
||||
const parentNotes = note.getParentNotes();
|
||||
const realParents = parentNotes.filter(
|
||||
(parent) => !["_share", "_lbBookmarks"].includes(parent.noteId) && parent.type !== "search"
|
||||
);
|
||||
|
||||
if (realParents.length > 1) {
|
||||
const parentTitles = realParents.map((p) => p.title).join(", ");
|
||||
const tooltipText = realParents.length === 2
|
||||
? t("note_tree.clone-indicator-tooltip-single", { parent: realParents[1].title })
|
||||
: t("note_tree.clone-indicator-tooltip", { count: realParents.length, parents: parentTitles });
|
||||
|
||||
const $cloneIndicator = $(`<span class="note-indicator-icon clone-indicator"></span>`);
|
||||
$cloneIndicator.attr("title", tooltipText);
|
||||
$span.find(".fancytree-title").append($cloneIndicator);
|
||||
}
|
||||
|
||||
// Add shared indicator with tooltip if note is shared
|
||||
if (note.isShared()) {
|
||||
const shareId = note.getOwnedLabelValue("shareAlias") || note.noteId;
|
||||
const shareUrl = `${location.origin}${location.pathname}share/${shareId}`;
|
||||
const tooltipText = t("note_tree.shared-indicator-tooltip-with-url", { url: shareUrl });
|
||||
|
||||
const $sharedIndicator = $(`<span class="note-indicator-icon shared-indicator"></span>`);
|
||||
$sharedIndicator.attr("title", tooltipText);
|
||||
$span.find(".fancytree-title").append($sharedIndicator);
|
||||
}
|
||||
},
|
||||
// this is done to automatically lazy load all expanded notes after tree load
|
||||
loadChildren: (event, data) => {
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
*/
|
||||
|
||||
import { NoteType } from "@triliumnext/commons";
|
||||
import { VNode, type JSX } from "preact";
|
||||
import { type JSX, VNode } from "preact";
|
||||
|
||||
import { TypeWidgetProps } from "./type_widgets/type_widget";
|
||||
|
||||
/**
|
||||
@@ -13,7 +14,7 @@ import { TypeWidgetProps } from "./type_widgets/type_widget";
|
||||
*/
|
||||
export type ExtendedNoteType = Exclude<NoteType, "launcher" | "text" | "code"> | "empty" | "readOnlyCode" | "readOnlyText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession" | "aiChat";
|
||||
|
||||
export type TypeWidget = ((props: TypeWidgetProps) => VNode | JSX.Element);
|
||||
export type TypeWidget = ((props: TypeWidgetProps) => VNode | JSX.Element | undefined);
|
||||
type NoteTypeView = () => (Promise<{ default: TypeWidget } | TypeWidget> | TypeWidget);
|
||||
|
||||
interface NoteTypeMapping {
|
||||
|
||||
@@ -64,7 +64,8 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
|
||||
this.$widget.addClass(utils.getMimeTypeClass(note.mime));
|
||||
this.$widget.addClass(`view-mode-${this.noteContext?.viewScope?.viewMode ?? "default"}`);
|
||||
this.$widget.addClass(note.getColorClass());
|
||||
this.$widget.toggleClass(["bgfx", "options"], note.isOptions());
|
||||
this.$widget.toggleClass("options", note.isOptions());
|
||||
this.$widget.toggleClass("bgfx", this.#hasBackgroundEffects(note));
|
||||
this.$widget.toggleClass("protected", note.isProtected);
|
||||
|
||||
const noteLanguage = note?.getLabelValue("language");
|
||||
@@ -88,6 +89,22 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
|
||||
return !!note?.isLabelTruthy("fullContentWidth");
|
||||
}
|
||||
|
||||
#hasBackgroundEffects(note: FNote): boolean {
|
||||
const MIME_TYPES_WITH_BACKGROUND_EFFECTS = [
|
||||
"application/pdf"
|
||||
]
|
||||
|
||||
if (note.isOptions()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (note.type === "file" && MIME_TYPES_WITH_BACKGROUND_EFFECTS.includes(note.mime)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
// listening on changes of note.type and CSS class
|
||||
const LABELS_CAUSING_REFRESH = ["cssClass", "language", "viewType", "color"];
|
||||
|
||||
@@ -8,7 +8,7 @@ import { MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayou
|
||||
|
||||
import appContext, { EventData, EventNames } from "../../components/app_context";
|
||||
import Component from "../../components/component";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import NoteContext, { NoteContextDataMap } from "../../components/note_context";
|
||||
import FBlob from "../../entities/fblob";
|
||||
import FNote from "../../entities/fnote";
|
||||
import attributes from "../../services/attributes";
|
||||
@@ -19,7 +19,7 @@ import options, { type OptionValue } from "../../services/options";
|
||||
import protected_session_holder from "../../services/protected_session_holder";
|
||||
import server from "../../services/server";
|
||||
import shortcuts, { Handler, removeIndividualBinding } from "../../services/shortcuts";
|
||||
import SpacedUpdate from "../../services/spaced_update";
|
||||
import SpacedUpdate, { type StateCallback } from "../../services/spaced_update";
|
||||
import toast, { ToastOptions } from "../../services/toast";
|
||||
import tree from "../../services/tree";
|
||||
import utils, { escapeRegExp, getErrorMessage, randomString, reloadFrontendApp } from "../../services/utils";
|
||||
@@ -63,22 +63,29 @@ export function useTriliumEvents<T extends EventNames>(eventNames: T[], handler:
|
||||
useDebugValue(() => eventNames.join(", "));
|
||||
}
|
||||
|
||||
export function useSpacedUpdate(callback: () => void | Promise<void>, interval = 1000) {
|
||||
export function useSpacedUpdate(callback: () => void | Promise<void>, interval = 1000, stateCallback?: StateCallback) {
|
||||
const callbackRef = useRef(callback);
|
||||
const stateCallbackRef = useRef(stateCallback);
|
||||
const spacedUpdateRef = useRef<SpacedUpdate>(new SpacedUpdate(
|
||||
() => callbackRef.current(),
|
||||
interval
|
||||
interval,
|
||||
(state) => stateCallbackRef.current?.(state)
|
||||
));
|
||||
|
||||
// Update callback ref when it changes
|
||||
useEffect(() => {
|
||||
callbackRef.current = callback;
|
||||
}, [callback]);
|
||||
}, [ callback ]);
|
||||
|
||||
// Update state callback when it changes.
|
||||
useEffect(() => {
|
||||
stateCallbackRef.current = stateCallback;
|
||||
}, [ stateCallback ]);
|
||||
|
||||
// Update interval if it changes
|
||||
useEffect(() => {
|
||||
spacedUpdateRef.current?.setUpdateInterval(interval);
|
||||
}, [interval]);
|
||||
}, [ interval ]);
|
||||
|
||||
return spacedUpdateRef.current;
|
||||
}
|
||||
@@ -121,7 +128,12 @@ export function useEditorSpacedUpdate({ note, noteType, noteContext, getData, on
|
||||
dataSaved?.(data);
|
||||
};
|
||||
}, [ note, getData, dataSaved, noteType, parentComponent ]);
|
||||
const spacedUpdate = useSpacedUpdate(callback);
|
||||
const stateCallback = useCallback<StateCallback>((state) => {
|
||||
noteContext?.setContextData("saveState", {
|
||||
state
|
||||
});
|
||||
}, [ noteContext ]);
|
||||
const spacedUpdate = useSpacedUpdate(callback, updateInterval, stateCallback);
|
||||
|
||||
// React to note/blob changes.
|
||||
useEffect(() => {
|
||||
@@ -158,6 +170,73 @@ export function useEditorSpacedUpdate({ note, noteType, noteContext, getData, on
|
||||
return spacedUpdate;
|
||||
}
|
||||
|
||||
export function useBlobEditorSpacedUpdate({ note, noteType, noteContext, getData, onContentChange, dataSaved, updateInterval, replaceWithoutRevision }: {
|
||||
noteType: NoteType;
|
||||
note: FNote,
|
||||
noteContext: NoteContext | null | undefined,
|
||||
getData: () => Promise<Blob | undefined> | Blob | undefined,
|
||||
onContentChange: (newBlob: FBlob) => void,
|
||||
dataSaved?: (savedData: Blob) => void,
|
||||
updateInterval?: number;
|
||||
/** If set to true, then the blob is replaced directly without saving a revision before. */
|
||||
replaceWithoutRevision?: boolean;
|
||||
}) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const blob = useNoteBlob(note, parentComponent?.componentId);
|
||||
|
||||
const callback = useMemo(() => {
|
||||
return async () => {
|
||||
const data = await getData();
|
||||
|
||||
// for read only notes
|
||||
if (data === undefined || note.type !== noteType) return;
|
||||
|
||||
protected_session_holder.touchProtectedSessionIfNecessary(note);
|
||||
await server.upload(`notes/${note.noteId}/file?replace=${replaceWithoutRevision ? "1" : "0"}`, new File([ data ], note.title, { type: note.mime }), parentComponent?.componentId);
|
||||
dataSaved?.(data);
|
||||
};
|
||||
}, [ note, getData, dataSaved, noteType, parentComponent, replaceWithoutRevision ]);
|
||||
const stateCallback = useCallback<StateCallback>((state) => {
|
||||
noteContext?.setContextData("saveState", {
|
||||
state
|
||||
});
|
||||
}, [ noteContext ]);
|
||||
const spacedUpdate = useSpacedUpdate(callback, updateInterval, stateCallback);
|
||||
|
||||
// React to note/blob changes.
|
||||
useEffect(() => {
|
||||
if (!blob) return;
|
||||
spacedUpdate.allowUpdateWithoutChange(() => onContentChange(blob));
|
||||
}, [ blob ]);
|
||||
|
||||
// React to update interval changes.
|
||||
useEffect(() => {
|
||||
if (!updateInterval) return;
|
||||
spacedUpdate.setUpdateInterval(updateInterval);
|
||||
}, [ updateInterval ]);
|
||||
|
||||
// Save if needed upon switching tabs.
|
||||
useTriliumEvent("beforeNoteSwitch", async ({ noteContext: eventNoteContext }) => {
|
||||
if (eventNoteContext.ntxId !== noteContext?.ntxId) return;
|
||||
await spacedUpdate.updateNowIfNecessary();
|
||||
});
|
||||
|
||||
// Save if needed upon tab closing.
|
||||
useTriliumEvent("beforeNoteContextRemove", async ({ ntxIds }) => {
|
||||
if (!noteContext?.ntxId || !ntxIds.includes(noteContext.ntxId)) return;
|
||||
await spacedUpdate.updateNowIfNecessary();
|
||||
});
|
||||
|
||||
// Save if needed upon window/browser closing.
|
||||
useEffect(() => {
|
||||
const listener = () => spacedUpdate.isAllSavedAndTriggerUpdate();
|
||||
appContext.addBeforeUnloadListener(listener);
|
||||
return () => appContext.removeBeforeUnloadListener(listener);
|
||||
}, []);
|
||||
|
||||
return spacedUpdate;
|
||||
}
|
||||
|
||||
export function useNoteSavedData(noteId: string | undefined) {
|
||||
return useSyncExternalStore(
|
||||
(cb) => noteId ? noteSavedDataStore.subscribe(noteId, cb) : () => {},
|
||||
@@ -634,7 +713,8 @@ export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
|
||||
// Render the widget once.
|
||||
// Render the widget once - note that noteContext is intentionally NOT a dependency
|
||||
// to prevent creating new widget instances on every note switch.
|
||||
const [ widget, renderedWidget ] = useMemo(() => {
|
||||
const widget = widgetFactory();
|
||||
|
||||
@@ -642,14 +722,21 @@ export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, {
|
||||
parentComponent.child(widget);
|
||||
}
|
||||
|
||||
if (noteContext && widget instanceof NoteContextAwareWidget) {
|
||||
widget.setNoteContextEvent({ noteContext });
|
||||
}
|
||||
|
||||
const renderedWidget = widget.render();
|
||||
return [ widget, renderedWidget ];
|
||||
}, [ noteContext, parentComponent ]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
// widgetFactory() is intentionally left out
|
||||
}, [ parentComponent ]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
// widgetFactory() and noteContext are intentionally left out - widget should be created once
|
||||
// and updated via activeContextChangedEvent when noteContext changes.
|
||||
|
||||
// Cleanup: remove widget from parent's children when unmounted
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (parentComponent) {
|
||||
parentComponent.removeChild(widget);
|
||||
}
|
||||
widget.cleanup();
|
||||
};
|
||||
}, [ parentComponent, widget ]);
|
||||
|
||||
// Attach the widget to the parent.
|
||||
useEffect(() => {
|
||||
@@ -660,10 +747,17 @@ export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, {
|
||||
}
|
||||
}, [ renderedWidget ]);
|
||||
|
||||
// Inject the note context.
|
||||
// Inject the note context - this updates the existing widget without recreating it.
|
||||
// We check if the context actually changed to avoid double refresh when the event system
|
||||
// also delivers activeContextChanged to the widget through component tree propagation.
|
||||
useEffect(() => {
|
||||
if (noteContext && widget instanceof NoteContextAwareWidget) {
|
||||
widget.activeContextChangedEvent({ noteContext });
|
||||
// Only trigger refresh if the context actually changed.
|
||||
// The event system may have already updated the widget, in which case
|
||||
// widget.noteContext will already equal noteContext.
|
||||
if (widget.noteContext !== noteContext) {
|
||||
widget.activeContextChangedEvent({ noteContext });
|
||||
}
|
||||
}
|
||||
}, [ noteContext, widget ]);
|
||||
|
||||
@@ -1192,3 +1286,92 @@ export function useContentElement(noteContext: NoteContext | null | undefined) {
|
||||
|
||||
return contentElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set context data on the current note context.
|
||||
* This allows type widgets to publish data (e.g., table of contents, PDF pages)
|
||||
* that can be consumed by sidebar/toolbar components.
|
||||
*
|
||||
* Data is automatically cleared when navigating to a different note.
|
||||
*
|
||||
* @param key - Unique identifier for the data type (e.g., "toc", "pdfPages")
|
||||
* @param value - The data to publish
|
||||
*
|
||||
* @example
|
||||
* // In a PDF viewer widget:
|
||||
* const { noteContext } = useActiveNoteContext();
|
||||
* useSetContextData(noteContext, "pdfPages", pages);
|
||||
*/
|
||||
export function useSetContextData<K extends keyof NoteContextDataMap>(
|
||||
noteContext: NoteContext | null | undefined,
|
||||
key: K,
|
||||
value: NoteContextDataMap[K] | undefined
|
||||
) {
|
||||
useEffect(() => {
|
||||
if (!noteContext) return;
|
||||
|
||||
if (value !== undefined) {
|
||||
noteContext.setContextData(key, value);
|
||||
} else {
|
||||
noteContext.clearContextData(key);
|
||||
}
|
||||
|
||||
return () => {
|
||||
noteContext.clearContextData(key);
|
||||
};
|
||||
}, [noteContext, key, value]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context data from the active note context.
|
||||
* This is typically used in sidebar/toolbar components that need to display
|
||||
* data published by type widgets.
|
||||
*
|
||||
* The component will automatically re-render when the data changes.
|
||||
*
|
||||
* @param key - The data key to retrieve (e.g., "toc", "pdfPages")
|
||||
* @returns The current data, or undefined if not available
|
||||
*
|
||||
* @example
|
||||
* // In a Table of Contents sidebar widget:
|
||||
* function TableOfContents() {
|
||||
* const headings = useGetContextData<Heading[]>("toc");
|
||||
* if (!headings) return <div>No headings available</div>;
|
||||
* return <ul>{headings.map(h => <li>{h.text}</li>)}</ul>;
|
||||
* }
|
||||
*/
|
||||
export function useGetContextData<K extends keyof NoteContextDataMap>(key: K): NoteContextDataMap[K] | undefined {
|
||||
const { noteContext } = useActiveNoteContext();
|
||||
return useGetContextDataFrom(noteContext, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context data from a specific note context (not necessarily the active one).
|
||||
*
|
||||
* @param noteContext - The specific note context to get data from
|
||||
* @param key - The data key to retrieve
|
||||
* @returns The current data, or undefined if not available
|
||||
*/
|
||||
export function useGetContextDataFrom<K extends keyof NoteContextDataMap>(
|
||||
noteContext: NoteContext | null | undefined,
|
||||
key: K
|
||||
): NoteContextDataMap[K] | undefined {
|
||||
const [data, setData] = useState<NoteContextDataMap[K] | undefined>(() =>
|
||||
noteContext?.getContextData(key)
|
||||
);
|
||||
|
||||
// Update initial value when noteContext changes
|
||||
useEffect(() => {
|
||||
setData(noteContext?.getContextData(key));
|
||||
}, [noteContext, key]);
|
||||
|
||||
// Subscribe to changes via Trilium event system
|
||||
useTriliumEvent("contextDataChanged", ({ noteContext: eventNoteContext, key: changedKey, value }) => {
|
||||
if (eventNoteContext === noteContext && changedKey === key) {
|
||||
setData(value as NoteContextDataMap[K]);
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import { t } from "../../services/i18n";
|
||||
import { downloadFileNote, openNoteExternally } from "../../services/open";
|
||||
@@ -8,11 +10,14 @@ import { formatSize } from "../../services/utils";
|
||||
import Button from "../react/Button";
|
||||
import { FormFileUploadButton } from "../react/FormFileUpload";
|
||||
import { useNoteBlob, useNoteLabel } from "../react/hooks";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
|
||||
export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
|
||||
export default function FilePropertiesTab({ note, ntxId }: Pick<TabContext, "note" | "ntxId">) {
|
||||
const [ originalFileName ] = useNoteLabel(note, "originalFileName");
|
||||
const canAccessProtectedNote = !note?.isProtected || protected_session_holder.isProtectedSessionAvailable();
|
||||
const blob = useNoteBlob(note);
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
|
||||
return (
|
||||
<div className="file-properties-widget">
|
||||
@@ -40,7 +45,7 @@ export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
|
||||
text={t("file_properties.download")}
|
||||
primary
|
||||
disabled={!canAccessProtectedNote}
|
||||
onClick={() => downloadFileNote(note.noteId)}
|
||||
onClick={() => downloadFileNote(note, parentComponent, ntxId)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function ImagePropertiesTab({ note, ntxId }: TabContext) {
|
||||
text={t("image_properties.download")}
|
||||
icon="bx bx-download"
|
||||
primary
|
||||
onClick={() => downloadFileNote(note.noteId)}
|
||||
onClick={() => downloadFileNote(note, parentComponent, ntxId)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -135,13 +135,13 @@ function OpenExternallyButton({ note, noteMime }: NoteActionsCustomInnerProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function DownloadFileButton({ note }: NoteActionsCustomInnerProps) {
|
||||
function DownloadFileButton({ note, parentComponent, ntxId }: NoteActionsCustomInnerProps) {
|
||||
return (
|
||||
<ActionButton
|
||||
icon="bx bx-download"
|
||||
text={t("file_properties.download")}
|
||||
disabled={!note.isContentAvailable()}
|
||||
onClick={() => downloadFileNote(note.noteId)}
|
||||
onClick={() => downloadFileNote(note, parentComponent, ntxId)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import "./RightPanelContainer.css";
|
||||
|
||||
import Split from "@triliumnext/split.js";
|
||||
import { VNode } from "preact";
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
import { useState, useEffect, useRef, useCallback } from "preact/hooks";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import { WidgetsByParent } from "../../services/bundle";
|
||||
@@ -11,10 +11,13 @@ import { t } from "../../services/i18n";
|
||||
import options from "../../services/options";
|
||||
import { DEFAULT_GUTTER_SIZE } from "../../services/resizer";
|
||||
import Button from "../react/Button";
|
||||
import { useActiveNoteContext, useLegacyWidget, useNoteProperty, useTriliumEvent, useTriliumOptionBool, useTriliumOptionJson } from "../react/hooks";
|
||||
import { useActiveNoteContext, useLegacyWidget, useNoteProperty, useTriliumEvent, useTriliumOptionJson } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import LegacyRightPanelWidget from "../right_panel_widget";
|
||||
import HighlightsList from "./HighlightsList";
|
||||
import PdfAttachments from "./pdf/PdfAttachments";
|
||||
import PdfLayers from "./pdf/PdfLayers";
|
||||
import PdfPages from "./pdf/PdfPages";
|
||||
import RightPanelWidget from "./RightPanelWidget";
|
||||
import TableOfContents from "./TableOfContents";
|
||||
|
||||
@@ -27,12 +30,16 @@ interface RightPanelWidgetDefinition {
|
||||
}
|
||||
|
||||
export default function RightPanelContainer({ widgetsByParent }: { widgetsByParent: WidgetsByParent }) {
|
||||
const [ rightPaneVisible, setRightPaneVisible ] = useTriliumOptionBool("rightPaneVisible");
|
||||
const [ rightPaneVisible, setRightPaneVisible ] = useState(options.is("rightPaneVisible"));
|
||||
const items = useItems(rightPaneVisible, widgetsByParent);
|
||||
useSplit(rightPaneVisible);
|
||||
useTriliumEvent("toggleRightPane", () => {
|
||||
setRightPaneVisible(!rightPaneVisible);
|
||||
});
|
||||
useTriliumEvent("toggleRightPane", useCallback(() => {
|
||||
setRightPaneVisible(current => {
|
||||
const newValue = !current;
|
||||
options.save("rightPaneVisible", newValue.toString());
|
||||
return newValue;
|
||||
});
|
||||
}, []));
|
||||
|
||||
return (
|
||||
<div id="right-pane">
|
||||
@@ -45,7 +52,7 @@ export default function RightPanelContainer({ widgetsByParent }: { widgetsByPare
|
||||
{t("right_pane.empty_message")}
|
||||
<Button
|
||||
text={t("right_pane.empty_button")}
|
||||
onClick={() => setRightPaneVisible(!rightPaneVisible)}
|
||||
triggerCommand="toggleRightPane"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -57,13 +64,27 @@ export default function RightPanelContainer({ widgetsByParent }: { widgetsByPare
|
||||
function useItems(rightPaneVisible: boolean, widgetsByParent: WidgetsByParent) {
|
||||
const { note } = useActiveNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const noteMime = useNoteProperty(note, "mime");
|
||||
const [ highlightsList ] = useTriliumOptionJson<string[]>("highlightsList");
|
||||
const isPdf = noteType === "file" && noteMime === "application/pdf";
|
||||
|
||||
if (!rightPaneVisible) return [];
|
||||
const definitions: RightPanelWidgetDefinition[] = [
|
||||
{
|
||||
el: <TableOfContents />,
|
||||
enabled: (noteType === "text" || noteType === "doc"),
|
||||
enabled: (noteType === "text" || noteType === "doc" || isPdf),
|
||||
},
|
||||
{
|
||||
el: <PdfPages />,
|
||||
enabled: isPdf,
|
||||
},
|
||||
{
|
||||
el: <PdfAttachments />,
|
||||
enabled: isPdf,
|
||||
},
|
||||
{
|
||||
el: <PdfLayers />,
|
||||
enabled: isPdf,
|
||||
},
|
||||
{
|
||||
el: <HighlightsList />,
|
||||
|
||||
@@ -29,6 +29,11 @@
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.toc li.active > .item-content {
|
||||
font-weight: bold;
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.toc > ol {
|
||||
--toc-depth-level: 1;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useCallback, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { t } from "../../services/i18n";
|
||||
import { randomString } from "../../services/utils";
|
||||
import { useActiveNoteContext, useContentElement, useIsNoteReadOnly, useNoteProperty, useTextEditor } from "../react/hooks";
|
||||
import { useActiveNoteContext, useContentElement, useGetContextData, useIsNoteReadOnly, useNoteProperty, useTextEditor } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import RightPanelWidget from "./RightPanelWidget";
|
||||
|
||||
@@ -21,29 +21,50 @@ interface HeadingsWithNesting extends RawHeading {
|
||||
children: HeadingsWithNesting[];
|
||||
}
|
||||
|
||||
export interface HeadingContext {
|
||||
scrollToHeading(heading: RawHeading): void;
|
||||
headings: RawHeading[];
|
||||
activeHeadingId?: string | null;
|
||||
}
|
||||
|
||||
export default function TableOfContents() {
|
||||
const { note, noteContext } = useActiveNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const noteMime = useNoteProperty(note, "mime");
|
||||
const { isReadOnly } = useIsNoteReadOnly(note, noteContext);
|
||||
|
||||
return (
|
||||
<RightPanelWidget id="toc" title={t("toc.table_of_contents")} grow>
|
||||
{((noteType === "text" && isReadOnly) || (noteType === "doc")) && <ReadOnlyTextTableOfContents />}
|
||||
{noteType === "text" && !isReadOnly && <EditableTextTableOfContents />}
|
||||
{noteType === "file" && noteMime === "application/pdf" && <PdfTableOfContents />}
|
||||
</RightPanelWidget>
|
||||
);
|
||||
}
|
||||
|
||||
function AbstractTableOfContents<T extends RawHeading>({ headings, scrollToHeading }: {
|
||||
function PdfTableOfContents() {
|
||||
const data = useGetContextData("toc");
|
||||
|
||||
return (
|
||||
<AbstractTableOfContents
|
||||
headings={data?.headings || []}
|
||||
scrollToHeading={data?.scrollToHeading || (() => {})}
|
||||
activeHeadingId={data?.activeHeadingId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AbstractTableOfContents<T extends RawHeading>({ headings, scrollToHeading, activeHeadingId }: {
|
||||
headings: T[];
|
||||
scrollToHeading(heading: T): void;
|
||||
activeHeadingId?: string | null;
|
||||
}) {
|
||||
const nestedHeadings = buildHeadingTree(headings);
|
||||
return (
|
||||
<span className="toc">
|
||||
{nestedHeadings.length > 0 ? (
|
||||
<ol>
|
||||
{nestedHeadings.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} />)}
|
||||
{nestedHeadings.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} activeHeadingId={activeHeadingId} />)}
|
||||
</ol>
|
||||
) : (
|
||||
<div className="no-headings">{t("toc.no_headings")}</div>
|
||||
@@ -52,14 +73,16 @@ function AbstractTableOfContents<T extends RawHeading>({ headings, scrollToHeadi
|
||||
);
|
||||
}
|
||||
|
||||
function TableOfContentsHeading({ heading, scrollToHeading }: {
|
||||
function TableOfContentsHeading({ heading, scrollToHeading, activeHeadingId }: {
|
||||
heading: HeadingsWithNesting;
|
||||
scrollToHeading(heading: RawHeading): void;
|
||||
activeHeadingId?: string | null;
|
||||
}) {
|
||||
const [ collapsed, setCollapsed ] = useState(false);
|
||||
const isActive = heading.id === activeHeadingId;
|
||||
return (
|
||||
<>
|
||||
<li className={clsx(collapsed && "collapsed")}>
|
||||
<li className={clsx(collapsed && "collapsed", isActive && "active")}>
|
||||
{heading.children.length > 0 && (
|
||||
<Icon
|
||||
className="collapse-button"
|
||||
@@ -74,7 +97,7 @@ function TableOfContentsHeading({ heading, scrollToHeading }: {
|
||||
</li>
|
||||
{heading.children && (
|
||||
<ol>
|
||||
{heading.children.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} />)}
|
||||
{heading.children.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} activeHeadingId={activeHeadingId} />)}
|
||||
</ol>
|
||||
)}
|
||||
</>
|
||||
|
||||
57
apps/client/src/widgets/sidebar/pdf/PdfAttachments.css
Normal file
57
apps/client/src/widgets/sidebar/pdf/PdfAttachments.css
Normal file
@@ -0,0 +1,57 @@
|
||||
.pdf-attachments-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pdf-attachment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.pdf-attachment-item:hover {
|
||||
background-color: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
.pdf-attachment-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.pdf-attachment-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pdf-attachment-filename {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--main-text-color);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdf-attachment-size {
|
||||
font-size: 11px;
|
||||
color: var(--muted-text-color);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.no-attachments {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.pdf-attachment-item .bx {
|
||||
flex-shrink: 0;
|
||||
font-size: 18px;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.pdf-attachment-item:hover .bx {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
62
apps/client/src/widgets/sidebar/pdf/PdfAttachments.tsx
Normal file
62
apps/client/src/widgets/sidebar/pdf/PdfAttachments.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import "./PdfAttachments.css";
|
||||
|
||||
import { t } from "../../../services/i18n";
|
||||
import { formatSize } from "../../../services/utils";
|
||||
import { useActiveNoteContext, useGetContextData, useNoteProperty } from "../../react/hooks";
|
||||
import Icon from "../../react/Icon";
|
||||
import RightPanelWidget from "../RightPanelWidget";
|
||||
|
||||
interface AttachmentInfo {
|
||||
filename: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export default function PdfAttachments() {
|
||||
const { note } = useActiveNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const noteMime = useNoteProperty(note, "mime");
|
||||
const attachmentsData = useGetContextData("pdfAttachments");
|
||||
|
||||
if (noteType !== "file" || noteMime !== "application/pdf") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!attachmentsData || attachmentsData.attachments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RightPanelWidget id="pdf-attachments" title={t("pdf.attachments", { count: attachmentsData.attachments.length })}>
|
||||
<div className="pdf-attachments-list">
|
||||
{attachmentsData.attachments.map((attachment) => (
|
||||
<PdfAttachmentItem
|
||||
key={attachment.filename}
|
||||
attachment={attachment}
|
||||
onDownload={attachmentsData.downloadAttachment}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</RightPanelWidget>
|
||||
);
|
||||
}
|
||||
|
||||
function PdfAttachmentItem({
|
||||
attachment,
|
||||
onDownload
|
||||
}: {
|
||||
attachment: AttachmentInfo;
|
||||
onDownload: (filename: string) => void;
|
||||
}) {
|
||||
const sizeText = formatSize(attachment.size);
|
||||
|
||||
return (
|
||||
<div className="pdf-attachment-item" onClick={() => onDownload(attachment.filename)}>
|
||||
<Icon icon="bx bx-paperclip" />
|
||||
<div className="pdf-attachment-info">
|
||||
<div className="pdf-attachment-filename">{attachment.filename}</div>
|
||||
<div className="pdf-attachment-size">{sizeText}</div>
|
||||
</div>
|
||||
<Icon icon="bx bx-download" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
54
apps/client/src/widgets/sidebar/pdf/PdfLayers.css
Normal file
54
apps/client/src/widgets/sidebar/pdf/PdfLayers.css
Normal file
@@ -0,0 +1,54 @@
|
||||
.pdf-layers-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pdf-layer-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.pdf-layer-item:hover {
|
||||
background-color: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
.pdf-layer-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.pdf-layer-item.hidden {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.pdf-layer-name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: var(--main-text-color);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.no-layers {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.pdf-layer-item .bx {
|
||||
flex-shrink: 0;
|
||||
font-size: 18px;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.pdf-layer-item:hover .bx {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.pdf-layer-item.visible .bx {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
55
apps/client/src/widgets/sidebar/pdf/PdfLayers.tsx
Normal file
55
apps/client/src/widgets/sidebar/pdf/PdfLayers.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import "./PdfLayers.css";
|
||||
|
||||
import { t } from "../../../services/i18n";
|
||||
import { useActiveNoteContext, useGetContextData, useNoteProperty } from "../../react/hooks";
|
||||
import Icon from "../../react/Icon";
|
||||
import RightPanelWidget from "../RightPanelWidget";
|
||||
|
||||
interface LayerInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export default function PdfLayers() {
|
||||
const { note } = useActiveNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const noteMime = useNoteProperty(note, "mime");
|
||||
const layersData = useGetContextData("pdfLayers");
|
||||
|
||||
if (noteType !== "file" || noteMime !== "application/pdf") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (layersData?.layers && layersData.layers.length > 0 &&
|
||||
<RightPanelWidget id="pdf-layers" title={t("pdf.layers", { count: layersData.layers.length })}>
|
||||
<div className="pdf-layers-list">
|
||||
{layersData.layers.map((layer) => (
|
||||
<PdfLayerItem
|
||||
key={layer.id}
|
||||
layer={layer}
|
||||
onToggle={layersData.toggleLayer}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</RightPanelWidget>
|
||||
);
|
||||
}
|
||||
|
||||
function PdfLayerItem({
|
||||
layer,
|
||||
onToggle
|
||||
}: {
|
||||
layer: LayerInfo;
|
||||
onToggle: (layerId: string, visible: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`pdf-layer-item ${layer.visible ? 'visible' : 'hidden'}`}
|
||||
onClick={() => onToggle(layer.id, !layer.visible)}
|
||||
>
|
||||
<Icon icon={layer.visible ? "bx bx-show" : "bx bx-hide"} />
|
||||
<div className="pdf-layer-name">{layer.name}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
67
apps/client/src/widgets/sidebar/pdf/PdfPages.css
Normal file
67
apps/client/src/widgets/sidebar/pdf/PdfPages.css
Normal file
@@ -0,0 +1,67 @@
|
||||
.pdf-pages-list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
.pdf-page-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.2s;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
|
||||
.pdf-page-number {
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
color: var(--main-text-color);
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: var(--accented-background-color);
|
||||
padding: 0.2em 0.5em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.pdf-page-item:hover {
|
||||
background-color: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
.pdf-page-item.active {
|
||||
border-color: var(--main-border-color);
|
||||
background-color: var(--active-item-background-color);
|
||||
}
|
||||
|
||||
.pdf-page-thumbnail {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pdf-page-thumbnail img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.pdf-page-loading {
|
||||
color: var(--muted-text-color);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.no-pages {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
111
apps/client/src/widgets/sidebar/pdf/PdfPages.tsx
Normal file
111
apps/client/src/widgets/sidebar/pdf/PdfPages.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import "./PdfPages.css";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { NoteContextDataMap } from "../../../components/note_context";
|
||||
import { t } from "../../../services/i18n";
|
||||
import { useActiveNoteContext, useGetContextData, useNoteProperty } from "../../react/hooks";
|
||||
import RightPanelWidget from "../RightPanelWidget";
|
||||
|
||||
export default function PdfPages() {
|
||||
const { note } = useActiveNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const noteMime = useNoteProperty(note, "mime");
|
||||
const pagesData = useGetContextData("pdfPages");
|
||||
|
||||
if (noteType !== "file" || noteMime !== "application/pdf") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (pagesData &&
|
||||
<RightPanelWidget id="pdf-pages" title={t("pdf.pages", { count: pagesData?.totalPages || 0 })} grow>
|
||||
<PdfPagesList key={note?.noteId} pagesData={pagesData} />
|
||||
</RightPanelWidget>
|
||||
);
|
||||
}
|
||||
|
||||
function PdfPagesList({ pagesData }: { pagesData: NoteContextDataMap["pdfPages"] }) {
|
||||
const [thumbnails, setThumbnails] = useState<Map<number, string>>(new Map());
|
||||
const requestedThumbnails = useRef<Set<number>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for thumbnail responses via custom event
|
||||
function handleThumbnail(event: CustomEvent) {
|
||||
const { pageNumber, dataUrl } = event.detail;
|
||||
setThumbnails(prev => new Map(prev).set(pageNumber, dataUrl));
|
||||
}
|
||||
|
||||
window.addEventListener("pdf-thumbnail", handleThumbnail as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener("pdf-thumbnail", handleThumbnail as EventListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const requestThumbnail = useCallback((pageNumber: number) => {
|
||||
// Only request if we haven't already requested it and don't have it
|
||||
if (!requestedThumbnails.current.has(pageNumber) && !thumbnails.has(pageNumber) && pagesData) {
|
||||
requestedThumbnails.current.add(pageNumber);
|
||||
pagesData.requestThumbnail(pageNumber);
|
||||
}
|
||||
}, [pagesData, thumbnails]);
|
||||
|
||||
if (!pagesData || pagesData.totalPages === 0) {
|
||||
return <div className="no-pages">No pages available</div>;
|
||||
}
|
||||
|
||||
const pages = Array.from({ length: pagesData.totalPages }, (_, i) => i + 1);
|
||||
|
||||
return (
|
||||
<div className="pdf-pages-list">
|
||||
{pages.map(pageNumber => (
|
||||
<PdfPageItem
|
||||
key={pageNumber}
|
||||
pageNumber={pageNumber}
|
||||
isActive={pageNumber === pagesData.currentPage}
|
||||
thumbnail={thumbnails.get(pageNumber)}
|
||||
onRequestThumbnail={requestThumbnail}
|
||||
onPageClick={() => pagesData.scrollToPage(pageNumber)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PdfPageItem({
|
||||
pageNumber,
|
||||
isActive,
|
||||
thumbnail,
|
||||
onRequestThumbnail,
|
||||
onPageClick
|
||||
}: {
|
||||
pageNumber: number;
|
||||
isActive: boolean;
|
||||
thumbnail?: string;
|
||||
onRequestThumbnail(page: number): void;
|
||||
onPageClick(): void;
|
||||
}) {
|
||||
const hasRequested = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!thumbnail && !hasRequested.current) {
|
||||
hasRequested.current = true;
|
||||
onRequestThumbnail(pageNumber);
|
||||
}
|
||||
}, [pageNumber, thumbnail, onRequestThumbnail]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`pdf-page-item ${isActive ? 'active' : ''}`}
|
||||
onClick={onPageClick}
|
||||
>
|
||||
<div className="pdf-page-number">{pageNumber}</div>
|
||||
<div className="pdf-page-thumbnail">
|
||||
{thumbnail ? (
|
||||
<img src={thumbnail} alt={t("pdf.pages_alt", { pageNumber })} />
|
||||
) : (
|
||||
<div className="pdf-page-loading">{t("pdf.pages_loading")}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export default function SqlResults() {
|
||||
{t("sql_result.no_rows")}
|
||||
</Alert>
|
||||
) : (
|
||||
<div class="sql-console-result-container">
|
||||
<div className="sql-console-result-container selectable-text">
|
||||
{results?.map(rows => {
|
||||
// inserts, updates
|
||||
if (typeof rows === "object" && !Array.isArray(rows)) {
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
import { useNoteBlob } from "../react/hooks";
|
||||
import "./File.css";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import { t } from "../../services/i18n";
|
||||
import { getUrlForDownload } from "../../services/open";
|
||||
import Alert from "../react/Alert";
|
||||
import { t } from "../../services/i18n";
|
||||
import { useNoteBlob } from "../react/hooks";
|
||||
import PdfPreview from "./file/Pdf";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
|
||||
const TEXT_MAX_NUM_CHARS = 5000;
|
||||
|
||||
export default function File({ note }: TypeWidgetProps) {
|
||||
const blob = useNoteBlob(note);
|
||||
export default function FileTypeWidget({ note, parentComponent, noteContext }: TypeWidgetProps) {
|
||||
const blob = useNoteBlob(note, parentComponent?.componentId);
|
||||
|
||||
if (blob?.content) {
|
||||
return <TextPreview content={blob.content} />
|
||||
return <TextPreview content={blob.content} />;
|
||||
} else if (note.mime === "application/pdf") {
|
||||
return <PdfPreview note={note} />
|
||||
return noteContext && <PdfPreview blob={blob} note={note} componentId={parentComponent?.componentId} noteContext={noteContext} />;
|
||||
} else if (note.mime.startsWith("video/")) {
|
||||
return <VideoPreview note={note} />
|
||||
return <VideoPreview note={note} />;
|
||||
} else if (note.mime.startsWith("audio/")) {
|
||||
return <AudioPreview note={note} />
|
||||
} else {
|
||||
return <NoPreview />
|
||||
return <AudioPreview note={note} />;
|
||||
}
|
||||
return <NoPreview />;
|
||||
|
||||
}
|
||||
|
||||
function TextPreview({ content }: { content: string }) {
|
||||
@@ -37,14 +39,6 @@ function TextPreview({ content }: { content: string }) {
|
||||
)}
|
||||
<pre class="file-preview-content">{trimmedContent}</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function PdfPreview({ note }: { note: FNote }) {
|
||||
return (
|
||||
<iframe
|
||||
class="pdf-preview"
|
||||
src={getUrlForDownload(`api/notes/${note.noteId}/open`)} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,7 +50,7 @@ function VideoPreview({ note }: { note: FNote }) {
|
||||
datatype={note?.mime}
|
||||
controls
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AudioPreview({ note }: { note: FNote }) {
|
||||
@@ -66,7 +60,7 @@ function AudioPreview({ note }: { note: FNote }) {
|
||||
src={getUrlForDownload(`api/notes/${note.noteId}/open-partial`)}
|
||||
controls
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function NoPreview() {
|
||||
|
||||
@@ -29,6 +29,7 @@ export default function Mermaid(props: TypeWidgetProps) {
|
||||
<SvgSplitEditor
|
||||
attachmentName="mermaid-export"
|
||||
renderSvg={renderSvg}
|
||||
noteType="mermaid"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -37,6 +37,7 @@ const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Options["locale"] | null>
|
||||
it: "it",
|
||||
ja: "ja",
|
||||
pt: "pt",
|
||||
pl: null,
|
||||
pt_br: "pt",
|
||||
ro: "ro",
|
||||
ru: "ru",
|
||||
|
||||
@@ -13,6 +13,7 @@ export const LANGUAGE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Language["code"]
|
||||
it: "it-IT",
|
||||
ja: "ja-JP",
|
||||
pt: "pt-PT",
|
||||
pl: "pl-PL",
|
||||
pt_br: "pt-BR",
|
||||
ro: "ro-RO",
|
||||
ru: "ru-RU",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "./code.css";
|
||||
|
||||
import { default as VanillaCodeMirror, getThemeById } from "@triliumnext/codemirror";
|
||||
import { NoteType } from "@triliumnext/commons";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext, { CommandListenerData } from "../../../components/app_context";
|
||||
@@ -24,6 +25,7 @@ export interface EditableCodeProps extends TypeWidgetProps, Omit<CodeEditorProps
|
||||
debounceUpdate?: boolean;
|
||||
lineWrapping?: boolean;
|
||||
updateInterval?: number;
|
||||
noteType?: NoteType;
|
||||
/** Invoked when the content of the note is changed, such as a different revision or a note switch. */
|
||||
onContentChanged?: (content: string) => void;
|
||||
/** Invoked after the content of the note has been uploaded to the server, using a spaced update. */
|
||||
@@ -72,14 +74,14 @@ function formatViewSource(note: FNote, content: string) {
|
||||
return content;
|
||||
}
|
||||
|
||||
export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentComponent, updateInterval, onContentChanged, dataSaved, ...editorProps }: EditableCodeProps) {
|
||||
export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentComponent, updateInterval, noteType = "code", onContentChanged, dataSaved, ...editorProps }: EditableCodeProps) {
|
||||
const editorRef = useRef<VanillaCodeMirror>(null);
|
||||
const containerRef = useRef<HTMLPreElement>(null);
|
||||
const [ vimKeymapEnabled ] = useTriliumOptionBool("vimKeymapEnabled");
|
||||
const mime = useNoteProperty(note, "mime");
|
||||
const spacedUpdate = useEditorSpacedUpdate({
|
||||
note,
|
||||
noteType: "code",
|
||||
noteType,
|
||||
noteContext,
|
||||
getData: () => ({ content: editorRef.current?.getText() ?? "" }),
|
||||
onContentChange: (content) => {
|
||||
|
||||
234
apps/client/src/widgets/type_widgets/file/Pdf.tsx
Normal file
234
apps/client/src/widgets/type_widgets/file/Pdf.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
|
||||
import appContext from "../../../components/app_context";
|
||||
import type NoteContext from "../../../components/note_context";
|
||||
import FBlob from "../../../entities/fblob";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import { useViewModeConfig } from "../../collections/NoteList";
|
||||
import { useBlobEditorSpacedUpdate, useTriliumEvent } from "../../react/hooks";
|
||||
import PdfViewer from "./PdfViewer";
|
||||
|
||||
export default function PdfPreview({ note, blob, componentId, noteContext }: {
|
||||
note: FNote;
|
||||
noteContext: NoteContext;
|
||||
blob: FBlob | null | undefined;
|
||||
componentId: string | undefined;
|
||||
}) {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const historyConfig = useViewModeConfig<HistoryData>(note, "pdfHistory");
|
||||
|
||||
const spacedUpdate = useBlobEditorSpacedUpdate({
|
||||
note,
|
||||
noteType: "file",
|
||||
noteContext,
|
||||
getData() {
|
||||
if (!iframeRef.current?.contentWindow) return undefined;
|
||||
|
||||
return new Promise<Blob>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("Timeout while waiting for blob response"));
|
||||
}, 10_000);
|
||||
|
||||
const onMessageReceived = (event: PdfMessageEvent) => {
|
||||
if (event.data.type !== "pdfjs-viewer-blob") return;
|
||||
if (event.data.noteId !== note.noteId || event.data.ntxId !== noteContext.ntxId) return;
|
||||
const blob = new Blob([event.data.data as Uint8Array<ArrayBuffer>], { type: note.mime });
|
||||
|
||||
clearTimeout(timeout);
|
||||
window.removeEventListener("message", onMessageReceived);
|
||||
resolve(blob);
|
||||
};
|
||||
|
||||
window.addEventListener("message", onMessageReceived);
|
||||
iframeRef.current?.contentWindow?.postMessage({
|
||||
type: "trilium-request-blob",
|
||||
}, window.location.origin);
|
||||
});
|
||||
},
|
||||
onContentChange() {
|
||||
if (iframeRef.current?.contentWindow) {
|
||||
iframeRef.current.contentWindow.location.reload();
|
||||
}
|
||||
},
|
||||
replaceWithoutRevision: true
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function handleMessage(event: PdfMessageEvent) {
|
||||
if (event.data?.type === "pdfjs-viewer-document-modified") {
|
||||
if (event.data.noteId === note.noteId && event.data.ntxId === noteContext.ntxId) {
|
||||
spacedUpdate.resetUpdateTimer();
|
||||
spacedUpdate.scheduleUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
if (event.data.type === "pdfjs-viewer-save-view-history" && event.data?.data) {
|
||||
if (event.data.noteId === note.noteId && event.data.ntxId === noteContext.ntxId) {
|
||||
historyConfig?.storeFn(JSON.parse(event.data.data));
|
||||
}
|
||||
}
|
||||
|
||||
if (event.data.type === "pdfjs-viewer-toc") {
|
||||
if (event.data.data) {
|
||||
// Convert PDF outline to HeadingContext format
|
||||
const headings = convertPdfOutlineToHeadings(event.data.data);
|
||||
noteContext.setContextData("toc", {
|
||||
headings,
|
||||
activeHeadingId: null,
|
||||
scrollToHeading: (heading) => {
|
||||
iframeRef.current?.contentWindow?.postMessage({
|
||||
type: "trilium-scroll-to-heading",
|
||||
headingId: heading.id
|
||||
}, window.location.origin);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// No ToC available, use empty headings
|
||||
noteContext.setContextData("toc", {
|
||||
headings: [],
|
||||
activeHeadingId: null,
|
||||
scrollToHeading: () => {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (event.data.type === "pdfjs-viewer-active-heading") {
|
||||
const currentToc = noteContext.getContextData("toc");
|
||||
if (currentToc) {
|
||||
noteContext.setContextData("toc", {
|
||||
...currentToc,
|
||||
activeHeadingId: event.data.headingId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (event.data.type === "pdfjs-viewer-page-info") {
|
||||
noteContext.setContextData("pdfPages", {
|
||||
totalPages: event.data.totalPages,
|
||||
currentPage: event.data.currentPage,
|
||||
scrollToPage: (page: number) => {
|
||||
iframeRef.current?.contentWindow?.postMessage({
|
||||
type: "trilium-scroll-to-page",
|
||||
pageNumber: page
|
||||
}, window.location.origin);
|
||||
},
|
||||
requestThumbnail: (page: number) => {
|
||||
iframeRef.current?.contentWindow?.postMessage({
|
||||
type: "trilium-request-thumbnail",
|
||||
pageNumber: page
|
||||
}, window.location.origin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.type === "pdfjs-viewer-current-page") {
|
||||
const currentPages = noteContext.getContextData("pdfPages");
|
||||
if (currentPages) {
|
||||
noteContext.setContextData("pdfPages", {
|
||||
...currentPages,
|
||||
currentPage: event.data.currentPage
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (event.data.type === "pdfjs-viewer-thumbnail") {
|
||||
// Forward thumbnail to any listeners
|
||||
window.dispatchEvent(new CustomEvent("pdf-thumbnail", {
|
||||
detail: {
|
||||
pageNumber: event.data.pageNumber,
|
||||
dataUrl: event.data.dataUrl
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (event.data.type === "pdfjs-viewer-attachments") {
|
||||
noteContext.setContextData("pdfAttachments", {
|
||||
attachments: event.data.attachments,
|
||||
downloadAttachment: (filename: string) => {
|
||||
iframeRef.current?.contentWindow?.postMessage({
|
||||
type: "trilium-download-attachment",
|
||||
filename
|
||||
}, window.location.origin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.type === "pdfjs-viewer-layers") {
|
||||
noteContext.setContextData("pdfLayers", {
|
||||
layers: event.data.layers,
|
||||
toggleLayer: (layerId: string, visible: boolean) => {
|
||||
iframeRef.current?.contentWindow?.postMessage({
|
||||
type: "trilium-toggle-layer",
|
||||
layerId,
|
||||
visible
|
||||
}, window.location.origin);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage);
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage);
|
||||
};
|
||||
}, [ note, historyConfig, componentId, blob, noteContext ]);
|
||||
|
||||
useTriliumEvent("customDownload", ({ ntxId }) => {
|
||||
if (ntxId !== noteContext.ntxId) return;
|
||||
iframeRef.current?.contentWindow?.postMessage({
|
||||
type: "trilium-request-download"
|
||||
});
|
||||
});
|
||||
|
||||
return (historyConfig &&
|
||||
<PdfViewer
|
||||
iframeRef={iframeRef}
|
||||
tabIndex={300}
|
||||
pdfUrl={`../../api/notes/${note.noteId}/open`}
|
||||
onLoad={() => {
|
||||
const win = iframeRef.current?.contentWindow;
|
||||
if (win) {
|
||||
win.TRILIUM_VIEW_HISTORY_STORE = historyConfig.config;
|
||||
win.TRILIUM_NOTE_ID = note.noteId;
|
||||
win.TRILIUM_NTX_ID = noteContext.ntxId;
|
||||
}
|
||||
|
||||
if (iframeRef.current?.contentWindow) {
|
||||
iframeRef.current.contentWindow.addEventListener('click', () => {
|
||||
appContext.tabManager.activateNoteContext(noteContext.ntxId);
|
||||
});
|
||||
}
|
||||
}}
|
||||
editable
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface PdfHeading {
|
||||
level: number;
|
||||
text: string;
|
||||
id: string;
|
||||
element: null;
|
||||
}
|
||||
|
||||
function convertPdfOutlineToHeadings(outline: PdfOutlineItem[]): PdfHeading[] {
|
||||
const headings: PdfHeading[] = [];
|
||||
|
||||
function flatten(items: PdfOutlineItem[]) {
|
||||
for (const item of items) {
|
||||
headings.push({
|
||||
level: item.level + 1,
|
||||
text: item.title,
|
||||
id: item.id,
|
||||
element: null // PDFs don't have DOM elements
|
||||
});
|
||||
|
||||
if (item.items && item.items.length > 0) {
|
||||
flatten(item.items);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flatten(outline);
|
||||
return headings;
|
||||
}
|
||||
94
apps/client/src/widgets/type_widgets/file/PdfViewer.tsx
Normal file
94
apps/client/src/widgets/type_widgets/file/PdfViewer.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { HTMLAttributes, RefObject } from "preact";
|
||||
import { useCallback, useEffect, useRef } from "preact/hooks";
|
||||
|
||||
import { useSyncedRef, useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
||||
|
||||
const VARIABLE_WHITELIST = new Set([
|
||||
"root-background",
|
||||
"main-background-color",
|
||||
"main-border-color",
|
||||
"main-text-color"
|
||||
]);
|
||||
|
||||
interface PdfViewerProps extends Pick<HTMLAttributes<HTMLIFrameElement>, "tabIndex"> {
|
||||
iframeRef?: RefObject<HTMLIFrameElement>;
|
||||
/** Note: URLs are relative to /pdfjs/web. */
|
||||
pdfUrl: string;
|
||||
onLoad?(): void;
|
||||
/**
|
||||
* If set, enables editable mode which includes persistence of user settings, annotations as well as specific features such as sending table of contents data for the sidebar.
|
||||
*/
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable component displaying a PDF. The PDF needs to be provided via a URL.
|
||||
*/
|
||||
export default function PdfViewer({ iframeRef: externalIframeRef, pdfUrl, onLoad, editable }: PdfViewerProps) {
|
||||
const iframeRef = useSyncedRef(externalIframeRef, null);
|
||||
const [ locale ] = useTriliumOption("locale");
|
||||
const [ newLayout ] = useTriliumOptionBool("newLayout");
|
||||
const injectStyles = useStyleInjection(iframeRef);
|
||||
|
||||
return (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
class="pdf-preview"
|
||||
src={`pdfjs/web/viewer.html?file=${pdfUrl}&lang=${locale}&sidebar=${newLayout ? "0" : "1"}&editable=${editable ? "1" : "0"}`}
|
||||
onLoad={() => {
|
||||
injectStyles();
|
||||
onLoad?.();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function useStyleInjection(iframeRef: RefObject<HTMLIFrameElement>) {
|
||||
const styleRef = useRef<HTMLStyleElement | null>(null);
|
||||
|
||||
// First load.
|
||||
const onLoad = useCallback(() => {
|
||||
const doc = iframeRef.current?.contentDocument;
|
||||
if (!doc) return;
|
||||
|
||||
const style = doc.createElement('style');
|
||||
style.id = 'client-root-vars';
|
||||
style.textContent = cssVarsToString(getRootCssVariables());
|
||||
styleRef.current = style;
|
||||
|
||||
doc.head.appendChild(style);
|
||||
}, [ iframeRef ]);
|
||||
|
||||
// React to changes.
|
||||
useEffect(() => {
|
||||
const listener = () => {
|
||||
styleRef.current!.textContent = cssVarsToString(getRootCssVariables());
|
||||
};
|
||||
|
||||
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
media.addEventListener("change", listener);
|
||||
return () => media.removeEventListener("change", listener);
|
||||
}, [ iframeRef ]);
|
||||
|
||||
return onLoad;
|
||||
}
|
||||
|
||||
function getRootCssVariables() {
|
||||
const styles = getComputedStyle(document.documentElement);
|
||||
const vars: Record<string, string> = {};
|
||||
|
||||
for (let i = 0; i < styles.length; i++) {
|
||||
const prop = styles[i];
|
||||
if (prop.startsWith('--') && VARIABLE_WHITELIST.has(prop.substring(2))) {
|
||||
vars[`--tn-${prop.substring(2)}`] = styles.getPropertyValue(prop).trim();
|
||||
}
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
function cssVarsToString(vars: Record<string, string>) {
|
||||
return `:root {\n${Object.entries(vars)
|
||||
.map(([k, v]) => ` ${k}: ${v};`)
|
||||
.join('\n')}\n}`;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { t } from "../../../services/i18n";
|
||||
import SplitEditor, { PreviewButton, SplitEditorProps } from "./SplitEditor";
|
||||
import { RawHtmlBlock } from "../../react/RawHtml";
|
||||
@@ -55,7 +55,9 @@ export default function SvgSplitEditor({ ntxId, note, attachmentName, renderSvg,
|
||||
}
|
||||
|
||||
// Save as attachment.
|
||||
function onSave() {
|
||||
const onSave = useCallback(() => {
|
||||
if (!svg) return; // Don't save if SVG hasn't been rendered yet
|
||||
|
||||
const payload = {
|
||||
role: "image",
|
||||
title: `${attachmentName}.svg`,
|
||||
@@ -65,16 +67,18 @@ export default function SvgSplitEditor({ ntxId, note, attachmentName, renderSvg,
|
||||
};
|
||||
|
||||
server.post(`notes/${note.noteId}/attachments?matchBy=title`, payload);
|
||||
}
|
||||
}, [ svg, attachmentName, note.noteId ]);
|
||||
|
||||
// Save the SVG when entering a note only when it does not have an attachment.
|
||||
useEffect(() => {
|
||||
if (!svg) return; // Wait until SVG is rendered
|
||||
|
||||
note?.getAttachments().then((attachments) => {
|
||||
if (!attachments.find((a) => a.title === `${attachmentName}.svg`)) {
|
||||
onSave();
|
||||
}
|
||||
});
|
||||
}, [ note ]);
|
||||
}).catch(e => console.error("Failed to get attachments for SVGSplitEditor", e));
|
||||
}, [ note, svg, attachmentName, onSave ]);
|
||||
|
||||
// Import/export
|
||||
useTriliumEvent("exportSvg", async({ ntxId: eventNtxId }) => {
|
||||
|
||||
@@ -35,5 +35,5 @@ describe("CK config", () => {
|
||||
expect(config.translations, locale.id).toHaveLength(2);
|
||||
}
|
||||
}
|
||||
}, 10_000);
|
||||
}, 20_000);
|
||||
});
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/vitest",
|
||||
"types": [
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
],
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { writeFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
import BuildHelper from "../../../scripts/build-utils";
|
||||
import originalPackageJson from "../package.json" with { type: "json" };
|
||||
import { writeFileSync } from "fs";
|
||||
|
||||
const build = new BuildHelper("apps/desktop");
|
||||
|
||||
@@ -18,9 +19,7 @@ async function main() {
|
||||
build.copyNodeModules([ "better-sqlite3", "bindings", "file-uri-to-path", "@electron/remote" ]);
|
||||
build.copy("/node_modules/ckeditor5/dist/ckeditor5-content.css", "ckeditor5-content.css");
|
||||
|
||||
// Integrate the client.
|
||||
build.triggerBuildAndCopyTo("apps/client", "public/");
|
||||
build.deleteFromOutput("public/webpack-stats.json");
|
||||
build.buildFrontend();
|
||||
|
||||
generatePackageJson();
|
||||
}
|
||||
|
||||
129
apps/server-e2e/src/note_types/pdf.spec.ts
Normal file
129
apps/server-e2e/src/note_types/pdf.spec.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import test, { BrowserContext, expect, Page } from "@playwright/test";
|
||||
|
||||
import App from "../support/app";
|
||||
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
const app = await setLayout({ page, context }, true);
|
||||
await app.setOption("rightPaneCollapsedItems", "[]");
|
||||
});
|
||||
test.afterEach(async ({ page, context }) => await setLayout({ page, context }, false));
|
||||
|
||||
test("Table of contents works", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
await app.goToNoteInNewTab("Dacia Logan.pdf");
|
||||
|
||||
const toc = app.sidebar.locator(".toc");
|
||||
|
||||
await expect(toc.locator("li")).toHaveCount(48);
|
||||
await expect(toc.locator("li", { hasText: "Logan Van" })).toHaveCount(1);
|
||||
|
||||
const pdfHelper = new PdfHelper(app);
|
||||
await toc.locator("li", { hasText: "Logan Pick-Up" }).click();
|
||||
await pdfHelper.expectPageToBe(13);
|
||||
|
||||
await app.clickNoteOnNoteTreeByTitle("Layers test.pdf");
|
||||
await expect(toc.locator("li")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Page navigation works", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
await app.goToNoteInNewTab("Dacia Logan.pdf");
|
||||
|
||||
const pagesList = app.sidebar.locator(".pdf-pages-list");
|
||||
|
||||
// Check count is correct.
|
||||
await expect(app.sidebar).toContainText("28 pages");
|
||||
expect(await pagesList.locator(".pdf-page-item").count()).toBe(28);
|
||||
|
||||
// Go to page 3.
|
||||
await pagesList.locator(".pdf-page-item").nth(2).click();
|
||||
|
||||
const pdfHelper = new PdfHelper(app);
|
||||
await pdfHelper.expectPageToBe(3);
|
||||
|
||||
await app.clickNoteOnNoteTreeByTitle("Layers test.pdf");
|
||||
await expect(pagesList.locator(".pdf-page-item")).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("Attachments listing works", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
await app.goToNoteInNewTab("Dacia Logan.pdf");
|
||||
|
||||
const attachmentsList = app.sidebar.locator(".pdf-attachments-list");
|
||||
await expect(app.sidebar).toContainText("2 attachments");
|
||||
await expect(attachmentsList.locator(".pdf-attachment-item")).toHaveCount(2);
|
||||
|
||||
const attachmentInfo = attachmentsList.locator(".pdf-attachment-item", { hasText: "Note.trilium" });
|
||||
await expect(attachmentInfo).toContainText("3.36 MiB");
|
||||
|
||||
// Download the attachment and check its size.
|
||||
const [ download ] = await Promise.all([
|
||||
page.waitForEvent("download"),
|
||||
attachmentInfo.locator(".bx-download").click()
|
||||
]);
|
||||
expect(download).toBeDefined();
|
||||
|
||||
await app.clickNoteOnNoteTreeByTitle("Layers test.pdf");
|
||||
await expect(attachmentsList.locator(".pdf-attachment-item")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Download original PDF works", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
await app.goToNoteInNewTab("Dacia Logan.pdf");
|
||||
const pdfHelper = new PdfHelper(app);
|
||||
await pdfHelper.toBeInitialized();
|
||||
|
||||
const [ download ] = await Promise.all([
|
||||
page.waitForEvent("download"),
|
||||
app.currentNoteSplit.locator(".icon-action.bx.bx-download").click()
|
||||
]);
|
||||
expect(download).toBeDefined();
|
||||
});
|
||||
|
||||
test("Layers listing works", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
await app.goToNoteInNewTab("Layers test.pdf");
|
||||
|
||||
// Check count is correct.
|
||||
await expect(app.sidebar).toContainText("2 layers");
|
||||
const layersList = app.sidebar.locator(".pdf-layers-list");
|
||||
await expect(layersList.locator(".pdf-layer-item")).toHaveCount(2);
|
||||
|
||||
// Toggle visibility of the first layer.
|
||||
const firstLayer = layersList.locator(".pdf-layer-item").first();
|
||||
await expect(firstLayer).toContainText("Tongue out");
|
||||
await expect(firstLayer).toContainClass("hidden");
|
||||
await firstLayer.click();
|
||||
await expect(firstLayer).not.toContainClass("visible");
|
||||
|
||||
await app.clickNoteOnNoteTreeByTitle("Dacia Logan.pdf");
|
||||
await expect(layersList.locator(".pdf-layer-item")).toHaveCount(0);
|
||||
});
|
||||
|
||||
async function setLayout({ page, context}: { page: Page; context: BrowserContext }, newLayout: boolean) {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
await app.setOption("newLayout", newLayout ? "true" : "false");
|
||||
return app;
|
||||
}
|
||||
|
||||
class PdfHelper {
|
||||
private contentFrame: ReturnType<Page["frameLocator"]>;
|
||||
|
||||
constructor(app: App) {
|
||||
this.contentFrame = app.currentNoteSplit.frameLocator("iframe");
|
||||
}
|
||||
|
||||
async expectPageToBe(expectedPageNumber: number) {
|
||||
await expect(this.contentFrame.locator("#pageNumber")).toHaveValue(`${expectedPageNumber}`);
|
||||
}
|
||||
|
||||
async toBeInitialized() {
|
||||
await expect(this.contentFrame.locator("#pageNumber")).toBeVisible();
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_pass http://host.docker.internal:8082; # change it to a different port if non-default is used
|
||||
proxy_pass http://127.0.0.1:8082;
|
||||
proxy_cookie_path / /trilium/;
|
||||
proxy_read_timeout 90;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
"docker-start-rootless-debian": "pnpm docker-build-rootless-debian && docker run -p 8081:8080 triliumnext-rootless-debian",
|
||||
"docker-start-rootless-alpine": "pnpm docker-build-rootless-alpine && docker run -p 8081:8080 triliumnext-rootless-alpine",
|
||||
"generate-document": "cross-env TRILIUM_ENV=dev TRILIUM_DATA_DIR=data TRILIUM_RESOURCE_DIR=src tsx ./scripts/generate_document.ts",
|
||||
"proxy-traefik": "docker run --name trilium-traefik --rm --network=host -v ./docker/traefik/traefik.yml:/etc/traefik/traefik.yml -v ./docker/traefik/dynamic:/etc/traefik/dynamic traefik:latest"
|
||||
"proxy-traefik": "docker run --name trilium-traefik --rm --network=host -v ./docker/traefik/traefik.yml:/etc/traefik/traefik.yml:ro -v ./docker/traefik/dynamic:/etc/traefik/dynamic traefik:latest",
|
||||
"proxy-nginx-subdir": "docker run --name trilium-nginx-subdir --rm --network=host -v ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro nginx:latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "12.5.0",
|
||||
|
||||
@@ -14,9 +14,7 @@ async function main() {
|
||||
build.copyNodeModules([ "better-sqlite3", "bindings", "file-uri-to-path" ]);
|
||||
build.copy("/node_modules/ckeditor5/dist/ckeditor5-content.css", "ckeditor5-content.css");
|
||||
|
||||
// Integrate the client.
|
||||
build.triggerBuildAndCopyTo("apps/client", "public/");
|
||||
build.deleteFromOutput("public/webpack-stats.json");
|
||||
build.buildFrontend();
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
Binary file not shown.
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
28
apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI.html
generated
vendored
28
apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI.html
generated
vendored
@@ -35,11 +35,11 @@ class="image image_resized" style="width:74.04%;">
|
||||
href="#root/_help_vvUCN7FDkq7G">Installing Ollama</a>.</p>
|
||||
<p>To see what embedding models Ollama has available, you can check out
|
||||
<a
|
||||
href="https://ollama.com/search?c=embedding">this search</a>on their website, and then <code>pull</code> whichever one
|
||||
you want to try out. A popular choice is <code>mxbai-embed-large</code>.</p>
|
||||
href="https://ollama.com/search?c=embedding">this search</a>on their website, and then <code spellcheck="false">pull</code> whichever
|
||||
one you want to try out. A popular choice is <code spellcheck="false">mxbai-embed-large</code>.</p>
|
||||
<p>First, we'll need to select the Ollama provider from the tabs of providers,
|
||||
then we will enter in the Base URL for our Ollama. Since our Ollama is
|
||||
running on our local machine, our Base URL is <code>http://localhost:11434</code>.
|
||||
running on our local machine, our Base URL is <code spellcheck="false">http://localhost:11434</code>.
|
||||
We will then hit the “refresh” button to have it fetch our models:</p>
|
||||
<figure
|
||||
class="image image_resized" style="width:82.28%;">
|
||||
@@ -80,57 +80,57 @@ class="image image_resized" style="width:74.04%;">
|
||||
<p>These are the tools that currently exist, and will certainly be updated
|
||||
to be more effectively (and even more to be added!):</p>
|
||||
<ul>
|
||||
<li><code>search_notes</code>
|
||||
<li><code spellcheck="false">search_notes</code>
|
||||
<ul>
|
||||
<li>Semantic search</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><code>keyword_search</code>
|
||||
<li><code spellcheck="false">keyword_search</code>
|
||||
<ul>
|
||||
<li>Keyword-based search</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><code>attribute_search</code>
|
||||
<li><code spellcheck="false">attribute_search</code>
|
||||
<ul>
|
||||
<li>Attribute-specific search</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><code>search_suggestion</code>
|
||||
<li><code spellcheck="false">search_suggestion</code>
|
||||
<ul>
|
||||
<li>Search syntax helper</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><code>read_note</code>
|
||||
<li><code spellcheck="false">read_note</code>
|
||||
<ul>
|
||||
<li>Read note content (helps the LLM read Notes)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><code>create_note</code>
|
||||
<li><code spellcheck="false">create_note</code>
|
||||
<ul>
|
||||
<li>Create a Note</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><code>update_note</code>
|
||||
<li><code spellcheck="false">update_note</code>
|
||||
<ul>
|
||||
<li>Update a Note</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><code>manage_attributes</code>
|
||||
<li><code spellcheck="false">manage_attributes</code>
|
||||
<ul>
|
||||
<li>Manage attributes on a Note</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><code>manage_relationships</code>
|
||||
<li><code spellcheck="false">manage_relationships</code>
|
||||
<ul>
|
||||
<li>Manage the various relationships between Notes</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><code>extract_content</code>
|
||||
<li><code spellcheck="false">extract_content</code>
|
||||
<ul>
|
||||
<li>Used to smartly extract content from a Note</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><code>calendar_integration</code>
|
||||
<li><code spellcheck="false">calendar_integration</code>
|
||||
<ul>
|
||||
<li>Used to find date notes, create date notes, get the daily note, etc.</li>
|
||||
</ul>
|
||||
|
||||
@@ -23,14 +23,14 @@ class="image image_resized" style="width:50.49%;">
|
||||
<img style="aspect-ratio:1161/480;" src="2_Installing Ollama_image.png"
|
||||
width="1161" height="480">
|
||||
</figure>
|
||||
<p>Also, you should have access to the <code>ollama</code> CLI via Powershell
|
||||
or CMD:</p>
|
||||
<p>Also, you should have access to the <code spellcheck="false">ollama</code> CLI
|
||||
via Powershell or CMD:</p>
|
||||
<figure class="image image_resized" style="width:86.09%;">
|
||||
<img style="aspect-ratio:1730/924;" src="5_Installing Ollama_image.png"
|
||||
width="1730" height="924">
|
||||
</figure>
|
||||
<p>After Ollama is installed, you can go ahead and <code>pull</code> the models
|
||||
you want to use and run. Here's a command to pull my favorite tool-compatible
|
||||
<p>After Ollama is installed, you can go ahead and <code spellcheck="false">pull</code> the
|
||||
models you want to use and run. Here's a command to pull my favorite tool-compatible
|
||||
model and embedding model as of April 2025:</p><pre><code class="language-text-x-trilium-auto">ollama pull llama3.1:8b
|
||||
ollama pull mxbai-embed-large</code></pre>
|
||||
<p>Also, you can make sure it's running by going to <a href="http://localhost:11434">http://localhost:11434</a> and
|
||||
|
||||
@@ -28,125 +28,160 @@
|
||||
where you can track your daily weight. This data is then used in <a href="#root/_help_R7abl2fc6Mxi">Weight tracker</a>.</p>
|
||||
<h2>Week Note and Quarter Note</h2>
|
||||
<p>Week and quarter notes are disabled by default, since it might be too
|
||||
much for some people. To enable them, you need to set <code>#enableWeekNote</code> and <code>#enableQuarterNote</code> attributes
|
||||
on the root calendar note, which is identified by <code>#calendarRoot</code> label.
|
||||
Week note is affected by the first week of year option. Be careful when
|
||||
you already have some week notes created, it will not automatically change
|
||||
the existing week notes and might lead to some duplicates.</p>
|
||||
much for some people. To enable them, you need to set <code spellcheck="false">#enableWeekNote</code> and
|
||||
<code
|
||||
spellcheck="false">#enableQuarterNote</code>attributes on the root calendar note, which is
|
||||
identified by <code spellcheck="false">#calendarRoot</code> label. Week note
|
||||
is affected by the first week of year option. Be careful when you already
|
||||
have some week notes created, it will not automatically change the existing
|
||||
week notes and might lead to some duplicates.</p>
|
||||
<h2>Templates</h2>
|
||||
<p>Trilium provides <a href="#root/_help_KC1HB96bqqHX">template</a> functionality,
|
||||
and it could be used together with day notes.</p>
|
||||
<p>You can define one of the following relations on the root of the journal
|
||||
(identified by <code>#calendarRoot</code> label):</p>
|
||||
(identified by <code spellcheck="false">#calendarRoot</code> label):</p>
|
||||
<ul>
|
||||
<li>yearTemplate</li>
|
||||
<li>quarterTemplate (if <code>#enableQuarterNote</code> is set)</li>
|
||||
<li>quarterTemplate (if <code spellcheck="false">#enableQuarterNote</code> is
|
||||
set)</li>
|
||||
<li>monthTemplate</li>
|
||||
<li>weekTemplate (if <code>#enableWeekNote</code> is set)</li>
|
||||
<li>weekTemplate (if <code spellcheck="false">#enableWeekNote</code> is set)</li>
|
||||
<li>dateTemplate</li>
|
||||
</ul>
|
||||
<p>All of these are relations. When Trilium creates a new note for year or
|
||||
month or date, it will take a look at the root and attach a corresponding <code>~template</code> relation
|
||||
to the newly created role. Using this, you can e.g. create your daily template
|
||||
with e.g. checkboxes for daily routine etc.</p>
|
||||
month or date, it will take a look at the root and attach a corresponding
|
||||
<code
|
||||
spellcheck="false">~template</code>relation to the newly created role. Using this, you can
|
||||
e.g. create your daily template with e.g. checkboxes for daily routine
|
||||
etc.</p>
|
||||
<h3>Migrate from old template usage</h3>
|
||||
<p>If you have been using Journal prior to version v0.93.0, the previous
|
||||
template pattern likely used was <code>~child:template=</code>.
|
||||
template pattern likely used was <code spellcheck="false">~child:template=</code>.
|
||||
<br>To transition to the new system:</p>
|
||||
<ol>
|
||||
<li>Set up the new template pattern in the Calendar root note.</li>
|
||||
<li>Use <a href="#root/_help_ivYnonVFBxbQ">Bulk Actions</a> to remove <code>child:template</code> and <code>child:child:template</code> from
|
||||
all notes under the Journal (calendar root).</li>
|
||||
<li>Use <a href="#root/_help_ivYnonVFBxbQ">Bulk Actions</a> to remove <code spellcheck="false">child:template</code> and
|
||||
<code
|
||||
spellcheck="false">child:child:template</code>from all notes under the Journal (calendar
|
||||
root).</li>
|
||||
<li>Ensure that all old template patterns are fully removed to prevent conflicts
|
||||
with the new setup.</li>
|
||||
</ol>
|
||||
<h2>Naming pattern</h2>
|
||||
<p>You can customize the title of generated journal notes by defining a <code>#datePattern</code>, <code>#weekPattern</code>, <code>#monthPattern</code>, <code>#quarterPattern</code> and <code>#yearPattern</code> attribute
|
||||
on a root calendar note (identified by <code>#calendarRoot</code> label).
|
||||
The naming pattern replacements follow a level-up compatibility - each
|
||||
level can use replacements from itself and all levels above it. For example, <code>#monthPattern</code> can
|
||||
use month, quarter and year replacements, while <code>#weekPattern</code> can
|
||||
use week, month, quarter and year replacements. But it is not possible
|
||||
to use week replacements in <code>#monthPattern</code>.</p>
|
||||
<p>You can customize the title of generated journal notes by defining a
|
||||
<code
|
||||
spellcheck="false">#datePattern</code>, <code spellcheck="false">#weekPattern</code>,
|
||||
<code
|
||||
spellcheck="false">#monthPattern</code>, <code spellcheck="false">#quarterPattern</code> and
|
||||
<code
|
||||
spellcheck="false">#yearPattern</code>attribute on a root calendar note (identified by
|
||||
<code
|
||||
spellcheck="false">#calendarRoot</code>label). The naming pattern replacements follow a level-up
|
||||
compatibility - each level can use replacements from itself and all levels
|
||||
above it. For example, <code spellcheck="false">#monthPattern</code> can
|
||||
use month, quarter and year replacements, while <code spellcheck="false">#weekPattern</code> can
|
||||
use week, month, quarter and year replacements. But it is not possible
|
||||
to use week replacements in <code spellcheck="false">#monthPattern</code>.</p>
|
||||
<h3>Date pattern</h3>
|
||||
<p>It's possible to customize the title of generated date notes by defining
|
||||
a <code>#datePattern</code> attribute on a root calendar note (identified
|
||||
by <code>#calendarRoot</code> label). Following are possible values:</p>
|
||||
a <code spellcheck="false">#datePattern</code> attribute on a root calendar
|
||||
note (identified by <code spellcheck="false">#calendarRoot</code> label).
|
||||
Following are possible values:</p>
|
||||
<ul>
|
||||
<li><code>{isoDate}</code> results in an ISO 8061 formatted date (e.g. "2025-03-09"
|
||||
for March 9, 2025)</li>
|
||||
<li><code>{dateNumber}</code> results in a number like <code>9</code> for the
|
||||
9th day of the month, <code>11</code> for the 11th day of the month</li>
|
||||
<li><code>{dateNumberPadded}</code> results in a number like <code>09</code> for
|
||||
the 9th day of the month, <code>11</code> for the 11th day of the month</li>
|
||||
<li><code>{ordinal}</code> is replaced with the ordinal date (e.g. 1st, 2nd,
|
||||
3rd) etc.</li>
|
||||
<li><code>{weekDay}</code> results in the full day name (e.g. <code>Monday</code>)</li>
|
||||
<li><code>{weekDay3}</code> is replaced with the first 3 letters of the day,
|
||||
e.g. Mon, Tue, etc.</li>
|
||||
<li><code>{weekDay2}</code> is replaced with the first 2 letters of the day,
|
||||
e.g. Mo, Tu, etc.</li>
|
||||
<li><code spellcheck="false">{isoDate}</code> results in an ISO 8061 formatted
|
||||
date (e.g. "2025-03-09" for March 9, 2025)</li>
|
||||
<li><code spellcheck="false">{dateNumber}</code> results in a number like
|
||||
<code
|
||||
spellcheck="false">9</code>for the 9th day of the month, <code spellcheck="false">11</code> for
|
||||
the 11th day of the month</li>
|
||||
<li><code spellcheck="false">{dateNumberPadded}</code> results in a number
|
||||
like <code spellcheck="false">09</code> for the 9th day of the month,
|
||||
<code
|
||||
spellcheck="false">11</code>for the 11th day of the month</li>
|
||||
<li><code spellcheck="false">{ordinal}</code> is replaced with the ordinal
|
||||
date (e.g. 1st, 2nd, 3rd) etc.</li>
|
||||
<li><code spellcheck="false">{weekDay}</code> results in the full day name
|
||||
(e.g. <code spellcheck="false">Monday</code>)</li>
|
||||
<li><code spellcheck="false">{weekDay3}</code> is replaced with the first 3
|
||||
letters of the day, e.g. Mon, Tue, etc.</li>
|
||||
<li><code spellcheck="false">{weekDay2}</code> is replaced with the first 2
|
||||
letters of the day, e.g. Mo, Tu, etc.</li>
|
||||
</ul>
|
||||
<p>The default is <code>{dateNumberPadded} - {weekDay}</code>
|
||||
<p>The default is <code spellcheck="false">{dateNumberPadded} - {weekDay}</code>
|
||||
</p>
|
||||
<h3>Week pattern</h3>
|
||||
<p>It is also possible to customize the title of generated week notes through
|
||||
the <code>#weekPattern</code> attribute on the root calendar note. The options
|
||||
are:</p>
|
||||
the <code spellcheck="false">#weekPattern</code> attribute on the root calendar
|
||||
note. The options are:</p>
|
||||
<ul>
|
||||
<li><code>{weekNumber}</code> results in a number like <code>9</code> for the
|
||||
9th week of the year, <code>11</code> for the 11th week of the year</li>
|
||||
<li><code>{weekNumberPadded}</code> results in a number like <code>09</code> for
|
||||
the 9th week of the year, <code>11</code> for the 11th week of the year</li>
|
||||
<li><code>{shortWeek}</code> results in a short week string like <code>W9</code> for
|
||||
the 9th week of the year, <code>W11</code> for the 11th week of the year</li>
|
||||
<li><code>{shortWeek3}</code> results in a short week string like <code>W09</code> for
|
||||
the 9th week of the year, <code>W11</code> for the 11th week of the year</li>
|
||||
<li><code spellcheck="false">{weekNumber}</code> results in a number like
|
||||
<code
|
||||
spellcheck="false">9</code>for the 9th week of the year, <code spellcheck="false">11</code> for
|
||||
the 11th week of the year</li>
|
||||
<li><code spellcheck="false">{weekNumberPadded}</code> results in a number
|
||||
like <code spellcheck="false">09</code> for the 9th week of the year,
|
||||
<code
|
||||
spellcheck="false">11</code>for the 11th week of the year</li>
|
||||
<li><code spellcheck="false">{shortWeek}</code> results in a short week string
|
||||
like <code spellcheck="false">W9</code> for the 9th week of the year,
|
||||
<code
|
||||
spellcheck="false">W11</code>for the 11th week of the year</li>
|
||||
<li><code spellcheck="false">{shortWeek3}</code> results in a short week string
|
||||
like <code spellcheck="false">W09</code> for the 9th week of the year,
|
||||
<code
|
||||
spellcheck="false">W11</code>for the 11th week of the year</li>
|
||||
</ul>
|
||||
<p>The default is <code>Week {weekNumber}</code>
|
||||
<p>The default is <code spellcheck="false">Week {weekNumber}</code>
|
||||
</p>
|
||||
<h3>Month pattern</h3>
|
||||
<p>It is also possible to customize the title of generated month notes through
|
||||
the <code>#monthPattern</code> attribute on the root calendar note. The options
|
||||
are:</p>
|
||||
the <code spellcheck="false">#monthPattern</code> attribute on the root calendar
|
||||
note. The options are:</p>
|
||||
<ul>
|
||||
<li><code>{isoMonth}</code> results in an ISO 8061 formatted month (e.g. "2025-03"
|
||||
for March 2025)</li>
|
||||
<li><code>{monthNumber}</code> results in a number like <code>9</code> for September,
|
||||
and <code>11</code> for November</li>
|
||||
<li><code>{monthNumberPadded}</code> results in a number like <code>09</code> for
|
||||
September, and <code>11</code> for November</li>
|
||||
<li><code>{month}</code> results in the full month name (e.g. <code>September</code> or <code>October</code>)</li>
|
||||
<li><code>{shortMonth3}</code> is replaced with the first 3 letters of the
|
||||
month, e.g. Jan, Feb, etc.</li>
|
||||
<li><code>{shortMonth4}</code> is replaced with the first 4 letters of the
|
||||
month, e.g. Sept, Octo, etc.</li>
|
||||
<li><code spellcheck="false">{isoMonth}</code> results in an ISO 8061 formatted
|
||||
month (e.g. "2025-03" for March 2025)</li>
|
||||
<li><code spellcheck="false">{monthNumber}</code> results in a number like
|
||||
<code
|
||||
spellcheck="false">9</code>for September, and <code spellcheck="false">11</code> for November</li>
|
||||
<li><code spellcheck="false">{monthNumberPadded}</code> results in a number
|
||||
like <code spellcheck="false">09</code> for September, and <code spellcheck="false">11</code> for
|
||||
November</li>
|
||||
<li><code spellcheck="false">{month}</code> results in the full month name
|
||||
(e.g. <code spellcheck="false">September</code> or <code spellcheck="false">October</code>)</li>
|
||||
<li><code spellcheck="false">{shortMonth3}</code> is replaced with the first
|
||||
3 letters of the month, e.g. Jan, Feb, etc.</li>
|
||||
<li><code spellcheck="false">{shortMonth4}</code> is replaced with the first
|
||||
4 letters of the month, e.g. Sept, Octo, etc.</li>
|
||||
</ul>
|
||||
<p>The default is <code>{monthNumberPadded} - {month}</code>
|
||||
<p>The default is <code spellcheck="false">{monthNumberPadded} - {month}</code>
|
||||
</p>
|
||||
<h3>Quarter pattern</h3>
|
||||
<p>It is also possible to customize the title of generated quarter notes
|
||||
through the <code>#quarterPattern</code> attribute on the root calendar note.
|
||||
The options are:</p>
|
||||
through the <code spellcheck="false">#quarterPattern</code> attribute on
|
||||
the root calendar note. The options are:</p>
|
||||
<ul>
|
||||
<li><code>{quarterNumber}</code> results in a number like <code>1</code> for
|
||||
the 1st quarter of the year</li>
|
||||
<li><code>{shortQuarter}</code> results in a short quarter string like <code>Q1</code> for
|
||||
the 1st quarter of the year</li>
|
||||
<li><code spellcheck="false">{quarterNumber}</code> results in a number like
|
||||
<code
|
||||
spellcheck="false">1</code>for the 1st quarter of the year</li>
|
||||
<li><code spellcheck="false">{shortQuarter}</code> results in a short quarter
|
||||
string like <code spellcheck="false">Q1</code> for the 1st quarter of the
|
||||
year</li>
|
||||
</ul>
|
||||
<p>The default is <code>Quarter {quarterNumber}</code>
|
||||
<p>The default is <code spellcheck="false">Quarter {quarterNumber}</code>
|
||||
</p>
|
||||
<h3>Year pattern</h3>
|
||||
<p>It is also possible to customize the title of generated year notes through
|
||||
the <code>#yearPattern</code> attribute on the root calendar note. The options
|
||||
are:</p>
|
||||
the <code spellcheck="false">#yearPattern</code> attribute on the root calendar
|
||||
note. The options are:</p>
|
||||
<ul>
|
||||
<li><code>{year}</code> results in the full year (e.g. <code>2025</code>)</li>
|
||||
<li><code spellcheck="false">{year}</code> results in the full year (e.g.
|
||||
<code
|
||||
spellcheck="false">2025</code>)</li>
|
||||
</ul>
|
||||
<p>The default is <code>{year}</code>
|
||||
<p>The default is <code spellcheck="false">{year}</code>
|
||||
</p>
|
||||
<h2>Implementation</h2>
|
||||
<p>Trilium has some special support for day notes in the form of <a href="https://triliumnext.github.io/Notes/backend_api/BackendScriptApi.html">backend Script API</a> -
|
||||
see e.g. getDayNote() function.</p>
|
||||
<p>Day (and year, month) notes are created with a label - e.g. <code>#dateNote="2025-03-09"</code> this
|
||||
<p>Day (and year, month) notes are created with a label - e.g. <code spellcheck="false">#dateNote="2025-03-09"</code> this
|
||||
can then be used by other scripts to add new notes to day note etc.</p>
|
||||
@@ -14,13 +14,13 @@
|
||||
note and doneDate note (with <a href="#root/_help_kBrnXNG3Hplm">prefix</a> of either
|
||||
"TODO" or "DONE").</p>
|
||||
<h2>Implementation</h2>
|
||||
<p>New tasks are created in the TODO note which has <code>~child:template</code>
|
||||
<p>New tasks are created in the TODO note which has <code spellcheck="false">~child:template</code>
|
||||
<a
|
||||
href="#root/_help_zEY4DaJG4YT5">relation</a>(see <a href="#root/_help_bwZpz2ajCEwO">attribute inheritance</a>)
|
||||
pointing to the task template.</p>
|
||||
<h3>Attributes</h3>
|
||||
<p>Task template defines several <a href="#root/_help_OFXdgB2nNk1F">promoted attributes</a> -
|
||||
todoDate, doneDate, tags, location. Importantly it also defines <code>~runOnAttributeChange</code> relation
|
||||
todoDate, doneDate, tags, location. Importantly it also defines <code spellcheck="false">~runOnAttributeChange</code> relation
|
||||
- <a href="#root/_help_GPERMystNGTB">event</a> handler which is run on attribute
|
||||
change. This <a href="#root/_help_CdNpE2pqjmI6">script</a> handles when e.g. we
|
||||
fill out the doneDate attribute - meaning the task is done and should be
|
||||
@@ -56,10 +56,10 @@
|
||||
span.fancytree-node.done .fancytree-title {
|
||||
color: green !important;
|
||||
}</code></pre>
|
||||
<p>This <a href="#root/_help_6f9hih2hXXZk">code note</a> has <code>#appCss</code>
|
||||
<p>This <a href="#root/_help_6f9hih2hXXZk">code note</a> has <code spellcheck="false">#appCss</code>
|
||||
<a
|
||||
href="#root/_help_zEY4DaJG4YT5">label</a>which is recognized by Trilium on startup and loaded as CSS into
|
||||
the application.</p>
|
||||
<p>Second part of this functionality is based in event handler described
|
||||
above which assigns <code>#cssClass</code> label to the task to either "done"
|
||||
or "todo" based on the task status.</p>
|
||||
above which assigns <code spellcheck="false">#cssClass</code> label to the
|
||||
task to either "done" or "todo" based on the task status.</p>
|
||||
@@ -1,28 +1,31 @@
|
||||
<p>
|
||||
<img src="Weight Tracker_image.png">
|
||||
</p>
|
||||
<p>The <code>Weight Tracker</code> is a <a href="#root/_help_GLks18SNjxmC">Script API</a> showcase
|
||||
<p>The <code spellcheck="false">Weight Tracker</code> is a <a href="#root/_help_GLks18SNjxmC">Script API</a> showcase
|
||||
present in the <a href="#root/_help_wX4HbRucYSDD">demo notes</a>.</p>
|
||||
<p>By adding <code>weight</code> as a <a href="#root/_help_OFXdgB2nNk1F">promoted attribute</a> in
|
||||
<p>By adding <code spellcheck="false">weight</code> as a <a href="#root/_help_OFXdgB2nNk1F">promoted attribute</a> in
|
||||
the <a href="#root/_help_KC1HB96bqqHX">template</a> from which <a href="#root/_help_l0tKav7yLHGF">day notes</a> are
|
||||
created, you can aggregate the data and plot weight change over time.</p>
|
||||
<h2>Implementation</h2>
|
||||
<p>The <code>Weight Tracker</code> note in the screenshot above is of the type <code>Render Note</code>.
|
||||
That type of note doesn't have any useful content itself. Instead it is
|
||||
a placeholder where a <a href="#root/_help_CdNpE2pqjmI6">script</a> can render
|
||||
its output.</p>
|
||||
<p>Scripts for <code>Render Notes</code> are defined in a <a href="#root/_help_zEY4DaJG4YT5">relation</a> called <code>~renderNote</code>.
|
||||
In this example, it's the <code>Weight Tracker</code>'s child <code>Implementation</code>.
|
||||
The Implementation consists of two <a href="#root/_help_6f9hih2hXXZk">code notes</a> that
|
||||
contain some HTML and JavaScript respectively, which load all the notes
|
||||
with a <code>weight</code> attribute and display their values in a chart.</p>
|
||||
<p>The <code spellcheck="false">Weight Tracker</code> note in the screenshot
|
||||
above is of the type <code spellcheck="false">Render Note</code>. That type
|
||||
of note doesn't have any useful content itself. Instead it is a placeholder
|
||||
where a <a href="#root/_help_CdNpE2pqjmI6">script</a> can render its output.</p>
|
||||
<p>Scripts for <code spellcheck="false">Render Notes</code> are defined in
|
||||
a <a href="#root/_help_zEY4DaJG4YT5">relation</a> called <code spellcheck="false">~renderNote</code>.
|
||||
In this example, it's the <code spellcheck="false">Weight Tracker</code>'s
|
||||
child <code spellcheck="false">Implementation</code>. The Implementation
|
||||
consists of two <a href="#root/_help_6f9hih2hXXZk">code notes</a> that contain
|
||||
some HTML and JavaScript respectively, which load all the notes with a
|
||||
<code
|
||||
spellcheck="false">weight</code>attribute and display their values in a chart.</p>
|
||||
<p>To actually render the chart, we're using a third party library called
|
||||
<a
|
||||
href="https://www.chartjs.org/">chart.js</a>which is imported as an attachment, since it's not built into
|
||||
Trilium.</p>
|
||||
<h3>Code</h3>
|
||||
<p>Here's the content of the script which is placed in a <a href="#root/_help_6f9hih2hXXZk">code note</a> of
|
||||
type <code>JS Frontend</code>:</p><pre><code class="language-text-x-trilium-auto">async function getChartData() {
|
||||
type <code spellcheck="false">JS Frontend</code>:</p><pre><code class="language-text-x-trilium-auto">async function getChartData() {
|
||||
const days = await api.runOnBackend(async () => {
|
||||
const notes = api.getNotesWithLabel('weight');
|
||||
const days = [];
|
||||
@@ -68,6 +71,7 @@ new chartjs.Chart(ctx, {
|
||||
data: await getChartData()
|
||||
});</code></pre>
|
||||
<h2>How to remove the Weight Tracker button from the top bar</h2>
|
||||
<p>In the link map of the <code>Weight Tracker</code>, there is a note called <code>Button</code>.
|
||||
Open it and delete or comment out its contents. The <code>Weight Tracker</code> button
|
||||
<p>In the link map of the <code spellcheck="false">Weight Tracker</code>,
|
||||
there is a note called <code spellcheck="false">Button</code>. Open it and
|
||||
delete or comment out its contents. The <code spellcheck="false">Weight Tracker</code> button
|
||||
will disappear after you restart Trilium.</p>
|
||||
12
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Attributes.html
generated
vendored
12
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Attributes.html
generated
vendored
@@ -29,9 +29,9 @@
|
||||
<ol>
|
||||
<li><strong>System attributes</strong>
|
||||
<br>As the name suggest, these attributes have a special meaning since they
|
||||
are interpreted by Trilium. For example the <code>color</code> attribute
|
||||
are interpreted by Trilium. For example the <code spellcheck="false">color</code> attribute
|
||||
will change the color of the note as displayed in the <a class="reference-link"
|
||||
href="#root/_help_oPVyFC7WL2Lp">Note Tree</a> and links, and <code>iconClass</code> will
|
||||
href="#root/_help_oPVyFC7WL2Lp">Note Tree</a> and links, and <code spellcheck="false">iconClass</code> will
|
||||
change the icon of a note.</li>
|
||||
<li><strong>User-defined attributes</strong>
|
||||
<br>These are free-form labels or relations that can be used by the user.
|
||||
@@ -43,8 +43,8 @@
|
||||
<p>In practice, Trilium makes no direct distinction of whether an attribute
|
||||
is a system one or a user-defined one. A label or relation is considered
|
||||
a system attribute if it matches one of the built-in names (e.g. like the
|
||||
aforementioned <code>iconClass</code>). Keep this in mind when creating
|
||||
<a class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a> in
|
||||
aforementioned <code spellcheck="false">iconClass</code>). Keep this in
|
||||
mind when creating <a class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a> in
|
||||
order not to accidentally alter a system attribute (unless intended).</p>
|
||||
<h2>Viewing the list of attributes</h2>
|
||||
<p>Both the labels and relations for the current note are displayed in the <em>Owned Attributes</em> section
|
||||
@@ -52,8 +52,8 @@
|
||||
where they can be viewed and edited. Inherited attributes are displayed
|
||||
in the <em>Inherited Attributes</em> section of the ribbon, where they can
|
||||
only be viewed.</p>
|
||||
<p>In the list of attributes, labels are prefixed with the <code>#</code> character
|
||||
whereas relations are prefixed with the <code>~</code> character.</p>
|
||||
<p>In the list of attributes, labels are prefixed with the <code spellcheck="false">#</code> character
|
||||
whereas relations are prefixed with the <code spellcheck="false">~</code> character.</p>
|
||||
<h2>Attribute Definitions and Promoted Attributes</h2>
|
||||
<p><a class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a> create
|
||||
a form-like editing experience for attributes, which makes it easy to enhancing
|
||||
|
||||
@@ -3,43 +3,49 @@
|
||||
generally in parent-child relations (or anywhere if using templates).</p>
|
||||
<h2>Standard Inheritance</h2>
|
||||
<p>In Trilium, attributes can be automatically inherited by child notes if
|
||||
they have the <code>isInheritable</code> flag set to <code>true</code>. This
|
||||
means the attribute (a key-value pair) is applied to the note and all its
|
||||
descendants.</p>
|
||||
they have the <code spellcheck="false">isInheritable</code> flag set to
|
||||
<code
|
||||
spellcheck="false">true</code>. This means the attribute (a key-value pair) is applied to
|
||||
the note and all its descendants.</p>
|
||||
<p>To make an attribute inheritable, simply use the visual editor for
|
||||
<a
|
||||
class="reference-link" href="#root/_help_HI6GBBIduIgv">Labels</a> or <a class="reference-link" href="#root/_help_Cq5X6iKQop6R">Relations</a>.
|
||||
Alternatively, the attribute can be manually defined where <code>#myLabel=value</code> becomes <code>#myLabel(inheritable)=value</code> when
|
||||
inheritable.</p>
|
||||
<p>As an example, the <code>archived</code> label can be set to be inheritable,
|
||||
allowing you to hide a whole subtree of notes from searches and other dialogs
|
||||
by applying this label at the top level.</p>
|
||||
Alternatively, the attribute can be manually defined where <code spellcheck="false">#myLabel=value</code> becomes
|
||||
<code
|
||||
spellcheck="false">#myLabel(inheritable)=value</code>when inheritable.</p>
|
||||
<p>As an example, the <code spellcheck="false">archived</code> label can be
|
||||
set to be inheritable, allowing you to hide a whole subtree of notes from
|
||||
searches and other dialogs by applying this label at the top level.</p>
|
||||
<p>Standard inheritance forces all the notes that are children (and sub-children)
|
||||
of a note to have that particular label or relation. If there is a need
|
||||
to have some notes not inherit one of the labels, then <em>copying inheritance</em> or <em>template inheritance</em> needs
|
||||
to be used instead.</p>
|
||||
<h2>Copying Inheritance</h2>
|
||||
<p>Copying inheritance differs from standard inheritance by using a <code>child:</code> prefix
|
||||
in the attribute name. This prefix causes new child notes to automatically
|
||||
receive specific attributes from the parent note. These attributes are
|
||||
independent of the parent and will persist even if the note is moved elsewhere.</p>
|
||||
<p>If a parent note has the label <code>#child:exampleAttribute</code>, all
|
||||
newly created child notes (one level deep) will inherit the <code>#exampleAttribute</code> label.
|
||||
<p>Copying inheritance differs from standard inheritance by using a
|
||||
<code
|
||||
spellcheck="false">child:</code>prefix in the attribute name. This prefix causes new child
|
||||
notes to automatically receive specific attributes from the parent note.
|
||||
These attributes are independent of the parent and will persist even if
|
||||
the note is moved elsewhere.</p>
|
||||
<p>If a parent note has the label <code spellcheck="false">#child:exampleAttribute</code>,
|
||||
all newly created child notes (one level deep) will inherit the <code spellcheck="false">#exampleAttribute</code> label.
|
||||
This can be useful for setting default properties for notes in a specific
|
||||
section.</p>
|
||||
<p>Similarly, for relations use <code>~child:myRelation</code>.</p>
|
||||
<p>Similarly, for relations use <code spellcheck="false">~child:myRelation</code>.</p>
|
||||
<p>Due to the way it's designed, copying inheritance cannot be used to cascade
|
||||
infinitely within a hierarchy. For that use case, consider using either
|
||||
standard inheritance or templates.</p>
|
||||
<h3>Chained inheritance</h3>
|
||||
<p>It is possible to define labels across multiple levels of depth. For example, <code>#child:child:child:foo</code> applied
|
||||
to a root note would create:</p>
|
||||
<p>It is possible to define labels across multiple levels of depth. For example,
|
||||
<code
|
||||
spellcheck="false">#child:child:child:foo</code>applied to a root note would create:</p>
|
||||
<ul>
|
||||
<li><code>#child:child:foo</code> on the first-level children.</li>
|
||||
<li><code>#child:foo</code> on the second-level children.</li>
|
||||
<li><code>#foo</code> on the third-level children.</li>
|
||||
<li><code spellcheck="false">#child:child:foo</code> on the first-level children.</li>
|
||||
<li><code spellcheck="false">#child:foo</code> on the second-level children.</li>
|
||||
<li><code spellcheck="false">#foo</code> on the third-level children.</li>
|
||||
</ul>
|
||||
<p>Similarly, use <code>~child:child:child:foo</code> if dealing with relations.</p>
|
||||
<p>Similarly, use <code spellcheck="false">~child:child:child:foo</code> if
|
||||
dealing with relations.</p>
|
||||
<p>Do note that same as simple copying inheritance, the changes will not
|
||||
apply retroactively to existing notes in the hierarchy, it will only apply
|
||||
to the newly created notes.</p>
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
<h2>Common use cases</h2>
|
||||
<ul>
|
||||
<li><strong>Metadata for personal use</strong>: Assign labels with optional
|
||||
values for categorization, such as <code>#year=1999</code>, <code>#genre="sci-fi"</code>,
|
||||
or <code>#author="Neal Stephenson"</code>. This can be combined with
|
||||
<a
|
||||
class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a> to make their display more user-friendly.</li>
|
||||
values for categorization, such as <code spellcheck="false">#year=1999</code>,
|
||||
<code
|
||||
spellcheck="false">#genre="sci-fi"</code>, or <code spellcheck="false">#author="Neal Stephenson"</code>.
|
||||
This can be combined with <a class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a> to
|
||||
make their display more user-friendly.</li>
|
||||
<li><strong>Configuration</strong>: Labels can configure advanced features
|
||||
or settings (see reference below).</li>
|
||||
<li><strong>Scripts and Plugins</strong>: Used to tag notes with special metadata,
|
||||
@@ -36,21 +37,24 @@
|
||||
<p>In the <em>Owned Attributes</em> section in the <a class="reference-link"
|
||||
href="#root/_help_BlN9DFI679QC">Ribbon</a>:</p>
|
||||
<ul>
|
||||
<li>To create a label called <code>myLabel</code> with no value, simply type <code>#myLabel</code>.</li>
|
||||
<li>To create a label called <code>myLabel</code> with a value <code>value</code>,
|
||||
simply type <code>#myLabel=value</code>.</li>
|
||||
<li>If the value contains spaces, then the text must be quoted: <code>#myLabel="Hello world"</code>.</li>
|
||||
<li>To create a label called <code spellcheck="false">myLabel</code> with no
|
||||
value, simply type <code spellcheck="false">#myLabel</code>.</li>
|
||||
<li>To create a label called <code spellcheck="false">myLabel</code> with a
|
||||
value <code spellcheck="false">value</code>, simply type <code spellcheck="false">#myLabel=value</code>.</li>
|
||||
<li>If the value contains spaces, then the text must be quoted: <code spellcheck="false">#myLabel="Hello world"</code>.</li>
|
||||
<li>If the string contains quotes (regardless of whether it has spaces), then
|
||||
the text must be quoted with apostrophes instead: <code>#myLabel='Hello "world"'</code>.</li>
|
||||
<li>To create an inheritable label called <code>myLabel</code>, simply write <code>#myLabel(inheritable)</code> for
|
||||
no value or <code>#myLabel(inheritable)=value</code> if there is a value.</li>
|
||||
the text must be quoted with apostrophes instead: <code spellcheck="false">#myLabel='Hello "world"'</code>.</li>
|
||||
<li>To create an inheritable label called <code spellcheck="false">myLabel</code>,
|
||||
simply write <code spellcheck="false">#myLabel(inheritable)</code> for no
|
||||
value or <code spellcheck="false">#myLabel(inheritable)=value</code> if there
|
||||
is a value.</li>
|
||||
</ul>
|
||||
<h2>Predefined labels</h2>
|
||||
<p>This is a list of labels that Trilium natively supports.</p>
|
||||
<aside class="admonition tip">
|
||||
<p>Some labels presented here end with a <code>*</code>. That means that there
|
||||
are multiple labels with the same prefix, consult the specific page linked
|
||||
in the description of that label for more information.</p>
|
||||
<p>Some labels presented here end with a <code spellcheck="false">*</code>.
|
||||
That means that there are multiple labels with the same prefix, consult
|
||||
the specific page linked in the description of that label for more information.</p>
|
||||
</aside>
|
||||
<table class="ck-table-resized">
|
||||
<colgroup>
|
||||
|
||||
@@ -47,8 +47,9 @@
|
||||
</ol>
|
||||
<h2>How attribute definitions actually work</h2>
|
||||
<p>When a new promoted attribute definition is created, it creates a corresponding
|
||||
label prefixed with either <code>label</code> or <code>relation</code>, depending
|
||||
on the definition type:</p><pre><code class="language-text-x-trilium-auto">#label:myColor(inheritable)="promoted,alias=Color,multi,color"</code></pre>
|
||||
label prefixed with either <code spellcheck="false">label</code> or
|
||||
<code
|
||||
spellcheck="false">relation</code>, depending on the definition type:</p><pre><code class="language-text-x-trilium-auto">#label:myColor(inheritable)="promoted,alias=Color,multi,color"</code></pre>
|
||||
<p>The only purpose of the attribute definition is to set up a template.
|
||||
If the attribute was marked as promoted, then it's also displayed to the
|
||||
user for easy editing.</p>
|
||||
@@ -97,7 +98,7 @@
|
||||
make use of this practice, for example:
|
||||
<ul>
|
||||
<li>Calendars add “Start Date”, “End Date”, “Start Time” and “End Time” as
|
||||
promoted attributes. These map to system attributes such as <code>startDate</code> which
|
||||
promoted attributes. These map to system attributes such as <code spellcheck="false">startDate</code> which
|
||||
are then interpreted by the calendar view.</li>
|
||||
<li><a class="reference-link" href="#root/_help_zP3PMqaG71Ct">Presentation</a> adds
|
||||
a “Background” promoted attribute for each of the slide to easily be able
|
||||
@@ -105,29 +106,29 @@
|
||||
</ul>
|
||||
</li>
|
||||
<li>The Trilium documentation (which is edited in Trilium) uses a promoted
|
||||
attribute to be able to easily edit the <code>#shareAlias</code> (see
|
||||
attribute to be able to easily edit the <code spellcheck="false">#shareAlias</code> (see
|
||||
<a
|
||||
class="reference-link" href="#root/_help_R9pX4DGra2Vt">Sharing</a>) in order to form clean URLs.</li>
|
||||
<li>If you always edit a particular system attribute such as <code>#color</code>,
|
||||
<li>If you always edit a particular system attribute such as <code spellcheck="false">#color</code>,
|
||||
simply create a promoted attribute for it to make it easier.</li>
|
||||
</ul>
|
||||
<h3>Inverse relation</h3>
|
||||
<p>Some relations always occur in pairs - my favorite example is on the family.
|
||||
If you have a note representing husband and note representing wife, then
|
||||
there might be a relation between those two of <code>isPartnerOf</code>.
|
||||
there might be a relation between those two of <code spellcheck="false">isPartnerOf</code>.
|
||||
This is bidirectional relationship - meaning that if a relation is pointing
|
||||
from husband to wife then there should be always another relation pointing
|
||||
from wife to husband.</p>
|
||||
<p>Another example is with parent-child relationship. Again these always
|
||||
occur in pairs, but in this case it's not exact same relation - the one
|
||||
going from parent to child might be called <code>isParentOf</code> and the
|
||||
other one going from child to parent might be called <code>isChildOf</code>.</p>
|
||||
going from parent to child might be called <code spellcheck="false">isParentOf</code> and
|
||||
the other one going from child to parent might be called <code spellcheck="false">isChildOf</code>.</p>
|
||||
<p>Relation definition allows you to specify such "inverse relation" - for
|
||||
the relation you just define you specify which is the inverse relation.
|
||||
Note that in the second example we should have two relation definitions
|
||||
- one for <code>isParentOf</code> which defines <code>isChildOf</code> as inverse
|
||||
relation and then second relation definition for <code>isChildOf</code> which
|
||||
defines <code>isParentOf</code> as inverse relation.</p>
|
||||
- one for <code spellcheck="false">isParentOf</code> which defines <code spellcheck="false">isChildOf</code> as
|
||||
inverse relation and then second relation definition for <code spellcheck="false">isChildOf</code> which
|
||||
defines <code spellcheck="false">isParentOf</code> as inverse relation.</p>
|
||||
<p>What this does internally is that whenever we save a relation which has
|
||||
defined inverse relation, we check that this inverse relation exists on
|
||||
the relation target note. Similarly, when we delete relation, we also delete
|
||||
|
||||
@@ -39,27 +39,30 @@
|
||||
<p>In the <em>Owned Attributes</em> section in the <a class="reference-link"
|
||||
href="#root/_help_BlN9DFI679QC">Ribbon</a>:</p>
|
||||
<ul>
|
||||
<li>To create a relation called <code>myRelation</code>:
|
||||
<li>To create a relation called <code spellcheck="false">myRelation</code>:
|
||||
<ul>
|
||||
<li>First type <code>~myRelation=@</code> .</li>
|
||||
<li>First type <code spellcheck="false">~myRelation=@</code> .</li>
|
||||
<li>After this, an autocompletion box should appear.</li>
|
||||
<li>Type the title of the note to point to and press <kbd>Enter</kbd> to confirm
|
||||
(or click the desired note).</li>
|
||||
<li>Alternatively copy a note from the <a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a> and
|
||||
paste it after the <code>=</code> sign (without the <code>@</code> , in this
|
||||
case).</li>
|
||||
paste it after the <code spellcheck="false">=</code> sign (without the
|
||||
<code
|
||||
spellcheck="false">@</code> , in this case).</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>To create an inheritable relation, follow the same steps as previously
|
||||
described but instead of <code>~myRelation</code> write <code>~myRelation(inheritable)</code>.</li>
|
||||
described but instead of <code spellcheck="false">~myRelation</code> write
|
||||
<code
|
||||
spellcheck="false">~myRelation(inheritable)</code>.</li>
|
||||
</ul>
|
||||
<h2>Predefined relations</h2>
|
||||
<p>These relations are supported and used internally by Trilium.</p>
|
||||
<aside
|
||||
class="admonition tip">
|
||||
<p>Some relations presented here end with a <code>*</code>. That means that
|
||||
there are multiple relations with the same prefix, consult the specific
|
||||
page linked in the description of that relation for more information.</p>
|
||||
<p>Some relations presented here end with a <code spellcheck="false">*</code>.
|
||||
That means that there are multiple relations with the same prefix, consult
|
||||
the specific page linked in the description of that relation for more information.</p>
|
||||
</aside>
|
||||
<table>
|
||||
<thead>
|
||||
@@ -70,20 +73,20 @@ class="admonition tip">
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>runOn*</code>
|
||||
<td><code spellcheck="false">runOn*</code>
|
||||
</td>
|
||||
<td>See <a class="reference-link" href="#root/_help_GPERMystNGTB">Events</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>template</code>
|
||||
<td><code spellcheck="false">template</code>
|
||||
</td>
|
||||
<td>note's attributes will be inherited even without a parent-child relationship,
|
||||
note's content and subtree will be added to instance notes if empty. See
|
||||
documentation for details.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>inherit</code>
|
||||
<td><code spellcheck="false">inherit</code>
|
||||
</td>
|
||||
<td>note's attributes will be inherited even without a parent-child relationship.
|
||||
See <a class="reference-link" href="#root/_help_KC1HB96bqqHX">Templates</a> for
|
||||
@@ -91,50 +94,53 @@ class="admonition tip">
|
||||
the documentation.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>renderNote</code>
|
||||
<td><code spellcheck="false">renderNote</code>
|
||||
</td>
|
||||
<td>notes of type <a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a> will
|
||||
be rendered using a code note (HTML or script) and it is necessary to point
|
||||
using this relation to which note should be rendered</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>widget_relation</code>
|
||||
<td><code spellcheck="false">widget_relation</code>
|
||||
</td>
|
||||
<td>target of this relation will be executed and rendered as a widget in the
|
||||
sidebar</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>shareCss</code>
|
||||
<td><code spellcheck="false">shareCss</code>
|
||||
</td>
|
||||
<td>CSS note which will be injected into the share page. CSS note must be
|
||||
in the shared sub-tree as well. Consider using <code>share_hidden_from_tree</code> and <code>share_omit_default_css</code> as
|
||||
well.</td>
|
||||
in the shared sub-tree as well. Consider using <code spellcheck="false">share_hidden_from_tree</code> and
|
||||
<code
|
||||
spellcheck="false">share_omit_default_css</code>as well.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>shareJs</code>
|
||||
<td><code spellcheck="false">shareJs</code>
|
||||
</td>
|
||||
<td>JavaScript note which will be injected into the share page. JS note must
|
||||
be in the shared sub-tree as well. Consider using <code>share_hidden_from_tree</code>.</td>
|
||||
be in the shared sub-tree as well. Consider using <code spellcheck="false">share_hidden_from_tree</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>shareHtml</code>
|
||||
<td><code spellcheck="false">shareHtml</code>
|
||||
</td>
|
||||
<td>HTML note which will be injected into the share page at locations specified
|
||||
by the <code>shareHtmlLocation</code> label. HTML note must be in the shared
|
||||
sub-tree as well. Consider using <code>share_hidden_from_tree</code>.</td>
|
||||
by the <code spellcheck="false">shareHtmlLocation</code> label. HTML note
|
||||
must be in the shared sub-tree as well. Consider using <code spellcheck="false">share_hidden_from_tree</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>shareTemplate</code>
|
||||
<td><code spellcheck="false">shareTemplate</code>
|
||||
</td>
|
||||
<td>Embedded JavaScript note that will be used as the template for displaying
|
||||
the shared note. Falls back to the default template. Consider using <code>share_hidden_from_tree</code>.</td>
|
||||
the shared note. Falls back to the default template. Consider using
|
||||
<code
|
||||
spellcheck="false">share_hidden_from_tree</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>shareFavicon</code>
|
||||
<td><code spellcheck="false">shareFavicon</code>
|
||||
</td>
|
||||
<td>Favicon note to be set in the shared page. Typically you want to set it
|
||||
to share root and make it inheritable. Favicon note must be in the shared
|
||||
sub-tree as well. Consider using <code>share_hidden_from_tree</code>.</td>
|
||||
sub-tree as well. Consider using <code spellcheck="false">share_hidden_from_tree</code>.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -116,8 +116,9 @@
|
||||
<a
|
||||
class="reference-link" href="#root/_help_habiZ3HU8Kw8">FNote</a>, for example:
|
||||
<ul>
|
||||
<li><code>NEW: ${note.title}</code> will prefix all notes with <code>NEW:</code> .</li>
|
||||
<li><code>${note.dateCreatedObj.format('MM-DD:')}: ${note.title}</code> will
|
||||
<li><code spellcheck="false">NEW: ${note.title}</code> will prefix all notes
|
||||
with <code spellcheck="false">NEW:</code> .</li>
|
||||
<li><code spellcheck="false">${note.dateCreatedObj.format('MM-DD:')}: ${note.title}</code> will
|
||||
prefix the note titles with each note's creation date (in month-day format).</li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -155,12 +156,13 @@
|
||||
<li>Examples:
|
||||
<ul>
|
||||
<li>
|
||||
<p>To apply a suffix (<code>- suffix</code> in this example), to the note
|
||||
title:</p><pre><code class="language-application-javascript-env-backend">note.title = note.title + " - suffix";</code></pre>
|
||||
<p>To apply a suffix (<code spellcheck="false">- suffix</code> in this example),
|
||||
to the note title:</p><pre><code class="language-application-javascript-env-backend">note.title = note.title + " - suffix";</code></pre>
|
||||
</li>
|
||||
<li>
|
||||
<p>To alter attributes of a note based on another attribute, such as setting
|
||||
the <code>#shareAlias</code> label to the title of the note:</p><pre><code class="language-application-javascript-env-backend">note.setLabel("shareAlias", note.title)</code></pre>
|
||||
the <code spellcheck="false">#shareAlias</code> label to the title of the
|
||||
note:</p><pre><code class="language-application-javascript-env-backend">note.setLabel("shareAlias", note.title)</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<p>Trilium supports configuration via a file named <code>config.ini</code> and
|
||||
<p>Trilium supports configuration via a file named <code spellcheck="false">config.ini</code> and
|
||||
environment variables. This document provides a comprehensive reference
|
||||
for all configuration options.</p>
|
||||
<h2>Location of the configuration file</h2>
|
||||
<p>The configuration file is not located in the same directory as the application.
|
||||
Instead, the <code>config.ini</code> is located in the <a class="reference-link"
|
||||
href="#root/_help_tAassRL4RSQL">Data directory</a>. As such, the configuration
|
||||
file is only available after starting the application and creating a database.</p>
|
||||
Instead, the <code spellcheck="false">config.ini</code> is located in the
|
||||
<a
|
||||
class="reference-link" href="#root/_help_tAassRL4RSQL">Data directory</a>. As such, the configuration file is only available
|
||||
after starting the application and creating a database.</p>
|
||||
<h2>Configuration Precedence</h2>
|
||||
<p>Configuration values are loaded in the following order of precedence (highest
|
||||
to lowest):</p>
|
||||
@@ -18,13 +19,15 @@
|
||||
</ol>
|
||||
<h2>Environment Variable Patterns</h2>
|
||||
<p>Trilium supports multiple environment variable patterns for flexibility.
|
||||
The primary pattern is: <code>TRILIUM_[SECTION]_[KEY]</code>
|
||||
The primary pattern is: <code spellcheck="false">TRILIUM_[SECTION]_[KEY]</code>
|
||||
</p>
|
||||
<p>Where:</p>
|
||||
<ul>
|
||||
<li><code>SECTION</code> is the INI section name in UPPERCASE</li>
|
||||
<li><code>KEY</code> is the camelCase configuration key converted to UPPERCASE
|
||||
(e.g., <code>instanceName</code> → <code>INSTANCENAME</code>)</li>
|
||||
<li><code spellcheck="false">SECTION</code> is the INI section name in UPPERCASE</li>
|
||||
<li><code spellcheck="false">KEY</code> is the camelCase configuration key
|
||||
converted to UPPERCASE (e.g., <code spellcheck="false">instanceName</code> →
|
||||
<code
|
||||
spellcheck="false">INSTANCENAME</code>)</li>
|
||||
</ul>
|
||||
<p>Additionally, shorter aliases are available for common configurations
|
||||
(see Alternative Variables section below).</p>
|
||||
@@ -41,35 +44,35 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>TRILIUM_GENERAL_INSTANCENAME</code>
|
||||
<td><code spellcheck="false">TRILIUM_GENERAL_INSTANCENAME</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>Instance name for API identification</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_GENERAL_NOAUTHENTICATION</code>
|
||||
<td><code spellcheck="false">TRILIUM_GENERAL_NOAUTHENTICATION</code>
|
||||
</td>
|
||||
<td>boolean</td>
|
||||
<td>false</td>
|
||||
<td>Disable authentication (server only)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_GENERAL_NOBACKUP</code>
|
||||
<td><code spellcheck="false">TRILIUM_GENERAL_NOBACKUP</code>
|
||||
</td>
|
||||
<td>boolean</td>
|
||||
<td>false</td>
|
||||
<td>Disable automatic backups</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_GENERAL_NODESKTOPICON</code>
|
||||
<td><code spellcheck="false">TRILIUM_GENERAL_NODESKTOPICON</code>
|
||||
</td>
|
||||
<td>boolean</td>
|
||||
<td>false</td>
|
||||
<td>Disable desktop icon creation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_GENERAL_READONLY</code>
|
||||
<td><code spellcheck="false">TRILIUM_GENERAL_READONLY</code>
|
||||
</td>
|
||||
<td>boolean</td>
|
||||
<td>false</td>
|
||||
@@ -90,70 +93,70 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_HOST</code>
|
||||
<td><code spellcheck="false">TRILIUM_NETWORK_HOST</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>"0.0.0.0"</td>
|
||||
<td>Server host binding</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_PORT</code>
|
||||
<td><code spellcheck="false">TRILIUM_NETWORK_PORT</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>"3000"</td>
|
||||
<td>Server port</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_HTTPS</code>
|
||||
<td><code spellcheck="false">TRILIUM_NETWORK_HTTPS</code>
|
||||
</td>
|
||||
<td>boolean</td>
|
||||
<td>false</td>
|
||||
<td>Enable HTTPS</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_CERTPATH</code>
|
||||
<td><code spellcheck="false">TRILIUM_NETWORK_CERTPATH</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>SSL certificate path</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_KEYPATH</code>
|
||||
<td><code spellcheck="false">TRILIUM_NETWORK_KEYPATH</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>SSL key path</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_TRUSTEDREVERSEPROXY</code>
|
||||
<td><code spellcheck="false">TRILIUM_NETWORK_TRUSTEDREVERSEPROXY</code>
|
||||
</td>
|
||||
<td>boolean/string</td>
|
||||
<td>false</td>
|
||||
<td>Reverse proxy trust settings</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_CORSALLOWORIGIN</code>
|
||||
<td><code spellcheck="false">TRILIUM_NETWORK_CORSALLOWORIGIN</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>CORS allowed origins</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_CORSALLOWMETHODS</code>
|
||||
<td><code spellcheck="false">TRILIUM_NETWORK_CORSALLOWMETHODS</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>CORS allowed methods</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_CORSALLOWHEADERS</code>
|
||||
<td><code spellcheck="false">TRILIUM_NETWORK_CORSALLOWHEADERS</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>CORS allowed headers</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_CORSRESOURCEPOLICY</code>
|
||||
<td><code spellcheck="false">TRILIUM_NETWORK_CORSRESOURCEPOLICY</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>same-origin</td>
|
||||
@@ -175,7 +178,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>TRILIUM_SESSION_COOKIEMAXAGE</code>
|
||||
<td><code spellcheck="false">TRILIUM_SESSION_COOKIEMAXAGE</code>
|
||||
</td>
|
||||
<td>integer</td>
|
||||
<td>1814400</td>
|
||||
@@ -196,21 +199,21 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>TRILIUM_SYNC_SYNCSERVERHOST</code>
|
||||
<td><code spellcheck="false">TRILIUM_SYNC_SYNCSERVERHOST</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>Sync server host URL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_SYNC_SYNCSERVERTIMEOUT</code>
|
||||
<td><code spellcheck="false">TRILIUM_SYNC_SYNCSERVERTIMEOUT</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>"120000"</td>
|
||||
<td>Sync server timeout in milliseconds</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_SYNC_SYNCPROXY</code>
|
||||
<td><code spellcheck="false">TRILIUM_SYNC_SYNCPROXY</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
@@ -231,42 +234,42 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL</code>
|
||||
<td><code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>OAuth/OpenID base URL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID</code>
|
||||
<td><code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>OAuth client ID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET</code>
|
||||
<td><code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>OAuth client secret</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL</code>
|
||||
<td><code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>"<a href="https://accounts.google.com">https://accounts.google.com</a>"</td>
|
||||
<td>OAuth issuer base URL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME</code>
|
||||
<td><code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>"Google"</td>
|
||||
<td>OAuth issuer display name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON</code>
|
||||
<td><code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
@@ -287,7 +290,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>TRILIUM_LOGGING_RETENTIONDAYS</code>
|
||||
<td><code spellcheck="false">TRILIUM_LOGGING_RETENTIONDAYS</code>
|
||||
</td>
|
||||
<td>integer</td>
|
||||
<td>90</td>
|
||||
@@ -301,38 +304,59 @@
|
||||
and work identically to their longer counterparts:</p>
|
||||
<h3>Network CORS Variables</h3>
|
||||
<ul>
|
||||
<li><code>TRILIUM_NETWORK_CORS_ALLOW_ORIGIN</code> (alternative to <code>TRILIUM_NETWORK_CORSALLOWORIGIN</code>)</li>
|
||||
<li><code>TRILIUM_NETWORK_CORS_ALLOW_METHODS</code> (alternative to <code>TRILIUM_NETWORK_CORSALLOWMETHODS</code>)</li>
|
||||
<li><code>TRILIUM_NETWORK_CORS_ALLOW_HEADERS</code> (alternative to <code>TRILIUM_NETWORK_CORSALLOWHEADERS</code>)</li>
|
||||
<li><code>TRILIUM_NETWORK_CORS_RESOURCE_POLICY</code> (alternative to <code>TRILIUM_NETWORK_CORSRESOURCEPOLICY</code>)</li>
|
||||
<li><code spellcheck="false">TRILIUM_NETWORK_CORS_ALLOW_ORIGIN</code> (alternative
|
||||
to <code spellcheck="false">TRILIUM_NETWORK_CORSALLOWORIGIN</code>)</li>
|
||||
<li><code spellcheck="false">TRILIUM_NETWORK_CORS_ALLOW_METHODS</code> (alternative
|
||||
to <code spellcheck="false">TRILIUM_NETWORK_CORSALLOWMETHODS</code>)</li>
|
||||
<li><code spellcheck="false">TRILIUM_NETWORK_CORS_ALLOW_HEADERS</code> (alternative
|
||||
to <code spellcheck="false">TRILIUM_NETWORK_CORSALLOWHEADERS</code>)</li>
|
||||
<li><code spellcheck="false">TRILIUM_NETWORK_CORS_RESOURCE_POLICY</code> (alternative
|
||||
to <code spellcheck="false">TRILIUM_NETWORK_CORSRESOURCEPOLICY</code>)</li>
|
||||
</ul>
|
||||
<h3>Sync Variables</h3>
|
||||
<ul>
|
||||
<li><code>TRILIUM_SYNC_SERVER_HOST</code> (alternative to <code>TRILIUM_SYNC_SYNCSERVERHOST</code>)</li>
|
||||
<li><code>TRILIUM_SYNC_SERVER_TIMEOUT</code> (alternative to <code>TRILIUM_SYNC_SYNCSERVERTIMEOUT</code>)</li>
|
||||
<li><code>TRILIUM_SYNC_SERVER_PROXY</code> (alternative to <code>TRILIUM_SYNC_SYNCPROXY</code>)</li>
|
||||
<li><code spellcheck="false">TRILIUM_SYNC_SERVER_HOST</code> (alternative to
|
||||
<code
|
||||
spellcheck="false">TRILIUM_SYNC_SYNCSERVERHOST</code>)</li>
|
||||
<li><code spellcheck="false">TRILIUM_SYNC_SERVER_TIMEOUT</code> (alternative
|
||||
to <code spellcheck="false">TRILIUM_SYNC_SYNCSERVERTIMEOUT</code>)</li>
|
||||
<li><code spellcheck="false">TRILIUM_SYNC_SERVER_PROXY</code> (alternative
|
||||
to <code spellcheck="false">TRILIUM_SYNC_SYNCPROXY</code>)</li>
|
||||
</ul>
|
||||
<h3>OAuth/MFA Variables</h3>
|
||||
<ul>
|
||||
<li><code>TRILIUM_OAUTH_BASE_URL</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL</code>)</li>
|
||||
<li><code>TRILIUM_OAUTH_CLIENT_ID</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID</code>)</li>
|
||||
<li><code>TRILIUM_OAUTH_CLIENT_SECRET</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET</code>)</li>
|
||||
<li><code>TRILIUM_OAUTH_ISSUER_BASE_URL</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL</code>)</li>
|
||||
<li><code>TRILIUM_OAUTH_ISSUER_NAME</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME</code>)</li>
|
||||
<li><code>TRILIUM_OAUTH_ISSUER_ICON</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON</code>)</li>
|
||||
<li><code spellcheck="false">TRILIUM_OAUTH_BASE_URL</code> (alternative to
|
||||
<code
|
||||
spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL</code>)</li>
|
||||
<li><code spellcheck="false">TRILIUM_OAUTH_CLIENT_ID</code> (alternative to
|
||||
<code
|
||||
spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID</code>)</li>
|
||||
<li><code spellcheck="false">TRILIUM_OAUTH_CLIENT_SECRET</code> (alternative
|
||||
to <code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET</code>)</li>
|
||||
<li><code spellcheck="false">TRILIUM_OAUTH_ISSUER_BASE_URL</code> (alternative
|
||||
to <code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL</code>)</li>
|
||||
<li><code spellcheck="false">TRILIUM_OAUTH_ISSUER_NAME</code> (alternative
|
||||
to <code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME</code>)</li>
|
||||
<li><code spellcheck="false">TRILIUM_OAUTH_ISSUER_ICON</code> (alternative
|
||||
to <code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON</code>)</li>
|
||||
</ul>
|
||||
<h3>Logging Variables</h3>
|
||||
<ul>
|
||||
<li><code>TRILIUM_LOGGING_RETENTION_DAYS</code> (alternative to <code>TRILIUM_LOGGING_RETENTIONDAYS</code>)</li>
|
||||
<li><code spellcheck="false">TRILIUM_LOGGING_RETENTION_DAYS</code> (alternative
|
||||
to <code spellcheck="false">TRILIUM_LOGGING_RETENTIONDAYS</code>)</li>
|
||||
</ul>
|
||||
<h2>Boolean Values</h2>
|
||||
<p>Boolean environment variables accept the following values:</p>
|
||||
<ul>
|
||||
<li><strong>True</strong>: <code>"true"</code>, <code>"1"</code>, <code>1</code>
|
||||
<li><strong>True</strong>: <code spellcheck="false">"true"</code>, <code spellcheck="false">"1"</code>,
|
||||
<code
|
||||
spellcheck="false">1</code>
|
||||
</li>
|
||||
<li><strong>False</strong>: <code>"false"</code>, <code>"0"</code>, <code>0</code>
|
||||
<li><strong>False</strong>: <code spellcheck="false">"false"</code>, <code spellcheck="false">"0"</code>,
|
||||
<code
|
||||
spellcheck="false">0</code>
|
||||
</li>
|
||||
<li>Any other value defaults to <code>false</code>
|
||||
<li>Any other value defaults to <code spellcheck="false">false</code>
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Using Environment Variables</h2>
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
<p>By default, Trilium cannot be accessed in web browsers by requests coming
|
||||
from other domains/origins than Trilium itself. </p>
|
||||
<p>However, it is possible to manually configure <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS">Cross-Origin Resource Sharing (CORS)</a> since
|
||||
Trilium v0.93.0 using environment variables or <code>config.ini</code>,
|
||||
Trilium v0.93.0 using environment variables or <code spellcheck="false">config.ini</code>,
|
||||
as follows:</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>CORS Header</th>
|
||||
<th>Corresponding option in <code>config.ini</code>
|
||||
<th>Corresponding option in <code spellcheck="false">config.ini</code>
|
||||
</th>
|
||||
<th>Corresponding option in environment variables in the <code>Network</code> section</th>
|
||||
<th>Corresponding option in environment variables in the <code spellcheck="false">Network</code> section</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>Access-Control-Allow-Origin</code>
|
||||
<td><code spellcheck="false">Access-Control-Allow-Origin</code>
|
||||
</td>
|
||||
<td><code>TRILIUM_NETWORK_CORS_ALLOW_ORIGIN</code>
|
||||
<td><code spellcheck="false">TRILIUM_NETWORK_CORS_ALLOW_ORIGIN</code>
|
||||
</td>
|
||||
<td><code>corsAllowOrigin</code>
|
||||
<td><code spellcheck="false">corsAllowOrigin</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Access-Control-Allow-Methods</code>
|
||||
<td><code spellcheck="false">Access-Control-Allow-Methods</code>
|
||||
</td>
|
||||
<td><code>TRILIUM_NETWORK_CORS_ALLOW_METHODS</code>
|
||||
<td><code spellcheck="false">TRILIUM_NETWORK_CORS_ALLOW_METHODS</code>
|
||||
</td>
|
||||
<td><code>corsAllowMethods</code>
|
||||
<td><code spellcheck="false">corsAllowMethods</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Access-Control-Allow-Headers</code>
|
||||
<td><code spellcheck="false">Access-Control-Allow-Headers</code>
|
||||
</td>
|
||||
<td><code>TRILIUM_NETWORK_CORS_ALLOW_HEADERS</code>
|
||||
<td><code spellcheck="false">TRILIUM_NETWORK_CORS_ALLOW_HEADERS</code>
|
||||
</td>
|
||||
<td><code>corsAllowHeaders</code>
|
||||
<td><code spellcheck="false">corsAllowHeaders</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -4,14 +4,15 @@
|
||||
is set up with), sometimes it can be useful to distinguish the instance
|
||||
you are running on.</p>
|
||||
<h2>Setting the instance name</h2>
|
||||
<p>To set up a name for the instance, modify the <code>config.ini</code>:</p><pre><code class="language-text-x-trilium-auto">[General]
|
||||
<p>To set up a name for the instance, modify the <code spellcheck="false">config.ini</code>:</p><pre><code class="language-text-x-trilium-auto">[General]
|
||||
instanceName=Hello</code></pre>
|
||||
<h2>Distinguishing the instance on back-end</h2>
|
||||
<p>Use <code>api.getInstanceName()</code> to obtain the instance name of the
|
||||
current server, as specified in the config file or in environment variables.</p>
|
||||
<p>Use <code spellcheck="false">api.getInstanceName()</code> to obtain the
|
||||
instance name of the current server, as specified in the config file or
|
||||
in environment variables.</p>
|
||||
<h2>Limiting script runs based on instance</h2>
|
||||
<p>For a script that is run periodically or on a certain event, it's possible
|
||||
to limit it to certain instances without having to change the code. Just
|
||||
add <code>runOnInstance</code> and set as the value the instance name where
|
||||
the script should run. To run on multiple named instances, simply add the
|
||||
label multiple times.</p>
|
||||
add <code spellcheck="false">runOnInstance</code> and set as the value the
|
||||
instance name where the script should run. To run on multiple named instances,
|
||||
simply add the label multiple times.</p>
|
||||
@@ -1,7 +1,7 @@
|
||||
<p>Trilium provides a mechanism for <a href="#root/_help_CdNpE2pqjmI6">scripts</a> to
|
||||
open a public REST endpoint. This opens a way for various integrations
|
||||
with other services - a simple example would be creating new note from
|
||||
Slack by issuing a slash command (e.g. <code>/trilium buy milk</code>).</p>
|
||||
Slack by issuing a slash command (e.g. <code spellcheck="false">/trilium buy milk</code>).</p>
|
||||
<h2>Create note from outside Trilium</h2>
|
||||
<p>Let's take a look at an example. The goal is to provide a REST endpoint
|
||||
to which we can send title and content and Trilium will create a note.</p>
|
||||
@@ -24,10 +24,12 @@ else {
|
||||
}</code></pre>
|
||||
<p>This script note has also following two attributes:</p>
|
||||
<ul>
|
||||
<li>label <code>#customRequestHandler</code> with value <code>create-note</code>
|
||||
<li>label <code spellcheck="false">#customRequestHandler</code> with value
|
||||
<code
|
||||
spellcheck="false">create-note</code>
|
||||
</li>
|
||||
<li>relation <code>~targetNote</code> pointing to a note where new notes should
|
||||
be saved</li>
|
||||
<li>relation <code spellcheck="false">~targetNote</code> pointing to a note
|
||||
where new notes should be saved</li>
|
||||
</ul>
|
||||
<h3>Explanation</h3>
|
||||
<p>Let's test this by using an HTTP client to send a request:</p><pre><code class="language-text-x-trilium-auto">POST http://your-trilium-server/custom/create-note
|
||||
@@ -38,16 +40,18 @@ Content-Type: application/json
|
||||
"title": "hello",
|
||||
"content": "world"
|
||||
}+++++++++++++++++++++++++++++++++++++++++++++++</code></pre>
|
||||
<p>Notice the <code>/custom</code> part in the request path - Trilium considers
|
||||
any request with this prefix as "custom" and tries to find a matching handler
|
||||
by looking at all notes which have <code>customRequestHandler</code> <a href="#root/_help_zEY4DaJG4YT5">label</a>.
|
||||
Value of this label then contains a regular expression which will match
|
||||
the request path (in our case trivial regex "create-note").</p>
|
||||
<p>Trilium will then find our code note created above and execute it. <code>api.req</code>, <code>api.res</code> are
|
||||
set to <a href="https://expressjs.com/en/api.html#req">request</a> and
|
||||
<p>Notice the <code spellcheck="false">/custom</code> part in the request path
|
||||
- Trilium considers any request with this prefix as "custom" and tries
|
||||
to find a matching handler by looking at all notes which have <code spellcheck="false">customRequestHandler</code>
|
||||
<a
|
||||
href="https://expressjs.com/en/api.html#res">response</a>objects from which we can get details of the request and also
|
||||
respond.</p>
|
||||
href="#root/_help_zEY4DaJG4YT5">label</a>. Value of this label then contains a regular expression which
|
||||
will match the request path (in our case trivial regex "create-note").</p>
|
||||
<p>Trilium will then find our code note created above and execute it.
|
||||
<code
|
||||
spellcheck="false">api.req</code>, <code spellcheck="false">api.res</code> are set to <a href="https://expressjs.com/en/api.html#req">request</a> and
|
||||
<a
|
||||
href="https://expressjs.com/en/api.html#res">response</a>objects from which we can get details of the request and also
|
||||
respond.</p>
|
||||
<p>In the code note we check the request method and then use trivial authentication
|
||||
- keep in mind that these endpoints are by default totally unauthenticated,
|
||||
and you need to take care of this yourself.</p>
|
||||
@@ -56,20 +60,21 @@ Content-Type: application/json
|
||||
href="#root/_help_GLks18SNjxmC">Script API</a>.</p>
|
||||
<h2>Custom resource provider</h2>
|
||||
<p>Another common use case is that you want to just expose a file note -
|
||||
in such case you create label <code>customResourceProvider</code> (value
|
||||
in such case you create label <code spellcheck="false">customResourceProvider</code> (value
|
||||
is again path regex).</p>
|
||||
<p>For more information, see <a href="#root/_help_d3fAXQ2diepH">Custom Resource Providers</a>.</p>
|
||||
<h2>Advanced concepts</h2>
|
||||
<p><code>api.req</code> and <code>api.res</code> are Express.js objects - you
|
||||
can always look into its <a href="https://expressjs.com/en/api.html">documentation</a> for
|
||||
<p><code spellcheck="false">api.req</code> and <code spellcheck="false">api.res</code> are
|
||||
Express.js objects - you can always look into its <a href="https://expressjs.com/en/api.html">documentation</a> for
|
||||
details.</p>
|
||||
<h3>Parameters</h3>
|
||||
<p>REST request paths often contain parameters in the URL, e.g.:</p><pre><code class="language-text-x-trilium-auto">http://your-trilium-server/custom/notes/123</code></pre>
|
||||
<p>The last part is dynamic so the matching of the URL must also be dynamic
|
||||
- for this reason the matching is done with regular expressions. Following <code>customRequestHandler</code> value
|
||||
would match it:</p><pre><code class="language-text-x-trilium-auto">notes/([0-9]+)</code></pre>
|
||||
- for this reason the matching is done with regular expressions. Following
|
||||
<code
|
||||
spellcheck="false">customRequestHandler</code>value would match it:</p><pre><code class="language-text-x-trilium-auto">notes/([0-9]+)</code></pre>
|
||||
<p>Additionally, this also defines a matching group with the use of parenthesis
|
||||
which then makes it easier to extract the value. The matched groups are
|
||||
available in <code>api.pathParams</code>:</p><pre><code class="language-text-x-trilium-auto">const noteId = api.pathParams[0];</code></pre>
|
||||
<p>Often you also need query params (as in e.g. <code>http://your-trilium-server/custom/notes?noteId=123</code>),
|
||||
you can get those with standard express <code>req.query.noteId</code>.</p>
|
||||
available in <code spellcheck="false">api.pathParams</code>:</p><pre><code class="language-text-x-trilium-auto">const noteId = api.pathParams[0];</code></pre>
|
||||
<p>Often you also need query params (as in e.g. <code spellcheck="false">http://your-trilium-server/custom/notes?noteId=123</code>),
|
||||
you can get those with standard express <code spellcheck="false">req.query.noteId</code>.</p>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user