Compare commits

..

323 Commits

Author SHA1 Message Date
renovate[bot]
42f3968cc0 fix(deps): update dependency mind-elixir to v5.5.0 2026-01-06 01:51:40 +00:00
Elian Doran
7f2cc885fe Feat(math): Improve legacy math input with MathLive (#7842) 2026-01-06 00:12:38 +02:00
Elian Doran
19a365a370 fix(sql_console): cannot copy table data (#8268) 2026-01-06 00:10:11 +02:00
Elian Doran
9a50da328e chore(deps): update dependency rollup-plugin-webpack-stats to v2.1.9 (#8265) 2026-01-05 23:53:05 +02:00
Elian Doran
181e36a7c1 Merge remote-tracking branch 'origin/main' into Meinzzzz/main
; Conflicts:
;	.gitignore
2026-01-05 23:46:12 +02:00
Elian Doran
178508d245 Merge branch 'main' into fix/sql_select_text 2026-01-05 23:43:29 +02:00
Elian Doran
d132d084cf Merge branch 'main' into renovate/rollup-plugin-webpack-stats-2.x 2026-01-05 23:43:06 +02:00
Elian Doran
494b55d685 fix(ckeditor): missing pl locale 2026-01-05 23:39:36 +02:00
Elian Doran
51513d3779 fix(status_bar): count not refreshing properly after change 2026-01-05 21:03:32 +02:00
SngAbc
458398f2ca Merge branch 'main' into fix/sql_select_text 2026-01-05 13:51:45 +08:00
SngAbc
7a6cc4f51e fix(sql_console): cannot copy table data
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-05 12:16:16 +08:00
SiriusXT
f4ccce7de5 fix(sql_console): cannot copy table data 2026-01-05 11:23:50 +08:00
renovate[bot]
f8b5417d6c chore(deps): update dependency rollup-plugin-webpack-stats to v2.1.9 2026-01-05 01:03:52 +00:00
Elian Doran
13ce8cf498 fix(note_list): the note list cannot open the context menu. (#8254) 2026-01-04 23:49:25 +02:00
Elian Doran
6c2afc086c feat(i18n): add Polish 2026-01-04 23:38:51 +02:00
Elian Doran
93d50712a9 chore(scripts): fix typecheck issue 2026-01-04 23:38:51 +02:00
Elian Doran
ed91a44928 feat(scripts): check translation coverage 2026-01-04 23:38:50 +02:00
Elian Doran
cd10e66fbb chore(scripts): build scripts not working properly on Windows 2026-01-04 23:38:50 +02:00
Elian Doran
d6aa126fcc Translations update from Hosted Weblate (#8264) 2026-01-04 22:34:38 +02:00
noobhjy
3308c7bdf4 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1751 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-01-04 20:01:53 +00:00
Francis C.
56341a1a73 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1751 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-01-04 20:01:52 +00:00
green
0857e1a536 Translated using Weblate (Japanese)
Currently translated at 100.0% (1751 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-01-04 20:01:50 +00:00
Kim Nøglegaard
5d6b25a29e Translated using Weblate (Norwegian Bokmål)
Currently translated at 57.2% (87 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-04 20:01:49 +00:00
Elian Doran
5bc15a5448 PDFjs v1 final tweaks (#8256) 2026-01-04 21:21:51 +02:00
Elian Doran
51a19d0544 docs(user): add missing slug 2026-01-04 21:11:13 +02:00
Elian Doran
fb4e912ed0 chore(pdfjs): address requested changes 2026-01-04 21:08:39 +02:00
Elian Doran
20c2652013 chore(server): set up environment for starting Nginx proxy with subdir 2026-01-04 20:33:54 +02:00
Elian Doran
971d6ad9e3 chore(pdfjs): use version-based system for cache busting 2026-01-04 20:06:11 +02:00
Elian Doran
757fc7a7fe chore(pdfjs): embed sandbox file 2026-01-04 18:50:40 +02:00
Elian Doran
e4d0a4554a feat(client/note_list): use built-in PDF viewer 2026-01-04 18:40:17 +02:00
Elian Doran
dfab7dbc4b fix(note_list): missing margin in button 2026-01-04 18:39:23 +02:00
Elian Doran
0039f4c155 feat(pdfjs): replace blob instead of creating a new revision every time 2026-01-04 17:25:56 +02:00
Elian Doran
23f7dc63b8 feat(pdfjs): enable editing features only if in main editor 2026-01-04 17:11:16 +02:00
Elian Doran
e485b75a44 fix(pdfjs): saves as soon as document is opened 2026-01-04 17:06:41 +02:00
Elian Doran
dbef57d329 chore(deps): update dependency webdriverio to v9.23.0 (#8258) 2026-01-04 10:36:47 +02:00
SiriusXT
c650441655 Merge branch 'main' into fix/note_list 2026-01-04 10:49:46 +08:00
SiriusXT
e573a8af77 chore(note_grid): remove unused tree import 2026-01-04 10:48:45 +08:00
SiriusXT
b23252d046 fix(note_grid): the note grid cannot open the context menu 2026-01-04 10:25:43 +08:00
renovate[bot]
2f7448dbd4 chore(deps): update dependency webdriverio to v9.23.0 2026-01-04 02:13:34 +00:00
Adorian Doran
9bf4aa2968 readme: update screenshot 2026-01-04 00:28:00 +02:00
Elian Doran
d78a7bad3b feat(import/markdown): handle bash as sh 2026-01-03 23:30:38 +02:00
Elian Doran
b812177e78 docs(user): add spellcheck=false to inline code 2026-01-03 23:28:28 +02:00
Elian Doran
4710a6af41 feat(export/markdown): add spellcheck=false to inline code 2026-01-03 23:19:58 +02:00
Elian Doran
a613980ea4 docs(user): add missing jsx / HTML code blocks 2026-01-03 22:56:23 +02:00
Elian Doran
20ae1f844b feat(markdown): support html, jsx in code blocks 2026-01-03 22:44:48 +02:00
Elian Doran
69511134e5 refactor(client/pdf): handle blob request on client side 2026-01-03 20:54:28 +02:00
Elian Doran
75952563e4 Translations update from Hosted Weblate (#8255) 2026-01-03 20:31:35 +02:00
Elian Doran
21cf5e1df7 chore(client/pdf): use custom spaced update hook 2026-01-03 20:29:54 +02:00
Hosted Weblate
9df5505989 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-01-03 18:03:22 +00:00
Kim Nøglegaard
1809d59193 Translated using Weblate (Norwegian Bokmål)
Currently translated at 43.4% (66 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-03 18:03:20 +00:00
Francis C.
feaa54d660 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1745 of 1745 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-01-03 18:03:20 +00:00
noobhjy
c94bd41162 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.8% (1743 of 1745 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-01-03 18:03:19 +00:00
Elian Doran
f1f3e66537 Save indicator (#8249) 2026-01-03 20:03:12 +02:00
Elian Doran
80363cdc73 chore(client/save_indicator): fix some spacing issues 2026-01-03 19:53:46 +02:00
Elian Doran
02e08fdf12 chore(client/save_indicator): address requested changes 2026-01-03 19:47:33 +02:00
Elian Doran
42283b2469 doc(user): mention the save status indicator 2026-01-03 19:40:39 +02:00
Elian Doran
d3b598a5b2 fix(client/save_indicator): not visible on light theme 2026-01-03 19:39:16 +02:00
Elian Doran
0dd3a03c6b chore(client): fix type issue 2026-01-03 19:30:52 +02:00
Elian Doran
2144888447 Merge remote-tracking branch 'origin/main' into feature/save_indicator 2026-01-03 19:24:51 +02:00
Elian Doran
b2549066dc PDF.js refinement (#8247) 2026-01-03 19:24:28 +02:00
Elian Doran
cd1f3aa9a7 chore(client): address self-review 2026-01-03 10:05:44 +02:00
Elian Doran
1674401342 Merge remote-tracking branch 'origin/main' into feature/pdfjs_refinement 2026-01-03 09:57:42 +02:00
Elian Doran
7ba8dbbf6e fix(deps): update dependency mind-elixir to v5.4.0 (#8253) 2026-01-03 09:53:36 +02:00
Elian Doran
ad27d9ed0e chore(deps): update dependency @redocly/cli to v2.14.3 (#8252) 2026-01-03 09:53:03 +02:00
renovate[bot]
482d2f9624 fix(deps): update dependency mind-elixir to v5.4.0 2026-01-03 01:46:19 +00:00
renovate[bot]
824ef704d4 chore(deps): update dependency @redocly/cli to v2.14.3 2026-01-03 01:45:28 +00:00
Adorian Doran
58b73cfc7d Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-01-03 02:25:07 +02:00
Adorian Doran
0465fea2db style/pdf viewer: improve appearance 2026-01-03 02:24:59 +02:00
Elian Doran
39b75e3561 Translations update from Hosted Weblate (#8248) 2026-01-03 00:10:42 +02:00
Elian Doran
2933db9b16 feat(save_indicator): fade out after a few seconds 2026-01-02 23:53:14 +02:00
Kim Nøglegaard
d94914046b Translated using Weblate (Norwegian Bokmål)
Currently translated at 23.0% (35 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-02 21:51:24 +00:00
Kim Nøglegaard
9cf384b14b Translated using Weblate (Norwegian Bokmål)
Currently translated at 20.3% (31 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-02 21:51:23 +00:00
Kim Nøglegaard
614a2f0ccb Translated using Weblate (Norwegian Bokmål)
Currently translated at 3.2% (5 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-02 21:51:23 +00:00
Yatrik Patel
5cecc72384 Translated using Weblate (Hindi)
Currently translated at 2.8% (11 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-02 21:51:22 +00:00
Yatrik Patel
3ad37fb602 Translated using Weblate (Hindi)
Currently translated at 5.2% (8 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-02 21:51:21 +00:00
noobhjy
42b048c2bf Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.8% (1742 of 1745 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-01-02 21:51:21 +00:00
Kim Nøglegaard
a01bf3dfa1 Translated using Weblate (Norwegian Bokmål)
Currently translated at 2.0% (8 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/nb_NO/
2026-01-02 21:51:20 +00:00
Yatrik Patel
ad60988553 Translated using Weblate (Hindi)
Currently translated at 0.7% (13 of 1745 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hi/
2026-01-02 21:51:19 +00:00
green
c9ae4e4cc6 Translated using Weblate (Japanese)
Currently translated at 100.0% (1745 of 1745 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-01-02 21:51:18 +00:00
Kim Nøglegaard
d2639851d5 Translated using Weblate (Norwegian Bokmål)
Currently translated at 0.6% (11 of 1745 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/nb_NO/
2026-01-02 21:51:18 +00:00
Hosted Weblate
8dc5f9cfa4 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-01-02 21:51:17 +00:00
Adorian Doran
d99a408e04 style/pdf viewer: add support for background effects 2026-01-02 23:51:07 +02:00
Elian Doran
5f14861682 feat(save_indicator): indicate errors 2026-01-02 23:22:33 +02:00
Adorian Doran
8f8493f3ec client/note split: allow enabling background effects according to the MIME type 2026-01-02 23:06:08 +02:00
Elian Doran
62af66b5ae feat(save_indicator): report saving and saved states 2026-01-02 22:53:18 +02:00
Elian Doran
e8d1fa7447 chore(save_indicator): basic infrastructure to display state 2026-01-02 22:44:29 +02:00
Adorian Doran
ee03871405 style/pdf viewer: remove irrelevant elements 2026-01-02 22:28:33 +02:00
Elian Doran
345378d97f feat(save_indicator): add tooltip for each of the states 2026-01-02 22:14:58 +02:00
Elian Doran
07a463ee52 feat(save_indicator): improve display of some states 2026-01-02 22:10:41 +02:00
Elian Doran
3157047160 chore(save_indicator): add opacity 2026-01-02 22:00:25 +02:00
Elian Doran
a1dda3b578 chore(save_indicator): prepare icon and title 2026-01-02 21:58:13 +02:00
Elian Doran
e161ffce57 fix(client/pdf): not always focusing on click 2026-01-02 21:20:29 +02:00
Adorian Doran
0c1859dc43 style/note splits: highlight the active split only in a multi-split view 2026-01-02 20:59:51 +02:00
Elian Doran
e4dcc0f768 chore(client): fix typecheck issues 2026-01-02 20:45:28 +02:00
Elian Doran
74ab591214 chore(package): automatically build share theme & PDF viewer 2026-01-02 20:38:18 +02:00
Elian Doran
7bd7996893 feat(revisions): use customized PDF viewer 2026-01-02 20:17:27 +02:00
Elian Doran
505ae4eeb5 chore(revisions): remove "Preview" heading 2026-01-02 20:02:39 +02:00
Elian Doran
951d6d3ce3 feat(revisions): display PDF preview for revisions 2026-01-02 20:02:13 +02:00
Elian Doran
5ff7764699 style(revisions): prevent revision list from overflowing 2026-01-02 19:49:20 +02:00
Elian Doran
0d74998625 style(revisions): prevent buttons from overflowing 2026-01-02 19:47:07 +02:00
Elian Doran
29b70a12bd feat(revisions): display video preview for revisions 2026-01-02 19:44:23 +02:00
Elian Doran
d84150e97b feat(revisions): display audio preview for revisions 2026-01-02 19:21:51 +02:00
Elian Doran
2b2ef4251f style(revisions): minor spacing adjustments to file table 2026-01-02 18:44:06 +02:00
Elian Doran
2840ea0f38 chore(revisions): display a message when a preview is not available 2026-01-02 18:42:05 +02:00
Elian Doran
542d485267 fix(revisions): missing meta information about revisions 2026-01-02 18:34:45 +02:00
Elian Doran
cdd4fbc81d refactor(client): fix lint warnings in revisions modal 2026-01-02 18:23:04 +02:00
Elian Doran
bfdddab0a0 refactor(client): format revisions dialog 2026-01-02 18:20:55 +02:00
Elian Doran
44d1d01105 fix(pdfjs): preferences don't account for ntxId or noteId 2026-01-02 18:08:25 +02:00
Elian Doran
120bb09171 fix(pdfjs): saving doesn't account for ntxId or noteId 2026-01-02 17:57:43 +02:00
Elian Doran
b7af99c671 refactor(pdfjs): add type safety for messages 2026-01-02 17:57:28 +02:00
Elian Doran
869e0b3973 docs(user): mention updates to the new PDF functions 2026-01-02 12:49:30 +02:00
Elian Doran
b68613dee4 feat(share): integrate custom pdf.js viewer 2026-01-02 12:13:31 +02:00
Elian Doran
ce0f32e7d5 chore(client/pdfjs): remove open file 2026-01-02 11:44:33 +02:00
Elian Doran
78bc9b59c2 chore(client/pdfjs): remove download button from toolbar 2026-01-02 11:41:58 +02:00
Elian Doran
23cf3d2923 feat(client/pdfjs): rewrite download button 2026-01-02 11:40:32 +02:00
Elian Doran
335136f3a3 fix(deps): update dependency preact-render-to-string to v6.6.5 (#8240) 2026-01-02 11:13:42 +02:00
renovate[bot]
11dd7aef09 fix(deps): update dependency preact-render-to-string to v6.6.5 2026-01-02 09:09:55 +00:00
Elian Doran
2d1769e2f9 fix: toggling right pane visibility incorrectly affects all windows (#8226) 2026-01-02 11:08:27 +02:00
Elian Doran
21e26147b0 fix(deps): update dependency react-i18next to v16.5.1 (#8241) 2026-01-02 11:06:38 +02:00
Elian Doran
ba301f8c12 fix(deps): update dependency globals to v17 (#8242) 2026-01-02 11:06:13 +02:00
SiriusXT
3420374649 fix: toggling right pane visibility incorrectly affects all windows 2026-01-02 11:31:59 +08:00
SiriusXT
644d3a181f fix: toggling right pane visibility incorrectly affects all windows 2026-01-02 11:08:49 +08:00
SiriusXT
4be3011a8a fix: toggling right pane visibility incorrectly affects all windows 2026-01-02 10:30:15 +08:00
SiriusXT
5aa0a956dd fix: toggling right pane visibility incorrectly affects all windows 2026-01-02 10:25:34 +08:00
renovate[bot]
7fdb1bdce8 fix(deps): update dependency globals to v17 2026-01-02 01:53:43 +00:00
renovate[bot]
57c6cef2bd fix(deps): update dependency react-i18next to v16.5.1 2026-01-02 01:52:49 +00:00
Elian Doran
e5599adca1 feat(share): Add support for shareJs in static website export (#8173) 2026-01-02 00:39:36 +02:00
Elian Doran
ab392ffb7f Translations update from Hosted Weblate (#8239) 2026-01-02 00:38:23 +02:00
Hosted Weblate
7585d4b258 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-01-01 22:37:01 +00:00
Elian Doran
ff82d9c38c Clean up Vite (#8238) 2026-01-02 00:36:46 +02:00
Elian Doran
920fde69bb chore: add missing space from imports 2026-01-02 00:21:50 +02:00
Elian Doran
053812e5f0 e2e(server): wrong import 2026-01-02 00:14:07 +02:00
Elian Doran
c2f59c4b6c test(server): type error 2026-01-02 00:07:04 +02:00
Elian Doran
06980fe9b5 chore(tsconfig): fix empty type 2026-01-02 00:04:52 +02:00
Elian Doran
3f5616f1fc chore(vitest): fix node:test import 2026-01-02 00:03:45 +02:00
Elian Doran
b6af3b70b0 test(client): increase a timeout for local run 2026-01-01 23:57:43 +02:00
Elian Doran
d8e4547988 chore(vitest): get rid of warning about number of projects 2026-01-01 23:56:26 +02:00
Elian Doran
34f649155e chore(vite): remove vite/global for other projects 2026-01-01 23:44:17 +02:00
Elian Doran
11779fe3e3 chore(vite): remove vite/global for commons 2026-01-01 23:43:59 +02:00
Elian Doran
032cde67b0 chore(vite): remove vite/global for express-partial-content 2026-01-01 23:42:02 +02:00
Elian Doran
229636a796 chore(vite): remove vite/global for server 2026-01-01 23:39:53 +02:00
Elian Doran
da9c9ac346 chore(vite): remove vite/importMeta from spec types 2026-01-01 23:34:39 +02:00
Elian Doran
3fecc4c648 chore(vite): remove vite/client from spec types 2026-01-01 23:33:21 +02:00
Elian Doran
98cefcf77b fix(desktop/pdfjs): not working due to build script 2026-01-01 23:13:21 +02:00
Elian Doran
413ee81ffa Translations update from Hosted Weblate (#8234) 2026-01-01 22:51:03 +02:00
Yatrik Patel
578ca8785e Translated using Weblate (Hindi)
Currently translated at 0.5% (10 of 1740 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hi/
2026-01-01 20:49:31 +00:00
Yatrik Patel
da4112c078 Translated using Weblate (Hindi)
Currently translated at 2.0% (8 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-01 20:49:30 +00:00
Yatrik Patel
704c7c881d Translated using Weblate (Hindi)
Currently translated at 4.6% (7 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-01 20:49:29 +00:00
Elian Doran
63b6abdb9d PDF.js sidebar experiments for new layout (#8212) 2026-01-01 22:49:15 +02:00
Elian Doran
2e936a3d5c test(pdfjs): disable for now as there are no tests 2026-01-01 22:31:49 +02:00
Elian Doran
606574e18e chore(pdjs): address self-review 2026-01-01 22:15:40 +02:00
Elian Doran
1021879167 chore(client/pdfjs): add some missing translations 2026-01-01 22:15:31 +02:00
Elian Doran
dc4aa9c607 feat(ui): implement tooltips for share icons and clone icons (#8211) 2026-01-01 21:13:01 +02:00
Elian Doran
b2c3d78773 Fix excessive noteContext calls (#8233) 2026-01-01 21:09:39 +02:00
Elian Doran
8d3a0b5295 test(pdfjs): replace beforeAll with beforeEach 2026-01-01 21:06:03 +02:00
lzinga
9879d07bec fix(widget): remove redundant note context update in useLegacyWidget 2026-01-01 11:05:30 -08:00
Elian Doran
7bfce851e7 fix(mermaid) diagrams not saving content and SVG attachment (#8220) 2026-01-01 20:58:56 +02:00
Elian Doran
34e81881ec fix(popupEditor): fix closing of popupEditor when inserting note link (#8224) 2026-01-01 20:56:04 +02:00
Lucas
0143d6c60d Merge branch 'TriliumNext:main' into fix/layout-calls 2026-01-01 10:55:25 -08:00
Elian Doran
267c2bc907 Translations update from Hosted Weblate (#8231) 2026-01-01 19:25:36 +02:00
Yatrik Patel
316f27d88c Translated using Weblate (Hindi)
Currently translated at 0.1% (2 of 1736 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hi/
2026-01-01 17:39:40 +01:00
Yatrik Patel
452b56f470 Translated using Weblate (Hindi)
Currently translated at 2.6% (4 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-01 17:39:40 +01:00
Jan Klass
43aeaa4455 Translated using Weblate (German)
Currently translated at 96.1% (1669 of 1736 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-01-01 17:39:40 +01:00
Yatrik Patel
08b7a6985e Translated using Weblate (Hindi)
Currently translated at 1.0% (4 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-01 17:39:40 +01:00
dirlligafu
4bbd8e28c1 Translated using Weblate (French)
Currently translated at 95.3% (1656 of 1736 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fr/
2026-01-01 17:39:40 +01:00
Yatrik Patel
fcf4c09389 Translated using Weblate (Hindi)
Currently translated at 1.7% (2 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/hi/
2026-01-01 17:39:40 +01:00
Elian Doran
2c323cbe80 fix(client): color with uppercase causing exception (closes #8232) 2026-01-01 18:39:31 +02:00
Elian Doran
7ec7b6bd7b chore(pdfjs): manage requested changes 2026-01-01 12:36:33 +02:00
Elian Doran
b2378f2a53 test(server/pdf): move beforeAll 2026-01-01 12:19:51 +02:00
Elian Doran
8bf8d85bb7 test(server/pdf): switching to another note 2026-01-01 11:23:24 +02:00
Elian Doran
676173e895 test(server/pdf): layer listing works 2026-01-01 11:06:29 +02:00
Elian Doran
d8649c87e0 test(server/pdf): attachment listing works 2026-01-01 01:06:26 +02:00
Elian Doran
b9456ca466 test(server/pdf): basic page navigation test 2026-01-01 00:35:39 +02:00
Elian Doran
cfccbb8927 test(server/pdf): basic table of contents test 2025-12-31 23:40:32 +02:00
Elian Doran
a18578362a Merge remote-tracking branch 'origin/main' into feature/pdfjs_sidebar_experiments 2025-12-31 22:55:37 +02:00
Elian Doran
2f9f94dee0 fix(server): pdfjs not available in dist 2025-12-31 22:46:55 +02:00
Elian Doran
c84e45ddee test(pdfjs): set up basic vitest 2025-12-31 21:18:27 +02:00
Lucas
ea558d8c9d Merge branch 'TriliumNext:main' into fix/layout-calls 2025-12-31 07:33:11 -08:00
lzinga
b936a35b63 fix(widget): prevent unnecessary refresh by checking note context change 2025-12-31 07:31:22 -08:00
Elian Doran
b4ef4c2143 chore(pdfjs): fix code scanning issues 2025-12-31 17:27:58 +02:00
Elian Doran
0ff4756ef4 chore(pdfjs): fix typecheck issues 2025-12-31 17:00:56 +02:00
Elian Doran
94204b4739 style(pdf_pages): slight improvement to page layout 2025-12-31 16:45:11 +02:00
Elian Doran
bf3a2b768e chore(pdfjs): set proper target origin when posting messages 2025-12-31 16:37:51 +02:00
SiriusXT
5fb7badfb4 fix(rightPane): toggling right pane visibility incorrectly affects all windows 2025-12-31 19:54:31 +08:00
Elian Doran
239d56f9a3 fix(deps): update dependency @codemirror/view to v6.39.8 (#8222) 2025-12-31 10:38:22 +02:00
Elian Doran
9163fc23f4 chore(deps): update dependency @redocly/cli to v2.14.2 (#8221) 2025-12-31 10:37:30 +02:00
Elian Doran
d225c28fde chore(deps): update pnpm to v10.27.0 (#8223) 2025-12-31 10:29:28 +02:00
SiriusXT
8a3f02e845 fix(popupEditor): fix closing of popupEditor when inserting note link 2025-12-31 14:12:38 +08:00
renovate[bot]
d0dc92c891 chore(deps): update pnpm to v10.27.0 2025-12-31 02:34:03 +00:00
renovate[bot]
8d660f5a2f fix(deps): update dependency @codemirror/view to v6.39.8 2025-12-31 02:33:52 +00:00
renovate[bot]
b41b4e77b2 chore(deps): update dependency @redocly/cli to v2.14.2 2025-12-31 02:33:14 +00:00
lzinga
267a37d3bd feat(component): add removeChild method for cleanup of child components
feat(hooks): improve useLegacyWidget cleanup and memoization logic
2025-12-30 13:59:46 -08:00
Lucas
0cf23c7d7c Update apps/client/src/widgets/type_widgets/helpers/SvgSplitEditor.tsx
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-12-30 11:56:34 -08:00
lzinga
a632486229 fix(mermaid) diagrams not saving content and SVG attachment 2025-12-30 11:35:48 -08:00
Elian Doran
64a518a00b chore(deps): update typescript-eslint monorepo to v8.51.0 (#8214) 2025-12-30 12:07:14 +02:00
Elian Doran
2f3a914027 Translations update from Hosted Weblate (#8213) 2025-12-30 12:06:27 +02:00
Elian Doran
7182d32d9c Merge remote-tracking branch 'origin/main' into feature/pdfjs_sidebar_experiments 2025-12-30 11:47:00 +02:00
green
18381c5d32 Translated using Weblate (Japanese)
Currently translated at 100.0% (1736 of 1736 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-12-30 09:00:07 +01:00
Kuzma Simonov
79327073b4 Translated using Weblate (Russian)
Currently translated at 100.0% (1736 of 1736 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ru/
2025-12-30 09:00:06 +01:00
Giovi
018f2fd789 Translated using Weblate (Italian)
Currently translated at 100.0% (1736 of 1736 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2025-12-30 09:00:06 +01:00
noobhjy
3889392aed Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1736 of 1736 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2025-12-30 09:00:05 +01:00
Elian Doran
bd976a25f1 style(next): use border for focus instead 2025-12-30 09:59:46 +02:00
Elian Doran
01f05ac6fd fix(pdf): active context not changed when clicking preview 2025-12-30 09:47:02 +02:00
Elian Doran
52292cb5a5 style(next): indicate active note context 2025-12-30 09:31:03 +02:00
perfectra1n
c6dd1ba0ca fix(types): resolve typecheck issue with note_tree 2025-12-29 18:25:55 -08:00
renovate[bot]
84f069087c chore(deps): update typescript-eslint monorepo to v8.51.0 2025-12-30 01:05:53 +00:00
Elian Doran
76cfced60f chore(pdfjs): fix partially removed method 2025-12-30 01:43:07 +02:00
Elian Doran
5c2aea0a6b chore: remove LLM generated doc 2025-12-30 01:40:34 +02:00
Elian Doran
7a883c62df Merge remote-tracking branch 'origin/main' into feature/pdfjs_sidebar_experiments 2025-12-30 01:40:03 +02:00
Elian Doran
ff97461ff8 PDF.js integration (part I) (#8206) 2025-12-30 01:39:09 +02:00
Elian Doran
51b0eb74a5 chore(pdfjs): address requested changes 2025-12-30 01:37:44 +02:00
Elian Doran
2304407986 chore(pdfjs): address origin concerns 2025-12-30 01:27:05 +02:00
Elian Doran
a1ebdc3004 chore(pdfjs): integrate into typecheck 2025-12-30 01:23:19 +02:00
Elian Doran
fef30f4bea chore(client): fix typecheck 2025-12-30 01:20:29 +02:00
Elian Doran
eee8d9ab7c feat(pdfjs): optionally hide left sidebar 2025-12-30 01:09:45 +02:00
perfectra1n
4f2678d321 feat(ui): implement tooltips for share icons and clone icons
asdf
2025-12-29 14:42:34 -08:00
Elian Doran
c473fba628 refactor(right_pane): move PDF-specific components in own dir 2025-12-30 00:17:29 +02:00
Elian Doran
9a9cd8e6a5 feat(right_pane): add count in title for PDF items 2025-12-30 00:16:45 +02:00
Elian Doran
f5a89aa81a feat(right_pane): hide PDF attachments/layers when not needed 2025-12-30 00:10:23 +02:00
Elian Doran
3c1beab725 fix(pdf_pages): fix a few type errors 2025-12-30 00:00:03 +02:00
Elian Doran
79f03ad3ac fix(pdf_layers): toggling layers and updating state not working 2025-12-29 23:57:52 +02:00
Elian Doran
574138a1fb refactor(pdf_layers): get layers to show 2025-12-29 23:08:35 +02:00
Elian Doran
6513e2cfca refactor(pdf_attachments): deduplicate font size 2025-12-29 23:04:02 +02:00
Elian Doran
43a749b6a7 feat(right_pane): display attachments 2025-12-29 22:56:06 +02:00
Elian Doran
c1d6b3121a fix(pdf_pages): pages not updating between notes 2025-12-29 22:50:48 +02:00
Elian Doran
0d9c8ae4df style(pdf_pages): page numbers within pages 2025-12-29 22:46:20 +02:00
Elian Doran
62d8c089ed chore(pdf_pages): remove logs 2025-12-29 22:43:17 +02:00
Elian Doran
971a76ce11 style(pdf_pages): render in multiple columns 2025-12-29 22:39:38 +02:00
Elian Doran
cb33404122 feat(client/right_pane): use intersection observer for performance 2025-12-29 22:36:03 +02:00
Elian Doran
bcf72f4624 feat(client/right_pane): display pages 2025-12-29 22:34:36 +02:00
Elian Doran
77ad6950e8 feat(client/right_pane): highlight current heading 2025-12-29 22:11:25 +02:00
Elian Doran
e2d29aadca refactor(pdfjs): extract toc logic to separate file 2025-12-29 21:58:11 +02:00
Elian Doran
64ca04ad07 feat(client/right_pane): jump to heading 2025-12-29 21:55:47 +02:00
Elian Doran
b6506a9331 chore(client/right_pane): get table of contents to show 2025-12-29 21:49:02 +02:00
Elian Doran
fd7222242a chore(pdf): process PDF outline 2025-12-29 21:44:49 +02:00
Elian Doran
e36049cd43 chore(client/right_pane): get raw ToC data to show up 2025-12-29 21:44:15 +02:00
Elian Doran
257f6c5994 chore(client/right_pane): inject title into PDF toc sidebar 2025-12-29 21:28:17 +02:00
Elian Doran
9098bfb63a chore(client): prototype implementation to communicate data through note context 2025-12-29 21:26:52 +02:00
Wael Nasreddine
118d22c4ec Merge branch 'main' into static-implement-sharejs 2025-12-29 11:02:07 -08:00
Elian Doran
758df0d85a fix(share): Prevent crashing if candidate note is null (#8164) 2025-12-29 20:43:12 +02:00
Elian Doran
59bbd902fc feat(share): Render JS Frontend files as-is with extension .js (#8172) 2025-12-29 20:41:46 +02:00
Elian Doran
fffab73061 feat(pdfjs): auto-watch dev 2025-12-29 19:23:56 +02:00
Elian Doran
0a9ce84cf2 feat(client/pdf): respect locale 2025-12-29 19:10:14 +02:00
Elian Doran
07a1734d4b chore(pdfjs): copy locales during build 2025-12-29 19:09:03 +02:00
Elian Doran
6e41d3591d chore(pdfjs): add locales 2025-12-29 19:06:08 +02:00
Elian Doran
4134e5054a fix(client/pdf): not refreshing when uploading new revision 2025-12-29 17:02:22 +02:00
Elian Doran
bb374a5ce2 fix(client/pdf): blob reloaded when saving 2025-12-29 16:46:30 +02:00
Elian Doran
359f398afa feat(pdfjs): debounce saving view config 2025-12-29 16:19:21 +02:00
Elian Doran
84425e86e9 feat(client/pdf): filter out view config by fingerprint 2025-12-29 16:15:38 +02:00
Elian Doran
ebf725c949 feat(client/pdf): store and restore page position 2025-12-29 15:55:47 +02:00
Elian Doran
fc0ea36cf3 chore(pdfjs): first attempt at intercepting store 2025-12-29 14:06:59 +02:00
Elian Doran
7836de3f08 fix(client/pdf): form elements not detected for save 2025-12-29 13:29:04 +02:00
Elian Doran
406232c478 chore(pdfjs): log event bus 2025-12-29 13:23:54 +02:00
Elian Doran
9e0c29496f refactor(pdfjs): use TypeScript for the custom script 2025-12-29 13:14:00 +02:00
Elian Doran
480954ee87 feat(pdfjs): react to dark mode 2025-12-29 12:51:43 +02:00
Elian Doran
94039bd9b1 chore(pdfjs): improve toolbar contrast 2025-12-29 12:40:43 +02:00
Elian Doran
667eaca9f2 feat(pdfjs): improve style to better match Trilium 2025-12-29 12:35:49 +02:00
Elian Doran
446822a7ae chore(client/pdf): inject some CSS variables 2025-12-29 12:21:44 +02:00
Elian Doran
f09a3e06f4 refactor(client/pdf): split into own component 2025-12-29 12:01:47 +02:00
Elian Doran
7c4a56f5f2 chore(deps): add missing pdfjs dependency 2025-12-29 11:10:12 +02:00
Elian Doran
08f6a32c34 fix(client/pdfjs): not reacting to all changes 2025-12-29 10:33:57 +02:00
Elian Doran
3e255fa647 feat(client/pdf): add debouncing 2025-12-29 10:15:15 +02:00
Elian Doran
c0a90402ef feat(client/pdf): save annotations by uploading new revision 2025-12-29 09:51:54 +02:00
Elian Doran
5e42627bce chore(client/pdf): basic reaction to annotations 2025-12-29 02:00:59 +02:00
Elian Doran
41bcf9524a feat(client/pdf): integrate pdf.js 2025-12-29 01:16:56 +02:00
Elian Doran
914cf10911 chore(pdfjs): get icons to show up 2025-12-29 01:11:04 +02:00
Elian Doran
855d4d139d chore(pdfjs): get to actually render something 2025-12-29 01:03:16 +02:00
Elian Doran
abb7b0f8c8 feat(server): serve pdfjs over static route 2025-12-29 00:50:59 +02:00
Elian Doran
d78ad52662 chore(pdfjs): copy viewer to dist 2025-12-29 00:45:57 +02:00
Elian Doran
25b4bcd311 chore(pdfjs): create empty package 2025-12-29 00:26:37 +02:00
Wael Nasreddine
1d3e971ed7 Merge branch 'static-correct-type' into static-implement-sharejs
* static-correct-type:
  improve the protected note handling
  be loosy and honor startsWith application/javascript
2025-12-25 23:01:30 -08:00
Wael Nasreddine
3d1f6c4f91 be loosy and honor startsWith application/javascript 2025-12-25 22:54:14 -08:00
Wael Nasreddine
8368969932 implement the second part of the sharejs 2025-12-25 22:06:30 -08:00
Wael Nasreddine
cb016c4307 Address Gemini's comment 2025-12-25 16:26:58 -08:00
Wael Nasreddine
7c7797d35a fix(share/prev_next): Prevent crashing if candide page is null
When a note is not visible, attempting to export it ends up crashing the
server with this error:

```
TypeError: ejs:193
    191|
    192|                 <% if (hasTree) { %>
 >> 193|                     <%- include("prev_next", { note: note, subRoot: subRoot }) %>
    194|                 <% } %>
    195|             </footer>
    196|         </div>
ejs:1
 >> 1| <%
    2|     // TODO: code cleanup + putting this behind a toggle/attribute
    3|     const previousNote = (() => {
    4|         // If we are at the subRoot, there is no previous
Cannot read properties of undefined (reading 'hasVisibleChildren')
    at eval (eval at compile (/usr/src/app/main.cjs:553:203), <anonymous>:27:26)
    at eval (eval at compile (/usr/src/app/main.cjs:553:203), <anonymous>:34:7)
    at d (/usr/src/app/main.cjs:557:265)
    at g (/usr/src/app/main.cjs:557:251)
    at eval (eval at compile (/usr/src/app/main.cjs:553:203), <anonymous>:293:17)
    at d (/usr/src/app/main.cjs:557:265)
    at as.render (/usr/src/app/main.cjs:532:458)
    at Omr (/usr/src/app/main.cjs:581:109552)
    at Rmr (/usr/src/app/main.cjs:581:107637)
    at $W.prepareContent (/usr/src/app/main.cjs:653:28) {
  path: ''
```

fixes #8002
fixes #8162
2025-12-25 16:11:01 -08:00
meinzzzz
87ab41c80c Fix shift+tab behavior in MathInputView 2025-12-23 18:02:40 +01:00
Meinzzzz
d2391f94c0 Fix offline math rendering by bundling local fonts 2025-12-15 21:32:50 +01:00
Meinzzzz
050ddb8c55 Improve css to fix tooltips 2025-12-15 20:17:58 +01:00
Meinzzzz
bc23e0984a Undo unnecessary formatting changes 2025-12-14 22:00:56 +01:00
Meinzzzz
07de353207 Adding comments and improving code quality in math input views 2025-12-14 20:21:42 +01:00
Meinzzzz
c02491d2e6 Remove unnecessary any casts in math plugin 2025-12-12 23:09:20 +01:00
Meinzzzz
a6ede8f905 Improve mathinputview 2025-12-12 21:33:59 +01:00
Meinzzzz
22941a9ce0 Fix sync issues 2025-12-12 19:48:09 +01:00
Meinzzzz
633a09d414 Fix sync bug 2025-12-11 23:06:13 +01:00
Meinzzzz
29f0881c5a Fix clicking issue in Mathfield 2025-12-10 22:44:02 +01:00
Meinzzzz
60debca37b Improve comments 2025-12-10 18:36:34 +01:00
Meinzzzz
30ea81d0fb Improve virtual keyboard logic and fix Tab issues 2025-12-08 22:59:08 +01:00
Meinzzzz
b1d92c4fe6 Fix Tab issues 2025-12-08 22:39:12 +01:00
Meinzzzz
70f46de2d8 MathLive virtual keyboard only appears when focusing the mathfield 2025-12-08 20:30:07 +01:00
Meinzzzz
f1b2d0b870 Increas Mathfield font size and ensure virtual keyboard appears above CKEditor 2025-12-08 20:22:52 +01:00
Meinzzzz
8a385972fc Close Virtual Keyboard when Mathinput is closed 2025-12-08 18:49:06 +01:00
Meinzzzz
28dd85c1d1 Merge upstream changes and resolve conflicts 2025-12-07 23:51:41 +01:00
meinzzzz
827c8e0e72 Refactor: Combine MathLive and LaTeX inputs into one single component 2025-12-07 23:19:48 +01:00
meinzzzz
162c076a14 Improve MathLive integration and lazy loading 2025-12-02 22:30:37 +01:00
meinzzzz
9386465de7 Added mathrender error class for better error handling in math rendering 2025-12-02 22:29:20 +01:00
meinzzzz
acca22f3a1 Improve Synchronization Between Mathlive and rawlatex input 2025-12-02 22:28:16 +01:00
meinzzzz
f8d84814e0 Fix differential d problems 2025-11-26 23:02:34 +01:00
meinzzzz
c46cf41842 Small improvements 2025-11-26 22:48:57 +01:00
meinzzzz
64ab1c4116 Imrovement for Latex 2025-11-26 22:29:29 +01:00
meinzzzz
a6de1041c7 Fix bug in math rendering where old content was not cleared 2025-11-26 21:59:33 +01:00
meinzzzz
c8d34e65ea Improve max window size 2025-11-26 21:49:09 +01:00
meinzzzz
51db729546 Improve and simplify Mathfield integration 2025-11-25 23:27:06 +01:00
meinzzzz
d2052ad236 Disable mathlive sound effects 2025-11-24 21:51:59 +01:00
meinzzzz
9c4301467f Remove unused icons from ckeditor5-math package 2025-11-24 19:46:04 +01:00
meinzzzz
e7355dc0e4 remove gitignore unneccesary changes 2025-11-24 18:43:52 +01:00
meinzzzz
4110fec94f Removed unnecessary declare keyboard 2025-11-24 18:28:59 +01:00
meinzzzz
d5e601eae9 Simpliyfied resize logic for math input form and improved css 2025-11-24 17:56:18 +01:00
meinzzzz
4f044c4a57 Use icons form CKEditor5 icons, instead of testing icons. 2025-11-23 22:43:07 +01:00
meinzzzz
5821c350e1 Fixing class property initialization order 2025-11-23 17:58:51 +01:00
meinzzzz
edba8188fe Fix dark selection colors in MathLive math-field 2025-11-23 13:44:28 +01:00
meinzzzz
1471a72633 refactor: avoid recursive updates in mathLiveInput by normalizing value before updateing 2025-11-23 13:34:22 +01:00
meinzzzz
56834cb88a Improve MathLive and Raw LaTeX input views to propagate mousedown events 2025-11-23 13:29:26 +01:00
meinzzzz
a0f16f9184 Fix typos in mathform.css 2025-11-23 13:09:56 +01:00
meinzzzz
de80eb4806 Improve mathform.css styling for better visual integration 2025-11-22 22:42:34 +01:00
meinzzzz
48a4b81fbe remove automated screenshot files 2025-11-22 21:40:55 +01:00
meinzzzz
e225794f72 Better window focus handling in MathFormView 2025-11-22 21:35:37 +01:00
meinzzzz
4eef30f8b5 Fix names 2025-11-22 00:20:20 +01:00
meinzzzz
569b09609d Remove mathlive dependency and chunking 2025-11-22 00:01:14 +01:00
meinzzzz
39838c25c2 Fixed chaching problems 2025-11-21 23:50:49 +01:00
meinzzzz
49e90c08a9 Better Names for Math UI Components 2025-11-20 22:45:21 +01:00
meinzzzz
e777b06fb8 Math 2025-11-20 18:53:39 +01:00
meinzzzz
497ec2ac74 Merge branch 'main' of https://github.com/Meinzzzz/Trilium-Mathlive 2025-11-20 18:00:18 +01:00
meinzzzz
c5d282d203 Mathlive 2025-11-20 00:09:10 +01:00
548 changed files with 86128 additions and 4678 deletions

1
.gitignore vendored
View File

@@ -51,3 +51,4 @@ upload
# docs
site/
apps/*/coverage
scripts/translation/.language*.json

View File

@@ -9,9 +9,9 @@
"keywords": [],
"author": "Elian Doran <contact@eliandoran.me>",
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.26.2",
"packageManager": "pnpm@10.27.0",
"devDependencies": {
"@redocly/cli": "2.14.1",
"@redocly/cli": "2.14.3",
"archiver": "7.0.1",
"fs-extra": "11.3.3",
"react": "19.2.3",

View File

@@ -43,7 +43,7 @@
"debounce": "3.0.0",
"draggabilly": "3.0.0",
"force-graph": "1.51.0",
"globals": "16.5.0",
"globals": "17.0.0",
"i18next": "25.7.3",
"i18next-http-backend": "3.0.2",
"jquery": "3.7.1",
@@ -56,11 +56,11 @@
"mark.js": "8.11.1",
"marked": "17.0.1",
"mermaid": "11.12.2",
"mind-elixir": "5.3.8",
"mind-elixir": "5.5.0",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.28.1",
"react-i18next": "16.5.0",
"react-i18next": "16.5.1",
"react-window": "2.2.3",
"reveal.js": "5.2.1",
"svg-pan-zoom": "3.6.2",

View File

@@ -6,7 +6,7 @@ import { ColumnComponent } from "tabulator-tables";
import type { Attribute } from "../services/attribute_parser.js";
import froca from "../services/froca.js";
import { initLocale,t } from "../services/i18n.js";
import { initLocale, t } from "../services/i18n.js";
import keyboardActionsService from "../services/keyboard_actions.js";
import linkService, { type ViewScope } from "../services/link.js";
import type LoadResults from "../services/load_results.js";
@@ -382,7 +382,8 @@ export type CommandMappings = {
reloadTextEditor: CommandData;
chooseNoteType: CommandData & {
callback: ChooseNoteTypeCallback
}
};
customDownload: CommandData;
};
type EventMappings = {
@@ -473,6 +474,11 @@ type EventMappings = {
noteContextRemoved: {
ntxIds: string[];
};
contextDataChanged: {
noteContext: NoteContext;
key: string;
value: unknown;
};
exportSvg: { ntxId: string | null | undefined; };
exportPng: { ntxId: string | null | undefined; };
geoMapCreateChildNote: {

View File

@@ -57,6 +57,18 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
return this;
}
/**
* Removes a child component from this component's children array.
* This is used for cleanup when a widget is unmounted to prevent event listener accumulation.
*/
removeChild(component: ChildT) {
const index = this.children.indexOf(component);
if (index !== -1) {
this.children.splice(index, 1);
component.parent = undefined;
}
}
handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null | undefined {
try {
const callMethodPromise = this.initialized ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) : this.callMethod((this as any)[`${name}Event`], data);

View File

@@ -12,6 +12,7 @@ import server from "../services/server.js";
import treeService from "../services/tree.js";
import utils from "../services/utils.js";
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
import type { HeadingContext } from "../widgets/sidebar/TableOfContents.js";
import appContext, { type EventData, type EventListener } from "./app_context.js";
import Component from "./component.js";
@@ -22,6 +23,31 @@ export interface SetNoteOpts {
export type GetTextEditorCallback = (editor: CKTextEditor) => void;
export type SaveState = "saved" | "saving" | "unsaved" | "error";
export interface NoteContextDataMap {
toc: HeadingContext;
pdfPages: {
totalPages: number;
currentPage: number;
scrollToPage(page: number): void;
requestThumbnail(page: number): void;
};
pdfAttachments: {
attachments: PdfAttachment[];
downloadAttachment(filename: string): void;
};
pdfLayers: {
layers: PdfLayer[];
toggleLayer(layerId: string, visible: boolean): void;
};
saveState: {
state: SaveState;
};
}
type ContextDataKey = keyof NoteContextDataMap;
class NoteContext extends Component implements EventListener<"entitiesReloaded"> {
ntxId: string | null;
hoistedNoteId: string;
@@ -32,6 +58,13 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
parentNoteId?: string | null;
viewScope?: ViewScope;
/**
* Metadata storage for UI components (e.g., table of contents, PDF page list, code outline).
* This allows type widgets to publish data that sidebar/toolbar components can consume.
* Data is automatically cleared when navigating to a different note.
*/
private contextData: Map<string, unknown> = new Map();
constructor(ntxId: string | null = null, hoistedNoteId: string = "root", mainNtxId: string | null = null) {
super();
@@ -91,6 +124,22 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
this.viewScope = opts.viewScope;
({ noteId: this.noteId, parentNoteId: this.parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(resolvedNotePath));
// Clear context data when switching notes and notify subscribers
const oldKeys = Array.from(this.contextData.keys());
this.contextData.clear();
if (oldKeys.length > 0) {
// Notify subscribers asynchronously to avoid blocking navigation
window.setTimeout(() => {
for (const key of oldKeys) {
this.triggerEvent("contextDataChanged", {
noteContext: this,
key,
value: undefined
});
}
}, 0);
}
this.saveToRecentNotes(resolvedNotePath);
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
@@ -443,6 +492,52 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
return title;
}
/**
* Set metadata for this note context (e.g., table of contents, PDF pages, code outline).
* This data can be consumed by sidebar/toolbar components.
*
* @param key - Unique identifier for the data type (e.g., "toc", "pdfPages", "codeOutline")
* @param value - The data to store (will be cleared when switching notes)
*/
setContextData<K extends ContextDataKey>(key: K, value: NoteContextDataMap[K]): void {
this.contextData.set(key, value);
// Trigger event so subscribers can react
this.triggerEvent("contextDataChanged", {
noteContext: this,
key,
value
});
}
/**
* Get metadata for this note context.
*
* @param key - The data key to retrieve
* @returns The stored data, or undefined if not found
*/
getContextData<K extends ContextDataKey>(key: K): NoteContextDataMap[K] | undefined {
return this.contextData.get(key) as NoteContextDataMap[K] | undefined;
}
/**
* Check if context data exists for a given key.
*/
hasContextData(key: ContextDataKey): boolean {
return this.contextData.has(key);
}
/**
* Clear specific context data.
*/
clearContextData(key: ContextDataKey): void {
this.contextData.delete(key);
this.triggerEvent("contextDataChanged", {
noteContext: this,
key,
value: undefined
});
}
}
export function openInCurrentNoteContext(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, notePath: string, viewScope?: ViewScope) {

View File

@@ -8,7 +8,7 @@ import search from "../services/search.js";
import server from "../services/server.js";
import utils from "../services/utils.js";
import type FAttachment from "./fattachment.js";
import type { AttributeType,default as FAttribute } from "./fattribute.js";
import type { AttributeType, default as FAttribute } from "./fattribute.js";
const LABEL = "label";
const RELATION = "relation";

View File

@@ -1,35 +1,35 @@
import { applyModals } from "./layout_commons.js";
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import { useNoteContext } from "../widgets/react/hooks.jsx";
import CloseZenModeButton from "../widgets/close_zen_button.js";
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
import FlexContainer from "../widgets/containers/flex_container.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import type AppContext from "../components/app_context.js";
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
import CloseZenModeButton from "../widgets/close_zen_button.js";
import NoteList from "../widgets/collections/NoteList.jsx";
import NoteTitleWidget from "../widgets/note_title.js";
import ContentHeader from "../widgets/containers/content_header.js";
import FlexContainer from "../widgets/containers/flex_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
import NoteTitleWidget from "../widgets/note_title.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import QuickSearchWidget from "../widgets/quick_search.js";
import { useNoteContext } from "../widgets/react/hooks.jsx";
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
import RootContainer from "../widgets/containers/root_container.js";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx";
import SearchResult from "../widgets/search_result.jsx";
import SharedInfoWidget from "../widgets/shared_info.js";
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
import TabRowWidget from "../widgets/tab_row.js";
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
import type AppContext from "../components/app_context.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
import { applyModals } from "./layout_commons.js";
const MOBILE_CSS = `
<style>
@@ -194,11 +194,11 @@ export default class MobileLayout {
}
function FilePropertiesWrapper() {
const { note } = useNoteContext();
const { note, ntxId } = useNoteContext();
return (
<div>
{note?.type === "file" && <FilePropertiesTab note={note} />}
{note?.type === "file" && <FilePropertiesTab note={note} ntxId={ntxId} />}
</div>
);
}

View File

@@ -1,18 +1,19 @@
import renderService from "./render.js";
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import WheelZoom from 'vanilla-js-wheel-zoom';
import FAttachment from "../entities/fattachment.js";
import FNote from "../entities/fnote.js";
import imageContextMenuService from "../menus/image_context_menu.js";
import { t } from "../services/i18n.js";
import renderText from "./content_renderer_text.js";
import renderDoc from "./doc_renderer.js";
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
import openService from "./open.js";
import protectedSessionService from "./protected_session.js";
import protectedSessionHolder from "./protected_session_holder.js";
import openService from "./open.js";
import utils from "./utils.js";
import FNote from "../entities/fnote.js";
import FAttachment from "../entities/fattachment.js";
import imageContextMenuService from "../menus/image_context_menu.js";
import renderService from "./render.js";
import { applySingleBlockSyntaxHighlight } from "./syntax_highlight.js";
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
import renderDoc from "./doc_renderer.js";
import { t } from "../services/i18n.js";
import WheelZoom from 'vanilla-js-wheel-zoom';
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import renderText from "./content_renderer_text.js";
import utils from "./utils.js";
let idCounter = 1;
@@ -152,7 +153,7 @@ function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLE
const $img = $("<img>")
.attr("src", url || "")
.attr("id", "attachment-image-" + idCounter++)
.attr("id", `attachment-image-${idCounter++}`)
.css("max-width", "100%");
$renderedContent.append($img);
@@ -193,7 +194,7 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
if (type === "pdf") {
const $pdfPreview = $('<iframe class="pdf-preview" style="width: 100%; flex-grow: 100;"></iframe>');
$pdfPreview.attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open`));
$pdfPreview.attr("src", openService.getUrlForDownload(`pdfjs/web/viewer.html?file=../../api/${entityType}/${entityId}/open`));
$content.append($pdfPreview);
} else if (type === "audio") {
@@ -217,28 +218,28 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
// in attachment list
const $downloadButton = $(`
<button class="file-download btn btn-primary" type="button">
<span class="bx bx-download"></span>
<span class="tn-icon bx bx-download"></span>
${t("file_properties.download")}
</button>
`);
const $openButton = $(`
<button class="file-open btn btn-primary" type="button">
<span class="bx bx-link-external"></span>
<span class="tn-icon bx bx-link-external"></span>
${t("file_properties.open")}
</button>
`);
$downloadButton.on("click", (e) => {
e.stopPropagation();
openService.downloadFileNote(entity.noteId)
openService.downloadFileNote(entity, null, null);
});
$openButton.on("click", async (e) => {
const iconEl = $openButton.find("> .bx");
iconEl.removeClass("bx bx-link-external");
iconEl.addClass("bx bx-loader spin");
e.stopPropagation();
await openService.openNoteExternally(entity.noteId, entity.mime)
await openService.openNoteExternally(entity.noteId, entity.mime);
iconEl.removeClass("bx bx-loader spin");
iconEl.addClass("bx bx-link-external");
});
@@ -266,7 +267,7 @@ async function renderMermaid(note: FNote | FAttachment, $renderedContent: JQuery
try {
await loadElkIfNeeded(mermaid, content);
const { svg } = await mermaid.mermaidAPI.render("in-mermaid-graph-" + idCounter++, content);
const { svg } = await mermaid.mermaidAPI.render(`in-mermaid-graph-${idCounter++}`, content);
$renderedContent.append($(postprocessMermaidSvg(svg)));
} catch (e) {

View File

@@ -0,0 +1,14 @@
import { describe, expect, it } from "vitest";
import { getReadableTextColor } from "./css_class_manager";
describe("getReadableTextColor", () => {
it("doesn't crash for invalid color", () => {
expect(getReadableTextColor("RandomColor")).toBe("#000");
});
it("tolerates different casing", () => {
expect(getReadableTextColor("Blue"))
.toBe(getReadableTextColor("blue"));
});
});

View File

@@ -1,21 +1,22 @@
import clsx from "clsx";
import {readCssVar} from "../utils/css-var";
import Color, { ColorInstance } from "color";
import {readCssVar} from "../utils/css-var";
const registeredClasses = new Set<string>();
const colorsWithHue = new Set<string>();
// Read the color lightness limits defined in the theme as CSS variables
const lightThemeColorMaxLightness = readCssVar(
document.documentElement,
"tree-item-light-theme-max-color-lightness"
).asNumber(70);
document.documentElement,
"tree-item-light-theme-max-color-lightness"
).asNumber(70);
const darkThemeColorMinLightness = readCssVar(
document.documentElement,
"tree-item-dark-theme-min-color-lightness"
).asNumber(50);
document.documentElement,
"tree-item-dark-theme-min-color-lightness"
).asNumber(50);
function createClassForColor(colorString: string | null) {
if (!colorString?.trim()) return "";
@@ -27,7 +28,7 @@ function createClassForColor(colorString: string | null) {
if (!registeredClasses.has(className)) {
const adjustedColor = adjustColorLightness(color, lightThemeColorMaxLightness!,
darkThemeColorMinLightness!);
darkThemeColorMinLightness!);
const hue = getHue(color);
$("head").append(`<style>
@@ -50,7 +51,7 @@ function createClassForColor(colorString: string | null) {
function parseColor(color: string) {
try {
return Color(color);
return Color(color.toLowerCase());
} catch (ex) {
console.error(ex);
}
@@ -84,8 +85,8 @@ function getHue(color: ColorInstance) {
}
export function getReadableTextColor(bgColor: string) {
const colorInstance = Color(bgColor);
return colorInstance.isLight() ? "#000" : "#fff";
const colorInstance = parseColor(bgColor);
return !colorInstance || colorInstance?.isLight() ? "#000" : "#fff";
}
export default {

View File

@@ -1,6 +1,8 @@
import utils from "./utils.js";
import Component from "../components/component.js";
import FNote from "../entities/fnote.js";
import options from "./options.js";
import server from "./server.js";
import utils from "./utils.js";
type ExecFunction = (command: string, cb: (err: string, stdout: string, stderror: string) => void) => void;
@@ -36,9 +38,14 @@ function download(url: string) {
}
}
export function downloadFileNote(noteId: string) {
const url = `${getFileUrl("notes", noteId)}?${Date.now()}`; // don't use cache
export function downloadFileNote(note: FNote, parentComponent: Component | null, ntxId: string | null | undefined) {
if (note.type === "file" && note.mime === "application/pdf" && parentComponent) {
// Special handling, manages its own downloading process.
parentComponent.triggerEvent("customDownload", { ntxId });
return;
}
const url = `${getFileUrl("notes", note.noteId)}?${Date.now()}`; // don't use cache
download(url);
}
@@ -97,7 +104,7 @@ async function openCustom(type: string, entityId: string, mime: string) {
// Note that the path separator must be \ instead of /
filePath = filePath.replace(/\//g, "\\");
}
const command = `rundll32.exe shell32.dll,OpenAs_RunDLL ` + filePath;
const command = `rundll32.exe shell32.dll,OpenAs_RunDLL ${filePath}`;
exec(command, (err, stdout, stderr) => {
if (err) {
console.error("Open Note custom: ", err);
@@ -131,10 +138,10 @@ export function getUrlForDownload(url: string) {
if (utils.isElectron()) {
// electron needs absolute URL, so we extract current host, port, protocol
return `${getHost()}/${url}`;
} else {
// web server can be deployed on subdomain, so we need to use a relative path
return url;
}
// web server can be deployed on subdomain, so we need to use a relative path
return url;
}
function canOpenInBrowser(mime: string) {

View File

@@ -85,13 +85,15 @@ async function remove<T>(url: string, componentId?: string) {
return await call<T>("DELETE", url, componentId);
}
async function upload(url: string, fileToUpload: File) {
async function upload(url: string, fileToUpload: File, componentId?: string) {
const formData = new FormData();
formData.append("upload", fileToUpload);
return await $.ajax({
url: window.glob.baseApiUrl + url,
headers: await getHeaders(),
headers: await getHeaders(componentId ? {
"trilium-component-id": componentId
} : undefined),
data: formData,
type: "PUT",
timeout: 60 * 60 * 1000,

View File

@@ -1,22 +1,30 @@
import type { SaveState } from "../components/note_context";
import { getErrorMessage } from "./utils";
type Callback = () => Promise<void> | void;
export type StateCallback = (state: SaveState) => void;
export default class SpacedUpdate {
private updater: Callback;
private lastUpdated: number;
private changed: boolean;
private updateInterval: number;
private changeForbidden?: boolean;
private stateCallback?: StateCallback;
constructor(updater: Callback, updateInterval = 1000) {
constructor(updater: Callback, updateInterval = 1000, stateCallback?: StateCallback) {
this.updater = updater;
this.lastUpdated = Date.now();
this.changed = false;
this.updateInterval = updateInterval;
this.stateCallback = stateCallback;
}
scheduleUpdate() {
if (!this.changeForbidden) {
this.changed = true;
this.stateCallback?.("unsaved");
setTimeout(() => this.triggerUpdate());
}
}
@@ -26,10 +34,13 @@ export default class SpacedUpdate {
this.changed = false; // optimistic...
try {
this.stateCallback?.("saving");
await this.updater();
this.stateCallback?.("saved");
} catch (e) {
this.changed = true;
this.stateCallback?.("error");
logError(getErrorMessage(e));
throw e;
}
}
@@ -59,15 +70,22 @@ export default class SpacedUpdate {
this.updateInterval = interval;
}
triggerUpdate() {
async triggerUpdate() {
if (!this.changed) {
return;
}
if (Date.now() - this.lastUpdated > this.updateInterval) {
this.updater();
this.stateCallback?.("saving");
try {
await this.updater();
this.stateCallback?.("saved");
this.changed = false;
} catch (e) {
this.stateCallback?.("error");
logError(getErrorMessage(e));
}
this.lastUpdated = Date.now();
this.changed = false;
} else {
// update isn't triggered but changes are still pending, so we need to schedule another check
this.scheduleUpdate();

View File

@@ -187,13 +187,15 @@ export function formatSize(size: number | null | undefined) {
return "";
}
size = Math.max(Math.round(size / 1024), 1);
if (size < 1024) {
return `${size} KiB`;
if (size === 0) {
return "0 B";
}
return `${Math.round(size / 102.4) / 10} MiB`;
const k = 1024;
const sizes = ["B", "KiB", "MiB", "GiB"];
const i = Math.floor(Math.log(size) / Math.log(k));
return `${Math.round((size / Math.pow(k, i)) * 100) / 100} ${sizes[i]}`;
}
function toObject<T, R>(array: T[], fn: (arg0: T) => [key: string, value: R]) {

View File

@@ -17,6 +17,8 @@
*/
:root {
color-scheme: var(--theme-style);
--main-font-family: "Inter", sans-serif;
--main-font-size: normal;

View File

@@ -767,7 +767,7 @@ body.mobile .fancytree-node > span {
background: var(--left-pane-item-hover-background);
}
#left-pane span.fancytree-node.shared .fancytree-title::after {
#left-pane .note-indicator-icon.shared-indicator {
opacity: 0.5;
}
@@ -1259,8 +1259,16 @@ body.layout-horizontal #rest-pane > .classic-toolbar-widget {
#center-pane .note-split {
padding-top: 2px;
background-color: var(--note-split-background-color, var(--main-background-color));
transition: border-color 250ms ease-in;
border: 2px solid transparent;
}
/* The active split in a multi-split view */
#center-pane > .split-note-container-widget:has(> .note-split.visible ~ .note-split.visible) > .note-split.active {
border-color: var(--link-selection-outline-color);
}
body:not(.background-effects) #center-pane .note-split {
animation: note-entrance 100ms linear;
}

View File

@@ -148,29 +148,28 @@ span.fancytree-node.protected > span.fancytree-custom-icon {
filter: drop-shadow(2px 2px 2px var(--main-text-color));
}
span.fancytree-node.multiple-parents.shared .fancytree-title::after {
/* Note indicator icons (clone, shared) - real DOM elements for tooltip support */
.note-indicator-icon {
font-family: "boxicons" !important;
font-size: smaller;
content: " \eb3d\ec03";
margin-inline-start: 4px;
opacity: 0.8;
cursor: help;
}
span.fancytree-node.multiple-parents .fancytree-title::after {
font-family: "boxicons" !important;
font-size: smaller;
content: " \eb3d"; /* lookup code for "link-alt" in boxicons.css */
.note-indicator-icon.clone-indicator::before {
content: "\eb3d"; /* bx-link-alt */
}
body.experimental-feature-new-layout span.fancytree-node.multiple-parents .fancytree-title::after {
content: " \ed82";
.note-indicator-icon.shared-indicator::before {
content: "\ec03"; /* bx-share-alt */
}
body.experimental-feature-new-layout .note-indicator-icon.clone-indicator::before {
content: "\ed82";
opacity: 0.5;
}
span.fancytree-node.shared .fancytree-title::after {
font-family: "boxicons" !important;
font-size: smaller;
content: " \ec03"; /* lookup code for "share-alt" in boxicons.css */
}
span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-title {
font-weight: bold;
}

View File

@@ -484,7 +484,6 @@
"delete_button": "حذف",
"download_button": "تنزيل",
"restore_button": "أستعادة",
"preview": "معاينة:",
"note_revisions": "مراجعات الملاحظة",
"diff_on": "عرض الفروقات",
"diff_off": "عرض المحتوى",

View File

@@ -64,8 +64,7 @@
"restore_button": "Restaura",
"delete_button": "Suprimeix",
"download_button": "Descarrega",
"mime": "MIME: ",
"preview": "Vista prèvia:"
"mime": "MIME: "
},
"sort_child_notes": {
"title": "títol",

View File

@@ -290,7 +290,6 @@
"download_button": "下载",
"mime": "MIME 类型: ",
"file_size": "文件大小:",
"preview": "预览:",
"preview_not_available": "无法预览此类型的笔记。",
"diff_on": "显示差异",
"diff_off": "显示内容",
@@ -765,7 +764,14 @@
"note_icon": {
"change_note_icon": "更改笔记图标",
"search": "搜索:",
"reset-default": "重置为默认图标"
"reset-default": "重置为默认图标",
"search_placeholder_other": "在 {{count}} 个图标包中搜索 {{number}} 个图标",
"search_placeholder_filtered": "在 {{name}} 中搜索 {{number}} 个图标",
"filter": "筛选",
"filter-none": "所有图标",
"filter-default": "默认图标",
"icon_tooltip": "{{name}}\n图标包{{iconPack}}",
"no_results": "没有找到图标。"
},
"basic_properties": {
"note_type": "笔记类型",
@@ -1445,7 +1451,7 @@
"will_be_deleted_in": "此附件将在 {{time}} 后自动删除",
"will_be_deleted_soon": "该附件在不久后将被自动删除",
"deletion_reason": ",因为该附件未链接在笔记的内容中。为防止被删除,请将附件链接重新添加到内容中或将附件转换为笔记。",
"role_and_size": "角色:{{role}},大小:{{size}}",
"role_and_size": "角色:{{role}},大小:{{size}},文件类型:{{- mimeType}}",
"link_copied": "附件链接已复制到剪贴板。",
"unrecognized_role": "无法识别的附件角色 '{{role}}'。"
},
@@ -1588,7 +1594,11 @@
"create-child-note": "创建子笔记",
"unhoist": "取消聚焦",
"toggle-sidebar": "切换侧边栏",
"dropping-not-allowed": "不允许移动笔记到此处。"
"dropping-not-allowed": "不允许移动笔记到此处。",
"shared-indicator-tooltip": "此笔记已公开分享",
"shared-indicator-tooltip-with-url": "此笔记已公开分享至:{{- url}}",
"clone-indicator-tooltip": "此笔记有 {{- count}} 个父级: {{- parents}}",
"clone-indicator-tooltip-single": "此笔记已克隆1 个额外的父级:{{- parent}}"
},
"title_bar_buttons": {
"window-on-top": "保持此窗口置顶"
@@ -2186,7 +2196,14 @@
"execute_sql_description": "这是一篇 SQL 笔记。点击即可执行 SQL 查询。",
"shared_copy_to_clipboard": "复制链接到剪贴板",
"shared_open_in_browser": "在浏览器中打开链接",
"shared_unshare": "取消共享"
"shared_unshare": "取消共享",
"save_status_saved": "已保存",
"save_status_saving": "保存中...",
"save_status_unsaved": "未保存",
"save_status_error": "保存失败",
"save_status_unsaved_tooltip": "还有一些更改尚未保存。它们将稍后自动保存。",
"save_status_error_tooltip": "保存笔记时出错。如果可以,请尝试将笔记内容复制到其他位置并重新加载应用程序。",
"save_status_saving_tooltip": "更改正在保存。"
},
"status_bar": {
"language_title": "更改内容语言",
@@ -2217,5 +2234,12 @@
},
"attributes_panel": {
"title": "笔记属性"
},
"pdf": {
"attachments_other": "{{count}} 个附件",
"pages_other": "共{{count}}页",
"pages_alt": "第{{pageNumber}}页",
"pages_loading": "加载中...",
"layers_other": "{{count}} 层"
}
}

View File

@@ -281,7 +281,6 @@
"download_button": "Herunterladen",
"mime": "MIME: ",
"file_size": "Dateigröße:",
"preview": "Vorschau:",
"preview_not_available": "Für diesen Notiztyp ist keine Vorschau verfügbar.",
"restore_button": "Wiederherstellen",
"delete_button": "Löschen",
@@ -692,7 +691,11 @@
"convert_into_attachment_successful": "Notiz '{{title}}' wurde als Anhang konvertiert.",
"convert_into_attachment_prompt": "Bist du dir sicher, dass du die Notiz '{{title}}' in ein Anhang der übergeordneten Notiz konvertieren möchtest?",
"print_pdf": "Export als PDF...",
"open_note_on_server": "Öffne Notiz auf dem Server"
"open_note_on_server": "Öffne Notiz auf dem Server",
"export_as_image": "Als Bild exportieren",
"export_as_image_png": "PNG (Raster)",
"export_as_image_svg": "SVG (Vektor)",
"note_map": "Notizen Karte"
},
"onclick_button": {
"no_click_handler": "Das Schaltflächen-Widget „{{componentId}}“ hat keinen definierten Klick-Handler"
@@ -750,7 +753,15 @@
"note_icon": {
"change_note_icon": "Notiz-Icon ändern",
"search": "Suche:",
"reset-default": "Standard wiederherstellen"
"reset-default": "Standard wiederherstellen",
"search_placeholder_one": "Suche {{number}} Icons über {{count}} Pakete",
"search_placeholder_other": "Suche {{number}} Icons über {{count}} Pakete",
"search_placeholder_filtered": "Suche {{number}} Icons in {{name}}",
"filter": "Filter",
"filter-none": "Alle Icons",
"filter-default": "Standard Icons",
"icon_tooltip": "{{name}}\nIcon Paket: {{iconPack}}",
"no_results": "Keine Icons gefunden."
},
"basic_properties": {
"note_type": "Notiztyp",
@@ -810,7 +821,8 @@
},
"inherited_attribute_list": {
"title": "Geerbte Attribute",
"no_inherited_attributes": "Keine geerbten Attribute."
"no_inherited_attributes": "Keine geerbten Attribute.",
"none": "Keine"
},
"note_info_widget": {
"note_id": "Notiz-ID",
@@ -821,7 +833,9 @@
"note_size_info": "Die Notizgröße bietet eine grobe Schätzung des Speicherbedarfs für diese Notiz. Es berücksichtigt den Inhalt der Notiz und den Inhalt ihrer Notizrevisionen.",
"calculate": "berechnen",
"subtree_size": "(Teilbaumgröße: {{size}} in {{count}} Notizen)",
"title": "Notizinfo"
"title": "Notizinfo",
"mime": "MIME Typ",
"show_similar_notes": "Zeige ähnliche Notizen"
},
"note_map": {
"open_full": "Vollständig erweitern",
@@ -884,7 +898,8 @@
"search_parameters": "Suchparameter",
"unknown_search_option": "Unbekannte Suchoption {{searchOptionName}}",
"search_note_saved": "Suchnotiz wurde in {{-notePathTitle}} gespeichert",
"actions_executed": "Aktionen wurden ausgeführt."
"actions_executed": "Aktionen wurden ausgeführt.",
"view_options": "Anzeigeoptionen:"
},
"similar_notes": {
"title": "Ähnliche Notizen",
@@ -988,7 +1003,12 @@
"editable_text": {
"placeholder": "Gebe hier den Inhalt deiner Notiz ein...",
"auto-detect-language": "Automatisch erkannt",
"keeps-crashing": "Die Bearbeitungskomponente stürzt immer wieder ab. Bitte starten Sie Trilium neu. Wenn das Problem weiterhin besteht, erstellen Sie einen Fehlerbericht."
"keeps-crashing": "Die Bearbeitungskomponente stürzt immer wieder ab. Bitte starten Sie Trilium neu. Wenn das Problem weiterhin besteht, erstellen Sie einen Fehlerbericht.",
"editor_crashed_title": "Der Text Editor ist abgestürzt",
"editor_crashed_content": "Ihr Inhalt wurde erfolgreich wiederhergestellt, aber einzelne Ihrer letzten Änderungen waren möglicherweise noch nicht gespeichert.",
"editor_crashed_details_button": "Zeige mehr Details…",
"editor_crashed_details_intro": "Falls Sie diesen Fehler mehrmals sehen, melden Sie dies auf GitHub mit den folgenden Informationen.",
"editor_crashed_details_title": "Technische Informationen"
},
"empty": {
"open_note_instruction": "Öffne eine Notiz, indem du den Titel der Notiz in die Eingabe unten eingibst oder eine Notiz in der Baumstruktur auswählst.",
@@ -1503,7 +1523,12 @@
},
"highlights_list_2": {
"title": "Hervorhebungs-Liste",
"options": "Optionen"
"options": "Optionen",
"title_with_count_one": "{{count}} Highlight",
"title_with_count_other": "{{count}} Highlights",
"modal_title": "Highlight Liste konfigurieren",
"menu_configure": "Highlight Liste konfigurieren…",
"no_highlights": "Keine Highlights gefunden."
},
"quick-search": {
"placeholder": "Schnellsuche",
@@ -1535,10 +1560,21 @@
"note_detail": {
"could_not_find_typewidget": "Konnte typeWidget für Typ {{type}} nicht finden",
"printing": "Druckvorgang läuft…",
"printing_pdf": "PDF-Export läuft…"
"printing_pdf": "PDF-Export läuft…",
"print_report_title": "Druckreport",
"print_report_collection_details_button": "Details anzeigen",
"print_report_collection_details_ignored_notes": "Ignorierte Notizen"
},
"note_title": {
"placeholder": "Titel der Notiz hier eingeben…"
"placeholder": "Titel der Notiz hier eingeben…",
"created_on": "Erstellt am <Value />",
"last_modified": "Bearbeitet am <Value />",
"note_type_switcher_label": "Ändere von {{type}} zu:",
"note_type_switcher_others": "Andere Notizart",
"note_type_switcher_templates": "Template",
"note_type_switcher_collection": "Sammlung",
"edited_notes": "Notizen, bearbeitet an diesem Tag",
"promoted_attributes": "Hervorgehobene Attribute"
},
"search_result": {
"no_notes_found": "Es wurden keine Notizen mit den angegebenen Suchparametern gefunden.",
@@ -1567,7 +1603,8 @@
},
"toc": {
"table_of_contents": "Inhaltsverzeichnis",
"options": "Optionen"
"options": "Optionen",
"no_headings": "Keine Überschriften."
},
"watched_file_update_status": {
"file_last_modified": "Datei <code class=\"file-path\"></code> wurde zuletzt geändert am <span class=\"file-last-modified\"></span>.",
@@ -2106,5 +2143,10 @@
},
"popup-editor": {
"maximize": "Wechsele zum vollständigen Editor"
},
"experimental_features": {
"title": "Experimentelle Optionen",
"disclaimer": "Diese Optionen sind experimentell und können Instabilitäten verursachen. Achtsam zu verwenden.",
"new_layout_name": "Neues Layout"
}
}

View File

@@ -295,7 +295,6 @@
"download_button": "Download",
"mime": "MIME: ",
"file_size": "File size:",
"preview": "Preview:",
"preview_not_available": "Preview isn't available for this note type."
},
"sort_child_notes": {
@@ -1769,7 +1768,11 @@
"create-child-note": "Create child note",
"unhoist": "Unhoist",
"toggle-sidebar": "Toggle sidebar",
"dropping-not-allowed": "Dropping notes into this location is not allowed."
"dropping-not-allowed": "Dropping notes into this location is not allowed.",
"clone-indicator-tooltip": "This note has {{- count}} parents: {{- parents}}",
"clone-indicator-tooltip-single": "This note is cloned (1 additional parent: {{- parent}})",
"shared-indicator-tooltip": "This note is shared publicly",
"shared-indicator-tooltip-with-url": "This note is shared publicly at: {{- url}}"
},
"title_bar_buttons": {
"window-on-top": "Keep Window on Top"
@@ -2205,7 +2208,14 @@
"execute_script": "Run script",
"execute_script_description": "This note is a script note. Click to execute the script.",
"execute_sql": "Run SQL",
"execute_sql_description": "This note is a SQL note. Click to execute the SQL query."
"execute_sql_description": "This note is a SQL note. Click to execute the SQL query.",
"save_status_saved": "Saved",
"save_status_saving": "Saving...",
"save_status_unsaved": "Unsaved",
"save_status_error": "Save failed",
"save_status_saving_tooltip": "Changes are being saved.",
"save_status_unsaved_tooltip": "There are unsaved changes. They will be saved automatically in a moment.",
"save_status_error_tooltip": "An error occurred while saving the note. If possible, try copying the note content elsewhere and reloading the application."
},
"status_bar": {
"language_title": "Change content language",
@@ -2234,5 +2244,15 @@
"empty_button": "Hide the panel",
"toggle": "Toggle right panel",
"custom_widget_go_to_source": "Go to source code"
},
"pdf": {
"attachments_one": "{{count}} attachment",
"attachments_other": "{{count}} attachments",
"layers_one": "{{count}} layer",
"layers_other": "{{count}} layers",
"pages_one": "{{count}} page",
"pages_other": "{{count}} pages",
"pages_alt": "Page {{pageNumber}}",
"pages_loading": "Loading..."
}
}

View File

@@ -279,7 +279,6 @@
"download_button": "Descargar",
"mime": "MIME: ",
"file_size": "Tamaño del archivo:",
"preview": "Vista previa:",
"preview_not_available": "La vista previa no está disponible para este tipo de notas.",
"diff_off": "Mostrar contenido",
"diff_on": "Mostrar diferencia",

View File

@@ -22,6 +22,12 @@
"bundle-error": {
"title": "Echec du chargement d'un script personnalisé",
"message": "Le script de la note avec l'ID \"{{id}}\", intitulé \"{{title}}\" n'a pas pu être exécuté à cause de\n\n{{message}}"
},
"widget-list-error": {
"title": "Impossible d'obtenir la liste des widgets depuis le serveur"
},
"widget-render-error": {
"title": "Rendu impossible d'un widget React custom"
}
},
"add_link": {
@@ -279,7 +285,6 @@
"download_button": "Télécharger",
"mime": "MIME : ",
"file_size": "Taille du fichier :",
"preview": "Aperçu :",
"preview_not_available": "L'aperçu n'est pas disponible pour ce type de note.",
"restore_button": "Restaurer",
"delete_button": "Supprimer",
@@ -757,7 +762,11 @@
"note_icon": {
"change_note_icon": "Changer l'icône de note",
"search": "Recherche :",
"reset-default": "Réinitialiser l'icône par défaut"
"reset-default": "Réinitialiser l'icône par défaut",
"filter": "Filtre",
"filter-none": "Toutes les icônes",
"filter-default": "Icônes par défaut",
"icon_tooltip": "{{name}}\nPack d'icônes : {{iconPack}}"
},
"basic_properties": {
"note_type": "Type de note",
@@ -1541,7 +1550,8 @@
"refresh-saved-search-results": "Rafraîchir les résultats de recherche enregistrée",
"create-child-note": "Créer une note enfant",
"unhoist": "Désactiver le focus",
"toggle-sidebar": "Basculer la barre latérale"
"toggle-sidebar": "Basculer la barre latérale",
"dropping-not-allowed": "Lâcher des notes à cet endroit n'est pas autorisé"
},
"title_bar_buttons": {
"window-on-top": "Épingler cette fenêtre au premier plan"
@@ -1549,10 +1559,19 @@
"note_detail": {
"could_not_find_typewidget": "Impossible de trouver typeWidget pour le type '{{type}}'",
"printing": "Impression en cours...",
"printing_pdf": "Export au format PDF en cours..."
"printing_pdf": "Export au format PDF en cours...",
"print_report_title": "Imprimer le rapport",
"print_report_collection_details_button": "Consulter les détails",
"print_report_collection_details_ignored_notes": "Notes ignorées"
},
"note_title": {
"placeholder": "saisir le titre de la note ici..."
"placeholder": "saisir le titre de la note ici...",
"created_on": "Créé le <Value />",
"last_modified": "Modifié le <Value />",
"note_type_switcher_label": "Basculer de {{type}} à :",
"note_type_switcher_others": "Autre type de note",
"note_type_switcher_templates": "Modèle",
"note_type_switcher_collection": "Collection"
},
"search_result": {
"no_notes_found": "Aucune note n'a été trouvée pour les paramètres de recherche donnés.",
@@ -1581,7 +1600,8 @@
},
"toc": {
"table_of_contents": "Table des matières",
"options": "Options"
"options": "Options",
"no_headings": "Pas d'en-tête."
},
"watched_file_update_status": {
"file_last_modified": "Le fichier <code class=\"file-path\"></code> a été modifié pour la dernière fois le <span class=\"file-last-modified\"></span>.",
@@ -1682,7 +1702,8 @@
"copy-link": "Copier le lien",
"paste": "Coller",
"paste-as-plain-text": "Coller comme texte brut",
"search_online": "Rechercher «{{term}}» avec {{searchEngine}}"
"search_online": "Rechercher «{{term}}» avec {{searchEngine}}",
"search_in_trilium": "Rechercher \"{{term}}\" dans Trilium"
},
"image_context_menu": {
"copy_reference_to_clipboard": "Copier la référence dans le presse-papiers",
@@ -1991,7 +2012,8 @@
"add-column": "Ajouter une colonne",
"add-column-placeholder": "Entrez le nom de la colonne...",
"edit-note-title": "Cliquez pour modifier le titre de la note",
"edit-column-title": "Cliquez pour modifier le titre de la colonne"
"edit-column-title": "Cliquez pour modifier le titre de la colonne",
"column-already-exists": "Cette colonne existe déjà dans le tableau."
},
"presentation_view": {
"edit-slide": "Modifier cette diapositive",
@@ -2075,7 +2097,8 @@
"button_title": "Exporter le diagramme au format PNG"
},
"svg": {
"export_to_png": "Le diagramme n'a pas pu être exporté au format PNG."
"export_to_png": "Le diagramme n'a pas pu être exporté au format PNG.",
"export_to_svg": "Le diagramme n'a pas pu être exporté en SVG."
},
"code_theme": {
"title": "Apparence",
@@ -2108,6 +2131,10 @@
},
"read-only-info": {
"read-only-note": "Vous consultez actuellement une note en lecture seule.",
"auto-read-only-note": "Cette note s'affiche en mode lecture seule pour un chargement plus rapide."
"auto-read-only-note": "Cette note s'affiche en mode lecture seule pour un chargement plus rapide.",
"edit-note": "Editer la note"
},
"calendar_view": {
"delete_note": "Effacer la note..."
}
}

View File

@@ -1,5 +1,35 @@
{
"about": {
"title": "ट्रिलियम नोट्स के बारें में"
"title": "ट्रिलियम नोट्स के बारें में",
"build_date": "निर्माण की तारीख:"
},
"toast": {
"widget-error": {
"title": "एक विजेट को इनिशियलाइज़ करने में विफल रहा"
},
"bundle-error": {
"title": "एक कस्टम स्क्रिप्ट लोड करने में विफल रहा"
},
"widget-list-error": {
"title": "सर्वर से विजेट्स की सूची प्राप्त करने में विफल"
},
"open-script-note": "स्क्रिप्ट नोट खोलें"
},
"update_available": {
"update_available": "उपलब्ध अद्यतन"
},
"code_buttons": {
"execute_button_title": "स्क्रिप्ट एक्सीक्यूट करें",
"trilium_api_docs_button_title": "ट्रिलियम एपीआई डॉक्स खोलें",
"save_to_note_button_title": "नोट में सेव करें"
},
"hide_floating_buttons_button": {
"button_title": "बटन छुपाएं"
},
"show_floating_buttons_button": {
"button_title": "बटन दिखाएं"
},
"add_link": {
"note": "नोट"
}
}

View File

@@ -16,13 +16,22 @@
},
"bundle-error": {
"title": "Non si è riusciti a caricare uno script personalizzato",
"message": "Lo script della nota con ID \"{{id}}\", dal titolo \"{{title}}\" non è stato inizializzato a causa di:\n\n{{message}}"
"message": "Impossibile eseguire lo script a causa di:\n\n{{message}}"
},
"widget-error": {
"title": "Impossibile inizializzare un widget",
"message-custom": "Il widget personalizzato dalla nota con ID “{{id}}”, intitolato “{{title}}”, non è stato possibile inizializzare a causa di:\n\n{{message}}",
"message-unknown": "Un widget sconosciuto non è stato inizializzato a causa di:\n\n{{message}}"
}
},
"widget-list-error": {
"title": "Impossibile ottenere l'elenco dei widget dal server"
},
"widget-render-error": {
"title": "Impossibile eseguire il rendering di un widget React personalizzato"
},
"widget-missing-parent": "Il widget personalizzato non ha la proprietà obbligatoria '{{property}}' definita.\n\nSe questo script deve essere eseguito senza un elemento dell'interfaccia utente, utilizzare invece '#run=frontendStartup'.",
"open-script-note": "Apri script note",
"scripting-error": "Errore script personalizzato: {{title}}"
},
"add_link": {
"add_link": "Aggiungi un collegamento",
@@ -893,7 +902,6 @@
"download_button": "Scarica",
"mime": "MIME: ",
"file_size": "Dimensione del file:",
"preview": "Anteprima:",
"preview_not_available": "L'anteprima non è disponibile per questo tipo di nota."
},
"sort_child_notes": {
@@ -1334,7 +1342,16 @@
"note_icon": {
"change_note_icon": "Cambia icona nota",
"search": "Ricerca:",
"reset-default": "Ripristina l'icona predefinita"
"reset-default": "Ripristina l'icona predefinita",
"search_placeholder_one": "Cerca {{number}} icona in {{count}} pacchetto",
"search_placeholder_many": "Cerca {{number}} icone in {{count}} pacchetti",
"search_placeholder_other": "Cerca {{number}} icone in {{count}} pacchetti",
"search_placeholder_filtered": "Cerca {{number}} icone in {{name}}",
"filter": "Filtro",
"filter-none": "Tutte le icone",
"filter-default": "Icone predefinite",
"icon_tooltip": "{{name}}\nPacchetto icone: {{iconPack}}",
"no_results": "Nessuna icona trovata."
},
"basic_properties": {
"note_type": "Tipo di nota",
@@ -1792,7 +1809,7 @@
"will_be_deleted_in": "Questo allegato verrà eliminato automaticamente tra {{time}}",
"will_be_deleted_soon": "Questo allegato verrà eliminato automaticamente a breve",
"deletion_reason": ", perché l'allegato non è collegato al contenuto della nota. Per impedirne l'eliminazione, aggiungi nuovamente il collegamento all'allegato nel contenuto o converti l'allegato in nota.",
"role_and_size": "Ruolo: {{role}}, Dimensione: {{size}}",
"role_and_size": "Ruolo: {{role}}, dimensione: {{size}}, MIME: {{- mimeType}}",
"link_copied": "Link all'allegato copiato negli appunti.",
"unrecognized_role": "Ruolo di allegato non riconosciuto '{{role}}'."
},
@@ -1886,7 +1903,13 @@
"note_detail": {
"could_not_find_typewidget": "Impossibile trovare typeWidget per il tipo '{{type}}'",
"printing": "Stampa in corso...",
"printing_pdf": "Esportazione in PDF in corso..."
"printing_pdf": "Esportazione in PDF in corso...",
"print_report_title": "Stampa rapporto",
"print_report_collection_content_one": "{{count}} la note nella raccolta non può essere stampata perché non è supportata o è protetta.",
"print_report_collection_content_many": "{{count}} le note nella raccolta non possono essere stampate perché non sono supportate o sono protette.",
"print_report_collection_content_other": "{{count}} le note nella raccolta non possono essere stampate perché non sono supportate o sono protette.",
"print_report_collection_details_button": "Vedi dettagli",
"print_report_collection_details_ignored_notes": "Note ignorate"
},
"note_title": {
"placeholder": "scrivi qui il titolo della nota...",
@@ -1896,7 +1919,8 @@
"note_type_switcher_others": "Altro tipo di nota",
"note_type_switcher_templates": "Modello",
"note_type_switcher_collection": "Collezione",
"edited_notes": "Note modificate"
"edited_notes": "Note modificate in questo giorno",
"promoted_attributes": "Attributi promossi"
},
"search_result": {
"no_notes_found": "Non sono state trovate note per i parametri di ricerca specificati.",

View File

@@ -153,7 +153,14 @@
"note_icon": {
"change_note_icon": "ノートアイコンの変更",
"search": "検索:",
"reset-default": "アイコンをデフォルトに戻す"
"reset-default": "アイコンをデフォルトに戻す",
"search_placeholder_other": "{{count}} 個のパックから {{number}} 個のアイコンを検索",
"search_placeholder_filtered": "{{name}} で {{number}} 個のアイコンを検索",
"filter": "フィルター",
"filter-none": "すべてのアイコン",
"filter-default": "デフォルトアイコン",
"icon_tooltip": "{{name}}\nアイコンパック: {{iconPack}}",
"no_results": "アイコンが見つかりません。"
},
"basic_properties": {
"note_type": "ノートタイプ",
@@ -647,7 +654,6 @@
"revision_deleted": "ノートの変更履歴は削除されました。",
"settings": "ノートの変更履歴の設定",
"file_size": "ファイルサイズ:",
"preview": "プレビュー:",
"preview_not_available": "このノートタイプではプレビューは利用できません。",
"diff_on": "差分を表示",
"diff_off": "内容を表示",
@@ -1238,7 +1244,11 @@
"saved-search-note-refreshed": "保存した検索ノートが更新されました。",
"refresh-saved-search-results": "保存した検索結果を更新",
"toggle-sidebar": "サイドバーを切り替え",
"dropping-not-allowed": "この場所にノートをドロップすることはできません。"
"dropping-not-allowed": "この場所にノートをドロップすることはできません。",
"clone-indicator-tooltip": "このノートには {{- count}} 個の親があります: {{- parents}}",
"clone-indicator-tooltip-single": "このノートは複製されています (親が 1 件追加: {{- parent}})",
"shared-indicator-tooltip": "このノートは公開されています",
"shared-indicator-tooltip-with-url": "このノートは以下で公開されています: {{- url}}"
},
"bulk_actions": {
"bulk_actions": "一括操作",
@@ -2129,7 +2139,7 @@
"will_be_deleted_in": "この添付ファイルは {{time}} 後に自動的に削除されます",
"will_be_deleted_soon": "この添付ファイルはすぐに自動的に削除されます",
"deletion_reason": "、添付ファイルがノートのコンテンツにリンクされていないためです。削除されないようにするには、添付ファイルのリンクをコンテンツに再度追加するか、添付ファイルをノートに変換してください。",
"role_and_size": "ロール: {{role}},サイズ: {{size}}",
"role_and_size": "ロール: {{role}},サイズ: {{size}}, MIME: {{- mimeType}}",
"link_copied": "添付ファイルのリンクをクリップボードにコピーしました。",
"unrecognized_role": "添付ファイルのロール「{{role}}」は認識されません。"
},
@@ -2186,7 +2196,14 @@
"execute_sql_description": "このノートは SQL ノートです。クリックすると SQL クエリが実行されます。",
"shared_copy_to_clipboard": "リンクをクリップボードにコピー",
"shared_open_in_browser": "ブラウザでリンクを開く",
"shared_unshare": "共有を削除"
"shared_unshare": "共有を削除",
"save_status_saved": "保存されました",
"save_status_saving": "保存中...",
"save_status_unsaved": "未保存",
"save_status_error": "保存に失敗しました",
"save_status_saving_tooltip": "変更を保存しています。",
"save_status_unsaved_tooltip": "未保存の変更があります。すぐに自動的に保存されます。",
"save_status_error_tooltip": "ノートの保存中にエラーが発生しました。可能であれば、ノートの内容を別の場所にコピーして、アプリケーションを再読み込みしてください。"
},
"status_bar": {
"language_title": "コンテンツの言語を変更",
@@ -2217,5 +2234,12 @@
},
"attributes_panel": {
"title": "ノート属性"
},
"pdf": {
"attachments_other": "{{count}} 添付ファイル",
"layers_other": "{{count}} 層",
"pages_other": "{{count}} ページ",
"pages_alt": "ページ {{pageNumber}}",
"pages_loading": "読み込み中..."
}
}

View File

@@ -1 +1,22 @@
{}
{
"about": {
"title": "Om Trilium Notes",
"app_version": "App versjon:",
"db_version": "DB versjon:",
"sync_version": "Synk versjon:",
"build_date": "Byggdato:",
"build_revision": "Bygg versjon:",
"data_directory": "Datamappe:",
"homepage": "Hjemmeside:"
},
"experimental_features": {
"new_layout_description": "Prøv det nye grensesnittet for et mer moderne utseende og forbedret brukervenlighet. Det må påregnes betydelige endringer i kommende versjoner."
},
"cpu_arch_warning": {
"recommendation": "For den beste brukeropplevelsen, vennligst last ned den tilpassede ARM64-versjonen av TriliumNext fra siden for utgivelser."
},
"zpetne_odkazy": {
"backlink_one": "{{count}} Tilbakelenke",
"backlink_other": "{{count}} Tilbakelenker"
}
}

View File

@@ -959,7 +959,6 @@
"download_button": "Pobierz",
"mime": "MIME: ",
"file_size": "Rozmiar pliku:",
"preview": "Podgląd:",
"preview_not_available": "Podgląd nie jest dostępny dla tego typu notatki."
},
"sort_child_notes": {

View File

@@ -274,7 +274,6 @@
"download_button": "Descarregar",
"mime": "MIME: ",
"file_size": "Tamanho do ficheiro:",
"preview": "Visualizar:",
"preview_not_available": "A visualização não está disponível para este tipo de nota."
},
"sort_child_notes": {

View File

@@ -439,7 +439,6 @@
"download_button": "Download",
"mime": "MIME: ",
"file_size": "Tamanho do arquivo:",
"preview": "Visualizar:",
"preview_not_available": "A visualização não está disponível para este tipo de nota.",
"diff_on": "Exibir diferença",
"diff_off": "Exibir conteúdo",

View File

@@ -1103,7 +1103,6 @@
"mime": "MIME: ",
"no_revisions": "Nu există încă nicio revizie pentru această notiță...",
"note_revisions": "Revizii ale notiței",
"preview": "Previzualizare:",
"preview_not_available": "Nu este disponibilă o previzualizare pentru acest tip de notiță.",
"restore_button": "Restaurează",
"revision_deleted": "Revizia notiței a fost ștearsă.",

View File

@@ -387,7 +387,6 @@
"revision_deleted": "Версия заметки была удалена.",
"download_button": "Скачать",
"file_size": "Размер файла:",
"preview": "Предпросмотр:",
"preview_not_available": "Предпосмотр недоступен для заметки этого типа.",
"mime": "MIME: ",
"settings": "Настройка версионирования заметок",
@@ -1012,7 +1011,16 @@
"note_icon": {
"search": "Поиск:",
"change_note_icon": "Изменить иконку заметки",
"reset-default": "Сбросить к значку по умолчанию"
"reset-default": "Сбросить к значку по умолчанию",
"no_results": "Иконки не найдены.",
"icon_tooltip": "{{name}}\nНабор иконок: {{iconPack}}",
"filter-default": "Иконки по-умолчанию",
"filter-none": "Все иконки",
"filter": "Фильтр",
"search_placeholder_filtered": "Поиск {{number}} иконок в {{name}}",
"search_placeholder_one": "Поиск {{number}} иконки среди {{count}} наборов",
"search_placeholder_few": "Поиск {{number}} иконок среди {{count}} наборов",
"search_placeholder_many": "Поиск {{number}} иконок среди {{count}} наборов"
},
"basic_properties": {
"editable": "Изменяемое",
@@ -2025,7 +2033,7 @@
"lost-websocket-connection-message": "Проверьте конфигурацию обратного прокси (например, nginx или Apache), чтобы убедиться, что соединения WebSocket должным образом разрешены и не заблокированы."
},
"attachment_detail_2": {
"role_and_size": "Роль: {{role}}, Размер: {{size}}",
"role_and_size": "Роль: {{role}}, размер: {{size}}, MIME: {{- mimeType}}",
"unrecognized_role": "Нераспознанная роль вложения '{{role}}'.",
"link_copied": "Ссылка на вложение скопирована в буфер обмена.",
"will_be_deleted_soon": "Это вложение скоро будет автоматически удалено",

View File

@@ -271,7 +271,6 @@
"download_button": "Preuzmi",
"mime": "MIME: ",
"file_size": "Veličina datoteke:",
"preview": "Pregled:",
"preview_not_available": "Pregled nije dostupan za ovaj tip beleške."
},
"sort_child_notes": {

View File

@@ -29,8 +29,9 @@
"widget-render-error": {
"title": "無法渲染自訂 React 元件"
},
"widget-missing-parent": "自訂元件未定義強制性的 \"{{property}}\" 屬性。",
"open-script-note": "打開腳本筆記"
"widget-missing-parent": "自訂元件未定義強制性的 \"{{property}}\" 屬性。\n\n若此腳本需在無 UI 的情況下執行,請改用 \"#run=frontendStartup\"。",
"open-script-note": "打開腳本筆記",
"scripting-error": "自訂腳本錯誤:{{title}}"
},
"add_link": {
"add_link": "新增連結",
@@ -287,7 +288,6 @@
"download_button": "下載",
"mime": "MIME類型 ",
"file_size": "檔案大小:",
"preview": "預覽:",
"preview_not_available": "無法預覽此類型的筆記。",
"restore_button": "還原",
"delete_button": "刪除",
@@ -762,7 +762,15 @@
"note_icon": {
"change_note_icon": "更改筆記圖標",
"search": "搜尋:",
"reset-default": "重置為預設圖標"
"reset-default": "重置為預設圖標",
"search_placeholder_one": "在 {{count}} 個圖示包中搜尋 {{number}} 個圖示",
"search_placeholder_other": "",
"search_placeholder_filtered": "在 {{name}} 中搜尋 {{number}} 個圖示",
"filter": "篩選",
"filter-none": "所有圖示",
"filter-default": "預設圖示",
"icon_tooltip": "{{name}}\n圖示包{{iconPack}}",
"no_results": "找不到圖示。"
},
"basic_properties": {
"note_type": "筆記類型",
@@ -1404,7 +1412,7 @@
"will_be_deleted_in": "此附件將在 {{time}} 後自動刪除",
"will_be_deleted_soon": "該附件即將被自動刪除",
"deletion_reason": ",因為該附件未連結在筆記的內容中。為防止被刪除,請將附件連結重新新增至內容中或將附件轉換為筆記。",
"role_and_size": "角色:{{role}},大小:{{size}}",
"role_and_size": "角色:{{role}},大小:{{size}}MIME{{- mimeType}}",
"link_copied": "已複製附件連結到剪貼簿。",
"unrecognized_role": "無法識別的附件角色 '{{role}}'。"
},
@@ -1548,7 +1556,11 @@
"create-child-note": "建立子筆記",
"unhoist": "取消聚焦",
"toggle-sidebar": "切換側邊欄",
"dropping-not-allowed": "不允許移動筆記至此處。"
"dropping-not-allowed": "不允許移動筆記至此處。",
"clone-indicator-tooltip": "此筆記有 {{- count}} 個父級:{{- parents}}",
"clone-indicator-tooltip-single": "此筆記已克隆(新增 1 個父級:{{- parent}}",
"shared-indicator-tooltip": "此筆記已公開分享",
"shared-indicator-tooltip-with-url": "此筆記已公開分享至:{{- url}}"
},
"title_bar_buttons": {
"window-on-top": "保持此視窗置頂"
@@ -1556,7 +1568,12 @@
"note_detail": {
"could_not_find_typewidget": "找不到類型為 '{{type}}' 的 typeWidget",
"printing": "正在列印…",
"printing_pdf": "正在匯出為 PDF…"
"printing_pdf": "正在匯出為 PDF…",
"print_report_title": "列印報告",
"print_report_collection_content_one": "集合中的 {{count}} 篇筆記無法列印,因為它們不被支援或受到保護。",
"print_report_collection_content_other": "",
"print_report_collection_details_button": "查看詳情",
"print_report_collection_details_ignored_notes": "忽略的筆記"
},
"note_title": {
"placeholder": "請輸入筆記標題...",
@@ -1566,7 +1583,8 @@
"note_type_switcher_others": "其他筆記類型",
"note_type_switcher_templates": "模板",
"note_type_switcher_collection": "集合",
"edited_notes": "編輯過的筆記"
"edited_notes": "今天編輯過的筆記",
"promoted_attributes": "升級屬性"
},
"search_result": {
"no_notes_found": "沒有找到符合搜尋條件的筆記。",
@@ -2182,7 +2200,14 @@
"read_only_temporarily_disabled_description": "此筆記目前可編輯,但通常為唯讀狀態。當您切換至其他筆記時,本筆記將立即恢復為唯讀模式。\n\n點擊此處重新啟用唯讀模式。",
"clipped_note_description": "本筆記原始來源為 {{url}}。\n\n點擊此處前往原網頁。",
"execute_script_description": "此筆記為腳本筆記。點擊以執行腳本。",
"execute_sql_description": "此筆記為 SQL 筆記。點擊以執行 SQL 查詢。"
"execute_sql_description": "此筆記為 SQL 筆記。點擊以執行 SQL 查詢。",
"save_status_saved": "已儲存",
"save_status_saving": "正在儲存…",
"save_status_unsaved": "未儲存",
"save_status_error": "儲存失敗",
"save_status_saving_tooltip": "正在儲存更動。",
"save_status_unsaved_tooltip": "仍有更動尚未儲存。它們將在稍後自動儲存。",
"save_status_error_tooltip": "在儲存筆記時發生錯誤。如果可以,請嘗試將筆記內容複製至他處並重新載入應用程式。"
},
"breadcrumb": {
"hoisted_badge": "聚焦",
@@ -2219,5 +2244,15 @@
},
"attributes_panel": {
"title": "筆記屬性"
},
"pdf": {
"attachments_one": "{{count}} 個附件",
"attachments_other": "",
"layers_one": "{{count}} 層",
"layers_other": "",
"pages_one": "共 {{count}} 頁",
"pages_other": "",
"pages_alt": "第 {{pageNumber}} 頁",
"pages_loading": "正在載入…"
}
}

View File

@@ -321,7 +321,6 @@
"download_button": "Завантажити",
"mime": "МІМЕ: ",
"file_size": "Розмір файлу:",
"preview": "Попередній перегляд:",
"preview_not_available": "Попередній перегляд недоступний для цього типу нотатки.",
"diff_on": "Показати різницю",
"diff_off": "Показати вміст",

121
apps/client/src/types-pdfjs.d.ts vendored Normal file
View File

@@ -0,0 +1,121 @@
type HistoryData = {
files: {
fingerprint: string;
page: number;
zoom: string;
scrollLeft: number;
scrollTop: number;
rotation: number;
sidebarView: number;
}[];
};
interface Window {
/**
* By default, pdf.js will try to store information about the opened PDFs such as zoom and scroll position in local storage.
* The Trilium alternative is to use attachments stored at note level.
* This variable represents the direct content used by the pdf.js viewer in its local storage key, but in plain JS object format.
* The variable must be set early at startup, before pdf.js fully initializes.
*/
TRILIUM_VIEW_HISTORY_STORE?: HistoryData;
/**
* If set to true, hides the pdf.js viewer default sidebar containing the outline, page navigation, etc.
* This needs to be set early in the main method.
*/
TRILIUM_HIDE_SIDEBAR?: boolean;
TRILIUM_NOTE_ID: string;
TRILIUM_NTX_ID: string | null | undefined;
}
interface PdfOutlineItem {
title: string;
level: number;
dest: unknown;
id: string;
items: PdfOutlineItem[];
}
interface WithContext {
ntxId: string;
noteId: string | null | undefined;
}
interface PdfDocumentModifiedMessage extends WithContext {
type: "pdfjs-viewer-document-modified";
}
interface PdfDocumentBlobResultMessage extends WithContext {
type: "pdfjs-viewer-blob";
data: Uint8Array<ArrayBufferLike>;
}
interface PdfSaveViewHistoryMessage extends WithContext {
type: "pdfjs-viewer-save-view-history";
data: string;
}
interface PdfViewerTocMessage {
type: "pdfjs-viewer-toc";
data: PdfOutlineItem[];
}
interface PdfViewerActiveHeadingMessage {
type: "pdfjs-viewer-active-heading";
headingId: string;
}
interface PdfViewerPageInfoMessage {
type: "pdfjs-viewer-page-info";
totalPages: number;
currentPage: number;
}
interface PdfViewerCurrentPageMessage {
type: "pdfjs-viewer-current-page";
currentPage: number;
}
interface PdfViewerThumbnailMessage {
type: "pdfjs-viewer-thumbnail";
pageNumber: number;
dataUrl: string;
}
interface PdfAttachment {
filename: string;
size: number;
}
interface PdfViewerAttachmentsMessage {
type: "pdfjs-viewer-attachments";
attachments: PdfAttachment[];
downloadAttachment?: (fileName: string) => void;
}
interface PdfLayer {
id: string;
name: string;
visible: boolean;
}
interface PdfViewerLayersMessage {
type: "pdfjs-viewer-layers";
layers: PdfLayer[];
toggleLayer?: (layerId: string, visible: boolean) => void;
}
type PdfMessageEvent = MessageEvent<
PdfDocumentModifiedMessage
| PdfSaveViewHistoryMessage
| PdfViewerTocMessage
| PdfViewerActiveHeadingMessage
| PdfViewerPageInfoMessage
| PdfViewerCurrentPageMessage
| PdfViewerThumbnailMessage
| PdfViewerAttachmentsMessage
| PdfViewerLayersMessage
| PdfDocumentBlobResultMessage
>;

View File

@@ -1,11 +1,17 @@
import clsx from "clsx";
import { t } from "../../services/i18n";
import options from "../../services/options";
import ActionButton from "../react/ActionButton";
import { useTriliumOptionBool } from "../react/hooks";
import { useState, useCallback } from "preact/hooks";
import { useTriliumEvent } from "../react/hooks";
export default function RightPaneToggle() {
const [ rightPaneVisible, setRightPaneVisible ] = useTriliumOptionBool("rightPaneVisible");
const [ rightPaneVisible, setRightPaneVisible ] = useState(options.is("rightPaneVisible"));
useTriliumEvent("toggleRightPane", useCallback(() => {
setRightPaneVisible(current => !current);
}, []));
return (
<ActionButton
@@ -15,7 +21,7 @@ export default function RightPaneToggle() {
)}
text={t("right_pane.toggle")}
icon="bx bx-sidebar"
onClick={() => setRightPaneVisible(!rightPaneVisible)}
triggerCommand="toggleRightPane"
/>
);
}

View File

@@ -11,7 +11,8 @@ import froca from "../../services/froca";
import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } from "../../services/ws";
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent } from "../react/hooks";
import { allViewTypes, ViewModeMedia, ViewModeProps, ViewTypeOptions } from "./interface";
import ViewModeStorage from "./view_mode_storage";
import ViewModeStorage, { type ViewModeStorageType } from "./view_mode_storage";
interface NoteListProps {
note: FNote | null | undefined;
notePath: string | null | undefined;
@@ -215,7 +216,7 @@ export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOpt
return noteIds;
}
export function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined) {
export function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewModeStorageType | undefined) {
const [ viewConfig, setViewConfig ] = useState<{
config: T | undefined;
storeFn: (data: T) => void;

View File

@@ -1,27 +1,29 @@
import { DateSelectArg, EventChangeArg, EventMountArg, EventSourceFuncArg, LocaleInput, PluginDef } from "@fullcalendar/core/index.js";
import { ViewModeProps } from "../interface";
import Calendar from "./calendar";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import "./index.css";
import { useNoteLabel, useNoteLabelBoolean, useResizeObserver, useSpacedUpdate, useTriliumEvent, useTriliumOption, useTriliumOptionInt } from "../../react/hooks";
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
import { Calendar as FullCalendar } from "@fullcalendar/core";
import { parseStartEndDateFromEvent, parseStartEndTimeFromEvent } from "./utils";
import dialog from "../../../services/dialog";
import { t } from "../../../services/i18n";
import { buildEvents, buildEventsForCalendar } from "./event_builder";
import { changeEvent, newEvent } from "./api";
import froca from "../../../services/froca";
import date_notes from "../../../services/date_notes";
import appContext from "../../../components/app_context";
import { DateSelectArg, EventChangeArg, EventMountArg, EventSourceFuncArg, LocaleInput, PluginDef } from "@fullcalendar/core/index.js";
import { DateClickArg } from "@fullcalendar/interaction";
import FNote from "../../../entities/fnote";
import Button, { ButtonGroup } from "../../react/Button";
import ActionButton from "../../react/ActionButton";
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
import { RefObject } from "preact";
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
import { openCalendarContextMenu } from "./context_menu";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import appContext from "../../../components/app_context";
import FNote from "../../../entities/fnote";
import date_notes from "../../../services/date_notes";
import dialog from "../../../services/dialog";
import froca from "../../../services/froca";
import { t } from "../../../services/i18n";
import { isMobile } from "../../../services/utils";
import ActionButton from "../../react/ActionButton";
import Button, { ButtonGroup } from "../../react/Button";
import { useNoteLabel, useNoteLabelBoolean, useResizeObserver, useSpacedUpdate, useTriliumEvent, useTriliumOption, useTriliumOptionInt } from "../../react/hooks";
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
import { ViewModeProps } from "../interface";
import { changeEvent, newEvent } from "./api";
import Calendar from "./calendar";
import { openCalendarContextMenu } from "./context_menu";
import { buildEvents, buildEventsForCalendar } from "./event_builder";
import { parseStartEndDateFromEvent, parseStartEndTimeFromEvent } from "./utils";
interface CalendarViewData {
@@ -59,7 +61,7 @@ const CALENDAR_VIEWS = [
previousText: t("calendar.month_previous"),
nextText: t("calendar.month_next")
}
]
];
const SUPPORTED_CALENDAR_VIEW_TYPE = CALENDAR_VIEWS.map(v => v.type);
@@ -75,6 +77,7 @@ export const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, (() => Promise<{ de
ru: () => import("@fullcalendar/core/locales/ru"),
ja: () => import("@fullcalendar/core/locales/ja"),
pt: () => import("@fullcalendar/core/locales/pt"),
pl: () => import("@fullcalendar/core/locales/pl"),
"pt_br": () => import("@fullcalendar/core/locales/pt-br"),
uk: () => import("@fullcalendar/core/locales/uk"),
en: null,
@@ -102,9 +105,9 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
const eventBuilder = useMemo(() => {
if (!isCalendarRoot) {
return async () => await buildEvents(noteIds);
} else {
return async (e: EventSourceFuncArg) => await buildEventsForCalendar(note, e);
}
}
return async (e: EventSourceFuncArg) => await buildEventsForCalendar(note, e);
}, [isCalendarRoot, noteIds]);
const plugins = usePlugins(isEditable, isCalendarRoot);
@@ -178,7 +181,7 @@ function CalendarHeader({ calendarRef }: { calendarRef: RefObject<FullCalendar>
<ActionButton icon="bx bx-chevron-right" text={currentViewData?.nextText ?? ""} frame onClick={() => calendarRef.current?.next()} />
</ButtonGroup>
</div>
)
);
}
function usePlugins(isEditable: boolean, isCalendarRoot: boolean) {
@@ -293,7 +296,7 @@ function useEventDisplayCustomization(parentNote: FNote) {
if (promotedAttributes) {
let promotedAttributesHtml = "";
for (const [name, value] of promotedAttributes) {
promotedAttributesHtml = promotedAttributesHtml + /*html*/`\
promotedAttributesHtml = `${promotedAttributesHtml /*html*/}\
<div class="promoted-attribute">
<span class="promoted-attribute-name">${name}</span>: <span class="promoted-attribute-value">${value}</span>
</div>`;

View File

@@ -142,4 +142,10 @@
border: 1px solid var(--main-border-color);
background: var(--more-accented-background-color);
}
.note-list.grid-view .note-path {
margin-left: 0.5em;
vertical-align: middle;
opacity: 0.5;
}
/* #endregion */

View File

@@ -7,7 +7,6 @@ import attribute_renderer from "../../../services/attribute_renderer";
import content_renderer from "../../../services/content_renderer";
import { t } from "../../../services/i18n";
import link from "../../../services/link";
import tree from "../../../services/tree";
import { useImperativeSearchHighlighlighting, useNoteLabel, useNoteLabelBoolean } from "../../react/hooks";
import Icon from "../../react/Icon";
import NoteLink from "../../react/NoteLink";
@@ -103,16 +102,7 @@ function ListNoteCard({ note, parentNote, highlightedTokens, currentLevel, expan
}
function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined }) {
const titleRef = useRef<HTMLSpanElement>(null);
const [ noteTitle, setNoteTitle ] = useState<string>();
const notePath = getNotePath(parentNote, note);
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
useEffect(() => {
tree.getNoteTitle(note.noteId, parentNote.noteId).then(setNoteTitle);
}, [ note ]);
useEffect(() => highlightSearch(titleRef.current), [ noteTitle, highlightedTokens ]);
return (
<div
@@ -123,7 +113,7 @@ function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, pa
>
<h5 className="note-book-header">
<Icon className="note-icon" icon={note.getIcon()} />
<span ref={titleRef} className="note-book-title">{noteTitle}</span>
<NoteLink className="note-book-title" notePath={notePath} noPreview showNotePath={parentNote.type === "search"} highlightedTokens={highlightedTokens} />
<NoteAttributes note={note} />
</h5>
<NoteContent

View File

@@ -4,14 +4,16 @@ import { ViewTypeOptions } from "../collections/interface";
const ATTACHMENT_ROLE = "viewConfig";
export type ViewModeStorageType = ViewTypeOptions | "pdfHistory";
export default class ViewModeStorage<T extends object> {
private note: FNote;
private attachmentName: string;
constructor(note: FNote, viewType: ViewTypeOptions) {
constructor(note: FNote, viewType: ViewModeStorageType) {
this.note = note;
this.attachmentName = viewType + ".json";
this.attachmentName = `${viewType}.json`;
}
async store(data: T) {

View File

@@ -1,10 +1,11 @@
import FlexContainer from "./flex_container.js";
import appContext, { type CommandData, type CommandListenerData, type EventData, type EventNames, type NoteSwitchedContext } from "../../components/app_context.js";
import type BasicWidget from "../basic_widget.js";
import Component from "../../components/component.js";
import NoteContext from "../../components/note_context.js";
import splitService from "../../services/resizer.js";
import { isMobile } from "../../services/utils.js";
import NoteContext from "../../components/note_context.js";
import type BasicWidget from "../basic_widget.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import FlexContainer from "./flex_container.js";
interface SplitNoteWidget extends BasicWidget {
hasBeenAlreadyShown?: boolean;
@@ -74,7 +75,7 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
const subContexts = activeContext.getSubContexts();
let noteContext: NoteContext | undefined = undefined;
let noteContext: NoteContext | undefined;
if (isMobile() && subContexts.length > 1) {
noteContext = subContexts.find(s => s.ntxId !== ntxId);
}
@@ -201,6 +202,11 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
async refresh() {
this.toggleExt(true);
// Mark the active note context.
for (const child of this.children as NoteContextAwareWidget[]) {
child.$widget.toggleClass("active", !!child.noteContext?.isActive());
}
}
toggleInt(show: boolean) {} // not needed
@@ -239,16 +245,16 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
widget.hasBeenAlreadyShown = true;
return [widget.handleEvent("noteSwitched", noteSwitchedContext), this.refreshNotShown(noteSwitchedContext)];
} else {
return Promise.resolve();
}
return Promise.resolve();
}
if (name === "activeContextChanged") {
return this.refreshNotShown(data as EventData<"activeContextChanged">);
} else {
return super.handleEventInChildren(name, data);
}
return super.handleEventInChildren(name, data);
}
refreshNotShown(data: NoteSwitchedContext | EventData<"activeContextChanged">) {

View File

@@ -88,6 +88,7 @@ export default function PopupEditor() {
onHidden={() => setShown(false)}
keepInDom // needed for faster loading
noFocus // automatic focus breaks block popup
stackable
>
{!isNewLayout && <ReadOnlyNoteInfoBar />}
<PromotedAttributes />

View File

@@ -14,7 +14,7 @@ body.mobile .revisions-dialog {
flex-grow: 1;
width: 100%;
}
.modal-body {
height: fit-content !important;
flex-direction: column;
@@ -24,7 +24,7 @@ body.mobile .revisions-dialog {
.modal-footer {
font-size: 0.9em;
}
.revision-list {
height: fit-content !important;
max-height: 20vh;
@@ -32,7 +32,7 @@ body.mobile .revisions-dialog {
padding: 0 1em;
flex-shrink: 0;
}
.modal-body > .revision-content-wrapper {
flex-grow: 1;
max-width: unset !important;
@@ -40,24 +40,68 @@ body.mobile .revisions-dialog {
margin: 0;
display: block !important;
}
.modal-body > .revision-content-wrapper > div:first-of-type {
flex-direction: column;
}
.revision-title {
font-size: 1rem;
}
.revision-title-buttons {
text-align: center;
display: flex;
gap: 0.25em;
flex-wrap: wrap;
}
.revision-content {
padding: 0.5em;
height: fit-content;
}
}
}
.revisions-dialog {
.revision-title-buttons {
flex-shrink: 0;
}
.revision-list {
flex-shrink: 0;
}
.revision-content.type-file {
display: flex;
min-width: 0;
min-height: 0;
flex-grow: 1;
.file-preview-table {
th,
td {
padding: 0.25em 0;
}
}
.revision-file-preview {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
flex-grow: 1;
}
.revision-file-preview-content {
flex-grow: 1;
min-height: 0;
display: flex;
flex-direction: column;
> * {
height: 100%;
}
}
}
}

View File

@@ -1,26 +1,31 @@
import type { RevisionPojo, RevisionItem } from "@triliumnext/commons";
import "./revisions.css";
import type { RevisionItem, RevisionPojo } from "@triliumnext/commons";
import clsx from "clsx";
import { diffWords } from "diff";
import type { CSSProperties } from "preact/compat";
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
import appContext from "../../components/app_context";
import FNote from "../../entities/fnote";
import dialog from "../../services/dialog";
import froca from "../../services/froca";
import { t } from "../../services/i18n";
import { renderMathInElement } from "../../services/math";
import open from "../../services/open";
import options from "../../services/options";
import protected_session_holder from "../../services/protected_session_holder";
import server from "../../services/server";
import toast from "../../services/toast";
import Button from "../react/Button";
import FormToggle from "../react/FormToggle";
import Modal from "../react/Modal";
import FormList, { FormListItem } from "../react/FormList";
import utils from "../../services/utils";
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
import protected_session_holder from "../../services/protected_session_holder";
import { renderMathInElement } from "../../services/math";
import type { CSSProperties } from "preact/compat";
import open from "../../services/open";
import ActionButton from "../react/ActionButton";
import options from "../../services/options";
import Button from "../react/Button";
import FormList, { FormListItem } from "../react/FormList";
import FormToggle from "../react/FormToggle";
import { useTriliumEvent } from "../react/hooks";
import { diffWords } from "diff";
import "./revisions.css";
import Modal from "../react/Modal";
import { RawHtmlBlock } from "../react/RawHtml";
import PdfViewer from "../type_widgets/file/PdfViewer";
export default function RevisionsDialog() {
const [ note, setNote ] = useState<FNote>();
@@ -47,7 +52,7 @@ export default function RevisionsDialog() {
setRevisions(undefined);
setNoteContent(undefined);
}
}, [ note?.noteId, refreshCounter ]);
}, [ note, refreshCounter ]);
if (revisions?.length && !currentRevision) {
setCurrentRevision(revisions[0]);
@@ -102,38 +107,38 @@ export default function RevisionsDialog() {
setRevisions(undefined);
}}
show={shown}
>
<RevisionsList
revisions={revisions ?? []}
onSelect={(revisionId) => {
const correspondingRevision = (revisions ?? []).find((r) => r.revisionId === revisionId);
if (correspondingRevision) {
setCurrentRevision(correspondingRevision);
}
}}
currentRevision={currentRevision}
/>
>
<RevisionsList
revisions={revisions ?? []}
onSelect={(revisionId) => {
const correspondingRevision = (revisions ?? []).find((r) => r.revisionId === revisionId);
if (correspondingRevision) {
setCurrentRevision(correspondingRevision);
}
}}
currentRevision={currentRevision}
/>
<div className="revision-content-wrapper" style={{
flexGrow: "1",
marginInlineStart: "20px",
display: "flex",
flexDirection: "column",
maxWidth: "calc(100% - 150px)",
minWidth: 0
}}>
<RevisionPreview
noteContent={noteContent}
revisionItem={currentRevision}
showDiff={showDiff}
setShown={setShown}
onRevisionDeleted={() => {
setRefreshCounter(c => c + 1);
setCurrentRevision(undefined);
}} />
</div>
<div className="revision-content-wrapper" style={{
flexGrow: "1",
marginInlineStart: "20px",
display: "flex",
flexDirection: "column",
maxWidth: "calc(100% - 150px)",
minWidth: 0
}}>
<RevisionPreview
noteContent={noteContent}
revisionItem={currentRevision}
showDiff={showDiff}
setShown={setShown}
onRevisionDeleted={() => {
setRefreshCounter(c => c + 1);
setCurrentRevision(undefined);
}} />
</div>
</Modal>
)
);
}
function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: RevisionItem[], onSelect: (val: string) => void, currentRevision?: RevisionItem }) {
@@ -141,6 +146,7 @@ function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: Re
<FormList onSelect={onSelect} fullHeight wrapperClassName="revision-list">
{revisions.map((item) =>
<FormListItem
key={item.revisionId}
value={item.revisionId}
active={currentRevision && item.revisionId === currentRevision.revisionId}
>
@@ -202,14 +208,17 @@ function RevisionPreview({noteContent, revisionItem, showDiff, setShown, onRevis
text={t("revisions.download_button")}
onClick={() => {
if (revisionItem.revisionId) {
open.downloadRevision(revisionItem.noteId, revisionItem.revisionId)}
}
open.downloadRevision(revisionItem.noteId, revisionItem.revisionId);}
}
}/>
</>
}
</div>)}
</div>
<div className="revision-content use-tn-links selectable-text" style={{ overflow: "auto", wordBreak: "break-word" }}>
<div
className={clsx("revision-content use-tn-links selectable-text", `type-${revisionItem?.type}`)}
style={{ overflow: "auto", wordBreak: "break-word" }}
>
<RevisionContent noteContent={noteContent} revisionItem={revisionItem} fullRevision={fullRevision} showDiff={showDiff}/>
</div>
</>
@@ -230,16 +239,16 @@ const CODE_STYLE: CSSProperties = {
function RevisionContent({ noteContent, revisionItem, fullRevision, showDiff }: { noteContent?:string, revisionItem?: RevisionItem, fullRevision?: RevisionPojo, showDiff: boolean}) {
const content = fullRevision?.content;
if (!revisionItem || !content) {
if (!revisionItem || !fullRevision) {
return <></>;
}
if (showDiff) {
return <RevisionContentDiff noteContent={noteContent} itemContent={content} itemType={revisionItem.type}/>
return <RevisionContentDiff noteContent={noteContent} itemContent={content} itemType={revisionItem.type}/>;
}
switch (revisionItem.type) {
case "text":
return <RevisionContentText content={content} />
return <RevisionContentText content={content} />;
case "code":
return <pre style={CODE_STYLE}>{content}</pre>;
case "image":
@@ -256,28 +265,11 @@ function RevisionContent({ noteContent, revisionItem, fullRevision, showDiff }:
// as a URL to be used in a note. Instead, if they copy and paste it into a note, it will be uploaded as a new note
return <img
src={`data:${fullRevision.mime};base64,${fullRevision.content}`}
style={IMAGE_STYLE} />
style={IMAGE_STYLE} />;
}
}
case "file":
return <table cellPadding="10">
<tr>
<th>{t("revisions.mime")}</th>
<td>{revisionItem.mime}</td>
</tr>
<tr>
<th>{t("revisions.file_size")}</th>
<td>{revisionItem.contentLength && utils.formatSize(revisionItem.contentLength)}</td>
</tr>
{fullRevision.content &&
<tr>
<td colspan={2}>
<strong>{t("revisions.preview")}</strong>
<pre className="file-preview-content" style={CODE_STYLE}>{fullRevision.content}</pre>
</td>
</tr>
}
</table>;
return <FilePreview fullRevision={fullRevision} revisionItem={revisionItem} />;
case "canvas":
case "mindMap":
case "mermaid": {
@@ -287,7 +279,7 @@ function RevisionContent({ noteContent, revisionItem, fullRevision, showDiff }:
style={IMAGE_STYLE} />;
}
default:
return <>{t("revisions.preview_not_available")}</>
return <>{t("revisions.preview_not_available")}</>;
}
}
@@ -298,7 +290,7 @@ function RevisionContentText({ content }: { content: string | Buffer<ArrayBuffer
renderMathInElement(contentRef.current, { trust: true });
}
}, [content]);
return <div ref={contentRef} className="ck-content" dangerouslySetInnerHTML={{ __html: content as string }}></div>
return <RawHtmlBlock containerRef={contentRef} className="ck-content" html={content as string} />;
}
function RevisionContentDiff({ noteContent, itemContent, itemType }: {
@@ -330,9 +322,9 @@ function RevisionContentDiff({ noteContent, itemContent, itemType }: {
return `<span class="revision-diff-added">${utils.escapeHtml(part.value)}</span>`;
} else if (part.removed) {
return `<span class="revision-diff-removed">${utils.escapeHtml(part.value)}</span>`;
} else {
return utils.escapeHtml(part.value);
}
return utils.escapeHtml(part.value);
}).join("");
if (contentRef.current) {
@@ -340,7 +332,7 @@ function RevisionContentDiff({ noteContent, itemContent, itemType }: {
}
}, [noteContent, itemContent, itemType]);
return <div ref={contentRef} className="ck-content" style={{ whiteSpace: "pre-wrap" }}></div>;
return <div ref={contentRef} className="ck-content" style={{ whiteSpace: "pre-wrap" }} />;
}
function RevisionFooter({ note }: { note?: FNote }) {
@@ -348,7 +340,7 @@ function RevisionFooter({ note }: { note?: FNote }) {
return <></>;
}
let revisionsNumberLimit: number | string = parseInt(note?.getLabelValue("versioningLimit") ?? "");
let revisionsNumberLimit: number | string = parseInt(note?.getLabelValue("versioningLimit") ?? "", 10);
if (!Number.isInteger(revisionsNumberLimit)) {
revisionsNumberLimit = options.getInt("revisionSnapshotNumberLimit") ?? 0;
}
@@ -370,10 +362,67 @@ function RevisionFooter({ note }: { note?: FNote }) {
</>;
}
function FilePreview({ revisionItem, fullRevision }: { revisionItem: RevisionItem, fullRevision: RevisionPojo }) {
return (
<div className="revision-file-preview">
<table className="file-preview-table">
<tbody>
<tr>
<th>{t("revisions.mime")}</th>
<td>{revisionItem.mime}</td>
</tr>
<tr>
<th>{t("revisions.file_size")}</th>
<td>{revisionItem.contentLength && utils.formatSize(revisionItem.contentLength)}</td>
</tr>
</tbody>
</table>
<div class="revision-file-preview-content">
<FilePreviewInner revisionItem={revisionItem} fullRevision={fullRevision} />
</div>
</div>
);
}
function FilePreviewInner({ revisionItem, fullRevision }: { revisionItem: RevisionItem, fullRevision: RevisionPojo }) {
if (revisionItem.mime.startsWith("audio/")) {
return (
<audio
src={`api/revisions/${revisionItem.revisionId}/download`}
controls
/>
);
}
if (revisionItem.mime.startsWith("video/")) {
return (
<video
src={`api/revisions/${revisionItem.revisionId}/download`}
controls
/>
);
}
if (revisionItem.mime === "application/pdf") {
return (
<PdfViewer
pdfUrl={`../../api/revisions/${revisionItem.revisionId}/download`}
/>
);
}
if (fullRevision.content) {
return <pre className="file-preview-content" style={CODE_STYLE}>{fullRevision.content}</pre>;
}
return t("revisions.preview_not_available");
}
async function getNote(noteId?: string | null) {
if (noteId) {
return await froca.getNote(noteId);
} else {
return appContext.tabManager.getActiveContextNote();
}
return appContext.tabManager.getActiveContextNote();
}

View File

@@ -2,6 +2,11 @@
contain: none;
}
@keyframes fadeOut {
from { opacity: var(--default-opacity); }
to { opacity: 0; }
}
.note-badges {
display: flex;
gap: 5px;
@@ -16,6 +21,23 @@
&.share-badge {--color: var(--badge-share-background-color);}
&.clipped-note-badge {--color: var(--badge-clipped-note-background-color);}
&.execute-badge {--color: var(--badge-execute-background-color);}
&.save-status-badge {
--default-opacity: 0.4;
opacity: var(--default-opacity);
transition: opacity 250ms ease-in;
color: var(--main-text-color);
&.error {
color: var(--dropdown-item-icon-destructive-color);
opacity: 1;
}
&.saved {
animation: fadeOut 250ms ease-in 5s forwards;
pointer-events: none;
}
}
min-width: 0;
.text {

View File

@@ -1,17 +1,20 @@
import "./NoteBadges.css";
import { clsx } from "clsx";
import { copyTextWithToast } from "../../services/clipboard_ext";
import { t } from "../../services/i18n";
import { goToLinkExt } from "../../services/link";
import { Badge, BadgeWithDropdown } from "../react/Badge";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
import { useGetContextData, useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
import { useShareState } from "../ribbon/BasicPropertiesTab";
import { useShareInfo } from "../shared_info";
export default function NoteBadges() {
return (
<div className="note-badges">
<SaveStatusBadge />
<ReadOnlyBadge />
<ShareBadge />
<ClippedNoteBadge />
@@ -105,3 +108,42 @@ function ExecuteBadge() {
/>
);
}
export function SaveStatusBadge() {
const saveState = useGetContextData("saveState");
if (!saveState) return;
const stateConfig = {
saved: {
icon: "bx bx-check",
title: t("breadcrumb_badges.save_status_saved"),
tooltip: undefined
},
saving: {
icon: "bx bx-loader bx-spin",
title: t("breadcrumb_badges.save_status_saving"),
tooltip: t("breadcrumb_badges.save_status_saving_tooltip")
},
unsaved: {
icon: "bx bx-pencil",
title: t("breadcrumb_badges.save_status_unsaved"),
tooltip: t("breadcrumb_badges.save_status_unsaved_tooltip")
},
error: {
icon: "bx bxs-error",
title: t("breadcrumb_badges.save_status_error"),
tooltip: t("breadcrumb_badges.save_status_error_tooltip")
}
};
const { icon, title, tooltip } = stateConfig[saveState.state];
return (
<Badge
className={clsx("save-status-badge", saveState.state)}
icon={icon}
text={title}
tooltip={tooltip}
/>
);
}

View File

@@ -5,7 +5,7 @@ import { Dropdown as BootstrapDropdown } from "bootstrap";
import clsx from "clsx";
import { type ComponentChildren, RefObject } from "preact";
import { createPortal } from "preact/compat";
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { CommandNames } from "../../components/app_context";
import NoteContext from "../../components/note_context";
@@ -338,15 +338,19 @@ interface AttributesProps extends StatusBarContext {
function AttributesButton({ note, attributesShown, setAttributesShown }: AttributesProps) {
const [ count, setCount ] = useState(note.attributes.length);
const refreshCount = useCallback((note: FNote) => {
return note.getAttributes().filter(a => !a.isAutoLink).length;
}, []);
// React to note changes.
useEffect(() => {
setCount(note.getAttributes().filter(a => !a.isAutoLink).length);
}, [ note ]);
setCount(refreshCount(note));
}, [ note, refreshCount ]);
// React to changes in count.
useTriliumEvent("entitiesReloaded", (({loadResults}) => {
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
setCount(note.attributes.length);
setCount(refreshCount(note));
}
}));

View File

@@ -624,6 +624,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const $span = $(node.span);
$span.find(".tree-item-button").remove();
$span.find(".note-indicator-icon").remove();
const isHoistedNote = activeNoteContext && activeNoteContext.hoistedNoteId === note.noteId && note.noteId !== "root";
@@ -664,6 +665,34 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
$span.append($unhoistButton);
}
// Add clone indicator with tooltip if note has multiple parents
const parentNotes = note.getParentNotes();
const realParents = parentNotes.filter(
(parent) => !["_share", "_lbBookmarks"].includes(parent.noteId) && parent.type !== "search"
);
if (realParents.length > 1) {
const parentTitles = realParents.map((p) => p.title).join(", ");
const tooltipText = realParents.length === 2
? t("note_tree.clone-indicator-tooltip-single", { parent: realParents[1].title })
: t("note_tree.clone-indicator-tooltip", { count: realParents.length, parents: parentTitles });
const $cloneIndicator = $(`<span class="note-indicator-icon clone-indicator"></span>`);
$cloneIndicator.attr("title", tooltipText);
$span.find(".fancytree-title").append($cloneIndicator);
}
// Add shared indicator with tooltip if note is shared
if (note.isShared()) {
const shareId = note.getOwnedLabelValue("shareAlias") || note.noteId;
const shareUrl = `${location.origin}${location.pathname}share/${shareId}`;
const tooltipText = t("note_tree.shared-indicator-tooltip-with-url", { url: shareUrl });
const $sharedIndicator = $(`<span class="note-indicator-icon shared-indicator"></span>`);
$sharedIndicator.attr("title", tooltipText);
$span.find(".fancytree-title").append($sharedIndicator);
}
},
// this is done to automatically lazy load all expanded notes after tree load
loadChildren: (event, data) => {

View File

@@ -4,7 +4,8 @@
*/
import { NoteType } from "@triliumnext/commons";
import { VNode, type JSX } from "preact";
import { type JSX, VNode } from "preact";
import { TypeWidgetProps } from "./type_widgets/type_widget";
/**
@@ -13,7 +14,7 @@ import { TypeWidgetProps } from "./type_widgets/type_widget";
*/
export type ExtendedNoteType = Exclude<NoteType, "launcher" | "text" | "code"> | "empty" | "readOnlyCode" | "readOnlyText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession" | "aiChat";
export type TypeWidget = ((props: TypeWidgetProps) => VNode | JSX.Element);
export type TypeWidget = ((props: TypeWidgetProps) => VNode | JSX.Element | undefined);
type NoteTypeView = () => (Promise<{ default: TypeWidget } | TypeWidget> | TypeWidget);
interface NoteTypeMapping {

View File

@@ -64,7 +64,8 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
this.$widget.addClass(utils.getMimeTypeClass(note.mime));
this.$widget.addClass(`view-mode-${this.noteContext?.viewScope?.viewMode ?? "default"}`);
this.$widget.addClass(note.getColorClass());
this.$widget.toggleClass(["bgfx", "options"], note.isOptions());
this.$widget.toggleClass("options", note.isOptions());
this.$widget.toggleClass("bgfx", this.#hasBackgroundEffects(note));
this.$widget.toggleClass("protected", note.isProtected);
const noteLanguage = note?.getLabelValue("language");
@@ -88,6 +89,22 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
return !!note?.isLabelTruthy("fullContentWidth");
}
#hasBackgroundEffects(note: FNote): boolean {
const MIME_TYPES_WITH_BACKGROUND_EFFECTS = [
"application/pdf"
]
if (note.isOptions()) {
return true;
}
if (note.type === "file" && MIME_TYPES_WITH_BACKGROUND_EFFECTS.includes(note.mime)) {
return true;
}
return false;
}
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
// listening on changes of note.type and CSS class
const LABELS_CAUSING_REFRESH = ["cssClass", "language", "viewType", "color"];

View File

@@ -8,7 +8,7 @@ import { MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayou
import appContext, { EventData, EventNames } from "../../components/app_context";
import Component from "../../components/component";
import NoteContext from "../../components/note_context";
import NoteContext, { NoteContextDataMap } from "../../components/note_context";
import FBlob from "../../entities/fblob";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
@@ -19,7 +19,7 @@ import options, { type OptionValue } from "../../services/options";
import protected_session_holder from "../../services/protected_session_holder";
import server from "../../services/server";
import shortcuts, { Handler, removeIndividualBinding } from "../../services/shortcuts";
import SpacedUpdate from "../../services/spaced_update";
import SpacedUpdate, { type StateCallback } from "../../services/spaced_update";
import toast, { ToastOptions } from "../../services/toast";
import tree from "../../services/tree";
import utils, { escapeRegExp, getErrorMessage, randomString, reloadFrontendApp } from "../../services/utils";
@@ -63,22 +63,29 @@ export function useTriliumEvents<T extends EventNames>(eventNames: T[], handler:
useDebugValue(() => eventNames.join(", "));
}
export function useSpacedUpdate(callback: () => void | Promise<void>, interval = 1000) {
export function useSpacedUpdate(callback: () => void | Promise<void>, interval = 1000, stateCallback?: StateCallback) {
const callbackRef = useRef(callback);
const stateCallbackRef = useRef(stateCallback);
const spacedUpdateRef = useRef<SpacedUpdate>(new SpacedUpdate(
() => callbackRef.current(),
interval
interval,
(state) => stateCallbackRef.current?.(state)
));
// Update callback ref when it changes
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
}, [ callback ]);
// Update state callback when it changes.
useEffect(() => {
stateCallbackRef.current = stateCallback;
}, [ stateCallback ]);
// Update interval if it changes
useEffect(() => {
spacedUpdateRef.current?.setUpdateInterval(interval);
}, [interval]);
}, [ interval ]);
return spacedUpdateRef.current;
}
@@ -121,7 +128,12 @@ export function useEditorSpacedUpdate({ note, noteType, noteContext, getData, on
dataSaved?.(data);
};
}, [ note, getData, dataSaved, noteType, parentComponent ]);
const spacedUpdate = useSpacedUpdate(callback);
const stateCallback = useCallback<StateCallback>((state) => {
noteContext?.setContextData("saveState", {
state
});
}, [ noteContext ]);
const spacedUpdate = useSpacedUpdate(callback, updateInterval, stateCallback);
// React to note/blob changes.
useEffect(() => {
@@ -158,6 +170,73 @@ export function useEditorSpacedUpdate({ note, noteType, noteContext, getData, on
return spacedUpdate;
}
export function useBlobEditorSpacedUpdate({ note, noteType, noteContext, getData, onContentChange, dataSaved, updateInterval, replaceWithoutRevision }: {
noteType: NoteType;
note: FNote,
noteContext: NoteContext | null | undefined,
getData: () => Promise<Blob | undefined> | Blob | undefined,
onContentChange: (newBlob: FBlob) => void,
dataSaved?: (savedData: Blob) => void,
updateInterval?: number;
/** If set to true, then the blob is replaced directly without saving a revision before. */
replaceWithoutRevision?: boolean;
}) {
const parentComponent = useContext(ParentComponent);
const blob = useNoteBlob(note, parentComponent?.componentId);
const callback = useMemo(() => {
return async () => {
const data = await getData();
// for read only notes
if (data === undefined || note.type !== noteType) return;
protected_session_holder.touchProtectedSessionIfNecessary(note);
await server.upload(`notes/${note.noteId}/file?replace=${replaceWithoutRevision ? "1" : "0"}`, new File([ data ], note.title, { type: note.mime }), parentComponent?.componentId);
dataSaved?.(data);
};
}, [ note, getData, dataSaved, noteType, parentComponent, replaceWithoutRevision ]);
const stateCallback = useCallback<StateCallback>((state) => {
noteContext?.setContextData("saveState", {
state
});
}, [ noteContext ]);
const spacedUpdate = useSpacedUpdate(callback, updateInterval, stateCallback);
// React to note/blob changes.
useEffect(() => {
if (!blob) return;
spacedUpdate.allowUpdateWithoutChange(() => onContentChange(blob));
}, [ blob ]);
// React to update interval changes.
useEffect(() => {
if (!updateInterval) return;
spacedUpdate.setUpdateInterval(updateInterval);
}, [ updateInterval ]);
// Save if needed upon switching tabs.
useTriliumEvent("beforeNoteSwitch", async ({ noteContext: eventNoteContext }) => {
if (eventNoteContext.ntxId !== noteContext?.ntxId) return;
await spacedUpdate.updateNowIfNecessary();
});
// Save if needed upon tab closing.
useTriliumEvent("beforeNoteContextRemove", async ({ ntxIds }) => {
if (!noteContext?.ntxId || !ntxIds.includes(noteContext.ntxId)) return;
await spacedUpdate.updateNowIfNecessary();
});
// Save if needed upon window/browser closing.
useEffect(() => {
const listener = () => spacedUpdate.isAllSavedAndTriggerUpdate();
appContext.addBeforeUnloadListener(listener);
return () => appContext.removeBeforeUnloadListener(listener);
}, []);
return spacedUpdate;
}
export function useNoteSavedData(noteId: string | undefined) {
return useSyncExternalStore(
(cb) => noteId ? noteSavedDataStore.subscribe(noteId, cb) : () => {},
@@ -634,7 +713,8 @@ export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, {
const ref = useRef<HTMLDivElement>(null);
const parentComponent = useContext(ParentComponent);
// Render the widget once.
// Render the widget once - note that noteContext is intentionally NOT a dependency
// to prevent creating new widget instances on every note switch.
const [ widget, renderedWidget ] = useMemo(() => {
const widget = widgetFactory();
@@ -642,14 +722,21 @@ export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, {
parentComponent.child(widget);
}
if (noteContext && widget instanceof NoteContextAwareWidget) {
widget.setNoteContextEvent({ noteContext });
}
const renderedWidget = widget.render();
return [ widget, renderedWidget ];
}, [ noteContext, parentComponent ]); // eslint-disable-line react-hooks/exhaustive-deps
// widgetFactory() is intentionally left out
}, [ parentComponent ]); // eslint-disable-line react-hooks/exhaustive-deps
// widgetFactory() and noteContext are intentionally left out - widget should be created once
// and updated via activeContextChangedEvent when noteContext changes.
// Cleanup: remove widget from parent's children when unmounted
useEffect(() => {
return () => {
if (parentComponent) {
parentComponent.removeChild(widget);
}
widget.cleanup();
};
}, [ parentComponent, widget ]);
// Attach the widget to the parent.
useEffect(() => {
@@ -660,10 +747,17 @@ export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, {
}
}, [ renderedWidget ]);
// Inject the note context.
// Inject the note context - this updates the existing widget without recreating it.
// We check if the context actually changed to avoid double refresh when the event system
// also delivers activeContextChanged to the widget through component tree propagation.
useEffect(() => {
if (noteContext && widget instanceof NoteContextAwareWidget) {
widget.activeContextChangedEvent({ noteContext });
// Only trigger refresh if the context actually changed.
// The event system may have already updated the widget, in which case
// widget.noteContext will already equal noteContext.
if (widget.noteContext !== noteContext) {
widget.activeContextChangedEvent({ noteContext });
}
}
}, [ noteContext, widget ]);
@@ -1192,3 +1286,92 @@ export function useContentElement(noteContext: NoteContext | null | undefined) {
return contentElement;
}
/**
* Set context data on the current note context.
* This allows type widgets to publish data (e.g., table of contents, PDF pages)
* that can be consumed by sidebar/toolbar components.
*
* Data is automatically cleared when navigating to a different note.
*
* @param key - Unique identifier for the data type (e.g., "toc", "pdfPages")
* @param value - The data to publish
*
* @example
* // In a PDF viewer widget:
* const { noteContext } = useActiveNoteContext();
* useSetContextData(noteContext, "pdfPages", pages);
*/
export function useSetContextData<K extends keyof NoteContextDataMap>(
noteContext: NoteContext | null | undefined,
key: K,
value: NoteContextDataMap[K] | undefined
) {
useEffect(() => {
if (!noteContext) return;
if (value !== undefined) {
noteContext.setContextData(key, value);
} else {
noteContext.clearContextData(key);
}
return () => {
noteContext.clearContextData(key);
};
}, [noteContext, key, value]);
}
/**
* Get context data from the active note context.
* This is typically used in sidebar/toolbar components that need to display
* data published by type widgets.
*
* The component will automatically re-render when the data changes.
*
* @param key - The data key to retrieve (e.g., "toc", "pdfPages")
* @returns The current data, or undefined if not available
*
* @example
* // In a Table of Contents sidebar widget:
* function TableOfContents() {
* const headings = useGetContextData<Heading[]>("toc");
* if (!headings) return <div>No headings available</div>;
* return <ul>{headings.map(h => <li>{h.text}</li>)}</ul>;
* }
*/
export function useGetContextData<K extends keyof NoteContextDataMap>(key: K): NoteContextDataMap[K] | undefined {
const { noteContext } = useActiveNoteContext();
return useGetContextDataFrom(noteContext, key);
}
/**
* Get context data from a specific note context (not necessarily the active one).
*
* @param noteContext - The specific note context to get data from
* @param key - The data key to retrieve
* @returns The current data, or undefined if not available
*/
export function useGetContextDataFrom<K extends keyof NoteContextDataMap>(
noteContext: NoteContext | null | undefined,
key: K
): NoteContextDataMap[K] | undefined {
const [data, setData] = useState<NoteContextDataMap[K] | undefined>(() =>
noteContext?.getContextData(key)
);
// Update initial value when noteContext changes
useEffect(() => {
setData(noteContext?.getContextData(key));
}, [noteContext, key]);
// Subscribe to changes via Trilium event system
useTriliumEvent("contextDataChanged", ({ noteContext: eventNoteContext, key: changedKey, value }) => {
if (eventNoteContext === noteContext && changedKey === key) {
setData(value as NoteContextDataMap[K]);
}
});
return data;
}

View File

@@ -1,3 +1,5 @@
import { useContext } from "preact/hooks";
import FNote from "../../entities/fnote";
import { t } from "../../services/i18n";
import { downloadFileNote, openNoteExternally } from "../../services/open";
@@ -8,11 +10,14 @@ import { formatSize } from "../../services/utils";
import Button from "../react/Button";
import { FormFileUploadButton } from "../react/FormFileUpload";
import { useNoteBlob, useNoteLabel } from "../react/hooks";
import { ParentComponent } from "../react/react_utils";
import { TabContext } from "./ribbon-interface";
export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
export default function FilePropertiesTab({ note, ntxId }: Pick<TabContext, "note" | "ntxId">) {
const [ originalFileName ] = useNoteLabel(note, "originalFileName");
const canAccessProtectedNote = !note?.isProtected || protected_session_holder.isProtectedSessionAvailable();
const blob = useNoteBlob(note);
const parentComponent = useContext(ParentComponent);
return (
<div className="file-properties-widget">
@@ -40,7 +45,7 @@ export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
text={t("file_properties.download")}
primary
disabled={!canAccessProtectedNote}
onClick={() => downloadFileNote(note.noteId)}
onClick={() => downloadFileNote(note, parentComponent, ntxId)}
/>
<Button

View File

@@ -44,7 +44,7 @@ export default function ImagePropertiesTab({ note, ntxId }: TabContext) {
text={t("image_properties.download")}
icon="bx bx-download"
primary
onClick={() => downloadFileNote(note.noteId)}
onClick={() => downloadFileNote(note, parentComponent, ntxId)}
/>
<Button

View File

@@ -135,13 +135,13 @@ function OpenExternallyButton({ note, noteMime }: NoteActionsCustomInnerProps) {
);
}
function DownloadFileButton({ note }: NoteActionsCustomInnerProps) {
function DownloadFileButton({ note, parentComponent, ntxId }: NoteActionsCustomInnerProps) {
return (
<ActionButton
icon="bx bx-download"
text={t("file_properties.download")}
disabled={!note.isContentAvailable()}
onClick={() => downloadFileNote(note.noteId)}
onClick={() => downloadFileNote(note, parentComponent, ntxId)}
/>
);
}

View File

@@ -3,7 +3,7 @@ import "./RightPanelContainer.css";
import Split from "@triliumnext/split.js";
import { VNode } from "preact";
import { useEffect, useRef } from "preact/hooks";
import { useState, useEffect, useRef, useCallback } from "preact/hooks";
import appContext from "../../components/app_context";
import { WidgetsByParent } from "../../services/bundle";
@@ -11,10 +11,13 @@ import { t } from "../../services/i18n";
import options from "../../services/options";
import { DEFAULT_GUTTER_SIZE } from "../../services/resizer";
import Button from "../react/Button";
import { useActiveNoteContext, useLegacyWidget, useNoteProperty, useTriliumEvent, useTriliumOptionBool, useTriliumOptionJson } from "../react/hooks";
import { useActiveNoteContext, useLegacyWidget, useNoteProperty, useTriliumEvent, useTriliumOptionJson } from "../react/hooks";
import Icon from "../react/Icon";
import LegacyRightPanelWidget from "../right_panel_widget";
import HighlightsList from "./HighlightsList";
import PdfAttachments from "./pdf/PdfAttachments";
import PdfLayers from "./pdf/PdfLayers";
import PdfPages from "./pdf/PdfPages";
import RightPanelWidget from "./RightPanelWidget";
import TableOfContents from "./TableOfContents";
@@ -27,12 +30,16 @@ interface RightPanelWidgetDefinition {
}
export default function RightPanelContainer({ widgetsByParent }: { widgetsByParent: WidgetsByParent }) {
const [ rightPaneVisible, setRightPaneVisible ] = useTriliumOptionBool("rightPaneVisible");
const [ rightPaneVisible, setRightPaneVisible ] = useState(options.is("rightPaneVisible"));
const items = useItems(rightPaneVisible, widgetsByParent);
useSplit(rightPaneVisible);
useTriliumEvent("toggleRightPane", () => {
setRightPaneVisible(!rightPaneVisible);
});
useTriliumEvent("toggleRightPane", useCallback(() => {
setRightPaneVisible(current => {
const newValue = !current;
options.save("rightPaneVisible", newValue.toString());
return newValue;
});
}, []));
return (
<div id="right-pane">
@@ -45,7 +52,7 @@ export default function RightPanelContainer({ widgetsByParent }: { widgetsByPare
{t("right_pane.empty_message")}
<Button
text={t("right_pane.empty_button")}
onClick={() => setRightPaneVisible(!rightPaneVisible)}
triggerCommand="toggleRightPane"
/>
</div>
)
@@ -57,13 +64,27 @@ export default function RightPanelContainer({ widgetsByParent }: { widgetsByPare
function useItems(rightPaneVisible: boolean, widgetsByParent: WidgetsByParent) {
const { note } = useActiveNoteContext();
const noteType = useNoteProperty(note, "type");
const noteMime = useNoteProperty(note, "mime");
const [ highlightsList ] = useTriliumOptionJson<string[]>("highlightsList");
const isPdf = noteType === "file" && noteMime === "application/pdf";
if (!rightPaneVisible) return [];
const definitions: RightPanelWidgetDefinition[] = [
{
el: <TableOfContents />,
enabled: (noteType === "text" || noteType === "doc"),
enabled: (noteType === "text" || noteType === "doc" || isPdf),
},
{
el: <PdfPages />,
enabled: isPdf,
},
{
el: <PdfAttachments />,
enabled: isPdf,
},
{
el: <PdfLayers />,
enabled: isPdf,
},
{
el: <HighlightsList />,

View File

@@ -29,6 +29,11 @@
hyphens: auto;
}
.toc li.active > .item-content {
font-weight: bold;
color: var(--main-text-color);
}
.toc > ol {
--toc-depth-level: 1;
}

View File

@@ -6,7 +6,7 @@ import { useCallback, useEffect, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import { randomString } from "../../services/utils";
import { useActiveNoteContext, useContentElement, useIsNoteReadOnly, useNoteProperty, useTextEditor } from "../react/hooks";
import { useActiveNoteContext, useContentElement, useGetContextData, useIsNoteReadOnly, useNoteProperty, useTextEditor } from "../react/hooks";
import Icon from "../react/Icon";
import RightPanelWidget from "./RightPanelWidget";
@@ -21,29 +21,50 @@ interface HeadingsWithNesting extends RawHeading {
children: HeadingsWithNesting[];
}
export interface HeadingContext {
scrollToHeading(heading: RawHeading): void;
headings: RawHeading[];
activeHeadingId?: string | null;
}
export default function TableOfContents() {
const { note, noteContext } = useActiveNoteContext();
const noteType = useNoteProperty(note, "type");
const noteMime = useNoteProperty(note, "mime");
const { isReadOnly } = useIsNoteReadOnly(note, noteContext);
return (
<RightPanelWidget id="toc" title={t("toc.table_of_contents")} grow>
{((noteType === "text" && isReadOnly) || (noteType === "doc")) && <ReadOnlyTextTableOfContents />}
{noteType === "text" && !isReadOnly && <EditableTextTableOfContents />}
{noteType === "file" && noteMime === "application/pdf" && <PdfTableOfContents />}
</RightPanelWidget>
);
}
function AbstractTableOfContents<T extends RawHeading>({ headings, scrollToHeading }: {
function PdfTableOfContents() {
const data = useGetContextData("toc");
return (
<AbstractTableOfContents
headings={data?.headings || []}
scrollToHeading={data?.scrollToHeading || (() => {})}
activeHeadingId={data?.activeHeadingId}
/>
);
}
function AbstractTableOfContents<T extends RawHeading>({ headings, scrollToHeading, activeHeadingId }: {
headings: T[];
scrollToHeading(heading: T): void;
activeHeadingId?: string | null;
}) {
const nestedHeadings = buildHeadingTree(headings);
return (
<span className="toc">
{nestedHeadings.length > 0 ? (
<ol>
{nestedHeadings.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} />)}
{nestedHeadings.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} activeHeadingId={activeHeadingId} />)}
</ol>
) : (
<div className="no-headings">{t("toc.no_headings")}</div>
@@ -52,14 +73,16 @@ function AbstractTableOfContents<T extends RawHeading>({ headings, scrollToHeadi
);
}
function TableOfContentsHeading({ heading, scrollToHeading }: {
function TableOfContentsHeading({ heading, scrollToHeading, activeHeadingId }: {
heading: HeadingsWithNesting;
scrollToHeading(heading: RawHeading): void;
activeHeadingId?: string | null;
}) {
const [ collapsed, setCollapsed ] = useState(false);
const isActive = heading.id === activeHeadingId;
return (
<>
<li className={clsx(collapsed && "collapsed")}>
<li className={clsx(collapsed && "collapsed", isActive && "active")}>
{heading.children.length > 0 && (
<Icon
className="collapse-button"
@@ -74,7 +97,7 @@ function TableOfContentsHeading({ heading, scrollToHeading }: {
</li>
{heading.children && (
<ol>
{heading.children.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} />)}
{heading.children.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} activeHeadingId={activeHeadingId} />)}
</ol>
)}
</>

View File

@@ -0,0 +1,57 @@
.pdf-attachments-list {
width: 100%;
}
.pdf-attachment-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid var(--main-border-color);
transition: background-color 0.2s;
}
.pdf-attachment-item:hover {
background-color: var(--hover-item-background-color);
}
.pdf-attachment-item:last-child {
border-bottom: none;
}
.pdf-attachment-info {
flex: 1;
min-width: 0;
}
.pdf-attachment-filename {
font-size: 13px;
font-weight: 500;
color: var(--main-text-color);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pdf-attachment-size {
font-size: 11px;
color: var(--muted-text-color);
margin-top: 2px;
}
.no-attachments {
padding: 16px;
text-align: center;
color: var(--muted-text-color);
}
.pdf-attachment-item .bx {
flex-shrink: 0;
font-size: 18px;
color: var(--muted-text-color);
}
.pdf-attachment-item:hover .bx {
color: var(--main-text-color);
}

View File

@@ -0,0 +1,62 @@
import "./PdfAttachments.css";
import { t } from "../../../services/i18n";
import { formatSize } from "../../../services/utils";
import { useActiveNoteContext, useGetContextData, useNoteProperty } from "../../react/hooks";
import Icon from "../../react/Icon";
import RightPanelWidget from "../RightPanelWidget";
interface AttachmentInfo {
filename: string;
size: number;
}
export default function PdfAttachments() {
const { note } = useActiveNoteContext();
const noteType = useNoteProperty(note, "type");
const noteMime = useNoteProperty(note, "mime");
const attachmentsData = useGetContextData("pdfAttachments");
if (noteType !== "file" || noteMime !== "application/pdf") {
return null;
}
if (!attachmentsData || attachmentsData.attachments.length === 0) {
return null;
}
return (
<RightPanelWidget id="pdf-attachments" title={t("pdf.attachments", { count: attachmentsData.attachments.length })}>
<div className="pdf-attachments-list">
{attachmentsData.attachments.map((attachment) => (
<PdfAttachmentItem
key={attachment.filename}
attachment={attachment}
onDownload={attachmentsData.downloadAttachment}
/>
))}
</div>
</RightPanelWidget>
);
}
function PdfAttachmentItem({
attachment,
onDownload
}: {
attachment: AttachmentInfo;
onDownload: (filename: string) => void;
}) {
const sizeText = formatSize(attachment.size);
return (
<div className="pdf-attachment-item" onClick={() => onDownload(attachment.filename)}>
<Icon icon="bx bx-paperclip" />
<div className="pdf-attachment-info">
<div className="pdf-attachment-filename">{attachment.filename}</div>
<div className="pdf-attachment-size">{sizeText}</div>
</div>
<Icon icon="bx bx-download" />
</div>
);
}

View File

@@ -0,0 +1,54 @@
.pdf-layers-list {
width: 100%;
}
.pdf-layer-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid var(--main-border-color);
transition: background-color 0.2s;
}
.pdf-layer-item:hover {
background-color: var(--hover-item-background-color);
}
.pdf-layer-item:last-child {
border-bottom: none;
}
.pdf-layer-item.hidden {
opacity: 0.5;
}
.pdf-layer-name {
flex: 1;
font-size: 13px;
color: var(--main-text-color);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.no-layers {
padding: 16px;
text-align: center;
color: var(--muted-text-color);
}
.pdf-layer-item .bx {
flex-shrink: 0;
font-size: 18px;
color: var(--muted-text-color);
}
.pdf-layer-item:hover .bx {
color: var(--main-text-color);
}
.pdf-layer-item.visible .bx {
color: var(--main-text-color);
}

View File

@@ -0,0 +1,55 @@
import "./PdfLayers.css";
import { t } from "../../../services/i18n";
import { useActiveNoteContext, useGetContextData, useNoteProperty } from "../../react/hooks";
import Icon from "../../react/Icon";
import RightPanelWidget from "../RightPanelWidget";
interface LayerInfo {
id: string;
name: string;
visible: boolean;
}
export default function PdfLayers() {
const { note } = useActiveNoteContext();
const noteType = useNoteProperty(note, "type");
const noteMime = useNoteProperty(note, "mime");
const layersData = useGetContextData("pdfLayers");
if (noteType !== "file" || noteMime !== "application/pdf") {
return null;
}
return (layersData?.layers && layersData.layers.length > 0 &&
<RightPanelWidget id="pdf-layers" title={t("pdf.layers", { count: layersData.layers.length })}>
<div className="pdf-layers-list">
{layersData.layers.map((layer) => (
<PdfLayerItem
key={layer.id}
layer={layer}
onToggle={layersData.toggleLayer}
/>
))}
</div>
</RightPanelWidget>
);
}
function PdfLayerItem({
layer,
onToggle
}: {
layer: LayerInfo;
onToggle: (layerId: string, visible: boolean) => void;
}) {
return (
<div
className={`pdf-layer-item ${layer.visible ? 'visible' : 'hidden'}`}
onClick={() => onToggle(layer.id, !layer.visible)}
>
<Icon icon={layer.visible ? "bx bx-show" : "bx bx-hide"} />
<div className="pdf-layer-name">{layer.name}</div>
</div>
);
}

View File

@@ -0,0 +1,67 @@
.pdf-pages-list {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 8px;
padding: 8px;
align-content: flex-start;
}
.pdf-page-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
cursor: pointer;
border: 2px solid transparent;
transition: border-color 0.2s;
box-sizing: border-box;
position: relative;
.pdf-page-number {
font-size: 12px;
margin-bottom: 4px;
color: var(--main-text-color);
position: absolute;
bottom: 1em;
left: 50%;
transform: translateX(-50%);
background-color: var(--accented-background-color);
padding: 0.2em 0.5em;
border-radius: 4px;
}
}
.pdf-page-item:hover {
background-color: var(--hover-item-background-color);
}
.pdf-page-item.active {
border-color: var(--main-border-color);
background-color: var(--active-item-background-color);
}
.pdf-page-thumbnail {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.pdf-page-thumbnail img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.pdf-page-loading {
color: var(--muted-text-color);
font-size: 11px;
}
.no-pages {
padding: 16px;
text-align: center;
color: var(--muted-text-color);
}

View File

@@ -0,0 +1,111 @@
import "./PdfPages.css";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
import { NoteContextDataMap } from "../../../components/note_context";
import { t } from "../../../services/i18n";
import { useActiveNoteContext, useGetContextData, useNoteProperty } from "../../react/hooks";
import RightPanelWidget from "../RightPanelWidget";
export default function PdfPages() {
const { note } = useActiveNoteContext();
const noteType = useNoteProperty(note, "type");
const noteMime = useNoteProperty(note, "mime");
const pagesData = useGetContextData("pdfPages");
if (noteType !== "file" || noteMime !== "application/pdf") {
return null;
}
return (pagesData &&
<RightPanelWidget id="pdf-pages" title={t("pdf.pages", { count: pagesData?.totalPages || 0 })} grow>
<PdfPagesList key={note?.noteId} pagesData={pagesData} />
</RightPanelWidget>
);
}
function PdfPagesList({ pagesData }: { pagesData: NoteContextDataMap["pdfPages"] }) {
const [thumbnails, setThumbnails] = useState<Map<number, string>>(new Map());
const requestedThumbnails = useRef<Set<number>>(new Set());
useEffect(() => {
// Listen for thumbnail responses via custom event
function handleThumbnail(event: CustomEvent) {
const { pageNumber, dataUrl } = event.detail;
setThumbnails(prev => new Map(prev).set(pageNumber, dataUrl));
}
window.addEventListener("pdf-thumbnail", handleThumbnail as EventListener);
return () => {
window.removeEventListener("pdf-thumbnail", handleThumbnail as EventListener);
};
}, []);
const requestThumbnail = useCallback((pageNumber: number) => {
// Only request if we haven't already requested it and don't have it
if (!requestedThumbnails.current.has(pageNumber) && !thumbnails.has(pageNumber) && pagesData) {
requestedThumbnails.current.add(pageNumber);
pagesData.requestThumbnail(pageNumber);
}
}, [pagesData, thumbnails]);
if (!pagesData || pagesData.totalPages === 0) {
return <div className="no-pages">No pages available</div>;
}
const pages = Array.from({ length: pagesData.totalPages }, (_, i) => i + 1);
return (
<div className="pdf-pages-list">
{pages.map(pageNumber => (
<PdfPageItem
key={pageNumber}
pageNumber={pageNumber}
isActive={pageNumber === pagesData.currentPage}
thumbnail={thumbnails.get(pageNumber)}
onRequestThumbnail={requestThumbnail}
onPageClick={() => pagesData.scrollToPage(pageNumber)}
/>
))}
</div>
);
}
function PdfPageItem({
pageNumber,
isActive,
thumbnail,
onRequestThumbnail,
onPageClick
}: {
pageNumber: number;
isActive: boolean;
thumbnail?: string;
onRequestThumbnail(page: number): void;
onPageClick(): void;
}) {
const hasRequested = useRef(false);
useEffect(() => {
if (!thumbnail && !hasRequested.current) {
hasRequested.current = true;
onRequestThumbnail(pageNumber);
}
}, [pageNumber, thumbnail, onRequestThumbnail]);
return (
<div
className={`pdf-page-item ${isActive ? 'active' : ''}`}
onClick={onPageClick}
>
<div className="pdf-page-number">{pageNumber}</div>
<div className="pdf-page-thumbnail">
{thumbnail ? (
<img src={thumbnail} alt={t("pdf.pages_alt", { pageNumber })} />
) : (
<div className="pdf-page-loading">{t("pdf.pages_loading")}</div>
)}
</div>
</div>
);
}

View File

@@ -23,7 +23,7 @@ export default function SqlResults() {
{t("sql_result.no_rows")}
</Alert>
) : (
<div class="sql-console-result-container">
<div className="sql-console-result-container selectable-text">
{results?.map(rows => {
// inserts, updates
if (typeof rows === "object" && !Array.isArray(rows)) {

View File

@@ -1,27 +1,29 @@
import { useNoteBlob } from "../react/hooks";
import "./File.css";
import { TypeWidgetProps } from "./type_widget";
import FNote from "../../entities/fnote";
import { t } from "../../services/i18n";
import { getUrlForDownload } from "../../services/open";
import Alert from "../react/Alert";
import { t } from "../../services/i18n";
import { useNoteBlob } from "../react/hooks";
import PdfPreview from "./file/Pdf";
import { TypeWidgetProps } from "./type_widget";
const TEXT_MAX_NUM_CHARS = 5000;
export default function File({ note }: TypeWidgetProps) {
const blob = useNoteBlob(note);
export default function FileTypeWidget({ note, parentComponent, noteContext }: TypeWidgetProps) {
const blob = useNoteBlob(note, parentComponent?.componentId);
if (blob?.content) {
return <TextPreview content={blob.content} />
return <TextPreview content={blob.content} />;
} else if (note.mime === "application/pdf") {
return <PdfPreview note={note} />
return noteContext && <PdfPreview blob={blob} note={note} componentId={parentComponent?.componentId} noteContext={noteContext} />;
} else if (note.mime.startsWith("video/")) {
return <VideoPreview note={note} />
return <VideoPreview note={note} />;
} else if (note.mime.startsWith("audio/")) {
return <AudioPreview note={note} />
} else {
return <NoPreview />
return <AudioPreview note={note} />;
}
return <NoPreview />;
}
function TextPreview({ content }: { content: string }) {
@@ -37,14 +39,6 @@ function TextPreview({ content }: { content: string }) {
)}
<pre class="file-preview-content">{trimmedContent}</pre>
</>
)
}
function PdfPreview({ note }: { note: FNote }) {
return (
<iframe
class="pdf-preview"
src={getUrlForDownload(`api/notes/${note.noteId}/open`)} />
);
}
@@ -56,7 +50,7 @@ function VideoPreview({ note }: { note: FNote }) {
datatype={note?.mime}
controls
/>
)
);
}
function AudioPreview({ note }: { note: FNote }) {
@@ -66,7 +60,7 @@ function AudioPreview({ note }: { note: FNote }) {
src={getUrlForDownload(`api/notes/${note.noteId}/open-partial`)}
controls
/>
)
);
}
function NoPreview() {

View File

@@ -29,6 +29,7 @@ export default function Mermaid(props: TypeWidgetProps) {
<SvgSplitEditor
attachmentName="mermaid-export"
renderSvg={renderSvg}
noteType="mermaid"
{...props}
/>
);

View File

@@ -37,6 +37,7 @@ const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Options["locale"] | null>
it: "it",
ja: "ja",
pt: "pt",
pl: null,
pt_br: "pt",
ro: "ro",
ru: "ru",

View File

@@ -13,6 +13,7 @@ export const LANGUAGE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Language["code"]
it: "it-IT",
ja: "ja-JP",
pt: "pt-PT",
pl: "pl-PL",
pt_br: "pt-BR",
ro: "ro-RO",
ru: "ru-RU",

View File

@@ -1,6 +1,7 @@
import "./code.css";
import { default as VanillaCodeMirror, getThemeById } from "@triliumnext/codemirror";
import { NoteType } from "@triliumnext/commons";
import { useEffect, useRef, useState } from "preact/hooks";
import appContext, { CommandListenerData } from "../../../components/app_context";
@@ -24,6 +25,7 @@ export interface EditableCodeProps extends TypeWidgetProps, Omit<CodeEditorProps
debounceUpdate?: boolean;
lineWrapping?: boolean;
updateInterval?: number;
noteType?: NoteType;
/** Invoked when the content of the note is changed, such as a different revision or a note switch. */
onContentChanged?: (content: string) => void;
/** Invoked after the content of the note has been uploaded to the server, using a spaced update. */
@@ -72,14 +74,14 @@ function formatViewSource(note: FNote, content: string) {
return content;
}
export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentComponent, updateInterval, onContentChanged, dataSaved, ...editorProps }: EditableCodeProps) {
export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentComponent, updateInterval, noteType = "code", onContentChanged, dataSaved, ...editorProps }: EditableCodeProps) {
const editorRef = useRef<VanillaCodeMirror>(null);
const containerRef = useRef<HTMLPreElement>(null);
const [ vimKeymapEnabled ] = useTriliumOptionBool("vimKeymapEnabled");
const mime = useNoteProperty(note, "mime");
const spacedUpdate = useEditorSpacedUpdate({
note,
noteType: "code",
noteType,
noteContext,
getData: () => ({ content: editorRef.current?.getText() ?? "" }),
onContentChange: (content) => {

View File

@@ -0,0 +1,234 @@
import { useEffect, useRef } from "preact/hooks";
import appContext from "../../../components/app_context";
import type NoteContext from "../../../components/note_context";
import FBlob from "../../../entities/fblob";
import FNote from "../../../entities/fnote";
import { useViewModeConfig } from "../../collections/NoteList";
import { useBlobEditorSpacedUpdate, useTriliumEvent } from "../../react/hooks";
import PdfViewer from "./PdfViewer";
export default function PdfPreview({ note, blob, componentId, noteContext }: {
note: FNote;
noteContext: NoteContext;
blob: FBlob | null | undefined;
componentId: string | undefined;
}) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const historyConfig = useViewModeConfig<HistoryData>(note, "pdfHistory");
const spacedUpdate = useBlobEditorSpacedUpdate({
note,
noteType: "file",
noteContext,
getData() {
if (!iframeRef.current?.contentWindow) return undefined;
return new Promise<Blob>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("Timeout while waiting for blob response"));
}, 10_000);
const onMessageReceived = (event: PdfMessageEvent) => {
if (event.data.type !== "pdfjs-viewer-blob") return;
if (event.data.noteId !== note.noteId || event.data.ntxId !== noteContext.ntxId) return;
const blob = new Blob([event.data.data as Uint8Array<ArrayBuffer>], { type: note.mime });
clearTimeout(timeout);
window.removeEventListener("message", onMessageReceived);
resolve(blob);
};
window.addEventListener("message", onMessageReceived);
iframeRef.current?.contentWindow?.postMessage({
type: "trilium-request-blob",
}, window.location.origin);
});
},
onContentChange() {
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.location.reload();
}
},
replaceWithoutRevision: true
});
useEffect(() => {
function handleMessage(event: PdfMessageEvent) {
if (event.data?.type === "pdfjs-viewer-document-modified") {
if (event.data.noteId === note.noteId && event.data.ntxId === noteContext.ntxId) {
spacedUpdate.resetUpdateTimer();
spacedUpdate.scheduleUpdate();
}
}
if (event.data.type === "pdfjs-viewer-save-view-history" && event.data?.data) {
if (event.data.noteId === note.noteId && event.data.ntxId === noteContext.ntxId) {
historyConfig?.storeFn(JSON.parse(event.data.data));
}
}
if (event.data.type === "pdfjs-viewer-toc") {
if (event.data.data) {
// Convert PDF outline to HeadingContext format
const headings = convertPdfOutlineToHeadings(event.data.data);
noteContext.setContextData("toc", {
headings,
activeHeadingId: null,
scrollToHeading: (heading) => {
iframeRef.current?.contentWindow?.postMessage({
type: "trilium-scroll-to-heading",
headingId: heading.id
}, window.location.origin);
}
});
} else {
// No ToC available, use empty headings
noteContext.setContextData("toc", {
headings: [],
activeHeadingId: null,
scrollToHeading: () => {}
});
}
}
if (event.data.type === "pdfjs-viewer-active-heading") {
const currentToc = noteContext.getContextData("toc");
if (currentToc) {
noteContext.setContextData("toc", {
...currentToc,
activeHeadingId: event.data.headingId
});
}
}
if (event.data.type === "pdfjs-viewer-page-info") {
noteContext.setContextData("pdfPages", {
totalPages: event.data.totalPages,
currentPage: event.data.currentPage,
scrollToPage: (page: number) => {
iframeRef.current?.contentWindow?.postMessage({
type: "trilium-scroll-to-page",
pageNumber: page
}, window.location.origin);
},
requestThumbnail: (page: number) => {
iframeRef.current?.contentWindow?.postMessage({
type: "trilium-request-thumbnail",
pageNumber: page
}, window.location.origin);
}
});
}
if (event.data.type === "pdfjs-viewer-current-page") {
const currentPages = noteContext.getContextData("pdfPages");
if (currentPages) {
noteContext.setContextData("pdfPages", {
...currentPages,
currentPage: event.data.currentPage
});
}
}
if (event.data.type === "pdfjs-viewer-thumbnail") {
// Forward thumbnail to any listeners
window.dispatchEvent(new CustomEvent("pdf-thumbnail", {
detail: {
pageNumber: event.data.pageNumber,
dataUrl: event.data.dataUrl
}
}));
}
if (event.data.type === "pdfjs-viewer-attachments") {
noteContext.setContextData("pdfAttachments", {
attachments: event.data.attachments,
downloadAttachment: (filename: string) => {
iframeRef.current?.contentWindow?.postMessage({
type: "trilium-download-attachment",
filename
}, window.location.origin);
}
});
}
if (event.data.type === "pdfjs-viewer-layers") {
noteContext.setContextData("pdfLayers", {
layers: event.data.layers,
toggleLayer: (layerId: string, visible: boolean) => {
iframeRef.current?.contentWindow?.postMessage({
type: "trilium-toggle-layer",
layerId,
visible
}, window.location.origin);
}
});
}
}
window.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
};
}, [ note, historyConfig, componentId, blob, noteContext ]);
useTriliumEvent("customDownload", ({ ntxId }) => {
if (ntxId !== noteContext.ntxId) return;
iframeRef.current?.contentWindow?.postMessage({
type: "trilium-request-download"
});
});
return (historyConfig &&
<PdfViewer
iframeRef={iframeRef}
tabIndex={300}
pdfUrl={`../../api/notes/${note.noteId}/open`}
onLoad={() => {
const win = iframeRef.current?.contentWindow;
if (win) {
win.TRILIUM_VIEW_HISTORY_STORE = historyConfig.config;
win.TRILIUM_NOTE_ID = note.noteId;
win.TRILIUM_NTX_ID = noteContext.ntxId;
}
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.addEventListener('click', () => {
appContext.tabManager.activateNoteContext(noteContext.ntxId);
});
}
}}
editable
/>
);
}
interface PdfHeading {
level: number;
text: string;
id: string;
element: null;
}
function convertPdfOutlineToHeadings(outline: PdfOutlineItem[]): PdfHeading[] {
const headings: PdfHeading[] = [];
function flatten(items: PdfOutlineItem[]) {
for (const item of items) {
headings.push({
level: item.level + 1,
text: item.title,
id: item.id,
element: null // PDFs don't have DOM elements
});
if (item.items && item.items.length > 0) {
flatten(item.items);
}
}
}
flatten(outline);
return headings;
}

View File

@@ -0,0 +1,94 @@
import type { HTMLAttributes, RefObject } from "preact";
import { useCallback, useEffect, useRef } from "preact/hooks";
import { useSyncedRef, useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
const VARIABLE_WHITELIST = new Set([
"root-background",
"main-background-color",
"main-border-color",
"main-text-color"
]);
interface PdfViewerProps extends Pick<HTMLAttributes<HTMLIFrameElement>, "tabIndex"> {
iframeRef?: RefObject<HTMLIFrameElement>;
/** Note: URLs are relative to /pdfjs/web. */
pdfUrl: string;
onLoad?(): void;
/**
* If set, enables editable mode which includes persistence of user settings, annotations as well as specific features such as sending table of contents data for the sidebar.
*/
editable?: boolean;
}
/**
* Reusable component displaying a PDF. The PDF needs to be provided via a URL.
*/
export default function PdfViewer({ iframeRef: externalIframeRef, pdfUrl, onLoad, editable }: PdfViewerProps) {
const iframeRef = useSyncedRef(externalIframeRef, null);
const [ locale ] = useTriliumOption("locale");
const [ newLayout ] = useTriliumOptionBool("newLayout");
const injectStyles = useStyleInjection(iframeRef);
return (
<iframe
ref={iframeRef}
class="pdf-preview"
src={`pdfjs/web/viewer.html?file=${pdfUrl}&lang=${locale}&sidebar=${newLayout ? "0" : "1"}&editable=${editable ? "1" : "0"}`}
onLoad={() => {
injectStyles();
onLoad?.();
}}
/>
);
}
function useStyleInjection(iframeRef: RefObject<HTMLIFrameElement>) {
const styleRef = useRef<HTMLStyleElement | null>(null);
// First load.
const onLoad = useCallback(() => {
const doc = iframeRef.current?.contentDocument;
if (!doc) return;
const style = doc.createElement('style');
style.id = 'client-root-vars';
style.textContent = cssVarsToString(getRootCssVariables());
styleRef.current = style;
doc.head.appendChild(style);
}, [ iframeRef ]);
// React to changes.
useEffect(() => {
const listener = () => {
styleRef.current!.textContent = cssVarsToString(getRootCssVariables());
};
const media = window.matchMedia("(prefers-color-scheme: dark)");
media.addEventListener("change", listener);
return () => media.removeEventListener("change", listener);
}, [ iframeRef ]);
return onLoad;
}
function getRootCssVariables() {
const styles = getComputedStyle(document.documentElement);
const vars: Record<string, string> = {};
for (let i = 0; i < styles.length; i++) {
const prop = styles[i];
if (prop.startsWith('--') && VARIABLE_WHITELIST.has(prop.substring(2))) {
vars[`--tn-${prop.substring(2)}`] = styles.getPropertyValue(prop).trim();
}
}
return vars;
}
function cssVarsToString(vars: Record<string, string>) {
return `:root {\n${Object.entries(vars)
.map(([k, v]) => ` ${k}: ${v};`)
.join('\n')}\n}`;
}

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "preact/hooks";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
import { t } from "../../../services/i18n";
import SplitEditor, { PreviewButton, SplitEditorProps } from "./SplitEditor";
import { RawHtmlBlock } from "../../react/RawHtml";
@@ -55,7 +55,9 @@ export default function SvgSplitEditor({ ntxId, note, attachmentName, renderSvg,
}
// Save as attachment.
function onSave() {
const onSave = useCallback(() => {
if (!svg) return; // Don't save if SVG hasn't been rendered yet
const payload = {
role: "image",
title: `${attachmentName}.svg`,
@@ -65,16 +67,18 @@ export default function SvgSplitEditor({ ntxId, note, attachmentName, renderSvg,
};
server.post(`notes/${note.noteId}/attachments?matchBy=title`, payload);
}
}, [ svg, attachmentName, note.noteId ]);
// Save the SVG when entering a note only when it does not have an attachment.
useEffect(() => {
if (!svg) return; // Wait until SVG is rendered
note?.getAttachments().then((attachments) => {
if (!attachments.find((a) => a.title === `${attachmentName}.svg`)) {
onSave();
}
});
}, [ note ]);
}).catch(e => console.error("Failed to get attachments for SVGSplitEditor", e));
}, [ note, svg, attachmentName, onSave ]);
// Import/export
useTriliumEvent("exportSvg", async({ ntxId: eventNtxId }) => {

View File

@@ -35,5 +35,5 @@ describe("CK config", () => {
expect(config.translations, locale.id).toHaveLength(2);
}
}
}, 10_000);
}, 20_000);
});

View File

@@ -3,8 +3,6 @@
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],

View File

@@ -1,7 +1,8 @@
import { writeFileSync } from "fs";
import { join } from "path";
import BuildHelper from "../../../scripts/build-utils";
import originalPackageJson from "../package.json" with { type: "json" };
import { writeFileSync } from "fs";
const build = new BuildHelper("apps/desktop");
@@ -18,9 +19,7 @@ async function main() {
build.copyNodeModules([ "better-sqlite3", "bindings", "file-uri-to-path", "@electron/remote" ]);
build.copy("/node_modules/ckeditor5/dist/ckeditor5-content.css", "ckeditor5-content.css");
// Integrate the client.
build.triggerBuildAndCopyTo("apps/client", "public/");
build.deleteFromOutput("public/webpack-stats.json");
build.buildFrontend();
generatePackageJson();
}

View File

@@ -0,0 +1,129 @@
import test, { BrowserContext, expect, Page } from "@playwright/test";
import App from "../support/app";
test.beforeEach(async ({ page, context }) => {
const app = await setLayout({ page, context }, true);
await app.setOption("rightPaneCollapsedItems", "[]");
});
test.afterEach(async ({ page, context }) => await setLayout({ page, context }, false));
test("Table of contents works", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab("Dacia Logan.pdf");
const toc = app.sidebar.locator(".toc");
await expect(toc.locator("li")).toHaveCount(48);
await expect(toc.locator("li", { hasText: "Logan Van" })).toHaveCount(1);
const pdfHelper = new PdfHelper(app);
await toc.locator("li", { hasText: "Logan Pick-Up" }).click();
await pdfHelper.expectPageToBe(13);
await app.clickNoteOnNoteTreeByTitle("Layers test.pdf");
await expect(toc.locator("li")).toHaveCount(0);
});
test("Page navigation works", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab("Dacia Logan.pdf");
const pagesList = app.sidebar.locator(".pdf-pages-list");
// Check count is correct.
await expect(app.sidebar).toContainText("28 pages");
expect(await pagesList.locator(".pdf-page-item").count()).toBe(28);
// Go to page 3.
await pagesList.locator(".pdf-page-item").nth(2).click();
const pdfHelper = new PdfHelper(app);
await pdfHelper.expectPageToBe(3);
await app.clickNoteOnNoteTreeByTitle("Layers test.pdf");
await expect(pagesList.locator(".pdf-page-item")).toHaveCount(1);
});
test("Attachments listing works", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab("Dacia Logan.pdf");
const attachmentsList = app.sidebar.locator(".pdf-attachments-list");
await expect(app.sidebar).toContainText("2 attachments");
await expect(attachmentsList.locator(".pdf-attachment-item")).toHaveCount(2);
const attachmentInfo = attachmentsList.locator(".pdf-attachment-item", { hasText: "Note.trilium" });
await expect(attachmentInfo).toContainText("3.36 MiB");
// Download the attachment and check its size.
const [ download ] = await Promise.all([
page.waitForEvent("download"),
attachmentInfo.locator(".bx-download").click()
]);
expect(download).toBeDefined();
await app.clickNoteOnNoteTreeByTitle("Layers test.pdf");
await expect(attachmentsList.locator(".pdf-attachment-item")).toHaveCount(0);
});
test("Download original PDF works", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab("Dacia Logan.pdf");
const pdfHelper = new PdfHelper(app);
await pdfHelper.toBeInitialized();
const [ download ] = await Promise.all([
page.waitForEvent("download"),
app.currentNoteSplit.locator(".icon-action.bx.bx-download").click()
]);
expect(download).toBeDefined();
});
test("Layers listing works", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab("Layers test.pdf");
// Check count is correct.
await expect(app.sidebar).toContainText("2 layers");
const layersList = app.sidebar.locator(".pdf-layers-list");
await expect(layersList.locator(".pdf-layer-item")).toHaveCount(2);
// Toggle visibility of the first layer.
const firstLayer = layersList.locator(".pdf-layer-item").first();
await expect(firstLayer).toContainText("Tongue out");
await expect(firstLayer).toContainClass("hidden");
await firstLayer.click();
await expect(firstLayer).not.toContainClass("visible");
await app.clickNoteOnNoteTreeByTitle("Dacia Logan.pdf");
await expect(layersList.locator(".pdf-layer-item")).toHaveCount(0);
});
async function setLayout({ page, context}: { page: Page; context: BrowserContext }, newLayout: boolean) {
const app = new App(page, context);
await app.goto();
await app.setOption("newLayout", newLayout ? "true" : "false");
return app;
}
class PdfHelper {
private contentFrame: ReturnType<Page["frameLocator"]>;
constructor(app: App) {
this.contentFrame = app.currentNoteSplit.frameLocator("iframe");
}
async expectPageToBe(expectedPageNumber: number) {
await expect(this.contentFrame.locator("#pageNumber")).toHaveValue(`${expectedPageNumber}`);
}
async toBeInitialized() {
await expect(this.contentFrame.locator("#pageNumber")).toBeVisible();
}
}

View File

@@ -9,7 +9,7 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://host.docker.internal:8082; # change it to a different port if non-default is used
proxy_pass http://127.0.0.1:8082;
proxy_cookie_path / /trilium/;
proxy_read_timeout 90;
}

View File

@@ -25,7 +25,8 @@
"docker-start-rootless-debian": "pnpm docker-build-rootless-debian && docker run -p 8081:8080 triliumnext-rootless-debian",
"docker-start-rootless-alpine": "pnpm docker-build-rootless-alpine && docker run -p 8081:8080 triliumnext-rootless-alpine",
"generate-document": "cross-env TRILIUM_ENV=dev TRILIUM_DATA_DIR=data TRILIUM_RESOURCE_DIR=src tsx ./scripts/generate_document.ts",
"proxy-traefik": "docker run --name trilium-traefik --rm --network=host -v ./docker/traefik/traefik.yml:/etc/traefik/traefik.yml -v ./docker/traefik/dynamic:/etc/traefik/dynamic traefik:latest"
"proxy-traefik": "docker run --name trilium-traefik --rm --network=host -v ./docker/traefik/traefik.yml:/etc/traefik/traefik.yml:ro -v ./docker/traefik/dynamic:/etc/traefik/dynamic traefik:latest",
"proxy-nginx-subdir": "docker run --name trilium-nginx-subdir --rm --network=host -v ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro nginx:latest"
},
"dependencies": {
"better-sqlite3": "12.5.0",

View File

@@ -14,9 +14,7 @@ async function main() {
build.copyNodeModules([ "better-sqlite3", "bindings", "file-uri-to-path" ]);
build.copy("/node_modules/ckeditor5/dist/ckeditor5-content.css", "ckeditor5-content.css");
// Integrate the client.
build.triggerBuildAndCopyTo("apps/client", "public/");
build.deleteFromOutput("public/webpack-stats.json");
build.buildFrontend();
}
main();

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -35,11 +35,11 @@ class="image image_resized" style="width:74.04%;">
href="#root/_help_vvUCN7FDkq7G">Installing Ollama</a>.</p>
<p>To see what embedding models Ollama has available, you can check out
<a
href="https://ollama.com/search?c=embedding">this search</a>on their website, and then <code>pull</code> whichever one
you want to try out. A popular choice is <code>mxbai-embed-large</code>.</p>
href="https://ollama.com/search?c=embedding">this search</a>on their website, and then <code spellcheck="false">pull</code> whichever
one you want to try out. A popular choice is <code spellcheck="false">mxbai-embed-large</code>.</p>
<p>First, we'll need to select the Ollama provider from the tabs of providers,
then we will enter in the Base URL for our Ollama. Since our Ollama is
running on our local machine, our Base URL is <code>http://localhost:11434</code>.
running on our local machine, our Base URL is <code spellcheck="false">http://localhost:11434</code>.
We will then hit the “refresh” button to have it fetch our models:</p>
<figure
class="image image_resized" style="width:82.28%;">
@@ -80,57 +80,57 @@ class="image image_resized" style="width:74.04%;">
<p>These are the tools that currently exist, and will certainly be updated
to be more effectively (and even more to be added!):</p>
<ul>
<li><code>search_notes</code>
<li><code spellcheck="false">search_notes</code>
<ul>
<li>Semantic search</li>
</ul>
</li>
<li><code>keyword_search</code>
<li><code spellcheck="false">keyword_search</code>
<ul>
<li>Keyword-based search</li>
</ul>
</li>
<li><code>attribute_search</code>
<li><code spellcheck="false">attribute_search</code>
<ul>
<li>Attribute-specific search</li>
</ul>
</li>
<li><code>search_suggestion</code>
<li><code spellcheck="false">search_suggestion</code>
<ul>
<li>Search syntax helper</li>
</ul>
</li>
<li><code>read_note</code>
<li><code spellcheck="false">read_note</code>
<ul>
<li>Read note content (helps the LLM read Notes)</li>
</ul>
</li>
<li><code>create_note</code>
<li><code spellcheck="false">create_note</code>
<ul>
<li>Create a Note</li>
</ul>
</li>
<li><code>update_note</code>
<li><code spellcheck="false">update_note</code>
<ul>
<li>Update a Note</li>
</ul>
</li>
<li><code>manage_attributes</code>
<li><code spellcheck="false">manage_attributes</code>
<ul>
<li>Manage attributes on a Note</li>
</ul>
</li>
<li><code>manage_relationships</code>
<li><code spellcheck="false">manage_relationships</code>
<ul>
<li>Manage the various relationships between Notes</li>
</ul>
</li>
<li><code>extract_content</code>
<li><code spellcheck="false">extract_content</code>
<ul>
<li>Used to smartly extract content from a Note</li>
</ul>
</li>
<li><code>calendar_integration</code>
<li><code spellcheck="false">calendar_integration</code>
<ul>
<li>Used to find date notes, create date notes, get the daily note, etc.</li>
</ul>

View File

@@ -23,14 +23,14 @@ class="image image_resized" style="width:50.49%;">
<img style="aspect-ratio:1161/480;" src="2_Installing Ollama_image.png"
width="1161" height="480">
</figure>
<p>Also, you should have access to the <code>ollama</code> CLI via Powershell
or CMD:</p>
<p>Also, you should have access to the <code spellcheck="false">ollama</code> CLI
via Powershell or CMD:</p>
<figure class="image image_resized" style="width:86.09%;">
<img style="aspect-ratio:1730/924;" src="5_Installing Ollama_image.png"
width="1730" height="924">
</figure>
<p>After Ollama is installed, you can go ahead and <code>pull</code> the models
you want to use and run. Here's a command to pull my favorite tool-compatible
<p>After Ollama is installed, you can go ahead and <code spellcheck="false">pull</code> the
models you want to use and run. Here's a command to pull my favorite tool-compatible
model and embedding model as of April 2025:</p><pre><code class="language-text-x-trilium-auto">ollama pull llama3.1:8b
ollama pull mxbai-embed-large</code></pre>
<p>Also, you can make sure it's running by going to <a href="http://localhost:11434">http://localhost:11434</a> and

View File

@@ -28,125 +28,160 @@
where you can track your daily weight. This data is then used in <a href="#root/_help_R7abl2fc6Mxi">Weight tracker</a>.</p>
<h2>Week Note and Quarter Note</h2>
<p>Week and quarter notes are disabled by default, since it might be too
much for some people. To enable them, you need to set <code>#enableWeekNote</code> and <code>#enableQuarterNote</code> attributes
on the root calendar note, which is identified by <code>#calendarRoot</code> label.
Week note is affected by the first week of year option. Be careful when
you already have some week notes created, it will not automatically change
the existing week notes and might lead to some duplicates.</p>
much for some people. To enable them, you need to set <code spellcheck="false">#enableWeekNote</code> and
<code
spellcheck="false">#enableQuarterNote</code>attributes on the root calendar note, which is
identified by <code spellcheck="false">#calendarRoot</code> label. Week note
is affected by the first week of year option. Be careful when you already
have some week notes created, it will not automatically change the existing
week notes and might lead to some duplicates.</p>
<h2>Templates</h2>
<p>Trilium provides <a href="#root/_help_KC1HB96bqqHX">template</a> functionality,
and it could be used together with day notes.</p>
<p>You can define one of the following relations on the root of the journal
(identified by <code>#calendarRoot</code> label):</p>
(identified by <code spellcheck="false">#calendarRoot</code> label):</p>
<ul>
<li>yearTemplate</li>
<li>quarterTemplate (if <code>#enableQuarterNote</code> is set)</li>
<li>quarterTemplate (if <code spellcheck="false">#enableQuarterNote</code> is
set)</li>
<li>monthTemplate</li>
<li>weekTemplate (if <code>#enableWeekNote</code> is set)</li>
<li>weekTemplate (if <code spellcheck="false">#enableWeekNote</code> is set)</li>
<li>dateTemplate</li>
</ul>
<p>All of these are relations. When Trilium creates a new note for year or
month or date, it will take a look at the root and attach a corresponding <code>~template</code> relation
to the newly created role. Using this, you can e.g. create your daily template
with e.g. checkboxes for daily routine etc.</p>
month or date, it will take a look at the root and attach a corresponding
<code
spellcheck="false">~template</code>relation to the newly created role. Using this, you can
e.g. create your daily template with e.g. checkboxes for daily routine
etc.</p>
<h3>Migrate from old template usage</h3>
<p>If you have been using Journal prior to version v0.93.0, the previous
template pattern likely used was <code>~child:template=</code>.
template pattern likely used was <code spellcheck="false">~child:template=</code>.
<br>To transition to the new system:</p>
<ol>
<li>Set up the new template pattern in the Calendar root note.</li>
<li>Use <a href="#root/_help_ivYnonVFBxbQ">Bulk Actions</a> to remove <code>child:template</code> and <code>child:child:template</code> from
all notes under the Journal (calendar root).</li>
<li>Use <a href="#root/_help_ivYnonVFBxbQ">Bulk Actions</a> to remove <code spellcheck="false">child:template</code> and
<code
spellcheck="false">child:child:template</code>from all notes under the Journal (calendar
root).</li>
<li>Ensure that all old template patterns are fully removed to prevent conflicts
with the new setup.</li>
</ol>
<h2>Naming pattern</h2>
<p>You can customize the title of generated journal notes by defining a <code>#datePattern</code>, <code>#weekPattern</code>, <code>#monthPattern</code>, <code>#quarterPattern</code> and <code>#yearPattern</code> attribute
on a root calendar note (identified by <code>#calendarRoot</code> label).
The naming pattern replacements follow a level-up compatibility - each
level can use replacements from itself and all levels above it. For example, <code>#monthPattern</code> can
use month, quarter and year replacements, while <code>#weekPattern</code> can
use week, month, quarter and year replacements. But it is not possible
to use week replacements in <code>#monthPattern</code>.</p>
<p>You can customize the title of generated journal notes by defining a
<code
spellcheck="false">#datePattern</code>, <code spellcheck="false">#weekPattern</code>,
<code
spellcheck="false">#monthPattern</code>, <code spellcheck="false">#quarterPattern</code> and
<code
spellcheck="false">#yearPattern</code>attribute on a root calendar note (identified by
<code
spellcheck="false">#calendarRoot</code>label). The naming pattern replacements follow a level-up
compatibility - each level can use replacements from itself and all levels
above it. For example, <code spellcheck="false">#monthPattern</code> can
use month, quarter and year replacements, while <code spellcheck="false">#weekPattern</code> can
use week, month, quarter and year replacements. But it is not possible
to use week replacements in <code spellcheck="false">#monthPattern</code>.</p>
<h3>Date pattern</h3>
<p>It's possible to customize the title of generated date notes by defining
a <code>#datePattern</code> attribute on a root calendar note (identified
by <code>#calendarRoot</code> label). Following are possible values:</p>
a <code spellcheck="false">#datePattern</code> attribute on a root calendar
note (identified by <code spellcheck="false">#calendarRoot</code> label).
Following are possible values:</p>
<ul>
<li><code>{isoDate}</code> results in an ISO 8061 formatted date (e.g. "2025-03-09"
for March 9, 2025)</li>
<li><code>{dateNumber}</code> results in a number like <code>9</code> for the
9th day of the month, <code>11</code> for the 11th day of the month</li>
<li><code>{dateNumberPadded}</code> results in a number like <code>09</code> for
the 9th day of the month, <code>11</code> for the 11th day of the month</li>
<li><code>{ordinal}</code> is replaced with the ordinal date (e.g. 1st, 2nd,
3rd) etc.</li>
<li><code>{weekDay}</code> results in the full day name (e.g. <code>Monday</code>)</li>
<li><code>{weekDay3}</code> is replaced with the first 3 letters of the day,
e.g. Mon, Tue, etc.</li>
<li><code>{weekDay2}</code> is replaced with the first 2 letters of the day,
e.g. Mo, Tu, etc.</li>
<li><code spellcheck="false">{isoDate}</code> results in an ISO 8061 formatted
date (e.g. "2025-03-09" for March 9, 2025)</li>
<li><code spellcheck="false">{dateNumber}</code> results in a number like
<code
spellcheck="false">9</code>for the 9th day of the month, <code spellcheck="false">11</code> for
the 11th day of the month</li>
<li><code spellcheck="false">{dateNumberPadded}</code> results in a number
like <code spellcheck="false">09</code> for the 9th day of the month,
<code
spellcheck="false">11</code>for the 11th day of the month</li>
<li><code spellcheck="false">{ordinal}</code> is replaced with the ordinal
date (e.g. 1st, 2nd, 3rd) etc.</li>
<li><code spellcheck="false">{weekDay}</code> results in the full day name
(e.g. <code spellcheck="false">Monday</code>)</li>
<li><code spellcheck="false">{weekDay3}</code> is replaced with the first 3
letters of the day, e.g. Mon, Tue, etc.</li>
<li><code spellcheck="false">{weekDay2}</code> is replaced with the first 2
letters of the day, e.g. Mo, Tu, etc.</li>
</ul>
<p>The default is <code>{dateNumberPadded} - {weekDay}</code>
<p>The default is <code spellcheck="false">{dateNumberPadded} - {weekDay}</code>
</p>
<h3>Week pattern</h3>
<p>It is also possible to customize the title of generated week notes through
the <code>#weekPattern</code> attribute on the root calendar note. The options
are:</p>
the <code spellcheck="false">#weekPattern</code> attribute on the root calendar
note. The options are:</p>
<ul>
<li><code>{weekNumber}</code> results in a number like <code>9</code> for the
9th week of the year, <code>11</code> for the 11th week of the year</li>
<li><code>{weekNumberPadded}</code> results in a number like <code>09</code> for
the 9th week of the year, <code>11</code> for the 11th week of the year</li>
<li><code>{shortWeek}</code> results in a short week string like <code>W9</code> for
the 9th week of the year, <code>W11</code> for the 11th week of the year</li>
<li><code>{shortWeek3}</code> results in a short week string like <code>W09</code> for
the 9th week of the year, <code>W11</code> for the 11th week of the year</li>
<li><code spellcheck="false">{weekNumber}</code> results in a number like
<code
spellcheck="false">9</code>for the 9th week of the year, <code spellcheck="false">11</code> for
the 11th week of the year</li>
<li><code spellcheck="false">{weekNumberPadded}</code> results in a number
like <code spellcheck="false">09</code> for the 9th week of the year,
<code
spellcheck="false">11</code>for the 11th week of the year</li>
<li><code spellcheck="false">{shortWeek}</code> results in a short week string
like <code spellcheck="false">W9</code> for the 9th week of the year,
<code
spellcheck="false">W11</code>for the 11th week of the year</li>
<li><code spellcheck="false">{shortWeek3}</code> results in a short week string
like <code spellcheck="false">W09</code> for the 9th week of the year,
<code
spellcheck="false">W11</code>for the 11th week of the year</li>
</ul>
<p>The default is <code>Week {weekNumber}</code>
<p>The default is <code spellcheck="false">Week {weekNumber}</code>
</p>
<h3>Month pattern</h3>
<p>It is also possible to customize the title of generated month notes through
the <code>#monthPattern</code> attribute on the root calendar note. The options
are:</p>
the <code spellcheck="false">#monthPattern</code> attribute on the root calendar
note. The options are:</p>
<ul>
<li><code>{isoMonth}</code> results in an ISO 8061 formatted month (e.g. "2025-03"
for March 2025)</li>
<li><code>{monthNumber}</code> results in a number like <code>9</code> for September,
and <code>11</code> for November</li>
<li><code>{monthNumberPadded}</code> results in a number like <code>09</code> for
September, and <code>11</code> for November</li>
<li><code>{month}</code> results in the full month name (e.g. <code>September</code> or <code>October</code>)</li>
<li><code>{shortMonth3}</code> is replaced with the first 3 letters of the
month, e.g. Jan, Feb, etc.</li>
<li><code>{shortMonth4}</code> is replaced with the first 4 letters of the
month, e.g. Sept, Octo, etc.</li>
<li><code spellcheck="false">{isoMonth}</code> results in an ISO 8061 formatted
month (e.g. "2025-03" for March 2025)</li>
<li><code spellcheck="false">{monthNumber}</code> results in a number like
<code
spellcheck="false">9</code>for September, and <code spellcheck="false">11</code> for November</li>
<li><code spellcheck="false">{monthNumberPadded}</code> results in a number
like <code spellcheck="false">09</code> for September, and <code spellcheck="false">11</code> for
November</li>
<li><code spellcheck="false">{month}</code> results in the full month name
(e.g. <code spellcheck="false">September</code> or <code spellcheck="false">October</code>)</li>
<li><code spellcheck="false">{shortMonth3}</code> is replaced with the first
3 letters of the month, e.g. Jan, Feb, etc.</li>
<li><code spellcheck="false">{shortMonth4}</code> is replaced with the first
4 letters of the month, e.g. Sept, Octo, etc.</li>
</ul>
<p>The default is <code>{monthNumberPadded} - {month}</code>
<p>The default is <code spellcheck="false">{monthNumberPadded} - {month}</code>
</p>
<h3>Quarter pattern</h3>
<p>It is also possible to customize the title of generated quarter notes
through the <code>#quarterPattern</code> attribute on the root calendar note.
The options are:</p>
through the <code spellcheck="false">#quarterPattern</code> attribute on
the root calendar note. The options are:</p>
<ul>
<li><code>{quarterNumber}</code> results in a number like <code>1</code> for
the 1st quarter of the year</li>
<li><code>{shortQuarter}</code> results in a short quarter string like <code>Q1</code> for
the 1st quarter of the year</li>
<li><code spellcheck="false">{quarterNumber}</code> results in a number like
<code
spellcheck="false">1</code>for the 1st quarter of the year</li>
<li><code spellcheck="false">{shortQuarter}</code> results in a short quarter
string like <code spellcheck="false">Q1</code> for the 1st quarter of the
year</li>
</ul>
<p>The default is <code>Quarter {quarterNumber}</code>
<p>The default is <code spellcheck="false">Quarter {quarterNumber}</code>
</p>
<h3>Year pattern</h3>
<p>It is also possible to customize the title of generated year notes through
the <code>#yearPattern</code> attribute on the root calendar note. The options
are:</p>
the <code spellcheck="false">#yearPattern</code> attribute on the root calendar
note. The options are:</p>
<ul>
<li><code>{year}</code> results in the full year (e.g. <code>2025</code>)</li>
<li><code spellcheck="false">{year}</code> results in the full year (e.g.
<code
spellcheck="false">2025</code>)</li>
</ul>
<p>The default is <code>{year}</code>
<p>The default is <code spellcheck="false">{year}</code>
</p>
<h2>Implementation</h2>
<p>Trilium has some special support for day notes in the form of <a href="https://triliumnext.github.io/Notes/backend_api/BackendScriptApi.html">backend Script API</a> -
see e.g. getDayNote() function.</p>
<p>Day (and year, month) notes are created with a label - e.g. <code>#dateNote="2025-03-09"</code> this
<p>Day (and year, month) notes are created with a label - e.g. <code spellcheck="false">#dateNote="2025-03-09"</code> this
can then be used by other scripts to add new notes to day note etc.</p>

View File

@@ -14,13 +14,13 @@
note and doneDate note (with <a href="#root/_help_kBrnXNG3Hplm">prefix</a> of either
"TODO" or "DONE").</p>
<h2>Implementation</h2>
<p>New tasks are created in the TODO note which has <code>~child:template</code>
<p>New tasks are created in the TODO note which has <code spellcheck="false">~child:template</code>
<a
href="#root/_help_zEY4DaJG4YT5">relation</a>(see <a href="#root/_help_bwZpz2ajCEwO">attribute inheritance</a>)
pointing to the task template.</p>
<h3>Attributes</h3>
<p>Task template defines several <a href="#root/_help_OFXdgB2nNk1F">promoted attributes</a> -
todoDate, doneDate, tags, location. Importantly it also defines <code>~runOnAttributeChange</code> relation
todoDate, doneDate, tags, location. Importantly it also defines <code spellcheck="false">~runOnAttributeChange</code> relation
- <a href="#root/_help_GPERMystNGTB">event</a> handler which is run on attribute
change. This <a href="#root/_help_CdNpE2pqjmI6">script</a> handles when e.g. we
fill out the doneDate attribute - meaning the task is done and should be
@@ -56,10 +56,10 @@
span.fancytree-node.done .fancytree-title {
color: green !important;
}</code></pre>
<p>This <a href="#root/_help_6f9hih2hXXZk">code note</a> has <code>#appCss</code>
<p>This <a href="#root/_help_6f9hih2hXXZk">code note</a> has <code spellcheck="false">#appCss</code>
<a
href="#root/_help_zEY4DaJG4YT5">label</a>which is recognized by Trilium on startup and loaded as CSS into
the application.</p>
<p>Second part of this functionality is based in event handler described
above which assigns <code>#cssClass</code> label to the task to either "done"
or "todo" based on the task status.</p>
above which assigns <code spellcheck="false">#cssClass</code> label to the
task to either "done" or "todo" based on the task status.</p>

View File

@@ -1,28 +1,31 @@
<p>
<img src="Weight Tracker_image.png">
</p>
<p>The <code>Weight Tracker</code> is a <a href="#root/_help_GLks18SNjxmC">Script API</a> showcase
<p>The <code spellcheck="false">Weight Tracker</code> is a <a href="#root/_help_GLks18SNjxmC">Script API</a> showcase
present in the <a href="#root/_help_wX4HbRucYSDD">demo notes</a>.</p>
<p>By adding <code>weight</code> as a <a href="#root/_help_OFXdgB2nNk1F">promoted attribute</a> in
<p>By adding <code spellcheck="false">weight</code> as a <a href="#root/_help_OFXdgB2nNk1F">promoted attribute</a> in
the <a href="#root/_help_KC1HB96bqqHX">template</a> from which <a href="#root/_help_l0tKav7yLHGF">day notes</a> are
created, you can aggregate the data and plot weight change over time.</p>
<h2>Implementation</h2>
<p>The <code>Weight Tracker</code> note in the screenshot above is of the type <code>Render Note</code>.
That type of note doesn't have any useful content itself. Instead it is
a placeholder where a <a href="#root/_help_CdNpE2pqjmI6">script</a> can render
its output.</p>
<p>Scripts for <code>Render Notes</code> are defined in a <a href="#root/_help_zEY4DaJG4YT5">relation</a> called <code>~renderNote</code>.
In this example, it's the <code>Weight Tracker</code>'s child <code>Implementation</code>.
The Implementation consists of two <a href="#root/_help_6f9hih2hXXZk">code notes</a> that
contain some HTML and JavaScript respectively, which load all the notes
with a <code>weight</code> attribute and display their values in a chart.</p>
<p>The <code spellcheck="false">Weight Tracker</code> note in the screenshot
above is of the type <code spellcheck="false">Render Note</code>. That type
of note doesn't have any useful content itself. Instead it is a placeholder
where a <a href="#root/_help_CdNpE2pqjmI6">script</a> can render its output.</p>
<p>Scripts for <code spellcheck="false">Render Notes</code> are defined in
a <a href="#root/_help_zEY4DaJG4YT5">relation</a> called <code spellcheck="false">~renderNote</code>.
In this example, it's the <code spellcheck="false">Weight Tracker</code>'s
child <code spellcheck="false">Implementation</code>. The Implementation
consists of two <a href="#root/_help_6f9hih2hXXZk">code notes</a> that contain
some HTML and JavaScript respectively, which load all the notes with a
<code
spellcheck="false">weight</code>attribute and display their values in a chart.</p>
<p>To actually render the chart, we're using a third party library called
<a
href="https://www.chartjs.org/">chart.js</a>which is imported as an attachment, since it's not built into
Trilium.</p>
<h3>Code</h3>
<p>Here's the content of the script which is placed in a <a href="#root/_help_6f9hih2hXXZk">code note</a> of
type <code>JS Frontend</code>:</p><pre><code class="language-text-x-trilium-auto">async function getChartData() {
type <code spellcheck="false">JS Frontend</code>:</p><pre><code class="language-text-x-trilium-auto">async function getChartData() {
const days = await api.runOnBackend(async () =&gt; {
const notes = api.getNotesWithLabel('weight');
const days = [];
@@ -68,6 +71,7 @@ new chartjs.Chart(ctx, {
data: await getChartData()
});</code></pre>
<h2>How to remove the Weight Tracker button from the top bar</h2>
<p>In the link map of the <code>Weight Tracker</code>, there is a note called <code>Button</code>.
Open it and delete or comment out its contents. The <code>Weight Tracker</code> button
<p>In the link map of the <code spellcheck="false">Weight Tracker</code>,
there is a note called <code spellcheck="false">Button</code>. Open it and
delete or comment out its contents. The <code spellcheck="false">Weight Tracker</code> button
will disappear after you restart Trilium.</p>

View File

@@ -29,9 +29,9 @@
<ol>
<li><strong>System attributes</strong>
<br>As the name suggest, these attributes have a special meaning since they
are interpreted by Trilium. For example the <code>color</code> attribute
are interpreted by Trilium. For example the <code spellcheck="false">color</code> attribute
will change the color of the note as displayed in the&nbsp;<a class="reference-link"
href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;and links, and <code>iconClass</code> will
href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;and links, and <code spellcheck="false">iconClass</code> will
change the icon of a note.</li>
<li><strong>User-defined attributes</strong>
<br>These are free-form labels or relations that can be used by the user.
@@ -43,8 +43,8 @@
<p>In practice, Trilium makes no direct distinction of whether an attribute
is a system one or a user-defined one. A label or relation is considered
a system attribute if it matches one of the built-in names (e.g. like the
aforementioned <code>iconClass</code>). Keep this in mind when creating
&nbsp;<a class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>&nbsp;in
aforementioned <code spellcheck="false">iconClass</code>). Keep this in
mind when creating &nbsp;<a class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>&nbsp;in
order not to accidentally alter a system attribute (unless intended).</p>
<h2>Viewing the list of attributes</h2>
<p>Both the labels and relations for the current note are displayed in the <em>Owned Attributes</em> section
@@ -52,8 +52,8 @@
where they can be viewed and edited. Inherited attributes are displayed
in the <em>Inherited Attributes</em> section of the ribbon, where they can
only be viewed.</p>
<p>In the list of attributes, labels are prefixed with the <code>#</code> character
whereas relations are prefixed with the <code>~</code> character.</p>
<p>In the list of attributes, labels are prefixed with the <code spellcheck="false">#</code> character
whereas relations are prefixed with the <code spellcheck="false">~</code> character.</p>
<h2>Attribute Definitions and Promoted Attributes</h2>
<p><a class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>&nbsp;create
a form-like editing experience for attributes, which makes it easy to enhancing

View File

@@ -3,43 +3,49 @@
generally in parent-child relations (or anywhere if using templates).</p>
<h2>Standard Inheritance</h2>
<p>In Trilium, attributes can be automatically inherited by child notes if
they have the <code>isInheritable</code> flag set to <code>true</code>. This
means the attribute (a key-value pair) is applied to the note and all its
descendants.</p>
they have the <code spellcheck="false">isInheritable</code> flag set to
<code
spellcheck="false">true</code>. This means the attribute (a key-value pair) is applied to
the note and all its descendants.</p>
<p>To make an attribute inheritable, simply use the visual editor for&nbsp;
<a
class="reference-link" href="#root/_help_HI6GBBIduIgv">Labels</a>&nbsp;or&nbsp;<a class="reference-link" href="#root/_help_Cq5X6iKQop6R">Relations</a>.
Alternatively, the attribute can be manually defined where <code>#myLabel=value</code> becomes <code>#myLabel(inheritable)=value</code> when
inheritable.</p>
<p>As an example, the <code>archived</code> label can be set to be inheritable,
allowing you to hide a whole subtree of notes from searches and other dialogs
by applying this label at the top level.</p>
Alternatively, the attribute can be manually defined where <code spellcheck="false">#myLabel=value</code> becomes
<code
spellcheck="false">#myLabel(inheritable)=value</code>when inheritable.</p>
<p>As an example, the <code spellcheck="false">archived</code> label can be
set to be inheritable, allowing you to hide a whole subtree of notes from
searches and other dialogs by applying this label at the top level.</p>
<p>Standard inheritance forces all the notes that are children (and sub-children)
of a note to have that particular label or relation. If there is a need
to have some notes not inherit one of the labels, then <em>copying inheritance</em> or <em>template inheritance</em> needs
to be used instead.</p>
<h2>Copying Inheritance</h2>
<p>Copying inheritance differs from standard inheritance by using a <code>child:</code> prefix
in the attribute name. This prefix causes new child notes to automatically
receive specific attributes from the parent note. These attributes are
independent of the parent and will persist even if the note is moved elsewhere.</p>
<p>If a parent note has the label <code>#child:exampleAttribute</code>, all
newly created child notes (one level deep) will inherit the <code>#exampleAttribute</code> label.
<p>Copying inheritance differs from standard inheritance by using a
<code
spellcheck="false">child:</code>prefix in the attribute name. This prefix causes new child
notes to automatically receive specific attributes from the parent note.
These attributes are independent of the parent and will persist even if
the note is moved elsewhere.</p>
<p>If a parent note has the label <code spellcheck="false">#child:exampleAttribute</code>,
all newly created child notes (one level deep) will inherit the <code spellcheck="false">#exampleAttribute</code> label.
This can be useful for setting default properties for notes in a specific
section.</p>
<p>Similarly, for relations use <code>~child:myRelation</code>.</p>
<p>Similarly, for relations use <code spellcheck="false">~child:myRelation</code>.</p>
<p>Due to the way it's designed, copying inheritance cannot be used to cascade
infinitely within a hierarchy. For that use case, consider using either
standard inheritance or templates.</p>
<h3>Chained inheritance</h3>
<p>It is possible to define labels across multiple levels of depth. For example, <code>#child:child:child:foo</code> applied
to a root note would create:</p>
<p>It is possible to define labels across multiple levels of depth. For example,
<code
spellcheck="false">#child:child:child:foo</code>applied to a root note would create:</p>
<ul>
<li><code>#child:child:foo</code> on the first-level children.</li>
<li><code>#child:foo</code> on the second-level children.</li>
<li><code>#foo</code> on the third-level children.</li>
<li><code spellcheck="false">#child:child:foo</code> on the first-level children.</li>
<li><code spellcheck="false">#child:foo</code> on the second-level children.</li>
<li><code spellcheck="false">#foo</code> on the third-level children.</li>
</ul>
<p>Similarly, use <code>~child:child:child:foo</code> if dealing with relations.</p>
<p>Similarly, use <code spellcheck="false">~child:child:child:foo</code> if
dealing with relations.</p>
<p>Do note that same as simple copying inheritance, the changes will not
apply retroactively to existing notes in the hierarchy, it will only apply
to the newly created notes.</p>

View File

@@ -3,10 +3,11 @@
<h2>Common use cases</h2>
<ul>
<li><strong>Metadata for personal use</strong>: Assign labels with optional
values for categorization, such as <code>#year=1999</code>, <code>#genre="sci-fi"</code>,
or <code>#author="Neal Stephenson"</code>. This can be combined with&nbsp;
<a
class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>&nbsp;to make their display more user-friendly.</li>
values for categorization, such as <code spellcheck="false">#year=1999</code>,
<code
spellcheck="false">#genre="sci-fi"</code>, or <code spellcheck="false">#author="Neal Stephenson"</code>.
This can be combined with&nbsp;<a class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>&nbsp;to
make their display more user-friendly.</li>
<li><strong>Configuration</strong>: Labels can configure advanced features
or settings (see reference below).</li>
<li><strong>Scripts and Plugins</strong>: Used to tag notes with special metadata,
@@ -36,21 +37,24 @@
<p>In the <em>Owned Attributes</em> section in the&nbsp;<a class="reference-link"
href="#root/_help_BlN9DFI679QC">Ribbon</a>:</p>
<ul>
<li>To create a label called <code>myLabel</code> with no value, simply type <code>#myLabel</code>.</li>
<li>To create a label called <code>myLabel</code> with a value <code>value</code>,
simply type <code>#myLabel=value</code>.</li>
<li>If the value contains spaces, then the text must be quoted: <code>#myLabel="Hello world"</code>.</li>
<li>To create a label called <code spellcheck="false">myLabel</code> with no
value, simply type <code spellcheck="false">#myLabel</code>.</li>
<li>To create a label called <code spellcheck="false">myLabel</code> with a
value <code spellcheck="false">value</code>, simply type <code spellcheck="false">#myLabel=value</code>.</li>
<li>If the value contains spaces, then the text must be quoted: <code spellcheck="false">#myLabel="Hello world"</code>.</li>
<li>If the string contains quotes (regardless of whether it has spaces), then
the text must be quoted with apostrophes instead: <code>#myLabel='Hello "world"'</code>.</li>
<li>To create an inheritable label called <code>myLabel</code>, simply write <code>#myLabel(inheritable)</code> for
no value or <code>#myLabel(inheritable)=value</code> if there is a value.</li>
the text must be quoted with apostrophes instead: <code spellcheck="false">#myLabel='Hello "world"'</code>.</li>
<li>To create an inheritable label called <code spellcheck="false">myLabel</code>,
simply write <code spellcheck="false">#myLabel(inheritable)</code> for no
value or <code spellcheck="false">#myLabel(inheritable)=value</code> if there
is a value.</li>
</ul>
<h2>Predefined labels</h2>
<p>This is a list of labels that Trilium natively supports.</p>
<aside class="admonition tip">
<p>Some labels presented here end with a <code>*</code>. That means that there
are multiple labels with the same prefix, consult the specific page linked
in the description of that label for more information.</p>
<p>Some labels presented here end with a <code spellcheck="false">*</code>.
That means that there are multiple labels with the same prefix, consult
the specific page linked in the description of that label for more information.</p>
</aside>
<table class="ck-table-resized">
<colgroup>

View File

@@ -47,8 +47,9 @@
</ol>
<h2>How attribute definitions actually work</h2>
<p>When a new promoted attribute definition is created, it creates a corresponding
label prefixed with either <code>label</code> or <code>relation</code>, depending
on the definition type:</p><pre><code class="language-text-x-trilium-auto">#label:myColor(inheritable)="promoted,alias=Color,multi,color"</code></pre>
label prefixed with either <code spellcheck="false">label</code> or
<code
spellcheck="false">relation</code>, depending on the definition type:</p><pre><code class="language-text-x-trilium-auto">#label:myColor(inheritable)="promoted,alias=Color,multi,color"</code></pre>
<p>The only purpose of the attribute definition is to set up a template.
If the attribute was marked as promoted, then it's also displayed to the
user for easy editing.</p>
@@ -97,7 +98,7 @@
make use of this practice, for example:
<ul>
<li>Calendars add “Start Date”, “End Date”, “Start Time” and “End Time” as
promoted attributes. These map to system attributes such as <code>startDate</code> which
promoted attributes. These map to system attributes such as <code spellcheck="false">startDate</code> which
are then interpreted by the calendar view.</li>
<li><a class="reference-link" href="#root/_help_zP3PMqaG71Ct">Presentation</a>&nbsp;adds
a “Background” promoted attribute for each of the slide to easily be able
@@ -105,29 +106,29 @@
</ul>
</li>
<li>The Trilium documentation (which is edited in Trilium) uses a promoted
attribute to be able to easily edit the <code>#shareAlias</code> (see&nbsp;
attribute to be able to easily edit the <code spellcheck="false">#shareAlias</code> (see&nbsp;
<a
class="reference-link" href="#root/_help_R9pX4DGra2Vt">Sharing</a>) in order to form clean URLs.</li>
<li>If you always edit a particular system attribute such as <code>#color</code>,
<li>If you always edit a particular system attribute such as <code spellcheck="false">#color</code>,
simply create a promoted attribute for it to make it easier.</li>
</ul>
<h3>Inverse relation</h3>
<p>Some relations always occur in pairs - my favorite example is on the family.
If you have a note representing husband and note representing wife, then
there might be a relation between those two of <code>isPartnerOf</code>.
there might be a relation between those two of <code spellcheck="false">isPartnerOf</code>.
This is bidirectional relationship - meaning that if a relation is pointing
from husband to wife then there should be always another relation pointing
from wife to husband.</p>
<p>Another example is with parent-child relationship. Again these always
occur in pairs, but in this case it's not exact same relation - the one
going from parent to child might be called <code>isParentOf</code> and the
other one going from child to parent might be called <code>isChildOf</code>.</p>
going from parent to child might be called <code spellcheck="false">isParentOf</code> and
the other one going from child to parent might be called <code spellcheck="false">isChildOf</code>.</p>
<p>Relation definition allows you to specify such "inverse relation" - for
the relation you just define you specify which is the inverse relation.
Note that in the second example we should have two relation definitions
- one for <code>isParentOf</code> which defines <code>isChildOf</code> as inverse
relation and then second relation definition for <code>isChildOf</code> which
defines <code>isParentOf</code> as inverse relation.</p>
- one for <code spellcheck="false">isParentOf</code> which defines <code spellcheck="false">isChildOf</code> as
inverse relation and then second relation definition for <code spellcheck="false">isChildOf</code> which
defines <code spellcheck="false">isParentOf</code> as inverse relation.</p>
<p>What this does internally is that whenever we save a relation which has
defined inverse relation, we check that this inverse relation exists on
the relation target note. Similarly, when we delete relation, we also delete

View File

@@ -39,27 +39,30 @@
<p>In the <em>Owned Attributes</em> section in the&nbsp;<a class="reference-link"
href="#root/_help_BlN9DFI679QC">Ribbon</a>:</p>
<ul>
<li>To create a relation called <code>myRelation</code>:
<li>To create a relation called <code spellcheck="false">myRelation</code>:
<ul>
<li>First type <code>~myRelation=@</code>.</li>
<li>First type <code spellcheck="false">~myRelation=@</code>.</li>
<li>After this, an autocompletion box should appear.</li>
<li>Type the title of the note to point to and press <kbd>Enter</kbd> to confirm
(or click the desired note).</li>
<li>Alternatively copy a note from the&nbsp;<a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;and
paste it after the <code>=</code> sign (without the <code>@</code>, in this
case).</li>
paste it after the <code spellcheck="false">=</code> sign (without the
<code
spellcheck="false">@</code>, in this case).</li>
</ul>
</li>
<li>To create an inheritable relation, follow the same steps as previously
described but instead of <code>~myRelation</code> write <code>~myRelation(inheritable)</code>.</li>
described but instead of <code spellcheck="false">~myRelation</code> write
<code
spellcheck="false">~myRelation(inheritable)</code>.</li>
</ul>
<h2>Predefined relations</h2>
<p>These relations are supported and used internally by Trilium.</p>
<aside
class="admonition tip">
<p>Some relations presented here end with a <code>*</code>. That means that
there are multiple relations with the same prefix, consult the specific
page linked in the description of that relation for more information.</p>
<p>Some relations presented here end with a <code spellcheck="false">*</code>.
That means that there are multiple relations with the same prefix, consult
the specific page linked in the description of that relation for more information.</p>
</aside>
<table>
<thead>
@@ -70,20 +73,20 @@ class="admonition tip">
</thead>
<tbody>
<tr>
<td><code>runOn*</code>
<td><code spellcheck="false">runOn*</code>
</td>
<td>See&nbsp;<a class="reference-link" href="#root/_help_GPERMystNGTB">Events</a>
</td>
</tr>
<tr>
<td><code>template</code>
<td><code spellcheck="false">template</code>
</td>
<td>note's attributes will be inherited even without a parent-child relationship,
note's content and subtree will be added to instance notes if empty. See
documentation for details.</td>
</tr>
<tr>
<td><code>inherit</code>
<td><code spellcheck="false">inherit</code>
</td>
<td>note's attributes will be inherited even without a parent-child relationship.
See&nbsp;<a class="reference-link" href="#root/_help_KC1HB96bqqHX">Templates</a>&nbsp;for
@@ -91,50 +94,53 @@ class="admonition tip">
the documentation.</td>
</tr>
<tr>
<td><code>renderNote</code>
<td><code spellcheck="false">renderNote</code>
</td>
<td>notes of type&nbsp;<a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a>&nbsp;will
be rendered using a code note (HTML or script) and it is necessary to point
using this relation to which note should be rendered</td>
</tr>
<tr>
<td><code>widget_relation</code>
<td><code spellcheck="false">widget_relation</code>
</td>
<td>target of this relation will be executed and rendered as a widget in the
sidebar</td>
</tr>
<tr>
<td><code>shareCss</code>
<td><code spellcheck="false">shareCss</code>
</td>
<td>CSS note which will be injected into the share page. CSS note must be
in the shared sub-tree as well. Consider using <code>share_hidden_from_tree</code> and <code>share_omit_default_css</code> as
well.</td>
in the shared sub-tree as well. Consider using <code spellcheck="false">share_hidden_from_tree</code> and
<code
spellcheck="false">share_omit_default_css</code>as well.</td>
</tr>
<tr>
<td><code>shareJs</code>
<td><code spellcheck="false">shareJs</code>
</td>
<td>JavaScript note which will be injected into the share page. JS note must
be in the shared sub-tree as well. Consider using <code>share_hidden_from_tree</code>.</td>
be in the shared sub-tree as well. Consider using <code spellcheck="false">share_hidden_from_tree</code>.</td>
</tr>
<tr>
<td><code>shareHtml</code>
<td><code spellcheck="false">shareHtml</code>
</td>
<td>HTML note which will be injected into the share page at locations specified
by the <code>shareHtmlLocation</code> label. HTML note must be in the shared
sub-tree as well. Consider using <code>share_hidden_from_tree</code>.</td>
by the <code spellcheck="false">shareHtmlLocation</code> label. HTML note
must be in the shared sub-tree as well. Consider using <code spellcheck="false">share_hidden_from_tree</code>.</td>
</tr>
<tr>
<td><code>shareTemplate</code>
<td><code spellcheck="false">shareTemplate</code>
</td>
<td>Embedded JavaScript note that will be used as the template for displaying
the shared note. Falls back to the default template. Consider using <code>share_hidden_from_tree</code>.</td>
the shared note. Falls back to the default template. Consider using
<code
spellcheck="false">share_hidden_from_tree</code>.</td>
</tr>
<tr>
<td><code>shareFavicon</code>
<td><code spellcheck="false">shareFavicon</code>
</td>
<td>Favicon note to be set in the shared page. Typically you want to set it
to share root and make it inheritable. Favicon note must be in the shared
sub-tree as well. Consider using <code>share_hidden_from_tree</code>.</td>
sub-tree as well. Consider using <code spellcheck="false">share_hidden_from_tree</code>.</td>
</tr>
</tbody>
</table>

View File

@@ -116,8 +116,9 @@
<a
class="reference-link" href="#root/_help_habiZ3HU8Kw8">FNote</a>, for example:
<ul>
<li><code>NEW: ${note.title}</code> will prefix all notes with <code>NEW:</code> .</li>
<li><code>${note.dateCreatedObj.format('MM-DD:')}: ${note.title}</code> will
<li><code spellcheck="false">NEW: ${note.title}</code> will prefix all notes
with <code spellcheck="false">NEW:</code> .</li>
<li><code spellcheck="false">${note.dateCreatedObj.format('MM-DD:')}: ${note.title}</code> will
prefix the note titles with each note's creation date (in month-day format).</li>
</ul>
</li>
@@ -155,12 +156,13 @@
<li>Examples:
<ul>
<li>
<p>To apply a suffix (<code>- suffix</code> in this example), to the note
title:</p><pre><code class="language-application-javascript-env-backend">note.title = note.title + " - suffix";</code></pre>
<p>To apply a suffix (<code spellcheck="false">- suffix</code> in this example),
to the note title:</p><pre><code class="language-application-javascript-env-backend">note.title = note.title + " - suffix";</code></pre>
</li>
<li>
<p>To alter attributes of a note based on another attribute, such as setting
the <code>#shareAlias</code> label to the title of the note:</p><pre><code class="language-application-javascript-env-backend">note.setLabel("shareAlias", note.title)</code></pre>
the <code spellcheck="false">#shareAlias</code> label to the title of the
note:</p><pre><code class="language-application-javascript-env-backend">note.setLabel("shareAlias", note.title)</code></pre>
</li>
</ul>
</li>

View File

@@ -1,11 +1,12 @@
<p>Trilium supports configuration via a file named <code>config.ini</code> and
<p>Trilium supports configuration via a file named <code spellcheck="false">config.ini</code> and
environment variables. This document provides a comprehensive reference
for all configuration options.</p>
<h2>Location of the configuration file</h2>
<p>The configuration file is not located in the same directory as the application.
Instead, the <code>config.ini</code> is located in the&nbsp;<a class="reference-link"
href="#root/_help_tAassRL4RSQL">Data directory</a>. As such, the configuration
file is only available after starting the application and creating a database.</p>
Instead, the <code spellcheck="false">config.ini</code> is located in the&nbsp;
<a
class="reference-link" href="#root/_help_tAassRL4RSQL">Data directory</a>. As such, the configuration file is only available
after starting the application and creating a database.</p>
<h2>Configuration Precedence</h2>
<p>Configuration values are loaded in the following order of precedence (highest
to lowest):</p>
@@ -18,13 +19,15 @@
</ol>
<h2>Environment Variable Patterns</h2>
<p>Trilium supports multiple environment variable patterns for flexibility.
The primary pattern is: <code>TRILIUM_[SECTION]_[KEY]</code>
The primary pattern is: <code spellcheck="false">TRILIUM_[SECTION]_[KEY]</code>
</p>
<p>Where:</p>
<ul>
<li><code>SECTION</code> is the INI section name in UPPERCASE</li>
<li><code>KEY</code> is the camelCase configuration key converted to UPPERCASE
(e.g., <code>instanceName</code><code>INSTANCENAME</code>)</li>
<li><code spellcheck="false">SECTION</code> is the INI section name in UPPERCASE</li>
<li><code spellcheck="false">KEY</code> is the camelCase configuration key
converted to UPPERCASE (e.g., <code spellcheck="false">instanceName</code>
<code
spellcheck="false">INSTANCENAME</code>)</li>
</ul>
<p>Additionally, shorter aliases are available for common configurations
(see Alternative Variables section below).</p>
@@ -41,35 +44,35 @@
</thead>
<tbody>
<tr>
<td><code>TRILIUM_GENERAL_INSTANCENAME</code>
<td><code spellcheck="false">TRILIUM_GENERAL_INSTANCENAME</code>
</td>
<td>string</td>
<td>""</td>
<td>Instance name for API identification</td>
</tr>
<tr>
<td><code>TRILIUM_GENERAL_NOAUTHENTICATION</code>
<td><code spellcheck="false">TRILIUM_GENERAL_NOAUTHENTICATION</code>
</td>
<td>boolean</td>
<td>false</td>
<td>Disable authentication (server only)</td>
</tr>
<tr>
<td><code>TRILIUM_GENERAL_NOBACKUP</code>
<td><code spellcheck="false">TRILIUM_GENERAL_NOBACKUP</code>
</td>
<td>boolean</td>
<td>false</td>
<td>Disable automatic backups</td>
</tr>
<tr>
<td><code>TRILIUM_GENERAL_NODESKTOPICON</code>
<td><code spellcheck="false">TRILIUM_GENERAL_NODESKTOPICON</code>
</td>
<td>boolean</td>
<td>false</td>
<td>Disable desktop icon creation</td>
</tr>
<tr>
<td><code>TRILIUM_GENERAL_READONLY</code>
<td><code spellcheck="false">TRILIUM_GENERAL_READONLY</code>
</td>
<td>boolean</td>
<td>false</td>
@@ -90,70 +93,70 @@
</thead>
<tbody>
<tr>
<td><code>TRILIUM_NETWORK_HOST</code>
<td><code spellcheck="false">TRILIUM_NETWORK_HOST</code>
</td>
<td>string</td>
<td>"0.0.0.0"</td>
<td>Server host binding</td>
</tr>
<tr>
<td><code>TRILIUM_NETWORK_PORT</code>
<td><code spellcheck="false">TRILIUM_NETWORK_PORT</code>
</td>
<td>string</td>
<td>"3000"</td>
<td>Server port</td>
</tr>
<tr>
<td><code>TRILIUM_NETWORK_HTTPS</code>
<td><code spellcheck="false">TRILIUM_NETWORK_HTTPS</code>
</td>
<td>boolean</td>
<td>false</td>
<td>Enable HTTPS</td>
</tr>
<tr>
<td><code>TRILIUM_NETWORK_CERTPATH</code>
<td><code spellcheck="false">TRILIUM_NETWORK_CERTPATH</code>
</td>
<td>string</td>
<td>""</td>
<td>SSL certificate path</td>
</tr>
<tr>
<td><code>TRILIUM_NETWORK_KEYPATH</code>
<td><code spellcheck="false">TRILIUM_NETWORK_KEYPATH</code>
</td>
<td>string</td>
<td>""</td>
<td>SSL key path</td>
</tr>
<tr>
<td><code>TRILIUM_NETWORK_TRUSTEDREVERSEPROXY</code>
<td><code spellcheck="false">TRILIUM_NETWORK_TRUSTEDREVERSEPROXY</code>
</td>
<td>boolean/string</td>
<td>false</td>
<td>Reverse proxy trust settings</td>
</tr>
<tr>
<td><code>TRILIUM_NETWORK_CORSALLOWORIGIN</code>
<td><code spellcheck="false">TRILIUM_NETWORK_CORSALLOWORIGIN</code>
</td>
<td>string</td>
<td>""</td>
<td>CORS allowed origins</td>
</tr>
<tr>
<td><code>TRILIUM_NETWORK_CORSALLOWMETHODS</code>
<td><code spellcheck="false">TRILIUM_NETWORK_CORSALLOWMETHODS</code>
</td>
<td>string</td>
<td>""</td>
<td>CORS allowed methods</td>
</tr>
<tr>
<td><code>TRILIUM_NETWORK_CORSALLOWHEADERS</code>
<td><code spellcheck="false">TRILIUM_NETWORK_CORSALLOWHEADERS</code>
</td>
<td>string</td>
<td>""</td>
<td>CORS allowed headers</td>
</tr>
<tr>
<td><code>TRILIUM_NETWORK_CORSRESOURCEPOLICY</code>
<td><code spellcheck="false">TRILIUM_NETWORK_CORSRESOURCEPOLICY</code>
</td>
<td>string</td>
<td>same-origin</td>
@@ -175,7 +178,7 @@
</thead>
<tbody>
<tr>
<td><code>TRILIUM_SESSION_COOKIEMAXAGE</code>
<td><code spellcheck="false">TRILIUM_SESSION_COOKIEMAXAGE</code>
</td>
<td>integer</td>
<td>1814400</td>
@@ -196,21 +199,21 @@
</thead>
<tbody>
<tr>
<td><code>TRILIUM_SYNC_SYNCSERVERHOST</code>
<td><code spellcheck="false">TRILIUM_SYNC_SYNCSERVERHOST</code>
</td>
<td>string</td>
<td>""</td>
<td>Sync server host URL</td>
</tr>
<tr>
<td><code>TRILIUM_SYNC_SYNCSERVERTIMEOUT</code>
<td><code spellcheck="false">TRILIUM_SYNC_SYNCSERVERTIMEOUT</code>
</td>
<td>string</td>
<td>"120000"</td>
<td>Sync server timeout in milliseconds</td>
</tr>
<tr>
<td><code>TRILIUM_SYNC_SYNCPROXY</code>
<td><code spellcheck="false">TRILIUM_SYNC_SYNCPROXY</code>
</td>
<td>string</td>
<td>""</td>
@@ -231,42 +234,42 @@
</thead>
<tbody>
<tr>
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL</code>
<td><code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL</code>
</td>
<td>string</td>
<td>""</td>
<td>OAuth/OpenID base URL</td>
</tr>
<tr>
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID</code>
<td><code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID</code>
</td>
<td>string</td>
<td>""</td>
<td>OAuth client ID</td>
</tr>
<tr>
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET</code>
<td><code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET</code>
</td>
<td>string</td>
<td>""</td>
<td>OAuth client secret</td>
</tr>
<tr>
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL</code>
<td><code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL</code>
</td>
<td>string</td>
<td>"<a href="https://accounts.google.com">https://accounts.google.com</a>"</td>
<td>OAuth issuer base URL</td>
</tr>
<tr>
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME</code>
<td><code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME</code>
</td>
<td>string</td>
<td>"Google"</td>
<td>OAuth issuer display name</td>
</tr>
<tr>
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON</code>
<td><code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON</code>
</td>
<td>string</td>
<td>""</td>
@@ -287,7 +290,7 @@
</thead>
<tbody>
<tr>
<td><code>TRILIUM_LOGGING_RETENTIONDAYS</code>
<td><code spellcheck="false">TRILIUM_LOGGING_RETENTIONDAYS</code>
</td>
<td>integer</td>
<td>90</td>
@@ -301,38 +304,59 @@
and work identically to their longer counterparts:</p>
<h3>Network CORS Variables</h3>
<ul>
<li><code>TRILIUM_NETWORK_CORS_ALLOW_ORIGIN</code> (alternative to <code>TRILIUM_NETWORK_CORSALLOWORIGIN</code>)</li>
<li><code>TRILIUM_NETWORK_CORS_ALLOW_METHODS</code> (alternative to <code>TRILIUM_NETWORK_CORSALLOWMETHODS</code>)</li>
<li><code>TRILIUM_NETWORK_CORS_ALLOW_HEADERS</code> (alternative to <code>TRILIUM_NETWORK_CORSALLOWHEADERS</code>)</li>
<li><code>TRILIUM_NETWORK_CORS_RESOURCE_POLICY</code> (alternative to <code>TRILIUM_NETWORK_CORSRESOURCEPOLICY</code>)</li>
<li><code spellcheck="false">TRILIUM_NETWORK_CORS_ALLOW_ORIGIN</code> (alternative
to <code spellcheck="false">TRILIUM_NETWORK_CORSALLOWORIGIN</code>)</li>
<li><code spellcheck="false">TRILIUM_NETWORK_CORS_ALLOW_METHODS</code> (alternative
to <code spellcheck="false">TRILIUM_NETWORK_CORSALLOWMETHODS</code>)</li>
<li><code spellcheck="false">TRILIUM_NETWORK_CORS_ALLOW_HEADERS</code> (alternative
to <code spellcheck="false">TRILIUM_NETWORK_CORSALLOWHEADERS</code>)</li>
<li><code spellcheck="false">TRILIUM_NETWORK_CORS_RESOURCE_POLICY</code> (alternative
to <code spellcheck="false">TRILIUM_NETWORK_CORSRESOURCEPOLICY</code>)</li>
</ul>
<h3>Sync Variables</h3>
<ul>
<li><code>TRILIUM_SYNC_SERVER_HOST</code> (alternative to <code>TRILIUM_SYNC_SYNCSERVERHOST</code>)</li>
<li><code>TRILIUM_SYNC_SERVER_TIMEOUT</code> (alternative to <code>TRILIUM_SYNC_SYNCSERVERTIMEOUT</code>)</li>
<li><code>TRILIUM_SYNC_SERVER_PROXY</code> (alternative to <code>TRILIUM_SYNC_SYNCPROXY</code>)</li>
<li><code spellcheck="false">TRILIUM_SYNC_SERVER_HOST</code> (alternative to
<code
spellcheck="false">TRILIUM_SYNC_SYNCSERVERHOST</code>)</li>
<li><code spellcheck="false">TRILIUM_SYNC_SERVER_TIMEOUT</code> (alternative
to <code spellcheck="false">TRILIUM_SYNC_SYNCSERVERTIMEOUT</code>)</li>
<li><code spellcheck="false">TRILIUM_SYNC_SERVER_PROXY</code> (alternative
to <code spellcheck="false">TRILIUM_SYNC_SYNCPROXY</code>)</li>
</ul>
<h3>OAuth/MFA Variables</h3>
<ul>
<li><code>TRILIUM_OAUTH_BASE_URL</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL</code>)</li>
<li><code>TRILIUM_OAUTH_CLIENT_ID</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID</code>)</li>
<li><code>TRILIUM_OAUTH_CLIENT_SECRET</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET</code>)</li>
<li><code>TRILIUM_OAUTH_ISSUER_BASE_URL</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL</code>)</li>
<li><code>TRILIUM_OAUTH_ISSUER_NAME</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME</code>)</li>
<li><code>TRILIUM_OAUTH_ISSUER_ICON</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON</code>)</li>
<li><code spellcheck="false">TRILIUM_OAUTH_BASE_URL</code> (alternative to
<code
spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL</code>)</li>
<li><code spellcheck="false">TRILIUM_OAUTH_CLIENT_ID</code> (alternative to
<code
spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID</code>)</li>
<li><code spellcheck="false">TRILIUM_OAUTH_CLIENT_SECRET</code> (alternative
to <code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET</code>)</li>
<li><code spellcheck="false">TRILIUM_OAUTH_ISSUER_BASE_URL</code> (alternative
to <code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL</code>)</li>
<li><code spellcheck="false">TRILIUM_OAUTH_ISSUER_NAME</code> (alternative
to <code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME</code>)</li>
<li><code spellcheck="false">TRILIUM_OAUTH_ISSUER_ICON</code> (alternative
to <code spellcheck="false">TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON</code>)</li>
</ul>
<h3>Logging Variables</h3>
<ul>
<li><code>TRILIUM_LOGGING_RETENTION_DAYS</code> (alternative to <code>TRILIUM_LOGGING_RETENTIONDAYS</code>)</li>
<li><code spellcheck="false">TRILIUM_LOGGING_RETENTION_DAYS</code> (alternative
to <code spellcheck="false">TRILIUM_LOGGING_RETENTIONDAYS</code>)</li>
</ul>
<h2>Boolean Values</h2>
<p>Boolean environment variables accept the following values:</p>
<ul>
<li><strong>True</strong>: <code>"true"</code>, <code>"1"</code>, <code>1</code>
<li><strong>True</strong>: <code spellcheck="false">"true"</code>, <code spellcheck="false">"1"</code>,
<code
spellcheck="false">1</code>
</li>
<li><strong>False</strong>: <code>"false"</code>, <code>"0"</code>, <code>0</code>
<li><strong>False</strong>: <code spellcheck="false">"false"</code>, <code spellcheck="false">"0"</code>,
<code
spellcheck="false">0</code>
</li>
<li>Any other value defaults to <code>false</code>
<li>Any other value defaults to <code spellcheck="false">false</code>
</li>
</ul>
<h2>Using Environment Variables</h2>

View File

@@ -1,40 +1,40 @@
<p>By default, Trilium cannot be accessed in web browsers by requests coming
from other domains/origins than Trilium itself.&nbsp;</p>
<p>However, it is possible to manually configure <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS">Cross-Origin Resource Sharing (CORS)</a> since
Trilium v0.93.0 using environment variables or <code>config.ini</code>,
Trilium v0.93.0 using environment variables or <code spellcheck="false">config.ini</code>,
as follows:</p>
<table>
<thead>
<tr>
<th>CORS Header</th>
<th>Corresponding option in <code>config.ini</code>
<th>Corresponding option in <code spellcheck="false">config.ini</code>
</th>
<th>Corresponding option in environment variables in the <code>Network</code> section</th>
<th>Corresponding option in environment variables in the <code spellcheck="false">Network</code> section</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Access-Control-Allow-Origin</code>
<td><code spellcheck="false">Access-Control-Allow-Origin</code>
</td>
<td><code>TRILIUM_NETWORK_CORS_ALLOW_ORIGIN</code>
<td><code spellcheck="false">TRILIUM_NETWORK_CORS_ALLOW_ORIGIN</code>
</td>
<td><code>corsAllowOrigin</code>
<td><code spellcheck="false">corsAllowOrigin</code>
</td>
</tr>
<tr>
<td><code>Access-Control-Allow-Methods</code>
<td><code spellcheck="false">Access-Control-Allow-Methods</code>
</td>
<td><code>TRILIUM_NETWORK_CORS_ALLOW_METHODS</code>
<td><code spellcheck="false">TRILIUM_NETWORK_CORS_ALLOW_METHODS</code>
</td>
<td><code>corsAllowMethods</code>
<td><code spellcheck="false">corsAllowMethods</code>
</td>
</tr>
<tr>
<td><code>Access-Control-Allow-Headers</code>
<td><code spellcheck="false">Access-Control-Allow-Headers</code>
</td>
<td><code>TRILIUM_NETWORK_CORS_ALLOW_HEADERS</code>
<td><code spellcheck="false">TRILIUM_NETWORK_CORS_ALLOW_HEADERS</code>
</td>
<td><code>corsAllowHeaders</code>
<td><code spellcheck="false">corsAllowHeaders</code>
</td>
</tr>
</tbody>

View File

@@ -4,14 +4,15 @@
is set up with), sometimes it can be useful to distinguish the instance
you are running on.</p>
<h2>Setting the instance name</h2>
<p>To set up a name for the instance, modify the <code>config.ini</code>:</p><pre><code class="language-text-x-trilium-auto">[General]
<p>To set up a name for the instance, modify the <code spellcheck="false">config.ini</code>:</p><pre><code class="language-text-x-trilium-auto">[General]
instanceName=Hello</code></pre>
<h2>Distinguishing the instance on back-end</h2>
<p>Use <code>api.getInstanceName()</code> to obtain the instance name of the
current server, as specified in the config file or in environment variables.</p>
<p>Use <code spellcheck="false">api.getInstanceName()</code> to obtain the
instance name of the current server, as specified in the config file or
in environment variables.</p>
<h2>Limiting script runs based on instance</h2>
<p>For a script that is run periodically or on a certain event, it's possible
to limit it to certain instances without having to change the code. Just
add <code>runOnInstance</code> and set as the value the instance name where
the script should run. To run on multiple named instances, simply add the
label multiple times.</p>
add <code spellcheck="false">runOnInstance</code> and set as the value the
instance name where the script should run. To run on multiple named instances,
simply add the label multiple times.</p>

View File

@@ -1,7 +1,7 @@
<p>Trilium provides a mechanism for <a href="#root/_help_CdNpE2pqjmI6">scripts</a> to
open a public REST endpoint. This opens a way for various integrations
with other services - a simple example would be creating new note from
Slack by issuing a slash command (e.g. <code>/trilium buy milk</code>).</p>
Slack by issuing a slash command (e.g. <code spellcheck="false">/trilium buy milk</code>).</p>
<h2>Create note from outside Trilium</h2>
<p>Let's take a look at an example. The goal is to provide a REST endpoint
to which we can send title and content and Trilium will create a note.</p>
@@ -24,10 +24,12 @@ else {
}</code></pre>
<p>This script note has also following two attributes:</p>
<ul>
<li>label <code>#customRequestHandler</code> with value <code>create-note</code>
<li>label <code spellcheck="false">#customRequestHandler</code> with value
<code
spellcheck="false">create-note</code>
</li>
<li>relation <code>~targetNote</code> pointing to a note where new notes should
be saved</li>
<li>relation <code spellcheck="false">~targetNote</code> pointing to a note
where new notes should be saved</li>
</ul>
<h3>Explanation</h3>
<p>Let's test this by using an HTTP client to send a request:</p><pre><code class="language-text-x-trilium-auto">POST http://your-trilium-server/custom/create-note
@@ -38,16 +40,18 @@ Content-Type: application/json
"title": "hello",
"content": "world"
}+++++++++++++++++++++++++++++++++++++++++++++++</code></pre>
<p>Notice the <code>/custom</code> part in the request path - Trilium considers
any request with this prefix as "custom" and tries to find a matching handler
by looking at all notes which have <code>customRequestHandler</code> <a href="#root/_help_zEY4DaJG4YT5">label</a>.
Value of this label then contains a regular expression which will match
the request path (in our case trivial regex "create-note").</p>
<p>Trilium will then find our code note created above and execute it. <code>api.req</code>, <code>api.res</code> are
set to <a href="https://expressjs.com/en/api.html#req">request</a> and
<p>Notice the <code spellcheck="false">/custom</code> part in the request path
- Trilium considers any request with this prefix as "custom" and tries
to find a matching handler by looking at all notes which have <code spellcheck="false">customRequestHandler</code>
<a
href="https://expressjs.com/en/api.html#res">response</a>objects from which we can get details of the request and also
respond.</p>
href="#root/_help_zEY4DaJG4YT5">label</a>. Value of this label then contains a regular expression which
will match the request path (in our case trivial regex "create-note").</p>
<p>Trilium will then find our code note created above and execute it.
<code
spellcheck="false">api.req</code>, <code spellcheck="false">api.res</code> are set to <a href="https://expressjs.com/en/api.html#req">request</a> and
<a
href="https://expressjs.com/en/api.html#res">response</a>objects from which we can get details of the request and also
respond.</p>
<p>In the code note we check the request method and then use trivial authentication
- keep in mind that these endpoints are by default totally unauthenticated,
and you need to take care of this yourself.</p>
@@ -56,20 +60,21 @@ Content-Type: application/json
href="#root/_help_GLks18SNjxmC">Script API</a>.</p>
<h2>Custom resource provider</h2>
<p>Another common use case is that you want to just expose a file note -
in such case you create label <code>customResourceProvider</code> (value
in such case you create label <code spellcheck="false">customResourceProvider</code> (value
is again path regex).</p>
<p>For more information, see&nbsp;<a href="#root/_help_d3fAXQ2diepH">Custom Resource Providers</a>.</p>
<h2>Advanced concepts</h2>
<p><code>api.req</code> and <code>api.res</code> are Express.js objects - you
can always look into its <a href="https://expressjs.com/en/api.html">documentation</a> for
<p><code spellcheck="false">api.req</code> and <code spellcheck="false">api.res</code> are
Express.js objects - you can always look into its <a href="https://expressjs.com/en/api.html">documentation</a> for
details.</p>
<h3>Parameters</h3>
<p>REST request paths often contain parameters in the URL, e.g.:</p><pre><code class="language-text-x-trilium-auto">http://your-trilium-server/custom/notes/123</code></pre>
<p>The last part is dynamic so the matching of the URL must also be dynamic
- for this reason the matching is done with regular expressions. Following <code>customRequestHandler</code> value
would match it:</p><pre><code class="language-text-x-trilium-auto">notes/([0-9]+)</code></pre>
- for this reason the matching is done with regular expressions. Following
<code
spellcheck="false">customRequestHandler</code>value would match it:</p><pre><code class="language-text-x-trilium-auto">notes/([0-9]+)</code></pre>
<p>Additionally, this also defines a matching group with the use of parenthesis
which then makes it easier to extract the value. The matched groups are
available in <code>api.pathParams</code>:</p><pre><code class="language-text-x-trilium-auto">const noteId = api.pathParams[0];</code></pre>
<p>Often you also need query params (as in e.g. <code>http://your-trilium-server/custom/notes?noteId=123</code>),
you can get those with standard express <code>req.query.noteId</code>.</p>
available in <code spellcheck="false">api.pathParams</code>:</p><pre><code class="language-text-x-trilium-auto">const noteId = api.pathParams[0];</code></pre>
<p>Often you also need query params (as in e.g. <code spellcheck="false">http://your-trilium-server/custom/notes?noteId=123</code>),
you can get those with standard express <code spellcheck="false">req.query.noteId</code>.</p>

Some files were not shown because too many files have changed in this diff Show More