Compare commits

..

247 Commits

Author SHA1 Message Date
Nriver
5907b7090e fix note title color for legacy themes 2025-12-09 10:49:28 +08:00
Elian Doran
6570a55e7e Translations update from Hosted Weblate (#7993) 2025-12-08 23:56:13 +02:00
noobhjy
f4838bb3b5 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.1% (115 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/zh_Hans/
2025-12-08 21:21:24 +00:00
pythaac
edb2a65196 Translated using Weblate (Korean)
Currently translated at 61.8% (94 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ko/
2025-12-08 21:21:23 +00:00
Hosted Weblate
fd721cac51 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-12-08 21:21:23 +00:00
pythaac
a1ff3cc8f7 Translated using Weblate (Korean)
Currently translated at 57.2% (87 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ko/
2025-12-08 21:21:21 +00:00
Adorian Doran
3fa6b264e5 style/note colors: include the original custom color 2025-12-08 23:18:14 +02:00
Elian Doran
a6682be251 fix(webview): layout issues when when no webviewSrc 2025-12-08 14:21:59 +02:00
Elian Doran
baee2cd6b2 chore(scripts): add proper typechecking 2025-12-08 14:13:18 +02:00
Elian Doran
3ee8eac635 chore(readme): order languages alphabetically 2025-12-08 14:13:17 +02:00
Elian Doran
ce5a775160 Translations update from Hosted Weblate (#7991) 2025-12-08 13:22:19 +02:00
Elian Doran
2cf66d1c53 Delete CNAME 2025-12-08 12:59:39 +02:00
Hosted Weblate
eaee67d742 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-12-08 11:59:30 +01:00
Elian Doran
c681496b1b chore(readme): fix links to old docs 2025-12-08 12:58:48 +02:00
Elian Doran
829d3e046d chore(readme): fix links to docs 2025-12-08 12:51:07 +02:00
Elian Doran
5c8df540db chore(readme): fix language switcher links (closes #7246) 2025-12-08 12:43:00 +02:00
Elian Doran
8920e6e448 chore(readme): disable translation for language switcher 2025-12-08 12:00:30 +02:00
Elian Doran
3cb860232e Translations update from Hosted Weblate (#7990) 2025-12-08 11:57:43 +02:00
Hosted Weblate
d588518ba1 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-12-08 10:55:07 +01:00
Elian Doran
d6f727d17a Translations update from Hosted Weblate (#7989) 2025-12-08 11:54:51 +02:00
Wen QI
82c40302bd Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1646 of 1646 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2025-12-08 10:53:50 +01:00
Andreas H.
c40c62a247 Translated using Weblate (German)
Currently translated at 99.2% (1634 of 1646 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2025-12-08 10:53:50 +01:00
Wen QI
8048b5ebca Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 99.4% (1637 of 1646 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2025-12-08 10:53:49 +01:00
green
03ba43df5d Translated using Weblate (Japanese)
Currently translated at 100.0% (1646 of 1646 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-12-08 10:53:49 +01:00
Elian Doran
a4b95b45ec chore(readme): rewrite links 2025-12-08 11:52:28 +02:00
Elian Doran
59513962fe chore(readme): add script to copy README into docs 2025-12-08 11:49:12 +02:00
Elian Doran
a201b43cde fix(launch_bar): sync status not correctly rendered 2025-12-08 11:30:33 +02:00
Elian Doran
f98c77bd16 Translations update from Hosted Weblate (#7988) 2025-12-08 09:44:13 +02:00
green
2596359b25 Translated using Weblate (Japanese)
Currently translated at 100.0% (1646 of 1646 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-12-08 08:30:46 +01:00
minkipark
e0a0263607 Translated using Weblate (Korean)
Currently translated at 3.0% (50 of 1646 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ko/
2025-12-08 08:30:45 +01:00
green
2d3feedb07 Translated using Weblate (Japanese)
Currently translated at 100.0% (152 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ja/
2025-12-08 08:30:45 +01:00
Hosted Weblate
af392fad3d 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-12-08 08:30:44 +01:00
Elian Doran
ef74490c44 chore(deps): update dependency jsonc-eslint-parser to v2.4.2 (#7982) 2025-12-08 09:30:31 +02:00
Elian Doran
b6856e18a8 fix(deps): update dependency katex to v0.16.27 (#7983) 2025-12-08 09:30:08 +02:00
renovate[bot]
de1d4424d9 fix(deps): update dependency katex to v0.16.27 2025-12-08 01:03:42 +00:00
renovate[bot]
5ec45bb575 chore(deps): update dependency jsonc-eslint-parser to v2.4.2 2025-12-08 01:02:52 +00:00
Elian Doran
cb3aced2ed docs(script): clarify originEntity and activeContext 2025-12-07 23:58:02 +02:00
Elian Doran
2f13a1ad21 fix(mind-map): show text in links between nodes on export (#7938) 2025-12-07 23:20:18 +02:00
Elian Doran
045127adee chore(vscode): add errorlens to recommendations 2025-12-07 23:11:07 +02:00
Elian Doran
db1a0c0362 i18n(client): update renamed key 2025-12-07 23:07:30 +02:00
Elian Doran
dcaf91a878 CKEditor stability improvements (#7979) 2025-12-07 22:40:52 +02:00
Elian Doran
a9209f5103 chore(ckeditor/watchdog): accidental comment 2025-12-07 22:32:06 +02:00
Elian Doran
ea613986c2 chore(server): add OpenAPI spec for to-markdown 2025-12-07 22:26:21 +02:00
Elian Doran
1ed46bd47c refactor(server): add validation for HTML/Markdown rendering 2025-12-07 22:24:14 +02:00
Elian Doran
f1ee79e75a refactor(client): inconsistent prefix for messages 2025-12-07 22:23:19 +02:00
Elian Doran
cd27160905 chore(ckeditor/watchdog): fix typecheck issues 2025-12-07 22:09:30 +02:00
Elian Doran
9ddf4a1308 feat(ckeditor/watchdog): ignore model out of bounds (closes #7739) 2025-12-07 22:00:10 +02:00
Elian Doran
a1c5ed9eb5 feat(ckeditor/watchdog): ignore parent check (closes #5776) 2025-12-07 21:59:52 +02:00
Elian Doran
7a4f19eada chore(ckeditor): revert breaking change 2025-12-07 21:34:10 +02:00
Elian Doran
397fb785d6 feat(ckeditor/watchdog): functional copy to clipboard button 2025-12-07 21:21:55 +02:00
Elian Doran
75a1fcc933 chore(dialog): fix mandatory parameter 2025-12-07 21:06:56 +02:00
Elian Doran
292cbf1383 feat(ckeditor/watchdog): add a title to the details screen 2025-12-07 21:03:39 +02:00
Elian Doran
37a14fefb3 feat(ckeditor/watchdog): improve layout of info dialog 2025-12-07 21:01:56 +02:00
Elian Doran
f424633d8c feat(ckeditor/watchdog): improve dialog size 2025-12-07 20:59:53 +02:00
Elian Doran
048258d2d1 feat(toast): improve button layout 2025-12-07 20:59:41 +02:00
Elian Doran
f779108b6c feat(ckeditor/watchdog): add a more details button 2025-12-07 20:40:29 +02:00
Elian Doran
522f3ae0a1 feat(toast): support buttons 2025-12-07 20:39:27 +02:00
Elian Doran
3fc7067c59 feat(dialog): support React nodes in info dialog 2025-12-07 20:39:11 +02:00
Elian Doran
c600e8ef89 feat(ckeditor/watchdog): add a toast to indicate the error 2025-12-07 20:16:34 +02:00
Elian Doran
5ad267fe1b chore(ckeditor): try to disable watchdog timer 2025-12-07 20:09:56 +02:00
Elian Doran
d8b3e438f8 chore(ckeditor): solve a few eslint warnings 2025-12-07 19:46:06 +02:00
Elian Doran
2834af66e9 feat(ckeditor/watchdog): restore focus after crash 2025-12-07 19:36:46 +02:00
Elian Doran
1bbf86fbeb feat(ckeditor/watchdog): use data stored in the spaced update 2025-12-07 19:30:35 +02:00
Elian Doran
f662b95dc9 chore(vscode): remove now unnecessary extension recommendations 2025-12-07 19:03:05 +02:00
Elian Doran
9ad4b725ac chore(eslint): report missing semicolons 2025-12-07 00:42:25 +02:00
Elian Doran
0182c61aec chore(vscode): mark all eslint rules as warnings 2025-12-07 00:42:00 +02:00
Elian Doran
78362535c7 chore(eslint): downgrade import sorting to warning 2025-12-07 00:36:39 +02:00
Elian Doran
ae7b31f343 chore(dx): trim trailing whitespace in CSS 2025-12-07 00:35:28 +02:00
Elian Doran
f16441bba4 Port toast to React (#7975) 2025-12-07 00:34:25 +02:00
Elian Doran
d3f9bb6def chore(toast): address requested changes 2025-12-07 00:33:37 +02:00
Elian Doran
e02440aa59 fix(toast): persistent toasts no longer working 2025-12-07 00:20:56 +02:00
Elian Doran
f1d87c29d3 chore(toast): fix typecheck issues 2025-12-07 00:15:00 +02:00
Elian Doran
21335b1b00 refactor(toast): get rid of autohide 2025-12-07 00:07:43 +02:00
Elian Doran
7463570e76 refactor(toast): rename delay to timeout 2025-12-07 00:06:42 +02:00
Elian Doran
da17a63ef5 refactor(toast): get rid of closeAfter in favor of delay 2025-12-07 00:04:12 +02:00
Elian Doran
eb8f2021cb fix(toast): add fallback if icon doesn't start with bx- 2025-12-06 23:49:56 +02:00
Elian Doran
888ff33be1 fix(toast): missing message icon 2025-12-06 23:46:21 +02:00
Elian Doran
b46850e86e refactor(toast): relocate styles into own file 2025-12-06 23:38:40 +02:00
Elian Doran
f053587f09 chore(toast): port toast to react 2025-12-06 23:37:56 +02:00
Elian Doran
7a3092a23b fix(read-only-bar): button shrinks when window is small 2025-12-06 23:00:57 +02:00
Elian Doran
d95450ae07 feat(client): detect communication errors with Traefik 2025-12-06 22:57:13 +02:00
Elian Doran
230def10fe feat(client): improve error message for HTTP errors 2025-12-06 22:12:06 +02:00
Elian Doran
036f8e49a4 docs(server/proxy): breaking change in Traefik 3.6.4 2025-12-06 21:51:56 +02:00
Elian Doran
4eca8a5640 feat(dx): add a way to run traefik reverse proxy 2025-12-06 21:37:24 +02:00
Elian Doran
08f96a91f3 docs(readme): fix a space 2025-12-06 19:37:25 +02:00
Elian Doran
2e915eccd6 Clean up _regroup (#7970) 2025-12-06 19:27:15 +02:00
Elian Doran
c05c58c82b Merge remote-tracking branch 'origin/main' into chore/cleanup_regroup 2025-12-06 19:18:23 +02:00
Elian Doran
572feed918 chore(regroup): address requested changes 2025-12-06 19:17:56 +02:00
Elian Doran
d30d207ab5 test(server): solve some local timeouts 2025-12-06 19:08:19 +02:00
Elian Doran
2a6b91dd04 chore(regroup): reintroduce code coverage 2025-12-06 19:08:03 +02:00
Elian Doran
3dee1725b3 chore(regroup): reintroduce old e2e tests 2025-12-06 18:54:38 +02:00
Elian Doran
f7ac465e67 chore(eslint): integrate preact config 2025-12-06 15:31:38 +02:00
Elian Doran
912f14549c chore(eslint): integrate playwright e2e 2025-12-06 15:20:05 +02:00
Elian Doran
21079335e7 chore(eslint): remove unnecessary files 2025-12-06 14:58:38 +02:00
Elian Doran
85741240f1 chore(eslint): reintegrate simple import sort 2025-12-06 14:50:41 +02:00
Elian Doran
3df9a87b29 chore(eslint): clean up unnecessary dependency in client 2025-12-06 14:48:00 +02:00
Elian Doran
acd60007ac chore(eslint): integrate formatter config 2025-12-06 14:43:08 +02:00
Elian Doran
f66c9630e3 chore(deps): update dependency vite to v7.2.6 (#7951) 2025-12-06 12:25:52 +02:00
Elian Doran
50a5892a31 chore(deps): update typescript-eslint monorepo to v8.48.1 (#7953) 2025-12-06 12:25:14 +02:00
renovate[bot]
c5f58437b1 chore(deps): update typescript-eslint monorepo to v8.48.1 2025-12-06 10:10:32 +00:00
Elian Doran
a915424d9a chore(deps): update softprops/action-gh-release action to v2.5.0 (#7958) 2025-12-06 12:10:09 +02:00
renovate[bot]
be7699c600 chore(deps): update dependency vite to v7.2.6 2025-12-06 10:09:14 +00:00
Elian Doran
a7c946ddae fix(deps): update codemirror themes (#7955) 2025-12-06 12:08:43 +02:00
Elian Doran
4b741a9434 chore(deps): update dependency openai to v6.10.0 (#7957) 2025-12-06 12:07:52 +02:00
Elian Doran
cd963272d4 chore(deps): update vitest monorepo to v4.0.15 (#7954) 2025-12-06 12:07:11 +02:00
Elian Doran
9de17ead91 Translations update from Hosted Weblate (#7966) 2025-12-06 12:05:21 +02:00
pythaac
d98b133d63 Translated using Weblate (Korean)
Currently translated at 52.6% (80 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ko/
2025-12-06 11:04:27 +01:00
Elian Doran
361e8e0066 chore(deps): update dependency electron to v39 (#7536) 2025-12-06 12:04:20 +02:00
Elian Doran
523c44b796 fix(deps): update dependency mermaid to v11.12.2 (#7956) 2025-12-06 11:53:14 +02:00
Elian Doran
4bd60ed6a1 chore(deps): update react monorepo to v19.2.1 (#7952) 2025-12-06 11:52:11 +02:00
Elian Doran
7d57f08baf chore(deps): update dependency express to v5.2.1 (#7950) 2025-12-06 11:35:10 +02:00
Elian Doran
5e7f54dbc3 chore(deps): update dependency esbuild to v0.27.1 (#7949) 2025-12-06 11:34:36 +02:00
Elian Doran
dfb9ce990d chore(eslint): basic config 2025-12-06 11:05:34 +02:00
Elian Doran
e87a368e87 chore(regroup): remove some redundant files 2025-12-06 10:52:41 +02:00
Elian Doran
44506057fd chore(regroup): clean bin dir 2025-12-06 10:50:42 +02:00
Elian Doran
ff08eadb23 fix(server/scripts): generate-document failing (closes #5422) 2025-12-06 10:49:14 +02:00
Elian Doran
ebb1a3feb2 chore(regroup): integrate generate_document 2025-12-06 10:35:57 +02:00
Elian Doran
81bc85b8e4 chore(regroup): adapt export-schema script 2025-12-06 10:29:08 +02:00
Elian Doran
4a749f52e9 chore(regroup): remove old release scripts 2025-12-06 10:11:28 +02:00
Elian Doran
25e37ddd78 chore(regroup): relocate icon scripts 2025-12-06 10:07:23 +02:00
renovate[bot]
46a9cfcc67 chore(deps): update react monorepo to v19.2.1 2025-12-06 08:00:01 +00:00
Elian Doran
66ff944660 chore(deps): update dependency @types/reveal.js to v5.2.2 (#7948) 2025-12-06 09:58:15 +02:00
Elian Doran
988db59197 chore(deps): update dependency @smithy/middleware-retry to v4.4.14 (#7947) 2025-12-06 09:57:54 +02:00
Elian Doran
34e321407a chore(deps): update dependency @redocly/cli to v2.12.3 (#7946) 2025-12-06 09:57:31 +02:00
Elian Doran
44c0028a51 chore(regroup): clean up translation scripts 2025-12-06 09:43:54 +02:00
Elian Doran
264b75cd68 chore(regroup): clean up migration scripts 2025-12-06 09:43:02 +02:00
renovate[bot]
cf9ccdcab6 chore(deps): update dependency electron to v39 2025-12-06 07:36:35 +00:00
renovate[bot]
48e18e533c chore(deps): update dependency openai to v6.10.0 2025-12-06 07:35:53 +00:00
renovate[bot]
12ef778bf4 fix(deps): update dependency mermaid to v11.12.2 2025-12-06 07:35:15 +00:00
Elian Doran
6c31a1788a Port launch bar to React (#7934) 2025-12-06 09:35:06 +02:00
renovate[bot]
aa7c6da8ef fix(deps): update codemirror themes 2025-12-06 07:34:30 +00:00
renovate[bot]
1ff40cace0 chore(deps): update vitest monorepo to v4.0.15 2025-12-06 07:33:52 +00:00
renovate[bot]
0a1dadbea1 chore(deps): update dependency express to v5.2.1 2025-12-06 07:30:48 +00:00
renovate[bot]
24edbdba5e chore(deps): update dependency esbuild to v0.27.1 2025-12-06 07:30:09 +00:00
renovate[bot]
db504eff88 chore(deps): update dependency @types/reveal.js to v5.2.2 2025-12-06 07:29:26 +00:00
renovate[bot]
5e09925659 chore(deps): update dependency @smithy/middleware-retry to v4.4.14 2025-12-06 07:28:42 +00:00
renovate[bot]
d1b2b351c8 chore(deps): update dependency @redocly/cli to v2.12.3 2025-12-06 07:28:02 +00:00
Elian Doran
50b0dc178e chore(regroup): clean up old ETAPI tests
They are already integrated in apps/server/spec/etapi.
2025-12-06 09:27:56 +02:00
Elian Doran
b34118e395 fix(deps): update ckeditor monorepo to v47.3.0 (#7959) 2025-12-06 09:26:03 +02:00
Elian Doran
e6436f9021 fix(text): code block background not correct 2025-12-06 09:16:20 +02:00
Elian Doran
831d1b4f3a chore(text): remove workaround for superscript icon 2025-12-06 09:15:40 +02:00
renovate[bot]
0456d1ca29 fix(deps): update ckeditor monorepo to v47.3.0 2025-12-06 07:04:27 +00:00
Elian Doran
fcd151022e fix(deps): update dependency i18next to v25.7.1 (#7960) 2025-12-06 08:59:19 +02:00
Elian Doran
0c1998002e fix(deps): update dependency preact to v10.28.0 (#7961) 2025-12-06 08:58:33 +02:00
Elian Doran
b8c33ce7fa e2e(server): locator failing due to removal of "visible" 2025-12-06 08:54:35 +02:00
Elian Doran
454cd633e8 fix(read_only_text): missing padding in <pre> without <code> 2025-12-06 08:46:44 +02:00
renovate[bot]
53cb9a6e10 fix(deps): update dependency preact to v10.28.0 2025-12-06 06:43:06 +00:00
renovate[bot]
8d6ff763d6 fix(deps): update dependency i18next to v25.7.1 2025-12-06 06:42:29 +00:00
Elian Doran
ecf15af3a0 fix(deps): update dependency react-i18next to v16.4.0 (#7962) 2025-12-06 08:38:20 +02:00
Elian Doran
716823789d chore(deps): update apple-actions/import-codesign-certs action to v6 (#7963) 2025-12-06 08:37:58 +02:00
Elian Doran
3c192badce chore(deps): update dependency @anthropic-ai/sdk to v0.71.2 (#7945) 2025-12-06 08:37:04 +02:00
renovate[bot]
d94b611d10 chore(deps): update apple-actions/import-codesign-certs action to v6 2025-12-06 02:54:20 +00:00
renovate[bot]
e2ce329b6c fix(deps): update dependency react-i18next to v16.4.0 2025-12-06 02:54:16 +00:00
renovate[bot]
ef902fc706 chore(deps): update softprops/action-gh-release action to v2.5.0 2025-12-06 02:51:28 +00:00
renovate[bot]
a7fc94c303 chore(deps): update dependency @anthropic-ai/sdk to v0.71.2 2025-12-06 02:42:34 +00:00
Elian Doran
c8c6d1bb1e docs(user): document launch bar widgets 2025-12-05 23:24:18 +02:00
Elian Doran
a205108681 chore(react/launch_bar): address requested changes 2025-12-05 22:57:07 +02:00
Elian Doran
31561879b3 chore(launch_bar): address requested changes 2025-12-05 19:40:36 +02:00
Elian Doran
fdb6677153 chore(launch_bar): reintroduce keyboard shortcuts 2025-12-05 19:23:40 +02:00
Elian Doran
17241be4bc fix(launch_bar): note context not properly set for legacy widgets 2025-12-05 18:01:46 +02:00
Elian Doran
3bf6de9c76 fix(launch_bar): calendar month selector dropdown wrongly positioned 2025-12-05 17:36:04 +02:00
Elian Doran
a53322e7cb refactor(launch_bar): deduplicate launcher note props 2025-12-05 17:16:55 +02:00
Elian Doran
a107c126e4 fix(launch_bar): dynamic dropdown not repositioning 2025-12-05 17:11:53 +02:00
Elian Doran
3a8dcae53a fix(launch_bar): wrong position of dropdown 2025-12-05 16:45:38 +02:00
Elian Doran
b99d4532df fix(react): duplicate tooltips on focus vs hover 2025-12-05 16:39:55 +02:00
Elian Doran
6e8f8ea357 fix(launch_bar): horizontal launch bar scrolling with dropdown open 2025-12-05 15:53:17 +02:00
Elian Doran
83838bbe76 fix(launch_bar): calendar month selector not actually working 2025-12-05 15:50:25 +02:00
Elian Doran
66620aabe2 fix(launch_bar): calendar dropdown hiding on month selection 2025-12-05 15:43:44 +02:00
Elian Doran
74fcf8270d chore(launch_bar): improve positioning of the month dropdown 2025-12-05 15:31:14 +02:00
Elian Doran
91b4e32a38 fix(launch_bar): calendar month selector clipped 2025-12-05 15:29:25 +02:00
Elian Doran
3f0c114f24 fix(launch_bar): handle week number wrap-around for next year in calendar 2025-12-05 15:17:51 +02:00
Elian Doran
30fe6b93c4 chore(launch_bar): remove now redundant file 2025-12-05 15:10:48 +02:00
Elian Doran
ec99242314 fix(launch_bar): dropdown tooltip visible when hovering popup 2025-12-05 15:06:55 +02:00
Elian Doran
9149fb7a85 fix(launch_bar): tooltip error for dropdowns 2025-12-05 15:02:13 +02:00
Elian Doran
3c919d9a8a fix(launch_bar): global menu size 2025-12-05 14:52:57 +02:00
Elian Doran
22f9ce1e2e Merge remote-tracking branch 'origin/main' into react/launch_bar 2025-12-05 12:22:36 +02:00
Elian Doran
72b01cec70 Merge branch 'stable' 2025-12-05 12:22:14 +02:00
Elian Doran
57b8bc2645 feat(launch_bar): faster tooltips 2025-12-05 12:12:02 +02:00
Elian Doran
3ad4ca3943 fix(launch_bar): bootstrap tooltip for dropdown 2025-12-05 12:06:53 +02:00
Elian Doran
237ffeff52 fix(launch_bar): title position of action buttons 2025-12-05 12:04:26 +02:00
Elian Doran
fb491d9790 Fix (NoteMap): map overflows when switching type in ribbon (#7939) 2025-12-05 11:59:08 +02:00
Elian Doran
facd03b6ad feat(launch_bar): horizontal scroll using mouse wheel 2025-12-05 11:54:31 +02:00
Elian Doran
f5f38ca670 refactor(widgets): relocate AI chat button 2025-12-05 11:53:38 +02:00
Elian Doran
83e599f0e9 refactor(widgets): relocate note launcher & command button 2025-12-05 11:52:52 +02:00
Elian Doran
48cd06f37e feat(bundle): improve error handling message 2025-12-05 11:49:09 +02:00
Elian Doran
aac9d2d1c4 fix(toast): icon missing for error messages 2025-12-05 11:45:02 +02:00
Elian Doran
7e2e1c12b9 chore(widgets): fix error icon in toast & improve message reporting 2025-12-05 11:42:56 +02:00
Elian Doran
3410dd4eba chore(react/launch_bar): bring back reporting widget that failed 2025-12-05 11:40:50 +02:00
Elian Doran
d511085db3 chore(react/launch_bar): port launcher container & launcher 2025-12-05 11:31:52 +02:00
Elian Doran
caaa3583a7 chore(react/launch_bar): port sync status 2025-12-05 10:22:34 +02:00
Elian Doran
185e5691a4 chore(react/launch_bar): bring back week highlighting 2025-12-05 09:52:46 +02:00
Elian Doran
20f44cc64f chore(react/launch_bar): reimplement week notes 2025-12-05 09:36:21 +02:00
SiriusXT
07ef94afd9 Merge branch 'main' into fix/note_map 2025-12-05 15:21:30 +08:00
SiriusXT
2d33b8a958 fix(note_map): initialize map state when switching map type in ribbon 2025-12-05 15:21:06 +08:00
Elian Doran
d283f5dbb4 chore(react/launch_bar): hide dropdown when selecting date 2025-12-05 09:15:01 +02:00
Elian Doran
1af76c4d06 chore(react/launch_bar): clicking on calendar days 2025-12-05 09:11:50 +02:00
Elian Doran
07498c6bef chore(react/launch_bar): add link to existing days 2025-12-05 09:02:51 +02:00
Elian Doran
18f9ebbc4f chore(react/launch_bar): reintroduce day highlighting 2025-12-05 08:56:35 +02:00
SiriusXT
85b4f652f4 chore(note_map): improve the icon for expanding/collapsing the map 2025-12-05 11:21:44 +08:00
SiriusXT
eec6f7336c fix (note_map): map overflows when switching map type in ribbon 2025-12-05 11:21:33 +08:00
Lucas
f976dd8d30 Apply suggestions from code review
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-12-04 13:08:28 -08:00
Lucas
2d3aa3a96e Merge branch 'main' into main 2025-12-04 13:04:24 -08:00
Elian Doran
1195cbd772 Translations update from Hosted Weblate (#7937) 2025-12-04 22:59:00 +02:00
Hosted Weblate
cebfa674ef 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-12-04 21:56:39 +01:00
Elian Doran
3ed596496d Merge branch 'main' of github.com:TriliumNext/Trilium 2025-12-04 22:56:20 +02:00
Elian Doran
d99ef78348 fix(client): find and replace in floating toolbar 2025-12-04 22:56:15 +02:00
lzinga
2666c1e196 feat(snapdom): update screenshot generation options for SVG and PNG exports 2025-12-04 12:52:01 -08:00
Lucas
4b8c8888ee Merge branch 'TriliumNext:main' into main 2025-12-04 12:49:37 -08:00
lzinga
ce1fd64aa9 feat(export): enhance SVG and PNG export functionality with snapdom integration 2025-12-04 12:49:10 -08:00
Elian Doran
bd1479b14a Translations update from Hosted Weblate (#7935) 2025-12-04 22:32:47 +02:00
Elian Doran
baee9520d1 Apply suggestion from @gemini-code-assist[bot]
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-12-04 22:32:08 +02:00
green
adb30a526e Translated using Weblate (Japanese)
Currently translated at 100.0% (1636 of 1636 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-12-04 21:27:44 +01:00
noobhjy
60c40457fc Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1636 of 1636 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2025-12-04 21:27:43 +01:00
noobhjy
a6df457c9c Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1636 of 1636 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2025-12-04 21:27:42 +01:00
Francis C.
3fd8fb0308 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1636 of 1636 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2025-12-04 21:27:41 +01:00
Elian Doran
7d4a7d4ab6 feat(dev): option to crash CKEditor 2025-12-04 22:15:13 +02:00
Elian Doran
4571b95683 chore(react/launch_bar): fix style for month selector 2025-12-04 21:37:02 +02:00
Elian Doran
a65d2a1bba chore(react/launch_bar): reintroduce year selector 2025-12-04 21:24:35 +02:00
lzinga
5c9503732d fix(mind-map): show text in links between nodes on export 2025-12-04 11:08:44 -08:00
Elian Doran
2dbbf7f350 chore(react/launch_bar): reintroduce month dropdown 2025-12-04 20:13:46 +02:00
Elian Doran
e1cce220b3 chore(react/launch_bar): add back icons for previous/next month 2025-12-04 19:56:44 +02:00
Elian Doran
e0aed26f63 refactor(react/launch_bar): extract calendar impl into single file 2025-12-04 19:46:25 +02:00
Elian Doran
62fd07258e chore(react/launch_bar): get days of the week to render 2025-12-04 19:43:47 +02:00
Elian Doran
0d8127140f chore(react/launch_bar): get week numbers to render 2025-12-04 19:38:56 +02:00
Elian Doran
bab5326d7c refactor(react/launch_bar): use different mechanism for gathering calendar info 2025-12-04 19:22:28 +02:00
Elian Doran
5c8445f3fe chore(react/launch_bar): get calendar to render days 2025-12-04 19:03:28 +02:00
Elian Doran
604488b166 chore(react/launch_bar): port custom widget 2025-12-04 17:55:38 +02:00
Elian Doran
b7d7fc8b67 chore(react/launch_bar): remove an old file 2025-12-04 17:17:41 +02:00
Elian Doran
1f4872f72b chore(react/launch_bar): port script launcher 2025-12-04 17:16:47 +02:00
Elian Doran
f89c40cde6 fix(react/launch_bar): type error in bookmarks 2025-12-04 17:07:13 +02:00
Elian Doran
670edbc22a chore(react/launch_bar): port quick search launcher 2025-12-04 17:01:35 +02:00
Elian Doran
54f70c8158 chore(react/launch_bar): port today launcher 2025-12-04 16:53:05 +02:00
Elian Doran
0d6bcba023 chore(react/launch_bar): port note launcher 2025-12-04 16:33:58 +02:00
Elian Doran
06a9f95979 chore(react/launch_bar): remove old file 2025-12-04 16:23:14 +02:00
Elian Doran
03cdfc259e refactor(react/launch_bar): deduplicate launcher icon and title 2025-12-04 16:22:46 +02:00
Elian Doran
1963b5732a refactor(react/launch_bar): extract note launcher from bookmark button 2025-12-04 16:13:16 +02:00
Elian Doran
1e05dc937c refactor(react/launch_bar): port command buttons 2025-12-04 16:04:14 +02:00
Elian Doran
c3b22ff737 refactor(react/launch_bar): port protected session status widget 2025-12-04 15:54:27 +02:00
Elian Doran
991f07e148 refactor(react/launch_bar): port ai chat button 2025-12-04 15:47:42 +02:00
Elian Doran
8efb849391 refactor(react/launch_bar): port history navigation 2025-12-04 15:44:10 +02:00
Elian Doran
5b310f3e46 refactor(react/launch_bar): port spacer 2025-12-04 15:02:46 +02:00
Elian Doran
5f54e42a43 refactor(react/launch_bar): extract icon class 2025-12-04 14:47:30 +02:00
Elian Doran
a83f20e454 refactor(react/launch_bar): extract dropdown & button style 2025-12-04 14:46:23 +02:00
Elian Doran
cdab86bd83 chore(react/launch_bar): port bookmark_folder 2025-12-04 14:41:07 +02:00
Elian Doran
48cbb80e79 chore(react/launch_bar): port open_note_button_widget 2025-12-04 14:18:04 +02:00
Elian Doran
1af6200655 chore(react/launch_bar): get launch bar to render React widgets 2025-12-04 13:24:47 +02:00
Elian Doran
b8585594cd fix(text): duplicate search dialogs (closes #5735) 2025-12-04 13:03:08 +02:00
248 changed files with 10118 additions and 7332 deletions

View File

@@ -1,6 +1,6 @@
root = true
[*.{js,ts,tsx}]
[*.{js,ts,tsx,css}]
charset = utf-8
end_of_line = lf
indent_size = 4

View File

@@ -21,7 +21,7 @@ runs:
# Certificate setup
- name: Import Apple certificates
if: inputs.os == 'macos'
uses: apple-actions/import-codesign-certs@v5
uses: apple-actions/import-codesign-certs@v6
with:
p12-file-base64: ${{ env.APPLE_APP_CERTIFICATE_BASE64 }}
p12-password: ${{ env.APPLE_APP_CERTIFICATE_PASSWORD }}
@@ -30,7 +30,7 @@ runs:
- name: Install Installer certificate
if: inputs.os == 'macos'
uses: apple-actions/import-codesign-certs@v5
uses: apple-actions/import-codesign-certs@v6
with:
p12-file-base64: ${{ env.APPLE_INSTALLER_CERTIFICATE_BASE64 }}
p12-password: ${{ env.APPLE_INSTALLER_CERTIFICATE_PASSWORD }}

View File

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

View File

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

1
.gitignore vendored
View File

@@ -49,3 +49,4 @@ upload
# docs
site/
apps/*/coverage

View File

@@ -9,7 +9,6 @@
"tobermory.es6-string-html",
"vitest.explorer",
"yzhang.markdown-all-in-one",
"svelte.svelte-vscode",
"bradlc.vscode-tailwindcss"
"usernamehw.errorlens"
]
}

View File

@@ -36,5 +36,8 @@
"docs/**/*.png": true,
"apps/server/src/assets/doc_notes/**": true,
"apps/edit-docs/demo/**": true
}
},
"eslint.rules.customizations": [
{ "rule": "*", "severity": "warn" }
]
}

View File

@@ -16,13 +16,14 @@
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/triliumnext/trilium/total)
[![RelativeCI](https://badges.relative-ci.com/badges/Di5q7dz9daNDZ9UXi0Bp?branch=develop)](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) [![Translation status](https://hosted.weblate.org/widget/trilium/svg-badge.svg)](https://hosted.weblate.org/engage/trilium/)
[English](./README.md) | [Chinese (Simplified)](./docs/README-ZH_CN.md) | [Chinese (Traditional)](./docs/README-ZH_TW.md) | [Russian](./docs/README-ru.md) | [Japanese](./docs/README-ja.md) | [Italian](./docs/README-it.md) | [Spanish](./docs/README-es.md)
<!-- translate:off -->
<!-- LANGUAGE SWITCHER -->
[Chinese (Simplified Han script)](./docs/README-ZH_CN.md) | [Chinese (Traditional Han script)](./docs/README-ZH_TW.md) | [English](./docs/README.md) | [French](./docs/README-fr.md) | [German](./docs/README-de.md) | [Greek](./docs/README-el.md) | [Italian](./docs/README-it.md) | [Japanese](./docs/README-ja.md) | [Romanian](./docs/README-ro.md) | [Spanish](./docs/README-es.md)
<!-- translate:on -->
Trilium Notes is a free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases.
See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for quick overview:
<a href="https://triliumnext.github.io/Docs/Wiki/screenshot-tour"><img src="./docs/app.png" alt="Trilium Screenshot" width="1000"></a>
<img src="./docs/app.png" alt="Trilium Screenshot" width="1000">
## ⏬ Download
- [Latest release](https://github.com/TriliumNext/Trilium/releases/latest) stable version, recommended for most users.
@@ -39,39 +40,39 @@ Our documentation is available in multiple formats:
### Quick Links
- [Getting Started Guide](https://docs.triliumnotes.org/)
- [Installation Instructions](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation.md)
- [Docker Setup](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.md)
- [Upgrading TriliumNext](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Upgrading%20TriliumNext.md)
- [Basic Concepts and Features](./docs/User%20Guide/User%20Guide/Basic%20Concepts%20and%20Features/Notes.md)
- [Patterns of Personal Knowledge Base](https://triliumnext.github.io/Docs/Wiki/patterns-of-personal-knowledge)
- [Installation Instructions](https://docs.triliumnotes.org/user-guide/setup)
- [Docker Setup](https://docs.triliumnotes.org/user-guide/setup/server/installation/docker)
- [Upgrading TriliumNext](https://docs.triliumnotes.org/user-guide/setup/upgrading)
- [Basic Concepts and Features](https://docs.triliumnotes.org/user-guide/concepts/notes)
- [Patterns of Personal Knowledge Base](https://docs.triliumnotes.org/user-guide/misc/patterns-of-personal-knowledge)
## 🎁 Features
* Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://triliumnext.github.io/Docs/Wiki/cloning-notes))
* Rich WYSIWYG note editor including e.g. tables, images and [math](https://triliumnext.github.io/Docs/Wiki/text-notes) with markdown [autoformat](https://triliumnext.github.io/Docs/Wiki/text-notes#autoformat)
* Support for editing [notes with source code](https://triliumnext.github.io/Docs/Wiki/code-notes), including syntax highlighting
* Fast and easy [navigation between notes](https://triliumnext.github.io/Docs/Wiki/note-navigation), full text search and [note hoisting](https://triliumnext.github.io/Docs/Wiki/note-hoisting)
* Seamless [note versioning](https://triliumnext.github.io/Docs/Wiki/note-revisions)
* Note [attributes](https://triliumnext.github.io/Docs/Wiki/attributes) can be used for note organization, querying and advanced [scripting](https://triliumnext.github.io/Docs/Wiki/scripts)
* Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://docs.triliumnotes.org/user-guide/concepts/notes/cloning))
* Rich WYSIWYG note editor including e.g. tables, images and [math](https://docs.triliumnotes.org/user-guide/note-types/text) with markdown [autoformat](https://docs.triliumnotes.org/user-guide/note-types/text/markdown-formatting)
* Support for editing [notes with source code](https://docs.triliumnotes.org/user-guide/note-types/code), including syntax highlighting
* Fast and easy [navigation between notes](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-navigation), full text search and [note hoisting](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-hoisting)
* Seamless [note versioning](https://docs.triliumnotes.org/user-guide/concepts/notes/note-revisions)
* Note [attributes](https://docs.triliumnotes.org/user-guide/advanced-usage/attributes) can be used for note organization, querying and advanced [scripting](https://docs.triliumnotes.org/user-guide/scripts)
* UI available in English, German, Spanish, French, Romanian, and Chinese (simplified and traditional)
* Direct [OpenID and TOTP integration](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation/Multi-Factor%20Authentication.md) for more secure login
* [Synchronization](https://triliumnext.github.io/Docs/Wiki/synchronization) with self-hosted sync server
* there's a [3rd party service for hosting synchronisation server](https://trilium.cc/paid-hosting)
* [Sharing](https://triliumnext.github.io/Docs/Wiki/sharing) (publishing) notes to public internet
* Strong [note encryption](https://triliumnext.github.io/Docs/Wiki/protected-notes) with per-note granularity
* Direct [OpenID and TOTP integration](https://docs.triliumnotes.org/user-guide/setup/server/mfa) for more secure login
* [Synchronization](https://docs.triliumnotes.org/user-guide/setup/synchronization) with self-hosted sync server
* there are [3rd party services for hosting synchronisation server](https://docs.triliumnotes.org/user-guide/setup/server/cloud-hosting)
* [Sharing](https://docs.triliumnotes.org/user-guide/advanced-usage/sharing) (publishing) notes to public internet
* Strong [note encryption](https://docs.triliumnotes.org/user-guide/concepts/notes/protected-notes) with per-note granularity
* Sketching diagrams, based on [Excalidraw](https://excalidraw.com/) (note type "canvas")
* [Relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map) and [link maps](https://triliumnext.github.io/Docs/Wiki/link-map) for visualizing notes and their relations
* [Relation maps](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and [note/link maps](https://docs.triliumnotes.org/user-guide/note-types/note-map) for visualizing notes and their relations
* Mind maps, based on [Mind Elixir](https://docs.mind-elixir.com/)
* [Geo maps](./docs/User%20Guide/User%20Guide/Note%20Types/Geo%20Map.md) with location pins and GPX tracks
* [Scripting](https://triliumnext.github.io/Docs/Wiki/scripts) - see [Advanced showcases](https://triliumnext.github.io/Docs/Wiki/advanced-showcases)
* [REST API](https://triliumnext.github.io/Docs/Wiki/etapi) for automation
* [Geo maps](https://docs.triliumnotes.org/user-guide/collections/geomap) with location pins and GPX tracks
* [Scripting](https://docs.triliumnotes.org/user-guide/scripts) - see [Advanced showcases](https://docs.triliumnotes.org/user-guide/advanced-usage/advanced-showcases)
* [REST API](https://docs.triliumnotes.org/user-guide/advanced-usage/etapi) for automation
* Scales well in both usability and performance upwards of 100 000 notes
* Touch optimized [mobile frontend](https://triliumnext.github.io/Docs/Wiki/mobile-frontend) for smartphones and tablets
* Built-in [dark theme](https://triliumnext.github.io/Docs/Wiki/themes), support for user themes
* [Evernote](https://triliumnext.github.io/Docs/Wiki/evernote-import) and [Markdown import & export](https://triliumnext.github.io/Docs/Wiki/markdown)
* [Web Clipper](https://triliumnext.github.io/Docs/Wiki/web-clipper) for easy saving of web content
* Touch optimized [mobile frontend](https://docs.triliumnotes.org/user-guide/setup/mobile-frontend) for smartphones and tablets
* Built-in [dark theme](https://docs.triliumnotes.org/user-guide/concepts/themes), support for user themes
* [Evernote](https://docs.triliumnotes.org/user-guide/concepts/import-export/evernote) and [Markdown import & export](https://docs.triliumnotes.org/user-guide/concepts/import-export/markdown)
* [Web Clipper](https://docs.triliumnotes.org/user-guide/setup/web-clipper) for easy saving of web content
* Customizable UI (sidebar buttons, user-defined widgets, ...)
* [Metrics](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics.md), along with a [Grafana Dashboard](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics/grafana-dashboard.json)
* [Metrics](https://docs.triliumnotes.org/user-guide/advanced-usage/metrics), along with a Grafana Dashboard.
✨ Check out the following third-party resources/communities for more TriliumNext related goodies:
@@ -131,7 +132,7 @@ Note: It is best to disable automatic updates on your server installation (see b
### Server
To install TriliumNext on your own server (including via Docker from [Dockerhub](https://hub.docker.com/r/triliumnext/trilium)) follow [the server installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation).
To install TriliumNext on your own server (including via Docker from [Dockerhub](https://hub.docker.com/r/triliumnext/trilium)) follow [the server installation docs](https://docs.triliumnotes.org/user-guide/setup/server).
## 💻 Contribute
@@ -198,7 +199,7 @@ Trilium would not be possible without the technologies behind it:
* [Leaflet](https://github.com/Leaflet/Leaflet) - for rendering geographical maps.
* [Tabulator](https://github.com/olifolkerd/tabulator) - for the interactive table used in collections.
* [FancyTree](https://github.com/mar10/fancytree) - feature-rich tree library without real competition.
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library. Used in [relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map.html) and [link maps](https://triliumnext.github.io/Docs/Wiki/note-map.html#link-map)
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library. Used in [relation maps](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and [link maps](https://docs.triliumnotes.org/user-guide/advanced-usage/note-map#link-map)
## 🤝 Support

View File

@@ -1,7 +0,0 @@
#!/usr/bin/env node
import anonymizationService from "../src/services/anonymization.js";
import fs from "fs";
import path from "path";
fs.writeFileSync(path.resolve(__dirname, "tpl", "anonymize-database.sql"), anonymizationService.getFullAnonymizationScript());

View File

@@ -1,7 +0,0 @@
#!/usr/bin/env bash
SCHEMA_FILE_PATH=db/schema.sql
sqlite3 ./data/document.db .schema | grep -v "sqlite_sequence" > "$SCHEMA_FILE_PATH"
echo "DB schema exported to $SCHEMA_FILE_PATH"

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env bash
if [[ $# -eq 0 ]] ; then
echo "Missing argument of new version"
exit 1
fi
VERSION=$1
SERIES=${VERSION:0:4}-latest
docker push zadam/trilium:$VERSION
docker push zadam/trilium:$SERIES
if [[ $1 != *"beta"* ]]; then
docker push zadam/trilium:latest
fi

View File

@@ -1,57 +0,0 @@
#!/usr/bin/env bash
if [[ $# -eq 0 ]] ; then
echo "Missing argument of new version"
exit 1
fi
VERSION=$1
if ! [[ ${VERSION} =~ ^[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}(-.+)?$ ]] ;
then
echo "Version ${VERSION} isn't in format X.Y.Z"
exit 1
fi
VERSION_DATE=$(git log -1 --format=%aI "v${VERSION}" | cut -c -10)
VERSION_COMMIT=$(git rev-list -n 1 "v${VERSION}")
# expecting the directory at a specific path
cd ~/trilium-flathub || exit
if ! git diff-index --quiet HEAD --; then
echo "There are uncommitted changes"
exit 1
fi
BASE_BRANCH=main
if [[ "$VERSION" == *"beta"* ]]; then
BASE_BRANCH=beta
fi
git switch "${BASE_BRANCH}"
git pull
BRANCH=b${VERSION}
git branch "${BRANCH}"
git switch "${BRANCH}"
echo "Updating files with version ${VERSION}, date ${VERSION_DATE} and commit ${VERSION_COMMIT}"
flatpak-node-generator npm ../trilium/package-lock.json
xmlstarlet ed --inplace --update "/component/releases/release/@version" --value "${VERSION}" --update "/component/releases/release/@date" --value "${VERSION_DATE}" ./com.github.zadam.trilium.metainfo.xml
yq --inplace "(.modules[0].sources[0].tag = \"v${VERSION}\") | (.modules[0].sources[0].commit = \"${VERSION_COMMIT}\")" ./com.github.zadam.trilium.yml
git add ./generated-sources.json
git add ./com.github.zadam.trilium.metainfo.xml
git add ./com.github.zadam.trilium.yml
git commit -m "release $VERSION"
git push --set-upstream origin "${BRANCH}"
gh pr create --fill -B "${BASE_BRANCH}"
gh pr merge --auto --merge --delete-branch

View File

@@ -1,49 +0,0 @@
#!/usr/bin/env bash
set -e
if [[ $# -eq 0 ]] ; then
echo "Missing argument of new version"
exit 1
fi
if ! command -v jq &> /dev/null; then
echo "Missing command: jq"
exit 1
fi
VERSION=$1
if ! [[ ${VERSION} =~ ^[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}(-.+)?$ ]] ;
then
echo "Version ${VERSION} isn't in format X.Y.Z"
exit 1
fi
if ! git diff-index --quiet HEAD --; then
echo "There are uncommitted changes"
exit 1
fi
echo "Releasing Trilium $VERSION"
jq '.version = "'$VERSION'"' package.json > package.json.tmp
mv package.json.tmp package.json
git add package.json
npm run chore:update-build-info
git add src/services/build.ts
TAG=v$VERSION
echo "Committing package.json version change"
git commit -m "chore(release): $VERSION"
git push
echo "Tagging commit with $TAG"
git tag $TAG
git push origin $TAG

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>

View File

@@ -1,51 +0,0 @@
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import simpleImportSort from "eslint-plugin-simple-import-sort";
export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.recommended,
// consider using rules below, once we have a full TS codebase and can be more strict
// tseslint.configs.strictTypeChecked,
// tseslint.configs.stylisticTypeChecked,
// tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname
}
}
},
{
plugins: {
"simple-import-sort": simpleImportSort
}
},
{
rules: {
// add rule overrides here
"no-undef": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_"
}
],
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error"
}
},
{
ignores: [
"build/*",
"dist/*",
"docs/*",
"demo/*",
"src/public/app-dist/*",
"src/public/app/doc_notes/*"
]
}
);

View File

@@ -1,47 +0,0 @@
import stylistic from "@stylistic/eslint-plugin";
import tsParser from "@typescript-eslint/parser";
// eslint config just for formatting rules
// potentially to be merged with the linting rules into one single config,
// once we have fixed the majority of lint errors
// Go to https://eslint.style/rules/default/${rule_without_prefix} to check the rule details
export const stylisticRules = {
"@stylistic/indent": [ "error", 4 ],
"@stylistic/quotes": [ "error", "double", { avoidEscape: true, allowTemplateLiterals: "always" } ],
"@stylistic/semi": [ "error", "always" ],
"@stylistic/quote-props": [ "error", "consistent-as-needed" ],
"@stylistic/max-len": [ "error", { code: 100 } ],
"@stylistic/comma-dangle": [ "error", "never" ],
"@stylistic/linebreak-style": [ "error", "unix" ],
"@stylistic/array-bracket-spacing": [ "error", "always" ],
"@stylistic/object-curly-spacing": [ "error", "always" ],
"@stylistic/padded-blocks": [ "error", { classes: "always" } ]
};
export default [
{
files: [ "**/*.{js,ts,mjs,cjs}" ],
languageOptions: {
parser: tsParser
},
plugins: {
"@stylistic": stylistic
},
rules: {
...stylisticRules
}
},
{
ignores: [
"build/*",
"dist/*",
"docs/*",
"demo/*",
// TriliumNextTODO: check if we want to format packages here as well - for now skipping it
"packages/*",
"src/public/app-dist/*",
"src/public/app/doc_notes/*"
]
}
];

View File

@@ -1,17 +0,0 @@
import { test as setup, expect } from "@playwright/test";
const authFile = "playwright/.auth/user.json";
const ROOT_URL = "http://localhost:8082";
const LOGIN_PASSWORD = "demo1234";
// Reference: https://playwright.dev/docs/auth#basic-shared-account-in-all-tests
setup("authenticate", async ({ page }) => {
await page.goto(ROOT_URL);
await expect(page).toHaveURL(`${ROOT_URL}/login`);
await page.getByRole("textbox", { name: "Password" }).fill(LOGIN_PASSWORD);
await page.getByRole("button", { name: "Login" }).click();
await page.context().storageState({ path: authFile });
});

View File

@@ -1,9 +0,0 @@
import { test, expect } from "@playwright/test";
test("Can duplicate note with broken links", async ({ page }) => {
await page.goto(`http://localhost:8082/#2VammGGdG6Ie`);
await page.locator(".tree-wrapper .fancytree-active").getByText("Note map").click({ button: "right" });
await page.getByText("Duplicate subtree").click();
await expect(page.locator(".toast-body")).toBeHidden();
await expect(page.locator(".tree-wrapper").getByText("Note map (dup)")).toBeVisible();
});

View File

@@ -1,18 +0,0 @@
import { test, expect } from "@playwright/test";
test("has title", async ({ page }) => {
await page.goto("https://playwright.dev/");
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/);
});
test("get started link", async ({ page }) => {
await page.goto("https://playwright.dev/");
// Click the get started link.
await page.getByRole("link", { name: "Get started" }).click();
// Expects page to have a heading with the name of Installation.
await expect(page.getByRole("heading", { name: "Installation" })).toBeVisible();
});

View File

@@ -1,21 +0,0 @@
import test, { expect } from "@playwright/test";
test("Native Title Bar not displayed on web", async ({ page }) => {
await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsAppearance");
await expect(page.getByRole("heading", { name: "Theme" })).toBeVisible();
await expect(page.getByRole("heading", { name: "Native Title Bar (requires" })).toBeHidden();
});
test("Tray settings not displayed on web", async ({ page }) => {
await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsOther");
await expect(page.getByRole("heading", { name: "Note Erasure Timeout" })).toBeVisible();
await expect(page.getByRole("heading", { name: "Tray" })).toBeHidden();
});
test("Spellcheck settings not displayed on web", async ({ page }) => {
await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsSpellcheck");
await expect(page.getByRole("heading", { name: "Spell Check" })).toBeVisible();
await expect(page.getByRole("heading", { name: "Tray" })).toBeHidden();
await expect(page.getByText("These options apply only for desktop builds")).toBeVisible();
await expect(page.getByText("Enable spellcheck")).toBeHidden();
});

View File

@@ -1,18 +0,0 @@
import test, { expect } from "@playwright/test";
test("Renders on desktop", async ({ page, context }) => {
await page.goto("http://localhost:8082");
await expect(page.locator(".tree")).toContainText("Trilium Integration Test");
});
test("Renders on mobile", async ({ page, context }) => {
await context.addCookies([
{
url: "http://localhost:8082",
name: "trilium-device",
value: "mobile"
}
]);
await page.goto("http://localhost:8082");
await expect(page.locator(".tree")).toContainText("Trilium Integration Test");
});

View File

@@ -1,12 +0,0 @@
import { test, expect } from "@playwright/test";
const expectedVersion = "0.90.3";
test("Displays update badge when there is a version available", async ({ page }) => {
await page.goto("http://localhost:8080");
await page.getByRole("button", { name: "" }).click();
await page.getByText(`Version ${expectedVersion} is available,`).click();
const page1 = await page.waitForEvent("popup");
expect(page1.url()).toBe(`https://github.com/TriliumNext/Trilium/releases/tag/v${expectedVersion}`);
});

View File

@@ -1,56 +0,0 @@
{
"main": "./electron-main.js",
"bin": {
"trilium": "src/main.js"
},
"type": "module",
"scripts": {
"server:start-safe": "cross-env TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev nodemon src/main.ts",
"server:start-no-dir": "cross-env TRILIUM_ENV=dev nodemon src/main.ts",
"server:start-test": "npm run server:switch && rimraf ./data-test && cross-env TRILIUM_DATA_DIR=./data-test TRILIUM_ENV=dev TRILIUM_PORT=9999 nodemon src/main.ts",
"server:qstart": "npm run server:switch && npm run server:start",
"server:switch": "rimraf ./node_modules/better-sqlite3 && npm install",
"electron:start-no-dir": "cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_ENV=dev TRILIUM_PORT=37742 electron --inspect=5858 .",
"electron:start-nix": "electron-rebuild --version 33.3.1 && cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./electron-main.ts --inspect=5858 .\"",
"electron:start-nix-no-dir": "electron-rebuild --version 33.3.1 && cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_ENV=dev TRILIUM_PORT=37742 nix-shell -p electron_33 --run \"electron ./electron-main.ts --inspect=5858 .\"",
"electron:start-prod-no-dir": "npm run build:prepare-dist && cross-env TRILIUM_ENV=prod electron --inspect=5858 .",
"electron:start-prod-nix": "electron-rebuild --version 33.3.1 && npm run build:prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
"electron:start-prod-nix-no-dir": "electron-rebuild --version 33.3.1 && npm run build:prepare-dist && cross-env TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
"electron:qstart": "npm run electron:switch && npm run electron:start",
"electron:switch": "electron-rebuild",
"docs:build": "typedoc",
"test": "npm run client:test && npm run server:test",
"client:test": "cross-env TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db TRILIUM_INTEGRATION_TEST=memory vitest --root src/public/app",
"client:coverage": "cross-env TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db TRILIUM_INTEGRATION_TEST=memory vitest --root src/public/app --coverage",
"test:playwright": "playwright test --workers 1",
"test:integration-edit-db": "cross-env TRILIUM_INTEGRATION_TEST=edit TRILIUM_PORT=8081 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
"test:integration-mem-db": "cross-env nodemon src/main.ts",
"test:integration-mem-db-dev": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
"dev:watch-dist": "tsx ./bin/watch-dist.ts",
"dev:format-check": "eslint -c eslint.format.config.js .",
"dev:format-fix": "eslint -c eslint.format.config.js . --fix",
"dev:linter-check": "eslint .",
"dev:linter-fix": "eslint . --fix",
"chore:generate-document": "cross-env nodemon ./bin/generate_document.ts 1000",
"chore:generate-openapi": "tsx bin/generate-openapi.js"
},
"devDependencies": {
"@playwright/test": "1.57.0",
"@stylistic/eslint-plugin": "5.6.1",
"@types/express": "5.0.5",
"@types/node": "24.10.1",
"@types/yargs": "17.0.35",
"@vitest/coverage-v8": "4.0.14",
"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": "5.0.2",
"rimraf": "6.1.2",
"tslib": "2.8.1"
},
"optionalDependencies": {
"appdmg": "0.6.6"
}
}

View File

@@ -1,9 +0,0 @@
import etapi from "../support/etapi.js";
/* TriliumNextTODO: port to Vitest
etapi.describeEtapi("app_info", () => {
it("get", async () => {
const appInfo = await etapi.getEtapi("app-info");
expect(appInfo.clipperProtocolVersion).toEqual("1.0");
});
});
*/

View File

@@ -1,10 +0,0 @@
import etapi from "../support/etapi.js";
/* TriliumNextTODO: port to Vitest
etapi.describeEtapi("backup", () => {
it("create", async () => {
const response = await etapi.putEtapiContent("backup/etapi_test");
expect(response.status).toEqual(204);
});
});
*/

View File

@@ -1,26 +0,0 @@
import etapi from "../support/etapi.js";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
/* TriliumNextTODO: port to Vitest
etapi.describeEtapi("import", () => {
// temporarily skip this test since test-export.zip is missing
xit("import", async () => {
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const zipFileBuffer = fs.readFileSync(path.resolve(scriptDir, "test-export.zip"));
const response = await etapi.postEtapiContent("notes/root/import", zipFileBuffer);
expect(response.status).toEqual(201);
const { note, branch } = await response.json();
expect(note.title).toEqual("test-export");
expect(branch.parentNoteId).toEqual("root");
const content = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).text();
expect(content).toContain("test export content");
});
});
*/

View File

@@ -1,103 +0,0 @@
import crypto from "crypto";
import etapi from "../support/etapi.js";
/* TriliumNextTODO: port to Vitest
etapi.describeEtapi("notes", () => {
it("create", async () => {
const { note, branch } = await etapi.postEtapi("create-note", {
parentNoteId: "root",
type: "text",
title: "Hello World!",
content: "Content",
prefix: "Custom prefix"
});
expect(note.title).toEqual("Hello World!");
expect(branch.parentNoteId).toEqual("root");
expect(branch.prefix).toEqual("Custom prefix");
const rNote = await etapi.getEtapi(`notes/${note.noteId}`);
expect(rNote.title).toEqual("Hello World!");
const rContent = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).text();
expect(rContent).toEqual("Content");
const rBranch = await etapi.getEtapi(`branches/${branch.branchId}`);
expect(rBranch.parentNoteId).toEqual("root");
expect(rBranch.prefix).toEqual("Custom prefix");
});
it("patch", async () => {
const { note } = await etapi.postEtapi("create-note", {
parentNoteId: "root",
type: "text",
title: "Hello World!",
content: "Content"
});
await etapi.patchEtapi(`notes/${note.noteId}`, {
title: "new title",
type: "code",
mime: "text/apl",
dateCreated: "2000-01-01 12:34:56.999+0200",
utcDateCreated: "2000-01-01 10:34:56.999Z"
});
const rNote = await etapi.getEtapi(`notes/${note.noteId}`);
expect(rNote.title).toEqual("new title");
expect(rNote.type).toEqual("code");
expect(rNote.mime).toEqual("text/apl");
expect(rNote.dateCreated).toEqual("2000-01-01 12:34:56.999+0200");
expect(rNote.utcDateCreated).toEqual("2000-01-01 10:34:56.999Z");
});
it("update content", async () => {
const { note } = await etapi.postEtapi("create-note", {
parentNoteId: "root",
type: "text",
title: "Hello World!",
content: "Content"
});
await etapi.putEtapiContent(`notes/${note.noteId}/content`, "new content");
const rContent = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).text();
expect(rContent).toEqual("new content");
});
it("create / update binary content", async () => {
const { note } = await etapi.postEtapi("create-note", {
parentNoteId: "root",
type: "file",
title: "Hello World!",
content: "ZZZ"
});
const updatedContent = crypto.randomBytes(16);
await etapi.putEtapiContent(`notes/${note.noteId}/content`, updatedContent);
const rContent = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).arrayBuffer();
expect(Buffer.from(new Uint8Array(rContent))).toEqual(updatedContent);
});
it("delete note", async () => {
const { note } = await etapi.postEtapi("create-note", {
parentNoteId: "root",
type: "text",
title: "Hello World!",
content: "Content"
});
await etapi.deleteEtapi(`notes/${note.noteId}`);
const resp = await etapi.getEtapiResponse(`notes/${note.noteId}`);
expect(resp.status).toEqual(404);
const error = await resp.json();
expect(error.status).toEqual(404);
expect(error.code).toEqual("NOTE_NOT_FOUND");
expect(error.message).toEqual(`Note '${note.noteId}' not found.`);
});
});
*/

View File

@@ -1,152 +0,0 @@
import { describe, beforeAll, afterAll } from "vitest";
let etapiAuthToken: string | undefined;
const getEtapiAuthorizationHeader = (): string => "Basic " + Buffer.from(`etapi:${etapiAuthToken}`).toString("base64");
const PORT: string = "9999";
const HOST: string = "http://localhost:" + PORT;
type SpecDefinitionsFunc = () => void;
function describeEtapi(description: string, specDefinitions: SpecDefinitionsFunc): void {
describe(description, () => {
beforeAll(async () => {});
afterAll(() => {});
specDefinitions();
});
}
async function getEtapiResponse(url: string): Promise<Response> {
return await fetch(`${HOST}/etapi/${url}`, {
method: "GET",
headers: {
Authorization: getEtapiAuthorizationHeader()
}
});
}
async function getEtapi(url: string): Promise<any> {
const response = await getEtapiResponse(url);
return await processEtapiResponse(response);
}
async function getEtapiContent(url: string): Promise<Response> {
const response = await fetch(`${HOST}/etapi/${url}`, {
method: "GET",
headers: {
Authorization: getEtapiAuthorizationHeader()
}
});
checkStatus(response);
return response;
}
async function postEtapi(url: string, data: Record<string, unknown> = {}): Promise<any> {
const response = await fetch(`${HOST}/etapi/${url}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: getEtapiAuthorizationHeader()
},
body: JSON.stringify(data)
});
return await processEtapiResponse(response);
}
async function postEtapiContent(url: string, data: BodyInit): Promise<Response> {
const response = await fetch(`${HOST}/etapi/${url}`, {
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
Authorization: getEtapiAuthorizationHeader()
},
body: data
});
checkStatus(response);
return response;
}
async function putEtapi(url: string, data: Record<string, unknown> = {}): Promise<any> {
const response = await fetch(`${HOST}/etapi/${url}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: getEtapiAuthorizationHeader()
},
body: JSON.stringify(data)
});
return await processEtapiResponse(response);
}
async function putEtapiContent(url: string, data?: BodyInit): Promise<Response> {
const response = await fetch(`${HOST}/etapi/${url}`, {
method: "PUT",
headers: {
"Content-Type": "application/octet-stream",
Authorization: getEtapiAuthorizationHeader()
},
body: data
});
checkStatus(response);
return response;
}
async function patchEtapi(url: string, data: Record<string, unknown> = {}): Promise<any> {
const response = await fetch(`${HOST}/etapi/${url}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: getEtapiAuthorizationHeader()
},
body: JSON.stringify(data)
});
return await processEtapiResponse(response);
}
async function deleteEtapi(url: string): Promise<any> {
const response = await fetch(`${HOST}/etapi/${url}`, {
method: "DELETE",
headers: {
Authorization: getEtapiAuthorizationHeader()
}
});
return await processEtapiResponse(response);
}
async function processEtapiResponse(response: Response): Promise<any> {
const text = await response.text();
if (response.status < 200 || response.status >= 300) {
throw new Error(`ETAPI error ${response.status}: ${text}`);
}
return text?.trim() ? JSON.parse(text) : null;
}
function checkStatus(response: Response): void {
if (response.status < 200 || response.status >= 300) {
throw new Error(`ETAPI error ${response.status}`);
}
}
export default {
describeEtapi,
getEtapi,
getEtapiResponse,
getEtapiContent,
postEtapi,
postEtapiContent,
putEtapi,
putEtapiContent,
patchEtapi,
deleteEtapi
};

View File

@@ -1,22 +0,0 @@
{
"compilerOptions": {
"module": "NodeNext",
"declaration": false,
"sourceMap": true,
"outDir": "./build",
"strict": true,
"noImplicitAny": true,
"resolveJsonModule": true,
"lib": ["ES2023"],
"downlevelIteration": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowJs": true
},
"include": ["./src/public/app/**/*"],
"files": [
"./src/public/app/types.d.ts",
"./src/public/app/types-lib.d.ts",
"./src/types.d.ts"
]
}

View File

@@ -11,11 +11,11 @@
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.24.0",
"devDependencies": {
"@redocly/cli": "2.12.0",
"@redocly/cli": "2.12.3",
"archiver": "7.0.1",
"fs-extra": "11.3.2",
"react": "19.2.0",
"react-dom": "19.2.0",
"react": "19.2.1",
"react-dom": "19.2.1",
"typedoc": "0.28.15",
"typedoc-plugin-missing-exports": "4.1.2"
}

View File

@@ -1,5 +0,0 @@
import baseConfig from "../../eslint.config.mjs";
export default [
...baseConfig
];

View File

@@ -12,10 +12,10 @@
"scripts": {
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build",
"test": "vitest",
"coverage": "vitest --coverage",
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
},
"dependencies": {
"@eslint/js": "9.39.1",
"@excalidraw/excalidraw": "0.18.0",
"@fullcalendar/core": "6.1.19",
"@fullcalendar/daygrid": "6.1.19",
@@ -27,12 +27,14 @@
"@mermaid-js/layout-elk": "0.2.0",
"@mind-elixir/node-menu": "5.0.1",
"@popperjs/core": "2.11.8",
"@preact/signals": "2.5.1",
"@triliumnext/ckeditor5": "workspace:*",
"@triliumnext/codemirror": "workspace:*",
"@triliumnext/commons": "workspace:*",
"@triliumnext/highlightjs": "workspace:*",
"@triliumnext/share-theme": "workspace:*",
"@triliumnext/split.js": "workspace:*",
"@zumer/snapdom": "2.0.1",
"autocomplete.js": "0.38.1",
"bootstrap": "5.3.8",
"boxicons": "2.1.4",
@@ -42,23 +44,23 @@
"draggabilly": "3.0.0",
"force-graph": "1.51.0",
"globals": "16.5.0",
"i18next": "25.6.3",
"i18next": "25.7.1",
"i18next-http-backend": "3.0.2",
"jquery": "3.7.1",
"jquery.fancytree": "2.38.5",
"jsplumb": "2.15.6",
"katex": "0.16.25",
"katex": "0.16.27",
"knockout": "3.5.1",
"leaflet": "1.9.4",
"leaflet-gpx": "2.2.0",
"mark.js": "8.11.1",
"marked": "17.0.1",
"mermaid": "11.12.1",
"mermaid": "11.12.2",
"mind-elixir": "5.3.7",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.27.2",
"react-i18next": "16.3.5",
"preact": "10.28.0",
"react-i18next": "16.4.0",
"reveal.js": "5.2.1",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",
@@ -72,7 +74,7 @@
"@types/leaflet": "1.9.21",
"@types/leaflet-gpx": "1.3.8",
"@types/mark.js": "8.11.12",
"@types/reveal.js": "5.2.1",
"@types/reveal.js": "5.2.2",
"@types/tabulator-tables": "6.3.0",
"copy-webpack-plugin": "13.0.1",
"happy-dom": "20.0.11",

View File

@@ -34,6 +34,7 @@ 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";
import type { InfoProps } from "../widgets/dialogs/info.jsx";
interface Layout {
getRootWidget: (appContext: AppContext) => RootContainer;
@@ -124,7 +125,7 @@ export type CommandMappings = {
isNewNote?: boolean;
};
showPromptDialog: PromptDialogOptions;
showInfoDialog: ConfirmWithMessageOptions;
showInfoDialog: InfoProps;
showConfirmDialog: ConfirmWithMessageOptions;
showRecentChanges: CommandData & { ancestorNoteId: string };
showImportDialog: CommandData & { noteId: string };

View File

@@ -22,6 +22,7 @@ bundleService.getWidgetBundlesByParent().then(async (widgetBundles) => {
appContext.setLayout(new DesktopLayout(widgetBundles));
appContext.start().catch((e) => {
toastService.showPersistent({
id: "critical-error",
title: t("toast.critical-error.title"),
icon: "alert",
message: t("toast.critical-error.message", { message: e.message })

View File

@@ -10,7 +10,6 @@ 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";
@@ -30,7 +29,6 @@ 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";
@@ -43,8 +41,9 @@ 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 RightPanelWidget from "../widgets/sidebar/RightPanelWidget.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
export default class DesktopLayout {
@@ -125,7 +124,7 @@ export default class DesktopLayout {
.cssBlock(".title-row > * { margin: 5px; }")
.child(<NoteIconWidget />)
.child(<NoteTitleWidget />)
.child(new SpacerWidget(0, 1))
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
.child(<MovePaneButton direction="left" />)
.child(<MovePaneButton direction="right" />)
.child(<ClosePaneButton />)
@@ -185,14 +184,14 @@ export default class DesktopLayout {
launcherPane = new FlexContainer("row")
.css("height", "53px")
.class("horizontal")
.child(new LauncherContainer(true))
.child(<LauncherContainer isHorizontalLayout={true} />)
.child(<GlobalMenu isHorizontalLayout={true} />);
} else {
launcherPane = new FlexContainer("column")
.css("width", "53px")
.class("vertical")
.child(<GlobalMenu isHorizontalLayout={false} />)
.child(new LauncherContainer(false))
.child(<LauncherContainer isHorizontalLayout={false} />)
.child(<LeftPaneToggle isHorizontalLayout={false} />);
}

View File

@@ -24,6 +24,7 @@ import InfoDialog from "../widgets/dialogs/info.js";
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
import PopupEditorDialog from "../widgets/dialogs/PopupEditor.jsx";
import ToastContainer from "../widgets/Toast.jsx";
export function applyModals(rootContainer: RootContainer) {
rootContainer
@@ -50,5 +51,6 @@ export function applyModals(rootContainer: RootContainer) {
.child(<PromptDialog />)
.child(<IncorrectCpuArchDialog />)
.child(<PopupEditorDialog />)
.child(<CallToActionDialog />);
.child(<CallToActionDialog />)
.child(<ToastContainer />)
}

View File

@@ -6,7 +6,6 @@ import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
import FlexContainer from "../widgets/containers/flex_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 MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
import NoteList from "../widgets/collections/NoteList.jsx";
import NoteTitleWidget from "../widgets/note_title.js";
@@ -30,6 +29,7 @@ import NoteDetail from "../widgets/NoteDetail.jsx";
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
const MOBILE_CSS = `
<style>
@@ -183,7 +183,7 @@ export default class MobileLayout {
.child(new FlexContainer("row")
.class("horizontal")
.css("height", "53px")
.child(new LauncherContainer(true))
.child(<LauncherContainer isHorizontalLayout />)
.child(<GlobalMenuWidget isHorizontalLayout />)
.id("launcher-pane"))
)

View File

@@ -1,6 +1,6 @@
import utils from "./utils.js";
import server from "./server.js";
import toastService, { type ToastOptions } from "./toast.js";
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
import froca from "./froca.js";
import hoistedNoteService from "./hoisted_note.js";
import ws from "./ws.js";
@@ -195,11 +195,11 @@ function filterRootNote(branchIds: string[]) {
});
}
function makeToast(id: string, message: string): ToastOptions {
function makeToast(id: string, message: string): ToastOptionsWithRequiredId {
return {
id: id,
id,
title: t("branches.delete-status"),
message: message,
message,
icon: "trash"
};
}
@@ -216,7 +216,7 @@ ws.subscribeToMessages(async (message) => {
toastService.showPersistent(makeToast(message.taskId, t("branches.delete-notes-in-progress", { count: message.progressCount })));
} else if (message.type === "taskSucceeded") {
const toast = makeToast(message.taskId, t("branches.delete-finished-successfully"));
toast.closeAfter = 5000;
toast.timeout = 5000;
toastService.showPersistent(toast);
}
@@ -234,7 +234,7 @@ ws.subscribeToMessages(async (message) => {
toastService.showPersistent(makeToast(message.taskId, t("branches.undeleting-notes-in-progress", { count: message.progressCount })));
} else if (message.type === "taskSucceeded") {
const toast = makeToast(message.taskId, t("branches.undeleting-notes-finished-successfully"));
toast.closeAfter = 5000;
toast.timeout = 5000;
toastService.showPersistent(toast);
}
@@ -242,7 +242,7 @@ ws.subscribeToMessages(async (message) => {
async function cloneNoteToBranch(childNoteId: string, parentBranchId: string, prefix?: string) {
const resp = await server.put<Response>(`notes/${childNoteId}/clone-to-branch/${parentBranchId}`, {
prefix: prefix
prefix
});
if (!resp.success) {
@@ -252,7 +252,7 @@ async function cloneNoteToBranch(childNoteId: string, parentBranchId: string, pr
async function cloneNoteToParentNote(childNoteId: string, parentNoteId: string, prefix?: string) {
const resp = await server.put<Response>(`notes/${childNoteId}/clone-to-note/${parentNoteId}`, {
prefix: prefix
prefix
});
if (!resp.success) {

View File

@@ -36,10 +36,17 @@ export async function executeBundle(bundle: Bundle, originEntity?: Entity | null
}.call(apiContext);
} catch (e: any) {
const note = await froca.getNote(bundle.noteId);
const message = `Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`;
showError(message);
logError(message);
toastService.showPersistent({
id: `custom-script-failure-${note?.noteId}`,
title: t("toast.bundle-error.title"),
icon: "bx bx-error-circle",
message: t("toast.bundle-error.message", {
id: note?.noteId,
title: note?.title,
message: e.message
})
});
logError("Widget initialization failed: ", e);
}
}
@@ -102,8 +109,9 @@ async function getWidgetBundlesByParent() {
const noteId = bundle.noteId;
const note = await froca.getNote(noteId);
toastService.showPersistent({
id: `custom-script-failure-${noteId}`,
title: t("toast.bundle-error.title"),
icon: "alert",
icon: "bx bx-error-circle",
message: t("toast.bundle-error.message", {
id: noteId,
title: note?.title,

View File

@@ -32,6 +32,7 @@ function createClassForColor(colorString: string | null) {
$("head").append(`<style>
.${className}, span.fancytree-active.${className} {
--original-custom-color: ${color.hex()};
--light-theme-custom-color: ${adjustedColor.lightThemeColor};
--dark-theme-custom-color: ${adjustedColor.darkThemeColor};
--custom-color-hue: ${hue ?? 'unset'};

View File

@@ -1,8 +1,9 @@
import { Modal } from "bootstrap";
import appContext from "../components/app_context.js";
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions, MessageType } from "../widgets/dialogs/confirm.js";
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
import { focusSavedElement, saveFocusedElement } from "./focus.js";
import { InfoExtraProps } from "../widgets/dialogs/info.jsx";
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true, config?: Partial<Modal.Options>) {
if (closeActDialog) {
@@ -37,8 +38,8 @@ export function closeActiveDialog() {
}
}
async function info(message: string) {
return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res }));
async function info(message: MessageType, extraProps?: InfoExtraProps) {
return new Promise((res) => appContext.triggerCommand("showInfoDialog", { ...extraProps, message, callback: res }));
}
/**

View File

@@ -77,6 +77,10 @@ export interface Api {
/**
* Entity whose event triggered this execution.
*
* <p>
* For front-end scripts, generally there's no origin entity specified since the scripts are run by the user or automatically by the UI (widgets).
* If there is an origin entity specified, then it's going to be a note entity.
*/
originEntity: unknown | null;
@@ -278,12 +282,16 @@ export interface Api {
getActiveContextNote(): FNote;
/**
* @returns returns active context (split)
* Obtains the currently active/focused split in the current tab.
*
* Note that this method does not return the note context of the "Quick edit" panel, it will return the note context behind it.
*/
getActiveContext(): NoteContext;
/**
* @returns returns active main context (first split in a tab, represents the tab as a whole)
* Obtains the main context of the current tab. This is the left-most split.
*
* Note that this method does not return the note context of the "Quick edit" panel, it will return the note context behind it.
*/
getActiveMainContext(): NoteContext;

View File

@@ -1,4 +1,4 @@
import toastService, { type ToastOptions } from "./toast.js";
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
import server from "./server.js";
import ws from "./ws.js";
import utils from "./utils.js";
@@ -57,11 +57,11 @@ export async function uploadFiles(entityType: string, parentNoteId: string, file
}
}
function makeToast(id: string, message: string): ToastOptions {
function makeToast(id: string, message: string): ToastOptionsWithRequiredId {
return {
id: id,
id,
title: t("import.import-status"),
message: message,
message,
icon: "plus"
};
}
@@ -78,7 +78,7 @@ ws.subscribeToMessages(async (message) => {
toastService.showPersistent(makeToast(message.taskId, t("import.in-progress", { progress: message.progressCount })));
} else if (message.type === "taskSucceeded") {
const toast = makeToast(message.taskId, t("import.successful"));
toast.closeAfter = 5000;
toast.timeout = 5000;
toastService.showPersistent(toast);
@@ -100,7 +100,7 @@ ws.subscribeToMessages(async (message: WebSocketMessage) => {
toastService.showPersistent(makeToast(message.taskId, t("import.in-progress", { progress: message.progressCount })));
} else if (message.type === "taskSucceeded") {
const toast = makeToast(message.taskId, t("import.successful"));
toast.closeAfter = 5000;
toast.timeout = 5000;
toastService.showPersistent(toast);

View File

@@ -1,7 +1,7 @@
import server from "./server.js";
import protectedSessionHolder from "./protected_session_holder.js";
import toastService from "./toast.js";
import type { ToastOptions } from "./toast.js";
import type { ToastOptionsWithRequiredId } from "./toast.js";
import ws from "./ws.js";
import appContext from "../components/app_context.js";
import froca from "./froca.js";
@@ -97,7 +97,7 @@ async function protectNote(noteId: string, protect: boolean, includingSubtree: b
await server.put(`notes/${noteId}/protect/${protect ? 1 : 0}?subtree=${includingSubtree ? 1 : 0}`);
}
function makeToast(message: Message, title: string, text: string): ToastOptions {
function makeToast(message: Message, title: string, text: string): ToastOptionsWithRequiredId {
return {
id: message.taskId,
title,
@@ -124,7 +124,7 @@ ws.subscribeToMessages(async (message) => {
} else if (message.type === "taskSucceeded") {
const text = isProtecting ? t("protected_session.protecting-finished-successfully") : t("protected_session.unprotecting-finished-successfully");
const toast = makeToast(message, title, text);
toast.closeAfter = 3000;
toast.timeout = 3000;
toastService.showPersistent(toast);
}

View File

@@ -263,7 +263,7 @@ async function reportError(method: string, url: string, statusCode: number, resp
const toastService = (await import("./toast.js")).default;
const messageStr = typeof message === "string" ? message : JSON.stringify(message);
const messageStr = (typeof message === "string" ? message : JSON.stringify(message)) || "-";
if ([400, 404].includes(statusCode) && response && typeof response === "object") {
toastService.showError(messageStr);
@@ -274,10 +274,22 @@ async function reportError(method: string, url: string, statusCode: number, resp
...response
});
} else {
const title = `${statusCode} ${method} ${url}`;
toastService.showErrorTitleAndMessage(title, messageStr);
const { t } = await import("./i18n.js");
if (statusCode === 400 && (url.includes("%23") || url.includes("%2F"))) {
toastService.showPersistent({
id: "trafik-blocked",
icon: "bx bx-unlink",
title: t("server.unknown_http_error_title"),
message: t("server.traefik_blocks_requests")
});
} else {
toastService.showErrorTitleAndMessage(
t("server.unknown_http_error_title"),
t("server.unknown_http_error_content", { statusCode, method, url, message: messageStr }),
15_000);
}
const { throwError } = await import("./ws.js");
throwError(`${title} - ${message}`);
throwError(`${statusCode} ${method} ${url} - ${message}`);
}
}

View File

@@ -1,7 +1,7 @@
import utils from "./utils.js";
type ElementType = HTMLElement | Document;
type Handler = (e: KeyboardEvent) => void;
export type Handler = (e: KeyboardEvent) => void;
export interface ShortcutBinding {
element: HTMLElement | Document;

View File

@@ -1,3 +1,5 @@
import { signal } from "@preact/signals";
import utils from "./utils.js";
export interface ToastOptions {
@@ -5,117 +7,84 @@ export interface ToastOptions {
icon: string;
title?: string;
message: string;
delay?: number;
autohide?: boolean;
closeAfter?: number;
timeout?: number;
progress?: number;
buttons?: {
text: string;
onClick: (api: { dismissToast: () => void }) => void;
}[];
}
function toast({ title, icon, message, id, delay, autohide, progress }: ToastOptions) {
const $toast = $(title
? `\
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong class="me-auto">
<span class="bx bx-${icon}"></span>
<span class="toast-title"></span>
</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body"></div>
<div class="toast-progress"></div>
</div>`
: `
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-icon">
<span class="bx bx-${icon}"></span>
</div>
<div class="toast-body"></div>
<div class="toast-header">
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-progress"></div>
</div>`
);
export type ToastOptionsWithRequiredId = Omit<ToastOptions, "id"> & Required<Pick<ToastOptions, "id">>;
$toast.toggleClass("no-title", !title);
$toast.find(".toast-title").text(title ?? "");
$toast.find(".toast-body").html(message);
$toast.find(".toast-progress").css("width", `${(progress ?? 0) * 100}%`);
if (id) {
$toast.attr("id", `toast-${id}`);
}
$("#toast-container").append($toast);
$toast.toast({
delay: delay || 3000,
autohide: !!autohide
});
$toast.on("hidden.bs.toast", (e) => e.target.remove());
$toast.toast("show");
return $toast;
}
function showPersistent(options: ToastOptions) {
let $toast = $(`#toast-${options.id}`);
if ($toast.length > 0) {
$toast.find(".toast-body").html(options.message);
$toast.find(".toast-progress").css("width", `${(options.progress ?? 0) * 100}%`);
function showPersistent(options: ToastOptionsWithRequiredId) {
const existingToast = toasts.value.find(toast => toast.id === options.id);
if (existingToast) {
updateToast(options.id, options);
} else {
options.autohide = false;
$toast = toast(options);
}
if (options.closeAfter) {
setTimeout(() => $toast.remove(), options.closeAfter);
addToast(options);
}
}
function closePersistent(id: string) {
$(`#toast-${id}`).remove();
removeToastFromStore(id);
}
function showMessage(message: string, delay = 2000, icon = "check") {
function showMessage(message: string, timeout = 2000, icon = "bx bx-check") {
console.debug(utils.now(), "message:", message);
toast({
addToast({
icon,
message: message,
autohide: true,
delay
message,
timeout
});
}
export function showError(message: string, delay = 10000) {
export function showError(message: string, timeout = 10000) {
console.log(utils.now(), "error: ", message);
toast({
icon: "alert",
message: message,
autohide: true,
delay
addToast({
icon: "bx bx-error-circle",
message,
timeout
});
}
function showErrorTitleAndMessage(title: string, message: string, delay = 10000) {
function showErrorTitleAndMessage(title: string, message: string, timeout = 10000) {
console.log(utils.now(), "error: ", message);
toast({
title: title,
icon: "alert",
message: message,
autohide: true,
delay
addToast({
title,
icon: "bx bx-error-circle",
message,
timeout
});
}
//#region Toast store
export const toasts = signal<ToastOptionsWithRequiredId[]>([]);
function addToast(opts: ToastOptions) {
const id = opts.id ?? crypto.randomUUID();
const toast = { ...opts, id };
toasts.value = [ ...toasts.value, toast ];
return id;
}
function updateToast(id: string, partial: Partial<ToastOptions>) {
toasts.value = toasts.value.map(toast => {
if (toast.id === id) {
return { ...toast, ...partial }
}
return toast;
});
}
export function removeToastFromStore(id: string) {
toasts.value = toasts.value.filter(toast => toast.id !== id);
}
//#endregion
export default {
showMessage,
showError,

View File

@@ -1,6 +1,7 @@
import { dayjs } from "@triliumnext/commons";
import type { ViewScope } from "./link.js";
import FNote from "../entities/fnote";
import { snapdom } from "@zumer/snapdom";
const SVG_MIME = "image/svg+xml";
@@ -150,7 +151,7 @@ export function isMac() {
export const hasTouchBar = (isMac() && isElectron());
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement> | JQueryEventObject) {
export function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement> | JQueryEventObject) {
return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey);
}
@@ -236,7 +237,7 @@ export function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent);
}
function isDesktop() {
export function isDesktop() {
return (
window.glob?.device === "desktop" ||
// window.glob.device is not available in setup
@@ -628,16 +629,69 @@ export function createImageSrcUrl(note: FNote) {
return `api/images/${note.noteId}/${encodeURIComponent(note.title)}?timestamp=${Date.now()}`;
}
/**
* Given a string representation of an SVG, triggers a download of the file on the client device.
* Helper function to prepare an element for snapdom rendering.
* Handles string parsing and temporary DOM attachment for style computation.
*
* @param source - Either an SVG/HTML string to be parsed, or an existing SVG/HTML element.
* @returns An object containing the prepared element and a cleanup function.
* The cleanup function removes temporarily attached elements from the DOM,
* or is a no-op if the element was already in the DOM.
*/
function prepareElementForSnapdom(source: string | SVGElement | HTMLElement): {
element: SVGElement | HTMLElement;
cleanup: () => void;
} {
if (typeof source === 'string') {
const parser = new DOMParser();
// Detect if content is SVG or HTML
const isSvg = source.trim().startsWith('<svg');
const mimeType = isSvg ? SVG_MIME : 'text/html';
const doc = parser.parseFromString(source, mimeType);
const element = doc.documentElement;
// Temporarily attach to DOM for proper style computation
element.style.position = 'absolute';
element.style.left = '-9999px';
element.style.top = '-9999px';
document.body.appendChild(element);
return {
element,
cleanup: () => document.body.removeChild(element)
};
}
return {
element: source,
cleanup: () => {} // No-op for existing elements
};
}
/**
* Downloads an SVG using snapdom for proper rendering. Can accept either an SVG string, an SVG element, or an HTML element.
*
* @param nameWithoutExtension the name of the file. The .svg suffix is automatically added to it.
* @param svgContent the content of the SVG file download.
* @param svgSource either an SVG string, an SVGElement, or an HTMLElement to be downloaded.
*/
function downloadSvg(nameWithoutExtension: string, svgContent: string) {
const filename = `${nameWithoutExtension}.svg`;
const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`;
triggerDownload(filename, dataUrl);
async function downloadAsSvg(nameWithoutExtension: string, svgSource: string | SVGElement | HTMLElement) {
const { element, cleanup } = prepareElementForSnapdom(svgSource);
try {
const result = await snapdom(element, {
backgroundColor: "transparent",
scale: 2
});
triggerDownload(`${nameWithoutExtension}.svg`, result.url);
} finally {
cleanup();
}
}
/**
@@ -658,62 +712,26 @@ function triggerDownload(fileName: string, dataUrl: string) {
document.body.removeChild(element);
}
/**
* Given a string representation of an SVG, renders the SVG to PNG and triggers a download of the file on the client device.
*
* Note that the SVG must specify its width and height as attributes in order for it to be rendered.
* Downloads an SVG as PNG using snapdom. Can accept either an SVG string, an SVG element, or an HTML element.
*
* @param nameWithoutExtension the name of the file. The .png suffix is automatically added to it.
* @param svgContent the content of the SVG file download.
* @returns a promise which resolves if the operation was successful, or rejects if it failed (permissions issue or some other issue).
* @param svgSource either an SVG string, an SVGElement, or an HTMLElement to be converted to PNG.
*/
function downloadSvgAsPng(nameWithoutExtension: string, svgContent: string) {
return new Promise<void>((resolve, reject) => {
// First, we need to determine the width and the height from the input SVG.
const result = getSizeFromSvg(svgContent);
if (!result) {
reject();
return;
}
async function downloadAsPng(nameWithoutExtension: string, svgSource: string | SVGElement | HTMLElement) {
const { element, cleanup } = prepareElementForSnapdom(svgSource);
// Convert the image to a blob.
const { width, height } = result;
// Create an image element and load the SVG.
const imageEl = new Image();
imageEl.width = width;
imageEl.height = height;
imageEl.crossOrigin = "anonymous";
imageEl.onload = () => {
try {
// Draw the image with a canvas.
const canvasEl = document.createElement("canvas");
canvasEl.width = imageEl.width;
canvasEl.height = imageEl.height;
document.body.appendChild(canvasEl);
const ctx = canvasEl.getContext("2d");
if (!ctx) {
reject();
}
ctx?.drawImage(imageEl, 0, 0);
const imgUri = canvasEl.toDataURL("image/png")
triggerDownload(`${nameWithoutExtension}.png`, imgUri);
document.body.removeChild(canvasEl);
resolve();
} catch (e) {
console.warn(e);
reject();
}
};
imageEl.onerror = (e) => reject(e);
imageEl.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`;
});
try {
const result = await snapdom(element, {
backgroundColor: "transparent",
scale: 2
});
const pngImg = await result.toPng();
await triggerDownload(`${nameWithoutExtension}.png`, pngImg.src);
} finally {
cleanup();
}
}
export function getSizeFromSvg(svgContent: string) {
const svgDocument = (new DOMParser()).parseFromString(svgContent, SVG_MIME);
@@ -925,8 +943,8 @@ export default {
areObjectsEqual,
copyHtmlToClipboard,
createImageSrcUrl,
downloadSvg,
downloadSvgAsPng,
downloadAsSvg,
downloadAsPng,
compareVersions,
isUpdateAvailable,
isLaunchBarConfig

View File

@@ -1015,15 +1015,9 @@ div[data-notify="container"] {
font-family: var(--monospace-font-family);
}
svg.ck-icon {
&.ck-icon_inherit-color path[fill="#333"] {
fill: currentColor;
}
&.note-icon {
color: var(--main-text-color);
font-size: 20px;
}
svg.ck-icon.note-icon {
color: var(--main-text-color);
font-size: 20px;
}
.ck-content {
@@ -1141,61 +1135,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
margin: 0 12px;
}
#toast-container {
position: absolute;
width: 100%;
top: 20px;
pointer-events: none;
}
.toast {
--bs-toast-bg: var(--accented-background-color);
--bs-toast-color: var(--main-text-color);
z-index: 9999999999 !important;
pointer-events: all;
overflow: hidden;
}
.toast-header {
background-color: var(--more-accented-background-color) !important;
color: var(--main-text-color) !important;
}
.toast-body {
white-space: preserve-breaks;
overflow: hidden;
}
.toast.no-title {
display: flex;
flex-direction: row;
}
.toast.no-title .toast-icon {
display: flex;
align-items: center;
padding: var(--bs-toast-padding-y) var(--bs-toast-padding-x);
}
.toast.no-title .toast-body {
padding-inline-start: 0;
padding-inline-end: 0;
}
.toast.no-title .toast-header {
background-color: unset !important;
}
.toast .toast-progress {
position: absolute;
bottom: 0;
inset-inline-start: 0;
inset-inline-end: 0;
background-color: var(--toast-text-color) !important;
height: 4px;
transition: width 0.1s linear;
}
.ck-mentions .ck-button {
font-size: var(--detail-font-size) !important;
padding: 5px;

View File

@@ -114,4 +114,8 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
.use-note-color {
--custom-color: var(--dark-theme-custom-color);
}
}
span.fancytree-active {
color: var(--dark-theme-custom-color);
}

View File

@@ -98,4 +98,8 @@ html {
.use-note-color {
--custom-color: var(--light-theme-custom-color);
}
}
span.fancytree-active {
color: var(--light-theme-custom-color);
}

View File

@@ -526,11 +526,14 @@ button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel).ck
.ck-mermaid__editing-view {
border: 0;
border-radius: 6px;
box-shadow: var(--code-block-box-shadow);
padding: 0;
box-shadow: var(--code-block-box-shadow);
margin-top: 2px !important;
}
:root .ck-content pre:has(> code) {
padding: 0;
}
:root .ck-content pre {
--icon-button-size: 1.8em;
--copy-button-width: var(--icon-button-size);

View File

@@ -212,7 +212,8 @@ body[dir=ltr] #launcher-container {
}
#launcher-pane .launcher-button,
#launcher-pane .dropdown {
#launcher-pane .right-dropdown-widget,
#launcher-pane .global-menu {
width: calc(var(--launcher-pane-size) - (var(--launcher-pane-button-margin) * 2)) !important;
height: calc(var(--launcher-pane-size) - (var(--launcher-pane-button-margin) * 2)) !important;
margin: var(--launcher-pane-button-gap) var(--launcher-pane-button-margin);

View File

@@ -948,7 +948,7 @@
"move-to-available-launchers": "نقل الى المشغلات المتوفرة",
"duplicate-launcher": "تكرار المشغل <kbd data-command=\"duplicateSubtree\">"
},
"editable-text": {
"editable_text": {
"auto-detect-language": "تم اكتشافه تلقائيا"
},
"classic_editor_toolbar": {

View File

@@ -205,7 +205,8 @@
"info": {
"modalTitle": "信息消息",
"closeButton": "关闭",
"okButton": "确定"
"okButton": "确定",
"copy_to_clipboard": "复制到剪切板"
},
"jump_to_note": {
"search_button": "全文搜索",
@@ -986,7 +987,14 @@
"placeholder": "在这里输入您的代码笔记内容..."
},
"editable_text": {
"placeholder": "在这里输入您的笔记内容..."
"placeholder": "在这里输入您的笔记内容...",
"auto-detect-language": "自动检测",
"keeps-crashing": "编辑组件时持续崩溃。请尝试重启 Trilium。如果问题仍然存在请考虑提交错误报告。",
"editor_crashed_title": "文本编辑器崩溃",
"editor_crashed_content": "您的内容已经成功恢复,但是某些最近的内容可能没有保存。",
"editor_crashed_details_button": "浏览更多明细...",
"editor_crashed_details_intro": "如果您多次经历此错误考虑在Github上提交这些信息。",
"editor_crashed_details_title": "技术信息"
},
"empty": {
"open_note_instruction": "通过在下面的输入框中输入笔记标题或在树中选择笔记来打开笔记。",
@@ -1469,7 +1477,7 @@
"import-into-note": "导入到笔记",
"apply-bulk-actions": "应用批量操作",
"converted-to-attachments": "{{count}} 个笔记已被转换为附件。",
"convert-to-attachment-confirm": "确定要将选中的笔记转换为其父笔记的附件吗?",
"convert-to-attachment-confirm": "确定要将选中的笔记转换为其父笔记的附件吗?此操作仅适用于图像笔记,其他笔记将被跳过。",
"duplicate": "复制",
"open-in-popup": "快速编辑",
"archive": "归档",
@@ -1660,10 +1668,6 @@
"move-to-available-launchers": "移动到可用启动器",
"duplicate-launcher": "复制启动器 <kbd data-command=\"duplicateSubtree\">"
},
"editable-text": {
"auto-detect-language": "自动检测",
"keeps-crashing": "编辑组件时持续崩溃。请尝试重启 Trilium。如果问题仍然存在请考虑提交错误报告。"
},
"highlighting": {
"title": "代码块",
"description": "控制文本笔记中代码块的语法高亮,代码笔记不会受到影响。",
@@ -1714,7 +1718,8 @@
"open_note_in_new_tab": "在新标签页中打开笔记",
"open_note_in_new_split": "在新分屏中打开笔记",
"open_note_in_new_window": "在新窗口中打开笔记",
"open_note_in_popup": "快速编辑"
"open_note_in_popup": "快速编辑",
"open_note_in_other_split": "在另一个分屏中打开笔记"
},
"electron_integration": {
"desktop-application": "桌面应用程序",
@@ -1953,7 +1958,8 @@
"button_title": "将图表导出为PNG"
},
"svg": {
"export_to_png": "无法将图表导出为PNG。"
"export_to_png": "无法将图表导出为PNG。",
"export_to_svg": "此图像无法导出为SVG。"
},
"code_theme": {
"title": "外观",
@@ -2105,5 +2111,10 @@
},
"popup-editor": {
"maximize": "切换至完整编辑器"
},
"server": {
"unknown_http_error_title": "与服务器通讯错误",
"unknown_http_error_content": "状态码: {{statusCode}}\n地址: {{method}} {{url}}\n信息: {{message}}",
"traefik_blocks_requests": "如果您使用 Traefik 反向代理,它引入了一项影响与服务器的通信重大更改。"
}
}

View File

@@ -162,7 +162,8 @@
"quickSearch": "Fokus auf schnelle Sucheingabe",
"inPageSearch": "Auf-der-Seite-Suche",
"newTabWithActivationNoteLink": "auf einen Notiz-Link öffnet und aktiviert die Notiz in einem neuen Tab",
"title": "Spickzettel"
"title": "Spickzettel",
"editShortcuts": "Tastenkürzel bearbeiten"
},
"import": {
"importIntoNote": "In Notiz importieren",
@@ -983,7 +984,9 @@
"placeholder": "Gebe hier den Inhalt deiner Codenotiz ein..."
},
"editable_text": {
"placeholder": "Gebe hier den Inhalt deiner Notiz ein..."
"placeholder": "Gebe hier den Inhalt deiner Notiz ein...",
"auto-detect-language": "Automatisch erkannt",
"keeps-crashing": "Die Bearbeitungskomponente stürzt immer wieder ab. Bitte starten Sie Trilium neu. Wenn das Problem weiterhin besteht, erstellen Sie einen Fehlerbericht."
},
"empty": {
"open_note_instruction": "Öffne eine Notiz, indem du den Titel der Notiz in die Eingabe unten eingibst oder eine Notiz in der Baumstruktur auswählst.",
@@ -1624,10 +1627,6 @@
"move-to-available-launchers": "Zu verfügbaren Launchern verschieben",
"duplicate-launcher": "Launcher duplizieren <kbd data-command=\"duplicateSubtree\">"
},
"editable-text": {
"auto-detect-language": "Automatisch erkannt",
"keeps-crashing": "Die Bearbeitungskomponente stürzt immer wieder ab. Bitte starten Sie Trilium neu. Wenn das Problem weiterhin besteht, erstellen Sie einen Fehlerbericht."
},
"highlighting": {
"description": "Steuert die Syntaxhervorhebung für Codeblöcke in Textnotizen, Code-Notizen sind nicht betroffen.",
"color-scheme": "Farbschema",
@@ -2102,5 +2101,8 @@
"clear-color": "Notizfarbe entfernen",
"set-color": "Notizfarbe wählen",
"set-custom-color": "Eigene Notizfarbe wählen"
},
"popup-editor": {
"maximize": "Wechsele zum vollständigen Editor"
}
}

View File

@@ -205,7 +205,8 @@
"info": {
"modalTitle": "Info message",
"closeButton": "Close",
"okButton": "OK"
"okButton": "OK",
"copy_to_clipboard": "Copy to clipboard"
},
"jump_to_note": {
"search_placeholder": "Search for note by its name or type > for commands...",
@@ -987,7 +988,14 @@
"placeholder": "Type the content of your code note here..."
},
"editable_text": {
"placeholder": "Type the content of your note here..."
"placeholder": "Type the content of your note here...",
"editor_crashed_title": "The text editor crashed",
"editor_crashed_content": "Your content was recovered successfully, but a few of your most recent changes may not have been saved.",
"editor_crashed_details_button": "View more details...",
"editor_crashed_details_intro": "If you experience this error several times, consider reporting it on GitHub by pasting the information below.",
"editor_crashed_details_title": "Technical information",
"auto-detect-language": "Auto-detected",
"keeps-crashing": "Editing component keeps crashing. Please try restarting Trilium. If problem persists, consider creating a bug report."
},
"empty": {
"open_note_instruction": "Open a note by typing the note's title into the input below or choose a note in the tree.",
@@ -1826,10 +1834,6 @@
"move-to-available-launchers": "Move to available launchers",
"duplicate-launcher": "Duplicate launcher <kbd data-command=\"duplicateSubtree\">"
},
"editable-text": {
"auto-detect-language": "Auto-detected",
"keeps-crashing": "Editing component keeps crashing. Please try restarting Trilium. If problem persists, consider creating a bug report."
},
"highlighting": {
"title": "Code Blocks",
"description": "Controls the syntax highlighting for code blocks inside text notes, code notes will not be affected.",
@@ -1968,7 +1972,8 @@
"button_title": "Export diagram as PNG"
},
"svg": {
"export_to_png": "The diagram could not be exported to PNG."
"export_to_png": "The diagram could not be exported to PNG.",
"export_to_svg": "The diagram could not be exported to SVG."
},
"code_theme": {
"title": "Appearance",
@@ -2107,5 +2112,10 @@
},
"popup-editor": {
"maximize": "Switch to full editor"
},
"server": {
"unknown_http_error_title": "Communication error with the server",
"unknown_http_error_content": "Status code: {{statusCode}}\nURL: {{method}} {{url}}\nMessage: {{message}}",
"traefik_blocks_requests": "If you are using the Traefik reverse proxy, it introduced a breaking change which affects the communication with the server."
}
}

View File

@@ -987,7 +987,8 @@
"placeholder": "Escriba el contenido de su nota de código aquí..."
},
"editable_text": {
"placeholder": "Escribe aquí el contenido de tu nota..."
"placeholder": "Escribe aquí el contenido de tu nota...",
"auto-detect-language": "Detectado automáticamente"
},
"empty": {
"open_note_instruction": "Abra una nota escribiendo el título de la nota en la entrada a continuación o elija una nota en el árbol.",
@@ -1812,9 +1813,6 @@
"move-to-available-launchers": "Mover a lanzadores disponibles",
"duplicate-launcher": "Duplicar lanzador <kbd data-command=\"duplicateSubtree\">"
},
"editable-text": {
"auto-detect-language": "Detectado automáticamente"
},
"highlighting": {
"title": "Bloques de código",
"description": "Controla el resaltado de sintaxis para bloques de código dentro de las notas de texto, las notas de código no serán afectadas.",

View File

@@ -981,7 +981,8 @@
"placeholder": "Saisir le contenu de votre note de code ici..."
},
"editable_text": {
"placeholder": "Saisir le contenu de votre note ici..."
"placeholder": "Saisir le contenu de votre note ici...",
"auto-detect-language": "Détecté automatiquement"
},
"empty": {
"open_note_instruction": "Ouvrez une note en tapant son titre dans la zone ci-dessous ou choisissez une note dans l'arborescence.",
@@ -1617,9 +1618,6 @@
"move-to-available-launchers": "Déplacer vers les raccourcis disponibles",
"duplicate-launcher": "Dupliquer le raccourci <kbd data-command=\"duplicateSubtree\">"
},
"editable-text": {
"auto-detect-language": "Détecté automatiquement"
},
"highlighting": {
"description": "Contrôle la coloration syntaxique des blocs de code à l'intérieur des notes texte, les notes de code ne seront pas affectées.",
"color-scheme": "Jeu de couleurs",

View File

@@ -1491,7 +1491,9 @@
"placeholder": "Digita qui il contenuto della tua nota di codice..."
},
"editable_text": {
"placeholder": "Digita qui il contenuto della tua nota..."
"placeholder": "Digita qui il contenuto della tua nota...",
"auto-detect-language": "Rilevato automaticamente",
"keeps-crashing": "Il componente di modifica continua a bloccarsi. Prova a riavviare Trilium. Se il problema persiste, valuta la possibilità di creare una segnalazione di bug."
},
"empty": {
"open_note_instruction": "Apri una nota digitandone il titolo nel campo sottostante oppure scegli una nota nell'albero.",
@@ -1937,10 +1939,6 @@
"move-to-available-launchers": "Passa ai launcher disponibili",
"duplicate-launcher": "Duplica il launcher <kbd data-command=\"duplicateSubtree\">"
},
"editable-text": {
"auto-detect-language": "Rilevato automaticamente",
"keeps-crashing": "Il componente di modifica continua a bloccarsi. Prova a riavviare Trilium. Se il problema persiste, valuta la possibilità di creare una segnalazione di bug."
},
"highlighting": {
"title": "Blocchi di codice",
"description": "Controlla l'evidenziazione della sintassi per i blocchi di codice all'interno delle note di testo; le note di codice non saranno interessate.",

View File

@@ -290,7 +290,7 @@
"pasteNotes": "ノートをサブノートとしてアクティブノートに貼り付ける(コピーされたか切り取りされたかに よって、移動またはクローンになる)",
"deleteNotes": "ノート/サブツリーを削除",
"editingNotes": "ノート編集",
"editNoteTitle": "ツリーペインでEnterキーを押すと、ツリーペインからートタイトルに切り替わります。ートタイトルだとテキストエディタにフォーカスが切り替わります。<kbd>Ctrl+.</kbd> を押すと、エディタからツリーペインに戻ります。",
"editNoteTitle": "ツリーペインでEnterキーを押すと、ツリーペインからートタイトルに切り替わります。ートタイトルだとテキストエディタにフォーカスが切り替わります。<kbd>Ctrl+.</kbd> を押すと、エディタからツリーペインに戻ります。",
"createEditLink": "外部リンクの作成/編集",
"createInternalLink": "内部リンクの作成",
"followLink": "カーソル下のリンクをたどる",
@@ -421,7 +421,7 @@
"apply-bulk-actions": "一括操作の適用",
"converted-to-attachments": "{{count}}ノートが添付ファイルに変換されました。",
"convert-to-attachment": "添付ファイルに変換",
"convert-to-attachment-confirm": "選択したノートを親ノートの添付ファイルに変換しますか?",
"convert-to-attachment-confirm": "選択したノートを親ノートの添付ファイルに変換してもよろしいですか?この操作は画像ノートにのみ適用され、その他のノートはスキップされます。",
"open-in-popup": "クイック編集",
"hoist-note": "ホイストノート",
"unhoist-note": "ノートをホイストしない",
@@ -526,7 +526,7 @@
"button_title": "ボタンを表示"
},
"svg_export_button": {
"button_title": "図をSVGとしてエクスポート"
"button_title": "図をSVG形式でエクスポート"
},
"book_properties": {
"grid": "グリッド",
@@ -1011,7 +1011,8 @@
"info": {
"closeButton": "閉じる",
"modalTitle": "情報メッセージ",
"okButton": "OK"
"okButton": "OK",
"copy_to_clipboard": "クリップボードにコピー"
},
"protected_session_password": {
"close_label": "閉じる",
@@ -1268,10 +1269,6 @@
"duplicate-launcher": "ランチャーの複製 <kbd data-command=\"duplicateSubtree\">",
"reset_launcher_confirm": "本当に「{{title}}」をリセットしますか? このノート(およびその子ノート)のすべてのデータと設定が失われ、ランチャーは元の場所に戻ります。"
},
"editable-text": {
"auto-detect-language": "自動検出",
"keeps-crashing": "編集コンポーネントがクラッシュし続けます。Trilium を再起動してください。問題が解決しない場合は、バグレポートの作成をご検討ください。"
},
"highlighting": {
"title": "コードブロック",
"description": "テキストノート内のコードブロックのシンタックスハイライトを制御します。コードノートには影響しません。",
@@ -1344,7 +1341,8 @@
"button_title": "図をPNG形式でエクスポート"
},
"svg": {
"export_to_png": "図をPNG形式でエクスポートできませんでした。"
"export_to_png": "図をPNG形式でエクスポートできませんでした。",
"export_to_svg": "図をSVG形式でエクスポートできませんでした。"
},
"code_theme": {
"title": "外観",
@@ -1778,7 +1776,14 @@
"placeholder": "ここにコードノートの内容を入力..."
},
"editable_text": {
"placeholder": "ここにノートの内容を入力..."
"placeholder": "ここにノートの内容を入力...",
"auto-detect-language": "自動検出",
"keeps-crashing": "編集コンポーネントがクラッシュし続けます。Trilium を再起動してください。問題が解決しない場合は、バグレポートの作成をご検討ください。",
"editor_crashed_title": "テキストエディターがクラッシュしました",
"editor_crashed_content": "コンテンツは正常に復元されましたが、最近の変更の一部が保存されていない可能性があります。",
"editor_crashed_details_button": "詳細を見る...",
"editor_crashed_details_intro": "このエラーが何度も発生する場合は、以下の情報を貼り付けて GitHub に報告することを検討してください。",
"editor_crashed_details_title": "技術情報"
},
"empty": {
"open_note_instruction": "以下の入力欄にノートのタイトルを入力するか、ツリー内のノートを選択してノートを開きます。",
@@ -2106,5 +2111,10 @@
},
"popup-editor": {
"maximize": "フルエディターに切り替え"
},
"server": {
"unknown_http_error_title": "サーバーとの通信エラー",
"unknown_http_error_content": "ステータスコード: {{statusCode}}\nURL: {{method}} {{url}}\nメッセージ: {{message}}",
"traefik_blocks_requests": "Traefik リバース プロキシを使用している場合、サーバーとの通信に影響する重大な変更が導入されました。"
}
}

View File

@@ -50,12 +50,24 @@
"available_actions": "가능한 액션들",
"chosen_actions": "선택한 액션들",
"execute_bulk_actions": "대량 액션들 실행",
"bulk_actions_executed": "대량 액션들이 성공적으로 실행되었습니다."
"bulk_actions_executed": "대량 액션들이 성공적으로 실행되었습니다.",
"none_yet": "아직 없습니다... 위에 있는 가능한 작업 중 하나를 클릭하여 작업을 추가하세요.",
"labels": "라벨",
"relations": "관계",
"notes": "노트",
"other": "기타"
},
"i18n": {
"saturday": "토요일",
"sunday": "일요일",
"first-week-of-the-year": "일년의 첫째 주",
"first-week-contains-first-day": "첫 번째 주에는 올해의 첫날이 포함됩니다"
},
"clone_to": {
"clone_notes_to": "~로 노트 복제",
"help_on_links": "링크에 대한 도움말",
"notes_to_clone": "노트 클론 생성",
"target_parent_note": "부모 노트 타겟",
"search_for_note_by_its_name": "이름으로 노트 검색하기"
}
}

View File

@@ -1330,7 +1330,8 @@
"placeholder": "Wpisz tutaj treść swojej notatki kodowej..."
},
"editable_text": {
"placeholder": "Wpisz tutaj treść swojej notatki..."
"placeholder": "Wpisz tutaj treść swojej notatki...",
"auto-detect-language": "Wykryto automatycznie"
},
"empty": {
"open_note_instruction": "Otwórz notatkę, wpisując jej tytuł w poniższe pole lub wybierz notatkę z drzewa.",
@@ -2014,9 +2015,6 @@
"move-to-available-launchers": "Przenieś do dostępnych programów uruchamiających",
"duplicate-launcher": "Duplikuj program uruchamiający <kbd data-command=\"duplicateSubtree\">"
},
"editable-text": {
"auto-detect-language": "Wykryto automatycznie"
},
"highlighting": {
"title": "Bloki kodu",
"description": "Kontroluje podświetlanie składni dla bloków kodu w notatkach tekstowych, notatki kodowe nie będą miały wpływu.",

View File

@@ -954,7 +954,8 @@
"placeholder": "Digite o conteúdo da sua nota de código aqui…"
},
"editable_text": {
"placeholder": "Digite o conteúdo da sua nota aqui…"
"placeholder": "Digite o conteúdo da sua nota aqui…",
"auto-detect-language": "Detetado automaticamente"
},
"empty": {
"open_note_instruction": "Abra uma nota a digitar o título da nota no campo abaixo ou escolha uma nota na árvore.",
@@ -1768,9 +1769,6 @@
"move-to-available-launchers": "Mover para lançadores disponíveis",
"duplicate-launcher": "Duplicar o lançador <kbd data-command=\"duplicateSubtree\">"
},
"editable-text": {
"auto-detect-language": "Detetado automaticamente"
},
"highlighting": {
"title": "Blocos de Código",
"description": "Controla o destaque de sintaxe para blocos de código dentro de notas de texto, notas de código não serão afetadas.",

View File

@@ -1191,7 +1191,8 @@
"placeholder": "Digite o conteúdo da sua nota de código aqui…"
},
"editable_text": {
"placeholder": "Digite o conteúdo da sua nota aqui…"
"placeholder": "Digite o conteúdo da sua nota aqui…",
"auto-detect-language": "Detectado automaticamente"
},
"empty": {
"search_placeholder": "buscar uma nota pelo nome",
@@ -1689,9 +1690,6 @@
"move-to-available-launchers": "Mover para lançadores disponíveis",
"duplicate-launcher": "Duplicar o lançador <kbd data-command=\"duplicateSubtree\">"
},
"editable-text": {
"auto-detect-language": "Detectado automaticamente"
},
"highlighting": {
"title": "Blocos de Código",
"description": "Controla o destaque de sintaxe para blocos de código dentro de notas de texto, notas de código não serão afetadas.",

View File

@@ -491,7 +491,9 @@
"placeholder": "Scrieți conținutul notiței de cod aici..."
},
"editable_text": {
"placeholder": "Scrieți conținutul notiței aici..."
"placeholder": "Scrieți conținutul notiței aici...",
"auto-detect-language": "Automat",
"keeps-crashing": "Componenta de editare se blochează în continuu. Încercați să reporniți Trilium. Dacă problema persistă, luați în considerare să raportați această problemă."
},
"edited_notes": {
"deleted": "(șters)",
@@ -1626,10 +1628,6 @@
"move-to-visible-launchers": "Mută în Lansatoare vizibile",
"reset": "Resetează"
},
"editable-text": {
"auto-detect-language": "Automat",
"keeps-crashing": "Componenta de editare se blochează în continuu. Încercați să reporniți Trilium. Dacă problema persistă, luați în considerare să raportați această problemă."
},
"highlighting": {
"color-scheme": "Temă de culori",
"description": "Controlează evidențierea de sintaxă pentru blocurile de cod în interiorul notițelor text, notițele de tip cod nu vor fi afectate de aceste setări.",

View File

@@ -726,9 +726,6 @@
"title": "Блоки кода",
"description": "Управляет подсветкой синтаксиса для блоков кода внутри текстовых заметок. Заметки с типом \"Код\" не будут затронуты."
},
"editable-text": {
"auto-detect-language": "Определен автоматически"
},
"launcher_context_menu": {
"reset": "Сбросить",
"add-spacer": "Добавить разделитель",
@@ -2026,7 +2023,8 @@
"placeholder": "Введите содержимое для заметки с кодом..."
},
"editable_text": {
"placeholder": "Введите содержимое для заметки..."
"placeholder": "Введите содержимое для заметки...",
"auto-detect-language": "Определен автоматически"
},
"hoisted_note": {
"confirm_unhoisting": "Запрошенная заметка «{{requestedNote}}» находится за пределами поддерева закрепленной заметки \"{{hoistedNote}}\", и для доступа к ней необходимо снять закрепление. Открепить заметку?"

View File

@@ -734,7 +734,8 @@
},
"zpetne_odkazy": {
"relation": "關聯",
"backlink_one": "{{count}} 個反連結"
"backlink_one": "{{count}} 個反連結",
"backlink_other": "{{count}} 個反連結"
},
"mobile_detail_menu": {
"insert_child_note": "插入子筆記",
@@ -983,7 +984,9 @@
"placeholder": "在這裡輸入您的程式碼筆記內容…"
},
"editable_text": {
"placeholder": "在這裡輸入您的筆記內容…"
"placeholder": "在這裡輸入您的筆記內容…",
"auto-detect-language": "自動檢測",
"keeps-crashing": "編輯元件持續發生崩潰。請嘗試重新啟動 Trilium。若問題仍存在請考慮提交錯誤報告。"
},
"empty": {
"open_note_instruction": "透過在下面的輸入框中輸入筆記標題或在樹中選擇筆記來打開筆記。",
@@ -1428,7 +1431,7 @@
"import-into-note": "匯入至筆記",
"apply-bulk-actions": "套用批次操作",
"converted-to-attachments": "{{count}} 個筆記已被轉換為附件。",
"convert-to-attachment-confirm": "確定要將所選的筆記轉換為其父級筆記的附件嗎?",
"convert-to-attachment-confirm": "確定要將所選的筆記轉換為其父級筆記的附件嗎?此操作僅適用於圖像筆記,其他筆記將被跳過。",
"duplicate": "複製副本",
"open-in-popup": "快速編輯",
"archive": "封存",
@@ -1619,10 +1622,6 @@
"move-to-available-launchers": "移動至可用啟動器",
"duplicate-launcher": "複製啟動器 <kbd data-command=\"duplicateSubtree\">"
},
"editable-text": {
"auto-detect-language": "自動檢測",
"keeps-crashing": "編輯元件持續發生崩潰。請嘗試重新啟動 Trilium。若問題仍存在請考慮提交錯誤報告。"
},
"highlighting": {
"description": "控制文字筆記程式碼區塊中的語法高亮,程式碼筆記不會受到影響。",
"color-scheme": "配色方案",
@@ -1673,7 +1672,8 @@
"open_note_in_new_tab": "在新分頁中打開筆記",
"open_note_in_new_split": "在新頁面分割中打開筆記",
"open_note_in_new_window": "在新視窗中打開筆記",
"open_note_in_popup": "快速編輯"
"open_note_in_popup": "快速編輯",
"open_note_in_other_split": "在另一個頁面分割中打開筆記"
},
"zen_mode": {
"button_exit": "退出禪模式"
@@ -1953,7 +1953,8 @@
"button_title": "將圖表匯出為 PNG"
},
"svg": {
"export_to_png": "無法將圖表匯出為 PNG。"
"export_to_png": "無法將圖表匯出為 PNG。",
"export_to_svg": "此绘图无法导出为SVG格式。"
},
"code_theme": {
"title": "外觀",

View File

@@ -130,9 +130,6 @@
"move-to-available-launchers": "Перейти до доступних лаунчерів",
"duplicate-launcher": "Дублікат програми запуску <kbd data-command=\"duplicateSubtree\">"
},
"editable-text": {
"auto-detect-language": "Автовизначено"
},
"highlighting": {
"color-scheme": "Схема кольорів",
"title": "Блоки коду",
@@ -1076,7 +1073,8 @@
"placeholder": "Введіть тут вміст вашої нотатки з кодом..."
},
"editable_text": {
"placeholder": "Введіть тут вміст вашої нотатки..."
"placeholder": "Введіть тут вміст вашої нотатки...",
"auto-detect-language": "Автовизначено"
},
"empty": {
"open_note_instruction": "Відкрийте нотатку, ввівши її заголовок в поле нижче, або виберіть нотатку в дереві.",

View File

@@ -16,4 +16,5 @@ body.zen div.read-only-note-info-bar-widget {
:root div.read-only-note-info-bar-widget button {
white-space: nowrap;
padding: 2px 8px;
flex-shrink: 0;
}

View File

@@ -0,0 +1,69 @@
#toast-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: absolute;
width: 100%;
top: 20px;
pointer-events: none;
contain: none;
}
.toast {
--bs-toast-bg: var(--accented-background-color);
--bs-toast-color: var(--main-text-color);
z-index: 9999999999 !important;
pointer-events: all;
overflow: hidden;
}
.toast-header {
background-color: var(--more-accented-background-color) !important;
color: var(--main-text-color) !important;
}
.toast-body {
white-space: preserve-breaks;
overflow: hidden;
}
.toast.no-title {
display: flex;
flex-direction: row;
}
.toast.no-title .toast-icon {
display: flex;
align-items: center;
padding: var(--bs-toast-padding-y) var(--bs-toast-padding-x);
}
.toast.no-title .toast-body {
padding-inline-start: 0;
padding-inline-end: 0;
}
.toast.no-title .toast-header {
background-color: unset !important;
}
.toast {
.toast-buttons {
padding: 0 1em 1em 1em;
display: flex;
gap: 1em;
justify-content: space-between;
}
.toast-progress {
position: absolute;
bottom: 0;
inset-inline-start: 0;
inset-inline-end: 0;
background-color: var(--toast-text-color) !important;
height: 4px;
transition: width 0.1s linear;
}
}

View File

@@ -0,0 +1,75 @@
import "./Toast.css";
import clsx from "clsx";
import { useEffect } from "preact/hooks";
import { removeToastFromStore, ToastOptionsWithRequiredId, toasts } from "../services/toast";
import Icon from "./react/Icon";
import { RawHtmlBlock } from "./react/RawHtml";
import Button from "./react/Button";
export default function ToastContainer() {
return (
<div id="toast-container">
{toasts.value.map(toast => <Toast key={toast.id} {...toast} />)}
</div>
)
}
function Toast({ id, title, timeout, progress, message, icon, buttons }: ToastOptionsWithRequiredId) {
// Autohide.
useEffect(() => {
if (!timeout || timeout <= 0) return;
const timerId = setTimeout(() => removeToastFromStore(id), timeout);
return () => clearTimeout(timerId);
}, [ id, timeout ]);
function dismissToast() {
removeToastFromStore(id);
}
const closeButton = (
<button
type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"
onClick={dismissToast}
/>
);
const toastIcon = <Icon icon={icon.startsWith("bx ") ? icon : `bx bx-${icon}`} />;
return (
<div
class={clsx("toast", !title && "no-title")}
role="alert" aria-live="assertive" aria-atomic="true"
id={`toast-${id}`}
>
{title ? (
<div class="toast-header">
<strong class="me-auto">
{toastIcon}
<span class="toast-title">{title}</span>
</strong>
{closeButton}
</div>
) : (
<div class="toast-icon">{toastIcon}</div>
)}
<RawHtmlBlock className="toast-body" html={message} />
{!title && <div class="toast-header">{closeButton}</div>}
{buttons && (
<div class="toast-buttons">
{buttons.map(({ text, onClick }) => (
<Button text={text} onClick={() => onClick({ dismissToast })} />
))}
</div>
)}
<div
class="toast-progress"
style={{ width: `${(progress ?? 0) * 100}%` }}
/>
</div>
)
}

View File

@@ -169,29 +169,30 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
console.log("Got issue in widget ", this);
console.error(e);
let noteId = this._noteId;
const noteId = this._noteId;
if (this._noteId) {
froca.getNote(noteId, true).then((note) => {
toastService.showPersistent({
id: `custom-widget-failure-${noteId}`,
title: t("toast.widget-error.title"),
icon: "alert",
icon: "bx bx-error-circle",
message: t("toast.widget-error.message-custom", {
id: noteId,
title: note?.title,
message: e.message
message: e.message || e.toString()
})
});
});
return;
} else {
toastService.showPersistent({
id: `custom-widget-failure-unknown-${crypto.randomUUID()}`,
title: t("toast.widget-error.title"),
icon: "bx bx-error-circle",
message: t("toast.widget-error.message-unknown", {
message: e.message || e.toString()
})
});
}
toastService.showPersistent({
title: t("toast.widget-error.title"),
icon: "alert",
message: t("toast.widget-error.message-unknown", {
message: e.message
})
});
}
/**

View File

@@ -1,78 +0,0 @@
import FlexContainer from "./containers/flex_container.js";
import OpenNoteButtonWidget from "./buttons/open_note_button_widget.js";
import BookmarkFolderWidget from "./buttons/bookmark_folder.js";
import froca from "../services/froca.js";
import utils from "../services/utils.js";
import type { EventData } from "../components/app_context.js";
import type Component from "../components/component.js";
interface BookmarkButtonsSettings {
titlePlacement?: string;
}
export default class BookmarkButtons extends FlexContainer<Component> {
private settings: BookmarkButtonsSettings;
private noteIds: string[];
constructor(isHorizontalLayout: boolean) {
super(isHorizontalLayout ? "row" : "column");
this.contentSized();
this.settings = {};
this.noteIds = [];
}
async refresh(): Promise<void> {
this.$widget.empty();
this.children = [];
this.noteIds = [];
const bookmarkParentNote = await froca.getNote("_lbBookmarks");
if (!bookmarkParentNote) {
return;
}
for (const note of await bookmarkParentNote.getChildNotes()) {
this.noteIds.push(note.noteId);
let buttonWidget: OpenNoteButtonWidget | BookmarkFolderWidget = note.isLabelTruthy("bookmarkFolder")
? new BookmarkFolderWidget(note)
: new OpenNoteButtonWidget(note).class("launcher-button");
if (this.settings.titlePlacement) {
if (!("settings" in buttonWidget)) {
(buttonWidget as any).settings = {};
}
(buttonWidget as any).settings.titlePlacement = this.settings.titlePlacement;
}
this.child(buttonWidget);
this.$widget.append(buttonWidget.render());
buttonWidget.refreshIcon();
}
utils.reloadTray();
}
initialRenderCompleteEvent(): void {
this.refresh();
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">): void {
if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === "_lbBookmarks")) {
this.refresh();
}
if (loadResults.getAttributeRows().find((attr) =>
attr.type === "label" &&
attr.name && ["iconClass", "workspaceIconClass", "bookmarkFolder"].includes(attr.name) &&
attr.noteId && this.noteIds.includes(attr.noteId)
)) {
this.refresh();
}
}
}

View File

@@ -1,26 +0,0 @@
import type { EventData } from "../../components/app_context.js";
import type FNote from "../../entities/fnote.js";
import options from "../../services/options.js";
import CommandButtonWidget from "./command_button.js";
export default class AiChatButton extends CommandButtonWidget {
constructor(note: FNote) {
super();
this.command("createAiChat")
.title(() => note.title)
.icon(() => note.getIcon())
.class("launcher-button");
}
isEnabled() {
return options.get("aiEnabled") === "true";
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isOptionReloaded("aiEnabled")) {
this.refresh();
}
}
}

View File

@@ -1,88 +0,0 @@
import RightDropdownButtonWidget from "./right_dropdown_button.js";
import linkService from "../../services/link.js";
import utils from "../../services/utils.js";
import type FNote from "../../entities/fnote.js";
const DROPDOWN_TPL = `
<div class="bookmark-folder-widget">
<style>
.bookmark-folder-widget {
min-width: 400px;
max-height: 500px;
padding: 7px 15px 0 15px;
font-size: 1.2rem;
overflow: auto;
}
.bookmark-folder-widget ul {
padding: 0;
list-style-type: none;
}
.bookmark-folder-widget .note-link {
display: block;
padding: 5px 10px 5px 5px;
}
.bookmark-folder-widget .note-link:hover {
background-color: var(--accented-background-color);
text-decoration: none;
}
.dropdown-menu .bookmark-folder-widget a:hover {
text-decoration: none;
background: transparent !important;
}
.bookmark-folder-widget li .note-link {
padding-inline-start: 35px;
}
</style>
<div class="parent-note"></div>
<ul class="children-notes"></ul>
</div>`;
interface LinkOptions {
showTooltip: boolean;
showNoteIcon: boolean;
}
export default class BookmarkFolderWidget extends RightDropdownButtonWidget {
private note: FNote;
private $parentNote!: JQuery<HTMLElement>;
private $childrenNotes!: JQuery<HTMLElement>;
declare $dropdownContent: JQuery<HTMLElement>;
constructor(note: FNote) {
super(utils.escapeHtml(note.title), note.getIcon(), DROPDOWN_TPL);
this.note = note;
}
doRender(): void {
super.doRender();
this.$parentNote = this.$dropdownContent.find(".parent-note");
this.$childrenNotes = this.$dropdownContent.find(".children-notes");
}
async dropdownShown(): Promise<void> {
this.$parentNote.empty();
this.$childrenNotes.empty();
const linkOptions: LinkOptions = {
showTooltip: false,
showNoteIcon: true
};
this.$parentNote.append((await linkService.createLink(this.note.noteId, linkOptions)).addClass("note-link"));
for (const childNote of await this.note.getChildNotes()) {
this.$childrenNotes.append($("<li>").append((await linkService.createLink(childNote.noteId, linkOptions)).addClass("note-link")));
}
}
refreshIcon(): void {}
}

View File

@@ -1,63 +0,0 @@
import froca from "../../services/froca.js";
import attributeService from "../../services/attributes.js";
import CommandButtonWidget from "./command_button.js";
import type { EventData } from "../../components/app_context.js";
export type ButtonNoteIdProvider = () => string;
export default class ButtonFromNoteWidget extends CommandButtonWidget {
constructor() {
super();
this.settings.buttonNoteIdProvider = null;
}
buttonNoteIdProvider(provider: ButtonNoteIdProvider) {
this.settings.buttonNoteIdProvider = provider;
return this;
}
doRender() {
super.doRender();
this.updateIcon();
}
updateIcon() {
if (!this.settings.buttonNoteIdProvider) {
console.error(`buttonNoteId for '${this.componentId}' is not defined.`);
return;
}
const buttonNoteId = this.settings.buttonNoteIdProvider();
if (!buttonNoteId) {
console.error(`buttonNoteId for '${this.componentId}' is not defined.`);
return;
}
froca.getNote(buttonNoteId).then((note) => {
const icon = note?.getIcon();
if (icon) {
this.settings.icon = icon;
}
this.refreshIcon();
});
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
// TODO: this seems incorrect
//@ts-ignore
const buttonNote = froca.getNoteFromCache(this.buttonNoteIdProvider());
if (!buttonNote) {
return;
}
if (loadResults.getAttributeRows(this.componentId).find((attr) => attr.type === "label" && attr.name === "iconClass" && attributeService.isAffecting(attr, buttonNote))) {
this.updateIcon();
}
}
}

View File

@@ -1,420 +0,0 @@
import { t } from "../../services/i18n.js";
import dateNoteService from "../../services/date_notes.js";
import server from "../../services/server.js";
import appContext from "../../components/app_context.js";
import RightDropdownButtonWidget from "./right_dropdown_button.js";
import toastService from "../../services/toast.js";
import options from "../../services/options.js";
import { Dropdown } from "bootstrap";
import type { EventData } from "../../components/app_context.js";
import { dayjs, type Dayjs } from "@triliumnext/commons";
import "../../stylesheets/calendar.css";
import type { AttributeRow, OptionDefinitions } from "@triliumnext/commons";
const MONTHS = [
t("calendar.january"),
t("calendar.february"),
t("calendar.march"),
t("calendar.april"),
t("calendar.may"),
t("calendar.june"),
t("calendar.july"),
t("calendar.august"),
t("calendar.september"),
t("calendar.october"),
t("calendar.november"),
t("calendar.december")
];
const DROPDOWN_TPL = `
<div class="calendar-dropdown-widget">
<style>
.calendar-dropdown-widget {
width: 400px;
}
</style>
<div class="calendar-header">
<div class="calendar-month-selector">
<button class="calendar-btn tn-tool-button bx bx-chevron-left" data-calendar-toggle="previous"></button>
<button class="btn dropdown-toggle select-button" type="button"
data-bs-toggle="dropdown" data-bs-auto-close="true"
aria-expanded="false"
data-calendar-input="month"></button>
<ul class="dropdown-menu" data-calendar-input="month-list">
${Object.entries(MONTHS)
.map(([i, month]) => `<li><button class="dropdown-item" data-value=${i}>${month}</button></li>`)
.join("")}
</ul>
<button class="calendar-btn tn-tool-button bx bx-chevron-right" data-calendar-toggle="next"></button>
</div>
<div class="calendar-year-selector">
<button class="calendar-btn tn-tool-button bx bx-chevron-left" data-calendar-toggle="previousYear"></button>
<input type="number" min="1900" max="2999" step="1" data-calendar-input="year" />
<button class="calendar-btn tn-tool-button bx bx-chevron-right" data-calendar-toggle="nextYear"></button>
</div>
</div>
<div class="calendar-week"></div>
<div class="calendar-body" data-calendar-area="month"></div>
</div>`;
const DAYS_OF_WEEK = [
t("calendar.sun"),
t("calendar.mon"),
t("calendar.tue"),
t("calendar.wed"),
t("calendar.thu"),
t("calendar.fri"),
t("calendar.sat")
];
interface DateNotesForMonth {
[date: string]: string;
}
interface WeekCalculationOptions {
firstWeekType: number;
minDaysInFirstWeek: number;
}
export default class CalendarWidget extends RightDropdownButtonWidget {
private $month!: JQuery<HTMLElement>;
private $weekHeader!: JQuery<HTMLElement>;
private $monthSelect!: JQuery<HTMLElement>;
private $yearSelect!: JQuery<HTMLElement>;
private $next!: JQuery<HTMLElement>;
private $previous!: JQuery<HTMLElement>;
private $nextYear!: JQuery<HTMLElement>;
private $previousYear!: JQuery<HTMLElement>;
private monthDropdown!: Dropdown;
// stored in ISO 17
private firstDayOfWeekISO!: number;
private weekCalculationOptions!: WeekCalculationOptions;
private activeDate: Dayjs | null = null;
private todaysDate!: Dayjs;
private date!: Dayjs;
private weekNoteEnable: boolean = false;
private weekNotes: string[] = [];
constructor(title: string = "", icon: string = "") {
super(title, icon, DROPDOWN_TPL, "calendar-dropdown-menu");
}
doRender() {
super.doRender();
this.$month = this.$dropdownContent.find('[data-calendar-area="month"]');
this.$weekHeader = this.$dropdownContent.find(".calendar-week");
this.manageFirstDayOfWeek();
this.initWeekCalculation();
// Month navigation
this.$monthSelect = this.$dropdownContent.find('[data-calendar-input="month"]');
this.$monthSelect.on("show.bs.dropdown", (e) => {
// Don't trigger dropdownShown() at widget level when the month selection dropdown is shown, since it would cause a redundant refresh.
e.stopPropagation();
});
this.monthDropdown = Dropdown.getOrCreateInstance(this.$monthSelect[0]);
this.$dropdownContent.find('[data-calendar-input="month-list"] button').on("click", (e) => {
const target = e.target as HTMLElement;
const value = target.dataset.value;
if (value) {
this.date = this.date.month(parseInt(value));
this.createMonth();
}
});
this.$next = this.$dropdownContent.find('[data-calendar-toggle="next"]');
this.$next.on("click", () => {
this.date = this.date.add(1, 'month');
this.createMonth();
});
this.$previous = this.$dropdownContent.find('[data-calendar-toggle="previous"]');
this.$previous.on("click", () => {
this.date = this.date.subtract(1, 'month');
this.createMonth();
});
// Year navigation
this.$yearSelect = this.$dropdownContent.find('[data-calendar-input="year"]');
this.$yearSelect.on("input", (e) => {
const target = e.target as HTMLInputElement;
this.date = this.date.year(parseInt(target.value));
this.createMonth();
});
this.$nextYear = this.$dropdownContent.find('[data-calendar-toggle="nextYear"]');
this.$nextYear.on("click", () => {
this.date = this.date.add(1, 'year');
this.createMonth();
});
this.$previousYear = this.$dropdownContent.find('[data-calendar-toggle="previousYear"]');
this.$previousYear.on("click", () => {
this.date = this.date.subtract(1, 'year');
this.createMonth();
});
// Date click
this.$dropdownContent.on("click", ".calendar-date", async (ev) => {
const date = $(ev.target).closest(".calendar-date").attr("data-calendar-date");
if (date) {
const note = await dateNoteService.getDayNote(date);
if (note) {
appContext.tabManager.getActiveContext()?.setNote(note.noteId);
this.dropdown?.hide();
} else {
toastService.showError(t("calendar.cannot_find_day_note"));
}
}
ev.stopPropagation();
});
// Week click
this.$dropdownContent.on("click", ".calendar-week-number", async (ev) => {
if (!this.weekNoteEnable) {
return;
}
const week = $(ev.target).closest(".calendar-week-number").attr("data-calendar-week-number");
if (week) {
const note = await dateNoteService.getWeekNote(week);
if (note) {
appContext.tabManager.getActiveContext()?.setNote(note.noteId);
this.dropdown?.hide();
} else {
toastService.showError(t("calendar.cannot_find_week_note"));
}
}
ev.stopPropagation();
});
// Handle click events for the entire calendar widget
this.$dropdownContent.on("click", (e) => {
const $target = $(e.target);
// Keep dropdown open when clicking on month select button or year selector area
if ($target.closest('.btn.dropdown-toggle.select-button').length) {
e.stopPropagation();
return;
}
// Hide dropdown for all other cases
this.monthDropdown.hide();
// Prevent dismissing the calendar popup by clicking on an empty space inside it.
e.stopPropagation();
});
}
private async getWeekNoteEnable() {
const noteId = await server.get<string[]>(`search/${encodeURIComponent('#calendarRoot')}`);
if (noteId.length === 0) {
this.weekNoteEnable = false;
return;
}
const noteAttributes = await server.get<AttributeRow[]>(`notes/${noteId}/attributes`);
this.weekNoteEnable = noteAttributes.some(a => a.name === 'enableWeekNote');
}
// Store firstDayOfWeek as ISO (17)
manageFirstDayOfWeek() {
const rawFirstDayOfWeek = options.getInt("firstDayOfWeek") || 0;
this.firstDayOfWeekISO = rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek;
let localeDaysOfWeek = [...DAYS_OF_WEEK];
const shifted = localeDaysOfWeek.splice(0, rawFirstDayOfWeek);
localeDaysOfWeek = ['', ...localeDaysOfWeek, ...shifted];
this.$weekHeader.html(localeDaysOfWeek.map((el) => `<span>${el}</span>`).join(''));
}
initWeekCalculation() {
this.weekCalculationOptions = {
firstWeekType: options.getInt("firstWeekOfYear") || 0,
minDaysInFirstWeek: options.getInt("minDaysInFirstWeek") || 4
};
}
getWeekStartDate(date: Dayjs): Dayjs {
const currentISO = date.isoWeekday();
const diff = (currentISO - this.firstDayOfWeekISO + 7) % 7;
return date.clone().subtract(diff, "day").startOf("day");
}
getWeekNumber(date: Dayjs): number {
const weekStart = this.getWeekStartDate(date);
return weekStart.isoWeek();
}
async dropdownShown() {
await this.getWeekNoteEnable();
this.weekNotes = await server.get<string[]>(`attribute-values/weekNote`);
this.init(appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote") ?? null);
}
init(activeDate: string | null) {
this.activeDate = activeDate ? dayjs(`${activeDate}T12:00:00`) : null;
this.todaysDate = dayjs();
this.date = dayjs(this.activeDate || this.todaysDate).startOf('month');
this.createMonth();
}
createDay(dateNotesForMonth: DateNotesForMonth, num: number) {
const $newDay = $("<a>")
.addClass("calendar-date")
.attr("data-calendar-date", this.date.local().format('YYYY-MM-DD'));
const $date = $("<span>").html(String(num));
const dateNoteId = dateNotesForMonth[this.date.local().format('YYYY-MM-DD')];
if (dateNoteId) {
$newDay.addClass("calendar-date-exists").attr("data-href", `#root/${dateNoteId}`);
}
if (this.date.isSame(this.activeDate, 'day')) $newDay.addClass("calendar-date-active");
if (this.date.isSame(this.todaysDate, 'day')) $newDay.addClass("calendar-date-today");
$newDay.append($date);
return $newDay;
}
createWeekNumber(weekNumber: number) {
const weekNoteId = this.date.local().format('YYYY-') + 'W' + String(weekNumber).padStart(2, '0');
let $newWeekNumber;
if (this.weekNoteEnable) {
$newWeekNumber = $("<a>").addClass("calendar-date");
if (this.weekNotes.includes(weekNoteId)) {
$newWeekNumber.addClass("calendar-date-exists").attr("data-href", `#root/${weekNoteId}`);
}
} else {
$newWeekNumber = $("<span>").addClass("calendar-week-number-disabled");
}
$newWeekNumber.addClass("calendar-week-number").attr("data-calendar-week-number", weekNoteId);
$newWeekNumber.append($("<span>").html(String(weekNumber)));
return $newWeekNumber;
}
// Use isoWeekday() consistently
private getPrevMonthDays(firstDayISO: number): { weekNumber: number, dates: Dayjs[] } {
const prevMonthLastDay = this.date.subtract(1, 'month').endOf('month');
const daysToAdd = (firstDayISO - this.firstDayOfWeekISO + 7) % 7;
const dates: Dayjs[] = [];
const firstDay = this.date.startOf('month');
const weekNumber = this.getWeekNumber(firstDay);
// Get dates from previous month
for (let i = daysToAdd - 1; i >= 0; i--) {
dates.push(prevMonthLastDay.subtract(i, 'day'));
}
return { weekNumber, dates };
}
private getNextMonthDays(lastDayISO: number): Dayjs[] {
const nextMonthFirstDay = this.date.add(1, 'month').startOf('month');
const dates: Dayjs[] = [];
const lastDayOfUserWeek = ((this.firstDayOfWeekISO + 6 - 1) % 7) + 1; // ISO wrap
const daysToAdd = (lastDayOfUserWeek - lastDayISO + 7) % 7;
for (let i = 0; i < daysToAdd; i++) {
dates.push(nextMonthFirstDay.add(i, 'day'));
}
return dates;
}
async createMonth() {
const month = this.date.format('YYYY-MM');
const dateNotesForMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${month}`);
this.$month.empty();
const firstDay = this.date.startOf('month');
const firstDayISO = firstDay.isoWeekday();
// Previous month filler
if (firstDayISO !== this.firstDayOfWeekISO) {
const { weekNumber, dates } = this.getPrevMonthDays(firstDayISO);
const prevMonth = this.date.subtract(1, 'month').format('YYYY-MM');
const dateNotesForPrevMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${prevMonth}`);
const $weekNumber = this.createWeekNumber(weekNumber);
this.$month.append($weekNumber);
dates.forEach(date => {
const tempDate = this.date;
this.date = date;
const $day = this.createDay(dateNotesForPrevMonth, date.date());
$day.addClass('calendar-date-prev-month');
this.$month.append($day);
this.date = tempDate;
});
}
const currentMonth = this.date.month();
// Main month
while (this.date.month() === currentMonth) {
const weekNumber = this.getWeekNumber(this.date);
if (this.date.isoWeekday() === this.firstDayOfWeekISO) {
const $weekNumber = this.createWeekNumber(weekNumber);
this.$month.append($weekNumber);
}
const $day = this.createDay(dateNotesForMonth, this.date.date());
this.$month.append($day);
this.date = this.date.add(1, 'day');
}
// while loop trips over and day is at 30/31, bring it back
this.date = this.date.startOf('month').subtract(1, 'month');
// Add dates from next month
const lastDayOfMonth = this.date.endOf('month');
const lastDayISO = lastDayOfMonth.isoWeekday();
const lastDayOfUserWeek = ((this.firstDayOfWeekISO + 6 - 1) % 7) + 1;
if (lastDayISO !== lastDayOfUserWeek) {
const dates = this.getNextMonthDays(lastDayISO);
const nextMonth = this.date.add(1, 'month').format('YYYY-MM');
const dateNotesForNextMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${nextMonth}`);
dates.forEach(date => {
const tempDate = this.date;
this.date = date;
const $day = this.createDay(dateNotesForNextMonth, date.date());
$day.addClass('calendar-date-next-month');
this.$month.append($day);
this.date = tempDate;
});
}
this.$monthSelect.text(MONTHS[this.date.month()]);
this.$yearSelect.val(this.date.year());
}
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
const WEEK_OPTIONS: (keyof OptionDefinitions)[] = [
"firstDayOfWeek",
"firstWeekOfYear",
"minDaysInFirstWeek",
];
if (!WEEK_OPTIONS.some(opt => loadResults.getOptionNames().includes(opt))) {
return;
}
this.manageFirstDayOfWeek();
this.initWeekCalculation();
this.createMonth();
}
}

View File

@@ -1,73 +0,0 @@
import { ActionKeyboardShortcut } from "@triliumnext/commons";
import type { CommandNames } from "../../components/app_context.js";
import keyboardActionsService from "../../services/keyboard_actions.js";
import AbstractButtonWidget, { type AbstractButtonWidgetSettings } from "./abstract_button.js";
import type { ButtonNoteIdProvider } from "./button_from_note.js";
let actions: ActionKeyboardShortcut[];
keyboardActionsService.getActions().then((as) => (actions = as));
// TODO: Is this actually used?
export type ClickHandler = (widget: CommandButtonWidget, e: JQuery.ClickEvent<any, any, any, any>) => void;
type CommandOrCallback = CommandNames | (() => CommandNames);
interface CommandButtonWidgetSettings extends AbstractButtonWidgetSettings {
command?: CommandOrCallback;
onClick?: ClickHandler;
buttonNoteIdProvider?: ButtonNoteIdProvider | null;
}
export default class CommandButtonWidget extends AbstractButtonWidget<CommandButtonWidgetSettings> {
constructor() {
super();
this.settings = {
titlePlacement: "right",
title: null,
icon: null,
onContextMenu: null
};
}
doRender() {
super.doRender();
if (this.settings.command) {
this.$widget.on("click", () => {
this.tooltip.hide();
if (this._command) {
this.triggerCommand(this._command);
}
});
} else {
console.warn(`Button widget '${this.componentId}' has no defined command`, this.settings);
}
}
getTitle() {
const title = super.getTitle();
const action = actions.find((act) => act.actionName === this._command);
if (action?.effectiveShortcuts && action.effectiveShortcuts.length > 0) {
return `${title} (${action.effectiveShortcuts.join(", ")})`;
} else {
return title;
}
}
onClick(handler: ClickHandler) {
this.settings.onClick = handler;
return this;
}
command(command: CommandOrCallback) {
this.settings.command = command;
return this;
}
get _command() {
return typeof this.settings.command === "function" ? this.settings.command() : this.settings.command;
}
}

View File

@@ -1,90 +0,0 @@
import utils from "../../services/utils.js";
import contextMenu, { MenuCommandItem } from "../../menus/context_menu.js";
import treeService from "../../services/tree.js";
import ButtonFromNoteWidget from "./button_from_note.js";
import type FNote from "../../entities/fnote.js";
import type { CommandNames } from "../../components/app_context.js";
import type { WebContents } from "electron";
import link from "../../services/link.js";
export default class HistoryNavigationButton extends ButtonFromNoteWidget {
private webContents?: WebContents;
constructor(launcherNote: FNote, command: string) {
super();
this.title(() => launcherNote.title)
.icon(() => launcherNote.getIcon())
.command(() => command as CommandNames)
.titlePlacement("right")
.buttonNoteIdProvider(() => launcherNote.noteId)
.onContextMenu((e) => { if (e) this.showContextMenu(e); })
.class("launcher-button");
}
doRender() {
super.doRender();
if (utils.isElectron()) {
this.webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents();
// without this, the history is preserved across frontend reloads
this.webContents?.clearHistory();
this.refresh();
}
}
async showContextMenu(e: JQuery.ContextMenuEvent) {
e.preventDefault();
if (!this.webContents || this.webContents.navigationHistory.length() < 2) {
return;
}
let items: MenuCommandItem<string>[] = [];
const history = this.webContents.navigationHistory.getAllEntries();
const activeIndex = this.webContents.navigationHistory.getActiveIndex();
for (const idx in history) {
const { notePath } = link.parseNavigationStateFromUrl(history[idx].url);
if (!notePath) continue;
const title = await treeService.getNotePathTitle(notePath);
items.push({
title,
command: idx,
uiIcon:
parseInt(idx) === activeIndex
? "bx bx-radio-circle-marked" // compare with type coercion!
: parseInt(idx) < activeIndex
? "bx bx-left-arrow-alt"
: "bx bx-right-arrow-alt"
});
}
items.reverse();
if (items.length > 20) {
items = items.slice(0, 50);
}
contextMenu.show({
x: e.pageX,
y: e.pageY,
items,
selectMenuItemHandler: (item: MenuCommandItem<string>) => {
if (item && item.command && this.webContents) {
const idx = parseInt(item.command, 10);
this.webContents.navigationHistory.goToIndex(idx);
}
}
});
}
activeNoteChangedEvent() {
this.refresh();
}
}

View File

@@ -1,99 +0,0 @@
import { t } from "../../../services/i18n.js";
import AbstractLauncher from "./abstract_launcher.js";
import dialogService from "../../../services/dialog.js";
import appContext from "../../../components/app_context.js";
import utils from "../../../services/utils.js";
import linkContextMenuService from "../../../menus/link_context_menu.js";
import type FNote from "../../../entities/fnote.js";
// we're intentionally displaying the launcher title and icon instead of the target,
// e.g. you want to make launchers to 2 mermaid diagrams which both have mermaid icon (ok),
// but on the launchpad you want them distinguishable.
// for titles, the note titles may follow a different scheme than maybe desirable on the launchpad
// another reason is the discrepancy between what user sees on the launchpad and in the config (esp. icons).
// The only downside is more work in setting up the typical case
// where you actually want to have both title and icon in sync, but for those cases there are bookmarks
export default class NoteLauncher extends AbstractLauncher {
constructor(launcherNote: FNote) {
super(launcherNote);
this.title(() => this.launcherNote.title)
.icon(() => this.launcherNote.getIcon())
.onClick((widget, evt) => this.launch(evt))
.onAuxClick((widget, evt) => this.launch(evt))
.onContextMenu(async (evt) => {
let targetNoteId = await Promise.resolve(this.getTargetNoteId());
if (!targetNoteId || !evt) {
return;
}
const hoistedNoteId = this.getHoistedNoteId();
linkContextMenuService.openContextMenu(targetNoteId, evt, {}, hoistedNoteId);
});
}
async launch(evt?: JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent) {
// await because subclass overrides can be async
const targetNoteId = await this.getTargetNoteId();
if (!targetNoteId || evt?.which === 3) {
return;
}
const hoistedNoteId = await this.getHoistedNoteId();
if (!hoistedNoteId) {
return;
}
if (!evt) {
// keyboard shortcut
// TODO: Fix once tabManager is ported.
//@ts-ignore
await appContext.tabManager.openInSameTab(targetNoteId, hoistedNoteId);
} else {
const ctrlKey = utils.isCtrlKey(evt);
const activate = evt.shiftKey ? true : false;
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
// TODO: Fix once tabManager is ported.
//@ts-ignore
await appContext.tabManager.openInNewTab(targetNoteId, hoistedNoteId, activate);
} else {
// TODO: Fix once tabManager is ported.
//@ts-ignore
await appContext.tabManager.openInSameTab(targetNoteId, hoistedNoteId);
}
}
}
getTargetNoteId(): void | string | Promise<string | undefined> {
const targetNoteId = this.launcherNote.getRelationValue("target");
if (!targetNoteId) {
dialogService.info(t("note_launcher.this_launcher_doesnt_define_target_note"));
return;
}
return targetNoteId;
}
getHoistedNoteId() {
return this.launcherNote.getRelationValue("hoistedNote") || appContext.tabManager.getActiveContext()?.hoistedNoteId;
}
getTitle() {
const shortcuts = this.launcherNote
.getLabels("keyboardShortcut")
.map((l) => l.value)
.filter((v) => !!v)
.join(", ");
let title = super.getTitle();
if (shortcuts) {
title += ` (${shortcuts})`;
}
return title;
}
}

View File

@@ -1,23 +0,0 @@
import type FNote from "../../../entities/fnote.js";
import AbstractLauncher from "./abstract_launcher.js";
export default class ScriptLauncher extends AbstractLauncher {
constructor(launcherNote: FNote) {
super(launcherNote);
this.title(() => this.launcherNote.title)
.icon(() => this.launcherNote.getIcon())
.onClick(() => this.launch());
}
async launch() {
if (this.launcherNote.isLabelTruthy("scriptInLauncherContent")) {
await this.launcherNote.executeScript();
} else {
const script = await this.launcherNote.getRelationTarget("script");
if (script) {
await script.executeScript();
}
}
}
}

View File

@@ -1,15 +0,0 @@
import NoteLauncher from "./note_launcher.js";
import dateNotesService from "../../../services/date_notes.js";
import appContext from "../../../components/app_context.js";
export default class TodayLauncher extends NoteLauncher {
async getTargetNoteId() {
const todayNote = await dateNotesService.getTodayNote();
return todayNote?.noteId;
}
getHoistedNoteId() {
return appContext.tabManager.getActiveContext()?.hoistedNoteId;
}
}

View File

@@ -1,49 +0,0 @@
import OnClickButtonWidget from "./onclick_button.js";
import linkContextMenuService from "../../menus/link_context_menu.js";
import utils from "../../services/utils.js";
import appContext from "../../components/app_context.js";
import type FNote from "../../entities/fnote.js";
export default class OpenNoteButtonWidget extends OnClickButtonWidget {
private noteToOpen: FNote;
constructor(noteToOpen: FNote) {
super();
this.noteToOpen = noteToOpen;
this.title(() => utils.escapeHtml(this.noteToOpen.title))
.icon(() => this.noteToOpen.getIcon())
.onClick((widget, evt) => this.launch(evt))
.onAuxClick((widget, evt) => this.launch(evt))
.onContextMenu((evt) => {
if (evt) {
linkContextMenuService.openContextMenu(this.noteToOpen.noteId, evt);
}
});
}
async launch(evt: JQuery.ClickEvent | JQuery.TriggeredEvent | JQuery.ContextMenuEvent) {
if (evt.which === 3) {
return;
}
const hoistedNoteId = this.getHoistedNoteId();
const ctrlKey = utils.isCtrlKey(evt);
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
const activate = evt.shiftKey ? true : false;
await appContext.tabManager.openInNewTab(this.noteToOpen.noteId, hoistedNoteId, activate);
} else {
await appContext.tabManager.openInSameTab(this.noteToOpen.noteId);
}
}
getHoistedNoteId() {
return this.noteToOpen.getRelationValue("hoistedNote") || appContext.tabManager.getActiveContext()?.hoistedNoteId;
}
initialRenderCompleteEvent() {
// we trigger refresh above
}
}

View File

@@ -1,21 +0,0 @@
import { t } from "../../services/i18n.js";
import protectedSessionHolder from "../../services/protected_session_holder.js";
import CommandButtonWidget from "./command_button.js";
export default class ProtectedSessionStatusWidget extends CommandButtonWidget {
constructor() {
super();
this.class("launcher-button");
this.settings.icon = () => (protectedSessionHolder.isProtectedSessionAvailable() ? "bx-check-shield" : "bx-shield-quarter");
this.settings.title = () => (protectedSessionHolder.isProtectedSessionAvailable() ? t("protected_session_status.active") : t("protected_session_status.inactive"));
this.settings.command = () => (protectedSessionHolder.isProtectedSessionAvailable() ? "leaveProtectedSession" : "enterProtectedSession");
}
protectedSessionStartedEvent() {
this.refreshIcon();
}
}

View File

@@ -1,86 +0,0 @@
import { handleRightToLeftPlacement } from "../../services/utils.js";
import BasicWidget from "../basic_widget.js";
import { Tooltip, Dropdown } from "bootstrap";
type PopoverPlacement = Tooltip.PopoverPlacement;
const TPL = /*html*/`
<div class="dropdown right-dropdown-widget">
<button type="button" data-bs-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"
class="bx right-dropdown-button launcher-button">
<div class="tooltip-trigger"></div>
</button>
<div class="dropdown-menu"></div>
</div>
`;
export default class RightDropdownButtonWidget extends BasicWidget {
protected iconClass: string;
protected title: string;
protected dropdownTpl: string;
protected settings: { titlePlacement: PopoverPlacement };
protected $dropdownMenu!: JQuery<HTMLElement>;
protected dropdown!: Dropdown;
protected $tooltip!: JQuery<HTMLElement>;
protected tooltip!: Tooltip;
private dropdownClass?: string;
public $dropdownContent!: JQuery<HTMLElement>;
constructor(title: string, iconClass: string, dropdownTpl: string, dropdownClass?: string) {
super();
this.iconClass = iconClass;
this.title = title;
this.dropdownTpl = dropdownTpl;
this.dropdownClass = dropdownClass;
this.settings = {
titlePlacement: "right"
};
}
doRender() {
this.$widget = $(TPL);
this.$dropdownMenu = this.$widget.find(".dropdown-menu");
if (this.dropdownClass) {
this.$dropdownMenu.addClass(this.dropdownClass);
}
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0], {
popperConfig: {
placement: this.settings.titlePlacement,
}
});
this.$tooltip = this.$widget.find(".tooltip-trigger").attr("title", this.title);
this.tooltip = new Tooltip(this.$tooltip[0], {
placement: handleRightToLeftPlacement(this.settings.titlePlacement),
fallbackPlacements: [ handleRightToLeftPlacement(this.settings.titlePlacement) ]
});
this.$widget
.find(".right-dropdown-button")
.addClass(this.iconClass)
.on("click", () => this.tooltip.hide())
.on("mouseenter", () => this.tooltip.show())
.on("mouseleave", () => this.tooltip.hide());
this.$widget.on("show.bs.dropdown", async () => {
await this.dropdownShown();
const rect = this.$dropdownMenu[0].getBoundingClientRect();
const windowHeight = $(window).height() || 0;
const pixelsToBottom = windowHeight - rect.bottom;
if (pixelsToBottom < 0) {
this.$dropdownMenu.css("top", pixelsToBottom);
}
});
this.$dropdownContent = $(this.dropdownTpl);
this.$widget.find(".dropdown-menu").append(this.$dropdownContent);
}
// to be overridden
async dropdownShown(): Promise<void> {}
}

View File

@@ -1,133 +0,0 @@
import CalendarWidget from "../buttons/calendar.js";
import SpacerWidget from "../spacer.js";
import BookmarkButtons from "../bookmark_buttons.js";
import ProtectedSessionStatusWidget from "../buttons/protected_session_status.js";
import SyncStatusWidget from "../sync_status.js";
import BasicWidget from "../basic_widget.js";
import NoteLauncher from "../buttons/launcher/note_launcher.js";
import ScriptLauncher from "../buttons/launcher/script_launcher.js";
import CommandButtonWidget from "../buttons/command_button.js";
import utils from "../../services/utils.js";
import TodayLauncher from "../buttons/launcher/today_launcher.js";
import HistoryNavigationButton from "../buttons/history_navigation.js";
import QuickSearchLauncherWidget from "../quick_search_launcher.js";
import type FNote from "../../entities/fnote.js";
import type { CommandNames } from "../../components/app_context.js";
import AiChatButton from "../buttons/ai_chat_button.js";
interface InnerWidget extends BasicWidget {
settings?: {
titlePlacement: "bottom";
};
}
export default class LauncherWidget extends BasicWidget {
private innerWidget!: InnerWidget;
private isHorizontalLayout: boolean;
constructor(isHorizontalLayout: boolean) {
super();
this.isHorizontalLayout = isHorizontalLayout;
}
isEnabled() {
return this.innerWidget.isEnabled();
}
doRender() {
this.$widget = this.innerWidget.render();
}
async initLauncher(note: FNote) {
if (note.type !== "launcher") {
throw new Error(`Note '${note.noteId}' '${note.title}' is not a launcher even though it's in the launcher subtree`);
}
if (!utils.isDesktop() && note.isLabelTruthy("desktopOnly")) {
return false;
}
const launcherType = note.getLabelValue("launcherType");
if (glob.TRILIUM_SAFE_MODE && launcherType === "customWidget") {
return false;
}
let widget: BasicWidget;
if (launcherType === "command") {
widget = this.initCommandLauncherWidget(note).class("launcher-button");
} else if (launcherType === "note") {
widget = new NoteLauncher(note).class("launcher-button");
} else if (launcherType === "script") {
widget = new ScriptLauncher(note).class("launcher-button");
} else if (launcherType === "customWidget") {
widget = await this.initCustomWidget(note);
} else if (launcherType === "builtinWidget") {
widget = this.initBuiltinWidget(note);
} else {
throw new Error(`Unrecognized launcher type '${launcherType}' for launcher '${note.noteId}' title '${note.title}'`);
}
if (!widget) {
throw new Error(`Unknown initialization error for note '${note.noteId}', title '${note.title}'`);
}
this.child(widget);
this.innerWidget = widget as InnerWidget;
if (this.isHorizontalLayout && this.innerWidget.settings) {
this.innerWidget.settings.titlePlacement = "bottom";
}
return true;
}
initCommandLauncherWidget(note: FNote) {
return new CommandButtonWidget()
.title(() => note.title)
.icon(() => note.getIcon())
.command(() => note.getLabelValue("command") as CommandNames);
}
async initCustomWidget(note: FNote) {
const widget = await note.getRelationTarget("widget");
if (widget) {
return await widget.executeScript();
} else {
throw new Error(`Custom widget of launcher '${note.noteId}' '${note.title}' is not defined.`);
}
}
initBuiltinWidget(note: FNote) {
const builtinWidget = note.getLabelValue("builtinWidget");
switch (builtinWidget) {
case "calendar":
return new CalendarWidget(note.title, note.getIcon());
case "spacer":
// || has to be inside since 0 is a valid value
const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100");
return new SpacerWidget(baseSize, growthFactor);
case "bookmarks":
return new BookmarkButtons(this.isHorizontalLayout);
case "protectedSession":
return new ProtectedSessionStatusWidget();
case "syncStatus":
return new SyncStatusWidget();
case "backInHistoryButton":
return new HistoryNavigationButton(note, "backInNoteHistory");
case "forwardInHistoryButton":
return new HistoryNavigationButton(note, "forwardInNoteHistory");
case "todayInJournal":
return new TodayLauncher(note);
case "quickSearch":
return new QuickSearchLauncherWidget(this.isHorizontalLayout);
case "aiChatLauncher":
return new AiChatButton(note);
default:
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
}
}
}

View File

@@ -1,78 +0,0 @@
import FlexContainer from "./flex_container.js";
import froca from "../../services/froca.js";
import appContext, { type EventData } from "../../components/app_context.js";
import LauncherWidget from "./launcher.js";
import utils from "../../services/utils.js";
export default class LauncherContainer extends FlexContainer<LauncherWidget> {
private isHorizontalLayout: boolean;
constructor(isHorizontalLayout: boolean) {
super(isHorizontalLayout ? "row" : "column");
this.id("launcher-container");
this.filling();
this.isHorizontalLayout = isHorizontalLayout;
this.load();
}
async load() {
await froca.initializedPromise;
const visibleLaunchersRootId = utils.isMobile() ? "_lbMobileVisibleLaunchers" : "_lbVisibleLaunchers";
const visibleLaunchersRoot = await froca.getNote(visibleLaunchersRootId, true);
if (!visibleLaunchersRoot) {
console.log("Visible launchers root note doesn't exist.");
return;
}
const newChildren: LauncherWidget[] = [];
for (const launcherNote of await visibleLaunchersRoot.getChildNotes()) {
try {
const launcherWidget = new LauncherWidget(this.isHorizontalLayout);
const success = await launcherWidget.initLauncher(launcherNote);
if (success) {
newChildren.push(launcherWidget);
}
} catch (e) {
console.error(e);
}
}
this.children = [];
this.child(...newChildren);
this.$widget.empty();
this.renderChildren();
await this.handleEventInChildren("initialRenderComplete", {});
const activeContext = appContext.tabManager.getActiveContext();
if (activeContext) {
await this.handleEvent("setNoteContext", {
noteContext: activeContext
});
if (activeContext.notePath) {
await this.handleEvent("noteSwitched", {
noteContext: activeContext,
notePath: activeContext.notePath
});
}
}
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.getBranchRows().find((branch) => branch.parentNoteId && froca.getNoteFromCache(branch.parentNoteId)?.isLaunchBarConfig())) {
// changes in note placement require reload of all launchers, all other changes are handled by individual
// launchers
this.load();
}
}
}

View File

@@ -4,12 +4,14 @@ import { t } from "../../services/i18n";
import { useState } from "preact/hooks";
import FormCheckbox from "../react/FormCheckbox";
import { useTriliumEvent } from "../react/hooks";
import { isValidElement, type VNode } from "preact";
import { RawHtmlBlock } from "../react/RawHtml";
interface ConfirmDialogProps {
title?: string;
message?: string | HTMLElement;
message?: MessageType;
callback?: ConfirmDialogCallback;
isConfirmDeleteNoteBox?: boolean;
isConfirmDeleteNoteBox?: boolean;
}
export default function ConfirmDialog() {
@@ -20,7 +22,7 @@ export default function ConfirmDialog() {
function showDialog(title: string | null, message: MessageType, callback: ConfirmDialogCallback, isConfirmDeleteNoteBox: boolean) {
setOpts({
title: title ?? undefined,
message: (typeof message === "object" && "length" in message ? message[0] : message),
message,
callback,
isConfirmDeleteNoteBox
});
@@ -30,7 +32,7 @@ export default function ConfirmDialog() {
useTriliumEvent("showConfirmDialog", ({ message, callback }) => showDialog(null, message, callback, false));
useTriliumEvent("showConfirmDeleteNoteBoxWithNoteDialog", ({ title, callback }) => showDialog(title, t("confirm.are_you_sure_remove_note", { title: title }), callback, true));
return (
return (
<Modal
className="confirm-dialog"
title={opts?.title ?? t("confirm.confirmation")}
@@ -57,9 +59,10 @@ export default function ConfirmDialog() {
show={shown}
stackable
>
{!opts?.message || typeof opts?.message === "string"
? <div>{(opts?.message as string) ?? ""}</div>
: <div dangerouslySetInnerHTML={{ __html: opts?.message.outerHTML ?? "" }} />}
{isValidElement(opts?.message)
? opts?.message
: <RawHtmlBlock html={opts?.message} />
}
{opts?.isConfirmDeleteNoteBox && (
<FormCheckbox
@@ -74,7 +77,7 @@ export default function ConfirmDialog() {
export type ConfirmDialogResult = false | ConfirmDialogOptions;
export type ConfirmDialogCallback = (val?: ConfirmDialogResult) => void;
type MessageType = string | HTMLElement | JQuery<HTMLElement>;
export type MessageType = string | HTMLElement | JQuery<HTMLElement> | VNode;
export interface ConfirmDialogOptions {
confirmed: boolean;

View File

@@ -6,7 +6,7 @@ import FormRadioGroup from "../react/FormRadioGroup";
import Modal from "../react/Modal";
import "./export.css";
import ws from "../../services/ws";
import toastService, { ToastOptions } from "../../services/toast";
import toastService, { type ToastOptionsWithRequiredId } from "../../services/toast";
import utils from "../../services/utils";
import open from "../../services/open";
import froca from "../../services/froca";
@@ -132,11 +132,11 @@ function exportBranch(branchId: string, type: string, format: string, version: s
}
ws.subscribeToMessages(async (message) => {
function makeToast(id: string, message: string): ToastOptions {
function makeToast(id: string, message: string): ToastOptionsWithRequiredId {
return {
id: id,
id,
title: t("export.export_status"),
message: message,
message,
icon: "export"
};
}
@@ -152,7 +152,7 @@ ws.subscribeToMessages(async (message) => {
toastService.showPersistent(makeToast(message.taskId, t("export.export_in_progress", { progressCount: message.progressCount })));
} else if (message.type === "taskSucceeded") {
const toast = makeToast(message.taskId, t("export.export_finished_successfully"));
toast.closeAfter = 5000;
toast.timeout = 5000;
toastService.showPersistent(toast);
}

View File

@@ -0,0 +1,11 @@
.modal.info-dialog {
user-select: text;
h3 {
font-size: 1.25em;
}
pre {
font-size: 0.75em;
}
}

View File

@@ -1,12 +1,26 @@
import { EventData } from "../../components/app_context";
import Modal from "../react/Modal";
import Modal, { type ModalProps } from "../react/Modal";
import { t } from "../../services/i18n";
import Button from "../react/Button";
import { useRef, useState } from "preact/hooks";
import { RawHtmlBlock } from "../react/RawHtml";
import { useTriliumEvent } from "../react/hooks";
import { isValidElement } from "preact";
import { ConfirmWithMessageOptions } from "./confirm";
import "./info.css";
import server from "../../services/server";
import { ToMarkdownResponse } from "@triliumnext/commons";
import { copyTextWithToast } from "../../services/clipboard_ext";
export interface InfoExtraProps extends Partial<Pick<ModalProps, "size" | "title">> {
/** Adds a button in the footer that allows easily copying the content of the infobox to clipboard. */
copyToClipboardButton?: boolean;
}
export type InfoProps = ConfirmWithMessageOptions & InfoExtraProps;
export default function InfoDialog() {
const modalRef = useRef<HTMLDivElement>(null);
const [ opts, setOpts ] = useState<EventData<"showInfoDialog">>();
const [ shown, setShown ] = useState(false);
const okButtonRef = useRef<HTMLButtonElement>(null);
@@ -18,21 +32,42 @@ export default function InfoDialog() {
return (<Modal
className="info-dialog"
size="sm"
title={t("info.modalTitle")}
size={opts?.size ?? "sm"}
title={opts?.title ?? t("info.modalTitle")}
onHidden={() => {
opts?.callback?.();
setShown(false);
}}
onShown={() => okButtonRef.current?.focus?.()}
footer={<Button
buttonRef={okButtonRef}
text={t("info.okButton")}
onClick={() => setShown(false)}
/>}
modalRef={modalRef}
footer={<>
{opts?.copyToClipboardButton && (
<Button
text={t("info.copy_to_clipboard")}
icon="bx bx-copy"
onClick={async () => {
const htmlContent = modalRef.current?.querySelector<HTMLDivElement>(".modal-body")?.innerHTML;
if (!htmlContent) return;
const { markdownContent } = await server.post<ToMarkdownResponse>("other/to-markdown", { htmlContent });
copyTextWithToast(markdownContent);
}}
/>
)}
<Button
buttonRef={okButtonRef}
text={t("info.okButton")}
onClick={() => setShown(false)}
/>
</>}
show={shown}
stackable
scrollable
>
<RawHtmlBlock className="info-dialog-content" html={opts?.message ?? ""} />
{isValidElement(opts?.message)
? opts?.message
: <RawHtmlBlock className="info-dialog-content" html={opts?.message} />
}
</Modal>);
}

View File

@@ -7,15 +7,12 @@ import Modal from "../react/Modal";
import Button from "../react/Button";
import { useTriliumEvent } from "../react/hooks";
import { CKEditorApi } from "../type_widgets/text/CKEditorWithWatchdog";
import { RenderMarkdownResponse } from "@triliumnext/commons";
export interface MarkdownImportOpts {
editorApi: CKEditorApi;
}
interface RenderMarkdownResponse {
htmlContent: string;
}
export default function MarkdownImportDialog() {
const markdownImportTextArea = useRef<HTMLTextAreaElement>(null);
const editorApiRef = useRef<CKEditorApi>(null);

View File

@@ -0,0 +1,31 @@
.bookmark-folder-widget {
min-width: 400px;
max-height: 500px;
padding: 7px 15px 0 15px;
font-size: 1.2rem;
overflow: auto;
}
.bookmark-folder-widget ul {
padding: 0;
list-style-type: none;
}
.bookmark-folder-widget .note-link {
display: block;
padding: 5px 10px 5px 5px;
}
.bookmark-folder-widget .note-link:hover {
background-color: var(--accented-background-color);
text-decoration: none;
}
.dropdown-menu .bookmark-folder-widget a:hover:not(.disabled) {
text-decoration: none;
background-color: transparent !important;
}
.bookmark-folder-widget li .note-link {
padding-inline-start: 35px;
}

View File

@@ -0,0 +1,59 @@
import { useContext, useMemo } from "preact/hooks";
import { LaunchBarContext, LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
import { CSSProperties } from "preact";
import type FNote from "../../entities/fnote";
import { useChildNotes, useNoteLabelBoolean } from "../react/hooks";
import "./BookmarkButtons.css";
import NoteLink from "../react/NoteLink";
import { CustomNoteLauncher } from "./GenericButtons";
const PARENT_NOTE_ID = "_lbBookmarks";
export default function BookmarkButtons() {
const { isHorizontalLayout } = useContext(LaunchBarContext);
const style = useMemo<CSSProperties>(() => ({
display: "flex",
flexDirection: isHorizontalLayout ? "row" : "column",
contain: "none"
}), [ isHorizontalLayout ]);
const childNotes = useChildNotes(PARENT_NOTE_ID);
return (
<div style={style}>
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
</div>
)
}
function SingleBookmark({ note }: { note: FNote }) {
const [ bookmarkFolder ] = useNoteLabelBoolean(note, "bookmarkFolder");
return bookmarkFolder
? <BookmarkFolder note={note} />
: <CustomNoteLauncher launcherNote={note} getTargetNoteId={() => note.noteId} />
}
function BookmarkFolder({ note }: { note: FNote }) {
const { icon, title } = useLauncherIconAndTitle(note);
const childNotes = useChildNotes(note.noteId);
return (
<LaunchBarDropdownButton
icon={icon}
title={title}
>
<div className="bookmark-folder-widget">
<div className="parent-note">
<NoteLink notePath={note.noteId} noPreview showNoteIcon containerClassName="note-link" noTnLink />
</div>
<ul className="children-notes">
{childNotes.map(childNote => (
<li key={childNote.noteId}>
<NoteLink notePath={childNote.noteId} noPreview showNoteIcon containerClassName="note-link" noTnLink />
</li>
))}
</ul>
</div>
</LaunchBarDropdownButton>
)
}

View File

@@ -0,0 +1,221 @@
import { useTriliumOptionInt } from "../react/hooks";
import clsx from "clsx";
import server from "../../services/server";
import { TargetedMouseEvent, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { Dayjs } from "@triliumnext/commons";
import { t } from "../../services/i18n";
interface DateNotesForMonth {
[date: string]: string;
}
const DAYS_OF_WEEK = [
t("calendar.sun"),
t("calendar.mon"),
t("calendar.tue"),
t("calendar.wed"),
t("calendar.thu"),
t("calendar.fri"),
t("calendar.sat")
];
interface DateRangeInfo {
weekNumbers: number[];
dates: Dayjs[];
}
export interface CalendarArgs {
date: Dayjs;
todaysDate: Dayjs;
activeDate: Dayjs | null;
onDateClicked(date: string, e: TargetedMouseEvent<HTMLAnchorElement>): void;
onWeekClicked?: (week: string, e: TargetedMouseEvent<HTMLAnchorElement>) => void;
weekNotes: string[];
}
export default function Calendar(args: CalendarArgs) {
const [ rawFirstDayOfWeek ] = useTriliumOptionInt("firstDayOfWeek");
const firstDayOfWeekISO = (rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek);
const date = args.date;
const firstDay = date.startOf('month');
const firstDayISO = firstDay.isoWeekday();
const monthInfo = getMonthInformation(date, firstDayISO, firstDayOfWeekISO);
return (
<>
<CalendarWeekHeader rawFirstDayOfWeek={rawFirstDayOfWeek} />
<div className="calendar-body" data-calendar-area="month">
{firstDayISO !== firstDayOfWeekISO && <PreviousMonthDays info={monthInfo.prevMonth} {...args} />}
<CurrentMonthDays firstDayOfWeekISO={firstDayOfWeekISO} {...args} />
<NextMonthDays dates={monthInfo.nextMonth.dates} {...args} />
</div>
</>
)
}
function CalendarWeekHeader({ rawFirstDayOfWeek }: { rawFirstDayOfWeek: number }) {
let localeDaysOfWeek = [...DAYS_OF_WEEK];
const shifted = localeDaysOfWeek.splice(0, rawFirstDayOfWeek);
localeDaysOfWeek = ['', ...localeDaysOfWeek, ...shifted];
return (
<div className="calendar-week">
{localeDaysOfWeek.map(dayOfWeek => <span key={dayOfWeek}>{dayOfWeek}</span>)}
</div>
)
}
function PreviousMonthDays({ date, info: { dates, weekNumbers }, ...args }: { date: Dayjs, info: DateRangeInfo } & CalendarArgs) {
const prevMonth = date.subtract(1, 'month').format('YYYY-MM');
const [ dateNotesForPrevMonth, setDateNotesForPrevMonth ] = useState<DateNotesForMonth>();
useEffect(() => {
server.get<DateNotesForMonth>(`special-notes/notes-for-month/${prevMonth}`).then(setDateNotesForPrevMonth);
}, [ date ]);
return (
<>
<CalendarWeek date={date} weekNumber={weekNumbers[0]} {...args} />
{dates.map(date => <CalendarDay key={date.toISOString()} date={date} dateNotesForMonth={dateNotesForPrevMonth} className="calendar-date-prev-month" {...args} />)}
</>
)
}
function CurrentMonthDays({ date, firstDayOfWeekISO, ...args }: { date: Dayjs, firstDayOfWeekISO: number } & CalendarArgs) {
let dateCursor = date;
const currentMonth = date.month();
const items: VNode[] = [];
const curMonthString = date.format('YYYY-MM');
const [ dateNotesForCurMonth, setDateNotesForCurMonth ] = useState<DateNotesForMonth>();
useEffect(() => {
server.get<DateNotesForMonth>(`special-notes/notes-for-month/${curMonthString}`).then(setDateNotesForCurMonth);
}, [ date ]);
while (dateCursor.month() === currentMonth) {
const weekNumber = getWeekNumber(dateCursor, firstDayOfWeekISO);
if (dateCursor.isoWeekday() === firstDayOfWeekISO) {
items.push(<CalendarWeek key={`${dateCursor.year()}-W${weekNumber}`} date={dateCursor} weekNumber={weekNumber} {...args}/>)
}
items.push(<CalendarDay key={dateCursor.toISOString()} date={dateCursor} dateNotesForMonth={dateNotesForCurMonth} {...args} />)
dateCursor = dateCursor.add(1, "day");
}
return items;
}
function NextMonthDays({ date, dates, ...args }: { date: Dayjs, dates: Dayjs[] } & CalendarArgs) {
const nextMonth = date.add(1, 'month').format('YYYY-MM');
const [ dateNotesForNextMonth, setDateNotesForNextMonth ] = useState<DateNotesForMonth>();
useEffect(() => {
server.get<DateNotesForMonth>(`special-notes/notes-for-month/${nextMonth}`).then(setDateNotesForNextMonth);
}, [ date ]);
return dates.map(date => (
<CalendarDay key={date.toISOString()} date={date} dateNotesForMonth={dateNotesForNextMonth} className="calendar-date-next-month" {...args} />
));
}
function CalendarDay({ date, dateNotesForMonth, className, activeDate, todaysDate, onDateClicked }: { date: Dayjs, dateNotesForMonth?: DateNotesForMonth, className?: string } & CalendarArgs) {
const dateString = date.local().format('YYYY-MM-DD');
const dateNoteId = dateNotesForMonth?.[dateString];
return (
<a
className={clsx("calendar-date", className,
dateNoteId && "calendar-date-exists",
date.isSame(activeDate, "day") && "calendar-date-active",
date.isSame(todaysDate, "day") && "calendar-date-today"
)}
data-calendar-date={date.local().format("YYYY-MM-DD")}
data-href={dateNoteId && `#root/${dateNoteId}`}
onClick={(e) => onDateClicked(dateString, e)}
>
<span>
{date.date()}
</span>
</a>
);
}
function CalendarWeek({ date, weekNumber, weekNotes, onWeekClicked }: { weekNumber: number, weekNotes: string[] } & Pick<CalendarArgs, "date" | "onWeekClicked">) {
const localDate = date.local();
// Handle case where week is in between years.
let year = localDate.year();
if (localDate.month() === 11 && weekNumber === 1) year++;
const weekString = `${year}-W${String(weekNumber).padStart(2, '0')}`;
if (onWeekClicked) {
return (
<a
className={clsx("calendar-week-number", "calendar-date",
weekNotes.includes(weekString) && "calendar-date-exists")}
data-calendar-week-number={weekNumber}
data-date={date.local().format("YYYY-MM-DD")}
onClick={(e) => onWeekClicked(weekString, e)}
>{weekNumber}</a>
)
}
return (
<span
className="calendar-week-number calendar-week-number-disabled"
data-calendar-week-number={weekNumber}
>{weekNumber}</span>);
}
export function getMonthInformation(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number) {
return {
prevMonth: getPrevMonthDays(date, firstDayISO, firstDayOfWeekISO),
nextMonth: getNextMonthDays(date, firstDayOfWeekISO)
}
}
function getPrevMonthDays(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number): DateRangeInfo {
const prevMonthLastDay = date.subtract(1, 'month').endOf('month');
const daysToAdd = (firstDayISO - firstDayOfWeekISO + 7) % 7;
const dates: Dayjs[] = [];
const firstDay = date.startOf('month');
const weekNumber = getWeekNumber(firstDay, firstDayOfWeekISO);
// Get dates from previous month
for (let i = daysToAdd - 1; i >= 0; i--) {
dates.push(prevMonthLastDay.subtract(i, 'day'));
}
return { weekNumbers: [ weekNumber ], dates };
}
function getNextMonthDays(date: Dayjs, firstDayOfWeekISO: number): DateRangeInfo {
const lastDayOfMonth = date.endOf('month');
const lastDayISO = lastDayOfMonth.isoWeekday();
const lastDayOfUserWeek = ((firstDayOfWeekISO + 6 - 1) % 7) + 1;
const nextMonthFirstDay = date.add(1, 'month').startOf('month');
const dates: Dayjs[] = [];
if (lastDayISO !== lastDayOfUserWeek) {
const daysToAdd = (lastDayOfUserWeek - lastDayISO + 7) % 7;
for (let i = 0; i < daysToAdd; i++) {
dates.push(nextMonthFirstDay.add(i, 'day'));
}
}
return { weekNumbers: [], dates };
}
export function getWeekNumber(date: Dayjs, firstDayOfWeekISO: number): number {
const weekStart = getWeekStartDate(date, firstDayOfWeekISO);
return weekStart.isoWeek();
}
function getWeekStartDate(date: Dayjs, firstDayOfWeekISO: number): Dayjs {
const currentISO = date.isoWeekday();
const diff = (currentISO - firstDayOfWeekISO + 7) % 7;
return date.clone().subtract(diff, "day").startOf("day");
}

View File

@@ -10,7 +10,6 @@
.calendar-dropdown-widget {
margin: 0 auto;
overflow: hidden;
width: 100%;
}
@@ -174,4 +173,13 @@
background-color: var(--hover-item-background-color);
color: var(--hover-item-text-color);
text-decoration: underline;
}
.calendar-dropdown-widget .form-control {
padding: 0;
}
.calendar-dropdown-widget .calendar-month-selector .dropdown-menu {
left: 50%;
transform: translateX(-50%);
}

View File

@@ -0,0 +1,188 @@
import { Dispatch, StateUpdater, useMemo, useRef, useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import { LaunchBarDropdownButton, LauncherNoteProps, useLauncherIconAndTitle } from "./launch_bar_widgets";
import { Dayjs, dayjs } from "@triliumnext/commons";
import appContext from "../../components/app_context";
import "./CalendarWidget.css";
import Calendar, { CalendarArgs } from "./Calendar";
import ActionButton from "../react/ActionButton";
import { t } from "../../services/i18n";
import FormDropdownList from "../react/FormDropdownList";
import FormTextBox from "../react/FormTextBox";
import toast from "../../services/toast";
import date_notes from "../../services/date_notes";
import { Dropdown } from "bootstrap";
import search from "../../services/search";
import server from "../../services/server";
const MONTHS = [
t("calendar.january"),
t("calendar.february"),
t("calendar.march"),
t("calendar.april"),
t("calendar.may"),
t("calendar.june"),
t("calendar.july"),
t("calendar.august"),
t("calendar.september"),
t("calendar.october"),
t("calendar.november"),
t("calendar.december")
];
export default function CalendarWidget({ launcherNote }: LauncherNoteProps) {
const { title, icon } = useLauncherIconAndTitle(launcherNote);
const [ calendarArgs, setCalendarArgs ] = useState<Pick<CalendarArgs, "activeDate" | "todaysDate">>();
const [ date, setDate ] = useState<Dayjs>();
const dropdownRef = useRef<Dropdown>(null);
const [ enableWeekNotes, setEnableWeekNotes ] = useState(false);
const [ weekNotes, setWeekNotes ] = useState<string[]>([]);
const calendarRootRef = useRef<FNote>();
async function checkEnableWeekNotes() {
if (!calendarRootRef.current) {
const notes = await search.searchForNotes("#calendarRoot");
if (!notes.length) return;
calendarRootRef.current = notes[0];
}
if (!calendarRootRef.current) return;
const enableWeekNotes = calendarRootRef.current.hasLabel("enableWeekNote");
setEnableWeekNotes(enableWeekNotes);
if (enableWeekNotes) {
server.get<string[]>(`attribute-values/weekNote`).then(setWeekNotes);
}
}
return (
<LaunchBarDropdownButton
icon={icon} title={title}
onShown={async () => {
const dateNote = appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote");
const activeDate = dateNote ? dayjs(`${dateNote}T12:00:00`) : null
const todaysDate = dayjs();
setCalendarArgs({
activeDate,
todaysDate,
});
setDate(dayjs(activeDate || todaysDate).startOf('month'));
try {
await checkEnableWeekNotes();
} catch (e: unknown) {
// Non-critical.
}
}}
dropdownRef={dropdownRef}
dropdownOptions={{
autoClose: "outside"
}}
>
{calendarArgs && date && <div className="calendar-dropdown-widget" style={{ width: 400 }}>
<CalendarHeader date={date} setDate={setDate} />
<Calendar
date={date}
onDateClicked={async (date, e) => {
const note = await date_notes.getDayNote(date);
if (note) {
appContext.tabManager.getActiveContext()?.setNote(note.noteId);
dropdownRef.current?.hide();
} else {
toast.showError(t("calendar.cannot_find_day_note"));
}
e.stopPropagation();
}}
onWeekClicked={enableWeekNotes ? async (week, e) => {
const note = await date_notes.getWeekNote(week);
if (note) {
appContext.tabManager.getActiveContext()?.setNote(note.noteId);
dropdownRef.current?.hide();
} else {
toast.showError(t("calendar.cannot_find_week_note"));
}
e.stopPropagation();
} : undefined}
weekNotes={weekNotes}
{...calendarArgs}
/>
</div>}
</LaunchBarDropdownButton>
)
}
interface CalendarHeaderProps {
date: Dayjs;
setDate: Dispatch<StateUpdater<Dayjs | undefined>>;
}
function CalendarHeader(props: CalendarHeaderProps) {
return (
<div className="calendar-header">
<CalendarMonthSelector {...props} />
<CalendarYearSelector {...props} />
</div>
)
}
function CalendarMonthSelector({ date, setDate }: CalendarHeaderProps) {
const months = useMemo(() => (
Array.from(MONTHS.entries().map(([ index, text ]) => ({
index: index.toString(), text
})))
), []);
return (
<div className="calendar-month-selector">
<AdjustDateButton date={date} setDate={setDate} direction="prev" unit="month" />
<FormDropdownList
values={months} currentValue={date.month().toString()}
keyProperty="index" titleProperty="text"
onChange={(index) => setDate(date.set("month", parseInt(index, 10)))}
buttonProps={{ "data-calendar-input": "month" }}
dropdownOptions={{ display: "static" }}
/>
<AdjustDateButton date={date} setDate={setDate} direction="next" unit="month" />
</div>
);
}
function CalendarYearSelector({ date, setDate }: CalendarHeaderProps) {
return (
<div className="calendar-year-selector">
<AdjustDateButton date={date} setDate={setDate} direction="prev" unit="year" />
<FormTextBox
type="number"
min="1900" max="2999" step="1"
currentValue={date.year().toString()}
onChange={(newValue) => {
const year = parseInt(newValue, 10);
if (!Number.isNaN(year)) {
setDate(date.set("year", year));
}
}}
data-calendar-input="year"
/>
<AdjustDateButton date={date} setDate={setDate} direction="next" unit="year" />
</div>
)
}
function AdjustDateButton({ date, setDate, unit, direction }: CalendarHeaderProps & {
direction: "prev" | "next",
unit: "month" | "year"
}) {
return (
<ActionButton
icon={direction === "prev" ? "bx bx-chevron-left" : "bx bx-chevron-right" }
className="calendar-btn tn-tool-button"
noIconActionClass
text=""
onClick={(e) => {
e.stopPropagation();
const newDate = direction === "prev" ? date.subtract(1, unit) : date.add(1, unit);
setDate(newDate);
}}
/>
)
}

View File

@@ -0,0 +1,55 @@
import { useCallback } from "preact/hooks";
import appContext from "../../components/app_context";
import FNote from "../../entities/fnote";
import link_context_menu from "../../menus/link_context_menu";
import { escapeHtml, isCtrlKey } from "../../services/utils";
import { useGlobalShortcut, useNoteLabel } from "../react/hooks";
import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
export function CustomNoteLauncher({ launcherNote, getTargetNoteId, getHoistedNoteId }: {
launcherNote: FNote;
getTargetNoteId: (launcherNote: FNote) => string | null | Promise<string | null>;
getHoistedNoteId?: (launcherNote: FNote) => string | null;
keyboardShortcut?: string;
}) {
const { icon, title } = useLauncherIconAndTitle(launcherNote);
const launch = useCallback(async (evt: MouseEvent | KeyboardEvent) => {
if (evt.which === 3) {
return;
}
const targetNoteId = await getTargetNoteId(launcherNote);
if (!targetNoteId) return;
const hoistedNoteIdWithDefault = getHoistedNoteId?.(launcherNote) || appContext.tabManager.getActiveContext()?.hoistedNoteId;
const ctrlKey = isCtrlKey(evt);
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
const activate = evt.shiftKey ? true : false;
await appContext.tabManager.openInNewTab(targetNoteId, hoistedNoteIdWithDefault, activate);
} else {
await appContext.tabManager.openInSameTab(targetNoteId);
}
}, [ launcherNote, getTargetNoteId, getHoistedNoteId ]);
// Keyboard shortcut.
const [ shortcut ] = useNoteLabel(launcherNote, "keyboardShortcut");
useGlobalShortcut(shortcut, launch);
return (
<LaunchBarActionButton
icon={icon}
text={escapeHtml(title)}
onClick={launch}
onAuxClick={launch}
onContextMenu={async evt => {
evt.preventDefault();
const targetNoteId = await getTargetNoteId(launcherNote);
if (targetNoteId) {
link_context_menu.openContextMenu(targetNoteId, evt);
}
}}
/>
)
}

View File

@@ -0,0 +1,86 @@
import { useEffect, useRef } from "preact/hooks";
import FNote from "../../entities/fnote";
import { dynamicRequire, isElectron } from "../../services/utils";
import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
import type { WebContents } from "electron";
import contextMenu, { MenuCommandItem } from "../../menus/context_menu";
import tree from "../../services/tree";
import link from "../../services/link";
interface HistoryNavigationProps {
launcherNote: FNote;
command: "backInNoteHistory" | "forwardInNoteHistory";
}
const HISTORY_LIMIT = 20;
export default function HistoryNavigationButton({ launcherNote, command }: HistoryNavigationProps) {
const { icon, title } = useLauncherIconAndTitle(launcherNote);
const webContentsRef = useRef<WebContents>(null);
useEffect(() => {
if (isElectron()) {
const webContents = dynamicRequire("@electron/remote").getCurrentWebContents();
// without this, the history is preserved across frontend reloads
webContents?.clearHistory();
webContentsRef.current = webContents;
}
}, []);
return (
<LaunchBarActionButton
icon={icon}
text={title}
triggerCommand={command}
onContextMenu={async (e) => {
e.preventDefault();
const webContents = webContentsRef.current;
if (!webContents || webContents.navigationHistory.length() < 2) {
return;
}
let items: MenuCommandItem<string>[] = [];
const history = webContents.navigationHistory.getAllEntries();
const activeIndex = webContents.navigationHistory.getActiveIndex();
for (const idx in history) {
const { notePath } = link.parseNavigationStateFromUrl(history[idx].url);
if (!notePath) continue;
const title = await tree.getNotePathTitle(notePath);
items.push({
title,
command: idx,
uiIcon:
parseInt(idx) === activeIndex
? "bx bx-radio-circle-marked" // compare with type coercion!
: parseInt(idx) < activeIndex
? "bx bx-left-arrow-alt"
: "bx bx-right-arrow-alt"
});
}
items.reverse();
if (items.length > HISTORY_LIMIT) {
items = items.slice(0, HISTORY_LIMIT);
}
contextMenu.show({
x: e.pageX,
y: e.pageY,
items,
selectMenuItemHandler: (item: MenuCommandItem<string>) => {
if (item && item.command && webContents) {
const idx = parseInt(item.command, 10);
webContents.navigationHistory.goToIndex(idx);
}
}
});
}}
/>
)
}

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