Compare commits

..

355 Commits

Author SHA1 Message Date
Elian Doran
51753ad82a chore(ci): run tests on standalone branch as well 2026-01-12 21:51:26 +02:00
Elian Doran
7e00634f3d chore(deps): align package lock 2026-01-12 21:44:25 +02:00
Elian Doran
daf41804d4 chore(core): address requested changes 2026-01-12 21:43:57 +02:00
Elian Doran
43d087f886 chore(deps): update lock file 2026-01-12 21:32:06 +02:00
Elian Doran
503a6e520d Merge remote-tracking branch 'origin/main' into lightweight/decouple_server_api 2026-01-12 21:31:32 +02:00
Elian Doran
fcfa64ae52 Translations update from Hosted Weblate (#8358) 2026-01-12 19:29:13 +02:00
Hosted Weblate
62f5b800b6 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-12 17:28:06 +00:00
Elian Doran
5b910cce56 fix: add "latex" alias for math command (#8357) 2026-01-12 19:27:50 +02:00
Atmois
2c8edb413e fix: add "latex" alias for math command 2026-01-12 16:02:45 +00:00
Elian Doran
71d3eb4fde Translations update from Hosted Weblate (#8340) 2026-01-12 07:58:21 +02:00
Elian Doran
7c2340d60e Apply suggestion from @gemini-code-assist[bot]
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-12 07:55:25 +02:00
Elian Doran
24013ef020 Apply suggestion from @gemini-code-assist[bot]
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-12 07:55:16 +02:00
Yatrik Patel
b572ea0954 Translated using Weblate (Hindi)
Currently translated at 12.0% (14 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/hi/
2026-01-12 06:51:45 +01:00
Francis C.
060257fa06 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1759 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-01-12 06:51:44 +01:00
Yatrik Patel
1c6bb0a20e Translated using Weblate (Hindi)
Currently translated at 6.9% (27 of 388 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-12 06:51:43 +01:00
Hosted Weblate
1479109582 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/
2026-01-12 06:51:42 +01:00
Yatrik Patel
13f4e38f48 Translated using Weblate (Hindi)
Currently translated at 6.6% (26 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-12 06:51:41 +01:00
Yatrik Patel
5cbde8d32a Translated using Weblate (Hindi)
Currently translated at 1.1% (20 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hi/
2026-01-12 06:51:40 +01:00
Yatrik Patel
f3e3ef2f7d Translated using Weblate (Hindi)
Currently translated at 37.5% (57 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-12 06:51:39 +01:00
Yatrik Patel
0a58f8108a Translated using Weblate (Hindi)
Currently translated at 10.3% (12 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/hi/
2026-01-12 06:51:38 +01:00
green
768213438a Translated using Weblate (Japanese)
Currently translated at 100.0% (1759 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-01-12 06:51:38 +01:00
Kim Nøglegaard
00e0eb6f8a Translated using Weblate (Norwegian Bokmål)
Currently translated at 2.8% (11 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/nb_NO/
2026-01-12 06:51:37 +01:00
Kim Nøglegaard
3abea13d79 Translated using Weblate (Norwegian Bokmål)
Currently translated at 1.8% (33 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/nb_NO/
2026-01-12 06:51:36 +01:00
noobhjy
67ab7f0c1e Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.8% (1757 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-01-12 06:51:35 +01:00
Kim Nøglegaard
b38e8e27b2 Translated using Weblate (Norwegian Bokmål)
Currently translated at 2.5% (10 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/nb_NO/
2026-01-12 06:51:34 +01:00
Kim Nøglegaard
a70c103b93 Translated using Weblate (Norwegian Bokmål)
Currently translated at 1.4% (26 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/nb_NO/
2026-01-12 06:51:34 +01:00
Yatrik Patel
b83c3090f7 Translated using Weblate (Hindi)
Currently translated at 1.0% (19 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hi/
2026-01-12 06:51:33 +01:00
Bart Louwers
59ee38e7a6 Translated using Weblate (Dutch)
Currently translated at 23.0% (35 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nl/
2026-01-12 06:51:32 +01:00
Bart Louwers
890fe5929b Translated using Weblate (Dutch)
Currently translated at 4.2% (75 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/nl/
2026-01-12 06:51:31 +01:00
nvno
56cc312565 Translated using Weblate (Portuguese)
Currently translated at 91.5% (1603 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pt/
2026-01-12 06:51:30 +01:00
Elian Doran
9dfd015a27 fix(deps): update dependency preact to v10.28.2 [security] (#8301) 2026-01-12 07:51:22 +02:00
Elian Doran
04618dcdab fix(deps): update dependency react-window to v2.2.5 (#8353) 2026-01-12 07:50:09 +02:00
Elian Doran
f408e15c32 chore(deps): update dependency typedoc to v0.28.16 (#8352) 2026-01-12 07:49:20 +02:00
renovate[bot]
00e60c147c fix(deps): update dependency react-window to v2.2.5 2026-01-12 00:58:16 +00:00
renovate[bot]
ad6fd64226 chore(deps): update dependency typedoc to v0.28.16 2026-01-12 00:57:28 +00:00
Elian Doran
750fa2e647 Merge remote-tracking branch 'origin/main' into lightweight/decouple_server_api 2026-01-11 17:15:35 +02:00
Elian Doran
a268507b80 chore(deps): update dependency ws to v8.19.0 (#8276) 2026-01-11 16:41:38 +02:00
renovate[bot]
9e847f67f2 chore(deps): update dependency ws to v8.19.0 2026-01-11 14:27:35 +00:00
renovate[bot]
df4992122b fix(deps): update dependency preact to v10.28.2 [security] 2026-01-11 14:26:08 +00:00
Elian Doran
a325ba7b8f chore(deps): update dependency @types/node to v24.10.7 (#8347) 2026-01-11 16:23:31 +02:00
Elian Doran
26e64ae7d0 Lightweight: Bootstrap index EJS (#8283) 2026-01-11 15:45:25 +02:00
renovate[bot]
d793774f51 chore(deps): update dependency @types/node to v24.10.7 2026-01-11 13:32:09 +00:00
Elian Doran
c5f2b5c177 fix(deps): update dependency react-window to v2.2.4 (#8275) 2026-01-11 15:31:53 +02:00
Elian Doran
b2f782f2a3 fix(deps): update dependency node-html-parser to v7.0.2 (#8306) 2026-01-11 15:31:24 +02:00
Elian Doran
b65b31ca4d chore(deps): update dependency vite to v7.3.1 (#8304) 2026-01-11 15:29:23 +02:00
Elian Doran
64827dcdcf fix(deps): update dependency mind-elixir to v5.5.0 (#8278) 2026-01-11 15:28:42 +02:00
Elian Doran
96d5d07087 chore(deps): update dependency @redocly/cli to v2.14.4 (#8316) 2026-01-11 15:27:44 +02:00
Elian Doran
ffd0f4727a chore(deps): update typescript-eslint monorepo to v8.52.0 (#8277) 2026-01-11 15:26:38 +02:00
Elian Doran
e33cd86d30 chore(deps): update dependency supertest to v7.2.2 (#8291) 2026-01-11 15:25:53 +02:00
Elian Doran
fc85c23a67 fix(deps): update dependency @codemirror/view to v6.39.9 (#8289) 2026-01-11 15:25:17 +02:00
Elian Doran
a55c8fb210 chore(deps): update dependency sax to v1.4.4 (#8317) 2026-01-11 15:24:44 +02:00
Elian Doran
19b865a5b4 chore(deps): update dependency express-openid-connect to v2.19.4 (#8327) 2026-01-11 15:24:10 +02:00
Elian Doran
61d90dda36 chore(deps): update dependency @smithy/middleware-retry to v4.4.19 (#8303) 2026-01-11 14:15:07 +02:00
Elian Doran
a4b1f06475 chore(deps): update dependency eslint-plugin-playwright to v2.4.1 (#8326) 2026-01-11 14:14:10 +02:00
Elian Doran
49704ea928 chore(deps): update dependency openai to v6.16.0 (#8328) 2026-01-11 14:13:32 +02:00
Elian Doran
a73df362d5 fix(deps): update dependency better-sqlite3 to v12.6.0 (#8349) 2026-01-11 14:13:06 +02:00
renovate[bot]
f42010e22a fix(deps): update dependency mind-elixir to v5.5.0 2026-01-11 10:28:35 +00:00
renovate[bot]
0b85e0fe2d fix(deps): update dependency better-sqlite3 to v12.6.0 2026-01-11 10:27:40 +00:00
renovate[bot]
c7f0720237 chore(deps): update typescript-eslint monorepo to v8.52.0 2026-01-11 10:26:45 +00:00
renovate[bot]
cc3031eaad chore(deps): update dependency supertest to v7.2.2 2026-01-11 10:25:13 +00:00
renovate[bot]
2850c7808c chore(deps): update dependency openai to v6.16.0 2026-01-11 10:24:24 +00:00
renovate[bot]
30bbcd866f fix(deps): update dependency react-window to v2.2.4 2026-01-11 10:22:53 +00:00
renovate[bot]
55b6f322ac fix(deps): update dependency node-html-parser to v7.0.2 2026-01-11 10:21:22 +00:00
Elian Doran
4518c9bb99 Merge remote-tracking branch 'origin/main' into lightweight/bootstrap_ejs 2026-01-11 12:20:17 +02:00
renovate[bot]
c551c863f4 fix(deps): update dependency @codemirror/view to v6.39.9 2026-01-11 10:19:51 +00:00
renovate[bot]
3c25f8b4f3 chore(deps): update dependency vite to v7.3.1 2026-01-11 10:19:14 +00:00
renovate[bot]
d3c37556c3 chore(deps): update dependency sax to v1.4.4 2026-01-11 10:18:42 +00:00
renovate[bot]
cf60fcd6c1 chore(deps): update dependency express-openid-connect to v2.19.4 2026-01-11 10:18:10 +00:00
renovate[bot]
97075ff91b chore(deps): update dependency eslint-plugin-playwright to v2.4.1 2026-01-11 10:17:37 +00:00
renovate[bot]
ce57a43d90 chore(deps): update dependency @smithy/middleware-retry to v4.4.19 2026-01-11 10:15:52 +00:00
renovate[bot]
33485369c3 chore(deps): update dependency @redocly/cli to v2.14.4 2026-01-11 10:15:00 +00:00
Elian Doran
7fcd93a61b e2e(server): fix broken e2e test after MathTex integration 2026-01-11 11:55:49 +02:00
Elian Doran
3d4b84c7c4 e2e(server): format file 2026-01-11 11:49:49 +02:00
Elian Doran
61eb4017dd Vite performance improvement (#8345) 2026-01-11 11:28:36 +02:00
Elian Doran
b4df4aaf0d chore(client): address requested changes 2026-01-11 11:06:59 +02:00
Elian Doran
244294f699 Revert "chore(scripts): remove analyze-perf"
This reverts commit 94d4a307cf.
2026-01-11 10:55:47 +02:00
Elian Doran
d609ee028e chore(client): get rid of workspace projects from optimizeDeps 2026-01-11 10:55:13 +02:00
Elian Doran
8dc8b046fb chore(client): reintroduce live refresh 2026-01-11 00:55:42 +02:00
Elian Doran
349203e300 chore(client): remove dependency to @preact/preset-vite 2026-01-11 00:35:11 +02:00
Elian Doran
83a8f07998 chore(client): change vite config 2026-01-11 00:33:40 +02:00
Elian Doran
94d4a307cf chore(scripts): remove analyze-perf 2026-01-11 00:21:38 +02:00
Elian Doran
2e35e0a830 chore(client): re-enable source maps 2026-01-11 00:21:32 +02:00
Elian Doran
ecdb819067 chore(scripts): address requested changes 2026-01-11 00:11:13 +02:00
Elian Doran
e2cf0c6e3e chore(client): disable preact preset-vite 2026-01-10 23:27:14 +02:00
Elian Doran
5dd600a291 chore(client): shared config 2026-01-10 23:09:51 +02:00
Elian Doran
df1beb1ffb chore(client): set up LightningCSS 2026-01-10 23:04:04 +02:00
Elian Doran
7773059ac0 chore(client): optimize babel 2026-01-10 22:40:20 +02:00
Elian Doran
a238fc16b2 chore(client): add more optimize deps 2026-01-10 22:26:01 +02:00
Elian Doran
e298f5ea6f chore(client): optimize dependencies more aggressively 2026-01-10 22:12:41 +02:00
Elian Doran
a5512267c1 feat(ckeditor5): lazy-load premium features 2026-01-10 21:54:35 +02:00
Elian Doran
f503c4ca6c feat(ckeditor5-math): use dynamic import to reduce loading time 2026-01-10 21:49:16 +02:00
Elian Doran
c834c01c8e chore(scripts): improve performance analysis script by timing out earlier 2026-01-10 21:48:27 +02:00
Elian Doran
1f72ab9593 chore(scripts): spawn process automatically 2026-01-10 21:20:13 +02:00
Elian Doran
c7d446f4aa chore(scripts): display time in seconds 2026-01-10 21:13:34 +02:00
Elian Doran
fb1530423d chore(scripts): add more categories to analyze performance 2026-01-10 21:12:35 +02:00
Elian Doran
545464efee chore(scripts): add a script to anlayze performance logs 2026-01-10 21:10:56 +02:00
Elian Doran
29d0223fd1 chore(server): address requested changes 2026-01-10 20:45:03 +02:00
Elian Doran
00852277f2 fix(server): some routes not working on prod 2026-01-10 20:43:43 +02:00
Elian Doran
edfe23d88c fix(server): server-side images not served in dev mode 2026-01-10 19:58:05 +02:00
Elian Doran
2b8a7a28d9 Merge remote-tracking branch 'origin/main' into lightweight/bootstrap_ejs 2026-01-10 19:51:05 +02:00
Elian Doran
7f29480237 Revert "refactor(client): get rid of runtime in favor of bootstrap script"
This reverts commit 2e845a9faa.
2026-01-10 19:51:03 +02:00
Elian Doran
61ac482946 Revert "chore(server): remove runtime from login"
This reverts commit 455edbfb5d.
2026-01-10 19:48:14 +02:00
Elian Doran
de6a6cbb07 feat(tree): open notes in new window from tree (#8269) 2026-01-10 19:37:12 +02:00
Elian Doran
2dcb003909 chore(deps): update pnpm to v10.28.0 (#8329) 2026-01-10 19:36:34 +02:00
Elian Doran
d5c934a518 add logseq to supported protocols (#8320) 2026-01-10 19:36:03 +02:00
Elian Doran
779909837c Allow hiding children from tree (especially for collections) (#8335) 2026-01-10 18:32:41 +02:00
Elian Doran
a4cb375a0f test(client): fix broken tests 2026-01-10 18:21:38 +02:00
Elian Doran
e2da8c28ca fix(tree): spotlighted note has + button & insert child context menu 2026-01-10 18:21:16 +02:00
Elian Doran
7808848f05 Translations update from Hosted Weblate (#8339) 2026-01-10 18:02:07 +02:00
Elian Doran
7c05109645 Merge remote-tracking branch 'origin/main' into feature/hide_from_tree 2026-01-10 18:01:05 +02:00
Hosted Weblate
8f9e89b73b 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-10 15:59:25 +00:00
Elian Doran
861a61a4d8 Tree performance improvement (#8274) 2026-01-10 17:59:08 +02:00
Elian Doran
52d4083814 chore(tree): address requested changes 2026-01-10 17:46:45 +02:00
Elian Doran
0272189b22 fix(tree): performance issue due to batch update 2026-01-10 17:32:06 +02:00
Elian Doran
ddba0e823c fix(tree): tree is updated on note content updates 2026-01-10 17:32:02 +02:00
Elian Doran
be81acb9e7 feat(tree): respect motion settings instead of always disabling animation 2026-01-10 16:11:15 +02:00
Elian Doran
3bb97385c9 fix(client): a case where inheritance boolean is not correct 2026-01-10 13:17:44 +02:00
Elian Doran
a72cec0494 chore(client): address a few requested changes 2026-01-10 13:06:13 +02:00
Elian Doran
cb02198c6f docs(user): document hiding the subtree 2026-01-10 12:57:11 +02:00
Elian Doran
298d438230 Merge branch 'feature/tree_performance_improvement' into feature/hide_from_tree 2026-01-10 12:34:44 +02:00
Elian Doran
cb2f7932dd Merge remote-tracking branch 'origin/main' into feature/tree_performance_improvement 2026-01-10 12:34:26 +02:00
Elian Doran
3354bd669f fix(client/tree): toast displayed when doing operations outside of tree 2026-01-10 12:32:55 +02:00
Elian Doran
8ad779be66 fix(client/load_results): component ID not preserved for attributes & branches 2026-01-10 12:26:08 +02:00
Elian Doran
f462034868 fix(client/tree): toast not appearing when inserted via paste 2026-01-10 11:55:49 +02:00
Elian Doran
26c25cd4cd fix(client): edge case not handled when parent note overrides to false 2026-01-10 11:25:00 +02:00
Elian Doran
6398830c2d test(client): attribute toggling 2026-01-10 11:15:21 +02:00
Elian Doran
0b065063f2 refactor(client): use same logic for setting boolean with inheritance 2026-01-10 10:37:19 +02:00
Elian Doran
a3a9de6fdd feat(collections): add setting to hide subtree 2026-01-10 10:34:06 +02:00
Elian Doran
d77d30f29e fix(note_tree): subtree hidden cannot be overridden through inheritance 2026-01-10 10:17:09 +02:00
Elian Doran
5cabc6379d fix(note_tree): not reacting to changes in subtreeHidden 2026-01-10 10:14:15 +02:00
Elian Doran
af537e6a48 feat(tree): add option to hide or show subtree 2026-01-10 10:08:50 +02:00
Elian Doran
faf3797663 feat(tree): disable insert child note if subnotes are hidden 2026-01-10 09:57:09 +02:00
Elian Doran
db57f3ff62 feat(tree): add tooltip when moving into hidden subtree 2026-01-10 09:53:23 +02:00
Elian Doran
0f77caad69 feat(tree): hide items dragged into a subtreeHidden 2026-01-10 09:40:19 +02:00
renovate[bot]
751b91e1b8 chore(deps): update pnpm to v10.28.0 2026-01-10 00:41:10 +00:00
Elian Doran
968a17fbfb fix(tree): alignment of note count badge 2026-01-10 00:55:33 +02:00
Elian Doran
712e87b39f feat(tree): distinguish spotlighted node 2026-01-10 00:52:11 +02:00
Elian Doran
0b40b42315 fix(tree): random exceptions when switching between two spotlighted notes 2026-01-10 00:42:24 +02:00
Elian Doran
62996b1162 feat(tree): remove spotlighted note after switching to another one 2026-01-10 00:38:28 +02:00
Elian Doran
b67ccc6091 feat(tree): basic spotlight support for hidden child 2026-01-10 00:15:59 +02:00
Elian Doran
211d2dcf99 feat(collections): hide children by default for some collection types 2026-01-09 20:06:49 +02:00
Elian Doran
ee52e16a75 feat(tree): add title for subnote count badge 2026-01-09 19:15:57 +02:00
Elian Doran
0c27bd25fa chore(tree): align child count to the right 2026-01-09 17:03:56 +02:00
Elian Doran
b6a6e78d01 feat(tree): hide add button if subtree is hidden 2026-01-09 16:59:55 +02:00
Elian Doran
92e6a29e70 feat(tree): display number of children if subtree is hidden 2026-01-09 16:54:04 +02:00
Elian Doran
acc8cee7cd Merge remote-tracking branch 'origin/feature/tree_performance_improvement' into feature/hide_from_tree 2026-01-09 16:38:56 +02:00
Elian Doran
afefbe154b feat(tree): hide arrow if children are hidden 2026-01-09 16:37:11 +02:00
SngAbc
83fa55b7d9 Merge branch 'main' into feat/tree/new_window 2026-01-09 22:35:30 +08:00
Elian Doran
4f6c10d995 feat(tree): allow hiding child notes via attribute 2026-01-09 16:32:55 +02:00
Elian Doran
ed972d2601 e2e(server): math popup fails on CI 2026-01-09 12:13:39 +02:00
Elian Doran
6b57ee5654 e2e(server): flakiness in tab_bar 2026-01-09 11:54:16 +02:00
contributor
e469af1ca5 add logseq to supported protocols 2026-01-09 11:51:49 +02:00
Elian Doran
6d41f076c2 Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-01-09 11:25:26 +02:00
Elian Doran
8cff591746 fix(toc): unnecessary <ol> when no children 2026-01-09 11:25:24 +02:00
Elian Doran
b3ccf89094 Translations update from Hosted Weblate (#8315) 2026-01-09 00:06:09 +02:00
Ulices
d31c6b1627 Translated using Weblate (Spanish)
Currently translated at 100.0% (152 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/es/
2026-01-08 23:01:53 +01:00
Yatrik Patel
1481356d1f Translated using Weblate (Hindi)
Currently translated at 36.8% (56 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-08 23:01:52 +01:00
Ulices
a54661fd0a Translated using Weblate (Spanish)
Currently translated at 93.8% (1643 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-01-08 23:01:49 +01:00
Elian Doran
ae4a3f10ae fix(toc): equations not rendered in new layout 2026-01-08 21:26:04 +02:00
Elian Doran
fe3160e7a1 e2e(server): adapt tests to new layout directly 2026-01-08 20:32:54 +02:00
Elian Doran
66659d4786 e2e(server): flaky test in PDF 2026-01-08 20:11:21 +02:00
Elian Doran
0b25b09040 feat(ci): check version consistency before releasing 2026-01-08 19:49:29 +02:00
Elian Doran
0d41cc2660 Merge remote-tracking branch 'origin/stable' 2026-01-08 19:42:10 +02:00
Elian Doran
f5e8822718 chore(release): prepare for v0.101.3 2026-01-08 19:38:21 +02:00
Elian Doran
bdc220ec12 Merge remote-tracking branch 'origin/stable' 2026-01-08 18:19:16 +02:00
Elian Doran
3eb68e5271 Stable fixes (#8310) 2026-01-08 18:16:55 +02:00
Elian Doran
521952ebcc test(client): remove debug statements 2026-01-08 18:10:00 +02:00
Elian Doran
034091a696 docs(release): prepare for v0.101.2 2026-01-08 18:08:34 +02:00
Elian Doran
ae881101d8 fix(note_list): archived notes displayed in empty grid card (closes #8184) 2026-01-08 17:23:40 +02:00
Elian Doran
b11a30c49c fix(launcher_bar): crashing if there is a non-launcher note (closes #8218) 2026-01-08 16:55:51 +02:00
Elian Doran
4625efda7f fix(note_list): skip rendering of included notes for performance (closes #8017) 2026-01-08 16:50:27 +02:00
Elian Doran
3c168d750d fix(client): cycle in include causing infinite loop (closes #8294) 2026-01-08 16:44:35 +02:00
Elian Doran
5cc7b259ce fix(client): max content width not preserved (closes #8065) 2026-01-08 15:59:57 +02:00
Elian Doran
f7ae046b20 fix(mermaid): error container not scrollable (closes #8299) 2026-01-08 15:52:19 +02:00
Elian Doran
02f43d6239 fix(mermaid): code not scrollable (closes #8299) 2026-01-08 15:33:16 +02:00
Elian Doran
53e1fa1047 fix(mermaid) diagrams not saving content and SVG attachment (#8220) 2026-01-08 15:22:07 +02:00
Elian Doran
b1dc0e234f fix(popupEditor): fix closing of popupEditor when inserting note link (#8224) 2026-01-08 15:21:23 +02:00
Elian Doran
9d380dd828 fix(sql_console): cannot copy table data (#8268) 2026-01-08 15:20:36 +02:00
Elian Doran
1f77540dbb fix(text): Title is not selected when creating a note via the launcher (#8292) 2026-01-08 15:20:14 +02:00
Elian Doran
455edbfb5d chore(server): remove runtime from login 2026-01-07 23:31:28 +02:00
Elian Doran
7288b66d27 chore(client): address requested changes 2026-01-07 23:04:34 +02:00
Elian Doran
3d72ec80bb refactor(client): get rid of any 2026-01-07 22:01:35 +02:00
Elian Doran
f2a74df511 feat(client): use hashes for assets 2026-01-07 21:49:05 +02:00
Elian Doran
68c6052d10 chore(client): remove useless manual chunk 2026-01-07 21:39:35 +02:00
Elian Doran
c4edb56bd4 fix(server): not starting due to serving of assets 2026-01-07 21:38:44 +02:00
Elian Doran
b6a3fe7cfb chore(client): get rid of translation issue 2026-01-07 21:18:09 +02:00
Elian Doran
7a088c5b7d refactor(client): handle everything in bootstrap 2026-01-07 21:11:38 +02:00
Elian Doran
2e845a9faa refactor(client): get rid of runtime in favor of bootstrap script 2026-01-07 21:08:19 +02:00
Elian Doran
ac3ae0dbbe chore(client): fix type issues 2026-01-07 21:02:27 +02:00
Elian Doran
a3fc13de3a refactor(client): extract bootstrap script into separate file 2026-01-07 21:00:40 +02:00
Elian Doran
ee6cbc710c chore(server): remove font size globs 2026-01-07 20:52:27 +02:00
Elian Doran
18d701525e fix(client): print broken due to lack of query forwarding
; Conflicts:
;	apps/client/src/index.html
2026-01-07 20:52:04 +02:00
Elian Doran
e47c848ec8 chore(server): reintegrate mobile layout 2026-01-07 20:51:33 +02:00
Elian Doran
cd64548299 fix(client): load custom fonts 2026-01-07 20:51:29 +02:00
Elian Doran
8645d053de fix(client): ckeditor theme not loaded properly 2026-01-07 20:51:24 +02:00
Elian Doran
91f2dabed7 Merge remote-tracking branch 'origin/main' into lightweight/bootstrap_ejs
; Conflicts:
;	apps/client/src/widgets/layout/StatusBar.tsx
2026-01-07 20:51:17 +02:00
Elian Doran
716612680d Translations update from Hosted Weblate (#8293) 2026-01-07 19:35:42 +02:00
Michael
3800fb85eb Translated using Weblate (German)
Currently translated at 95.4% (1672 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-01-07 18:32:56 +01:00
Rafa Osuna
d807984be4 Translated using Weblate (Spanish)
Currently translated at 92.7% (1624 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-01-07 18:32:56 +01:00
Giovi
2c92ae8898 Translated using Weblate (Italian)
Currently translated at 100.0% (1751 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2026-01-07 18:32:55 +01:00
Argann Bonneau
3d8cbc81c4 Translated using Weblate (French)
Currently translated at 94.5% (1656 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fr/
2026-01-07 18:32:54 +01:00
Yatrik Patel
d747c94450 Translated using Weblate (Hindi)
Currently translated at 3.4% (4 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/hi/
2026-01-07 18:32:53 +01:00
pythaac
a627d1f96e Translated using Weblate (Korean)
Currently translated at 76.3% (116 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ko/
2026-01-07 18:32:53 +01:00
Yatrik Patel
869db5e478 Translated using Weblate (Hindi)
Currently translated at 0.9% (17 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hi/
2026-01-07 18:32:52 +01:00
Yatrik Patel
73e94d385e Translated using Weblate (Hindi)
Currently translated at 5.9% (23 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-07 18:32:51 +01:00
Kim Nøglegaard
8f4ebeb335 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (152 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-07 18:32:51 +01:00
Yatrik Patel
263ee864be Translated using Weblate (Hindi)
Currently translated at 9.2% (14 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-07 18:32:50 +01:00
Elian Doran
f078732624 fix(text): Title is not selected when creating a note via the launcher (#8292) 2026-01-07 19:32:37 +02:00
SngAbc
fac1f6b16c fix(text): Title is not focused when creating a note via the launcher
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-07 10:33:17 +08:00
SiriusXT
a5841c1423 fix(text): Title is not focused when creating a note via the launcher 2026-01-07 10:11:24 +08:00
Elian Doran
8523c369e1 fix(server): imports preventing start-up 2026-01-06 19:15:53 +02:00
Elian Doran
7c16aeca4a chore(core): crash due to dbReady before CLS init 2026-01-06 16:30:45 +02:00
Elian Doran
8399600e79 chore(core): address some missing methods in utils 2026-01-06 16:29:30 +02:00
Elian Doran
edac58f3fa chore(core): integrate revisions 2026-01-06 16:24:14 +02:00
Elian Doran
51d0d848c5 chore(core): no-op request service 2026-01-06 16:22:47 +02:00
Elian Doran
1edab8e8da chore(core): no-op image service 2026-01-06 16:21:42 +02:00
Elian Doran
e1e294914a chore(core): no-op search 2026-01-06 16:20:10 +02:00
Elian Doran
4668fdc15c chore(core): no-op sqlInit 2026-01-06 16:18:06 +02:00
Elian Doran
f1e0d5558c chore(core): integrate erase 2026-01-06 16:16:54 +02:00
Elian Doran
c94c54c641 chore(core): integrate task_context with ws no-op 2026-01-06 16:09:21 +02:00
Elian Doran
18416eb89a chore(core): no op script 2026-01-06 16:07:30 +02:00
Elian Doran
263c9028e2 chore(core): integrate hidden_subtree 2026-01-06 16:05:16 +02:00
Elian Doran
0b528e9937 chore(core): integrate handlers 2026-01-06 15:57:36 +02:00
Elian Doran
e905c1ec11 chore(core): integrate cloning service 2026-01-06 15:52:37 +02:00
Elian Doran
ecb27fe9f7 chore(core): integrate tree service 2026-01-06 15:48:48 +02:00
Elian Doran
a8f6db4b20 chore(core): fix some imports 2026-01-06 15:45:07 +02:00
Elian Doran
78262e55ec chore(core): integrate escape/unescape & toMap 2026-01-06 15:43:36 +02:00
Elian Doran
c6197e520d chore(core): integrate some more utils 2026-01-06 15:41:34 +02:00
Elian Doran
299c06c1a6 chore(core): fix inaccessible NoteParams 2026-01-06 15:33:48 +02:00
Elian Doran
674593b38c chore(core): integrate html_sanitizer 2026-01-06 15:31:22 +02:00
Elian Doran
f5535657ad chore(core): port note_types 2026-01-06 15:17:10 +02:00
Elian Doran
de4d07e904 chore(core): get rid of note_interface 2026-01-06 15:15:12 +02:00
Elian Doran
5508b505c8 chore(core): port notes service partially 2026-01-06 15:14:08 +02:00
Elian Doran
aaca18003d Translations update from Hosted Weblate (#8279) 2026-01-06 13:54:24 +02:00
Elian Doran
8cdfc108ba fix(core): wrong imports to src 2026-01-06 13:52:57 +02:00
Elian Doran
6a0f6fab83 fix(core): server not starting due to crypto not initialized 2026-01-06 13:50:22 +02:00
Elian Doran
ad3be73e1b chore(core): integrate note_set 2026-01-06 13:45:53 +02:00
Elian Doran
64b212b93e chore(core): integrate entity_changes 2026-01-06 13:42:29 +02:00
Elian Doran
60cb8d950e chore(core): integrate promoted_attribute_definition_parser 2026-01-06 13:30:21 +02:00
Elian Doran
61f6f94295 chore(core): integrate sanitize_attribute_name 2026-01-06 13:26:19 +02:00
Elian Doran
ebe7276f40 chore(core): fix some use of logs 2026-01-06 13:21:39 +02:00
Elian Doran
26d299aa44 chore(core): integrate options, options_init & keyboard_actions 2026-01-06 13:20:42 +02:00
Elian Doran
bd45c32251 chore(core): integrate utils partially 2026-01-06 13:06:14 +02:00
Elian Doran
321558a01f chore(core): fix minor type issue 2026-01-06 12:43:45 +02:00
Elian Doran
f5a77477aa chore(core): fix missing CLS method 2026-01-06 12:42:35 +02:00
Elian Doran
20c90d1296 chore(server): fix incompatibility with Uint8Array 2026-01-06 12:40:43 +02:00
Elian Doran
bbfef0315f chore(core): fix incompatibility with Uint8Array 2026-01-06 12:34:16 +02:00
Elian Doran
321fcf34f2 chore(core): fix references to getHoistedNoteId 2026-01-06 12:29:13 +02:00
Elian Doran
b9a59fe0c4 chore(core): fixs some imports to protected_session 2026-01-06 12:27:00 +02:00
Elian Doran
01f3c32d92 refactor(server): remove Blob interface in favor of BlobRow 2026-01-06 12:24:09 +02:00
Elian Doran
05b9e2ec2a chore(core): fix references to core 2026-01-06 12:20:01 +02:00
Elian Doran
c8d3b091fd chore(commons): fix Node reference 2026-01-06 12:19:42 +02:00
Elian Doran
d717a89163 chore(core): fix references to Buffer 2026-01-06 12:16:38 +02:00
Elian Doran
8149460547 chore(commons): fix issues with Buffer 2026-01-06 12:07:16 +02:00
Elian Doran
b7ad76827a chore(server): various references to core 2026-01-06 12:05:52 +02:00
Elian Doran
e19e9b3830 chore(core): fix references to blob-service 2026-01-06 12:01:18 +02:00
Elian Doran
40b07c3e8a chore(core): fix references to becca-interface 2026-01-06 11:59:45 +02:00
Elian Doran
a15b84b4e5 chore(core): fix type error in getFlatText 2026-01-06 11:56:52 +02:00
Elian Doran
544c52931c chore(server): fix references to abstract becca entity 2026-01-06 11:55:10 +02:00
Elian Doran
9391159413 chore(server): fix references to becca service 2026-01-06 11:52:25 +02:00
Elian Doran
f9e22a9ba9 chore(server): fix references to becca loader 2026-01-06 11:49:22 +02:00
Elian Doran
f88ac5dfae chore(server): fix imports to becca entities 2026-01-06 11:46:15 +02:00
Elian Doran
3459d2906e chore(server): fix imports to validation error 2026-01-06 11:41:06 +02:00
Elian Doran
4506b717d5 chore(server): fix imports to becca 2026-01-06 11:38:25 +02:00
Elian Doran
af8744ef2a chore(core): integrate errors 2026-01-06 11:31:13 +02:00
Elian Doran
320d8e3b45 chore(core): partially integrate becca 2026-01-06 11:23:52 +02:00
Elian Doran
c20da77f83 chore(core): integrate events service 2026-01-06 11:10:09 +02:00
Elian Doran
14e2e85da7 chore(core): integrate date_utils 2026-01-06 11:03:33 +02:00
Elian Doran
c7f0d541c2 fix(server): blob errors out 2026-01-06 10:41:50 +02:00
Kim Nøglegaard
5ec521b024 Translated using Weblate (Norwegian Bokmål)
Currently translated at 68.4% (104 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-06 04:01:53 +01:00
Yatrik Patel
b3c0be7559 Translated using Weblate (Hindi)
Currently translated at 3.0% (12 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-06 04:01:51 +01:00
Máté Zsólya
d52b735b99 Translated using Weblate (Hungarian)
Currently translated at 1.9% (34 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hu/
2026-01-06 04:01:49 +01:00
Yatrik Patel
639b1f2863 Translated using Weblate (Hindi)
Currently translated at 5.9% (9 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-06 04:01:47 +01:00
Elian Doran
aff4f7e010 feat(tree): disable animation for performance 2026-01-06 01:34:02 +02:00
Elian Doran
dec4dafba6 feat(tree): avoid async 2026-01-06 01:26:56 +02:00
Elian Doran
d0cdcfc32c refactor(tree): use loop for mini optimisation 2026-01-06 01:21:43 +02:00
Elian Doran
0867b81c7a feat(tree): use template for create child to improve performance 2026-01-06 01:13:31 +02:00
Elian Doran
bde6068f2d refactor(tree): extract enchance title into separate method 2026-01-06 01:07:37 +02:00
Elian Doran
47fd2affa4 feat(tree): use direct DOM manipulation instead of jQuery 2026-01-06 00:59:32 +02: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
c1548b0f54 chore(server): integrate data_encryption, and protected_session 2026-01-05 17:47:25 +02:00
Elian Doran
6f04738629 chore(core): add documentation for SQL 2026-01-05 16:07:17 +02:00
Elian Doran
f79af7b045 fix(server): request content empty due to CLS 2026-01-05 16:01:27 +02:00
Elian Doran
527f502083 fix(server): requests failing due to cls namespacing issue 2026-01-05 15:58:24 +02:00
Elian Doran
d61e2c6f2c chore(server): get DB to be loaded 2026-01-05 15:52:31 +02:00
Elian Doran
ea31d2f446 chore(core): basic integration of SQL + CLS + log 2026-01-05 15:45:45 +02:00
Elian Doran
62803a1817 chore(server): set up dependency to trilium-core 2026-01-05 14:42:32 +02:00
Elian Doran
a67464b4a0 refactor(server): decouple bettersqlite3 from sql service 2026-01-05 14:03:03 +02:00
SiriusXT
5b95b9875b feat(tree): open notes in new window from tree 2026-01-05 19:27:44 +08:00
Elian Doran
00e7482968 chore(core): create empty package 2026-01-05 12:26:13 +02:00
Elian Doran
688d197472 chore(client): set up body classes 2026-01-05 11:55:51 +02:00
Elian Doran
b745fb476e chore(client): get icons to load 2026-01-05 11:50:21 +02:00
Elian Doran
047b5a85d2 chore(client): load stylesheets 2026-01-05 11:48:19 +02:00
Elian Doran
370a0c6a05 feat(client): get desktop to start 2026-01-05 11:40:53 +02:00
Elian Doran
0d4558fee1 feat(client): get glob to be populated 2026-01-05 11:37:03 +02:00
Elian Doran
76526e0a96 feat(server): bootstrap route 2026-01-05 11:22:10 +02:00
Elian Doran
70093e0a7d feat(server): render static Vite HTML 2026-01-05 11:07:40 +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
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
331 changed files with 14087 additions and 11666 deletions

View File

@@ -1,9 +1,9 @@
name: Dev
on:
push:
branches: [ main ]
branches: [ main, standalone ]
pull_request:
branches: [ main ]
branches: [ main, standalone ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}

View File

@@ -11,6 +11,14 @@ concurrency:
cancel-in-progress: true
jobs:
sanity-check:
name: Sanity Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Check version consistency
run: pnpm tsx ${{ github.workspace }}/scripts/check-version-consistency.ts ${{ github.ref_name }}
make-electron:
name: Make Electron
strategy:

2
.gitignore vendored
View File

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

View File

@@ -9,14 +9,14 @@
"keywords": [],
"author": "Elian Doran <contact@eliandoran.me>",
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.27.0",
"packageManager": "pnpm@10.28.0",
"devDependencies": {
"@redocly/cli": "2.14.3",
"@redocly/cli": "2.14.4",
"archiver": "7.0.1",
"fs-extra": "11.3.3",
"react": "19.2.3",
"react-dom": "19.2.3",
"typedoc": "0.28.15",
"typedoc": "0.28.16",
"typedoc-plugin-missing-exports": "4.1.2"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/client",
"version": "0.101.1",
"version": "0.101.3",
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
"private": true,
"license": "AGPL-3.0-only",
@@ -56,13 +56,12 @@
"mark.js": "8.11.1",
"marked": "17.0.1",
"mermaid": "11.12.2",
"mind-elixir": "5.4.0",
"mind-elixir": "5.5.0",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.28.1",
"preact": "10.28.2",
"react-i18next": "16.5.1",
"react-window": "2.2.3",
"react-zoom-pan-pinch": "3.7.0",
"react-window": "2.2.5",
"reveal.js": "5.2.1",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",
@@ -70,7 +69,7 @@
},
"devDependencies": {
"@ckeditor/ckeditor5-inspector": "5.0.0",
"@preact/preset-vite": "2.10.2",
"@prefresh/vite": "2.4.11",
"@types/bootstrap": "5.2.10",
"@types/jquery": "3.5.33",
"@types/leaflet": "1.9.21",
@@ -80,6 +79,7 @@
"@types/tabulator-tables": "6.3.1",
"copy-webpack-plugin": "13.0.1",
"happy-dom": "20.0.11",
"lightningcss": "1.30.2",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "3.1.4"
}

View File

@@ -154,6 +154,7 @@ export type CommandMappings = {
};
openInTab: ContextMenuCommandData;
openNoteInSplit: ContextMenuCommandData;
openNoteInWindow: ContextMenuCommandData;
openNoteInPopup: ContextMenuCommandData;
toggleNoteHoisting: ContextMenuCommandData;
insertNoteAfter: ContextMenuCommandData;

View File

@@ -616,7 +616,9 @@ export default class FNote {
}
isFolder() {
return this.type === "search" || this.getFilteredChildBranches().length > 0;
if (this.isLabelTruthy("subtreeHidden")) return false;
if (this.type === "search") return true;
return this.getFilteredChildBranches().length > 0;
}
getFilteredChildBranches() {

View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
<title>Trilium Notes</title>
</head>
<body id="trilium-app">
<noscript>Trilium requires JavaScript to be enabled.</noscript>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container" style="display: none"></div>
<!-- Required to match the PWA's top bar color with the theme -->
<!-- This works even when the user directly changes --root-background in CSS -->
<div id="background-color-tracker" style="position: absolute; visibility: hidden; color: var(--root-background); transition: color 1ms;"></div>
<script src="./index.ts" type="module"></script>
<!-- Required for correct loading of scripts in Electron -->
<script>
if (typeof module === 'object') {window.module = module; module = undefined;}
</script>
</body>
</html>

View File

@@ -0,0 +1,110 @@
async function bootstrap() {
showSplash();
await setupGlob();
await Promise.all([
initJQuery(),
loadBootstrapCss()
]);
loadStylesheets();
loadIcons();
setBodyAttributes();
await loadScripts();
hideSplash();
}
async function initJQuery() {
const $ = (await import("jquery")).default;
window.$ = $;
window.jQuery = $;
}
async function setupGlob() {
const response = await fetch(`/bootstrap${window.location.search}`);
const json = await response.json();
window.global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
window.glob = {
...json,
activeDialog: null
};
}
async function loadBootstrapCss() {
// We have to selectively import Bootstrap CSS based on text direction.
if (glob.isRtl) {
await import("bootstrap/dist/css/bootstrap.rtl.min.css");
} else {
await import("bootstrap/dist/css/bootstrap.min.css");
}
}
function loadStylesheets() {
const { assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
const cssToLoad: string[] = [];
cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`);
cssToLoad.push(`api/fonts`);
cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`);
if (themeCssUrl) {
cssToLoad.push(themeCssUrl);
}
if (themeUseNextAsBase === "next") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`);
} else if (themeUseNextAsBase === "next-dark") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`);
} else if (themeUseNextAsBase === "next-light") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`);
}
cssToLoad.push(`${assetPath}/stylesheets/style.css`);
for (const href of cssToLoad) {
const linkEl = document.createElement("link");
linkEl.href = href;
linkEl.rel = "stylesheet";
document.head.appendChild(linkEl);
}
}
function loadIcons() {
const styleEl = document.createElement("style");
styleEl.innerText = window.glob.iconPackCss;
document.head.appendChild(styleEl);
}
function setBodyAttributes() {
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale } = window.glob;
const classesToSet = [
device,
`heading-style-${headingStyle}`,
`layout-${layoutOrientation}`,
`platform-${platform}`,
isElectron && "electron",
hasNativeTitleBar && "native-titlebar",
hasBackgroundEffects && "background-effects"
].filter(Boolean) as string[];
for (const classToSet of classesToSet) {
document.body.classList.add(classToSet);
}
document.body.lang = currentLocale.id;
document.body.dir = currentLocale.rtl ? "rtl" : "ltr";
}
async function loadScripts() {
if (glob.device === "mobile") {
await import("./mobile.js");
} else {
await import("./desktop.js");
}
}
function showSplash() {
// hide body to reduce flickering on the startup. This is done through JS and not CSS to not hide <noscript>
document.body.style.display = "none";
}
function hideSplash() {
document.body.style.display = "block";
}
bootstrap();

View File

@@ -1,21 +1,21 @@
import NoteColorPicker from "./custom-items/NoteColorPicker.jsx";
import treeService from "../services/tree.js";
import froca from "../services/froca.js";
import clipboard from "../services/clipboard.js";
import noteCreateService from "../services/note_create.js";
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
import appContext, { type ContextMenuCommandData, type FilteredCommandNames } from "../components/app_context.js";
import type { SelectMenuItemEventListener } from "../components/events.js";
import type FAttachment from "../entities/fattachment.js";
import attributes from "../services/attributes.js";
import { executeBulkActions } from "../services/bulk_action.js";
import clipboard from "../services/clipboard.js";
import dialogService from "../services/dialog.js";
import froca from "../services/froca.js";
import { t } from "../services/i18n.js";
import noteCreateService from "../services/note_create.js";
import noteTypesService from "../services/note_types.js";
import server from "../services/server.js";
import toastService from "../services/toast.js";
import dialogService from "../services/dialog.js";
import { t } from "../services/i18n.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import type FAttachment from "../entities/fattachment.js";
import type { SelectMenuItemEventListener } from "../components/events.js";
import treeService from "../services/tree.js";
import utils from "../services/utils.js";
import attributes from "../services/attributes.js";
import { executeBulkActions } from "../services/bulk_action.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
import NoteColorPicker from "./custom-items/NoteColorPicker.jsx";
// TODO: Deduplicate once client/server is well split.
interface ConvertToAttachmentResponse {
@@ -72,6 +72,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
const noSelectedNotes = selNodes.length === 0 || (selNodes.length === 1 && selNodes[0] === this.node);
const notSearch = note?.type !== "search";
const hasSubtreeHidden = note?.isLabelTruthy("subtreeHidden") ?? false;
const isSpotlighted = this.node.extraClasses.includes("spotlighted-node");
const notOptionsOrHelp = !note?.noteId.startsWith("_options") && !note?.noteId.startsWith("_help");
const parentNotSearch = !parentNote || parentNote.type !== "search";
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
@@ -79,17 +81,18 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
const items: (MenuItem<TreeCommandNames> | null)[] = [
{ title: t("tree-context-menu.open-in-a-new-tab"), command: "openInTab", shortcut: "Ctrl+Click", uiIcon: "bx bx-link-external", enabled: noSelectedNotes },
{ title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes },
{ title: t("tree-context-menu.open-in-a-new-window"), command: "openNoteInWindow", uiIcon: "bx bx-window-open", enabled: noSelectedNotes },
{ title: t("tree-context-menu.open-in-popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit", enabled: noSelectedNotes },
isHoisted
? null
: {
title: `${t("tree-context-menu.hoist-note")}`,
command: "toggleNoteHoisting",
keyboardShortcut: "toggleNoteHoisting",
uiIcon: "bx bxs-chevrons-up",
enabled: noSelectedNotes && notSearch
},
title: `${t("tree-context-menu.hoist-note")}`,
command: "toggleNoteHoisting",
keyboardShortcut: "toggleNoteHoisting",
uiIcon: "bx bxs-chevrons-up",
enabled: noSelectedNotes && notSearch
},
!isHoisted || !isNotRoot
? null
: { title: t("tree-context-menu.unhoist-note"), command: "toggleNoteHoisting", keyboardShortcut: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
@@ -112,7 +115,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
keyboardShortcut: "createNoteInto",
uiIcon: "bx bx-plus",
items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
enabled: notSearch && noSelectedNotes && notOptionsOrHelp,
enabled: notSearch && noSelectedNotes && notOptionsOrHelp && !hasSubtreeHidden && !isSpotlighted,
columns: 2
},
@@ -150,8 +153,17 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
{ kind: "separator" },
{ title: t("tree-context-menu.expand-subtree"), command: "expandSubtree", keyboardShortcut: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
{ title: t("tree-context-menu.collapse-subtree"), command: "collapseSubtree", keyboardShortcut: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
!hasSubtreeHidden && { title: t("tree-context-menu.expand-subtree"), command: "expandSubtree", keyboardShortcut: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
!hasSubtreeHidden && { title: t("tree-context-menu.collapse-subtree"), command: "collapseSubtree", keyboardShortcut: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
{
title: hasSubtreeHidden ? t("tree-context-menu.show-subtree") : t("tree-context-menu.hide-subtree"),
uiIcon: "bx bx-show",
handler: async () => {
const note = await froca.getNote(this.node.data.noteId);
if (!note) return;
attributes.setBooleanWithInheritance(note, "subtreeHidden", !hasSubtreeHidden);
}
},
{
title: t("tree-context-menu.sort-by"),
command: "sortChildNotes",
@@ -164,7 +176,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
{ title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-directions", enabled: true },
{ title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptionsOrHelp }
]
].filter(Boolean) as MenuItem<TreeCommandNames>[]
},
{ kind: "separator" },
@@ -292,25 +304,30 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
noteCreateService.createNote(parentNotePath, {
target: "after",
targetBranchId: this.node.data.branchId,
type: type,
isProtected: isProtected,
templateNoteId: templateNoteId
type,
isProtected,
templateNoteId
});
} else if (command === "insertChildNote") {
const parentNotePath = treeService.getNotePath(this.node);
noteCreateService.createNote(parentNotePath, {
type: type,
type,
isProtected: this.node.data.isProtected,
templateNoteId: templateNoteId
templateNoteId
});
} else if (command === "openNoteInSplit") {
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath });
} else if (command === "openNoteInWindow") {
appContext.triggerCommand("openInWindow", {
notePath,
hoistedNoteId: appContext.tabManager.getActiveContext()?.hoistedNoteId
});
} else if (command === "openNoteInPopup") {
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath })
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
} else if (command === "convertNoteToAttachment") {
if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) {
return;
@@ -332,11 +349,11 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
toastService.showMessage(t("tree-context-menu.converted-to-attachments", { count: converted }));
} else if (command === "copyNotePathToClipboard") {
navigator.clipboard.writeText("#" + notePath);
navigator.clipboard.writeText(`#${ notePath}`);
} else if (command) {
this.treeWidget.triggerCommand<TreeCommandNames>(command, {
node: this.node,
notePath: notePath,
notePath,
noteId: this.node.data.noteId,
selectedOrActiveBranchIds: this.treeWidget.getSelectedOrActiveBranchIds(this.node),
selectedOrActiveNoteIds: this.treeWidget.getSelectedOrActiveNoteIds(this.node)

View File

@@ -0,0 +1,139 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { buildNote } from "../test/easy-froca";
import { setBooleanWithInheritance } from "./attributes";
import froca from "./froca";
import server from "./server.js";
// Spy on server methods to track calls
// @ts-expect-error the generic typing is causing issues here
server.put = vi.fn(async <T> (url: string, data?: T) => ({} as T));
// @ts-expect-error the generic typing is causing issues here
server.remove = vi.fn(async <T> (url: string) => ({} as T));
describe("Set boolean with inheritance", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("doesn't call server if value matches directly", async () => {
const noteWithLabel = buildNote({
title: "New note",
"#foo": ""
});
const noteWithoutLabel = buildNote({
title: "New note"
});
await setBooleanWithInheritance(noteWithLabel, "foo", true);
await setBooleanWithInheritance(noteWithoutLabel, "foo", false);
expect(server.put).not.toHaveBeenCalled();
expect(server.remove).not.toHaveBeenCalled();
});
it("sets boolean normally without inheritance", async () => {
const standaloneNote = buildNote({
title: "New note"
});
await setBooleanWithInheritance(standaloneNote, "foo", true);
expect(server.put).toHaveBeenCalledWith(`notes/${standaloneNote.noteId}/set-attribute`, {
type: "label",
name: "foo",
value: "",
isInheritable: false
});
});
it("removes boolean normally without inheritance", async () => {
const standaloneNote = buildNote({
title: "New note",
"#foo": ""
});
const attributeId = standaloneNote.getLabel("foo")!.attributeId;
await setBooleanWithInheritance(standaloneNote, "foo", false);
expect(server.remove).toHaveBeenCalledWith(`notes/${standaloneNote.noteId}/attributes/${attributeId}`);
});
it("doesn't call server if value matches inherited", async () => {
const parentNote = buildNote({
title: "Parent note",
"#foo(inheritable)": "",
"children": [
{
title: "Child note"
}
]
});
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
expect(childNote.isLabelTruthy("foo")).toBe(true);
await setBooleanWithInheritance(childNote, "foo", true);
expect(server.put).not.toHaveBeenCalled();
expect(server.remove).not.toHaveBeenCalled();
});
it("overrides boolean with inheritance", async () => {
const parentNote = buildNote({
title: "Parent note",
"#foo(inheritable)": "",
"children": [
{
title: "Child note"
}
]
});
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
expect(childNote.isLabelTruthy("foo")).toBe(true);
await setBooleanWithInheritance(childNote, "foo", false);
expect(server.put).toHaveBeenCalledWith(`notes/${childNote.noteId}/set-attribute`, {
type: "label",
name: "foo",
value: "false",
isInheritable: false
});
});
it("overrides boolean with inherited false", async () => {
const parentNote = buildNote({
title: "Parent note",
"#foo(inheritable)": "false",
"children": [
{
title: "Child note"
}
]
});
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
expect(childNote.isLabelTruthy("foo")).toBe(false);
await setBooleanWithInheritance(childNote, "foo", true);
expect(server.put).toHaveBeenCalledWith(`notes/${childNote.noteId}/set-attribute`, {
type: "label",
name: "foo",
value: "",
isInheritable: false
});
});
it("deletes override boolean with inherited false with already existing value", async () => {
const parentNote = buildNote({
title: "Parent note",
"#foo(inheritable)": "false",
"children": [
{
title: "Child note",
"#foo": "false",
}
]
});
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
expect(childNote.isLabelTruthy("foo")).toBe(false);
await setBooleanWithInheritance(childNote, "foo", true);
expect(server.put).toBeCalledWith(`notes/${childNote.noteId}/set-attribute`, {
type: "label",
name: "foo",
value: "",
isInheritable: false
});
});
});

View File

@@ -1,14 +1,15 @@
import server from "./server.js";
import froca from "./froca.js";
import type FNote from "../entities/fnote.js";
import type { AttributeRow } from "./load_results.js";
import { AttributeType } from "@triliumnext/commons";
import type FNote from "../entities/fnote.js";
import froca from "./froca.js";
import type { AttributeRow } from "./load_results.js";
import server from "./server.js";
async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
await server.put(`notes/${noteId}/attribute`, {
type: "label",
name: name,
value: value,
name,
value,
isInheritable
});
}
@@ -16,8 +17,8 @@ async function addLabel(noteId: string, name: string, value: string = "", isInhe
export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
await server.put(`notes/${noteId}/set-attribute`, {
type: "label",
name: name,
value: value,
name,
value,
isInheritable
});
}
@@ -25,12 +26,42 @@ export async function setLabel(noteId: string, name: string, value: string = "",
export async function setRelation(noteId: string, name: string, value: string = "", isInheritable = false) {
await server.put(`notes/${noteId}/set-attribute`, {
type: "relation",
name: name,
value: value,
name,
value,
isInheritable
});
}
/**
* Sets a boolean label on the given note, taking inheritance into account. If the desired value matches the inherited
* value, any owned label will be removed to allow the inherited value to take effect. If the desired value differs
* from the inherited value, an owned label will be created or updated to reflect the desired value.
*
* When checking if the boolean value is set, don't use `note.hasLabel`; instead use `note.isLabelTruthy`.
*
* @param note the note on which to set the boolean label.
* @param labelName the name of the label to set.
* @param value the boolean value to set for the label.
*/
export async function setBooleanWithInheritance(note: FNote, labelName: string, value: boolean) {
const actualValue = note.isLabelTruthy(labelName);
if (actualValue === value) return;
const hasInheritedValue = !note.hasOwnedLabel(labelName) && note.hasLabel(labelName);
if (hasInheritedValue) {
if (value) {
setLabel(note.noteId, labelName, "");
} else {
// Label is inherited - override to false.
setLabel(note.noteId, labelName, "false");
}
} else if (value) {
setLabel(note.noteId, labelName, "");
} else {
removeOwnedLabelByName(note, labelName);
}
}
async function removeAttributeById(noteId: string, attributeId: string) {
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
}
@@ -142,6 +173,7 @@ export default {
setLabel,
setRelation,
setAttribute,
setBooleanWithInheritance,
removeAttributeById,
removeOwnedLabelByName,
removeOwnedRelationByName,

View File

@@ -1,12 +1,12 @@
import utils from "./utils.js";
import server from "./server.js";
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
import appContext from "../components/app_context.js";
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
import froca from "./froca.js";
import hoistedNoteService from "./hoisted_note.js";
import ws from "./ws.js";
import appContext from "../components/app_context.js";
import { t } from "./i18n.js";
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
import server from "./server.js";
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
import utils from "./utils.js";
import ws from "./ws.js";
// TODO: Deduplicate type with server
interface Response {
@@ -66,7 +66,7 @@ async function moveAfterBranch(branchIdsToMove: string[], afterBranchId: string)
}
}
async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: string) {
async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: string, componentId?: string) {
const newParentBranch = froca.getBranch(newParentBranchId);
if (!newParentBranch) {
return;
@@ -86,7 +86,7 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st
continue;
}
const resp = await server.put<Response>(`branches/${branchIdToMove}/move-to/${newParentBranchId}`);
const resp = await server.put<Response>(`branches/${branchIdToMove}/move-to/${newParentBranchId}`, undefined, componentId);
if (!resp.success) {
toastService.showError(resp.message);

View File

@@ -23,6 +23,12 @@ export interface RenderOptions {
imageHasZoom?: boolean;
/** If enabled, it will prevent the default behavior in which an empty note would display a list of children. */
noChildrenList?: boolean;
/** If enabled, it will prevent rendering of included notes. */
noIncludedNotes?: boolean;
/** If enabled, it will include archived notes when rendering children list. */
includeArchivedNotes?: boolean;
/** Set of note IDs that have already been seen during rendering to prevent infinite recursion. */
seenNoteIds?: Set<string>;
}
const CODE_MIME_TYPES = new Set(["application/json"]);

View File

@@ -0,0 +1,132 @@
import { trimIndentation } from "@triliumnext/commons";
import { describe, expect, it } from "vitest";
import { buildNote } from "../test/easy-froca";
import renderText from "./content_renderer_text";
describe("Text content renderer", () => {
it("renders included note", async () => {
const contentEl = document.createElement("div");
const includedNote = buildNote({
title: "Included note",
content: "<p>This is the included note.</p>"
});
const note = buildNote({
title: "New note",
content: trimIndentation`
<p>
Hi there
</p>
<section class="include-note" data-note-id="${includedNote.noteId}" data-box-size="medium">
&nbsp;
</section>
`
});
await renderText(note, $(contentEl));
expect(contentEl.querySelectorAll("section.include-note").length).toBe(1);
expect(contentEl.querySelectorAll("section.include-note p").length).toBe(1);
});
it("skips rendering included note", async () => {
const contentEl = document.createElement("div");
const includedNote = buildNote({
title: "Included note",
content: "<p>This is the included note.</p>"
});
const note = buildNote({
title: "New note",
content: trimIndentation`
<p>
Hi there
</p>
<section class="include-note" data-note-id="${includedNote.noteId}" data-box-size="medium">
&nbsp;
</section>
`
});
await renderText(note, $(contentEl), { noIncludedNotes: true });
expect(contentEl.querySelectorAll("section.include-note").length).toBe(0);
});
it("doesn't enter infinite loop on direct recursion", async () => {
const contentEl = document.createElement("div");
const note = buildNote({
title: "New note",
id: "Y7mBwmRjQyb4",
content: trimIndentation`
<p>
Hi there
</p>
<section class="include-note" data-note-id="Y7mBwmRjQyb4" data-box-size="medium">
&nbsp;
</section>
<section class="include-note" data-note-id="Y7mBwmRjQyb4" data-box-size="medium">
&nbsp;
</section>
`
});
await renderText(note, $(contentEl));
expect(contentEl.querySelectorAll("section.include-note").length).toBe(0);
});
it("doesn't enter infinite loop on indirect recursion", async () => {
const contentEl = document.createElement("div");
buildNote({
id: "first",
title: "Included note",
content: trimIndentation`\
<p>This is the included note.</p>
<section class="include-note" data-note-id="second" data-box-size="medium">
&nbsp;
</section>
`
});
const note = buildNote({
id: "second",
title: "New note",
content: trimIndentation`
<p>
Hi there
</p>
<section class="include-note" data-note-id="first" data-box-size="medium">
&nbsp;
</section>
`
});
await renderText(note, $(contentEl));
expect(contentEl.querySelectorAll("section.include-note").length).toBe(1);
});
it("renders children list when note is empty", async () => {
const contentEl = document.createElement("div");
const parentNote = buildNote({
title: "Parent note",
children: [
{ title: "Child note 1" },
{ title: "Child note 2" }
]
});
await renderText(parentNote, $(contentEl));
const items = contentEl.querySelectorAll("a");
expect(items.length).toBe(2);
expect(items[0].textContent).toBe("Child note 1");
expect(items[1].textContent).toBe("Child note 2");
});
it("skips archived notes in children list", async () => {
const contentEl = document.createElement("div");
const parentNote = buildNote({
title: "Parent note",
children: [
{ title: "Child note 1" },
{ title: "Child note 2", "#archived": "" },
{ title: "Child note 3" }
]
});
await renderText(parentNote, $(contentEl));
const items = contentEl.querySelectorAll("a");
expect(items.length).toBe(2);
expect(items[0].textContent).toBe("Child note 1");
expect(items[1].textContent).toBe("Child note 3");
});
});

View File

@@ -15,7 +15,14 @@ export default async function renderText(note: FNote | FAttachment, $renderedCon
if (blob && !isHtmlEmpty(blob.content)) {
$renderedContent.append($('<div class="ck-content">').html(blob.content));
await renderIncludedNotes($renderedContent[0]);
const seenNoteIds = options.seenNoteIds ?? new Set<string>();
seenNoteIds.add("noteId" in note ? note.noteId : note.attachmentId);
if (!options.noIncludedNotes) {
await renderIncludedNotes($renderedContent[0], seenNoteIds);
} else {
$renderedContent.find("section.include-note").remove();
}
if ($renderedContent.find("span.math-tex").length > 0) {
renderMathInElement($renderedContent[0], { trust: true });
@@ -35,11 +42,11 @@ export default async function renderText(note: FNote | FAttachment, $renderedCon
await rewriteMermaidDiagramsInContainer($renderedContent[0] as HTMLDivElement);
await formatCodeBlocks($renderedContent);
} else if (note instanceof FNote && !options.noChildrenList) {
await renderChildrenList($renderedContent, note);
await renderChildrenList($renderedContent, note, options.includeArchivedNotes ?? false);
}
}
async function renderIncludedNotes(contentEl: HTMLElement) {
async function renderIncludedNotes(contentEl: HTMLElement, seenNoteIds: Set<string>) {
// TODO: Consider duplicating with server's share/content_renderer.ts.
const includeNoteEls = contentEl.querySelectorAll("section.include-note");
@@ -66,7 +73,15 @@ async function renderIncludedNotes(contentEl: HTMLElement) {
continue;
}
const renderedContent = (await content_renderer.getRenderedContent(note)).$renderedContent;
if (seenNoteIds.has(noteId)) {
console.warn(`Skipping inclusion of ${noteId} to avoid circular reference.`);
includeNoteEl.remove();
continue;
}
const renderedContent = (await content_renderer.getRenderedContent(note, {
seenNoteIds
})).$renderedContent;
includeNoteEl.replaceChildren(...renderedContent);
}
}
@@ -98,7 +113,7 @@ export async function applyInlineMermaid(container: HTMLDivElement) {
}
}
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote) {
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote, includeArchivedNotes: boolean) {
let childNoteIds = note.getChildNoteIds();
if (!childNoteIds.length) {
@@ -108,14 +123,16 @@ async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: F
$renderedContent.css("padding", "10px");
$renderedContent.addClass("text-with-ellipsis");
// just load the first 10 child notes
if (childNoteIds.length > 10) {
childNoteIds = childNoteIds.slice(0, 10);
}
// just load the first 10 child notes
const childNotes = await froca.getNotes(childNoteIds);
for (const childNote of childNotes) {
if (childNote.isArchived && !includeArchivedNotes) continue;
$renderedContent.append(
await link.createLink(`${note.noteId}/${childNote.noteId}`, {
showTooltip: false,

View File

@@ -1,4 +1,5 @@
import type { AttachmentRow, EtapiTokenRow, NoteType, OptionNames } from "@triliumnext/commons";
import type { AttributeType } from "../entities/fattribute.js";
import type { EntityChange } from "../server_types.js";
@@ -135,7 +136,14 @@ export default class LoadResults {
}
getBranchRows() {
return this.branchRows.map((row) => this.getEntityRow("branches", row.branchId)).filter((branch) => !!branch);
return this.branchRows.map((row) => {
const branch = this.getEntityRow("branches", row.branchId);
if (branch) {
// Merge the componentId from the tracked row with the entity data
return { ...branch, componentId: row.componentId };
}
return null;
}).filter((branch) => !!branch) as BranchRow[];
}
addNoteReordering(parentNoteId: string, componentId: string) {
@@ -153,7 +161,14 @@ export default class LoadResults {
getAttributeRows(componentId = "none"): AttributeRow[] {
return this.attributeRows
.filter((row) => row.componentId !== componentId)
.map((row) => this.getEntityRow("attributes", row.attributeId))
.map((row) => {
const attr = this.getEntityRow("attributes", row.attributeId);
if (attr) {
// Merge the componentId from the tracked row with the entity data
return { ...attr, componentId: row.componentId };
}
return null;
})
.filter((attr) => !!attr) as AttributeRow[];
}

View File

@@ -1,14 +1,4 @@
export type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url" | "color";
type Multiplicity = "single" | "multi";
export interface DefinitionObject {
isPromoted?: boolean;
labelType?: LabelType;
multiplicity?: Multiplicity;
numberPrecision?: number;
promotedAlias?: string;
inverseRelation?: string;
}
import { DefinitionObject, LabelType, Multiplicity } from "@triliumnext/commons";
function parse(value: string) {
const tokens = value.split(",").map((t) => t.trim());

View File

@@ -69,24 +69,6 @@ export function buildNote(noteDef: NoteDefinition) {
});
note.getBlob = async () => blob;
// Manage children.
if (noteDef.children) {
for (const childDef of noteDef.children) {
const childNote = buildNote(childDef);
const branchId = `${note.noteId}_${childNote.noteId}`;
const branch = new FBranch(froca, {
branchId,
noteId: childNote.noteId,
parentNoteId: note.noteId,
notePosition: childNotePosition,
fromSearchNote: false
});
froca.branches[branchId] = branch;
note.addChild(childNote.noteId, branchId, false);
childNotePosition += 10;
}
}
let position = 0;
for (const [ key, value ] of Object.entries(noteDef)) {
const attributeId = utils.randomString(12);
@@ -136,5 +118,25 @@ export function buildNote(noteDef: NoteDefinition) {
}
noteAttributeCache.attributes[note.noteId].push(attribute);
}
// Manage children.
if (noteDef.children) {
for (const childDef of noteDef.children) {
const childNote = buildNote(childDef);
const branchId = `${note.noteId}_${childNote.noteId}`;
const branch = new FBranch(froca, {
branchId,
noteId: childNote.noteId,
parentNoteId: note.noteId,
notePosition: childNotePosition,
fromSearchNote: false
});
froca.branches[branchId] = branch;
note.addChild(childNote.noteId, branchId, false);
childNote.addParent(note.noteId, branchId, false);
childNotePosition += 10;
}
}
return note;
}

View File

@@ -798,7 +798,8 @@
"expand_tooltip": "展开此集合的直接子代(单层深度)。点击右方箭头以查看更多选项。",
"expand_first_level": "展开直接子代",
"expand_nth_level": "展开 {{depth}} 层",
"expand_all_levels": "展开所有层级"
"expand_all_levels": "展开所有层级",
"hide_child_notes": "隐藏树中的子笔记"
},
"edited_notes": {
"no_edited_notes_found": "今天还没有编辑过的笔记...",
@@ -1505,7 +1506,10 @@
"duplicate": "复制",
"open-in-popup": "快速编辑",
"archive": "归档",
"unarchive": "解压"
"unarchive": "解压",
"open-in-a-new-window": "在新窗口中打开",
"hide-subtree": "隐藏子树",
"show-subtree": "显示子树"
},
"shared_info": {
"help_link": "访问 <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a> 获取帮助。",
@@ -1598,7 +1602,9 @@
"shared-indicator-tooltip": "此笔记已公开分享",
"shared-indicator-tooltip-with-url": "此笔记已公开分享至:{{- url}}",
"clone-indicator-tooltip": "此笔记有 {{- count}} 个父级: {{- parents}}",
"clone-indicator-tooltip-single": "此笔记已克隆1 个额外的父级:{{- parent}}"
"clone-indicator-tooltip-single": "此笔记已克隆1 个额外的父级:{{- parent}}",
"subtree-hidden-tooltip_other": "从树中隐藏的 {{count}} 篇子笔记",
"subtree-hidden-moved-title": "已添加到 {{title}}"
},
"title_bar_buttons": {
"window-on-top": "保持此窗口置顶"

View File

@@ -25,7 +25,8 @@
},
"widget-list-error": {
"title": "Abruf der Liste von Widgets vom Server ist fehlgeschlagen"
}
},
"open-script-note": "Script-Notiz öffnen"
},
"add_link": {
"add_link": "Link hinzufügen",
@@ -208,7 +209,8 @@
"info": {
"modalTitle": "Infonachricht",
"closeButton": "Schließen",
"okButton": "OK"
"okButton": "OK",
"copy_to_clipboard": "In die Zwischenablage kopieren"
},
"jump_to_note": {
"search_button": "Suche im Volltext",
@@ -695,7 +697,9 @@
"export_as_image": "Als Bild exportieren",
"export_as_image_png": "PNG (Raster)",
"export_as_image_svg": "SVG (Vektor)",
"note_map": "Notizen Karte"
"note_map": "Notizen Karte",
"view_revisions": "Notizrevisionen",
"advanced": "Erweitert"
},
"onclick_button": {
"no_click_handler": "Das Schaltflächen-Widget „{{componentId}}“ hat keinen definierten Klick-Handler"

View File

@@ -800,7 +800,8 @@
"geo-map": "Geo Map",
"board": "Board",
"presentation": "Presentation",
"include_archived_notes": "Show archived notes"
"include_archived_notes": "Show archived notes",
"hide_child_notes": "Hide child notes in tree"
},
"edited_notes": {
"no_edited_notes_found": "No edited notes on this day yet...",
@@ -1643,6 +1644,7 @@
"tree-context-menu": {
"open-in-a-new-tab": "Open in a new tab",
"open-in-a-new-split": "Open in a new split",
"open-in-a-new-window": "Open in a new window",
"insert-note-after": "Insert note after",
"insert-child-note": "Insert child note",
"archive": "Archive",
@@ -1655,6 +1657,8 @@
"advanced": "Advanced",
"expand-subtree": "Expand subtree",
"collapse-subtree": "Collapse subtree",
"hide-subtree": "Hide subtree",
"show-subtree": "Show subtree",
"sort-by": "Sort by...",
"recent-changes-in-subtree": "Recent changes in subtree",
"convert-to-attachment": "Convert to attachment",
@@ -1772,7 +1776,12 @@
"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}}"
"shared-indicator-tooltip-with-url": "This note is shared publicly at: {{- url}}",
"subtree-hidden-tooltip_one": "{{count}} child note that is hidden from the tree",
"subtree-hidden-tooltip_other": "{{count}} child notes that are hidden from the tree",
"subtree-hidden-moved-title": "Added to {{title}}",
"subtree-hidden-moved-description-collection": "This collection hides its child notes in the tree.",
"subtree-hidden-moved-description-other": "Child notes are hidden in the tree for this note."
},
"title_bar_buttons": {
"window-on-top": "Keep Window on Top"

View File

@@ -21,7 +21,13 @@
},
"bundle-error": {
"title": "Hubo un fallo al cargar un script personalizado",
"message": "El script de la nota con ID \"{{id}}\", titulado \"{{title}}\" no pudo ser ejecutado debido a:\n\n{{message}}"
"message": "El script no pudo ser ejecutado debido a:\n\n{{message}}"
},
"widget-list-error": {
"title": "Hubo un fallo al obtener la lista de widgets del servidor"
},
"widget-render-error": {
"title": "Hubo un fallo al renderizar un widget personalizado de React"
}
},
"add_link": {
@@ -162,7 +168,8 @@
"other": "Otro",
"quickSearch": "centrarse en la entrada de búsqueda rápida",
"inPageSearch": "búsqueda en la página",
"title": "Hoja de ayuda"
"title": "Hoja de ayuda",
"editShortcuts": "Editar atajos de teclado"
},
"import": {
"importIntoNote": "Importar a nota",
@@ -690,7 +697,7 @@
"convert_into_attachment_successful": "La nota '{{title}}' ha sido convertida a un archivo adjunto.",
"convert_into_attachment_prompt": "¿Está seguro que desea convertir la nota '{{title}}' en un archivo adjunto de la nota padre?",
"print_pdf": "Exportar como PDF...",
"open_note_on_server": "Abrir nota en el servidor"
"open_note_on_server": "Abrir nota en servidor"
},
"onclick_button": {
"no_click_handler": "El widget de botón '{{componentId}}' no tiene un controlador de clics definido"
@@ -736,7 +743,7 @@
"zpetne_odkazy": {
"relation": "relación",
"backlink_one": "{{count}} Vínculo de retroceso",
"backlink_many": "",
"backlink_many": "{{count}} Vínculos de retroceso",
"backlink_other": "{{count}} vínculos de retroceso"
},
"mobile_detail_menu": {
@@ -749,7 +756,10 @@
"note_icon": {
"change_note_icon": "Cambiar icono de nota",
"search": "Búsqueda:",
"reset-default": "Restablecer a icono por defecto"
"reset-default": "Restablecer a icono por defecto",
"search_placeholder_one": "Buscar {{number}} icono a través de {{count}} paquetes",
"search_placeholder_many": "Buscar {{number}} iconos a través de {{count}} paquetes",
"search_placeholder_other": "Buscar {{number}} iconos a través de {{count}} paquetes"
},
"basic_properties": {
"note_type": "Tipo de nota",
@@ -789,7 +799,7 @@
"file_type": "Tipo de archivo",
"file_size": "Tamaño del archivo",
"download": "Descargar",
"open": "Abrir",
"open": "Abrir externamente",
"upload_new_revision": "Subir nueva revisión",
"upload_success": "Se ha subido una nueva revisión de archivo.",
"upload_failed": "Error al cargar una nueva revisión de archivo.",
@@ -1302,11 +1312,11 @@
"code_mime_types": {
"title": "Tipos MIME disponibles en el menú desplegable",
"tooltip_syntax_highlighting": "Resaltado de sintaxis",
"tooltip_code_block_syntax": "Bloques de código en notas de texto",
"tooltip_code_note_syntax": "Notas de código"
"tooltip_code_block_syntax": "Bloques de Código en notas de Texto",
"tooltip_code_note_syntax": "Notas de Código"
},
"vim_key_bindings": {
"use_vim_keybindings_in_code_notes": "Atajos de teclas de Vim",
"use_vim_keybindings_in_code_notes": "Combinaciones de teclas Vim",
"enable_vim_keybindings": "Habilitar los atajos de teclas de Vim en la notas de código (no es modo ex)"
},
"wrap_lines": {
@@ -1571,7 +1581,7 @@
"will_be_deleted_in": "Este archivo adjunto se eliminará automáticamente en {{time}}",
"will_be_deleted_soon": "Este archivo adjunto se eliminará automáticamente pronto",
"deletion_reason": ", porque el archivo adjunto no está vinculado en el contenido de la nota. Para evitar la eliminación, vuelva a agregar el enlace del archivo adjunto al contenido o convierta el archivo adjunto en una nota.",
"role_and_size": "Rol: {{role}}, Tamaño: {{size}}",
"role_and_size": "Rol: {{role}}, tamaño: {{size}}, MIME: {{- mimeType}}",
"link_copied": "Enlace del archivo adjunto copiado al portapapeles.",
"unrecognized_role": "Rol de archivo adjunto no reconocido '{{role}}'."
},
@@ -1622,7 +1632,7 @@
"import-into-note": "Importar a nota",
"apply-bulk-actions": "Aplicar acciones en lote",
"converted-to-attachments": "{{count}} notas han sido convertidas en archivos adjuntos.",
"convert-to-attachment-confirm": "¿Está seguro que desea convertir las notas seleccionadas en archivos adjuntos de sus notas padres?",
"convert-to-attachment-confirm": "¿Está seguro que desea convertir las notas seleccionadas en archivos adjuntos de sus notas padres? Esta operación solo aplica a notas de Imagen, otras notas serán omitidas.",
"open-in-popup": "Edición rápida",
"archive": "Archivar",
"unarchive": "Desarchivar"
@@ -1717,7 +1727,10 @@
"note_detail": {
"could_not_find_typewidget": "No se pudo encontrar typeWidget para el tipo '{{type}}'",
"printing": "Impresión en curso...",
"printing_pdf": "Exportando a PDF en curso.."
"printing_pdf": "Exportando a PDF en curso..",
"print_report_collection_content_one": "{{count}} nota en la colección no se puede imprimir porque no son compatibles o está protegida.",
"print_report_collection_content_many": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas.",
"print_report_collection_content_other": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas."
},
"note_title": {
"placeholder": "escriba el título de la nota aquí..."
@@ -1929,7 +1942,7 @@
"unknown_widget": "Widget desconocido para \"{{id}}\"."
},
"note_language": {
"not_set": "No establecido",
"not_set": "Idioma no establecido",
"configure-languages": "Configurar idiomas..."
},
"content_language": {
@@ -1968,7 +1981,7 @@
"hide-weekends": "Ocultar fines de semana",
"show-scale": "Mostrar escala",
"display-week-numbers": "Mostrar números de semana",
"map-style": "Estilo de mapa:",
"map-style": "Estilo de mapa",
"max-nesting-depth": "Máxima profundidad de anidamiento:",
"vector_light": "Vector (claro)",
"vector_dark": "Vector (oscuro)",
@@ -2097,5 +2110,36 @@
"clear-color": "Borrar color de nota",
"set-color": "Asignar color de nota",
"set-custom-color": "Asignar color de nota personalizado"
},
"status_bar": {
"backlinks_one": "{{count}} vínculo de retroceso",
"backlinks_many": "{{count}} vínculos de retroceso",
"backlinks_other": "{{count}} vínculos de retroceso",
"backlinks_title_one": "Ver vínculo de retroceso",
"backlinks_title_many": "Ver vínculos de retroceso",
"backlinks_title_other": "Ver vínculos de retroceso",
"attachments_one": "{{count}} adjunto",
"attachments_many": "{{count}} adjuntos",
"attachments_other": "{{count}} adjuntos",
"attachments_title_one": "Ver adjunto en una nueva pestaña",
"attachments_title_many": "Ver adjuntos en una nueva pestaña",
"attachments_title_other": "Ver adjuntos en una nueva pestaña",
"attributes_one": "{{count}} atributo",
"attributes_many": "{{count}} atributos",
"attributes_other": "{{count}} atributos",
"note_paths_one": "{{count}} ruta",
"note_paths_many": "{{count}} rutas",
"note_paths_other": "{{count}} rutas"
},
"pdf": {
"attachments_one": "{{count}} adjunto",
"attachments_many": "{{count}} adjuntos",
"attachments_other": "{{count}} adjuntos",
"layers_one": "{{count}} capa",
"layers_many": "{{count}} capas",
"layers_other": "{{count}} capas",
"pages_one": "{{count}} página",
"pages_many": "{{count}} páginas",
"pages_other": "{{count}} páginas"
}
}

View File

@@ -21,7 +21,7 @@
},
"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}}"
"message": "Le script 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"

View File

@@ -1,7 +1,10 @@
{
"about": {
"title": "ट्रिलियम नोट्स के बारें में",
"build_date": "निर्माण की तारीख:"
"build_date": "निर्माण की तारीख:",
"app_version": "ऐप वर्ज़न:",
"db_version": "DB वर्ज़न:",
"build_revision": "बिल्ड रिविज़न:"
},
"toast": {
"widget-error": {
@@ -31,5 +34,17 @@
},
"add_link": {
"note": "नोट"
},
"bulk_actions": {
"other": "अन्य"
},
"clone_to": {
"search_for_note_by_its_name": "नोट क नाम से नोट खोजें"
},
"confirm": {
"also_delete_note": "नोट भी डिलीट करें"
},
"delete_notes": {
"delete_notes_preview": "नोट्स प्रिव्यू डिलीट करें"
}
}

View File

@@ -21,7 +21,13 @@
},
"bundle-error": {
"title": "Nem sikerült betölteni az egyéni szkriptet",
"message": "A(z) \"{{id}}\" azonosítójú, \"{{title}}\" című jegyzetből származó szkript nem hajtható végre a következő ok miatt:\n\n{{message}}"
"message": "A skript nem hajtható végre a következő ok miatt:\n\n{{message}}"
},
"widget-list-error": {
"title": "A Widget-ek letöltése sikertelen volt"
},
"widget-render-error": {
"title": "Nem sikerült renderelni a React widget-et"
}
},
"add_link": {

View File

@@ -1895,7 +1895,11 @@
"create-child-note": "Crea nota figlio",
"unhoist": "Sganciare",
"toggle-sidebar": "Attiva/disattiva la barra laterale",
"dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione."
"dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione.",
"clone-indicator-tooltip": "Questa nota ha {{- count}} genitori: {{- parents}}",
"clone-indicator-tooltip-single": "Questa nota è stata clonata (1 genitore aggiuntivo: {{- parent}})",
"shared-indicator-tooltip": "Questa nota è condivisa pubblicamente",
"shared-indicator-tooltip-with-url": "Questa nota è condivisa pubblicamente all'indirizzo: {{- url}}"
},
"title_bar_buttons": {
"window-on-top": "Mantieni la finestra in primo piano"
@@ -2200,7 +2204,14 @@
"execute_sql_description": "Questa nota è una nota SQL. Clicca per eseguire la query SQL.",
"shared_copy_to_clipboard": "Copia link negli appunti",
"shared_open_in_browser": "Apri il link nel browser",
"shared_unshare": "Rimuovi condivisione"
"shared_unshare": "Rimuovi condivisione",
"save_status_saved": "Salvato",
"save_status_saving": "Salvataggio in corso...",
"save_status_unsaved": "Non salvato",
"save_status_error": "Salvataggio non riuscito",
"save_status_saving_tooltip": "Le modifiche sono state salvate.",
"save_status_unsaved_tooltip": "Ci sono modifiche non salvate. Verranno salvate automaticamente tra un attimo.",
"save_status_error_tooltip": "Si è verificato un errore durante il salvataggio della nota. Se possibile, prova a copiare il contenuto della nota altrove e a ricaricare l'applicazione."
},
"breadcrumb": {
"workspace_badge": "Area di lavoro",
@@ -2243,5 +2254,18 @@
"empty_button": "Nascondi il pannello",
"toggle": "Attiva/disattiva pannello destro",
"custom_widget_go_to_source": "Vai al codice sorgente"
},
"pdf": {
"attachments_one": "{{count}} allegato",
"attachments_many": "{{count}} allegati",
"attachments_other": "{{count}} allegati",
"layers_one": "{{count}} livello",
"layers_many": "{{count}} livelli",
"layers_other": "{{count}} livelli",
"pages_one": "{{count}} pagina",
"pages_many": "{{count}} pagine",
"pages_other": "{{count}} pagine",
"pages_alt": "Pagina {{pageNumber}}",
"pages_loading": "Caricamento in corso..."
}
}

View File

@@ -443,7 +443,10 @@
"unhoist-note": "ノートのホイストを解除",
"edit-branch-prefix": "ブランチの接頭辞を編集",
"archive": "アーカイブ",
"unarchive": "アーカイブ解除"
"unarchive": "アーカイブ解除",
"open-in-a-new-window": "新しいウィンドウで開く",
"hide-subtree": "サブツリーを非表示",
"show-subtree": "サブツリーを表示"
},
"zen_mode": {
"button_exit": "禅モードを退出"
@@ -568,7 +571,8 @@
"expand_tooltip": "このコレクションの直下の子1階層下を展開します。その他のオプションについては、右側の矢印を押してください。",
"expand_first_level": "直下の子を展開",
"expand_nth_level": "{{depth}} 階層下まで展開",
"expand_all_levels": "すべての階層を展開"
"expand_all_levels": "すべての階層を展開",
"hide_child_notes": "ツリー内の子ノートを非表示"
},
"note_types": {
"geo-map": "ジオマップ",
@@ -1248,7 +1252,11 @@
"clone-indicator-tooltip": "このノートには {{- count}} 個の親があります: {{- parents}}",
"clone-indicator-tooltip-single": "このノートは複製されています (親が 1 件追加: {{- parent}})",
"shared-indicator-tooltip": "このノートは公開されています",
"shared-indicator-tooltip-with-url": "このノートは以下で公開されています: {{- url}}"
"shared-indicator-tooltip-with-url": "このノートは以下で公開されています: {{- url}}",
"subtree-hidden-tooltip_other": "{{count}} 個の子ノートがツリーで非表示になっています",
"subtree-hidden-moved-title": "{{title}} に追加されました",
"subtree-hidden-moved-description-collection": "このコレクションはツリー内の子ノートを非表示にします。",
"subtree-hidden-moved-description-other": "このノートのツリーでは子ノートは非表示になっています。"
},
"bulk_actions": {
"bulk_actions": "一括操作",

View File

@@ -18,5 +18,47 @@
"zpetne_odkazy": {
"backlink_one": "{{count}} Tilbakelenke",
"backlink_other": "{{count}} Tilbakelenker"
},
"add_link": {
"note": "Notat"
},
"branch_prefix": {
"prefix": "Prefiks : ",
"save": "Lagre"
},
"bulk_actions": {
"labels": "Etiketter",
"relations": "Relasjoner",
"notes": "Notater",
"other": "Andre"
},
"confirm": {
"confirmation": "Bekreftelse",
"cancel": "Avbryt",
"ok": "OK"
},
"delete_notes": {
"close": "Lukk",
"cancel": "Avbryt",
"ok": "OK"
},
"export": {
"close": "Lukk",
"export": "Eksporter"
},
"note_type_chooser": {
"templates": "Maler"
},
"help": {
"title": "Hurtigveiledning",
"troubleshooting": "Feilsøking",
"other": "Andre"
},
"import": {
"options": "Alternativer",
"import": "Importer"
},
"include_note": {
"label_note": "Notat"
}
}

View File

@@ -12,7 +12,7 @@
"toast": {
"critical-error": {
"title": "Kritische Error",
"message": "Een kritieke fout heeft plaatsgevonden waardoor de cliënt zich aanmeldt vanaf het begin:\n\n84X\n\nDit is waarschijnlijk veroorzaakt door een script dat op een onverwachte manier faalt. Probeer de sollicitatie in veilige modus te starten en de kwestie aan te spreken."
"message": "Een kritieke fout heeft plaatsgevonden waardoor de applicatie niet kon opstarten:\n\n{{message}}\n\nDit is waarschijnlijk veroorzaakt door een onverwachte fout in een script. Probeer de applicatie op te starten in veilige modus en het probleem op te lossen."
},
"widget-error": {
"title": "Starten widget mislukt",
@@ -22,7 +22,16 @@
"bundle-error": {
"title": "Custom script laden mislukt",
"message": "Script van notitie met ID \"{{id}}\", getiteld \"{{title}}\" kon niet worden uitgevoerd vanwege:\n\n{{message}}"
}
},
"scripting-error": "Error met script: {{title}}",
"widget-list-error": {
"title": "Kon geen lijst met widgets ophalen van de server"
},
"widget-render-error": {
"title": "React-widget kon niet geladen worden"
},
"widget-missing-parent": "Widget heeft niet het verplichte '{{property}}'-veld gedefinieerd.\n\nAls dit script is bedoeld om zonder interface te draaien, gebruik dan in plaats daarvan '#run=frontendStartup'.",
"open-script-note": "Open scriptnotitie"
},
"add_link": {
"add_link": "Voeg link toe",
@@ -41,7 +50,8 @@
"help_on_tree_prefix": "Help bij boomvoorvoegsel",
"prefix": "Voorvoegsel: ",
"edit_branch_prefix_multiple": "Bewerk zijtakvoorvoegsel voor {{count}} zijtakken",
"branch_prefix_saved_multiple": "Vertakkingsvoorvoegsel opgeslagen voor {{count}} vertakkingen."
"branch_prefix_saved_multiple": "Vertakkingsvoorvoegsel opgeslagen voor {{count}} vertakkingen.",
"affected_branches": "Aangetaste takken ({{count}}):"
},
"bulk_actions": {
"bulk_actions": "Bulk acties",
@@ -54,7 +64,8 @@
"labels": "Labels",
"relations": "Relaties",
"notes": "Notities",
"other": "Andere"
"other": "Andere",
"include_descendants": "Tel afstammelingen van de geselecteerde notities mee"
},
"calendar": {
"april": "April",
@@ -78,5 +89,35 @@
},
"show_toc_widget_button": {
"show_toc": "Laat Inhoudsopgave zien"
},
"status_bar": {
"note_paths_one": "{{count}} pad",
"note_paths_other": "{{count}} paden",
"note_paths_title": "Notitiepaden",
"code_note_switcher": "Verander de taalmodus"
},
"attributes_panel": {
"title": "Notitie-attributen"
},
"right_pane": {
"empty_message": "Geen informatie voor deze notitie",
"empty_button": "Verberg dit paneel",
"toggle": "Schakel rechterpaneel in/uit",
"custom_widget_go_to_source": "Go naar de broncode"
},
"pdf": {
"attachments_one": "{{count}} bijlage",
"attachments_other": "{{count}} bijlagen",
"layers_one": "{{count}} laag",
"layers_other": "{{count}} lagen",
"pages_one": "{{count}} pagina",
"pages_other": "{{count}} pagina's",
"pages_alt": "Pagina {{pageNumber}}",
"pages_loading": "Laden..."
},
"clone_to": {
"clone_notes_to": "Kloon de notities naar...",
"help_on_links": "Hulp op links",
"notes_to_clone": "Notities om te klonen"
}
}

View File

@@ -22,7 +22,16 @@
"bundle-error": {
"title": "Falha para carregar o script customizado",
"message": "O script da nota com ID \"{{id}}\", intitulada \"{{title}}\", não pôde ser executado devido a:\n\n{{message}}"
}
},
"widget-list-error": {
"title": "Falha ao obter a lista de widgets do servidor"
},
"scripting-error": "Erro do script específicado: {{title}}",
"open-script-note": "Abrir script da nota",
"widget-render-error": {
"title": "Falha do renderizar um widget React personalizado"
},
"widget-missing-parent": "Widget adaptado não tem a propriedade '{{property}}' mandatória definida.\n\nSe este script é para ser executado sem um element de UI, usar '#run=frontendStartup'."
},
"add_link": {
"add_link": "Adicionar ligação",
@@ -39,7 +48,10 @@
"help_on_tree_prefix": "Ajuda sobre o prefixo da árvore de notas",
"prefix": "Prefixo: ",
"save": "Gravar",
"branch_prefix_saved": "O prefixo de ramificação foi gravado."
"branch_prefix_saved": "O prefixo de ramificação foi gravado.",
"edit_branch_prefix_multiple": "Editar prefixo para {{count}} branches",
"branch_prefix_saved_multiple": "Prefixo dos branches foi editado para {{count}} branches.",
"affected_branches": "Alterados ({{count}}) branches:"
},
"bulk_actions": {
"bulk_actions": "Ações em massa",
@@ -104,7 +116,8 @@
"export_status": "Estado da exportação",
"export_in_progress": "Exportação em andamento: {{progressCount}}",
"export_finished_successfully": "Exportação concluída com sucesso.",
"format_pdf": "PDF para impressão ou compartilhamento."
"format_pdf": "PDF para impressão ou compartilhamento.",
"share-format": "HTML para publicação web - usa o mesmo tema que é usado para notas partilhadas, mas pode ser publicado como um site estatico."
},
"help": {
"title": "Folha de Dicas",
@@ -158,7 +171,8 @@
"showSQLConsole": "mostrar console SQL",
"other": "Outros",
"quickSearch": "focar no campo de pesquisa rápida",
"inPageSearch": "pesquisa na página"
"inPageSearch": "pesquisa na página",
"editShortcuts": "Editar atalhos do teclado"
},
"import": {
"importIntoNote": "Importar para a nota",
@@ -184,7 +198,8 @@
},
"import-status": "Estado da importação",
"in-progress": "Importação em andamento: {{progress}}",
"successful": "Importação concluída com sucesso."
"successful": "Importação concluída com sucesso.",
"importZipRecommendation": "Quando a importar ficheiro ZIP, a hierarquia de notas vai reflectir a estrutura da sub directoria dentro do ficheiro."
},
"include_note": {
"dialog_title": "Incluir nota",
@@ -199,7 +214,8 @@
"info": {
"modalTitle": "Mensagem informativa",
"closeButton": "Fechar",
"okButton": "OK"
"okButton": "OK",
"copy_to_clipboard": "Copiar para a área de transferência"
},
"jump_to_note": {
"search_placeholder": "Pesquise uma nota pelo nome ou digite > para comandos...",
@@ -274,7 +290,12 @@
"download_button": "Descarregar",
"mime": "MIME: ",
"file_size": "Tamanho do ficheiro:",
"preview_not_available": "A visualização não está disponível para este tipo de nota."
"preview_not_available": "A visualização não está disponível para este tipo de nota.",
"diff_on": "Mostrar diferenças",
"diff_off": "Mostrar conteúdos",
"diff_on_hint": "Carregar para mostrar diferenças da fonte da nota",
"diff_off_hint": "Carregar para mostrar conteúdos da nota",
"diff_not_available": "Diferenças não disponível."
},
"sort_child_notes": {
"sort_children_by": "Ordenar notas filhas por...",
@@ -585,7 +606,18 @@
"september": "Setembro",
"october": "Outubro",
"november": "Novembro",
"december": "Dezembro"
"december": "Dezembro",
"week": "Semana",
"week_previous": "Semana anterior",
"week_next": "Próxima semana",
"month": "Mês",
"month_previous": "Mês anterior",
"month_next": "Próximo mês",
"year": "Ano",
"year_previous": "Ano anterior",
"year_next": "Próximo ano",
"list": "Lista",
"today": "Hoje"
},
"close_pane_button": {
"close_this_pane": "Fechar este painel"
@@ -628,7 +660,9 @@
"about": "Sobre o Trilium Notes",
"logout": "Sair",
"show-cheatsheet": "Exibir Cheatsheet",
"toggle-zen-mode": "Modo Zen"
"toggle-zen-mode": "Modo Zen",
"new-version-available": "Nova actualização disponível",
"download-update": "Obter versão {{latestVersion}}"
},
"zen_mode": {
"button_exit": "Sair do Modo Zen"
@@ -666,7 +700,14 @@
"convert_into_attachment_failed": "A conversão da nota '{{title}}' falhou.",
"convert_into_attachment_successful": "A nota '{{title}}' foi convertida para anexo.",
"convert_into_attachment_prompt": "Tem certeza que quer converter a nota '{{title}}' num anexo da nota pai?",
"print_pdf": "Exportar como PDF…"
"print_pdf": "Exportar como PDF…",
"open_note_on_server": "Abrir nota no servidor",
"export_as_image": "Exportar como imagem",
"note_map": "Mapa de notas",
"advanced": "Avançadas",
"view_revisions": "Revisões da nota...",
"export_as_image_svg": "SVG (vectorial)",
"export_as_image_png": "PNG (matricial)"
},
"onclick_button": {
"no_click_handler": "Componente de botão '{{componentId}}' não possui manipulador de clique definido"
@@ -712,19 +753,26 @@
"zpetne_odkazy": {
"relation": "relação",
"backlink_one": "{{count}} Ligação Reversa",
"backlink_many": "",
"backlink_many": "{{count}} Ligações Reversas",
"backlink_other": "{{count}} Ligações Reversas"
},
"mobile_detail_menu": {
"insert_child_note": "Inserir nota filha",
"delete_this_note": "Apagar esta nota",
"error_cannot_get_branch_id": "Não foi possível obter o branchId para o notePath '{{notePath}} '",
"error_unrecognized_command": "Comando não reconhecido {{command}}"
"error_unrecognized_command": "Comando não reconhecido {{command}}",
"note_revisions": "Revisões da nota"
},
"note_icon": {
"change_note_icon": "Alterar ícone da nota",
"search": "Pesquisa:",
"reset-default": "Redefinir para o ícone padrão"
"reset-default": "Redefinir para o ícone padrão",
"filter": "Filtrar",
"filter-none": "Todos os icons",
"filter-default": "Icons default",
"no_results": "Não foram encontrados icons.",
"search_placeholder_filtered": "Procurar {{number}} icons no {{name}}",
"icon_tooltip": "{{name}}\nPacote de icons: {{iconPack}}"
},
"basic_properties": {
"note_type": "Tipo da nota",
@@ -745,7 +793,13 @@
"calendar": "Calendário",
"table": "Tabela",
"geo-map": "Mapa geográfico",
"board": "Quadro"
"board": "Quadro",
"expand_first_level": "Expandir descendentes directos",
"presentation": "Apresentação",
"expand_nth_level": "Expandir {{depth}} níveis",
"expand_all_levels": "Expandir todos os níveis",
"include_archived_notes": "Mostrar notas arquivadas",
"expand_tooltip": "Expande a direcção dos descendentes desta colecção (um nível). Para mais opções, carregar na seta à direita."
},
"edited_notes": {
"no_edited_notes_found": "Ainda não há nenhuma nota editada neste dia…",
@@ -778,7 +832,8 @@
},
"inherited_attribute_list": {
"title": "Atributos Herdados",
"no_inherited_attributes": "Nenhum atributo herdado."
"no_inherited_attributes": "Nenhum atributo herdado.",
"none": "Nenhum"
},
"note_info_widget": {
"note_id": "ID da Nota",
@@ -789,7 +844,9 @@
"note_size_info": "O tamanho da nota fornece uma estimativa aproximada dos requisitos de armazenamento para esta nota. Leva em conta o conteúdo e o conteúdo das suas revisões de nota.",
"calculate": "calcular",
"subtree_size": "(tamanho da subárvore: {{size}} em {{count}} notas)",
"title": "Informações da nota"
"title": "Informações da nota",
"mime": "Tipo MIME",
"show_similar_notes": "Mostrar notas semelhantes"
},
"note_map": {
"open_full": "Expandir completamente",
@@ -852,7 +909,8 @@
"search_parameters": "Parâmetros de Pesquisa",
"unknown_search_option": "Opção de pesquisa desconhecida {{searchOptionName}}",
"search_note_saved": "Nota de pesquisa foi gravada em {{- notePathTitle}}",
"actions_executed": "As ações foram executadas."
"actions_executed": "As ações foram executadas.",
"view_options": "Ver opções:"
},
"similar_notes": {
"title": "Notas Similares",
@@ -946,14 +1004,20 @@
"no_attachments": "Esta nota não possuí anexos."
},
"book": {
"no_children_help": "Esta coleção não possui nenhum nota filha, então não há nada para exibir. Veja <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> para pormenores."
"no_children_help": "Esta coleção não possui nenhum nota filha, então não há nada para exibir. Veja <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> para pormenores.",
"drag_locked_title": "Bloqueado para edição",
"drag_locked_message": "Arrastar não permitida pois a coleção está bloqueada para edição."
},
"editable_code": {
"placeholder": "Digite o conteúdo da sua nota de código aqui…"
},
"editable_text": {
"placeholder": "Digite o conteúdo da sua nota aqui…",
"auto-detect-language": "Detetado automaticamente"
"auto-detect-language": "Detetado automaticamente",
"editor_crashed_title": "O editor de texto quebrou",
"editor_crashed_details_button": "Ver mais detalhes...",
"editor_crashed_details_title": "Informação técnica",
"editor_crashed_details_intro": "Se teve este erro várias vezes, considerer reportar no GitHub disponibilizando a informação abaixo."
},
"empty": {
"open_note_instruction": "Abra uma nota a digitar o título da nota no campo abaixo ou escolha uma nota na árvore.",

View File

@@ -797,7 +797,8 @@
"expand_tooltip": "展開此集合的直接子級(單層深度)。按下右側箭頭以查看更多選項。",
"expand_first_level": "展開直接子級",
"expand_nth_level": "展開 {{depth}} 層",
"expand_all_levels": "展開所有層級"
"expand_all_levels": "展開所有層級",
"hide_child_notes": "隱藏樹中的子筆記"
},
"edited_notes": {
"no_edited_notes_found": "今天還沒有編輯過的筆記...",
@@ -1466,7 +1467,10 @@
"duplicate": "複製副本",
"open-in-popup": "快速編輯",
"archive": "封存",
"unarchive": "解除封存"
"unarchive": "解除封存",
"open-in-a-new-window": "在新視窗打開",
"hide-subtree": "隱藏子階層",
"show-subtree": "顯示子階層"
},
"shared_info": {
"help_link": "如需幫助,請訪問 <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a>。",
@@ -1560,7 +1564,11 @@
"clone-indicator-tooltip": "此筆記有 {{- count}} 個父級:{{- parents}}",
"clone-indicator-tooltip-single": "此筆記已克隆(新增 1 個父級:{{- parent}}",
"shared-indicator-tooltip": "此筆記已公開分享",
"shared-indicator-tooltip-with-url": "此筆記已公開分享至:{{- url}}"
"shared-indicator-tooltip-with-url": "此筆記已公開分享至:{{- url}}",
"subtree-hidden-tooltip_one": "從樹中隱藏的 {{count}} 篇子筆記",
"subtree-hidden-moved-title": "已新增至 {{title}}",
"subtree-hidden-moved-description-collection": "此集合隱藏其樹中的子筆記。",
"subtree-hidden-moved-description-other": "子筆記隱藏於此筆記的樹中。"
},
"title_bar_buttons": {
"window-on-top": "保持此視窗置頂"

View File

@@ -69,7 +69,7 @@ declare namespace Fancytree {
debug(msg: any): void;
/** Expand (or collapse) all parent nodes. */
expandAll(flag?: boolean, options?: Object): void;
expandAll(flag?: boolean, options?: object): void;
/** [ext-filter] Dimm or hide whole branches.
* @returns {integer} count
@@ -221,6 +221,7 @@ declare namespace Fancytree {
branchId: string;
isProtected: boolean;
noteType: NoteType;
subtreeHidden: boolean;
}
interface FancytreeNewNode extends FancytreeNodeData {
@@ -369,7 +370,7 @@ declare namespace Fancytree {
* @param mode 'before', 'after', or 'child' (default='child')
* @param init NodeData (or simple title string)
*/
editCreateNode(mode?: string, init?: Object): void;
editCreateNode(mode?: string, init?: object): void;
/** [ext-edit] Stop inline editing.
*
@@ -526,7 +527,7 @@ declare namespace Fancytree {
*
* @param opts passed to `setExpanded()`. Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true}
*/
makeVisible(opts?: Object): JQueryPromise<any>;
makeVisible(opts?: object): JQueryPromise<any>;
/** Move this node to targetNode.
*
@@ -589,25 +590,25 @@ declare namespace Fancytree {
* @param effects animation options.
* @param options {topNode: null, effects: ..., parent: ...} this node will remain visible in any case, even if `this` is outside the scroll pane.
*/
scrollIntoView(effects?: boolean, options?: Object): JQueryPromise<any>;
scrollIntoView(effects?: boolean, options?: object): JQueryPromise<any>;
/**
* @param effects animation options.
* @param options {topNode: null, effects: ..., parent: ...} this node will remain visible in any case, even if `this` is outside the scroll pane.
*/
scrollIntoView(effects?: Object, options?: Object): JQueryPromise<any>;
scrollIntoView(effects?: object, options?: object): JQueryPromise<any>;
/**
* @param flag pass false to deactivate
* @param opts additional options. Defaults to {noEvents: false}
*/
setActive(flag?: boolean, opts?: Object): JQueryPromise<any>;
setActive(flag?: boolean, opts?: object): JQueryPromise<any>;
/**
* @param flag pass false to collapse.
* @param opts additional options. Defaults to {noAnimation:false, noEvents:false}
*/
setExpanded(flag?: boolean, opts?: Object): JQueryPromise<any>;
setExpanded(flag?: boolean, opts?: object): JQueryPromise<any>;
/**
* Set keyboard focus to this node.
@@ -1109,7 +1110,7 @@ declare namespace Fancytree {
/** class names added to the node markup (separate with space) */
extraClasses?: string | undefined;
/** all properties from will be copied to `node.data` */
data?: Object | undefined;
data?: object | undefined;
/** Will be added as title attribute of the node's icon span,thus enabling a tooltip. */
iconTooltip?: string | undefined;
@@ -1160,7 +1161,7 @@ declare namespace Fancytree {
escapeHtml(s: string): string;
getEventTarget(event: Event): Object;
getEventTarget(event: Event): object;
getEventTargetType(event: Event): string;
@@ -1179,7 +1180,7 @@ declare namespace Fancytree {
parseHtml($ul: JQuery): NodeData[];
/** Add Fancytree extension definition to the list of globally available extensions. */
registerExtension(definition: Object): void;
registerExtension(definition: object): void;
unescapeHtml(s: string): string;

View File

@@ -1,4 +1,4 @@
import { IconRegistry } from "@triliumnext/commons";
import { IconRegistry, Locale } from "@triliumnext/commons";
import appContext, { AppContext } from "./components/app_context";
import type FNote from "./entities/fnote";
@@ -47,14 +47,25 @@ interface CustomGlobals {
platform?: typeof process.platform;
linter: typeof lint;
hasNativeTitleBar: boolean;
hasBackgroundEffects: boolean;
isElectron: boolean;
isRtl: boolean;
iconRegistry: IconRegistry;
themeCssUrl: string;
themeUseNextAsBase?: "next" | "next-light" | "next-dark";
iconPackCss: string;
headingStyle: "plain" | "underline" | "markdown";
layoutOrientation: "vertical" | "horizontal";
currentLocale: Locale;
}
type RequireMethod = (moduleName: string) => any;
declare global {
interface Window {
$: JQueryStatic;
jQuery: JQueryStatic;
logError(message: string);
logInfo(message: string);

View File

@@ -215,7 +215,7 @@ export default function NoteDetail() {
return (
<div
ref={containerRef}
class={`note-detail ${isFullHeight ? "full-height" : ""}`}
class={`component note-detail ${isFullHeight ? "full-height" : ""}`}
>
{Object.entries(noteTypesToRender).map(([ itemType, Element ]) => {
return <NoteDetailWrapper

View File

@@ -1,14 +1,16 @@
import { useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import "./UserAttributesList.css";
import { useTriliumEvent } from "../react/hooks";
import attributes from "../../services/attributes";
import { DefinitionObject } from "../../services/promoted_attribute_definition_parser";
import { formatDateTime } from "../../utils/formatters";
import type { DefinitionObject } from "@triliumnext/commons";
import { ComponentChildren, CSSProperties } from "preact";
import { useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import { getReadableTextColor } from "../../services/css_class_manager";
import { formatDateTime } from "../../utils/formatters";
import { useTriliumEvent } from "../react/hooks";
import Icon from "../react/Icon";
import NoteLink from "../react/NoteLink";
import { getReadableTextColor } from "../../services/css_class_manager";
interface UserAttributesListProps {
note: FNote;
@@ -29,7 +31,7 @@ export default function UserAttributesDisplay({ note, ignoredAttributes }: UserA
<div className="user-attributes">
{userAttributes?.map(attr => buildUserAttribute(attr))}
</div>
)
);
}
@@ -46,13 +48,13 @@ function useNoteAttributesWithDefinitions(note: FNote, attributesToIgnore: stri
}
function UserAttribute({ attr, children, style }: { attr: AttributeWithDefinitions, children: ComponentChildren, style?: CSSProperties }) {
const className = `${attr.type === "label" ? "label" + " " + attr.def.labelType : "relation"}`;
const className = attr.type === "label" ? `label ${attr.def.labelType}` : "relation";
return (
<span key={attr.friendlyName} className={`user-attribute type-${className}`} style={style}>
{children}
</span>
)
);
}
function buildUserAttribute(attr: AttributeWithDefinitions): ComponentChildren {
@@ -61,7 +63,7 @@ function buildUserAttribute(attr: AttributeWithDefinitions): ComponentChildren {
let style: CSSProperties | undefined;
if (attr.type === "label") {
let value = attr.value;
const value = attr.value;
switch (attr.def.labelType) {
case "number":
let formattedValue = value;
@@ -102,7 +104,7 @@ function buildUserAttribute(attr: AttributeWithDefinitions): ComponentChildren {
content = <>{defaultLabel}<NoteLink notePath={attr.value} showNoteIcon /></>;
}
return <UserAttribute attr={attr} style={style}>{content}</UserAttribute>
return <UserAttribute attr={attr} style={style}>{content}</UserAttribute>;
}
function getAttributesWithDefinitions(note: FNote, attributesToIgnore: string[] = []): AttributeWithDefinitions[] {

View File

@@ -44,6 +44,7 @@ export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
const { pageNotes, ...pagination } = usePagination(note, noteIds);
const [ includeArchived ] = useNoteLabelBoolean(note, "includeArchived");
return (
<div class="note-list grid-view">
@@ -52,7 +53,7 @@ export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
<div class="note-list-container use-tn-links">
{pageNotes?.map(childNote => (
<GridNoteCard note={childNote} parentNote={note} highlightedTokens={highlightedTokens} />
<GridNoteCard note={childNote} parentNote={note} highlightedTokens={highlightedTokens} includeArchived={includeArchived} />
))}
</div>
@@ -94,14 +95,16 @@ function ListNoteCard({ note, parentNote, highlightedTokens, currentLevel, expan
</h5>
{isExpanded && <>
<NoteContent note={note} highlightedTokens={highlightedTokens} noChildrenList />
<NoteContent note={note} highlightedTokens={highlightedTokens} noChildrenList includeArchivedNotes={includeArchived} />
<NoteChildren note={note} parentNote={parentNote} highlightedTokens={highlightedTokens} currentLevel={currentLevel} expandDepth={expandDepth} includeArchived={includeArchived} />
</>}
</div>
);
}
function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined }) {
function GridNoteCard({ note, parentNote, highlightedTokens, includeArchived }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined, includeArchived: boolean }) {
const titleRef = useRef<HTMLSpanElement>(null);
const [ noteTitle, setNoteTitle ] = useState<string>();
const notePath = getNotePath(parentNote, note);
return (
@@ -120,6 +123,7 @@ function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, pa
note={note}
trim
highlightedTokens={highlightedTokens}
includeArchivedNotes={includeArchived}
/>
</div>
);
@@ -136,14 +140,22 @@ function NoteAttributes({ note }: { note: FNote }) {
return <span className="note-list-attributes" ref={ref} />;
}
function NoteContent({ note, trim, noChildrenList, highlightedTokens }: { note: FNote, trim?: boolean, noChildrenList?: boolean, highlightedTokens: string[] | null | undefined }) {
function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes }: {
note: FNote;
trim?: boolean;
noChildrenList?: boolean;
highlightedTokens: string[] | null | undefined;
includeArchivedNotes: boolean;
}) {
const contentRef = useRef<HTMLDivElement>(null);
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
useEffect(() => {
content_renderer.getRenderedContent(note, {
trim,
noChildrenList
noChildrenList,
noIncludedNotes: true,
includeArchivedNotes
})
.then(({ $renderedContent, type }) => {
if (!contentRef.current) return;

View File

@@ -1,11 +1,12 @@
import type { CellComponent, ColumnDefinition, EmptyCallback, FormatterParams, ValueBooleanCallback, ValueVoidCallback } from "tabulator-tables";
import { LabelType } from "../../../services/promoted_attribute_definition_parser.js";
import { LabelType } from "@triliumnext/commons";
import { JSX } from "preact";
import { renderReactWidget } from "../../react/react_utils.jsx";
import Icon from "../../react/Icon.jsx";
import { useEffect, useRef, useState } from "preact/hooks";
import type { CellComponent, ColumnDefinition, EmptyCallback, FormatterParams, ValueBooleanCallback, ValueVoidCallback } from "tabulator-tables";
import froca from "../../../services/froca.js";
import Icon from "../../react/Icon.jsx";
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";
import { renderReactWidget } from "../../react/react_utils.jsx";
type ColumnType = LabelType | "relation";
@@ -78,7 +79,7 @@ export function buildColumnDefinitions({ info, movableRows, existingColumnData,
rowHandle: movableRows,
width: calculateIndexColumnWidth(rowNumberHint, movableRows),
formatter: wrapFormatter(({ cell, formatterParams }) => <div>
{(formatterParams as RowNumberFormatterParams).movableRows && <><span class="bx bx-dots-vertical-rounded"></span>{" "}</>}
{(formatterParams as RowNumberFormatterParams).movableRows && <><span class="bx bx-dots-vertical-rounded" />{" "}</>}
{cell.getRow().getPosition(true)}
</div>),
formatterParams: { movableRows } satisfies RowNumberFormatterParams
@@ -200,14 +201,14 @@ function wrapEditor(Component: (opts: EditorOpts) => JSX.Element): ((
editorParams: {},
) => HTMLElement | false) {
return (cell, _, success, cancel, editorParams) => {
const elWithParams = <Component cell={cell} success={success} cancel={cancel} editorParams={editorParams} />
const elWithParams = <Component cell={cell} success={success} cancel={cancel} editorParams={editorParams} />;
return renderReactWidget(null, elWithParams)[0];
};
}
function NoteFormatter({ cell }: FormatterOpts) {
const noteId = cell.getValue();
const [ note, setNote ] = useState(noteId ? froca.getNoteFromCache(noteId) : null)
const [ note, setNote ] = useState(noteId ? froca.getNoteFromCache(noteId) : null);
useEffect(() => {
if (!noteId || note?.noteId === noteId) return;
@@ -231,5 +232,5 @@ function RelationEditor({ cell, success }: EditorOpts) {
hideAllButtons: true
}}
noteIdChanged={success}
/>
/>;
}

View File

@@ -1,17 +1,18 @@
import { useCallback, useLayoutEffect, useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import froca from "../../services/froca";
import { isDesktop, isMobile } from "../../services/utils";
import CalendarWidget from "./CalendarWidget";
import SpacerWidget from "./SpacerWidget";
import BookmarkButtons from "./BookmarkButtons";
import ProtectedSessionStatusWidget from "./ProtectedSessionStatusWidget";
import SyncStatus from "./SyncStatus";
import HistoryNavigationButton from "./HistoryNavigation";
import { AiChatButton, CommandButton, CustomWidget, NoteLauncher, QuickSearchLauncherWidget, ScriptLauncher, TodayLauncher } from "./LauncherDefinitions";
import { useTriliumEvent } from "../react/hooks";
import { onWheelHorizontalScroll } from "../widget_utils";
import BookmarkButtons from "./BookmarkButtons";
import CalendarWidget from "./CalendarWidget";
import HistoryNavigationButton from "./HistoryNavigation";
import { LaunchBarContext } from "./launch_bar_widgets";
import { AiChatButton, CommandButton, CustomWidget, NoteLauncher, QuickSearchLauncherWidget, ScriptLauncher, TodayLauncher } from "./LauncherDefinitions";
import ProtectedSessionStatusWidget from "./ProtectedSessionStatusWidget";
import SpacerWidget from "./SpacerWidget";
import SyncStatus from "./SyncStatus";
export default function LauncherContainer({ isHorizontalLayout }: { isHorizontalLayout: boolean }) {
const childNotes = useLauncherChildNotes();
@@ -34,18 +35,19 @@ export default function LauncherContainer({ isHorizontalLayout }: { isHorizontal
}}>
{childNotes?.map(childNote => {
if (childNote.type !== "launcher") {
throw new Error(`Note '${childNote.noteId}' '${childNote.title}' is not a launcher even though it's in the launcher subtree`);
console.warn(`Note '${childNote.noteId}' '${childNote.title}' is not a launcher even though it's in the launcher subtree`);
return false;
}
if (!isDesktop() && childNote.isLabelTruthy("desktopOnly")) {
return false;
}
return <Launcher key={childNote.noteId} note={childNote} isHorizontalLayout={isHorizontalLayout} />
return <Launcher key={childNote.noteId} note={childNote} isHorizontalLayout={isHorizontalLayout} />;
})}
</LaunchBarContext.Provider>
</div>
)
);
}
function Launcher({ note, isHorizontalLayout }: { note: FNote, isHorizontalLayout: boolean }) {
@@ -72,7 +74,7 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) {
const builtinWidget = note.getLabelValue("builtinWidget");
switch (builtinWidget) {
case "calendar":
return <CalendarWidget launcherNote={note} />
return <CalendarWidget launcherNote={note} />;
case "spacer":
// || has to be inside since 0 is a valid value
const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
@@ -86,15 +88,15 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) {
case "syncStatus":
return <SyncStatus />;
case "backInHistoryButton":
return <HistoryNavigationButton launcherNote={note} command="backInNoteHistory" />
return <HistoryNavigationButton launcherNote={note} command="backInNoteHistory" />;
case "forwardInHistoryButton":
return <HistoryNavigationButton launcherNote={note} command="forwardInNoteHistory" />
return <HistoryNavigationButton launcherNote={note} command="forwardInNoteHistory" />;
case "todayInJournal":
return <TodayLauncher launcherNote={note} />
return <TodayLauncher launcherNote={note} />;
case "quickSearch":
return <QuickSearchLauncherWidget />
return <QuickSearchLauncherWidget />;
case "aiChatLauncher":
return <AiChatButton launcherNote={note} />
return <AiChatButton launcherNote={note} />;
default:
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
}

View File

@@ -338,19 +338,19 @@ interface AttributesProps extends StatusBarContext {
function AttributesButton({ note, attributesShown, setAttributesShown }: AttributesProps) {
const [ count, setCount ] = useState(note.attributes.length);
const refreshCount = useCallback((note: FNote) => {
const getAttributeCount = useCallback((note: FNote) => {
return note.getAttributes().filter(a => !a.isAutoLink).length;
}, []);
// React to note changes.
useEffect(() => {
setCount(refreshCount(note));
}, [ note, refreshCount ]);
setCount(getAttributeCount(note));
}, [ note, getAttributeCount ]);
// React to changes in count.
useTriliumEvent("entitiesReloaded", (({loadResults}) => {
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
setCount(refreshCount(note));
setCount(getAttributeCount(note));
}
}));

View File

@@ -82,6 +82,13 @@ function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOption
))}
{properties.length > 0 && <FormDropdownDivider />}
<ViewProperty note={note} property={{
type: "checkbox",
icon: "bx bx-hide",
label: t("book_properties.hide_child_notes"),
bindToLabel: "subtreeHidden"
} as CheckBoxProperty} />
<ViewProperty note={note} property={{
type: "checkbox",
icon: "bx bx-archive",

View File

@@ -0,0 +1,20 @@
#left-pane .tree-wrapper {
.note-indicator-icon.subtree-hidden-badge {
font-family: inherit !important;
margin-inline: 0.5em;
margin-top: 0.3em;
background: var(--left-pane-item-action-button-background);
color: var(--left-pane-item-action-button-color);
padding: 0.1em 0.6em;
border-radius: 0.5em;
font-size: 0.7rem;
font-weight: normal;
float: right;
vertical-align: middle;
}
.spotlighted-node {
opacity: 0.8;
font-style: italic;
}
}

View File

@@ -3,13 +3,13 @@ import "jquery.fancytree/dist/modules/jquery.fancytree.dnd5.js";
import "jquery.fancytree/dist/modules/jquery.fancytree.clones.js";
import "jquery.fancytree/dist/modules/jquery.fancytree.filter.js";
import "../stylesheets/tree.css";
import "./note_tree.css";
import appContext, { type CommandListenerData, type EventData } from "../components/app_context.js";
import type { SetNoteOpts } from "../components/note_context.js";
import type { TouchBarItem } from "../components/touch_bar.js";
import type FBranch from "../entities/fbranch.js";
import type FNote from "../entities/fnote.js";
import type { NoteType } from "../entities/fnote.js";
import contextMenu from "../menus/context_menu.js";
import type { TreeCommandNames } from "../menus/tree_context_menu.js";
import branchService from "../services/branches.js";
@@ -153,7 +153,7 @@ const TPL = /*html*/`
const MAX_SEARCH_RESULTS_IN_TREE = 100;
// this has to be hanged on the actual elements to effectively intercept and stop click event
const cancelClickPropagation: (e: JQuery.ClickEvent) => void = (e) => e.stopPropagation();
const cancelClickPropagation: (e: Event) => void = (e) => e.stopPropagation();
// TODO: Fix once we remove Node.js API from public
type Timeout = NodeJS.Timeout | string | number | undefined;
@@ -190,6 +190,9 @@ export interface DragData {
export const TREE_CLIPBOARD_TYPE = "application/x-fancytree-node";
/** Entity changes below the given threshold will be processed without batching to avoid performance degradation. */
const BATCH_UPDATE_THRESHOLD = 10;
export default class NoteTreeWidget extends NoteContextAwareWidget {
private $tree!: JQuery<HTMLElement>;
private $treeActions!: JQuery<HTMLElement>;
@@ -201,6 +204,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
private treeName: "main";
private autoCollapseTimeoutId?: Timeout;
private lastFilteredHoistedNotePath?: string | null;
private spotlightedNotePath?: string | null;
private spotlightedNode: Fancytree.FancytreeNode | null = null;
private tree!: Fancytree.Fancytree;
constructor() {
@@ -353,6 +358,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
this.$tree.fancytree({
titlesTabbable: true,
keyboard: true,
toggleEffect: options.is("motionEnabled") ? undefined : false,
extensions: ["dnd5", "clones", "filter"],
source: treeData,
scrollOfs: {
@@ -552,7 +558,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} else if (data.hitMode === "after") {
branchService.moveAfterBranch(selectedBranchIds, node.data.branchId);
} else if (data.hitMode === "over") {
branchService.moveToParentNote(selectedBranchIds, node.data.branchId);
branchService.moveToParentNote(selectedBranchIds, node.data.branchId, this.componentId);
} else {
throw new Error(`Unknown hitMode '${data.hitMode}'`);
}
@@ -598,102 +604,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
clones: {
highlightActiveClones: true
},
async enhanceTitle (
event: Event,
data: {
node: Fancytree.FancytreeNode;
noteId: string;
}
) {
const node = data.node;
if (!node.data.noteId) {
// if there's "non-note" node, then don't enhance
// this can happen for e.g. "Load error!" node
return;
}
const note = await froca.getNote(node.data.noteId, true);
if (!note) {
return;
}
const activeNoteContext = appContext.tabManager.getActiveContext();
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";
if (note.hasLabel("workspace") && !isHoistedNote) {
const $enterWorkspaceButton = $(`<span class="tree-item-button tn-icon enter-workspace-button bx bx-door-open" title="${t("note_tree.hoist-this-note-workspace")}"></span>`).on(
"click",
cancelClickPropagation
);
$span.append($enterWorkspaceButton);
}
if (note.type === "search") {
const $refreshSearchButton = $(`<span class="tree-item-button tn-icon refresh-search-button bx bx-refresh" title="${t("note_tree.refresh-saved-search-results")}"></span>`).on(
"click",
cancelClickPropagation
);
$span.append($refreshSearchButton);
}
// TODO: Deduplicate with server's notes.ts#getAndValidateParent
if (!["search", "launcher"].includes(note.type)
&& !note.isOptions()
&& !note.isLaunchBarConfig()
&& !note.noteId.startsWith("_help")
) {
const $createChildNoteButton = $(`<span class="tree-item-button tn-icon add-note-button bx bx-plus" title="${t("note_tree.create-child-note")}"></span>`).on(
"click",
cancelClickPropagation
);
$span.append($createChildNoteButton);
}
if (isHoistedNote) {
const $unhoistButton = $(`<span class="tree-item-button tn-icon unhoist-button bx bx-door-open" title="${t("note_tree.unhoist")}"></span>`).on("click", cancelClickPropagation);
$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);
}
},
enhanceTitle: buildEnhanceTitle(),
// this is done to automatically lazy load all expanded notes after tree load
loadChildren: (event, data) => {
data.node.visit((subNode) => {
@@ -803,6 +714,23 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
childBranches = childBranches.slice(0, MAX_SEARCH_RESULTS_IN_TREE);
}
if (parentNote.isLabelTruthy("subtreeHidden")) {
// If we have a spotlighted note path, show only the child that leads to it
if (this.spotlightedNotePath) {
const spotlightPathSegments = this.spotlightedNotePath.split('/');
const parentIndex = spotlightPathSegments.indexOf(parentNote.noteId);
if (parentIndex >= 0 && parentIndex < spotlightPathSegments.length - 1) {
const nextNoteIdInPath = spotlightPathSegments[parentIndex + 1];
childBranches = childBranches.filter(branch => branch.noteId === nextNoteIdInPath);
} else {
childBranches = [];
}
} else {
childBranches = [];
}
}
for (const branch of childBranches) {
if (hideArchivedNotes) {
const note = branch.getNoteFromCache();
@@ -874,6 +802,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
lazy: true,
folder: isFolder,
expanded: !!branch.isExpanded && note.type !== "search",
subtreeHidden: note.isLabelTruthy("subtreeHidden"),
key: utils.randomString(12) // this should prevent some "duplicate key" errors
};
@@ -932,6 +861,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
extraClasses.push(...["tinted", colorClass]);
}
if (this.spotlightedNotePath && this.spotlightedNotePath.endsWith(`/${note.noteId}`)) {
extraClasses.push("spotlighted-node");
}
return extraClasses.join(" ");
}
@@ -1082,18 +1015,43 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
foundChildNode = this.findChildNode(parentNode, childNoteId);
if (!foundChildNode) {
if (logErrors) {
// besides real errors, this can be also caused by hiding of e.g. included images
// these are real notes with real notePath, user can display them in a detail,
// but they don't have a node in the tree
const childNote = await froca.getNote(childNoteId);
const childNote = await froca.getNote(childNoteId);
if (childNote?.type === "image") return;
if (!childNote || childNote.type !== "image") {
ws.logError(
`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`
);
// The child note can be part of a note with #subtreeHidden, case in which we need to "spotlight" it.
const parentNote = froca.getNoteFromCache(parentNode.data.noteId);
if (parentNote?.isLabelTruthy("subtreeHidden")) {
// Enable spotlight mode and reload the parent to show only the path to this note
this.spotlightedNotePath = notePath;
await parentNode.load(true);
// Try to find the child again after reload
foundChildNode = this.findChildNode(parentNode, childNoteId);
this.spotlightedNode = foundChildNode ?? null;
if (!foundChildNode) {
if (logErrors || !childNote) {
ws.logError(
`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`
);
return;
}
return;
}
parentNode = foundChildNode;
continue;
}
// besides real errors, this can be also caused by hiding of e.g. included images
// these are real notes with real notePath, user can display them in a detail,
// but they don't have a node in the tree
if (logErrors || !childNote) {
ws.logError(
`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`
);
return;
}
return;
@@ -1108,7 +1066,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}
findChildNode(parentNode: Fancytree.FancytreeNode, childNoteId: string) {
return parentNode.getChildren().find((childNode) => childNode.data.noteId === childNoteId);
return parentNode.getChildren()?.find((childNode) => childNode.data.noteId === childNoteId);
}
async expandToNote(notePath: string, logErrors = true) {
@@ -1149,12 +1107,20 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
(!treeService.isNotePathInHiddenSubtree(this.noteContext.notePath) || (await hoistedNoteService.isHoistedInHiddenSubtree())) &&
(await this.getNodeFromPath(this.noteContext.notePath));
if (this.spotlightedNode && newActiveNode !== this.spotlightedNode) {
// Can get removed when switching to another note in a spotlighted subtree.
if (this.spotlightedNode.parent) {
this.spotlightedNode.remove();
}
this.spotlightedNode = null;
this.spotlightedNotePath = null;
}
if (newActiveNode !== oldActiveNode) {
let oldActiveNodeFocused = false;
if (oldActiveNode) {
oldActiveNodeFocused = oldActiveNode.hasFocus();
oldActiveNode.setActive(false);
oldActiveNode.setFocus(false);
}
@@ -1257,10 +1223,18 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const { movedActiveNode, parentsOfAddedNodes } = await this.#processBranchRows(branchRows, refreshCtx);
for (const noteId of loadResults.getNoteIds()) {
const contentReloaded = loadResults.isNoteContentReloaded(noteId);
if (contentReloaded && !loadResults.isNoteReloaded(noteId, contentReloaded.componentId)) {
// Only the note content was reloaded, not the note itself. This would cause a redundant update on every few seconds while editing a note.
continue;
}
refreshCtx.noteIdsToUpdate.add(noteId);
}
await this.#executeTreeUpdates(refreshCtx, loadResults);
if (refreshCtx.noteIdsToUpdate.size + refreshCtx.noteIdsToReload.size > 0) {
await this.#executeTreeUpdates(refreshCtx, loadResults);
}
await this.#setActiveNode(activeNotePath, activeNodeFocused, movedActiveNode, parentsOfAddedNodes);
@@ -1280,7 +1254,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} else {
refreshCtx.noteIdsToUpdate.add(attrRow.noteId);
}
} else if (attrRow.type === "label" && attrRow.name === "archived" && attrRow.noteId) {
} else if (attrRow.type === "label" && (attrRow.name === "archived" || attrRow.name === "subtreeHidden") && attrRow.noteId) {
const note = froca.getNoteFromCache(attrRow.noteId);
if (note) {
@@ -1365,18 +1339,34 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} else if (frocaBranch) {
// make sure it's loaded
// we're forcing lazy since it's not clear if the whole required subtree is in froca
const newNode = this.prepareNode(frocaBranch, true);
if (newNode) {
parentNode.addChildren([newNode]);
}
if (!parentNode.data.subtreeHidden) {
const newNode = this.prepareNode(frocaBranch, true);
if (newNode) {
parentNode.addChildren([newNode]);
}
if (frocaBranch?.isExpanded && note && note.hasChildren()) {
refreshCtx.noteIdsToReload.add(frocaBranch.noteId);
}
if (frocaBranch?.isExpanded && note && note.hasChildren()) {
refreshCtx.noteIdsToReload.add(frocaBranch.noteId);
}
this.sortChildren(parentNode);
this.sortChildren(parentNode);
} else if (branchRow.componentId === this.componentId) {
// Display the toast and focus to parent note only if we know for sure that the operation comes from the tree.
const parentNote = froca.getNoteFromCache(parentNode.data.noteId || "");
toastService.showPersistent({
id: `subtree-hidden-moved`,
title: t("note_tree.subtree-hidden-moved-title", { title: parentNote?.title }),
message: parentNote?.type === "book"
? t("note_tree.subtree-hidden-moved-description-collection")
: t("note_tree.subtree-hidden-moved-description-other"),
icon: "bx bx-hide",
timeout: 5_000,
});
parentNode.setActive(true);
}
// this might be a first child which would force an icon change
// also update the count if the subtree is hidden.
if (branchRow.parentNoteId) {
refreshCtx.noteIdsToUpdate.add(branchRow.parentNoteId);
}
@@ -1392,7 +1382,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}
async #executeTreeUpdates(refreshCtx: RefreshContext, loadResults: LoadResults) {
await this.batchUpdate(async () => {
const performUpdates = async () => {
for (const noteId of refreshCtx.noteIdsToReload) {
for (const node of this.getNodesByNoteId(noteId)) {
await node.load(true);
@@ -1408,7 +1398,19 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}
}
}
});
};
if (refreshCtx.noteIdsToReload.size + refreshCtx.noteIdsToUpdate.size >= BATCH_UPDATE_THRESHOLD) {
/**
* Batch updates are used for large number of updates to prevent multiple re-renders, however in the context of small updates (such as changing a note title)
* it can cause up to 400ms of delay for ~8k notes which is not acceptable. Therefore we use batching only for larger number of updates.
* Without batching, the updates would take a couple of milliseconds.
* We still keep the batching for potential cases where there are many updates, for example in a sync.
*/
await this.batchUpdate(performUpdates);
} else {
await performUpdates();
}
// for some reason, node update cannot be in the batchUpdate() block (node is not re-rendered)
for (const noteId of refreshCtx.noteIdsToUpdate) {
@@ -1672,7 +1674,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const toNode = node.getPrevSibling();
if (toNode !== null) {
branchService.moveToParentNote([node.data.branchId], toNode.data.branchId);
branchService.moveToParentNote([node.data.branchId], toNode.data.branchId, this.componentId);
}
}
@@ -1809,12 +1811,12 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
#moveLaunchers(selectedOrActiveBranchIds: string[], desktopParent: string, mobileParent: string) {
const desktopLaunchersToMove = selectedOrActiveBranchIds.filter((branchId) => !branchId.startsWith("_lbMobile"));
if (desktopLaunchersToMove) {
branchService.moveToParentNote(desktopLaunchersToMove, `_lbRoot_${ desktopParent}`);
branchService.moveToParentNote(desktopLaunchersToMove, `_lbRoot_${ desktopParent}`, this.componentId);
}
const mobileLaunchersToMove = selectedOrActiveBranchIds.filter((branchId) => branchId.startsWith("_lbMobile"));
if (mobileLaunchersToMove) {
branchService.moveToParentNote(mobileLaunchersToMove, `_lbMobileRoot_${ mobileParent}`);
branchService.moveToParentNote(mobileLaunchersToMove, `_lbMobileRoot_${mobileParent}`, this.componentId);
}
}
@@ -1882,3 +1884,112 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return items;
}
}
function buildEnhanceTitle() {
const createChildTemplate = document.createElement("span");
createChildTemplate.className = "tree-item-button tn-icon add-note-button bx bx-plus";
createChildTemplate.title = t("note_tree.create-child-note");
return async function enhanceTitle(event: Event,
data: {
node: Fancytree.FancytreeNode;
noteId: string;
}) {
const node = data.node;
if (!node.data.noteId) {
// if there's "non-note" node, then don't enhance
// this can happen for e.g. "Load error!" node
return;
}
const note = froca.getNoteFromCache(node.data.noteId);
if (!note) return;
const activeNoteContext = appContext.tabManager.getActiveContext();
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";
if (note.hasLabel("workspace") && !isHoistedNote) {
const $enterWorkspaceButton = $(`<span class="tree-item-button tn-icon enter-workspace-button bx bx-door-open" title="${t("note_tree.hoist-this-note-workspace")}"></span>`).on(
"click",
cancelClickPropagation
);
$span.append($enterWorkspaceButton);
}
if (note.type === "search") {
const $refreshSearchButton = $(`<span class="tree-item-button tn-icon refresh-search-button bx bx-refresh" title="${t("note_tree.refresh-saved-search-results")}"></span>`).on(
"click",
cancelClickPropagation
);
$span.append($refreshSearchButton);
}
// TODO: Deduplicate with server's notes.ts#getAndValidateParent
const isSubtreeHidden = note.isLabelTruthy("subtreeHidden");
if (!["search", "launcher"].includes(note.type)
&& !note.isOptions()
&& !note.isLaunchBarConfig()
&& !note.noteId.startsWith("_help")
&& !isSubtreeHidden
&& !node.extraClasses.includes("spotlighted-node")
) {
const createChildItem = createChildTemplate.cloneNode();
createChildItem.addEventListener("click", cancelClickPropagation);
node.span.append(createChildItem);
}
if (isHoistedNote) {
const $unhoistButton = $(`<span class="tree-item-button tn-icon unhoist-button bx bx-door-open" title="${t("note_tree.unhoist")}"></span>`).on("click", cancelClickPropagation);
$span.append($unhoistButton);
}
// Add clone indicator with tooltip if note has multiple parents
const parentNotes = note.getParentNotes();
const realParents: FNote[] = [];
for (const parent of parentNotes) {
if (parent.noteId !== "_share" && parent.noteId !== "_lbBookmarks" && parent.type !== "search") {
realParents.push(parent);
}
}
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);
}
// Add a badge with the number of items if it hides children.
const count = note.getChildNoteIds().length;
if (isSubtreeHidden && count > 0) {
const $badge = $(`<span class="note-indicator-icon subtree-hidden-badge">${count}</span>`);
$badge.attr("title", t("note_tree.subtree-hidden-tooltip", { count }));
$span.find(".fancytree-title").append($badge);
}
};
}

View File

@@ -646,17 +646,13 @@ export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: F
const setter = useCallback((value: boolean) => {
if (note) {
if (value) {
attributes.setLabel(note.noteId, labelName, "");
} else {
attributes.removeOwnedLabelByName(note, labelName);
}
attributes.setBooleanWithInheritance(note, labelName, value);
}
}, [note]);
}, [note, labelName]);
useDebugValue(labelName);
const labelValue = !!note?.hasLabel(labelName);
const labelValue = !!note?.isLabelTruthy(labelName);
return [ labelValue, setter ] as const;
}

View File

@@ -2,12 +2,14 @@ import "./TableOfContents.css";
import { CKTextEditor, ModelElement } from "@triliumnext/ckeditor5";
import clsx from "clsx";
import { useCallback, useEffect, useState } from "preact/hooks";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import math from "../../services/math";
import { randomString } from "../../services/utils";
import { useActiveNoteContext, useContentElement, useGetContextData, useIsNoteReadOnly, useNoteProperty, useTextEditor } from "../react/hooks";
import Icon from "../react/Icon";
import RawHtml from "../react/RawHtml";
import RightPanelWidget from "./RightPanelWidget";
//#region Generic impl.
@@ -80,6 +82,22 @@ function TableOfContentsHeading({ heading, scrollToHeading, activeHeadingId }: {
}) {
const [ collapsed, setCollapsed ] = useState(false);
const isActive = heading.id === activeHeadingId;
const contentRef = useRef<HTMLElement>(null);
// Render math equations after component mounts/updates
useEffect(() => {
if (!contentRef.current) return;
const mathElements = contentRef.current.querySelectorAll(".ck-math-tex");
for (const mathEl of mathElements ?? []) {
try {
math.render(mathEl.textContent || "", mathEl as HTMLElement);
} catch (e) {
console.warn("Failed to render math in TOC:", e);
}
}
}, [heading.text]);
return (
<>
<li className={clsx(collapsed && "collapsed", isActive && "active")}>
@@ -90,12 +108,14 @@ function TableOfContentsHeading({ heading, scrollToHeading, activeHeadingId }: {
onClick={() => setCollapsed(!collapsed)}
/>
)}
<span
<RawHtml
containerRef={contentRef}
className="item-content"
onClick={() => scrollToHeading(heading)}
>{heading.text}</span>
html={heading.text}
/>
</li>
{heading.children && (
{heading.children.length > 0 && (
<ol>
{heading.children.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} activeHeadingId={activeHeadingId} />)}
</ol>
@@ -189,9 +209,23 @@ function extractTocFromTextEditor(editor: CKTextEditor) {
if (type !== "elementStart" || !item.is('element') || !item.name.startsWith('heading')) continue;
const level = Number(item.name.replace( 'heading', '' ));
const text = Array.from( item.getChildren() )
.map( c => c.is( '$text' ) ? c.data : '' )
.join( '' );
// Convert model element to view, then to DOM to get HTML
const viewEl = editor.editing.mapper.toViewElement(item);
let text = '';
if (viewEl) {
const domEl = editor.editing.view.domConverter.mapViewToDom(viewEl);
if (domEl instanceof HTMLElement) {
text = domEl.innerHTML;
}
}
// Fallback to plain text if conversion fails
if (!text) {
text = Array.from( item.getChildren() )
.map( c => c.is( '$text' ) ? c.data : '' )
.join( '' );
}
// Assign a unique ID
let tocId = item.getAttribute(TOC_ID) as string | undefined;

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,20 +1,29 @@
import "./Image.css";
import { useEffect, useRef, useState } from "preact/hooks";
import { TransformComponent,TransformWrapper } from "react-zoom-pan-pinch";
import image_context_menu from "../../menus/image_context_menu";
import { copyImageReferenceToClipboard } from "../../services/image";
import { createImageSrcUrl } from "../../services/utils";
import { useTriliumEvent, useUniqueName } from "../react/hooks";
import { refToJQuerySelector } from "../react/react_utils";
import "./Image.css";
import { TypeWidgetProps } from "./type_widget";
import WheelZoom from 'vanilla-js-wheel-zoom';
import image_context_menu from "../../menus/image_context_menu";
import { refToJQuerySelector } from "../react/react_utils";
import { copyImageReferenceToClipboard } from "../../services/image";
export default function Image({ note, ntxId }: TypeWidgetProps) {
const uniqueId = useUniqueName("image");
const containerRef = useRef<HTMLDivElement>(null);
const [ refreshCounter, setRefreshCounter ] = useState(0);
// Set up pan & zoom
useEffect(() => {
const zoomInstance = WheelZoom.create(`#${uniqueId}`, {
maxScale: 50,
speed: 1.3,
zoomOnClick: false
});
return () => zoomInstance.destroy();
}, [ note ]);
// Set up context menu
useEffect(() => image_context_menu.setupContextMenu(refToJQuerySelector(containerRef)), []);
@@ -33,23 +42,11 @@ export default function Image({ note, ntxId }: TypeWidgetProps) {
return (
<div ref={containerRef} className="note-detail-image-wrapper">
<TransformWrapper
initialScale={1}
centerOnInit
>
<TransformComponent
wrapperStyle={{
width: "100%",
height: "100%"
}}
>
<img
id={uniqueId}
className="note-detail-image-view"
src={createImageSrcUrl(note)}
/>
</TransformComponent>
</TransformWrapper>
<img
id={uniqueId}
className="note-detail-image-view"
src={createImageSrcUrl(note)}
/>
</div>
);
)
}

View File

@@ -15,6 +15,8 @@
.note-detail-split .note-detail-split-editor {
width: 100%;
flex-grow: 1;
min-width: 0;
min-height: 0;
}
.note-detail-split .note-detail-split-editor .note-detail-code {
@@ -30,6 +32,7 @@
margin: 5px;
white-space: pre-wrap;
font-size: 0.85em;
overflow: auto;
}
.note-detail-split .note-detail-split-preview {

View File

@@ -1,15 +1,13 @@
import { RefObject } from "preact";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
import svgPanZoom from "svg-pan-zoom";
import { t } from "../../../services/i18n";
import server from "../../../services/server";
import toast from "../../../services/toast";
import utils from "../../../services/utils";
import { useElementSize, useTriliumEvent } from "../../react/hooks";
import { RawHtmlBlock } from "../../react/RawHtml";
import SplitEditor, { PreviewButton, SplitEditorProps } from "./SplitEditor";
import { RawHtmlBlock } from "../../react/RawHtml";
import server from "../../../services/server";
import svgPanZoom from "svg-pan-zoom";
import { RefObject } from "preact";
import { useElementSize, useTriliumEvent } from "../../react/hooks";
import utils from "../../../services/utils";
import toast from "../../../services/toast";
interface SvgSplitEditorProps extends Omit<SplitEditorProps, "previewContent"> {
/**
@@ -119,20 +117,11 @@ export default function SvgSplitEditor({ ntxId, note, attachmentName, renderSvg,
onContentChanged={onContentChanged}
dataSaved={onSave}
previewContent={(
<TransformWrapper>
<TransformComponent
wrapperStyle={{
width: "100%",
height: "100%"
}}
>
<RawHtmlBlock
className="render-container"
containerRef={containerRef}
html={svg}
/>
</TransformComponent>
</TransformWrapper>
<RawHtmlBlock
className="render-container"
containerRef={containerRef}
html={svg}
/>
)}
previewButtons={
<>
@@ -155,7 +144,7 @@ export default function SvgSplitEditor({ ntxId, note, attachmentName, renderSvg,
}
{...props}
/>
);
)
}
function useResizer(containerRef: RefObject<HTMLDivElement>, noteId: string, svg: string | undefined) {
@@ -192,7 +181,7 @@ function useResizer(containerRef: RefObject<HTMLDivElement>, noteId: string, svg
lastPanZoom.current = {
pan: zoomInstance.getPan(),
zoom: zoomInstance.getZoom()
};
}
zoomRef.current = undefined;
zoomInstance.destroy();
};

View File

@@ -286,7 +286,7 @@ function useWatchdogCrashHandling() {
const currentState = watchdog.state;
logInfo(`CKEditor state changed to ${currentState}`);
if (currentState === "ready") {
if (currentState === "ready" && hasCrashed.current) {
hasCrashed.current = false;
watchdog.editor?.focus();
}

View File

@@ -1,15 +1,14 @@
import { ALLOWED_PROTOCOLS, DISPLAYABLE_LOCALE_IDS, MIME_TYPE_AUTO } from "@triliumnext/commons";
import { buildExtraCommands, type EditorConfig, getCkLocale, PREMIUM_PLUGINS, TemplateDefinition } from "@triliumnext/ckeditor5";
import { getHighlightJsNameForMime } from "../../../services/mime_types.js";
import options from "../../../services/options.js";
import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
import { buildExtraCommands, type EditorConfig, getCkLocale, loadPremiumPlugins, TemplateDefinition } from "@triliumnext/ckeditor5";
import emojiDefinitionsUrl from "@triliumnext/ckeditor5/src/emoji_definitions/en.json?url";
import { ALLOWED_PROTOCOLS, DISPLAYABLE_LOCALE_IDS, MIME_TYPE_AUTO, normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
import { t } from "../../../services/i18n.js";
import { getMermaidConfig } from "../../../services/mermaid.js";
import { default as mimeTypesService, getHighlightJsNameForMime } from "../../../services/mime_types.js";
import noteAutocompleteService, { type Suggestion } from "../../../services/note_autocomplete.js";
import mimeTypesService from "../../../services/mime_types.js";
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import options from "../../../services/options.js";
import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
import { buildToolbarConfig } from "./toolbar.js";
export const OPEN_SOURCE_LICENSE_KEY = "GPL";
@@ -36,7 +35,7 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
engine: "katex",
outputType: "span", // or script
lazyLoad: async () => {
(window as any).katex = (await import("../../../services/math.js")).default
(window as any).katex = (await import("../../../services/math.js")).default;
},
forceOutputType: false, // forces output to use outputType
enablePreview: true // Enable preview view
@@ -172,7 +171,7 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
config.language = {
ui: (typeof config.language === "string" ? config.language : "en"),
content: contentLanguage
}
};
}
// Mention customisation.
@@ -195,11 +194,9 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
};
}
// Enable premium plugins.
// Enable premium plugins dynamically to avoid eager loading.
if (hasPremiumLicense) {
config.extraPlugins = [
...PREMIUM_PLUGINS
];
config.extraPlugins = await loadPremiumPlugins();
}
return {
@@ -237,7 +234,7 @@ function getLicenseKey() {
}
function getDisabledPlugins() {
let disabledPlugins: string[] = [];
const disabledPlugins: string[] = [];
if (options.get("textNoteEmojiCompletionEnabled") !== "true") {
disabledPlugins.push("EmojiMention");

View File

@@ -1,24 +1,22 @@
/// <reference types='vitest' />
import { join, resolve } from 'path';
import { defineConfig, type Plugin } from 'vite';
import { viteStaticCopy } from 'vite-plugin-static-copy'
import prefresh from '@prefresh/vite';
import { join } from 'path';
import webpackStatsPlugin from 'rollup-plugin-webpack-stats';
import preact from "@preact/preset-vite";
import { defineConfig } from 'vite';
import { viteStaticCopy } from 'vite-plugin-static-copy'
const assets = [ "assets", "stylesheets", "fonts", "translations" ];
const isDev = process.env.NODE_ENV === "development";
let plugins: any = [
preact({
babel: {
compact: !isDev
}
})
];
let plugins: any = [];
if (!isDev) {
if (isDev) {
// Add Prefresh for Preact HMR in development
plugins = [
prefresh()
];
} else {
plugins = [
...plugins,
viteStaticCopy({
targets: assets.map((asset) => ({
src: `src/${asset}/*`,
@@ -40,9 +38,19 @@ if (!isDev) {
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/client',
cacheDir: '../../.cache/vite',
base: "",
plugins,
// Use esbuild for JSX transformation (much faster than Babel)
esbuild: {
jsx: 'automatic',
jsxImportSource: 'preact',
jsxDev: isDev
},
css: {
transformer: 'lightningcss',
devSourcemap: isDev
},
resolve: {
alias: [
{
@@ -62,6 +70,13 @@ export default defineConfig(() => ({
"preact/hooks"
]
},
optimizeDeps: {
include: [
"ckeditor5-premium-features",
"ckeditor5",
"mathlive"
]
},
build: {
target: "esnext",
outDir: './dist',
@@ -70,8 +85,7 @@ export default defineConfig(() => ({
sourcemap: false,
rollupOptions: {
input: {
desktop: join(__dirname, "src", "desktop.ts"),
mobile: join(__dirname, "src", "mobile.ts"),
index: join(__dirname, "src", "index.html"),
login: join(__dirname, "src", "login.ts"),
setup: join(__dirname, "src", "setup.ts"),
set_password: join(__dirname, "src", "set_password.ts"),
@@ -80,11 +94,10 @@ export default defineConfig(() => ({
},
output: {
entryFileNames: "src/[name].js",
chunkFileNames: "src/[name].js",
assetFileNames: "src/[name].[ext]",
chunkFileNames: "src/[name]-[hash].js",
assetFileNames: "src/[name]-[hash].[ext]",
manualChunks: {
"ckeditor5": [ "@triliumnext/ckeditor5" ],
"boxicons": [ "../../node_modules/boxicons/css/boxicons.min.css" ]
"ckeditor5": [ "@triliumnext/ckeditor5" ]
},
},
onwarn(warning, rollupWarn) {

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/desktop",
"version": "0.101.1",
"version": "0.101.3",
"description": "Build your personal knowledge base with Trilium Notes",
"private": true,
"main": "src/main.ts",
@@ -23,7 +23,7 @@
},
"dependencies": {
"@electron/remote": "2.1.3",
"better-sqlite3": "12.5.0",
"better-sqlite3": "12.6.0",
"electron-debug": "4.1.0",
"electron-dl": "4.0.0",
"electron-squirrel-startup": "1.0.1",

View File

@@ -4,7 +4,7 @@
"description": "Standalone tool to dump contents of Trilium document.db file into a directory tree of notes",
"private": true,
"dependencies": {
"better-sqlite3": "12.5.0",
"better-sqlite3": "12.6.0",
"mime-types": "3.0.2",
"sanitize-filename": "1.6.3",
"tsx": "4.21.0",

View File

@@ -5,7 +5,7 @@
"description": "Desktop version of Trilium which imports the demo database (presented to new users at start-up) or the user guide and other documentation and saves the modifications for committing.",
"dependencies": {
"archiver": "7.0.1",
"better-sqlite3": "12.5.0"
"better-sqlite3": "12.6.0"
},
"devDependencies": {
"@triliumnext/client": "workspace:*",

View File

@@ -9,36 +9,36 @@ const baseURL = process.env['BASE_URL'] || `http://127.0.0.1:${port}`;
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "src",
reporter: [["list"], ["html", { outputFolder: "test-output" }]],
outputDir: "test-output",
retries: 3,
testDir: "src",
reporter: [["list"], ["html", { outputFolder: "test-output" }]],
outputDir: "test-output",
retries: 3,
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
baseURL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Run your local dev server before starting the tests */
webServer: !process.env.TRILIUM_DOCKER ? {
command: 'pnpm start-prod-no-dir',
url: baseURL,
reuseExistingServer: !process.env.CI,
cwd: join(__dirname, "../server"),
env: {
TRILIUM_DATA_DIR: "spec/db",
TRILIUM_PORT: port,
TRILIUM_INTEGRATION_TEST: "memory"
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
baseURL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
timeout: 5 * 60 * 1000
} : undefined,
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
}
]
/* Run your local dev server before starting the tests */
webServer: !process.env.TRILIUM_DOCKER ? {
command: 'pnpm start-prod-no-dir',
url: baseURL,
reuseExistingServer: !process.env.CI,
cwd: join(__dirname, "../server"),
env: {
TRILIUM_DATA_DIR: "spec/db",
TRILIUM_PORT: port,
TRILIUM_INTEGRATION_TEST: "memory"
},
timeout: 5 * 60 * 1000
} : undefined,
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
}
]
});

View File

@@ -1,4 +1,5 @@
import { test, expect } from "@playwright/test";
import { expect,test } from "@playwright/test";
import App from "../support/app";
const NOTE_TITLE = "Trilium Integration Test DB";
@@ -65,21 +66,21 @@ test("Tabs are restored in right order", async ({ page, context }) => {
// Open three tabs.
await app.closeAllTabs();
await app.goToNoteInNewTab("Code notes");
await expect(app.getActiveTab()).toContainText("Code notes");
await app.addNewTab();
await app.goToNoteInNewTab("Text notes");
await expect(app.getActiveTab()).toContainText("Text notes");
await app.addNewTab();
await app.goToNoteInNewTab("Mermaid");
await expect(app.getActiveTab()).toContainText("Mermaid");
// Select the mid one.
await app.getTab(1).click();
await expect(app.noteTreeActiveNote).toContainText("Text notes");
await expect(app.getTab(0)).toContainText("Code notes");
await expect(app.getTab(1)).toContainText("Text notes");
await expect(app.getTab(2)).toContainText("Mermaid");
// Refresh the page and check the order.
await app.goto( { preserveTabs: true });
await expect(app.getTab(0)).toContainText("Code notes", { timeout: 15_000 });
await expect(app.getTab(0)).toContainText("Code notes");
await expect(app.getTab(1)).toContainText("Text notes");
await expect(app.getTab(2)).toContainText("Mermaid");
@@ -128,8 +129,8 @@ test("New tab displays workspaces", async ({ page, context }) => {
const workspaceNotesEl = app.currentNoteSplitContent.locator(".workspace-notes");
await expect(workspaceNotesEl).toBeVisible();
expect(workspaceNotesEl).toContainText("Personal");
expect(workspaceNotesEl).toContainText("Work");
await expect(workspaceNotesEl).toContainText("Personal");
await expect(workspaceNotesEl).toContainText("Work");
await expect(workspaceNotesEl.locator(".bx.bxs-user")).toBeVisible();
await expect(workspaceNotesEl.locator(".bx.bx-briefcase-alt")).toBeVisible();

View File

@@ -1,12 +1,12 @@
import test, { BrowserContext, expect, Page } from "@playwright/test";
import test, { expect, Page } from "@playwright/test";
import App from "../support/app";
test.beforeEach(async ({ page, context }) => {
const app = await setLayout({ page, context }, true);
const app = new App(page, context);
await app.goto();
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);
@@ -73,13 +73,15 @@ test("Attachments listing works", async ({ page, context }) => {
test("Download original PDF works", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab("Dacia Logan.pdf");
await app.goToNoteInNewTab("Layers test.pdf");
const pdfHelper = new PdfHelper(app);
await pdfHelper.toBeInitialized();
const downloadButton = app.currentNoteSplit.locator(".icon-action.bx.bx-download");
await expect(downloadButton).toBeVisible();
const [ download ] = await Promise.all([
page.waitForEvent("download"),
app.currentNoteSplit.locator(".icon-action.bx.bx-download").click()
downloadButton.click()
]);
expect(download).toBeDefined();
});
@@ -105,13 +107,6 @@ test("Layers listing works", async ({ page, context }) => {
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"]>;
@@ -125,5 +120,6 @@ class PdfHelper {
async toBeInitialized() {
await expect(this.contentFrame.locator("#pageNumber")).toBeVisible();
await expect(this.contentFrame.locator(".page")).toBeVisible();
}
}

View File

@@ -1,4 +1,5 @@
import { test, expect, Page } from "@playwright/test";
import { expect, test } from "@playwright/test";
import App from "../support/app";
test("Table of contents is displayed", async ({ page, context }) => {
@@ -8,7 +9,7 @@ test("Table of contents is displayed", async ({ page, context }) => {
await app.goToNoteInNewTab("Table of contents");
await expect(app.sidebar).toContainText("Table of Contents");
const rootList = app.sidebar.locator(".toc-widget > span > ol");
const rootList = app.sidebar.locator(".toc > ol");
// Heading 1.1
// Heading 1.1
@@ -42,7 +43,7 @@ test("Highlights list is displayed", async ({ page, context }) => {
await app.closeAllTabs();
await app.goToNoteInNewTab("Highlights list");
await expect(app.sidebar).toContainText("Highlights List");
await expect(app.sidebar).toContainText("10 highlights");
const rootList = app.sidebar.locator(".highlights-list ol");
let index = 0;
for (const highlightedEl of ["Bold 1", "Italic 1", "Underline 1", "Colored text 1", "Background text 1", "Bold 2", "Italic 2", "Underline 2", "Colored text 2", "Background text 2"]) {
@@ -63,7 +64,9 @@ test("Displays math popup", async ({ page, context }) => {
const mathForm = page.locator(".ck-math-form");
await expect(mathForm).toBeVisible();
const input = mathForm.locator(".ck-input").first();
const input = mathForm.locator(".ck-latex-textarea").first();
await expect(input).toBeVisible();
await expect(input).toBeEnabled();
await input.click();
await input.fill("e=mc^2");
await page.waitForTimeout(100);

View File

@@ -37,7 +37,7 @@ export default class App {
this.noteTreeHoistedNote = this.noteTree.locator(".fancytree-node", { has: page.locator(".unhoist-button") });
this.launcherBar = page.locator("#launcher-container");
this.currentNoteSplit = page.locator(".note-split:not(.hidden-ext)");
this.currentNoteSplitTitle = this.currentNoteSplit.locator(".note-title");
this.currentNoteSplitTitle = this.currentNoteSplit.locator(".note-title").first();
this.currentNoteSplitContent = this.currentNoteSplit.locator(".note-detail-printable.visible");
this.sidebar = page.locator("#right-pane");
}
@@ -68,13 +68,15 @@ export default class App {
async goToNoteInNewTab(noteTitle: string) {
const autocomplete = this.currentNoteSplit.locator(".note-autocomplete");
await expect(autocomplete).toBeVisible();
await autocomplete.fill(noteTitle);
const resultsSelector = this.currentNoteSplit.locator(".note-detail-empty-results");
await expect(resultsSelector).toContainText(noteTitle);
await resultsSelector.locator(".aa-suggestion", { hasText: noteTitle })
.nth(1) // Select the second one, as the first one is "Create a new note"
.click();
const suggestionSelector = resultsSelector.locator(".aa-suggestion")
.nth(1); // Select the second one (best candidate), as the first one is "Create a new note"
await expect(suggestionSelector).toContainText(noteTitle);
suggestionSelector.click();
}
async goToSettings() {

View File

@@ -1,5 +1,5 @@
{
"dependencies": {
"better-sqlite3": "12.5.0"
"better-sqlite3": "12.6.0"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/server",
"version": "0.101.1",
"version": "0.101.3",
"description": "The server-side component of TriliumNext, which exposes the client via the web, allows for sync and provides a REST API for both internal and external use.",
"private": true,
"main": "./src/main.ts",
@@ -29,17 +29,16 @@
"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",
"better-sqlite3": "12.6.0",
"html-to-text": "9.0.5",
"node-html-parser": "7.0.1",
"node-html-parser": "7.0.2",
"sucrase": "3.35.1"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.71.2",
"@braintree/sanitize-url": "7.1.1",
"@anthropic-ai/sdk": "0.71.2",
"@electron/remote": "2.1.3",
"@preact/preset-vite": "2.10.2",
"@triliumnext/commons": "workspace:*",
"@triliumnext/core": "workspace:*",
"@triliumnext/express-partial-content": "workspace:*",
"@triliumnext/highlightjs": "workspace:*",
"@triliumnext/turndown-plugin-gfm": "workspace:*",
@@ -49,17 +48,14 @@
"@types/compression": "1.8.1",
"@types/cookie-parser": "1.4.10",
"@types/debounce": "1.2.4",
"@types/ejs": "3.1.5",
"@types/escape-html": "1.0.4",
"@types/ejs": "3.1.5",
"@types/express-http-proxy": "1.6.7",
"@types/express-session": "1.18.2",
"@types/fs-extra": "11.0.4",
"@types/html": "1.0.4",
"@types/ini": "4.1.1",
"@types/mime-types": "3.0.1",
"@types/ini": "4.1.1",
"@types/multer": "2.0.0",
"@types/safe-compare": "1.1.2",
"@types/sanitize-html": "2.16.0",
"@types/safe-compare": "1.1.2",
"@types/sax": "1.2.7",
"@types/serve-favicon": "2.5.7",
"@types/serve-static": "2.2.0",
@@ -86,11 +82,10 @@
"ejs": "3.1.10",
"electron": "39.2.7",
"electron-debug": "4.1.0",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
"electron-window-state": "5.0.3",
"express": "5.2.1",
"express-http-proxy": "2.1.2",
"express-openid-connect": "2.19.3",
"express-openid-connect": "2.19.4",
"express-rate-limit": "8.2.1",
"express-session": "1.18.2",
"file-uri-to-path": "2.0.0",
@@ -109,28 +104,24 @@
"jimp": "1.6.0",
"lorem-ipsum": "2.0.8",
"marked": "17.0.1",
"mime-types": "3.0.2",
"multer": "2.0.2",
"normalize-strings": "1.1.1",
"ollama": "0.6.3",
"openai": "6.15.0",
"openai": "6.16.0",
"rand-token": "1.0.1",
"safe-compare": "1.1.4",
"sanitize-filename": "1.6.3",
"sanitize-html": "2.17.0",
"sax": "1.4.3",
"safe-compare": "1.1.4",
"sax": "1.4.4",
"serve-favicon": "2.5.1",
"stream-throttle": "0.1.3",
"strip-bom": "5.0.0",
"striptags": "3.2.0",
"supertest": "7.1.4",
"supertest": "7.2.2",
"swagger-jsdoc": "6.2.8",
"time2fa": "1.4.2",
"tmp": "0.2.5",
"turndown": "7.2.2",
"unescape": "1.0.1",
"vite": "7.3.0",
"ws": "8.18.3",
"vite": "7.3.1",
"ws": "8.19.0",
"xml2js": "0.6.2",
"yauzl": "3.2.0"
}

Binary file not shown.

View File

@@ -1,6 +1,6 @@
import anonymizationService from "./services/anonymization.js";
import sqlInit from "./services/sql_init.js";
await import("./becca/entity_constructor.js");
await import("@triliumnext/core");
sqlInit.dbReady.then(async () => {
try {

View File

@@ -1,25 +1,25 @@
import("@triliumnext/core");
import { erase } from "@triliumnext/core";
import compression from "compression";
import cookieParser from "cookie-parser";
import express from "express";
import { auth } from "express-openid-connect";
import helmet from "helmet";
import { t } from "i18next";
import path from "path";
import favicon from "serve-favicon";
import cookieParser from "cookie-parser";
import helmet from "helmet";
import compression from "compression";
import config from "./services/config.js";
import utils, { getResourceDir, isDev } from "./services/utils.js";
import assets from "./routes/assets.js";
import routes from "./routes/routes.js";
import custom from "./routes/custom.js";
import error_handlers from "./routes/error_handlers.js";
import { startScheduledCleanup } from "./services/erase.js";
import sql_init from "./services/sql_init.js";
import { auth } from "express-openid-connect";
import openID from "./services/open_id.js";
import { t } from "i18next";
import eventService from "./services/events.js";
import routes from "./routes/routes.js";
import config from "./services/config.js";
import log from "./services/log.js";
import "./services/handlers.js";
import "./becca/becca_loader.js";
import openID from "./services/open_id.js";
import { RESOURCE_DIR } from "./services/resource_dir.js";
import sql_init from "./services/sql_init.js";
import utils, { getResourceDir, isDev } from "./services/utils.js";
export default async function buildApp() {
const app = express();
@@ -107,7 +107,7 @@ export default async function buildApp() {
await import("./services/scheduler.js");
startScheduledCleanup();
erase.startScheduledCleanup();
if (utils.isElectron) {
(await import("@electron/remote/main/index.js")).initialize();

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,95 @@
<figure class="image image-style-align-right">
<img style="aspect-ratio:328/45;" src="1_Hiding the subtree_image.png"
width="328" height="45">
<figcaption>An example of a collection with a relatively large number of children
that are hidden from the tree.</figcaption>
</figure>
<p>The tree works well when the notes are structured in a hierarchy so that
the number of items stays small. When a note has a large number of notes
(in the order of thousands or tens of thousands), two problems arise:</p>
<ul>
<li data-list-item-id="e536c86d371061c12f76f7de2a0af67be">Navigating between notes becomes cumbersome and the tree itself gets cluttered
with a large amount of notes.</li>
<li data-list-item-id="ecc37d6c4d0430254e98615842b94429d">The large amount of notes can slow down the application considerably.</li>
</ul>
<p>Since v0.102.0, Trilium allows the tree to hide the child notes of particular
notes. This works for both&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GTwFsgaA0lCt">Collections</a>&nbsp;and
normal notes.</p>
<h2>Interaction</h2>
<p>When the subtree of a note is hidden, there are a few subtle changes:</p>
<ul>
<li data-list-item-id="ec1ce3d2030f36e4847f3bbd9468d28e3">To indicate that the subtree is hidden, the note will not have an expand
button and it will display the number of children to the right.</li>
<li
data-list-item-id="ea99d38ea6c8a816cf2ab7a7e73cfcac5">It's not possible to add a new note directly from the tree.
<ul>
<li data-list-item-id="ef0132a903a11e9f667b2b2f4c4fff17a">For&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GTwFsgaA0lCt">Collections</a>,
it's best to use the built-in mechanism to create notes (for example by
creating a new point on a geo-map, or by adding a new row in a table).</li>
<li
data-list-item-id="e7db44100046c8c79bf79841285aacd1f">For normal notes, it's still possible to create children via other means
such as using the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/QEAPj01N5f7w/_help_hrZ1D00cLbal">Internal (reference) links</a>&nbsp;system.</li>
</ul>
</li>
<li data-list-item-id="eb049f46cf91db6de113af1099a14944e">Notes can be dragged from outside the note, case in which they will be
cloned into it.
<ul>
<li data-list-item-id="e96d9b7a0755e9c054bab5db4fc1aa25e">Instead of switching to the child notes that were copied, the parent note
is highlighted instead.</li>
<li data-list-item-id="ec667e3f94a0cfa3fa41ce38d3ed6ee95">A notification will indicate this behavior.</li>
</ul>
</li>
<li data-list-item-id="eb64670dd7ace6764c18602b440f88049">Similarly, features such as cut/copy and then paste into the note will
also work.</li>
</ul>
<h2>Spotlighting</h2>
<figure class="image image-style-align-right">
<img style="aspect-ratio:322/83;" src="Hiding the subtree_image.png"
width="322" height="83">
</figure>
<p>Even if the subtree of a note is hidden, if a child note manages to become
active, it will still appear inside the tree in a special state called <em>spotlighted</em>.</p>
<p>During this state, the note remains under its normal hierarchy, so that
its easy to tell its location. In addition, this means that:</p>
<ul>
<li data-list-item-id="e2490369eb3d99ca694dba23a3410abef">The note position is clearly visible when using the&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/wArbEsdSae6g/_help_eIg8jdvaoNNd">Search</a>.</li>
<li
data-list-item-id="e041d3807f80dc77b022540b0551b8376">The note can still be operated on from the tree, such as adding a&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/IakOLONlIfGI/_help_TBwsyfadTA18">Branch prefix</a>&nbsp;or moving it outside the collection.</li>
</ul>
<p>The note appears in italics to indicate its temporary display. When switching
to another note, the spotlighted note will disappear.</p>
<aside class="admonition note">
<p>Only one note can be highlighted at the time. When working with multiple
notes such as dragging them into the collection, no note will be spotlighted.
This is intentional to avoid displaying a partial state of the subtree.</p>
</aside>
<h2>Working with collections</h2>
<p>By default, some of the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GTwFsgaA0lCt">Collections</a>&nbsp;will
automatically hide their child notes, for example the&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_CtBQqbwXDx1w">Kanban Board</a>&nbsp;or
the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_2FvYrpmOXm29">Table</a>.</p>
<p>The reasoning behind this is that collections are generally opaque to
the rest of the notes and they can generate a large amount of sub-notes
since they intentionally lack structure (in order to allow easy swapping
between views).</p>
<p>Some types of collections have the child notes intentionally shown, for
example the legacy ones (Grid and List), but also the&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_zP3PMqaG71Ct">Presentation</a>&nbsp;which
requires the tree structure in order to organize and edit the slides.</p>
<p>To toggle this behavior:</p>
<ul>
<li data-list-item-id="e6d8c8c98802d70f13df626ea1f062122">In the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_IjZS7iK5EXtb">New Layout</a>,
press the Options button underneath the title and uncheck <em>Hide child notes in tree</em>.</li>
<li
data-list-item-id="e2398432e127c54239d679a6b13d8390b">Right click the collection note in the&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;and
select <em>Advanced</em><em>Show subtree</em>.</li>
</ul>
<h2>Working with normal notes</h2>
<p>It's possible to hide the subtree for normal notes as well, not just collections.
To do so, right click the note in the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;and
select <em>Advanced</em><em>Hide subtree.</em>
</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -47,7 +47,7 @@
href="#root/_help_tAassRL4RSQL">data directory</a>in the <code spellcheck="false">TRILIUM_DATA_DIR</code> environment
variable and separate port on <code spellcheck="false">TRILIUM_PORT</code> environment
variable. How to do that depends on the platform, in Unix-based systems
you can achieve that by running command such as this:</p><pre><code class="language-text-x-trilium-auto">TRILIUM_DATA_DIR=/home/me/path/to/data/dir TRILIUM_PORT=12345 trilium </code></pre>
you can achieve that by running command such as this:</p><pre><code class="language-text-x-sh">TRILIUM_DATA_DIR=/home/me/path/to/data/dir TRILIUM_PORT=12345 trilium </code></pre>
<p>You can save this command into a <code spellcheck="false">.sh</code> script
file or make an alias. Do this similarly for a second instance with different
data directory and port.</p>

View File

@@ -123,7 +123,6 @@
"password-confirmation": "密码确认",
"button": "设置密码"
},
"javascript-required": "Trilium需要启用JavaScript。",
"setup": {
"heading": "TriliumNext笔记设置",
"new-document": "我是新用户我想为我的笔记创建一个新的Trilium文档",

View File

@@ -123,7 +123,6 @@
"password-confirmation": "Passwortbestätigung",
"button": "Passwort festlegen"
},
"javascript-required": "Trilium erfordert, dass JavaScript aktiviert ist.",
"setup": {
"heading": "Trilium Notes Setup",
"new-document": "Ich bin ein neuer Benutzer und möchte ein neues Trilium-Dokument für meine Notizen erstellen",

View File

@@ -220,7 +220,6 @@
"password-confirmation": "Password confirmation",
"button": "Set password"
},
"javascript-required": "Trilium requires JavaScript to be enabled.",
"setup": {
"heading": "Trilium Notes setup",
"new-document": "I'm a new user, and I want to create a new Trilium document for my notes",

View File

@@ -123,7 +123,6 @@
"password-confirmation": "Confirmación de contraseña",
"button": "Establecer contraseña"
},
"javascript-required": "Trilium requiere que JavaScript esté habilitado.",
"setup": {
"heading": "Configuración de Trilium Notes",
"new-document": "Soy un usuario nuevo y quiero crear un nuevo documento de Trilium para mis notas",

View File

@@ -123,7 +123,6 @@
"password-confirmation": "Confirmation du mot de passe",
"button": "Définir le mot de passe"
},
"javascript-required": "Trilium nécessite que JavaScript soit activé.",
"setup": {
"heading": "Configuration de Trilium Notes",
"new-document": "Je suis un nouvel utilisateur et je souhaite créer un nouveau document Trilium pour mes notes",

View File

@@ -10,6 +10,22 @@
"creating-and-moving-notes": "नोट्स बनाना और स्थानांतरित करना",
"move-note-up": "नोट को ऊपर ले जाएं",
"move-note-down": "नोट को नीचे ले जाएं",
"note-clipboard": "नोट क्लिपबोर्ड"
"note-clipboard": "नोट क्लिपबोर्ड",
"duplicate-subtree": "डुप्लिकेट सबट्री",
"open-new-tab": "नया टैब खोलें",
"second-tab": "लिस्ट में दूसरी टैब एक्टिवेट करें",
"third-tab": "लिस्ट में तीसरी टैब एक्टिवेट करें",
"fourth-tab": "लिस्ट में चौथी टैब एक्टिवेट करें",
"sixth-tab": "लिस्ट में छठी टैब एक्टिवेट करें",
"seventh-tab": "लिस्ट में सातवीं टैब एक्टिवेट करें",
"eight-tab": "लिस्ट में आठवीं टैब एक्टिवेट करें",
"ninth-tab": "लिस्ट में नौवीं टैब एक्टिवेट करें",
"last-tab": "लिस्ट में आखिरी टैब एक्टिवेट करें",
"show-sql-console": "\"SQL कंसोल\" पेज खोलें",
"show-backend-log": "\"बैकेंड लॉग\" पेज खोलें",
"quick-search": "क्विक सर्च बार को एक्टिवेट करें",
"search-in-subtree": "एक्टिव नोट के सब-ट्री में नोट्स खोजें",
"expand-subtree": "मौजूदा नोट के सब-ट्री को (subtree) एक्सपैंड करें",
"delete-note": "नोट डिलीट करें"
}
}

View File

@@ -324,7 +324,6 @@
"password-confirmation": "Conferma della password",
"button": "Imposta password"
},
"javascript-required": "Trilium richiede JavaScript abilitato per funzionare.",
"setup": {
"heading": "Configurazione di Trilium Notes",
"new-document": "Sono un nuovo utente, e desidero creare un nuovo documento Trilium per le mie note",

View File

@@ -220,7 +220,6 @@
"button": "パスワードの設定",
"password-confirmation": "パスワードの再入力"
},
"javascript-required": "Triliumを使用するにはJavaScriptを有効にする必要があります。",
"setup": {
"heading": "Trilium Notes セットアップ",
"new-document": "私は新しいユーザーで、ートを取るために新しいTriliumドキュメントを作成したい",

View File

@@ -7,6 +7,11 @@
"scroll-to-active-note": "Skroll notat-treet til aktivt notat",
"quick-search": "Aktiver hurtigsøk-feltet",
"search-in-subtree": "Søk etter notater i det aktive notatets understruktur",
"creating-and-moving-notes": "Lage og flytte notater"
"creating-and-moving-notes": "Lage og flytte notater",
"dialogs": "Dialogbokser",
"other": "Andre"
},
"setup_sync-from-desktop": {
"step6-here": "her"
}
}

View File

@@ -212,7 +212,6 @@
"button": "Zaloguj",
"sign_in_with_sso": "Zaloguj przez {{ ssoIssuerName }}"
},
"javascript-required": "Trilium wymaga włączenia obsługi JavaScript.",
"setup_sync-from-server": {
"server-host": "Adres serwera Trilium",
"proxy-server": "Serwer proxy (opcjonalnie)",

View File

@@ -220,7 +220,6 @@
"password-confirmation": "Confirmar Palavra-passe",
"button": "Definir palavra-passe"
},
"javascript-required": "Trilium precisa que JavaScript esteja ativado.",
"setup": {
"heading": "Trilium Notes setup",
"new-document": "Sou um novo utilizador e quero criar um documento Trilium para as minhas notas",

View File

@@ -123,7 +123,6 @@
"password-confirmation": "Confirmar Senha",
"button": "Definir senha"
},
"javascript-required": "Trilium precisa que JavaScript esteja habilitado.",
"setup": {
"heading": "Trilium Notes setup",
"new-document": "Sou um novo usuário e quero criar um novo documento Trilium para minhas notas",

View File

@@ -123,7 +123,6 @@
"password": "Parolă",
"password-confirmation": "Confirmarea parolei"
},
"javascript-required": "Trilium necesită JavaScript să fie activat pentru a putea funcționa.",
"setup": {
"heading": "Instalarea Trilium Notes",
"init-in-progress": "Se inițializează documentul",

View File

@@ -398,7 +398,6 @@
"parent": "родитель:",
"clipped-from": "Эта заметка изначально была вырезана из {{- url}}"
},
"javascript-required": "Для работы Trilium требуется JavaScript.",
"setup_sync-from-desktop": {
"heading": "Синхронизация с приложения ПК",
"description": "Эту настройку необходимо инициировать из приложения для ПК:",

View File

@@ -123,7 +123,6 @@
"password-confirmation": "確認密碼",
"button": "設定密碼"
},
"javascript-required": "Trilium 需要啟用 JavaScript。",
"setup": {
"heading": "Trilium 筆記設定",
"new-document": "我是新用戶,我想為我的筆記建立一個新的 Trilium 文件",

View File

@@ -220,7 +220,6 @@
"password-confirmation": "Підтвердження пароля",
"button": "Встановити пароль"
},
"javascript-required": "Для роботи Trilium потрібен JavaScript.",
"setup": {
"heading": "Налаштування Trilium Notes",
"new-document": "Я новий користувач і хочу створити новий документ Trilium для своїх нотаток",

View File

@@ -1,60 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
<title>Trilium Notes</title>
<style id="trilium-icon-packs">
<%- iconPackCss %>
</style>
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
</head>
<body
id="trilium-app"
class="desktop heading-style-<%= headingStyle %> layout-<%= layoutOrientation %> platform-<%= platform %> <%= isElectron ? 'electron' : '' %> <%= hasNativeTitleBar ? 'native-titlebar' : '' %> <%= hasBackgroundEffects ? 'background-effects' : '' %>"
lang="<%= currentLocale.id %>" dir="<%= currentLocale.rtl ? 'rtl' : 'ltr' %>"
>
<noscript><%= t("javascript-required") %></noscript>
<script>
// hide body to reduce flickering on the startup. This is done through JS and not CSS to not hide <noscript>
document.getElementsByTagName("body")[0].style.display = "none";
</script>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container" style="display: none"></div>
<%- include("./partials/windowGlobal.ejs", locals) %>
<!-- Required for match the PWA's top bar color with the theme -->
<!-- This works even when the user directly changes --root-background in CSS -->
<div id="background-color-tracker" style="position: absolute; visibility: hidden; color: var(--root-background); transition: color 1ms;"></div>
<!-- Required for correct loading of scripts in Electron -->
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
<link href="<%= assetPath %>/stylesheets/ckeditor-theme.css" rel="stylesheet">
<link href="api/fonts" rel="stylesheet">
<link href="<%= assetPath %>/stylesheets/theme-light.css" rel="stylesheet">
<% if (themeCssUrl) { %>
<link href="<%= themeCssUrl %>" rel="stylesheet">
<% } %>
<% if (themeUseNextAsBase === "next") { %>
<link href="<%= assetPath %>/stylesheets/theme-next.css" rel="stylesheet">
<% } else if (themeUseNextAsBase === "next-dark") { %>
<link href="<%= assetPath %>/stylesheets/theme-next-dark.css" rel="stylesheet">
<% } else if (themeUseNextAsBase === "next-light") { %>
<link href="<%= assetPath %>/stylesheets/theme-next-light.css" rel="stylesheet">
<% } %>
<link href="<%= assetPath %>/stylesheets/style.css" rel="stylesheet">
<script src="<%= appPath %>/desktop.js" crossorigin type="module"></script>
</body>
</html>

View File

@@ -1,137 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
<meta name="theme-color" content="#fff">
<title>Trilium Notes</title>
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
<style>
.lds-roller {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-roller div {
animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
transform-origin: 40px 40px;
}
.lds-roller div:after {
content: " ";
display: block;
position: absolute;
width: 7px;
height: 7px;
border-radius: 50%;
background: #000;
margin: -4px 0 0 -4px;
}
.lds-roller div:nth-child(1) {
animation-delay: -0.036s;
}
.lds-roller div:nth-child(1):after {
top: 63px;
left: 63px;
}
.lds-roller div:nth-child(2) {
animation-delay: -0.072s;
}
.lds-roller div:nth-child(2):after {
top: 68px;
left: 56px;
}
.lds-roller div:nth-child(3) {
animation-delay: -0.108s;
}
.lds-roller div:nth-child(3):after {
top: 71px;
left: 48px;
}
.lds-roller div:nth-child(4) {
animation-delay: -0.144s;
}
.lds-roller div:nth-child(4):after {
top: 72px;
left: 40px;
}
.lds-roller div:nth-child(5) {
animation-delay: -0.18s;
}
.lds-roller div:nth-child(5):after {
top: 71px;
left: 32px;
}
.lds-roller div:nth-child(6) {
animation-delay: -0.216s;
}
.lds-roller div:nth-child(6):after {
top: 68px;
left: 24px;
}
.lds-roller div:nth-child(7) {
animation-delay: -0.252s;
}
.lds-roller div:nth-child(7):after {
top: 63px;
left: 17px;
}
.lds-roller div:nth-child(8) {
animation-delay: -0.288s;
}
.lds-roller div:nth-child(8):after {
top: 56px;
left: 12px;
}
@keyframes lds-roller {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<style id="trilium-icon-packs">
<%- iconPackCss %>
</style>
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
</head>
<body
class="mobile heading-style-<%= headingStyle %>"
lang="<%= currentLocale.id %>" dir="<%= currentLocale.rtl ? 'rtl' : 'ltr' %>"
>
<noscript><%= t("javascript-required") %></noscript>
<div id="context-menu-cover"></div>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container"></div>
<%- include("./partials/windowGlobal.ejs", locals) %>
<script src="<%= appPath %>/mobile.js" crossorigin type="module"></script>
<link href="api/fonts" rel="stylesheet">
<link href="<%= assetPath %>/stylesheets/ckeditor-theme.css" rel="stylesheet">
<link href="<%= assetPath %>/stylesheets/theme-light.css" rel="stylesheet">
<% if (themeCssUrl) { %>
<link href="<%= themeCssUrl %>" rel="stylesheet">
<% } %>
<% if (themeUseNextAsBase === "next") { %>
<link href="<%= assetPath %>/stylesheets/theme-next.css" rel="stylesheet">
<% } else if (themeUseNextAsBase === "next-dark") { %>
<link href="<%= assetPath %>/stylesheets/theme-next-dark.css" rel="stylesheet">
<% } else if (themeUseNextAsBase === "next-light") { %>
<link href="<%= assetPath %>/stylesheets/theme-next-light.css" rel="stylesheet">
<% } %>
<link href="<%= assetPath %>/stylesheets/style.css" rel="stylesheet">
</body>
</html>

View File

@@ -1,25 +0,0 @@
<script type="text/javascript">
global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
window.glob = {
device: "<%= device %>",
baseApiUrl: "<%= baseApiUrl %>",
activeDialog: null,
maxEntityChangeIdAtLoad: <%= maxEntityChangeIdAtLoad %>,
maxEntityChangeSyncIdAtLoad: <%= maxEntityChangeSyncIdAtLoad %>,
instanceName: '<%= instanceName %>',
csrfToken: '<%= csrfToken %>',
isDev: <%= isDev %>,
appCssNoteIds: <%- JSON.stringify(appCssNoteIds) %>,
isMainWindow: <%= isMainWindow %>,
isProtectedSessionAvailable: <%= isProtectedSessionAvailable %>,
triliumVersion: "<%= triliumVersion %>",
assetPath: "<%= assetPath %>",
appPath: "<%= appPath %>",
platform: "<%= platform %>",
hasNativeTitleBar: <%= hasNativeTitleBar %>,
TRILIUM_SAFE_MODE: <%= !!process.env.TRILIUM_SAFE_MODE %>,
isRtl: <%= !!currentLocale.rtl %>,
iconRegistry: <%- JSON.stringify(iconRegistry) %>
};
</script>

View File

@@ -1,7 +1,2 @@
"use strict";
import Becca from "./becca-interface.js";
const becca = new Becca();
import { becca } from "@triliumnext/core";
export default becca;

View File

@@ -1,260 +1,2 @@
import type { AttachmentRow } from "@triliumnext/commons";
import dateUtils from "../../services/date_utils.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
import protectedSessionService from "../../services/protected_session.js";
import sql from "../../services/sql.js";
import utils from "../../services/utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import type BBranch from "./bbranch.js";
import type BNote from "./bnote.js";
const attachmentRoleToNoteTypeMapping = {
image: "image",
file: "file"
};
interface ContentOpts {
// TODO: Found in bnote.ts, to check if it's actually used and not a typo.
forceSave?: boolean;
/** will also save this BAttachment entity */
forceFullSave?: boolean;
/** override frontend heuristics on when to reload, instruct to reload */
forceFrontendReload?: boolean;
}
/**
* Attachment represent data related/attached to the note. Conceptually similar to attributes, but intended for
* larger amounts of data and generally not accessible to the user.
*/
class BAttachment extends AbstractBeccaEntity<BAttachment> {
static get entityName() {
return "attachments";
}
static get primaryKeyName() {
return "attachmentId";
}
static get hashedProperties() {
return ["attachmentId", "ownerId", "role", "mime", "title", "blobId", "utcDateScheduledForErasureSince"];
}
noteId?: number;
attachmentId?: string;
/** either noteId or revisionId to which this attachment belongs */
ownerId!: string;
role!: string;
mime!: string;
title!: string;
type?: keyof typeof attachmentRoleToNoteTypeMapping;
position?: number;
utcDateScheduledForErasureSince?: string | null;
/** optionally added to the entity */
contentLength?: number;
isDecrypted?: boolean;
constructor(row: AttachmentRow) {
super();
this.updateFromRow(row);
this.decrypt();
}
updateFromRow(row: AttachmentRow): void {
if (!row.ownerId?.trim()) {
throw new Error("'ownerId' must be given to initialize a Attachment entity");
} else if (!row.role?.trim()) {
throw new Error("'role' must be given to initialize a Attachment entity");
} else if (!row.mime?.trim()) {
throw new Error("'mime' must be given to initialize a Attachment entity");
} else if (!row.title?.trim()) {
throw new Error("'title' must be given to initialize a Attachment entity");
}
this.attachmentId = row.attachmentId;
this.ownerId = row.ownerId;
this.role = row.role;
this.mime = row.mime;
this.title = row.title;
this.position = row.position;
this.blobId = row.blobId;
this.isProtected = !!row.isProtected;
this.dateModified = row.dateModified;
this.utcDateModified = row.utcDateModified;
this.utcDateScheduledForErasureSince = row.utcDateScheduledForErasureSince;
this.contentLength = row.contentLength;
}
copy(): BAttachment {
return new BAttachment({
ownerId: this.ownerId,
role: this.role,
mime: this.mime,
title: this.title,
blobId: this.blobId,
isProtected: this.isProtected
});
}
getNote(): BNote {
return this.becca.notes[this.ownerId];
}
/** @returns true if the note has string content (not binary) */
override hasStringContent(): boolean {
return utils.isStringNote(this.type, this.mime); // here was !== undefined && utils.isStringNote(this.type, this.mime); I dont know why we need !=undefined. But it filters out canvas libary items
}
isContentAvailable() {
return (
!this.attachmentId || // new attachment which was not encrypted yet
!this.isProtected ||
protectedSessionService.isProtectedSessionAvailable()
);
}
getTitleOrProtected() {
return this.isContentAvailable() ? this.title : "[protected]";
}
decrypt() {
if (!this.isProtected || !this.attachmentId) {
this.isDecrypted = true;
return;
}
if (!this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
try {
this.title = protectedSessionService.decryptString(this.title) || "";
this.isDecrypted = true;
} catch (e: any) {
log.error(`Could not decrypt attachment ${this.attachmentId}: ${e.message} ${e.stack}`);
}
}
}
getContent(): Buffer {
return this._getContent() as Buffer;
}
setContent(content: string | Buffer, opts?: ContentOpts) {
this._setContent(content, opts);
}
convertToNote(): { note: BNote; branch: BBranch } {
// TODO: can this ever be "search"?
if ((this.type as string) === "search") {
throw new Error(`Note of type search cannot have child notes`);
}
if (!this.getNote()) {
throw new Error("Cannot find note of this attachment. It is possible that this is note revision's attachment. " + "Converting note revision's attachments to note is not (yet) supported.");
}
if (!(this.role in attachmentRoleToNoteTypeMapping)) {
throw new Error(`Mapping from attachment role '${this.role}' to note's type is not defined`);
}
if (!this.isContentAvailable()) {
// isProtected is the same for attachment
throw new Error(`Cannot convert protected attachment outside of protected session`);
}
const { note, branch } = noteService.createNewNote({
parentNoteId: this.ownerId,
title: this.title,
type: (attachmentRoleToNoteTypeMapping as any)[this.role],
mime: this.mime,
content: this.getContent(),
isProtected: this.isProtected
});
this.markAsDeleted();
const parentNote = this.getNote();
if (this.role === "image" && parentNote.type === "text") {
const origContent = parentNote.getContent();
if (typeof origContent !== "string") {
throw new Error(`Note with ID '${note.noteId} has a text type but non-string content.`);
}
const oldAttachmentUrl = `api/attachments/${this.attachmentId}/image/`;
const newNoteUrl = `api/images/${note.noteId}/`;
const fixedContent = utils.replaceAll(origContent, oldAttachmentUrl, newNoteUrl);
if (fixedContent !== origContent) {
parentNote.setContent(fixedContent);
}
noteService.asyncPostProcessContent(note, fixedContent);
}
return { note, branch };
}
getFileName() {
const type = this.role === "image" ? "image" : "file";
return utils.formatDownloadTitle(this.title, type, this.mime);
}
override beforeSaving() {
super.beforeSaving();
if (this.position === undefined || this.position === null) {
this.position =
10 +
sql.getValue<number>(
/*sql*/`SELECT COALESCE(MAX(position), 0)
FROM attachments
WHERE ownerId = ?`,
[this.noteId]
);
}
this.dateModified = dateUtils.localNowDateTime();
this.utcDateModified = dateUtils.utcNowDateTime();
}
getPojo() {
return {
attachmentId: this.attachmentId,
ownerId: this.ownerId,
role: this.role,
mime: this.mime,
title: this.title || undefined,
position: this.position,
blobId: this.blobId,
isProtected: !!this.isProtected,
isDeleted: false,
dateModified: this.dateModified,
utcDateModified: this.utcDateModified,
utcDateScheduledForErasureSince: this.utcDateScheduledForErasureSince,
contentLength: this.contentLength
};
}
override getPojoToSave() {
const pojo = this.getPojo();
delete pojo.contentLength;
if (pojo.isProtected) {
if (this.isDecrypted) {
pojo.title = protectedSessionService.encrypt(pojo.title || "") || undefined;
} else {
// updating protected note outside of protected session means we will keep original ciphertexts
delete pojo.title;
}
}
return pojo;
}
}
import { BAttachment } from "@triliumnext/core";
export default BAttachment;

View File

@@ -1,227 +1,2 @@
"use strict";
import BNote from "./bnote.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import dateUtils from "../../services/date_utils.js";
import promotedAttributeDefinitionParser from "../../services/promoted_attribute_definition_parser.js";
import sanitizeAttributeName from "../../services/sanitize_attribute_name.js";
import type { AttributeRow, AttributeType } from "@triliumnext/commons";
interface SavingOpts {
skipValidation?: boolean;
}
/**
* Attribute is an abstract concept which has two real uses - label (key - value pair)
* and relation (representing named relationship between source and target note)
*/
class BAttribute extends AbstractBeccaEntity<BAttribute> {
static get entityName() {
return "attributes";
}
static get primaryKeyName() {
return "attributeId";
}
static get hashedProperties() {
return ["attributeId", "noteId", "type", "name", "value", "isInheritable"];
}
attributeId!: string;
noteId!: string;
type!: AttributeType;
name!: string;
position!: number;
value!: string;
isInheritable!: boolean;
constructor(row?: AttributeRow) {
super();
if (!row) {
return;
}
this.updateFromRow(row);
this.init();
}
updateFromRow(row: AttributeRow) {
this.update([row.attributeId, row.noteId, row.type, row.name, row.value, row.isInheritable, row.position, row.utcDateModified]);
}
update([attributeId, noteId, type, name, value, isInheritable, position, utcDateModified]: any) {
this.attributeId = attributeId;
this.noteId = noteId;
this.type = type;
this.name = name;
this.position = position;
this.value = value || "";
this.isInheritable = !!isInheritable;
this.utcDateModified = utcDateModified;
return this;
}
override init() {
if (this.attributeId) {
this.becca.attributes[this.attributeId] = this;
}
if (!(this.noteId in this.becca.notes)) {
// entities can come out of order in sync, create skeleton which will be filled later
this.becca.addNote(this.noteId, new BNote({ noteId: this.noteId }));
}
this.becca.notes[this.noteId].ownedAttributes.push(this);
const key = `${this.type}-${this.name.toLowerCase()}`;
this.becca.attributeIndex[key] = this.becca.attributeIndex[key] || [];
this.becca.attributeIndex[key].push(this);
const targetNote = this.targetNote;
if (targetNote) {
targetNote.targetRelations.push(this);
}
}
validate() {
if (!["label", "relation"].includes(this.type)) {
throw new Error(`Invalid attribute type '${this.type}' in attribute '${this.attributeId}' of note '${this.noteId}'`);
}
if (!this.name?.trim()) {
throw new Error(`Invalid empty name in attribute '${this.attributeId}' of note '${this.noteId}'`);
}
if (this.type === "relation" && !(this.value in this.becca.notes)) {
throw new Error(`Cannot save relation '${this.name}' of note '${this.noteId}' since it targets not existing note '${this.value}'.`);
}
}
get isAffectingSubtree() {
return this.isInheritable || (this.type === "relation" && ["template", "inherit"].includes(this.name));
}
get targetNoteId() {
// alias
return this.type === "relation" ? this.value : undefined;
}
isAutoLink() {
return this.type === "relation" && ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name);
}
get note() {
return this.becca.notes[this.noteId];
}
get targetNote() {
if (this.type === "relation") {
return this.becca.notes[this.value];
}
}
getNote() {
const note = this.becca.getNote(this.noteId);
if (!note) {
throw new Error(`Note '${this.noteId}' of attribute '${this.attributeId}', type '${this.type}', name '${this.name}' does not exist.`);
}
return note;
}
getTargetNote() {
if (this.type !== "relation") {
throw new Error(`Attribute '${this.attributeId}' is not a relation.`);
}
if (!this.value) {
return null;
}
return this.becca.getNote(this.value);
}
isDefinition() {
return this.type === "label" && (this.name.startsWith("label:") || this.name.startsWith("relation:"));
}
getDefinition() {
return promotedAttributeDefinitionParser.parse(this.value);
}
getDefinedName() {
if (this.type === "label" && this.name.startsWith("label:")) {
return this.name.substr(6);
} else if (this.type === "label" && this.name.startsWith("relation:")) {
return this.name.substr(9);
} else {
return this.name;
}
}
override get isDeleted() {
return !(this.attributeId in this.becca.attributes);
}
override beforeSaving(opts: SavingOpts = {}) {
if (!opts.skipValidation) {
this.validate();
}
this.name = sanitizeAttributeName(this.name);
if (!this.value) {
// null value isn't allowed
this.value = "";
}
if (this.position === undefined || this.position === null) {
const maxExistingPosition = this.getNote()
.getAttributes()
.reduce((maxPosition, attr) => Math.max(maxPosition, attr.position || 0), 0);
this.position = maxExistingPosition + 10;
}
if (!this.isInheritable) {
this.isInheritable = false;
}
this.utcDateModified = dateUtils.utcNowDateTime();
super.beforeSaving();
this.becca.attributes[this.attributeId] = this;
}
getPojo() {
return {
attributeId: this.attributeId,
noteId: this.noteId,
type: this.type,
name: this.name,
position: this.position,
value: this.value,
isInheritable: this.isInheritable,
utcDateModified: this.utcDateModified,
isDeleted: false
};
}
createClone(type: AttributeType, name: string, value: string, isInheritable?: boolean) {
return new BAttribute({
noteId: this.noteId,
type: type,
name: name,
value: value,
position: this.position,
isInheritable: isInheritable,
utcDateModified: this.utcDateModified
});
}
}
import { BAttribute } from "@triliumnext/core";
export default BAttribute;

View File

@@ -1,288 +1,2 @@
"use strict";
import BNote from "./bnote.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import dateUtils from "../../services/date_utils.js";
import utils from "../../services/utils.js";
import TaskContext from "../../services/task_context.js";
import cls from "../../services/cls.js";
import log from "../../services/log.js";
import type { BranchRow } from "@triliumnext/commons";
import handlers from "../../services/handlers.js";
/**
* Branch represents a relationship between a child note and its parent note. Trilium allows a note to have multiple
* parents.
*
* Note that you should not rely on the branch's identity, since it can change easily with a note's move.
* Always check noteId instead.
*/
class BBranch extends AbstractBeccaEntity<BBranch> {
static get entityName() {
return "branches";
}
static get primaryKeyName() {
return "branchId";
}
// notePosition is not part of hash because it would produce a lot of updates in case of reordering
static get hashedProperties() {
return ["branchId", "noteId", "parentNoteId", "prefix"];
}
branchId?: string;
noteId!: string;
parentNoteId!: string;
prefix!: string | null;
notePosition!: number;
isExpanded!: boolean;
constructor(row?: BranchRow) {
super();
if (!row) {
return;
}
this.updateFromRow(row);
this.init();
}
updateFromRow(row: BranchRow) {
this.update([row.branchId, row.noteId, row.parentNoteId, row.prefix, row.notePosition, row.isExpanded, row.utcDateModified]);
}
update([branchId, noteId, parentNoteId, prefix, notePosition, isExpanded, utcDateModified]: any) {
this.branchId = branchId;
this.noteId = noteId;
this.parentNoteId = parentNoteId;
this.prefix = prefix;
this.notePosition = notePosition;
this.isExpanded = !!isExpanded;
this.utcDateModified = utcDateModified;
return this;
}
override init() {
if (this.branchId) {
this.becca.branches[this.branchId] = this;
}
this.becca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
const childNote = this.childNote;
if (!childNote.parentBranches.includes(this)) {
childNote.parentBranches.push(this);
}
if (this.noteId === "root") {
return;
}
const parentNote = this.parentNote;
if (parentNote) {
if (!childNote.parents.includes(parentNote)) {
childNote.parents.push(parentNote);
}
if (!parentNote.children.includes(childNote)) {
parentNote.children.push(childNote);
}
}
}
get childNote(): BNote {
if (!(this.noteId in this.becca.notes)) {
// entities can come out of order in sync/import, create skeleton which will be filled later
this.becca.addNote(this.noteId, new BNote({ noteId: this.noteId }));
}
return this.becca.notes[this.noteId];
}
getNote(): BNote {
return this.childNote;
}
/** @returns root branch will have undefined parent, all other branches have to have a parent note */
get parentNote(): BNote | undefined {
if (!(this.parentNoteId in this.becca.notes) && this.parentNoteId !== "none") {
// entities can come out of order in sync/import, create skeleton which will be filled later
this.becca.addNote(this.parentNoteId, new BNote({ noteId: this.parentNoteId }));
}
return this.becca.notes[this.parentNoteId];
}
override get isDeleted() {
return this.branchId == undefined || !(this.branchId in this.becca.branches);
}
/**
* Branch is weak when its existence should not hinder deletion of its note.
* As a result, note with only weak branches should be immediately deleted.
* An example is shared or bookmarked clones - they are created automatically and exist for technical reasons,
* not as user-intended actions. From user perspective, they don't count as real clones and for the purpose
* of deletion should not act as a clone.
*/
get isWeak() {
return ["_share", "_lbBookmarks"].includes(this.parentNoteId);
}
/**
* Delete a branch. If this is a last note's branch, delete the note as well.
*
* @param deleteId - optional delete identified
*
* @returns true if note has been deleted, false otherwise
*/
deleteBranch(deleteId?: string, taskContext?: TaskContext<"deleteNotes">): boolean {
if (!deleteId) {
deleteId = utils.randomString(10);
}
if (!taskContext) {
taskContext = new TaskContext("no-progress-reporting", "deleteNotes", null);
}
taskContext.increaseProgressCount();
const note = this.getNote();
if (!taskContext.noteDeletionHandlerTriggered) {
const parentBranches = note.getParentBranches();
if (parentBranches.length === 1 && parentBranches[0] === this) {
// needs to be run before branches and attributes are deleted and thus attached relations disappear
handlers.runAttachedRelations(note, "runOnNoteDeletion", note);
}
}
if ((this.noteId === "root" || this.noteId === cls.getHoistedNoteId()) && !this.isWeak) {
throw new Error("Can't delete root or hoisted branch/note");
}
this.markAsDeleted(deleteId);
const notDeletedBranches = note.getStrongParentBranches();
if (notDeletedBranches.length === 0) {
for (const weakBranch of note.getParentBranches()) {
weakBranch.markAsDeleted(deleteId);
}
for (const childBranch of note.getChildBranches()) {
if (childBranch) {
childBranch.deleteBranch(deleteId, taskContext);
}
}
// first delete children and then parent - this will show up better in recent changes
log.info(`Deleting note '${note.noteId}'`);
this.becca.notes[note.noteId].isBeingDeleted = true;
for (const attribute of note.getOwnedAttributes().slice()) {
attribute.markAsDeleted(deleteId);
}
for (const relation of note.getTargetRelations()) {
relation.markAsDeleted(deleteId);
}
for (const attachment of note.getAttachments()) {
attachment.markAsDeleted(deleteId);
}
note.markAsDeleted(deleteId);
return true;
} else {
return false;
}
}
override beforeSaving() {
if (!this.noteId || !this.parentNoteId) {
throw new Error(`noteId and parentNoteId are mandatory properties for Branch`);
}
this.branchId = `${this.parentNoteId}_${this.noteId}`;
if (this.notePosition === undefined || this.notePosition === null) {
let maxNotePos = 0;
if (this.parentNote) {
for (const childBranch of this.parentNote.getChildBranches()) {
if (!childBranch) {
continue;
}
if (
maxNotePos < childBranch.notePosition &&
childBranch.noteId !== "_hidden" // hidden has a very large notePosition to always stay last
) {
maxNotePos = childBranch.notePosition;
}
}
}
this.notePosition = maxNotePos + 10;
}
if (!this.isExpanded) {
this.isExpanded = false;
}
if (!this.prefix?.trim()) {
this.prefix = null;
}
this.utcDateModified = dateUtils.utcNowDateTime();
super.beforeSaving();
this.becca.branches[this.branchId] = this;
}
getPojo() {
return {
branchId: this.branchId,
noteId: this.noteId,
parentNoteId: this.parentNoteId,
prefix: this.prefix,
notePosition: this.notePosition,
isExpanded: this.isExpanded,
isDeleted: false,
utcDateModified: this.utcDateModified
};
}
createClone(parentNoteId: string, notePosition?: number) {
const existingBranch = this.becca.getBranchFromChildAndParent(this.noteId, parentNoteId);
if (existingBranch) {
if (notePosition) {
existingBranch.notePosition = notePosition;
}
return existingBranch;
} else {
return new BBranch({
noteId: this.noteId,
parentNoteId: parentNoteId,
notePosition: notePosition || null,
prefix: this.prefix,
isExpanded: this.isExpanded
});
}
}
getParentNote() {
return this.parentNote;
}
}
import { BBranch } from "@triliumnext/core";
export default BBranch;

View File

@@ -1,89 +1,2 @@
"use strict";
import type { EtapiTokenRow } from "@triliumnext/commons";
import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
/**
* EtapiToken is an entity representing token used to authenticate against Trilium REST API from client applications.
* Used by:
* - Trilium Sender
* - ETAPI clients
*
* The format user is presented with is "<etapiTokenId>_<tokenHash>". This is also called "authToken" to distinguish it
* from tokenHash and token.
*/
class BEtapiToken extends AbstractBeccaEntity<BEtapiToken> {
static get entityName() {
return "etapi_tokens";
}
static get primaryKeyName() {
return "etapiTokenId";
}
static get hashedProperties() {
return ["etapiTokenId", "name", "tokenHash", "utcDateCreated", "utcDateModified", "isDeleted"];
}
etapiTokenId?: string;
name!: string;
tokenHash!: string;
private _isDeleted?: boolean;
constructor(row?: EtapiTokenRow) {
super();
if (!row) {
return;
}
this.updateFromRow(row);
this.init();
}
override get isDeleted() {
return !!this._isDeleted;
}
updateFromRow(row: EtapiTokenRow) {
this.etapiTokenId = row.etapiTokenId;
this.name = row.name;
this.tokenHash = row.tokenHash;
this.utcDateCreated = row.utcDateCreated || dateUtils.utcNowDateTime();
this.utcDateModified = row.utcDateModified || this.utcDateCreated;
this._isDeleted = !!row.isDeleted;
if (this.etapiTokenId) {
this.becca.etapiTokens[this.etapiTokenId] = this;
}
}
override init() {
if (this.etapiTokenId) {
this.becca.etapiTokens[this.etapiTokenId] = this;
}
}
getPojo() {
return {
etapiTokenId: this.etapiTokenId,
name: this.name,
tokenHash: this.tokenHash,
utcDateCreated: this.utcDateCreated,
utcDateModified: this.utcDateModified,
isDeleted: this.isDeleted
};
}
override beforeSaving() {
this.utcDateModified = dateUtils.utcNowDateTime();
super.beforeSaving();
if (this.etapiTokenId) {
this.becca.etapiTokens[this.etapiTokenId] = this;
}
}
}
import { BEtapiToken } from "@triliumnext/core";
export default BEtapiToken;

File diff suppressed because it is too large Load Diff

View File

@@ -1,56 +1,2 @@
"use strict";
import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import type { OptionRow } from "@triliumnext/commons";
/**
* Option represents a name-value pair, either directly configurable by the user or some system property.
*/
class BOption extends AbstractBeccaEntity<BOption> {
static get entityName() {
return "options";
}
static get primaryKeyName() {
return "name";
}
static get hashedProperties() {
return ["name", "value"];
}
name!: string;
value!: string;
constructor(row?: OptionRow) {
super();
if (row) {
this.updateFromRow(row);
}
this.becca.options[this.name] = this;
}
updateFromRow(row: OptionRow) {
this.name = row.name;
this.value = row.value;
this.isSynced = !!row.isSynced;
this.utcDateModified = row.utcDateModified;
}
override beforeSaving() {
super.beforeSaving();
this.utcDateModified = dateUtils.utcNowDateTime();
}
getPojo() {
return {
name: this.name,
value: this.value,
isSynced: this.isSynced,
utcDateModified: this.utcDateModified
};
}
}
import { BOption } from "@triliumnext/core";
export default BOption;

View File

@@ -1,46 +1,2 @@
"use strict";
import type { RecentNoteRow } from "@triliumnext/commons";
import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
/**
* RecentNote represents recently visited note.
*/
class BRecentNote extends AbstractBeccaEntity<BRecentNote> {
static get entityName() {
return "recent_notes";
}
static get primaryKeyName() {
return "noteId";
}
static get hashedProperties() {
return ["noteId", "notePath"];
}
noteId!: string;
notePath!: string;
constructor(row: RecentNoteRow) {
super();
this.updateFromRow(row);
}
updateFromRow(row: RecentNoteRow): void {
this.noteId = row.noteId;
this.notePath = row.notePath;
this.utcDateCreated = row.utcDateCreated || dateUtils.utcNowDateTime();
}
getPojo() {
return {
noteId: this.noteId,
notePath: this.notePath,
utcDateCreated: this.utcDateCreated
};
}
}
import { BRecentNote } from "@triliumnext/core";
export default BRecentNote;

View File

@@ -1,225 +1,2 @@
"use strict";
import protectedSessionService from "../../services/protected_session.js";
import utils from "../../services/utils.js";
import dateUtils from "../../services/date_utils.js";
import becca from "../becca.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import sql from "../../services/sql.js";
import BAttachment from "./battachment.js";
import type { AttachmentRow, NoteType, RevisionPojo, RevisionRow } from "@triliumnext/commons";
import eraseService from "../../services/erase.js";
interface ContentOpts {
/** will also save this BRevision entity */
forceSave?: boolean;
}
interface GetByIdOpts {
includeContentLength?: boolean;
}
/**
* Revision represents a snapshot of note's title and content at some point in the past.
* It's used for seamless note versioning.
*/
class BRevision extends AbstractBeccaEntity<BRevision> {
static get entityName() {
return "revisions";
}
static get primaryKeyName() {
return "revisionId";
}
static get hashedProperties() {
return ["revisionId", "noteId", "title", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified", "blobId"];
}
revisionId?: string;
noteId!: string;
type!: NoteType;
mime!: string;
title!: string;
dateLastEdited?: string;
utcDateLastEdited?: string;
contentLength?: number;
content?: string | Buffer;
constructor(row: RevisionRow, titleDecrypted = false) {
super();
this.updateFromRow(row);
if (this.isProtected && !titleDecrypted) {
const decryptedTitle = protectedSessionService.isProtectedSessionAvailable() ? protectedSessionService.decryptString(this.title) : null;
this.title = decryptedTitle || "[protected]";
}
}
updateFromRow(row: RevisionRow) {
this.revisionId = row.revisionId;
this.noteId = row.noteId;
this.type = row.type;
this.mime = row.mime;
this.isProtected = !!row.isProtected;
this.title = row.title;
this.blobId = row.blobId;
this.dateLastEdited = row.dateLastEdited;
this.dateCreated = row.dateCreated;
this.utcDateLastEdited = row.utcDateLastEdited;
this.utcDateCreated = row.utcDateCreated;
this.utcDateModified = row.utcDateModified;
this.contentLength = row.contentLength;
}
getNote() {
return becca.notes[this.noteId];
}
/** @returns true if the note has string content (not binary) */
override hasStringContent(): boolean {
return utils.isStringNote(this.type, this.mime);
}
isContentAvailable() {
return (
!this.revisionId || // new note which was not encrypted yet
!this.isProtected ||
protectedSessionService.isProtectedSessionAvailable()
);
}
/*
* Note revision content has quite special handling - it's not a separate entity, but a lazily loaded
* part of Revision entity with its own sync. The reason behind this hybrid design is that
* content can be quite large, and it's not necessary to load it / fill memory for any note access even
* if we don't need a content, especially for bulk operations like search.
*
* This is the same approach as is used for Note's content.
*/
getContent(): string | Buffer {
return this._getContent();
}
/**
* @throws Error in case of invalid JSON */
getJsonContent(): {} | null {
const content = this.getContent();
if (!content || typeof content !== "string" || !content.trim()) {
return null;
}
return JSON.parse(content);
}
/** @returns valid object or null if the content cannot be parsed as JSON */
getJsonContentSafely(): {} | null {
try {
return this.getJsonContent();
} catch (e) {
return null;
}
}
setContent(content: string | Buffer, opts: ContentOpts = {}) {
this._setContent(content, opts);
}
getAttachments(): BAttachment[] {
return sql
.getRows<AttachmentRow>(
`
SELECT attachments.*
FROM attachments
WHERE ownerId = ?
AND isDeleted = 0`,
[this.revisionId]
)
.map((row) => new BAttachment(row));
}
getAttachmentById(attachmentId: String, opts: GetByIdOpts = {}): BAttachment | null {
opts.includeContentLength = !!opts.includeContentLength;
const query = opts.includeContentLength
? /*sql*/`SELECT attachments.*, LENGTH(blobs.content) AS contentLength
FROM attachments
JOIN blobs USING (blobId)
WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`
: /*sql*/`SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`;
return sql.getRows<AttachmentRow>(query, [this.revisionId, attachmentId]).map((row) => new BAttachment(row))[0];
}
getAttachmentsByRole(role: string): BAttachment[] {
return sql
.getRows<AttachmentRow>(
`
SELECT attachments.*
FROM attachments
WHERE ownerId = ?
AND role = ?
AND isDeleted = 0
ORDER BY position`,
[this.revisionId, role]
)
.map((row) => new BAttachment(row));
}
getAttachmentByTitle(title: string): BAttachment {
// cannot use SQL to filter by title since it can be encrypted
return this.getAttachments().filter((attachment) => attachment.title === title)[0];
}
/**
* Revisions are not soft-deletable, they are immediately hard-deleted (erased).
*/
eraseRevision() {
if (this.revisionId) {
eraseService.eraseRevisions([this.revisionId]);
}
}
override beforeSaving() {
super.beforeSaving();
this.utcDateModified = dateUtils.utcNowDateTime();
}
getPojo() {
return {
revisionId: this.revisionId,
noteId: this.noteId,
type: this.type,
mime: this.mime,
isProtected: this.isProtected,
title: this.title,
blobId: this.blobId,
dateLastEdited: this.dateLastEdited,
dateCreated: this.dateCreated,
utcDateLastEdited: this.utcDateLastEdited,
utcDateCreated: this.utcDateCreated,
utcDateModified: this.utcDateModified,
content: this.content, // used when retrieving full note revision to frontend
contentLength: this.contentLength
} satisfies RevisionPojo;
}
override getPojoToSave() {
const pojo = this.getPojo();
delete pojo.content; // not getting persisted
delete pojo.contentLength; // not getting persisted
if (pojo.isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) {
pojo.title = protectedSessionService.encrypt(this.title) ?? "";
} else {
// updating protected note outside of protected session means we will keep original ciphertexts
pojo.title = "";
}
}
return pojo;
}
}
import { BRevision } from "@triliumnext/core";
export default BRevision;

View File

@@ -0,0 +1,24 @@
import { ExecutionContext } from "@triliumnext/core";
import clsHooked from "cls-hooked";
export const namespace = clsHooked.createNamespace("trilium");
export default class ClsHookedExecutionContext implements ExecutionContext {
get<T = any>(key: string): T | undefined {
return namespace.get(key);
}
set(key: string, value: any): void {
namespace.set(key, value);
}
reset(): void {
clsHooked.reset();
}
init<T>(callback: () => T): T {
return namespace.runAndReturn(callback);
}
}

View File

@@ -0,0 +1,29 @@
import { CryptoProvider } from "@triliumnext/core";
import crypto from "crypto";
import { generator } from "rand-token";
const randtoken = generator({ source: "crypto" });
export default class NodejsCryptoProvider implements CryptoProvider {
createHash(algorithm: "sha1", content: string | Uint8Array): Uint8Array {
return crypto.createHash(algorithm).update(content).digest();
}
createCipheriv(algorithm: "aes-128-cbc", key: Uint8Array, iv: Uint8Array): { update(data: Uint8Array): Uint8Array; final(): Uint8Array; } {
return crypto.createCipheriv(algorithm, key, iv);
}
createDecipheriv(algorithm: "aes-128-cbc", key: Uint8Array, iv: Uint8Array) {
return crypto.createDecipheriv(algorithm, key, iv);
}
randomBytes(size: number): Uint8Array {
return crypto.randomBytes(size);
}
randomString(length: number): string {
return randtoken.generate(length);
}
}

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