Compare commits

..

113 Commits

Author SHA1 Message Date
renovate[bot]
cafcdf838f chore(deps): update dependency https-proxy-agent to v8 2026-03-12 01:28:09 +00:00
Elian Doran
caa428c1a2 Translations update from Hosted Weblate (#8990) 2026-03-11 21:44:21 +02:00
Hosted Weblate
517c721664 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/
2026-03-11 20:39:34 +01:00
green
a8cdaa69f7 Translated using Weblate (Japanese)
Currently translated at 100.0% (1693 of 1693 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-03-11 20:39:33 +01:00
Luk On
53d221ef34 Translated using Weblate (Polish)
Currently translated at 100.0% (1676 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pl/
2026-03-11 20:39:33 +01:00
pythaac
5450fde472 Translated using Weblate (Korean)
Currently translated at 93.1% (108 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/ko/
2026-03-11 20:39:33 +01:00
pythaac
808446cef5 Translated using Weblate (Korean)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ko/
2026-03-11 20:39:33 +01:00
ibs-allaow
921c663199 Translated using Weblate (Arabic)
Currently translated at 57.2% (959 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ar/
2026-03-11 20:39:33 +01:00
Микола Копитін
1b8a75b615 Translated using Weblate (Ukrainian)
Currently translated at 98.2% (114 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/uk/
2026-03-11 20:39:33 +01:00
Микола Копитін
f78ced5bc3 Translated using Weblate (Ukrainian)
Currently translated at 99.3% (157 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/uk/
2026-03-11 20:39:33 +01:00
JYC333
81bf5f4f3b Translated using Weblate (Swedish)
Currently translated at 17.6% (21 of 119 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/sv/
2026-03-11 20:39:33 +01:00
Hosted Weblate
aaed368670 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-03-11 20:39:33 +01:00
Elian Doran
5e8de14721 Audio player improvements (#9008) 2026-03-11 21:38:58 +02:00
Elian Doran
634ab5b5c0 chore(media): address requested changes 2026-03-11 21:26:24 +02:00
Elian Doran
906889a035 fix(video): auto-hide no longer working 2026-03-11 21:07:48 +02:00
Elian Doran
ab9d50b905 feat(video): add blur for controls 2026-03-11 21:06:08 +02:00
Elian Doran
e61b7c7cfc feat(audio): add background effects 2026-03-11 20:58:49 +02:00
Elian Doran
1c628fba4c fix(video): playing button not working 2026-03-11 20:53:08 +02:00
Elian Doran
f8b4c6cb15 fix(audio): styling on light theme 2026-03-11 20:41:37 +02:00
Elian Doran
3edd8f6c5a chore(media): solve linter warnings 2026-03-11 20:38:53 +02:00
Elian Doran
7777f72893 chore(media): change translations prefix 2026-03-11 20:36:51 +02:00
Elian Doran
9af85b767b feat(audio): add an icon placeholder 2026-03-11 20:35:41 +02:00
Elian Doran
73260b91eb feat(audio): add mime to unsupported format 2026-03-11 20:29:34 +02:00
Elian Doran
2858f63873 feat(audio): report unsupported format 2026-03-11 19:30:12 +02:00
Elian Doran
15ca328727 feat(audio): make player full-width 2026-03-11 19:26:17 +02:00
Elian Doran
5b3fbecc0f feat(audio): introduce keyboard shortcuts 2026-03-11 19:24:47 +02:00
Elian Doran
365d0f0aac feat(audio): introduce playback speed 2026-03-11 19:17:14 +02:00
Elian Doran
e86d84c463 feat(audio): introduce loop button 2026-03-11 19:14:52 +02:00
Elian Doran
6b974c2ac7 feat(audio): introduce skip buttons 2026-03-11 19:12:09 +02:00
Elian Doran
d2afcbb98d feat(audio): introduce volume slider 2026-03-11 19:10:18 +02:00
Elian Doran
68a122fcf5 chore(audio): reintroduce some styles 2026-03-11 19:06:59 +02:00
Elian Doran
92f0144b48 feat(audio): reintroduce seek bar 2026-03-11 19:03:09 +02:00
Elian Doran
a5a345728c feat(audio): reintroduce play button 2026-03-11 18:56:43 +02:00
Elian Doran
23890e64e9 refactor(audio): extract to separate file 2026-03-11 18:51:20 +02:00
Elian Doran
3de712aca4 fix(server/search): invalid canvas crashing search (closes #9004) 2026-03-11 18:32:53 +02:00
Elian Doran
cb5b4d870f refactor(server/search): extract fulltext preprocessing to separate file 2026-03-11 18:29:36 +02:00
Elian Doran
f81aef2de5 docs(user): specify spreadsheets 2026-03-11 18:01:22 +02:00
Elian Doran
06aed16ea1 refactor(spreadsheet): simplify the checks for popups 2026-03-11 12:11:00 +02:00
Elian Doran
aa2d8af15c fix(spreadsheet): popups show up and hide 2026-03-11 12:10:46 +02:00
Elian Doran
dc7b91433b docs(user): mention changes to video player 2026-03-11 09:44:24 +02:00
Elian Doran
72951386b1 Video player improvements (#8992) 2026-03-11 08:33:04 +02:00
Elian Doran
db8df01d82 fix(deps): update dependency eslint-linter-browserify to v10.0.3 (#8948) 2026-03-11 08:32:06 +02:00
Elian Doran
98713ed111 chore(deps): update dependency lint-staged to v16.3.3 (#8997) 2026-03-11 08:31:29 +02:00
Elian Doran
3e88fecb15 chore(deps): update dependency yauzl to v3.2.1 (#8998) 2026-03-11 08:31:12 +02:00
Elian Doran
fe4255f2fc fix(deps): update dependency @codemirror/view to v6.39.17 (#8999) 2026-03-11 08:31:00 +02:00
Elian Doran
c046a57654 chore(deps): update dependency webdriverio to v9.25.0 (#9000) 2026-03-11 08:30:47 +02:00
Elian Doran
d8fc0d45a8 fix(deps): update dependency @zumer/snapdom to v2.1.0 (#9001) 2026-03-11 08:30:01 +02:00
Elian Doran
567b96cfb4 fix(deps): update dependency preact to v10.29.0 (#9002) 2026-03-11 08:29:34 +02:00
renovate[bot]
d25849d280 fix(deps): update dependency preact to v10.29.0 2026-03-11 00:06:32 +00:00
renovate[bot]
d4d73995db fix(deps): update dependency @zumer/snapdom to v2.1.0 2026-03-11 00:05:36 +00:00
renovate[bot]
f4657b5da9 chore(deps): update dependency webdriverio to v9.25.0 2026-03-11 00:04:47 +00:00
renovate[bot]
614f43cb8a fix(deps): update dependency @codemirror/view to v6.39.17 2026-03-11 00:04:00 +00:00
renovate[bot]
ca2fbf8dba chore(deps): update dependency yauzl to v3.2.1 2026-03-11 00:03:14 +00:00
renovate[bot]
a421513442 chore(deps): update dependency lint-staged to v16.3.3 2026-03-11 00:02:25 +00:00
JYC333
a9599c471a Merge branch 'main' into renovate/eslint-linter-browserify-10.x 2026-03-10 23:56:13 +00:00
JYC333
415bcac641 chore(deps): update dependency lightningcss to v1.32.0 (#8985) 2026-03-10 23:54:00 +00:00
JYC333
9527017314 fix(deps): update dependency mind-elixir to v5.9.3 (#8984) 2026-03-10 23:53:12 +00:00
JYC333
1d3d7c77f8 fix(deps): update dependency i18next to v25.8.17 (#8983) 2026-03-10 23:52:55 +00:00
Elian Doran
e868615fd5 chore(client): address requested changes 2026-03-10 22:19:50 +02:00
Elian Doran
80493a52be feat(video_player): move loop to center section 2026-03-10 20:46:28 +02:00
Elian Doran
3fed2ba42e feat(video_player): add zoom to fit button 2026-03-10 20:44:32 +02:00
Elian Doran
82592ada54 fix(video_player): unreadable controls on light theme 2026-03-10 20:36:03 +02:00
Elian Doran
5528701744 feat(video_player): indicate unsupported file formats 2026-03-10 20:33:47 +02:00
Elian Doran
0ca665fb85 chore(video_player): mention keys 2026-03-10 20:24:16 +02:00
Elian Doran
7eb452ed8b refactor(video_player): use translations 2026-03-10 20:22:03 +02:00
Elian Doran
d81dec94a9 feat(video_player): add keyboard shortcuts for toggling volume 2026-03-10 20:18:16 +02:00
Elian Doran
6631a4a806 feat(video_player): add shortcuts to just to beginning/end 2026-03-10 20:16:53 +02:00
Elian Doran
12f817c896 feat(video_player): add keyboard shortcut to toggle mute 2026-03-10 20:16:04 +02:00
Elian Doran
87229600d2 feat(video_player): keyboard shortcut to toggle full-screen 2026-03-10 20:15:10 +02:00
Elian Doran
471a46a030 feat(video_player): flash controls when pressing shortcuts 2026-03-10 20:14:11 +02:00
Elian Doran
41220eebd5 feat(video_player): arrow keys to seek 2026-03-10 20:11:56 +02:00
Elian Doran
755872277b feat(video_player): space to toggle play/pause 2026-03-10 20:10:40 +02:00
Elian Doran
2cb54d7021 fix(video_player): loop can get out of sync with external control 2026-03-10 20:09:33 +02:00
Elian Doran
5a16bafbbf fix(video_player): playback speed can get out of sync with external control 2026-03-10 20:08:17 +02:00
Elian Doran
fc6e9d89d9 fix(video_player): volume can get out of sync with external control 2026-03-10 20:07:45 +02:00
Elian Doran
8af35da279 feat(video_player): add loop button 2026-03-10 20:05:40 +02:00
Elian Doran
7107fec1a4 feat(video_player): add rotate button 2026-03-10 20:03:58 +02:00
Elian Doran
4bb662c5fb feat(video_player): button to toggle PIP 2026-03-10 20:00:38 +02:00
Elian Doran
89297b92f8 feat(video_player): click toggles play/pause instead of controls 2026-03-10 19:53:24 +02:00
Elian Doran
e019271e74 feat(video_player): hide immediately on play 2026-03-10 19:50:31 +02:00
Elian Doran
f6d61eefcc feat(video_player): don't hide controls if not playing 2026-03-10 19:48:21 +02:00
Elian Doran
fabc07be42 refactor(video_player): extract hiding visibility to hook 2026-03-10 19:47:25 +02:00
Elian Doran
bccfa7956c refactor(video_player): extract more buttons into separate components 2026-03-10 19:45:42 +02:00
Elian Doran
42a05f411b feat(video_player): basic toggle of the controls 2026-03-10 19:42:54 +02:00
Elian Doran
7ba7b98f5f feat(video_player): add playback speed indicator 2026-03-10 19:38:15 +02:00
Elian Doran
2132c2ab38 refactor(video_player): extract full screen to separate component 2026-03-10 19:29:00 +02:00
Elian Doran
2ce4d512e7 feat(video_player): add full screen button 2026-03-10 19:23:45 +02:00
Elian Doran
1258d32820 feat(video_player): add skip left/right buttons 2026-03-10 19:22:29 +02:00
Elian Doran
db763ba229 feat(video_player): improve style of bottom bar 2026-03-10 19:20:49 +02:00
Elian Doran
951fdaec70 chore(video_player): change button alignment 2026-03-10 19:17:51 +02:00
Elian Doran
4303f3687e refactor(video_player): extract seek bar & volume control 2026-03-10 19:12:52 +02:00
Elian Doran
540b0e0b83 feat(video_player): volume changer 2026-03-10 19:11:08 +02:00
Elian Doran
08a0326cb0 feat(video_player): add elapsed/remaining time 2026-03-10 19:05:59 +02:00
Elian Doran
8b0a45e4fd feat(video_player): add a trackbar for seeking the video 2026-03-10 18:57:58 +02:00
Elian Doran
0e0ad2ed73 feat(video_player): single play/pause button 2026-03-10 18:56:20 +02:00
Elian Doran
4c73f31aca feat(video_player): start adding custom controls (play/pause) 2026-03-10 18:54:53 +02:00
Elian Doran
6b2ae8fd12 feat(video_player): black background 2026-03-10 18:49:36 +02:00
Elian Doran
88d84fae1e refactor(video_player): extract to separate file 2026-03-10 18:48:54 +02:00
Elian Doran
cdc46faaad fix(board): add column not snappable on mobile 2026-03-10 18:41:53 +02:00
Elian Doran
24dbc79961 fix(board): clipped on horizontal scroll 2026-03-10 18:40:17 +02:00
Elian Doran
8cb58dcc45 fix(icon_packs): missing empty icon 2026-03-10 18:35:20 +02:00
Elian Doran
fe70b8aee6 fix(note_badges): saved indicator not disappearing if reduced motion was activated 2026-03-10 18:32:31 +02:00
Elian Doran
00f66cfb49 fix(popup_editor): note content no longer rendering
The commit f44b47ec added a hasTabBeenActive guard in NoteDetail that defers rendering until the tab has been active at least once. It initializes via noteContext?.isActive() and then listens for activeNoteChanged events.

The popup editor creates its own NoteContext("_popup-editor") which is never the activeNtxId in the tab manager — isActive() always returns false, and activeNoteChanged never fires for it. So hasTabBeenActive stays false forever, and the if (!type || !hasTabBeenActive) return guard at NoteDetail.tsx:64 prevents the note type widget from ever loading.
2026-03-10 18:32:31 +02:00
Elian Doran
3a4b080765 Table of contents fixes (#8933) 2026-03-10 18:31:24 +02:00
renovate[bot]
a06fa5222f chore(deps): update dependency lightningcss to v1.32.0 2026-03-10 02:12:35 +00:00
renovate[bot]
8d3e40a28a fix(deps): update dependency mind-elixir to v5.9.3 2026-03-10 02:11:34 +00:00
renovate[bot]
8e32f99790 fix(deps): update dependency i18next to v25.8.17 2026-03-10 02:10:34 +00:00
Elian Doran
aac4774326 Merge remote-tracking branch 'origin/main' into feature/toc_improvements 2026-03-08 12:20:53 +02:00
renovate[bot]
7f32fe5ef7 fix(deps): update dependency eslint-linter-browserify to v10.0.3 2026-03-07 02:15:20 +00:00
Elian Doran
d3337eab9c Merge branch 'main' into feature/toc_improvements 2026-03-05 21:05:17 +02:00
Elian Doran
8128a8192a refactor(ckeditor): address requested changes 2026-03-05 19:28:52 +02:00
Elian Doran
65514a6fd7 fix(toc): title is extracted before changes are made 2026-03-05 19:08:56 +02:00
Elian Doran
93a7f8c711 fix(toc): not reacting to attribute changes in CKEditor 2026-03-05 19:03:32 +02:00
81 changed files with 2302 additions and 1842 deletions

View File

@@ -43,7 +43,7 @@
"@univerjs/preset-sheets-note": "0.16.1",
"@univerjs/preset-sheets-sort": "0.16.1",
"@univerjs/presets": "0.16.1",
"@zumer/snapdom": "2.0.2",
"@zumer/snapdom": "2.1.0",
"autocomplete.js": "0.38.1",
"bootstrap": "5.3.8",
"boxicons": "2.1.4",
@@ -53,7 +53,7 @@
"draggabilly": "3.0.0",
"force-graph": "1.51.1",
"globals": "17.4.0",
"i18next": "25.8.14",
"i18next": "25.8.17",
"i18next-http-backend": "3.0.2",
"jquery": "4.0.0",
"jquery.fancytree": "2.38.5",
@@ -65,10 +65,10 @@
"mark.js": "8.11.1",
"marked": "17.0.4",
"mermaid": "11.12.3",
"mind-elixir": "5.9.2",
"mind-elixir": "5.9.3",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.28.4",
"preact": "10.29.0",
"react-i18next": "16.5.6",
"react-window": "2.2.7",
"reveal.js": "5.2.1",
@@ -89,7 +89,7 @@
"@types/tabulator-tables": "6.3.1",
"copy-webpack-plugin": "14.0.0",
"happy-dom": "20.8.3",
"lightningcss": "1.31.1",
"lightningcss": "1.32.0",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "3.2.0"
}

View File

@@ -803,12 +803,13 @@
"web-view": "عرض الويب",
"mind-map": "خريطة ذهنية",
"geo-map": "خريطة جغرافية",
"task-list": "قائمة المهام"
"task-list": "قائمة المهام",
"spreadsheet": "جدول البيانات"
},
"shared_switch": {
"shared": "مشترك",
"toggle-on-title": "مشاركة الملاحظة",
"toggle-off-title": "الغاء مشاركة الملاحظة"
"toggle-off-title": "إلغاء مشاركة الملاحظة"
},
"template_switch": {
"template": "قالب"
@@ -1286,8 +1287,10 @@
"search-for": "بحث ل \"{{term}}\""
},
"protect_note": {
"toggle-off": "ازالة الحماية عن الملاحظة",
"toggle-on": "حماية الملاحظة"
"toggle-off": "إزالة الحماية عن الملاحظة",
"toggle-on": "حماية الملاحظة",
"toggle-on-hint": "الملاحظة غير محمة، انقر لحمايتها",
"toggle-off-hint": "الملاحظة محمية، انقر لإزالة الحماية منها"
},
"open-help-page": "فتح صفحة المساعدة",
"empty": {

View File

@@ -1036,6 +1036,25 @@
"file_preview_not_available": "File preview is not available for this file format.",
"too_big": "The preview only shows the first {{maxNumChars}} characters of the file for performance reasons. Download the file and open it externally to be able to see the entire content."
},
"media": {
"play": "Play (Space)",
"pause": "Pause (Space)",
"back-10s": "Back 10s (Left arrow key)",
"forward-30s": "Forward 30s",
"mute": "Mute (M)",
"unmute": "Unmute (M)",
"playback-speed": "Playback speed",
"loop": "Loop",
"disable-loop": "Disable loop",
"rotate": "Rotate",
"picture-in-picture": "Picture-in-picture",
"exit-picture-in-picture": "Exit picture-in-picture",
"fullscreen": "Fullscreen (F)",
"exit-fullscreen": "Exit fullscreen",
"unsupported-format": "Media preview is not available for this file format:\n{{mime}}",
"zoom-to-fit": "Zoom to fill",
"zoom-reset": "Reset zoom to fill"
},
"protected_session": {
"enter_password_instruction": "Showing protected note requires entering your password:",
"start_session_button": "Start protected session",

View File

@@ -1780,7 +1780,8 @@
"ai-chat": "Czat AI",
"task-list": "Lista zadań",
"new-feature": "Nowość",
"collections": "Kolekcje"
"collections": "Kolekcje",
"spreadsheet": "Arkusz"
},
"protect_note": {
"toggle-on": "Chroń notatkę",

View File

@@ -41,7 +41,9 @@ export default function NoteDetail() {
const hasFixedTree = note && noteContext?.hoistedNoteId === "_lbMobileRoot" && isMobile() && note.noteId.startsWith("_lbMobile");
// Defer loading for tabs that haven't been active yet (e.g. on app refresh).
const [ hasTabBeenActive, setHasTabBeenActive ] = useState(() => noteContext?.isActive() ?? false);
// Special contexts (ntxId starting with "_", e.g. popup editor) are always considered active.
const isSpecialContext = ntxId?.startsWith("_") ?? false;
const [ hasTabBeenActive, setHasTabBeenActive ] = useState(() => isSpecialContext || (noteContext?.isActive() ?? false));
useEffect(() => {
if (!hasTabBeenActive && noteContext?.isActive()) {
setHasTabBeenActive(true);

View File

@@ -14,8 +14,7 @@
height: 100%;
display: flex;
gap: 1em;
margin-inline: var(--content-margin-inline);
padding-block: 4px;
padding: 4px var(--content-margin-inline);
align-items: flex-start;
overflow-x: auto;
}
@@ -42,7 +41,11 @@ body.mobile .board-view-container {
body.mobile .board-view-container .board-column {
width: 75vw;
max-width: 300px;
scroll-snap-align: center;
}
body.mobile .board-view-container .board-column,
body.mobile .board-view-container .board-add-column {
scroll-snap-align: center;
}
.board-view-container .board-column.drag-over {

View File

@@ -36,6 +36,10 @@
animation: fadeOut 250ms ease-in 5s forwards;
pointer-events: none;
}
body#trilium-app.motion-disabled &.saved {
animation: fadeOut 0s 5s forwards !important;
}
}
&.active-content-badge { --color: var(--badge-active-content-background-color); }
&.active-content-badge.disabled {

View File

@@ -83,7 +83,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
return true;
}
if (note.type === "file" && (note.mime === "application/pdf" || note.mime.startsWith("video/"))) {
if (note.type === "file" && (note.mime === "application/pdf" || note.mime.startsWith("video/") || note.mime.startsWith("audio/"))) {
return true;
}
@@ -108,7 +108,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
return true;
}
if (note.type === "file" && MIME_TYPES_WITH_BACKGROUND_EFFECTS.includes(note.mime)) {
if (note.type === "file" && (MIME_TYPES_WITH_BACKGROUND_EFFECTS.includes(note.mime) || note.mime.startsWith("audio/"))) {
return true;
}

View File

@@ -8,6 +8,7 @@
color: var(--muted-text-color);
height: 100%;
text-align: center;
white-space: pre-line;
.tn-icon {
font-size: 4em;

View File

@@ -1,6 +1,6 @@
import "./TableOfContents.css";
import { CKTextEditor, ModelElement } from "@triliumnext/ckeditor5";
import { attributeChangeAffectsHeading, CKTextEditor, ModelElement } from "@triliumnext/ckeditor5";
import clsx from "clsx";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
@@ -170,11 +170,14 @@ function EditableTextTableOfContents() {
const affectsHeadings = changes.some( change => {
return (
change.type === 'insert' || change.type === 'remove' || (change.type === 'attribute' && change.attributeKey === 'headingLevel')
change.type === 'insert' || change.type === 'remove' ||
(change.type === 'attribute' && attributeChangeAffectsHeading(change, textEditor))
);
});
if (affectsHeadings) {
setHeadings(extractTocFromTextEditor(textEditor));
requestAnimationFrame(() => {
setHeadings(extractTocFromTextEditor(textEditor));
});
}
};

View File

@@ -24,8 +24,7 @@
margin: 10px;
}
.note-detail-file > .pdf-preview,
.note-detail-file > .video-preview {
.note-detail-file > .pdf-preview {
width: 100%;
height: 100%;
flex-grow: 100;
@@ -38,4 +37,4 @@
right: 15px;
width: calc(100% - 30px);
transform: translateY(-50%);
}
}

View File

@@ -1,11 +1,11 @@
import "./File.css";
import FNote from "../../entities/fnote";
import { t } from "../../services/i18n";
import { getUrlForDownload } from "../../services/open";
import Alert from "../react/Alert";
import { useNoteBlob } from "../react/hooks";
import AudioPreview from "./file/Audio";
import PdfPreview from "./file/Pdf";
import VideoPreview from "./file/Video";
import { TypeWidgetProps } from "./type_widget";
const TEXT_MAX_NUM_CHARS = 5000;
@@ -42,27 +42,6 @@ function TextPreview({ content }: { content: string }) {
);
}
function VideoPreview({ note }: { note: FNote }) {
return (
<video
class="video-preview"
src={getUrlForDownload(`api/notes/${note.noteId}/open-partial`)}
datatype={note?.mime}
controls
/>
);
}
function AudioPreview({ note }: { note: FNote }) {
return (
<audio
class="audio-preview"
src={getUrlForDownload(`api/notes/${note.noteId}/open-partial`)}
controls
/>
);
}
function NoPreview() {
return (
<Alert className="file-preview-not-available" type="info">

View File

@@ -0,0 +1,112 @@
import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks";
import FNote from "../../../entities/fnote";
import { t } from "../../../services/i18n";
import { getUrlForDownload } from "../../../services/open";
import Icon from "../../react/Icon";
import NoItems from "../../react/NoItems";
import { LoopButton, PlaybackSpeed, PlayPauseButton, SeekBar, SkipButton, VolumeControl } from "./MediaPlayer";
export default function AudioPreview({ note }: { note: FNote }) {
const wrapperRef = useRef<HTMLDivElement>(null);
const audioRef = useRef<HTMLAudioElement>(null);
const [playing, setPlaying] = useState(false);
const [error, setError] = useState(false);
const togglePlayback = useCallback(() => {
const audio = audioRef.current;
if (!audio) return;
if (audio.paused) {
audio.play();
} else {
audio.pause();
}
}, []);
const onKeyDown = useKeyboardShortcuts(audioRef, togglePlayback);
useEffect(() => setError(false), [note.noteId]);
const onError = useCallback(() => setError(true), []);
if (error) {
return <NoItems icon="bx bx-volume-mute" text={t("media.unsupported-format", { mime: note.mime.replace("/", "-") })} />;
}
return (
<div ref={wrapperRef} className="audio-preview-wrapper" onKeyDown={onKeyDown} tabIndex={0}>
<audio
class="audio-preview"
src={getUrlForDownload(`api/notes/${note.noteId}/open-partial`)}
ref={audioRef}
onPlay={() => setPlaying(true)}
onPause={() => setPlaying(false)}
onError={onError}
/>
<div className="audio-preview-icon-wrapper">
<Icon icon="bx bx-music" className="audio-preview-icon" />
</div>
<div className="media-preview-controls">
<SeekBar mediaRef={audioRef} />
<div class="media-buttons-row">
<div className="left">
<PlaybackSpeed mediaRef={audioRef} />
</div>
<div className="center">
<div className="spacer" />
<SkipButton mediaRef={audioRef} seconds={-10} icon="bx bx-rewind" text={t("media.back-10s")} />
<PlayPauseButton playing={playing} togglePlayback={togglePlayback} />
<SkipButton mediaRef={audioRef} seconds={30} icon="bx bx-fast-forward" text={t("media.forward-30s")} />
<LoopButton mediaRef={audioRef} />
</div>
<div className="right">
<VolumeControl mediaRef={audioRef} />
</div>
</div>
</div>
</div>
);
}
function useKeyboardShortcuts(audioRef: MutableRef<HTMLAudioElement | null>, togglePlayback: () => void) {
return useCallback((e: KeyboardEvent) => {
const audio = audioRef.current;
if (!audio) return;
switch (e.key) {
case " ":
e.preventDefault();
togglePlayback();
break;
case "ArrowLeft":
e.preventDefault();
audio.currentTime = Math.max(0, audio.currentTime - (e.ctrlKey ? 60 : 10));
break;
case "ArrowRight":
e.preventDefault();
audio.currentTime = Math.min(audio.duration, audio.currentTime + (e.ctrlKey ? 60 : 10));
break;
case "m":
case "M":
e.preventDefault();
audio.muted = !audio.muted;
break;
case "ArrowUp":
e.preventDefault();
audio.volume = Math.min(1, audio.volume + 0.05);
break;
case "ArrowDown":
e.preventDefault();
audio.volume = Math.max(0, audio.volume - 0.05);
break;
case "Home":
e.preventDefault();
audio.currentTime = 0;
break;
case "End":
e.preventDefault();
audio.currentTime = audio.duration;
break;
}
}, [ audioRef, togglePlayback ]);
}

View File

@@ -0,0 +1,98 @@
.media-preview-controls {
padding: 1.25em;
display: flex;
flex-direction: column;
gap: 0.5em;
.media-buttons-row {
display: flex;
> * {
flex: 1;
align-items: center;
gap: 0.5em;
display: flex;
}
.spacer {
width: var(--icon-button-size, 32px);
height: var(--icon-button-size, 32px);
}
.center {
justify-content: center;
}
.right {
display: flex;
justify-content: flex-end;
}
.play-button {
--icon-button-size: 48px;
}
}
.media-seekbar-row {
display: flex;
align-items: center;
gap: 0.5em;
.media-time {
font-size: 0.85em;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.media-trackbar {
flex: 1;
cursor: pointer;
}
}
.media-volume-row {
display: flex;
align-items: center;
gap: 0.25em;
.media-volume-slider {
width: 80px;
cursor: pointer;
}
}
.speed-dropdown {
position: relative;
.tn-icon {
transform: translateY(-10%);
}
.media-speed-label {
position: absolute;
bottom: 0;
left: 0;
right: 0;
transform: translateY(15%);
text-align: center;
font-size: 0.6rem;
font-variant-numeric: tabular-nums;
}
}
}
.audio-preview-wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.audio-preview-icon-wrapper {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 8em;
opacity: 0.6;
}
}

View File

@@ -0,0 +1,220 @@
import "./MediaPlayer.css";
import { RefObject } from "preact";
import { useEffect, useState } from "preact/hooks";
import { t } from "../../../services/i18n";
import ActionButton from "../../react/ActionButton";
import Dropdown from "../../react/Dropdown";
import Icon from "../../react/Icon";
export function SeekBar({ mediaRef }: { mediaRef: RefObject<HTMLVideoElement | HTMLAudioElement> }) {
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
useEffect(() => {
const media = mediaRef.current;
if (!media) return;
const onTimeUpdate = () => setCurrentTime(media.currentTime);
const onDurationChange = () => setDuration(media.duration);
media.addEventListener("timeupdate", onTimeUpdate);
media.addEventListener("durationchange", onDurationChange);
return () => {
media.removeEventListener("timeupdate", onTimeUpdate);
media.removeEventListener("durationchange", onDurationChange);
};
}, [ mediaRef ]);
const onSeek = (e: Event) => {
const media = mediaRef.current;
if (!media) return;
media.currentTime = parseFloat((e.target as HTMLInputElement).value);
};
return (
<div class="media-seekbar-row">
<span class="media-time">{formatTime(currentTime)}</span>
<input
type="range"
class="media-trackbar"
min={0}
max={duration || 0}
step={0.1}
value={currentTime}
onInput={onSeek}
/>
<span class="media-time">-{formatTime(Math.max(0, duration - currentTime))}</span>
</div>
);
}
function formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, "0")}`;
}
export function PlayPauseButton({ playing, togglePlayback }: {
playing: boolean,
togglePlayback: () => void
}) {
return (
<ActionButton
className="play-button"
icon={playing ? "bx bx-pause" : "bx bx-play"}
text={playing ? t("media.pause") : t("media.play")}
onClick={togglePlayback}
/>
);
}
export function VolumeControl({ mediaRef }: { mediaRef: RefObject<HTMLVideoElement | HTMLAudioElement> }) {
const [volume, setVolume] = useState(() => mediaRef.current?.volume ?? 1);
const [muted, setMuted] = useState(() => mediaRef.current?.muted ?? false);
// Sync state when the media element changes volume externally.
useEffect(() => {
const media = mediaRef.current;
if (!media) return;
setVolume(media.volume);
setMuted(media.muted);
const onVolumeChange = () => {
setVolume(media.volume);
setMuted(media.muted);
};
media.addEventListener("volumechange", onVolumeChange);
return () => media.removeEventListener("volumechange", onVolumeChange);
}, [ mediaRef ]);
const onVolumeChange = (e: Event) => {
const media = mediaRef.current;
if (!media) return;
const val = parseFloat((e.target as HTMLInputElement).value);
media.volume = val;
setVolume(val);
if (val > 0 && media.muted) {
media.muted = false;
setMuted(false);
}
};
const toggleMute = () => {
const media = mediaRef.current;
if (!media) return;
media.muted = !media.muted;
setMuted(media.muted);
};
return (
<div class="media-volume-row">
<ActionButton
icon={muted || volume === 0 ? "bx bx-volume-mute" : volume < 0.5 ? "bx bx-volume-low" : "bx bx-volume-full"}
text={muted ? t("media.unmute") : t("media.mute")}
onClick={toggleMute}
/>
<input
type="range"
class="media-volume-slider"
min={0}
max={1}
step={0.05}
value={muted ? 0 : volume}
onInput={onVolumeChange}
/>
</div>
);
}
export function SkipButton({ mediaRef, seconds, icon, text }: { mediaRef: RefObject<HTMLVideoElement | HTMLAudioElement>, seconds: number, icon: string, text: string }) {
const skip = () => {
const media = mediaRef.current;
if (!media) return;
media.currentTime = Math.max(0, Math.min(media.duration, media.currentTime + seconds));
};
return (
<ActionButton icon={icon} text={text} onClick={skip} />
);
}
export function LoopButton({ mediaRef }: { mediaRef: RefObject<HTMLVideoElement | HTMLAudioElement> }) {
const [loop, setLoop] = useState(() => mediaRef.current?.loop ?? false);
useEffect(() => {
const media = mediaRef.current;
if (!media) return;
setLoop(media.loop);
const observer = new MutationObserver(() => setLoop(media.loop));
observer.observe(media, { attributes: true, attributeFilter: ["loop"] });
return () => observer.disconnect();
}, [ mediaRef ]);
const toggle = () => {
const media = mediaRef.current;
if (!media) return;
media.loop = !media.loop;
setLoop(media.loop);
};
return (
<ActionButton
className={loop ? "active" : ""}
icon="bx bx-repeat"
text={loop ? t("media.disable-loop") : t("media.loop")}
onClick={toggle}
/>
);
}
const PLAYBACK_SPEEDS = [0.5, 1, 1.25, 1.5, 2];
export function PlaybackSpeed({ mediaRef }: { mediaRef: RefObject<HTMLVideoElement | HTMLAudioElement> }) {
const [speed, setSpeed] = useState(() => mediaRef.current?.playbackRate ?? 1);
useEffect(() => {
const media = mediaRef.current;
if (!media) return;
setSpeed(media.playbackRate);
const onRateChange = () => setSpeed(media.playbackRate);
media.addEventListener("ratechange", onRateChange);
return () => media.removeEventListener("ratechange", onRateChange);
}, [ mediaRef ]);
const selectSpeed = (rate: number) => {
const media = mediaRef.current;
if (!media) return;
media.playbackRate = rate;
setSpeed(rate);
};
return (
<Dropdown
iconAction
hideToggleArrow
buttonClassName="speed-dropdown"
text={<>
<Icon icon="bx bx-tachometer" />
<span class="media-speed-label">{speed}x</span>
</>}
title={t("media.playback-speed")}
>
{PLAYBACK_SPEEDS.map((rate) => (
<li key={rate}>
<button
class={`dropdown-item ${rate === speed ? "active" : ""}`}
onClick={() => selectSpeed(rate)}
>
{rate}x
</button>
</li>
))}
</Dropdown>
);
}

View File

@@ -0,0 +1,35 @@
.note-detail-file > .video-preview-wrapper {
width: 100%;
height: 100%;
position: relative;
background-color: black;
.video-preview {
background-color: black;
width: 100%;
height: 100%;
}
&.controls-hidden {
cursor: pointer;
.media-preview-controls {
opacity: 0;
pointer-events: none;
}
}
.media-preview-controls {
--icon-button-hover-color: white;
--icon-button-hover-background: rgba(255, 255, 255, 0.2);
opacity: 1;
transition: opacity 300ms ease;
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(6px);
color: white;
}
}

View File

@@ -0,0 +1,298 @@
import "./Video.css";
import { RefObject } from "preact";
import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks";
import FNote from "../../../entities/fnote";
import { t } from "../../../services/i18n";
import { getUrlForDownload } from "../../../services/open";
import ActionButton from "../../react/ActionButton";
import NoItems from "../../react/NoItems";
import { LoopButton, PlaybackSpeed, PlayPauseButton, SeekBar, SkipButton, VolumeControl } from "./MediaPlayer";
const AUTO_HIDE_DELAY = 3000;
export default function VideoPreview({ note }: { note: FNote }) {
const wrapperRef = useRef<HTMLDivElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const [playing, setPlaying] = useState(false);
const [error, setError] = useState(false);
const { visible: controlsVisible, onMouseMove, flash: flashControls } = useAutoHideControls(videoRef, playing);
useEffect(() => setError(false), [note.noteId]);
const onError = useCallback(() => setError(true), []);
const togglePlayback = useCallback(() => {
const video = videoRef.current;
if (!video) return;
if (video.paused) {
video.play();
} else {
video.pause();
}
}, []);
const onVideoClick = useCallback((e: MouseEvent) => {
if ((e.target as HTMLElement).closest(".media-preview-controls")) return;
togglePlayback();
}, [togglePlayback]);
const onKeyDown = useKeyboardShortcuts(videoRef, wrapperRef, togglePlayback, flashControls);
if (error) {
return <NoItems icon="bx bx-video-off" text={t("media.unsupported-format", { mime: note.mime.replace("/", "-") })} />;
}
return (
<div ref={wrapperRef} className={`video-preview-wrapper ${controlsVisible ? "" : "controls-hidden"}`} tabIndex={0} onClick={onVideoClick} onKeyDown={onKeyDown} onMouseMove={onMouseMove}>
<video
ref={videoRef}
class="video-preview"
src={getUrlForDownload(`api/notes/${note.noteId}/open-partial`)}
datatype={note?.mime}
onPlay={() => setPlaying(true)}
onPause={() => setPlaying(false)}
onError={onError}
/>
<div className="media-preview-controls">
<SeekBar mediaRef={videoRef} />
<div class="media-buttons-row">
<div className="left">
<PlaybackSpeed mediaRef={videoRef} />
<RotateButton videoRef={videoRef} />
</div>
<div className="center">
<div className="spacer" />
<SkipButton mediaRef={videoRef} seconds={-10} icon="bx bx-rewind" text={t("media.back-10s")} />
<PlayPauseButton playing={playing} togglePlayback={togglePlayback} />
<SkipButton mediaRef={videoRef} seconds={30} icon="bx bx-fast-forward" text={t("media.forward-30s")} />
<LoopButton mediaRef={videoRef} />
</div>
<div className="right">
<VolumeControl mediaRef={videoRef} />
<ZoomToFitButton videoRef={videoRef} />
<PictureInPictureButton videoRef={videoRef} />
<FullscreenButton targetRef={wrapperRef} />
</div>
</div>
</div>
</div>
);
}
function useKeyboardShortcuts(videoRef: MutableRef<HTMLVideoElement | null>, wrapperRef: MutableRef<HTMLDivElement | null>, togglePlayback: () => void, flashControls: () => void) {
return useCallback((e: KeyboardEvent) => {
const video = videoRef.current;
if (!video) return;
switch (e.key) {
case " ":
e.preventDefault();
togglePlayback();
flashControls();
break;
case "ArrowLeft":
e.preventDefault();
video.currentTime = Math.max(0, video.currentTime - (e.ctrlKey ? 60 : 10));
flashControls();
break;
case "ArrowRight":
e.preventDefault();
video.currentTime = Math.min(video.duration, video.currentTime + (e.ctrlKey ? 60 : 10));
flashControls();
break;
case "f":
case "F":
e.preventDefault();
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
wrapperRef.current?.requestFullscreen();
}
break;
case "m":
case "M":
e.preventDefault();
video.muted = !video.muted;
flashControls();
break;
case "ArrowUp":
e.preventDefault();
video.volume = Math.min(1, video.volume + 0.05);
flashControls();
break;
case "ArrowDown":
e.preventDefault();
video.volume = Math.max(0, video.volume - 0.05);
flashControls();
break;
case "Home":
e.preventDefault();
video.currentTime = 0;
flashControls();
break;
case "End":
e.preventDefault();
video.currentTime = video.duration;
flashControls();
break;
}
}, [ wrapperRef, videoRef, togglePlayback, flashControls ]);
}
function useAutoHideControls(videoRef: RefObject<HTMLVideoElement>, playing: boolean) {
const [visible, setVisible] = useState(true);
const hideTimerRef = useRef<ReturnType<typeof setTimeout>>();
const scheduleHide = useCallback(() => {
clearTimeout(hideTimerRef.current);
if (videoRef.current && !videoRef.current.paused) {
hideTimerRef.current = setTimeout(() => setVisible(false), AUTO_HIDE_DELAY);
}
}, [ videoRef]);
const onMouseMove = useCallback(() => {
setVisible(true);
scheduleHide();
}, [scheduleHide]);
// Hide immediately when playback starts, show when paused.
useEffect(() => {
if (playing) {
setVisible(false);
} else {
clearTimeout(hideTimerRef.current);
setVisible(true);
}
return () => clearTimeout(hideTimerRef.current);
}, [playing, scheduleHide]);
return { visible, onMouseMove, flash: onMouseMove };
}
function RotateButton({ videoRef }: { videoRef: RefObject<HTMLVideoElement> }) {
const [rotation, setRotation] = useState(0);
const rotate = () => {
const video = videoRef.current;
if (!video) return;
const next = (rotation + 90) % 360;
setRotation(next);
const isSideways = next === 90 || next === 270;
if (isSideways) {
// Scale down so the rotated video fits within its container.
const container = video.parentElement;
if (container) {
const ratio = container.clientWidth / container.clientHeight;
video.style.transform = `rotate(${next}deg) scale(${1 / ratio})`;
} else {
video.style.transform = `rotate(${next}deg)`;
}
} else {
video.style.transform = next === 0 ? "" : `rotate(${next}deg)`;
}
};
return (
<ActionButton
icon="bx bx-rotate-right"
text={t("media.rotate")}
onClick={rotate}
/>
);
}
function ZoomToFitButton({ videoRef }: { videoRef: RefObject<HTMLVideoElement> }) {
const [fitted, setFitted] = useState(false);
const toggle = () => {
const video = videoRef.current;
if (!video) return;
const next = !fitted;
video.style.objectFit = next ? "cover" : "";
setFitted(next);
};
return (
<ActionButton
className={fitted ? "active" : ""}
icon={fitted ? "bx bx-collapse" : "bx bx-expand"}
text={fitted ? t("media.zoom-reset") : t("media.zoom-to-fit")}
onClick={toggle}
/>
);
}
function PictureInPictureButton({ videoRef }: { videoRef: RefObject<HTMLVideoElement> }) {
const [active, setActive] = useState(false);
// The standard PiP API is only supported in Chromium-based browsers.
// Firefox uses its own proprietary PiP implementation.
const supported = "requestPictureInPicture" in HTMLVideoElement.prototype;
useEffect(() => {
const video = videoRef.current;
if (!video || !supported) return;
const onEnter = () => setActive(true);
const onLeave = () => setActive(false);
video.addEventListener("enterpictureinpicture", onEnter);
video.addEventListener("leavepictureinpicture", onLeave);
return () => {
video.removeEventListener("enterpictureinpicture", onEnter);
video.removeEventListener("leavepictureinpicture", onLeave);
};
}, [ videoRef, supported ]);
if (!supported) return null;
const toggle = () => {
const video = videoRef.current;
if (!video) return;
if (document.pictureInPictureElement) {
document.exitPictureInPicture();
} else {
video.requestPictureInPicture();
}
};
return (
<ActionButton
icon={active ? "bx bx-exit" : "bx bx-window-open"}
text={active ? t("media.exit-picture-in-picture") : t("media.picture-in-picture")}
onClick={toggle}
/>
);
}
function FullscreenButton({ targetRef }: { targetRef: RefObject<HTMLElement> }) {
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
const onFullscreenChange = () => setIsFullscreen(!!document.fullscreenElement);
document.addEventListener("fullscreenchange", onFullscreenChange);
return () => document.removeEventListener("fullscreenchange", onFullscreenChange);
}, []);
const toggleFullscreen = () => {
const target = targetRef.current;
if (!target) return;
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
target.requestFullscreen();
}
};
return (
<ActionButton
icon={isFullscreen ? "bx bx-exit-fullscreen" : "bx bx-fullscreen"}
text={isFullscreen ? t("media.exit-fullscreen") : t("media.fullscreen")}
onClick={toggleFullscreen}
/>
);
}

View File

@@ -43,6 +43,7 @@ function SpreadsheetEditor({ note, noteContext, readOnly }: TypeWidgetProps & {
useDarkMode(apiRef);
usePersistence(note, noteContext, apiRef, containerRef, readOnly);
useSearchIntegration(apiRef);
useFixRadixPortals();
// Focus the spreadsheet when the note is focused.
useTriliumEvent("focusOnDetail", () => {
@@ -55,6 +56,38 @@ function SpreadsheetEditor({ note, noteContext, readOnly }: TypeWidgetProps & {
return <div ref={containerRef} className="spreadsheet" />;
}
/**
* Univer's design system uses Radix UI primitives whose DismissableLayer detects
* "outside" clicks/focus via document-level pointerdown/focusin listeners combined
* with a React capture-phase flag. In React, portal events bubble through the
* component tree so onPointerDownCapture fires on the DismissableLayer, setting an
* internal flag that suppresses the "outside" detection. With preact/compat, portal
* events don't bubble through the React tree, so the flag never gets set and Radix
* immediately dismisses popups.
*
* Radix dispatches cancelable custom events ("dismissableLayer.pointerDownOutside"
* and "dismissableLayer.focusOutside") on the original event target before calling
* onDismiss. The dismiss is skipped if defaultPrevented is true. This hook intercepts
* those custom events in the capture phase and prevents default when the target is
* inside a Radix portal, restoring the expected behavior.
*/
function useFixRadixPortals() {
useEffect(() => {
function preventDismiss(e: Event) {
if (e.target instanceof HTMLElement && e.target.closest("[id^='radix-']")) {
e.preventDefault();
}
}
document.addEventListener("dismissableLayer.pointerDownOutside", preventDismiss, true);
document.addEventListener("dismissableLayer.focusOutside", preventDismiss, true);
return () => {
document.removeEventListener("dismissableLayer.pointerDownOutside", preventDismiss, true);
document.removeEventListener("dismissableLayer.focusOutside", preventDismiss, true);
};
}, []);
}
function useInitializeSpreadsheet(containerRef: MutableRef<HTMLDivElement | null>, apiRef: MutableRef<FUniver | undefined>, readOnly: boolean) {
useEffect(() => {
if (!containerRef.current) return;

View File

@@ -97,8 +97,8 @@
"html": "1.0.0",
"html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.6",
"i18next": "25.8.14",
"https-proxy-agent": "8.0.0",
"i18next": "25.8.17",
"i18next-fs-backend": "2.6.1",
"image-type": "6.0.0",
"ini": "6.0.0",
@@ -128,6 +128,6 @@
"vite": "7.3.1",
"ws": "8.19.0",
"xml2js": "0.6.2",
"yauzl": "3.2.0"
"yauzl": "3.2.1"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -4,21 +4,17 @@
maintaining and supporting it long-term proved to be unsustainable.</p>
<p>When upgrading to v0.102.0, your Chat notes will be preserved, but instead
of the dedicated chat window they will be turned to a normal&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6f9hih2hXXZk">Code</a>&nbsp;note,
revealing the underlying JSON of the conversation.</p>
href="#root/_help_6f9hih2hXXZk">Code</a>&nbsp;note, revealing the underlying
JSON of the conversation.</p>
<h2>Alternative solutions (MCP)</h2>
<p>Given the recent advancements of the AI scene, MCP has grown to be more
powerful and facilitates easier integrations with various application.</p>
<p>As such, there are third-party solutions that integrate an MCP server
that can be used with Trilium:</p>
<ul>
<li>
<p><a href="https://github.com/tan-yong-sheng/triliumnext-mcp">tan-yong-sheng/triliumnext-mcp</a>
</p>
<li><a href="https://github.com/tan-yong-sheng/triliumnext-mcp">tan-yong-sheng/triliumnext-mcp</a>
</li>
<li>
<p><a href="https://github.com/perfectra1n/triliumnext-mcp">perfectra1n/triliumnext-mcp</a>
</p>
<li><a href="https://github.com/perfectra1n/triliumnext-mcp">perfectra1n/triliumnext-mcp</a>
</li>
</ul>
<aside class="admonition important">

View File

@@ -209,7 +209,7 @@
<tr>
<td><code spellcheck="false">#calendar:color</code>
</td>
<td><strong>❌️ Removed since v0.100.0. Use</strong> <code spellcheck="false">**#color**</code> <strong>instead.</strong>&nbsp;
<td><strong>❌️ Removed since v0.100.0. Use</strong> <code spellcheck="false">**#color**</code> <strong>instead.</strong>&nbsp;&nbsp;
<br>
<br>Similar to <code spellcheck="false">#color</code>, but applies the color
only for the event in the calendar and not for other places such as the
@@ -233,15 +233,15 @@
<td><code spellcheck="false">#calendar:displayedAttributes</code>
</td>
<td>Allows displaying the value of one or more attributes in the calendar
like this:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
like this:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<br>
<br>
<img src="7_Calendar_image.png">&nbsp;&nbsp;&nbsp;&nbsp;
<img src="7_Calendar_image.png">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<br>
<br><code spellcheck="false">#weight="70" #Mood="Good" #calendar:displayedAttributes="weight,Mood"</code>&nbsp;&nbsp;&nbsp;&nbsp;
<br><code spellcheck="false">#weight="70" #Mood="Good" #calendar:displayedAttributes="weight,Mood"</code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<br>
<br>It can also be used with relations, case in which it will display the
title of the target note:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
title of the target note:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<br>
<br><code spellcheck="false">~assignee=@My assignee #calendar:displayedAttributes="assignee"</code>
</td>
@@ -294,44 +294,27 @@
<p>When not used in a Journal, the calendar is recursive. That is, it will
look for events not just in its child notes but also in the children of
these child notes.</p>
<p>&nbsp;</p>
<h2>Recurrence</h2>
<p>The built in calendar view also supports repeating tasks. If a child note
of the calendar has a #recurrence label with a valid recurrence, that event
will repeat on the calendar according to the recurrence string.&nbsp;</p>
<p>For example, to make a note repeat on the calendar:</p>
<ul>
<li>
<p>Every Day - <code spellcheck="false">#recurrence="FREQ=DAILY;INTERVAL=1"</code>
</p>
<li>Every Day - <code spellcheck="false">#recurrence="FREQ=DAILY;INTERVAL=1"</code>
</li>
<li>
<p>Every 3 days - <code spellcheck="false">#recurrence="FREQ=DAILY;INTERVAL=3"</code>
</p>
<li>Every 3 days - <code spellcheck="false">#recurrence="FREQ=DAILY;INTERVAL=3"</code>
</li>
<li>
<p>Every week - <code spellcheck="false">#recurrence="FREQ=WEEKLY;INTERVAL=1"</code>
</p>
<li>Every week - <code spellcheck="false">#recurrence="FREQ=WEEKLY;INTERVAL=1"</code>
</li>
<li>
<p>Every 2 weeks on Monday, Wednesday and Friday - <code spellcheck="false">#recurrence="FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR"</code>
</p>
<li>Every 2 weeks on Monday, Wednesday and Friday - <code spellcheck="false">#recurrence="FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR"</code>
</li>
<li>
<p>Every 3 months - <code spellcheck="false">#recurrence="FREQ=MONTHLY;INTERVAL=3"</code>
</p>
<li>Every 3 months - <code spellcheck="false">#recurrence="FREQ=MONTHLY;INTERVAL=3"</code>
</li>
<li>
<p>Every 2 months on the First Sunday - <code spellcheck="false">#recurrence="FREQ=MONTHLY;INTERVAL=2;BYDAY=1SU"</code>
</p>
<li>Every 2 months on the First Sunday - <code spellcheck="false">#recurrence="FREQ=MONTHLY;INTERVAL=2;BYDAY=1SU"</code>
</li>
<li>
<p>Every month on the Last Friday - <code spellcheck="false">#recurrence="FREQ=MONTHLY;INTERVAL=1;BYDAY=-1FR"</code>
</p>
</li>
<li>
<p>And so on.</p>
<li>Every month on the Last Friday - <code spellcheck="false">#recurrence="FREQ=MONTHLY;INTERVAL=1;BYDAY=-1FR"</code>
</li>
<li>And so on.</li>
</ul>
<p>For other examples of valid <code spellcheck="false">RRULE</code> strings
see <a href="https://icalendar.org/rrule-tool.html">https://icalendar.org/rrule-tool.html</a>
@@ -352,7 +335,6 @@
note ID and title of the note with the erroneous recurrence message. This
note will not be added to the calendar</p>
</aside>
<p>&nbsp;</p>
<h2>Use-cases</h2>
<h3>Using with the Journal / calendar</h3>
<p>It is possible to integrate the calendar view into the Journal with day

View File

@@ -9,7 +9,8 @@
note where to place the new one and select:</p>
<ul>
<li><em>Insert note after</em>, to put the new note underneath the one selected.</li>
<li><em>Insert child note</em>, to insert the note as a child of the selected
<li
><em>Insert child note</em>, to insert the note as a child of the selected
note.</li>
</ul>
<p>
@@ -20,7 +21,8 @@
<li>When adding a <a href="#root/_help_QEAPj01N5f7w">link</a> in a&nbsp;<a class="reference-link"
href="#root/_help_iPIMuisry3hd">Text</a>&nbsp;note, type the desired title of
the new note and press Enter. Afterwards the type of the note will be asked.</li>
<li>Similarly, when creating a new tab, type the desired title and press Enter.</li>
<li
>Similarly, when creating a new tab, type the desired title and press Enter.</li>
</ul>
<h2>Changing the type of a note</h2>
<p>It is possible to change the type of a note after it has been created
@@ -30,94 +32,96 @@
edit the <a href="#root/_help_4FahAwuGTAwC">source of a note</a>.</p>
<h2>Supported note types</h2>
<p>The following note types are supported by Trilium:</p>
<table>
<thead>
<tr>
<th>Note Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><a class="reference-link" href="#root/_help_iPIMuisry3hd">Text</a>
</td>
<td>The default note type, which allows for rich text formatting, images,
admonitions and right-to-left support.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a>
</td>
<td>Uses a mono-space font and can be used to store larger chunks of code
or plain text than a text note, and has better syntax highlighting.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_m523cpzocqaD">Saved Search</a>
</td>
<td>Stores the information about a search (the search text, criteria, etc.)
for later use. Can be used for quick filtering of a large amount of notes,
for example. The search can easily be triggered.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_iRwzGnHPzonm">Relation Map</a>
</td>
<td>Allows easy creation of notes and relations between them. Can be used
for mainly relational data such as a family tree.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_bdUJEHsAPYQR">Note Map</a>
</td>
<td>Displays the relationships between the notes, whether via relations or
their hierarchical structure.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a>
</td>
<td>Used in&nbsp;<a class="reference-link" href="#root/_help_CdNpE2pqjmI6">Scripting</a>,
it displays the HTML content of another note. This allows displaying any
kind of content, provided there is a script behind it to generate it.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a>
</td>
<td>Displays the children of the note either as a grid, a list, or for a more
specialized case: a calendar.
<br>
<br>Generally useful for easy reading of short notes.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_s1aBHPd79XYj">Mermaid Diagrams</a>
</td>
<td>Displays diagrams such as bar charts, flow charts, state diagrams, etc.
Requires a bit of technical knowledge since the diagrams are written in
a specialized format.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_grjYqerjn243">Canvas</a>
</td>
<td>Allows easy drawing of sketches, diagrams, handwritten content. Uses the
same technology behind <a href="https://excalidraw.com">excalidraw.com</a>.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_1vHRoWCEjj0L">Web View</a>
</td>
<td>Displays the content of an external web page, similar to a browser.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_gBbsAeiuUxI5">Mind Map</a>
</td>
<td>Easy for brainstorming ideas, by placing them in a hierarchical layout.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_81SGnPGMk7Xc">Geo Map View</a>
</td>
<td>Displays the children of the note as a geographical map, one use-case
would be to plan vacations. It even has basic support for tracks. Notes
can also be created from it.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_W8vYD3Q1zjCR">File</a>
</td>
<td>Represents an uploaded file such as PDFs, images, video or audio files.</td>
</tr>
</tbody>
</table>
<figure class="table">
<table>
<thead>
<tr>
<th>Note Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><a class="reference-link" href="#root/_help_iPIMuisry3hd">Text</a>
</td>
<td>The default note type, which allows for rich text formatting, images,
admonitions and right-to-left support.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a>
</td>
<td>Uses a mono-space font and can be used to store larger chunks of code
or plain text than a text note, and has better syntax highlighting.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_m523cpzocqaD">Saved Search</a>
</td>
<td>Stores the information about a search (the search text, criteria, etc.)
for later use. Can be used for quick filtering of a large amount of notes,
for example. The search can easily be triggered.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_iRwzGnHPzonm">Relation Map</a>
</td>
<td>Allows easy creation of notes and relations between them. Can be used
for mainly relational data such as a family tree.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_bdUJEHsAPYQR">Note Map</a>
</td>
<td>Displays the relationships between the notes, whether via relations or
their hierarchical structure.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a>
</td>
<td>Used in&nbsp;<a class="reference-link" href="#root/_help_CdNpE2pqjmI6">Scripting</a>,
it displays the HTML content of another note. This allows displaying any
kind of content, provided there is a script behind it to generate it.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a>
</td>
<td>Displays the children of the note either as a grid, a list, or for a more
specialized case: a calendar.&nbsp;
<br>
<br>Generally useful for easy reading of short notes.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_s1aBHPd79XYj">Mermaid Diagrams</a>
</td>
<td>Displays diagrams such as bar charts, flow charts, state diagrams, etc.
Requires a bit of technical knowledge since the diagrams are written in
a specialized format.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_grjYqerjn243">Canvas</a>
</td>
<td>Allows easy drawing of sketches, diagrams, handwritten content. Uses the
same technology behind <a href="https://excalidraw.com">excalidraw.com</a>.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_1vHRoWCEjj0L">Web View</a>
</td>
<td>Displays the content of an external web page, similar to a browser.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_gBbsAeiuUxI5">Mind Map</a>
</td>
<td>Easy for brainstorming ideas, by placing them in a hierarchical layout.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_81SGnPGMk7Xc">Geo Map</a>
</td>
<td>Displays the children of the note as a geographical map, one use-case
would be to plan vacations. It even has basic support for tracks. Notes
can also be created from it.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_W8vYD3Q1zjCR">File</a>
</td>
<td>Represents an uploaded file such as PDFs, images, video or audio files.</td>
</tr>
</tbody>
</table>
</figure>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 612 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 612 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -13,7 +13,7 @@
<p>See&nbsp;<a class="reference-link" href="#root/_help_XJGJrpu7F9sh">PDFs</a>.</p>
<h3>Images</h3>
<figure class="image image-style-align-center image_resized" style="width:50%;">
<img style="aspect-ratio:879/766;" src="3_File_image.png"
<img style="aspect-ratio:879/766;" src="2_File_image.png"
width="879" height="766">
</figure>
<p>Interaction:</p>
@@ -30,25 +30,10 @@
</li>
</ul>
<h3>Videos</h3>
<figure class="image image-style-align-center image_resized" style="width:50%;">
<img style="aspect-ratio:854/700;" src="File_image.png"
width="854" height="700">
</figure>
<p>Video files can be added in as well. The file is streamed directly, so
when accessing the note from a server it doesn't have to download the entire
video to start playing it.</p>
<aside class="admonition caution">
<p>Although Trilium offers support for videos, it is generally not meant
to be used with very large files. Uploading large videos will cause the&nbsp;
<a
class="reference-link" href="#root/_help_wX4HbRucYSDD">Database</a>&nbsp;to balloon as well as the any&nbsp;<a class="reference-link"
href="#root/_help_ODY7qQn5m2FT">Backup</a>&nbsp;of it. In addition to that, there
might be slowdowns when first uploading the files. Otherwise, a large database
should not impact the general performance of Trilium significantly.</p>
</aside>
<p>See&nbsp;<a class="reference-link" href="#root/_help_AjqEeiDUOzj4">Videos</a>.</p>
<h3>Audio</h3>
<figure class="image image-style-align-center image_resized" style="width:50%;">
<img style="aspect-ratio:850/243;" src="2_File_image.png"
<img style="aspect-ratio:850/243;" src="1_File_image.png"
width="850" height="243">
</figure>
<p>Adding a supported audio file will reveal a basic audio player that can
@@ -64,7 +49,7 @@
</ul>
<h3>Text files</h3>
<figure class="image image-style-align-center image_resized" style="width:50%;">
<img style="aspect-ratio:926/347;" src="1_File_image.png"
<img style="aspect-ratio:926/347;" src="File_image.png"
width="926" height="347">
</figure>
<p>Files that are identified as containing text will show a preview of their
@@ -83,7 +68,7 @@
application.</p>
<h3>Unknown file types</h3>
<figure class="image image-style-align-center image_resized" style="width:50%;">
<img style="aspect-ratio:532/240;" src="4_File_image.png"
<img style="aspect-ratio:532/240;" src="3_File_image.png"
width="532" height="240">
</figure>
<p>If the file could not be identified as any of the supported file types
@@ -110,7 +95,7 @@
<p>Files are also displayed in the&nbsp;<a class="reference-link" href="#root/_help_0ESUbbAxVnoK">Note List</a>&nbsp;based
on their type:</p>
<img class="image_resized" style="aspect-ratio:853/315;width:50%;"
src="5_File_image.png" width="853" height="315">
src="4_File_image.png" width="853" height="315">
</li>
<li>
<p>Non-image files can be embedded into text notes as read-only widgets via

View File

@@ -0,0 +1,131 @@
<figure class="image image-style-align-right image_resized" style="width:61.8%;">
<img style="aspect-ratio:953/587;" src="Videos_image.png"
width="953" height="587">
</figure>
<p>Starting with v0.103.0, Trilium has a custom video player which offers
more features than the built-in video player.</p>
<p>Versions prior to v0.103.0 also support videos, but using the built-in
player.</p>
<p>The file is streamed directly, so when accessing the note from a server
it doesn't have to download the entire video to start playing it.</p>
<h2>Note on large video files</h2>
<p>Although Trilium offers support for videos, it is generally not meant
to be used with very large files. Uploading large videos will cause the&nbsp;
<a
class="reference-link" href="#root/_help_wX4HbRucYSDD">Database</a>&nbsp;to balloon as well as the any&nbsp;<a class="reference-link"
href="#root/_help_ODY7qQn5m2FT">Backup</a>&nbsp;of it. In addition to that, there
might be slowdowns when first uploading the files. Otherwise, a large database
should not impact the general performance of Trilium significantly.</p>
<h2>Supported formats</h2>
<p>Trilium uses the built-in video decoding mechanism of the browser (or
Electron/Chromium when running on the desktop). Starting with v0.103.0,
a message will be displayed instead when a video format is not supported.</p>
<h2>Interactions</h2>
<p>To play/pause the video, simply click anywhere on the video.</p>
<p>The controls at the bottom will hide automatically after playing, simply
move the mouse to show them again.</p>
<p>The bottom bar has the following features:</p>
<ul>
<li>
<p>A track bar to seek across the video.</p>
</li>
<li>
<p>On the left of the track bar, the current time is indicated.</p>
</li>
<li>
<p>On the right of the track bar, the remaining time is indicated.</p>
</li>
<li>
<p>On the left side there are buttons to:</p>
<ul>
<li>Adjust the playback speed (e.g. 0.5x, 1x).</li>
<li>Rotate the video by 90 degrees.</li>
</ul>
</li>
<li>
<p>In the center:</p>
<ul>
<li>Go back by 10s</li>
<li>Play/pause</li>
<li>Go forward by 30s</li>
<li>Loop, which when enabled will restart the video once it reaches the end.</li>
</ul>
</li>
<li>
<p>On the right side:</p>
<ul>
<li>Mute button</li>
<li>Volume adjustment</li>
<li>Full screen</li>
<li>Zoom to fill, which will crop the video so that it fills the entire window.</li>
<li>Picture-in-picture (if the browser supports it).</li>
</ul>
</li>
</ul>
<h2>Keyboard shortcuts</h2>
<p>The following keyboard shortcuts are supported by the video player:</p>
<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><kbd>Space</kbd>
</td>
<td>Play/pause</td>
</tr>
<tr>
<td><kbd>Left arrow key</kbd>
</td>
<td>Go back by 10s</td>
</tr>
<tr>
<td><kbd>Right arrow key</kbd>
</td>
<td>Go forward by 10s</td>
</tr>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>Left arrow key</kbd>
</td>
<td>Go back by 1 min</td>
</tr>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>Right arrow key</kbd>
</td>
<td>Go right by 1 min</td>
</tr>
<tr>
<td><kbd>F</kbd>
</td>
<td>Toggle full-screen</td>
</tr>
<tr>
<td><kbd>M</kbd>
</td>
<td>Mute/unmute</td>
</tr>
<tr>
<td><kbd>Home</kbd>
</td>
<td>Go to the beginning of the video</td>
</tr>
<tr>
<td><kbd>End</kbd>
</td>
<td>Go to the end of the video</td>
</tr>
<tr>
<td><kbd>Up</kbd>
</td>
<td>Increase volume by 5%</td>
</tr>
<tr>
<td><kbd>Down</kbd>
</td>
<td>Decrease volume by 5%</td>
</tr>
</tbody>
</table>

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 652 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -38,30 +38,34 @@
<img src="1_Mermaid Diagrams_image.png">
</li>
<li>The preview can be moved around by holding the left mouse button and dragging.</li>
<li>Zooming can also be done by using the scroll wheel.</li>
<li>The zoom and position on the preview will remain fixed as the diagram
changes, to be able to work more easily with large diagrams.</li>
</ul>
<li
>Zooming can also be done by using the scroll wheel.</li>
<li>The zoom and position on the preview will remain fixed as the diagram
changes, to be able to work more easily with large diagrams.</li>
</ul>
</li>
<li>The size of the source/preview panes can be adjusted by hovering over
the border between them and dragging it with the mouse.</li>
<li>In the&nbsp;<a class="reference-link" href="#root/_help_XpOYSgsLkTJy">Floating buttons</a>&nbsp;area:
<ul>
<li>The source/preview can be laid out left-right or bottom-top via the <em>Move editing pane to the left / bottom</em> option.</li>
<li>Press <em>Lock editing</em> to automatically mark the note as read-only.
<li
>Press <em>Lock editing</em> to automatically mark the note as read-only.
In this mode, the code pane is hidden and the diagram is displayed full-size.
Similarly, press <em>Unlock editing</em> to mark a read-only note as editable.</li>
<li>Press the <em>Copy image reference to the clipboard</em> to be able to insert
the image representation of the diagram into a text note. See&nbsp;<a class="reference-link"
href="#root/_help_0Ofbk1aSuVRu">Image references</a>&nbsp;for more information.</li>
<li>Press the <em>Export diagram as SVG</em> to download a scalable/vector rendering
of the diagram. Can be used to present the diagram without degrading when
zooming.</li>
<li
>Press the <em>Copy image reference to the clipboard</em> to be able to insert
the image representation of the diagram into a text note. See&nbsp;<a class="reference-link"
href="#root/_help_0Ofbk1aSuVRu">Image references</a>&nbsp;for more information.</li>
<li
>Press the <em>Export diagram as SVG</em> to download a scalable/vector rendering
of the diagram. Can be used to present the diagram without degrading when
zooming.</li>
<li>Press the <em>Export diagram as PNG</em> to download a normal image (at
1x scale, raster) of the diagram. Can be used to send the diagram in more
traditional channels such as e-mail.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2>Errors in the diagram</h2>
<p>If there is an error in the source code, the error will be displayed in

View File

@@ -0,0 +1,111 @@
<figure class="image">
<img style="aspect-ratio:1102/573;" src="Spreadsheets_image.png"
width="1102" height="573">
</figure>
<aside class="admonition important">
<p>Spreadsheets are a new type of note introduced in v0.103.0 and are currently
considered experimental/beta. As such, expect major changes to occur to
this note type.</p>
</aside>
<p>Spreadsheets provide a familiar experience to Microsoft Excel or LibreOffice
Calc, with support for formulas, data validation and text formatting.</p>
<h2>Spreadsheets vs. collections</h2>
<p>There is a slight overlap between spreadsheets and the&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_2FvYrpmOXm29">Table</a>&nbsp;collection.
In general the table collection is useful to track meta-information about
notes (for example a collection of people and their birthdays), whereas
spreadsheets are quite useful for calculations since they support formulas.</p>
<p>Spreadsheets also benefit from a wider range of features such as data
validation, formatting and can work on a relatively large dataset.</p>
<h2>Important statement regarding data format</h2>
<p>For Trilium as a knowledge database, it is important that data is stored
in a format that is easy to convert to something else. For example,&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_iPIMuisry3hd">Text</a>&nbsp;notes can be exported to either HTML or Markdown, making
it relatively easy to migrate to another software or simply to stand the
test of time.</p>
<p>For spreadsheets, Trilium uses a technology called <a href="https://docs.univer.ai/">Univer Sheets</a>,
developed by DreamNum Co., Ltd. Although this software library is quite
powerful and has a good track record (starting with Luckysheet from 2020,
becoming Univer somewhere in 2023), it uses its own JSON format to store
the sheets.</p>
<p>As such, if Univer were to become unmaintained or incompatible for some
reason, your data might become vendor locked-in.</p>
<p>With that in mind, spreadsheets can be really useful for quick calculations,
but it's important not to have critical information on it that you might
not want to need in a few years time.</p>
<h2>Regarding data export</h2>
<p>Currently, in Trilium there is no way to export the spreadsheets to CSV
or Excel formats. We might manage to add support for it at some point,
but currently this is not the case.</p>
<h2>Supported features</h2>
<p>The spreadsheet has support for the following features:</p>
<ul>
<li>
<p>Filtering</p>
</li>
<li>
<p>Sorting</p>
</li>
<li>
<p>Data validation</p>
</li>
<li>
<p>Conditional formatting</p>
</li>
<li>
<p>Notes / annotations</p>
</li>
<li>
<p>Find / replace</p>
</li>
</ul>
<p>We might consider adding <a href="https://docs.univer.ai/guides/sheets/features/filter">other features</a> from
Univer at some point. If there is a particular feature that can be added
easily, it can be discussed over <a href="#root/pOsGYCXsbNQG/BgmBlOIl72jZ/_help_wy8So3yZZlH9">GitHub Issues</a>.</p>
<h2>Features not supported yet</h2>
<h3>Regarding Pro features</h3>
<p>Univer spreadsheets also feature a <a href="https://univer.ai/pro">Pro plan</a> which
adds quite a lot of functionality such as charts, printing, pivot tables,
export, etc.</p>
<p>As the Pro plan needs a license, Trilium does not support any of the premium
features. Theoretically, pro features can be used in trial mode with some
limitations, we might explore this direction at some point.</p>
<h3>Planned features</h3>
<p>There are a few features that are already planned but are not supported
yet:</p>
<ul>
<li>
<p>Trilium-specific formulas (e.g. to obtain the title of a note).</p>
</li>
<li>
<p>User-defined formulas</p>
</li>
<li>
<p>Cross-workbook calculation</p>
</li>
</ul>
<p>If you would like us to work on these features, consider <a href="https://triliumnotes.org/en/support-us">supporting us</a>.</p>
<h2>Known limitations</h2>
<ul>
<li>
<p>It is possible to share a spreadsheet, case in which a best-effort HTML
rendering of the spreadsheet is done.</p>
<ul>
<li>
<p>For more advanced use cases, this will most likely not work as intended.
Feel free to <a href="#root/pOsGYCXsbNQG/BgmBlOIl72jZ/_help_wy8So3yZZlH9">report issues</a>,
but keep in mind that we might not be able to have a complete feature parity
with all the features of Univer.</p>
</li>
</ul>
</li>
<li>
<p>There is currently no export functionality, as stated previously.</p>
</li>
<li>
<p>There is no dedicated mobile support. Mobile support is currently experimental
in Univer and when it becomes stable, we could potentially integrate it
into Trilium as well.</p>
</li>
</ul>

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -10449,6 +10449,12 @@
"terms": [
"virus-block"
]
},
"bx-empty": {
"glyph": "",
"terms": [
"empty"
]
}
}
}

View File

@@ -1,18 +1,7 @@
import { describe, it, expect } from "vitest";
import { processMindmapContent } from "./note_content_fulltext.js";
import { describe, expect,it } from "vitest";
import NoteContentFulltextExp from "./note_content_fulltext.js";
describe("processMindmapContent", () => {
it("supports empty JSON", () => {
expect(processMindmapContent("{}")).toEqual("");
});
it("supports blank text / invalid JSON", () => {
expect(processMindmapContent("")).toEqual("");
expect(processMindmapContent(`{ "node": " }`)).toEqual("");
});
});
describe("Fuzzy Search Operators", () => {
it("~= operator works with typos", () => {
// Test that the ~= operator can handle common typos

View File

@@ -1,24 +1,19 @@
"use strict";
import type { NoteRow } from "@triliumnext/commons";
import type SearchContext from "../search_context.js";
import Expression from "./expression.js";
import NoteSet from "../note_set.js";
import log from "../../log.js";
import becca from "../../../becca/becca.js";
import log from "../../log.js";
import protectedSessionService from "../../protected_session.js";
import striptags from "striptags";
import { normalize } from "../../utils.js";
import sql from "../../sql.js";
import {
normalizeSearchText,
calculateOptimizedEditDistance,
validateFuzzySearchTokens,
validateAndPreprocessContent,
import NoteSet from "../note_set.js";
import type SearchContext from "../search_context.js";
import {
FUZZY_SEARCH_CONFIG,
fuzzyMatchWord,
FUZZY_SEARCH_CONFIG
} from "../utils/text_utils.js";
normalizeSearchText,
validateAndPreprocessContent,
validateFuzzySearchTokens} from "../utils/text_utils.js";
import Expression from "./expression.js";
import preprocessContent from "./note_content_fulltext_preprocessor.js";
const ALLOWED_OPERATORS = new Set(["=", "!=", "*=*", "*=", "=*", "%=", "~=", "~*"]);
@@ -218,7 +213,7 @@ class NoteContentFulltextExp extends Expression {
return;
}
content = this.preprocessContent(content, type, mime);
content = preprocessContent(content, type, mime, this.raw);
// Apply content size validation and preprocessing
const processedContent = validateAndPreprocessContent(content, noteId);
@@ -295,59 +290,22 @@ class NoteContentFulltextExp extends Expression {
return content;
}
preprocessContent(content: string | Buffer, type: string, mime: string) {
content = normalize(content.toString());
if (type === "text" && mime === "text/html") {
if (!this.raw) {
// Content size already filtered at DB level, safe to process
content = this.stripTags(content);
}
content = content.replace(/&nbsp;/g, " ");
} else if (type === "mindMap" && mime === "application/json") {
content = processMindmapContent(content);
} else if (type === "canvas" && mime === "application/json") {
interface Element {
type: string;
text?: string; // Optional since not all objects have a `text` property
id: string;
[key: string]: any; // Other properties that may exist
}
const canvasContent = JSON.parse(content);
const elements = canvasContent.elements;
if (Array.isArray(elements)) {
const texts = elements
.filter((element: Element) => element.type === "text" && element.text) // Filter for 'text' type elements with a 'text' property
.map((element: Element) => element.text!); // Use `!` to assert `text` is defined after filtering
content = normalize(texts.join(" "));
} else {
content = "";
}
}
return content.trim();
}
/**
* Checks if a token matches content with optional fuzzy matching
*/
private tokenMatchesContent(token: string, content: string, noteId: string): boolean {
const normalizedToken = normalizeSearchText(token);
const normalizedContent = normalizeSearchText(content);
if (normalizedContent.includes(normalizedToken)) {
return true;
}
// Check flat text for default fulltext search
if (!this.flatText || !becca.notes[noteId].getFlatText().includes(token)) {
return false;
}
return true;
}
@@ -358,15 +316,15 @@ class NoteContentFulltextExp extends Expression {
try {
const normalizedContent = normalizeSearchText(content);
const flatText = this.flatText ? normalizeSearchText(becca.notes[noteId].getFlatText()) : "";
// For phrase matching, check if tokens appear within reasonable proximity
if (this.tokens.length > 1) {
return this.matchesPhrase(normalizedContent, flatText);
}
// Single token fuzzy matching
const token = normalizeSearchText(this.tokens[0]);
return this.fuzzyMatchToken(token, normalizedContent) ||
return this.fuzzyMatchToken(token, normalizedContent) ||
(this.flatText && this.fuzzyMatchToken(token, flatText));
} catch (error) {
log.error(`Error in fuzzy matching for note ${noteId}: ${error}`);
@@ -379,45 +337,45 @@ class NoteContentFulltextExp extends Expression {
*/
private matchesPhrase(content: string, flatText: string): boolean {
const searchText = this.flatText ? `${content} ${flatText}` : content;
// Apply content size limits for phrase matching
const limitedText = validateAndPreprocessContent(searchText);
if (!limitedText) {
return false;
}
const words = limitedText.toLowerCase().split(/\s+/);
// Only skip phrase matching for truly extreme word counts that could crash the system
if (words.length > FUZZY_SEARCH_CONFIG.ABSOLUTE_MAX_WORD_COUNT) {
console.error(`Phrase matching skipped due to extreme word count that could cause system instability: ${words.length} words`);
return false;
}
// Warn about large word counts but still attempt matching
if (words.length > FUZZY_SEARCH_CONFIG.PERFORMANCE_WARNING_WORDS) {
console.info(`Large word count for phrase matching: ${words.length} words - may take longer but will attempt full matching`);
}
// Find positions of each token
const tokenPositions: number[][] = this.tokens.map(token => {
const normalizedToken = normalizeSearchText(token);
const positions: number[] = [];
words.forEach((word, index) => {
if (this.fuzzyMatchSingle(normalizedToken, word)) {
positions.push(index);
}
});
return positions;
});
// Check if we found all tokens
if (tokenPositions.some(positions => positions.length === 0)) {
return false;
}
// Check for phrase proximity using configurable distance
return this.hasProximityMatch(tokenPositions, FUZZY_SEARCH_CONFIG.MAX_PHRASE_PROXIMITY);
}
@@ -431,18 +389,18 @@ class NoteContentFulltextExp extends Expression {
const [pos1, pos2] = tokenPositions;
return pos1.some(p1 => pos2.some(p2 => Math.abs(p1 - p2) <= maxDistance));
}
// For more tokens, check if we can find a sequence where all tokens are within range
const findSequence = (remaining: number[][], currentPos: number): boolean => {
if (remaining.length === 0) return true;
const [nextPositions, ...rest] = remaining;
return nextPositions.some(pos =>
Math.abs(pos - currentPos) <= maxDistance &&
return nextPositions.some(pos =>
Math.abs(pos - currentPos) <= maxDistance &&
findSequence(rest, pos)
);
};
const [firstPositions, ...rest] = tokenPositions;
return firstPositions.some(startPos => findSequence(rest, startPos));
}
@@ -455,12 +413,12 @@ class NoteContentFulltextExp extends Expression {
// For short tokens, require exact match to avoid too many false positives
return content.includes(token);
}
const words = content.split(/\s+/);
// Only limit word processing for truly extreme cases to prevent system instability
const limitedWords = words.slice(0, FUZZY_SEARCH_CONFIG.ABSOLUTE_MAX_WORD_COUNT);
return limitedWords.some(word => this.fuzzyMatchSingle(token, word));
}
@@ -471,83 +429,6 @@ class NoteContentFulltextExp extends Expression {
// Use shared optimized fuzzy matching logic
return fuzzyMatchWord(token, word, FUZZY_SEARCH_CONFIG.MAX_EDIT_DISTANCE);
}
stripTags(content: string) {
// we want to allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412
// we want to insert space in place of block tags (because they imply text separation)
// but we don't want to insert text for typical formatting inline tags which can occur within one word
const linkTag = "a";
const inlineFormattingTags = ["b", "strong", "em", "i", "span", "big", "small", "font", "sub", "sup"];
// replace tags which imply text separation with a space
content = striptags(content, [linkTag, ...inlineFormattingTags], " ");
// replace the inline formatting tags (but not links) without a space
content = striptags(content, [linkTag], "");
// at least the closing link tag can be easily stripped
return content.replace(/<\/a>/gi, "");
}
}
export function processMindmapContent(content: string) {
let mindMapcontent;
try {
mindMapcontent = JSON.parse(content);
} catch (e) {
return "";
}
// Define interfaces for the JSON structure
interface MindmapNode {
id: string;
topic: string;
children: MindmapNode[]; // Recursive structure
direction?: number;
expanded?: boolean;
}
interface MindmapData {
nodedata: MindmapNode;
arrows: any[]; // If you know the structure, replace `any` with the correct type
summaries: any[];
direction: number;
theme: {
name: string;
type: string;
palette: string[];
cssvar: Record<string, string>; // Object with string keys and string values
};
}
// Recursive function to collect all topics
function collectTopics(node?: MindmapNode): string[] {
if (!node) {
return [];
}
// Collect the current node's topic
let topics = [node.topic];
// If the node has children, collect topics recursively
if (node.children && node.children.length > 0) {
for (const child of node.children) {
topics = topics.concat(collectTopics(child));
}
}
return topics;
}
// Start extracting from the root node
const topicsArray = collectTopics(mindMapcontent.nodedata);
// Combine topics into a single string
const topicsString = topicsArray.join(", ");
return normalize(topicsString.toString());
}
export default NoteContentFulltextExp;

View File

@@ -0,0 +1,40 @@
import { NoteType } from "@triliumnext/commons";
import { describe, expect,it } from "vitest";
import preprocessContent from "./note_content_fulltext_preprocessor";
describe("Mind map preprocessing", () => {
const type: NoteType = "mindMap";
const mime = "application/json";
it("supports empty JSON", () => {
expect(preprocessContent("{}", type, mime)).toEqual("");
});
it("supports blank text / invalid JSON", () => {
expect(preprocessContent("", type, mime)).toEqual("");
expect(preprocessContent(`{ "node": " }`, type, mime)).toEqual("");
});
it("reads data", () => {
expect(preprocessContent(`{ "nodedata": { "topic": "Root", "children": [ { "topic": "Child 1" }, { "topic": "Child 2", "children": [ { "topic": "Grandchild" } ] } ] } }`, type, mime)).toEqual("root, child 1, child 2, grandchild");
});
});
describe("Canvas preprocessing", () => {
const type: NoteType = "canvas";
const mime = "application/json";
it("supports empty JSON", () => {
expect(preprocessContent("{}", type, mime)).toEqual("");
});
it("supports blank text / invalid JSON", () => {
expect(preprocessContent("", type, mime)).toEqual("");
});
it("reads elements", () => {
expect(preprocessContent(`{ "elements": [ { "type": "text", "text": "Hello" } ] }`, type, mime)).toEqual("hello");
expect(preprocessContent(`{ "elements": [ { "type": "text" }, { "type": "text", "text": "World" }, { "type": "rectangle", "text": "Ignored" } ] }`, type, mime)).toEqual("world");
});
});

View File

@@ -0,0 +1,126 @@
import striptags from "striptags";
import { normalize } from "../../utils.js";
export default function preprocessContent(content: string | Buffer, type: string, mime: string, raw?: boolean) {
content = normalize(content.toString());
if (type === "text" && mime === "text/html") {
if (!raw) {
// Content size already filtered at DB level, safe to process
content = stripTags(content);
}
content = content.replace(/&nbsp;/g, " ");
} else if (type === "mindMap" && mime === "application/json") {
content = processMindmapContent(content);
} else if (type === "canvas" && mime === "application/json") {
content = processCanvasContent(content);
}
return content.trim();
}
function processMindmapContent(content: string) {
let mindMapcontent;
try {
mindMapcontent = JSON.parse(content);
} catch (e) {
return "";
}
// Define interfaces for the JSON structure
interface MindmapNode {
id: string;
topic: string;
children: MindmapNode[]; // Recursive structure
direction?: number;
expanded?: boolean;
}
interface MindmapData {
nodedata: MindmapNode;
arrows: any[]; // If you know the structure, replace `any` with the correct type
summaries: any[];
direction: number;
theme: {
name: string;
type: string;
palette: string[];
cssvar: Record<string, string>; // Object with string keys and string values
};
}
// Recursive function to collect all topics
function collectTopics(node?: MindmapNode): string[] {
if (!node) {
return [];
}
// Collect the current node's topic
let topics = [node.topic];
// If the node has children, collect topics recursively
if (node.children && node.children.length > 0) {
for (const child of node.children) {
topics = topics.concat(collectTopics(child));
}
}
return topics;
}
// Start extracting from the root node
const topicsArray = collectTopics(mindMapcontent.nodedata);
// Combine topics into a single string
const topicsString = topicsArray.join(", ");
return normalize(topicsString.toString());
}
function processCanvasContent(content: string) {
interface Element {
type: string;
text?: string; // Optional since not all objects have a `text` property
id: string;
[key: string]: any; // Other properties that may exist
}
let canvasContent;
try {
canvasContent = JSON.parse(content);
} catch (e) {
return "";
}
const elements = canvasContent.elements;
if (Array.isArray(elements)) {
const texts = elements
.filter((element: Element) => element.type === "text" && element.text) // Filter for 'text' type elements with a 'text' property
.map((element: Element) => element.text!); // Use `!` to assert `text` is defined after filtering
content = normalize(texts.join(" "));
} else {
content = "";
}
return content;
}
function stripTags(content: string) {
// we want to allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412
// we want to insert space in place of block tags (because they imply text separation)
// but we don't want to insert text for typical formatting inline tags which can occur within one word
const linkTag = "a";
const inlineFormattingTags = ["b", "strong", "em", "i", "span", "big", "small", "font", "sub", "sup"];
// replace tags which imply text separation with a space
content = striptags(content, [linkTag, ...inlineFormattingTags], " ");
// replace the inline formatting tags (but not links) without a space
content = striptags(content, [linkTag], "");
// at least the closing link tag can be easily stripped
return content.replace(/<\/a>/gi, "");
}

View File

@@ -9,9 +9,9 @@
"preview": "pnpm build && vite preview"
},
"dependencies": {
"i18next": "25.8.14",
"i18next": "25.8.17",
"i18next-http-backend": "3.0.2",
"preact": "10.28.4",
"preact": "10.29.0",
"preact-iso": "2.11.1",
"preact-render-to-string": "6.6.6",
"react-i18next": "16.5.6"

View File

@@ -197,5 +197,12 @@
"description": "Trilium Notes는 간편한 접근 및 관리를 위해 유료 서비스인 PikaPods에서 호스팅할 수 있습니다. Trilium 팀과 직접 제휴되어있지는 않습니다.",
"download_pikapod": "PikaPods에서 설치하기",
"download_triliumcc": "또는 trilium.cc를 참조하세요"
},
"resources": {
"title": "리소스",
"icon_packs": "아이콘 팩",
"icon_packs_intro": "아이콘 팩을 사용하여 노트에 사용할 수 있는 아이콘 종류를 늘려보세요. 아이콘 팩에 대한 자세한 내용은 <DocumentationLink>공식 문서</DocumentationLink>를 참조하세요.",
"download": "다운로드",
"website": "웹사이트"
}
}

View File

@@ -50,7 +50,9 @@
"canvas_description": "Розташовуйте фігури, зображення та текст на нескінченному полотні, використовуючи ту саму технологію, що й excalidraw.com. Ідеально підходить для діаграм, ескізів та візуального планування.",
"mermaid_description": "Створюйте діаграми, такі як блок-схеми, діаграми класів та послідовностей, діаграми Ганта та багато іншого, використовуючи синтаксис Mermaid.",
"others_list": "та інші: <0>карта нотаток</0>, <1>карта зв'язків</1>, <2>збережені пошуки</2>, <3>візуалізація нотаток</3> та <4>веб-перегляди</4>.",
"mermaid_title": "Mermaid діаграми"
"mermaid_title": "Mermaid діаграми",
"mindmap_title": "Карта думок",
"mindmap_description": "Візуально упорядкуйте свої думки або проведіть мозковий штурм."
},
"extensibility_benefits": {
"title": "Спільне використання та розширюваність",
@@ -59,7 +61,9 @@
"share_title": "Діліться нотатками в Інтернеті",
"share_description": "Якщо у Вас є сервер, Ви можете використати його, щоб поділитися частиною своїх нотаток з іншими людьми.",
"api_title": "REST API",
"api_description": "Взаємодійте з Trilium програмно, використовуючи його вбудований REST API."
"api_description": "Взаємодійте з Trilium програмно, використовуючи його вбудований REST API.",
"scripting_title": "Розширений скриптинг",
"scripting_description": "Створюйте власні інтеграції в Trilium за допомогою користувацьких віджетів або серверної логіки."
},
"collections": {
"title": "Колекції",
@@ -108,7 +112,8 @@
"header": {
"get-started": "Почати",
"documentation": "Документація",
"support-us": "Підтримайте нас"
"support-us": "Підтримайте нас",
"resources": "Ресурси"
},
"footer": {
"copyright_and_the": " і ",
@@ -148,7 +153,8 @@
"description_arm64": "Сумісний з пристроями ARM (наприклад, з Qualcomm Snapdragon).",
"quick_start": "Щоб встановити через Winget:",
"download_exe": "Завантажити інсталятор (.exe)",
"download_zip": "Портативний (.zip)"
"download_zip": "Портативний (.zip)",
"download_scoop": "Scoop"
},
"download_helper_desktop_linux": {
"title_x64": "Linux 64-bit",
@@ -159,23 +165,44 @@
"download_deb": ".deb",
"download_rpm": ".rpm",
"download_flatpak": ".flatpak",
"download_nixpkgs": "nixpkgs"
"download_nixpkgs": "nixpkgs",
"download_zip": "Portable (.zip)",
"download_aur": "AUR"
},
"download_helper_desktop_macos": {
"title_x64": "macOS для Intel",
"title_arm64": "macOS для Apple Silicon",
"quick_start": "Для того, щоб встановити за допомогою Homebrew:",
"download_homebrew_cask": "Homebrew Cask"
"download_homebrew_cask": "Homebrew Cask",
"description_x64": "Для комп’ютерів Mac на базі Intel з macOS Monterey або пізнішої версії.",
"description_arm64": "Для комп'ютерів Apple Silicon Mac, таких як ті, що мають чіпи M1 та M2.",
"download_dmg": "Завантажити інсталятор (.dmg)",
"download_zip": "Portable (.zip)"
},
"download_helper_server_docker": {
"download_dockerhub": "Docker Hub",
"download_ghcr": "ghcr.io"
"download_ghcr": "ghcr.io",
"title": "Self-hosted using Docker",
"description": "Легке розгортання на Windows, Linux або macOS за допомогою контейнера Docker."
},
"download_helper_server_linux": {
"download_tar_x64": "x64 (.tar.xz)",
"download_tar_arm64": "ARM (.tar.xz)"
"download_tar_arm64": "ARM (.tar.xz)",
"title": "Self-hosted on Linux",
"description": "Розгорніть Trilium Notes на власному сервері або VPS, сумісному з більшістю дистрибутивів.",
"download_nixos": "NixOS module"
},
"download_helper_server_hosted": {
"title": "Платний хостинг"
"title": "Платний хостинг",
"description": "Нотатки Trilium розміщені на PikaPods, платному сервісі для легкого доступу та керування. Не пов'язаний безпосередньо з командою Trilium.",
"download_pikapod": "Налаштування на PikaPods",
"download_triliumcc": "Або див. trilium.cc"
},
"resources": {
"title": "Ресурси",
"icon_packs": "Пакети піктограм",
"icon_packs_intro": "Розширте вибір доступних піктограм для ваших нотаток за допомогою пакету піктограм. Щоб отримати докладнішу інформацію про пакети піктограм, див. <DocumentationLink>офіційну документацію</DocumentationLink>.",
"download": "Завантажити",
"website": "Вебсайт"
}
}

28
docs/README-ko.md vendored
View File

@@ -263,23 +263,19 @@ docs](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/De
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - 텍스트 노트의 시각적 편집기입니다. 프리미엄
기능을 제공해주셔서 감사합니다.
* [CodeMirror](https://github.com/codemirror/CodeMirror) - 수많은 언어를 지원하는 코드 편집기.
* [Excalidraw](https://github.com/excalidraw/excalidraw) - the infinite
whiteboard used in Canvas notes.
* [Mind Elixir](https://github.com/SSShooter/mind-elixir-core) - providing the
mind map functionality.
* [Leaflet](https://github.com/Leaflet/Leaflet) - for rendering geographical
maps.
* [Tabulator](https://github.com/olifolkerd/tabulator) - for the interactive
table used in collections.
* [FancyTree](https://github.com/mar10/fancytree) - feature-rich tree library
without real competition.
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library.
Used in [relation
maps](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and
[link
maps](https://docs.triliumnotes.org/user-guide/advanced-usage/note-map#link-map)
* [Excalidraw](https://github.com/excalidraw/excalidraw) - Canvas 노트에서 사용되는 무한
화이트보드입니다.
* [Mind Elixir](https://github.com/SSShooter/mind-elixir-core) - 마인드맵 기능을 제공합니다.
* [Leaflet](https://github.com/Leaflet/Leaflet) - 지리 지도를 렌더링 합니다.
* [Tabulator](https://github.com/olifolkerd/tabulator) - 컬렉션에서 사용되는 인터랙티브
테이블입니다.
* [FancyTree](https://github.com/mar10/fancytree) - 독보적으로 기능이 풍부한 트리 라이브러리입니다.
* [jsPlumb](https://github.com/jsplumb/jsplumb) - 시각적 연결 라이브러리입니다. [관계
](https://docs.triliumnotes.org/user-guide/note-types/relation-map) 과 [링크
](https://docs.triliumnotes.org/user-guide/advanced-usage/note-map#link-map)에
사용됩니다
## 🤝 Support
## 🤝 후원
Trilium is built and maintained with [hundreds of hours of
work](https://github.com/TriliumNext/Trilium/graphs/commit-activity). Your

46
docs/README-uk.md vendored
View File

@@ -95,8 +95,8 @@ Trilium Notes — це безкоштовний кросплатформний
безпечнішого входу
* [Синхронізація](https://docs.triliumnotes.org/user-guide/setup/synchronization)
із власним сервером синхронізації
* there are [3rd party services for hosting synchronisation
server](https://docs.triliumnotes.org/user-guide/setup/server/cloud-hosting)
* існують [сторонні сервіси для розміщення сервера
синхронізації](https://docs.triliumnotes.org/user-guide/setup/server/cloud-hosting)
* [Спільне
використання](https://docs.triliumnotes.org/user-guide/advanced-usage/sharing)
(публікація) нотаток у загальнодоступному інтернеті
@@ -105,10 +105,11 @@ Trilium Notes — це безкоштовний кросплатформний
з деталізацією для кожної нотатки
* Створення ескізних схем на основі [Excalidraw](https://excalidraw.com/) (тип
нотатки "полотно")
* [Relation
maps](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and
[note/link maps](https://docs.triliumnotes.org/user-guide/note-types/note-map)
for visualizing notes and their relations
* [Карти
зв'язків](https://docs.triliumnotes.org/user-guide/note-types/relation-map) та
[карти
нотаток/посилань](https://docs.triliumnotes.org/user-guide/note-types/note-map)
для візуалізації нотаток та їх зв'язків
* Інтелект-карти, засновані на [Mind Elixir](https://docs.mind-elixir.com/)
* [Геокарти](https://docs.triliumnotes.org/user-guide/collections/geomap) з
географічними позначками та GPX-треками
@@ -148,19 +149,18 @@ TriliumNext:
надав репозиторій Trilium спільнотному проекту, який знаходиться за адресою
https://github.com/TriliumNext
### ⬆️Migrating from Zadam/Trilium?
### ⬆️Переходите із Zadam/Trilium?
There are no special migration steps to migrate from a zadam/Trilium instance to
a TriliumNext/Trilium instance. Simply [install
TriliumNext/Trilium](#-installation) as usual and it will use your existing
database.
Немає жодних спеціальних кроків для міграції з екземпляра zadam/Trilium до
екземпляра TriliumNext/Trilium. Просто [встановіть
TriliumNext/Trilium](#-installation) як завжди, і він використовуватиме вашу
існуючу базу даних.
Versions up to and including
[v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are
compatible with the latest zadam/trilium version of
[v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later
versions of TriliumNext/Trilium have their sync versions incremented which
prevents direct migration.
Версії до [v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4)
включно сумісні з останньою версією zadam/trilium
[v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Будь-які
пізніші версії TriliumNext/Trilium мають збільшені версії синхронізації, що
запобігає прямій міграції.
## Обговоріть це з нами
@@ -189,8 +189,8 @@ prevents direct migration.
Якщо ваш дистрибутив зазначено в таблиці нижче, використовуйте пакет вашого
дистрибутива.
[![Packaging
status](https://repology.org/badge/vertical-allrepos/triliumnext.svg)](https://repology.org/project/triliumnext/versions)
[![Стан
упаковки](https://repology.org/badge/vertical-allrepos/triliumnext.svg)](https://repology.org/project/triliumnext/versions)
Ви також можете завантажити бінарний реліз для вашої платформи зі сторінки
[останнього релізу](https://github.com/TriliumNext/Trilium/releases/latest),
@@ -281,10 +281,10 @@ pnpm run --filter desktop electron-forge:make --arch=x64 --platform=win32
### Документація розробника
Please view the [documentation
guide](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md)
for details. If you have more questions, feel free to reach out via the links
described in the "Discuss with us" section above.
Будь ласка, перегляньте
[документацію](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md)
для отримання детальної інформації. Якщо у вас виникнуть додаткові запитання,
звертайтеся до нас за посиланнями, описаними в розділі «Обговоріть з нами» вище.
## 👏 Привітання

View File

@@ -1,6 +1,6 @@
{
"formatVersion": 2,
"appVersion": "0.101.3",
"appVersion": "0.102.1",
"files": [
{
"isClone": false,
@@ -10394,65 +10394,58 @@
{
"type": "relation",
"name": "internalLink",
"value": "wX4HbRucYSDD",
"value": "AjqEeiDUOzj4",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "ODY7qQn5m2FT",
"value": "mHbBMPDPkVV5",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "mHbBMPDPkVV5",
"value": "6f9hih2hXXZk",
"isInheritable": false,
"position": 80
},
{
"type": "relation",
"name": "internalLink",
"value": "6f9hih2hXXZk",
"value": "BlN9DFI679QC",
"isInheritable": false,
"position": 90
},
{
"type": "relation",
"name": "internalLink",
"value": "BlN9DFI679QC",
"value": "0vhv7lsOLy82",
"isInheritable": false,
"position": 100
},
{
"type": "relation",
"name": "internalLink",
"value": "0vhv7lsOLy82",
"value": "8YBEPzcpUgxw",
"isInheritable": false,
"position": 110
},
{
"type": "relation",
"name": "internalLink",
"value": "8YBEPzcpUgxw",
"value": "0ESUbbAxVnoK",
"isInheritable": false,
"position": 120
},
{
"type": "relation",
"name": "internalLink",
"value": "0ESUbbAxVnoK",
"isInheritable": false,
"position": 130
},
{
"type": "relation",
"name": "internalLink",
"value": "nBAXQFj20hS1",
"isInheritable": false,
"position": 140
"position": 130
},
{
"type": "label",
@@ -10473,7 +10466,7 @@
"dataFileName": "File.md",
"attachments": [
{
"attachmentId": "FoEnowwOhzLT",
"attachmentId": "fZ7VMfQJWuLQ",
"title": "image.png",
"role": "image",
"mime": "image/png",
@@ -10481,7 +10474,7 @@
"dataFileName": "File_image.png"
},
{
"attachmentId": "fZ7VMfQJWuLQ",
"attachmentId": "hddkgf7kr9g4",
"title": "image.png",
"role": "image",
"mime": "image/png",
@@ -10489,7 +10482,7 @@
"dataFileName": "1_File_image.png"
},
{
"attachmentId": "hddkgf7kr9g4",
"attachmentId": "hIg9g5pgsjS3",
"title": "image.png",
"role": "image",
"mime": "image/png",
@@ -10497,28 +10490,20 @@
"dataFileName": "2_File_image.png"
},
{
"attachmentId": "hIg9g5pgsjS3",
"attachmentId": "IC0j8LFCOKka",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "3_File_image.png"
},
{
"attachmentId": "IC0j8LFCOKka",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "4_File_image.png"
},
{
"attachmentId": "wNHX24feZRAl",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "5_File_image.png"
"dataFileName": "4_File_image.png"
}
],
"dirFileName": "File",
@@ -10609,6 +10594,114 @@
"dataFileName": "1_PDFs_image.png"
}
]
},
{
"isClone": false,
"noteId": "AjqEeiDUOzj4",
"notePath": [
"pOsGYCXsbNQG",
"KSZ04uQ2D1St",
"W8vYD3Q1zjCR",
"AjqEeiDUOzj4"
],
"title": "Videos",
"notePosition": 20,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "wX4HbRucYSDD",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "ODY7qQn5m2FT",
"isInheritable": false,
"position": 20
},
{
"type": "label",
"name": "iconClass",
"value": "bx bx-video",
"isInheritable": false,
"position": 50
}
],
"format": "markdown",
"dataFileName": "Videos.md",
"attachments": [
{
"attachmentId": "649jtu5ELGa8",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Videos_image.png"
}
]
}
]
},
{
"isClone": false,
"noteId": "GWHEkY4I4OE3",
"notePath": [
"pOsGYCXsbNQG",
"KSZ04uQ2D1St",
"GWHEkY4I4OE3"
],
"title": "Spreadsheets",
"notePosition": 220,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "label",
"name": "iconClass",
"value": "bx bx-table",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "2FvYrpmOXm29",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "iPIMuisry3hd",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "wy8So3yZZlH9",
"isInheritable": false,
"position": 60
}
],
"format": "markdown",
"dataFileName": "Spreadsheets.md",
"attachments": [
{
"attachmentId": "Eedn7QHJQbiV",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Spreadsheets_image.png"
}
]
}
@@ -15810,6 +15903,13 @@
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "6f9hih2hXXZk",
"isInheritable": false,
"position": 10
},
{
"type": "label",
"name": "shareAlias",
@@ -15823,13 +15923,6 @@
"value": "bx bx-bot",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "6f9hih2hXXZk",
"isInheritable": false,
"position": 40
}
],
"format": "markdown",

View File

@@ -15,7 +15,7 @@ Trilium is an open-source solution for note-taking and organizing a personal kno
* <a class="reference-link" href="User%20Guide/Installation%20%26%20Setup/Desktop%20Installation.md">Desktop Installation</a>
* <a class="reference-link" href="User%20Guide/Installation%20%26%20Setup/Server%20Installation.md">Server Installation</a>
* <a class="reference-link" href="User%20Guide/Scripting/Script%20API/Frontend%20API">Frontend API</a> or <a class="reference-link" href="User%20Guide/Scripting/Script%20API/Backend%20API.dat">[missing note]</a>
* <a class="reference-link" href="User%20Guide/Scripting/Script%20API/Frontend%20API">Frontend API</a> or <a class="reference-link" href="User%20Guide/Scripting/Script%20API/Backend%20API.dat">Backend API</a>
* [ETAPI reference](User%20Guide/Advanced%20Usage/ETAPI%20\(REST%20API\)/API%20Reference.dat)
## External links

View File

@@ -45,7 +45,7 @@ When a new promoted attribute definition is created, it creates a corresponding
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.
| | |
| | |
| --- | --- |
| <figure class="image"><img style="aspect-ratio:495/157;" src="2_Promoted Attributes_image.png" width="495" height="157"></figure> | Notice how the promoted attribute definition only creates a “Due date” box above the text content. |
| <figure class="image"><img style="aspect-ratio:663/160;" src="3_Promoted Attributes_image.png" width="663" height="160"></figure> | Once a value is set by the user, a new label (or relation, depending on the type) is created. The name of the attribute matches one set when creating the promoted attribute. |

View File

@@ -30,7 +30,7 @@ Additionally, shorter aliases are available for common configurations (see Alter
| Environment Variable | Type | Default | Description |
| --- | --- | --- | --- |
| `TRILIUM_GENERAL_INSTANCENAME` | string | "" | Instance name for API identification |
| `TRILIUM_GENERAL_INSTANCENAME` | string | "" | Instance name for API identification |
| `TRILIUM_GENERAL_NOAUTHENTICATION` | boolean | false | Disable authentication (server only) |
| `TRILIUM_GENERAL_NOBACKUP` | boolean | false | Disable automatic backups |
| `TRILIUM_GENERAL_NODESKTOPICON` | boolean | false | Disable desktop icon creation |
@@ -43,12 +43,12 @@ Additionally, shorter aliases are available for common configurations (see Alter
| `TRILIUM_NETWORK_HOST` | string | "0.0.0.0" | Server host binding |
| `TRILIUM_NETWORK_PORT` | string | "3000" | Server port |
| `TRILIUM_NETWORK_HTTPS` | boolean | false | Enable HTTPS |
| `TRILIUM_NETWORK_CERTPATH` | string | "" | SSL certificate path |
| `TRILIUM_NETWORK_KEYPATH` | string | "" | SSL key path |
| `TRILIUM_NETWORK_CERTPATH` | string | "" | SSL certificate path |
| `TRILIUM_NETWORK_KEYPATH` | string | "" | SSL key path |
| `TRILIUM_NETWORK_TRUSTEDREVERSEPROXY` | boolean/string | false | Reverse proxy trust settings |
| `TRILIUM_NETWORK_CORSALLOWORIGIN` | string | "" | CORS allowed origins |
| `TRILIUM_NETWORK_CORSALLOWMETHODS` | string | "" | CORS allowed methods |
| `TRILIUM_NETWORK_CORSALLOWHEADERS` | string | "" | CORS allowed headers |
| `TRILIUM_NETWORK_CORSALLOWORIGIN` | string | "" | CORS allowed origins |
| `TRILIUM_NETWORK_CORSALLOWMETHODS` | string | "" | CORS allowed methods |
| `TRILIUM_NETWORK_CORSALLOWHEADERS` | string | "" | CORS allowed headers |
| `TRILIUM_NETWORK_CORSRESOURCEPOLICY` | string | same-origin | CORS Resource Policy allows same-origin/same-site/cross-origin as values, will error if not |
### Session Section
@@ -61,26 +61,26 @@ Additionally, shorter aliases are available for common configurations (see Alter
| Environment Variable | Type | Default | Description |
| --- | --- | --- | --- |
| `TRILIUM_SYNC_SYNCSERVERHOST` | string | "" | Sync server host URL |
| `TRILIUM_SYNC_SYNCSERVERHOST` | string | "" | Sync server host URL |
| `TRILIUM_SYNC_SYNCSERVERTIMEOUT` | string | "120000" | Sync server timeout in milliseconds |
| `TRILIUM_SYNC_SYNCPROXY` | string | "" | Sync proxy URL |
| `TRILIUM_SYNC_SYNCPROXY` | string | "" | Sync proxy URL |
### MultiFactorAuthentication Section
| Environment Variable | Type | Default | Description |
| --- | --- | --- | --- |
| `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL` | string | "" | OAuth/OpenID base URL |
| `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID` | string | "" | OAuth client ID |
| `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET` | string | "" | OAuth client secret |
| `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL` | string | "" | OAuth/OpenID base URL |
| `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID` | string | "" | OAuth client ID |
| `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET` | string | "" | OAuth client secret |
| `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL` | string | "[https://accounts.google.com](https://accounts.google.com)" | OAuth issuer base URL |
| `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME` | string | "Google" | OAuth issuer display name |
| `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON` | string | "" | OAuth issuer icon URL |
| `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON` | string | "" | OAuth issuer icon URL |
### Logging Section
| Environment Variable | Type | Default | Description |
| --- | --- | --- | --- |
| `TRILIUM_LOGGING_RETENTIONDAYS` | integer | 90 | Number of days to retain log files |
| `TRILIUM_LOGGING_RETENTIONDAYS` | integer | 90 | Number of days to retain log files |
## Alternative Environment Variables

View File

@@ -22,9 +22,9 @@ These are the types of active content in Trilium, along with a few examples of w
| <a class="reference-link" href="../Scripting/Backend%20scripts.md">Backend scripts</a> | Yes | Can run custom code on the server of Trilium (Node.js environment), with full access to the notes and the database. | Has access to all the unencrypted notes, but with full access to the database it can completely destroy the data. It also has access to execute other applications or alter the files and folders on the server). |
| <a class="reference-link" href="../Note%20Types/Web%20View.md">Web View</a> | Yes | Displays a website inside a note. | Can point to a phishing website which can collect the data (for example on a log in page). |
| <a class="reference-link" href="../Note%20Types/Render%20Note.md">Render Note</a> | Yes | Renders custom content inside a note, such as a dashboard or a new editor that is not officially supported by Trilium. | Can affect the UI similar to front-end scripts or custom widgets since the scripts are not completely encapsulated, or they can act similar to a web view where they can collect data entered by the user. |
| <a class="reference-link" href="../Theme%20development/Custom%20app-wide%20CSS.md">Custom app-wide CSS</a> | No | Can alter the layout and style of the UI using CSS, applied regardless of theme. | Generally less problematic than the rest of active content, but a badly written CSS can affect the layout of the application, requiring the use of <a class="reference-link" href="../Advanced%20Usage/Safe%20mode.md">Safe mode</a> to be able to use the application. |
| [Custom themes](../Theme%20development) | No | Can change the style of the entire UI. | Similar to custom app-wide CSS. |
| <a class="reference-link" href="Themes/Icon%20Packs.md">Icon Packs</a> | No | Introduces new icons that can be used for notes. | Generally are more contained and less prone to cause issues, but they can cause performance issues (for example if the icon pack has millions of icons in it). |
| <a class="reference-link" href="../Theme%20development/Custom%20app-wide%20CSS.md">Custom app-wide CSS</a> | No | Can alter the layout and style of the UI using CSS, applied regardless of theme. | Generally less problematic than the rest of active content, but a badly written CSS can affect the layout of the application, requiring the use of <a class="reference-link" href="../Advanced%20Usage/Safe%20mode.md">Safe mode</a> to be able to use the application. |
| [Custom themes](../Theme%20development) | No | Can change the style of the entire UI. | Similar to custom app-wide CSS. |
| <a class="reference-link" href="Themes/Icon%20Packs.md">Icon Packs</a> | No | Introduces new icons that can be used for notes. | Generally are more contained and less prone to cause issues, but they can cause performance issues (for example if the icon pack has millions of icons in it). |
## Active content badge

View File

@@ -229,9 +229,9 @@ You can open Trilium and automatically trigger a search by including the search
| Parameter | Value | Description |
| --- | --- | --- |
| MIN\_FUZZY\_TOKEN\_LENGTH | 3 | Minimum characters for fuzzy matching |
| MAX\_EDIT\_DISTANCE | 2 | Maximum character changes allowed |
| RESULT\_SUFFICIENCY\_THRESHOLD | 5 | Minimum exact results before fuzzy fallback |
| MIN\_FUZZY\_TOKEN\_LENGTH | 3 | Minimum characters for fuzzy matching |
| MAX\_EDIT\_DISTANCE | 2 | Maximum character changes allowed |
| RESULT\_SUFFICIENCY\_THRESHOLD | 5 | Minimum exact results before fuzzy fallback |
| MAX\_CONTENT\_SIZE | 10MB | Maximum note content size for search processing |
### Limits

View File

@@ -3,7 +3,7 @@ Collections are a unique type of note that don't have content, but instead displ
## Main collections
| | |
| | |
| --- | --- |
| <figure class="image"><img style="aspect-ratio:1651/810;" src="Collections_collection_ca.webp" width="1651" height="810"></figure> | <a class="reference-link" href="Collections/Calendar.md">Calendar</a>  <br>which displays a week, month or year calendar with the notes being shown as events. New events can be added easily by dragging across the calendar. |
| <figure class="image"><img style="aspect-ratio:1643/647;" src="Collections_collection_ta.webp" width="1643" height="647"></figure> | <a class="reference-link" href="Collections/Table.md">Table</a>  <br>displays each note as a row in a table, with <a class="reference-link" href="Advanced%20Usage/Attributes/Promoted%20Attributes.md">Promoted Attributes</a> being shown as well. This makes it easy to visualize attributes of notes, as well as making them easily editable. |

View File

@@ -23,12 +23,12 @@ The position on the map and the zoom are saved inside the map note and restored
### Adding a new note using the plus button
| | | |
| | | |
| --- | --- | --- |
| 1 | To create a marker, first navigate to the desired point on the map. Then press the ![](9_Geo%20Map_image.png) button in the [Floating buttons](../Basic%20Concepts%20and%20Features/UI%20Elements/Floating%20buttons.md) (top-right) area.       <br> <br>If the button is not visible, make sure the button section is visible by pressing the chevron button (![](15_Geo%20Map_image.png)) in the top-right of the map. | |
| 2 | <img class="image_resized" style="aspect-ratio:1730/416;width:100%;" src="2_Geo Map_image.png" width="1730" height="416"> | Once pressed, the map will enter in the insert mode, as illustrated by the notification.          <br> <br>Simply click the point on the map where to place the marker, or the Escape key to cancel. |
| 3 | <img class="image_resized" style="aspect-ratio:1586/404;width:100%;" src="7_Geo Map_image.png" width="1586" height="404"> | Enter the name of the marker/note to be created. |
| 4 | <img class="image_resized" style="aspect-ratio:1696/608;width:100%;" src="14_Geo Map_image.png" width="1696" height="608"> | Once confirmed, the marker will show up on the map and it will also be displayed as a child note of the map. |
| 1 | To create a marker, first navigate to the desired point on the map. Then press the ![](9_Geo%20Map_image.png) button in the [Floating buttons](../Basic%20Concepts%20and%20Features/UI%20Elements/Floating%20buttons.md) (top-right) area.       <br> <br>If the button is not visible, make sure the button section is visible by pressing the chevron button (![](15_Geo%20Map_image.png)) in the top-right of the map. | |
| 2 | <img class="image_resized" style="aspect-ratio:1730/416;width:100%;" src="2_Geo Map_image.png" width="1730" height="416"> | Once pressed, the map will enter in the insert mode, as illustrated by the notification.          <br> <br>Simply click the point on the map where to place the marker, or the Escape key to cancel. |
| 3 | <img class="image_resized" style="aspect-ratio:1586/404;width:100%;" src="7_Geo Map_image.png" width="1586" height="404"> | Enter the name of the marker/note to be created. |
| 4 | <img class="image_resized" style="aspect-ratio:1696/608;width:100%;" src="14_Geo Map_image.png" width="1696" height="608"> | Once confirmed, the marker will show up on the map and it will also be displayed as a child note of the map. |
### Adding a new note using the contextual menu
@@ -106,31 +106,31 @@ The value of the attribute is made up of the latitude and longitude separated by
### Adding from Google Maps
| | | |
| | | |
| --- | --- | --- |
| 1 | <figure class="image image-style-align-center image_resized" style="width:56.84%;"><img style="aspect-ratio:732/918;" src="11_Geo Map_image.png" width="732" height="918"></figure> | Go to Google Maps on the web and look for a desired location, right click on it and a context menu will show up.          <br> <br>Simply click on the first item displaying the coordinates and they will be copied to clipboard.          <br> <br>Then paste the value inside the text box into the `#geolocation` attribute of a child note of the map (don't forget to surround the value with a `"` character). |
| 2 | <figure class="image image-style-align-center image_resized" style="width:100%;"><img style="aspect-ratio:518/84;" src="4_Geo Map_image.png" width="518" height="84"></figure> | In Trilium, create a child note under the map. |
| 3 | <figure class="image image-style-align-center image_resized" style="width:100%;"><img style="aspect-ratio:1074/276;" src="10_Geo Map_image.png" width="1074" height="276"></figure> | And then go to Owned Attributes and type `#geolocation="`, then paste from the clipboard as-is and then add the ending `"` character. Press Enter to confirm and the map should now be updated to contain the new note. |
| 1 | <figure class="image image-style-align-center image_resized" style="width:56.84%;"><img style="aspect-ratio:732/918;" src="11_Geo Map_image.png" width="732" height="918"></figure> | Go to Google Maps on the web and look for a desired location, right click on it and a context menu will show up.          <br> <br>Simply click on the first item displaying the coordinates and they will be copied to clipboard.          <br> <br>Then paste the value inside the text box into the `#geolocation` attribute of a child note of the map (don't forget to surround the value with a `"` character). |
| 2 | <figure class="image image-style-align-center image_resized" style="width:100%;"><img style="aspect-ratio:518/84;" src="4_Geo Map_image.png" width="518" height="84"></figure> | In Trilium, create a child note under the map. |
| 3 | <figure class="image image-style-align-center image_resized" style="width:100%;"><img style="aspect-ratio:1074/276;" src="10_Geo Map_image.png" width="1074" height="276"></figure> | And then go to Owned Attributes and type `#geolocation="`, then paste from the clipboard as-is and then add the ending `"` character. Press Enter to confirm and the map should now be updated to contain the new note. |
### Adding from OpenStreetMap
Similarly to the Google Maps approach:
| | | |
| | | |
| --- | --- | --- |
| 1 | <img class="image_resized" style="aspect-ratio:562/454;width:100%;" src="1_Geo Map_image.png" width="562" height="454"> | Go to any location on openstreetmap.org and right click to bring up the context menu. Select the “Show address” item. |
| 2 | <img class="image_resized" style="aspect-ratio:696/480;width:100%;" src="Geo Map_image.png" width="696" height="480"> | The address will be visible in the top-left of the screen, in the place of the search bar.          <br> <br>Select the coordinates and copy them into the clipboard. |
| 3 | <img class="image_resized" style="aspect-ratio:640/276;width:100%;" src="5_Geo Map_image.png" width="640" height="276"> | Simply paste the value inside the text box into the `#geolocation` attribute of a child note of the map and then it should be displayed on the map. |
| 1 | <img class="image_resized" style="aspect-ratio:562/454;width:100%;" src="1_Geo Map_image.png" width="562" height="454"> | Go to any location on openstreetmap.org and right click to bring up the context menu. Select the “Show address” item. |
| 2 | <img class="image_resized" style="aspect-ratio:696/480;width:100%;" src="Geo Map_image.png" width="696" height="480"> | The address will be visible in the top-left of the screen, in the place of the search bar.          <br> <br>Select the coordinates and copy them into the clipboard. |
| 3 | <img class="image_resized" style="aspect-ratio:640/276;width:100%;" src="5_Geo Map_image.png" width="640" height="276"> | Simply paste the value inside the text box into the `#geolocation` attribute of a child note of the map and then it should be displayed on the map. |
## Adding GPS tracks (.gpx)
Trilium has basic support for displaying GPS tracks on the geo map.
| | | |
| | | |
| --- | --- | --- |
| 1 | <figure class="image image-style-align-center"><img style="aspect-ratio:226/74;" src="3_Geo Map_image.png" width="226" height="74"></figure> | To add a track, simply drag & drop a .gpx file inside the geo map in the note tree. |
| 2 | <figure class="image image-style-align-center"><img style="aspect-ratio:322/222;" src="13_Geo Map_image.png" width="322" height="222"></figure> | In order for the file to be recognized as a GPS track, it needs to show up as `application/gpx+xml` in the _File type_ field. |
| 3 | <figure class="image image-style-align-center"><img style="aspect-ratio:620/530;" src="6_Geo Map_image.png" width="620" height="530"></figure> | When going back to the map, the track should now be visible.          <br> <br>The start and end points of the track are indicated by the two blue markers. |
| 1 | <figure class="image image-style-align-center"><img style="aspect-ratio:226/74;" src="3_Geo Map_image.png" width="226" height="74"></figure> | To add a track, simply drag & drop a .gpx file inside the geo map in the note tree. |
| 2 | <figure class="image image-style-align-center"><img style="aspect-ratio:322/222;" src="13_Geo Map_image.png" width="322" height="222"></figure> | In order for the file to be recognized as a GPS track, it needs to show up as `application/gpx+xml` in the _File type_ field. |
| 3 | <figure class="image image-style-align-center"><img style="aspect-ratio:620/530;" src="6_Geo Map_image.png" width="620" height="530"></figure> | When going back to the map, the track should now be visible.          <br> <br>The start and end points of the track are indicated by the two blue markers. |
> [!NOTE]
> The starting point of the track will be displayed as a marker, with the name of the note underneath. The start marker will also respect the icon and the `color` of the note. The end marker is displayed with a distinct icon.

View File

@@ -33,10 +33,10 @@ The following note types are supported by Trilium:
| <a class="reference-link" href="Note%20Types/Relation%20Map.md">Relation Map</a> | Allows easy creation of notes and relations between them. Can be used for mainly relational data such as a family tree. |
| <a class="reference-link" href="Note%20Types/Note%20Map.md">Note Map</a> | Displays the relationships between the notes, whether via relations or their hierarchical structure. |
| <a class="reference-link" href="Note%20Types/Render%20Note.md">Render Note</a> | Used in <a class="reference-link" href="Scripting.md">Scripting</a>, it displays the HTML content of another note. This allows displaying any kind of content, provided there is a script behind it to generate it. |
| <a class="reference-link" href="Collections.md">Collections</a> | Displays the children of the note either as a grid, a list, or for a more specialized case: a calendar. <br> <br>Generally useful for easy reading of short notes. |
| <a class="reference-link" href="Collections.md">Collections</a> | Displays the children of the note either as a grid, a list, or for a more specialized case: a calendar.  <br> <br>Generally useful for easy reading of short notes. |
| <a class="reference-link" href="Note%20Types/Mermaid%20Diagrams.md">Mermaid Diagrams</a> | Displays diagrams such as bar charts, flow charts, state diagrams, etc. Requires a bit of technical knowledge since the diagrams are written in a specialized format. |
| <a class="reference-link" href="Note%20Types/Canvas.md">Canvas</a> | Allows easy drawing of sketches, diagrams, handwritten content. Uses the same technology behind [excalidraw.com](https://excalidraw.com). |
| <a class="reference-link" href="Note%20Types/Web%20View.md">Web View</a> | Displays the content of an external web page, similar to a browser. |
| <a class="reference-link" href="Note%20Types/Mind%20Map.md">Mind Map</a> | Easy for brainstorming ideas, by placing them in a hierarchical layout. |
| <a class="reference-link" href="Collections/Geo%20Map.md">Geo Map View</a> | Displays the children of the note as a geographical map, one use-case would be to plan vacations. It even has basic support for tracks. Notes can also be created from it. |
| <a class="reference-link" href="Collections/Geo%20Map.md">Geo Map</a> | Displays the children of the note as a geographical map, one use-case would be to plan vacations. It even has basic support for tracks. Notes can also be created from it. |
| <a class="reference-link" href="Note%20Types/File.md">File</a> | Represents an uploaded file such as PDFs, images, video or audio files. |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 612 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 612 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -16,7 +16,7 @@ See <a class="reference-link" href="File/PDFs.md">PDFs</a>.
### Images
<figure class="image image-style-align-center image_resized" style="width:50%;"><img style="aspect-ratio:879/766;" src="3_File_image.png" width="879" height="766"></figure>
<figure class="image image-style-align-center image_resized" style="width:50%;"><img style="aspect-ratio:879/766;" src="2_File_image.png" width="879" height="766"></figure>
Interaction:
@@ -26,16 +26,11 @@ Interaction:
### Videos
<figure class="image image-style-align-center image_resized" style="width:50%;"><img style="aspect-ratio:854/700;" src="File_image.png" width="854" height="700"></figure>
Video files can be added in as well. The file is streamed directly, so when accessing the note from a server it doesn't have to download the entire video to start playing it.
> [!CAUTION]
> Although Trilium offers support for videos, it is generally not meant to be used with very large files. Uploading large videos will cause the <a class="reference-link" href="../Advanced%20Usage/Database.md">Database</a> to balloon as well as the any <a class="reference-link" href="../Installation%20%26%20Setup/Backup.md">Backup</a> of it. In addition to that, there might be slowdowns when first uploading the files. Otherwise, a large database should not impact the general performance of Trilium significantly.
See <a class="reference-link" href="File/Videos.md">Videos</a>.
### Audio
<figure class="image image-style-align-center image_resized" style="width:50%;"><img style="aspect-ratio:850/243;" src="2_File_image.png" width="850" height="243"></figure>
<figure class="image image-style-align-center image_resized" style="width:50%;"><img style="aspect-ratio:850/243;" src="1_File_image.png" width="850" height="243"></figure>
Adding a supported audio file will reveal a basic audio player that can be used to play it.
@@ -48,7 +43,7 @@ Interactions:
### Text files
<figure class="image image-style-align-center image_resized" style="width:50%;"><img style="aspect-ratio:926/347;" src="1_File_image.png" width="926" height="347"></figure>
<figure class="image image-style-align-center image_resized" style="width:50%;"><img style="aspect-ratio:926/347;" src="File_image.png" width="926" height="347"></figure>
Files that are identified as containing text will show a preview of their content. One common use case for this type of file is to embed text files whose content is not necessarily of interest to the user, such as third-party libraries or generated content, that can then be downloaded if needed.
@@ -58,7 +53,7 @@ Since one of the use cases for having files instead of notes is to display large
### Unknown file types
<figure class="image image-style-align-center image_resized" style="width:50%;"><img style="aspect-ratio:532/240;" src="4_File_image.png" width="532" height="240"></figure>
<figure class="image image-style-align-center image_resized" style="width:50%;"><img style="aspect-ratio:532/240;" src="3_File_image.png" width="532" height="240"></figure>
If the file could not be identified as any of the supported file types from above, it will be treated as an unknown file. In this case, all the default interactions will be available such as downloading or opening the file externally, but there will be no preview of the content.
@@ -75,6 +70,6 @@ If the file could not be identified as any of the supported file types from abov
* Files are also displayed in the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/Notes/Note%20List.md">Note List</a> based on their type:
<img class="image_resized" style="aspect-ratio:853/315;width:50%;" src="5_File_image.png" width="853" height="315">
<img class="image_resized" style="aspect-ratio:853/315;width:50%;" src="4_File_image.png" width="853" height="315">
* Non-image files can be embedded into text notes as read-only widgets via the <a class="reference-link" href="Text/Include%20Note.md">Include Note</a> functionality.
* Image files can be embedded into text notes like normal images via <a class="reference-link" href="Text/Images/Image%20references.md">Image references</a>.

View File

@@ -0,0 +1,63 @@
# Videos
<figure class="image image-style-align-right image_resized" style="width:61.8%;"><img style="aspect-ratio:953/587;" src="Videos_image.png" width="953" height="587"></figure>
Starting with v0.103.0, Trilium has a custom video player which offers more features than the built-in video player.
Versions prior to v0.103.0 also support videos, but using the built-in player.
The file is streamed directly, so when accessing the note from a server it doesn't have to download the entire video to start playing it.
## Note on large video files
Although Trilium offers support for videos, it is generally not meant to be used with very large files. Uploading large videos will cause the <a class="reference-link" href="../../Advanced%20Usage/Database.md">Database</a> to balloon as well as the any <a class="reference-link" href="../../Installation%20%26%20Setup/Backup.md">Backup</a> of it. In addition to that, there might be slowdowns when first uploading the files. Otherwise, a large database should not impact the general performance of Trilium significantly.
## Supported formats
Trilium uses the built-in video decoding mechanism of the browser (or Electron/Chromium when running on the desktop). Starting with v0.103.0, a message will be displayed instead when a video format is not supported.
## Interactions
To play/pause the video, simply click anywhere on the video.
The controls at the bottom will hide automatically after playing, simply move the mouse to show them again.
The bottom bar has the following features:
* A track bar to seek across the video.
* On the left of the track bar, the current time is indicated.
* On the right of the track bar, the remaining time is indicated.
* On the left side there are buttons to:
* Adjust the playback speed (e.g. 0.5x, 1x).
* Rotate the video by 90 degrees.
* In the center:
* Go back by 10s
* Play/pause
* Go forward by 30s
* Loop, which when enabled will restart the video once it reaches the end.
* On the right side:
* Mute button
* Volume adjustment
* Full screen
* Zoom to fill, which will crop the video so that it fills the entire window.
* Picture-in-picture (if the browser supports it).
## Keyboard shortcuts
The following keyboard shortcuts are supported by the video player:
| | |
| --- | --- |
| <kbd>Space</kbd> | Play/pause |
| <kbd>Left arrow key</kbd> | Go back by 10s |
| <kbd>Right arrow key</kbd> | Go forward by 10s |
| <kbd>Ctrl</kbd> + <kbd>Left arrow key</kbd> | Go back by 1 min |
| <kbd>Ctrl</kbd> + <kbd>Right arrow key</kbd> | Go right by 1 min |
| <kbd>F</kbd> | Toggle full-screen |
| <kbd>M</kbd> | Mute/unmute |
| <kbd>Home</kbd> | Go to the beginning of the video |
| <kbd>End</kbd> | Go to the end of the video |
| <kbd>Up</kbd> | Increase volume by 5% |
| <kbd>Down</kbd> | Decrease volume by 5% |

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 652 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,66 @@
# Spreadsheets
<figure class="image"><img style="aspect-ratio:1102/573;" src="Spreadsheets_image.png" width="1102" height="573"></figure>
> [!IMPORTANT]
> Spreadsheets are a new type of note introduced in v0.103.0 and are currently considered experimental/beta. As such, expect major changes to occur to this note type.
Spreadsheets provide a familiar experience to Microsoft Excel or LibreOffice Calc, with support for formulas, data validation and text formatting.
## Spreadsheets vs. collections
There is a slight overlap between spreadsheets and the <a class="reference-link" href="../Collections/Table.md">Table</a> collection. In general the table collection is useful to track meta-information about notes (for example a collection of people and their birthdays), whereas spreadsheets are quite useful for calculations since they support formulas.
Spreadsheets also benefit from a wider range of features such as data validation, formatting and can work on a relatively large dataset.
## Important statement regarding data format
For Trilium as a knowledge database, it is important that data is stored in a format that is easy to convert to something else. For example, <a class="reference-link" href="Text.md">Text</a> notes can be exported to either HTML or Markdown, making it relatively easy to migrate to another software or simply to stand the test of time.
For spreadsheets, Trilium uses a technology called [Univer Sheets](https://docs.univer.ai/), developed by DreamNum Co., Ltd. Although this software library is quite powerful and has a good track record (starting with Luckysheet from 2020, becoming Univer somewhere in 2023), it uses its own JSON format to store the sheets.
As such, if Univer were to become unmaintained or incompatible for some reason, your data might become vendor locked-in.
With that in mind, spreadsheets can be really useful for quick calculations, but it's important not to have critical information on it that you might not want to need in a few years time.
## Regarding data export
Currently, in Trilium there is no way to export the spreadsheets to CSV or Excel formats. We might manage to add support for it at some point, but currently this is not the case.
## Supported features
The spreadsheet has support for the following features:
* Filtering
* Sorting
* Data validation
* Conditional formatting
* Notes / annotations
* Find / replace
We might consider adding [other features](https://docs.univer.ai/guides/sheets/features/filter) from Univer at some point. If there is a particular feature that can be added easily, it can be discussed over [GitHub Issues](../Troubleshooting/Reporting%20issues.md).
## Features not supported yet
### Regarding Pro features
Univer spreadsheets also feature a [Pro plan](https://univer.ai/pro) which adds quite a lot of functionality such as charts, printing, pivot tables, export, etc.
As the Pro plan needs a license, Trilium does not support any of the premium features. Theoretically, pro features can be used in trial mode with some limitations, we might explore this direction at some point.
### Planned features
There are a few features that are already planned but are not supported yet:
* Trilium-specific formulas (e.g. to obtain the title of a note).
* User-defined formulas
* Cross-workbook calculation
If you would like us to work on these features, consider [supporting us](https://triliumnotes.org/en/support-us).
## Known limitations
* It is possible to share a spreadsheet, case in which a best-effort HTML rendering of the spreadsheet is done.
* For more advanced use cases, this will most likely not work as intended. Feel free to [report issues](../Troubleshooting/Reporting%20issues.md), but keep in mind that we might not be able to have a complete feature parity with all the features of Univer.
* There is currently no export functionality, as stated previously.
* There is no dedicated mobile support. Mobile support is currently experimental in Univer and when it becomes stable, we could potentially integrate it into Trilium as well.

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -1,7 +1,7 @@
# Keyboard shortcuts
## Trilium-specific shortcuts
| Action | PC | Mac |
| Action | PC | Mac |
| --- | --- | --- |
| Bring up inline formatting toolbar (arrow keys <kbd><span>←</span></kbd>,<kbd><span>→</span></kbd> to navigate, <kbd>Enter</kbd> to apply) | <kbd>Alt</kbd>+<kbd>F10</kbd> | <kbd>⌥</kbd>+<kbd>F10</kbd> |
| Bring up block formatting toolbar | <kbd>Alt</kbd>+<kbd>F10</kbd> | <kbd>⌥</kbd>+<kbd>F10</kbd> |
@@ -13,9 +13,9 @@
| Mark selected text as [keyboard shortcut](Developer-specific%20formatting.md) | <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>K</kbd> | <kbd>⌘</kbd>\+ <kbd>⌥</kbd>\+ <kbd>K</kbd> |
| Insert <a class="reference-link" href="Math%20Equations.md">Math Equations</a> | <kbd>Ctrl</kbd> + <kbd>M</kbd> | <kbd>⌘</kbd>\+ <kbd>M</kbd> |
| Move blocks (lists, paragraphs, etc.) up | <kbd>Ctrl</kbd>+<kbd>↑</kbd> | <kbd>⌘</kbd>+<kbd>↑</kbd> |
| <kbd>Alt</kbd>+<kbd>↑</kbd> | <kbd>⌥</kbd>+<kbd>↑</kbd> | |
| <kbd>Alt</kbd>+<kbd>↑</kbd> | <kbd>⌥</kbd>+<kbd>↑</kbd> | |
| Move blocks (lists, paragraphs, etc.) down | <kbd>Ctrl</kbd>+<kbd>↑</kbd> | <kbd>⌘</kbd>+<kbd>↑</kbd> |
| <kbd>Alt</kbd>+<kbd>↓</kbd> | <kbd>⌥</kbd>+<kbd>↓</kbd> | |
| <kbd>Alt</kbd>+<kbd>↓</kbd> | <kbd>⌥</kbd>+<kbd>↓</kbd> | |
## Common shortcuts
@@ -24,9 +24,9 @@
### Content editing
| Action | PC | Mac |
| Action | PC | Mac |
| --- | --- | --- |
| Insert a hard break (a new paragraph) | <kbd>Enter</kbd> | |
| Insert a hard break (a new paragraph) | <kbd>Enter</kbd> | |
| Insert a soft break (a `<br>` element) | <kbd>Shift</kbd>+<kbd>Enter</kbd> | <kbd>⇧Enter</kbd> |
| Copy selected content | <kbd>Ctrl</kbd>+<kbd>C</kbd> | <kbd>⌘C</kbd> |
| Paste content | <kbd>Ctrl</kbd>+<kbd>V</kbd> | <kbd>⌘V</kbd> |
@@ -36,8 +36,8 @@
| Bold text | <kbd>Ctrl</kbd>+<kbd>B</kbd> | <kbd>⌘B</kbd> |
| Change text case | <kbd>Shift</kbd>+<kbd>F3</kbd> | <kbd>⇧F3</kbd> (may require <kbd>Fn</kbd>) |
| Create link | <kbd>Ctrl</kbd>+<kbd>K</kbd> | <kbd>⌘K</kbd> |
| Move out of a link | <kbd>←←</kbd>, <kbd>→→</kbd> | |
| Move out of an inline code style | <kbd>←←</kbd>, <kbd>→→</kbd> | |
| Move out of a link | <kbd>←←</kbd>, <kbd>→→</kbd> | |
| Move out of an inline code style | <kbd>←←</kbd>, <kbd>→→</kbd> | |
| Select all | <kbd>Ctrl</kbd>+<kbd>A</kbd> | <kbd>⌘A</kbd> |
| Find in the document | <kbd>Ctrl</kbd>+<kbd>F</kbd> | <kbd>⌘F</kbd> |
| Copy text formatting | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>C</kbd> | <kbd>⌘⇧C</kbd> |
@@ -45,45 +45,45 @@
| Italic text | <kbd>Ctrl</kbd>+<kbd>I</kbd> | <kbd>⌘I</kbd> |
| Strikethrough text | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>X</kbd> | <kbd>⌘⇧X</kbd> |
| Underline text | <kbd>Ctrl</kbd>+<kbd>U</kbd> | <kbd>⌘U</kbd> |
| Revert autoformatting action | <kbd>Backspace</kbd> | |
| Revert autoformatting action | <kbd>Backspace</kbd> | |
### Interacting with blocks
Blocks are images, tables, blockquotes, annotations.
| Action | PC | Mac |
| Action | PC | Mac |
| --- | --- | --- |
| Insert a new paragraph directly after a widget | <kbd>Enter</kbd> | |
| Insert a new paragraph directly after a widget | <kbd>Enter</kbd> | |
| Insert a new paragraph directly before a widget | <kbd>Shift</kbd>+<kbd>Enter</kbd> | <kbd>⇧Enter</kbd> |
| Move the caret to allow typing directly before a widget | <kbd>↑</kbd>, <kbd>←</kbd> | |
| Move the caret to allow typing directly after a widget | <kbd>↓</kbd>, <kbd>→</kbd> | |
| After entering a nested editable, move the selection to the closest ancestor widget. For example: move from an image caption to the whole image widget. | <kbd>Tab</kbd> then <kbd>Esc</kbd> | |
| Move the caret to allow typing directly before a widget | <kbd>↑</kbd>, <kbd>←</kbd> | |
| Move the caret to allow typing directly after a widget | <kbd>↓</kbd>, <kbd>→</kbd> | |
| After entering a nested editable, move the selection to the closest ancestor widget. For example: move from an image caption to the whole image widget. | <kbd>Tab</kbd> then <kbd>Esc</kbd> | |
Specifically for lists:
| Action | PC | Mac |
| Action | PC | Mac |
| --- | --- | --- |
| Increase list item indent | <kbd>⇥</kbd> | |
| Increase list item indent | <kbd>⇥</kbd> | |
| Decrease list item indent | <kbd>Shift</kbd>+<kbd>⇥</kbd> | <kbd>⇧⇥</kbd> |
In tables:
| Action | PC | Mac |
| Action | PC | Mac |
| --- | --- | --- |
| Move the selection to the next cell | <kbd>⇥</kbd> | |
| Move the selection to the next cell | <kbd>⇥</kbd> | |
| Move the selection to the previous cell | <kbd>Shift</kbd>+<kbd>⇥</kbd> | <kbd>⇧⇥</kbd> |
| Insert a new table row (when in the last cell of a table) | <kbd>⇥</kbd> | |
| Navigate through the table | <kbd>↑</kbd>, <kbd>→</kbd>, <kbd>↓</kbd>, <kbd>←</kbd> | |
| Insert a new table row (when in the last cell of a table) | <kbd>⇥</kbd> | |
| Navigate through the table | <kbd>↑</kbd>, <kbd>→</kbd>, <kbd>↓</kbd>, <kbd>←</kbd> | |
### General UI shortcuts
| Action | PC | Mac |
| Action | PC | Mac |
| --- | --- | --- |
| Close contextual balloons, dropdowns, and dialogs | <kbd>Esc</kbd> | |
| Close contextual balloons, dropdowns, and dialogs | <kbd>Esc</kbd> | |
| Open the accessibility help dialog | <kbd>Alt</kbd>+<kbd>0</kbd> | <kbd>⌥0</kbd> |
| Move focus between form fields (inputs, buttons, etc.) | <kbd>⇥</kbd>, <kbd>Shift</kbd>+<kbd>⇥</kbd> | <kbd>⇥</kbd>, <kbd>⇧⇥</kbd> |
| Move focus to the toolbar, navigate between toolbars | <kbd>Alt</kbd>+<kbd>F10</kbd> | <kbd>⌥F10</kbd> (may require <kbd>Fn</kbd>) |
| Navigate through the toolbar or menu bar | <kbd>↑</kbd>, <kbd>→</kbd>, <kbd>↓</kbd>, <kbd>←</kbd> | |
| Navigate to the next focusable field or an element outside the editor | <kbd>Tab</kbd>, <kbd>Shift</kbd>+<kbd>Tab</kbd> | |
| Execute the currently focused button. Executing buttons that interact with the editor content moves the focus back to the content. | <kbd>Enter</kbd>, <kbd>Space</kbd> | |
| Navigate through the toolbar or menu bar | <kbd>↑</kbd>, <kbd>→</kbd>, <kbd>↓</kbd>, <kbd>←</kbd> | |
| Navigate to the next focusable field or an element outside the editor | <kbd>Tab</kbd>, <kbd>Shift</kbd>+<kbd>Tab</kbd> | |
| Execute the currently focused button. Executing buttons that interact with the editor content moves the focus back to the content. | <kbd>Enter</kbd>, <kbd>Space</kbd> | |
| Move focus in and out of an active dialog window | <kbd>Ctrl</kbd>+<kbd>F6</kbd> | <kbd>⌘F6</kbd> (may require <kbd>Fn</kbd>) |

View File

@@ -23,14 +23,14 @@ For bulleted and numbered lists, it's possible to configure an alternative marke
It possible to add content-level blocks such as headings, code blocks, tables within lists, as follows:
| | | |
| | | |
| --- | --- | --- |
| 1 | ![](6_Lists_image.png) | First, create a list. |
| 2 | ![](9_Lists_image.png) | Press Enter to create a new list item. |
| 3 | ![](5_Lists_image.png) | Press Backspace to get rid of the bullet point. Notice the cursor position. |
| 4 | <img class="image_resized" style="aspect-ratio:676/112;width:98.29%;" src="10_Lists_image.png" width="676" height="112"> | At this point, insert any desired block-level item such as a code block. |
| 5 | <img class="image_resized" style="aspect-ratio:675/129;width:94.22%;" src="8_Lists_image.png" width="675" height="129"> | To continue with a new bullet point, press Enter until the cursor moves to a new blank position. |
| 6 | <img class="image_resized" style="aspect-ratio:675/129;width:100%;" src="3_Lists_image.png" width="675" height="129"> | Press Enter once more to create the new bullet. |
| 1 | ![](6_Lists_image.png) | First, create a list. |
| 2 | ![](9_Lists_image.png) | Press Enter to create a new list item. |
| 3 | ![](5_Lists_image.png) | Press Backspace to get rid of the bullet point. Notice the cursor position. |
| 4 | <img class="image_resized" style="aspect-ratio:676/112;width:98.29%;" src="10_Lists_image.png" width="676" height="112"> | At this point, insert any desired block-level item such as a code block. |
| 5 | <img class="image_resized" style="aspect-ratio:675/129;width:94.22%;" src="8_Lists_image.png" width="675" height="129"> | To continue with a new bullet point, press Enter until the cursor moves to a new blank position. |
| 6 | <img class="image_resized" style="aspect-ratio:675/129;width:100%;" src="3_Lists_image.png" width="675" height="129"> | Press Enter once more to create the new bullet. |
The same principle applies to all three list types (bullet, numbered and to-do).

View File

@@ -3,7 +3,7 @@ Front-end scripts are custom JavaScript notes that are run on the client (browse
There are four flavors of front-end scripts:
| | |
| | |
| --- | --- |
| Regular scripts | These are run with the current app and note context. These can be run either manually or automatically on start-up. |
| <a class="reference-link" href="Frontend%20Basics/Custom%20Widgets.md">Custom Widgets</a> | These can introduce new UI elements in various positions, such as near the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Note%20Tree.md">Note Tree</a>, content area or even the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Right%20Sidebar.md">Right Sidebar</a>. |

View File

@@ -9,7 +9,7 @@ As such, the first step is to create a new note to gather all the themes.
## Step 2. Create the theme
| | |
| | |
| --- | --- |
| ![](Creating%20a%20custom%20theme_3_.png) | Themes are code notes with a special attribute. Start by creating a new code note. |
| ![](Creating%20a%20custom%20theme_1_.png) | Then change the note type to a CSS code. |

View File

@@ -1,7 +1,7 @@
# Custom app-wide CSS
It is possible to provide a CSS file to be used regardless of the theme set by the user.
| | |
| | |
| --- | --- |
| ![](Custom%20app-wide%20CSS_image.png) | Start by creating a new note and changing the note type to CSS |
| ![](2_Custom%20app-wide%20CSS_image.png) | In the ribbon, press the “Owned Attributes” section and type `#appCss`. |

View File

@@ -101,7 +101,7 @@
},
"overrides": {
"mermaid": "11.12.3",
"preact": "10.28.4",
"preact": "10.29.0",
"roughjs": "4.6.6",
"@types/express-serve-static-core": "5.1.1",
"flat@<5.0.1": ">=5.0.1",

View File

@@ -28,21 +28,21 @@
"@typescript-eslint/parser": "8.57.0",
"@vitest/browser": "4.0.18",
"@vitest/coverage-istanbul": "4.0.18",
"ckeditor5": "47.6.0",
"ckeditor5": "47.4.0",
"eslint": "10.0.3",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.3.2",
"lint-staged": "16.3.3",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vite-plugin-svgo": "2.0.0",
"vitest": "4.0.18",
"webdriverio": "9.24.0"
"webdriverio": "9.25.0"
},
"peerDependencies": {
"ckeditor5": "47.6.0"
"ckeditor5": "47.4.0"
},
"author": "Elian Doran <contact@eliandoran.me>",
"license": "GPL-2.0-or-later",

View File

@@ -29,21 +29,21 @@
"@typescript-eslint/parser": "8.57.0",
"@vitest/browser": "4.0.18",
"@vitest/coverage-istanbul": "4.0.18",
"ckeditor5": "47.6.0",
"ckeditor5": "47.4.0",
"eslint": "10.0.3",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.3.2",
"lint-staged": "16.3.3",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vite-plugin-svgo": "2.0.0",
"vitest": "4.0.18",
"webdriverio": "9.24.0"
"webdriverio": "9.25.0"
},
"peerDependencies": {
"ckeditor5": "47.6.0"
"ckeditor5": "47.4.0"
},
"scripts": {
"build": "node ./scripts/build-dist.mjs",

View File

@@ -31,21 +31,21 @@
"@typescript-eslint/parser": "8.57.0",
"@vitest/browser": "4.0.18",
"@vitest/coverage-istanbul": "4.0.18",
"ckeditor5": "47.6.0",
"ckeditor5": "47.4.0",
"eslint": "10.0.3",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.3.2",
"lint-staged": "16.3.3",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vite-plugin-svgo": "2.0.0",
"vitest": "4.0.18",
"webdriverio": "9.24.0"
"webdriverio": "9.25.0"
},
"peerDependencies": {
"ckeditor5": "47.6.0"
"ckeditor5": "47.4.0"
},
"scripts": {
"build": "node ./scripts/build-dist.mjs",

View File

@@ -31,21 +31,21 @@
"@typescript-eslint/parser": "8.57.0",
"@vitest/browser": "4.0.18",
"@vitest/coverage-istanbul": "4.0.18",
"ckeditor5": "47.6.0",
"ckeditor5": "47.4.0",
"eslint": "10.0.3",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.3.2",
"lint-staged": "16.3.3",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vite-plugin-svgo": "2.0.0",
"vitest": "4.0.18",
"webdriverio": "9.24.0"
"webdriverio": "9.25.0"
},
"peerDependencies": {
"ckeditor5": "47.6.0"
"ckeditor5": "47.4.0"
},
"scripts": {
"build": "node ./scripts/build-dist.mjs",

View File

@@ -31,21 +31,21 @@
"@typescript-eslint/parser": "8.57.0",
"@vitest/browser": "4.0.18",
"@vitest/coverage-istanbul": "4.0.18",
"ckeditor5": "47.6.0",
"ckeditor5": "47.4.0",
"eslint": "10.0.3",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.3.2",
"lint-staged": "16.3.3",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vite-plugin-svgo": "2.0.0",
"vitest": "4.0.18",
"webdriverio": "9.24.0"
"webdriverio": "9.25.0"
},
"peerDependencies": {
"ckeditor5": "47.6.0"
"ckeditor5": "47.4.0"
},
"scripts": {
"build": "node ./scripts/build-dist.mjs",

View File

@@ -12,7 +12,7 @@
"@triliumnext/ckeditor5-keyboard-marker": "workspace:*",
"@triliumnext/ckeditor5-math": "workspace:*",
"@triliumnext/ckeditor5-mermaid": "workspace:*",
"ckeditor5": "47.6.0",
"ckeditor5": "47.4.0",
"ckeditor5-premium-features": "47.4.0"
},
"devDependencies": {

View File

@@ -11,6 +11,7 @@ export type { EditorConfig, MentionFeed, MentionFeedObjectItem, ModelNode, Model
export type { TemplateDefinition } from "ckeditor5-premium-features";
export { default as buildExtraCommands } from "./extra_slash_commands.js";
export { default as getCkLocale } from "./i18n.js";
export * from "./utils.js";
// Import with sideffects to ensure that type augmentations are present.
import "@triliumnext/ckeditor5-math";

View File

@@ -0,0 +1,28 @@
import type { DifferItemAttribute, Editor, ModelDocumentFragment, ModelElement, ModelNode } from "ckeditor5";
function hasHeadingAncestor(node: ModelElement | ModelNode | ModelDocumentFragment | null): boolean {
let current: ModelElement | ModelNode | ModelDocumentFragment | null = node;
while (current) {
if (!!current && current.is('element') && (current as ModelElement).name.startsWith("heading")) return true;
current = current.parent;
}
return false;
}
export function attributeChangeAffectsHeading(change: DifferItemAttribute, editor: Editor): boolean {
if (change.type !== "attribute") return false;
// Fast checks on range boundaries
if (hasHeadingAncestor(change.range.start.parent) || hasHeadingAncestor(change.range.end.parent)) {
return true;
}
// Robust check across the whole changed range
const range = editor.model.createRange(change.range.start, change.range.end);
for (const item of range.getItems()) {
const baseNode = item.is("$textProxy") ? item.parent : item;
if (hasHeadingAncestor(baseNode)) return true;
}
return false;
}

View File

@@ -16,7 +16,7 @@
"@codemirror/lang-xml": "6.1.0",
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.6.0",
"@codemirror/view": "6.39.16",
"@codemirror/view": "6.39.17",
"@fsegurai/codemirror-theme-abcdef": "6.2.3",
"@fsegurai/codemirror-theme-abyss": "6.2.3",
"@fsegurai/codemirror-theme-android-studio": "6.2.3",
@@ -50,6 +50,6 @@
"codemirror-lang-elixir": "4.0.1",
"codemirror-lang-hcl": "0.1.0",
"codemirror-lang-mermaid": "0.5.0",
"eslint-linter-browserify": "10.0.2"
"eslint-linter-browserify": "10.0.3"
}
}

1649
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff