Compare commits

..

557 Commits

Author SHA1 Message Date
Elian Doran
54c8322960 chore(react/promoted_attributes): multiplicity 2025-11-13 08:17:32 +02:00
Elian Doran
3d0d1fa36e chore(react/promoted_attributes): basic structures 2025-11-12 10:21:28 +02:00
Elian Doran
3f93d191b3 Translations update from Hosted Weblate (#7701) 2025-11-12 08:27:40 +02:00
green
3a83e7f632 Translated using Weblate (Japanese)
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ja/
2025-11-12 07:18:19 +01:00
green
2acede95d7 Translated using Weblate (Japanese)
Currently translated at 100.0% (1623 of 1623 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-11-12 07:18:18 +01:00
Unknown
052a60ba58 Translated using Weblate (Spanish)
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/es/
2025-11-12 07:18:17 +01:00
Unknown
a633c6eecd Translated using Weblate (Spanish)
Currently translated at 100.0% (1623 of 1623 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2025-11-12 07:18:16 +01:00
Elian Doran
b6561e6375 "Open note on server" menu item (#7477) 2025-11-12 08:17:53 +02:00
Elian Doran
7af9df2ee3 chore(deps): update dependency @redocly/cli to v2.11.1 (#7695) 2025-11-12 08:08:29 +02:00
Elian Doran
3c4b7fd490 chore(deps): update dependency @smithy/middleware-retry to v4.4.7 (#7696) 2025-11-12 08:08:15 +02:00
Elian Doran
f5ed4007e3 chore(deps): update typescript-eslint monorepo to v8.46.4 (#7698) 2025-11-12 08:07:55 +02:00
Elian Doran
f26469bdc2 fix(deps): update dependency i18next to v25.6.2 (#7699) 2025-11-12 08:07:38 +02:00
Elian Doran
3326d803a0 Translations update from Hosted Weblate (#7700) 2025-11-12 08:07:15 +02:00
Hosted Weblate
b60a28c52c 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/
2025-11-12 05:55:12 +00:00
Elian Doran
139c60ac73 Hotfix (#7679) 2025-11-12 07:54:54 +02:00
renovate[bot]
c7ad6131cb fix(deps): update dependency i18next to v25.6.2 2025-11-12 02:54:22 +00:00
renovate[bot]
86e2a762a4 chore(deps): update typescript-eslint monorepo to v8.46.4 2025-11-12 02:53:34 +00:00
renovate[bot]
bf9f880a21 chore(deps): update dependency @smithy/middleware-retry to v4.4.7 2025-11-12 02:51:14 +00:00
renovate[bot]
5491aaab85 chore(deps): update dependency @redocly/cli to v2.11.1 2025-11-12 02:50:29 +00:00
Elian Doran
25563c6687 chore(release): prepare for v0.99.5 2025-11-11 22:16:13 +02:00
Elian Doran
48608adbd3 Merge branch 'hotfix' of https://github.com/TriliumNext/Trilium into hotfix 2025-11-11 15:33:00 +02:00
Elian Doran
362ecba98d fix(popup_editor): drag indicator not visible (closes #7686) 2025-11-11 15:32:59 +02:00
Elian Doran
a13892da66 refactor(popup_editor): different handling for z-index 2025-11-11 15:32:18 +02:00
Elian Doran
40568ac547 Translations update from Hosted Weblate (#7689) 2025-11-11 13:56:40 +02:00
Elian Doran
e8dc19a1a6 chore(forge): display logs when building for flatpak 2025-11-11 13:49:00 +02:00
Elian Doran
cc326547d1 Merge branch 'hotfix' of github.com:TriliumNext/Trilium into hotfix 2025-11-11 13:41:22 +02:00
Elian Doran
a45b147462 fix(flatpak): global shortcuts not working (closes #7563) 2025-11-11 13:41:18 +02:00
Elian Doran
4e73f20165 fix(flatpak): system tray missing 2025-11-11 13:37:35 +02:00
hulmgulm
0039e5e60c Translated using Weblate (German)
Currently translated at 21.7% (33 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/de/
2025-11-11 11:51:35 +01:00
Ondřej Soldát
813e2e8c9b Translated using Weblate (Czech)
Currently translated at 1.9% (3 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/cs/
2025-11-11 11:51:34 +01:00
Dong-ha, Lee
f186e929b2 Translated using Weblate (Korean)
Currently translated at 10.5% (41 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ko/
2025-11-11 11:51:25 +01:00
Giovi
31d2abf954 Translated using Weblate (Italian)
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/it/
2025-11-11 11:51:23 +01:00
Giovi
bd69280735 Translated using Weblate (Italian)
Currently translated at 100.0% (1623 of 1623 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2025-11-11 11:51:21 +01:00
Elian Doran
80c77eeb18 fix(ribbon): "Open attribute list" shortcut not focusing (closes #7463) 2025-11-11 12:20:16 +02:00
Elian Doran
c30282fbd0 Merge branch 'hotfix' of https://github.com/TriliumNext/Trilium into hotfix 2025-11-11 11:24:47 +02:00
Elian Doran
7b8f1ed6ec fix(collections/calendar): unable to drag to/from all-day (closes #7685) 2025-11-11 11:24:45 +02:00
Elian Doran
b86c656895 Note info ribbon flex layout (#7678) 2025-11-10 22:18:05 +02:00
Elian Doran
bc32fe749d chore(deps): update pnpm to v10.21.0 (#7672) 2025-11-10 22:12:20 +02:00
Elian Doran
5970a242c9 chore(deps): update dependency esbuild to v0.27.0 (#7671) 2025-11-10 22:11:36 +02:00
Elian Doran
118e11c3fd edited notes: better sql like statement (#7681) 2025-11-10 21:16:29 +02:00
Elian Doran
4fc9848a3b Translations update from Hosted Weblate (#7682) 2025-11-10 20:36:02 +02:00
Hosted Weblate
b8de5b3348 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/
2025-11-10 18:29:41 +00:00
Elian Doran
5a01f75d67 edited notes: better sql like statement (#7681) 2025-11-10 20:29:25 +02:00
contributor
d0a994c102 fix sql like pattern 2025-11-10 20:15:08 +02:00
contributor
e883f32f89 edited notes: better sql like statement 2025-11-10 20:04:50 +02:00
Elian Doran
a6fc54cb81 fix(mindmap): switching between read-only and editable mode 2025-11-10 19:59:01 +02:00
Elian Doran
76bd6a5ab9 refactor(mindmap): unnecessary content logic 2025-11-10 19:44:45 +02:00
Elian Doran
3b5d749d86 docs(dev): branching strategy 2025-11-10 19:31:10 +02:00
Elian Doran
b54765113e fix(ci): wrong dir for arm64 2025-11-10 19:10:55 +02:00
Elian Doran
ed08893996 chore(ci): disable fail-fast for matrix 2025-11-10 19:05:41 +02:00
Elian Doran
20286d53c8 feat(ci): test server on ARM as well 2025-11-10 18:56:04 +02:00
Elian Doran
9c1a34fe7c fix(ci): proper port 2025-11-10 18:30:03 +02:00
Elian Doran
e70c6b69b8 fix(ci): set proper environment variables for playwright 2025-11-10 18:20:35 +02:00
Elian Doran
9b69b0ad0d fix(server): use right packaged version 2025-11-10 17:51:04 +02:00
Elian Doran
3776c40b8d chore(ci): add playwright testing to the server 2025-11-10 17:25:39 +02:00
renovate[bot]
6761f741ca chore(deps): update dependency esbuild to v0.27.0 2025-11-10 13:48:16 +00:00
Elian Doran
c7369bc9b3 fix(print): copy to clipboard button visible 2025-11-10 14:26:24 +02:00
Adorian Doran
b741662fde style/empty note: fix alignment 2025-11-10 14:17:49 +02:00
Adorian Doran
624610b17c close #7668 2025-11-10 14:17:44 +02:00
Adorian Doran
de004bd8ba fix #7667 2025-11-10 14:17:37 +02:00
contributor
a1c959aabd use class instead of inline style for note info calculate button 2025-11-10 13:18:07 +02:00
contributor
4d1ebd011c ribbon Note Info in flex layout instead of table 2025-11-10 13:18:07 +02:00
renovate[bot]
2947967b79 chore(deps): update pnpm to v10.21.0 2025-11-10 02:58:46 +00:00
Adorian Doran
52691b9e88 style/empty note: fix alignment 2025-11-10 03:05:05 +02:00
Adorian Doran
daba806e12 close #7668 2025-11-10 02:09:14 +02:00
Adorian Doran
d7f7049b5d fix #7667 2025-11-10 02:01:45 +02:00
contributor
e811db3651 only show "Open note on server" for electron app 2025-11-10 00:26:28 +02:00
contributor
a1d38b6bb8 add 'Open note on server' menu item to NoteActions drop-down 2025-11-10 00:26:28 +02:00
Elian Doran
c781eab061 Translations update from Hosted Weblate (#7664) 2025-11-09 22:53:15 +02:00
Elian Doran
fb91d4fbd4 docs(user): mention updates after react improvements 2025-11-09 22:52:02 +02:00
Sir-Ivysaur
cd6fcbd283 Translated using Weblate (Indonesian)
Currently translated at 9.8% (15 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/id/
2025-11-09 20:32:53 +00:00
Elian Doran
0ccc350ddf Port type widgets to React (#7044) 2025-11-09 22:32:35 +02:00
Elian Doran
efa7fd0b7d chore(client): use backward-compatible hiding mechanism 2025-11-09 22:20:49 +02:00
Elian Doran
5f4d0325aa Merge remote-tracking branch 'origin/main' into react/type_widgets 2025-11-09 21:37:56 +02:00
Elian Doran
06eb30c69d chore(release): prepare for v0.99.4 2025-11-09 21:29:21 +02:00
Elian Doran
b9b5c13d9c fix(client): ribbon adapter not working 2025-11-09 21:14:28 +02:00
Elian Doran
763fa0b515 fix(client/type_widgets): empty tab not working when closing last tab 2025-11-09 20:41:29 +02:00
Elian Doran
8e697d0578 fix(ribbon): not dismissing active tab after change 2025-11-09 20:17:13 +02:00
Elian Doran
6f245ec8d5 fix(ribbon): not refreshing if note is temporarily edited 2025-11-09 20:15:06 +02:00
Elian Doran
532df6559a fix(ribbon): formatting toolbar displayed in read-only notes 2025-11-09 20:15:06 +02:00
Adorian Doran
8589f7f164 style/note list widget: fix broken CSS selector 2025-11-09 19:50:48 +02:00
Elian Doran
7ba91b7a9d chore(desktop): fix import 2025-11-09 19:36:58 +02:00
Adorian Doran
4eadf40e20 Merge branch 'main' of https://github.com/TriliumNext/Trilium 2025-11-09 19:35:59 +02:00
Adorian Doran
7b9303b392 style/zen mode: do not show an empty toolbar container when the toolbar is not actually available 2025-11-09 19:35:47 +02:00
Elian Doran
7fc9f08843 Merge remote-tracking branch 'origin/main' into react/type_widgets 2025-11-09 19:30:46 +02:00
Elian Doran
e28794d706 chore(edit-docs): fix typecheck 2025-11-09 19:28:08 +02:00
Elian Doran
521152ec0e Translations update from Hosted Weblate (#7661) 2025-11-09 17:50:54 +02:00
Hosted Weblate
2ff746253d 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/
2025-11-09 16:39:21 +01:00
Elian Doran
c270aef738 Translations update from Hosted Weblate (#7660) 2025-11-09 17:39:06 +02:00
Sir-Ivysaur
d0881c09ed Translated using Weblate (Indonesian)
Currently translated at 1.9% (3 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/id/
2025-11-09 16:15:11 +01:00
Adorian Doran
b905c1d03a style: different appearance tweaks 2025-11-09 17:10:23 +02:00
Adorian Doran
f03448bae4 style/gutter: tweak color on horizontal layout when background effects are enabled 2025-11-09 16:32:32 +02:00
Adorian Doran
f8ac09df38 style/right pane: tweak appearance 2025-11-09 16:16:37 +02:00
Adorian Doran
15c329c331 style/scrolling container widget: fix CSS selector 2025-11-09 15:51:29 +02:00
Adorian Doran
f6afc0b718 style/scrolling container widget: improve full-height widget handling 2025-11-09 15:46:57 +02:00
Elian Doran
ae1c8f0a0b chore(client): review from Copilot 2025-11-09 15:13:57 +02:00
Elian Doran
1257e46852 chore(client): review from Gemini 2025-11-09 15:10:40 +02:00
Elian Doran
40a7f286a3 fix(client): typecheck issues 2025-11-09 15:04:29 +02:00
Elian Doran
652cccadd7 fix(type_widgets/code): read-only bar not visible for dark themes 2025-11-09 14:59:47 +02:00
Elian Doran
308fd00508 fix(type_widgets/code): not reacting to language changes 2025-11-09 14:48:31 +02:00
Adorian Doran
ecfa333491 style/note cards: remove shadow 2025-11-09 14:47:31 +02:00
Adorian Doran
21b0ef9554 style/note cards: improve style, remove no longer unused CSS variables 2025-11-09 14:43:59 +02:00
Elian Doran
35f413505c fix(type_widgets/code): background color leaking in SQLite 2025-11-09 14:40:14 +02:00
Elian Doran
a82d15e83d fix(type_widgets/attachments): attachments not refreshing when uploading new revision 2025-11-09 14:26:32 +02:00
Adorian Doran
2f3be96dff style/info bar: improve style 2025-11-09 14:17:04 +02:00
Adorian Doran
4ba7907bee client/content header container: fix parent detection 2025-11-09 14:07:27 +02:00
Elian Doran
24b169d667 fix(type_widgets): "empty" note type displayed on refresh 2025-11-09 13:52:07 +02:00
Elian Doran
b36ef54507 fix(type_widgets): code background leaking when switching types 2025-11-09 13:37:31 +02:00
Elian Doran
c772430dd0 fix(type_widgets): switching between note types shows wrong content 2025-11-09 13:24:12 +02:00
Elian Doran
d4194c503c fix(type_widgets/relation_map): map emptied when switching fast 2025-11-09 12:38:12 +02:00
Elian Doran
33a41d2f86 Merge remote-tracking branch 'origin/main' into react/type_widgets
; Conflicts:
;	apps/client/src/layouts/desktop_layout.tsx
;	apps/client/src/layouts/mobile_layout.tsx
;	apps/client/src/widgets/note_detail.ts
;	apps/client/src/widgets/react/hooks.tsx
2025-11-09 12:08:50 +02:00
Elian Doran
afa92551ea fix(tree): type issues & error if active node is empty 2025-11-09 11:16:35 +02:00
Elian Doran
cc0646e79c style(next): remove alignment of last toolbar group 2025-11-09 10:35:03 +02:00
Elian Doran
4c8f20be9a ignore toggle tray command if tray is disabled (#7654) 2025-11-09 09:45:37 +02:00
Elian Doran
3407528c03 chore(deps): update softprops/action-gh-release action to v2.4.2 (#7657) 2025-11-09 09:44:45 +02:00
Elian Doran
70575a00cb Translations update from Hosted Weblate (#7656) 2025-11-09 09:34:41 +02:00
renovate[bot]
74ba4b9ee5 chore(deps): update softprops/action-gh-release action to v2.4.2 2025-11-09 00:42:36 +00:00
Francis C.
706abeb307 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1621 of 1621 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2025-11-09 01:41:35 +01:00
Francis C.
7158c48831 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1621 of 1621 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2025-11-09 01:41:35 +01:00
Adorian Doran
78e2814068 UI improvements (#7655) 2025-11-09 02:41:20 +02:00
Adorian Doran
7659224e3a style/read-only note info bar: fix zen mode appearance 2025-11-09 02:26:37 +02:00
Adorian Doran
baff349fa2 client: refactor 2025-11-09 02:23:57 +02:00
Adorian Doran
50869d29db client/shared note info bar: improve appearance 2025-11-09 02:14:43 +02:00
Adorian Doran
c4603fce25 client/shared note message: convert to an info bar 2025-11-09 01:56:52 +02:00
Adorian Doran
220aab2b76 client/read-only note info bar: refactor 2025-11-09 01:35:33 +02:00
Adorian Doran
285a7253e3 client: make the info bar part of the scrollable content, prevent overlapping with the floating buttons 2025-11-09 01:19:45 +02:00
Adorian Doran
d8d80ed936 Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-11-08 21:25:39 +02:00
Elian Doran
3463cb83a0 fix(collections/board): cloned notes appearing twice (closes #6786) 2025-11-08 20:17:56 +02:00
Elian Doran
b0bd60b9a4 feat(collections/calendar): use formatting locale 2025-11-08 18:01:58 +02:00
contributor
3d70a0534b ignore toggle tray command if tray is disabled 2025-11-08 13:03:42 +02:00
Elian Doran
53805e9c49 fix(server/notes): images not saved on duplication (fixes #7471) 2025-11-08 11:27:18 +02:00
Elian Doran
0e95610d4e fix(server): attachments not copied for templates (closes #7612) 2025-11-08 11:18:03 +02:00
Elian Doran
051e2b4eef Send global shortcut to current window (#7645) 2025-11-08 08:31:17 +02:00
Elian Doran
1d750bde64 chore(deps): update dependency vite to v7.2.2 (#7647) 2025-11-08 08:28:13 +02:00
Elian Doran
4476615d33 fix(deps): update dependency marked to v16.4.2 (#7649) 2025-11-08 08:27:36 +02:00
Elian Doran
bbcc16daab fix(deps): update dependency i18next to v25.6.1 (#7648) 2025-11-08 08:27:05 +02:00
Elian Doran
457dd070c6 chore(deps): update dependency rcedit to v5 (#7651) 2025-11-08 08:26:05 +02:00
Elian Doran
ce229dd6f5 chore(deps): update dependency electron to v38.6.0 (#7650) 2025-11-08 08:22:22 +02:00
renovate[bot]
2e59d9d7bc chore(deps): update dependency rcedit to v5 2025-11-08 01:39:22 +00:00
renovate[bot]
bbe96c3967 chore(deps): update dependency electron to v38.6.0 2025-11-08 01:39:17 +00:00
renovate[bot]
d95eb9f5d3 fix(deps): update dependency marked to v16.4.2 2025-11-08 01:38:37 +00:00
renovate[bot]
d75279316a fix(deps): update dependency i18next to v25.6.1 2025-11-08 01:37:54 +00:00
renovate[bot]
7f6be13a18 chore(deps): update dependency vite to v7.2.2 2025-11-08 01:37:08 +00:00
Adorian Doran
2f74b40095 client/read only note info bar: refactor 2025-11-07 23:45:48 +02:00
Adorian Doran
a844e1faab client: fix typo 2025-11-07 21:19:11 +02:00
contributor
f629f564cd add reusable showAndFocusWindow function 2025-11-07 21:17:34 +02:00
contributor
9a5f2f8d3b make global shortcut work with windows in system tray 2025-11-07 21:17:34 +02:00
Adorian Doran
9bccc72668 client/options/keep content centered: simplify the inner workings 2025-11-07 21:15:51 +02:00
contributor
a29597a4bf add safety check to ensure electron window is not destroyed 2025-11-07 20:25:16 +02:00
contributor
44b34d1ea0 send global shortcut to current window, not the first one 2025-11-07 20:25:09 +02:00
Adorian Doran
c617c84d86 style/options: properly align cards when content centering is enabled 2025-11-07 19:02:59 +02:00
Adorian Doran
b38780755a client: add a CSS class to identify option splits 2025-11-07 19:01:50 +02:00
Adorian Doran
78a54fa9f7 client: refactor 2025-11-07 18:23:30 +02:00
Adorian Doran
fa64ca2c93 client: add an option to center the content 2025-11-07 18:17:28 +02:00
Elian Doran
71d7403690 feat(print): support CSS for collections too 2025-11-07 17:48:37 +02:00
Elian Doran
e4cd946ea8 docs(user): fix wrong page for documentation 2025-11-07 17:31:50 +02:00
Elian Doran
cedd1c4789 docs(user): document custom print CSS 2025-11-07 17:25:14 +02:00
Elian Doran
7a677cff5f feat(print): allow custom CSS (closes #7608) 2025-11-07 17:25:05 +02:00
Adorian Doran
2d03dd22e3 Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-11-07 17:06:21 +02:00
Elian Doran
e28da416ba Translations update from Hosted Weblate (#7639) 2025-11-06 22:59:32 +02:00
green
28c0ef52f3 Translated using Weblate (Japanese)
Currently translated at 100.0% (1621 of 1621 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-11-06 20:28:01 +01:00
Unknown
9464a64d64 Translated using Weblate (Spanish)
Currently translated at 99.7% (1617 of 1621 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2025-11-06 20:28:00 +01:00
SngAbc
c3af6a6aa2 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.7% (1617 of 1621 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2025-11-06 20:28:00 +01:00
Elian Doran
319e28387f refactor(react/type_widgets): separate persistence from canvas 2025-11-06 15:44:09 +02:00
Elian Doran
0b740bb007 chore(react/type_widgets): remove logging 2025-11-06 12:05:38 +02:00
Elian Doran
bb7fa9a2e6 fix(react/type_widgets): unable to switch to notes of same note type 2025-11-06 12:02:48 +02:00
Elian Doran
5e83e6fa34 Merge remote-tracking branch 'origin/main' into react/type_widgets 2025-11-06 11:40:04 +02:00
Elian Doran
a4281fe26f fix(deps): update ckeditor monorepo to v47.2.0 (#7634) 2025-11-06 09:04:07 +02:00
Adorian Doran
5c0d6e1fef style/alert bar: update message 2025-11-06 08:55:08 +02:00
Adorian Doran
23f1103822 style/alert bar: update message 2025-11-06 08:45:53 +02:00
renovate[bot]
b45ee6879c fix(deps): update ckeditor monorepo to v47.2.0 2025-11-06 06:34:53 +00:00
Elian Doran
a8116aa264 chore(deps): update dependency vite to v7.2.1 (#7633) 2025-11-06 08:31:51 +02:00
Elian Doran
db3960a23e fix(deps): update dependency mind-elixir to v5.3.5 (#7632) 2025-11-06 08:31:14 +02:00
Elian Doran
a43e08500e chore(deps): update dependency openai to v6.8.1 (#7631) 2025-11-06 08:30:45 +02:00
Adorian Doran
9011d648b5 client: refactor 2025-11-06 08:26:21 +02:00
Adorian Doran
30c1708979 style/alert bar: update the background color for the light color scheme 2025-11-06 08:15:59 +02:00
Adorian Doran
1a55d3433d client/floating buttons: hide the "Edit note" button for the moment 2025-11-06 08:08:56 +02:00
Adorian Doran
bec47c0bb2 client/note context menu: add "Edit note" for read-only notes 2025-11-06 07:49:57 +02:00
Adorian Doran
4fdb502a19 client: allow the isNoteReadOnly hook operate on a note and context other of the current one 2025-11-06 07:48:11 +02:00
renovate[bot]
e9ccd52fd5 chore(deps): update dependency vite to v7.2.1 2025-11-06 04:54:41 +00:00
renovate[bot]
dcd30972bd fix(deps): update dependency mind-elixir to v5.3.5 2025-11-06 02:49:14 +00:00
renovate[bot]
4578541fa8 chore(deps): update dependency openai to v6.8.1 2025-11-06 02:48:32 +00:00
Adorian Doran
914fa3625f client/read only note info bar: style tweaks for zen mode 2025-11-06 02:01:30 +02:00
Adorian Doran
5e3ffc12ce style/zen mode: fix content refusing to get narrower than the max content width preference 2025-11-06 02:01:03 +02:00
Adorian Doran
aa3a8d19ae client: increase the default size limit for auto read-only 2025-11-06 01:46:55 +02:00
Adorian Doran
a57c237c69 client/read only note info bar: style tweaks for zen mode 2025-11-06 01:34:39 +02:00
Adorian Doran
b52e615f0c client/read only note info bar: add support for zen mode 2025-11-06 01:08:26 +02:00
Adorian Doran
95e5c2563e client/read only note info bar: refactor 2025-11-06 00:45:16 +02:00
Adorian Doran
33be7f828b client/read only note info bar: fix file name 2025-11-06 00:24:27 +02:00
Adorian Doran
d23d37baac client/read only note info bar: add translation string 2025-11-06 00:22:00 +02:00
Adorian Doran
dda8b2795b client/read only note info bar: tweak style 2025-11-06 00:17:14 +02:00
Adorian Doran
0b9eb6c532 client/read only note info bar: shorten link caption 2025-11-05 23:56:18 +02:00
Adorian Doran
728f574eac Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-11-05 23:52:53 +02:00
Adorian Doran
8e90826aef client: add a read-only note info bar 2025-11-05 23:52:38 +02:00
Elian Doran
5e5e0afcf0 Translations update from Hosted Weblate (#7630) 2025-11-05 23:36:01 +02:00
kamykO
de85d1f0df Translated using Weblate (Polish)
Currently translated at 100.0% (152 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/pl/
2025-11-05 21:17:29 +00:00
Elian Doran
b287b892e1 Merge branch 'main' of github.com:TriliumNext/Trilium 2025-11-05 23:17:19 +02:00
Elian Doran
a0edf00caa feat(collections/calendar): context menu option to delete event 2025-11-05 23:17:15 +02:00
Elian Doran
218b9404fc fix(print): missing admonition icons 2025-11-05 22:08:31 +02:00
Elian Doran
54af120e96 fix(search): resolve issue when using = operator in search (#7268) 2025-11-05 21:51:44 +02:00
Elian Doran
ba61ab18ff docs: sync 2025-11-05 21:01:39 +02:00
Elian Doran
2e14522f86 Merge remote-tracking branch 'origin/main' 2025-11-05 21:00:28 +02:00
Elian Doran
4ae38ac5d6 docs(user): widget basics missing template (closes #6148) 2025-11-05 18:32:34 +02:00
Adorian Doran
d5cb6a86c8 Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-11-05 17:35:10 +02:00
Elian Doran
a577fd45e2 fix(quick_search): enable Numpad Enter to trigger quick search (#7624) 2025-11-05 14:49:01 +02:00
Elian Doran
977284fe57 docs(user): document read-only database (closes #4341) 2025-11-05 10:20:35 +02:00
SiriusXT
26ea43d604 chore(test): add vitest for NumpadEnter 2025-11-05 16:01:26 +08:00
SiriusXT
4cb328bdb3 Merge branch 'main' into quick_search 2025-11-05 15:41:13 +08:00
SiriusXT
16785a5c0b fix(quick_search): enable Numpad Enter to trigger quick search 2025-11-05 15:40:12 +08:00
Elian Doran
d271fe7fdd docs(demo): clean up expansion state 2025-11-05 09:26:14 +02:00
Elian Doran
88b9709f15 docs(user): launch bar clarifications in the LLM doc 2025-11-05 08:26:34 +02:00
Elian Doran
f55edabe92 chore(edit-docs): set different port to edit-demo 2025-11-05 08:19:37 +02:00
Elian Doran
2b983f871e docs(demo): link to old documentation (closes #5211) 2025-11-05 08:19:25 +02:00
Elian Doran
ab298cbb3b docs(user): how to set data dir on Windows (closes #4853) 2025-11-05 08:09:15 +02:00
Elian Doran
abeeea584f chore(deps): update dependency sax to v1.4.3 (#7622) 2025-11-05 07:47:19 +02:00
Elian Doran
4d5597cc75 chore(deps): update dependency @redocly/cli to v2.11.0 (#7623) 2025-11-05 07:46:39 +02:00
Elian Doran
c684712141 chore(deps): update dependency axios to v1.13.2 (#7621) 2025-11-05 07:45:33 +02:00
renovate[bot]
a8bb301296 chore(deps): update dependency @redocly/cli to v2.11.0 2025-11-05 01:34:43 +00:00
renovate[bot]
d5bfa466a2 chore(deps): update dependency sax to v1.4.3 2025-11-05 01:34:00 +00:00
renovate[bot]
7651c58c47 chore(deps): update dependency axios to v1.13.2 2025-11-05 01:33:17 +00:00
Elian Doran
c2ce36d963 docs(user): mention structure difference 2025-11-04 23:13:34 +02:00
Elian Doran
3359ff5470 docs(dev): update database structure 2025-11-04 22:53:42 +02:00
Elian Doran
421c1f257e fix(electron): port-in-use dialog shown when opening a new window | (#7595) 2025-11-04 19:29:46 +02:00
Elian Doran
97e87741ee Add multi-branch prefix editing support (#7598) 2025-11-04 19:17:31 +02:00
Elian Doran
193caf8c21 chore: clean up generated type definitions 2025-11-04 19:07:42 +02:00
Elian Doran
d521bda6ab Add comprehensive technical and architectural documentation (#7600) 2025-11-04 18:50:48 +02:00
Elian Doran
b80cb22985 chore(deps): clean up package lock 2025-11-04 18:20:12 +02:00
Elian Doran
7131d44d03 docs(dev): integrate rest of the documentation 2025-11-04 18:16:20 +02:00
Elian Doran
7369f9d532 docs(dev): integrate architecture guide 2025-11-04 17:24:38 +02:00
Elian Doran
58ac325634 docs(dev): integrate some of the architecture notes 2025-11-04 15:51:54 +02:00
Elian Doran
579b2ce76e Merge remote-tracking branch 'origin/main' into copilot/add-technical-documentation 2025-11-04 15:19:49 +02:00
copilot-swe-agent[bot]
0494032fb5 Convert ASCII diagrams to Mermaid.js format
Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2025-11-04 12:35:56 +00:00
Elian Doran
48853555f5 Fix documentation errors, broken links, and standardize to American English (#7599) 2025-11-04 14:31:39 +02:00
Elian Doran
0cd6f0d267 Merge branch 'main' into copilot/improve-user-documentation 2025-11-04 14:30:51 +02:00
Elian Doran
0ae4defc6d docs(dev): reorganize and clean up technical documentation 2025-11-04 10:55:48 +02:00
Elian Doran
db644f20ed docs(dev): releasing a new version 2025-11-04 09:23:56 +02:00
Elian Doran
59a2ef7527 chore(build-docs): add preview entrypoint 2025-11-04 09:15:27 +02:00
Elian Doran
757a046474 feat(build-docs): add root 404 page 2025-11-04 09:13:12 +02:00
Elian Doran
aeb0f44a43 chore(website): fix typecheck issue 2025-11-04 08:55:05 +02:00
Elian Doran
5186ea3fff chore(deps): duplicate dependency in lock 2025-11-04 08:51:54 +02:00
Elian Doran
70a4feff50 edited notes: remove comma for flexible styling (#7609) 2025-11-04 08:43:27 +02:00
Elian Doran
91f85e6675 chore(deps): update typescript-eslint monorepo to v8.46.3 (#7614) 2025-11-04 08:41:51 +02:00
Elian Doran
0cb989e74f chore(deps): update dependency openai to v6.8.0 (#7618) 2025-11-04 07:42:44 +02:00
Elian Doran
d4e31e9d98 fix(deps): update dependency eslint-linter-browserify to v9.39.1 (#7615) 2025-11-04 07:40:52 +02:00
Elian Doran
6c3b5314c8 chore(deps): update dependency sax to v1.4.2 (#7613) 2025-11-04 07:40:20 +02:00
renovate[bot]
3faac9f26e chore(deps): update typescript-eslint monorepo to v8.46.3 2025-11-04 05:35:53 +00:00
Elian Doran
c31ac1a6ee fix(deps): update dependency react-i18next to v16.2.4 (#7616) 2025-11-04 07:34:35 +02:00
Elian Doran
9eff6ad4c2 fix(deps): update eslint monorepo to v9.39.1 (#7617) 2025-11-04 07:33:26 +02:00
Elian Doran
9deb7ba4e9 Translations update from Hosted Weblate (#7619) 2025-11-04 07:30:59 +02:00
Francis C.
93d77ca06e Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (152 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/zh_Hant/
2025-11-04 01:51:28 +00:00
Giovi
a42daccc2e Translated using Weblate (Italian)
Currently translated at 99.3% (151 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/it/
2025-11-04 01:51:26 +00:00
Giovi
33c64b604e Translated using Weblate (Italian)
Currently translated at 99.7% (386 of 387 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/it/
2025-11-04 01:51:21 +00:00
Giovi
f89c14b35a Translated using Weblate (Italian)
Currently translated at 100.0% (1617 of 1617 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2025-11-04 01:51:16 +00:00
Francis C.
1fa3420abe Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (387 of 387 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/zh_Hant/
2025-11-04 01:51:14 +00:00
renovate[bot]
f8b8edd5aa chore(deps): update dependency openai to v6.8.0 2025-11-04 01:42:30 +00:00
renovate[bot]
4c90319c9e fix(deps): update eslint monorepo to v9.39.1 2025-11-04 01:41:49 +00:00
renovate[bot]
3b531544a3 fix(deps): update dependency react-i18next to v16.2.4 2025-11-04 01:41:09 +00:00
renovate[bot]
25d9695db0 fix(deps): update dependency eslint-linter-browserify to v9.39.1 2025-11-04 01:40:28 +00:00
renovate[bot]
caf88473f6 chore(deps): update dependency sax to v1.4.2 2025-11-04 01:38:35 +00:00
SngAbc
b32dc18cf6 fix: incorrect options description 2025-11-04 09:36:34 +08:00
SiriusXT
5a7349121a fix: incorrect options description 2025-11-04 09:24:18 +08:00
Adorian Doran
dcc5b9f422 Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-11-03 23:44:18 +02:00
Adorian Doran
0b01890a11 client: add a launcher to open the command palette 2025-11-03 23:44:06 +02:00
Elian Doran
686c8936cb docs(dev): integrate more of the old documentation 2025-11-03 22:48:14 +02:00
Elian Doran
d9071f2d8e docs(dev): update recent technical documentation 2025-11-03 22:16:15 +02:00
perf3ct
2a0472ae42 fix(search): resolve compilation issue due to variables with the same name 2025-11-03 11:44:43 -08:00
Adorian Doran
8c2354df71 client: format code 2025-11-03 18:16:41 +02:00
Adorian Doran
d650b801e6 client: add a launcher for zen mode 2025-11-03 17:52:08 +02:00
Adorian Doran
fbb27b512e style/zen mode/translucent title bar: fix broken title bar in multi-split windows 2025-11-03 16:09:05 +02:00
Adorian Doran
3ae7bd59ec style/zen mode: disable translucency for the title bar when backdrop effects are turned off 2025-11-03 15:59:34 +02:00
Adorian Doran
55f7a26634 style/zen mode: use translucency for the title bar 2025-11-03 15:53:05 +02:00
Adorian Doran
aa7f01313a style/zen mode: improve scrolling for text notes 2025-11-03 15:24:09 +02:00
Adorian Doran
6f82c283e9 Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-11-03 15:13:21 +02:00
contributor
aa7ecaf684 edited notes: space instead of comma for flexible styling 2025-11-03 15:13:06 +02:00
copilot-swe-agent[bot]
50a69248a7 Fix keyboard shortcut support and address code review feedback
- Add editBranchPrefixCommand in note_tree.ts to support F2 keyboard shortcut with multi-selection
- Extract CSS to separate branch_prefix.css file
- Remove hard-coded color, use CSS class instead
- Fix translation key usage to keep t() calls visible in IDE
- Remove all inline styles

Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2025-11-03 08:01:56 +00:00
SngAbc
5782e58db1 Merge branch 'main' into electron_newwindow 2025-11-03 09:49:46 +08:00
Adorian Doran
e8bf12c4ab style/zen mode: tweak text vertical centering offset 2025-11-03 00:40:00 +02:00
Adorian Doran
344f2d819e style/zen mode: hide the shared note info bar 2025-11-03 00:19:40 +02:00
Adorian Doran
d2e9101675 style/zen mode/fixed formatting toolbar: allow items to overflow if narrow window 2025-11-03 00:14:44 +02:00
copilot-swe-agent[bot]
82e5de2261 Add input validation for prefix to address security concerns
Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2025-11-02 22:06:14 +00:00
copilot-swe-agent[bot]
ff4cd7eae5 Fix grammar: 'type of notes' → 'type of note' in Collections.md
Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2025-11-02 22:04:34 +00:00
copilot-swe-agent[bot]
a62c9a1a2f Fix broken wiki-style link in Scripting.md
Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2025-11-02 22:03:29 +00:00
copilot-swe-agent[bot]
cf406383c3 Standardize spelling to American English (behavior, categorizing)
Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2025-11-02 22:01:44 +00:00
copilot-swe-agent[bot]
07fe42d04e Add quick reference guide for technical documentation
Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2025-11-02 22:01:17 +00:00
copilot-swe-agent[bot]
5b8bb8587d Address code review feedback - add logging and constant for virtual branches
Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2025-11-02 22:00:54 +00:00
copilot-swe-agent[bot]
154492e454 Add comprehensive technical and architectural documentation
Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2025-11-02 21:59:29 +00:00
copilot-swe-agent[bot]
b1729ad7ec Fix additional clarity and grammar issues
Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2025-11-02 21:59:22 +00:00
copilot-swe-agent[bot]
daec11b981 Fix additional grammar and clarity issues in documentation
Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2025-11-02 21:57:26 +00:00
copilot-swe-agent[bot]
7cdd8ffbe2 Add gitignore for build artifacts and verify tests pass
Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2025-11-02 21:56:46 +00:00
copilot-swe-agent[bot]
25ac9e2aa1 Fix spelling, grammar, and broken links in documentation
Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2025-11-02 21:54:40 +00:00
copilot-swe-agent[bot]
4fc434a52e Implement multi-branch prefix editing functionality
- Add setPrefixBatch API endpoint to handle batch prefix updates
- Update branch_prefix dialog to support multiple branches
- Remove noSelectedNotes constraint from edit branch prefix menu
- Add translations for multi-branch prefix editing

Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2025-11-02 21:52:02 +00:00
copilot-swe-agent[bot]
3e0d1bfa44 Initial plan 2025-11-02 21:47:57 +00:00
copilot-swe-agent[bot]
3b02eb8851 Initial plan 2025-11-02 21:47:17 +00:00
copilot-swe-agent[bot]
4c5b2a7c75 Initial plan 2025-11-02 21:39:44 +00:00
Adorian Doran
e6d2009605 Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-11-02 23:00:20 +02:00
Adorian Doran
5d706a88d8 style/zen mode/fixed formatting toolbar: fix the position in multi-split mode 2025-11-02 23:00:00 +02:00
Adorian Doran
c7c7e05106 Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-11-02 21:12:36 +02:00
Adorian Doran
e250107202 style/zen mode: fix collections not showing up 2025-11-02 21:12:23 +02:00
Adorian Doran
c141cbcf07 style/zen mode/fixed formatting toolbar: improve pointer events handling 2025-11-02 19:08:59 +02:00
Adorian Doran
8e83562e6a style/zen mode/fixed formatting toolbar: fix backdrop blur 2025-11-02 18:37:02 +02:00
Adorian Doran
08197f56d0 style/zen mode/fixed formatting toolbar: add entrance animation 2025-11-02 18:22:07 +02:00
Adorian Doran
ff0d8a70ad style/zen mode/fixed formatting toolbar: add backdrop blur 2025-11-02 18:08:05 +02:00
Adorian Doran
605b317f29 style/zen mode: restyle the fixed formatting toolbar, relocate at the bottom of the screen 2025-11-02 17:21:27 +02:00
SngAbc
6f0a264869 Merge branch 'main' into electron_newwindow 2025-11-02 23:06:58 +08:00
Adorian Doran
6a89e096e5 style/zen mode: hide promoted attributes 2025-11-02 16:37:19 +02:00
Adorian Doran
e4e0f7619b Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-11-02 16:34:18 +02:00
SiriusXT
f3f07cdd28 fix(electron): port-in-use dialog shown when opening a new window 2025-11-02 20:30:01 +08:00
SiriusXT
ef82c3d48b fix(electron): port-in-use dialog shown when opening a new window | 2025-11-02 16:48:05 +08:00
Adorian Doran
214ba5265a Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-11-01 18:20:08 +02:00
Adorian Doran
ecb6dc7923 Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-10-31 11:33:07 +02:00
Adorian Doran
131fb43ab7 style/zen mode: tweak 2025-10-30 02:48:38 +02:00
Adorian Doran
8a76fdb8d1 style/zen mode: center content, increase font size 2025-10-30 02:30:51 +02:00
Adorian Doran
8e3dbb2f65 client: remove redundant code 2025-10-30 02:27:57 +02:00
Adorian Doran
8b4fee1680 client: make code notes full-width 2025-10-30 01:03:22 +02:00
Adorian Doran
a370b52614 client: remove no longer used translation strings 2025-10-30 00:57:27 +02:00
Adorian Doran
4063229982 client: allow changing the max content width option without a frontend reload 2025-10-30 00:49:02 +02:00
Adorian Doran
1ef03b7a77 client: rework max content width handling 2025-10-30 00:23:41 +02:00
Adorian Doran
e510653edb Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-10-29 23:38:05 +02:00
Adorian Doran
a2c523def1 style: fix a bug preventing background effects to work properly 2025-10-29 20:28:53 +02:00
Adorian Doran
e3604edad7 Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-10-29 19:59:30 +02:00
Adorian Doran
426d8296be Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-10-29 09:30:50 +02:00
Adorian Doran
947e43a615 style: allow custom themes to turn off background effects 2025-10-29 03:20:25 +02:00
Adorian Doran
0424fe4fba client/css var utility: add support for parsing boolean values 2025-10-29 03:13:16 +02:00
Adorian Doran
f789b69506 style: disable tab switching animation while background effects are active, improve performance 2025-10-29 02:39:25 +02:00
Adorian Doran
5df512a69c style: use background effects for empty note, refactor 2025-10-29 01:32:47 +02:00
Adorian Doran
2a5f329ada style: darken the main background color 2025-10-29 00:45:40 +02:00
Adorian Doran
4fe3944585 style/right panel: use own background color when background effects are active 2025-10-29 00:34:20 +02:00
Adorian Doran
98b8e97fd9 style/right panel: add translucent background when background effects are active 2025-10-29 00:20:50 +02:00
Adorian Doran
38a1cd0d35 style: tweak gutters 2025-10-28 23:55:51 +02:00
Adorian Doran
ae544a80c2 Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-10-28 23:43:55 +02:00
Jon Fuller
8d88411fda Merge branch 'main' into fix/fix-equals-operator-in-search 2025-10-28 08:41:47 -07:00
Adorian Doran
ea45024559 Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-10-28 09:51:32 +02:00
Elian Doran
8828e36624 Merge remote-tracking branch 'origin/main' into react/type_widgets 2025-10-26 20:19:47 +02:00
Adorian Doran
64d3589b40 Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-10-25 22:42:20 +03:00
Adorian Doran
638cb4281e style/center pane: optimize the identification of options pages 2025-10-25 11:28:26 +03:00
Adorian Doran
1568908982 style/center pane: allow distinct background colors for note splits 2025-10-25 11:12:00 +03:00
Adorian Doran
4459561308 Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-10-25 10:20:28 +03:00
Adorian Doran
3341e59a80 Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-10-24 02:43:52 +03:00
Adorian Doran
74a805056b style/settings: use Mica for settings pages 2025-10-24 01:03:28 +03:00
Adorian Doran
f42e870de1 style/dropdowns: increase the radius of the backdrop blur 2025-10-23 20:28:36 +03:00
Adorian Doran
ca3964f8b7 style/floating buttons: tweak position 2025-10-23 20:18:52 +03:00
Adorian Doran
ddafda5f4e style/quick edit dialog: tweak the colors for the dark color scheme 2025-10-23 20:01:29 +03:00
Adorian Doran
40b08e1828 Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-10-23 19:43:17 +03:00
Adorian Doran
5141f0a0d5 style/quick edit dialog: tweak the colors for the dark color scheme 2025-10-23 19:43:06 +03:00
Adorian Doran
8c165c0401 style/quick edit dialog: add support for the dark color scheme 2025-10-23 10:57:01 +03:00
Adorian Doran
b4dd40e128 style/quick edit dialog: refactor 2025-10-23 10:48:04 +03:00
Adorian Doran
535b960b76 style/quick edit dialog: ignore monochromatic custom colors 2025-10-23 10:44:23 +03:00
Adorian Doran
b58f37cd4a style/quick edit dialog: tint the dialog background, border and promoted attributes card according to the note's custom color 2025-10-23 10:18:52 +03:00
Adorian Doran
a01fb39599 client/quick edit dialog: make available the CSS variables with the custom color of the edited note 2025-10-23 10:16:18 +03:00
Adorian Doran
be15934b22 Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-10-23 10:03:03 +03:00
Adorian Doran
96b3464f00 Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements 2025-10-22 21:53:07 +03:00
Adorian Doran
2470b0b334 style: tweak the appearance of the promoted attributes cards 2025-10-22 20:06:06 +03:00
Adorian Doran
4344687303 style: tweak the appearance of the promoted attributes cards 2025-10-22 19:58:42 +03:00
Adorian Doran
b4fe46eba3 style: tweak the appearance of option cards 2025-10-22 19:41:49 +03:00
perf3ct
8e227a6146 fix(search): make sure to highlight exact search results too 2025-10-21 14:35:31 -07:00
Jon Fuller
b03cb1ce1b Merge branch 'main' into fix/fix-equals-operator-in-search 2025-10-21 11:30:05 -07:00
perf3ct
fb0d971e48 fix(search): also support exact phrase matching such as ='test phrase' 2025-10-21 10:12:14 -07:00
Elian Doran
bca0846565 Merge remote-tracking branch 'origin/main' into react/type_widgets 2025-10-10 23:05:45 +03:00
Elian Doran
8fef28dcc7 chore(docs): revert changes to documentation 2025-10-10 22:48:20 +03:00
Elian Doran
040ffe945a Merge remote-tracking branch 'origin/main' into react/type_widgets 2025-10-10 22:46:53 +03:00
perf3ct
4fa4112840 feat(search): also support the use of ="exact match search string" 2025-10-10 12:23:57 -07:00
perf3ct
50f0b88eff fix(search): resolve issue when using = operator in search 2025-10-10 09:51:52 -07:00
Elian Doran
4997543fc7 doc(guide): interaction on relation map, including tree operation 2025-10-05 20:19:18 +03:00
Elian Doran
63c91b6741 Revert "fix(deps): downgrade mind-elixir (closes #7170)"
This reverts commit 5969815ed1.
2025-10-05 20:00:08 +03:00
Elian Doran
27b6e26fa5 Merge remote-tracking branch 'origin/main' into react/type_widgets 2025-10-05 19:59:46 +03:00
Elian Doran
7930745a01 chore(react/type_widgets): fix type errors 2025-10-05 19:55:25 +03:00
Elian Doran
6ffe8a2eb5 chore(react/type_widgets): save on window closing 2025-10-05 18:12:36 +03:00
Elian Doran
0dcaa8719f chore(react/type_widgets): save code note if needed 2025-10-05 17:56:31 +03:00
Elian Doran
608605af12 chore(react/type_widgets): save if needed 2025-10-05 17:53:19 +03:00
Elian Doran
3f7b8447d0 refactor(react/type_widgets): bring back executeInActiveNoteDetailWidget 2025-10-05 17:34:45 +03:00
Elian Doran
d3594e4a05 refactor(react/type_widgets): bring back printing and exporting to PDF 2025-10-05 17:07:34 +03:00
Elian Doran
156b4101a5 refactor(react/type_widgets): bring back cut to note 2025-10-05 17:05:05 +03:00
Elian Doran
73213d2a17 refactor(react/type_widgets): bring back execute with type widget 2025-10-05 16:59:46 +03:00
Elian Doran
763bcbd394 refactor(react/type_widgets): extract full height to note types 2025-10-05 16:45:26 +03:00
Elian Doran
d90043e586 chore(react/type_widgets): bring back launch bar fixed effect 2025-10-05 16:42:58 +03:00
Elian Doran
c209a699ea refactor(react/type_widgets): deduplicate containers 2025-10-05 16:36:59 +03:00
Elian Doran
22069d0aef refactor(react/type_widgets): extract note types to different file 2025-10-05 15:55:25 +03:00
Elian Doran
3248654820 chore(react/type_widgets): cache note types 2025-10-05 15:47:27 +03:00
Elian Doran
269c7c9ce7 feat(ai_chat): allow viewing source of the ai chat 2025-10-05 14:25:08 +03:00
Elian Doran
b0c984decd chore(react/type_widgets): fix refresh on first start 2025-10-05 14:15:51 +03:00
Elian Doran
cebb54ddf6 chore(react/type_widgets): get LLM note to load 2025-10-05 12:40:55 +03:00
Elian Doran
22f8929da6 Merge remote-tracking branch 'origin/main' into react/type_widgets 2025-10-05 12:14:14 +03:00
Elian Doran
7192d40e80 chore(react/type_widgets): get ai chat widget to render 2025-10-05 10:21:52 +03:00
Elian Doran
df9d481a93 refactor(mindmap): use proper way to detect direction
See https://github.com/SSShooter/mind-elixir-core/issues/150.
2025-10-04 19:17:13 +03:00
Elian Doran
cf37549f19 refactor(notemap): use proper types 2025-10-04 18:55:26 +03:00
Elian Doran
d2dda95654 fix(notemap): invisible hover color on light theme 2025-10-04 18:28:03 +03:00
Elian Doran
0770f97010 Merge remote-tracking branch 'origin/main' into react/type_widgets
; Conflicts:
;	apps/client/src/widgets/type_widgets/read_only_code.ts
2025-10-04 18:18:11 +03:00
Elian Doran
3caaf2ab79 chore(react/type_widgets): port note map type widget 2025-10-04 18:16:45 +03:00
Elian Doran
8f819a7786 chore(react): reintroduce reactivity to mapRootIdLabel 2025-10-04 18:09:40 +03:00
Elian Doran
0da66617a8 chore(react): reintroduce link distance 2025-10-04 14:14:02 +03:00
Elian Doran
5efe05490d chore(react): reintroduce force configuration 2025-10-04 13:58:29 +03:00
Elian Doran
656b234740 chore(react): reintroduce centering i note map 2025-10-04 13:55:22 +03:00
Elian Doran
e6e9cd3f35 chore(react): improve style of buttons 2025-10-04 13:44:11 +03:00
Elian Doran
845c76fc42 chore(react): bring back fixing nodes 2025-10-04 13:37:36 +03:00
Elian Doran
a4d6da72a1 chore(react): bring back interaction with nodes 2025-10-04 13:17:57 +03:00
Elian Doran
35438d2599 refactor(react): integrate get color for node in rendering 2025-10-04 13:15:13 +03:00
Elian Doran
9a1e7ca3ae chore(react): bring back node pointer & link configuration 2025-10-04 13:10:18 +03:00
Elian Doran
2d29d1b41f chore(react): add back note map link configuration 2025-10-04 13:04:40 +03:00
Elian Doran
ad5ff6e41a refactor(react): use component for map type switcher 2025-10-04 12:58:24 +03:00
Elian Doran
20dcbff68f chore(react): reintroduce map type toggles 2025-10-04 12:55:08 +03:00
Elian Doran
c127e19cfa chore(react): fix obtaining of CSS data for note map 2025-10-04 12:31:35 +03:00
Elian Doran
e32237559e chore(react): start rendering nodes in note map 2025-10-04 12:29:48 +03:00
Elian Doran
09811d23f6 chore(react): port data part of server API 2025-10-04 11:40:31 +03:00
Elian Doran
b41042fec4 chore(react):start porting note map 2025-10-04 11:07:16 +03:00
Elian Doran
08fae19d19 fix(react/type_widgets): crash when switching between relation maps 2025-10-04 10:37:28 +03:00
Elian Doran
9cceff4f02 chore(react/type_widgets): finalize porting relation map 2025-10-04 10:32:23 +03:00
Elian Doran
67d9154795 chore(react/type_widgets): reintroduce relation note dragging 2025-10-04 10:15:38 +03:00
Elian Doran
1eca9f6541 chore(react/type_widgets): reintroduce relation context menu 2025-10-04 09:54:47 +03:00
Elian Doran
c469fffb6e chore(react/type_widgets): reintroduce relation creation 2025-10-04 09:37:14 +03:00
Elian Doran
d076d54170 refactor(react/type_widgets): extract context menu to separate file 2025-10-04 09:15:15 +03:00
Elian Doran
3256c14a20 chore(react/type_widgets): accidental double source config 2025-10-04 09:06:24 +03:00
Elian Doran
460e01a2d6 refactor(react/type_widgets): split note box into separate file 2025-10-04 09:04:22 +03:00
Elian Doran
1913355069 chore(react/type_widgets): relation map source/target config 2025-10-04 08:58:06 +03:00
Elian Doran
f687d91201 chore(react/type_widgets): bring back dragging notes in relation map 2025-10-03 22:40:22 +03:00
Elian Doran
e8e93e985d fix(react/type_widgets): relation map getting occassionally wiped 2025-10-03 13:16:20 +03:00
Elian Doran
c5c304f85b chore(react/type_widgets): add dragigng logic for jsplumb 2025-10-03 11:58:00 +03:00
Elian Doran
58aea03114 fix(react/type_widgets): unable to add new items on existing map 2025-10-03 11:38:05 +03:00
Elian Doran
4af842d2f2 refactor(react/type_widgets): use dedicated component for items 2025-10-03 11:02:33 +03:00
Elian Doran
3b2f5bb09d refactor(react/type_widgets): extract JSPlumb into a separate file 2025-10-03 10:17:45 +03:00
Elian Doran
2d67aab288 fix(react/type_widgets): unable to add new items if the map is empty 2025-10-03 10:11:21 +03:00
Elian Doran
838d761b50 Merge remote-tracking branch 'origin/main' into react/type_widgets
; Conflicts:
;	apps/client/src/widgets/react/hooks.tsx
;	apps/client/src/widgets/type_widgets/abstract_text_type_widget.ts
2025-10-03 09:28:17 +03:00
Elian Doran
7a2d91e7de chore(type_widgets): get relations to render 2025-09-29 22:31:53 +03:00
Elian Doran
082ea7b5c1 chore(type_widgets): port relation map overlays 2025-09-29 21:35:44 +03:00
Elian Doran
c58414bbc1 chore(type_widgets): relation map rename title 2025-09-29 21:11:40 +03:00
Elian Doran
1c1243912b refactor(type_widgets): use API architecture for relation map 2025-09-29 21:05:29 +03:00
Elian Doran
614fc66890 refactor(type_widgets): move relation map to dedicated folder 2025-09-29 20:34:30 +03:00
Elian Doran
0937ef72e2 chore(type_widgets): start porting context menu 2025-09-29 20:33:15 +03:00
Elian Doran
3571023685 chore(type_widgets): bring back relation map zoom controls 2025-09-29 20:16:45 +03:00
Elian Doran
2cd3e3f9c8 chore(type_widgets): bring back relation map note creation 2025-09-29 20:06:22 +03:00
Elian Doran
3d08f686cf feat(code): pretty-print JSON in view source 2025-09-29 19:47:56 +03:00
Elian Doran
d2bf972305 chore(type_widgets): save pan & zoom 2025-09-29 19:41:08 +03:00
Elian Doran
39bd236799 refactor(type_widgets): use editorspaced update for data 2025-09-29 19:32:50 +03:00
Elian Doran
d8b9d14712 chore(type_widgets): introduce panzoom 2025-09-29 19:28:46 +03:00
Elian Doran
9d4127ba6d chore(type_widgets): render note box 2025-09-29 19:04:00 +03:00
Elian Doran
04b678ef4c chore(type_widgets): start porting relation map 2025-09-29 18:50:40 +03:00
Elian Doran
286d7c8228 Merge remote-tracking branch 'origin/main' into react/type_widgets 2025-09-29 17:53:16 +03:00
Elian Doran
5547c3fc2b feat(canvas): read-only mode 2025-09-25 19:28:03 +03:00
Elian Doran
4381399978 feat(mindmap): read-only mode 2025-09-25 19:12:37 +03:00
Elian Doran
5bfa0d13e3 chore(react/type_widgets): refresh on all viewscope changes 2025-09-25 18:44:06 +03:00
Elian Doran
5c21759de9 fix(react/type_widgets): unable to switch view-mode for same note 2025-09-25 18:39:47 +03:00
Elian Doran
e2ef58ed50 chore(react/type_widgets): finalize porting 2025-09-25 18:26:52 +03:00
Elian Doran
7af610a5b4 chore(react/type_widgets): set up image opening 2025-09-25 18:22:58 +03:00
Elian Doran
8a442ba492 chore(react/type_widgets): port executeWithTextEditor 2025-09-25 18:07:47 +03:00
Elian Doran
3ed399a888 chore(react/type_widgets): port text touchbar (untested) 2025-09-25 17:53:48 +03:00
Elian Doran
37d33fb975 chore(react/type_widgets): port @-mention note creation 2025-09-25 17:21:03 +03:00
Elian Doran
d443d79685 chore(react/type_widgets): port and fix follow link under cursor 2025-09-25 14:35:52 +03:00
Elian Doran
a975576214 chore(react/type_widgets): react to snippet changes 2025-09-25 14:21:12 +03:00
Elian Doran
3673162a48 chore(react/type_widgets): bring back add include to note 2025-09-25 13:53:11 +03:00
Elian Doran
0ac428b57a chore(react/type_widgets): remove already integrated file 2025-09-25 13:26:50 +03:00
Elian Doran
45bd9b72b9 chore(react/type_widgets): set up code block word wrap 2025-09-25 13:24:43 +03:00
Elian Doran
cc6ac7d1da chore(react/type_widgets): port text link insertion mechanism 2025-09-25 12:07:06 +03:00
Elian Doran
232fe4e63a refactor(react/type_widgets): move mobile_editor_toolbar 2025-09-25 11:17:55 +03:00
Elian Doran
597426f10d Merge remote-tracking branch 'origin/main' into react/type_widgets 2025-09-25 11:12:28 +03:00
Elian Doran
a0a904766f fix(options/mfa): significant calls to OAuth status endpoint 2025-09-25 10:29:09 +03:00
Elian Doran
db46ca0a76 chore(react/type_widget): insert date/time to text 2025-09-22 18:03:19 +03:00
Elian Doran
a26ee0d769 chore(react/type_widget): hot-pluggable keyboard shortcuts 2025-09-22 17:52:05 +03:00
Elian Doran
46db047fa0 chore(react/type_widget): scroll to end & focus 2025-09-22 13:36:18 +03:00
Elian Doran
efaa1815ec chore(react/type_widget): classic editor & inspector 2025-09-22 13:19:20 +03:00
Elian Doran
2eab8b92d5 chore(react/type_widget): react to content language changes 2025-09-22 12:49:03 +03:00
Elian Doran
8a185262fb chore(react/type_widget): refactor event handling slightly 2025-09-22 12:43:43 +03:00
Elian Doran
f6631b7b9a chore(react/type_widget): save on change 2025-09-22 12:41:32 +03:00
Elian Doran
1e323de01b chore(react/type_widget): port watchdog state change 2025-09-22 12:13:31 +03:00
Elian Doran
f00f2ee5e4 chore(react/type_widget): port notification warning 2025-09-22 12:07:44 +03:00
Elian Doran
78b83cd17b chore(react/type_widget): get editable text to show up 2025-09-22 12:02:45 +03:00
Elian Doran
adea3abff4 chore(react/type_widget): add missing interface 2025-09-22 10:52:03 +03:00
Elian Doran
206618fd54 style(next): improve code block hiehgt in note list 2025-09-22 10:45:55 +03:00
Elian Doran
58a6d70cbb chore(react/type_widget): finalize porting canvas 2025-09-22 10:40:57 +03:00
Elian Doran
44b92a024c chore(react/type_widget): set up self-hosted fonts 2025-09-22 10:14:24 +03:00
Elian Doran
68bf5b7e68 chore(react/type_widget): set up canvas persistence 2025-09-22 09:22:09 +03:00
Elian Doran
8c85aa343c chore(react/type_widget): add more options to canvas 2025-09-22 08:40:56 +03:00
Elian Doran
592a8b2232 chore(react/type_widgets): start porting canvas 2025-09-21 23:33:38 +03:00
Elian Doran
e1ac319a7b chore(react/type_widgets): active note not refreshing 2025-09-21 22:59:16 +03:00
Elian Doran
763c489cd3 feat(render): integrate with search 2025-09-21 22:58:58 +03:00
Elian Doran
b990770e48 feat(render): add a floating button to refresh 2025-09-21 22:44:39 +03:00
Elian Doran
344607d437 chore(react/type_widgets): get render to work 2025-09-21 22:33:11 +03:00
Elian Doran
70d0a5441a chore(react/type_widget): port render note partially 2025-09-21 22:24:51 +03:00
Elian Doran
61278e1f5a chore(react/type_widget): use different loading mechanism 2025-09-21 21:49:23 +03:00
Elian Doran
b73ea6ac4f chore(react/type_widget): reflect note type changes 2025-09-21 21:13:04 +03:00
Elian Doran
5d833c1ac4 chore(react/type_widget): finalize read-only text 2025-09-21 20:50:26 +03:00
Elian Doran
2947682783 chore(react/type_widget): add code block & image integration 2025-09-21 20:47:40 +03:00
Elian Doran
fb46e09428 chore(react/type_widget): render reference links 2025-09-21 20:34:02 +03:00
Elian Doran
ff941b2cb1 chore(react/type_widget): render math in read-only text 2025-09-21 20:29:38 +03:00
Elian Doran
a8007b9063 chore(react/type_widget): render included notes in read-only text 2025-09-21 20:27:58 +03:00
Elian Doran
2f3c2bbac8 chore(react/type_widget): render inline mermaid in read-only text 2025-09-21 20:15:57 +03:00
Elian Doran
e4eb96a1ae chore(react/type_widget): start porting read-only text 2025-09-21 20:03:28 +03:00
Elian Doran
ffe4e9b8de chore(react/type_widget): add deletion widget 2025-09-21 19:43:36 +03:00
Elian Doran
f2b4f49be2 chore(react/type_widget): convert attachment actions 2025-09-21 19:34:03 +03:00
Elian Doran
376ef0c679 chore(react/type_widgets): introduce disabled tooltip 2025-09-21 12:02:02 +03:00
Elian Doran
b7574b3ca7 chore(react/type_widget): start porting attachment actions 2025-09-21 11:06:20 +03:00
Elian Doran
ae1954c320 chore(react/type_widget): port attachment detail 2025-09-21 10:57:08 +03:00
Elian Doran
3171413a18 chore(react/type_widget): react to attachment changes 2025-09-21 10:40:19 +03:00
Elian Doran
dc73467d34 chore(react/type_widget): list attachments with content 2025-09-21 10:32:12 +03:00
Elian Doran
58b14ae31c fix(react/type_widget): mind map attachment incorrect 2025-09-21 10:32:01 +03:00
Elian Doran
e117fbd471 chore(react/type_widget): port atttachment list header 2025-09-21 09:57:09 +03:00
Elian Doran
9a3f675950 chore(react/type_widget): finalize mind map with export PNG/SVG 2025-09-21 09:46:58 +03:00
Elian Doran
26400f2590 fix(mindmap): search not working 2025-09-21 09:35:23 +03:00
Elian Doran
7d99a92bd9 chore(react/type_widget): save mindmap attachment 2025-09-21 09:26:44 +03:00
Elian Doran
3417e37f16 chore(react/type_widget): save direction upon button press 2025-09-21 09:20:00 +03:00
Elian Doran
143e6a556c chore(react/type_widget): persist data 2025-09-20 22:22:20 +03:00
Elian Doran
02259c55f3 chore(react/type_widget): get mindmap to render 2025-09-20 21:52:57 +03:00
Elian Doran
cc19a217ad chore(react/type_widget): finalize SVG split editor 2025-09-20 21:34:56 +03:00
Elian Doran
d95ed4a5d2 chore(react/type_widget): export as SVG/PNG 2025-09-20 21:29:20 +03:00
Elian Doran
469683f30f chore(react/type_widget): reimplement zoom + reset buttons 2025-09-20 21:23:34 +03:00
Elian Doran
42d0cc12b5 chore(react/type_widget): more generic repositioning 2025-09-20 21:19:19 +03:00
Elian Doran
b376842e2d chore(react/type_widget): reposition on split resize 2025-09-20 21:12:29 +03:00
Elian Doran
145ff1a2a5 chore(react/type_widget): restore pan/zoom 2025-09-20 21:07:51 +03:00
Elian Doran
8e9f5fb486 chore(react/type_widget): fix 4px scroll in SVG editor 2025-09-20 14:34:15 +03:00
Elian Doran
3dd757a857 chore(react/type_widget): disable code background change in split 2025-09-20 14:16:34 +03:00
Elian Doran
bde7b753a0 chore(react/type_widget): save SVG attachment 2025-09-20 14:08:50 +03:00
Elian Doran
02017ebd9d chore(react/type_widget): bring back on error opacity 2025-09-20 13:47:12 +03:00
Elian Doran
8caaa99415 chore(react/type_widget): basic SVG rendering 2025-09-20 13:27:58 +03:00
Elian Doran
c49b90d33f chore(react/type_widget): add preview buttons 2025-09-20 13:15:13 +03:00
Elian Doran
6dd939df14 chore(react/type_widget): bring back update interval 2025-09-20 13:04:36 +03:00
Elian Doran
b19da81572 chore(react/type_widget): force line wrapping 2025-09-20 13:00:15 +03:00
Elian Doran
425ffc02d8 chore(react/type_widget): bring back split resizer 2025-09-20 12:54:18 +03:00
Elian Doran
695e8489ad chore(react/type_widget): pass error information 2025-09-20 12:41:39 +03:00
Elian Doran
2f4e13b1bb chore(react/type_widget): bring back order of editor/preview 2025-09-20 12:38:05 +03:00
Elian Doran
c8a9b994d6 chore(react/type_widget): bring back read-only 2025-09-20 12:34:36 +03:00
Elian Doran
3d5b319eb2 chore(react/type_widget): bring back split orientation 2025-09-20 12:31:45 +03:00
Elian Doran
bed3c2dc67 chore(react/type_widget): prepare structure for split editor 2025-09-20 12:25:11 +03:00
Elian Doran
256d1863d2 chore(react/type_widget): port backend log 2025-09-20 12:16:51 +03:00
Elian Doran
4a4502dfea chore(react/type_widget): bring back read-only temporary disable 2025-09-20 11:59:43 +03:00
Elian Doran
91f21e149b chore(react/type_widget): bring back focusing after tab switch 2025-09-20 11:46:23 +03:00
Elian Doran
6ef468adc4 chore(react/type_widget): bring back scroll to end 2025-09-20 11:38:28 +03:00
Elian Doran
e576fa03da chore(react/type_widget): fix sizing 2025-09-20 11:30:40 +03:00
Elian Doran
6bcce08042 chore(react/type_widget): react to line wrapping 2025-09-20 11:25:07 +03:00
Elian Doran
f496caa92c refactor(react/type_widget): separate Trilium-specific implementation 2025-09-20 11:22:48 +03:00
Elian Doran
43dcdf8925 chore(react/type_widget): apply background color for read-only code notes 2025-09-20 11:11:00 +03:00
Elian Doran
2c014fb071 chore(react/type_widget): set up background color for code notes 2025-09-20 11:02:43 +03:00
Elian Doran
2273507ef4 chore(react/type_widget): unnecessary imports 2025-09-20 10:37:34 +03:00
Elian Doran
70a710be79 chore(react/type_widget): react to code theme 2025-09-20 10:14:21 +03:00
Elian Doran
7a3ee7971c chore(react/type_widget): add back keyboard shortcut for editable code 2025-09-20 10:08:46 +03:00
Elian Doran
c86123e3a9 chore(react/type_widget): integrate touch bar for editable code 2025-09-20 10:03:00 +03:00
Elian Doran
9480227b69 chore(react/type_widget): add more options to editable code 2025-09-20 09:59:49 +03:00
Elian Doran
79be13e6c7 chore(react/type_widget): reload content on external change 2025-09-20 09:56:55 +03:00
Elian Doran
63e3a27b34 refactor(react/type_widget): simplify handling of new notes 2025-09-20 09:47:28 +03:00
Elian Doran
9eae6620d0 chore(react/type_widget): basic editable code 2025-09-20 09:44:36 +03:00
Elian Doran
6517dd1190 chore(react/type_widget): finalize readonly code 2025-09-20 09:06:55 +03:00
Elian Doran
f72087acc3 chore(react/type_widget): port read only code basic functionality 2025-09-20 08:57:47 +03:00
Elian Doran
77e7c414b6 chore(react/type_widget): react to note revisions 2025-09-19 22:45:12 +03:00
Elian Doran
3a68395ca7 feat(react/type_widget): add copy image reference floating button to image 2025-09-19 22:42:06 +03:00
Elian Doran
0a0d9775b2 chore(react/type_widget): port image 2025-09-19 22:41:18 +03:00
Elian Doran
aa6e68ad39 chore(react/type_widget): port file 2025-09-19 22:22:45 +03:00
Elian Doran
034073a5e1 chore(react/type_widget): fix missing tbody 2025-09-19 22:18:10 +03:00
Elian Doran
d83ff641d7 chore(react/type_widget): bring back full-height 2025-09-19 21:55:37 +03:00
Elian Doran
071fcb85c9 chore(react/type_widget): basic integration of web view 2025-09-19 21:27:45 +03:00
Elian Doran
daa5ee93e9 chore(react/type_widget): port content widget 2025-09-19 21:18:09 +03:00
Elian Doran
db7cda3fe6 chore(react/type_widget): have book react to reloaded children 2025-09-19 19:06:07 +03:00
Elian Doran
fa55c5720e chore(react/type_widget): port book 2025-09-19 19:03:31 +03:00
Elian Doran
d1a9890932 chore(react/type_widget): port protected session 2025-09-19 18:55:04 +03:00
Elian Doran
c9fe358811 chore(react/type_widget): port none type widget 2025-09-19 18:35:49 +03:00
Elian Doran
bbb927c83f chore(react/type_widget): port doc widget 2025-09-19 18:32:45 +03:00
Elian Doran
07b86c8cf7 chore(react/type_widget): port empty workspace switcher 2025-09-19 18:15:10 +03:00
Elian Doran
3dbf20af52 chore(react/type_widget): port empty search 2025-09-19 18:09:23 +03:00
Elian Doran
1fb329565f chore(react/type_widget): move old widgets 2025-09-19 17:40:24 +03:00
Elian Doran
06bfb0073a chore(react/type_widget): determine note type 2025-09-19 17:31:10 +03:00
Elian Doran
3d64c320fb chore(react/type_widget): start with fresh note detail 2025-09-19 16:53:31 +03:00
548 changed files with 17678 additions and 28632 deletions

View File

@@ -77,7 +77,7 @@ jobs:
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
- name: Publish release
uses: softprops/action-gh-release@v2.4.1
uses: softprops/action-gh-release@v2.4.2
if: ${{ github.event_name != 'pull_request' }}
with:
make_latest: false
@@ -118,7 +118,7 @@ jobs:
arch: ${{ matrix.arch }}
- name: Publish release
uses: softprops/action-gh-release@v2.4.1
uses: softprops/action-gh-release@v2.4.2
if: ${{ github.event_name != 'pull_request' }}
with:
make_latest: false

View File

@@ -4,6 +4,7 @@ on:
push:
branches:
- main
- hotfix
paths-ignore:
- "apps/website/**"
pull_request:
@@ -13,8 +14,24 @@ permissions:
contents: read
jobs:
main:
runs-on: ubuntu-latest
e2e:
strategy:
fail-fast: false
matrix:
include:
- name: linux-x64
os: ubuntu-22.04
arch: x64
- name: linux-arm64
os: ubuntu-24.04-arm
arch: arm64
runs-on: ${{ matrix.os }}
name: E2E tests on ${{ matrix.name }}
env:
TRILIUM_DOCKER: 1
TRILIUM_PORT: 8082
TRILIUM_DATA_DIR: "${{ github.workspace }}/apps/server/spec/db"
TRILIUM_INTEGRATION_TEST: memory
steps:
- uses: actions/checkout@v5
with:
@@ -29,9 +46,34 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- run: pnpm exec playwright install --with-deps
- run: pnpm --filter server-e2e e2e
- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps
- name: Build the server
uses: ./.github/actions/build-server
with:
os: linux
arch: ${{ matrix.arch }}
- name: Unpack and start the server
run: |
version=$(node --eval "console.log(require('./package.json').version)")
file=$(find ./upload -name '*.tar.xz' -print -quit)
name=$(basename "$file" .tar.xz)
mkdir -p ./server-dist
tar -xvf "$file" -C ./server-dist
server_dir="./server-dist/TriliumNotes-Server-$version-linux-${{ matrix.arch }}"
if [ ! -d "$server_dir" ]; then
echo Missing dir.
exit 1
fi
cd "$server_dir"
"./trilium.sh" &
sleep 10
- name: Server end-to-end tests
run: pnpm --filter server-e2e e2e
- name: Upload test report
if: failure()
@@ -39,3 +81,7 @@ jobs:
with:
name: e2e report
path: apps/server-e2e/test-output
- name: Kill the server
if: always()
run: pkill -f trilium || true

View File

@@ -127,7 +127,7 @@ jobs:
path: upload
- name: Publish stable release
uses: softprops/action-gh-release@v2.4.1
uses: softprops/action-gh-release@v2.4.2
with:
draft: false
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md

View File

@@ -41,12 +41,12 @@
"@types/node": "24.10.0",
"@types/yargs": "17.0.34",
"@vitest/coverage-v8": "3.2.4",
"eslint": "9.39.0",
"eslint": "9.39.1",
"eslint-plugin-simple-import-sort": "12.1.1",
"esm": "3.2.25",
"jsdoc": "4.0.5",
"lorem-ipsum": "2.0.8",
"rcedit": "4.0.1",
"rcedit": "5.0.0",
"rimraf": "6.1.0",
"tslib": "2.8.1"
},

View File

@@ -9,9 +9,9 @@
"keywords": [],
"author": "Elian Doran <contact@eliandoran.me>",
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.20.0",
"packageManager": "pnpm@10.21.0",
"devDependencies": {
"@redocly/cli": "2.10.0",
"@redocly/cli": "2.11.1",
"archiver": "7.0.1",
"fs-extra": "11.3.2",
"react": "19.2.0",

View File

@@ -22,8 +22,9 @@ async function main() {
buildSwagger(context);
buildScriptApi(context);
// Copy index file.
// Copy index and 404 files.
cpSync(join(__dirname, "index.html"), join(context.baseDir, "index.html"));
cpSync(join(context.baseDir, "user-guide/404.html"), join(context.baseDir, "404.html"));
}
main();

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/client",
"version": "0.99.3",
"version": "0.99.5",
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
"private": true,
"license": "AGPL-3.0-only",
@@ -15,7 +15,7 @@
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
},
"dependencies": {
"@eslint/js": "9.39.0",
"@eslint/js": "9.39.1",
"@excalidraw/excalidraw": "0.18.0",
"@fullcalendar/core": "6.1.19",
"@fullcalendar/daygrid": "6.1.19",
@@ -43,7 +43,7 @@
"draggabilly": "3.0.0",
"force-graph": "1.51.0",
"globals": "16.5.0",
"i18next": "25.6.0",
"i18next": "25.6.2",
"i18next-http-backend": "3.0.2",
"jquery": "3.7.1",
"jquery.fancytree": "2.38.5",
@@ -53,13 +53,13 @@
"leaflet": "1.9.4",
"leaflet-gpx": "2.2.0",
"mark.js": "8.11.1",
"marked": "16.4.1",
"marked": "16.4.2",
"mermaid": "11.12.1",
"mind-elixir": "5.3.4",
"mind-elixir": "5.3.5",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.27.2",
"react-i18next": "16.2.3",
"react-i18next": "16.2.4",
"reveal.js": "5.2.1",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",

View File

@@ -13,7 +13,6 @@ import MainTreeExecutors from "./main_tree_executors.js";
import toast from "../services/toast.js";
import ShortcutComponent from "./shortcut_component.js";
import { t, initLocale } from "../services/i18n.js";
import type NoteDetailWidget from "../widgets/note_detail.js";
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
import type { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js";
@@ -21,8 +20,6 @@ import type LoadResults from "../services/load_results.js";
import type { Attribute } from "../services/attribute_parser.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js";
import type TypeWidget from "../widgets/type_widgets/type_widget.js";
import type EditableTextTypeWidget from "../widgets/type_widgets/editable_text.js";
import type { NativeImage, TouchBar } from "electron";
import TouchBarComponent from "./touch_bar.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5";
@@ -33,6 +30,10 @@ import { ColumnComponent } from "tabulator-tables";
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
import type RootContainer from "../widgets/containers/root_container.js";
import { SqlExecuteResults } from "@triliumnext/commons";
import { AddLinkOpts } from "../widgets/dialogs/add_link.jsx";
import { IncludeNoteOpts } from "../widgets/dialogs/include_note.jsx";
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
import type { MarkdownImportOpts } from "../widgets/dialogs/markdown_import.jsx";
interface Layout {
getRootWidget: (appContext: AppContext) => RootContainer;
@@ -199,7 +200,7 @@ export type CommandMappings = {
resetLauncher: ContextMenuCommandData;
executeInActiveNoteDetailWidget: CommandData & {
callback: (value: NoteDetailWidget | PromiseLike<NoteDetailWidget>) => void;
callback: (value: ReactWrappedWidget) => void;
};
executeWithTextEditor: CommandData &
ExecuteCommandData<CKTextEditor> & {
@@ -211,7 +212,7 @@ export type CommandMappings = {
* Generally should not be invoked manually, as it is used by {@link NoteContext.getContentElement}.
*/
executeWithContentElement: CommandData & ExecuteCommandData<JQuery<HTMLElement>>;
executeWithTypeWidget: CommandData & ExecuteCommandData<TypeWidget | null>;
executeWithTypeWidget: CommandData & ExecuteCommandData<ReactWrappedWidget | null>;
addTextToActiveEditor: CommandData & {
text: string;
};
@@ -221,9 +222,9 @@ export type CommandMappings = {
showPasswordNotSet: CommandData;
showProtectedSessionPasswordDialog: CommandData;
showUploadAttachmentsDialog: CommandData & { noteId: string };
showIncludeNoteDialog: CommandData & { textTypeWidget: EditableTextTypeWidget };
showAddLinkDialog: CommandData & { textTypeWidget: EditableTextTypeWidget, text: string };
showPasteMarkdownDialog: CommandData & { textTypeWidget: EditableTextTypeWidget };
showIncludeNoteDialog: CommandData & IncludeNoteOpts;
showAddLinkDialog: CommandData & AddLinkOpts;
showPasteMarkdownDialog: CommandData & MarkdownImportOpts;
closeProtectedSessionPasswordDialog: CommandData;
copyImageReferenceToClipboard: CommandData;
copyImageToClipboard: CommandData;
@@ -328,6 +329,7 @@ export type CommandMappings = {
exportAsPdf: CommandData;
openNoteExternally: CommandData;
openNoteCustom: CommandData;
openNoteOnServer: CommandData;
renderActiveNote: CommandData;
unhoist: CommandData;
reloadFrontendApp: CommandData;
@@ -485,13 +487,8 @@ type EventMappings = {
relationMapResetZoomIn: { ntxId: string | null | undefined };
relationMapResetZoomOut: { ntxId: string | null | undefined };
activeNoteChanged: {};
showAddLinkDialog: {
textTypeWidget: EditableTextTypeWidget;
text: string;
};
showIncludeDialog: {
textTypeWidget: EditableTextTypeWidget;
};
showAddLinkDialog: AddLinkOpts;
showIncludeDialog: IncludeNoteOpts;
openBulkActionsDialog: {
selectedOrActiveNoteIds: string[];
};
@@ -499,6 +496,10 @@ type EventMappings = {
noteIds: string[];
};
refreshData: { ntxId: string | null | undefined };
contentSafeMarginChanged: {
top: number;
noteContext: NoteContext;
}
};
export type EventListener<T extends EventNames> = {
@@ -666,6 +667,10 @@ export class AppContext extends Component {
this.beforeUnloadListeners.push(obj);
}
}
removeBeforeUnloadListener(listener: (() => boolean)) {
this.beforeUnloadListeners = this.beforeUnloadListeners.filter(l => l !== listener);
}
}
const appContext = new AppContext(window.glob.isMainWindow);

View File

@@ -9,10 +9,10 @@ import hoistedNoteService from "../services/hoisted_note.js";
import options from "../services/options.js";
import type { ViewScope } from "../services/link.js";
import type FNote from "../entities/fnote.js";
import type TypeWidget from "../widgets/type_widgets/type_widget.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror";
import { closeActiveDialog } from "../services/dialog.js";
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
export interface SetNoteOpts {
triggerSwitchEvent?: unknown;
@@ -397,7 +397,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
async getTypeWidget() {
return this.timeout(
new Promise<TypeWidget | null>((resolve) =>
new Promise<ReactWrappedWidget | null>((resolve) =>
appContext.triggerCommand("executeWithTypeWidget", {
resolve,
ntxId: this.ntxId

View File

@@ -7,7 +7,6 @@ import protectedSessionService from "../services/protected_session.js";
import options from "../services/options.js";
import froca from "../services/froca.js";
import utils from "../services/utils.js";
import LlmChatPanel from "../widgets/llm_chat_panel.js";
import toastService from "../services/toast.js";
import noteCreateService from "../services/note_create.js";
@@ -67,6 +66,13 @@ export default class RootCommandExecutor extends Component {
}
}
openNoteOnServerCommand() {
const noteId = appContext.tabManager.getActiveContextNoteId();
if (noteId) {
openService.openNoteOnServer(noteId);
}
}
enterProtectedSessionCommand() {
protectedSessionService.enterProtectedSession();
}
@@ -171,7 +177,8 @@ export default class RootCommandExecutor extends Component {
}
toggleTrayCommand() {
if (!utils.isElectron()) return;
if (!utils.isElectron() || options.is("disableTray")) return;
const { BrowserWindow } = utils.dynamicRequire("@electron/remote");
const windows = BrowserWindow.getAllWindows() as Electron.BaseWindow[];
const isVisible = windows.every((w) => w.isVisible());

View File

@@ -265,6 +265,7 @@ export default class TabManager extends Component {
mainNtxId: string | null = null
): Promise<NoteContext> {
const noteContext = new NoteContext(ntxId, hoistedNoteId, mainNtxId);
noteContext.setEmpty();
const existingNoteContext = this.children.find((nc) => nc.ntxId === noteContext.ntxId);

View File

@@ -1,47 +1,49 @@
import FlexContainer from "../widgets/containers/flex_container.js";
import TabRowWidget from "../widgets/tab_row.js";
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteTitleWidget from "../widgets/note_title.jsx";
import NoteDetailWidget from "../widgets/note_detail.js";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import NoteIconWidget from "../widgets/note_icon.jsx";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
import SpacerWidget from "../widgets/spacer.js";
import QuickSearchWidget from "../widgets/quick_search.js";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import FindWidget from "../widgets/find.js";
import TocWidget from "../widgets/toc.js";
import HighlightsListWidget from "../widgets/highlights_list.js";
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
import LauncherContainer from "../widgets/containers/launcher_container.js";
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import ScrollPadding from "../widgets/scroll_padding.js";
import options from "../services/options.js";
import utils from "../services/utils.js";
import type { AppContext } from "../components/app_context.js";
import type { WidgetsByParent } from "../services/bundle.js";
import { applyModals } from "./layout_commons.js";
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import SearchResult from "../widgets/search_result.jsx";
import ApiLog from "../widgets/api_log.jsx";
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
import ContentHeader from "../widgets/containers/content-header.js";
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
import FindWidget from "../widgets/find.js";
import FlexContainer from "../widgets/containers/flex_container.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
import HighlightsListWidget from "../widgets/highlights_list.js";
import LauncherContainer from "../widgets/containers/launcher_container.js";
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
import NoteIconWidget from "../widgets/note_icon.jsx";
import NoteList from "../widgets/collections/NoteList.jsx";
import NoteTitleWidget from "../widgets/note_title.jsx";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import options from "../services/options.js";
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
import QuickSearchWidget from "../widgets/quick_search.js";
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import ScrollPadding from "../widgets/scroll_padding.js";
import SearchResult from "../widgets/search_result.jsx";
import SharedInfo from "../widgets/shared_info.jsx";
import SpacerWidget from "../widgets/spacer.js";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import SqlResults from "../widgets/sql_result.js";
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
import TabRowWidget from "../widgets/tab_row.js";
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
import ApiLog from "../widgets/api_log.jsx";
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
import SharedInfo from "../widgets/shared_info.jsx";
import NoteList from "../widgets/collections/NoteList.jsx";
import TocWidget from "../widgets/toc.js";
import type { AppContext } from "../components/app_context.js";
import type { WidgetsByParent } from "../services/bundle.js";
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import utils from "../services/utils.js";
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
export default class DesktopLayout {
@@ -129,15 +131,18 @@ export default class DesktopLayout {
.child(<CreatePaneButton />)
)
.child(<Ribbon />)
.child(<SharedInfo />)
.child(new WatchedFileUpdateStatusWidget())
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
.child(
new ScrollingContainer()
.filling()
.child(new PromotedAttributesWidget())
.child(new ContentHeader()
.child(<ReadOnlyNoteInfoBar />)
.child(<SharedInfo />)
)
.child(<PromotedAttributes />)
.child(<SqlTableSchemas />)
.child(new NoteDetailWidget())
.child(<NoteDetail />)
.child(<NoteList media="screen" />)
.child(<SearchResult />)
.child(<SqlResults />)

View File

@@ -26,11 +26,11 @@ import PopupEditorDialog from "../widgets/dialogs/popup_editor.js";
import FlexContainer from "../widgets/containers/flex_container.js";
import NoteIconWidget from "../widgets/note_icon";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import NoteDetailWidget from "../widgets/note_detail.js";
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
import NoteTitleWidget from "../widgets/note_title.jsx";
import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.js";
import NoteList from "../widgets/collections/NoteList.jsx";
import NoteDetail from "../widgets/NoteDetail.jsx";
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
export function applyModals(rootContainer: RootContainer) {
@@ -66,7 +66,7 @@ export function applyModals(rootContainer: RootContainer) {
.child(<NoteTitleWidget />))
.child(<StandaloneRibbonAdapter component={FormattingToolbar} />)
.child(new PromotedAttributesWidget())
.child(new NoteDetailWidget())
.child(<NoteDetail />)
.child(<NoteList media="screen" displayOnlyCollections />))
.child(<CallToActionDialog />);
}

View File

@@ -1,32 +1,34 @@
import { applyModals } from "./layout_commons.js";
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import { useNoteContext } from "../widgets/react/hooks.jsx";
import CloseZenModeButton from "../widgets/close_zen_button.js";
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
import FlexContainer from "../widgets/containers/flex_container.js";
import NoteTitleWidget from "../widgets/note_title.js";
import NoteDetailWidget from "../widgets/note_detail.js";
import QuickSearchWidget from "../widgets/quick_search.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
import LauncherContainer from "../widgets/containers/launcher_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import SharedInfoWidget from "../widgets/shared_info.js";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
import type AppContext from "../components/app_context.js";
import TabRowWidget from "../widgets/tab_row.js";
import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js";
import { applyModals } from "./layout_commons.js";
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
import { useNoteContext } from "../widgets/react/hooks.jsx";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
import CloseZenModeButton from "../widgets/close_zen_button.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
import NoteList from "../widgets/collections/NoteList.jsx";
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
import NoteTitleWidget from "../widgets/note_title.js";
import ContentHeader from "../widgets/containers/content-header.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import QuickSearchWidget from "../widgets/quick_search.js";
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
import RootContainer from "../widgets/containers/root_container.js";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx";
import SearchResult from "../widgets/search_result.jsx";
import SharedInfoWidget from "../widgets/shared_info.js";
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
import TabRowWidget from "../widgets/tab_row.js";
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
import type AppContext from "../components/app_context.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
const MOBILE_CSS = `
<style>
@@ -149,14 +151,17 @@ export default class MobileLayout {
.child(<NoteTitleWidget />)
.child(<MobileDetailMenu />)
)
.child(<SharedInfoWidget />)
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
.child(new PromotedAttributesWidget())
.child(
new ScrollingContainer()
.filling()
.contentSized()
.child(new NoteDetailWidget())
.child(new ContentHeader()
.child(<ReadOnlyNoteInfoBar />)
.child(<SharedInfoWidget />)
)
.child(<NoteDetail />)
.child(<NoteList media="screen" />)
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
.child(<SearchResult />)

View File

@@ -137,7 +137,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
command: "editBranchPrefix",
keyboardShortcut: "editBranchPrefix",
uiIcon: "bx bx-rename",
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp
enabled: isNotRoot && parentNotSearch && notOptionsOrHelp
},
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },

View File

@@ -1,3 +1,5 @@
@import "boxicons/css/boxicons.min.css";
:root {
--print-font-size: 11pt;
--ck-content-color-image-caption-background: transparent !important;

View File

@@ -70,6 +70,9 @@ function SingleNoteRenderer({ note, onReady }: RendererProps) {
});
})
);
// Check custom CSS.
await loadCustomCss(note);
}
load().then(() => requestAnimationFrame(onReady))
@@ -89,7 +92,10 @@ function CollectionRenderer({ note, onReady }: RendererProps) {
ntxId="print"
highlightedTokens={null}
media="print"
onReady={onReady}
onReady={async () => {
await loadCustomCss(note);
onReady();
}}
/>;
}
@@ -102,4 +108,25 @@ function Error404({ noteId }: { noteId: string }) {
)
}
async function loadCustomCss(note: FNote) {
const printCssNotes = await note.getRelationTargets("printCss");
let loadPromises: JQueryPromise<void>[] = [];
for (const printCssNote of printCssNotes) {
if (!printCssNote || (printCssNote.type !== "code" && printCssNote.mime !== "text/css")) continue;
const linkEl = document.createElement("link");
linkEl.href = `/api/notes/${printCssNote.noteId}/download`;
linkEl.rel = "stylesheet";
const promise = $.Deferred();
loadPromises.push(promise.promise());
linkEl.onload = () => promise.resolve();
document.head.appendChild(linkEl);
}
await Promise.allSettled(loadPromises);
}
main();

View File

@@ -11,7 +11,7 @@ import RightPanelWidget from "../widgets/right_panel_widget.js";
import ws from "./ws.js";
import appContext from "../components/app_context.js";
import NoteContextAwareWidget from "../widgets/note_context_aware_widget.js";
import BasicWidget from "../widgets/basic_widget.js";
import BasicWidget, { ReactWrappedWidget } from "../widgets/basic_widget.js";
import SpacedUpdate from "./spaced_update.js";
import shortcutService from "./shortcuts.js";
import dialogService from "./dialog.js";
@@ -19,7 +19,6 @@ import type FNote from "../entities/fnote.js";
import { t } from "./i18n.js";
import dayjs from "dayjs";
import type NoteContext from "../components/note_context.js";
import type NoteDetailWidget from "../widgets/note_detail.js";
import type Component from "../components/component.js";
import { formatLogMessage } from "@triliumnext/commons";
@@ -317,7 +316,7 @@ export interface Api {
* Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the
* implementation of actual widget type.
*/
getActiveNoteDetailWidget(): Promise<NoteDetailWidget>;
getActiveNoteDetailWidget(): Promise<ReactWrappedWidget>;
/**
* @returns returns a note path of active note or null if there isn't active note
*/

View File

@@ -1,6 +1,6 @@
import server from "./server.js";
import appContext, { type CommandNames } from "../components/app_context.js";
import shortcutService from "./shortcuts.js";
import appContext from "../components/app_context.js";
import shortcutService, { ShortcutBinding } from "./shortcuts.js";
import type Component from "../components/component.js";
import type { ActionKeyboardShortcut } from "@triliumnext/commons";
@@ -30,12 +30,18 @@ async function getActionsForScope(scope: string) {
async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, component: Component) {
const actions = await getActionsForScope(scope);
const bindings: ShortcutBinding[] = [];
for (const action of actions) {
for (const shortcut of action.effectiveShortcuts ?? []) {
shortcutService.bindElShortcut($el, shortcut, () => component.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId }));
const binding = shortcutService.bindElShortcut($el, shortcut, () => component.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId }));
if (binding) {
bindings.push(binding);
}
}
}
return bindings;
}
getActionsForScope("window").then((actions) => {

View File

@@ -280,7 +280,7 @@ function goToLink(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent) {
* @param $link the jQuery element of the link that was clicked, used to determine if the link is an anchor link (e.g., `#fn1` or `#fnref1`) and to handle it accordingly.
* @returns `true` if the link was handled (i.e., the element was found and scrolled to), or a falsy value otherwise.
*/
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) {
export function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) {
if (hrefLink?.startsWith("data:")) {
return true;
}

View File

@@ -1,4 +1,5 @@
import utils from "./utils.js";
import options from "./options.js";
import server from "./server.js";
type ExecFunction = (command: string, cb: (err: string, stdout: string, stderror: string) => void) => void;
@@ -126,7 +127,7 @@ function downloadRevision(noteId: string, revisionId: string) {
/**
* @param url - should be without initial slash!!!
*/
function getUrlForDownload(url: string) {
export function getUrlForDownload(url: string) {
if (utils.isElectron()) {
// electron needs absolute URL, so we extract current host, port, protocol
return `${getHost()}/${url}`;
@@ -171,6 +172,21 @@ function getHost() {
return `${url.protocol}//${url.hostname}:${url.port}`;
}
async function openNoteOnServer(noteId: string) {
// Get the sync server host from options
const syncServerHost = options.get("syncServerHost");
if (!syncServerHost) {
console.error("No sync server host configured");
return;
}
const url = new URL(`#root/${noteId}`, syncServerHost).toString();
// Use window.open to ensure link opens in external browser in Electron
window.open(url, '_blank', 'noopener,noreferrer');
}
async function openDirectory(directory: string) {
try {
if (utils.isElectron()) {
@@ -198,5 +214,6 @@ export default {
openAttachmentExternally,
openNoteCustom,
openAttachmentCustom,
openNoteOnServer,
openDirectory
};

View File

@@ -159,7 +159,7 @@ describe("shortcuts", () => {
expect(matchesShortcut(event, "Shift+F1")).toBeTruthy();
// Special keys
for (const keyCode of [ "Delete", "Enter" ]) {
for (const keyCode of [ "Delete", "Enter", "NumpadEnter" ]) {
event = createKeyboardEvent({ key: keyCode, code: keyCode });
expect(matchesShortcut(event, keyCode), `Key ${keyCode}`).toBeTruthy();
}

View File

@@ -3,7 +3,7 @@ import utils from "./utils.js";
type ElementType = HTMLElement | Document;
type Handler = (e: KeyboardEvent) => void;
interface ShortcutBinding {
export interface ShortcutBinding {
element: HTMLElement | Document;
shortcut: string;
handler: Handler;
@@ -46,6 +46,7 @@ for (let i = 1; i <= 19; i++) {
const KEYCODES_WITH_NO_MODIFIER = new Set([
"Delete",
"Enter",
"NumpadEnter",
...functionKeyCodes
]);
@@ -126,10 +127,20 @@ function bindElShortcut($el: JQuery<ElementType | Element>, keyboardShortcut: st
activeBindings.set(key, []);
}
activeBindings.get(key)!.push(binding);
return binding;
}
}
}
export function removeIndividualBinding(binding: ShortcutBinding) {
const key = binding.namespace ?? "global";
const activeBindingsInNamespace = activeBindings.get(key);
if (activeBindingsInNamespace) {
activeBindings.set(key, activeBindingsInNamespace.filter(aBinding => aBinding.handler === binding.handler));
}
binding.element.removeEventListener("keydown", binding.listener);
}
function removeNamespaceBindings(namespace: string) {
const bindings = activeBindings.get(namespace);
if (bindings) {

View File

@@ -24,7 +24,9 @@ export async function formatCodeBlocks($container: JQuery<HTMLElement>) {
continue;
}
applyCopyToClipboardButton($(codeBlock));
if (glob.device !== "print") {
applyCopyToClipboardButton($(codeBlock));
}
if (syntaxHighlightingEnabled) {
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType);

View File

@@ -173,7 +173,7 @@ const entityMap: Record<string, string> = {
"=": "&#x3D;"
};
function escapeHtml(str: string) {
export function escapeHtml(str: string) {
return str.replace(/[&<>"'`=\/]/g, (s) => entityMap[s]);
}
@@ -841,7 +841,7 @@ export function arrayEqual<T>(a: T[], b: T[]) {
return true;
}
type Indexed<T extends object> = T & { index: number };
export type Indexed<T extends object> = T & { index: number };
/**
* Given an object array, alters every object in the array to have an index field assigned to it.

View File

@@ -407,7 +407,7 @@ body.desktop .tabulator-popup-container,
.dropdown-menu .disabled .disabled-tooltip {
pointer-events: all;
margin-inline-start: 8px;
font-size: 0.5em;
font-size: 0.75rem;
color: var(--disabled-tooltip-icon-color);
cursor: help;
opacity: 0.75;
@@ -1104,7 +1104,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
.card {
color: inherit !important;
background-color: inherit !important;
border-color: var(--main-border-color) !important;
}
@@ -1759,10 +1758,10 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
flex-direction: column;
margin-inline-start: 10px;
margin-inline-end: 5px;
background: transparent;
}
#right-pane .card-header {
background: inherit;
padding: 6px 0 3px 0;
width: 99%; /* to give minimal right margin */
background-color: var(--button-background-color);
@@ -1809,12 +1808,15 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
}
.note-split {
/* Limits the maximum width of the note */
--max-content-width: var(--preferred-max-content-width);
margin-inline-start: auto;
margin-inline-end: auto;
}
.note-split.full-content-width {
max-width: 999999px;
--max-content-width: unset;
}
button.close:hover {
@@ -2034,13 +2036,16 @@ body.zen #right-pane,
body.zen #mobile-sidebar-wrapper,
body.zen .tab-row-container,
body.zen .tab-row-widget,
body.zen .shared-info-widget,
body.zen .ribbon-container:not(:has(.classic-toolbar-widget)),
body.zen .ribbon-container:has(.classic-toolbar-widget) .ribbon-top-row,
body.zen .ribbon-container .ribbon-body:not(:has(.classic-toolbar-widget)),
body.zen .note-icon-widget,
body.zen .title-row .icon-action,
body.zen .promoted-attributes-widget,
body.zen .floating-buttons-children > *:not(.bx-edit-alt),
body.zen .action-button {
body.zen .action-button,
body.zen .note-split:not(.type-book) .note-list-widget {
display: none !important;
}
@@ -2084,12 +2089,121 @@ body.zen .note-title-widget,
body.zen .note-title-widget input {
font-size: 1rem !important;
background: transparent !important;
pointer-events: none;
}
body.zen #detail-container {
width: 100%;
}
body.zen .note-split:not(.full-content-width) .scrolling-container {
display: flex;
flex-direction: column;
scroll-behavior: unset !important;
}
body.zen .note-split:not(.full-content-width) .note-detail {
margin: auto;
padding-bottom: 25vh;
max-width: var(--max-content-width);
width: 100%;
}
body.zen .note-split:not(.full-content-width) .scroll-padding-widget {
display: none;
}
body.zen .note-split.type-text {
position: relative;
font-size: 1.15em;
}
body.zen:not(.backdrop-effects-disabled) .note-split.type-text .title-row {
--start-color: var(--main-background-color);
position: absolute;
width: 100%;
background: linear-gradient(var(--start-color) 30%, transparent 100%);
z-index: 1000;
}
@supports (background: color-mix(in srgb, white, transparent)) {
body.zen .note-split.type-text .title-row {
--start-color: color-mix(in srgb, var(--main-background-color), transparent 10%);
}
}
body.zen .note-split.type-text .scrolling-container {
--padding-bottom: 130px; /* Should be enough to avoid caret being hidden by the formatting toolbar */
/* (Usually) keeps the caret above the fixed toolbar */
scroll-padding-bottom: var(--padding-bottom);
}
body.zen:not(.backdrop-effects-disabled) .note-split.type-text .scrolling-container {
--padding-top: 50px; /* Should be enough to cover the title row */
padding-top: var(--padding-top);
scroll-padding-top: var(--padding-top);
}
/* Fixed formatting toolbar */
body.zen .note-split .ribbon-container {
position: fixed;
left: 0;
bottom: 20px;
width: 100%;
z-index: 1000;
opacity: 0; /* Hidden unless the current note split is focused */
pointer-events: none;
transition: opacity 100ms linear;
}
body.zen .note-split:focus-within .ribbon-container {
opacity: 1; /* Show when the note split is focused */
}
body.zen .note-split .ribbon-container .ribbon-body {
border: 0;
}
body.zen .note-split .ribbon-container .classic-toolbar-widget {
margin: auto;
width: fit-content;
box-shadow: 0px 10px 20px rgba(0, 0, 0, .1);
border-radius: 8px;
border: 1px solid var(--main-border-color);
padding: 4px;
background: var(--menu-background-color);
}
body.zen .note-split .ribbon-container .classic-toolbar-widget:not(:has(> .ck-toolbar)) {
/* Hide the toolbar wrapper if the toolbar is missing */
display: none;
}
body.zen .note-split:focus-within .ribbon-container .classic-toolbar-widget {
pointer-events: all;
}
@media (max-width: 1300px) {
body.zen .note-split .ribbon-container .classic-toolbar-widget {
/* Set the toolbar to full with */
width: 100%;
}
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_se,
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sw,
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_smw,
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sme,
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_s {
/* Force toolbar items overflow dropdowns open upwards */
top: auto;
bottom: 100%;
}
}
/* Content renderer */
footer.file-footer,
@@ -2406,7 +2520,7 @@ footer.webview-footer button {
transform: rotate(180deg);
}
/* CK Edito */
/* CK Editor */
/* Insert text snippet: limit the width of the listed items to avoid overly long names */
:root body.desktop div.ck-template-form li.ck-list__item .ck-template-form__text-part > span {
@@ -2436,4 +2550,18 @@ iframe.print-iframe {
.excalidraw.theme--dark canvas {
--theme-filter: invert(100%) hue-rotate(180deg);
}
/* Scrolling container */
.scrolling-container:has(> :is(.note-detail.full-height, .note-list-widget.full-height)) {
display: flex;
flex-direction: column;
}
.scrolling-container > .note-detail.full-height,
.scrolling-container > .note-list-widget.full-height {
position: relative;
flex-grow: 1;
width: 100%;
}

View File

@@ -15,7 +15,7 @@
--native-titlebar-background: #00000000;
--window-background-color-bgfx: transparent; /* When background effects enabled */
--main-background-color: #272727;
--main-background-color: #242424;
--main-text-color: #ccc;
--main-border-color: #454545;
--subtle-border-color: #313131;
@@ -166,6 +166,9 @@
--protected-session-active-icon-color: #8edd8e;
--sync-status-error-pulse-color: #f47871;
--center-pane-vert-layout-background-color-bgfx: #0c0c0c69;
--center-pane-horiz-layout-background-color-bgfx: #1e1e1ec7;
--right-pane-heading-color: gray;
--root-background: var(--left-pane-background-color);
@@ -192,9 +195,9 @@
--badge-background-color: #ffffff1a;
--badge-text-color: var(--muted-text-color);
--promoted-attribute-card-background-color: var(--card-background-color);
--promoted-attribute-card-shadow-color: #000000b3;
--promoted-attribute-card-background-color: #ffffff21;
--promoted-attribute-card-shadow: none;
--floating-button-shadow-color: #00000080;
--floating-button-background-color: #494949d2;
--floating-button-color: var(--button-text-color);
@@ -208,6 +211,8 @@
--floating-button-hide-button-background: #00000029;
--floating-button-hide-button-color: #ffffff63;
--right-pane-background-color: var(--main-background-color);
--right-pane-background-color-bgfx: #0c0c0c24; /* Only for the vertical layout */
--right-pane-item-hover-background: #ffffff26;
--right-pane-item-hover-color: white;
@@ -225,10 +230,9 @@
--code-block-box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6);
--card-background-color: #ffffff12;
--card-background-hover-color: #3c3c3c;
--card-background-press-color: #464646;
--card-border-color: #222222;
--card-box-shadow: 0 0 12px rgba(0, 0, 0, 0.15);
--card-background-hover-color: #ffffff20;
--card-border-color: transparent;
--card-box-shadow: none;
--calendar-color: var(--menu-text-color);
--calendar-weekday-labels-color: var(--muted-text-color);
@@ -294,4 +298,10 @@ body ::-webkit-calendar-picker-indicator {
body .todo-list input[type="checkbox"]:not(:checked):before {
border-color: var(--muted-text-color) !important;
}
.tinted-quick-edit-dialog {
--modal-background-color: hsl(var(--custom-color-hue), 8.8%, 11.2%);
--modal-border-color: hsl(var(--custom-color-hue), 9.4%, 25.1%);
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 13.2%, 20.8%);
}

View File

@@ -159,6 +159,9 @@
--protected-session-active-icon-color: #16b516;
--sync-status-error-pulse-color: #ff5528;
--center-pane-vert-layout-background-color-bgfx: #ffffff75;
--center-pane-horiz-layout-background-color-bgfx: #ffffffd6;
--right-pane-heading-color: gray;
--root-background: var(--left-pane-background-color);
@@ -180,13 +183,13 @@
--inactive-tab-hover-background-color: #00000016;
--inactive-tab-text-color: #4e4e4e;
--alert-bar-background: #32637b29;
--alert-bar-background: #f9cf2b29;
--badge-background-color: #00000011;
--badge-text-color: var(--muted-text-color);
--promoted-attribute-card-background-color: var(--card-background-color);
--promoted-attribute-card-shadow-color: #00000033;
--promoted-attribute-card-background-color: #00000014;
--promoted-attribute-card-shadow: none;
--floating-button-shadow-color: #00000042;
--floating-button-background-color: #eaeaeacc;
@@ -207,7 +210,9 @@
--new-tab-button-hover-background: white;
--new-tab-button-hover-color: black;
--right-pane-item-hover-background: #ececec;
--right-pane-background-color: var(--main-background-color);
--right-pane-background-color-bgfx: #ffffff9e; /* Only for the vertical layout */
--right-pane-item-hover-background: #00000013;
--right-pane-item-hover-color: inherit;
--scrollbar-thumb-color: #0000005c;
@@ -223,12 +228,11 @@
--code-block-box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.1), 0px 0px 2px rgba(0, 0, 0, 0.2);
--card-background-color: var(--accented-background-color);
--card-background-hover-color: #f9f9f9;
--card-background-press-color: #efefef;
--card-border-color: #eaeaea;
--card-background-color: #0000000d;
--card-background-hover-color: #0000001c;
--card-border-color: transparent;
--card-shadow-color: rgba(0, 0, 0, 0.1);
--card-box-shadow: 0 0 12px var(--card-shadow-color);
--card-box-shadow: none;
--calendar-color: var(--menu-text-color);
--calendar-weekday-labels-color: var(--muted-text-color);
@@ -270,4 +274,10 @@
* The --custom-color-hue variable contains the hue of the user-selected note color.
* This value is unset for gray tones. */
--custom-bg-color: hsl(var(--custom-color-hue), 37%, 89%, 1);
}
.tinted-quick-edit-dialog {
--modal-background-color: hsl(var(--custom-color-hue), 56%, 96%);
--modal-border-color: hsl(var(--custom-color-hue), 33%, 41%);
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 40%, 88%);
}

View File

@@ -82,6 +82,7 @@
/* Theme capabilities */
--tab-note-icons: true;
--allow-background-effects: true;
/* To ensure that a tree item's custom color remains sufficiently contrasted and readable,
* the color is adjusted based on the current color scheme (light or dark). The lightness
@@ -131,7 +132,8 @@ body.mobile .dropdown-menu .dropdown-menu {
body.desktop .dropdown-menu::before,
:root .ck.ck-dropdown__panel::before,
:root .excalidraw .popover::before {
:root .excalidraw .popover::before,
body.zen .note-split .ribbon-container .classic-toolbar-widget::before {
content: "";
backdrop-filter: var(--dropdown-backdrop-filter);
border-radius: var(--dropdown-border-radius);
@@ -485,13 +487,21 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
--note-list-vertical-padding: 15px;
background-color: var(--card-background-color);
border: 1px solid var(--card-border-color) !important;
box-shadow: 2px 3px 4px var(--card-shadow-color);
border-radius: 12px;
user-select: none;
padding: 0;
margin: 5px 10px 5px 0;
}
:root .note-list .note-book-card:hover {
background-color: var(--card-background-hover-color);
transition: background-color 200ms ease-out;
}
:root .note-list.grid-view .note-book-card:active {
transform: scale(.98);
}
.note-list.list-view .note-book-card {
box-shadow: 0 0 3px var(--card-shadow-color);
}
@@ -500,10 +510,6 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
vertical-align: middle;
}
.note-list-wrapper .note-book-card:active {
background-color: var(--card-background-press-color);
}
.note-list-wrapper .note-book-card a {
color: inherit !important;
}
@@ -575,9 +581,14 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
padding: 0;
}
.note-list-wrapper .note-book-card .note-book-content.type-code {
height: 100%;
}
.note-list-wrapper .note-book-card .note-book-content.type-code pre {
height: 100%;
padding: 1em;
margin-bottom: 0;
}
.note-list-wrapper .note-book-card .bx {
@@ -585,7 +596,6 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
}
.note-list.grid-view .note-book-card:hover {
background: var(--card-background-color) !important;
filter: contrast(105%);
}

View File

@@ -258,11 +258,6 @@
border-inline-start: 1px solid var(--ck-color-toolbar-border);
}
/* The last separator of the toolbar */
:root .classic-toolbar-widget .ck.ck-toolbar__separator:last-of-type {
flex-grow: 1;
}
/* Heading dropdown */
:root .ck.ck-dropdown.ck-heading-dropdown .ck-dropdown__panel .ck-list__item {
@@ -679,4 +674,17 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
.ck-content a.reference-link > span {
text-decoration: underline;
}
/*
* Read-only text content
*/
.note-detail-readonly-text:focus-visible {
outline: 2px solid var(--input-focus-outline-color);
border-radius: 4px;
}
.note-list-widget {
outline: 0 !important;
}

View File

@@ -101,7 +101,7 @@
.sql-table-schemas-widget .sql-table-schemas button:hover,
.sql-table-schemas-widget .sql-table-schemas button:active,
.sql-table-schemas-widget .sql-table-schemas button:focus-visible {
--background: var(--card-background-press-color);
--background: var(--card-background-hover-color);
--color: var(--main-text-color);
}
@@ -123,8 +123,12 @@
*/
/* The container */
div.note-detail-empty {
max-width: 70%;
.note-split.empty-note {
--max-content-width: 70%;
}
.note-split.empty-note div.note-detail {
margin: 50px auto;
}
@@ -148,7 +152,7 @@ div.note-detail-empty {
--options-card-min-width: 500px;
--options-card-max-width: 900px;
--options-card-padding: 17px;
--options-title-font-size: 1rem;
--options-title-font-size: .75rem;
--options-title-offset: 13px;
}
/* Create a gap at the top of the option pages */
@@ -173,16 +177,19 @@ div.note-detail-empty {
}
.options-section:not(.tn-no-card) {
margin: auto;
border-radius: 12px;
border: 1px solid var(--card-border-color) !important;
margin-bottom: calc(var(--options-title-offset) + 26px) !important;
box-shadow: var(--card-box-shadow);
border: 1px solid var(--card-border-color) !important;
border-radius: 8px;
background: var(--card-background-color);
padding: var(--options-card-padding);
margin-bottom: calc(var(--options-title-offset) + 26px) !important;
}
body.desktop .option-section:not(.tn-no-card) {
body.prefers-centered-content .options-section:not(.tn-no-card) {
margin-inline: auto;
}
body.desktop .options-section:not(.tn-no-card) {
min-width: var(--options-card-min-width);
max-width: var(--options-card-max-width);
}
@@ -193,9 +200,16 @@ body.desktop .option-section:not(.tn-no-card) {
padding-bottom: var(--default-padding);
}
.options-section:not(.tn-no-card) h4,
.options-section:not(.tn-no-card) h5 {
text-transform: uppercase;
letter-spacing: .4pt;
}
.options-section:not(.tn-no-card) h4 {
font-size: var(--options-title-font-size);
font-weight: bold;
font-weight: 600;
color: var(--launcher-pane-text-color);
margin-top: calc(-1 * var(--options-card-padding) - var(--options-title-font-size) - var(--options-title-offset)) !important;
margin-bottom: calc(var(--options-title-offset) + var(--options-card-padding)) !important;

View File

@@ -34,6 +34,7 @@
div.promoted-attributes-container {
margin-top: 8px;
margin-bottom: 8px;
margin-inline-start: 12px;
}
/*
@@ -41,7 +42,7 @@ div.promoted-attributes-container {
*/
/* The property label */
.note-info-widget-table th,
.note-info-item > span:first-child,
.file-properties-widget .file-table th,
.image-properties > div:first-child > span > strong {
opacity: 0.65;
@@ -49,7 +50,6 @@ div.promoted-attributes-container {
vertical-align: top;
}
.note-info-widget-table td,
.file-properties-widget .file-table td {
vertical-align: top;
}

View File

@@ -8,7 +8,7 @@
}
:root {
--dropdown-backdrop-filter: blur(10px) saturate(6);
--dropdown-backdrop-filter: blur(20px) saturate(6);
--dropdown-border-radius: 10px;
}
@@ -35,30 +35,53 @@ body.mobile {
}
/* #region Mica */
body.background-effects.platform-win32 {
/* Quirk: --background-material is read before "theme-supports-background-effects" class
* is applied. Apply the matterial even if the theme doesn't support it. */
--background-material: tabbed;
}
body.background-effects.theme-supports-background-effects.platform-win32 {
--launcher-pane-horiz-border-color: var(--launcher-pane-horiz-border-color-bgfx);
--launcher-pane-horiz-background-color: var(--launcher-pane-horiz-background-color-bgfx);
--launcher-pane-vert-background-color: var(--launcher-pane-vert-background-color-bgfx);
--tab-background-color: var(--window-background-color-bgfx);
--new-tab-button-background: var(--window-background-color-bgfx);
--active-tab-background-color: var(--launcher-pane-horiz-background-color);
--root-background: transparent;
}
body.background-effects.platform-win32.layout-vertical {
--left-pane-background-color: var(--window-background-color-bgfx);
--background-material: mica;
}
body.background-effects.platform-win32,
body.background-effects.platform-win32 #root-widget {
body.background-effects.theme-supports-background-effects.platform-win32.layout-vertical {
--left-pane-background-color: var(--window-background-color-bgfx);
--center-pane-background-color-bgfx: var(--center-pane-vert-layout-background-color-bgfx);
--right-pane-background-color: var(--right-pane-background-color-bgfx);
}
body.background-effects.theme-supports-background-effects.platform-win32.layout-horizontal {
--center-pane-background-color-bgfx: var(--center-pane-horiz-layout-background-color-bgfx);
--gutter-color: var(--left-pane-background-color);
}
body.background-effects.theme-supports-background-effects.platform-win32,
body.background-effects.theme-supports-background-effects.platform-win32 #root-widget {
background: var(--window-background-color-bgfx) !important;
}
body.background-effects.platform-win32.layout-horizontal #horizontal-main-container,
body.background-effects.platform-win32.layout-vertical #vertical-main-container {
body.background-effects.theme-supports-background-effects.platform-win32.layout-horizontal #horizontal-main-container,
body.background-effects.theme-supports-background-effects.platform-win32.layout-vertical #vertical-main-container {
background-color: var(--root-background);
}
/* Note split with background effects */
body.background-effects.theme-supports-background-effects.platform-win32 #center-pane .note-split.bgfx {
--note-split-background-color: var(--center-pane-background-color-bgfx);
}
/* #endregion */
/* Matches when the left pane is collapsed */
@@ -72,9 +95,21 @@ body.layout-vertical #horizontal-main-container.left-pane-hidden #launcher-pane.
border-inline-end: 2px solid var(--left-pane-collapsed-border-color);
}
body.background-effects.zen #root-widget {
--main-background-color: transparent;
--root-background: transparent;
/*
* Zen mode
*/
@keyframes zen-formatting-toolbar-entrance {
from {
transform: translateY(200%);
} to {
transform: translateY(0);
}
}
body.zen .note-split .ribbon-container .classic-toolbar-widget {
position: relative;
animation: zen-formatting-toolbar-entrance 300ms ease-out;
}
/*
@@ -1171,23 +1206,18 @@ body.layout-vertical .tab-row-widget-is-sorting .note-tab.note-tab-is-dragging .
* CENTER PANE
*/
#center-pane {
background: var(--main-background-color);
}
.vertical-layout #center-pane {
/* The first visible note split */
.vertical-layout #center-pane .note-split:not(.visible ~ .visible) {
border-radius: var(--center-pane-border-radius) 0 0 0;
}
.note-split {
#center-pane .note-split {
padding-top: 2px;
animation: note-entrance 100ms linear;
/* will-change: opacity; -- causes some weird artifacts to the note menu in split view */
background-color: var(--note-split-background-color, var(--main-background-color));
}
.split-note-container-widget > .gutter {
background: var(--root-background) !important;
transition: background 150ms ease-out;
body:not(.background-effects) #center-pane .note-split {
animation: note-entrance 100ms linear;
}
/*
@@ -1200,9 +1230,9 @@ body.layout-vertical .tab-row-widget-is-sorting .note-tab.note-tab-is-dragging .
@keyframes note-entrance {
from {
opacity: 0;
filter: opacity(0);
} to {
opacity: 1;
filter: opacity(1);
}
}
@@ -1328,8 +1358,7 @@ div.promoted-attribute-cell {
--pa-card-padding-inline-end: 2px;
--input-background-color: transparent;
box-shadow: 1px 1px 2px var(--promoted-attribute-card-shadow-color);
box-shadow: var(--promoted-attribute-card-shadow);
display: inline-flex;
margin: 0;
border-radius: 8px;
@@ -1716,7 +1745,7 @@ div.find-replace-widget div.find-widget-found-wrapper > span {
*/
#right-pane {
background: var(--main-background-color);
background: var(--right-pane-background-color);
}
#right-pane div.card-header {

View File

@@ -520,9 +520,7 @@
"max_content_width": {
"max_width_unit": "بكسل",
"title": "عرض المحتوى",
"reload_button": "اعادة تحميل الواجهة",
"max_width_label": "اقصى عرض للمحتوى",
"reload_description": "تغييرات من خيارات المظهر"
"max_width_label": "اقصى عرض للمحتوى"
},
"native_title_bar": {
"enabled": "مفعل",

View File

@@ -39,7 +39,10 @@
"help_on_tree_prefix": "有关树前缀的帮助",
"prefix": "前缀: ",
"save": "保存",
"branch_prefix_saved": "分支前缀已保存。"
"branch_prefix_saved": "分支前缀已保存。",
"edit_branch_prefix_multiple": "编辑 {{count}} 个分支的前缀",
"branch_prefix_saved_multiple": "已为 {{count}} 个分支保存分支前缀。",
"affected_branches": "受影响的分支 {{count}}:"
},
"bulk_actions": {
"bulk_actions": "批量操作",
@@ -992,7 +995,7 @@
},
"protected_session": {
"enter_password_instruction": "显示受保护的笔记需要输入您的密码:",
"start_session_button": "开始受保护的会话 <kbd>Enter</kbd>",
"start_session_button": "开始受保护的会话",
"started": "受保护的会话已启动。",
"wrong_password": "密码错误。",
"protecting-finished-successfully": "保护操作已成功完成。",
@@ -1107,9 +1110,6 @@
"title": "内容宽度",
"default_description": "Trilium默认会限制内容的最大宽度以提高在宽屏中全屏时的可读性。",
"max_width_label": "内容最大宽度(像素)",
"apply_changes_description": "要应用内容宽度更改,请点击",
"reload_button": "重载前端",
"reload_description": "来自外观选项的更改",
"max_width_unit": "像素"
},
"native_title_bar": {
@@ -1917,7 +1917,7 @@
},
"custom_date_time_format": {
"title": "自定义日期/时间格式",
"description": "通过<shortcut />或工具栏的方式可自定义日期和时间格式有关日期/时间格式字符串中各个字符的含义,请参阅<doc>Day.js docs</doc>。",
"description": "自定义通过 <shortcut /> 或工具栏插入的日期和时间格式有关日期/时间格式字符串中各个字符的含义,请参阅<doc>Day.js docs</doc>。",
"format_string": "日期/时间格式字符串:",
"formatted_time": "格式化后日期/时间:"
},
@@ -2079,5 +2079,8 @@
"edit-slide": "编辑此幻灯片",
"start-presentation": "开始演示",
"slide-overview": "切换幻灯片概览"
},
"calendar_view": {
"delete_note": "删除笔记..."
}
}

View File

@@ -989,7 +989,7 @@
},
"protected_session": {
"enter_password_instruction": "Um die geschützte Notiz anzuzeigen, musst du dein Passwort eingeben:",
"start_session_button": "Starte eine geschützte Sitzung <kbd>Eingabetaste</kbd>",
"start_session_button": "Starte eine geschützte Sitzung",
"started": "Geschützte Sitzung gestartet.",
"wrong_password": "Passwort falsch.",
"protecting-finished-successfully": "Geschützt erfolgreich beendet.",
@@ -1104,9 +1104,6 @@
"title": "Inhaltsbreite",
"default_description": "Trilium begrenzt standardmäßig die maximale Inhaltsbreite, um die Lesbarkeit für maximierte Bildschirme auf Breitbildschirmen zu verbessern.",
"max_width_label": "Maximale Inhaltsbreite in Pixel",
"apply_changes_description": "Um Änderungen an der Inhaltsbreite anzuwenden, klicke auf",
"reload_button": "Frontend neu laden",
"reload_description": "Änderungen an den Darstellungsoptionen",
"max_width_unit": "Pixel"
},
"native_title_bar": {

View File

@@ -36,10 +36,13 @@
},
"branch_prefix": {
"edit_branch_prefix": "Edit branch prefix",
"edit_branch_prefix_multiple": "Edit branch prefix for {{count}} branches",
"help_on_tree_prefix": "Help on Tree prefix",
"prefix": "Prefix: ",
"save": "Save",
"branch_prefix_saved": "Branch prefix has been saved."
"branch_prefix_saved": "Branch prefix has been saved.",
"branch_prefix_saved_multiple": "Branch prefix has been saved for {{count}} branches.",
"affected_branches": "Affected branches ({{count}}):"
},
"bulk_actions": {
"bulk_actions": "Bulk actions",
@@ -679,6 +682,7 @@
"open_note_externally": "Open note externally",
"open_note_externally_title": "File will be open in an external application and watched for changes. You'll then be able to upload the modified version back to Trilium.",
"open_note_custom": "Open note custom",
"open_note_on_server": "Open note on server",
"import_files": "Import files",
"export_note": "Export note",
"delete_note": "Delete note",
@@ -992,7 +996,7 @@
},
"protected_session": {
"enter_password_instruction": "Showing protected note requires entering your password:",
"start_session_button": "Start protected session <kbd>enter</kbd>",
"start_session_button": "Start protected session",
"started": "Protected session has been started.",
"wrong_password": "Wrong password.",
"protecting-finished-successfully": "Protecting finished successfully.",
@@ -1108,9 +1112,7 @@
"default_description": "Trilium by default limits max content width to improve readability for maximized screens on wide screens.",
"max_width_label": "Max content width",
"max_width_unit": "pixels",
"apply_changes_description": "To apply content width changes, click on",
"reload_button": "reload frontend",
"reload_description": "changes from appearance options"
"centerContent": "Keep content centered"
},
"native_title_bar": {
"title": "Native Title Bar (requires app restart)",
@@ -1636,6 +1638,12 @@
"shared_locally": "This note is shared locally on {{- link}}.",
"help_link": "For help visit <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a>."
},
"read-only-info": {
"read-only-note": "Currently viewing a read-only note.",
"auto-read-only-note": "This note is shown in a read-only mode for faster loading.",
"auto-read-only-learn-more": "Learn more",
"edit-note": "Edit note"
},
"note_types": {
"text": "Text",
"code": "Code",
@@ -2034,6 +2042,9 @@
"start-presentation": "Start presentation",
"slide-overview": "Toggle an overview of the slides"
},
"calendar_view": {
"delete_note": "Delete note..."
},
"command_palette": {
"tree-action-name": "Tree: {{name}}",
"export_note_title": "Export Note",

View File

@@ -39,7 +39,10 @@
"help_on_tree_prefix": "Ayuda sobre el prefijo del árbol",
"prefix": "Prefijo: ",
"save": "Guardar",
"branch_prefix_saved": "Se ha guardado el prefijo de rama."
"branch_prefix_saved": "Se ha guardado el prefijo de rama.",
"edit_branch_prefix_multiple": "Editar prefijo de rama para {{count}} ramas",
"branch_prefix_saved_multiple": "El prefijo de rama se ha guardado para {{count}} ramas.",
"affected_branches": "Ramas afectadas ({{count}}):"
},
"bulk_actions": {
"bulk_actions": "Acciones en bloque",
@@ -992,7 +995,7 @@
},
"protected_session": {
"enter_password_instruction": "Para mostrar una nota protegida es necesario ingresar su contraseña:",
"start_session_button": "Iniciar sesión protegida <kbd>Enter</kbd>",
"start_session_button": "Iniciar sesión protegida",
"started": "La sesión protegida ha iniciado.",
"wrong_password": "Contraseña incorrecta.",
"protecting-finished-successfully": "La protección finalizó exitosamente.",
@@ -1108,9 +1111,7 @@
"default_description": "Trilium limita de forma predeterminada el ancho máximo del contenido para mejorar la legibilidad de ventanas maximizadas en pantallas anchas.",
"max_width_label": "Ancho máximo del contenido en píxeles",
"max_width_unit": "píxeles",
"apply_changes_description": "Para aplicar cambios en el ancho del contenido, haga clic en",
"reload_button": "recargar la interfaz",
"reload_description": "cambios desde las opciones de apariencia"
"centerContent": "Mantener el contenido centrado"
},
"native_title_bar": {
"title": "Barra de título nativa (requiere reiniciar la aplicación)",
@@ -1592,7 +1593,7 @@
"tree-context-menu": {
"open-in-a-new-tab": "Abrir en nueva pestaña",
"open-in-a-new-split": "Abrir en nueva división",
"insert-note-after": "Insertar nota después de",
"insert-note-after": "Insertar nota contigua",
"insert-child-note": "Insertar subnota",
"delete": "Eliminar",
"search-in-subtree": "Buscar en subárbol",
@@ -2082,5 +2083,14 @@
},
"collections": {
"rendering_error": "No se puede mostrar contenido debido a un error."
},
"read-only-info": {
"read-only-note": "Actualmente, está viendo una nota de solo lectura.",
"auto-read-only-note": "Esta nota se muestra en modo de solo lectura para una carga más rápida.",
"auto-read-only-learn-more": "Para saber más",
"edit-note": "Editar nota"
},
"calendar_view": {
"delete_note": "Eliminar nota..."
}
}

View File

@@ -991,7 +991,7 @@
},
"protected_session": {
"enter_password_instruction": "L'affichage de la note protégée nécessite la saisie de votre mot de passe :",
"start_session_button": "Démarrer une session protégée <kbd>Entrée</kbd>",
"start_session_button": "Démarrer une session protégée",
"started": "La session protégée a démarré.",
"wrong_password": "Mot de passe incorrect.",
"protecting-finished-successfully": "La protection de la note s'est terminée avec succès.",
@@ -1106,9 +1106,6 @@
"title": "Largeur du contenu",
"default_description": "Trilium limite par défaut la largeur maximale du contenu pour améliorer la lisibilité sur des écrans larges.",
"max_width_label": "Largeur maximale du contenu en pixels",
"apply_changes_description": "Pour appliquer les modifications de largeur du contenu, cliquez sur",
"reload_button": "recharger l'interface",
"reload_description": "changements par rapport aux options d'apparence",
"max_width_unit": "Pixels"
},
"native_title_bar": {

View File

@@ -39,7 +39,10 @@
"help_on_tree_prefix": "Aiuto sui prefissi dell'Albero",
"prefix": "Prefisso: ",
"save": "Salva",
"branch_prefix_saved": "Il prefisso del ramo è stato salvato."
"branch_prefix_saved": "Il prefisso del ramo è stato salvato.",
"edit_branch_prefix_multiple": "Modifica prefisso ramo per {{count}} rami",
"branch_prefix_saved_multiple": "Il prefisso del ramo è stato salvato per {{count}} rami.",
"affected_branches": "Rami interessati ({{count}}):"
},
"bulk_actions": {
"bulk_actions": "Azioni massive",
@@ -109,7 +112,8 @@
"export_type_single": "Solo questa nota, senza le sottostanti",
"format_opml": "OPML - formato per scambio informazioni outline. Formattazione, immagini e files non sono inclusi.",
"opml_version_1": "OPML v.1.0 - solo testo semplice",
"opml_version_2": "OPML v2.0 - supporta anche HTML"
"opml_version_2": "OPML v2.0 - supporta anche HTML",
"share-format": "HTML per la pubblicazione sul web - utilizza lo stesso tema utilizzato per le note condivise, ma può essere pubblicato come sito web statico."
},
"password_not_set": {
"body1": "Le note protette sono crittografate utilizzando una password utente, ma la password non è stata ancora impostata.",
@@ -1498,7 +1502,7 @@
},
"protected_session": {
"enter_password_instruction": "Per visualizzare la nota protetta è necessario inserire la password:",
"start_session_button": "Avvia sessione protetta <kbd>invio</kbd>",
"start_session_button": "Avvia sessione protetta",
"started": "La sessione protetta è stata avviata.",
"wrong_password": "Password errata.",
"protecting-finished-successfully": "Protezione completata con successo.",
@@ -1570,9 +1574,7 @@
"default_description": "Per impostazione predefinita, Trilium limita la larghezza massima del contenuto per migliorare la leggibilità sugli schermi più grandi.",
"max_width_label": "Larghezza massima del contenuto",
"max_width_unit": "pixel",
"apply_changes_description": "Per applicare le modifiche alla larghezza del contenuto, fare clic su",
"reload_button": "ricarica frontend",
"reload_description": "modifiche dalle opzioni di aspetto"
"centerContent": "Mantieni il contenuto centrato"
},
"native_title_bar": {
"title": "Barra del titolo nativa (richiede il riavvio dell'app)",
@@ -2082,5 +2084,14 @@
},
"collections": {
"rendering_error": "Impossibile mostrare il contenuto a causa di un errore."
},
"read-only-info": {
"read-only-note": "Stai visualizzando una nota di sola lettura.",
"auto-read-only-note": "Questa nota viene visualizzata in modalità di sola lettura per un caricamento più rapido.",
"auto-read-only-learn-more": "Per saperne di più",
"edit-note": "Modifica nota"
},
"calendar_view": {
"delete_note": "Eliminazione nota..."
}
}

View File

@@ -39,7 +39,10 @@
"edit_branch_prefix": "ブランチ接頭辞の編集",
"help_on_tree_prefix": "ツリー接頭辞に関するヘルプ",
"prefix": "接頭辞: ",
"branch_prefix_saved": "ブランチの接頭辞が保存されました。"
"branch_prefix_saved": "ブランチの接頭辞が保存されました。",
"edit_branch_prefix_multiple": "{{count}} ブランチのブランチ接頭辞を編集",
"branch_prefix_saved_multiple": "{{count}} 個のブランチのブランチ接頭辞が保存されました。",
"affected_branches": "影響を受けるブランチ {{count}}:"
},
"global_menu": {
"menu": "メニュー",
@@ -830,13 +833,11 @@
"theme_defined": "テーマが定義されました"
},
"max_content_width": {
"reload_button": "フロントエンドをリロード",
"title": "コンテンツ幅",
"default_description": "Triliumは、ワイドスクリーンで最大化された画面での可読性を向上させるために、デフォルトでコンテンツの最大幅を制限しています。",
"max_width_label": "最大コンテンツ幅",
"max_width_unit": "ピクセル",
"apply_changes_description": "コンテンツ幅の変更を適用するには、クリックしてください",
"reload_description": "外観設定から変更"
"centerContent": "コンテンツを中央に配置"
},
"theme": {
"title": "アプリのテーマ",
@@ -1783,7 +1784,7 @@
},
"protected_session": {
"enter_password_instruction": "保護されたノートを表示するにはパスワードを入力する必要があります:",
"start_session_button": "保護されたセッションを開始 <kbd>enter</kbd>",
"start_session_button": "保護されたセッションを開始",
"started": "保護されたセッションが開始されました。",
"wrong_password": "パスワードが間違っています。",
"protecting-finished-successfully": "保護が正常に完了しました。",
@@ -2079,5 +2080,14 @@
"edit-slide": "このスライドを編集",
"start-presentation": "プレゼンテーションを開始",
"slide-overview": "スライドの概要を切り替え"
},
"calendar_view": {
"delete_note": "ノートを削除..."
},
"read-only-info": {
"read-only-note": "現在、読み取り専用のノートを表示しています。",
"auto-read-only-note": "このノートは読み込みを高速化するために読み取り専用モードで表示されています。",
"auto-read-only-learn-more": "さらに詳しく",
"edit-note": "ノートを編集"
}
}

View File

@@ -1464,10 +1464,7 @@
"title": "Szerokość zawartości",
"default_description": "Trilium domyślnie ogranicza maksymalną szerokość zawartości, aby poprawić czytelność na zmaksymalizowanych ekranach o dużej szerokości.",
"max_width_label": "Maksymalna szerokość zawartości",
"max_width_unit": "piksele",
"apply_changes_description": "Aby zastosować zmiany szerokości zawartości, kliknij na",
"reload_button": "przeładuj frontend",
"reload_description": "zmiany z opcji wyglądu"
"max_width_unit": "piksele"
},
"native_title_bar": {
"title": "Natywny pasek tytułu (wymaga ponownego uruchomienia aplikacji)",

View File

@@ -967,7 +967,7 @@
},
"protected_session": {
"enter_password_instruction": "É necessário digitar a sua palavra-passe para mostar notas protegidas:",
"start_session_button": "Iniciar sessão protegida <kbd>enter</kbd>",
"start_session_button": "Iniciar sessão protegida",
"started": "A sessão protegida foi iniciada.",
"wrong_password": "Palavra-passe incorreta.",
"protecting-finished-successfully": "A proteção foi finalizada com sucesso.",
@@ -1082,10 +1082,7 @@
"title": "Largura do Conteúdo",
"default_description": "Por padrão, o Trilium limita a largura máxima do conteúdo para melhorar a legibilidade em janelas maximizadas em ecrãs largos.",
"max_width_label": "Largura máxima do conteúdo",
"max_width_unit": "pixels",
"apply_changes_description": "Para aplicar as alterações de largura do conteúdo, clique em",
"reload_button": "recarregar frontend",
"reload_description": "alterações de opções de aparência"
"max_width_unit": "pixels"
},
"native_title_bar": {
"title": "Barra de Título Nativa (requer recarregar a app)",

View File

@@ -1218,7 +1218,7 @@
"unprotecting-in-progress-count": "Remoções de proteção em andamento: {{count}}",
"protecting-title": "Estado da proteção",
"unprotecting-title": "Estado da remoção de proteção",
"start_session_button": "Iniciar sessão protegida <kbd>enter</kbd>"
"start_session_button": "Iniciar sessão protegida"
},
"relation_map": {
"open_in_new_tab": "Abrir em nova aba",
@@ -1304,9 +1304,6 @@
"title": "Largura do Conteúdo",
"max_width_label": "Largura máxima do conteúdo",
"max_width_unit": "pixels",
"apply_changes_description": "Para aplicar as alterações de largura do conteúdo, clique em",
"reload_button": "recarregar frontend",
"reload_description": "alterações de opções de aparência",
"default_description": "Por padrão, o Trilium limita a largura máxima do conteúdo para melhorar a legibilidade em janelas maximizadas em telas wide."
},
"native_title_bar": {

View File

@@ -796,12 +796,9 @@
"modal_body_text": "Din cauza limitărilor la nivel de navigator, nu este posibilă citirea clipboard-ului din JavaScript. Inserați Markdown-ul pentru a-l importa în caseta de mai jos și dați clic pe butonul Import"
},
"max_content_width": {
"apply_changes_description": "Pentru a aplica schimbările de lățime a conținutului, dați click pe",
"default_description": "În mod implicit Trilium limitează lățimea conținutului pentru a îmbunătăți lizibilitatea pentru ferestrele maximizate pe ecrane late.",
"max_width_label": "Lungimea maximă a conținutului",
"max_width_unit": "pixeli",
"reload_button": "reîncarcă interfața",
"reload_description": "schimbări din opțiunile de afișare",
"title": "Lățime conținut"
},
"mobile_detail_menu": {
@@ -985,7 +982,7 @@
},
"protected_session": {
"enter_password_instruction": "Afișarea notițelor protejate necesită introducerea parolei:",
"start_session_button": "Deschide sesiunea protejată <kbd>enter</kbd>",
"start_session_button": "Deschide sesiunea protejată",
"started": "Sesiunea protejată este activă.",
"wrong_password": "Parolă greșită.",
"protecting-finished-successfully": "Protejarea a avut succes.",

View File

@@ -1203,11 +1203,8 @@
"max_content_width": {
"max_width_unit": "пикселей",
"title": "Ширина контентной области",
"reload_button": "перезагрузить интерфейс",
"default_description": "Trilium по умолчанию ограничивает максимальную ширину контента, чтобы улучшить читаемость на широких экранах.",
"max_width_label": "Максимальная ширина контентной области",
"apply_changes_description": "Чтобы применить изменения, нажмите на",
"reload_description": "изменения в параметрах внешнего вида"
"max_width_label": "Максимальная ширина контентной области"
},
"native_title_bar": {
"enabled": "включено",
@@ -1688,7 +1685,7 @@
"unprotecting-title": "Статус снятия защиты",
"protecting-finished-successfully": "Защита успешно завершена.",
"unprotecting-finished-successfully": "Снятие защиты успешно завершено.",
"start_session_button": "Начать защищенный сеанс <kbd>enter</kbd>",
"start_session_button": "Начать защищенный сеанс",
"protecting-in-progress": "Защита в процессе: {{count}}",
"unprotecting-in-progress-count": "Снятие защиты в процессе: {{count}}",
"started": "Защищенный сеанс запущен.",

View File

@@ -39,7 +39,10 @@
"help_on_tree_prefix": "有關樹前綴的說明",
"prefix": "前綴: ",
"save": "儲存",
"branch_prefix_saved": "已儲存分支前綴。"
"branch_prefix_saved": "已儲存分支前綴。",
"edit_branch_prefix_multiple": "編輯 {{count}} 個分支的前綴",
"branch_prefix_saved_multiple": "已為 {{count}} 個分支儲存分支前綴。",
"affected_branches": "受影響的分支 ({{count}}):"
},
"bulk_actions": {
"bulk_actions": "批次操作",
@@ -989,7 +992,7 @@
},
"protected_session": {
"enter_password_instruction": "顯示受保護的筆記需要輸入您的密碼:",
"start_session_button": "開始受保護的作業階段 <kbd>Enter</kbd>",
"start_session_button": "開始受保護的作業階段",
"started": "已啟動受保護的作業階段。",
"wrong_password": "密碼錯誤。",
"protecting-finished-successfully": "已成功完成保護操作。",
@@ -1104,9 +1107,6 @@
"title": "內容寬度",
"default_description": "Trilium 預設會限制內容的最大寬度以提高在寬螢幕中全螢幕時的可讀性。",
"max_width_label": "內容最大寬度(像素)",
"apply_changes_description": "要套用內容寬度更改,請點擊",
"reload_button": "重新載入前端",
"reload_description": "來自外觀選項的更改",
"max_width_unit": "像素"
},
"native_title_bar": {
@@ -2079,5 +2079,8 @@
"edit-slide": "編輯此投影片",
"start-presentation": "開始簡報",
"slide-overview": "切換投影片概覽"
},
"calendar_view": {
"delete_note": "刪除筆記…"
}
}

View File

@@ -1089,7 +1089,7 @@
},
"protected_session": {
"enter_password_instruction": "Для відображення захищеної нотатки потрібно ввести пароль:",
"start_session_button": "Розпочати захищений сеанс <kbd>enter</kbd>",
"start_session_button": "Розпочати захищений сеанс",
"started": "Захищений сеанс розпочато.",
"wrong_password": "Неправильний пароль.",
"protecting-finished-successfully": "Захист успішно завершено.",
@@ -1204,10 +1204,7 @@
"title": "Ширина вмісту",
"default_description": "Trilium за замовчуванням обмежує максимальну ширину вмісту, щоб поліпшити читабельність на широкоформатних екранах у режимі максимального розширення.",
"max_width_label": "Максимальна ширина вмісту",
"max_width_unit": "пікселів",
"apply_changes_description": "Щоб застосувати зміни ширини вмісту, натисніть на",
"reload_button": "перезавантажити інтерфейс",
"reload_description": "зміни в параметрах зовнішнього вигляду"
"max_width_unit": "пікселів"
},
"native_title_bar": {
"title": "Нативний рядок заголовка (потрібен перезапуск)",

View File

@@ -215,6 +215,30 @@ declare namespace Fancytree {
enableUpdate(enabled: boolean): void;
}
interface FancytreeNodeData {
noteId: string;
parentNoteId: string;
branchId: string;
isProtected: boolean;
noteType: NoteType;
}
interface FancytreeNewNode extends FancytreeNodeData {
title: string;
extraClasses: string;
icon: string;
refKey: string;
/** True if this node is loaded on demand, i.e. on first expansion. */
lazy: boolean;
/** Folder nodes have different default icons and click behavior. Note: Also non-folders may have children. */
folder: boolean;
/** Use isExpanded(), setExpanded() to access this property. */
expanded: boolean;
/** Node id (must be unique inside the tree) */
key: string;
children?: FancytreeNewNode[];
}
/** A FancytreeNode represents the hierarchical data model and operations. */
interface FancytreeNode {
// #region Properties
@@ -227,7 +251,7 @@ declare namespace Fancytree {
/** Display name (may contain HTML) */
title: string;
/** Contains all extra data that was passed on node creation */
data: any;
data: FancytreeNodeData;
/** Array of child nodes. For lazy nodes, null or undefined means 'not yet loaded'. Use an empty array to define a node that has no children. */
children: FancytreeNode[];
/** Use isExpanded(), setExpanded() to access this property. */

View File

@@ -60,3 +60,14 @@ declare global {
windowControlsOverlay?: unknown;
}
}
declare module "preact" {
namespace JSX {
interface IntrinsicElements {
webview: {
src: string;
class: string;
}
}
}
}

View File

@@ -118,11 +118,17 @@ declare global {
filterKey: (e: { altKey: boolean }, dx: number, dy: number, dz: number) => void;
});
interface PanZoomTransform {
x: number;
y: number;
scale: number;
}
interface PanZoom {
zoomTo(x: number, y: number, scale: number);
moveTo(x: number, y: number);
on(event: string, callback: () => void);
getTransform(): unknown;
getTransform(): PanZoomTransform;
dispose(): void;
}
}

View File

@@ -23,6 +23,24 @@ export class CssVarReader {
return (!isNaN(number.valueOf()) ? number.valueOf() : defaultValue)
}
asBoolean(defaultValue?: boolean) {
let value = this.value.toLocaleLowerCase().trim();
let result: boolean | undefined;
switch (value) {
case "true":
case "1":
result = true;
break;
case "false":
case "0":
result = false;
break;
}
return (result !== undefined) ? result : defaultValue;
}
asEnum<T>(enumType: T, defaultValue?: T[keyof T]): T[keyof T] | undefined {
let result: T[keyof T] | undefined;

View File

@@ -6,7 +6,7 @@
.floating-buttons-children,
.show-floating-buttons {
position: absolute;
top: var(--floating-buttons-vert-offset, 10px);
top: var(--floating-buttons-vert-offset, 14px);
inset-inline-end: var(--floating-buttons-horiz-offset, 10px);
display: flex;
flex-direction: row;

View File

@@ -1,6 +1,6 @@
import { t } from "i18next";
import "./FloatingButtons.css";
import { useNoteContext, useNoteLabel, useNoteLabelBoolean } from "./react/hooks";
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useTriliumEvent } from "./react/hooks";
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
import { ParentComponent } from "./react/react_utils";
import { EventData, EventNames } from "../components/app_context";
@@ -20,6 +20,7 @@ interface FloatingButtonsProps {
* properly handle rounded corners, as defined by the --border-radius CSS variable.
*/
export default function FloatingButtons({ items }: FloatingButtonsProps) {
const [ top, setTop ] = useState(0);
const { note, noteContext } = useNoteContext();
const parentComponent = useContext(ParentComponent);
const [ viewType ] = useNoteLabel(note, "viewType");
@@ -47,8 +48,14 @@ export default function FloatingButtons({ items }: FloatingButtonsProps) {
const [ visible, setVisible ] = useState(true);
useEffect(() => setVisible(true), [ note ]);
useTriliumEvent("contentSafeMarginChanged", (e) => {
if (e.noteContext === noteContext) {
setTop(e.top);
}
});
return (
<div className="floating-buttons no-print">
<div className="floating-buttons no-print" style={{top}}>
<div className={`floating-buttons-children ${!visible ? "temporarily-hidden" : ""}`}>
{context && items.map((Component) => (
<Component {...context} />

View File

@@ -4,7 +4,7 @@ import Component from "../components/component";
import NoteContext from "../components/note_context";
import FNote from "../entities/fnote";
import ActionButton, { ActionButtonProps } from "./react/ActionButton";
import { useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks";
import { useIsNoteReadOnly, useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils";
import server from "../services/server";
@@ -13,8 +13,6 @@ import toast from "../services/toast";
import { t } from "../services/i18n";
import { copyImageReferenceToClipboard } from "../services/image";
import tree from "../services/tree";
import protected_session_holder from "../services/protected_session_holder";
import options from "../services/options";
import { getHelpUrlForNote } from "../services/in_app_help";
import froca from "../services/froca";
import NoteLink from "./react/NoteLink";
@@ -23,7 +21,7 @@ import { ViewTypeOptions } from "./collections/interface";
export interface FloatingButtonContext {
parentComponent: Component;
note: FNote;
note: FNote;
noteContext: NoteContext;
isDefaultViewMode: boolean;
isReadOnly: boolean;
@@ -65,11 +63,11 @@ export const MOBILE_FLOATING_BUTTONS: FloatingButtonsList = [
EditButton,
RelationMapButtons,
ExportImageButtons,
Backlinks
Backlinks
]
function RefreshBackendLogButton({ note, parentComponent, noteContext, isDefaultViewMode }: FloatingButtonContext) {
const isEnabled = note.noteId === "_backendLog" && isDefaultViewMode;
const isEnabled = (note.noteId === "_backendLog" || note.type === "render") && isDefaultViewMode;
return isEnabled && <FloatingButton
text={t("backend_log.refresh")}
icon="bx bx-refresh"
@@ -84,14 +82,14 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: F
return isEnabled && <FloatingButton
text={upcomingOrientation === "vertical" ? t("switch_layout_button.title_vertical") : t("switch_layout_button.title_horizontal")}
icon={upcomingOrientation === "vertical" ? "bx bxs-dock-bottom" : "bx bxs-dock-left"}
icon={upcomingOrientation === "vertical" ? "bx bxs-dock-bottom" : "bx bxs-dock-left"}
onClick={() => setSplitEditorOrientation(upcomingOrientation)}
/>
}
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingButtonContext) {
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
const isEnabled = (note.type === "mermaid" || viewType === "geoMap")
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap")
&& note.isContentAvailable() && isDefaultViewMode;
return isEnabled && <FloatingButton
@@ -101,48 +99,26 @@ function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingBut
/>
}
function EditButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) {
const [ animationClass, setAnimationClass ] = useState("");
const [ isEnabled, setIsEnabled ] = useState(false);
function EditButton({ note, noteContext }: FloatingButtonContext) {
const [animationClass, setAnimationClass] = useState("");
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext);
const isReadOnlyInfoBarDismissed = false; // TODO
useEffect(() => {
noteContext.isReadOnly().then(isReadOnly => {
setIsEnabled(
isDefaultViewMode
&& (!note.isProtected || protected_session_holder.isProtectedSessionAvailable())
&& !options.is("databaseReadonly")
&& isReadOnly
);
});
}, [ note ]);
useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => {
if (noteContext?.ntxId === eventNoteContext.ntxId) {
setIsEnabled(false);
}
});
// make the edit button stand out on the first display, otherwise
// it's difficult to notice that the note is readonly
useEffect(() => {
if (isEnabled) {
if (isReadOnly) {
setAnimationClass("bx-tada bx-lg");
setTimeout(() => {
setAnimationClass("");
}, 1700);
}
}, [ isEnabled ]);
}, [ isReadOnly ]);
return isEnabled && <FloatingButton
return !!isReadOnly && isReadOnlyInfoBarDismissed && <FloatingButton
text={t("edit_button.edit_this_note")}
icon="bx bx-pencil"
className={animationClass}
onClick={() => {
if (noteContext.viewScope) {
noteContext.viewScope.readOnlyTemporarilyDisabled = true;
appContext.triggerEvent("readOnlyTemporarilyDisabled", { noteContext });
}
}}
onClick={() => enableEditing()}
/>
}
@@ -264,7 +240,7 @@ function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonCon
function CopyImageReferenceButton({ note, isDefaultViewMode }: FloatingButtonContext) {
const hiddenImageCopyRef = useRef<HTMLDivElement>(null);
const isEnabled = ["mermaid", "canvas", "mindMap"].includes(note?.type ?? "")
const isEnabled = ["mermaid", "canvas", "mindMap", "image"].includes(note?.type ?? "")
&& note?.isContentAvailable() && isDefaultViewMode;
return isEnabled && (
@@ -325,7 +301,7 @@ function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
let [ backlinkCount, setBacklinkCount ] = useState(0);
let [ popupOpen, setPopupOpen ] = useState(false);
const backlinksContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isDefaultViewMode) return;
@@ -338,7 +314,7 @@ function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
const { windowHeight } = useWindowSize();
useLayoutEffect(() => {
const el = backlinksContainerRef.current;
if (popupOpen && el) {
if (popupOpen && el) {
const box = el.getBoundingClientRect();
const maxHeight = windowHeight - box.top - 10;
el.style.maxHeight = `${maxHeight}px`;
@@ -374,7 +350,7 @@ function BacklinksList({ noteId }: { noteId: string }) {
.filter(bl => "noteId" in bl)
.map((bl) => bl.noteId);
await froca.getNotes(noteIds);
setBacklinks(backlinks);
setBacklinks(backlinks);
});
}, [ noteId ]);
@@ -395,4 +371,4 @@ function BacklinksList({ noteId }: { noteId: string }) {
)}
</div>
));
}
}

View File

@@ -0,0 +1,15 @@
.component.note-detail {
max-width: var(--max-content-width); /* Inherited from .note-split */
font-family: var(--detail-font-family);
font-size: var(--detail-font-size);
contain: none;
}
body.prefers-centered-content .note-detail {
/* Horizontally center the widget in its parent when the "Keep content centered" option is on */
margin-inline: auto;
}
.note-detail > * {
contain: none;
}

View File

@@ -0,0 +1,324 @@
import { useNoteContext, useTriliumEvent } from "./react/hooks"
import FNote from "../entities/fnote";
import protected_session_holder from "../services/protected_session_holder";
import { useEffect, useRef, useState } from "preact/hooks";
import NoteContext from "../components/note_context";
import { isValidElement, VNode } from "preact";
import { TypeWidgetProps } from "./type_widgets/type_widget";
import "./NoteDetail.css";
import attributes from "../services/attributes";
import { ExtendedNoteType, TYPE_MAPPINGS, TypeWidget } from "./note_types";
import { dynamicRequire, isElectron, isMobile } from "../services/utils";
import toast from "../services/toast.js";
import { t } from "../services/i18n";
/**
* The note detail is in charge of rendering the content of a note, by determining its type (e.g. text, code) and using the appropriate view widget.
*
* Apart from that, it:
* - Applies a full-height style depending on the content type (e.g. canvas notes).
* - Focuses the content when switching tabs.
* - Caches the note type elements based on what the user has accessed, in order to quickly load it again.
* - Fixes the tree for launch bar configurations on mobile.
* - Provides scripting events such as obtaining the active note detail widget, or note type widget.
* - Printing and exporting to PDF.
*/
export default function NoteDetail() {
const containerRef = useRef<HTMLDivElement>(null);
const { note, type, mime, noteContext, parentComponent } = useNoteInfo();
const { ntxId, viewScope } = noteContext ?? {};
const isFullHeight = checkFullHeight(noteContext, type);
const noteTypesToRender = useRef<{ [ key in ExtendedNoteType ]?: (props: TypeWidgetProps) => VNode }>({});
const [ activeNoteType, setActiveNoteType ] = useState<ExtendedNoteType>();
const props: TypeWidgetProps = {
note: note!,
viewScope,
ntxId,
parentComponent,
noteContext
};
useEffect(() => {
if (!type) return;
if (!noteTypesToRender.current[type]) {
getCorrespondingWidget(type).then((el) => {
if (!el) return;
noteTypesToRender.current[type] = el;
setActiveNoteType(type);
});
} else {
setActiveNoteType(type);
}
}, [ note, viewScope, type ]);
// Detect note type changes.
useTriliumEvent("entitiesReloaded", async ({ loadResults }) => {
if (!note) return;
// we're detecting note type change on the note_detail level, but triggering the noteTypeMimeChanged
// globally, so it gets also to e.g. ribbon components. But this means that the event can be generated multiple
// times if the same note is open in several tabs.
if (note.noteId && loadResults.isNoteContentReloaded(note.noteId, parentComponent.componentId)) {
// probably incorrect event
// calling this.refresh() is not enough since the event needs to be propagated to children as well
// FIXME: create a separate event to force hierarchical refresh
// this uses handleEvent to make sure that the ordinary content updates are propagated only in the subtree
// to avoid the problem in #3365
parentComponent.handleEvent("noteTypeMimeChanged", { noteId: note.noteId });
} else if (note.noteId
&& loadResults.isNoteReloaded(note.noteId, parentComponent.componentId)
&& (type !== (await getWidgetType(note, noteContext)) || mime !== note?.mime)) {
// this needs to have a triggerEvent so that e.g., note type (not in the component subtree) is updated
parentComponent.triggerEvent("noteTypeMimeChanged", { noteId: note.noteId });
} else {
const attrs = loadResults.getAttributeRows();
const label = attrs.find(
(attr) =>
attr.type === "label" &&
["readOnly", "autoReadOnlyDisabled", "cssClass", "displayRelations", "hideRelations"].includes(attr.name ?? "") &&
attributes.isAffecting(attr, note)
);
const relation = attrs.find((attr) => attr.type === "relation" && ["template", "inherit", "renderNote"]
.includes(attr.name ?? "") && attributes.isAffecting(attr, note));
if (note.noteId && (label || relation)) {
// probably incorrect event
// calling this.refresh() is not enough since the event needs to be propagated to children as well
parentComponent.triggerEvent("noteTypeMimeChanged", { noteId: note.noteId });
}
}
});
// Automatically focus the editor.
useTriliumEvent("activeNoteChanged", () => {
// Restore focus to the editor when switching tabs, but only if the note tree is not already focused.
if (!document.activeElement?.classList.contains("fancytree-title")) {
parentComponent.triggerCommand("focusOnDetail", { ntxId });
}
});
// Fixed tree for launch bar config on mobile.
useEffect(() => {
if (!isMobile) return;
const hasFixedTree = noteContext?.hoistedNoteId === "_lbMobileRoot";
document.body.classList.toggle("force-fixed-tree", hasFixedTree);
}, [ note ]);
// Handle toast notifications.
useEffect(() => {
if (!isElectron()) return;
const { ipcRenderer } = dynamicRequire("electron");
const listener = () => {
toast.closePersistent("printing");
};
ipcRenderer.on("print-done", listener);
return () => ipcRenderer.off("print-done", listener);
}, []);
useTriliumEvent("executeInActiveNoteDetailWidget", ({ callback }) => {
if (!noteContext?.isActive()) return;
callback(parentComponent);
});
useTriliumEvent("executeWithTypeWidget", ({ resolve, ntxId: eventNtxId }) => {
if (eventNtxId !== ntxId || !activeNoteType || !containerRef.current) return;
const classNameToSearch = TYPE_MAPPINGS[activeNoteType].className;
const componentEl = containerRef.current.querySelector<HTMLElement>(`.${classNameToSearch}`);
if (!componentEl) return;
const component = glob.getComponentByEl(componentEl);
resolve(component);
});
useTriliumEvent("printActiveNote", () => {
if (!noteContext?.isActive() || !note) return;
toast.showPersistent({
icon: "bx bx-loader-circle bx-spin",
message: t("note_detail.printing"),
id: "printing"
});
if (isElectron()) {
const { ipcRenderer } = dynamicRequire("electron");
ipcRenderer.send("print-note", {
notePath: noteContext.notePath
});
} else {
const iframe = document.createElement('iframe');
iframe.src = `?print#${noteContext.notePath}`;
iframe.className = "print-iframe";
document.body.appendChild(iframe);
iframe.onload = () => {
if (!iframe.contentWindow) {
toast.closePersistent("printing");
document.body.removeChild(iframe);
return;
}
iframe.contentWindow.addEventListener("note-ready", () => {
toast.closePersistent("printing");
iframe.contentWindow?.print();
document.body.removeChild(iframe);
});
};
}
});
useTriliumEvent("exportAsPdf", () => {
if (!noteContext?.isActive() || !note) return;
toast.showPersistent({
icon: "bx bx-loader-circle bx-spin",
message: t("note_detail.printing_pdf"),
id: "printing"
});
const { ipcRenderer } = dynamicRequire("electron");
ipcRenderer.send("export-as-pdf", {
title: note.title,
notePath: noteContext.notePath,
pageSize: note.getAttributeValue("label", "printPageSize") ?? "Letter",
landscape: note.hasAttribute("label", "printLandscape")
});
});
return (
<div
ref={containerRef}
class={`note-detail ${isFullHeight ? "full-height" : ""}`}
>
{Object.entries(noteTypesToRender.current).map(([ itemType, Element ]) => {
return <NoteDetailWrapper
Element={Element}
key={itemType}
type={itemType as ExtendedNoteType}
isVisible={type === itemType}
isFullHeight={isFullHeight}
props={props}
/>
})}
</div>
);
}
/**
* Wraps a single note type widget, in order to keep it in the DOM even after the user has switched away to another note type. This allows faster loading of the same note type again. The properties are cached, so that they are updated only
* while the widget is visible, to avoid rendering in the background. When not visible, the DOM element is simply hidden.
*/
function NoteDetailWrapper({ Element, type, isVisible, isFullHeight, props }: { Element: (props: TypeWidgetProps) => VNode, type: ExtendedNoteType, isVisible: boolean, isFullHeight: boolean, props: TypeWidgetProps }) {
const [ cachedProps, setCachedProps ] = useState(props);
useEffect(() => {
if (isVisible) {
setCachedProps(props);
} else {
// Do nothing, keep the old props.
}
}, [ props, isVisible ]);
const typeMapping = TYPE_MAPPINGS[type];
return (
<div
className={`${typeMapping.className} ${typeMapping.printable ? "note-detail-printable" : ""} ${isVisible ? "visible" : "hidden-ext"}`}
style={{
height: isFullHeight ? "100%" : ""
}}
>
{ <Element {...cachedProps} /> }
</div>
);
}
/** Manages both note changes and changes to the widget type, which are asynchronous. */
function useNoteInfo() {
const { note: actualNote, noteContext, parentComponent } = useNoteContext();
const [ note, setNote ] = useState<FNote | null | undefined>();
const [ type, setType ] = useState<ExtendedNoteType>();
const [ mime, setMime ] = useState<string>();
function refresh() {
getWidgetType(actualNote, noteContext).then(type => {
setNote(actualNote);
setType(type);
setMime(actualNote?.mime);
});
}
useEffect(refresh, [ actualNote, noteContext, noteContext?.viewScope ]);
useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => {
if (eventNoteContext?.ntxId !== noteContext?.ntxId) return;
refresh();
});
useTriliumEvent("noteTypeMimeChanged", refresh);
return { note, type, mime, noteContext, parentComponent };
}
async function getCorrespondingWidget(type: ExtendedNoteType): Promise<null | TypeWidget> {
const correspondingType = TYPE_MAPPINGS[type].view;
if (!correspondingType) return null;
const result = await correspondingType();
if ("default" in result) {
return result.default;
} else if (isValidElement(result)) {
// Direct VNode provided.
return result;
} else {
return result;
}
}
async function getWidgetType(note: FNote | null | undefined, noteContext: NoteContext | undefined): Promise<ExtendedNoteType | undefined> {
if (!noteContext) return undefined;
if (!note) {
// If the note is null, then it's a new tab. If it's undefined, then it's not loaded yet.
return note === null ? "empty" : undefined;
}
const type = note.type;
let resultingType: ExtendedNoteType;
if (noteContext?.viewScope?.viewMode === "source") {
resultingType = "readOnlyCode";
} else if (noteContext?.viewScope && noteContext.viewScope.viewMode === "attachments") {
resultingType = noteContext.viewScope.attachmentId ? "attachmentDetail" : "attachmentList";
} else if (type === "text" && (await noteContext?.isReadOnly())) {
resultingType = "readOnlyText";
} else if ((type === "code" || type === "mermaid") && (await noteContext?.isReadOnly())) {
resultingType = "readOnlyCode";
} else if (type === "text") {
resultingType = "editableText";
} else if (type === "code") {
resultingType = "editableCode";
} else if (type === "launcher") {
resultingType = "doc";
} else {
resultingType = type;
}
if (note.isProtected && !protected_session_holder.isProtectedSessionAvailable()) {
resultingType = "protectedSession";
}
return resultingType;
}
function checkFullHeight(noteContext: NoteContext | undefined, type: ExtendedNoteType | undefined) {
if (!noteContext) return false;
// https://github.com/zadam/trilium/issues/2522
const isBackendNote = noteContext?.noteId === "_backendLog";
const isSqlNote = noteContext.note?.mime === "text/x-sqlite;schema=trilium";
const isFullHeightNoteType = type && TYPE_MAPPINGS[type].isFullHeight;
return (!noteContext?.hasNoteList() && isFullHeightNoteType && !isSqlNote)
|| noteContext?.viewScope?.viewMode === "attachments"
|| isBackendNote;
}

View File

@@ -0,0 +1,91 @@
body.mobile .promoted-attributes-widget {
/* https://github.com/zadam/trilium/issues/4468 */
flex-shrink: 0.4;
overflow: auto;
}
.component.promoted-attributes-widget {
contain: none;
}
.promoted-attributes-container {
margin: 0 1.5em;
overflow: auto;
max-height: 400px;
flex-wrap: wrap;
display: table;
}
.promoted-attribute-cell {
display: flex;
align-items: center;
margin: 10px;
display: table-row;
}
.promoted-attribute-cell > label {
user-select: none;
font-weight: bold;
vertical-align: middle;
}
.promoted-attribute-cell > * {
display: table-cell;
padding: 1px 0;
}
.promoted-attribute-cell div.input-group {
margin-inline-start: 10px;
display: flex;
min-height: 40px;
}
.promoted-attribute-cell strong {
word-break:keep-all;
white-space: nowrap;
}
.promoted-attribute-cell input[type="checkbox"] {
width: 22px !important;
flex-grow: 0;
width: unset;
}
/* Restore default apperance */
.promoted-attribute-cell input[type="number"],
.promoted-attribute-cell input[type="checkbox"] {
appearance: auto;
}
.promoted-attribute-cell input[type="color"] {
width: 24px;
height: 24px;
margin-top: 2px;
appearance: none;
padding: 0;
border: 0;
outline: none;
border-radius: 25% !important;
}
.promoted-attribute-cell input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
.promoted-attribute-cell input[type="color"]::-webkit-color-swatch {
border: none;
border-radius: 25%;
}
.promoted-attribute-label-color input[type="hidden"][value=""] + input[type="color"] {
position: relative;
opacity: 0.5;
}
.promoted-attribute-label-color input[type="hidden"][value=""] + input[type="color"]:after {
content: "";
position: absolute;
top: 10px;
inset-inline-start: 0px;
inset-inline-end: 0;
height: 2px;
background: rgba(0, 0, 0, 0.5);
transform: rotate(45deg);
pointer-events: none;
}

View File

@@ -0,0 +1,120 @@
import { useEffect, useState } from "preact/hooks";
import "./PromotedAttributes.css";
import { useNoteContext, useNoteLabel } from "./react/hooks";
import { Attribute } from "../services/attribute_parser";
import { ComponentChild } from "preact";
import FAttribute from "../entities/fattribute";
import { t } from "../services/i18n";
import ActionButton from "./react/ActionButton";
export default function PromotedAttributes() {
const { note } = useNoteContext();
const [ promotedAttributes, setPromotedAttributes ] = useState<ComponentChild[]>();
const [ viewType ] = useNoteLabel(note, "viewType");
useEffect(() => {
if (!note) {
setPromotedAttributes([]);
return;
}
const promotedDefAttrs = note.getPromotedDefinitionAttributes();
const ownedAttributes = note.getOwnedAttributes();
// attrs are not resorted if position changes after the initial load
// promoted attrs are sorted primarily by order of definitions, but with multi-valued promoted attrs
// the order of attributes is important as well
ownedAttributes.sort((a, b) => a.position - b.position);
let promotedAttributes: ComponentChild[] = [];
for (const definitionAttr of promotedDefAttrs) {
const valueType = definitionAttr.name.startsWith("label:") ? "label" : "relation";
const valueName = definitionAttr.name.substr(valueType.length + 1);
let valueAttrs = ownedAttributes.filter((el) => el.name === valueName && el.type === valueType) as Attribute[];
if (valueAttrs.length === 0) {
valueAttrs.push({
attributeId: "",
type: valueType,
name: valueName,
value: ""
});
}
if (definitionAttr.getDefinition().multiplicity === "single") {
valueAttrs = valueAttrs.slice(0, 1);
}
for (const valueAttr of valueAttrs) {
promotedAttributes.push(<PromotedAttributeCell
noteId={note.noteId}
definitionAttr={definitionAttr}
valueAttr={valueAttr} valueName={valueName} />)
}
}
setPromotedAttributes(promotedAttributes);
console.log("Got ", promotedAttributes);
}, [ note ]);
return (
<div className="promoted-attributes-widget">
{viewType !== "table" && (
<div className="promoted-attributes-container">
{promotedAttributes}
</div>
)}
</div>
);
}
function PromotedAttributeCell({ noteId, definitionAttr, valueAttr, valueName }: {
noteId: string;
definitionAttr: FAttribute;
valueAttr: Attribute;
valueName: string;
}) {
const definition = definitionAttr.getDefinition();
const id = `value-${valueAttr.attributeId}`;
return (
<div className="promoted-attribute-cell">
<label
for={id}
>{definition.promotedAlias ?? valueName}</label>
<div className="input-group">
<input
className="form-control promoted-attribute-input"
tabindex={200 + definitionAttr.position}
id={id}
// if not owned, we'll force creation of a new attribute instead of updating the inherited one
data-attribute-id={valueAttr.noteId === noteId ? valueAttr.attributeId ?? "" : ""}
data-attribute-type={valueAttr.type}
data-attribute-name={valueAttr.name}
value={valueAttr.value}
placeholder={t("promoted_attributes.unset-field-placeholder")}
/>
</div>
<div />
{definition.multiplicity === "multi" && (
<td className="multiplicity">
<ActionButton
icon="bx bx-plus"
className="pointer tn-tool-button"
text={t("promoted_attributes.add_new_attribute")}
noIconActionClass
/>
<ActionButton
icon="bx bx-trash"
className="pointer tn-tool-button"
text={t("promoted_attributes.remove_this_attribute")}
noIconActionClass
/>
</td>
)}
</div>
)
}

View File

@@ -0,0 +1,19 @@
body.zen div.read-only-note-info-bar-widget {
width: fit-content;
max-width: var(--max-content-width);
border-radius: 8px;
border: unset;
margin: 0 auto 10px auto;
}
.read-only-note-info-bar-widget-content {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
}
:root div.read-only-note-info-bar-widget button {
white-space: nowrap;
padding: 2px 8px;
}

View File

@@ -0,0 +1,36 @@
import "./ReadOnlyNoteInfoBar.css";
import { t } from "../services/i18n";
import { useIsNoteReadOnly, useNoteContext, useTriliumEvent } from "./react/hooks"
import Button from "./react/Button";
import InfoBar from "./react/InfoBar";
export default function ReadOnlyNoteInfoBar(props: {}) {
const {note, noteContext} = useNoteContext();
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext);
const isExplicitReadOnly = note?.isLabelTruthy("readOnly");
return <InfoBar className="read-only-note-info-bar-widget"
type={(isExplicitReadOnly ? "subtle" : "prominent")}
style={{display: (!isReadOnly) ? "none" : undefined}}>
<div class="read-only-note-info-bar-widget-content">
{(isExplicitReadOnly) ? (
<div>{t("read-only-info.read-only-note")}</div>
) : (
<div>
{t("read-only-info.auto-read-only-note")}
&nbsp;
<a class="tn-link"
href="https://docs.triliumnotes.org/user-guide/concepts/notes/read-only-notes#automatic-read-only-mode">
{t("read-only-info.auto-read-only-learn-more")}
</a>
</div>
)}
<Button text={t("read-only-info.edit-note")}
icon="bx-pencil" onClick={() => enableEditing()} />
</div>
</InfoBar>
}

View File

@@ -1,207 +0,0 @@
import { t } from "../services/i18n.js";
import utils from "../services/utils.js";
import AttachmentActionsWidget from "./buttons/attachments_actions.js";
import BasicWidget from "./basic_widget.js";
import options from "../services/options.js";
import imageService from "../services/image.js";
import linkService from "../services/link.js";
import contentRenderer from "../services/content_renderer.js";
import toastService from "../services/toast.js";
import type FAttachment from "../entities/fattachment.js";
import type { EventData } from "../components/app_context.js";
const TPL = /*html*/`
<div class="attachment-detail-widget">
<style>
.attachment-detail-widget {
height: 100%;
}
.attachment-detail-wrapper {
margin-bottom: 20px;
display: flex;
flex-direction: column;
}
.attachment-title-line {
display: flex;
align-items: baseline;
gap: 1em;
}
.attachment-details {
margin-inline-start: 10px;
}
.attachment-content-wrapper {
flex-grow: 1;
}
.attachment-content-wrapper .rendered-content {
height: 100%;
}
.attachment-content-wrapper pre {
padding: 10px;
margin-top: 10px;
margin-bottom: 10px;
}
.attachment-detail-wrapper.list-view .attachment-content-wrapper {
max-height: 300px;
}
.attachment-detail-wrapper.full-detail {
height: 100%;
}
.attachment-detail-wrapper.full-detail .attachment-content-wrapper {
height: 100%;
}
.attachment-detail-wrapper.list-view .attachment-content-wrapper pre {
max-height: 400px;
}
.attachment-content-wrapper img {
margin: 10px;
}
.attachment-detail-wrapper.list-view .attachment-content-wrapper img, .attachment-detail-wrapper.list-view .attachment-content-wrapper video {
max-height: 300px;
max-width: 90%;
object-fit: contain;
}
.attachment-detail-wrapper.full-detail .attachment-content-wrapper img {
max-width: 90%;
object-fit: contain;
}
.attachment-detail-wrapper.scheduled-for-deletion .attachment-content-wrapper img {
filter: contrast(10%);
}
</style>
<div class="attachment-detail-wrapper">
<div class="attachment-title-line">
<div class="attachment-actions-container"></div>
<h4 class="attachment-title"></h4>
<div class="attachment-details"></div>
<div style="flex: 1 1;"></div>
</div>
<div class="attachment-deletion-warning alert alert-info" style="margin-top: 15px;"></div>
<div class="attachment-content-wrapper"></div>
</div>
</div>`;
export default class AttachmentDetailWidget extends BasicWidget {
attachment: FAttachment;
attachmentActionsWidget: AttachmentActionsWidget;
isFullDetail: boolean;
$wrapper!: JQuery<HTMLElement>;
constructor(attachment: FAttachment, isFullDetail: boolean) {
super();
this.contentSized();
this.attachment = attachment;
this.attachmentActionsWidget = new AttachmentActionsWidget(attachment, isFullDetail);
this.isFullDetail = isFullDetail;
this.child(this.attachmentActionsWidget);
}
doRender() {
this.$widget = $(TPL);
this.refresh();
super.doRender();
}
async refresh() {
this.$widget.find(".attachment-detail-wrapper").empty().append($(TPL).find(".attachment-detail-wrapper").html());
this.$wrapper = this.$widget.find(".attachment-detail-wrapper");
this.$wrapper.addClass(this.isFullDetail ? "full-detail" : "list-view");
if (!this.isFullDetail) {
const $link = await linkService.createLink(this.attachment.ownerId, {
title: this.attachment.title,
viewScope: {
viewMode: "attachments",
attachmentId: this.attachment.attachmentId
}
});
$link.addClass("use-tn-links");
this.$wrapper.find(".attachment-title").append($link);
} else {
this.$wrapper.find(".attachment-title").text(this.attachment.title);
}
const $deletionWarning = this.$wrapper.find(".attachment-deletion-warning");
const { utcDateScheduledForErasureSince } = this.attachment;
if (utcDateScheduledForErasureSince) {
this.$wrapper.addClass("scheduled-for-deletion");
const scheduledSinceTimestamp = utils.parseDate(utcDateScheduledForErasureSince)?.getTime();
// use default value (30 days in seconds) from options_init as fallback, in case getInt returns null
const intervalMs = options.getInt("eraseUnusedAttachmentsAfterSeconds") || 2592000 * 1000;
const deletionTimestamp = scheduledSinceTimestamp + intervalMs;
const willBeDeletedInMs = deletionTimestamp - Date.now();
$deletionWarning.show();
if (willBeDeletedInMs >= 60000) {
$deletionWarning.text(t("attachment_detail_2.will_be_deleted_in", { time: utils.formatTimeInterval(willBeDeletedInMs) }));
} else {
$deletionWarning.text(t("attachment_detail_2.will_be_deleted_soon"));
}
$deletionWarning.append(t("attachment_detail_2.deletion_reason"));
} else {
this.$wrapper.removeClass("scheduled-for-deletion");
$deletionWarning.hide();
}
this.$wrapper.find(".attachment-details").text(t("attachment_detail_2.role_and_size", { role: this.attachment.role, size: utils.formatSize(this.attachment.contentLength) }));
this.$wrapper.find(".attachment-actions-container").append(this.attachmentActionsWidget.render());
const { $renderedContent } = await contentRenderer.getRenderedContent(this.attachment, { imageHasZoom: this.isFullDetail });
this.$wrapper.find(".attachment-content-wrapper").append($renderedContent);
}
async copyAttachmentLinkToClipboard() {
if (this.attachment.role === "image") {
imageService.copyImageReferenceToClipboard(this.$wrapper.find(".attachment-content-wrapper"));
} else if (this.attachment.role === "file") {
const $link = await linkService.createLink(this.attachment.ownerId, {
referenceLink: true,
viewScope: {
viewMode: "attachments",
attachmentId: this.attachment.attachmentId
}
});
utils.copyHtmlToClipboard($link[0].outerHTML);
toastService.showMessage(t("attachment_detail_2.link_copied"));
} else {
throw new Error(t("attachment_detail_2.unrecognized_role", { role: this.attachment.role }));
}
}
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
const attachmentRow = loadResults.getAttachmentRows().find((att) => att.attachmentId === this.attachment.attachmentId);
if (attachmentRow) {
if (attachmentRow.isDeleted) {
this.toggleInt(false);
} else {
this.refresh();
}
}
}
}

View File

@@ -4,8 +4,6 @@ import froca from "../services/froca.js";
import { t } from "../services/i18n.js";
import toastService from "../services/toast.js";
import { renderReactWidget } from "./react/react_utils.jsx";
import { EventNames, EventData } from "../components/app_context.js";
import { Handler } from "leaflet";
export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedComponent<T> {
protected attrs: Record<string, string>;

View File

@@ -1,195 +0,0 @@
import { t } from "../../services/i18n.js";
import BasicWidget from "../basic_widget.js";
import server from "../../services/server.js";
import dialogService from "../../services/dialog.js";
import toastService from "../../services/toast.js";
import ws from "../../services/ws.js";
import appContext from "../../components/app_context.js";
import openService from "../../services/open.js";
import utils from "../../services/utils.js";
import { Dropdown } from "bootstrap";
import type FAttachment from "../../entities/fattachment.js";
import type AttachmentDetailWidget from "../attachment_detail.js";
import type { NoteRow } from "@triliumnext/commons";
const TPL = /*html*/`
<div class="dropdown attachment-actions">
<style>
.attachment-actions {
width: 35px;
height: 35px;
}
.attachment-actions .dropdown-menu {
width: 20em;
}
.attachment-actions .dropdown-item .bx {
position: relative;
top: 3px;
font-size: 120%;
margin-inline-end: 5px;
}
.attachment-actions .dropdown-item[disabled], .attachment-actions .dropdown-item[disabled]:hover {
color: var(--muted-text-color) !important;
background-color: transparent !important;
pointer-events: none; /* makes it unclickable */
}
</style>
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true"
aria-expanded="false" class="icon-action icon-action-always-border bx bx-dots-vertical-rounded"
style="position: relative; top: 3px;"></button>
<div class="dropdown-menu dropdown-menu-right">
<li data-trigger-command="openAttachment" class="dropdown-item"
title="${t("attachments_actions.open_externally_title")}"><span class="bx bx-file-find"></span> ${t("attachments_actions.open_externally")}</li>
<li data-trigger-command="openAttachmentCustom" class="dropdown-item"
title="${t("attachments_actions.open_custom_title")}"><span class="bx bx-customize"></span> ${t("attachments_actions.open_custom")}</li>
<li data-trigger-command="downloadAttachment" class="dropdown-item">
<span class="bx bx-download"></span> ${t("attachments_actions.download")}</li>
<li data-trigger-command="copyAttachmentLinkToClipboard" class="dropdown-item"><span class="bx bx-link">
</span> ${t("attachments_actions.copy_link_to_clipboard")}</li>
<div class="dropdown-divider"></div>
<li data-trigger-command="uploadNewAttachmentRevision" class="dropdown-item"><span class="bx bx-upload">
</span> ${t("attachments_actions.upload_new_revision")}</li>
<li data-trigger-command="renameAttachment" class="dropdown-item">
<span class="bx bx-rename"></span> ${t("attachments_actions.rename_attachment")}</li>
<li data-trigger-command="deleteAttachment" class="dropdown-item">
<span class="bx bx-trash destructive-action-icon"></span> ${t("attachments_actions.delete_attachment")}</li>
<div class="dropdown-divider"></div>
<li data-trigger-command="convertAttachmentIntoNote" class="dropdown-item"><span class="bx bx-note">
</span> ${t("attachments_actions.convert_attachment_into_note")}</li>
</div>
<input type="file" class="attachment-upload-new-revision-input" style="display: none">
</div>`;
// TODO: Deduplicate
interface AttachmentResponse {
note: NoteRow;
}
export default class AttachmentActionsWidget extends BasicWidget {
$uploadNewRevisionInput!: JQuery<HTMLInputElement>;
attachment: FAttachment;
isFullDetail: boolean;
dropdown!: Dropdown;
constructor(attachment: FAttachment, isFullDetail: boolean) {
super();
this.attachment = attachment;
this.isFullDetail = isFullDetail;
}
get attachmentId() {
return this.attachment.attachmentId;
}
doRender() {
this.$widget = $(TPL);
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]);
this.$widget.on("click", ".dropdown-item", () => this.dropdown.toggle());
this.$uploadNewRevisionInput = this.$widget.find(".attachment-upload-new-revision-input");
this.$uploadNewRevisionInput.on("change", async () => {
const fileToUpload = this.$uploadNewRevisionInput[0].files?.item(0); // copy to allow reset below
this.$uploadNewRevisionInput.val("");
if (fileToUpload) {
const result = await server.upload(`attachments/${this.attachmentId}/file`, fileToUpload);
if (result.uploaded) {
toastService.showMessage(t("attachments_actions.upload_success"));
} else {
toastService.showError(t("attachments_actions.upload_failed"));
}
}
});
const isElectron = utils.isElectron();
if (!this.isFullDetail) {
const $openAttachmentButton = this.$widget.find("[data-trigger-command='openAttachment']");
$openAttachmentButton.addClass("disabled").append($('<span class="bx bx-info-circle disabled-tooltip" />').attr("title", t("attachments_actions.open_externally_detail_page")));
if (isElectron) {
const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']");
$openAttachmentCustomButton.addClass("disabled").append($('<span class="bx bx-info-circle disabled-tooltip" />').attr("title", t("attachments_actions.open_externally_detail_page")));
}
}
if (!isElectron) {
const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']");
$openAttachmentCustomButton.addClass("disabled").append($('<span class="bx bx-info-circle disabled-tooltip" />').attr("title", t("attachments_actions.open_custom_client_only")));
}
}
async openAttachmentCommand() {
await openService.openAttachmentExternally(this.attachmentId, this.attachment.mime);
}
async openAttachmentCustomCommand() {
await openService.openAttachmentCustom(this.attachmentId, this.attachment.mime);
}
async downloadAttachmentCommand() {
await openService.downloadAttachment(this.attachmentId);
}
async uploadNewAttachmentRevisionCommand() {
this.$uploadNewRevisionInput.trigger("click");
}
async copyAttachmentLinkToClipboardCommand() {
if (this.parent && "copyAttachmentLinkToClipboard" in this.parent) {
(this.parent as AttachmentDetailWidget).copyAttachmentLinkToClipboard();
}
}
async deleteAttachmentCommand() {
if (!(await dialogService.confirm(t("attachments_actions.delete_confirm", { title: this.attachment.title })))) {
return;
}
await server.remove(`attachments/${this.attachmentId}`);
toastService.showMessage(t("attachments_actions.delete_success", { title: this.attachment.title }));
}
async convertAttachmentIntoNoteCommand() {
if (!(await dialogService.confirm(t("attachments_actions.convert_confirm", { title: this.attachment.title })))) {
return;
}
const { note: newNote } = await server.post<AttachmentResponse>(`attachments/${this.attachmentId}/convert-to-note`);
toastService.showMessage(t("attachments_actions.convert_success", { title: this.attachment.title }));
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext()?.setNote(newNote.noteId);
}
async renameAttachmentCommand() {
const attachmentTitle = await dialogService.prompt({
title: t("attachments_actions.rename_attachment"),
message: t("attachments_actions.enter_new_name"),
defaultValue: this.attachment.title
});
if (!attachmentTitle?.trim()) {
return;
}
await server.put(`attachments/${this.attachmentId}/rename`, { title: attachmentTitle });
}
}

View File

@@ -1,9 +1,16 @@
.note-list-widget {
min-height: 0;
max-width: var(--max-content-width); /* Inherited from .note-split */
overflow: auto;
contain: none !important;
}
body.prefers-centered-content .note-list-widget:not(.full-height) {
/* Horizontally center the widget in its parent when the "Keep content centered" option is on */
margin-inline: auto;
}
.note-list-widget .note-list {
padding: 10px;
}

View File

@@ -0,0 +1,32 @@
import { it, describe, expect } from "vitest";
import { buildNote } from "../../../test/easy-froca";
import { getBoardData } from "./data";
import FBranch from "../../../entities/fbranch";
import froca from "../../../services/froca";
describe("Board data", () => {
it("deduplicates cloned notes", async () => {
const parentNote = buildNote({
title: "Board",
"#collection": "",
"#viewType": "board",
children: [
{ id: "note1", title: "First note", "#status": "To Do" },
{ id: "note2", title: "Second note", "#status": "In progress" },
{ id: "note3", title: "Third note", "#status": "Done" }
]
});
const branch = new FBranch(froca, {
branchId: "note1_note2",
notePosition: 10,
fromSearchNote: false,
noteId: "note2",
parentNoteId: "note1"
});
froca.branches["note1_note2"] = branch;
froca.getNoteFromCache("note1").addChild("note2", "note1_note2", false);
const data = await getBoardData(parentNote, "status", {}, false);
const noteIds = Array.from(data.byColumn.values()).flat().map(item => item.note.noteId);
expect(noteIds.length).toBe(3);
});
});

View File

@@ -11,7 +11,7 @@ export async function getBoardData(parentNote: FNote, groupByColumn: string, per
const byColumn: ColumnMap = new Map();
// First, scan all notes to find what columns actually exist
await recursiveGroupBy(parentNote.getChildBranches(), byColumn, groupByColumn, includeArchived);
await recursiveGroupBy(parentNote.getChildBranches(), byColumn, groupByColumn, includeArchived, new Set<string>());
// Get all columns that exist in the notes
const columnsFromNotes = [...byColumn.keys()];
@@ -61,26 +61,28 @@ export async function getBoardData(parentNote: FNote, groupByColumn: string, per
};
}
async function recursiveGroupBy(branches: FBranch[], byColumn: ColumnMap, groupByColumn: string, includeArchived: boolean) {
async function recursiveGroupBy(branches: FBranch[], byColumn: ColumnMap, groupByColumn: string, includeArchived: boolean, seenNoteIds: Set<string>) {
for (const branch of branches) {
const note = await branch.getNote();
if (!note || (!includeArchived && note.isArchived)) continue;
if (note.type !== "search" && note.hasChildren()) {
await recursiveGroupBy(note.getChildBranches(), byColumn, groupByColumn, includeArchived);
await recursiveGroupBy(note.getChildBranches(), byColumn, groupByColumn, includeArchived, seenNoteIds);
}
const group = note.getLabelValue(groupByColumn);
if (!group) {
if (!group || seenNoteIds.has(note.noteId)) {
continue;
}
if (!byColumn.has(group)) {
byColumn.set(group, []);
}
byColumn.get(group)!.push({
branch,
note
});
seenNoteIds.add(note.noteId);
}
}

View File

@@ -58,8 +58,6 @@ export async function changeEvent(note: FNote, { startDate, endDate, startTime,
startAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:startTime").shift()?.value||"startTime";
endAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:endTime").shift()?.value||"endTime";
if (startTime && endTime) {
setAttribute(note, "label", startAttribute, startTime);
setAttribute(note, "label", endAttribute, endTime);
}
setAttribute(note, "label", startAttribute, startTime);
setAttribute(note, "label", endAttribute, endTime);
}

View File

@@ -0,0 +1,28 @@
import FNote from "../../../entities/fnote";
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
import link_context_menu from "../../../menus/link_context_menu";
import branches from "../../../services/branches";
import { t } from "../../../services/i18n";
export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, parentNote: FNote) {
e.preventDefault();
e.stopPropagation();
contextMenu.show({
x: e.pageX,
y: e.pageY,
items: [
...link_context_menu.getItems(),
{ kind: "separator" },
{
title: t("calendar_view.delete_note"),
uiIcon: "bx bx-trash",
handler: async () => {
const branchId = parentNote.childToBranch[noteId];
await branches.deleteNotes([ branchId ], false, false);
}
}
],
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, noteId),
})
}

View File

@@ -1,6 +1,7 @@
.calendar-view {
overflow: hidden;
position: relative;
outline: 0;
height: 100%;
user-select: none;
padding: 10px;
@@ -67,6 +68,7 @@
}
body.desktop:not(.zen) .calendar-view .calendar-header {
padding-block-start: 4px;
padding-inline-end: 5em;
}

View File

@@ -4,7 +4,7 @@ import Calendar from "./calendar";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import "./index.css";
import { useNoteLabel, useNoteLabelBoolean, useResizeObserver, useSpacedUpdate, useTriliumEvent, useTriliumOption, useTriliumOptionInt } from "../../react/hooks";
import { DISPLAYABLE_LOCALE_IDS, LOCALE_IDS } from "@triliumnext/commons";
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
import { Calendar as FullCalendar } from "@fullcalendar/core";
import { parseStartEndDateFromEvent, parseStartEndTimeFromEvent } from "./utils";
import dialog from "../../../services/dialog";
@@ -20,6 +20,7 @@ import Button, { ButtonGroup } from "../../react/Button";
import ActionButton from "../../react/ActionButton";
import { RefObject } from "preact";
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
import { openCalendarContextMenu } from "./context_menu";
interface CalendarViewData {
@@ -106,7 +107,7 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
const plugins = usePlugins(isEditable, isCalendarRoot);
const locale = useLocale();
const { eventDidMount } = useEventDisplayCustomization();
const { eventDidMount } = useEventDisplayCustomization(note);
const editingProps = useEditing(note, isEditable, isCalendarRoot);
// React to changes.
@@ -196,11 +197,11 @@ function usePlugins(isEditable: boolean, isCalendarRoot: boolean) {
}
function useLocale() {
const [ locale ] = useTriliumOption("locale");
const [ formattingLocale ] = useTriliumOption("formattingLocale");
const [ calendarLocale, setCalendarLocale ] = useState<LocaleInput>();
useEffect(() => {
const correspondingLocale = LOCALE_MAPPINGS[locale];
const correspondingLocale = LOCALE_MAPPINGS[formattingLocale];
if (correspondingLocale) {
correspondingLocale().then((locale) => setCalendarLocale(locale.default));
} else {
@@ -253,7 +254,7 @@ function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean) {
};
}
function useEventDisplayCustomization() {
function useEventDisplayCustomization(parentNote: FNote) {
const eventDidMount = useCallback((e: EventMountArg) => {
const { iconClass, promotedAttributes } = e.event.extendedProps;
@@ -302,6 +303,11 @@ function useEventDisplayCustomization() {
}
$(mainContainer ?? e.el).append($(promotedAttributesHtml));
}
e.el.addEventListener("contextmenu", (contextMenuEvent) => {
const noteId = e.event.extendedProps.noteId;
openCalendarContextMenu(contextMenuEvent, noteId, parentNote);
});
}, []);
return { eventDidMount };
}

View File

@@ -1,7 +1,7 @@
import Map from "./map";
import "./index.css";
import { ViewModeProps } from "../interface";
import { useNoteBlob, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useNoteTreeDrag, useSpacedUpdate, useTouchBar, useTriliumEvent } from "../../react/hooks";
import { useNoteBlob, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useNoteTreeDrag, useSpacedUpdate, useTriliumEvent } from "../../react/hooks";
import { DEFAULT_MAP_LAYER_NAME } from "./map_layer";
import { divIcon, GPXOptions, LatLng, LeafletMouseEvent } from "leaflet";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";

View File

@@ -0,0 +1,62 @@
import { EventData } from "../../components/app_context";
import BasicWidget from "../basic_widget";
import Container from "./container";
import NoteContext from "../../components/note_context";
export default class ContentHeader extends Container<BasicWidget> {
noteContext?: NoteContext;
thisElement?: HTMLElement;
parentElement?: HTMLElement;
resizeObserver: ResizeObserver;
currentHeight: number = 0;
currentSafeMargin: number = NaN;
constructor() {
super();
this.css("contain", "unset");
this.resizeObserver = new ResizeObserver(this.onResize.bind(this));
}
setNoteContextEvent({ noteContext }: EventData<"setNoteContext">) {
this.noteContext = noteContext;
this.init();
}
init() {
this.parentElement = this.parent?.$widget.get(0);
if (!this.parentElement) {
console.warn("No parent set for <ContentHeader>.");
return;
}
this.thisElement = this.$widget.get(0)!;
this.resizeObserver.observe(this.thisElement);
this.parentElement.addEventListener("scroll", this.updateSafeMargin.bind(this));
}
updateSafeMargin() {
const newSafeMargin = Math.max(this.currentHeight - this.parentElement!.scrollTop, 0);
if (newSafeMargin !== this.currentSafeMargin) {
this.currentSafeMargin = newSafeMargin;
this.triggerEvent("contentSafeMarginChanged", {
top: newSafeMargin,
noteContext: this.noteContext!
});
}
}
onResize(entries: ResizeObserverEntry[]) {
for (const entry of entries) {
if (entry.target === this.thisElement) {
this.currentHeight = entry.contentRect.height;
this.updateSafeMargin();
}
}
}
}

View File

@@ -1,9 +1,10 @@
import { EventData } from "../../components/app_context.js";
import { LOCALES } from "@triliumnext/commons";
import { readCssVar } from "../../utils/css-var.js";
import FlexContainer from "./flex_container.js";
import options from "../../services/options.js";
import type BasicWidget from "../basic_widget.js";
import utils from "../../services/utils.js";
import { LOCALES } from "@triliumnext/commons";
/**
* The root container is the top-most widget/container, from which the entire layout derives.
@@ -30,9 +31,11 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
window.visualViewport?.addEventListener("resize", () => this.#onMobileResize());
}
this.#setMotion(options.is("motionEnabled"));
this.#setShadows(options.is("shadowsEnabled"));
this.#setBackdropEffects(options.is("backdropEffectsEnabled"));
this.#setMaxContentWidth();
this.#setMotion();
this.#setShadows();
this.#setBackdropEffects();
this.#setThemeCapabilities();
this.#setLocaleAndDirection(options.get("locale"));
return super.render();
@@ -40,15 +43,21 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isOptionReloaded("motionEnabled")) {
this.#setMotion(options.is("motionEnabled"));
this.#setMotion();
}
if (loadResults.isOptionReloaded("shadowsEnabled")) {
this.#setShadows(options.is("shadowsEnabled"));
this.#setShadows();
}
if (loadResults.isOptionReloaded("backdropEffectsEnabled")) {
this.#setBackdropEffects(options.is("backdropEffectsEnabled"));
this.#setBackdropEffects();
}
if (loadResults.isOptionReloaded("maxContentWidth")
|| loadResults.isOptionReloaded("centerContent")) {
this.#setMaxContentWidth();
}
}
@@ -58,19 +67,38 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened);
}
#setMotion(enabled: boolean) {
#setMaxContentWidth() {
const width = Math.max(options.getInt("maxContentWidth") || 0, 640);
document.body.style.setProperty("--preferred-max-content-width", `${width}px`);
document.body.classList.toggle("prefers-centered-content", options.is("centerContent"));
}
#setMotion() {
const enabled = options.is("motionEnabled");
document.body.classList.toggle("motion-disabled", !enabled);
jQuery.fx.off = !enabled;
}
#setShadows(enabled: boolean) {
#setShadows() {
const enabled = options.is("shadowsEnabled");
document.body.classList.toggle("shadows-disabled", !enabled);
}
#setBackdropEffects(enabled: boolean) {
#setBackdropEffects() {
const enabled = options.is("backdropEffectsEnabled");
document.body.classList.toggle("backdrop-effects-disabled", !enabled);
}
#setThemeCapabilities() {
// Supports background effects
const useBgfx = readCssVar(document.documentElement, "allow-background-effects")
.asBoolean(false);
document.body.classList.toggle("theme-supports-background-effects", useBgfx);
}
#setLocaleAndDirection(locale: string) {
const correspondingLocale = LOCALES.find(l => l.id === locale);
document.body.lang = locale;

View File

@@ -0,0 +1,9 @@
.scrolling-container {
overflow: auto;
scroll-behavior: smooth;
position: relative;
}
.note-split.type-code:not(.mime-text-x-sqlite) > .scrolling-container {
background-color: var(--code-background-color);
}

View File

@@ -2,6 +2,7 @@ import type { CommandListenerData, EventData, EventNames } from "../../component
import type NoteContext from "../../components/note_context.js";
import type BasicWidget from "../basic_widget.js";
import Container from "./container.js";
import "./scrolling_container.css";
export default class ScrollingContainer extends Container<BasicWidget> {
@@ -11,9 +12,6 @@ export default class ScrollingContainer extends Container<BasicWidget> {
super();
this.class("scrolling-container");
this.css("overflow", "auto");
this.css("scroll-behavior", "smooth");
this.css("position", "relative");
}
setNoteContextEvent({ noteContext }: EventData<"setNoteContext">) {

View File

@@ -6,7 +6,6 @@ import NoteAutocomplete from "../react/NoteAutocomplete";
import { useRef, useState, useEffect } from "preact/hooks";
import tree from "../../services/tree";
import note_autocomplete, { Suggestion } from "../../services/note_autocomplete";
import { default as TextTypeWidget } from "../type_widgets/editable_text.js";
import { logError } from "../../services/ws";
import FormGroup from "../react/FormGroup.js";
import { refToJQuerySelector } from "../react/react_utils";
@@ -14,29 +13,32 @@ import { useTriliumEvent } from "../react/hooks";
type LinkType = "reference-link" | "external-link" | "hyper-link";
export interface AddLinkOpts {
text: string;
hasSelection: boolean;
addLink(notePath: string, linkTitle: string | null, externalLink?: boolean): Promise<void>;
}
export default function AddLinkDialog() {
const [ textTypeWidget, setTextTypeWidget ] = useState<TextTypeWidget>();
const initialText = useRef<string>();
const [ opts, setOpts ] = useState<AddLinkOpts>();
const [ linkTitle, setLinkTitle ] = useState("");
const hasSelection = textTypeWidget?.hasSelection();
const [ linkType, setLinkType ] = useState<LinkType>(hasSelection ? "hyper-link" : "reference-link");
const [ linkType, setLinkType ] = useState<LinkType>();
const [ suggestion, setSuggestion ] = useState<Suggestion | null>(null);
const [ shown, setShown ] = useState(false);
const hasSubmittedRef = useRef(false);
useTriliumEvent("showAddLinkDialog", ( { textTypeWidget, text }) => {
setTextTypeWidget(textTypeWidget);
initialText.current = text;
useTriliumEvent("showAddLinkDialog", opts => {
setOpts(opts);
setShown(true);
});
useEffect(() => {
if (hasSelection) {
if (opts?.hasSelection) {
setLinkType("hyper-link");
} else {
setLinkType("reference-link");
}
}, [ hasSelection ])
}, [ opts ]);
async function setDefaultLinkTitle(noteId: string) {
const noteTitle = await tree.getNoteTitle(noteId);
@@ -71,10 +73,10 @@ export default function AddLinkDialog() {
function onShown() {
const $autocompleteEl = refToJQuerySelector(autocompleteRef);
if (!initialText.current) {
if (!opts?.text) {
note_autocomplete.showRecentNotes($autocompleteEl);
} else {
note_autocomplete.setText($autocompleteEl, initialText.current);
note_autocomplete.setText($autocompleteEl, opts.text);
}
// to be able to quickly remove entered text
@@ -108,15 +110,15 @@ export default function AddLinkDialog() {
onShown={onShown}
onHidden={() => {
// Insert the link.
if (hasSubmittedRef.current && suggestion && textTypeWidget) {
if (hasSubmittedRef.current && suggestion && opts) {
hasSubmittedRef.current = false;
if (suggestion.notePath) {
// Handle note link
textTypeWidget.addLink(suggestion.notePath, linkType === "reference-link" ? null : linkTitle);
opts.addLink(suggestion.notePath, linkType === "reference-link" ? null : linkTitle);
} else if (suggestion.externalLink) {
// Handle external link
textTypeWidget.addLink(suggestion.externalLink, linkTitle, true);
opts.addLink(suggestion.externalLink, linkTitle, true);
}
}
@@ -136,7 +138,7 @@ export default function AddLinkDialog() {
/>
</FormGroup>
{!hasSelection && (
{!opts?.hasSelection && (
<div className="add-link-title-settings">
{(linkType !== "external-link") && (
<>

View File

@@ -0,0 +1,13 @@
.branch-prefix-dialog .branch-prefix-notes-list {
margin-top: 10px;
}
.branch-prefix-dialog .branch-prefix-notes-list ul {
max-height: 200px;
overflow: auto;
margin-top: 5px;
}
.branch-prefix-dialog .branch-prefix-current {
opacity: 0.6;
}

View File

@@ -10,53 +10,86 @@ import Button from "../react/Button.jsx";
import FormGroup from "../react/FormGroup.js";
import { useTriliumEvent } from "../react/hooks.jsx";
import FBranch from "../../entities/fbranch.js";
import type { ContextMenuCommandData } from "../../components/app_context.js";
import "./branch_prefix.css";
// Virtual branches (e.g., from search results) start with this prefix
const VIRTUAL_BRANCH_PREFIX = "virt-";
export default function BranchPrefixDialog() {
const [ shown, setShown ] = useState(false);
const [ branch, setBranch ] = useState<FBranch>();
const [ branches, setBranches ] = useState<FBranch[]>([]);
const [ prefix, setPrefix ] = useState("");
const branchInput = useRef<HTMLInputElement>(null);
useTriliumEvent("editBranchPrefix", async () => {
const notePath = appContext.tabManager.getActiveContextNotePath();
if (!notePath) {
useTriliumEvent("editBranchPrefix", async (data?: ContextMenuCommandData) => {
let branchIds: string[] = [];
if (data?.selectedOrActiveBranchIds && data.selectedOrActiveBranchIds.length > 0) {
// Multi-select mode from tree context menu
branchIds = data.selectedOrActiveBranchIds.filter((branchId) => !branchId.startsWith(VIRTUAL_BRANCH_PREFIX));
} else {
// Single branch mode from keyboard shortcut or when no selection
const notePath = appContext.tabManager.getActiveContextNotePath();
if (!notePath) {
return;
}
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
if (!noteId || !parentNoteId) {
return;
}
const branchId = await froca.getBranchId(parentNoteId, noteId);
if (!branchId) {
return;
}
const parentNote = await froca.getNote(parentNoteId);
if (!parentNote || parentNote.type === "search") {
return;
}
branchIds = [branchId];
}
if (branchIds.length === 0) {
return;
}
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
const newBranches = branchIds
.map(id => froca.getBranch(id))
.filter((branch): branch is FBranch => branch !== null);
if (!noteId || !parentNoteId) {
if (newBranches.length === 0) {
return;
}
const newBranchId = await froca.getBranchId(parentNoteId, noteId);
if (!newBranchId) {
return;
}
const parentNote = await froca.getNote(parentNoteId);
if (!parentNote || parentNote.type === "search") {
return;
}
const newBranch = froca.getBranch(newBranchId);
setBranch(newBranch);
setPrefix(newBranch?.prefix ?? "");
setBranches(newBranches);
// Use the prefix of the first branch as the initial value
setPrefix(newBranches[0]?.prefix ?? "");
setShown(true);
});
async function onSubmit() {
if (!branch) {
if (branches.length === 0) {
return;
}
savePrefix(branch.branchId, prefix);
if (branches.length === 1) {
await savePrefix(branches[0].branchId, prefix);
} else {
await savePrefixBatch(branches.map(b => b.branchId), prefix);
}
setShown(false);
}
const isSingleBranch = branches.length === 1;
return (
<Modal
className="branch-prefix-dialog"
title={t("branch_prefix.edit_branch_prefix")}
title={isSingleBranch ? t("branch_prefix.edit_branch_prefix") : t("branch_prefix.edit_branch_prefix_multiple", { count: branches.length })}
size="lg"
onShown={() => branchInput.current?.focus()}
onHidden={() => setShown(false)}
@@ -69,9 +102,27 @@ export default function BranchPrefixDialog() {
<div class="input-group">
<input class="branch-prefix-input form-control" value={prefix} ref={branchInput}
onChange={(e) => setPrefix((e.target as HTMLInputElement).value)} />
<div class="branch-prefix-note-title input-group-text"> - {branch && branch.getNoteFromCache().title}</div>
{isSingleBranch && branches[0] && (
<div class="branch-prefix-note-title input-group-text"> - {branches[0].getNoteFromCache().title}</div>
)}
</div>
</FormGroup>
{!isSingleBranch && (
<div className="branch-prefix-notes-list">
<strong>{t("branch_prefix.affected_branches", { count: branches.length })}</strong>
<ul>
{branches.map((branch) => {
const note = branch.getNoteFromCache();
return (
<li key={branch.branchId}>
{branch.prefix && <span className="branch-prefix-current">{branch.prefix} - </span>}
{note.title}
</li>
);
})}
</ul>
</div>
)}
</Modal>
);
}
@@ -80,3 +131,8 @@ async function savePrefix(branchId: string, prefix: string) {
await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix });
toast.showMessage(t("branch_prefix.branch_prefix_saved"));
}
async function savePrefixBatch(branchIds: string[], prefix: string) {
await server.put("branches/set-prefix-batch", { branchIds, prefix });
toast.showMessage(t("branch_prefix.branch_prefix_saved_multiple", { count: branchIds.length }));
}

View File

@@ -8,17 +8,21 @@ import Button from "../react/Button";
import { Suggestion, triggerRecentNotes } from "../../services/note_autocomplete";
import tree from "../../services/tree";
import froca from "../../services/froca";
import EditableTextTypeWidget, { type BoxSize } from "../type_widgets/editable_text";
import { useTriliumEvent } from "../react/hooks";
import { type BoxSize, CKEditorApi } from "../type_widgets/text/CKEditorWithWatchdog";
export interface IncludeNoteOpts {
editorApi: CKEditorApi;
}
export default function IncludeNoteDialog() {
const [textTypeWidget, setTextTypeWidget] = useState<EditableTextTypeWidget>();
const editorApiRef = useRef<CKEditorApi>(null);
const [suggestion, setSuggestion] = useState<Suggestion | null>(null);
const [boxSize, setBoxSize] = useState("medium");
const [boxSize, setBoxSize] = useState<string>("medium");
const [shown, setShown] = useState(false);
useTriliumEvent("showIncludeNoteDialog", ({ textTypeWidget }) => {
setTextTypeWidget(textTypeWidget);
useTriliumEvent("showIncludeNoteDialog", ({ editorApi }) => {
editorApiRef.current = editorApi;
setShown(true);
});
@@ -32,12 +36,9 @@ export default function IncludeNoteDialog() {
onShown={() => triggerRecentNotes(autoCompleteRef.current)}
onHidden={() => setShown(false)}
onSubmit={() => {
if (!suggestion?.notePath || !textTypeWidget) {
return;
}
if (!suggestion?.notePath || !editorApiRef.current) return;
setShown(false);
includeNote(suggestion.notePath, textTypeWidget, boxSize as BoxSize);
includeNote(suggestion.notePath, editorApiRef.current, boxSize as BoxSize);
}}
footer={<Button text={t("include_note.button_include")} keyboardShortcut="Enter" />}
show={shown}
@@ -69,7 +70,7 @@ export default function IncludeNoteDialog() {
)
}
async function includeNote(notePath: string, textTypeWidget: EditableTextTypeWidget, boxSize: BoxSize) {
async function includeNote(notePath: string, editorApi: CKEditorApi, boxSize: BoxSize) {
const noteId = tree.getNoteIdFromUrl(notePath);
if (!noteId) {
return;
@@ -79,8 +80,8 @@ async function includeNote(notePath: string, textTypeWidget: EditableTextTypeWid
if (["image", "canvas", "mermaid"].includes(note?.type ?? "")) {
// there's no benefit to use insert note functionlity for images,
// so we'll just add an IMG tag
textTypeWidget.addImage(noteId);
editorApi.addImage(noteId);
} else {
textTypeWidget.addIncludeNote(noteId, boxSize);
editorApi.addIncludeNote(noteId, boxSize);
}
}
}

View File

@@ -1,5 +1,4 @@
import { useCallback, useRef, useState } from "preact/hooks";
import appContext from "../../components/app_context";
import { useRef, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import server from "../../services/server";
import toast from "../../services/toast";
@@ -7,7 +6,11 @@ import utils from "../../services/utils";
import Modal from "../react/Modal";
import Button from "../react/Button";
import { useTriliumEvent } from "../react/hooks";
import EditableTextTypeWidget from "../type_widgets/editable_text";
import { CKEditorApi } from "../type_widgets/text/CKEditorWithWatchdog";
export interface MarkdownImportOpts {
editorApi: CKEditorApi;
}
interface RenderMarkdownResponse {
htmlContent: string;
@@ -15,18 +18,18 @@ interface RenderMarkdownResponse {
export default function MarkdownImportDialog() {
const markdownImportTextArea = useRef<HTMLTextAreaElement>(null);
const [textTypeWidget, setTextTypeWidget] = useState<EditableTextTypeWidget>();
const editorApiRef = useRef<CKEditorApi>(null);
const [ text, setText ] = useState("");
const [ shown, setShown ] = useState(false);
useTriliumEvent("showPasteMarkdownDialog", ({ textTypeWidget }) => {
setTextTypeWidget(textTypeWidget);
useTriliumEvent("showPasteMarkdownDialog", ({ editorApi }) => {
if (utils.isElectron()) {
const { clipboard } = utils.dynamicRequire("electron");
const text = clipboard.readText();
convertMarkdownToHtml(text, textTypeWidget);
convertMarkdownToHtml(text, editorApi);
} else {
editorApiRef.current = editorApi;
setShown(true);
}
});
@@ -37,8 +40,8 @@ export default function MarkdownImportDialog() {
footer={<Button className="markdown-import-button" text={t("markdown_import.import_button")} onClick={() => setShown(false)} keyboardShortcut="Ctrl+Enter" />}
onShown={() => markdownImportTextArea.current?.focus()}
onHidden={async () => {
if (textTypeWidget) {
await convertMarkdownToHtml(text, textTypeWidget);
if (editorApiRef.current) {
await convertMarkdownToHtml(text, editorApiRef.current);
}
setShown(false);
setText("");
@@ -59,10 +62,8 @@ export default function MarkdownImportDialog() {
)
}
async function convertMarkdownToHtml(markdownContent: string, textTypeWidget: EditableTextTypeWidget) {
async function convertMarkdownToHtml(markdownContent: string, textTypeWidget: CKEditorApi) {
const { htmlContent } = await server.post<RenderMarkdownResponse>("other/render-markdown", { markdownContent });
await textTypeWidget.addHtmlToEditor(htmlContent);
textTypeWidget.addHtmlToEditor(htmlContent);
toast.showMessage(t("markdown_import.import_success"));
}
}

View File

@@ -1,13 +1,17 @@
import type { EventNames, EventData } from "../../components/app_context.js";
import NoteContext from "../../components/note_context.js";
import { openDialog } from "../../services/dialog.js";
import BasicWidget from "../basic_widget.js";
import BasicWidget, { ReactWrappedWidget } from "../basic_widget.js";
import Container from "../containers/container.js";
import TypeWidget from "../type_widgets/type_widget.js";
const TPL = /*html*/`\
<div class="popup-editor-dialog modal fade mx-auto" tabindex="-1" role="dialog">
<style>
/** Reduce the z-index of modals so that ckeditor popups are properly shown on top of it. */
body.popup-editor-open > .modal-backdrop { z-index: 998; }
body.popup-editor-open .popup-editor-dialog { z-index: 999; }
body.popup-editor-open .ck-clipboard-drop-target-line { z-index: 1000; }
body.desktop .modal.popup-editor-dialog .modal-dialog {
max-width: 75vw;
}
@@ -57,17 +61,19 @@ const TPL = /*html*/`\
}
</style>
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">
<!-- This is where the first child will be injected -->
<div class="quick-edit-dialog-wrapper">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">
<!-- This is where the first child will be injected -->
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- This is where all but the first child will be injected. -->
<div class="modal-body">
<!-- This is where all but the first child will be injected. -->
</div>
</div>
</div>
</div>
@@ -79,6 +85,7 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
private noteContext: NoteContext;
private $modalHeader!: JQuery<HTMLElement>;
private $modalBody!: JQuery<HTMLElement>;
private $wrapper!: JQuery<HTMLDivElement>;
constructor() {
super();
@@ -93,6 +100,7 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
const $newWidget = $(TPL);
this.$modalHeader = $newWidget.find(".modal-title");
this.$modalBody = $newWidget.find(".modal-body");
this.$wrapper = $newWidget.find(".quick-edit-dialog-wrapper");
const children = this.$widget.children();
this.$modalHeader.append(children[0]);
@@ -112,17 +120,27 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
}
});
const colorClass = this.noteContext.note?.getColorClass();
const wrapperElement = this.$wrapper.get(0)!;
if (colorClass) {
wrapperElement.className = "quick-edit-dialog-wrapper " + colorClass;
} else {
wrapperElement.className = "quick-edit-dialog-wrapper";
}
const customHue = getComputedStyle(wrapperElement).getPropertyValue("--custom-color-hue");
if (customHue) {
/* Apply the tinted-dialog class only if the custom color CSS class specifies a hue */
wrapperElement.classList.add("tinted-quick-edit-dialog");
}
const activeEl = document.activeElement;
if (activeEl && "blur" in activeEl) {
(activeEl as HTMLElement).blur();
}
$dialog.on("shown.bs.modal", async () => {
// Reduce the z-index of modals so that ckeditor popups are properly shown on top of it.
// The backdrop instance is not shared so it's OK to make a one-off modification.
$("body > .modal-backdrop").css("z-index", "998");
$dialog.css("z-index", "999");
await this.handleEventInChildren("activeContextChanged", { noteContext: this.noteContext });
this.setVisibility(true);
await this.handleEventInChildren("focusOnDetail", { ntxId: this.noteContext.ntxId });
@@ -130,7 +148,7 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
$dialog.on("hidden.bs.modal", () => {
const $typeWidgetEl = $dialog.find(".note-detail-printable");
if ($typeWidgetEl.length) {
const typeWidget = glob.getComponentByEl($typeWidgetEl[0]) as TypeWidget;
const typeWidget = glob.getComponentByEl($typeWidgetEl[0]) as ReactWrappedWidget;
typeWidget.cleanup();
}
@@ -143,9 +161,12 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
if (visible) {
$bodyItems.fadeIn();
this.$modalHeader.children().show();
document.body.classList.add("popup-editor-open");
} else {
$bodyItems.hide();
this.$modalHeader.children().hide();
document.body.classList.remove("popup-editor-open");
}
}

View File

@@ -1,462 +0,0 @@
import { t } from "../services/i18n.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import protectedSessionHolder from "../services/protected_session_holder.js";
import SpacedUpdate from "../services/spaced_update.js";
import server from "../services/server.js";
import appContext, { type CommandListenerData, type EventData } from "../components/app_context.js";
import keyboardActionsService from "../services/keyboard_actions.js";
import noteCreateService from "../services/note_create.js";
import attributeService from "../services/attributes.js";
import EmptyTypeWidget from "./type_widgets/empty.js";
import EditableTextTypeWidget from "./type_widgets/editable_text.js";
import EditableCodeTypeWidget from "./type_widgets/editable_code.js";
import FileTypeWidget from "./type_widgets/file.js";
import ImageTypeWidget from "./type_widgets/image.js";
import RenderTypeWidget from "./type_widgets/render.js";
import RelationMapTypeWidget from "./type_widgets/relation_map.js";
import CanvasTypeWidget from "./type_widgets/canvas.js";
import ProtectedSessionTypeWidget from "./type_widgets/protected_session.js";
import BookTypeWidget from "./type_widgets/book.js";
import ReadOnlyTextTypeWidget from "./type_widgets/read_only_text.js";
import ReadOnlyCodeTypeWidget from "./type_widgets/read_only_code.js";
import NoneTypeWidget from "./type_widgets/none.js";
import NoteMapTypeWidget from "./type_widgets/note_map.js";
import WebViewTypeWidget from "./type_widgets/web_view.js";
import DocTypeWidget from "./type_widgets/doc.js";
import ContentWidgetTypeWidget from "./type_widgets/content_widget.js";
import AttachmentListTypeWidget from "./type_widgets/attachment_list.js";
import AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.js";
import MindMapWidget from "./type_widgets/mind_map.js";
import utils, { isElectron } from "../services/utils.js";
import type { NoteType } from "../entities/fnote.js";
import type TypeWidget from "./type_widgets/type_widget.js";
import { MermaidTypeWidget } from "./type_widgets/mermaid.js";
import AiChatTypeWidget from "./type_widgets/ai_chat.js";
import toast from "../services/toast.js";
const TPL = /*html*/`
<div class="note-detail">
<style>
.note-detail {
font-family: var(--detail-font-family);
font-size: var(--detail-font-size);
}
.note-detail.full-height {
height: 100%;
}
</style>
</div>
`;
const typeWidgetClasses = {
empty: EmptyTypeWidget,
editableText: EditableTextTypeWidget,
readOnlyText: ReadOnlyTextTypeWidget,
editableCode: EditableCodeTypeWidget,
readOnlyCode: ReadOnlyCodeTypeWidget,
file: FileTypeWidget,
image: ImageTypeWidget,
search: NoneTypeWidget,
render: RenderTypeWidget,
relationMap: RelationMapTypeWidget,
canvas: CanvasTypeWidget,
protectedSession: ProtectedSessionTypeWidget,
book: BookTypeWidget,
noteMap: NoteMapTypeWidget,
webView: WebViewTypeWidget,
doc: DocTypeWidget,
contentWidget: ContentWidgetTypeWidget,
attachmentDetail: AttachmentDetailTypeWidget,
attachmentList: AttachmentListTypeWidget,
mindMap: MindMapWidget,
aiChat: AiChatTypeWidget,
// Split type editors
mermaid: MermaidTypeWidget
};
/**
* A `NoteType` altered by the note detail widget, taking into consideration whether the note is editable or not and adding special note types such as an empty one,
* for protected session or attachment information.
*/
type ExtendedNoteType =
| Exclude<NoteType, "launcher" | "text" | "code">
| "empty"
| "readOnlyCode"
| "readOnlyText"
| "editableText"
| "editableCode"
| "attachmentDetail"
| "attachmentList"
| "protectedSession"
| "aiChat";
export default class NoteDetailWidget extends NoteContextAwareWidget {
private typeWidgets: Record<string, TypeWidget>;
private spacedUpdate: SpacedUpdate;
private type?: ExtendedNoteType;
private mime?: string;
constructor() {
super();
this.typeWidgets = {};
this.spacedUpdate = new SpacedUpdate(async () => {
if (!this.noteContext) {
return;
}
const { note } = this.noteContext;
if (!note) {
return;
}
const { noteId } = note;
const data = await this.getTypeWidget().getData();
// for read only notes
if (data === undefined) {
return;
}
protectedSessionHolder.touchProtectedSessionIfNecessary(note);
await server.put(`notes/${noteId}/data`, data, this.componentId);
this.getTypeWidget().dataSaved();
});
appContext.addBeforeUnloadListener(this);
}
isEnabled() {
return true;
}
doRender() {
this.$widget = $(TPL);
this.contentSized();
if (utils.isElectron()) {
const { ipcRenderer } = utils.dynamicRequire("electron");
ipcRenderer.on("print-done", () => {
toast.closePersistent("printing");
});
}
}
async refresh() {
this.type = await this.getWidgetType();
this.mime = this.note?.mime;
if (!(this.type in this.typeWidgets)) {
const clazz = typeWidgetClasses[this.type];
if (!clazz) {
throw new Error(`Cannot find type widget for type '${this.type}'`);
}
const typeWidget = (this.typeWidgets[this.type] = new clazz());
typeWidget.spacedUpdate = this.spacedUpdate;
typeWidget.setParent(this);
if (this.noteContext) {
typeWidget.setNoteContextEvent({ noteContext: this.noteContext });
}
const $renderedWidget = typeWidget.render();
keyboardActionsService.updateDisplayedShortcuts($renderedWidget);
this.$widget.append($renderedWidget);
if (this.noteContext) {
await typeWidget.handleEvent("setNoteContext", { noteContext: this.noteContext });
}
// this is happening in update(), so note has been already set, and we need to reflect this
if (this.noteContext) {
await typeWidget.handleEvent("noteSwitched", {
noteContext: this.noteContext,
notePath: this.noteContext.notePath
});
}
this.child(typeWidget);
}
this.checkFullHeight();
if (utils.isMobile()) {
const hasFixedTree = this.noteContext?.hoistedNoteId === "_lbMobileRoot";
$("body").toggleClass("force-fixed-tree", hasFixedTree);
}
}
/**
* sets full height of container that contains note content for a subset of note-types
*/
checkFullHeight() {
// https://github.com/zadam/trilium/issues/2522
const isBackendNote = this.noteContext?.noteId === "_backendLog";
const isSqlNote = this.mime === "text/x-sqlite;schema=trilium";
const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "mermaid", "file", "aiChat"].includes(this.type ?? "");
const isFullHeight = (!this.noteContext?.hasNoteList() && isFullHeightNoteType && !isSqlNote)
|| this.noteContext?.viewScope?.viewMode === "attachments"
|| isBackendNote;
this.$widget.toggleClass("full-height", isFullHeight);
}
getTypeWidget() {
if (!this.type || !this.typeWidgets[this.type]) {
throw new Error(t(`note_detail.could_not_find_typewidget`, { type: this.type }));
}
return this.typeWidgets[this.type];
}
async getWidgetType(): Promise<ExtendedNoteType> {
const note = this.note;
if (!note) {
return "empty";
}
const type = note.type;
let resultingType: ExtendedNoteType;
const viewScope = this.noteContext?.viewScope;
if (viewScope?.viewMode === "source") {
resultingType = "readOnlyCode";
} else if (viewScope && viewScope.viewMode === "attachments") {
resultingType = viewScope.attachmentId ? "attachmentDetail" : "attachmentList";
} else if (type === "text" && (await this.noteContext?.isReadOnly())) {
resultingType = "readOnlyText";
} else if ((type === "code" || type === "mermaid") && (await this.noteContext?.isReadOnly())) {
resultingType = "readOnlyCode";
} else if (type === "text") {
resultingType = "editableText";
} else if (type === "code") {
resultingType = "editableCode";
} else if (type === "launcher") {
resultingType = "doc";
} else {
resultingType = type;
}
if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) {
resultingType = "protectedSession";
}
return resultingType;
}
async focusOnDetailEvent({ ntxId }: EventData<"focusOnDetail">) {
if (this.noteContext?.ntxId !== ntxId) {
return;
}
await this.refresh();
const widget = this.getTypeWidget();
await widget.initialized;
widget.focus();
}
async scrollToEndEvent({ ntxId }: EventData<"scrollToEnd">) {
if (this.noteContext?.ntxId !== ntxId) {
return;
}
await this.refresh();
const widget = this.getTypeWidget();
await widget.initialized;
if (widget.scrollToEnd) {
widget.scrollToEnd();
}
}
async beforeNoteSwitchEvent({ noteContext }: EventData<"beforeNoteSwitch">) {
if (this.isNoteContext(noteContext.ntxId)) {
await this.spacedUpdate.updateNowIfNecessary();
}
}
async beforeNoteContextRemoveEvent({ ntxIds }: EventData<"beforeNoteContextRemove">) {
if (this.isNoteContext(ntxIds)) {
await this.spacedUpdate.updateNowIfNecessary();
}
}
async runActiveNoteCommand(params: CommandListenerData<"runActiveNote">) {
if (this.isNoteContext(params.ntxId)) {
// make sure that script is saved before running it #4028
await this.spacedUpdate.updateNowIfNecessary();
}
return await this.parent?.triggerCommand("runActiveNote", params);
}
async printActiveNoteEvent() {
if (!this.noteContext?.isActive()) {
return;
}
toast.showPersistent({
icon: "bx bx-loader-circle bx-spin",
message: t("note_detail.printing"),
id: "printing"
});
if (isElectron()) {
const { ipcRenderer } = utils.dynamicRequire("electron");
ipcRenderer.send("print-note", {
notePath: this.notePath
});
} else {
const iframe = document.createElement('iframe');
iframe.src = `?print#${this.notePath}`;
iframe.className = "print-iframe";
document.body.appendChild(iframe);
iframe.onload = () => {
if (!iframe.contentWindow) {
toast.closePersistent("printing");
document.body.removeChild(iframe);
return;
}
iframe.contentWindow.addEventListener("note-ready", () => {
toast.closePersistent("printing");
iframe.contentWindow?.print();
document.body.removeChild(iframe);
});
};
}
}
async exportAsPdfEvent() {
if (!this.noteContext?.isActive() || !this.note || !this.notePath) {
return;
}
toast.showPersistent({
icon: "bx bx-loader-circle bx-spin",
message: t("note_detail.printing_pdf"),
id: "printing"
});
const { ipcRenderer } = utils.dynamicRequire("electron");
ipcRenderer.send("export-as-pdf", {
title: this.note.title,
notePath: this.notePath,
pageSize: this.note.getAttributeValue("label", "printPageSize") ?? "Letter",
landscape: this.note.hasAttribute("label", "printLandscape")
});
}
hoistedNoteChangedEvent({ ntxId }: EventData<"hoistedNoteChanged">) {
if (this.isNoteContext(ntxId)) {
this.refresh();
}
}
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
// we're detecting note type change on the note_detail level, but triggering the noteTypeMimeChanged
// globally, so it gets also to e.g. ribbon components. But this means that the event can be generated multiple
// times if the same note is open in several tabs.
if (this.noteId && loadResults.isNoteContentReloaded(this.noteId, this.componentId)) {
// probably incorrect event
// calling this.refresh() is not enough since the event needs to be propagated to children as well
// FIXME: create a separate event to force hierarchical refresh
// this uses handleEvent to make sure that the ordinary content updates are propagated only in the subtree
// to avoid the problem in #3365
this.handleEvent("noteTypeMimeChanged", { noteId: this.noteId });
} else if (this.noteId && loadResults.isNoteReloaded(this.noteId, this.componentId) && (this.type !== (await this.getWidgetType()) || this.mime !== this.note?.mime)) {
// this needs to have a triggerEvent so that e.g., note type (not in the component subtree) is updated
this.triggerEvent("noteTypeMimeChanged", { noteId: this.noteId });
} else {
const attrs = loadResults.getAttributeRows();
const label = attrs.find(
(attr) =>
attr.type === "label" &&
["readOnly", "autoReadOnlyDisabled", "cssClass", "displayRelations", "hideRelations"].includes(attr.name ?? "") &&
attributeService.isAffecting(attr, this.note)
);
const relation = attrs.find((attr) => attr.type === "relation" && ["template", "inherit", "renderNote"].includes(attr.name ?? "") && attributeService.isAffecting(attr, this.note));
if (this.noteId && (label || relation)) {
// probably incorrect event
// calling this.refresh() is not enough since the event needs to be propagated to children as well
this.triggerEvent("noteTypeMimeChanged", { noteId: this.noteId });
}
}
}
beforeUnloadEvent() {
return this.spacedUpdate.isAllSavedAndTriggerUpdate();
}
readOnlyTemporarilyDisabledEvent({ noteContext }: EventData<"readOnlyTemporarilyDisabled">) {
if (this.isNoteContext(noteContext.ntxId)) {
this.refresh();
}
}
async executeInActiveNoteDetailWidgetEvent({ callback }: EventData<"executeInActiveNoteDetailWidget">) {
if (!this.isActiveNoteContext()) {
return;
}
await this.initialized;
callback(this);
}
async cutIntoNoteCommand() {
const note = appContext.tabManager.getActiveContextNote();
if (!note) {
return;
}
// without await as this otherwise causes deadlock through component mutex
const parentNotePath = appContext.tabManager.getActiveContextNotePath();
if (this.noteContext && parentNotePath) {
noteCreateService.createNote(parentNotePath, {
isProtected: note.isProtected,
saveSelection: true,
textEditor: await this.noteContext.getTextEditor()
});
}
}
// used by cutToNote in CKEditor build
async saveNoteDetailNowCommand() {
await this.spacedUpdate.updateNowIfNecessary();
}
renderActiveNoteEvent() {
if (this.noteContext?.isActive()) {
this.refresh();
}
}
async executeWithTypeWidgetEvent({ resolve, ntxId }: EventData<"executeWithTypeWidget">) {
if (!this.isNoteContext(ntxId)) {
return;
}
await this.initialized;
await this.getWidgetType();
resolve(this.getTypeWidget());
}
}

View File

@@ -1,671 +0,0 @@
import server from "../services/server.js";
import attributeService from "../services/attributes.js";
import hoistedNoteService from "../services/hoisted_note.js";
import appContext, { type EventData } from "../components/app_context.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import linkContextMenuService from "../menus/link_context_menu.js";
import utils from "../services/utils.js";
import { t } from "../services/i18n.js";
import type ForceGraph from "force-graph";
import type { GraphData, LinkObject, NodeObject } from "force-graph";
import type FNote from "../entities/fnote.js";
const esc = utils.escapeHtml;
const TPL = /*html*/`<div class="note-map-widget">
<style>
.note-detail-note-map {
height: 100%;
overflow: hidden;
}
/* Style Ui Element to Drag Nodes */
.fixnodes-type-switcher {
display: flex;
align-items: center;
z-index: 10; /* should be below dropdown (note actions) */
border-radius: .2rem;
}
.fixnodes-type-switcher button.toggled {
background: var(--active-item-background-color);
color: var(--active-item-text-color);
}
/* Start of styling the slider */
.fixnodes-type-switcher input[type="range"] {
/* removing default appearance */
-webkit-appearance: none;
appearance: none;
margin-inline-start: 15px;
width: 150px;
}
/* Changing slider tracker */
.fixnodes-type-switcher input[type="range"]::-webkit-slider-runnable-track {
height: 4px;
background-color: var(--main-border-color);
border-radius: 4px;
}
/* Changing Slider Thumb */
.fixnodes-type-switcher input[type="range"]::-webkit-slider-thumb {
/* removing default appearance */
-webkit-appearance: none;
appearance: none;
/* creating a custom design */
height: 15px;
width: 15px;
margin-top:-5px;
background-color: var(--accented-background-color);
border: 1px solid var(--main-text-color);
border-radius: 50%;
}
.fixnodes-type-switcher input[type="range"]::-moz-range-track {
background-color: var(--main-border-color);
border-radius: 4px;
}
.fixnodes-type-switcher input[type="range"]::-moz-range-thumb {
background-color: var(--accented-background-color);
border-color: var(--main-text-color);
height: 10px;
width: 10px;
}
/* End of styling the slider */
</style>
<div class="btn-group btn-group-sm map-type-switcher content-floating-buttons top-left" role="group">
<button type="button" class="btn bx bx-network-chart tn-tool-button" title="${t("note-map.button-link-map")}" data-type="link"></button>
<button type="button" class="btn bx bx-sitemap tn-tool-button" title="${t("note-map.button-tree-map")}" data-type="tree"></button>
</div>
<! UI for dragging Notes and link force >
<div class="btn-group-sm fixnodes-type-switcher content-floating-buttons bottom-left" role="group">
<button type="button" data-toggle="button" class="btn bx bx-lock-alt tn-tool-button" title="${t("note_map.fix-nodes")}" data-type="moveable"></button>
<input type="range" class="slider" min="1" title="${t("note_map.link-distance")}" max="100" value="40" >
</div>
<div class="style-resolver"></div>
<div class="note-map-container"></div>
</div>`;
type WidgetMode = "type" | "ribbon";
type MapType = "tree" | "link";
type Data = GraphData<NodeObject, LinkObject<NodeObject>>;
interface Node extends NodeObject {
id: string;
name: string;
type: string;
color: string;
}
interface Link extends LinkObject<NodeObject> {
id: string;
name: string;
x: number;
y: number;
source: Node;
target: Node;
}
interface NotesAndRelationsData {
nodes: Node[];
links: {
id: string;
source: string;
target: string;
name: string;
}[];
}
// Replace
interface ResponseLink {
key: string;
sourceNoteId: string;
targetNoteId: string;
name: string;
}
interface PostNotesMapResponse {
notes: string[];
links: ResponseLink[];
noteIdToDescendantCountMap: Record<string, number>;
}
interface GroupedLink {
id: string;
sourceNoteId: string;
targetNoteId: string;
names: string[];
}
interface CssData {
fontFamily: string;
textColor: string;
mutedTextColor: string;
}
export default class NoteMapWidget extends NoteContextAwareWidget {
private fixNodes: boolean;
private widgetMode: WidgetMode;
private mapType?: MapType;
private cssData!: CssData;
private themeStyle!: string;
private $container!: JQuery<HTMLElement>;
private $styleResolver!: JQuery<HTMLElement>;
private $fixNodesButton!: JQuery<HTMLElement>;
graph!: ForceGraph;
private noteIdToSizeMap!: Record<string, number>;
private zoomLevel!: number;
private nodes!: Node[];
constructor(widgetMode: WidgetMode) {
super();
this.fixNodes = false; // needed to save the status of the UI element. Is set later in the code
this.widgetMode = widgetMode; // 'type' or 'ribbon'
}
doRender() {
this.$widget = $(TPL);
const documentStyle = window.getComputedStyle(document.documentElement);
this.themeStyle = documentStyle.getPropertyValue("--theme-style")?.trim();
this.$container = this.$widget.find(".note-map-container");
this.$styleResolver = this.$widget.find(".style-resolver");
this.$fixNodesButton = this.$widget.find(".fixnodes-type-switcher > button");
new ResizeObserver(() => this.setDimensions()).observe(this.$container[0]);
this.$widget.find(".map-type-switcher button").on("click", async (e) => {
const type = $(e.target).closest("button").attr("data-type");
await attributeService.setLabel(this.noteId ?? "", "mapType", type);
});
// Reading the status of the Drag nodes Ui element. Changing it´s color when activated.
// Reading Force value of the link distance.
this.$fixNodesButton.on("click", async (event) => {
this.fixNodes = !this.fixNodes;
this.$fixNodesButton.toggleClass("toggled", this.fixNodes);
});
super.doRender();
}
setDimensions() {
if (!this.graph) {
// no graph has been even rendered
return;
}
const $parent = this.$widget.parent();
this.graph
.height($parent.height() || 0)
.width($parent.width() || 0);
}
async refreshWithNote(note: FNote) {
this.$widget.show();
this.cssData = {
fontFamily: this.$container.css("font-family"),
textColor: this.rgb2hex(this.$container.css("color")),
mutedTextColor: this.rgb2hex(this.$styleResolver.css("color"))
};
this.mapType = note.getLabelValue("mapType") === "tree" ? "tree" : "link";
//variables for the hover effekt. We have to save the neighbours of a hovered node in a set. Also we need to save the links as well as the hovered node itself
let hoverNode: NodeObject | null = null;
const highlightLinks = new Set();
const neighbours = new Set();
const ForceGraph = (await import("force-graph")).default;
this.graph = new ForceGraph(this.$container[0])
.width(this.$container.width() || 0)
.height(this.$container.height() || 0)
.onZoom((zoom) => this.setZoomLevel(zoom.k))
.d3AlphaDecay(0.01)
.d3VelocityDecay(0.08)
//Code to fixate nodes when dragged
.onNodeDragEnd((node) => {
if (this.fixNodes) {
node.fx = node.x;
node.fy = node.y;
} else {
node.fx = undefined;
node.fy = undefined;
}
})
//check if hovered and set the hovernode variable, saving the hovered node object into it. Clear links variable everytime you hover. Without clearing links will stay highlighted
.onNodeHover((node) => {
hoverNode = node || null;
highlightLinks.clear();
})
// set link width to immitate a highlight effekt. Checking the condition if any links are saved in the previous defined set highlightlinks
.linkWidth((link) => (highlightLinks.has(link) ? 3 : 0.4))
.linkColor((link) => (highlightLinks.has(link) ? "white" : this.cssData.mutedTextColor))
.linkDirectionalArrowLength(4)
.linkDirectionalArrowRelPos(0.95)
// main code for highlighting hovered nodes and neighbours. here we "style" the nodes. the nodes are rendered several hundred times per second.
.nodeCanvasObject((_node, ctx) => {
const node = _node as Node;
if (hoverNode == node) {
//paint only hovered node
this.paintNode(node, "#661822", ctx);
neighbours.clear(); //clearing neighbours or the effect would be maintained after hovering is over
for (const _link of data.links) {
const link = _link as unknown as Link;
//check if node is part of a link in the canvas, if so add it´s neighbours and related links to the previous defined variables to paint the nodes
if (link.source.id == node.id || link.target.id == node.id) {
neighbours.add(link.source);
neighbours.add(link.target);
highlightLinks.add(link);
neighbours.delete(node);
}
}
} else if (neighbours.has(node) && hoverNode != null) {
//paint neighbours
this.paintNode(node, "#9d6363", ctx);
} else {
this.paintNode(node, this.getColorForNode(node), ctx); //paint rest of nodes in canvas
}
})
.nodePointerAreaPaint((node, _, ctx) => this.paintNode(node as Node, this.getColorForNode(node as Node), ctx))
.nodePointerAreaPaint((node, color, ctx) => {
if (!node.id) {
return;
}
ctx.fillStyle = color;
ctx.beginPath();
if (node.x && node.y) {
ctx.arc(node.x, node.y, this.noteIdToSizeMap[node.id], 0, 2 * Math.PI, false);
}
ctx.fill();
})
.nodeLabel((node) => esc((node as Node).name))
.maxZoom(7)
.warmupTicks(30)
.onNodeClick((node) => {
if (node.id) {
appContext.tabManager.getActiveContext()?.setNote((node as Node).id);
}
})
.onNodeRightClick((node, e) => {
if (node.id) {
linkContextMenuService.openContextMenu((node as Node).id, e);
}
});
if (this.mapType === "link") {
this.graph
.linkLabel((l) => `${esc((l as Link).source.name)} - <strong>${esc((l as Link).name)}</strong> - ${esc((l as Link).target.name)}`)
.linkCanvasObject((link, ctx) => this.paintLink(link as Link, ctx))
.linkCanvasObjectMode(() => "after");
}
const mapRootNoteId = this.getMapRootNoteId();
const labelValues = (name: string) => this.note?.getLabels(name).map(l => l.value) ?? [];
const excludeRelations = labelValues("mapExcludeRelation");
const includeRelations = labelValues("mapIncludeRelation");
const data = await this.loadNotesAndRelations(mapRootNoteId, excludeRelations, includeRelations);
const nodeLinkRatio = data.nodes.length / data.links.length;
const magnifiedRatio = Math.pow(nodeLinkRatio, 1.5);
const charge = -20 / magnifiedRatio;
const boundedCharge = Math.min(-3, charge);
let distancevalue = 40; // default value for the link force of the nodes
this.$widget.find(".fixnodes-type-switcher input").on("change", async (e) => {
distancevalue = parseInt(e.target.closest("input")?.value ?? "0");
this.graph.d3Force("link")?.distance(distancevalue);
this.renderData(data);
});
this.graph.d3Force("center")?.strength(0.2);
this.graph.d3Force("charge")?.strength(boundedCharge);
this.graph.d3Force("charge")?.distanceMax(1000);
this.renderData(data);
}
getMapRootNoteId(): string {
if (this.noteId && this.widgetMode === "ribbon") {
return this.noteId;
}
let mapRootNoteId = this.note?.getLabelValue("mapRootNoteId");
if (mapRootNoteId === "hoisted") {
mapRootNoteId = hoistedNoteService.getHoistedNoteId();
} else if (!mapRootNoteId) {
mapRootNoteId = appContext.tabManager.getActiveContext()?.parentNoteId;
}
return mapRootNoteId ?? "";
}
getColorForNode(node: Node) {
if (node.color) {
return node.color;
} else if (this.widgetMode === "ribbon" && node.id === this.noteId) {
return "red"; // subtree root mark as red
} else {
return this.generateColorFromString(node.type);
}
}
generateColorFromString(str: string) {
if (this.themeStyle === "dark") {
str = `0${str}`; // magic lightning modifier
}
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
let color = "#";
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff;
color += `00${value.toString(16)}`.substr(-2);
}
return color;
}
rgb2hex(rgb: string) {
return `#${(rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/) || [])
.slice(1)
.map((n) => parseInt(n, 10).toString(16).padStart(2, "0"))
.join("")}`;
}
setZoomLevel(level: number) {
this.zoomLevel = level;
}
paintNode(node: Node, color: string, ctx: CanvasRenderingContext2D) {
const { x, y } = node;
if (!x || !y) {
return;
}
const size = this.noteIdToSizeMap[node.id];
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, size * 0.8, 0, 2 * Math.PI, false);
ctx.fill();
const toRender = this.zoomLevel > 2 || (this.zoomLevel > 1 && size > 6) || (this.zoomLevel > 0.3 && size > 10);
if (!toRender) {
return;
}
ctx.fillStyle = this.cssData.textColor;
ctx.font = `${size}px ${this.cssData.fontFamily}`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
let title = node.name;
if (title.length > 15) {
title = `${title.substr(0, 15)}...`;
}
ctx.fillText(title, x, y + Math.round(size * 1.5));
}
paintLink(link: Link, ctx: CanvasRenderingContext2D) {
if (this.zoomLevel < 5) {
return;
}
ctx.font = `3px ${this.cssData.fontFamily}`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = this.cssData.mutedTextColor;
const { source, target } = link;
if (typeof source !== "object" || typeof target !== "object") {
return;
}
if (source.x && source.y && target.x && target.y) {
const x = (source.x + target.x) / 2;
const y = (source.y + target.y) / 2;
ctx.save();
ctx.translate(x, y);
const deltaY = source.y - target.y;
const deltaX = source.x - target.x;
let angle = Math.atan2(deltaY, deltaX);
let moveY = 2;
if (angle < -Math.PI / 2 || angle > Math.PI / 2) {
angle += Math.PI;
moveY = -2;
}
ctx.rotate(angle);
ctx.fillText(link.name, 0, moveY);
}
ctx.restore();
}
async loadNotesAndRelations(mapRootNoteId: string, excludeRelations: string[], includeRelations: string[]): Promise<NotesAndRelationsData> {
const resp = await server.post<PostNotesMapResponse>(`note-map/${mapRootNoteId}/${this.mapType}`, {
excludeRelations, includeRelations
});
this.calculateNodeSizes(resp);
const links = this.getGroupedLinks(resp.links);
this.nodes = resp.notes.map(([noteId, title, type, color]) => ({
id: noteId,
name: title,
type: type,
color: color
}));
return {
nodes: this.nodes,
links: links.map((link) => ({
id: `${link.sourceNoteId}-${link.targetNoteId}`,
source: link.sourceNoteId,
target: link.targetNoteId,
name: link.names.join(", ")
}))
};
}
getGroupedLinks(links: ResponseLink[]): GroupedLink[] {
const linksGroupedBySourceTarget: Record<string, GroupedLink> = {};
for (const link of links) {
const key = `${link.sourceNoteId}-${link.targetNoteId}`;
if (key in linksGroupedBySourceTarget) {
if (!linksGroupedBySourceTarget[key].names.includes(link.name)) {
linksGroupedBySourceTarget[key].names.push(link.name);
}
} else {
linksGroupedBySourceTarget[key] = {
id: key,
sourceNoteId: link.sourceNoteId,
targetNoteId: link.targetNoteId,
names: [link.name]
};
}
}
return Object.values(linksGroupedBySourceTarget);
}
calculateNodeSizes(resp: PostNotesMapResponse) {
this.noteIdToSizeMap = {};
if (this.mapType === "tree") {
const { noteIdToDescendantCountMap } = resp;
for (const noteId in noteIdToDescendantCountMap) {
this.noteIdToSizeMap[noteId] = 4;
const count = noteIdToDescendantCountMap[noteId];
if (count > 0) {
this.noteIdToSizeMap[noteId] += 1 + Math.round(Math.log(count) / Math.log(1.5));
}
}
} else if (this.mapType === "link") {
const noteIdToLinkCount: Record<string, number> = {};
for (const link of resp.links) {
noteIdToLinkCount[link.targetNoteId] = 1 + (noteIdToLinkCount[link.targetNoteId] || 0);
}
for (const [noteId] of resp.notes) {
this.noteIdToSizeMap[noteId] = 4;
if (noteId in noteIdToLinkCount) {
this.noteIdToSizeMap[noteId] += Math.min(Math.pow(noteIdToLinkCount[noteId], 0.5), 15);
}
}
}
}
renderData(data: Data) {
this.graph.graphData(data);
if (this.widgetMode === "ribbon" && this.note?.type !== "search") {
setTimeout(() => {
this.setDimensions();
const subGraphNoteIds = this.getSubGraphConnectedToCurrentNote(data);
this.graph.zoomToFit(400, 50, (node) => subGraphNoteIds.has(node.id));
if (subGraphNoteIds.size < 30) {
this.graph.d3VelocityDecay(0.4);
}
}, 1000);
} else {
if (data.nodes.length > 1) {
setTimeout(() => {
this.setDimensions();
const noteIdsWithLinks = this.getNoteIdsWithLinks(data);
if (noteIdsWithLinks.size > 0) {
this.graph.zoomToFit(400, 30, (node) => noteIdsWithLinks.has(node.id ?? ""));
}
if (noteIdsWithLinks.size < 30) {
this.graph.d3VelocityDecay(0.4);
}
}, 1000);
}
}
}
getNoteIdsWithLinks(data: Data) {
const noteIds = new Set<string | number>();
for (const link of data.links) {
if (typeof link.source === "object" && link.source.id) {
noteIds.add(link.source.id);
}
if (typeof link.target === "object" && link.target.id) {
noteIds.add(link.target.id);
}
}
return noteIds;
}
getSubGraphConnectedToCurrentNote(data: Data) {
function getGroupedLinks(links: LinkObject<NodeObject>[], type: "source" | "target") {
const map: Record<string | number, LinkObject<NodeObject>[]> = {};
for (const link of links) {
if (typeof link[type] !== "object") {
continue;
}
const key = link[type].id;
if (key) {
map[key] = map[key] || [];
map[key].push(link);
}
}
return map;
}
const linksBySource = getGroupedLinks(data.links, "source");
const linksByTarget = getGroupedLinks(data.links, "target");
const subGraphNoteIds = new Set();
function traverseGraph(noteId?: string | number) {
if (!noteId || subGraphNoteIds.has(noteId)) {
return;
}
subGraphNoteIds.add(noteId);
for (const link of linksBySource[noteId] || []) {
if (typeof link.target === "object") {
traverseGraph(link.target?.id);
}
}
for (const link of linksByTarget[noteId] || []) {
if (typeof link.source === "object") {
traverseGraph(link.source?.id);
}
}
}
traverseGraph(this.noteId);
return subGraphNoteIds;
}
cleanup() {
this.$container.html("");
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.getAttributeRows(this.componentId)
.find((attr) => attr.type === "label" && ["mapType", "mapRootNoteId"].includes(attr.name || "") && attributeService.isAffecting(attr, this.note))) {
this.refresh();
}
}
}

View File

@@ -0,0 +1,57 @@
.note-detail-note-map {
height: 100%;
overflow: hidden;
}
/* Style Ui Element to Drag Nodes */
.fixnodes-type-switcher {
display: flex;
align-items: center;
z-index: 10; /* should be below dropdown (note actions) */
border-radius: .2rem;
}
/* Start of styling the slider */
.fixnodes-type-switcher input[type="range"] {
/* removing default appearance */
-webkit-appearance: none;
appearance: none;
margin-inline-start: 15px;
width: 150px;
}
/* Changing slider tracker */
.fixnodes-type-switcher input[type="range"]::-webkit-slider-runnable-track {
height: 4px;
background-color: var(--main-border-color);
border-radius: 4px;
}
/* Changing Slider Thumb */
.fixnodes-type-switcher input[type="range"]::-webkit-slider-thumb {
/* removing default appearance */
-webkit-appearance: none;
appearance: none;
/* creating a custom design */
height: 15px;
width: 15px;
margin-top:-5px;
background-color: var(--accented-background-color);
border: 1px solid var(--main-text-color);
border-radius: 50%;
}
.fixnodes-type-switcher input[type="range"]::-moz-range-track {
background-color: var(--main-border-color);
border-radius: 4px;
}
.fixnodes-type-switcher input[type="range"]::-moz-range-thumb {
background-color: var(--accented-background-color);
border-color: var(--main-text-color);
height: 10px;
width: 10px;
}
/* End of styling the slider */

View File

@@ -0,0 +1,174 @@
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import "./NoteMap.css";
import { getThemeStyle, MapType, NoteMapWidgetMode, rgb2hex } from "./utils";
import { RefObject } from "preact";
import FNote from "../../entities/fnote";
import { useElementSize, useNoteLabel } from "../react/hooks";
import ForceGraph from "force-graph";
import { loadNotesAndRelations, NoteMapLinkObject, NoteMapNodeObject, NotesAndRelationsData } from "./data";
import { CssData, setupRendering } from "./rendering";
import ActionButton from "../react/ActionButton";
import { t } from "../../services/i18n";
import link_context_menu from "../../menus/link_context_menu";
import appContext from "../../components/app_context";
import Slider from "../react/Slider";
import hoisted_note from "../../services/hoisted_note";
interface NoteMapProps {
note: FNote;
widgetMode: NoteMapWidgetMode;
parentRef: RefObject<HTMLElement>;
}
export default function NoteMap({ note, widgetMode, parentRef }: NoteMapProps) {
const containerRef = useRef<HTMLDivElement>(null);
const styleResolverRef = useRef<HTMLDivElement>(null);
const [ mapTypeRaw, setMapType ] = useNoteLabel(note, "mapType");
const [ mapRootIdLabel ] = useNoteLabel(note, "mapRootNoteId");
const mapType: MapType = mapTypeRaw === "tree" ? "tree" : "link";
const graphRef = useRef<ForceGraph<NoteMapNodeObject, NoteMapLinkObject>>();
const containerSize = useElementSize(parentRef);
const [ fixNodes, setFixNodes ] = useState(false);
const [ linkDistance, setLinkDistance ] = useState(40);
const notesAndRelationsRef = useRef<NotesAndRelationsData>();
const mapRootId = useMemo(() => {
if (note.noteId && widgetMode === "ribbon") {
return note.noteId;
} else if (mapRootIdLabel === "hoisted") {
return hoisted_note.getHoistedNoteId();
} else if (mapRootIdLabel) {
return mapRootIdLabel;
} else {
return appContext.tabManager.getActiveContext()?.parentNoteId ?? null;
}
}, [ note ]);
// Build the note graph instance.
useEffect(() => {
const container = containerRef.current;
if (!container || !mapRootId) return;
const graph = new ForceGraph<NoteMapNodeObject, NoteMapLinkObject>(container);
graphRef.current = graph;
const labelValues = (name: string) => note.getLabels(name).map(l => l.value) ?? [];
const excludeRelations = labelValues("mapExcludeRelation");
const includeRelations = labelValues("mapIncludeRelation");
loadNotesAndRelations(mapRootId, excludeRelations, includeRelations, mapType).then((notesAndRelations) => {
if (!containerRef.current || !styleResolverRef.current) return;
const cssData = getCssData(containerRef.current, styleResolverRef.current);
// Configure rendering properties.
setupRendering(graph, {
note,
noteId: note.noteId,
noteIdToSizeMap: notesAndRelations.noteIdToSizeMap,
cssData,
notesAndRelations,
themeStyle: getThemeStyle(),
widgetMode,
mapType
});
// Interaction
graph
.onNodeClick((node) => {
if (!node.id) return;
appContext.tabManager.getActiveContext()?.setNote(node.id);
})
.onNodeRightClick((node, e) => {
if (!node.id) return;
link_context_menu.openContextMenu(node.id, e);
});
// Set data
graph.graphData(notesAndRelations);
notesAndRelationsRef.current = notesAndRelations;
});
return () => container.replaceChildren();
}, [ note, mapType ]);
useEffect(() => {
if (!graphRef.current || !notesAndRelationsRef.current) return;
graphRef.current.d3Force("link")?.distance(linkDistance);
graphRef.current.graphData(notesAndRelationsRef.current);
}, [ linkDistance ]);
// React to container size
useEffect(() => {
if (!containerSize || !graphRef.current) return;
graphRef.current.width(containerSize.width).height(containerSize.height);
}, [ containerSize?.width, containerSize?.height ]);
// Fixing nodes when dragged.
useEffect(() => {
graphRef.current?.onNodeDragEnd((node) => {
if (fixNodes) {
node.fx = node.x;
node.fy = node.y;
} else {
node.fx = undefined;
node.fy = undefined;
}
})
}, [ fixNodes ]);
return (
<div className="note-map-widget">
<div className="btn-group btn-group-sm map-type-switcher content-floating-buttons top-left" role="group">
<MapTypeSwitcher type="link" icon="bx bx-network-chart" text={t("note-map.button-link-map")} currentMapType={mapType} setMapType={setMapType} />
<MapTypeSwitcher type="tree" icon="bx bx-sitemap" text={t("note-map.button-tree-map")} currentMapType={mapType} setMapType={setMapType} />
</div>
<div class="btn-group-sm fixnodes-type-switcher content-floating-buttons bottom-left" role="group">
<ActionButton
icon="bx bx-lock-alt"
text={t("note_map.fix-nodes")}
className={fixNodes ? "active" : ""}
onClick={() => setFixNodes(!fixNodes)}
frame
/>
<Slider
min={1} max={100}
value={linkDistance} onChange={setLinkDistance}
title={t("note_map.link-distance")}
/>
</div>
<div ref={styleResolverRef} class="style-resolver" />
<div ref={containerRef} className="note-map-container" />
</div>
);
}
function MapTypeSwitcher({ icon, text, type, currentMapType, setMapType }: {
icon: string;
text: string;
type: MapType;
currentMapType: MapType;
setMapType: (type: MapType) => void;
}) {
return (
<ActionButton
icon={icon} text={text}
active={currentMapType === type}
onClick={() => setMapType(type)}
frame
/>
)
}
function getCssData(container: HTMLElement, styleResolver: HTMLElement): CssData {
const containerStyle = window.getComputedStyle(container);
const styleResolverStyle = window.getComputedStyle(styleResolver);
return {
fontFamily: containerStyle.fontFamily,
textColor: rgb2hex(containerStyle.color),
mutedTextColor: rgb2hex(styleResolverStyle.color)
}
}

View File

@@ -0,0 +1,120 @@
import { NoteMapLink, NoteMapPostResponse } from "@triliumnext/commons";
import server from "../../services/server";
import { LinkObject, NodeObject } from "force-graph";
type MapType = "tree" | "link";
interface GroupedLink {
id: string;
sourceNoteId: string;
targetNoteId: string;
names: string[];
}
export interface NoteMapNodeObject extends NodeObject {
id: string;
name: string;
type: string;
color: string;
}
export interface NoteMapLinkObject extends LinkObject<NoteMapNodeObject> {
id: string;
name: string;
x?: number;
y?: number;
}
export interface NotesAndRelationsData {
nodes: NoteMapNodeObject[];
links: {
id: string;
source: string | NoteMapNodeObject;
target: string | NoteMapNodeObject;
name: string;
}[];
noteIdToSizeMap: Record<string, number>;
}
export async function loadNotesAndRelations(mapRootNoteId: string, excludeRelations: string[], includeRelations: string[], mapType: MapType): Promise<NotesAndRelationsData> {
const resp = await server.post<NoteMapPostResponse>(`note-map/${mapRootNoteId}/${mapType}`, {
excludeRelations, includeRelations
});
const noteIdToSizeMap = calculateNodeSizes(resp, mapType);
const links = getGroupedLinks(resp.links);
const nodes = resp.notes.map(([noteId, title, type, color]) => ({
id: noteId,
name: title,
type: type,
color: color
}));
return {
noteIdToSizeMap,
nodes,
links: links.map((link) => ({
id: `${link.sourceNoteId}-${link.targetNoteId}`,
source: link.sourceNoteId,
target: link.targetNoteId,
name: link.names.join(", ")
}))
};
}
function calculateNodeSizes(resp: NoteMapPostResponse, mapType: MapType) {
const noteIdToSizeMap: Record<string, number> = {};
if (mapType === "tree") {
const { noteIdToDescendantCountMap } = resp;
for (const noteId in noteIdToDescendantCountMap) {
noteIdToSizeMap[noteId] = 4;
const count = noteIdToDescendantCountMap[noteId];
if (count > 0) {
noteIdToSizeMap[noteId] += 1 + Math.round(Math.log(count) / Math.log(1.5));
}
}
} else if (mapType === "link") {
const noteIdToLinkCount: Record<string, number> = {};
for (const link of resp.links) {
noteIdToLinkCount[link.targetNoteId] = 1 + (noteIdToLinkCount[link.targetNoteId] || 0);
}
for (const [noteId] of resp.notes) {
noteIdToSizeMap[noteId] = 4;
if (noteId in noteIdToLinkCount) {
noteIdToSizeMap[noteId] += Math.min(Math.pow(noteIdToLinkCount[noteId], 0.5), 15);
}
}
}
return noteIdToSizeMap;
}
function getGroupedLinks(links: NoteMapLink[]): GroupedLink[] {
const linksGroupedBySourceTarget: Record<string, GroupedLink> = {};
for (const link of links) {
const key = `${link.sourceNoteId}-${link.targetNoteId}`;
if (key in linksGroupedBySourceTarget) {
if (!linksGroupedBySourceTarget[key].names.includes(link.name)) {
linksGroupedBySourceTarget[key].names.push(link.name);
}
} else {
linksGroupedBySourceTarget[key] = {
id: key,
sourceNoteId: link.sourceNoteId,
targetNoteId: link.targetNoteId,
names: [link.name]
};
}
}
return Object.values(linksGroupedBySourceTarget);
}

View File

@@ -0,0 +1,282 @@
import type ForceGraph from "force-graph";
import { NoteMapLinkObject, NoteMapNodeObject, NotesAndRelationsData } from "./data";
import { LinkObject, NodeObject } from "force-graph";
import { generateColorFromString, MapType, NoteMapWidgetMode } from "./utils";
import { escapeHtml } from "../../services/utils";
import FNote from "../../entities/fnote";
export interface CssData {
fontFamily: string;
textColor: string;
mutedTextColor: string;
}
interface RenderData {
note: FNote;
noteIdToSizeMap: Record<string, number>;
cssData: CssData;
noteId: string;
themeStyle: "light" | "dark";
widgetMode: NoteMapWidgetMode;
notesAndRelations: NotesAndRelationsData;
mapType: MapType;
}
export function setupRendering(graph: ForceGraph<NoteMapNodeObject, NoteMapLinkObject>, { note, noteId, themeStyle, widgetMode, noteIdToSizeMap, notesAndRelations, cssData, mapType }: RenderData) {
// variables for the hover effect. We have to save the neighbours of a hovered node in a set. Also we need to save the links as well as the hovered node itself
const neighbours = new Set();
const highlightLinks = new Set();
let hoverNode: NodeObject | null = null;
let zoomLevel: number;
function getColorForNode(node: NoteMapNodeObject) {
if (node.color) {
return node.color;
} else if (widgetMode === "ribbon" && node.id === noteId) {
return "red"; // subtree root mark as red
} else {
return generateColorFromString(node.type, themeStyle);
}
}
function paintNode(node: NoteMapNodeObject, color: string, ctx: CanvasRenderingContext2D) {
const { x, y } = node;
if (!x || !y) {
return;
}
const size = noteIdToSizeMap[node.id];
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, size * 0.8, 0, 2 * Math.PI, false);
ctx.fill();
const toRender = zoomLevel > 2 || (zoomLevel > 1 && size > 6) || (zoomLevel > 0.3 && size > 10);
if (!toRender) {
return;
}
ctx.fillStyle = cssData.textColor;
ctx.font = `${size}px ${cssData.fontFamily}`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
let title = node.name;
if (title.length > 15) {
title = `${title.substr(0, 15)}...`;
}
ctx.fillText(title, x, y + Math.round(size * 1.5));
}
function paintLink(link: NoteMapLinkObject, ctx: CanvasRenderingContext2D) {
if (zoomLevel < 5) {
return;
}
ctx.font = `3px ${cssData.fontFamily}`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = cssData.mutedTextColor;
const { source, target } = link;
if (typeof source !== "object" || typeof target !== "object") {
return;
}
if (source.x && source.y && target.x && target.y) {
const x = (source.x + target.x) / 2;
const y = (source.y + target.y) / 2;
ctx.save();
ctx.translate(x, y);
const deltaY = source.y - target.y;
const deltaX = source.x - target.x;
let angle = Math.atan2(deltaY, deltaX);
let moveY = 2;
if (angle < -Math.PI / 2 || angle > Math.PI / 2) {
angle += Math.PI;
moveY = -2;
}
ctx.rotate(angle);
ctx.fillText(link.name, 0, moveY);
}
ctx.restore();
}
// main code for highlighting hovered nodes and neighbours. here we "style" the nodes. the nodes are rendered several hundred times per second.
graph
.d3AlphaDecay(0.01)
.d3VelocityDecay(0.08)
.maxZoom(7)
.warmupTicks(30)
.nodeCanvasObject((node, ctx) => {
if (hoverNode == node) {
//paint only hovered node
paintNode(node, "#661822", ctx);
neighbours.clear(); //clearing neighbours or the effect would be maintained after hovering is over
for (const link of notesAndRelations.links) {
const { source, target } = link;
if (typeof source !== "object" || typeof target !== "object") continue;
//check if node is part of a link in the canvas, if so add it´s neighbours and related links to the previous defined variables to paint the nodes
if (source.id == node.id || target.id == node.id) {
neighbours.add(link.source);
neighbours.add(link.target);
highlightLinks.add(link);
neighbours.delete(node);
}
}
} else if (neighbours.has(node) && hoverNode != null) {
//paint neighbours
paintNode(node, "#9d6363", ctx);
} else {
paintNode(node, getColorForNode(node), ctx); //paint rest of nodes in canvas
}
})
//check if hovered and set the hovernode variable, saving the hovered node object into it. Clear links variable everytime you hover. Without clearing links will stay highlighted
.onNodeHover((node) => {
hoverNode = node || null;
highlightLinks.clear();
})
.nodePointerAreaPaint((node, _, ctx) => paintNode(node, getColorForNode(node), ctx))
.nodePointerAreaPaint((node, color, ctx) => {
if (!node.id) {
return;
}
ctx.fillStyle = color;
ctx.beginPath();
if (node.x && node.y) {
ctx.arc(node.x, node.y, noteIdToSizeMap[node.id], 0, 2 * Math.PI, false);
}
ctx.fill();
})
.nodeLabel((node) => escapeHtml(node.name))
.onZoom((zoom) => zoomLevel = zoom.k);
// set link width to immitate a highlight effect. Checking the condition if any links are saved in the previous defined set highlightlinks
graph
.linkWidth((link) => (highlightLinks.has(link) ? 3 : 0.4))
.linkColor((link) => (highlightLinks.has(link) ? cssData.textColor : cssData.mutedTextColor))
.linkDirectionalArrowLength(4)
.linkDirectionalArrowRelPos(0.95);
// Link-specific config
if (mapType) {
graph
.linkLabel((link) => {
const { source, target } = link;
if (typeof source !== "object" || typeof target !== "object") return escapeHtml(link.name);
return `${escapeHtml(source.name)} - <strong>${escapeHtml(link.name)}</strong> - ${escapeHtml(target.name)}`;
})
.linkCanvasObject((link, ctx) => paintLink(link, ctx))
.linkCanvasObjectMode(() => "after");
}
// Forces
const nodeLinkRatio = notesAndRelations.nodes.length / notesAndRelations.links.length;
const magnifiedRatio = Math.pow(nodeLinkRatio, 1.5);
const charge = -20 / magnifiedRatio;
const boundedCharge = Math.min(-3, charge);
graph.d3Force("center")?.strength(0.2);
graph.d3Force("charge")?.strength(boundedCharge);
graph.d3Force("charge")?.distanceMax(1000);
// Zoom to notes
if (widgetMode === "ribbon" && note?.type !== "search") {
setTimeout(() => {
const subGraphNoteIds = getSubGraphConnectedToCurrentNote(noteId, notesAndRelations);
graph.zoomToFit(400, 50, (node) => subGraphNoteIds.has(node.id));
if (subGraphNoteIds.size < 30) {
graph.d3VelocityDecay(0.4);
}
}, 1000);
} else {
if (notesAndRelations.nodes.length > 1) {
setTimeout(() => {
const noteIdsWithLinks = getNoteIdsWithLinks(notesAndRelations);
if (noteIdsWithLinks.size > 0) {
graph.zoomToFit(400, 30, (node) => noteIdsWithLinks.has(node.id ?? ""));
}
if (noteIdsWithLinks.size < 30) {
graph.d3VelocityDecay(0.4);
}
}, 1000);
}
}
}
function getNoteIdsWithLinks(data: NotesAndRelationsData) {
const noteIds = new Set<string | number>();
for (const link of data.links) {
if (typeof link.source === "object" && link.source.id) {
noteIds.add(link.source.id);
}
if (typeof link.target === "object" && link.target.id) {
noteIds.add(link.target.id);
}
}
return noteIds;
}
function getSubGraphConnectedToCurrentNote(noteId: string, data: NotesAndRelationsData) {
function getGroupedLinks(links: LinkObject<NodeObject>[], type: "source" | "target") {
const map: Record<string | number, LinkObject<NodeObject>[]> = {};
for (const link of links) {
if (typeof link[type] !== "object") {
continue;
}
const key = link[type].id;
if (key) {
map[key] = map[key] || [];
map[key].push(link);
}
}
return map;
}
const linksBySource = getGroupedLinks(data.links, "source");
const linksByTarget = getGroupedLinks(data.links, "target");
const subGraphNoteIds = new Set();
function traverseGraph(noteId?: string | number) {
if (!noteId || subGraphNoteIds.has(noteId)) {
return;
}
subGraphNoteIds.add(noteId);
for (const link of linksBySource[noteId] || []) {
if (typeof link.target === "object") {
traverseGraph(link.target?.id);
}
}
for (const link of linksByTarget[noteId] || []) {
if (typeof link.source === "object") {
traverseGraph(link.source?.id);
}
}
}
traverseGraph(noteId);
return subGraphNoteIds;
}

View File

@@ -0,0 +1,33 @@
export type NoteMapWidgetMode = "ribbon" | "hoisted" | "type";
export type MapType = "tree" | "link";
export function rgb2hex(rgb: string) {
return `#${(rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/) || [])
.slice(1)
.map((n) => parseInt(n, 10).toString(16).padStart(2, "0"))
.join("")}`;
}
export function generateColorFromString(str: string, themeStyle: "light" | "dark") {
if (themeStyle === "dark") {
str = `0${str}`; // magic lightning modifier
}
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
let color = "#";
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff;
color += `00${value.toString(16)}`.substr(-2);
}
return color;
}
export function getThemeStyle() {
const documentStyle = window.getComputedStyle(document.documentElement);
return documentStyle.getPropertyValue("--theme-style")?.trim() as "light" | "dark";
}

View File

@@ -47,7 +47,9 @@ export default function NoteTitleWidget() {
// Prevent user from navigating away if the spaced update is not done.
useEffect(() => {
appContext.addBeforeUnloadListener(() => spacedUpdate.isAllSavedAndTriggerUpdate());
const listener = () => spacedUpdate.isAllSavedAndTriggerUpdate();
appContext.addBeforeUnloadListener(listener);
return () => appContext.removeBeforeUnloadListener(listener);
}, []);
useTriliumEvents([ "beforeNoteSwitch", "beforeNoteContextRemove" ], () => spacedUpdate.updateNowIfNecessary());

View File

@@ -173,14 +173,6 @@ interface ExpandedSubtreeResponse {
branchIds: string[];
}
interface Node extends Fancytree.NodeData {
noteId: string;
parentNoteId: string;
branchId: string;
isProtected: boolean;
noteType: NoteType;
}
interface RefreshContext {
noteIdsToUpdate: Set<string>;
noteIdsToReload: Set<string>;
@@ -769,7 +761,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
prepareChildren(parentNote: FNote) {
utils.assertArguments(parentNote);
const noteList: Node[] = [];
const noteList: Fancytree.FancytreeNewNode[] = [];
const hideArchivedNotes = this.hideArchivedNotes;
@@ -837,7 +829,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const isFolder = note.isFolder();
const node: Node = {
const node: Fancytree.FancytreeNewNode = {
noteId: note.noteId,
parentNoteId: branch.parentNoteId,
branchId: branch.branchId,
@@ -849,7 +841,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
refKey: note.noteId,
lazy: true,
folder: isFolder,
expanded: branch.isExpanded && note.type !== "search",
expanded: !!branch.isExpanded && note.type !== "search",
key: utils.randomString(12) // this should prevent some "duplicate key" errors
};
@@ -911,7 +903,6 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return extraClasses.join(" ");
}
/** @returns {FancytreeNode[]} */
getSelectedNodes(stopOnParents = false) {
return this.tree.getSelectedNodes(stopOnParents);
}
@@ -1532,7 +1523,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
// Automatically expand the hoisted note by default
const node = this.getActiveNode();
if (node?.data.noteId === this.noteContext.hoistedNoteId){
if (node && node.data.noteId === this.noteContext.hoistedNoteId){
this.setExpanded(node.data.branchId, true);
}
}
@@ -1591,6 +1582,20 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
this.clearSelectedNodes();
}
async editBranchPrefixCommand({ node }: CommandListenerData<"editBranchPrefix">) {
const branchIds = this.getSelectedOrActiveBranchIds(node).filter((branchId) => !branchId.startsWith("virt-"));
if (!branchIds.length) {
return;
}
// Trigger the event with the selected branch IDs
appContext.triggerEvent("editBranchPrefix", {
selectedOrActiveBranchIds: branchIds,
node: node
});
}
canBeMovedUpOrDown(node: Fancytree.FancytreeNode) {
if (node.data.noteId === "root") {
return false;

View File

@@ -0,0 +1,143 @@
/**
* @module
* Contains the definitions for all the note types supported by the application.
*/
import { NoteType } from "@triliumnext/commons";
import { VNode, type JSX } from "preact";
import { TypeWidgetProps } from "./type_widgets/type_widget";
/**
* A `NoteType` altered by the note detail widget, taking into consideration whether the note is editable or not and adding special note types such as an empty one,
* for protected session or attachment information.
*/
export type ExtendedNoteType = Exclude<NoteType, "launcher" | "text" | "code"> | "empty" | "readOnlyCode" | "readOnlyText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession" | "aiChat";
export type TypeWidget = ((props: TypeWidgetProps) => VNode | JSX.Element);
type NoteTypeView = () => (Promise<{ default: TypeWidget } | TypeWidget> | TypeWidget);
interface NoteTypeMapping {
view: NoteTypeView;
printable?: boolean;
/** The class name to assign to the note type wrapper */
className: string;
isFullHeight?: boolean;
}
export const TYPE_MAPPINGS: Record<ExtendedNoteType, NoteTypeMapping> = {
empty: {
view: () => import("./type_widgets/Empty"),
className: "note-detail-empty",
printable: true
},
doc: {
view: () => import("./type_widgets/Doc"),
className: "note-detail-doc",
printable: true
},
search: {
view: () => (props: TypeWidgetProps) => <></>,
className: "note-detail-none",
printable: true
},
protectedSession: {
view: () => import("./type_widgets/ProtectedSession"),
className: "protected-session-password-component"
},
book: {
view: () => import("./type_widgets/Book"),
className: "note-detail-book",
printable: true,
},
contentWidget: {
view: () => import("./type_widgets/ContentWidget"),
className: "note-detail-content-widget",
printable: true
},
webView: {
view: () => import("./type_widgets/WebView"),
className: "note-detail-web-view",
printable: true,
isFullHeight: true
},
file: {
view: () => import("./type_widgets/File"),
className: "note-detail-file",
printable: true,
isFullHeight: true
},
image: {
view: () => import("./type_widgets/Image"),
className: "note-detail-image",
printable: true
},
readOnlyCode: {
view: async () => (await import("./type_widgets/code/Code")).ReadOnlyCode,
className: "note-detail-readonly-code",
printable: true
},
editableCode: {
view: async () => (await import("./type_widgets/code/Code")).EditableCode,
className: "note-detail-code",
printable: true
},
mermaid: {
view: () => import("./type_widgets/Mermaid"),
className: "note-detail-mermaid",
printable: true,
isFullHeight: true
},
mindMap: {
view: () => import("./type_widgets/MindMap"),
className: "note-detail-mind-map",
printable: true,
isFullHeight: true
},
attachmentList: {
view: async () => (await import("./type_widgets/Attachment")).AttachmentList,
className: "attachment-list",
printable: true
},
attachmentDetail: {
view: async () => (await import("./type_widgets/Attachment")).AttachmentDetail,
className: "attachment-detail",
printable: true
},
readOnlyText: {
view: () => import("./type_widgets/text/ReadOnlyText"),
className: "note-detail-readonly-text"
},
editableText: {
view: () => import("./type_widgets/text/EditableText"),
className: "note-detail-editable-text",
printable: true
},
render: {
view: () => import("./type_widgets/Render"),
className: "note-detail-render",
printable: true
},
canvas: {
view: () => import("./type_widgets/canvas/Canvas"),
className: "note-detail-canvas",
printable: true,
isFullHeight: true
},
relationMap: {
view: () => import("./type_widgets/relation_map/RelationMap"),
className: "note-detail-relation-map",
printable: true,
isFullHeight: true
},
noteMap: {
view: () => import("./type_widgets/NoteMap"),
className: "note-detail-note-map",
printable: true,
isFullHeight: true
},
aiChat: {
view: () => import("./type_widgets/AiChat"),
className: "ai-chat-widget-container",
isFullHeight: true
}
};

View File

@@ -52,6 +52,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
const note = this.noteContext?.note;
if (!note) {
this.$widget.addClass("bgfx empty-note");
return;
}
@@ -61,7 +62,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
this.$widget.addClass(utils.getNoteTypeClass(note.type));
this.$widget.addClass(utils.getMimeTypeClass(note.mime));
this.$widget.toggleClass(["bgfx", "options"], note.isOptions());
this.$widget.toggleClass("protected", note.isProtected);
const noteLanguage = note?.getLabelValue("language");
@@ -70,7 +71,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
}
#isFullWidthNote(note: FNote) {
if (["image", "mermaid", "book", "render", "canvas", "webView", "mindMap"].includes(note.type)) {
if (["code", "image", "mermaid", "book", "render", "canvas", "webView", "mindMap"].includes(note.type)) {
return true;
}

View File

@@ -12,102 +12,6 @@ import type { Attribute } from "../services/attribute_parser.js";
import type FAttribute from "../entities/fattribute.js";
import type { EventData } from "../components/app_context.js";
const TPL = /*html*/`
<div class="promoted-attributes-widget">
<style>
body.mobile .promoted-attributes-widget {
/* https://github.com/zadam/trilium/issues/4468 */
flex-shrink: 0.4;
overflow: auto;
}
.promoted-attributes-container {
margin: 0 1.5em;
overflow: auto;
max-height: 400px;
flex-wrap: wrap;
display: table;
}
.promoted-attribute-cell {
display: flex;
align-items: center;
margin: 10px;
display: table-row;
}
.promoted-attribute-cell > label {
user-select: none;
font-weight: bold;
vertical-align: middle;
}
.promoted-attribute-cell > * {
display: table-cell;
padding: 1px 0;
}
.promoted-attribute-cell div.input-group {
margin-inline-start: 10px;
display: flex;
min-height: 40px;
}
.promoted-attribute-cell strong {
word-break:keep-all;
white-space: nowrap;
}
.promoted-attribute-cell input[type="checkbox"] {
width: 22px !important;
flex-grow: 0;
width: unset;
}
/* Restore default apperance */
.promoted-attribute-cell input[type="number"],
.promoted-attribute-cell input[type="checkbox"] {
appearance: auto;
}
.promoted-attribute-cell input[type="color"] {
width: 24px;
height: 24px;
margin-top: 2px;
appearance: none;
padding: 0;
border: 0;
outline: none;
border-radius: 25% !important;
}
.promoted-attribute-cell input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
.promoted-attribute-cell input[type="color"]::-webkit-color-swatch {
border: none;
border-radius: 25%;
}
.promoted-attribute-label-color input[type="hidden"][value=""] + input[type="color"] {
position: relative;
opacity: 0.5;
}
.promoted-attribute-label-color input[type="hidden"][value=""] + input[type="color"]:after {
content: "";
position: absolute;
top: 10px;
inset-inline-start: 0px;
inset-inline-end: 0;
height: 2px;
background: rgba(0, 0, 0, 0.5);
transform: rotate(45deg);
pointer-events: none;
}
</style>
<div class="promoted-attributes-container"></div>
</div>`;
// TODO: Deduplicate
interface AttributeResult {
attributeId: string;
@@ -117,115 +21,17 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
private $container!: JQuery<HTMLElement>;
get name() {
return "promotedAttributes";
}
get toggleCommand() {
return "toggleRibbonTabPromotedAttributes";
}
doRender() {
this.$widget = $(TPL);
this.$widget = $("");
this.contentSized();
this.$container = this.$widget.find(".promoted-attributes-container");
}
getTitle(note: FNote) {
const promotedDefAttrs = note.getPromotedDefinitionAttributes();
if (promotedDefAttrs.length === 0) {
return { show: false };
}
return {
show: true,
activate: options.is("promotedAttributesOpenInRibbon"),
title: t("promoted_attributes.promoted_attributes"),
icon: "bx bx-table"
};
}
async refreshWithNote(note: FNote) {
this.$container.empty();
const promotedDefAttrs = note.getPromotedDefinitionAttributes();
const ownedAttributes = note.getOwnedAttributes();
// attrs are not resorted if position changes after the initial load
// promoted attrs are sorted primarily by order of definitions, but with multi-valued promoted attrs
// the order of attributes is important as well
ownedAttributes.sort((a, b) => a.position - b.position);
if (promotedDefAttrs.length === 0 || note.getLabelValue("viewType") === "table") {
this.toggleInt(false);
return;
}
const $cells: JQuery<HTMLElement>[] = [];
for (const definitionAttr of promotedDefAttrs) {
const valueType = definitionAttr.name.startsWith("label:") ? "label" : "relation";
const valueName = definitionAttr.name.substr(valueType.length + 1);
let valueAttrs = ownedAttributes.filter((el) => el.name === valueName && el.type === valueType) as Attribute[];
if (valueAttrs.length === 0) {
valueAttrs.push({
attributeId: "",
type: valueType,
name: valueName,
value: ""
});
}
if (definitionAttr.getDefinition().multiplicity === "single") {
valueAttrs = valueAttrs.slice(0, 1);
}
for (const valueAttr of valueAttrs) {
const $cell = await this.createPromotedAttributeCell(definitionAttr, valueAttr, valueName);
if ($cell) {
$cells.push($cell);
}
}
}
// we replace the whole content in one step, so there can't be any race conditions
// (previously we saw promoted attributes doubling)
this.$container.empty().append(...$cells);
this.toggleInt(true);
}
async createPromotedAttributeCell(definitionAttr: FAttribute, valueAttr: Attribute, valueName: string) {
const definition = definitionAttr.getDefinition();
const id = `value-${valueAttr.attributeId}`;
// .on("change", (event) => this.promotedAttributeChanged(event));
const $input = $("<input>")
.prop("tabindex", 200 + definitionAttr.position)
.prop("id", id)
.attr("data-attribute-id", valueAttr.noteId === this.noteId ? valueAttr.attributeId ?? "" : "") // if not owned, we'll force creation of a new attribute instead of updating the inherited one
.attr("data-attribute-type", valueAttr.type)
.attr("data-attribute-name", valueAttr.name)
.prop("value", valueAttr.value)
.prop("placeholder", t("promoted_attributes.unset-field-placeholder"))
.addClass("form-control")
.addClass("promoted-attribute-input")
.on("change", (event) => this.promotedAttributeChanged(event));
const $actionCell = $("<div>");
const $multiplicityCell = $("<td>").addClass("multiplicity").attr("nowrap", "true");
const $wrapper = $('<div class="promoted-attribute-cell">')
.append(
$("<label>")
.prop("for", id)
.text(definition.promotedAlias ?? valueName)
)
.append($("<div>").addClass("input-group").append($input))
.append($actionCell)
.append($multiplicityCell);
if (valueAttr.type === "label") {
$wrapper.addClass(`promoted-attribute-label-${definition.labelType}`);
if (definition.labelType === "text") {
@@ -359,8 +165,6 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
if (definition.multiplicity === "multi") {
const $addButton = $("<span>")
.addClass("bx bx-plus pointer tn-tool-button")
.prop("title", t("promoted_attributes.add_new_attribute"))
.on("click", async () => {
const $new = await this.createPromotedAttributeCell(
definitionAttr,

View File

@@ -3,12 +3,13 @@ import { ComponentChildren } from "preact";
interface AdmonitionProps {
type: "warning" | "note" | "caution";
children: ComponentChildren;
className?: string;
}
export default function Admonition({ type, children }: AdmonitionProps) {
export default function Admonition({ type, children, className }: AdmonitionProps) {
return (
<div className={`admonition ${type}`} role="alert">
<div className={`admonition ${type} ${className}`} role="alert">
{children}
</div>
)
}
}

View File

@@ -6,9 +6,9 @@ export interface CKEditorApi {
focus(): void;
/**
* Imperatively sets the text in the editor.
*
*
* Prefer setting `currentValue` prop where possible.
*
*
* @param text text to set in the editor
*/
setText(text: string): void;
@@ -27,15 +27,16 @@ interface CKEditorOpts {
onClick?: (e: MouseEvent, pos?: ModelPosition | null) => void;
onKeyDown?: (e: KeyboardEvent) => void;
onBlur?: () => void;
onInitialized?: (editorInstance: CKTextEditor) => void;
}
export default function CKEditor({ apiRef, currentValue, editor, config, disableNewlines, disableSpellcheck, onChange, onClick, ...restProps }: CKEditorOpts) {
const editorContainerRef = useRef<HTMLDivElement>(null);
export default function CKEditor({ apiRef, currentValue, editor, config, disableNewlines, disableSpellcheck, onChange, onClick, onInitialized, ...restProps }: CKEditorOpts) {
const editorContainerRef = useRef<HTMLDivElement>(null);
const textEditorRef = useRef<CKTextEditor>(null);
useImperativeHandle(apiRef, () => {
return {
focus() {
editorContainerRef.current?.focus();
textEditorRef.current?.editing.view.focus();
textEditorRef.current?.model.change((writer) => {
const documentRoot = textEditorRef.current?.editing.model.document.getRoot();
if (documentRoot) {
@@ -83,6 +84,8 @@ export default function CKEditor({ apiRef, currentValue, editor, config, disable
if (currentValue) {
textEditor.setData(currentValue);
}
onInitialized?.(textEditor);
});
}, []);
@@ -103,4 +106,4 @@ export default function CKEditor({ apiRef, currentValue, editor, config, disable
{...restProps}
/>
)
}
}

View File

@@ -82,6 +82,8 @@ interface FormListItemOpts {
active?: boolean;
badges?: FormListBadge[];
disabled?: boolean;
/** Will indicate the reason why the item is disabled via an icon, when hovered over it. */
disabledTooltip?: string;
checked?: boolean | null;
selected?: boolean;
container?: boolean;
@@ -119,21 +121,24 @@ export function FormListItem({ className, icon, value, title, active, disabled,
<Icon icon={icon} />&nbsp;
{description ? (
<div>
<FormListContent description={description} {...contentProps} />
<FormListContent description={description} disabled={disabled} {...contentProps} />
</div>
) : (
<FormListContent description={description} {...contentProps} />
<FormListContent description={description} disabled={disabled} {...contentProps} />
)}
</li>
);
}
function FormListContent({ children, badges, description }: Pick<FormListItemOpts, "children" | "badges" | "description">) {
function FormListContent({ children, badges, description, disabled, disabledTooltip }: Pick<FormListItemOpts, "children" | "badges" | "description" | "disabled" | "disabledTooltip">) {
return <>
{children}
{badges && badges.map(({ className, text }) => (
<span className={`badge ${className ?? ""}`}>{text}</span>
))}
{disabled && disabledTooltip && (
<span class="bx bx-info-circle disabled-tooltip" title={disabledTooltip} />
)}
{description && <div className="description">{description}</div>}
</>;
}

View File

@@ -5,17 +5,18 @@ import { openInAppHelpFromUrl } from "../../services/utils";
interface HelpButtonProps {
className?: string;
helpPage: string;
title?: string;
style?: CSSProperties;
}
export default function HelpButton({ className, helpPage, style }: HelpButtonProps) {
export default function HelpButton({ className, helpPage, title, style }: HelpButtonProps) {
return (
<button
class={`${className ?? ""} icon-action bx bx-help-circle`}
type="button"
onClick={() => openInAppHelpFromUrl(helpPage)}
title={t("open-help-page")}
title={title ?? t("open-help-page")}
style={style}
/>
);
}
}

View File

@@ -0,0 +1,23 @@
.info-bar {
--link-color: currentColor;
margin-top: 4px;
contain: unset !important;
padding: 8px 20px;
color: var(--read-only-note-info-bar-color);
font-size: .9em;
cursor: default;
user-select: none;
}
.info-bar-prominent {
background: var(--alert-bar-background, var(--accented-background-color));
}
.info-bar-subtle {
color: var(--muted-text-color);
background: var(--main-background-color);
border-bottom: 1px solid var(--main-border-color);
margin-block: 0;
padding-inline: 22px;
}

View File

@@ -0,0 +1,19 @@
import "./InfoBar.css";
import { ComponentChildren, CSSProperties } from "preact";
export type InfoBarParams = {
type: "prominent" | "subtle",
className: string;
style: CSSProperties
children: ComponentChildren;
};
export default function InfoBar(props: InfoBarParams) {
return <div className={`info-bar ${props.className} info-bar-${props.type}`} style={props.style}>
{props?.children}
</div>
}
InfoBar.defaultProps = {
type: "prominent"
} as InfoBarParams

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