Compare commits

..

333 Commits

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

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

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

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

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

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

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

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

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

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

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

The popup editor creates its own NoteContext("_popup-editor") which is never the activeNtxId in the tab manager — isActive() always returns false, and activeNoteChanged never fires for it. So hasTabBeenActive stays false forever, and the if (!type || !hasTabBeenActive) return guard at NoteDetail.tsx:64 prevents the note type widget from ever loading.
2026-03-10 18:32:31 +02:00
Elian Doran
3a4b080765 Table of contents fixes (#8933) 2026-03-10 18:31:24 +02:00
Elian Doran
41269ef987 chore(deps): update dependency express-rate-limit to v8.3.1 (#8981) 2026-03-10 08:30:06 +02:00
Elian Doran
e521c6a386 fix(deps): update dependency @mermaid-js/layout-elk to v0.2.1 (#8982) 2026-03-10 08:29:41 +02:00
Elian Doran
1c35a557c1 chore(deps): update pnpm to v10.32.0 (#8986) 2026-03-10 08:29:20 +02:00
Elian Doran
99eb8389c5 chore(deps): update typescript-eslint monorepo to v8.57.0 (#8987) 2026-03-10 08:29:03 +02:00
renovate[bot]
c5e560ef5b chore(deps): update typescript-eslint monorepo to v8.57.0 2026-03-10 02:13:50 +00:00
renovate[bot]
a7d7a078b1 chore(deps): update pnpm to v10.32.0 2026-03-10 02:12:47 +00:00
renovate[bot]
a06fa5222f chore(deps): update dependency lightningcss to v1.32.0 2026-03-10 02:12:35 +00:00
renovate[bot]
8d3e40a28a fix(deps): update dependency mind-elixir to v5.9.3 2026-03-10 02:11:34 +00:00
renovate[bot]
8e32f99790 fix(deps): update dependency i18next to v25.8.17 2026-03-10 02:10:34 +00:00
renovate[bot]
57bce62e48 fix(deps): update dependency @mermaid-js/layout-elk to v0.2.1 2026-03-10 02:09:36 +00:00
renovate[bot]
1c873394d5 chore(deps): update dependency express-rate-limit to v8.3.1 2026-03-10 02:08:32 +00:00
JYC333
d652f67364 Translations update from Hosted Weblate (#8977) 2026-03-09 10:43:00 +00:00
JYC333
5e54d098c5 Apply suggestions from code review
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-09 10:23:14 +00:00
JYC333
ec95303c31 Apply suggestion from @gemini-code-assist[bot]
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-09 10:22:31 +00:00
Hosted Weblate
07aafe7e89 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-03-09 11:17:40 +01:00
Giovi
dc7acbb70e Translated using Weblate (Italian)
Currently translated at 100.0% (1676 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2026-03-09 11:17:32 +01:00
JYC333
0dcb8b3ff8 Translations update from Hosted Weblate (#8975) 2026-03-09 10:17:22 +00:00
JYC333
e4ddff01ca Update docs/README-sv.md
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-09 10:13:43 +00:00
JYC333
015c1161d4 Update docs/README-sv.md
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-09 10:11:43 +00:00
Robert Magnusson
ca0c6076c5 Translated using Weblate (Swedish)
Currently translated at 5.4% (21 of 387 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/sv/
2026-03-09 05:47:59 +00:00
Robert Magnusson
80a02f88be Translated using Weblate (Swedish)
Currently translated at 1.3% (22 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/sv/
2026-03-09 05:47:58 +00:00
Robert Magnusson
430833bedb Translated using Weblate (Swedish)
Currently translated at 13.2% (21 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/sv/
2026-03-09 05:47:57 +00:00
Hosted Weblate
dc80d83964 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-03-09 05:47:56 +00:00
Elian Doran
5f7ade45f4 fix(deps): update dependency katex to v0.16.38 (#8969) 2026-03-09 07:47:35 +02:00
Elian Doran
8b36a7ab1e Spreadsheet experiment v0.5 (#8966) 2026-03-09 07:47:08 +02:00
Elian Doran
fd18276693 fix(deps): update dependency @preact/signals to v2.8.2 (#8968) 2026-03-09 07:46:47 +02:00
Elian Doran
0becfc16ba chore(deps): update pnpm to v10.31.0 (#8971) 2026-03-09 07:46:02 +02:00
renovate[bot]
d480d1f6ba chore(deps): update pnpm to v10.31.0 2026-03-09 01:36:51 +00:00
renovate[bot]
f5c9a71ba0 fix(deps): update dependency katex to v0.16.38 2026-03-09 01:35:41 +00:00
renovate[bot]
c177a8a464 fix(deps): update dependency @preact/signals to v2.8.2 2026-03-09 01:34:42 +00:00
Elian Doran
c826564c9e chore(spreadsheet): address requested changes 2026-03-08 23:25:47 +02:00
Elian Doran
ccb13fa6b9 fix(commons): typecheck 2026-03-08 23:19:23 +02:00
Elian Doran
69e374138f fix(spreadsheet): missing some CSS imports 2026-03-08 23:07:48 +02:00
Elian Doran
3156b2cb59 feat(spreadsheet): enable conditional formatting 2026-03-08 23:02:54 +02:00
Elian Doran
d6217ffed4 feat(spreadsheet): enable data validation 2026-03-08 22:59:41 +02:00
Elian Doran
fc90c6af9d feat(spreadsheet): enable sorting 2026-03-08 22:56:11 +02:00
Elian Doran
a1118419ec feat(spreadsheet): enable filtering 2026-03-08 22:53:04 +02:00
Elian Doran
8599785ee8 refactor(spreadsheet): use multiple modules 2026-03-08 22:39:43 +02:00
Elian Doran
99ba192a44 feat(spreadsheet): allow triggering find/replace from context menu 2026-03-08 22:35:08 +02:00
Elian Doran
b86d3587ac feat(spreadsheet): basic integration of find/replace 2026-03-08 22:24:03 +02:00
Elian Doran
b2a0baf56a fix(spreadsheet): jumping when editing in another split 2026-03-08 22:15:29 +02:00
Elian Doran
22f37817e5 fix(spreadsheet): fix The column width is less than 0 when switching tabs 2026-03-08 22:01:45 +02:00
Elian Doran
6b4fe03625 fix(spreadsheet): mitigate The column width is less than 0, need to adjust page width to make it great than 0 when changing an inactive tab 2026-03-08 21:57:26 +02:00
Elian Doran
f44b47ec23 fix(client): tabs still rendering in the background 2026-03-08 21:48:45 +02:00
Elian Doran
8d667e838a feat(spreadsheet): hide cell protection mechanism 2026-03-08 21:28:12 +02:00
Elian Doran
f32385de2e feat(spreadsheet): hide toolbars while in read-only 2026-03-08 21:24:24 +02:00
Elian Doran
90796fc4fa feat(spreadsheet): basic read-only support 2026-03-08 21:09:11 +02:00
Elian Doran
4960c49cb2 feat(spreadsheet): add note plugin 2026-03-08 20:39:07 +02:00
Elian Doran
b112e8b56b feat(spreadsheet): basic support for note revision using image 2026-03-08 20:30:24 +02:00
Elian Doran
83095130f6 feat(spreadsheet): basic rendering as HTML for share 2026-03-08 20:04:14 +02:00
Elian Doran
d005c0ef2d feat(spreadsheet): basic note list preview using SVG 2026-03-08 19:49:53 +02:00
Elian Doran
c135578626 fix(spreadsheet): not focusing on tab switch 2026-03-08 13:05:47 +02:00
Elian Doran
9a6e20029e fix(client): all tabs loaded in the background 2026-03-08 12:59:57 +02:00
Elian Doran
39bd4ccea1 Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-03-08 12:59:44 +02:00
Elian Doran
aac4774326 Merge remote-tracking branch 'origin/main' into feature/toc_improvements 2026-03-08 12:20:53 +02:00
Elian Doran
ea7aac2030 v0.102.1 (#8961) 2026-03-08 12:13:12 +02:00
Elian Doran
e7f98f08d0 Merge remote-tracking branch 'origin/main' into stable 2026-03-08 12:12:52 +02:00
Elian Doran
8ac9daa5d3 chore(release): prepare for v0.102.1 2026-03-08 10:43:59 +02:00
Elian Doran
0b506c6327 chore(pdfjs): bump pdfjs viewer version 2026-03-08 10:41:21 +02:00
Elian Doran
d2b62540ec fix(ci): migrate all the jank docker ci to use crane instead (#8869) 2026-03-08 10:37:49 +02:00
Elian Doran
64418c7fec docs(release): prepare for v0.102.1 2026-03-08 10:36:06 +02:00
Elian Doran
8c1a58e64f fix(pdf): cache buster not working in all circumstances 2026-03-08 10:29:57 +02:00
Adorian Doran
b27fd31c1f style/pdf viewer: fix some layout issues in toolbar 2026-03-08 10:25:05 +02:00
Elian Doran
f18a531924 fix(mindmap): crashing on auto-switch to dark theme (closes #8879) 2026-03-08 10:22:21 +02:00
Elian Doran
3cabb4b661 fix(pdf): not accessible on Nginx Proxy Manager with block common exploits (closes #8877) 2026-03-08 09:30:27 +02:00
Elian Doran
5c88b1c6b8 chore(server): add infrastructure for running Nginx Proxy Manager 2026-03-08 09:01:47 +02:00
Elian Doran
c2adc43780 chore(deps): update dependency @types/multer to v2.1.0 (#8921) 2026-03-07 23:17:45 +02:00
Elian Doran
7eaa5352ba Translations update from Hosted Weblate (#8956) 2026-03-07 23:17:15 +02:00
Patric Siesing
17e3e3187b Translated using Weblate (Swedish)
Currently translated at 4.6% (18 of 387 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/sv/
2026-03-07 22:15:57 +01:00
Robert Magnusson
2ad7cd3a49 Translated using Weblate (Swedish)
Currently translated at 4.6% (18 of 387 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/sv/
2026-03-07 22:15:56 +01:00
Patric Siesing
39aa8d61c2 Translated using Weblate (Swedish)
Currently translated at 11.3% (18 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/sv/
2026-03-07 22:15:55 +01:00
Robert Magnusson
1a3ea977b7 Translated using Weblate (Swedish)
Currently translated at 11.3% (18 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/sv/
2026-03-07 22:15:54 +01:00
Robert Magnusson
4cd8f9a1e6 Translated using Weblate (Swedish)
Currently translated at 1.0% (18 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/sv/
2026-03-07 22:15:53 +01:00
Hosted Weblate
87ce6d1231 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-03-07 22:15:52 +01:00
Elian Doran
8fdbeacf77 fix(deps): update dependency katex to v0.16.37 (#8935) 2026-03-07 23:15:44 +02:00
Elian Doran
f4f775a1c9 chore(deps): update dependency @smithy/middleware-retry to v4.4.40 (#8945) 2026-03-07 23:13:13 +02:00
Elian Doran
fe1154cb2d chore(deps): update dependency @types/sanitize-html to v2.16.1 (#8946) 2026-03-07 23:12:53 +02:00
Elian Doran
638f479ff3 chore(deps): update dependency eslint to v10.0.3 (#8947) 2026-03-07 23:12:27 +02:00
Elian Doran
70436bdb04 fix(deps): update dependency react-i18next to v16.5.6 (#8949) 2026-03-07 23:12:05 +02:00
Elian Doran
575ecaae07 fix(deps): update dependency tabulator-tables to v6.4.0 (#8950) 2026-03-07 23:11:37 +02:00
Elian Doran
d277e6db94 chore(deps): update actions/upload-artifact action to v7 (#8951) 2026-03-07 08:29:42 +02:00
renovate[bot]
25efcd12d0 chore(deps): update actions/upload-artifact action to v7 2026-03-07 02:18:00 +00:00
renovate[bot]
10129321be fix(deps): update dependency tabulator-tables to v6.4.0 2026-03-07 02:17:54 +00:00
renovate[bot]
72710a8f6b chore(deps): update dependency @types/multer to v2.1.0 2026-03-07 02:17:10 +00:00
renovate[bot]
6a7c5c04d8 fix(deps): update dependency react-i18next to v16.5.6 2026-03-07 02:16:21 +00:00
renovate[bot]
7f32fe5ef7 fix(deps): update dependency eslint-linter-browserify to v10.0.3 2026-03-07 02:15:20 +00:00
renovate[bot]
5d89591dea chore(deps): update dependency eslint to v10.0.3 2026-03-07 02:14:21 +00:00
renovate[bot]
a88bf5a87b chore(deps): update dependency @types/sanitize-html to v2.16.1 2026-03-07 02:13:18 +00:00
renovate[bot]
bbe5d3506e chore(deps): update dependency @smithy/middleware-retry to v4.4.40 2026-03-07 02:12:12 +00:00
renovate[bot]
c2993d4e7d fix(deps): update dependency katex to v0.16.37 2026-03-06 21:42:06 +00:00
Elian Doran
17ba479182 chore(deps): update dependency @smithy/middleware-retry to v4.4.39 (#8906) 2026-03-06 19:01:41 +02:00
Elian Doran
a465014bbe fix(deps): update codemirror (#8885) 2026-03-06 19:01:13 +02:00
Elian Doran
5dfe253ef6 chore(deps): update imjasonh/setup-crane action to v0.5 (#8910) 2026-03-06 19:00:14 +02:00
Elian Doran
ae7ca6021f Translations update from Hosted Weblate (#8919) 2026-03-06 18:57:49 +02:00
noobhjy
c389697acd Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1676 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-03-06 16:50:15 +00:00
Aleksandr Reid
c13c3e0f4a Translated using Weblate (Russian)
Currently translated at 100.0% (1676 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ru/
2026-03-06 16:50:14 +00:00
Ulices
82c042d045 Translated using Weblate (Spanish)
Currently translated at 100.0% (1676 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-03-06 16:50:14 +00:00
Aleksandr Reid
9145ba1690 Translated using Weblate (Russian)
Currently translated at 100.0% (387 of 387 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ru/
2026-03-06 16:50:13 +00:00
Marcel
d60653ee17 Translated using Weblate (German)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/de/
2026-03-06 16:50:12 +00:00
Marcel
dae8613b4e Translated using Weblate (German)
Currently translated at 100.0% (1676 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-03-06 16:50:12 +00:00
Aindriú Mac Giolla Eoin
2f8e2c40be Translated using Weblate (Irish)
Currently translated at 100.0% (1676 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-03-06 16:50:11 +00:00
Francis C.
d85225a0dc Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1676 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-03-06 16:50:11 +00:00
green
0cb66df2b2 Translated using Weblate (Japanese)
Currently translated at 100.0% (1676 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-03-06 16:50:10 +00:00
Aleksandr Reid
92e0578cb6 Translated using Weblate (Russian)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ru/
2026-03-06 16:50:09 +00:00
Elian Doran
2eee06786e chore(deps): update dependency lint-staged to v16.3.2 (#8908) 2026-03-06 18:50:00 +02:00
Elian Doran
19053dcb3b fix(deps): update dependency mind-elixir to v5.9.2 (#8909) 2026-03-06 18:49:24 +02:00
JYC333
e10c30c59f fix(deps): update dependency i18next to v25.8.14 (#8922) 2026-03-06 14:12:55 +00:00
Elian Doran
c356159664 fix(deps): update dependency marked to v17.0.4 (#8923) 2026-03-06 15:45:12 +02:00
Elian Doran
579be68ca1 chore(deps): update dependency electron to v40.8.0 (#8924) 2026-03-06 15:28:24 +02:00
Elian Doran
a6326a682e chore(deps): update dependency @types/node to v24.12.0 (#8934) 2026-03-06 15:27:19 +02:00
renovate[bot]
4595a3a5dd fix(deps): update dependency i18next to v25.8.14 2026-03-06 12:42:27 +00:00
renovate[bot]
ee21185e64 chore(deps): update dependency electron to v40.8.0 2026-03-06 12:39:17 +00:00
Elian Doran
6d0676c37d chore(deps): update docker/login-action action to v4 (#8925) 2026-03-06 14:38:39 +02:00
Elian Doran
1d4768a581 chore(deps): update docker/setup-qemu-action action to v4 (#8926) 2026-03-06 14:38:14 +02:00
Elian Doran
d086bb7fcb chore(deps): update dependency multer to v2.1.1 [security] (#8929) 2026-03-06 14:37:33 +02:00
Elian Doran
2607c4a32e fix(deps): update dependency react-i18next to v16.5.5 (#8936) 2026-03-06 14:37:12 +02:00
Elian Doran
624333a2ef chore(deps): update dependency express-rate-limit to v8.3.0 (#8937) 2026-03-06 14:36:43 +02:00
Elian Doran
d4acb37f21 chore(deps): update dependency ejs to v5 (#8938) 2026-03-06 14:36:24 +02:00
Elian Doran
6c1a1e9812 chore(deps): update docker/build-push-action action to v7 (#8939) 2026-03-06 13:19:16 +02:00
Elian Doran
9a13641f9b chore(deps): update docker/metadata-action action to v6 (#8940) 2026-03-06 13:18:33 +02:00
renovate[bot]
699e0624c9 chore(deps): update docker/setup-qemu-action action to v4 2026-03-06 06:58:29 +00:00
renovate[bot]
47ceb0d4d2 chore(deps): update docker/metadata-action action to v6 2026-03-06 06:58:27 +00:00
renovate[bot]
15c42f4a09 chore(deps): update docker/login-action action to v4 2026-03-06 06:58:24 +00:00
renovate[bot]
bf8401bb26 chore(deps): update docker/build-push-action action to v7 2026-03-06 06:58:21 +00:00
renovate[bot]
f234433c63 chore(deps): update dependency ejs to v5 2026-03-06 06:58:18 +00:00
renovate[bot]
1b70101123 chore(deps): update imjasonh/setup-crane action to v0.5 2026-03-06 06:57:50 +00:00
renovate[bot]
d610c63c28 chore(deps): update dependency express-rate-limit to v8.3.0 2026-03-06 06:57:17 +00:00
renovate[bot]
5e820a407f chore(deps): update dependency @types/node to v24.12.0 2026-03-06 06:56:18 +00:00
renovate[bot]
62610979b7 fix(deps): update dependency react-i18next to v16.5.5 2026-03-06 06:55:50 +00:00
renovate[bot]
700e99e854 fix(deps): update dependency mind-elixir to v5.9.2 2026-03-06 06:55:19 +00:00
renovate[bot]
7767116b3d fix(deps): update dependency marked to v17.0.4 2026-03-06 06:54:40 +00:00
renovate[bot]
0206e8247b fix(deps): update codemirror 2026-03-06 06:52:48 +00:00
renovate[bot]
5476fe3df9 chore(deps): update dependency lint-staged to v16.3.2 2026-03-06 06:50:46 +00:00
renovate[bot]
d9a4581d37 chore(deps): update dependency @smithy/middleware-retry to v4.4.39 2026-03-06 06:49:46 +00:00
renovate[bot]
8d9c888481 chore(deps): update dependency multer to v2.1.1 [security] 2026-03-06 06:46:38 +00:00
Elian Doran
11e4b672d1 Fix CI test issues (#8932) 2026-03-06 08:43:51 +02:00
Elian Doran
bace3daadc Update apps/server/src/routes/session_parser.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-06 08:43:31 +02:00
Elian Doran
dee5380e60 fix(ci): sequential tests ended up run in parallel 2026-03-06 08:20:17 +02:00
Elian Doran
bc6a6fd860 Revert "test(server): reset ws module"
This reverts commit 0212398815.
2026-03-05 23:44:24 +02:00
Elian Doran
e928337fe9 test(server): adjust timeout 2026-03-05 23:40:43 +02:00
Elian Doran
432f86ea4b Revert "test(server): switch to forks with 2 max workers"
This reverts commit 4ac22678df.
2026-03-05 23:37:28 +02:00
Elian Doran
5d2daecee0 test(server): switch to forks with 6 max workers 2026-03-05 23:35:15 +02:00
Elian Doran
7c8eb311af test(server): switch to forks with 3 max workers 2026-03-05 23:31:54 +02:00
Elian Doran
4ac22678df test(server): switch to forks with 2 max workers 2026-03-05 23:25:45 +02:00
Elian Doran
5057c02176 test(server): fix errors due to database already existing 2026-03-05 22:52:26 +02:00
Elian Doran
d301e56216 refactor(server): don't set up other timers on module init 2026-03-05 22:19:04 +02:00
Elian Doran
3c22ab8c9c refactor(server): don't set up session timer on module init 2026-03-05 22:17:19 +02:00
Elian Doran
0212398815 test(server): reset ws module 2026-03-05 22:14:34 +02:00
Elian Doran
db0c515bad test(server): fake timers not restored 2026-03-05 22:11:51 +02:00
Elian Doran
9b4f8c5003 feat(ci/client): HTML output 2026-03-05 22:07:11 +02:00
Elian Doran
85d8c4c8fa feat(ci/server): HTML output 2026-03-05 22:06:46 +02:00
Elian Doran
5afab6938a test(server): reduce max workers to 1 2026-03-05 21:54:30 +02:00
Elian Doran
a437169ad5 test(server): increase hook timeout 2026-03-05 21:20:12 +02:00
Elian Doran
f632d3aeb6 Merge remote-tracking branch 'origin/main' into fix/ci 2026-03-05 21:14:57 +02:00
Elian Doran
513fffcb1a ci(dev): escape test filter 2026-03-05 21:14:21 +02:00
Elian Doran
d3337eab9c Merge branch 'main' into feature/toc_improvements 2026-03-05 21:05:17 +02:00
Elian Doran
8128a8192a refactor(ckeditor): address requested changes 2026-03-05 19:28:52 +02:00
Elian Doran
c80bb9657c fix(mindmap): crashing on auto-switch to dark theme 2026-03-05 19:25:07 +02:00
Elian Doran
65514a6fd7 fix(toc): title is extracted before changes are made 2026-03-05 19:08:56 +02:00
Elian Doran
93a7f8c711 fix(toc): not reacting to attribute changes in CKEditor 2026-03-05 19:03:32 +02:00
Elian Doran
0ca179f990 ci(test): quote command 2026-03-05 18:40:24 +02:00
Elian Doran
9d104015f3 ci(test): quote command 2026-03-05 18:30:08 +02:00
Elian Doran
2c4cf2dcf1 ci(test): separate running of heavy tests to avoid OOM issues 2026-03-05 18:28:27 +02:00
Elian Doran
d2e0124962 chore(deps): update dependency fs-extra to v11.3.4 (#8907) 2026-03-05 16:51:11 +02:00
renovate[bot]
cd59c75c04 chore(deps): update dependency fs-extra to v11.3.4 2026-03-04 01:13:39 +00:00
Elian Doran
caa9143591 chore(deps): update dependency happy-dom to v20.8.3 (#8887) 2026-03-03 22:15:58 +02:00
renovate[bot]
7e53810c02 chore(deps): update dependency happy-dom to v20.8.3 2026-03-03 19:42:03 +00:00
Elian Doran
12efa8dc0b chore(deps): update dependency eslint-plugin-playwright to v2.9.0 (#8886) 2026-03-03 21:21:22 +02:00
Elian Doran
4d0ccac7b5 fix(deps): update dependency node-html-parser to v7.1.0 (#8888) 2026-03-03 21:20:21 +02:00
Elian Doran
8b023a55d0 chore(deps): update dependency copy-webpack-plugin to v14 (#8889) 2026-03-03 21:10:22 +02:00
Elian Doran
b4df5fcbd9 chore(deps): update dependency rollup-plugin-webpack-stats to v3 (#8890) 2026-03-03 20:58:24 +02:00
renovate[bot]
6fbe5718e9 chore(deps): update dependency rollup-plugin-webpack-stats to v3 2026-03-03 18:54:56 +00:00
Elian Doran
908bafca63 Translations update from Hosted Weblate (#8901) 2026-03-03 20:54:00 +02:00
Elian Doran
d7313efd67 fix(ci): migrate all the jank docker ci to use crane instead (#8869) 2026-03-03 20:48:42 +02:00
Микола Копитін
a51e15c9b8 Translated using Weblate (Ukrainian)
Currently translated at 90.0% (1508 of 1675 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/uk/
2026-03-03 18:44:20 +00:00
Hosted Weblate
37e9c7d639 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-03-03 18:44:20 +00:00
Elian Doran
2d00ac4dfb Univer Sheets v0 (#8902) 2026-03-03 20:44:06 +02:00
Elian Doran
6aec7eae00 chore(server): increase sync version to avoid data loss due to unsupported note type 2026-03-03 20:25:33 +02:00
Elian Doran
6bfbc2d35e chore(spreadsheet): use better clean up mechanism 2026-03-03 20:12:54 +02:00
Elian Doran
2ffc854ce6 chore(spreadsheet): mark note type as beta 2026-03-03 19:59:12 +02:00
Elian Doran
ddd4a374e4 chore(client): fix some whitespace issues 2026-03-03 19:52:15 +02:00
Elian Doran
0d6e2fc00f chore(client): fix typecheck 2026-03-03 19:41:51 +02:00
Elian Doran
366a8e8726 fix(spreadsheet): persistence hook on every render 2026-03-03 19:24:04 +02:00
Elian Doran
7f0aa0697a fix(spreadsheet): error due to duplicate unit IDs 2026-03-03 19:20:25 +02:00
Elian Doran
d123ce33b8 feat(spreadsheet): restore from JSON 2026-03-03 19:09:33 +02:00
Elian Doran
55588f5962 feat(spreadsheet): restore from JSON 2026-03-03 19:05:01 +02:00
Elian Doran
f32130d5c2 feat(spreadsheet): allow source to be viewed 2026-03-03 19:00:23 +02:00
Elian Doran
03f4ff9e7c feat(spreadsheet): save spreadsheet to JSON 2026-03-03 19:00:14 +02:00
Elian Doran
6de78c7154 refactor(spreadsheet): make use of hooks 2026-03-03 18:48:45 +02:00
Elian Doran
d331e418d4 feat(spreadsheet): support dark mode 2026-03-03 18:42:26 +02:00
Elian Doran
4ace74bcb8 feat(spreadsheet): make full-width 2026-03-03 18:36:17 +02:00
Elian Doran
1d4a336256 feat(spreadsheet): integrate spreadsheet with full-height 2026-03-03 18:34:46 +02:00
Elian Doran
ee6c192ab9 chore(spreadsheet): create new note type 2026-03-03 18:24:55 +02:00
Elian Doran
b220bdce9c fix(note_list): affected by floating images (closes #8899) 2026-03-03 18:14:43 +02:00
Elian Doran
4d86c6c4f1 feat(import/single): trim extension for audio files + default icon 2026-03-03 16:19:44 +02:00
Elian Doran
4fd68bf12d feat(import/single): trim extension for video files 2026-03-03 14:29:18 +02:00
Elian Doran
3ffe34964f feat(notes): add default icon for videos 2026-03-03 14:26:45 +02:00
Elian Doran
faaf26c174 fix(quick_edit): save indicator not shown 2026-03-03 14:19:24 +02:00
Elian Doran
f9c7518db2 fix(spaced_update): triggering events too often while typing 2026-03-03 14:19:24 +02:00
Elian Doran
8357c2a39c chore(pdfjs): version not updated for releases 2026-03-03 14:19:24 +02:00
renovate[bot]
793dcee562 chore(deps): update dependency copy-webpack-plugin to v14 2026-03-03 02:02:49 +00:00
renovate[bot]
00368fc131 fix(deps): update dependency node-html-parser to v7.1.0 2026-03-03 02:01:49 +00:00
renovate[bot]
f81b686f41 chore(deps): update dependency eslint-plugin-playwright to v2.9.0 2026-03-03 02:00:00 +00:00
Elian Doran
4c5aada5d3 chore(deps): update dependency @types/express-serve-static-core to v5.1.1 (#8346) 2026-03-02 22:42:10 +02:00
Elian Doran
05551cec9e chore(deps): update dependency sax to v1.5.0 (#8875) 2026-03-02 22:41:20 +02:00
Elian Doran
6300a8c8d1 chore(deps): update dependency @redocly/cli to v2.20.2 (#8853) 2026-03-02 22:34:22 +02:00
Elian Doran
ca4d15727d Merge branch 'main' into renovate/express-serve-static-core-5.x 2026-03-02 22:30:43 +02:00
renovate[bot]
2fe076086e chore(deps): update dependency sax to v1.5.0 2026-03-02 20:25:56 +00:00
Elian Doran
56b65ddfae Translations update from Hosted Weblate (#8870) 2026-03-02 22:22:37 +02:00
Hasan Kara
fcf6673825 Translated using Weblate (Turkish)
Currently translated at 16.3% (19 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/tr/
2026-03-02 20:54:15 +01:00
Hasan Kara
9eda264f52 Translated using Weblate (Turkish)
Currently translated at 5.1% (20 of 387 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/tr/
2026-03-02 20:54:14 +01:00
Hasan Kara
fe1270c679 Translated using Weblate (Turkish)
Currently translated at 4.2% (71 of 1675 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/tr/
2026-03-02 20:54:13 +01:00
Hasan Kara
679e1ac678 Translated using Weblate (Turkish)
Currently translated at 12.0% (19 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/tr/
2026-03-02 20:54:12 +01:00
ibs-allaow
e309ff2d17 Translated using Weblate (Arabic)
Currently translated at 100.0% (116 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/ar/
2026-03-02 20:54:12 +01:00
Francis C.
c910335155 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1675 of 1675 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-03-02 20:54:11 +01:00
Yatrik Patel
5606cde506 Translated using Weblate (Hindi)
Currently translated at 100.0% (1675 of 1675 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hi/
2026-03-02 20:54:10 +01:00
Yatrik Patel
0e2f4f4e13 Translated using Weblate (Hindi)
Currently translated at 38.6% (61 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-03-02 20:54:09 +01:00
renovate[bot]
1f6c6f2acd chore(deps): update dependency @redocly/cli to v2.20.2 2026-03-02 18:09:58 +00:00
Elian Doran
37d2e9f14b fix(deps): update dependency globals to v17.4.0 (#8876) 2026-03-02 16:44:44 +02:00
Adorian Doran
0fcf30a3b8 Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-03-02 11:22:08 +02:00
Adorian Doran
8712e7dd16 style/pdf viewer: fix some layout issues in toolbar 2026-03-02 11:21:47 +02:00
renovate[bot]
2ee4e9cc14 fix(deps): update dependency globals to v17.4.0 2026-03-02 01:18:53 +00:00
perfectra1n
b257b75be2 fix(ci): remove fragile jq where possible 2026-03-01 13:49:45 -08:00
perfectra1n
2de2709420 fix(ci): migrate all the jank docker ci to use crane instead 2026-03-01 13:47:18 -08:00
Elian Doran
34ca7912fc Merge remote-tracking branch 'origin/main' into renovate/express-serve-static-core-5.x 2026-02-28 19:11:57 +02:00
Elian Doran
dc3de5bf36 chore(server): address requested changes 2026-02-27 00:05:54 +02:00
Elian Doran
1041bf70e1 test(express-partial-content): fix type errors 2026-02-26 21:11:22 +02:00
Elian Doran
0c6326b678 refactor(server): use strong typing for routes 2026-02-26 21:08:54 +02:00
renovate[bot]
fd805a5279 chore(deps): update dependency @types/express-serve-static-core to v5.1.1 2026-02-26 18:18:55 +00:00
196 changed files with 9030 additions and 2990 deletions

View File

@@ -37,8 +37,35 @@ jobs:
- name: Typecheck
run: pnpm typecheck
- name: Run the unit tests
run: pnpm run test:all
- name: Run the client-side tests
run: pnpm run --filter=client test
- name: Upload client test report
uses: actions/upload-artifact@v7
if: always()
with:
name: client-test-report
path: apps/client/test-output/vitest/html/
retention-days: 30
- name: Run the server-side tests
run: pnpm run --filter=server test
- name: Upload server test report
uses: actions/upload-artifact@v7
if: always()
with:
name: server-test-report
path: apps/server/test-output/vitest/html/
retention-days: 30
- name: Run CKEditor e2e tests
run: |
pnpm run --filter=ckeditor5-mermaid test
pnpm run --filter=ckeditor5-math test
- name: Run the rest of the tests
run: pnpm run --filter=\!client --filter=\!server --filter=\!ckeditor5-mermaid --filter=\!ckeditor5-math test
build_docker:
name: Build Docker image
@@ -63,7 +90,7 @@ jobs:
- name: Trigger server build
run: pnpm run server:build
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
- uses: docker/build-push-action@v7
with:
context: apps/server
cache-from: type=gha
@@ -100,7 +127,7 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Build and export to Docker
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: apps/server
file: apps/server/${{ matrix.dockerfile }}

View File

@@ -59,7 +59,7 @@ jobs:
run: pnpm run server:build
- name: Build and export to Docker
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: apps/server
file: apps/server/${{ matrix.dockerfile }}
@@ -164,11 +164,9 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: |
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}
images: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
@@ -177,36 +175,27 @@ jobs:
latest=false
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.GHCR_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: apps/server
file: apps/server/${{ matrix.dockerfile }}
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: |
type=image,name=${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
type=image,name=${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
outputs: type=image,name=${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest
run: |
@@ -239,75 +228,86 @@ jobs:
- name: Set TEST_TAG to lowercase
run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=false
- name: Set up crane
uses: imjasonh/setup-crane@v0.5
- name: Login to GHCR
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.GHCR_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create manifest list and push
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha
flavor: |
latest=false
- name: Verify digests exist on GHCR
working-directory: /tmp/digests
run: |
# Extract the branch or tag name from the ref
REF_NAME=$(echo "${GITHUB_REF}" | sed 's/refs\/heads\///' | sed 's/refs\/tags\///')
echo "Verifying all digests are available on GHCR..."
for DIGEST_FILE in *; do
DIGEST="sha256:${DIGEST_FILE}"
echo -n " ${DIGEST}: "
crane manifest "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}" > /dev/null
echo "OK"
done
# Create and push the manifest list with both the branch/tag name and the commit SHA
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
-t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME} \
$(printf '${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
- name: Create and push multi-arch manifest
working-directory: /tmp/digests
run: |
GHCR_IMAGE="${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}"
DOCKERHUB_IMAGE="${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}"
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME} \
$(printf '${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
# Build -m flags for crane index append from digest files
MANIFEST_ARGS=""
for d in *; do
MANIFEST_ARGS="${MANIFEST_ARGS} -m ${GHCR_IMAGE}@sha256:${d}"
done
# If the ref is a tag, also tag the image as stable as this is part of a 'release'
# and only go in the `if` if there is NOT a `-` in the tag's name, due to tagging of `-alpha`, `-beta`, etc...
# Create multi-arch manifest for each tag from metadata, plus copy to DockerHub
while IFS= read -r TAG; do
echo "Creating manifest: ${TAG}"
crane index append ${MANIFEST_ARGS} -t "${TAG}"
SUFFIX="${TAG#*:}"
echo "Copying to DockerHub: ${DOCKERHUB_IMAGE}:${SUFFIX}"
crane copy "${TAG}" "${DOCKERHUB_IMAGE}:${SUFFIX}"
done <<< "${{ steps.meta.outputs.tags }}"
# For stable releases (tags without hyphens), also create stable + latest
REF_NAME="${GITHUB_REF#refs/tags/}"
if [[ "${GITHUB_REF}" == refs/tags/* && ! "${REF_NAME}" =~ - ]]; then
# First create stable tags
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
-t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:stable \
$(printf '${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:stable \
$(printf '${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
# Small delay to ensure stable tag is fully propagated
sleep 5
# Now update latest tags
docker buildx imagetools create \
-t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:stable
docker buildx imagetools create \
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:stable
echo "Creating stable tags..."
crane index append ${MANIFEST_ARGS} -t "${GHCR_IMAGE}:stable"
crane copy "${GHCR_IMAGE}:stable" "${DOCKERHUB_IMAGE}:stable"
echo "Creating latest tags..."
crane copy "${GHCR_IMAGE}:stable" "${GHCR_IMAGE}:latest"
crane copy "${GHCR_IMAGE}:latest" "${DOCKERHUB_IMAGE}:latest"
fi
- name: Inspect image
- name: Inspect manifests
run: |
docker buildx imagetools inspect ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
docker buildx imagetools inspect ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
REF_NAME="${GITHUB_REF#refs/heads/}"
REF_NAME="${REF_NAME#refs/tags/}"
echo "=== GHCR ==="
crane manifest "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME}"
echo ""
echo "=== DockerHub ==="
crane manifest "${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME}"

View File

@@ -14,11 +14,11 @@
"keywords": [],
"author": "Elian Doran <contact@eliandoran.me>",
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.30.3",
"packageManager": "pnpm@10.32.0",
"devDependencies": {
"@redocly/cli": "2.19.2",
"@redocly/cli": "2.20.2",
"archiver": "7.0.1",
"fs-extra": "11.3.3",
"fs-extra": "11.3.4",
"js-yaml": "4.1.1",
"react": "19.2.4",
"react-dom": "19.2.4",

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/client",
"version": "0.102.0",
"version": "0.102.1",
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
"private": true,
"license": "AGPL-3.0-only",
@@ -25,17 +25,25 @@
"@fullcalendar/rrule": "6.1.20",
"@fullcalendar/timegrid": "6.1.20",
"@maplibre/maplibre-gl-leaflet": "0.1.3",
"@mermaid-js/layout-elk": "0.2.0",
"@mermaid-js/layout-elk": "0.2.1",
"@mind-elixir/node-menu": "5.0.1",
"@popperjs/core": "2.11.8",
"@preact/signals": "2.8.1",
"@preact/signals": "2.8.2",
"@triliumnext/ckeditor5": "workspace:*",
"@triliumnext/codemirror": "workspace:*",
"@triliumnext/commons": "workspace:*",
"@triliumnext/highlightjs": "workspace:*",
"@triliumnext/share-theme": "workspace:*",
"@triliumnext/split.js": "workspace:*",
"@zumer/snapdom": "2.0.2",
"@univerjs/preset-sheets-conditional-formatting": "0.16.1",
"@univerjs/preset-sheets-core": "0.16.1",
"@univerjs/preset-sheets-data-validation": "0.16.1",
"@univerjs/preset-sheets-filter": "0.16.1",
"@univerjs/preset-sheets-find-replace": "0.16.1",
"@univerjs/preset-sheets-note": "0.16.1",
"@univerjs/preset-sheets-sort": "0.16.1",
"@univerjs/presets": "0.16.1",
"@zumer/snapdom": "2.1.0",
"autocomplete.js": "0.38.1",
"bootstrap": "5.3.8",
"boxicons": "2.1.4",
@@ -44,29 +52,29 @@
"debounce": "3.0.0",
"draggabilly": "3.0.0",
"force-graph": "1.51.1",
"globals": "17.3.0",
"i18next": "25.8.13",
"globals": "17.4.0",
"i18next": "25.8.17",
"i18next-http-backend": "3.0.2",
"jquery": "4.0.0",
"jquery.fancytree": "2.38.5",
"jsplumb": "2.15.6",
"katex": "0.16.33",
"katex": "0.16.38",
"knockout": "3.5.1",
"leaflet": "1.9.4",
"leaflet-gpx": "2.2.0",
"mark.js": "8.11.1",
"marked": "17.0.3",
"marked": "17.0.4",
"mermaid": "11.12.3",
"mind-elixir": "5.9.1",
"mind-elixir": "5.9.3",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.28.4",
"react-i18next": "16.5.4",
"preact": "10.29.0",
"react-i18next": "16.5.6",
"react-window": "2.2.7",
"reveal.js": "5.2.1",
"rrule": "2.8.1",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",
"tabulator-tables": "6.4.0",
"vanilla-js-wheel-zoom": "9.0.4"
},
"devDependencies": {
@@ -79,9 +87,9 @@
"@types/mark.js": "8.11.12",
"@types/reveal.js": "5.2.2",
"@types/tabulator-tables": "6.3.1",
"copy-webpack-plugin": "13.0.1",
"happy-dom": "20.7.0",
"lightningcss": "1.31.1",
"copy-webpack-plugin": "14.0.0",
"happy-dom": "20.8.3",
"lightningcss": "1.32.0",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "3.2.0"
}

View File

@@ -18,7 +18,7 @@ const RELATION = "relation";
* end user. Those types should be used only for checking against, they are
* not for direct use.
*/
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap";
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "spreadsheet";
export interface NotePathRecord {
isArchived: boolean;

View File

@@ -54,7 +54,7 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
await renderText(entity, $renderedContent, options);
} else if (type === "code") {
await renderCode(entity, $renderedContent);
} else if (["image", "canvas", "mindMap"].includes(type)) {
} else if (["image", "canvas", "mindMap", "spreadsheet"].includes(type)) {
renderImage(entity, $renderedContent, options);
} else if (!options.tooltip && ["file", "pdf", "audio", "video"].includes(type)) {
await renderFile(entity, type, $renderedContent);

View File

@@ -1,4 +1,5 @@
import { NoteType } from "@triliumnext/commons";
import FNote from "../entities/fnote";
import { ViewTypeOptions } from "../widgets/collections/interface";
@@ -17,7 +18,8 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
render: null,
search: null,
text: null,
webView: null
webView: null,
spreadsheet: null
};
export const byBookType: Record<ViewTypeOptions, string | null> = {
@@ -38,6 +40,6 @@ export function getHelpUrlForNote(note: FNote | null | undefined) {
} else if (note?.hasLabel("textSnippet")) {
return "pwc194wlRzcH";
} else if (note && note.type === "book") {
return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""]
return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""];
}
}

View File

@@ -1,9 +1,9 @@
import { t } from "./i18n.js";
import froca from "./froca.js";
import server from "./server.js";
import type { MenuCommandItem, MenuItem, MenuItemBadge, MenuSeparatorItem } from "../menus/context_menu.js";
import type { NoteType } from "../entities/fnote.js";
import type { MenuCommandItem, MenuItem, MenuItemBadge, MenuSeparatorItem } from "../menus/context_menu.js";
import type { TreeCommandNames } from "../menus/tree_context_menu.js";
import froca from "./froca.js";
import { t } from "./i18n.js";
import server from "./server.js";
export interface NoteTypeMapping {
type: NoteType;
@@ -26,6 +26,7 @@ export const NOTE_TYPES: NoteTypeMapping[] = [
// The default note type (always the first item)
{ type: "text", mime: "text/html", title: t("note_types.text"), icon: "bx-note" },
{ type: "spreadsheet", mime: "application/json", title: t("note_types.spreadsheet"), icon: "bx-table", isBeta: true },
// Text notes group
{ type: "book", mime: "", title: t("note_types.book"), icon: "bx-book" },
@@ -96,9 +97,9 @@ function getBlankNoteTypes(command?: TreeCommandNames): MenuItem<TreeCommandName
title: nt.title,
command,
type: nt.type,
uiIcon: "bx " + nt.icon,
uiIcon: `bx ${nt.icon}`,
badges: []
}
};
if (nt.isNew) {
menuItem.badges?.push(NEW_BADGE);
@@ -130,7 +131,7 @@ async function getUserTemplates(command?: TreeCommandNames) {
const item: MenuItem<TreeCommandNames> = {
title: templateNote.title,
uiIcon: templateNote.getIcon(),
command: command,
command,
type: templateNote.type,
templateNoteId: templateNote.noteId
};
@@ -159,7 +160,7 @@ async function getBuiltInTemplates(title: string | null, command: TreeCommandNam
const items: MenuItem<TreeCommandNames>[] = [];
if (title) {
items.push({
title: title,
title,
kind: "header"
});
} else {
@@ -175,7 +176,7 @@ async function getBuiltInTemplates(title: string | null, command: TreeCommandNam
const item: MenuItem<TreeCommandNames> = {
title: templateNote.title,
uiIcon: templateNote.getIcon(),
command: command,
command,
type: templateNote.type,
templateNoteId: templateNote.noteId
};
@@ -193,7 +194,7 @@ async function isNewTemplate(templateNoteId) {
if (rootCreationDate === undefined) {
// Retrieve the root note creation date
try {
let rootNoteInfo: any = await server.get("notes/root");
const rootNoteInfo: any = await server.get("notes/root");
if ("dateCreated" in rootNoteInfo) {
rootCreationDate = new Date(rootNoteInfo.dateCreated);
}
@@ -208,7 +209,7 @@ async function isNewTemplate(templateNoteId) {
if (creationDate === undefined) {
// The creation date isn't available in the cache, try to retrieve it from the server
try {
const noteInfo: any = await server.get("notes/" + templateNoteId);
const noteInfo: any = await server.get(`notes/${templateNoteId}`);
if ("dateCreated" in noteInfo) {
creationDate = new Date(noteInfo.dateCreated);
creationDateCache.set(templateNoteId, creationDate);
@@ -230,9 +231,8 @@ async function isNewTemplate(templateNoteId) {
const age = (new Date().getTime() - creationDate.getTime()) / DAY_LENGTH;
// Return true if the template is at most NEW_TEMPLATE_MAX_AGE days old
return (age <= NEW_TEMPLATE_MAX_AGE);
} else {
return false;
}
return false;
}
export default {

View File

@@ -89,7 +89,7 @@ async function remove<T>(url: string, componentId?: string) {
return await call<T>("DELETE", url, componentId);
}
async function upload(url: string, fileToUpload: File, componentId?: string) {
async function upload(url: string, fileToUpload: File, componentId?: string, method = "PUT") {
const formData = new FormData();
formData.append("upload", fileToUpload);
@@ -99,7 +99,7 @@ async function upload(url: string, fileToUpload: File, componentId?: string) {
"trilium-component-id": componentId
} : undefined),
data: formData,
type: "PUT",
type: method,
timeout: 60 * 60 * 1000,
contentType: false, // NEEDED, DON'T REMOVE THIS
processData: false // NEEDED, DON'T REMOVE THIS

View File

@@ -12,6 +12,7 @@ export default class SpacedUpdate {
private updateInterval: number;
private changeForbidden?: boolean;
private stateCallback?: StateCallback;
private lastState: SaveState = "saved";
constructor(updater: Callback, updateInterval = 1000, stateCallback?: StateCallback) {
this.updater = updater;
@@ -24,7 +25,7 @@ export default class SpacedUpdate {
scheduleUpdate() {
if (!this.changeForbidden) {
this.changed = true;
this.stateCallback?.("unsaved");
this.onStateChanged("unsaved");
setTimeout(() => this.triggerUpdate());
}
}
@@ -34,12 +35,12 @@ export default class SpacedUpdate {
this.changed = false; // optimistic...
try {
this.stateCallback?.("saving");
this.onStateChanged("saving");
await this.updater();
this.stateCallback?.("saved");
this.onStateChanged("saved");
} catch (e) {
this.changed = true;
this.stateCallback?.("error");
this.onStateChanged("error");
logError(getErrorMessage(e));
throw e;
}
@@ -76,13 +77,13 @@ export default class SpacedUpdate {
}
if (Date.now() - this.lastUpdated > this.updateInterval) {
this.stateCallback?.("saving");
this.onStateChanged("saving");
try {
await this.updater();
this.stateCallback?.("saved");
this.onStateChanged("saved");
this.changed = false;
} catch (e) {
this.stateCallback?.("error");
this.onStateChanged("error");
logError(getErrorMessage(e));
}
this.lastUpdated = Date.now();
@@ -92,6 +93,13 @@ export default class SpacedUpdate {
}
}
onStateChanged(state: SaveState) {
if (state === this.lastState) return;
this.stateCallback?.(state);
this.lastState = state;
}
async allowUpdateWithoutChange(callback: Callback) {
this.changeForbidden = true;

View File

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

View File

@@ -1535,7 +1535,8 @@
"new-feature": "新建",
"collections": "集合",
"book": "集合",
"ai-chat": "AI聊天"
"ai-chat": "AI聊天",
"spreadsheet": "电子表格"
},
"protect_note": {
"toggle-on": "保护笔记",

View File

@@ -1488,20 +1488,21 @@
"mermaid-diagram": "Mermaid Diagramm",
"canvas": "Leinwand",
"web-view": "Webansicht",
"mind-map": "Mind Map",
"mind-map": "Mindmap",
"file": "Datei",
"image": "Bild",
"launcher": "Starter",
"doc": "Dokument",
"widget": "Widget",
"confirm-change": "Es is nicht empfehlenswert den Notiz-Typ zu ändern, wenn der Inhalt der Notiz nicht leer ist. Möchtest du dennoch fortfahren?",
"confirm-change": "Es ist nicht empfehlenswert den Notiz-Typ zu ändern, wenn der Inhalt der Notiz nicht leer ist. Möchtest du dennoch fortfahren?",
"geo-map": "Geo-Karte",
"beta-feature": "Beta",
"book": "Sammlung",
"ai-chat": "KI Chat",
"ai-chat": "KI-Chat",
"task-list": "Aufgabenliste",
"new-feature": "Neu",
"collections": "Sammlungen"
"collections": "Sammlungen",
"spreadsheet": "Tabelle"
},
"protect_note": {
"toggle-on": "Notiz schützen",

View File

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

View File

@@ -1548,7 +1548,8 @@
"task-list": "Lista de tareas",
"book": "Colección",
"new-feature": "Nuevo",
"collections": "Colecciones"
"collections": "Colecciones",
"spreadsheet": "Hoja de cálculo"
},
"protect_note": {
"toggle-on": "Proteger la nota",
@@ -1650,7 +1651,8 @@
},
"search_result": {
"no_notes_found": "No se han encontrado notas para los parámetros de búsqueda dados.",
"search_not_executed": "La búsqueda aún no se ha ejecutado. Dé clic en el botón «Buscar» para ver los resultados."
"search_not_executed": "La búsqueda aún no se ha ejecutado.",
"search_now": "Buscar ahora"
},
"spacer": {
"configure_launchbar": "Configurar barra de lanzamiento"

View File

@@ -1571,7 +1571,8 @@
"ai-chat": "Comhrá AI",
"task-list": "Liosta Tascanna",
"new-feature": "Nua",
"collections": "Bailiúcháin"
"collections": "Bailiúcháin",
"spreadsheet": "Scarbhileog"
},
"protect_note": {
"toggle-on": "Cosain an nóta",

View File

@@ -51,7 +51,7 @@
},
"add_link": {
"note": "नोट",
"add_link": "लिंक जोड़ें",
"add_link": "लिंक ऐड करें",
"help_on_links": "लिंक्स पर मदद।",
"search_note": "नोट को नाम से खोजें",
"link_title_mirrors": "लिंक टाइटल नोट के करंट टाइटल के हिसाब से बदलता है",
@@ -112,7 +112,7 @@
"help_on_tree_prefix": "ट्री प्रीफ़िक्स पर मदद",
"prefix": "प्रीफ़िक्स: ",
"save": "सेव करें",
"branch_prefix_saved": "ब्रांच प्रीिक्स सेव कर दिया गया है।",
"branch_prefix_saved": "ब्रांच प्रीफ़िक्स सेव हो चुका है।",
"branch_prefix_saved_multiple": "{{count}} ब्रांचेस के लिए ब्रांच प्रीफ़िक्स सेव कर दिया गया है।",
"affected_branches": "प्रभावित ब्रांचेस ({{count}}):"
},

View File

@@ -520,7 +520,7 @@
"custom_name_label": "Nome del motore di ricerca personalizzato",
"custom_name_placeholder": "Personalizza il nome del motore di ricerca",
"custom_url_label": "L'URL del motore di ricerca personalizzato deve includere {keyword} come segnaposto per il termine di ricerca.",
"custom_url_placeholder": "Personalizza l'URL del motore di ricerca"
"custom_url_placeholder": "Personalizza l'URL del motore di ricerca"
},
"sql_table_schemas": {
"tables": "Tabelle"
@@ -1717,7 +1717,8 @@
"task-list": "Elenco delle attività",
"new-feature": "Nuovo",
"collections": "Collezioni",
"ai-chat": "Chat con IA"
"ai-chat": "Chat con IA",
"spreadsheet": "Foglio di calcolo"
},
"protect_note": {
"toggle-on": "Proteggi la nota",

View File

@@ -600,7 +600,8 @@
"task-list": "タスクリスト",
"new-feature": "New",
"collections": "コレクション",
"ai-chat": "AI チャット"
"ai-chat": "AI チャット",
"spreadsheet": "スプレッドシート"
},
"edited_notes": {
"no_edited_notes_found": "この日の編集されたノートはまだありません...",

View File

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

View File

@@ -257,7 +257,7 @@
"collapseExpand": "свернуть/развернуть узел",
"notSet": "не установлено",
"goBackForwards": "назад / вперед в истории",
"showJumpToNoteDialog": "показать <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">окно \"Перейти к\"</a>",
"showJumpToNoteDialog": "Перейти к <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">\"Перейти к\" окно</a>",
"scrollToActiveNote": "прокрутка к активной заметке",
"jumpToParentNote": "переход к родительской заметке",
"collapseWholeTree": "свернуть все дерево заметок",
@@ -471,7 +471,7 @@
"calendar_root": "отмечает заметку, которая должна использоваться в качестве корневой для заметок дня. Только одна должна быть отмечена как таковая.",
"archived": "заметки с этой меткой не будут отображаться в результатах поиска по умолчанию (а также в диалоговых окнах «Перейти к», «Добавить ссылку» и т. д.).",
"exclude_from_export": "заметки (с их поддеревьями) не будут включены ни в один экспорт заметок",
"run": "определяет, при каких событиях должен запускаться скрипт. Возможные значения:\n<ul>\n<li>frontendStartup — при запуске (или обновлении) фронтенда Trilium, но не на мобильном устройстве.</li>\n<li>mobileStartup — при запуске (или обновлении) фронтенда Trilium на мобильном устройстве.</li>\n<li>backendStartup — при запуске бэкенда Trilium.</li>\n<li>hourly — запускать каждый час. Для указания времени можно использовать дополнительную метку <code>runAtHour</code>.</li>\n<li>daily — запускать раз в день.</li>\n</ul>",
"run": "определяет, при каких событиях должен запускаться скрипт. Возможные значения:<ul>\n<li>frontendStartup — при запуске (или обновлении) фронтенда Trilium, но не на мобильном устройстве.</li>\n<li>mobileStartup — при запуске (или обновлении) фронтенда Trilium на мобильном устройстве.</li>\n<li>backendStartup — при запуске бэкенда Trilium.</li>\n<li>hourly — запускать каждый час. Для указания времени можно использовать дополнительную метку <code>runAtHour</code>.</li>\n<li>daily — запускать раз в день.</li></ul>",
"run_on_instance": "Определить, на каком экземпляре Trilium это должно выполняться. По умолчанию — для всех экземпляров.",
"run_at_hour": "В какой час это должно выполняться? Следует использовать вместе с <code>#run=hourly</code>. Можно задать несколько раз для большего количества запусков в течение дня.",
"disable_inclusion": "скрипты с этой меткой не будут включены в выполнение родительского скрипта.",
@@ -594,7 +594,8 @@
"display-week-numbers": "Отображать номера недель",
"hide-weekends": "Скрыть выходные",
"raster": "Растр",
"show-scale": "Показать масштаб"
"show-scale": "Показать масштаб",
"show-labels": "Показать названия маркеров"
},
"editorfeatures": {
"note_completion_enabled": "Включить автодополнение",
@@ -782,7 +783,13 @@
"shared-indicator-tooltip": "Эта заметка опубликована",
"shared-indicator-tooltip-with-url": "Эта заметка доступно публично по адресу: {{- url}}",
"subtree-hidden-moved-description-other": "В дереве, к которому относится эта заметка, скрыты дочерние заметки.",
"subtree-hidden-moved-description-collection": "Эта коллекция скрывает свои дочерние заметки в дереве."
"subtree-hidden-moved-description-collection": "Эта коллекция скрывает свои дочерние заметки в дереве.",
"clone-indicator-tooltip": "У этой заметки {{- count}} родителей: {{- parents}}",
"clone-indicator-tooltip-single": "Эта заметка клонирована (1 дополнительный родитель: {{- parent}})",
"subtree-hidden-moved-title": "Добавлено в {{title}}",
"subtree-hidden-tooltip_one": "{{count}} дочерняя заметка скрыта",
"subtree-hidden-tooltip_few": "Скрыто {{count}} дочерних заметок",
"subtree-hidden-tooltip_many": "Скрыто {{count}} дочерних заметок"
},
"quick-search": {
"no-results": "Результаты не найдены",
@@ -826,7 +833,9 @@
"mind-map": "Mind Map",
"geo-map": "Географическая карта",
"task-list": "Список задач",
"confirm-change": "Не рекомендуется менять тип заметки, если её содержимое не пустое. Вы всё равно хотите продолжить?"
"confirm-change": "Не рекомендуется менять тип заметки, если её содержимое не пустое. Вы всё равно хотите продолжить?",
"ai-chat": "Чат с ИИ",
"spreadsheet": "Электронная таблица"
},
"tree-context-menu": {
"open-in-popup": "Быстрое редактирование",
@@ -1153,7 +1162,8 @@
"search_note_saved": "Заметка с настройкой поиска сохранена в {{- notePathTitle}}",
"unknown_search_option": "Неизвестный параметр поиска {{searchOptionName}}",
"actions_executed": "Действия выполнены.",
"view_options": "Просмотреть опции:"
"view_options": "Просмотреть опции:",
"option": "опция"
},
"ancestor": {
"depth_label": "глубина",
@@ -1403,7 +1413,8 @@
"type_text_to_filter": "Введите текст для фильтрации сочетаний клавиш...",
"reload_app": "Перезагрузить приложение, чтобы применить изменения",
"confirm_reset": "Вы действительно хотите сбросить все сочетания клавиш до значений по умолчанию?",
"set_all_to_default": "Установить все сочетания клавиш по умолчанию"
"set_all_to_default": "Установить все сочетания клавиш по умолчанию",
"no_results": "Не найдено ярлыков, соответствующих '{{filter}}'"
},
"sync_2": {
"timeout_unit": "миллисекунд",
@@ -1713,7 +1724,8 @@
"delete_this_note": "Удалить эту заметку",
"insert_child_note": "Вставить дочернюю заметку",
"note_revisions": "История изменений",
"content_language_switcher": "Язык содержимого: {{language}}"
"content_language_switcher": "Язык содержимого: {{language}}",
"backlinks": "Ссылки"
},
"svg_export_button": {
"button_title": "Экспортировать диаграмму как SVG"
@@ -1790,7 +1802,8 @@
},
"search_result": {
"no_notes_found": "По заданным параметрам поиска заметки не найдены.",
"search_not_executed": "Поиск ещё не выполнен. Нажмите кнопку «Поиск» выше, чтобы увидеть результаты."
"search_not_executed": "Поиск ещё не выполнен.",
"search_now": "Искать сейчас"
},
"empty": {
"search_placeholder": "поиск заметки по ее названию",
@@ -1988,10 +2001,12 @@
"print_report_collection_content_few": "{{count}} заметки в коллекции не удалось распечатать, поскольку они не поддерживаются или защищены.",
"print_report_collection_content_many": "{{count}} заметок в коллекции не удалось распечатать, поскольку они не поддерживаются или защищены.",
"print_report_collection_details_button": "Подробнее",
"print_report_collection_details_ignored_notes": "Пропущенные заметки"
"print_report_collection_details_ignored_notes": "Пропущенные заметки",
"print_report_error_title": "Не удалось напечатать",
"print_report_stack_trace": "Трассировка стека"
},
"book": {
"no_children_help": "В этой коллекции нет дочерних заметок, поэтому отображать нечего. Подробности см. в <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a>.",
"no_children_help": "В этой коллекции нет дочерних заметок, поэтому отображать нечего.",
"drag_locked_title": "Защищено от изменения",
"drag_locked_message": "Перетаскивание не допускается, так как коллекция защищена от редактирования."
},
@@ -2007,7 +2022,9 @@
"rendering_error": "Невозможно отобразить содержимое из-за ошибки."
},
"pagination": {
"total_notes": "{{count}} заметок"
"total_notes": "{{count}} заметок",
"prev_page": "Предыдущая страница",
"next_page": "Следующая страница"
},
"status_bar": {
"attributes_one": "{{count}} атрибут",
@@ -2137,5 +2154,49 @@
},
"platform_indicator": {
"available_on": "Доступно для {{platform}}"
},
"render": {
"setup_title": "Отобразить настраиваемый HTML или Preact JSX в этой заметке",
"setup_create_sample_preact": "Создать образец заметки с помощью Preact",
"setup_create_sample_html": "Создать образец заметки с помощью HTML",
"setup_sample_created": "Образец заметки был создан в качестве дочерней записи.",
"disabled_description": "Эти заметки для рендера поступают из внешнего источника. Чтобы защитить вас от вредоносного содержимого, они не включены по умолчанию. Убедитесь, что вы доверяете источнику до его включения.",
"disabled_button_enable": "Включить заметки для рендера"
},
"web_view_setup": {
"title": "Создайте живой просмотр веб-страницы прямо в Trilium",
"url_placeholder": "Введите или вставьте адрес сайта, например https://triliumnotes.org",
"create_button": "Создать веб-просмотр",
"invalid_url_title": "Неверный адрес",
"invalid_url_message": "Введите корректный веб-адрес, например https://triliumnotes.org.",
"disabled_description": "Этот веб-просмотр был импортирован из внешнего источника. Чтобы защитить вас от фишинга или вредоносного контента, он не загружается автоматически. Вы можете включить его, если доверяете источнику.",
"disabled_button_enable": "Включить просмотр веб-страниц"
},
"active_content_badges": {
"type_icon_pack": "Набор иконок",
"type_backend_script": "Бэкенд скрипт",
"type_frontend_script": "Фронтенд скрипт",
"type_widget": "Виджет",
"type_app_css": "Пользовательский CSS",
"type_render_note": "Заметка для рендера",
"type_web_view": "Просмотр веб-страницы",
"type_app_theme": "Пользовательская тема",
"toggle_tooltip_enable_tooltip": "Нажмите, чтобы включить этот {{type}}.",
"toggle_tooltip_disable_tooltip": "Нажмите, чтобы выключить этот {{type}}.",
"menu_docs": "Открытая документация",
"menu_execute_now": "Выполнить скрипт сейчас",
"menu_run": "Выполнять автоматически",
"menu_run_disabled": "Вручную",
"menu_run_backend_startup": "При запуске бэкенда",
"menu_run_hourly": "Ежечасно",
"menu_run_daily": "Ежедневно",
"menu_run_frontend_startup": "Когда запускается интерфейс ПК",
"menu_run_mobile_startup": "При запуске мобильного интерфейса",
"menu_change_to_widget": "Изменить виджет",
"menu_change_to_frontend_script": "Перейти к фронтенд скрипту",
"menu_theme_base": "Базовая тема"
},
"setup_form": {
"more_info": "Узнать больше"
}
}

View File

@@ -3,6 +3,38 @@
"title": "Om Trilium Notes",
"homepage": "Hemsida:",
"app_version": "App version:",
"db_version": "DB version:"
"db_version": "DB version:",
"sync_version": "Sync version:",
"build_date": "Bygg datum:",
"build_revision": "Bygg version:",
"data_directory": "Data sökväg:"
},
"toast": {
"critical-error": {
"title": "Kritiskt fel",
"message": "Ett kritiskt fel har inträffat som förhindrar klientprogrammet från att starta:\n\n{{message}}\n\nDetta beror troligen på att ett skript har misslyckats på ett oväntat sätt. Försök att starta programmet i felsäkert läge och åtgärda problemet."
},
"widget-error": {
"title": "Misslyckades att starta widget",
"message-custom": "Anpassad widget från anteckning med ID \"{{id}}\", med rubrik \"{{title}}\" kunde inte startas på grund av:\n\n{{message}}",
"message-unknown": "Okänd widget kunde inte startas på grund av:\n\n{{message}}"
},
"bundle-error": {
"title": "Misslyckades att starta ett anpassat skript",
"message": "Skript kunde inte startas på grund av:\n\n{{message}}"
},
"widget-list-error": {
"title": "Misslyckades att hämta widget-listan från servern"
},
"widget-render-error": {
"title": "Misslyckades att renderera en anpassad React-widget"
},
"widget-missing-parent": "Anpassad widget saknar '{{property}}', som måste vara definierad.\n\nOm skriptet är avsett att köras utan gränssnitt, använd '#run-frontendStartup' istället.",
"open-script-note": "Öppna skriptanteckning",
"scripting-error": "Fel i anpassat skript: {{title}}"
},
"add_link": {
"add_link": "Infoga länk",
"help_on_links": "Hjälp om länkar"
}
}

View File

@@ -50,8 +50,15 @@
},
"bundle-error": {
"title": "Özel bir betik yüklenemedi",
"message": "ID'si \"{{id}}\" ve başlığı \"{{title}}\" olan nottan alınan komut dosyası şunun nedeniyle yürütülemedi:\n\n{{message}}"
}
"message": "Komut şu nedenle yürütülemedi:\n\n{{message}}"
},
"widget-list-error": {
"title": "Sunucudan widget listesi alınamadı"
},
"widget-render-error": {
"title": "Özel React widget'ı çizilirken sorun yaşandı"
},
"scripting-error": "Kullanıcı tanımlı betik hatası: {{title}}"
},
"add_link": {
"add_link": "Bağlantı ekle",

View File

@@ -1495,7 +1495,9 @@
"beta-feature": "Beta",
"task-list": "任務列表",
"new-feature": "新增",
"collections": "集合"
"collections": "集合",
"ai-chat": "AI 聊天",
"spreadsheet": "試算表"
},
"protect_note": {
"toggle-on": "保護筆記",
@@ -1594,7 +1596,8 @@
},
"search_result": {
"no_notes_found": "沒有找到符合搜尋條件的筆記。",
"search_not_executed": "尚未執行搜尋。請點擊上方的「搜尋」按鈕查看結果。"
"search_not_executed": "尚未執行搜尋。",
"search_now": "立即搜尋"
},
"spacer": {
"configure_launchbar": "設定啟動欄"
@@ -2011,7 +2014,9 @@
"app-restart-required": "(需要重啟程式以套用更改)"
},
"pagination": {
"total_notes": "{{count}} 筆記"
"total_notes": "{{count}} 筆記",
"prev_page": "上一頁",
"next_page": "下一頁"
},
"collections": {
"rendering_error": "發現錯誤,無法顯示內容。"

View File

@@ -55,7 +55,10 @@
"show_help": "Показати Довідку",
"logout": "Вийти",
"show-cheatsheet": "Показати Шпаргалку",
"toggle-zen-mode": "Дзен-режим"
"toggle-zen-mode": "Дзен-режим",
"new-version-available": "Доступне оновлення",
"download-update": "Отримати версію {{latest Version}}",
"search_notes": "Пошук нотаток"
},
"modal": {
"help_title": "Показати більше інформації про це вікно",
@@ -293,7 +296,8 @@
},
"import-status": "Статус Імпорту",
"in-progress": "Триває Імпорт: {{progress}}",
"successful": "Імпорт успішно завершено."
"successful": "Імпорт успішно завершено.",
"importZipRecommendation": "Під час імпорту ZIP-файлу ієрархія нотаток відображатиме структуру підкаталогів в архіві."
},
"prompt": {
"title": "Запит(prompt)",
@@ -355,7 +359,8 @@
"info": {
"modalTitle": "Інформаційне повідомлення",
"closeButton": "Закрити",
"okButton": "ОК"
"okButton": "ОК",
"copy_to_clipboard": "Копіювати в буфер обміну"
},
"jump_to_note": {
"search_placeholder": "Пошук нотатки за її назвою або типом > для команд...",
@@ -805,7 +810,14 @@
"convert_into_attachment_failed": "Не вдалося конвертувати нотатку '{{title}}'.",
"convert_into_attachment_successful": "Нотатку '{{title}}' перетворено на вкладення.",
"convert_into_attachment_prompt": "Ви впевнені, що хочете перетворити нотатку '{{title}}' на вкладення батьківської нотатки?",
"print_pdf": "Експортувати як PDF..."
"print_pdf": "Експортувати як PDF...",
"open_note_on_server": "Відкрити нотатку на сервері",
"view_revisions": "Ревізії нотатки...",
"advanced": "Розширені",
"export_as_image": "Експортувати як зображення",
"export_as_image_png": "PNG (растровий)",
"export_as_image_svg": "SVG (векторний)",
"note_map": "Карта нотатки"
},
"onclick_button": {
"no_click_handler": "Віджет кнопки '{{componentId}}' не має визначеного обробника кліків"
@@ -858,7 +870,10 @@
"insert_child_note": "Вставити дочірню нотатку",
"delete_this_note": "Видалити цю нотатку",
"error_cannot_get_branch_id": "Не вдається отримати branchId для notePath '{{notePath}}'",
"error_unrecognized_command": "Нерозпізнана команда {{command}}"
"error_unrecognized_command": "Нерозпізнана команда {{command}}",
"note_revisions": "Ревізії нотатки",
"backlinks": "Зворотні посилання",
"content_language_switcher": "Мова контенту: {{language}}"
},
"note_icon": {
"change_note_icon": "Змінити значок нотатки",
@@ -866,7 +881,13 @@
"reset-default": "Скинути значок до стандартного значення",
"search_placeholder_one": "Пошук {{number}} значка у {{count}} пакеті",
"search_placeholder_few": "Пошук {{number}} значків у {{count}} пакетах",
"search_placeholder_many": "Пошук {{number}} значків у {{count}} пакетах"
"search_placeholder_many": "Пошук {{number}} значків у {{count}} пакетах",
"search_placeholder_filtered": "Пошук {{number}} іконок у {{name}}",
"filter": "Фільтр",
"filter-none": "Всі іконки",
"filter-default": "Іконки за замовчуванням",
"icon_tooltip": "{{name}}\nПакет іконок: {{iconPack}}",
"no_results": "Іконки не знайдено"
},
"basic_properties": {
"note_type": "Тип нотатки",
@@ -888,7 +909,13 @@
"table": "Таблиця",
"geo-map": "Географічна карта",
"board": "Дошка",
"include_archived_notes": "Показати архівовані нотатки"
"include_archived_notes": "Показати архівовані нотатки",
"expand_tooltip": "Розгортає безпосередні дочірні елементи цієї колекції (на один рівень у глибину). Щоб переглянути більше параметрів, натисніть стрілку праворуч.",
"expand_first_level": "Розгорнути прямі дочірні елементи",
"expand_nth_level": "Розгорнути {{depth}} рівнів",
"expand_all_levels": "Розгорнути всі рівні",
"presentation": "Презентація",
"hide_child_notes": "Приховати дочірні нотатки в дереві"
},
"edited_notes": {
"no_edited_notes_found": "Цього дня ще немає редагованих нотаток...",
@@ -921,7 +948,8 @@
},
"inherited_attribute_list": {
"title": "Успадковані Атрибути",
"no_inherited_attributes": "Немає успадкованих атрибутів."
"no_inherited_attributes": "Немає успадкованих атрибутів.",
"none": "пусто"
},
"note_info_widget": {
"note_id": "ID Нотатки",
@@ -932,7 +960,9 @@
"note_size_info": "Розмір нотатки надає приблизну оцінку вимог до зберігання для цієї нотатки. Він враховує вміст нотатки та вміст її версій.",
"calculate": "обчислити",
"subtree_size": "(розмір піддерева: {{size}} у {{count}} нотатках)",
"title": "Інформація про нотатку"
"title": "Інформація про нотатку",
"mime": "Тип MIME",
"show_similar_notes": "Показати схожі нотатки"
},
"note_map": {
"open_full": "Розгорнути на повний розмір",
@@ -995,7 +1025,9 @@
"search_parameters": "Параметри пошуку",
"unknown_search_option": "Невідомий параметр пошуку {{searchOptionName}}",
"actions_executed": "Дії виконано.",
"search_note_saved": "Нотатка з пошуку збережена у {{- notePathTitle}}"
"search_note_saved": "Нотатка з пошуку збережена у {{- notePathTitle}}",
"option": "опції",
"view_options": "Опції перегляду:"
},
"similar_notes": {
"title": "Схожі нотатки",
@@ -1089,7 +1121,13 @@
},
"editable_text": {
"placeholder": "Введіть тут вміст вашої нотатки...",
"auto-detect-language": "Автовизначено"
"auto-detect-language": "Автовизначено",
"editor_crashed_title": "Збій текстового редактора",
"editor_crashed_content": "Ваш контент успішно відновлено, але деякі з ваших останніх змін могли бути не збережені.",
"editor_crashed_details_button": "Переглянути більше деталей...",
"editor_crashed_details_intro": "Якщо ви стикаєтеся з цією помилкою кілька разів, подумайте про те, щоб повідомити про неї на GitHub, вставивши наведену нижче інформацію.",
"editor_crashed_details_title": "Технічна інформація",
"keeps-crashing": "Компонент редагування постійно аварійно завершує роботу. Спробуйте перезапустити Trilium. Якщо проблема не зникає, спробуйте створити звіт про помилку."
},
"empty": {
"open_note_instruction": "Відкрийте нотатку, ввівши її заголовок в поле нижче, або виберіть нотатку в дереві.",
@@ -1955,5 +1993,11 @@
"pages_one": "{{count}} сторінка",
"pages_few": "{{count}} сторінки",
"pages_many": "{{count}} сторінок"
},
"render": {
"setup_title": "Відображати власний HTML або Preact JSX у цій нотатці",
"setup_create_sample_preact": "Створіть зразок нотатки за допомогою Preact",
"setup_create_sample_html": "Створити зразок нотатки за допомогою HTML",
"setup_sample_created": "Зразок нотатки було створено як дочірню нотатку."
}
}

View File

@@ -40,6 +40,21 @@ export default function NoteDetail() {
const widgetRequestId = useRef(0);
const hasFixedTree = note && noteContext?.hoistedNoteId === "_lbMobileRoot" && isMobile() && note.noteId.startsWith("_lbMobile");
// Defer loading for tabs that haven't been active yet (e.g. on app refresh).
// Special contexts (ntxId starting with "_", e.g. popup editor) are always considered active.
const isSpecialContext = ntxId?.startsWith("_") ?? false;
const [ hasTabBeenActive, setHasTabBeenActive ] = useState(() => isSpecialContext || (noteContext?.isActive() ?? false));
useEffect(() => {
if (!hasTabBeenActive && noteContext?.isActive()) {
setHasTabBeenActive(true);
}
}, [ noteContext, hasTabBeenActive ]);
useTriliumEvent("activeNoteChanged", ({ ntxId: eventNtxId }) => {
if (eventNtxId === ntxId && !hasTabBeenActive) {
setHasTabBeenActive(true);
}
});
const props: TypeWidgetProps = {
note: note!,
viewScope,
@@ -49,7 +64,7 @@ export default function NoteDetail() {
};
useEffect(() => {
if (!type) return;
if (!type || !hasTabBeenActive) return;
const requestId = ++widgetRequestId.current;
if (!noteTypesToRender[type]) {
@@ -68,7 +83,7 @@ export default function NoteDetail() {
} else {
setActiveNoteType(type);
}
}, [ note, viewScope, type, noteTypesToRender ]);
}, [ note, viewScope, type, noteTypesToRender, hasTabBeenActive ]);
// Detect note type changes.
useTriliumEvent("entitiesReloaded", async ({ loadResults }) => {
@@ -247,9 +262,8 @@ function NoteDetailWrapper({ Element, type, isVisible, isFullHeight, props }: {
useEffect(() => {
if (isVisible) {
setCachedProps(props);
} else {
// Do nothing, keep the old props.
}
// When not visible, keep the old props to avoid re-rendering in the background.
}, [ props, isVisible ]);
const typeMapping = TYPE_MAPPINGS[type];
@@ -260,7 +274,7 @@ function NoteDetailWrapper({ Element, type, isVisible, isFullHeight, props }: {
height: isFullHeight ? "100%" : ""
}}
>
{ <Element {...cachedProps} /> }
<Element {...cachedProps} />
</div>
);
}

View File

@@ -4,6 +4,7 @@
overflow: visible;
contain: none !important;
clear: both;
&.full-height {
overflow: auto;

View File

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

View File

@@ -54,6 +54,8 @@ export default function PopupEditor() {
}
});
// Events triggered at note context level (e.g. the save indicator) would not work since the note context has no parent component. Propagate events to parent component so that they can be handled properly.
noteContext.triggerEvent = (name, data) => parentComponent?.handleEventInChildren(name, data);
setNoteContext(noteContext);
setShown(true);
});

View File

@@ -272,7 +272,8 @@ function RevisionContent({ noteContent, revisionItem, fullRevision, showDiff }:
return <FilePreview fullRevision={fullRevision} revisionItem={revisionItem} />;
case "canvas":
case "mindMap":
case "mermaid": {
case "mermaid":
case "spreadsheet": {
const encodedTitle = encodeURIComponent(revisionItem.title);
return <img
src={`api/revisions/${revisionItem.revisionId}/image/${encodedTitle}?${Math.random()}`}

View File

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

View File

@@ -7,7 +7,7 @@ import { t } from "../../services/i18n";
import { goToLinkExt } from "../../services/link";
import { Badge, BadgeWithDropdown } from "../react/Badge";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { useGetContextData, useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
import { useGetContextDataFrom, useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
import { useShareState } from "../ribbon/BasicPropertiesTab";
import { useShareInfo } from "../shared_info";
import { ActiveContentBadges } from "./ActiveContentBadges";
@@ -112,7 +112,8 @@ function ExecuteBadge() {
}
export function SaveStatusBadge() {
const saveState = useGetContextData("saveState");
const { noteContext} = useNoteContext();
const saveState = useGetContextDataFrom(noteContext, "saveState");
if (!saveState) return;
const stateConfig = {

View File

@@ -141,5 +141,11 @@ export const TYPE_MAPPINGS: Record<ExtendedNoteType, NoteTypeMapping> = {
view: () => import("./type_widgets/SqlConsole"),
className: "sql-console-widget-container",
isFullHeight: true
},
spreadsheet: {
view: () => import("./type_widgets/spreadsheet/Spreadsheet"),
className: "note-detail-spreadsheet",
printable: true,
isFullHeight: true
}
};

View File

@@ -79,11 +79,11 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
}
#isFullWidthNote(note: FNote) {
if (["code", "image", "mermaid", "book", "render", "canvas", "webView", "mindMap"].includes(note.type)) {
if (["code", "image", "mermaid", "book", "render", "canvas", "webView", "mindMap", "spreadsheet"].includes(note.type)) {
return true;
}
if (note.type === "file" && (note.mime === "application/pdf" || note.mime.startsWith("video/"))) {
if (note.type === "file" && (note.mime === "application/pdf" || note.mime.startsWith("video/") || note.mime.startsWith("audio/"))) {
return true;
}
@@ -102,13 +102,13 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
const COLLECTIONS_WITH_BACKGROUND_EFFECTS = [
"grid",
"list"
]
];
if (note.isOptions()) {
return true;
}
if (note.type === "file" && MIME_TYPES_WITH_BACKGROUND_EFFECTS.includes(note.mime)) {
if (note.type === "file" && (MIME_TYPES_WITH_BACKGROUND_EFFECTS.includes(note.mime) || note.mime.startsWith("audio/"))) {
return true;
}

View File

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

View File

@@ -98,6 +98,7 @@ export interface SavedData {
mime: string;
content: string;
position: number;
encoding?: "base64";
}[];
}

View File

@@ -75,7 +75,7 @@ export function NoteContextMenu({ note, noteContext, itemsAtStart, itemsNearNote
const noteType = useNoteProperty(note, "type") ?? "";
const [viewType] = useNoteLabel(note, "viewType");
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType);
const isSearchable = ["text", "code", "book", "mindMap", "doc", "spreadsheet"].includes(noteType);
const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
const isExportableToImage = ["mermaid", "mindMap"].includes(noteType);
const isContentAvailable = note.isContentAvailable();
@@ -85,7 +85,7 @@ export function NoteContextMenu({ note, noteContext, itemsAtStart, itemsNearNote
);
const isElectron = getIsElectron();
const isMac = getIsMac();
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(noteType);
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "spreadsheet"].includes(noteType);
const isSearchOrBook = ["search", "book"].includes(noteType);
const isHelpPage = note.noteId.startsWith("_help");
const [syncServerHost] = useTriliumOption("syncServerHost");

View File

@@ -189,7 +189,7 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: N
export function ToggleReadOnlyButton({ note, isDefaultViewMode }: NoteActionsCustomInnerProps) {
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
const isSavedSqlite = note.isTriliumSqlite() && !note.isHiddenCompletely();
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || isSavedSqlite)
const isEnabled = ([ "mermaid", "mindMap", "canvas", "spreadsheet" ].includes(note.type) || isSavedSqlite)
&& note.isContentAvailable() && isDefaultViewMode;
return isEnabled && <NoteAction

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import "./MindMap.css";
import nodeMenu from "@mind-elixir/node-menu";
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
import { snapdom } from "@zumer/snapdom";
import { default as VanillaMindElixir,MindElixirData, MindElixirInstance, Operation, Options, THEME as LIGHT_THEME, DARK_THEME } from "mind-elixir";
import { DARK_THEME, default as VanillaMindElixir, MindElixirData, MindElixirInstance, Operation, Options, THEME as LIGHT_THEME } from "mind-elixir";
import { HTMLAttributes, RefObject } from "preact";
import { useCallback, useEffect, useRef } from "preact/hooks";
@@ -154,6 +154,7 @@ function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef
const apiRef = useRef<MindElixirInstance>(null);
const [ locale ] = useTriliumOption("locale");
const colorScheme = useColorScheme();
const defaultColorScheme = useRef(colorScheme);
function reinitialize() {
if (!containerRef.current) return;
@@ -162,7 +163,7 @@ function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef
el: containerRef.current,
locale: LOCALE_MAPPINGS[locale as DISPLAYABLE_LOCALE_IDS] ?? undefined,
editable,
theme: LIGHT_THEME
theme: defaultColorScheme.current === "dark" ? DARK_THEME : LIGHT_THEME
});
if (editable) {
@@ -188,7 +189,11 @@ function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef
if (!apiRef.current) return;
const newTheme = colorScheme === "dark" ? DARK_THEME : LIGHT_THEME;
if (apiRef.current.theme === newTheme) return; // Avoid unnecessary theme changes, which can be expensive to render.
apiRef.current.changeTheme(newTheme);
try {
apiRef.current.changeTheme(newTheme);
} catch (e) {
console.warn("Failed to change mind map theme:", e);
}
}, [ colorScheme ]);
useEffect(() => {

View File

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

View File

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

View File

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

View File

@@ -184,7 +184,7 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: {
<PdfViewer
iframeRef={iframeRef}
tabIndex={300}
pdfUrl={`../../api/notes/${note.noteId}/open`}
pdfUrl={new URL(`${window.glob.baseApiUrl}notes/${note.noteId}/open`, window.location.href).pathname}
onLoad={() => {
const win = iframeRef.current?.contentWindow;
if (win) {

View File

@@ -1,7 +1,8 @@
import type { HTMLAttributes, RefObject } from "preact";
import { useCallback, useEffect, useRef } from "preact/hooks";
import Inter from "./../../../fonts/Inter/Inter-VariableFont_opsz,wght.ttf";
import { useSyncedRef, useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
import Inter from "./../../../fonts/Inter/Inter-VariableFont_opsz,wght.ttf";
interface FontDefinition {
name: string;
@@ -10,11 +11,11 @@ interface FontDefinition {
const FONTS: FontDefinition[] = [
{name: "Inter", url: Inter},
]
];
interface PdfViewerProps extends Pick<HTMLAttributes<HTMLIFrameElement>, "tabIndex"> {
iframeRef?: RefObject<HTMLIFrameElement>;
/** Note: URLs are relative to /pdfjs/web. */
/** Note: URLs are relative to /pdfjs/web, ideally use absolute paths (but without domain name) to avoid issues with some proxies. */
pdfUrl: string;
onLoad?(): void;
/**
@@ -37,7 +38,7 @@ export default function PdfViewer({ iframeRef: externalIframeRef, pdfUrl, onLoad
ref={iframeRef}
class="pdf-preview"
style={{width: "100%", height: "100%"}}
src={`pdfjs/web/viewer.html?file=${pdfUrl}&lang=${locale}&sidebar=${newLayout ? "0" : "1"}&editable=${editable ? "1" : "0"}`}
src={`pdfjs/web/viewer.html?v=${glob.triliumVersion}&file=${pdfUrl}&lang=${locale}&sidebar=${newLayout ? "0" : "1"}&editable=${editable ? "1" : "0"}`}
onLoad={() => {
injectStyles();
onLoad?.();
@@ -63,7 +64,7 @@ function useStyleInjection(iframeRef: RefObject<HTMLIFrameElement>) {
const fontStyles = doc.createElement("style");
fontStyles.textContent = FONTS.map(injectFont).join("\n");
doc.head.appendChild(fontStyles);
}, [ iframeRef ]);
// React to changes.
@@ -107,4 +108,4 @@ function injectFont(font: FontDefinition) {
src: url('${font.url}');
}
`;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
.note-detail-spreadsheet > .spreadsheet {
height: 100%;
}

View File

@@ -0,0 +1,153 @@
import "./Spreadsheet.css";
import "@univerjs/preset-sheets-core/lib/index.css";
import "@univerjs/preset-sheets-sort/lib/index.css";
import "@univerjs/preset-sheets-conditional-formatting/lib/index.css";
import "@univerjs/preset-sheets-find-replace/lib/index.css";
import "@univerjs/preset-sheets-note/lib/index.css";
import "@univerjs/preset-sheets-filter/lib/index.css";
import "@univerjs/preset-sheets-data-validation/lib/index.css";
import { UniverSheetsConditionalFormattingPreset } from '@univerjs/preset-sheets-conditional-formatting';
import UniverPresetSheetsConditionalFormattingEnUS from '@univerjs/preset-sheets-conditional-formatting/locales/en-US';
import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core';
import sheetsCoreEnUS from '@univerjs/preset-sheets-core/locales/en-US';
import { UniverSheetsDataValidationPreset } from '@univerjs/preset-sheets-data-validation';
import UniverPresetSheetsDataValidationEnUS from '@univerjs/preset-sheets-data-validation/locales/en-US';
import { UniverSheetsFilterPreset } from '@univerjs/preset-sheets-filter';
import UniverPresetSheetsFilterEnUS from '@univerjs/preset-sheets-filter/locales/en-US';
import { UniverSheetsFindReplacePreset } from '@univerjs/preset-sheets-find-replace';
import sheetsFindReplaceEnUS from '@univerjs/preset-sheets-find-replace/locales/en-US';
import { UniverSheetsNotePreset } from '@univerjs/preset-sheets-note';
import sheetsNoteEnUS from '@univerjs/preset-sheets-note/locales/en-US';
import { UniverSheetsSortPreset } from '@univerjs/preset-sheets-sort';
import UniverPresetSheetsSortEnUS from '@univerjs/preset-sheets-sort/locales/en-US';
import { createUniver, FUniver, LocaleType, mergeLocales } from '@univerjs/presets';
import { MutableRef, useEffect, useRef } from "preact/hooks";
import { useColorScheme, useNoteLabelBoolean, useTriliumEvent } from "../../react/hooks";
import { TypeWidgetProps } from "../type_widget";
import usePersistence from "./persistence";
export default function Spreadsheet(props: TypeWidgetProps) {
const [ readOnly ] = useNoteLabelBoolean(props.note, "readOnly");
// Use readOnly as key to force full remount (and data reload) when it changes.
return <SpreadsheetEditor key={String(readOnly)} {...props} readOnly={readOnly} />;
}
function SpreadsheetEditor({ note, noteContext, readOnly }: TypeWidgetProps & { readOnly: boolean }) {
const containerRef = useRef<HTMLDivElement>(null);
const apiRef = useRef<FUniver>();
useInitializeSpreadsheet(containerRef, apiRef, readOnly);
useDarkMode(apiRef);
usePersistence(note, noteContext, apiRef, containerRef, readOnly);
useSearchIntegration(apiRef);
useFixRadixPortals();
// Focus the spreadsheet when the note is focused.
useTriliumEvent("focusOnDetail", () => {
const focusable = containerRef.current?.querySelector('[data-u-comp="editor"]');
if (focusable instanceof HTMLElement) {
focusable.focus();
}
});
return <div ref={containerRef} className="spreadsheet" />;
}
/**
* Univer's design system uses Radix UI primitives whose DismissableLayer detects
* "outside" clicks/focus via document-level pointerdown/focusin listeners combined
* with a React capture-phase flag. In React, portal events bubble through the
* component tree so onPointerDownCapture fires on the DismissableLayer, setting an
* internal flag that suppresses the "outside" detection. With preact/compat, portal
* events don't bubble through the React tree, so the flag never gets set and Radix
* immediately dismisses popups.
*
* Radix dispatches cancelable custom events ("dismissableLayer.pointerDownOutside"
* and "dismissableLayer.focusOutside") on the original event target before calling
* onDismiss. The dismiss is skipped if defaultPrevented is true. This hook intercepts
* those custom events in the capture phase and prevents default when the target is
* inside a Radix portal, restoring the expected behavior.
*/
function useFixRadixPortals() {
useEffect(() => {
function preventDismiss(e: Event) {
if (e.target instanceof HTMLElement && e.target.closest("[id^='radix-']")) {
e.preventDefault();
}
}
document.addEventListener("dismissableLayer.pointerDownOutside", preventDismiss, true);
document.addEventListener("dismissableLayer.focusOutside", preventDismiss, true);
return () => {
document.removeEventListener("dismissableLayer.pointerDownOutside", preventDismiss, true);
document.removeEventListener("dismissableLayer.focusOutside", preventDismiss, true);
};
}, []);
}
function useInitializeSpreadsheet(containerRef: MutableRef<HTMLDivElement | null>, apiRef: MutableRef<FUniver | undefined>, readOnly: boolean) {
useEffect(() => {
if (!containerRef.current) return;
const { univerAPI } = createUniver({
locale: LocaleType.EN_US,
locales: {
[LocaleType.EN_US]: mergeLocales(
sheetsCoreEnUS,
sheetsFindReplaceEnUS,
sheetsNoteEnUS,
UniverPresetSheetsFilterEnUS,
UniverPresetSheetsSortEnUS,
UniverPresetSheetsDataValidationEnUS,
UniverPresetSheetsConditionalFormattingEnUS,
),
},
presets: [
UniverSheetsCorePreset({
container: containerRef.current,
toolbar: !readOnly,
contextMenu: !readOnly,
formulaBar: !readOnly,
footer: readOnly ? false : undefined,
menu: {
"sheet.contextMenu.permission": { hidden: true },
"sheet-permission.operation.openPanel": { hidden: true },
"sheet.command.add-range-protection-from-toolbar": { hidden: true },
},
}),
UniverSheetsFindReplacePreset(),
UniverSheetsNotePreset(),
UniverSheetsFilterPreset(),
UniverSheetsSortPreset(),
UniverSheetsDataValidationPreset(),
UniverSheetsConditionalFormattingPreset()
]
});
apiRef.current = univerAPI;
return () => univerAPI.dispose();
}, [ apiRef, containerRef, readOnly ]);
}
function useDarkMode(apiRef: MutableRef<FUniver | undefined>) {
const colorScheme = useColorScheme();
// React to dark mode.
useEffect(() => {
const univerAPI = apiRef.current;
if (!univerAPI) return;
univerAPI.toggleDarkMode(colorScheme === 'dark');
}, [ colorScheme, apiRef ]);
}
function useSearchIntegration(apiRef: MutableRef<FUniver | undefined>) {
useTriliumEvent("findInText", () => {
const univerAPI = apiRef.current;
if (!univerAPI) return;
// Open find/replace panel and populate the search term.
univerAPI.executeCommand("ui.operation.open-find-dialog");
});
}

View File

@@ -0,0 +1,194 @@
import { CommandType, FUniver, IDisposable, IWorkbookData } from "@univerjs/presets";
import { MutableRef, useEffect, useRef } from "preact/hooks";
import NoteContext from "../../../components/note_context";
import FNote from "../../../entities/fnote";
import { SavedData, useEditorSpacedUpdate } from "../../react/hooks";
interface PersistedData {
version: number;
workbook: Parameters<FUniver["createWorkbook"]>[0];
}
interface SpreadsheetViewState {
activeSheetId?: string;
cursorRow?: number;
cursorCol?: number;
scrollRow?: number;
scrollCol?: number;
}
export default function usePersistence(note: FNote, noteContext: NoteContext | null | undefined, apiRef: MutableRef<FUniver | undefined>, containerRef: MutableRef<HTMLDivElement | null>, readOnly: boolean) {
const changeListener = useRef<IDisposable>(null);
const pendingContent = useRef<string | null>(null);
function saveViewState(univerAPI: FUniver): SpreadsheetViewState {
const state: SpreadsheetViewState = {};
try {
const workbook = univerAPI.getActiveWorkbook();
if (!workbook) return state;
const activeSheet = workbook.getActiveSheet();
state.activeSheetId = activeSheet?.getSheetId();
const currentCell = activeSheet?.getSelection()?.getCurrentCell();
if (currentCell) {
state.cursorRow = currentCell.actualRow;
state.cursorCol = currentCell.actualColumn;
}
const scrollState = activeSheet?.getScrollState?.();
if (scrollState) {
state.scrollRow = scrollState.sheetViewStartRow;
state.scrollCol = scrollState.sheetViewStartColumn;
}
} catch {
// Ignore errors when reading state from a workbook being disposed.
}
return state;
}
function restoreViewState(workbook: ReturnType<FUniver["createWorkbook"]>, state: SpreadsheetViewState) {
try {
if (state.activeSheetId) {
const targetSheet = workbook.getSheetBySheetId(state.activeSheetId);
if (targetSheet) {
workbook.setActiveSheet(targetSheet);
}
}
if (state.cursorRow !== undefined && state.cursorCol !== undefined) {
workbook.getActiveSheet().getRange(state.cursorRow, state.cursorCol).activate();
}
if (state.scrollRow !== undefined && state.scrollCol !== undefined) {
workbook.getActiveSheet().scrollToCell(state.scrollRow, state.scrollCol);
}
} catch {
// Ignore errors when restoring state (e.g. sheet no longer exists).
}
}
function applyContent(univerAPI: FUniver, newContent: string) {
const viewState = saveViewState(univerAPI);
// Dispose the existing workbook.
const existingWorkbook = univerAPI.getActiveWorkbook();
if (existingWorkbook) {
univerAPI.disposeUnit(existingWorkbook.getId());
}
let workbookData: Partial<IWorkbookData> = {};
if (newContent) {
try {
const parsedContent = JSON.parse(newContent) as unknown;
if (parsedContent && typeof parsedContent === "object" && "workbook" in parsedContent) {
const persistedData = parsedContent as PersistedData;
workbookData = persistedData.workbook;
}
} catch (e) {
console.error("Failed to parse spreadsheet content", e);
}
}
const workbook = univerAPI.createWorkbook(workbookData);
if (readOnly) {
workbook.disableSelection();
const permission = workbook.getPermission();
permission.setWorkbookEditPermission(workbook.getId(), false);
permission.setPermissionDialogVisible(false);
}
restoreViewState(workbook, viewState);
if (changeListener.current) {
changeListener.current.dispose();
}
changeListener.current = workbook.onCommandExecuted(command => {
if (command.type !== CommandType.MUTATION) return;
spacedUpdate.scheduleUpdate();
});
}
function isContainerVisible() {
const el = containerRef.current;
if (!el) return false;
return el.offsetWidth > 0 && el.offsetHeight > 0;
}
const spacedUpdate = useEditorSpacedUpdate({
noteType: "spreadsheet",
note,
noteContext,
async getData() {
const univerAPI = apiRef.current;
if (!univerAPI) return undefined;
const workbook = univerAPI.getActiveWorkbook();
if (!workbook) return undefined;
const content = {
version: 1,
workbook: workbook.save()
};
const attachments: SavedData["attachments"] = [];
const canvasEl = containerRef.current?.querySelector<HTMLCanvasElement>("canvas[id]");
if (canvasEl) {
const dataUrl = canvasEl.toDataURL("image/png");
const base64 = dataUrl.split(",")[1];
attachments.push({
role: "image",
title: "spreadsheet-export.png",
mime: "image/png",
content: base64,
position: 0,
encoding: "base64"
});
}
return {
content: JSON.stringify(content),
attachments
};
},
onContentChange(newContent) {
const univerAPI = apiRef.current;
if (!univerAPI) return undefined;
// Defer content application if the container is hidden (zero size),
// since the spreadsheet library cannot calculate layout in that state.
if (!isContainerVisible()) {
pendingContent.current = newContent;
return;
}
pendingContent.current = null;
applyContent(univerAPI, newContent);
},
});
// Apply pending content once the container becomes visible (non-zero size).
useEffect(() => {
if (!containerRef.current) return;
const observer = new ResizeObserver(() => {
if (pendingContent.current === null || !isContainerVisible()) return;
const univerAPI = apiRef.current;
if (!univerAPI) return;
const content = pendingContent.current;
pendingContent.current = null;
applyContent(univerAPI, content);
});
observer.observe(containerRef.current);
return () => observer.disconnect();
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally stable: applyContent/isContainerVisible use refs
}, [ containerRef ]);
useEffect(() => {
return () => {
if (changeListener.current) {
changeListener.current.dispose();
changeListener.current = null;
}
};
}, []);
}

View File

@@ -120,7 +120,11 @@ export default defineConfig(() => ({
environment: "happy-dom",
setupFiles: [
"./src/test/setup.ts"
]
],
reporters: [
"verbose",
["html", { outputFile: "./test-output/vitest/html/index.html" }]
],
},
commonjsOptions: {
transformMixedEsModules: true,

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/desktop",
"version": "0.102.0",
"version": "0.102.1",
"description": "Build your personal knowledge base with Trilium Notes",
"private": true,
"main": "src/main.ts",
@@ -34,8 +34,8 @@
"@types/electron-squirrel-startup": "1.0.2",
"@triliumnext/commons": "workspace:*",
"@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "13.0.1",
"electron": "40.6.1",
"copy-webpack-plugin": "14.0.0",
"electron": "40.8.0",
"@electron-forge/cli": "7.11.1",
"@electron-forge/maker-deb": "7.11.1",
"@electron-forge/maker-dmg": "7.11.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/edit-docs",
"version": "0.102.0",
"version": "0.102.1",
"private": true,
"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": {
@@ -11,9 +11,9 @@
"@triliumnext/client": "workspace:*",
"@triliumnext/desktop": "workspace:*",
"@types/fs-extra": "11.0.4",
"copy-webpack-plugin": "13.0.1",
"electron": "40.6.1",
"fs-extra": "11.3.3"
"copy-webpack-plugin": "14.0.0",
"electron": "40.8.0",
"fs-extra": "11.3.4"
},
"scripts": {
"build": "tsx scripts/build.ts",

View File

@@ -0,0 +1,51 @@
# Nginx Proxy Manager (for testing reverse proxy setups)
## Quick start
1. Start Trilium on the host (default port 8080):
```bash
pnpm run server:start
```
2. Start Nginx Proxy Manager:
```bash
docker compose up -d
```
3. Open the NPM admin panel at **http://localhost:8081** and log in with:
- Email: `admin@example.com`
- Password: `changeme`
(You'll be asked to change these on first login.)
4. Add a proxy host:
- **Domain Names**: `localhost`
- **Scheme**: `http`
- **Forward Hostname / IP**: `host.docker.internal`
- **Forward Port**: `8080`
- Enable **Websockets Support** (required for Trilium sync)
5. Access Trilium through NPM at **http://localhost:8090**.
## With a subpath
To test Trilium behind a subpath (e.g. `/trilium/`), add a **Custom Nginx Configuration** in NPM under the **Advanced** tab of the proxy host:
```nginx
location /trilium/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://host.docker.internal:8080/;
proxy_cookie_path / /trilium/;
proxy_read_timeout 90;
}
```
## Cleanup
```bash
docker compose down -v
```

View File

@@ -0,0 +1,19 @@
services:
nginx-proxy-manager:
image: "jc21/nginx-proxy-manager:latest"
restart: unless-stopped
ports:
# Public HTTP port
- "8090:80"
# Admin panel
- "8081:81"
volumes:
- npm_data:/data
- npm_letsencrypt:/etc/letsencrypt
# Use host network mode so NPM can reach Trilium on the host.
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
npm_data:
npm_letsencrypt:

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/server",
"version": "0.102.0",
"version": "0.102.1",
"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",
@@ -31,7 +31,7 @@
"dependencies": {
"better-sqlite3": "12.6.2",
"html-to-text": "9.0.5",
"node-html-parser": "7.0.2",
"node-html-parser": "7.1.0",
"sucrase": "3.35.1"
},
"devDependencies": {
@@ -55,9 +55,9 @@
"@types/html": "1.0.4",
"@types/ini": "4.1.1",
"@types/mime-types": "3.0.1",
"@types/multer": "2.0.0",
"@types/multer": "2.1.0",
"@types/safe-compare": "1.1.2",
"@types/sanitize-html": "2.16.0",
"@types/sanitize-html": "2.16.1",
"@types/sax": "1.2.7",
"@types/serve-favicon": "2.5.7",
"@types/serve-static": "2.2.0",
@@ -81,24 +81,24 @@
"csrf-csrf": "3.2.2",
"debounce": "3.0.0",
"debug": "4.4.3",
"ejs": "4.0.1",
"electron": "40.6.1",
"ejs": "5.0.1",
"electron": "40.8.0",
"electron-debug": "4.1.0",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
"express": "5.2.1",
"express-http-proxy": "2.1.2",
"express-openid-connect": "2.19.4",
"express-rate-limit": "8.2.1",
"express-rate-limit": "8.3.1",
"express-session": "1.19.0",
"file-uri-to-path": "2.0.0",
"fs-extra": "11.3.3",
"fs-extra": "11.3.4",
"helmet": "8.1.0",
"html": "1.0.0",
"html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.6",
"i18next": "25.8.13",
"https-proxy-agent": "8.0.0",
"i18next": "25.8.17",
"i18next-fs-backend": "2.6.1",
"image-type": "6.0.0",
"ini": "6.0.0",
@@ -106,15 +106,15 @@
"is-svg": "6.1.0",
"jimp": "1.6.0",
"lorem-ipsum": "2.0.8",
"marked": "17.0.3",
"marked": "17.0.4",
"mime-types": "3.0.2",
"multer": "2.1.0",
"multer": "2.1.1",
"normalize-strings": "1.1.1",
"rand-token": "1.0.1",
"safe-compare": "1.1.4",
"sanitize-filename": "1.6.3",
"sanitize-html": "2.17.1",
"sax": "1.4.4",
"sax": "1.5.0",
"serve-favicon": "2.5.1",
"stream-throttle": "0.1.3",
"strip-bom": "5.0.0",
@@ -128,6 +128,6 @@
"vite": "7.3.1",
"ws": "8.19.0",
"xml2js": "0.6.2",
"yauzl": "3.2.0"
"yauzl": "3.2.1"
}
}

View File

@@ -86,8 +86,9 @@ export default async function buildApp() {
app.use(`/robots.txt`, express.static(path.join(publicAssetsDir, "robots.txt")));
app.use(`/icon.png`, express.static(path.join(publicAssetsDir, "icon.png")));
const sessionParser = (await import("./routes/session_parser.js")).default;
const { default: sessionParser, startSessionCleanup } = await import("./routes/session_parser.js");
app.use(sessionParser);
startSessionCleanup();
app.use(favicon(path.join(assetsDir, isDev ? "icon-dev.ico" : "icon.ico")));
if (openID.isOpenIDEnabled())
@@ -98,16 +99,16 @@ export default async function buildApp() {
custom.register(app);
error_handlers.register(app);
// triggers sync timer
await import("./services/sync.js");
const { startSyncTimer } = await import("./services/sync.js");
startSyncTimer();
// triggers backup timer
await import("./services/backup.js");
// trigger consistency checks timer
await import("./services/consistency_checks.js");
const { startConsistencyChecks } = await import("./services/consistency_checks.js");
startConsistencyChecks();
await import("./services/scheduler.js");
const { startScheduler } = await import("./services/scheduler.js");
startScheduler();
startScheduledCleanup();

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 612 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 612 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 652 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -156,7 +156,8 @@
"go-to-next-note-title": "К следующей заметке",
"open-today-journal-note-title": "Открыть сегодняшнюю заметку в журнале",
"zen-mode": "Режим \"Дзен\"",
"command-palette": "Открыть панель команд"
"command-palette": "Открыть панель команд",
"tab-switcher-title": "Переключатель вкладок"
},
"tray": {
"bookmarks": "Закладки",
@@ -313,7 +314,7 @@
"title": "Настройка",
"heading": "Настройка Trilium",
"new-document": "Я новый пользователь и хочу создать новый документ Trilium для своих заметок",
"sync-from-desktop": "У меня уже есть приложение ПК, и я хочу настроить синхронизацию с ним",
"sync-from-desktop": "У меня уже есть настольное приложение, и я хочу настроить синхронизацию с ним",
"sync-from-server": "У меня уже есть сервер, и я хочу настроить синхронизацию с ним",
"init-in-progress": "Идет инициализация документа",
"redirecting": "Вскоре вы будете перенаправлены на страницу приложения."
@@ -397,8 +398,8 @@
"clipped-from": "Эта заметка изначально была вырезана из {{- url}}"
},
"setup_sync-from-desktop": {
"heading": "Синхронизация с приложения ПК",
"description": "Эту настройку необходимо инициировать из приложения для ПК:",
"heading": "Синхронизация с настольной версией",
"description": "Это настройку нужно выполнить с помощью настольной версии:",
"step1": "Откройте приложение Trilium Notes на ПК.",
"step2": "В меню Trilium выберите «Параметры».",
"step3": "Нажмите на категорию «Синхронизация».",

View File

@@ -2,6 +2,24 @@
"keyboard_actions": {
"back-in-note-history": "Gå till föregående anteckning i historiken",
"forward-in-note-history": "Gå till nästa anteckning i historiken",
"open-jump-to-note-dialog": "Öppna \"Hoppa till anteckning\" dialog"
"open-jump-to-note-dialog": "Öppna \"Hoppa till anteckning\" dialog",
"open-command-palette": "Öppna kommandomenyn",
"quick-search": "Öppna snabbsökning",
"search-in-subtree": "Sök anteckningar nedåt i anteckningshierarkin",
"expand-subtree": "Expandera hierarkin under denna anteckning",
"collapse-tree": "Stänger anteckningshierarkin",
"collapse-subtree": "Stänger hierarkin under aktuell anteckning",
"sort-child-notes": "Sortera underordnade anteckningar",
"creating-and-moving-notes": "Skapa och flytta anteckningar",
"create-note-after": "Skapa ny anteckning efter aktiv anteckning",
"create-note-into": "Skapa ny anteckning underordnad aktiv anteckning",
"create-note-into-inbox": "Skapa en anteckning i inboxen (om angiven) eller som daganteckning",
"delete-note": "Radera anteckning",
"move-note-up": "Flytta anteckning uppåt",
"move-note-down": "Flytta anteckning nedåt",
"scroll-to-active-note": "Bläddra i anteckningshierarkin till aktiv anteckning",
"move-note-up-in-hierarchy": "Flytta anteckning uppåt i hierarkin",
"move-note-down-in-hierarchy": "Flytta anteckning neråt i hierarkin",
"edit-note-title": "Hoppa från träd till anteckning och redigera titel"
}
}

View File

@@ -16,6 +16,9 @@
"create-note-into": "Aktif nota bağlı alt not oluştur",
"create-note-after": "Aktif nottan sonra yeni bir not oluştur",
"delete-note": "Notu sil",
"move-note-down": "Notu aşağıya kaydır"
"move-note-down": "Notu aşağıya kaydır",
"create-note-into-inbox": "Eğer tanımlandıysa gelen kutusunda bir not veya günlük not oluşturun",
"move-note-up-in-hierarchy": "Notu hiyerarşide yukarı taşı",
"move-note-down-in-hierarchy": "Notu hiyerarşide aşağı taşı"
}
}

View File

@@ -1,14 +1,15 @@
import type { AttachmentRow } from "@triliumnext/commons";
import type { Router } from "express";
import becca from "../becca/becca.js";
import utils from "../services/utils.js";
import eu from "./etapi_utils.js";
import type { ValidatorMap } from "./etapi-interface.js";
import mappers from "./mappers.js";
import v from "./validators.js";
import utils from "../services/utils.js";
import type { Router } from "express";
import type { AttachmentRow } from "@triliumnext/commons";
import type { ValidatorMap } from "./etapi-interface.js";
function register(router: Router) {
eu.route(router, "get", "/etapi/notes/:noteId/attachments", (req, res, next) => {
eu.route<{ noteId: string }>(router, "get", "/etapi/notes/:noteId/attachments", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const attachments = note.getAttachments();
res.json(attachments.map((attachment) => mappers.mapAttachmentToPojo(attachment)));
@@ -41,7 +42,7 @@ function register(router: Router) {
}
});
eu.route(router, "get", "/etapi/attachments/:attachmentId", (req, res, next) => {
eu.route<{ attachmentId: string }>(router, "get", "/etapi/attachments/:attachmentId", (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
res.json(mappers.mapAttachmentToPojo(attachment));
@@ -54,7 +55,7 @@ function register(router: Router) {
position: [v.notNull, v.isInteger]
};
eu.route(router, "patch", "/etapi/attachments/:attachmentId", (req, res, next) => {
eu.route<{ attachmentId: string }>(router, "patch", "/etapi/attachments/:attachmentId", (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
if (attachment.isProtected) {
@@ -67,7 +68,7 @@ function register(router: Router) {
res.json(mappers.mapAttachmentToPojo(attachment));
});
eu.route(router, "get", "/etapi/attachments/:attachmentId/content", (req, res, next) => {
eu.route<{ attachmentId: string }>(router, "get", "/etapi/attachments/:attachmentId/content", (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
if (attachment.isProtected) {
@@ -84,7 +85,7 @@ function register(router: Router) {
res.send(attachment.getContent());
});
eu.route(router, "put", "/etapi/attachments/:attachmentId/content", (req, res, next) => {
eu.route<{ attachmentId: string }>(router, "put", "/etapi/attachments/:attachmentId/content", (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
if (attachment.isProtected) {
@@ -96,7 +97,7 @@ function register(router: Router) {
return res.sendStatus(204);
});
eu.route(router, "delete", "/etapi/attachments/:attachmentId", (req, res, next) => {
eu.route<{ attachmentId: string }>(router, "delete", "/etapi/attachments/:attachmentId", (req, res, next) => {
const attachment = becca.getAttachment(req.params.attachmentId);
if (!attachment) {

View File

@@ -1,14 +1,15 @@
import becca from "../becca/becca.js";
import eu from "./etapi_utils.js";
import mappers from "./mappers.js";
import attributeService from "../services/attributes.js";
import v from "./validators.js";
import type { Router } from "express";
import type { AttributeRow } from "@triliumnext/commons";
import type { Router } from "express";
import becca from "../becca/becca.js";
import attributeService from "../services/attributes.js";
import eu from "./etapi_utils.js";
import type { ValidatorMap } from "./etapi-interface.js";
import mappers from "./mappers.js";
import v from "./validators.js";
function register(router: Router) {
eu.route(router, "get", "/etapi/attributes/:attributeId", (req, res, next) => {
eu.route<{ attributeId: string }>(router, "get", "/etapi/attributes/:attributeId", (req, res, next) => {
const attribute = eu.getAndCheckAttribute(req.params.attributeId);
res.json(mappers.mapAttributeToPojo(attribute));
@@ -51,7 +52,7 @@ function register(router: Router) {
position: [v.notNull, v.isInteger]
};
eu.route(router, "patch", "/etapi/attributes/:attributeId", (req, res, next) => {
eu.route<{ attributeId: string }>(router, "patch", "/etapi/attributes/:attributeId", (req, res, next) => {
const attribute = eu.getAndCheckAttribute(req.params.attributeId);
if (attribute.type === "label") {
@@ -67,7 +68,7 @@ function register(router: Router) {
res.json(mappers.mapAttributeToPojo(attribute));
});
eu.route(router, "delete", "/etapi/attributes/:attributeId", (req, res, next) => {
eu.route<{ attributeId: string }>(router, "delete", "/etapi/attributes/:attributeId", (req, res, next) => {
const attribute = becca.getAttribute(req.params.attributeId);
if (!attribute) {

View File

@@ -1,10 +1,10 @@
import type { Router } from "express";
import eu from "./etapi_utils.js";
import backupService from "../services/backup.js";
import eu from "./etapi_utils.js";
function register(router: Router) {
eu.route(router, "put", "/etapi/backup/:backupName", (req, res, next) => {
eu.route<{ backupName: string }>(router, "put", "/etapi/backup/:backupName", (req, res, next) => {
backupService.backupNow(req.params.backupName)
.then(() => res.sendStatus(204))
.catch(() => res.sendStatus(500));

View File

@@ -1,15 +1,15 @@
import type { BranchRow } from "@triliumnext/commons";
import type { Router } from "express";
import becca from "../becca/becca.js";
import eu from "./etapi_utils.js";
import mappers from "./mappers.js";
import BBranch from "../becca/entities/bbranch.js";
import entityChangesService from "../services/entity_changes.js";
import eu from "./etapi_utils.js";
import mappers from "./mappers.js";
import v from "./validators.js";
import type { BranchRow } from "@triliumnext/commons";
function register(router: Router) {
eu.route(router, "get", "/etapi/branches/:branchId", (req, res, next) => {
eu.route<{ branchId: string }>(router, "get", "/etapi/branches/:branchId", (req, res, next) => {
const branch = eu.getAndCheckBranch(req.params.branchId);
res.json(mappers.mapBranchToPojo(branch));
@@ -37,15 +37,15 @@ function register(router: Router) {
existing.save();
return res.status(200).json(mappers.mapBranchToPojo(existing));
} else {
try {
const branch = new BBranch(params).save();
}
try {
const branch = new BBranch(params).save();
res.status(201).json(mappers.mapBranchToPojo(branch));
} catch (e: any) {
throw new eu.EtapiError(400, eu.GENERIC_CODE, e.message);
}
res.status(201).json(mappers.mapBranchToPojo(branch));
} catch (e: any) {
throw new eu.EtapiError(400, eu.GENERIC_CODE, e.message);
}
});
const ALLOWED_PROPERTIES_FOR_PATCH = {
@@ -54,7 +54,7 @@ function register(router: Router) {
isExpanded: [v.notNull, v.isBoolean]
};
eu.route(router, "patch", "/etapi/branches/:branchId", (req, res, next) => {
eu.route<{ branchId: string }>(router, "patch", "/etapi/branches/:branchId", (req, res, next) => {
const branch = eu.getAndCheckBranch(req.params.branchId);
eu.validateAndPatch(branch, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
@@ -63,7 +63,7 @@ function register(router: Router) {
res.json(mappers.mapBranchToPojo(branch));
});
eu.route(router, "delete", "/etapi/branches/:branchId", (req, res, next) => {
eu.route<{ branchId: string }>(router, "delete", "/etapi/branches/:branchId", (req, res, next) => {
const branch = becca.getBranch(req.params.branchId);
if (!branch) {
@@ -75,7 +75,7 @@ function register(router: Router) {
res.sendStatus(204);
});
eu.route(router, "post", "/etapi/refresh-note-ordering/:parentNoteId", (req, res, next) => {
eu.route<{ parentNoteId: string }>(router, "post", "/etapi/refresh-note-ordering/:parentNoteId", (req, res, next) => {
eu.getAndCheckNote(req.params.parentNoteId);
entityChangesService.putNoteReorderingEntityChange(req.params.parentNoteId, "etapi");

View File

@@ -1,12 +1,14 @@
import cls from "../services/cls.js";
import sql from "../services/sql.js";
import log from "../services/log.js";
import becca from "../becca/becca.js";
import etapiTokenService from "../services/etapi_tokens.js";
import config from "../services/config.js";
import type { NextFunction, Request, RequestHandler, Response, Router } from "express";
import type { ValidatorMap } from "./etapi-interface.js";
import type { ParamsDictionary } from "express-serve-static-core";
import becca from "../becca/becca.js";
import type { ApiRequestHandler, SyncRouteRequestHandler } from "../routes/route_api.js";
import cls from "../services/cls.js";
import config from "../services/config.js";
import etapiTokenService from "../services/etapi_tokens.js";
import log from "../services/log.js";
import sql from "../services/sql.js";
import type { ValidatorMap } from "./etapi-interface.js";
const GENERIC_CODE = "GENERIC";
type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head";
@@ -35,8 +37,8 @@ function sendError(res: Response, statusCode: number, code: string, message: str
.send(
JSON.stringify({
status: statusCode,
code: code,
message: message
code,
message
})
);
}
@@ -49,7 +51,7 @@ function checkEtapiAuth(req: Request, res: Response, next: NextFunction) {
}
}
function processRequest(req: Request, res: Response, routeHandler: ApiRequestHandler, next: NextFunction, method: string, path: string) {
function processRequest<P extends ParamsDictionary>(req: Request<P>, res: Response, routeHandler: ApiRequestHandler<P>, next: NextFunction, method: string, path: string) {
try {
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
@@ -73,12 +75,12 @@ function processRequest(req: Request, res: Response, routeHandler: ApiRequestHan
}
}
function route(router: Router, method: HttpMethod, path: string, routeHandler: SyncRouteRequestHandler) {
router[method](path, checkEtapiAuth, (req: Request, res: Response, next: NextFunction) => processRequest(req, res, routeHandler, next, method, path));
function route<P extends ParamsDictionary>(router: Router, method: HttpMethod, path: string, routeHandler: SyncRouteRequestHandler<P>) {
router[method](path, checkEtapiAuth, (req: Request<P>, res: Response, next: NextFunction) => processRequest(req, res, routeHandler, next, method, path));
}
function NOT_AUTHENTICATED_ROUTE(router: Router, method: HttpMethod, path: string, middleware: RequestHandler[], routeHandler: SyncRouteRequestHandler) {
router[method](path, ...middleware, (req: Request, res: Response, next: NextFunction) => processRequest(req, res, routeHandler, next, method, path));
function NOT_AUTHENTICATED_ROUTE<P extends ParamsDictionary>(router: Router, method: HttpMethod, path: string, middleware: RequestHandler[], routeHandler: SyncRouteRequestHandler<P>) {
router[method](path, ...middleware, (req: Request<P>, res: Response, next: NextFunction) => processRequest(req, res, routeHandler, next, method, path));
}
function getAndCheckNote(noteId: string) {
@@ -86,9 +88,8 @@ function getAndCheckNote(noteId: string) {
if (note) {
return note;
} else {
throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found.`);
}
throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found.`);
}
function getAndCheckAttachment(attachmentId: string) {
@@ -96,9 +97,8 @@ function getAndCheckAttachment(attachmentId: string) {
if (attachment) {
return attachment;
} else {
throw new EtapiError(404, "ATTACHMENT_NOT_FOUND", `Attachment '${attachmentId}' not found.`);
}
throw new EtapiError(404, "ATTACHMENT_NOT_FOUND", `Attachment '${attachmentId}' not found.`);
}
function getAndCheckBranch(branchId: string) {
@@ -106,9 +106,8 @@ function getAndCheckBranch(branchId: string) {
if (branch) {
return branch;
} else {
throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found.`);
}
throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found.`);
}
function getAndCheckAttribute(attributeId: string) {
@@ -116,9 +115,8 @@ function getAndCheckAttribute(attributeId: string) {
if (attribute) {
return attribute;
} else {
throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found.`);
}
throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found.`);
}
function getAndCheckRevision(revisionId: string) {
@@ -126,9 +124,8 @@ function getAndCheckRevision(revisionId: string) {
if (revision) {
return revision;
} else {
throw new EtapiError(404, "REVISION_NOT_FOUND", `Revision '${revisionId}' not found.`);
}
throw new EtapiError(404, "REVISION_NOT_FOUND", `Revision '${revisionId}' not found.`);
}
function validateAndPatch(target: any, source: any, allowedProperties: ValidatorMap) {

View File

@@ -1,20 +1,21 @@
import becca from "../becca/becca.js";
import utils from "../services/utils.js";
import eu from "./etapi_utils.js";
import mappers from "./mappers.js";
import noteService from "../services/notes.js";
import TaskContext from "../services/task_context.js";
import v from "./validators.js";
import searchService from "../services/search/services/search.js";
import SearchContext from "../services/search/search_context.js";
import zipExportService from "../services/export/zip.js";
import zipImportService from "../services/import/zip.js";
import type { Request, Router } from "express";
import type { ParsedQs } from "qs";
import type { NoteParams } from "../services/note-interface.js";
import type { SearchParams } from "../services/search/services/types.js";
import type { ValidatorMap } from "./etapi-interface.js";
import becca from "../becca/becca.js";
import zipExportService from "../services/export/zip.js";
import type { ExportFormat } from "../services/export/zip/abstract_provider.js";
import zipImportService from "../services/import/zip.js";
import type { NoteParams } from "../services/note-interface.js";
import noteService from "../services/notes.js";
import SearchContext from "../services/search/search_context.js";
import searchService from "../services/search/services/search.js";
import type { SearchParams } from "../services/search/services/types.js";
import TaskContext from "../services/task_context.js";
import utils from "../services/utils.js";
import eu from "./etapi_utils.js";
import type { ValidatorMap } from "./etapi-interface.js";
import mappers from "./mappers.js";
import v from "./validators.js";
function register(router: Router) {
eu.route(router, "get", "/etapi/notes", (req, res, next) => {
@@ -41,7 +42,7 @@ function register(router: Router) {
res.json(resp);
});
eu.route(router, "get", "/etapi/notes/:noteId", (req, res, next) => {
eu.route<{ noteId: string }>(router, "get", "/etapi/notes/:noteId", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
res.json(mappers.mapNoteToPojo(note));
@@ -86,7 +87,7 @@ function register(router: Router) {
utcDateCreated: [v.notNull, v.isString, v.isUtcDateTime]
};
eu.route(router, "patch", "/etapi/notes/:noteId", (req, res, next) => {
eu.route<{ noteId: string }>(router, "patch", "/etapi/notes/:noteId", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
if (note.isProtected) {
@@ -100,7 +101,7 @@ function register(router: Router) {
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, "delete", "/etapi/notes/:noteId", (req, res, next) => {
eu.route<{ noteId: string }>(router, "delete", "/etapi/notes/:noteId", (req, res, next) => {
const { noteId } = req.params;
const note = becca.getNote(noteId);
@@ -114,7 +115,7 @@ function register(router: Router) {
res.sendStatus(204);
});
eu.route(router, "get", "/etapi/notes/:noteId/content", (req, res, next) => {
eu.route<{ noteId: string }>(router, "get", "/etapi/notes/:noteId/content", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
if (note.isProtected) {
@@ -131,7 +132,7 @@ function register(router: Router) {
res.send(note.getContent());
});
eu.route(router, "put", "/etapi/notes/:noteId/content", (req, res, next) => {
eu.route<{ noteId: string }>(router, "put", "/etapi/notes/:noteId/content", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
if (note.isProtected) {
@@ -146,7 +147,7 @@ function register(router: Router) {
return res.sendStatus(204);
});
eu.route(router, "get", "/etapi/notes/:noteId/export", (req, res, next) => {
eu.route<{ noteId: string }>(router, "get", "/etapi/notes/:noteId/export", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const format = req.query.format || "html";
@@ -163,7 +164,7 @@ function register(router: Router) {
zipExportService.exportToZip(taskContext, branch, format as ExportFormat, res);
});
eu.route(router, "post", "/etapi/notes/:noteId/import", (req, res, next) => {
eu.route<{ noteId: string }>(router, "post", "/etapi/notes/:noteId/import", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const taskContext = new TaskContext("no-progress-reporting", "importNotes", null);
@@ -175,7 +176,7 @@ function register(router: Router) {
}); // we need better error handling here, async errors won't be properly processed.
});
eu.route(router, "post", "/etapi/notes/:noteId/revision", (req, res, next) => {
eu.route<{ noteId: string }>(router, "post", "/etapi/notes/:noteId/revision", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
note.saveRevision();
@@ -183,7 +184,7 @@ function register(router: Router) {
return res.sendStatus(204);
});
eu.route(router, "get", "/etapi/notes/:noteId/attachments", (req, res, next) => {
eu.route<{ noteId: string }>(router, "get", "/etapi/notes/:noteId/attachments", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const attachments = note.getAttachments();

View File

@@ -1,13 +1,14 @@
import type { NoteRow, RecentChangeRow } from "@triliumnext/commons";
import type { Router } from "express";
import becca from "../becca/becca.js";
import noteService from "../services/notes.js";
import protectedSessionService from "../services/protected_session.js";
import sql from "../services/sql.js";
import TaskContext from "../services/task_context.js";
import utils from "../services/utils.js";
import eu from "./etapi_utils.js";
import mappers from "./mappers.js";
import noteService from "../services/notes.js";
import TaskContext from "../services/task_context.js";
import protectedSessionService from "../services/protected_session.js";
import utils from "../services/utils.js";
import type { Router } from "express";
import type { NoteRow, RecentChangeRow } from "@triliumnext/commons";
function register(router: Router) {
// GET /etapi/notes/history - must be registered before /etapi/notes/:noteId routes
@@ -130,7 +131,7 @@ function register(router: Router) {
});
// GET /etapi/notes/:noteId/revisions - List all revisions for a note
eu.route(router, "get", "/etapi/notes/:noteId/revisions", (req, res, next) => {
eu.route<{ noteId: string }>(router, "get", "/etapi/notes/:noteId/revisions", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const revisions = becca.getRevisionsFromQuery(
@@ -146,7 +147,7 @@ function register(router: Router) {
});
// POST /etapi/notes/:noteId/undelete - Restore a deleted note
eu.route(router, "post", "/etapi/notes/:noteId/undelete", (req, res, next) => {
eu.route<{ noteId: string }>(router, "post", "/etapi/notes/:noteId/undelete", (req, res, next) => {
const { noteId } = req.params;
const noteRow = sql.getRow<NoteRow | null>("SELECT * FROM notes WHERE noteId = ?", [noteId]);
@@ -172,7 +173,7 @@ function register(router: Router) {
});
// GET /etapi/revisions/:revisionId - Get revision metadata
eu.route(router, "get", "/etapi/revisions/:revisionId", (req, res, next) => {
eu.route<{ revisionId: string }>(router, "get", "/etapi/revisions/:revisionId", (req, res, next) => {
const revision = eu.getAndCheckRevision(req.params.revisionId);
if (revision.isProtected) {
@@ -183,7 +184,7 @@ function register(router: Router) {
});
// GET /etapi/revisions/:revisionId/content - Get revision content
eu.route(router, "get", "/etapi/revisions/:revisionId/content", (req, res, next) => {
eu.route<{ revisionId: string }>(router, "get", "/etapi/revisions/:revisionId/content", (req, res, next) => {
const revision = eu.getAndCheckRevision(req.params.revisionId);
if (revision.isProtected) {

View File

@@ -1,8 +1,9 @@
import specialNotesService from "../services/special_notes.js";
import type { Router } from "express";
import dateNotesService from "../services/date_notes.js";
import specialNotesService from "../services/special_notes.js";
import eu from "./etapi_utils.js";
import mappers from "./mappers.js";
import type { Router } from "express";
const getDateInvalidError = (date: string) => new eu.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`);
const getWeekInvalidError = (week: string) => new eu.EtapiError(400, "WEEK_INVALID", `Week "${week}" is not valid.`);
@@ -15,7 +16,7 @@ function isValidDate(date: string) {
}
function register(router: Router) {
eu.route(router, "get", "/etapi/inbox/:date", (req, res, next) => {
eu.route<{ date: string }>(router, "get", "/etapi/inbox/:date", (req, res, next) => {
const { date } = req.params;
if (!isValidDate(date)) {
@@ -25,7 +26,7 @@ function register(router: Router) {
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, "get", "/etapi/calendar/days/:date", (req, res, next) => {
eu.route<{ date: string }>(router, "get", "/etapi/calendar/days/:date", (req, res, next) => {
const { date } = req.params;
if (!isValidDate(date)) {
@@ -36,7 +37,7 @@ function register(router: Router) {
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, "get", "/etapi/calendar/week-first-day/:date", (req, res, next) => {
eu.route<{ date: string }>(router, "get", "/etapi/calendar/week-first-day/:date", (req, res, next) => {
const { date } = req.params;
if (!isValidDate(date)) {
@@ -47,7 +48,7 @@ function register(router: Router) {
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, "get", "/etapi/calendar/weeks/:week", (req, res, next) => {
eu.route<{ week: string }>(router, "get", "/etapi/calendar/weeks/:week", (req, res, next) => {
const { week } = req.params;
if (!/[0-9]{4}-W[0-9]{2}/.test(week)) {
@@ -63,7 +64,7 @@ function register(router: Router) {
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, "get", "/etapi/calendar/months/:month", (req, res, next) => {
eu.route<{ month: string }>(router, "get", "/etapi/calendar/months/:month", (req, res, next) => {
const { month } = req.params;
if (!/[0-9]{4}-[0-9]{2}/.test(month)) {
@@ -74,7 +75,7 @@ function register(router: Router) {
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, "get", "/etapi/calendar/years/:year", (req, res, next) => {
eu.route<{ year: string }>(router, "get", "/etapi/calendar/years/:year", (req, res, next) => {
const { year } = req.params;
if (!/[0-9]{4}/.test(year)) {

View File

@@ -1,29 +1,30 @@
import becca from "../../becca/becca.js";
import blobService from "../../services/blob.js";
import ValidationError from "../../errors/validation_error.js";
import imageService from "../../services/image.js";
import type { Request } from "express";
import { ConvertAttachmentToNoteResponse } from "@triliumnext/commons";
import type { Request } from "express";
function getAttachmentBlob(req: Request) {
import becca from "../../becca/becca.js";
import ValidationError from "../../errors/validation_error.js";
import blobService from "../../services/blob.js";
import imageService from "../../services/image.js";
function getAttachmentBlob(req: Request<{ attachmentId: string }>) {
const preview = req.query.preview === "true";
return blobService.getBlobPojo("attachments", req.params.attachmentId, { preview });
}
function getAttachments(req: Request) {
function getAttachments(req: Request<{ noteId: string }>) {
const note = becca.getNoteOrThrow(req.params.noteId);
return note.getAttachments();
}
function getAttachment(req: Request) {
function getAttachment(req: Request<{ attachmentId: string }>) {
const { attachmentId } = req.params;
return becca.getAttachmentOrThrow(attachmentId);
}
function getAllAttachments(req: Request) {
function getAllAttachments(req: Request<{ attachmentId: string }>) {
const { attachmentId } = req.params;
// one particular attachment is requested, but return all note's attachments
@@ -31,10 +32,10 @@ function getAllAttachments(req: Request) {
return attachment.getNote()?.getAttachments() || [];
}
function saveAttachment(req: Request) {
function saveAttachment(req: Request<{ noteId: string }>) {
const { noteId } = req.params;
const { attachmentId, role, mime, title, content } = req.body;
const matchByQuery = req.query.matchBy
const matchByQuery = req.query.matchBy;
const isValidMatchBy = (typeof matchByQuery === "string") && (matchByQuery === "attachmentId" || matchByQuery === "title");
const matchBy = isValidMatchBy ? matchByQuery : undefined;
@@ -42,7 +43,7 @@ function saveAttachment(req: Request) {
note.saveAttachment({ attachmentId, role, mime, title, content }, matchBy);
}
function uploadAttachment(req: Request) {
function uploadAttachment(req: Request<{ noteId: string }>) {
const { noteId } = req.params;
const { file } = req;
@@ -76,7 +77,7 @@ function uploadAttachment(req: Request) {
};
}
function renameAttachment(req: Request) {
function renameAttachment(req: Request<{ attachmentId: string }>) {
const { title } = req.body;
const { attachmentId } = req.params;
@@ -90,7 +91,7 @@ function renameAttachment(req: Request) {
attachment.save();
}
function deleteAttachment(req: Request) {
function deleteAttachment(req: Request<{ attachmentId: string }>) {
const { attachmentId } = req.params;
const attachment = becca.getAttachment(attachmentId);
@@ -100,7 +101,7 @@ function deleteAttachment(req: Request) {
}
}
function convertAttachmentToNote(req: Request) {
function convertAttachmentToNote(req: Request<{ attachmentId: string }>) {
const { attachmentId } = req.params;
const attachment = becca.getAttachmentOrThrow(attachmentId);

View File

@@ -1,21 +1,22 @@
"use strict";
import sql from "../../services/sql.js";
import log from "../../services/log.js";
import attributeService from "../../services/attributes.js";
import BAttribute from "../../becca/entities/battribute.js";
import becca from "../../becca/becca.js";
import ValidationError from "../../errors/validation_error.js";
import type { Request } from "express";
import { UpdateAttributeResponse } from "@triliumnext/commons";
import type { Request } from "express";
function getEffectiveNoteAttributes(req: Request) {
import becca from "../../becca/becca.js";
import BAttribute from "../../becca/entities/battribute.js";
import ValidationError from "../../errors/validation_error.js";
import attributeService from "../../services/attributes.js";
import log from "../../services/log.js";
import sql from "../../services/sql.js";
function getEffectiveNoteAttributes(req: Request<{ noteId: string }>) {
const note = becca.getNote(req.params.noteId);
return note?.getAttributes();
}
function updateNoteAttribute(req: Request) {
function updateNoteAttribute(req: Request<{ noteId: string }>) {
const noteId = req.params.noteId;
const body = req.body;
@@ -47,7 +48,7 @@ function updateNoteAttribute(req: Request) {
}
attribute = new BAttribute({
noteId: noteId,
noteId,
name: body.name,
type: body.type,
isInheritable: body.isInheritable
@@ -96,7 +97,7 @@ function addNoteAttribute(req: Request) {
new BAttribute({ ...body, noteId }).save();
}
function deleteNoteAttribute(req: Request) {
function deleteNoteAttribute(req: Request<{ noteId: string; attributeId: string }>) {
const noteId = req.params.noteId;
const attributeId = req.params.attributeId;
@@ -111,7 +112,7 @@ function deleteNoteAttribute(req: Request) {
}
}
function updateNoteAttributes(req: Request) {
function updateNoteAttributes(req: Request<{ noteId: string }>) {
const noteId = req.params.noteId;
const incomingAttributes = req.body;
@@ -193,7 +194,7 @@ function getValuesForAttribute(req: Request) {
return sql.getColumn("SELECT DISTINCT value FROM attributes WHERE isDeleted = 0 AND name = ? AND type = 'label' AND value != '' ORDER BY value", [attributeName]);
}
function createRelation(req: Request) {
function createRelation(req: Request<{ noteId: string; targetNoteId: string; name: string }>) {
const sourceNoteId = req.params.noteId;
const targetNoteId = req.params.targetNoteId;
const name = req.params.name;
@@ -208,7 +209,7 @@ function createRelation(req: Request) {
if (!attribute) {
attribute = new BAttribute({
noteId: sourceNoteId,
name: name,
name,
type: "relation",
value: targetNoteId
}).save();

View File

@@ -1,24 +1,23 @@
"use strict";
import sql from "../../services/sql.js";
import utils from "../../services/utils.js";
import entityChangesService from "../../services/entity_changes.js";
import treeService from "../../services/tree.js";
import eraseService from "../../services/erase.js";
import becca from "../../becca/becca.js";
import TaskContext from "../../services/task_context.js";
import branchService from "../../services/branches.js";
import log from "../../services/log.js";
import ValidationError from "../../errors/validation_error.js";
import eventService from "../../services/events.js";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import ValidationError from "../../errors/validation_error.js";
import branchService from "../../services/branches.js";
import entityChangesService from "../../services/entity_changes.js";
import eraseService from "../../services/erase.js";
import eventService from "../../services/events.js";
import log from "../../services/log.js";
import sql from "../../services/sql.js";
import TaskContext from "../../services/task_context.js";
import treeService from "../../services/tree.js";
import utils from "../../services/utils.js";
/**
* Code in this file deals with moving and cloning branches. The relationship between note and parent note is unique
* for not deleted branches. There may be multiple deleted note-parent note relationships.
*/
function moveBranchToParent(req: Request) {
function moveBranchToParent(req: Request<{ branchId: string, parentBranchId: string }>) {
const { branchId, parentBranchId } = req.params;
const branchToMove = becca.getBranch(branchId);
@@ -31,7 +30,7 @@ function moveBranchToParent(req: Request) {
return branchService.moveBranchToBranch(branchToMove, targetParentBranch, branchId);
}
function moveBranchBeforeNote(req: Request) {
function moveBranchBeforeNote(req: Request<{ branchId: string, beforeBranchId: string }>) {
const { branchId, beforeBranchId } = req.params;
const branchToMove = becca.getBranchOrThrow(branchId);
@@ -79,7 +78,7 @@ function moveBranchBeforeNote(req: Request) {
return { success: true };
}
function moveBranchAfterNote(req: Request) {
function moveBranchAfterNote(req: Request<{ branchId: string, afterBranchId: string }>) {
const { branchId, afterBranchId } = req.params;
const branchToMove = becca.getBranchOrThrow(branchId);
@@ -128,7 +127,7 @@ function moveBranchAfterNote(req: Request) {
return { success: true };
}
function setExpanded(req: Request) {
function setExpanded(req: Request<{ branchId: string, expanded: string }>) {
const { branchId } = req.params;
const expanded = parseInt(req.params.expanded);
@@ -150,7 +149,7 @@ function setExpanded(req: Request) {
}
}
function setExpandedForSubtree(req: Request) {
function setExpandedForSubtree(req: Request<{ branchId: string, expanded: string }>) {
const { branchId } = req.params;
const expanded = parseInt(req.params.expanded);
@@ -232,7 +231,7 @@ function setExpandedForSubtree(req: Request) {
* - session: []
* tags: ["data"]
*/
function deleteBranch(req: Request) {
function deleteBranch(req: Request<{ branchId: string }>) {
const last = req.query.last === "true";
const eraseNotes = req.query.eraseNotes === "true";
const branch = becca.getBranchOrThrow(req.params.branchId);
@@ -256,11 +255,11 @@ function deleteBranch(req: Request) {
}
return {
noteDeleted: noteDeleted
noteDeleted
};
}
function setPrefix(req: Request) {
function setPrefix(req: Request<{ branchId: string }>) {
const branchId = req.params.branchId;
//TriliumNextTODO: req.body arrives as string, so req.body.prefix will be undefined did the code below ever even work?
const prefix = utils.isEmptyOrWhitespace(req.body.prefix) ? null : req.body.prefix;
@@ -272,7 +271,7 @@ function setPrefix(req: Request) {
function setPrefixBatch(req: Request) {
const { branchIds, prefix } = req.body;
if (!Array.isArray(branchIds)) {
throw new ValidationError("branchIds must be an array");
}

View File

@@ -38,7 +38,7 @@ async function addClipping(req: Request) {
if (!clippingNote) {
clippingNote = noteService.createNewNote({
parentNoteId: clipperInbox.noteId,
title: title,
title,
content: "",
type: "text"
}).note;
@@ -188,7 +188,7 @@ export function processContent(images: Image[], note: BNote, content: string) {
return rewrittenContent;
}
function openNote(req: Request) {
function openNote(req: Request<{ noteId: string }>) {
if (utils.isElectron) {
ws.sendMessageToAllClients({
type: "openNote",
@@ -198,11 +198,11 @@ function openNote(req: Request) {
return {
result: "ok"
};
} else {
return {
result: "open-in-browser"
};
}
}
return {
result: "open-in-browser"
};
}
function handshake() {
@@ -212,7 +212,7 @@ function handshake() {
};
}
async function findNotesByUrl(req: Request) {
async function findNotesByUrl(req: Request<{ noteUrl: string }>) {
const pageUrl = req.params.noteUrl;
const clipperInbox = await getClipperInboxNote();
const foundPage = findClippingNote(clipperInbox, pageUrl, null);

View File

@@ -1,29 +1,28 @@
"use strict";
import type { Request } from "express";
import cloningService from "../../services/cloning.js";
function cloneNoteToBranch(req: Request) {
function cloneNoteToBranch(req: Request<{ noteId: string; parentBranchId: string }>) {
const { noteId, parentBranchId } = req.params;
const { prefix } = req.body;
return cloningService.cloneNoteToBranch(noteId, parentBranchId, prefix);
}
function cloneNoteToParentNote(req: Request) {
function cloneNoteToParentNote(req: Request<{ noteId: string; parentNoteId: string }>) {
const { noteId, parentNoteId } = req.params;
const { prefix } = req.body;
return cloningService.cloneNoteToParentNote(noteId, parentNoteId, prefix);
}
function cloneNoteAfter(req: Request) {
function cloneNoteAfter(req: Request<{ noteId: string; afterBranchId: string }>) {
const { noteId, afterBranchId } = req.params;
return cloningService.cloneNoteAfter(noteId, afterBranchId);
}
function toggleNoteInParent(req: Request) {
function toggleNoteInParent(req: Request<{ noteId: string; parentNoteId: string; present: string }>) {
const { noteId, parentNoteId, present } = req.params;
return cloningService.toggleNoteInParent(present === "true", noteId, parentNoteId);

View File

@@ -1,6 +1,7 @@
import type { Request } from "express";
import etapiTokenService from "../../services/etapi_tokens.js";
import { EtapiToken, PostTokensResponse } from "@triliumnext/commons";
import type { Request } from "express";
import etapiTokenService from "../../services/etapi_tokens.js";
function getTokens() {
const tokens = etapiTokenService.getTokens();
@@ -14,11 +15,11 @@ function createToken(req: Request) {
return etapiTokenService.createToken(req.body.tokenName) satisfies PostTokensResponse;
}
function patchToken(req: Request) {
function patchToken(req: Request<{ etapiTokenId: string }>) {
etapiTokenService.renameToken(req.params.etapiTokenId, req.body.name);
}
function deleteToken(req: Request) {
function deleteToken(req: Request<{ etapiTokenId: string }>) {
etapiTokenService.deleteToken(req.params.etapiTokenId);
}

View File

@@ -1,17 +1,16 @@
"use strict";
import zipExportService from "../../services/export/zip.js";
import singleExportService from "../../services/export/single.js";
import opmlExportService from "../../services/export/opml.js";
import becca from "../../becca/becca.js";
import TaskContext from "../../services/task_context.js";
import log from "../../services/log.js";
import NotFoundError from "../../errors/not_found_error.js";
import type { Request, Response } from "express";
import becca from "../../becca/becca.js";
import NotFoundError from "../../errors/not_found_error.js";
import ValidationError from "../../errors/validation_error.js";
import opmlExportService from "../../services/export/opml.js";
import singleExportService from "../../services/export/single.js";
import zipExportService from "../../services/export/zip.js";
import log from "../../services/log.js";
import TaskContext from "../../services/task_context.js";
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
function exportBranch(req: Request, res: Response) {
function exportBranch(req: Request<{ branchId: string; type: string; format: string; version: string; taskId: string }>, res: Response) {
const { branchId, type, format, version, taskId } = req.params;
const branch = becca.getBranch(branchId);

View File

@@ -1,5 +1,3 @@
import chokidar from "chokidar";
import type { Request, Response } from "express";
import fs from "fs";
@@ -17,7 +15,7 @@ import protectedSessionService from "../../services/protected_session.js";
import utils from "../../services/utils.js";
import ws from "../../services/ws.js";
function updateFile(req: Request) {
function updateFile(req: Request<{ noteId: string }>) {
const note = becca.getNoteOrThrow(req.params.noteId);
const file = req.file;
@@ -46,7 +44,7 @@ function updateFile(req: Request) {
};
}
function updateAttachment(req: Request) {
function updateAttachment(req: Request<{ attachmentId: string }>) {
const attachment = becca.getAttachmentOrThrow(req.params.attachmentId);
const file = req.file;
if (!file) {
@@ -103,20 +101,20 @@ function downloadAttachmentInt(attachmentId: string, res: Response, contentDispo
return downloadData(attachment, res, contentDisposition);
}
const downloadFile = (req: Request, res: Response) => downloadNoteInt(req.params.noteId, res, true);
const openFile = (req: Request, res: Response) => downloadNoteInt(req.params.noteId, res, false);
const downloadFile = (req: Request<{ noteId: string }>, res: Response) => downloadNoteInt(req.params.noteId, res, true);
const openFile = (req: Request<{ noteId: string }>, res: Response) => downloadNoteInt(req.params.noteId, res, false);
const downloadAttachment = (req: Request, res: Response) => downloadAttachmentInt(req.params.attachmentId, res, true);
const openAttachment = (req: Request, res: Response) => downloadAttachmentInt(req.params.attachmentId, res, false);
const downloadAttachment = (req: Request<{ attachmentId: string }>, res: Response) => downloadAttachmentInt(req.params.attachmentId, res, true);
const openAttachment = (req: Request<{ attachmentId: string }>, res: Response) => downloadAttachmentInt(req.params.attachmentId, res, false);
function fileContentProvider(req: Request) {
function fileContentProvider(req: Request<{ noteId: string }>) {
// Read the file name from route params.
const note = becca.getNoteOrThrow(req.params.noteId);
return streamContent(note.getContent(), note.getFileName(), note.mime);
}
function attachmentContentProvider(req: Request) {
function attachmentContentProvider(req: Request<{ attachmentId: string }>) {
// Read the file name from route params.
const attachment = becca.getAttachmentOrThrow(req.params.attachmentId);
@@ -149,7 +147,7 @@ async function streamContent(content: string | Buffer, fileName: string, mimeTyp
};
}
function saveNoteToTmpDir(req: Request) {
function saveNoteToTmpDir(req: Request<{ noteId: string }>) {
const note = becca.getNoteOrThrow(req.params.noteId);
const fileName = note.getFileName();
const content = note.getContent();
@@ -157,7 +155,7 @@ function saveNoteToTmpDir(req: Request) {
return saveToTmpDir(fileName, content, "notes", note.noteId);
}
function saveAttachmentToTmpDir(req: Request) {
function saveAttachmentToTmpDir(req: Request<{ attachmentId: string }>) {
const attachment = becca.getAttachmentOrThrow(req.params.attachmentId);
const fileName = attachment.getFileName();
const content = attachment.getContent();
@@ -205,7 +203,7 @@ function saveToTmpDir(fileName: string, content: string | Buffer, entityType: st
};
}
function uploadModifiedFileToNote(req: Request) {
function uploadModifiedFileToNote(req: Request<{ noteId: string }>) {
const noteId = req.params.noteId;
const { filePath } = req.body;
@@ -228,7 +226,7 @@ function uploadModifiedFileToNote(req: Request) {
note.setContent(fileContent);
}
function uploadModifiedFileToAttachment(req: Request) {
function uploadModifiedFileToAttachment(req: Request<{ attachmentId: string }>) {
const { attachmentId } = req.params;
const { filePath } = req.body;

View File

@@ -1,20 +1,19 @@
"use strict";
import imageService from "../../services/image.js";
import becca from "../../becca/becca.js";
import fs from "fs";
import type { Request, Response } from "express";
import fs from "fs";
import becca from "../../becca/becca.js";
import type BNote from "../../becca/entities/bnote.js";
import type BRevision from "../../becca/entities/brevision.js";
import imageService from "../../services/image.js";
import { RESOURCE_DIR } from "../../services/resource_dir.js";
function returnImageFromNote(req: Request, res: Response) {
function returnImageFromNote(req: Request<{ noteId: string }>, res: Response) {
const image = becca.getNote(req.params.noteId);
return returnImageInt(image, res);
}
function returnImageFromRevision(req: Request, res: Response) {
function returnImageFromRevision(req: Request<{ revisionId: string }>, res: Response) {
const image = becca.getRevision(req.params.revisionId);
return returnImageInt(image, res);
@@ -24,7 +23,7 @@ function returnImageInt(image: BNote | BRevision | null, res: Response) {
if (!image) {
res.set("Content-Type", "image/png");
return res.send(fs.readFileSync(`${RESOURCE_DIR}/db/image-deleted.png`));
} else if (!["image", "canvas", "mermaid", "mindMap"].includes(image.type)) {
} else if (!["image", "canvas", "mermaid", "mindMap", "spreadsheet"].includes(image.type)) {
return res.sendStatus(400);
}
@@ -34,6 +33,8 @@ function returnImageInt(image: BNote | BRevision | null, res: Response) {
renderSvgAttachment(image, res, "mermaid-export.svg");
} else if (image.type === "mindMap") {
renderSvgAttachment(image, res, "mindmap-export.svg");
} else if (image.type === "spreadsheet") {
renderPngAttachment(image, res, "spreadsheet-export.png");
} else {
res.set("Content-Type", image.mime);
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
@@ -61,7 +62,19 @@ export function renderSvgAttachment(image: BNote | BRevision, res: Response, att
res.send(svg);
}
function returnAttachedImage(req: Request, res: Response) {
export function renderPngAttachment(image: BNote | BRevision, res: Response, attachmentName: string) {
const attachment = image.getAttachmentByTitle(attachmentName);
if (attachment) {
res.set("Content-Type", "image/png");
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
res.send(attachment.getContent());
} else {
res.sendStatus(404);
}
}
function returnAttachedImage(req: Request<{ attachmentId: string }>, res: Response) {
const attachment = becca.getAttachment(req.params.attachmentId);
if (!attachment) {
@@ -78,7 +91,7 @@ function returnAttachedImage(req: Request, res: Response) {
res.send(attachment.getContent());
}
function updateImage(req: Request) {
function updateImage(req: Request<{ noteId: string }>) {
const { noteId } = req.params;
const { file } = req;

View File

@@ -1,21 +1,20 @@
"use strict";
import enexImportService from "../../services/import/enex.js";
import opmlImportService from "../../services/import/opml.js";
import zipImportService from "../../services/import/zip.js";
import singleImportService from "../../services/import/single.js";
import cls from "../../services/cls.js";
import type { Request } from "express";
import path from "path";
import becca from "../../becca/becca.js";
import beccaLoader from "../../becca/becca_loader.js";
import type BNote from "../../becca/entities/bnote.js";
import ValidationError from "../../errors/validation_error.js";
import cls from "../../services/cls.js";
import enexImportService from "../../services/import/enex.js";
import opmlImportService from "../../services/import/opml.js";
import singleImportService from "../../services/import/single.js";
import zipImportService from "../../services/import/zip.js";
import log from "../../services/log.js";
import TaskContext from "../../services/task_context.js";
import ValidationError from "../../errors/validation_error.js";
import type { Request } from "express";
import type BNote from "../../becca/entities/bnote.js";
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
async function importNotesToBranch(req: Request) {
async function importNotesToBranch(req: Request<{ parentNoteId: string }>) {
const { parentNoteId } = req.params;
const { taskId, last } = req.body;
@@ -88,7 +87,7 @@ async function importNotesToBranch(req: Request) {
setTimeout(
() =>
taskContext.taskSucceeded({
parentNoteId: parentNoteId,
parentNoteId,
importedNoteId: note?.noteId
}),
1000
@@ -101,7 +100,7 @@ async function importNotesToBranch(req: Request) {
return note.getPojo();
}
function importAttachmentsToNote(req: Request) {
function importAttachmentsToNote(req: Request<{ parentNoteId: string }>) {
const { parentNoteId } = req.params;
const { taskId, last } = req.body;
@@ -138,7 +137,7 @@ function importAttachmentsToNote(req: Request) {
setTimeout(
() =>
taskContext.taskSucceeded({
parentNoteId: parentNoteId
parentNoteId
}),
1000
);

View File

@@ -1,11 +1,12 @@
"use strict";
import becca from "../../becca/becca.js";
import type BNote from "../../becca/entities/bnote.js";
import type BAttribute from "../../becca/entities/battribute.js";
import { BacklinkCountResponse, BacklinksResponse } from "@triliumnext/commons";
import type { Request } from "express";
import { HTMLElement, parse, TextNode } from "node-html-parser";
import { BacklinkCountResponse, BacklinksResponse } from "@triliumnext/commons";
import becca from "../../becca/becca.js";
import type BAttribute from "../../becca/entities/battribute.js";
import type BNote from "../../becca/entities/bnote.js";
interface TreeLink {
sourceNoteId: string;
@@ -97,7 +98,7 @@ function getNeighbors(note: BNote, depth: number): string[] {
return retNoteIds;
}
function getLinkMap(req: Request) {
function getLinkMap(req: Request<{ noteId: string }>) {
const mapRootNote = becca.getNoteOrThrow(req.params.noteId);
// if the map root itself has "excludeFromNoteMap" attribute (journal typically) then there wouldn't be anything
@@ -156,9 +157,9 @@ function getLinkMap(req: Request) {
return false;
} else if (excludeRelations.has(rel.name)) {
return false;
} else {
return true;
}
}
return true;
})
.map((rel) => ({
id: `${rel.noteId}-${rel.name}-${rel.value}`,
@@ -168,13 +169,13 @@ function getLinkMap(req: Request) {
}));
return {
notes: notes,
notes,
noteIdToDescendantCountMap: buildDescendantCountMap(noteIdsArray),
links: links
links
};
}
function getTreeMap(req: Request) {
function getTreeMap(req: Request<{ noteId: string }>) {
const mapRootNote = becca.getNoteOrThrow(req.params.noteId);
// if the map root itself has "excludeFromNoteMap" (journal typically) then there wouldn't be anything to display,
// so we'll just ignore it
@@ -223,9 +224,9 @@ function getTreeMap(req: Request) {
updateDescendantCountMapForSearch(noteIdToDescendantCountMap, subtree.relationships);
return {
notes: notes,
noteIdToDescendantCountMap: noteIdToDescendantCountMap,
links: links
notes,
noteIdToDescendantCountMap,
links
};
}
@@ -350,7 +351,7 @@ function getFilteredBacklinks(note: BNote): BAttribute[] {
);
}
function getBacklinkCount(req: Request) {
function getBacklinkCount(req: Request<{ noteId: string }>) {
const { noteId } = req.params;
const note = becca.getNoteOrThrow(noteId);
@@ -360,7 +361,7 @@ function getBacklinkCount(req: Request) {
} satisfies BacklinkCountResponse;
}
function getBacklinks(req: Request): BacklinksResponse {
function getBacklinks(req: Request<{ noteId: string }>): BacklinksResponse {
const { noteId } = req.params;
const note = becca.getNoteOrThrow(noteId);

View File

@@ -1,18 +1,19 @@
"use strict";
import noteService from "../../services/notes.js";
import eraseService from "../../services/erase.js";
import treeService from "../../services/tree.js";
import sql from "../../services/sql.js";
import utils from "../../services/utils.js";
import log from "../../services/log.js";
import TaskContext from "../../services/task_context.js";
import type { AttributeRow, CreateChildrenResponse, DeleteNotesPreview, MetadataResponse } from "@triliumnext/commons";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import type BBranch from "../../becca/entities/bbranch.js";
import ValidationError from "../../errors/validation_error.js";
import blobService from "../../services/blob.js";
import type { Request } from "express";
import type BBranch from "../../becca/entities/bbranch.js";
import type { AttributeRow, CreateChildrenResponse, DeleteNotesPreview, MetadataResponse } from "@triliumnext/commons";
import eraseService from "../../services/erase.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
import sql from "../../services/sql.js";
import TaskContext from "../../services/task_context.js";
import treeService from "../../services/tree.js";
import utils from "../../services/utils.js";
/**
* @swagger
@@ -39,7 +40,7 @@ import type { AttributeRow, CreateChildrenResponse, DeleteNotesPreview, Metadata
* - session: []
* tags: ["data"]
*/
function getNote(req: Request) {
function getNote(req: Request<{ noteId: string }>) {
return becca.getNoteOrThrow(req.params.noteId);
}
@@ -66,7 +67,7 @@ function getNote(req: Request) {
* - session: []
* tags: ["data"]
*/
function getNoteBlob(req: Request) {
function getNoteBlob(req: Request<{ noteId: string }>) {
return blobService.getBlobPojo("notes", req.params.noteId);
}
@@ -93,7 +94,7 @@ function getNoteBlob(req: Request) {
* - session: []
* tags: ["data"]
*/
function getNoteMetadata(req: Request) {
function getNoteMetadata(req: Request<{ noteId: string }>) {
const note = becca.getNoteOrThrow(req.params.noteId);
return {
@@ -126,7 +127,7 @@ function createNote(req: Request) {
} satisfies CreateChildrenResponse;
}
function updateNoteData(req: Request) {
function updateNoteData(req: Request<{ noteId: string }>) {
const { content, attachments } = req.body;
const { noteId } = req.params;
@@ -170,7 +171,7 @@ function updateNoteData(req: Request) {
* - session: []
* tags: ["data"]
*/
function deleteNote(req: Request) {
function deleteNote(req: Request<{ noteId: string }>) {
const noteId = req.params.noteId;
const taskId = req.query.taskId;
const eraseNotes = req.query.eraseNotes === "true";
@@ -197,7 +198,7 @@ function deleteNote(req: Request) {
}
}
function undeleteNote(req: Request) {
function undeleteNote(req: Request<{ noteId: string }>) {
const taskContext = TaskContext.getInstance(utils.randomString(10), "undeleteNotes", null);
noteService.undeleteNote(req.params.noteId, taskContext);
@@ -205,7 +206,7 @@ function undeleteNote(req: Request) {
taskContext.taskSucceeded(null);
}
function sortChildNotes(req: Request) {
function sortChildNotes(req: Request<{ noteId: string }>) {
const noteId = req.params.noteId;
const { sortBy, sortDirection, foldersFirst, sortNatural, sortLocale } = req.body;
@@ -216,7 +217,7 @@ function sortChildNotes(req: Request) {
treeService.sortNotes(noteId, sortBy, reverse, foldersFirst, sortNatural, sortLocale);
}
function protectNote(req: Request) {
function protectNote(req: Request<{ noteId: string; isProtected: string }>) {
const noteId = req.params.noteId;
const note = becca.notes[noteId];
const protect = !!parseInt(req.params.isProtected);
@@ -229,7 +230,7 @@ function protectNote(req: Request) {
taskContext.taskSucceeded(null);
}
function setNoteTypeMime(req: Request) {
function setNoteTypeMime(req: Request<{ noteId: string }>) {
// can't use [] destructuring because req.params is not iterable
const { noteId } = req.params;
const { type, mime } = req.body;
@@ -240,7 +241,7 @@ function setNoteTypeMime(req: Request) {
note.save();
}
function changeTitle(req: Request) {
function changeTitle(req: Request<{ noteId: string }>) {
const noteId = req.params.noteId;
const title = req.body.title;
@@ -267,7 +268,7 @@ function changeTitle(req: Request) {
return note;
}
function duplicateSubtree(req: Request) {
function duplicateSubtree(req: Request<{ noteId: string; parentNoteId: string }>) {
const { noteId, parentNoteId } = req.params;
return noteService.duplicateSubtree(noteId, parentNoteId);
@@ -342,7 +343,7 @@ function getDeleteNotesPreview(req: Request) {
} satisfies DeleteNotesPreview;
}
function forceSaveRevision(req: Request) {
function forceSaveRevision(req: Request<{ noteId: string }>) {
const { noteId } = req.params;
const note = becca.getNoteOrThrow(noteId);
@@ -353,7 +354,7 @@ function forceSaveRevision(req: Request) {
note.saveRevision();
}
function convertNoteToAttachment(req: Request) {
function convertNoteToAttachment(req: Request<{ noteId: string }>) {
const { noteId } = req.params;
const note = becca.getNoteOrThrow(noteId);

View File

@@ -128,7 +128,7 @@ function getOptions() {
return resultMap;
}
function updateOption(req: Request) {
function updateOption(req: Request<{ name: string; value: string }>) {
const { name, value } = req.params;
if (!update(name, value)) {

View File

@@ -1,13 +1,12 @@
"use strict";
import sql from "../../services/sql.js";
import protectedSessionService from "../../services/protected_session.js";
import noteService from "../../services/notes.js";
import becca from "../../becca/becca.js";
import type { Request } from "express";
import type { RecentChangeRow } from "@triliumnext/commons";
import type { Request } from "express";
function getRecentChanges(req: Request) {
import becca from "../../becca/becca.js";
import noteService from "../../services/notes.js";
import protectedSessionService from "../../services/protected_session.js";
import sql from "../../services/sql.js";
function getRecentChanges(req: Request<{ ancestorNoteId: string }>) {
const { ancestorNoteId } = req.params;
let recentChanges: RecentChangeRow[] = [];

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