Compare commits

..

315 Commits

Author SHA1 Message Date
Adorian Doran
516f0052ef ui/grid view: use flex gap instead of margins for cards 2026-02-17 20:36:01 +02:00
Elian Doran
7dceca475d Translations update from Hosted Weblate (#8746) 2026-02-17 15:57:15 +02:00
Hosted Weblate
62a78bc272 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/
2026-02-17 14:02:25 +01:00
Adorian Doran
6aaf277b45 UI: improved pager (#8730) 2026-02-17 15:02:11 +02:00
Adorian Doran
a1c61768c4 Merge branch 'main' into feat/ui/improved-pager 2026-02-17 15:01:52 +02:00
Adorian Doran
8e4c88c10c ui/pager: add support for custom alignment 2026-02-17 14:59:21 +02:00
Adorian Doran
b57780519c ui/pager: fix attribute names 2026-02-17 14:55:43 +02:00
Adorian Doran
c07c4013be ui/pager: update colors 2026-02-17 14:55:27 +02:00
Elian Doran
768211cc26 fix(desktop): web view note in second window does not load content (#8739) 2026-02-17 13:22:33 +02:00
Elian Doran
b4a31503ee chore(deps): update dependency @redocly/cli to v2.18.2 (#8741) 2026-02-17 13:20:41 +02:00
Elian Doran
6c2b3e238c fix(deps): update dependency @preact/signals to v2.8.1 (#8742) 2026-02-17 13:20:16 +02:00
Elian Doran
f2ff834b9c fix(deps): update dependency mind-elixir to v5.8.2 (#8743) 2026-02-17 13:19:55 +02:00
Elian Doran
86e1688358 Translations update from Hosted Weblate (#8745) 2026-02-17 13:19:10 +02:00
Adorian Doran
d259eb1f9d ui/icon buttons: decrease opacity for disabled buttons 2026-02-17 12:46:57 +02:00
Adorian Doran
568a668e9f ui/pager: improve layout 2026-02-17 12:42:55 +02:00
Adorian Doran
0621dfbac7 ui/pager: extract colors as CSS variables 2026-02-17 12:12:34 +02:00
Adorian Doran
827bb9a32f ui/pager: tweak appearance 2026-02-17 12:07:51 +02:00
Stas Kies
fd08654dd5 Translated using Weblate (German)
Currently translated at 100.0% (1807 of 1807 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-02-17 10:28:43 +01:00
Stas Kies
e5d750722e Translated using Weblate (German)
Currently translated at 99.5% (1798 of 1807 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-02-17 10:28:43 +01:00
renovate[bot]
b43c4ad666 fix(deps): update dependency mind-elixir to v5.8.2 2026-02-17 01:43:23 +00:00
renovate[bot]
c9f93a4706 fix(deps): update dependency @preact/signals to v2.8.1 2026-02-17 01:42:30 +00:00
renovate[bot]
a38834f7e2 chore(deps): update dependency @redocly/cli to v2.18.2 2026-02-17 01:41:41 +00:00
Adorian Doran
fcfc6f6476 ui/pager: improve code 2026-02-17 01:14:14 +02:00
contributor
b23a5348b5 fix(desktop): web view note in second window does not load content 2026-02-17 00:47:33 +02:00
Adorian Doran
782cd59407 ui/pager: improve layout 2026-02-16 21:46:16 +02:00
Adorian Doran
7964bd3be4 ui/pager: make responsive 2026-02-16 21:31:03 +02:00
Adorian Doran
3d41ce13b1 ui/pager: restyle the note counter 2026-02-16 20:48:51 +02:00
Adorian Doran
6f0881ab8a ui/pager: improve layout 2026-02-16 20:39:33 +02:00
Adorian Doran
a6309715f3 ui/pager: refactor 2026-02-16 20:24:17 +02:00
Adorian Doran
5a06193e65 ui/pager: restyle 2026-02-16 20:11:11 +02:00
Adorian Doran
4912834537 ui/buttons: refactor 2026-02-16 18:23:28 +02:00
Adorian Doran
d64f2c72b7 ui/buttons: add support for low profile style 2026-02-16 18:15:29 +02:00
Elian Doran
358d06a402 Translations update from Hosted Weblate (#8731) 2026-02-16 18:06:57 +02:00
Elian Doran
5deabe4260 fix(deps): update dependency mind-elixir to v5.8.1 (#8733) 2026-02-16 18:06:16 +02:00
Giovi
1b1162e26e Translated using Weblate (Italian)
Currently translated at 100.0% (1807 of 1807 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2026-02-16 17:04:44 +01:00
Francis C.
30ad5d531c Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1807 of 1807 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-02-16 17:04:43 +01:00
Toto Yullian
99efc73e93 Translated using Weblate (Indonesian)
Currently translated at 3.9% (72 of 1807 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/id/
2026-02-16 17:04:42 +01:00
Toto Yullian
17c0071dd1 Translated using Weblate (Indonesian)
Currently translated at 44.9% (71 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/id/
2026-02-16 17:04:42 +01:00
Francis C.
a26bec047f Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1807 of 1807 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-02-16 17:04:41 +01:00
Stas Kies
748e7cc1df Translated using Weblate (German)
Currently translated at 99.1% (1792 of 1807 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-02-16 17:04:40 +01:00
green
f653c86965 Translated using Weblate (Japanese)
Currently translated at 100.0% (1807 of 1807 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-02-16 17:04:39 +01:00
Toto Yullian
58a41e58ee Translated using Weblate (Indonesian)
Currently translated at 71.5% (83 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/id/
2026-02-16 17:04:38 +01:00
Aindriú Mac Giolla Eoin
309a55f276 Translated using Weblate (Irish)
Currently translated at 100.0% (1807 of 1807 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-02-16 17:04:38 +01:00
green
df96c6b9fa Translated using Weblate (Japanese)
Currently translated at 99.3% (1796 of 1807 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-02-16 17:04:37 +01:00
Elian Doran
d779e078c7 fix(deps): update dependency i18next to v25.8.10 (#8732) 2026-02-16 18:04:12 +02:00
renovate[bot]
ae224151a0 fix(deps): update dependency i18next to v25.8.10 2026-02-16 12:47:46 +00:00
renovate[bot]
9f11717b69 fix(deps): update dependency mind-elixir to v5.8.1 2026-02-16 02:36:40 +00:00
Adorian Doran
ea25477e3d ui/pager: fix some issues 2026-02-16 02:06:39 +02:00
Adorian Doran
82576c9703 ui/pager: rewrite the pagination button distribution algorithm to better control the length 2026-02-16 01:51:32 +02:00
Elian Doran
80f1fc4c7c Translations update from Hosted Weblate (#8726) 2026-02-15 22:10:14 +02:00
Aindriú Mac Giolla Eoin
41cd4bcef6 Translated using Weblate (Irish)
Currently translated at 100.0% (1807 of 1807 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-02-15 20:49:02 +01:00
Ulices
46a414e155 Translated using Weblate (Spanish)
Currently translated at 100.0% (1806 of 1806 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-02-15 20:49:01 +01:00
Hosted Weblate
70a903f811 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/
2026-02-15 20:49:00 +01:00
Elian Doran
52db410c82 fix(collections/map): shifting into collection properties when switching styles 2026-02-15 21:48:46 +02:00
Elian Doran
08da8d73e0 fix(collections): collection properties still clipped in list/grid view 2026-02-15 21:48:39 +02:00
Elian Doran
81a572af25 feat(collections): improve display of no children 2026-02-15 21:48:33 +02:00
Elian Doran
23c50f34fe fix(collections): list/grid collection properties clipped when little results 2026-02-15 21:48:29 +02:00
Elian Doran
70aa115933 fix(collections): collection properties not shown if there are no children 2026-02-15 21:48:21 +02:00
Adorian Doran
0b8cba78d5 ui/pager: remove unused styles 2026-02-15 21:36:53 +02:00
Adorian Doran
d8806eaa04 ui/pager: replace the page number links with buttons 2026-02-15 21:31:58 +02:00
Adorian Doran
b8bc85856b ui/pager: add prev/next page navigation buttons 2026-02-15 21:12:42 +02:00
Adorian Doran
cc097c5414 UI: improved cards (#8728) 2026-02-15 20:32:29 +02:00
Adorian Doran
7551d0e044 ui/cards: fix attribute name 2026-02-15 20:32:01 +02:00
Adorian Doran
489a88b8da ui/cards: refactor 2026-02-15 20:23:19 +02:00
Adorian Doran
48013dc264 ui/cards: use a better HTML structure 2026-02-15 20:14:14 +02:00
Adorian Doran
4ee9d45dfc ui/cards: implement card titles (heading) 2026-02-15 20:06:01 +02:00
Adorian Doran
e00150a876 ui/cards: do not include nesting level CSS variables if not necessary 2026-02-15 19:46:08 +02:00
Adorian Doran
71668f8f8d List collections & search results: overhaul the list UI (#8705) 2026-02-15 19:32:21 +02:00
Adorian Doran
b71424d239 Merge branch 'main' into feat/list-view-overhaul 2026-02-15 19:20:41 +02:00
Adorian Doran
d1a3bceaa6 client/list view: refactor 2026-02-15 19:15:58 +02:00
Adorian Doran
483c57029a client/list view: extract event handler 2026-02-15 19:10:31 +02:00
Adorian Doran
7e6daf5b36 style/list view: refactor 2026-02-15 18:55:23 +02:00
Adorian Doran
b658253687 style/card: refactor 2026-02-15 18:55:04 +02:00
Adorian Doran
80b488deec style/list view: tweak indentation size 2026-02-15 18:34:54 +02:00
Adorian Doran
10cf1a371e style/card: improve 2026-02-15 18:34:00 +02:00
Elian Doran
81445901fa Support for custom geomap tiles (#8703) 2026-02-15 18:21:49 +02:00
Adorian Doran
f645d9d721 style/list view: tweak 2026-02-15 18:20:45 +02:00
Adorian Doran
900bfdff9d style/list view: refactor 2026-02-15 18:18:50 +02:00
Adorian Doran
108ca5afb5 client/list view: add support for archived items 2026-02-15 18:16:46 +02:00
Adorian Doran
3df03a551c client/list view: add support for archived items 2026-02-15 18:15:07 +02:00
Adorian Doran
6ffbe19667 client/list view: add support for tinted notes 2026-02-15 18:07:35 +02:00
Elian Doran
e3172ebf1c fix(client): unreadable disabled item 2026-02-15 17:51:06 +02:00
Elian Doran
9b2876a8ff docs(user): mention custom tiles & hiding labels for geomap 2026-02-15 17:47:28 +02:00
Elian Doran
3db2c910e0 fix(client/active_badges): wrong enable + missing toggle for icon pack & custom CSS 2026-02-15 17:32:34 +02:00
Elian Doran
2799e4392f Merge remote-tracking branch 'origin/main' into feature/custom_map_improvements
; Conflicts:
;	apps/client/src/widgets/note_bars/CollectionProperties.tsx
;	apps/client/src/widgets/ribbon/collection-properties-config.tsx
2026-02-15 17:30:39 +02:00
Elian Doran
61963fcfda UX improvements for web view & render note (#8725) 2026-02-15 17:23:28 +02:00
Elian Doran
b0a3d54276 chore(client): address requested changes 2026-02-15 17:15:51 +02:00
Elian Doran
2135412c84 Translations update from Hosted Weblate (#8723) 2026-02-15 17:08:54 +02:00
Elian Doran
7a534c3ea7 chore(client/active_badges): disable toggle for web view & render note 2026-02-15 17:06:23 +02:00
Elian Doran
a6e596a5e3 feat(client/render_note): display rendering errors 2026-02-15 17:04:31 +02:00
Elian Doran
8b3e3c2c3a chore(react): not reacting properly to deleted relations 2026-02-15 16:56:37 +02:00
Elian Doran
ec8b0a3801 feat(client/render_note): warning for disabled render note 2026-02-15 16:54:34 +02:00
Elian Doran
197b769838 feat(client/render_note): create sample note with HTML 2026-02-15 16:46:19 +02:00
Elian Doran
1ba498c0e3 feat(client/render_note): create sample note with preact 2026-02-15 16:38:09 +02:00
Elian Doran
5550cb7b95 feat(client/render_note): basic setup for with note picker 2026-02-15 16:30:10 +02:00
Elian Doran
06a005acec refactor(client/render_note): separate content from setup 2026-02-15 16:17:41 +02:00
Elian Doran
2103be9d28 refactor(client): extract learn more into component 2026-02-15 16:11:52 +02:00
Elian Doran
a02f3c4440 refactor(client): extract setup form to dedicated component 2026-02-15 16:08:58 +02:00
Elian Doran
4b8d341e00 feat(web_view): add a screen if web view is disabled 2026-02-15 16:02:51 +02:00
Elian Doran
964633f426 refactor(client/web_view): fix lint & formatting 2026-02-15 15:36:58 +02:00
Marcel
d11fb38280 Translated using Weblate (German)
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/de/
2026-02-15 13:34:30 +00:00
Elian Doran
04f4530990 Mobile improvements (#8724) 2026-02-15 15:34:20 +02:00
Elian Doran
5a77318a9e chore(mobile): address requested changes 2026-02-15 15:33:03 +02:00
Elian Doran
9ed2894a0c fix(mobile): badges not collapsing in quick edit 2026-02-15 15:29:27 +02:00
Elian Doran
e0766ad439 feat(mobile): display grid view on two columns 2026-02-15 15:25:28 +02:00
Elian Doran
67acfaab62 fix(mobile): missing scroll padding 2026-02-15 15:17:08 +02:00
Elian Doran
10b5d29107 fix(mobile): promoted attributes having a max-height 2026-02-15 15:16:23 +02:00
Elian Doran
297dd41170 fix(mobile): incorrect tab hue on light theme 2026-02-15 15:12:12 +02:00
Elian Doran
1364223599 fix(mobile): horizontall scroll in empty tab due to long attributes 2026-02-15 15:00:34 +02:00
Elian Doran
a6b7761dfa fix(mobile): double-scroll bar for empty tab 2026-02-15 14:58:28 +02:00
Elian Doran
2e6290c514 feat(mobile): improve empty tab workspace switcher 2026-02-15 14:55:01 +02:00
Elian Doran
78f4928611 feat(mobile): reduce margins for empty tab 2026-02-15 14:53:01 +02:00
Elian Doran
d044fce9c1 fix(mobile): remove redundant file properties footer 2026-02-15 14:44:21 +02:00
Elian Doran
11fa815e13 Active content badges (#8716) 2026-02-15 14:34:04 +02:00
Elian Doran
8d917eb970 Merge branch 'main' into feature/active_content_badges 2026-02-15 14:20:21 +02:00
Elian Doran
e411e5f2cf chore(client): fix typecheck 2026-02-15 14:20:09 +02:00
Adorian Doran
fea8de89c6 style/list view: handle the title and attributes when overflowing the container 2026-02-15 14:08:50 +02:00
Elian Doran
48773636ca feat(collections): improve properties dropdown on mobile 2026-02-15 13:54:01 +02:00
Elian Doran
5a3c7355c1 chore(badges/content): improve dropdown on mobile 2026-02-15 13:51:48 +02:00
Adorian Doran
4afbabb977 style/list view: tweak 2026-02-15 13:51:13 +02:00
Elian Doran
afa9fe3063 refactor(badges/content): integrate title into definition 2026-02-15 13:46:47 +02:00
Elian Doran
178ac088b4 refactor(badges/content): integrate widget switcher using additional options 2026-02-15 13:44:16 +02:00
Adorian Doran
b2ebaf111f style/card: add legacy theme fallback 2026-02-15 13:43:06 +02:00
Elian Doran
ffd5ebbe79 refactor(badges/content): integrate execute using additional options 2026-02-15 13:38:02 +02:00
Elian Doran
6b78bfecb4 refactor(badges/content): integrate run options using additional options 2026-02-15 13:34:48 +02:00
Adorian Doran
9c2b01e3c9 style/list view: tweak 2026-02-15 13:34:45 +02:00
Adorian Doran
dca201ce42 style/list view: tweak 2026-02-15 13:31:45 +02:00
Elian Doran
d3c0a44c00 chore(badges/content): remove the base attribute for legacy 2026-02-15 13:26:23 +02:00
Elian Doran
33ea2de231 chore(badges/content): use translations 2026-02-15 13:23:36 +02:00
Elian Doran
43ebbfc321 chore(badges/content): add separator 2026-02-15 13:21:29 +02:00
Elian Doran
1e5b294eb3 feat(badges/content): adjustable base for app theme 2026-02-15 13:19:12 +02:00
Elian Doran
29855112c8 refactor(client): move note properties mapping outside collection properties 2026-02-15 13:13:18 +02:00
Elian Doran
8af7b3c81a feat(badges/content): make app theme toggleable 2026-02-15 12:42:46 +02:00
Elian Doran
b72b82ff1a feat(badges/content): add badge for app themes 2026-02-15 12:37:37 +02:00
Elian Doran
1588c8103c docs(user): improve documentation on scripts & active content 2026-02-15 12:32:41 +02:00
Elian Doran
15e569dcea chore(client): address requested changes 2026-02-15 12:32:30 +02:00
Adorian Doran
3774ea3768 style/list view: tweak 2026-02-15 02:28:52 +02:00
Adorian Doran
0a9c6a3119 client/list view: add a menu button for list items 2026-02-15 02:26:23 +02:00
Adorian Doran
a6cbde88bb client/list view: add an alternate style for search results 2026-02-15 01:57:10 +02:00
Elian Doran
4bdb407404 Translations update from Hosted Weblate (#8718) 2026-02-14 22:16:09 +02:00
Elian Doran
4568cedcd3 chore(deps): update dependency @redocly/cli to v2.18.1 (#8706) 2026-02-14 22:05:15 +02:00
AggelosPnS
f290317acc Translated using Weblate (Greek)
Currently translated at 12.3% (48 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/el/
2026-02-14 20:03:29 +00:00
AggelosPnS
645279c8fa Translated using Weblate (Greek)
Currently translated at 2.7% (49 of 1777 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/el/
2026-02-14 20:03:28 +00:00
Hosted Weblate
09436f8d65 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-02-14 20:03:27 +00:00
Elian Doran
b616e0e5f9 chore(deps): update dependency electron to v40.4.1 (#8707) 2026-02-14 22:03:19 +02:00
Elian Doran
f06a0852a1 chore(deps): update dependency typedoc to v0.28.17 (#8708) 2026-02-14 22:02:02 +02:00
Elian Doran
9e688138be fix(deps): update dependency i18next to v25.8.7 (#8709) 2026-02-14 21:56:00 +02:00
Elian Doran
5d46970a38 fix(deps): update dependency react-window to v2.2.7 (#8710) 2026-02-14 21:54:40 +02:00
Elian Doran
ecc441c074 chore(deps): update dependency openai to v6.22.0 (#8711) 2026-02-14 21:53:59 +02:00
Elian Doran
f2ce3678c4 chore(deps): update dependency stylelint to v17.3.0 (#8712) 2026-02-14 21:53:41 +02:00
Elian Doran
c9ad390647 fix(deps): update dependency @preact/signals to v2.8.0 (#8713) 2026-02-14 21:52:55 +02:00
Elian Doran
92acc7accd Translations update from Hosted Weblate (#8715) 2026-02-14 21:52:11 +02:00
Adorian Doran
9c13f36ca0 client/list view: show the contents of a note only after its rendering completes 2026-02-14 16:39:20 +02:00
Adorian Doran
fe4a11c5ad client/list view: improve appearance 2026-02-14 16:22:13 +02:00
Elian Doran
2ef4eb7eae chore(client): fix type errors 2026-02-14 12:43:40 +02:00
Elian Doran
7b230706cb feat(badges/content): improve color support 2026-02-14 12:41:41 +02:00
Elian Doran
5a2b04adba feat(badges/content): add support for web view 2026-02-14 12:35:06 +02:00
Elian Doran
50dcd3ba44 feat(badges/content): add support for render note 2026-02-14 12:30:43 +02:00
Elian Doran
740b02952f feat(badges/content): disable toggle when not necessary 2026-02-14 12:22:27 +02:00
Aindriú Mac Giolla Eoin
5d3d42ffdd Translated using Weblate (Irish)
Currently translated at 100.0% (1777 of 1777 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-02-14 10:09:46 +00:00
Elian Doran
866d3110da feat(badges/content): add badge for custom CSS 2026-02-14 12:05:24 +02:00
Elian Doran
7a3e7fccec feat(badges/content): handle widgets as separate content type 2026-02-14 11:56:40 +02:00
Elian Doran
3107bc8840 feat(badges/content): add toggle for widget 2026-02-14 11:47:42 +02:00
Elian Doran
2d34cdef5e feat(badges/content): integrate options for frontend script 2026-02-14 11:37:20 +02:00
Elian Doran
bd1f0909a2 feat(badges/content): configurable backend run options 2026-02-14 11:30:05 +02:00
Elian Doran
ef75de63fe feat(badges/content): option to execute now 2026-02-14 11:15:08 +02:00
Elian Doran
a739d28563 feat(badges/content): option to open API docs 2026-02-14 11:12:55 +02:00
Elian Doran
66ff009b72 feat(badges/content): option to open documentation 2026-02-14 11:08:30 +02:00
Elian Doran
a68e82c1c8 feat(badges/content): basic support for backend scripts 2026-02-14 10:57:32 +02:00
Elian Doran
46556c1c14 chore(badges/content): make toggle more compact 2026-02-14 10:40:19 +02:00
Elian Doran
7be637798f feat(badges/content): functional enable/disable toggle 2026-02-14 10:30:34 +02:00
Elian Doran
9b3396349e refactor(commons): add builtin_attributes to commons 2026-02-14 10:30:24 +02:00
Elian Doran
ccff210b4c feat(badges/content): indicate enabled/disabled state 2026-02-14 09:57:40 +02:00
Elian Doran
a2264847b6 refactor(badges/content): use shared mechanism for extracting info 2026-02-14 09:53:37 +02:00
Elian Doran
7d103f8c50 refactor(badges/content): extract to separate file 2026-02-14 09:37:48 +02:00
Elian Doran
f3dccc0aec feat(badges/content): detect icon pack 2026-02-14 09:19:46 +02:00
renovate[bot]
311b1d8a64 fix(deps): update dependency @preact/signals to v2.8.0 2026-02-14 02:08:43 +00:00
renovate[bot]
f3094e3079 chore(deps): update dependency stylelint to v17.3.0 2026-02-14 02:08:04 +00:00
renovate[bot]
f3b37b16d5 chore(deps): update dependency openai to v6.22.0 2026-02-14 02:07:20 +00:00
renovate[bot]
5f16ecf02d fix(deps): update dependency react-window to v2.2.7 2026-02-14 02:06:40 +00:00
renovate[bot]
8b75287827 fix(deps): update dependency i18next to v25.8.7 2026-02-14 02:05:55 +00:00
renovate[bot]
bb38e806cd chore(deps): update dependency typedoc to v0.28.17 2026-02-14 02:05:04 +00:00
renovate[bot]
8cbc15f1b0 chore(deps): update dependency electron to v40.4.1 2026-02-14 02:04:20 +00:00
renovate[bot]
870524f9cf chore(deps): update dependency @redocly/cli to v2.18.1 2026-02-14 02:03:29 +00:00
Adorian Doran
218343ca14 client/list view: use a sectioned card to display the list items 2026-02-14 01:14:54 +02:00
Adorian Doran
61953fd713 client/ui/cards: make a property optional 2026-02-14 00:10:54 +02:00
Adorian Doran
62ddf3a11b client/ui/cards: automatically determine the opacity of the background color of nested card sections 2026-02-14 00:08:59 +02:00
Adorian Doran
be12658864 client/ui/cards: add support for custom class names and action callbacks 2026-02-14 00:08:00 +02:00
Adorian Doran
b618e5a00f client: define the foundation of sectioned cards with multi-level nesting 2026-02-13 22:50:03 +02:00
Elian Doran
5da9963f31 fix(website): web clipper URL incorrect 2026-02-13 19:43:07 +02:00
Elian Doran
34e885812f fix(global_menu): advanced menu not accessible in other languages (closes #8694) 2026-02-13 19:40:51 +02:00
Elian Doran
a9ac11452d fix(relation_map): crash when valid JSON, but missing data (closes #8702) 2026-02-13 19:32:29 +02:00
Elian Doran
04b91308b1 chore(search): remove redundant border on mobile 2026-02-13 19:26:08 +02:00
Elian Doran
b09ef222f5 fix(search): errors not displayed (closes #8704) 2026-02-13 19:22:21 +02:00
Elian Doran
2d0ed06d50 Update Node.js to v24.13.1 (#8629) 2026-02-13 13:23:22 +02:00
renovate[bot]
8dd7cf6085 Update Node.js to v24.13.1 2026-02-13 05:50:03 +00:00
Elian Doran
4999bd4f1e Translations update from Hosted Weblate (#8701) 2026-02-13 07:48:43 +02:00
green
22f408addb Translated using Weblate (Japanese)
Currently translated at 100.0% (1777 of 1777 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-02-13 06:48:06 +01:00
AggelosPnS
9ca1dbe638 Translated using Weblate (Greek)
Currently translated at 8.9% (35 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/el/
2026-02-13 06:48:06 +01:00
AggelosPnS
26662952e3 Translated using Weblate (Greek)
Currently translated at 2.0% (36 of 1777 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/el/
2026-02-13 06:48:06 +01:00
Hosted Weblate
f0c9fa4ca3 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-02-13 06:48:06 +01:00
Elian Doran
b51aa1dd71 Update pnpm to v10.29.3 (#8683) 2026-02-13 07:48:00 +02:00
Elian Doran
846253c9e3 Update dependency @codemirror/view to v6.39.14 (#8695) 2026-02-13 07:47:46 +02:00
Elian Doran
6ab6ea97ac Update dependency i18next to v25.8.6 (#8696) 2026-02-13 07:47:31 +02:00
Elian Doran
bf41f70b98 Update dependency wxt to v0.20.17 (#8697) 2026-02-13 07:46:48 +02:00
Elian Doran
67ddbedd08 Update dependency dotenv to v17.3.1 (#8698) 2026-02-13 07:45:17 +02:00
renovate[bot]
2573e219dc Update dependency dotenv to v17.3.1 2026-02-13 01:02:52 +00:00
renovate[bot]
7e368678ab Update dependency wxt to v0.20.17 2026-02-13 01:02:01 +00:00
renovate[bot]
4a9fcf7ab6 Update dependency i18next to v25.8.6 2026-02-13 01:01:10 +00:00
renovate[bot]
65856c61c5 Update dependency @codemirror/view to v6.39.14 2026-02-13 01:00:19 +00:00
renovate[bot]
b5a97bffab Update pnpm to v10.29.3 2026-02-12 17:58:49 +00:00
Elian Doran
e6d728715f feat(geomap): support hiding labels 2026-02-12 19:47:24 +02:00
Elian Doran
54a52f0589 feat(geomap): support for custom tile URLs 2026-02-12 19:32:22 +02:00
Elian Doran
badfa23f86 refactor(geomap): delegate layer data handling to index 2026-02-12 19:12:56 +02:00
Elian Doran
30ccd3487a Translations update from Hosted Weblate (#8666) 2026-02-12 07:50:46 +02:00
Toto Yullian
75e012f2c9 Translated using Weblate (Indonesian)
Currently translated at 3.5% (63 of 1777 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/id/
2026-02-12 06:48:36 +01:00
Ulices
5ecb1d1e2d Translated using Weblate (Spanish)
Currently translated at 100.0% (1777 of 1777 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-02-12 06:48:36 +01:00
Toto Yullian
f8c24c838a Translated using Weblate (Indonesian)
Currently translated at 41.1% (65 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/id/
2026-02-12 06:48:35 +01:00
noobhjy
4ad9cfcdf4 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1777 of 1777 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-02-12 06:48:34 +01:00
Toto Yullian
a57253dd35 Translated using Weblate (Indonesian)
Currently translated at 65.5% (76 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/id/
2026-02-12 06:48:33 +01:00
Marcel
222e65bd45 Translated using Weblate (German)
Currently translated at 100.0% (1777 of 1777 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-02-12 06:48:32 +01:00
Jason Kuanca
3192ea3383 Translated using Weblate (Indonesian)
Currently translated at 60.3% (70 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/id/
2026-02-12 06:48:32 +01:00
green
3a27f873cd Translated using Weblate (Japanese)
Currently translated at 100.0% (1776 of 1776 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-02-12 06:48:31 +01:00
noobhjy
e8fb279036 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1776 of 1776 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-02-12 06:48:30 +01:00
Ulices
21ec7078d2 Translated using Weblate (Spanish)
Currently translated at 100.0% (1776 of 1776 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-02-12 06:48:29 +01:00
Aindriú Mac Giolla Eoin
94937e9fa4 Translated using Weblate (Irish)
Currently translated at 100.0% (1774 of 1774 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-02-12 06:48:28 +01:00
Giovi
36401a20b8 Translated using Weblate (Italian)
Currently translated at 99.7% (1769 of 1774 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2026-02-12 06:48:27 +01:00
noobhjy
8d1c4e4661 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1774 of 1774 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-02-12 06:48:27 +01:00
Kuzma Simonov
14362060c8 Translated using Weblate (Russian)
Currently translated at 99.3% (1763 of 1774 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ru/
2026-02-12 06:48:26 +01:00
Kuzma Simonov
de47e94f62 Translated using Weblate (Russian)
Currently translated at 99.3% (157 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ru/
2026-02-12 06:48:25 +01:00
Giovi
c78ed78bf6 Translated using Weblate (Italian)
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/it/
2026-02-12 06:48:24 +01:00
Ulices
7674c95124 Translated using Weblate (Spanish)
Currently translated at 100.0% (1774 of 1774 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-02-12 06:48:24 +01:00
Giovi
ec522c20b2 Translated using Weblate (Italian)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/it/
2026-02-12 06:48:23 +01:00
green
81f9578526 Translated using Weblate (Japanese)
Currently translated at 100.0% (1774 of 1774 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-02-12 06:48:22 +01:00
Hosted Weblate
20bca751d4 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/
2026-02-12 06:48:22 +01:00
Francis C.
49b5c49776 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/zh_Hant/
2026-02-12 06:48:20 +01:00
Francis C.
5d514fae61 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1772 of 1772 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-02-12 06:48:20 +01:00
Hosted Weblate
9e17e93dd7 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-02-12 06:48:19 +01:00
Elian Doran
7f70c641dc chore(deps): update dependency dotenv to v17.2.4 (#8630) 2026-02-12 07:47:59 +02:00
Elian Doran
7e3af4b7bc chore(deps): update dependency happy-dom to v20.6.1 (#8662) 2026-02-12 07:47:18 +02:00
Elian Doran
59ff4c0aef chore(deps): update dependency wxt to v0.20.15 (#8682) 2026-02-12 07:46:46 +02:00
Elian Doran
875e6b8e53 fix(deps): update dependency i18next to v25.8.5 (#8685) 2026-02-12 07:45:10 +02:00
Elian Doran
fafc44de7c fix(deps): update dependency marked to v17.0.2 (#8686) 2026-02-12 07:44:53 +02:00
Elian Doran
e0d7eb10d5 fix(deps): update dependency mathlive to v0.108.3 (#8687) 2026-02-12 07:44:40 +02:00
Elian Doran
ae01eb28a3 chore(deps): update dependency @redocly/cli to v2.18.0 (#8688) 2026-02-12 07:44:20 +02:00
Elian Doran
637c66c04f chore(deps): update dependency electron to v40.4.0 (#8689) 2026-02-12 07:44:01 +02:00
renovate[bot]
1c061e4428 chore(deps): update dependency electron to v40.4.0 2026-02-12 02:18:16 +00:00
renovate[bot]
cbe0626572 chore(deps): update dependency @redocly/cli to v2.18.0 2026-02-12 02:17:42 +00:00
renovate[bot]
86dc98174a fix(deps): update dependency mathlive to v0.108.3 2026-02-12 02:17:07 +00:00
renovate[bot]
f0ac8ea977 fix(deps): update dependency marked to v17.0.2 2026-02-12 02:16:29 +00:00
renovate[bot]
35d4c2cdfc fix(deps): update dependency i18next to v25.8.5 2026-02-12 02:15:53 +00:00
renovate[bot]
394f7c0d09 chore(deps): update dependency wxt to v0.20.15 2026-02-12 02:13:10 +00:00
Elian Doran
b83eee9bdc Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-02-11 20:21:55 +02:00
Elian Doran
e41b2e8d31 fix(options/shortcuts): filter text box forces lower case 2026-02-11 20:21:52 +02:00
Elian Doran
d93cec2bfd feat(options/shortcuts): add no results 2026-02-11 20:19:53 +02:00
Elian Doran
9eb87a39cd chore(options/shortcuts): refactor filtering 2026-02-11 20:15:39 +02:00
Elian Doran
07818ec1df fix(options): unnecessary full-height 2026-02-11 20:11:46 +02:00
Elian Doran
d4052dbe37 feat(options/shortcuts): equal height 2026-02-11 20:04:09 +02:00
Elian Doran
656f5e0a7f feat(options/shortcuts): make header and footer sticky (closes #8675) 2026-02-11 20:02:37 +02:00
Elian Doran
0cc5e4dac3 chore(deps): update dependency @ckeditor/ckeditor5-dev-build-tools to v54.3.3 (#8609) 2026-02-11 07:51:46 +02:00
Elian Doran
8bbfff3cb2 fix(deps): update dependency i18next to v25.8.4 (#8611) 2026-02-11 07:51:31 +02:00
renovate[bot]
93059798b0 chore(deps): update dependency dotenv to v17.2.4 2026-02-11 05:51:15 +00:00
Elian Doran
33fae88cad chore(deps): update dependency stylelint to v17.2.0 (#8612) 2026-02-11 07:51:15 +02:00
Elian Doran
4feb23e9ca fix(deps): update dependency @preact/signals to v2.7.1 (#8619) 2026-02-11 07:50:04 +02:00
Elian Doran
c2b6b7ba72 chore(deps): update dependency esbuild to v0.27.3 (#8631) 2026-02-11 07:48:22 +02:00
Elian Doran
fb76aee258 chore(deps): update dependency @anthropic-ai/sdk to v0.74.0 (#8633) 2026-02-11 07:47:46 +02:00
Elian Doran
320b1829cc chore(deps): update dependency openai to v6.21.0 (#8634) 2026-02-11 07:47:16 +02:00
Elian Doran
601f0255a4 chore(deps): update dependency @playwright/test to v1.58.2 (#8642) 2026-02-11 07:46:46 +02:00
Elian Doran
b6cc2b227a chore(deps): update dependency wxt to v0.20.14 (#8643) 2026-02-11 07:46:36 +02:00
renovate[bot]
cc7da4b948 chore(deps): update dependency happy-dom to v20.6.1 2026-02-11 05:46:19 +00:00
Elian Doran
1ae3be2fda fix(deps): update codemirror (#8644) 2026-02-11 07:44:55 +02:00
Elian Doran
5b4d35ea86 chore(deps): update dependency @redocly/cli to v2.17.0 (#8645) 2026-02-11 07:44:38 +02:00
Elian Doran
00735e6c8e chore(deps): update dependency axios to v1.13.5 (#8661) 2026-02-11 07:44:27 +02:00
Elian Doran
66a42a38c9 chore(deps): update dependency @smithy/middleware-retry to v4.4.31 (#8669) 2026-02-11 07:43:54 +02:00
Elian Doran
05af1fba80 chore(deps): update dependency rollup-plugin-webpack-stats to v2.1.11 (#8670) 2026-02-11 07:43:45 +02:00
Elian Doran
3b2dd0f5e9 chore(deps): update pnpm to v10.29.2 (#8671) 2026-02-11 07:43:33 +02:00
Elian Doran
1493a66a36 chore(deps): update dependency webdriverio to v9.24.0 (#8672) 2026-02-11 07:43:15 +02:00
Elian Doran
b4bf103fd8 chore(deps): update typescript-eslint monorepo to v8.55.0 (#8673) 2026-02-11 07:43:00 +02:00
renovate[bot]
94286becfd chore(deps): update typescript-eslint monorepo to v8.55.0 2026-02-11 01:04:05 +00:00
renovate[bot]
a68aade58c chore(deps): update dependency webdriverio to v9.24.0 2026-02-11 01:03:25 +00:00
renovate[bot]
014201edf4 chore(deps): update pnpm to v10.29.2 2026-02-11 01:02:09 +00:00
renovate[bot]
8ae6297148 chore(deps): update dependency rollup-plugin-webpack-stats to v2.1.11 2026-02-11 01:01:56 +00:00
renovate[bot]
5e0300aa8e chore(deps): update dependency @smithy/middleware-retry to v4.4.31 2026-02-11 01:01:18 +00:00
renovate[bot]
80a7e18413 chore(deps): update dependency openai to v6.21.0 2026-02-10 21:08:56 +00:00
Elian Doran
43be0a1a3f chore(desktop): disable dev tools for the print window 2026-02-10 17:29:00 +02:00
Elian Doran
5eb32744c3 feat(desktop): display print errors 2026-02-10 17:26:58 +02:00
Elian Doran
89d39f5f2b feat(desktop): add logs when printing 2026-02-10 17:12:28 +02:00
Elian Doran
1f4900dd1e fix(desktop): print failing on upgrade due to cache issues 2026-02-10 16:30:16 +02:00
renovate[bot]
1c561c1483 chore(deps): update dependency stylelint to v17.2.0 2026-02-10 11:44:18 +00:00
Adorian Doran
6baaf60b67 style/web view setup: update icon 2026-02-10 02:53:46 +02:00
Adorian Doran
dde73f6c2b style/attachments: tweak 2026-02-10 02:46:10 +02:00
Adorian Doran
f445a49b34 client/notes/web view: create a web view setup form 2026-02-10 02:45:43 +02:00
Adorian Doran
29016d1cf5 style/refactor: promote centered form as global style 2026-02-10 01:26:07 +02:00
Adorian Doran
c06435046b style/alert bars: use a global style, tweak spacing 2026-02-09 23:39:22 +02:00
Adorian Doran
134422802f style/protected session password request: improve layout and appearance 2026-02-09 23:14:32 +02:00
Adorian Doran
5e00d6a305 style/collapsible: use low profile style for the expand/collapse button 2026-02-09 20:34:58 +02:00
Adorian Doran
b5f0137d8e Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-02-09 20:33:32 +02:00
Adorian Doran
081d041cbc style/buttons: define a style for low profile buttons 2026-02-09 20:33:24 +02:00
Elian Doran
95e733a67c Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-02-09 20:22:43 +02:00
Elian Doran
a664057312 fix(website): missing RPM GPG key (closes #8618) 2026-02-09 20:22:41 +02:00
Elian Doran
5b1a2d93bf fix(tree): child note badge overlapping text (closes #8567) 2026-02-09 20:20:20 +02:00
Adorian Doran
0323f95828 style/status bar: do not wrap the action buttons text 2026-02-09 19:57:13 +02:00
Elian Doran
375838449f fix(geomap): z-index issues with toolbar and buttons (closes #8649) 2026-02-09 19:55:57 +02:00
Adorian Doran
4562de8c2c close #2137 2026-02-09 19:19:41 +02:00
Adorian Doran
68d21669e7 style/similar notes: fix font size variation according to similarity 2026-02-09 19:12:47 +02:00
renovate[bot]
625e0cf159 chore(deps): update dependency @redocly/cli to v2.17.0 2026-02-09 13:58:05 +00:00
renovate[bot]
551ef00c61 fix(deps): update dependency @preact/signals to v2.7.1 2026-02-09 10:46:41 +00:00
renovate[bot]
10518f6364 chore(deps): update dependency axios to v1.13.5 2026-02-09 00:39:09 +00:00
renovate[bot]
3c33b5b169 fix(deps): update codemirror 2026-02-08 13:39:10 +00:00
renovate[bot]
000c31b66c chore(deps): update dependency @anthropic-ai/sdk to v0.74.0 2026-02-07 20:46:58 +00:00
renovate[bot]
fd7780abb0 chore(deps): update dependency esbuild to v0.27.3 2026-02-07 11:53:22 +00:00
renovate[bot]
c8a981e8d6 chore(deps): update dependency wxt to v0.20.14 2026-02-07 00:33:00 +00:00
renovate[bot]
faac45784c chore(deps): update dependency @playwright/test to v1.58.2 2026-02-07 00:32:04 +00:00
renovate[bot]
f6b454cb9a fix(deps): update dependency i18next to v25.8.4 2026-02-05 13:44:21 +00:00
renovate[bot]
563463782c Update dependency @ckeditor/ckeditor5-dev-build-tools to v54.3.3 2026-02-04 01:51:41 +00:00
425 changed files with 15141 additions and 22653 deletions

View File

@@ -1,67 +0,0 @@
name: Deploy Standalone App
on:
# Trigger on push to main branch
push:
branches:
- standalone
# Only run when app files change
paths:
- 'apps/client/**'
- 'apps/client-standalone/**'
- 'packages/trilium-core/**'
- '.github/workflows/deploy-app.yml'
# Allow manual triggering from Actions tab
workflow_dispatch:
# Run on pull requests for preview deployments
pull_request:
paths:
- 'apps/client/**'
- 'apps/client-standalone/**'
- 'packages/trilium-core/**'
- '.github/workflows/deploy-app.yml'
jobs:
build-and-deploy:
name: Build and Deploy App
runs-on: ubuntu-latest
timeout-minutes: 10
# Required permissions for deployment
permissions:
contents: read
deployments: write
pull-requests: write # For PR preview comments
id-token: write # For OIDC authentication (if needed)
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Trigger build of app
run: pnpm --filter=client-standalone build
- name: Deploy
uses: ./.github/actions/deploy-to-cloudflare-pages
if: github.repository == vars.REPO_MAIN
with:
project_name: "trilium-app"
comment_body: "🖥️ App preview is ready"
production_url: "https://app.triliumnotes.org"
deploy_dir: "apps/client-standalone/dist"
cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare_account_id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

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

2
.nvmrc
View File

@@ -1 +1 @@
24.13.0
24.13.1

View File

@@ -9,14 +9,14 @@
"keywords": [],
"author": "Elian Doran <contact@eliandoran.me>",
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.29.1",
"packageManager": "pnpm@10.29.3",
"devDependencies": {
"@redocly/cli": "2.15.1",
"@redocly/cli": "2.18.2",
"archiver": "7.0.1",
"fs-extra": "11.3.3",
"react": "19.2.4",
"react-dom": "19.2.4",
"typedoc": "0.28.16",
"typedoc": "0.28.17",
"typedoc-plugin-missing-exports": "4.1.2"
}
}

View File

@@ -1,4 +0,0 @@
# The development license key for premium CKEditor features.
# Note: This key must only be used for the Trilium Notes project.
VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3ODcyNzA0MDAsImp0aSI6IjkyMWE1MWNlLTliNDMtNGRlMC1iOTQwLTc5ZjM2MDBkYjg1NyIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOiJ0cmlsaXVtIiwiZmVhdHVyZXMiOlsiVFJJTElVTSJdLCJ2YyI6ImU4YzRhMjBkIn0.hny77p-U4-jTkoqbwPytrEar5ylGCWBN7Ez3SlB8i6_mJCBIeCSTOlVQk_JMiOEq3AGykUMHzWXzjdMFwgniOw
VITE_CKEDITOR_ENABLE_INSPECTOR=false

View File

@@ -1 +0,0 @@
VITE_CKEDITOR_ENABLE_INSPECTOR=false

View File

@@ -1,86 +0,0 @@
{
"name": "@triliumnext/client-standalone",
"version": "0.101.3",
"description": "Standalone client for TriliumNext with SQLite WASM backend",
"private": true,
"license": "AGPL-3.0-only",
"scripts": {
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build",
"dev": "vite dev",
"test": "vitest",
"start-prod": "pnpm build && pnpm http-server dist -p 8888",
"coverage": "vitest --coverage"
},
"dependencies": {
"@excalidraw/excalidraw": "0.18.0",
"@fullcalendar/core": "6.1.20",
"@fullcalendar/daygrid": "6.1.20",
"@fullcalendar/interaction": "6.1.20",
"@fullcalendar/list": "6.1.20",
"@fullcalendar/multimonth": "6.1.20",
"@fullcalendar/timegrid": "6.1.20",
"@maplibre/maplibre-gl-leaflet": "0.1.3",
"@mermaid-js/layout-elk": "0.2.0",
"@mind-elixir/node-menu": "5.0.1",
"@popperjs/core": "2.11.8",
"@preact/signals": "2.5.1",
"@sqlite.org/sqlite-wasm": "3.51.1-build2",
"@triliumnext/ckeditor5": "workspace:*",
"@triliumnext/codemirror": "workspace:*",
"@triliumnext/commons": "workspace:*",
"@triliumnext/core": "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",
"clsx": "2.1.1",
"color": "5.0.3",
"debounce": "3.0.0",
"draggabilly": "3.0.0",
"force-graph": "1.51.0",
"globals": "17.0.0",
"i18next": "25.7.3",
"i18next-http-backend": "3.0.2",
"jquery": "3.7.1",
"jquery.fancytree": "2.38.5",
"js-sha1": "0.7.0",
"js-sha512": "0.9.0",
"jsplumb": "2.15.6",
"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.2",
"mind-elixir": "5.4.0",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.28.2",
"react-i18next": "16.5.1",
"react-window": "2.2.3",
"reveal.js": "5.2.1",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",
"vanilla-js-wheel-zoom": "9.0.4"
},
"devDependencies": {
"@ckeditor/ckeditor5-inspector": "5.0.0",
"@preact/preset-vite": "2.10.2",
"@types/bootstrap": "5.2.10",
"@types/jquery": "3.5.33",
"@types/leaflet": "1.9.21",
"@types/leaflet-gpx": "1.3.8",
"@types/mark.js": "8.11.12",
"@types/reveal.js": "5.2.2",
"@types/tabulator-tables": "6.3.1",
"copy-webpack-plugin": "13.0.1",
"cross-env": "7.0.3",
"happy-dom": "20.0.11",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "3.1.4"
}
}

View File

@@ -1,3 +0,0 @@
/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -1,20 +0,0 @@
{
"name": "Trilium Notes",
"short_name": "Trilium",
"description": "Trilium Notes is a hierarchical note taking application with focus on building large personal knowledge bases.",
"theme_color": "#333333",
"background_color": "#1F1F1F",
"display": "standalone",
"scope": "/",
"start_url": "/",
"display_override": [
"window-controls-overlay"
],
"icons": [
{
"src": "assets/icon.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@@ -1,2 +0,0 @@
// Re-export desktop from client
export * from "../../client/src/desktop";

View File

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

View File

@@ -1,254 +0,0 @@
/**
* Browser-compatible router that mimics Express routing patterns.
* Supports path parameters (e.g., /api/notes/:noteId) and query strings.
*/
import { getContext, routes } from "@triliumnext/core";
export interface BrowserRequest {
method: string;
url: string;
path: string;
params: Record<string, string>;
query: Record<string, string | undefined>;
body?: unknown;
}
export interface BrowserResponse {
status: number;
headers: Record<string, string>;
body: ArrayBuffer | null;
}
export type RouteHandler = (req: BrowserRequest) => unknown | Promise<unknown>;
interface Route {
method: string;
pattern: RegExp;
paramNames: string[];
handler: RouteHandler;
}
const encoder = new TextEncoder();
/**
* Convert an Express-style path pattern to a RegExp.
* Supports :param syntax for path parameters.
*
* Examples:
* /api/notes/:noteId -> /^\/api\/notes\/([^\/]+)$/
* /api/notes/:noteId/revisions -> /^\/api\/notes\/([^\/]+)\/revisions$/
*/
function pathToRegex(path: string): { pattern: RegExp; paramNames: string[] } {
const paramNames: string[] = [];
// Escape special regex characters except for :param patterns
const regexPattern = path
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape special chars
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, paramName) => {
paramNames.push(paramName);
return '([^/]+)';
});
return {
pattern: new RegExp(`^${regexPattern}$`),
paramNames
};
}
/**
* Parse query string into an object.
*/
function parseQuery(search: string): Record<string, string | undefined> {
const query: Record<string, string | undefined> = {};
if (!search || search === '?') return query;
const params = new URLSearchParams(search);
for (const [key, value] of params) {
query[key] = value;
}
return query;
}
/**
* Convert a result to a JSON response.
*/
function jsonResponse(obj: unknown, status = 200, extraHeaders: Record<string, string> = {}): BrowserResponse {
const parsedObj = routes.convertEntitiesToPojo(obj);
const body = encoder.encode(JSON.stringify(parsedObj)).buffer as ArrayBuffer;
return {
status,
headers: { "content-type": "application/json; charset=utf-8", ...extraHeaders },
body
};
}
/**
* Convert a string to a text response.
*/
function textResponse(text: string, status = 200, extraHeaders: Record<string, string> = {}): BrowserResponse {
const body = encoder.encode(text).buffer as ArrayBuffer;
return {
status,
headers: { "content-type": "text/plain; charset=utf-8", ...extraHeaders },
body
};
}
/**
* Browser router class that handles route registration and dispatching.
*/
export class BrowserRouter {
private routes: Route[] = [];
/**
* Register a route handler.
*/
register(method: string, path: string, handler: RouteHandler): void {
const { pattern, paramNames } = pathToRegex(path);
this.routes.push({
method: method.toUpperCase(),
pattern,
paramNames,
handler
});
}
/**
* Convenience methods for common HTTP methods.
*/
get(path: string, handler: RouteHandler): void {
this.register('GET', path, handler);
}
post(path: string, handler: RouteHandler): void {
this.register('POST', path, handler);
}
put(path: string, handler: RouteHandler): void {
this.register('PUT', path, handler);
}
patch(path: string, handler: RouteHandler): void {
this.register('PATCH', path, handler);
}
delete(path: string, handler: RouteHandler): void {
this.register('DELETE', path, handler);
}
/**
* Dispatch a request to the appropriate handler.
*/
async dispatch(method: string, urlString: string, body?: unknown, headers?: Record<string, string>): Promise<BrowserResponse> {
const url = new URL(urlString);
const path = url.pathname;
const query = parseQuery(url.search);
const upperMethod = method.toUpperCase();
// Parse JSON body if it's an ArrayBuffer and content-type suggests JSON
let parsedBody = body;
if (body instanceof ArrayBuffer && headers) {
const contentType = headers['content-type'] || headers['Content-Type'] || '';
if (contentType.includes('application/json')) {
try {
const text = new TextDecoder().decode(body);
if (text.trim()) {
parsedBody = JSON.parse(text);
}
} catch (e) {
console.warn('[Router] Failed to parse JSON body:', e);
// Keep original body if JSON parsing fails
parsedBody = body;
}
}
}
// Find matching route
for (const route of this.routes) {
if (route.method !== upperMethod) continue;
const match = path.match(route.pattern);
if (!match) continue;
// Extract path parameters
const params: Record<string, string> = {};
for (let i = 0; i < route.paramNames.length; i++) {
params[route.paramNames[i]] = decodeURIComponent(match[i + 1]);
}
const request: BrowserRequest = {
method: upperMethod,
url: urlString,
path,
params,
query,
body: parsedBody
};
try {
const result = await getContext().init(async () => await route.handler(request));
return this.formatResult(result);
} catch (error) {
return this.formatError(error, `Error handling ${method} ${path}`);
}
}
// No route matched
return textResponse(`Not found: ${method} ${path}`, 404);
}
/**
* Format a handler result into a response.
* Follows the same patterns as the server's apiResultHandler.
*/
private formatResult(result: unknown): BrowserResponse {
// Handle [statusCode, response] format
if (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) {
const [statusCode, response] = result;
return jsonResponse(response, statusCode);
}
// Handle undefined (no content) - 204 should have no body
if (result === undefined) {
return {
status: 204,
headers: {},
body: null
};
}
// Default: JSON response with 200
return jsonResponse(result, 200);
}
/**
* Format an error into a response.
*/
private formatError(error: unknown, context: string): BrowserResponse {
console.error('[Router] Handler error:', context, error);
// Check for known error types
if (error && typeof error === 'object') {
const err = error as { constructor?: { name?: string }; message?: string };
if (err.constructor?.name === 'NotFoundError') {
return jsonResponse({ message: err.message || 'Not found' }, 404);
}
if (err.constructor?.name === 'ValidationError') {
return jsonResponse({ message: err.message || 'Validation error' }, 400);
}
}
// Generic error
const message = error instanceof Error ? error.message : String(error);
return jsonResponse({ message }, 500);
}
}
/**
* Create a new router instance.
*/
export function createRouter(): BrowserRouter {
return new BrowserRouter();
}

View File

@@ -1,101 +0,0 @@
/**
* Browser route definitions.
* This integrates with the shared route builder from @triliumnext/core.
*/
import { BootstrapDefinition } from '@triliumnext/commons';
import { getSharedBootstrapItems, getSql, routes } from '@triliumnext/core';
import packageJson from '../../package.json' with { type: 'json' };
import { type BrowserRequest,BrowserRouter } from './browser_router';
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
/**
* Wraps a core route handler to work with the BrowserRouter.
* Core handlers expect an Express-like request object with params, query, and body.
*/
function wrapHandler(handler: (req: any) => unknown, transactional: boolean) {
return (req: BrowserRequest) => {
// Create an Express-like request object
const expressLikeReq = {
params: req.params,
query: req.query,
body: req.body
};
if (transactional) {
return getSql().transactional(() => handler(expressLikeReq));
}
return handler(expressLikeReq);
};
}
/**
* Creates an apiRoute function compatible with buildSharedApiRoutes.
* This bridges the core's route registration to the BrowserRouter.
*/
function createApiRoute(router: BrowserRouter) {
return (method: HttpMethod, path: string, handler: (req: any) => unknown) => {
router.register(method, path, wrapHandler(handler, true));
};
}
/**
* Register all API routes on the browser router using the shared builder.
*
* @param router - The browser router instance
*/
export function registerRoutes(router: BrowserRouter): void {
const apiRoute = createApiRoute(router);
routes.buildSharedApiRoutes(apiRoute);
apiRoute('get', '/bootstrap', bootstrapRoute);
// Dummy routes for compatibility.
apiRoute("get", "/api/script/widgets", () => []);
apiRoute("get", "/api/script/startup", () => []);
apiRoute("get", "/api/system-checks", () => ({ isCpuArchMismatch: false }));
apiRoute("get", "/api/search/:searchString", () => []);
apiRoute("get", "/api/search-templates", () => []);
apiRoute("get", "/api/autocomplete", () => []);
}
function bootstrapRoute() {
const assetPath = ".";
return {
...getSharedBootstrapItems(assetPath),
appPath: assetPath,
device: false, // Let the client detect device type.
csrfToken: "dummy-csrf-token",
themeCssUrl: false,
themeUseNextAsBase: "next",
triliumVersion: packageJson.version,
baseApiUrl: "../api/",
headingStyle: "plain",
layoutOrientation: "vertical",
platform: "web",
isDev: import.meta.env.DEV,
isMainWindow: true,
isElectron: false,
isStandalone: true,
hasNativeTitleBar: false,
hasBackgroundEffects: false,
// TODO: Fill properly
currentLocale: { id: "en", name: "English", rtl: false },
isRtl: false,
instanceName: null,
appCssNoteIds: [],
TRILIUM_SAFE_MODE: false
} satisfies BootstrapDefinition;
}
/**
* Create and configure a router with all routes registered.
*/
export function createConfiguredRouter(): BrowserRouter {
const router = new BrowserRouter();
registerRoutes(router);
return router;
}

View File

@@ -1,77 +0,0 @@
import { ExecutionContext } from "@triliumnext/core";
/**
* Browser execution context implementation.
*
* Handles per-request context isolation with support for fire-and-forget async operations
* using a context stack and grace-period cleanup to allow unawaited promises to complete.
*/
export default class BrowserExecutionContext implements ExecutionContext {
private contextStack: Map<string, any>[] = [];
private cleanupTimers = new WeakMap<Map<string, any>, ReturnType<typeof setTimeout>>();
private readonly CLEANUP_GRACE_PERIOD = 1000; // 1 second for fire-and-forget operations
private getCurrentContext(): Map<string, any> {
if (this.contextStack.length === 0) {
throw new Error("ExecutionContext not initialized");
}
return this.contextStack[this.contextStack.length - 1];
}
get<T = any>(key: string): T {
return this.getCurrentContext().get(key);
}
set(key: string, value: any): void {
this.getCurrentContext().set(key, value);
}
reset(): void {
this.contextStack = [];
}
init<T>(callback: () => T): T {
const context = new Map<string, any>();
this.contextStack.push(context);
// Cancel any pending cleanup timer for this context
const existingTimer = this.cleanupTimers.get(context);
if (existingTimer) {
clearTimeout(existingTimer);
this.cleanupTimers.delete(context);
}
try {
const result = callback();
// If the result is a Promise
if (result && typeof result === 'object' && 'then' in result && 'catch' in result) {
const promise = result as unknown as Promise<any>;
return promise.finally(() => {
this.scheduleContextCleanup(context);
}) as T;
} else {
// For synchronous results, schedule delayed cleanup to allow fire-and-forget operations
this.scheduleContextCleanup(context);
return result;
}
} catch (error) {
// Always clean up on error with grace period
this.scheduleContextCleanup(context);
throw error;
}
}
private scheduleContextCleanup(context: Map<string, any>): void {
const timer = setTimeout(() => {
// Remove from stack if still present
const index = this.contextStack.indexOf(context);
if (index !== -1) {
this.contextStack.splice(index, 1);
}
this.cleanupTimers.delete(context);
}, this.CLEANUP_GRACE_PERIOD);
this.cleanupTimers.set(context, timer);
}
}

View File

@@ -1,145 +0,0 @@
import type { CryptoProvider } from "@triliumnext/core";
import { sha1 } from "js-sha1";
import { sha512 } from "js-sha512";
interface Cipher {
update(data: Uint8Array): Uint8Array;
final(): Uint8Array;
}
const CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
/**
* Crypto provider for browser environments using the Web Crypto API.
*/
export default class BrowserCryptoProvider implements CryptoProvider {
createHash(algorithm: "sha1" | "sha512", content: string | Uint8Array): Uint8Array {
const data = typeof content === "string" ? content :
new TextDecoder().decode(content);
const hexHash = algorithm === "sha1" ? sha1(data) : sha512(data);
// Convert hex string to Uint8Array
const bytes = new Uint8Array(hexHash.length / 2);
for (let i = 0; i < hexHash.length; i += 2) {
bytes[i / 2] = parseInt(hexHash.substr(i, 2), 16);
}
return bytes;
}
createCipheriv(algorithm: "aes-128-cbc", key: Uint8Array, iv: Uint8Array): Cipher {
// Web Crypto API doesn't support streaming cipher like Node.js
// We need to implement a wrapper that collects data and encrypts on final()
return new WebCryptoCipher(algorithm, key, iv, "encrypt");
}
createDecipheriv(algorithm: "aes-128-cbc", key: Uint8Array, iv: Uint8Array): Cipher {
return new WebCryptoCipher(algorithm, key, iv, "decrypt");
}
randomBytes(size: number): Uint8Array {
const bytes = new Uint8Array(size);
crypto.getRandomValues(bytes);
return bytes;
}
randomString(length: number): string {
const bytes = this.randomBytes(length);
let result = "";
for (let i = 0; i < length; i++) {
result += CHARS[bytes[i] % CHARS.length];
}
return result;
}
}
/**
* A cipher implementation that wraps Web Crypto API.
* Note: This buffers all data until final() is called, which differs from
* Node.js's streaming cipher behavior.
*/
class WebCryptoCipher implements Cipher {
private chunks: Uint8Array[] = [];
private algorithm: string;
private key: Uint8Array;
private iv: Uint8Array;
private mode: "encrypt" | "decrypt";
private finalized = false;
constructor(
algorithm: "aes-128-cbc",
key: Uint8Array,
iv: Uint8Array,
mode: "encrypt" | "decrypt"
) {
this.algorithm = algorithm;
this.key = key;
this.iv = iv;
this.mode = mode;
}
update(data: Uint8Array): Uint8Array {
if (this.finalized) {
throw new Error("Cipher has already been finalized");
}
// Buffer the data - Web Crypto doesn't support streaming
this.chunks.push(data);
// Return empty array since we process everything in final()
return new Uint8Array(0);
}
final(): Uint8Array {
if (this.finalized) {
throw new Error("Cipher has already been finalized");
}
this.finalized = true;
// Web Crypto API is async, but we need sync behavior
// This is a fundamental limitation that requires architectural changes
// For now, throw an error directing users to use async methods
throw new Error(
"Synchronous cipher finalization not available in browser. " +
"The Web Crypto API is async-only. Use finalizeAsync() instead."
);
}
/**
* Async version that actually performs the encryption/decryption.
*/
async finalizeAsync(): Promise<Uint8Array> {
if (this.finalized) {
throw new Error("Cipher has already been finalized");
}
this.finalized = true;
// Concatenate all chunks
const totalLength = this.chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const data = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of this.chunks) {
data.set(chunk, offset);
offset += chunk.length;
}
// Copy key and iv to ensure they're plain ArrayBuffer-backed
const keyBuffer = new Uint8Array(this.key);
const ivBuffer = new Uint8Array(this.iv);
// Import the key
const cryptoKey = await crypto.subtle.importKey(
"raw",
keyBuffer,
{ name: "AES-CBC" },
false,
[this.mode]
);
// Perform encryption/decryption
const result = this.mode === "encrypt"
? await crypto.subtle.encrypt({ name: "AES-CBC", iv: ivBuffer }, cryptoKey, data)
: await crypto.subtle.decrypt({ name: "AES-CBC", iv: ivBuffer }, cryptoKey, data);
return new Uint8Array(result);
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,90 +0,0 @@
import type { WebSocketMessage } from "@triliumnext/commons";
import type { MessagingProvider, MessageHandler } from "@triliumnext/core";
/**
* Messaging provider for browser Worker environments.
*
* This provider uses the Worker's postMessage API to communicate
* with the main thread. It's designed to be used inside a Web Worker
* that runs the core services.
*
* Message flow:
* - Outbound (worker → main): Uses self.postMessage() with type: "WS_MESSAGE"
* - Inbound (main → worker): Listens to onmessage for type: "WS_MESSAGE"
*/
export default class WorkerMessagingProvider implements MessagingProvider {
private messageHandlers: MessageHandler[] = [];
private isDisposed = false;
constructor() {
// Listen for incoming messages from the main thread
self.addEventListener("message", this.handleIncomingMessage);
}
private handleIncomingMessage = (event: MessageEvent) => {
if (this.isDisposed) return;
const { type, message } = event.data || {};
if (type === "WS_MESSAGE" && message) {
// Dispatch to all registered handlers
for (const handler of this.messageHandlers) {
try {
handler(message as WebSocketMessage);
} catch (e) {
console.error("[WorkerMessagingProvider] Error in message handler:", e);
}
}
}
};
/**
* Send a message to all clients (in this case, the main thread).
* The main thread is responsible for further distribution if needed.
*/
sendMessageToAllClients(message: WebSocketMessage): void {
if (this.isDisposed) {
console.warn("[WorkerMessagingProvider] Cannot send message - provider is disposed");
return;
}
try {
self.postMessage({
type: "WS_MESSAGE",
message
});
} catch (e) {
console.error("[WorkerMessagingProvider] Error sending message:", e);
}
}
/**
* Subscribe to incoming messages from the main thread.
*/
onMessage(handler: MessageHandler): () => void {
this.messageHandlers.push(handler);
return () => {
this.messageHandlers = this.messageHandlers.filter(h => h !== handler);
};
}
/**
* Get the number of connected "clients".
* In worker context, there's always exactly 1 client (the main thread).
*/
getClientCount(): number {
return this.isDisposed ? 0 : 1;
}
/**
* Clean up resources.
*/
dispose(): void {
if (this.isDisposed) return;
this.isDisposed = true;
self.removeEventListener("message", this.handleIncomingMessage);
this.messageHandlers = [];
}
}

View File

@@ -1,618 +0,0 @@
import { type BindableValue, default as sqlite3InitModule } from "@sqlite.org/sqlite-wasm";
import type { DatabaseProvider, RunResult, Statement, Transaction } from "@triliumnext/core";
import demoDbSql from "./db.sql?raw";
// Type definitions for SQLite WASM (the library doesn't export these directly)
type Sqlite3Module = Awaited<ReturnType<typeof sqlite3InitModule>>;
type Sqlite3Database = InstanceType<Sqlite3Module["oo1"]["DB"]>;
type Sqlite3PreparedStatement = ReturnType<Sqlite3Database["prepare"]>;
/**
* Wraps an SQLite WASM PreparedStatement to match the Statement interface
* expected by trilium-core.
*/
class WasmStatement implements Statement {
private isRawMode = false;
private isPluckMode = false;
private isFinalized = false;
constructor(
private stmt: Sqlite3PreparedStatement,
private db: Sqlite3Database,
private sqlite3: Sqlite3Module
) {}
run(...params: unknown[]): RunResult {
if (this.isFinalized) {
throw new Error("Cannot call run() on finalized statement");
}
this.bindParams(params);
try {
// Use step() and then reset instead of stepFinalize()
// This allows the statement to be reused
this.stmt.step();
const changes = this.db.changes();
// Get the last insert row ID using the C API
const lastInsertRowid = this.db.pointer ? this.sqlite3.capi.sqlite3_last_insert_rowid(this.db.pointer) : 0;
this.stmt.reset();
return {
changes,
lastInsertRowid: typeof lastInsertRowid === "bigint" ? Number(lastInsertRowid) : lastInsertRowid
};
} catch (e) {
// Reset on error to allow reuse
this.stmt.reset();
throw e;
}
}
get(params: unknown): unknown {
if (this.isFinalized) {
throw new Error("Cannot call get() on finalized statement");
}
this.bindParams(Array.isArray(params) ? params : params !== undefined ? [params] : []);
try {
if (this.stmt.step()) {
if (this.isPluckMode) {
// In pluck mode, return only the first column value
const row = this.stmt.get([]);
return Array.isArray(row) && row.length > 0 ? row[0] : undefined;
}
return this.isRawMode ? this.stmt.get([]) : this.stmt.get({});
}
return undefined;
} finally {
this.stmt.reset();
}
}
all(...params: unknown[]): unknown[] {
if (this.isFinalized) {
throw new Error("Cannot call all() on finalized statement");
}
this.bindParams(params);
const results: unknown[] = [];
try {
while (this.stmt.step()) {
if (this.isPluckMode) {
// In pluck mode, return only the first column value for each row
const row = this.stmt.get([]);
if (Array.isArray(row) && row.length > 0) {
results.push(row[0]);
}
} else {
results.push(this.isRawMode ? this.stmt.get([]) : this.stmt.get({}));
}
}
return results;
} finally {
this.stmt.reset();
}
}
iterate(...params: unknown[]): IterableIterator<unknown> {
if (this.isFinalized) {
throw new Error("Cannot call iterate() on finalized statement");
}
this.bindParams(params);
const stmt = this.stmt;
const isRaw = this.isRawMode;
const isPluck = this.isPluckMode;
return {
[Symbol.iterator]() {
return this;
},
next(): IteratorResult<unknown> {
if (stmt.step()) {
if (isPluck) {
const row = stmt.get([]);
const value = Array.isArray(row) && row.length > 0 ? row[0] : undefined;
return { value, done: false };
}
return { value: isRaw ? stmt.get([]) : stmt.get({}), done: false };
}
stmt.reset();
return { value: undefined, done: true };
}
};
}
raw(toggleState?: boolean): this {
// In raw mode, rows are returned as arrays instead of objects
// If toggleState is undefined, enable raw mode (better-sqlite3 behavior)
this.isRawMode = toggleState !== undefined ? toggleState : true;
return this;
}
pluck(toggleState?: boolean): this {
// In pluck mode, only the first column of each row is returned
// If toggleState is undefined, enable pluck mode (better-sqlite3 behavior)
this.isPluckMode = toggleState !== undefined ? toggleState : true;
return this;
}
private bindParams(params: unknown[]): void {
this.stmt.clearBindings();
if (params.length === 0) {
return;
}
// Handle single object with named parameters
if (params.length === 1 && typeof params[0] === "object" && params[0] !== null && !Array.isArray(params[0])) {
const inputBindings = params[0] as { [paramName: string]: BindableValue };
// SQLite WASM expects parameter names to include the prefix (@ : or $)
// better-sqlite3 automatically maps unprefixed names to @name
// We need to add the @ prefix for compatibility
const bindings: { [paramName: string]: BindableValue } = {};
for (const [key, value] of Object.entries(inputBindings)) {
// If the key already has a prefix, use it as-is
if (key.startsWith('@') || key.startsWith(':') || key.startsWith('$')) {
bindings[key] = value;
} else {
// Add @ prefix to match better-sqlite3 behavior
bindings[`@${key}`] = value;
}
}
this.stmt.bind(bindings);
} else {
// Handle positional parameters - flatten and cast to BindableValue[]
const flatParams = params.flat() as BindableValue[];
if (flatParams.length > 0) {
this.stmt.bind(flatParams);
}
}
}
finalize(): void {
if (!this.isFinalized) {
try {
this.stmt.finalize();
} catch (e) {
console.warn("Error finalizing SQLite statement:", e);
} finally {
this.isFinalized = true;
}
}
}
}
/**
* SQLite database provider for browser environments using SQLite WASM.
*
* This provider wraps the official @sqlite.org/sqlite-wasm package to provide
* a DatabaseProvider implementation compatible with trilium-core.
*
* @example
* ```typescript
* const provider = new BrowserSqlProvider();
* await provider.initWasm(); // Initialize SQLite WASM module
* provider.loadFromMemory(); // Open an in-memory database
* // or
* provider.loadFromBuffer(existingDbBuffer); // Load from existing data
* ```
*/
export default class BrowserSqlProvider implements DatabaseProvider {
private db?: Sqlite3Database;
private sqlite3?: Sqlite3Module;
private _inTransaction = false;
private initPromise?: Promise<void>;
private initError?: Error;
private statementCache: Map<string, WasmStatement> = new Map();
// OPFS state tracking
private opfsDbPath?: string;
/**
* Get the SQLite WASM module version info.
* Returns undefined if the module hasn't been initialized yet.
*/
get version(): { libVersion: string; sourceId: string } | undefined {
return this.sqlite3?.version;
}
/**
* Initialize the SQLite WASM module.
* This must be called before using any database operations.
* Safe to call multiple times - subsequent calls return the same promise.
*
* @returns A promise that resolves when the module is initialized
* @throws Error if initialization fails
*/
async initWasm(): Promise<void> {
// Return existing promise if already initializing/initialized
if (this.initPromise) {
return this.initPromise;
}
// Fail fast if we already tried and failed
if (this.initError) {
throw this.initError;
}
this.initPromise = this.doInitWasm();
return this.initPromise;
}
private async doInitWasm(): Promise<void> {
try {
console.log("[BrowserSqlProvider] Initializing SQLite WASM...");
const startTime = performance.now();
this.sqlite3 = await sqlite3InitModule({
print: console.log,
printErr: console.error,
});
const initTime = performance.now() - startTime;
console.log(
`[BrowserSqlProvider] SQLite WASM initialized in ${initTime.toFixed(2)}ms:`,
this.sqlite3.version.libVersion
);
} catch (e) {
this.initError = e instanceof Error ? e : new Error(String(e));
console.error("[BrowserSqlProvider] SQLite WASM initialization failed:", this.initError);
throw this.initError;
}
}
/**
* Check if the SQLite WASM module has been initialized.
*/
get isInitialized(): boolean {
return this.sqlite3 !== undefined;
}
// ==================== OPFS Support ====================
/**
* Check if the OPFS VFS is available.
* This requires:
* - Running in a Worker context
* - Browser support for OPFS APIs
* - COOP/COEP headers sent by the server (for SharedArrayBuffer)
*
* @returns true if OPFS VFS is available for use
*/
isOpfsAvailable(): boolean {
this.ensureSqlite3();
// SQLite WASM automatically installs the OPFS VFS if the environment supports it
// We can check for its presence via sqlite3_vfs_find or the OpfsDb class
return this.sqlite3!.oo1.OpfsDb !== undefined;
}
/**
* Load or create a database stored in OPFS for persistent storage.
* The database will persist across browser sessions.
*
* Requires COOP/COEP headers to be set by the server:
* - Cross-Origin-Opener-Policy: same-origin
* - Cross-Origin-Embedder-Policy: require-corp
*
* @param path - The path for the database file in OPFS (e.g., "/trilium.db")
* Paths without a leading slash are treated as relative to OPFS root.
* Leading directories are created automatically.
* @param options - Additional options
* @throws Error if OPFS VFS is not available
*
* @example
* ```typescript
* const provider = new BrowserSqlProvider();
* await provider.initWasm();
* if (provider.isOpfsAvailable()) {
* provider.loadFromOpfs("/my-database.db");
* } else {
* console.warn("OPFS not available, using in-memory database");
* provider.loadFromMemory();
* }
* ```
*/
loadFromOpfs(path: string, options: { createIfNotExists?: boolean } = {}): void {
this.ensureSqlite3();
if (!this.isOpfsAvailable()) {
throw new Error(
"OPFS VFS is not available. This requires:\n" +
"1. Running in a Worker context\n" +
"2. Browser support for OPFS (Chrome 102+, Firefox 111+, Safari 17+)\n" +
"3. COOP/COEP headers from the server:\n" +
" Cross-Origin-Opener-Policy: same-origin\n" +
" Cross-Origin-Embedder-Policy: require-corp"
);
}
console.log(`[BrowserSqlProvider] Loading database from OPFS: ${path}`);
const startTime = performance.now();
try {
// OpfsDb automatically creates directories in the path
// Mode 'c' = create if not exists
const mode = options.createIfNotExists !== false ? 'c' : '';
this.db = new this.sqlite3!.oo1.OpfsDb(path, mode);
this.opfsDbPath = path;
// Configure the database for OPFS
// Note: WAL mode requires exclusive locking in OPFS environment
this.db.exec("PRAGMA journal_mode = DELETE");
this.db.exec("PRAGMA synchronous = NORMAL");
const loadTime = performance.now() - startTime;
console.log(`[BrowserSqlProvider] OPFS database loaded in ${loadTime.toFixed(2)}ms`);
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
console.error(`[BrowserSqlProvider] Failed to load OPFS database: ${error.message}`);
throw error;
}
}
/**
* Check if the currently open database is stored in OPFS.
*/
get isUsingOpfs(): boolean {
return this.opfsDbPath !== undefined;
}
/**
* Get the OPFS path of the currently open database.
* Returns undefined if not using OPFS.
*/
get currentOpfsPath(): string | undefined {
return this.opfsDbPath;
}
/**
* Check if the database has been initialized with a schema.
* This is a simple sanity check that looks for the existence of core tables.
*
* @returns true if the database appears to be initialized
*/
isDbInitialized(): boolean {
this.ensureDb();
// Check if the 'notes' table exists (a core table that must exist in an initialized DB)
const tableExists = this.db!.selectValue(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'notes'"
);
return tableExists !== undefined;
}
// ==================== End OPFS Support ====================
loadFromFile(_path: string, _isReadOnly: boolean): void {
// Browser environment doesn't have direct file system access.
// Use OPFS for persistent storage.
throw new Error(
"loadFromFile is not supported in browser environment. " +
"Use loadFromMemory() for temporary databases, loadFromBuffer() to load from data, " +
"or loadFromOpfs() for persistent storage."
);
}
/**
* Create an empty in-memory database.
* Data will be lost when the page is closed.
*
* For persistent storage, use loadFromOpfs() instead.
* To load demo data, call initializeDemoDatabase() after this.
*/
loadFromMemory(): void {
this.ensureSqlite3();
console.log("[BrowserSqlProvider] Creating in-memory database...");
const startTime = performance.now();
this.db = new this.sqlite3!.oo1.DB(":memory:", "c");
this.opfsDbPath = undefined; // Not using OPFS
this.db.exec("PRAGMA journal_mode = WAL");
// Initialize with demo data for in-memory databases
// (since they won't persist anyway)
this.initializeDemoDatabase();
const loadTime = performance.now() - startTime;
console.log(`[BrowserSqlProvider] In-memory database created in ${loadTime.toFixed(2)}ms`);
}
/**
* Initialize the database with demo/starter data.
* This should only be called once when creating a new database.
*
* For OPFS databases, this is called automatically only if the database
* doesn't already exist.
*/
initializeDemoDatabase(): void {
this.ensureDb();
console.log("[BrowserSqlProvider] Initializing database with demo data...");
const startTime = performance.now();
this.db!.exec(demoDbSql);
const loadTime = performance.now() - startTime;
console.log(`[BrowserSqlProvider] Demo data loaded in ${loadTime.toFixed(2)}ms`);
}
loadFromBuffer(buffer: Uint8Array): void {
this.ensureSqlite3();
// SQLite WASM can deserialize a database from a byte array
const p = this.sqlite3!.wasm.allocFromTypedArray(buffer);
try {
this.db = new this.sqlite3!.oo1.DB({ filename: ":memory:", flags: "c" });
this.opfsDbPath = undefined; // Not using OPFS
const rc = this.sqlite3!.capi.sqlite3_deserialize(
this.db.pointer!,
"main",
p,
buffer.byteLength,
buffer.byteLength,
this.sqlite3!.capi.SQLITE_DESERIALIZE_FREEONCLOSE |
this.sqlite3!.capi.SQLITE_DESERIALIZE_RESIZEABLE
);
if (rc !== 0) {
throw new Error(`Failed to deserialize database: ${rc}`);
}
} catch (e) {
this.sqlite3!.wasm.dealloc(p);
throw e;
}
}
backup(_destinationFile: string): void {
// In browser, we can serialize the database to a byte array
// For actual file backup, we'd need to use File System Access API or download
throw new Error(
"backup to file is not supported in browser environment. " +
"Use serialize() to get the database as a Uint8Array instead."
);
}
/**
* Serialize the database to a byte array.
* This can be used to save the database to IndexedDB, download it, etc.
*/
serialize(): Uint8Array {
this.ensureDb();
// Use the convenience wrapper which handles all the memory management
return this.sqlite3!.capi.sqlite3_js_db_export(this.db!);
}
prepare(query: string): Statement {
this.ensureDb();
// Check if we already have this statement cached
if (this.statementCache.has(query)) {
return this.statementCache.get(query)!;
}
// Create new statement and cache it
const stmt = this.db!.prepare(query);
const wasmStatement = new WasmStatement(stmt, this.db!, this.sqlite3!);
this.statementCache.set(query, wasmStatement);
return wasmStatement;
}
transaction<T>(func: (statement: Statement) => T): Transaction {
this.ensureDb();
const self = this;
let savepointCounter = 0;
// Helper function to execute within a transaction
const executeTransaction = (beginStatement: string, ...args: unknown[]): T => {
// If we're already in a transaction, use SAVEPOINTs for nesting
// This mimics better-sqlite3's behavior
if (self._inTransaction) {
const savepointName = `sp_${++savepointCounter}_${Date.now()}`;
self.db!.exec(`SAVEPOINT ${savepointName}`);
try {
const result = func.apply(null, args as [Statement]);
self.db!.exec(`RELEASE SAVEPOINT ${savepointName}`);
return result;
} catch (e) {
self.db!.exec(`ROLLBACK TO SAVEPOINT ${savepointName}`);
throw e;
}
}
// Not in a transaction, start a new one
self._inTransaction = true;
self.db!.exec(beginStatement);
try {
const result = func.apply(null, args as [Statement]);
self.db!.exec("COMMIT");
return result;
} catch (e) {
self.db!.exec("ROLLBACK");
throw e;
} finally {
self._inTransaction = false;
}
};
// Create the transaction function that acts like better-sqlite3's Transaction interface
// In better-sqlite3, the transaction function is callable and has .deferred(), .immediate(), etc.
const transactionWrapper = Object.assign(
// Default call executes with BEGIN (same as immediate)
(...args: unknown[]): T => executeTransaction("BEGIN", ...args),
{
// Deferred transaction - locks acquired on first data access
deferred: (...args: unknown[]): T => executeTransaction("BEGIN DEFERRED", ...args),
// Immediate transaction - acquires write lock immediately
immediate: (...args: unknown[]): T => executeTransaction("BEGIN IMMEDIATE", ...args),
// Exclusive transaction - exclusive lock
exclusive: (...args: unknown[]): T => executeTransaction("BEGIN EXCLUSIVE", ...args),
// Default is same as calling directly
default: (...args: unknown[]): T => executeTransaction("BEGIN", ...args)
}
);
return transactionWrapper as unknown as Transaction;
}
get inTransaction(): boolean {
return this._inTransaction;
}
exec(query: string): void {
this.ensureDb();
this.db!.exec(query);
}
close(): void {
// Clean up all cached statements first
for (const statement of this.statementCache.values()) {
try {
statement.finalize();
} catch (e) {
// Ignore errors during cleanup
console.warn("Error finalizing statement during cleanup:", e);
}
}
this.statementCache.clear();
if (this.db) {
this.db.close();
this.db = undefined;
}
// Reset OPFS state
this.opfsDbPath = undefined;
}
/**
* Get the number of rows changed by the last INSERT, UPDATE, or DELETE statement.
*/
changes(): number {
this.ensureDb();
return this.db!.changes();
}
/**
* Check if the database is currently open.
*/
isOpen(): boolean {
return this.db !== undefined && this.db.isOpen();
}
private ensureSqlite3(): void {
if (!this.sqlite3) {
throw new Error(
"SQLite WASM module not initialized. Call initialize() first with the sqlite3 module."
);
}
}
private ensureDb(): void {
this.ensureSqlite3();
if (!this.db) {
throw new Error("Database not opened. Call loadFromMemory(), loadFromBuffer(), or loadFromOpfs() first.");
}
}
}

View File

@@ -1,16 +0,0 @@
import { LOCALE_IDS } from "@triliumnext/commons";
import type i18next from "i18next";
import I18NextHttpBackend from "i18next-http-backend";
export default async function translationProvider(i18nextInstance: typeof i18next, locale: LOCALE_IDS) {
await i18nextInstance.use(I18NextHttpBackend).init({
lng: locale,
fallbackLng: "en",
ns: "server",
backend: {
loadPath: `${import.meta.resolve("../server-assets/translations")}/{{lng}}/{{ns}}.json`
},
returnEmptyString: false,
debug: true
});
}

View File

@@ -1,88 +0,0 @@
import LocalServerWorker from "./local-server-worker?worker";
let localWorker: Worker | null = null;
const pending = new Map();
export function startLocalServerWorker() {
if (localWorker) return localWorker;
localWorker = new LocalServerWorker();
// Handle worker errors during initialization
localWorker.onerror = (event) => {
console.error("[LocalBridge] Worker error:", event);
// Reject all pending requests
for (const [, resolver] of pending) {
resolver.reject(new Error(`Worker error: ${event.message}`));
}
pending.clear();
};
localWorker.onmessage = (event) => {
const msg = event.data;
// Handle worker error reports
if (msg?.type === "WORKER_ERROR") {
console.error("[LocalBridge] Worker reported error:", msg.error);
// Reject all pending requests with the error
for (const [, resolver] of pending) {
resolver.reject(new Error(msg.error?.message || "Unknown worker error"));
}
pending.clear();
return;
}
if (!msg || msg.type !== "LOCAL_RESPONSE") return;
const { id, response, error } = msg;
const resolver = pending.get(id);
if (!resolver) return;
pending.delete(id);
if (error) resolver.reject(new Error(error));
else resolver.resolve(response);
};
return localWorker;
}
export function attachServiceWorkerBridge() {
navigator.serviceWorker.addEventListener("message", async (event) => {
const msg = event.data;
if (!msg || msg.type !== "LOCAL_FETCH") return;
const port = event.ports && event.ports[0];
if (!port) return;
try {
startLocalServerWorker();
const id = msg.id;
const req = msg.request;
const response = await new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
// Transfer body to worker for efficiency (if present)
localWorker.postMessage({
type: "LOCAL_REQUEST",
id,
request: req
}, req.body ? [req.body] : []);
});
port.postMessage({
type: "LOCAL_FETCH_RESPONSE",
id,
response
}, response.body ? [response.body] : []);
} catch (e) {
port.postMessage({
type: "LOCAL_FETCH_RESPONSE",
id: msg.id,
response: {
status: 500,
headers: { "content-type": "text/plain; charset=utf-8" },
body: new TextEncoder().encode(String(e?.message || e)).buffer
}
});
}
});
}

View File

@@ -1,207 +0,0 @@
import { BrowserRouter } from './lightweight/browser_router';
import { createConfiguredRouter } from './lightweight/browser_routes';
import BrowserExecutionContext from './lightweight/cls_provider';
import BrowserCryptoProvider from './lightweight/crypto_provider';
import WorkerMessagingProvider from './lightweight/messaging_provider';
import BrowserSqlProvider from './lightweight/sql_provider';
import translationProvider from './lightweight/translation_provider';
// Global error handlers - MUST be set up before any async imports
self.onerror = (message, source, lineno, colno, error) => {
console.error("[Worker] Uncaught error:", message, source, lineno, colno, error);
// Try to notify the main thread about the error
try {
self.postMessage({
type: "WORKER_ERROR",
error: {
message: String(message),
source,
lineno,
colno,
stack: error?.stack
}
});
} catch (e) {
// Can't even post message, just log
console.error("[Worker] Failed to report error:", e);
}
return false; // Don't suppress the error
};
self.onunhandledrejection = (event) => {
console.error("[Worker] Unhandled rejection:", event.reason);
try {
self.postMessage({
type: "WORKER_ERROR",
error: {
message: String(event.reason?.message || event.reason),
stack: event.reason?.stack
}
});
} catch (e) {
console.error("[Worker] Failed to report rejection:", e);
}
};
console.log("[Worker] Error handlers installed");
// Shared SQL provider instance
const sqlProvider = new BrowserSqlProvider();
// Messaging provider for worker-to-main-thread communication
const messagingProvider = new WorkerMessagingProvider();
// Core module, router, and initialization state
let coreModule: typeof import("@triliumnext/core") | null = null;
let router: BrowserRouter | null = null;
let initPromise: Promise<void> | null = null;
let initError: Error | null = null;
/**
* Initialize SQLite WASM and load the core module.
* This happens once at worker startup.
*/
async function initialize(): Promise<void> {
if (initPromise) {
return initPromise; // Already initializing
}
if (initError) {
throw initError; // Failed before, don't retry
}
initPromise = (async () => {
try {
console.log("[Worker] Initializing SQLite WASM...");
await sqlProvider.initWasm();
// Try to use OPFS for persistent storage
if (sqlProvider.isOpfsAvailable()) {
console.log("[Worker] OPFS available, loading persistent database...");
sqlProvider.loadFromOpfs("/trilium.db");
// Check if database is initialized (schema exists)
if (!sqlProvider.isDbInitialized()) {
console.log("[Worker] Database not initialized, loading demo data...");
sqlProvider.initializeDemoDatabase();
console.log("[Worker] Demo data loaded");
} else {
console.log("[Worker] Existing initialized database loaded");
}
} else {
// Fall back to in-memory database (non-persistent)
console.warn("[Worker] OPFS not available, using in-memory database (data will not persist)");
console.warn("[Worker] To enable persistence, ensure COOP/COEP headers are set by the server");
sqlProvider.loadFromMemory();
}
console.log("[Worker] Database loaded");
console.log("[Worker] Loading @triliumnext/core...");
coreModule = await import("@triliumnext/core");
await coreModule.initializeCore({
executionContext: new BrowserExecutionContext(),
crypto: new BrowserCryptoProvider(),
messaging: messagingProvider,
translations: translationProvider,
dbConfig: {
provider: sqlProvider,
isReadOnly: false,
onTransactionCommit: () => {
// No-op for now
},
onTransactionRollback: () => {
// No-op for now
}
}
});
console.log("[Worker] Supported routes", Object.keys(coreModule.routes));
// Create and configure the router
router = createConfiguredRouter();
console.log("[Worker] Router configured");
console.log("[Worker] Initializing becca...");
await coreModule.becca_loader.beccaLoaded;
console.log("[Worker] Initialization complete");
} catch (error) {
initError = error instanceof Error ? error : new Error(String(error));
console.error("[Worker] Initialization failed:", initError);
throw initError;
}
})();
return initPromise;
}
/**
* Ensure the worker is initialized before processing requests.
* Returns the router if initialization was successful.
*/
async function ensureInitialized() {
await initialize();
if (!router) {
throw new Error("Router not initialized");
}
return router;
}
interface LocalRequest {
method: string;
url: string;
body?: unknown;
headers?: Record<string, string>;
}
// Main dispatch
async function dispatch(request: LocalRequest) {
const url = new URL(request.url);
console.log("[Worker] Dispatch:", url.pathname);
// Ensure initialization is complete and get the router
const appRouter = await ensureInitialized();
// Dispatch to the router
return appRouter.dispatch(request.method, request.url, request.body, request.headers);
}
// Start initialization immediately when the worker loads
console.log("[Worker] Starting initialization...");
initialize().catch(err => {
console.error("[Worker] Initialization failed:", err);
// Post error to main thread
self.postMessage({
type: "WORKER_ERROR",
error: {
message: String(err?.message || err),
stack: err?.stack
}
});
});
self.onmessage = async (event) => {
const msg = event.data;
if (!msg || msg.type !== "LOCAL_REQUEST") return;
const { id, request } = msg;
try {
const response = await dispatch(request);
// Transfer body back (if any) - use options object for proper typing
(self as unknown as Worker).postMessage({
type: "LOCAL_RESPONSE",
id,
response
}, { transfer: response.body ? [response.body] : [] });
} catch (e) {
console.error("[Worker] Dispatch error:", e);
(self as unknown as Worker).postMessage({
type: "LOCAL_RESPONSE",
id,
error: String((e as Error)?.message || e)
});
}
};

View File

@@ -1,84 +0,0 @@
import { attachServiceWorkerBridge, startLocalServerWorker } from "./local-bridge.js";
async function waitForServiceWorkerControl(): Promise<void> {
if (!("serviceWorker" in navigator)) {
throw new Error("Service Worker not supported in this browser");
}
// If already controlling, we're good
if (navigator.serviceWorker.controller) {
console.log("[Bootstrap] Service worker already controlling");
return;
}
console.log("[Bootstrap] Waiting for service worker to take control...");
// Register service worker
await navigator.serviceWorker.register("./sw.js", { scope: "/" });
// Wait for it to be ready (installed + activated)
await navigator.serviceWorker.ready;
// Check if we're now controlling
if (navigator.serviceWorker.controller) {
console.log("[Bootstrap] Service worker now controlling");
return;
}
// If not controlling yet, we need to reload the page for SW to take control
// This is standard PWA behavior on first install
console.log("[Bootstrap] Service worker installed but not controlling yet - reloading page");
// Wait a tiny bit for SW to fully activate
await new Promise(resolve => setTimeout(resolve, 100));
// Reload to let SW take control
window.location.reload();
// Throw to stop execution (page will reload)
throw new Error("Reloading for service worker activation");
}
async function bootstrap() {
/* fixes https://github.com/webpack/webpack/issues/10035 */
window.global = globalThis;
try {
// 1) Start local worker ASAP (so /bootstrap is fast)
startLocalServerWorker();
// 2) Bridge SW -> local worker
attachServiceWorkerBridge();
// 3) Wait for service worker to control the page (may reload on first install)
await waitForServiceWorkerControl();
await loadScripts();
} catch (err) {
// If error is from reload, it will stop here (page reloads)
// Otherwise, show error to user
if (err instanceof Error && err.message.includes("Reloading")) {
// Page is reloading, do nothing
return;
}
console.error("[Bootstrap] Fatal error:", err);
document.body.innerHTML = `
<div style="padding: 40px; max-width: 600px; margin: 0 auto; font-family: system-ui, sans-serif;">
<h1 style="color: #d32f2f;">Failed to Initialize</h1>
<p>The application failed to start. Please check the browser console for details.</p>
<pre style="background: #f5f5f5; padding: 16px; border-radius: 4px; overflow: auto;">${err instanceof Error ? err.message : String(err)}</pre>
<button onclick="location.reload()" style="padding: 12px 24px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px;">
Reload Page
</button>
</div>
`;
document.body.style.display = "block";
}
}
async function loadScripts() {
await import("../../client/src/index.js");
}
bootstrap();

View File

@@ -1,185 +0,0 @@
// public/sw.js
const VERSION = "localserver-v1.4";
const STATIC_CACHE = `static-${VERSION}`;
// Check if running in dev mode (passed via URL parameter)
const isDev = true;
if (isDev) {
console.log('[Service Worker] Running in DEV mode - caching disabled');
}
// Adjust these to your routes:
const LOCAL_FIRST_PREFIXES = [
"/bootstrap",
"/api/",
"/sync/",
"/search/"
];
// Optional: basic precache list (keep small; you can expand later)
const PRECACHE_URLS = [
// "/",
// "/index.html",
// "/manifest.webmanifest",
// "/favicon.ico",
];
self.addEventListener("install", (event) => {
event.waitUntil((async () => {
// Skip precaching in dev mode
if (!isDev) {
const cache = await caches.open(STATIC_CACHE);
await cache.addAll(PRECACHE_URLS);
}
self.skipWaiting();
})());
});
self.addEventListener("activate", (event) => {
event.waitUntil((async () => {
// Cleanup old caches
const keys = await caches.keys();
await Promise.all(keys.map((k) => (k === STATIC_CACHE ? Promise.resolve() : caches.delete(k))));
await self.clients.claim();
})());
});
function isLocalFirst(url) {
return LOCAL_FIRST_PREFIXES.some((p) => url.pathname.startsWith(p));
}
async function cacheFirst(request) {
// In dev mode, always bypass cache
if (isDev) {
return fetch(request);
}
const cache = await caches.open(STATIC_CACHE);
const cached = await cache.match(request);
if (cached) return cached;
const fresh = await fetch(request);
// Cache only successful GETs
if (request.method === "GET" && fresh.ok) cache.put(request, fresh.clone());
return fresh;
}
async function networkFirst(request) {
// In dev mode, always bypass cache
if (isDev) {
return fetch(request);
}
const cache = await caches.open(STATIC_CACHE);
try {
const fresh = await fetch(request);
// Cache only successful GETs
if (request.method === "GET" && fresh.ok) cache.put(request, fresh.clone());
return fresh;
} catch (error) {
// Fallback to cache if network fails
const cached = await cache.match(request);
if (cached) return cached;
throw error;
}
}
async function forwardToClientLocalServer(request, clientId) {
// Find a client to handle the request (prefer the initiating client if available)
let client = clientId ? await self.clients.get(clientId) : null;
if (!client) {
const all = await self.clients.matchAll({ type: "window", includeUncontrolled: true });
client = all[0] || null;
}
// If no page is available, fall back to network
if (!client) return fetch(request);
const reqUrl = request.url;
const headersObj = {};
for (const [k, v] of request.headers.entries()) headersObj[k] = v;
const body = (request.method === "GET" || request.method === "HEAD")
? null
: await request.arrayBuffer();
const id = crypto.randomUUID();
const channel = new MessageChannel();
const responsePromise = new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("Local server timeout"));
}, 30_000);
channel.port1.onmessage = (event) => {
clearTimeout(timeout);
resolve(event.data);
};
channel.port1.onmessageerror = () => {
clearTimeout(timeout);
reject(new Error("Local server message error"));
};
});
// Send to the client with a reply port
client.postMessage({
type: "LOCAL_FETCH",
id,
request: {
url: reqUrl,
method: request.method,
headers: headersObj,
body // ArrayBuffer or null
}
}, [channel.port2]);
const localResp = await responsePromise;
if (!localResp || localResp.type !== "LOCAL_FETCH_RESPONSE" || localResp.id !== id) {
// Protocol mismatch; fall back
return fetch(request);
}
// localResp.response: { status, headers, body }
const { status, headers, body: respBody } = localResp.response;
const respHeaders = new Headers();
if (headers) {
for (const [k, v] of Object.entries(headers)) respHeaders.set(k, String(v));
}
return new Response(respBody ? respBody : null, {
status: status || 200,
headers: respHeaders
});
}
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
// Only handle same-origin
if (url.origin !== self.location.origin) return;
// HTML files: network-first to ensure updates are reflected immediately
if (event.request.mode === "navigate" || url.pathname.endsWith(".html")) {
event.respondWith(networkFirst(event.request));
return;
}
// Static assets: cache-first for performance
if (event.request.method === "GET" && !isLocalFirst(url)) {
event.respondWith(cacheFirst(event.request));
return;
}
// API-ish: local-first via bridge
if (isLocalFirst(url)) {
event.respondWith(forwardToClientLocalServer(event.request, event.clientId));
return;
}
// Default
event.respondWith(fetch(event.request));
});

View File

@@ -1,31 +0,0 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
interface Window {
glob: {
assetPath: string;
themeCssUrl?: string;
themeUseNextAsBase?: string;
iconPackCss: string;
device: string;
headingStyle: string;
layoutOrientation: string;
platform: string;
isElectron: boolean;
hasNativeTitleBar: boolean;
hasBackgroundEffects: boolean;
currentLocale: {
id: string;
rtl: boolean;
};
activeDialog: any;
};
global: typeof globalThis;
}

View File

@@ -1,26 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"lib": [
"ES2022",
"dom",
"dom.iterable"
],
"skipLibCheck": true,
"types": [
"vite/client"
],
"jsx": "react-jsx",
"jsxImportSource": "preact"
},
"include": [
"src/**/*",
"../client/src/**/*"
],
"exclude": [
"src/**/*.spec.ts",
"src/**/*.test.ts",
"../client/src/**/*.spec.ts",
"../client/src/**/*.test.ts"
]
}

View File

@@ -1,7 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.spec.json" }
]
}

View File

@@ -1,18 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"lib": [
"ES2022",
"dom",
"dom.iterable"
],
"types": [
"vitest/globals",
"happy-dom"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.test.ts"
]
}

View File

@@ -1,188 +0,0 @@
import prefresh from '@prefresh/vite';
import { join } from 'path';
import { defineConfig } from 'vite';
import { viteStaticCopy } from 'vite-plugin-static-copy';
const clientAssets = ["assets", "stylesheets", "fonts", "translations"];
const isDev = process.env.NODE_ENV === "development";
// Watch client files and trigger reload in development
const clientWatchPlugin = () => ({
name: 'client-watch',
configureServer(server: any) {
if (isDev) {
// Watch client source files (adjusted for new root)
server.watcher.add('../../client/src/**/*');
server.watcher.on('change', (file: string) => {
if (file.includes('../../client/src/')) {
server.ws.send({
type: 'full-reload'
});
}
});
}
}
});
// Always copy SQLite WASM files so they're available to the module
const sqliteWasmPlugin = viteStaticCopy({
targets: [
{
src: "../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm",
dest: "assets"
},
{
src: "../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-opfs-async-proxy.js",
dest: "assets"
}
]
});
let plugins: any = [
sqliteWasmPlugin, // Always include SQLite WASM files
viteStaticCopy({
targets: clientAssets.map((asset) => ({
src: `../../client/src/${asset}/*`,
dest: asset
})),
// Enable watching in development
...(isDev && {
watch: {
reloadPageOnChange: true
}
})
}),
viteStaticCopy({
targets: [
{
src: "../../server/src/assets/*",
dest: "server-assets"
}
]
}),
// Watch client files for changes in development
...(isDev ? [
prefresh(),
clientWatchPlugin()
] : [])
];
if (!isDev) {
plugins = [
...plugins,
viteStaticCopy({
structured: true,
targets: [
{
src: "../../../node_modules/@excalidraw/excalidraw/dist/prod/fonts/*",
dest: "",
}
]
})
]
}
export default defineConfig(() => ({
root: join(__dirname, 'src'), // Set src as root so index.html is served from /
envDir: __dirname, // Load .env files from client-standalone directory, not src/
cacheDir: '../../../node_modules/.vite/apps/client-standalone',
base: "",
plugins,
esbuild: {
jsx: 'automatic',
jsxImportSource: 'preact',
jsxDev: isDev
},
css: {
transformer: 'lightningcss',
devSourcemap: isDev
},
publicDir: join(__dirname, 'public'),
resolve: {
alias: [
{
find: "react",
replacement: "preact/compat"
},
{
find: "react-dom",
replacement: "preact/compat"
},
{
find: "@client",
replacement: join(__dirname, "../client/src")
}
],
dedupe: [
"react",
"react-dom",
"preact",
"preact/compat",
"preact/hooks"
]
},
server: {
watch: {
// Watch workspace packages
ignored: ['!**/node_modules/@triliumnext/**'],
// Also watch client assets for live reload
usePolling: false,
interval: 100,
binaryInterval: 300
},
// Watch additional directories for changes
fs: {
allow: [
// Allow access to workspace root
'../../../',
// Explicitly allow client directory
'../../client/src/'
]
},
headers: {
// Required for SharedArrayBuffer which is needed by SQLite WASM OPFS VFS
// See: https://sqlite.org/wasm/doc/trunk/persistence.md#coop-coep
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp"
}
},
optimizeDeps: {
exclude: ['@sqlite.org/sqlite-wasm', '@triliumnext/core']
},
worker: {
format: "es" as const
},
commonjsOptions: {
transformMixedEsModules: true,
},
build: {
target: "esnext",
outDir: join(__dirname, 'dist'),
emptyOutDir: true,
rollupOptions: {
input: {
main: join(__dirname, 'src', 'index.html'),
sw: join(__dirname, 'src', 'sw.ts'),
'local-bridge': join(__dirname, 'src', 'local-bridge.ts'),
},
output: {
entryFileNames: (chunkInfo) => {
// Service worker and other workers should be at root level
if (chunkInfo.name === 'sw') {
return '[name].js';
}
return 'src/[name].js';
},
chunkFileNames: "src/[name].js",
assetFileNames: "src/[name].[ext]"
}
}
},
test: {
environment: "happy-dom"
},
define: {
"process.env.IS_PREACT": JSON.stringify("true"),
}
}));

View File

@@ -27,7 +27,7 @@
"@mermaid-js/layout-elk": "0.2.0",
"@mind-elixir/node-menu": "5.0.1",
"@popperjs/core": "2.11.8",
"@preact/signals": "2.6.2",
"@preact/signals": "2.8.1",
"@triliumnext/ckeditor5": "workspace:*",
"@triliumnext/codemirror": "workspace:*",
"@triliumnext/commons": "workspace:*",
@@ -44,7 +44,7 @@
"draggabilly": "3.0.0",
"force-graph": "1.51.1",
"globals": "17.3.0",
"i18next": "25.8.0",
"i18next": "25.8.10",
"i18next-http-backend": "3.0.2",
"jquery": "4.0.0",
"jquery.fancytree": "2.38.5",
@@ -54,14 +54,14 @@
"leaflet": "1.9.4",
"leaflet-gpx": "2.2.0",
"mark.js": "8.11.1",
"marked": "17.0.1",
"marked": "17.0.2",
"mermaid": "11.12.2",
"mind-elixir": "5.8.0",
"mind-elixir": "5.8.2",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.28.3",
"react-i18next": "16.5.4",
"react-window": "2.2.6",
"react-window": "2.2.7",
"reveal.js": "5.2.1",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",
@@ -78,7 +78,7 @@
"@types/reveal.js": "5.2.2",
"@types/tabulator-tables": "6.3.1",
"copy-webpack-plugin": "13.0.1",
"happy-dom": "20.5.0",
"happy-dom": "20.6.1",
"lightningcss": "1.31.1",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "3.2.0"

View File

@@ -700,6 +700,15 @@ export default class FNote {
return this.hasAttribute(LABEL, name);
}
/**
* Returns `true` if the note has a label with the given name (same as {@link hasOwnedLabel}), or it has a label with the `disabled:` prefix (for example due to a safe import).
* @param name the name of the label to look for.
* @returns `true` if the label exists, or its version with the `disabled:` prefix.
*/
hasLabelOrDisabled(name: string) {
return this.hasLabel(name) || this.hasLabel(`disabled:${name}`);
}
/**
* @param name - label name
* @returns true if label exists (including inherited) and does not have "false" value.

View File

@@ -36,37 +36,10 @@ async function setupGlob() {
window.global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
window.glob = {
...json,
activeDialog: null,
device: json.device || getDevice()
activeDialog: null
};
}
function getDevice() {
// Respect user's manual override via URL.
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has("print")) {
return "print";
} else if (urlParams.has("desktop")) {
return "desktop";
} else if (urlParams.has("mobile")) {
return "mobile";
}
const deviceCookie = document.cookie.split("; ").find(row => row.startsWith("trilium-device="))?.split("=")[1];
if (deviceCookie === "desktop" || deviceCookie === "mobile") return deviceCookie;
return isMobile() ? "mobile" : "desktop";
}
// https://stackoverflow.com/a/73731646/944162
function isMobile() {
const mQ = matchMedia?.("(pointer:coarse)");
if (mQ?.media === "(pointer:coarse)") return !!mQ.matches;
if ("orientation" in window) return true;
const userAgentsRegEx = /\b(Android|iPhone|iPad|iPod|Windows Phone|BlackBerry|webOS|IEMobile)\b/i;
return userAgentsRegEx.test(navigator.userAgent);
}
async function loadBootstrapCss() {
// We have to selectively import Bootstrap CSS based on text direction.
if (glob.isRtl) {

View File

@@ -30,7 +30,6 @@ import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
import StandaloneWarningBar from "../widgets/layout/StandaloneWarningBar.jsx";
import StatusBar from "../widgets/layout/StatusBar.jsx";
import NoteIconWidget from "../widgets/note_icon.jsx";
import NoteTitleWidget from "../widgets/note_title.jsx";
@@ -187,7 +186,6 @@ export default class DesktopLayout {
)
)
.optChild(launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
.optChild(glob.isStandalone, <StandaloneWarningBar />)
.child(<CloseZenModeButton />)
// Desktop-specific dialogs.

View File

@@ -13,7 +13,6 @@ import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
import StandaloneWarningBar from "../widgets/layout/StandaloneWarningBar";
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
@@ -26,6 +25,7 @@ import NoteDetail from "../widgets/NoteDetail.jsx";
import QuickSearchWidget from "../widgets/quick_search.js";
import { useNoteContext } from "../widgets/react/hooks.jsx";
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
import ScrollPadding from "../widgets/scroll_padding";
import SearchResult from "../widgets/search_result.jsx";
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
import { applyModals } from "./layout_commons.js";
@@ -57,7 +57,6 @@ export default class MobileLayout {
.child(
new SplitNoteContainer(() =>
new NoteWrapperWidget()
.optChild(glob.isStandalone, <StandaloneWarningBar />)
.child(
new FlexContainer("row")
.class("title-row note-split-title")
@@ -78,7 +77,7 @@ export default class MobileLayout {
.child(<NoteDetail />)
.child(<NoteList media="screen" />)
.child(<SearchResult />)
.child(<FilePropertiesWrapper />)
.child(<ScrollPadding />)
)
.child(<MobileEditorToolbar />)
.child(new FindWidget())
@@ -102,13 +101,3 @@ export default class MobileLayout {
return rootContainer;
}
}
function FilePropertiesWrapper() {
const { note, ntxId } = useNoteContext();
return (
<div>
{note?.type === "file" && <FilePropertiesTab note={note} ntxId={ntxId} />}
</div>
);
}

View File

@@ -18,6 +18,10 @@ export type PrintReport = {
} | {
type: "collection";
ignoredNoteIds: string[];
} | {
type: "error";
message: string;
stack?: string;
};
async function main() {

View File

@@ -168,6 +168,49 @@ function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefin
return false;
}
/**
* Toggles whether a dangerous attribute is enabled or not. When an attribute is disabled, its name is prefixed with `disabled:`.
*
* Note that this work for non-dangerous attributes as well.
*
* If there are multiple attributes with the same name, all of them will be toggled at the same time.
*
* @param note the note whose attribute to change.
* @param type the type of dangerous attribute (label or relation).
* @param name the name of the dangerous attribute.
* @param willEnable whether to enable or disable the attribute.
* @returns a promise that will resolve when the request to the server completes.
*/
async function toggleDangerousAttribute(note: FNote, type: "label" | "relation", name: string, willEnable: boolean) {
const attrs = [
...note.getOwnedAttributes(type, name),
...note.getOwnedAttributes(type, `disabled:${name}`)
];
for (const attr of attrs) {
const baseName = getNameWithoutDangerousPrefix(attr.name);
const newName = willEnable ? baseName : `disabled:${baseName}`;
if (newName === attr.name) continue;
// We are adding and removing afterwards to avoid a flicker (because for a moment there would be no active content attribute anymore) because the operations are done in sequence and not atomically.
if (attr.type === "label") {
await setLabel(note.noteId, newName, attr.value);
} else {
await setRelation(note.noteId, newName, attr.value);
}
await removeAttributeById(note.noteId, attr.attributeId);
}
}
/**
* Returns the name of an attribute without the `disabled:` prefix, or the same name if it's not disabled.
* @param name the name of an attribute.
* @returns the name without the `disabled:` prefix.
*/
function getNameWithoutDangerousPrefix(name: string) {
return name.startsWith("disabled:") ? name.substring(9) : name;
}
export default {
addLabel,
setLabel,
@@ -177,5 +220,7 @@ export default {
removeAttributeById,
removeOwnedLabelByName,
removeOwnedRelationByName,
isAffecting
isAffecting,
toggleDangerousAttribute,
getNameWithoutDangerousPrefix
};

View File

@@ -5,7 +5,7 @@ import { formatCodeBlocks } from "./syntax_highlight.js";
export default function renderDoc(note: FNote) {
return new Promise<JQuery<HTMLElement>>((resolve) => {
const docName = note.getLabelValue("docName");
let docName = note.getLabelValue("docName");
const $content = $("<div>");
if (docName) {
@@ -16,7 +16,7 @@ export default function renderDoc(note: FNote) {
if (status === "error") {
const fallbackUrl = getUrl(docName, "en");
$content.load(fallbackUrl, async () => {
await processContent(fallbackUrl, $content);
await processContent(fallbackUrl, $content)
resolve($content);
});
return;
@@ -37,9 +37,9 @@ async function processContent(url: string, $content: JQuery<HTMLElement>) {
const dir = url.substring(0, url.lastIndexOf("/"));
// Images are relative to the docnote but that will not work when rendered in the application since the path breaks.
$content.find("img").each((_i, el) => {
$content.find("img").each((i, el) => {
const $img = $(el);
$img.attr("src", `${dir}/${$img.attr("src")}`);
$img.attr("src", dir + "/" + $img.attr("src"));
});
formatCodeBlocks($content);
@@ -51,17 +51,7 @@ async function processContent(url: string, $content: JQuery<HTMLElement>) {
function getUrl(docNameValue: string, language: string) {
// Cannot have spaces in the URL due to how JQuery.load works.
docNameValue = docNameValue.replaceAll(" ", "%20");
// The user guide is available only in English, so make sure we are requesting correctly since 404s in standalone client are treated differently.
if (docNameValue.includes("User%20Guide")) language = "en";
return `${getBasePath()}/doc_notes/${language}/${docNameValue}.html`;
}
function getBasePath() {
if (window.glob.isStandalone) {
return `server-assets`;
}
if (window.glob.isDev) {
return `${window.glob.assetPath }/..`;
}
return window.glob.assetPath;
const basePath = window.glob.isDev ? window.glob.assetPath + "/.." : window.glob.assetPath;
return `${basePath}/doc_notes/${language}/${docNameValue}.html`;
}

View File

@@ -12,7 +12,7 @@ const SELECTED_NOTE_PATH_KEY = "data-note-path";
const SELECTED_EXTERNAL_LINK_KEY = "data-external-link";
// To prevent search lag when there are a large number of notes, set a delay based on the number of notes to avoid jitter.
const notesCount = 10000; // TODO: Replace with dynamic count from becca once available.
const notesCount = await server.get<number>(`autocomplete/notesCount`);
let debounceTimeoutId: ReturnType<typeof setTimeout>;
function getSearchDelay(notesCount: number): number {

View File

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

View File

@@ -6,7 +6,7 @@ import bundleService, { type Bundle } from "./bundle.js";
import froca from "./froca.js";
import server from "./server.js";
async function render(note: FNote, $el: JQuery<HTMLElement>) {
async function render(note: FNote, $el: JQuery<HTMLElement>, onError?: (e: unknown) => void) {
const relations = note.getRelations("renderNote");
const renderNoteIds = relations.map((rel) => rel.value).filter((noteId) => noteId);
@@ -21,12 +21,14 @@ async function render(note: FNote, $el: JQuery<HTMLElement>) {
$scriptContainer.append(bundle.html);
// async so that scripts cannot block trilium execution
bundleService.executeBundle(bundle, note, $scriptContainer).then(result => {
// Render JSX
if (bundle.html === "") {
renderIfJsx(bundle, result, $el);
}
});
bundleService.executeBundle(bundle, note, $scriptContainer)
.catch(onError)
.then(result => {
// Render JSX
if (bundle.html === "") {
renderIfJsx(bundle, result, $el).catch(onError);
}
});
}
return renderNoteIds.length > 0;

View File

@@ -133,8 +133,6 @@ export function isElectron() {
return !!(window && window.process && window.process.type);
}
export const isStandalone = window.glob.isStandalone;
/**
* Returns `true` if the client is running as a PWA, otherwise `false`.
*/
@@ -816,7 +814,7 @@ function compareVersions(v1: string, v2: string): number {
/**
* Compares two semantic version strings and returns `true` if the latest version is greater than the current version.
*/
export function isUpdateAvailable(latestVersion: string | null | undefined, currentVersion: string): boolean {
function isUpdateAvailable(latestVersion: string | null | undefined, currentVersion: string): boolean {
if (!latestVersion) {
return false;
}

View File

@@ -304,7 +304,7 @@ async function sendPing() {
}
setTimeout(() => {
if (glob.device === "print" || glob.isStandalone) return;
if (glob.device === "print") return;
ws = connectWebSocket();

View File

@@ -153,6 +153,11 @@ textarea,
background: var(--input-background-color);
}
.form-control:disabled {
background-color: var(--input-background-color);
opacity: 0.6;
}
.form-control:focus {
color: var(--input-text-color);
background: var(--input-background-color);
@@ -942,6 +947,7 @@ table.promoted-attributes-in-tooltip th {
color: var(--muted-text-color);
opacity: 0.6;
line-height: 1;
word-wrap: break-word;
}
.aa-dropdown-menu .aa-suggestion p {
@@ -1336,15 +1342,12 @@ body.desktop .dropdown-submenu > .dropdown-menu {
max-width: 300px;
}
.dropdown-submenu.dropstart > .dropdown-menu {
.dropdown-submenu.dropstart > .dropdown-menu,
body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
inset-inline-start: auto;
inset-inline-end: calc(100% - 2px);
}
body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
inset-inline-start: calc(-100% + 10px);
}
.right-dropdown-widget {
flex-shrink: 0;
}

View File

@@ -210,6 +210,7 @@
--badge-share-background-color: #4d4d4d;
--badge-clipped-note-background-color: #295773;
--badge-execute-background-color: #604180;
--badge-active-content-background-color: rgb(12, 68, 70);
--note-icon-background-color: #444444;
--note-icon-color: #d4d4d4;
@@ -238,9 +239,9 @@
--bottom-panel-background-color: #11111180;
--bottom-panel-title-bar-background-color: #3F3F3F80;
--status-bar-border-color: var(--main-border-color);
--scrollbar-thumb-color: #fdfdfd5c;
--scrollbar-thumb-hover-color: #ffffff7d;
--scrollbar-background-color: transparent;
@@ -290,6 +291,15 @@
--ck-editor-toolbar-button-on-shadow: 1px 1px 2px rgba(0, 0, 0, .75);
--ck-editor-toolbar-dropdown-button-open-background: #ffffff14;
--note-list-view-icon-color: var(--left-pane-icon-color);
--note-list-view-large-icon-background: var(--note-icon-background-color);
--note-list-view-large-icon-color: var(--note-icon-color);
--note-list-view-search-result-highlight-background: transparent;
--note-list-view-search-result-highlight-color: var(--quick-search-result-highlight-color);
--note-list-view-content-background: rgba(0, 0, 0, .2);
--note-list-view-content-search-result-highlight-background: var(--quick-search-result-highlight-color);
--note-list-view-content-search-result-highlight-color: black;
--calendar-coll-event-background-saturation: 25%;
--calendar-coll-event-background-lightness: 20%;
--calendar-coll-event-background-color: #3c3c3c;
@@ -303,7 +313,8 @@
* Dark color scheme tweaks
*/
#left-pane .fancytree-node.tinted {
#left-pane .fancytree-node.tinted,
.nested-note-list-item.use-note-color {
--custom-color: var(--dark-theme-custom-color);
/* The background color of the active item in the note tree.
@@ -337,12 +348,24 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 13.2%, 20.8%);
}
.modal.tab-bar-modal .tabs .tab-card.with-hue {
background-color: hsl(var(--bg-hue), 8.8%, 11.2%);
border-color: hsl(var(--bg-hue), 9.4%, 25.1%);
}
.modal.tab-bar-modal .tabs .tab-card.active.with-hue {
background-color: hsl(var(--bg-hue), 8.8%, 16.2%);
border-color: hsl(var(--bg-hue), 9.4%, 25.1%);
}
.use-note-color {
--custom-color: var(--dark-theme-custom-color);
}
.note-split.with-hue,
.quick-edit-dialog-wrapper.with-hue {
.quick-edit-dialog-wrapper.with-hue,
.nested-note-list-item.with-hue {
--note-icon-custom-background-color: hsl(var(--custom-color-hue), 15.8%, 30.9%);
--note-icon-custom-color: hsl(var(--custom-color-hue), 100%, 76.5%);
--note-icon-hover-custom-background-color: hsl(var(--custom-color-hue), 28.3%, 36.7%);
@@ -351,4 +374,4 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
.note-split.with-hue *::selection,
.quick-edit-dialog-wrapper.with-hue *::selection {
--selection-background-color: hsl(var(--custom-color-hue), 49.2%, 35%);
}
}

View File

@@ -202,6 +202,7 @@
--badge-share-background-color: #6b6b6b;
--badge-clipped-note-background-color: #2284c0;
--badge-execute-background-color: #7b47af;
--badge-active-content-background-color: rgb(27, 164, 168);
--note-icon-background-color: #4f4f4f;
--note-icon-color: white;
@@ -288,6 +289,15 @@
--ck-editor-toolbar-button-on-shadow: none;
--ck-editor-toolbar-dropdown-button-open-background: #0000000f;
--note-list-view-icon-color: var(--left-pane-icon-color);
--note-list-view-large-icon-background: var(--note-icon-background-color);
--note-list-view-large-icon-color: var(--note-icon-color);
--note-list-view-search-result-highlight-background: transparent;
--note-list-view-search-result-highlight-color: var(--quick-search-result-highlight-color);
--note-list-view-content-background: #b1b1b133;
--note-list-view-content-search-result-highlight-background: var(--quick-search-result-highlight-color);
--note-list-view-content-search-result-highlight-color: white;
--calendar-coll-event-background-lightness: 95%;
--calendar-coll-event-background-saturation: 80%;
--calendar-coll-event-background-color: #eaeaea;
@@ -297,7 +307,8 @@
--calendar-coll-today-background-color: #00000006;
}
#left-pane .fancytree-node.tinted {
#left-pane .fancytree-node.tinted,
.nested-note-list-item.use-note-color {
--custom-color: var(--light-theme-custom-color);
/* The background color of the active item in the note tree.
@@ -312,8 +323,19 @@
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 40%, 88%);
}
.modal.tab-bar-modal .tabs .tab-card.with-hue {
background-color: hsl(var(--bg-hue), 56%, 96%);
border-color: hsl(var(--bg-hue), 33%, 41%);
}
.modal.tab-bar-modal .tabs .tab-card.active.with-hue {
background-color: hsl(var(--bg-hue), 86%, 96%);
border-color: hsl(var(--bg-hue), 33%, 41%);
}
.note-split.with-hue,
.quick-edit-dialog-wrapper.with-hue {
.quick-edit-dialog-wrapper.with-hue,
.nested-note-list-item.with-hue {
--note-icon-custom-background-color: hsl(var(--custom-color-hue), 44.5%, 43.1%);
--note-icon-custom-color: hsl(var(--custom-color-hue), 91.3%, 91%);
--note-icon-hover-custom-background-color: hsl(var(--custom-color-hue), 55.1%, 50.2%);
@@ -322,4 +344,4 @@
.note-split.with-hue *::selection,
.quick-edit-dialog-wrapper.with-hue *::selection {
--selection-background-color: hsl(var(--custom-color-hue), 60%, 90%);
}
}

View File

@@ -655,7 +655,6 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
border-radius: 12px;
user-select: none;
padding: 0;
margin: 5px 10px 5px 0;
}
:root .note-list .note-book-card:hover {
@@ -800,3 +799,18 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
background: var(--hover-item-background-color);
color: var(--hover-item-text-color);
}
/*
* Alert bars
*/
div.alert {
margin-bottom: 8px;
background: var(--alert-bar-background) !important;
border-radius: 8px;
font-size: .85em;
}
div.alert p + p {
margin-block: 1em 0;
}

View File

@@ -84,6 +84,22 @@ button.btn.btn-success kbd {
letter-spacing: 0.5pt;
}
/*
* Low profile buttons
*/
button.tn-low-profile {
appearance: none;
background: transparent;
border: 0;
border-radius: 8px;
color: inherit;
}
button.tn-low-profile:hover {
background-color: var(--icon-button-hover-background);
}
/*
* Icon buttons
*/
@@ -129,6 +145,10 @@ button.btn.btn-success kbd {
font-size: calc(var(--icon-button-size) * var(--icon-button-icon-ratio));
}
:root .icon-action.disabled::before {
opacity: .5;
}
:root .icon-action:not(.global-menu-button):hover,
:root .icon-action:not(.global-menu-button).show,
:root .tn-tool-button:hover,
@@ -794,3 +814,35 @@ input[type="range"] {
scrollbar-width: unset;
}
}
/*
* Centered forms
*/
.tn-centered-form {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20vh;
}
.tn-centered-form .form-group {
text-align: center;
color: var(--muted-text-color);
}
.tn-centered-form .form-icon {
font-size: 140px;
color: var(--main-border-color);
}
.tn-centered-form .protected-session-password {
margin-inline: auto;
max-width: 350px;
text-align: center;
}
.tn-centered-form .input-group,
.tn-centered-form button {
margin-top: 12px;
}

View File

@@ -265,13 +265,6 @@ body.desktop .options-section:not(.tn-no-card) {
margin-bottom: 6px;
}
.options-section .alert {
margin-bottom: 8px;
background: var(--alert-bar-background) !important;
border-radius: 8px;
font-size: .85em;
}
nav.options-section-tabs {
min-width: var(--options-card-min-width);
max-width: var(--options-card-max-width);

View File

@@ -751,12 +751,14 @@ body[dir=rtl] #left-pane span.fancytree-node.protected > span.fancytree-custom-i
}
}
#left-pane .fancytree-expander {
#left-pane .fancytree-expander,
.nested-note-list-item .note-expander {
opacity: 0.65;
transition: opacity 150ms ease-in;
}
#left-pane .fancytree-expander:hover {
#left-pane .fancytree-expander:hover,
.nested-note-list-item .note-expander:hover {
opacity: 1;
transition: opacity 300ms ease-out;
}

View File

@@ -1180,9 +1180,6 @@
"note_not_found": "الملاحظة {{noteId}} غير موجودة!",
"cannot_match_transform": "تعذر مطابقة التحويل: {{transform}}"
},
"web_view": {
"web_view": "عرض الويب"
},
"consistency_checks": {
"title": "فحوصات التناسق"
},

View File

@@ -1008,7 +1008,7 @@
"no_attachments": "此笔记没有附件。"
},
"book": {
"no_children_help": "此类型为书籍的笔记没有任何子笔记,因此没有内容显示。请参阅 <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> 了解详情。",
"no_children_help": "此集合没有任何子笔记,因此没有内容显示。",
"drag_locked_title": "锁定编辑",
"drag_locked_message": "无法拖拽,因为集合已被锁定编辑。"
},
@@ -1064,15 +1064,6 @@
"default_new_note_title": "新笔记",
"click_on_canvas_to_place_new_note": "点击画布以放置新笔记"
},
"render": {
"note_detail_render_help_1": "之所以显示此帮助说明,是因为这个类型为渲染 HTML 的笔记没有正常工作所需的关系。",
"note_detail_render_help_2": "渲染 HTML 笔记类型用于<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">编写脚本</a>。简而言之,您有一份 HTML 代码笔记(可包含一些 JavaScript然后这个笔记会把页面渲染出来。要使其正常工作您需要定义一个名为 \"renderNote\" 的<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">关系</a>指向要渲染的 HTML 笔记。"
},
"web_view": {
"web_view": "网页视图",
"embed_websites": "网页视图类型的笔记允许您将网站嵌入到 Trilium 中。",
"create_label": "首先,请创建一个带有您要嵌入的 URL 地址的标签,例如 #webViewSrc=\"https://www.bing.com\""
},
"backend_log": {
"refresh": "刷新"
},
@@ -1421,7 +1412,8 @@
"description": "描述",
"reload_app": "重载应用以应用更改",
"set_all_to_default": "将所有快捷键重置为默认值",
"confirm_reset": "您确定要将所有键盘快捷键重置为默认值吗?"
"confirm_reset": "您确定要将所有键盘快捷键重置为默认值吗?",
"no_results": "未找到与“{{filter}}”匹配的快捷方式"
},
"spellcheck": {
"title": "拼写检查",
@@ -1622,7 +1614,9 @@
"print_report_title": "打印报告",
"print_report_collection_content_other": "集合中的 {{count}} 篇笔记无法打印,因为它们不受支持或受到保护。",
"print_report_collection_details_button": "查看详情",
"print_report_collection_details_ignored_notes": "忽略的笔记"
"print_report_collection_details_ignored_notes": "忽略的笔记",
"print_report_error_title": "打印失败",
"print_report_stack_trace": "堆栈跟踪"
},
"note_title": {
"placeholder": "请输入笔记标题...",
@@ -2075,7 +2069,8 @@
"raster": "栅格",
"vector_light": "矢量(浅色)",
"vector_dark": "矢量(深色)",
"show-scale": "显示比例尺"
"show-scale": "显示比例尺",
"show-labels": "显示标记名称"
},
"table_context_menu": {
"delete_row": "删除行"
@@ -2154,7 +2149,6 @@
"app-restart-required": "(需重启程序以应用更改)"
},
"pagination": {
"page_title": "第 {{startIndex}} 页 - 第 {{endIndex}} 页",
"total_notes": "{{count}} 篇笔记"
},
"collections": {
@@ -2268,5 +2262,49 @@
},
"bookmark_buttons": {
"bookmarks": "书签"
},
"web_view_setup": {
"title": "直接在 Trilium 中创建网页的实时视图",
"url_placeholder": "输入或粘贴网站地址,例如 https://triliumnotes.org",
"create_button": "创建网页视图",
"invalid_url_title": "无效的地址",
"invalid_url_message": "请输入有效的网址,例如 https://triliumnotes.org。",
"disabled_description": "此网页视图来自外部来源。为保护您免受网络钓鱼或恶意内容侵害,该视图不会自动加载。若您信任该来源,可手动启用加载功能。",
"disabled_button_enable": "启用网页视图"
},
"render": {
"setup_title": "在此笔记中显示自定义 HTML 或 Preact JSX",
"setup_create_sample_preact": "使用 Preact 建立范例笔记",
"setup_create_sample_html": "使用 HTML 建立范例笔记",
"setup_sample_created": "已建立一个范例笔记作为子笔记。",
"disabled_description": "此渲染笔记来自外部来源。为保护您免受恶意内容侵害,该功能默认处于禁用状态。启用前请确保您信任该来源。",
"disabled_button_enable": "启用渲染笔记"
},
"active_content_badges": {
"type_icon_pack": "图标包",
"type_backend_script": "后端脚本",
"type_frontend_script": "前端脚本",
"type_widget": "小部件",
"type_app_css": "自定义 CSS",
"type_render_note": "渲染笔记",
"type_web_view": "网页视图",
"type_app_theme": "自定义主题",
"toggle_tooltip_enable_tooltip": "点击以启用此 {{type}}。",
"toggle_tooltip_disable_tooltip": "点击以禁用此 {{type}}。",
"menu_docs": "打开文档",
"menu_execute_now": "立即执行脚本",
"menu_run": "自动执行",
"menu_run_disabled": "手动",
"menu_run_backend_startup": "当后端启动时",
"menu_run_hourly": "每小时",
"menu_run_daily": "每日",
"menu_run_frontend_startup": "当桌面前端启动时",
"menu_run_mobile_startup": "当移动前端启动时",
"menu_change_to_widget": "更改为小部件",
"menu_change_to_frontend_script": "更改为前端脚本",
"menu_theme_base": "主题基底"
},
"setup_form": {
"more_info": "了解更多"
}
}

View File

@@ -1007,7 +1007,7 @@
"no_attachments": "Diese Notiz enthält keine Anhänge."
},
"book": {
"no_children_help": "Diese Notiz mit dem Notiztyp Buch besitzt keine Unternotizen, deshalb ist nichts zum Anzeigen vorhanden. Siehe <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">Wiki</a> für mehr Details.",
"no_children_help": "Diese Sammlung enthält keineUnternotizen, daher gibt es nichts anzuzeigen.",
"drag_locked_title": "Für Bearbeitung gesperrt",
"drag_locked_message": "Das Ziehen ist nicht möglich, da die Sammlung für die Bearbeitung gesperrt ist."
},
@@ -1063,15 +1063,6 @@
"default_new_note_title": "neue Notiz",
"click_on_canvas_to_place_new_note": "Klicke auf den Canvas, um eine neue Notiz zu platzieren"
},
"render": {
"note_detail_render_help_1": "Diese Hilfesnotiz wird angezeigt, da diese Notiz vom Typ „HTML rendern“ nicht über die erforderliche Beziehung verfügt, um ordnungsgemäß zu funktionieren.",
"note_detail_render_help_2": "Render-HTML-Notiztyp wird benutzt für <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. Kurzgesagt, du hast ein HTML-Code-Notiz (optional mit JavaScript) und diese Notiz rendert es. Damit es funktioniert, musst du eine a <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">Beziehung</a> namens \"renderNote\" zeigend auf die HTML-Notiz zum rendern definieren."
},
"web_view": {
"web_view": "Webansicht",
"embed_websites": "Notiz vom Typ Web View ermöglicht das Einbetten von Websites in Trilium.",
"create_label": "Um zu beginnen, erstelle bitte ein Label mit einer URL-Adresse, die eingebettet werden soll, z. B. #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Aktualisieren"
},
@@ -1387,7 +1378,8 @@
"description": "Beschreibung",
"reload_app": "Lade die App neu, um die Änderungen zu übernehmen",
"set_all_to_default": "Setze alle Verknüpfungen auf die Standardeinstellungen",
"confirm_reset": "Möchtest du wirklich alle Tastaturkürzel auf die Standardeinstellungen zurücksetzen?"
"confirm_reset": "Möchtest du wirklich alle Tastaturkürzel auf die Standardeinstellungen zurücksetzen?",
"no_results": "Keine Tastenkürzel für '{{filter}}' gefunden"
},
"spellcheck": {
"title": "Rechtschreibprüfung",
@@ -1591,7 +1583,9 @@
"print_report_collection_details_button": "Details anzeigen",
"print_report_collection_details_ignored_notes": "Ignorierte Notizen",
"print_report_collection_content_one": "{{count}} Notiz in der Sammlung konnte nicht gedruckt werden, weil sie nicht unterstützt oder geschützt ist.",
"print_report_collection_content_other": "{{count}} Notizen in der Sammlung konnten nicht gedruckt werden, weil sie nicht unterstützt oder geschützt sind."
"print_report_collection_content_other": "{{count}} Notizen in der Sammlung konnten nicht gedruckt werden, weil sie nicht unterstützt oder geschützt sind.",
"print_report_error_title": "Druck fehlgeschlagen",
"print_report_stack_trace": "Stapelzurückverfolgung"
},
"note_title": {
"placeholder": "Titel der Notiz hier eingeben…",
@@ -2092,7 +2086,8 @@
"raster": "Raster",
"vector_light": "Vektor (Hell)",
"vector_dark": "Vektor (Dunkel)",
"show-scale": "Zeige Skalierung"
"show-scale": "Zeige Skalierung",
"show-labels": "Zeige Markierungsnamen"
},
"table_context_menu": {
"delete_row": "Zeile entfernen"
@@ -2159,7 +2154,6 @@
"percentage": "%"
},
"pagination": {
"page_title": "Seite {{startIndex}} von {{endIndex}}",
"total_notes": "{{count}} Notizen"
},
"collections": {
@@ -2283,5 +2277,49 @@
},
"bookmark_buttons": {
"bookmarks": "Lesezeichen"
},
"web_view_setup": {
"title": "Erstelle eine Live-Ansicht einer Webseite direkt in Trilium",
"url_placeholder": "Gib oder füge die Adresse der Webseite ein, zum Beispiel https://triliumnotes.org",
"create_button": "Erstelle Web Ansicht",
"invalid_url_title": "Ungültige Adresse",
"invalid_url_message": "Füge eine valide Webadresse ein, zum Beispiel https://triliumnotes.org.",
"disabled_description": "Diese Webansicht wurde von einer externen Quelle importiert. Um Sie vor Phishing oder schädlichen Inhalten zu schützen, wird sie nicht automatisch geladen. Sie können sie aktivieren, wenn Sie der Quelle vertrauen.",
"disabled_button_enable": "Webansicht aktivieren"
},
"render": {
"setup_create_sample_html": "Eine Beispielnotiz mit HTML erstellen",
"setup_create_sample_preact": "Eine Beispielnotiz mit Preact erstellen",
"setup_title": "Benutzerdefiniertes HTML oder Preact JSX in dieser Notiz anzeigen",
"setup_sample_created": "Eine Beispielnotiz wurde als untergeordnete Notiz erstellt.",
"disabled_description": "Diese Rendering-Notizen stammen aus einer externen Quelle. Um Sie vor schädlichen Inhalten zu schützen, ist diese Funktion standardmäßig deaktiviert. Stellen Sie sicher, dass Sie der Quelle vertrauen, bevor Sie sie aktivieren.",
"disabled_button_enable": "Rendering-Notiz aktivieren"
},
"active_content_badges": {
"type_icon_pack": "Icon-Paket",
"type_backend_script": "Backend-Skript",
"type_frontend_script": "Frontend-Skript",
"type_widget": "Widget",
"type_app_css": "Benutzerdefiniertes CSS",
"type_render_note": "Rendering-Notiz",
"type_web_view": "Webansicht",
"type_app_theme": "Benutzerdefiniertes Thema",
"toggle_tooltip_enable_tooltip": "Klicken, um diesen {{type}} zu aktivieren.",
"toggle_tooltip_disable_tooltip": "Klicken, um diesen {{type}} zu deaktivieren.",
"menu_docs": "Dokumentation öffnen",
"menu_execute_now": "Skript jetzt ausführen",
"menu_run": "Automatisch ausführen",
"menu_run_disabled": "Manuell",
"menu_run_backend_startup": "Wenn das Backend startet",
"menu_run_hourly": "Stündlich",
"menu_run_daily": "Täglich",
"menu_run_frontend_startup": "Wenn das Desktop-Frontend startet",
"menu_run_mobile_startup": "Wenn das mobile Frontend startet",
"menu_change_to_widget": "Zum Widget wechseln",
"menu_change_to_frontend_script": "Zum Frontend-Skript wechseln",
"menu_theme_base": "Themenbasis"
},
"setup_form": {
"more_info": "Mehr erfahren"
}
}

View File

@@ -13,6 +13,61 @@
"critical-error": {
"title": "Κρίσιμο σφάλμα",
"message": "Συνέβη κάποιο κρίσιμο σφάλμα, το οποίο δεν επιτρέπει στην εφαρμογή χρήστη να ξεκινήσει:\n\n{{message}}\n\nΤο πιθανότερο είναι να προκλήθηκε από κάποιο script που απέτυχε απρόοπτα. Δοκιμάστε να ξεκινήσετε την εφαρμογή σε ασφαλή λειτουργία για να λύσετε το πρόβλημα."
}
},
"widget-error": {
"title": "Δεν ήταν δυνατή η αρχικοποίηση του widget",
"message-custom": "Προσαρμοσμένο widget της σημείωσης με ID \"{{id}}\", με τίτλο \"{{title}}\", δεν ήταν δυνατό να αρχικοποιηθεί λόγω:\n\n{{message}}",
"message-unknown": "Άγνωστο widget δεν ήταν δυνατό να αρχικοποιηθεί λόγω:\n\n{{message}}"
},
"bundle-error": {
"title": "Δεν ήταν δυνατή η φόρτωση προσαρμοσμένου script",
"message": "Το script δεν ήταν δυνατό να εκτελεστεί λόγω:\n\n{{message}}"
},
"widget-list-error": {
"title": "Δεν ήταν δυνατή η λήψη της λίστας των widgets από τον server"
},
"widget-render-error": {
"title": "Δεν ήταν δυνατή η απόδοση προσαρμοσμένου React widget"
},
"widget-missing-parent": "Το προσαρμοσμένο widget δεν έχει ορισμένη την υποχρεωτική ιδιότητα '{{property}}'.\n\nΕάν το script προορίζεται για εκτέλεση χωρίς UI element, χρησιμοποιήστε '#run=frontendStartup' αντί για αυτό.",
"open-script-note": "Άνοιγμα σημείωσης script",
"scripting-error": "Σφάλμα προσαρμοσμένου script: {{title}}"
},
"bookmark_buttons": {
"bookmarks": "Σελιδοδείκτες"
},
"add_link": {
"add_link": "Προσθήκη συνδέσμου",
"help_on_links": "Βοήθεια για συνδέσμους",
"note": "Σημείωση",
"search_note": "Αναζήτηση σημείωσης με βάση το όνομά της",
"link_title_mirrors": "Ο τίτλος του συνδέσμου αντικατοπτρίζει τον τρέχοντα τίτλο της σημείωσης",
"link_title_arbitrary": "Ο τίτλος του συνδέσμου μπορεί να τροποποιηθεί ελεύθερα",
"link_title": "Τίτλος συνδέσμου",
"button_add_link": "Προσθήκη συνδέσμου"
},
"branch_prefix": {
"edit_branch_prefix": "Επεξεργασία προθέματος κλάδου",
"edit_branch_prefix_multiple": "Επεξεργασία προθέματος κλάδου για {{count}} κλάδους",
"help_on_tree_prefix": "Βοήθεια για πρόθεμα δέντρου",
"prefix": "Πρόθεμα: ",
"save": "Αποθήκευση",
"branch_prefix_saved": "Το πρόθεμα κλάδου αποθηκεύτηκε.",
"branch_prefix_saved_multiple": "Το πρόθεμα κλάδου αποθηκεύτηκε για {{count}} κλάδους.",
"affected_branches": "Επηρεαζόμενοι κλάδοι ({{count}}):"
},
"bulk_actions": {
"bulk_actions": "Μαζικές ενέργειες",
"affected_notes": "Επηρεαζόμενες σημειώσεις",
"include_descendants": "Συμπερίληψη απογόνων των επιλεγμένων σημειώσεων",
"available_actions": "Διαθέσιμες ενέργειες",
"chosen_actions": "Επιλεγμένες ενέργειες",
"execute_bulk_actions": "Εκτέλεση μαζικών ενεργειών",
"bulk_actions_executed": "Οι μαζικές ενέργειες εκτελέστηκαν επιτυχώς.",
"none_yet": "Καμία ακόμη… προσθέστε μια ενέργεια επιλέγοντας μία από τις διαθέσιμες παραπάνω.",
"labels": "Ετικέτες",
"relations": "Συσχετίσεις",
"notes": "Σημειώσεις",
"other": "Λοιπά"
}
}

View File

@@ -1010,7 +1010,7 @@
"no_attachments": "This note has no attachments."
},
"book": {
"no_children_help": "This collection doesn't have any child notes so there's nothing to display. See <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> for details.",
"no_children_help": "This collection doesn't have any child notes so there's nothing to display.",
"drag_locked_title": "Locked for editing",
"drag_locked_message": "Dragging not allowed since the collection is locked for editing."
},
@@ -1067,13 +1067,21 @@
"click_on_canvas_to_place_new_note": "Click on canvas to place new note"
},
"render": {
"note_detail_render_help_1": "This help note is shown because this note of type Render HTML doesn't have required relation to function properly.",
"note_detail_render_help_2": "Render HTML note type is used for <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. In short, you have a HTML code note (optionally with some JavaScript) and this note will render it. To make it work, you need to define a <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relation</a> called \"renderNote\" pointing to the HTML note to render."
"setup_title": "Display custom HTML or Preact JSX inside this note",
"setup_create_sample_preact": "Create sample note with Preact",
"setup_create_sample_html": "Create sample note with HTML",
"setup_sample_created": "A sample note was created as a child note.",
"disabled_description": "This render notes comes from an external source. To protect you from malicious content, it is not enabled by default. Make sure you trust the source before enabling it.",
"disabled_button_enable": "Enable render note"
},
"web_view": {
"web_view": "Web View",
"embed_websites": "Note of type Web View allows you to embed websites into Trilium.",
"create_label": "To start, please create a label with a URL address you want to embed, e.g. #webViewSrc=\"https://www.google.com\""
"web_view_setup": {
"title": "Create a live view of a webpage directly into Trilium",
"url_placeholder": "Enter or paste the website address, for example https://triliumnotes.org",
"create_button": "Create Web View",
"invalid_url_title": "Invalid address",
"invalid_url_message": "Insert a valid web address, for example https://triliumnotes.org.",
"disabled_description": "This web view was imported from an external source. To help protect you from phishing or malicious content, it isnt loading automatically. You can enable it if you trust the source.",
"disabled_button_enable": "Enable web view"
},
"backend_log": {
"refresh": "Refresh"
@@ -1589,7 +1597,8 @@
"description": "Description",
"reload_app": "Reload app to apply changes",
"set_all_to_default": "Set all shortcuts to the default",
"confirm_reset": "Do you really want to reset all keyboard shortcuts to the default?"
"confirm_reset": "Do you really want to reset all keyboard shortcuts to the default?",
"no_results": "No shortcuts found matching '{{filter}}'"
},
"spellcheck": {
"title": "Spell Check",
@@ -1795,6 +1804,8 @@
"printing": "Printing in progress...",
"printing_pdf": "Exporting to PDF in progress...",
"print_report_title": "Print report",
"print_report_error_title": "Failed to print",
"print_report_stack_trace": "Stack trace",
"print_report_collection_content_one": "{{count}} note in the collection could not be printed because they are not supported or they are protected.",
"print_report_collection_content_other": "{{count}} notes in the collection could not be printed because they are not supported or they are protected.",
"print_report_collection_details_button": "See details",
@@ -2099,7 +2110,8 @@
"raster": "Raster",
"vector_light": "Vector (Light)",
"vector_dark": "Vector (Dark)",
"show-scale": "Show scale"
"show-scale": "Show scale",
"show-labels": "Show marker names"
},
"table_context_menu": {
"delete_row": "Delete row"
@@ -2178,8 +2190,9 @@
"percentage": "%"
},
"pagination": {
"page_title": "Page of {{startIndex}} - {{endIndex}}",
"total_notes": "{{count}} notes"
"total_notes": "{{count}} notes",
"prev_page": "Previous page",
"next_page": "Next page"
},
"collections": {
"rendering_error": "Unable to show content due to an error."
@@ -2283,5 +2296,32 @@
},
"bookmark_buttons": {
"bookmarks": "Bookmarks"
},
"active_content_badges": {
"type_icon_pack": "Icon pack",
"type_backend_script": "Backend script",
"type_frontend_script": "Frontend script",
"type_widget": "Widget",
"type_app_css": "Custom CSS",
"type_render_note": "Render note",
"type_web_view": "Web view",
"type_app_theme": "Custom theme",
"toggle_tooltip_enable_tooltip": "Click to enable this {{type}}.",
"toggle_tooltip_disable_tooltip": "Click to disable this {{type}}.",
"menu_docs": "Open documentation",
"menu_execute_now": "Execute script now",
"menu_run": "Run automatically",
"menu_run_disabled": "Manually",
"menu_run_backend_startup": "When the backend starts up",
"menu_run_hourly": "Hourly",
"menu_run_daily": "Daily",
"menu_run_frontend_startup": "When the desktop frontend starts up",
"menu_run_mobile_startup": "When the mobile frontend starts up",
"menu_change_to_widget": "Change to widget",
"menu_change_to_frontend_script": "Change to frontend script",
"menu_theme_base": "Theme base"
},
"setup_form": {
"more_info": "Learn more"
}
}

View File

@@ -1068,15 +1068,6 @@
"default_new_note_title": "nueva nota",
"click_on_canvas_to_place_new_note": "Haga clic en el lienzo para colocar una nueva nota"
},
"render": {
"note_detail_render_help_1": "Esta nota de ayuda se muestra porque esta nota de tipo Renderizar HTML no tiene la relación requerida para funcionar correctamente.",
"note_detail_render_help_2": "El tipo de nota Render HTML es usado para <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. De forma resumida, tiene una nota con código HTML (opcionalmente con algo de JavaScript) y esta nota la renderizará. Para que funcione, es necesario definir una <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relación</a> llamada \"renderNote\" apuntando a la nota HTML nota a renderizar."
},
"web_view": {
"web_view": "Vista web",
"embed_websites": "La nota de tipo Web View le permite insertar sitios web en Trilium.",
"create_label": "Para comenzar, por favor cree una etiqueta con una dirección URL que desee empotrar, e.g. #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Refrescar"
},
@@ -1577,7 +1568,8 @@
"description": "Descripción",
"reload_app": "Vuelva a cargar la aplicación para aplicar los cambios",
"set_all_to_default": "Establecer todos los accesos directos al valor predeterminado",
"confirm_reset": "¿Realmente desea restablecer todos los atajos de teclado a sus valores predeterminados?"
"confirm_reset": "¿Realmente desea restablecer todos los atajos de teclado a sus valores predeterminados?",
"no_results": "No se encontraron atajos que coincidan con '{{filter}} '"
},
"spellcheck": {
"title": "Revisión ortográfica",
@@ -1784,7 +1776,9 @@
"print_report_collection_content_other": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas.",
"print_report_title": "Imprimir informe",
"print_report_collection_details_button": "Ver detalles",
"print_report_collection_details_ignored_notes": "Notas ignoradas"
"print_report_collection_details_ignored_notes": "Notas ignoradas",
"print_report_stack_trace": "Rastreo de pila",
"print_report_error_title": "Fallo al imprimir"
},
"note_title": {
"placeholder": "escriba el título de la nota aquí...",
@@ -2164,8 +2158,7 @@
"percentage": "%"
},
"pagination": {
"total_notes": "{{count}} notas",
"page_title": "Página de {{startIndex}} - {{endIndex}}"
"total_notes": "{{count}} notas"
},
"presentation_view": {
"edit-slide": "Editar este slide",
@@ -2298,5 +2291,49 @@
},
"bookmark_buttons": {
"bookmarks": "Marcadores"
},
"web_view_setup": {
"title": "Crear una vista en vivo de una página web directamente en Trilium",
"url_placeholder": "Ingresar o pegar la dirección del sitio web, por ejemplo https://triliumnotes.org",
"create_button": "Crear Vista Web",
"invalid_url_title": "Dirección inválida",
"invalid_url_message": "Ingrese una dirección web válida, por ejemplo https://triliumnotes.org.",
"disabled_description": "Esta vista web fue importada de una fuente externa. Para ayudarlo a protegerse del phishing o el contenido malicioso, no se está cargando automáticamente. Puede activarlo si confía en la fuente.",
"disabled_button_enable": "Habilita vista web"
},
"render": {
"setup_title": "Mostrar HTML personalizado o Preact JSX dentro de esta nota",
"setup_create_sample_preact": "Crear nota de muestra con Preact",
"setup_create_sample_html": "Crear nota de muestra con HTML",
"setup_sample_created": "Se creó una nota de muestra como subnota.",
"disabled_description": "Esta nota de renderización proviene de una fuente externa. Para protegerlo de contenido malicioso, no está habilitado por defecto. Asegúrese de confiar en la fuente antes de habilitarla.",
"disabled_button_enable": "Habilitar nota de renderización"
},
"active_content_badges": {
"type_icon_pack": "Paquete de iconos",
"type_backend_script": "Script de backend",
"type_frontend_script": "Script de frontend",
"type_widget": "Widget",
"type_app_css": "CSS personalizado",
"type_render_note": "Nota de renderización",
"type_web_view": "Vista web",
"type_app_theme": "Tema personalizado",
"toggle_tooltip_enable_tooltip": "Haga clic para habilitar este {{type}}.",
"toggle_tooltip_disable_tooltip": "Haga clic para deshabilitar este {{type}}.",
"menu_docs": "Abrir documentación",
"menu_execute_now": "Ejecutar script ahora",
"menu_run": "Ejecutar automáticamente",
"menu_run_disabled": "Manualmente",
"menu_run_backend_startup": "Cuando el backend inicia",
"menu_run_hourly": "Cada hora",
"menu_run_daily": "Diariamente",
"menu_run_frontend_startup": "Cuando el frontend de escritorio inicia",
"menu_run_mobile_startup": "Cuando el frontend móvil inicia",
"menu_change_to_widget": "Cambiar a widget",
"menu_change_to_frontend_script": "Cambiar a script de frontend",
"menu_theme_base": "Tema base"
},
"setup_form": {
"more_info": "Para saber más"
}
}

View File

@@ -1053,15 +1053,6 @@
"default_new_note_title": "nouvelle note",
"click_on_canvas_to_place_new_note": "Cliquez sur le canevas pour placer une nouvelle note"
},
"render": {
"note_detail_render_help_1": "Cette note d'aide s'affiche car cette note de type Rendu HTML n'a pas la relation requise pour fonctionner correctement.",
"note_detail_render_help_2": "Le type de note Rendu HTML est utilisé pour les <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripts</a>. En résumé, vous disposez d'une note de code HTML (éventuellement contenant JavaScript) et cette note affichera le rendu. Pour que cela fonctionne, vous devez définir une <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relation</a> appelée \"renderNote\" pointant vers la note HTML à rendre."
},
"web_view": {
"web_view": "Affichage Web",
"embed_websites": "Les notes de type Affichage Web vous permet d'intégrer des sites Web dans Trilium.",
"create_label": "Pour commencer, veuillez créer un label avec l'adresse URL que vous souhaitez intégrer, par ex. #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Rafraîchir"
},
@@ -2058,7 +2049,6 @@
"percentage": "%"
},
"pagination": {
"page_title": "Page de {{startIndex}} - {{endIndex}}",
"total_notes": "{{count}} notes"
},
"collections": {

View File

@@ -1016,7 +1016,7 @@
"no_attachments": "Níl aon cheangaltáin leis an nóta seo."
},
"book": {
"no_children_help": "Níl aon nótaí faoi mhíbhuntáiste sa bhailiúchán seo mar sin níl aon rud le taispeáint. Féach ar an <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">vicí</a> le haghaidh tuilleadh sonraí.",
"no_children_help": "Níl aon nótaí faoi mhíbhuntáiste sa bhailiúchán seo mar sin níl aon rud le taispeáint.",
"drag_locked_title": "Glasáilte le haghaidh eagarthóireachta",
"drag_locked_message": "Ní cheadaítear tarraingt ós rud é go bhfuil an bailiúchán faoi ghlas le haghaidh eagarthóireachta."
},
@@ -1072,15 +1072,6 @@
"default_new_note_title": "nóta nua",
"click_on_canvas_to_place_new_note": "Cliceáil ar chanbhás chun nóta nua a chur"
},
"render": {
"note_detail_render_help_1": "Taispeántar an nóta cabhrach seo mar nach bhfuil aon ghaol riachtanach ag an nóta seo den chineál Render HTML le go bhfeidhmeoidh sé i gceart.",
"note_detail_render_help_2": "Úsáidtear cineál nóta HTML rindreála le haghaidh <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scriptithe</a>. Go hachomair, tá nóta cóid HTML agat (le roinnt JavaScript más féidir) agus déanfaidh an nóta seo é a rindreáil. Chun go n-oibreoidh sé, ní mór duit <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">gaol</a> ar a dtugtar \"renderNote\" a shainiú ag pointeáil chuig an nóta HTML atá le rindreáil."
},
"web_view": {
"web_view": "Radharc Gréasáin",
"embed_websites": "Nóta den chineál Gréasáin a ligeann duit suíomhanna gréasáin a leabú i Trilium.",
"create_label": "Chun tús a chur leis, cruthaigh lipéad le seoladh URL ar mhaith leat a leabú, m.sh. #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Athnuachan"
},
@@ -1595,7 +1586,8 @@
"description": "Cur síos",
"reload_app": "Athlódáil an aip chun na hathruithe a chur i bhfeidhm",
"set_all_to_default": "Socraigh gach aicearra go dtí an réamhshocrú",
"confirm_reset": "An bhfuil tú cinnte gur mhaith leat na haicearraí méarchláir go léir a athshocrú go dtí an rogha réamhshocraithe?"
"confirm_reset": "An bhfuil tú cinnte gur mhaith leat na haicearraí méarchláir go léir a athshocrú go dtí an rogha réamhshocraithe?",
"no_results": "Níor aimsíodh aon aicearraí a mheaitseálann '{{filter}}'"
},
"spellcheck": {
"title": "Seiceáil Litrithe",
@@ -1813,7 +1805,9 @@
"print_report_collection_content_many": "Níorbh fhéidir {{count}} nótaí sa bhailiúchán a phriontáil mar nach dtacaítear leo nó mar go bhfuil siad faoi chosaint.",
"print_report_collection_content_other": "Níorbh fhéidir {{count}} nótaí sa bhailiúchán a phriontáil mar nach dtacaítear leo nó mar go bhfuil siad faoi chosaint.",
"print_report_collection_details_button": "Féach sonraí",
"print_report_collection_details_ignored_notes": "Nótaí neamhairdithe"
"print_report_collection_details_ignored_notes": "Nótaí neamhairdithe",
"print_report_error_title": "Theip ar phriontáil",
"print_report_stack_trace": "Rian cruachta"
},
"note_title": {
"placeholder": "clóscríobh teideal an nóta anseo...",
@@ -2114,7 +2108,8 @@
"raster": "Raster",
"vector_light": "Veicteoir (Solas)",
"vector_dark": "Veicteoir (Dorcha)",
"show-scale": "Taispeáin scála"
"show-scale": "Taispeáin scála",
"show-labels": "Taispeáin ainmneacha marcóirí"
},
"table_context_menu": {
"delete_row": "Scrios an tsraith"
@@ -2193,7 +2188,6 @@
"percentage": "%"
},
"pagination": {
"page_title": "Leathanach de {{startIndex}} - {{endIndex}}",
"total_notes": "{{count}} nótaí"
},
"collections": {
@@ -2328,5 +2322,49 @@
},
"bookmark_buttons": {
"bookmarks": "Leabharmharcanna"
},
"web_view_setup": {
"title": "Cruthaigh radharc beo de leathanach gréasáin go díreach isteach i Trilium",
"url_placeholder": "Cuir isteach nó greamaigh seoladh an tsuímh ghréasáin, mar shampla https://triliumnotes.org",
"create_button": "Cruthaigh Radharc Gréasáin",
"invalid_url_title": "Seoladh neamhbhailí",
"invalid_url_message": "Cuir isteach seoladh gréasáin bailí, mar shampla https://triliumnotes.org.",
"disabled_description": "Iompórtáladh an radharc gréasáin seo ó fhoinse sheachtrach. Chun cabhrú leat a chosaint ar ábhar fioscaireachta nó mailíseach, níl sé ag lódáil go huathoibríoch. Is féidir leat é a chumasú má tá muinín agat as an bhfoinse.",
"disabled_button_enable": "Cumasaigh radharc gréasáin"
},
"render": {
"setup_title": "Taispeáin HTML saincheaptha nó Preact JSX taobh istigh den nóta seo",
"setup_create_sample_preact": "Cruthaigh nóta samplach le Preact",
"setup_create_sample_html": "Cruthaigh nóta samplach le HTML",
"setup_sample_created": "Cruthaíodh nóta samplach mar nóta linbh.",
"disabled_description": "Tagann na nótaí rindreála seo ó fhoinse sheachtrach. Chun tú a chosaint ar ábhar mailíseach, níl sé cumasaithe de réir réamhshocraithe. Déan cinnte go bhfuil muinín agat as an bhfoinse sula gcumasaíonn tú é.",
"disabled_button_enable": "Cumasaigh nóta rindreála"
},
"active_content_badges": {
"type_icon_pack": "Pacáiste deilbhín",
"type_backend_script": "Script chúltaca",
"type_frontend_script": "Script tosaigh",
"type_widget": "Giuirléid",
"type_app_css": "CSS saincheaptha",
"type_render_note": "Nóta rindreála",
"type_web_view": "Radharc gréasáin",
"type_app_theme": "Téama saincheaptha",
"toggle_tooltip_enable_tooltip": "Cliceáil chun an {{type}} seo a chumasú.",
"toggle_tooltip_disable_tooltip": "Cliceáil chun an {{type}} seo a dhíchumasú.",
"menu_docs": "Doiciméadú oscailte",
"menu_execute_now": "Rith an script anois",
"menu_run": "Rith go huathoibríoch",
"menu_run_disabled": "De láimh",
"menu_run_backend_startup": "Nuair a thosaíonn an cúltaca",
"menu_run_hourly": "Gach uair an chloig",
"menu_run_daily": "Laethúil",
"menu_run_frontend_startup": "Nuair a thosaíonn tosaigh an deisce",
"menu_run_mobile_startup": "Nuair a thosaíonn an taobhlíne soghluaiste",
"menu_change_to_widget": "Athraigh go giuirléid",
"menu_change_to_frontend_script": "Athraigh chuig an script tosaigh",
"menu_theme_base": "Bunús téama"
},
"setup_form": {
"more_info": "Foghlaim níos mó"
}
}

View File

@@ -50,7 +50,8 @@
"save": "Simpan",
"branch_prefix_saved": "Prefiks cabang telah disimpan.",
"branch_prefix_saved_multiple": "Prefix cabang telah disimpan pada {{count}} cabang.",
"affected_branches": "Cabang terdampak ({{count}}):"
"affected_branches": "Cabang terdampak ({{count}}):",
"edit_branch_prefix": "Sunting awalan cabang"
},
"bulk_actions": {
"bulk_actions": "Aksi borongan",
@@ -61,14 +62,18 @@
"execute_bulk_actions": "Eksekusi aksi borongan",
"bulk_actions_executed": "Aksi borongan telah di eksekusi dengan sukses.",
"none_yet": "Belum ada... tambahkan aksi dengan memilih salah satu dari aksi di atas.",
"labels": "Label-label"
"labels": "Label-label",
"relations": "Hubungan",
"notes": "Catatan",
"other": "Lainnya"
},
"confirm": {
"cancel": "Batal",
"ok": "Oke",
"are_you_sure_remove_note": "Apakah anda yakin mau membuang catatan \"{{title}}\" dari peta relasi? ",
"if_you_dont_check": "Jika Anda tidak mencentang ini, catatan hanya akan dihapus dari peta relasi.",
"also_delete_note": "Hapus juga catatannya"
"also_delete_note": "Hapus juga catatannya",
"confirmation": "Konfirmasi"
},
"delete_notes": {
"delete_notes_preview": "Hapus pratinjau catatan",
@@ -77,9 +82,19 @@
"erase_notes_description": "Penghapusan normal hanya menandai catatan sebagai dihapus dan dapat dipulihkan (melalui dialog versi revisi) dalam jangka waktu tertentu. Mencentang opsi ini akan menghapus catatan secara permanen seketika dan catatan tidak akan bisa dipulihkan kembali.",
"erase_notes_warning": "Hapus catatan secara permanen (tidak bisa dikembalikan), termasuk semua duplikat. Aksi akan memaksa aplikasi untuk mengulang kembali.",
"notes_to_be_deleted": "Catatan-catatan berikut akan dihapuskan ({{notesCount}})",
"no_note_to_delete": "Tidak ada Catatan yang akan dihapus (hanya duplikat)."
"no_note_to_delete": "Tidak ada Catatan yang akan dihapus (hanya duplikat).",
"broken_relations_to_be_deleted": "Hubungan berikut akan diputus dan dihapus ({{ relationCount}})"
},
"clone_to": {
"clone_notes_to": "Duplikat catatan ke…"
"clone_notes_to": "Duplikat catatan ke…",
"help_on_links": "Bantuan pada tautan",
"notes_to_clone": "Catatan untuk kloning",
"target_parent_note": "Sasaran catatan utama",
"search_for_note_by_its_name": "cari catatan berdasarkan namanya",
"cloned_note_prefix_title": "Catatan yang dikloning akan ditampilkan diruntutan catatan dengan awalan yang diberikan",
"prefix_optional": "Awalan (opsional)",
"clone_to_selected_note": "Salin ke catatan yang dipilih",
"no_path_to_clone_to": "Tidak ada jalur untuk digandakan.",
"note_cloned": "Catatan \"{{clonedTitle}}\" telah digandakan ke dalam \"{{targetTitle}}\""
}
}

View File

@@ -167,8 +167,8 @@
"desktop-application": "Applicazione Desktop",
"native-title-bar": "Barra del titolo nativa",
"native-title-bar-description": "Su Windows e macOS, disattivare la barra del titolo nativa rende l'applicazione più compatta. Su Linux, attivarla si integra meglio con il resto del sistema.",
"background-effects": "Abilita effetti di sfondo (solo Windows 11)",
"background-effects-description": "L'effetto Mica aggiunge uno sfondo sfocato ed elegante alle finestre delle app, creando profondità e un aspetto moderno. La \"Barra del titolo nativa\" deve essere disattivata.",
"background-effects": "Abilita effetti di sfondo",
"background-effects-description": "Aggiunge uno sfondo sfocato ed elegante alle finestre dell'app, creando profondità e un look moderno. La \"barra del titolo nativa\" deve essere disabilitata.",
"restart-app-button": "Riavviare l'applicazione per visualizzare le modifiche"
},
"note_autocomplete": {
@@ -186,7 +186,8 @@
"geo-map": {
"create-child-note-title": "Crea una nota figlia e aggiungila alla mappa",
"create-child-note-instruction": "Clicca sulla mappa per creare una nuova nota qui o premi Escape per uscire.",
"unable-to-load-map": "Impossibile caricare la mappa."
"unable-to-load-map": "Impossibile caricare la mappa.",
"create-child-note-text": "Aggiungi indicatore"
},
"geo-map-context": {
"open-location": "Apri la posizione",
@@ -368,7 +369,8 @@
"description": "Descrizione",
"reload_app": "Ricarica l'app per applicare le modifiche",
"set_all_to_default": "Imposta tutte le scorciatoie sui valori predefiniti",
"confirm_reset": "Vuoi davvero ripristinare tutte le scorciatoie da tastiera ai valori predefiniti?"
"confirm_reset": "Vuoi davvero ripristinare tutte le scorciatoie da tastiera ai valori predefiniti?",
"no_results": "Nessuna scorciatoia trovata corrispondente '{{filter}}'"
},
"shared_switch": {
"toggle-on-title": "Condividi la nota",
@@ -422,7 +424,8 @@
"unknown_search_option": "Opzione di ricerca sconosciuta {{searchOptionName}}",
"search_note_saved": "La nota di ricerca è stata salvata in {{- notePathTitle}}",
"actions_executed": "Le azioni sono state eseguite.",
"view_options": "Opzioni di visualizzazione:"
"view_options": "Opzioni di visualizzazione:",
"option": "opzione"
},
"modal": {
"close": "Chiudi",
@@ -1241,7 +1244,8 @@
"show-cheatsheet": "Mostra il foglietto illustrativo",
"toggle-zen-mode": "Modalità Zen",
"new-version-available": "Nuovo aggiornamento disponibile",
"download-update": "Ottieni la versione {{latestVersion}}"
"download-update": "Ottieni la versione {{latestVersion}}",
"search_notes": "Cerca note"
},
"zen_mode": {
"button_exit": "Esci dalla modalità Zen"
@@ -1324,7 +1328,7 @@
"button_title": "Esporta diagramma come SVG"
},
"relation_map_buttons": {
"create_child_note_title": "Crea una nuova nota secondaria e aggiungila a questa mappa delle relazioni",
"create_child_note_title": "Crea una nota secondaria e aggiungila alla mappa",
"reset_pan_zoom_title": "Ripristina panoramica e zoom alle coordinate e all'ingrandimento iniziali",
"zoom_in_title": "Ingrandisci",
"zoom_out_title": "Rimpicciolisci"
@@ -1340,7 +1344,9 @@
"delete_this_note": "Elimina questa nota",
"note_revisions": "Revisioni delle note",
"error_cannot_get_branch_id": "Impossibile ottenere branchId per notePath '{{notePath}}'",
"error_unrecognized_command": "Comando non riconosciuto {{command}}"
"error_unrecognized_command": "Comando non riconosciuto {{command}}",
"backlinks": "Backlinks",
"content_language_switcher": "Lingua dei contenuti: {{language}}"
},
"note_icon": {
"change_note_icon": "Cambia icona nota",
@@ -1521,7 +1527,7 @@
"no_attachments": "Questa nota non ha allegati."
},
"book": {
"no_children_help": "Questa raccolta non ha note secondarie, quindi non c'è nulla da visualizzare. Consulta la <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> per i dettagli.",
"no_children_help": "Questa raccolta non ha note secondarie, quindi non c'è nulla da visualizzare.",
"drag_locked_title": "Bloccato per la modifica",
"drag_locked_message": "Trascinamento non consentito poiché la raccolta è bloccata per la modifica."
},
@@ -1577,15 +1583,6 @@
"default_new_note_title": "nuova nota",
"click_on_canvas_to_place_new_note": "Clicca sulla tela per inserire una nuova nota"
},
"render": {
"note_detail_render_help_1": "Questa nota di aiuto viene visualizzata perché questa nota di tipo Render HTML non ha la relazione richiesta per funzionare correttamente.",
"note_detail_render_help_2": "Il tipo di nota HTML Render viene utilizzato per lo <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. In breve, si ottiene una nota in codice HTML (opzionalmente con un po' di JavaScript) che verrà visualizzata. Per farla funzionare, è necessario definire una <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relazione</a> denominata \"renderNote\" che punti alla nota HTML da visualizzare."
},
"web_view": {
"web_view": "Visualizzazione Web",
"embed_websites": "La nota di tipo Web View consente di incorporare siti web in Trilium.",
"create_label": "Per iniziare, crea un'etichetta con l'indirizzo URL che desideri incorporare, ad esempio #webViewSrc=\"https://www.google.com\""
},
"vacuum_database": {
"title": "Pulizia del database",
"description": "Questa operazione ricostruirà il database, generando in genere un file di dimensioni inferiori. In realtà, nessun dato verrà modificato.",
@@ -1923,7 +1920,9 @@
"print_report_collection_content_many": "{{count}} le note nella raccolta non possono essere stampate perché non sono supportate o sono protette.",
"print_report_collection_content_other": "{{count}} le note nella raccolta non possono essere stampate perché non sono supportate o sono protette.",
"print_report_collection_details_button": "Vedi dettagli",
"print_report_collection_details_ignored_notes": "Note ignorate"
"print_report_collection_details_ignored_notes": "Note ignorate",
"print_report_error_title": "Impossibile stampare",
"print_report_stack_trace": "Traccia dello stack"
},
"note_title": {
"placeholder": "scrivi qui il titolo della nota...",
@@ -2110,7 +2109,8 @@
"raster": "Trama",
"vector_light": "Vettore (Luce)",
"vector_dark": "Vettore (scuro)",
"show-scale": "Mostra scala"
"show-scale": "Mostra scala",
"show-labels": "Mostra nomi dei marcatori"
},
"table_context_menu": {
"delete_row": "Elimina riga"
@@ -2143,9 +2143,9 @@
"next_theme_message": "Al momento stai utilizzando il tema legacy. Vuoi provare il nuovo tema?",
"next_theme_button": "Prova il nuovo tema",
"background_effects_title": "Gli effetti di sfondo sono ora stabili",
"background_effects_message": "Sui dispositivi Windows, gli effetti di sfondo sono ora completamente stabili. Gli effetti di sfondo aggiungono un tocco di colore all'interfaccia utente sfocando lo sfondo retrostante. Questa tecnica è utilizzata anche in altre applicazioni come Esplora risorse di Windows.",
"background_effects_message": "Su dispositivi Windows e macOS, gli effetti di sfondo sono ora stabili. Gli effetti di sfondo aggiungono un tocco di colore all'interfaccia utente sfocando lo sfondo dietro di essa.",
"background_effects_button": "Abilita gli effetti di sfondo",
"dismiss": "Congedare",
"dismiss": "Chiudi",
"new_layout_title": "Nuovo layout",
"new_layout_message": "Abbiamo introdotto un layout modernizzato per Trilium. La barra multifunzione è stata rimossa e integrata perfettamente nell'interfaccia principale, con una nuova barra di stato e sezioni espandibili (come gli attributi promossi) che assumono le funzioni chiave.\n\nIl nuovo layout è abilitato di default e può essere temporaneamente disabilitato tramite Opzioni → Aspetto.",
"new_layout_button": "Maggiori informazioni"
@@ -2164,7 +2164,6 @@
"percentage": "%"
},
"pagination": {
"page_title": "Pagina di {{startIndex}} - {{endIndex}}",
"total_notes": "{{count}} note"
},
"collections": {
@@ -2281,5 +2280,61 @@
"pages_other": "{{count}} pagine",
"pages_alt": "Pagina {{pageNumber}}",
"pages_loading": "Caricamento in corso..."
},
"web_view_setup": {
"title": "Crea una visualizzazione live di una pagina web direttamente in Trilium",
"url_placeholder": "Inserisci o incolla l'indirizzo del sito web, ad esempio https://triliumnotes.org",
"create_button": "Crea vista Web",
"invalid_url_title": "Indirizzo non valido",
"invalid_url_message": "Inserisci un indirizzo web valido, ad esempio https://triliumnotes.org.",
"disabled_description": "Questa visualizzazione web è stata importata da una fonte esterna. Per proteggerti dal phishing o da contenuti dannosi, non viene caricata automaticamente. Puoi abilitarla se ritieni che la fonte sia affidabile.",
"disabled_button_enable": "Abilita visualizzazione web"
},
"platform_indicator": {
"available_on": "Disponibile su {{platform}}"
},
"mobile_tab_switcher": {
"title_one": "Scheda {{count}}",
"title_many": "Schede {{count}}",
"title_other": "Schede {{count}}",
"more_options": "Altre opzioni"
},
"bookmark_buttons": {
"bookmarks": "Segnalibri"
},
"render": {
"setup_title": "Visualizza HTML personalizzato o Preact JSX all'interno di questa nota",
"setup_create_sample_preact": "Crea una nota di esempio con Preact",
"setup_create_sample_html": "Crea una nota di esempio con HTML",
"setup_sample_created": "È stata creata una nota di esempio come nota secondaria.",
"disabled_description": "Queste note di rendering provengono da una fonte esterna. Per proteggerti da contenuti dannosi, non sono abilitate per impostazione predefinita. Assicurati di fidarti della fonte prima di abilitarle.",
"disabled_button_enable": "Abilita nota di rendering"
},
"active_content_badges": {
"type_icon_pack": "Pacchetto icone",
"type_backend_script": "Script di backend",
"type_frontend_script": "Script frontend",
"type_widget": "Widget",
"type_app_css": "CSS personalizzato",
"type_render_note": "Nota di rendering",
"type_web_view": "Visualizzazione web",
"type_app_theme": "Tema personalizzato",
"toggle_tooltip_enable_tooltip": "Clicca per abilitare questa funzione {{type}}.",
"toggle_tooltip_disable_tooltip": "Clicca per disattivare questa funzione {{type}}.",
"menu_docs": "Documentazione aperta",
"menu_execute_now": "Esegui lo script ora",
"menu_run": "Esegui automaticamente",
"menu_run_disabled": "Manualmente",
"menu_run_backend_startup": "Quando il backend si avvia",
"menu_run_hourly": "Ogni ora",
"menu_run_daily": "Giornaliero",
"menu_run_frontend_startup": "Quando si avvia il frontend desktop",
"menu_run_mobile_startup": "Quando si avvia il frontend mobile",
"menu_change_to_widget": "Passa al widget",
"menu_change_to_frontend_script": "Modifica allo script frontend",
"menu_theme_base": "Tema base"
},
"setup_form": {
"more_info": "Per saperne di più"
}
}

View File

@@ -249,7 +249,8 @@
"reload_app": "リロードして変更を適用する",
"set_all_to_default": "すべてのショートカットをデフォルトに戻す",
"confirm_reset": "キーボードショートカットをすべてデフォルトにリセットしますか?",
"keyboard_shortcuts": "キーボードショートカット"
"keyboard_shortcuts": "キーボードショートカット",
"no_results": "'{{filter}}' に一致するショートカットが見つかりません"
},
"confirm": {
"confirmation": "確認",
@@ -826,11 +827,6 @@
"error_no_path": "移動するパスがありません。",
"move_success_message": "選択したノートは以下に移動されました "
},
"web_view": {
"web_view": "Web ビュー",
"embed_websites": "Web ビュータイプでは、web サイトを Trilium に埋め込むことができます。",
"create_label": "まず始めに、埋め込みたいURLアドレスのラベルを作成してください。例: #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "リフレッシュ"
},
@@ -1864,10 +1860,6 @@
"protecting-title": "保護の状態",
"unprotecting-title": "保護解除の状態"
},
"render": {
"note_detail_render_help_1": "このヘルプートが表示されるのは、このートの「HTML のレンダリング」タイプには、正常に機能するために必要なリレーションがないためです。",
"note_detail_render_help_2": "レンダリングHTMLートタイプは、<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">スクリプティング</a>に使用されます。簡単に言うと、HTMLコードートオプションでJavaScriptを含むがあり、このートがそれをレンダリングします。これを動作させるには、レンダリングするHTMLートを指す「renderNote」という<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">リレーション</a>を定義する必要があります。"
},
"consistency_checks": {
"find_and_fix_button": "一貫性の問題を見つけて修正する",
"finding_and_fixing_message": "一貫性の問題を見つけて修正中…",
@@ -1959,7 +1951,9 @@
"print_report_title": "レポートを印刷",
"print_report_collection_content_other": "コレクション内の {{count}} 件のノートは、サポートされていないか保護されているため、印刷できませんでした。",
"print_report_collection_details_button": "詳細を見る",
"print_report_collection_details_ignored_notes": "無視されたノート"
"print_report_collection_details_ignored_notes": "無視されたノート",
"print_report_error_title": "印刷に失敗しました",
"print_report_stack_trace": "スタックトレース"
},
"watched_file_update_status": {
"ignore_this_change": "この変更を無視する",
@@ -2042,7 +2036,8 @@
"show-scale": "スケールを表示",
"raster": "Raster",
"vector_light": "Vectorライト",
"vector_dark": "Vector (ダーク)"
"vector_dark": "Vector (ダーク)",
"show-labels": "マーカー名を表示"
},
"call_to_action": {
"next_theme_title": "新しいTriliumテーマをお試しください",
@@ -2070,7 +2065,6 @@
"percentage": "%"
},
"pagination": {
"page_title": "{{startIndex}} - {{endIndex}} ページ",
"total_notes": "{{count}} ノート"
},
"collections": {
@@ -2099,7 +2093,7 @@
"no_attachments": "このノートには添付ファイルはありません。"
},
"book": {
"no_children_help": "このコレクションには子ノートがないため、表示するものがありません。詳細は<a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a>をご覧ください。",
"no_children_help": "このコレクションには子ノートがないため、表示するものがありません。",
"drag_locked_title": "編集をロック中",
"drag_locked_message": "コレクションは編集がロックされているため、ドラッグは許可されていません。"
},
@@ -2268,5 +2262,49 @@
},
"bookmark_buttons": {
"bookmarks": "ブックマーク"
},
"web_view_setup": {
"title": "Trilium に直接 Web ページのライブビューを作成",
"url_placeholder": "Web サイトのアドレスを入力または貼り付けて下さい。 例: https://triliumnotes.org",
"create_button": "Web ビューを作成",
"invalid_url_title": "無効なアドレス",
"invalid_url_message": "有効な Web アドレスを入力してください。 例: https://triliumnotes.org",
"disabled_description": "この Web ビューは外部ソースからインポートされました。フィッシングや悪意のあるコンテンツから保護するため、自動的には読み込まれません。ソースを信頼できる場合は、有効にすることができます。",
"disabled_button_enable": "Web ビューを有効"
},
"render": {
"setup_title": "このノート内にカスタム HTML または Preact JSX を表示",
"setup_create_sample_preact": "Preact でサンプルノートを作成",
"setup_create_sample_html": "HTML でサンプルノートを作成",
"setup_sample_created": "子ノートとしてサンプルノートが作成されました。",
"disabled_description": "このレンダリングノートは外部ソースから提供されています。悪意のあるコンテンツからユーザーを保護するため、デフォルトでは有効になっていません。有効にする前に、ソースが信頼できるかどうかをご確認ください。",
"disabled_button_enable": "レンダリングノートを有効"
},
"active_content_badges": {
"type_icon_pack": "アイコンパック",
"type_backend_script": "バックエンドスクリプト",
"type_frontend_script": "フロントエンドスクリプト",
"type_widget": "ウィジェット",
"type_app_css": "カスタム CSS",
"type_render_note": "レンダリングノート",
"type_web_view": "Web ビュー",
"type_app_theme": "カスタムテーマ",
"toggle_tooltip_enable_tooltip": "この {{type}} を有効にするにはクリックしてください。",
"toggle_tooltip_disable_tooltip": "この {{type}} を無効にするにはクリックしてください。",
"menu_docs": "ドキュメントを開く",
"menu_execute_now": "今すぐスクリプトを実行",
"menu_run": "自動で実行",
"menu_run_disabled": "手動で実行",
"menu_run_backend_startup": "バックエンドの起動時",
"menu_run_hourly": "毎時",
"menu_run_daily": "毎日",
"menu_run_frontend_startup": "デスクトップ フロントエンドの起動時",
"menu_run_mobile_startup": "モバイル フロントエンドの起動時",
"menu_change_to_widget": "ウィジェットの変更",
"menu_change_to_frontend_script": "フロントエンドスクリプトの変更",
"menu_theme_base": "テーマベース"
},
"setup_form": {
"more_info": "さらに詳しく"
}
}

View File

@@ -261,7 +261,6 @@
"percentage": "%"
},
"pagination": {
"page_title": "Strona {{startIndex}} - {{endIndex}}",
"total_notes": "{{count}} notatek"
},
"collections": {
@@ -1432,15 +1431,6 @@
"default_new_note_title": "nowa notatka",
"click_on_canvas_to_place_new_note": "Kliknij na płótnie, aby umieścić nową notatkę"
},
"render": {
"note_detail_render_help_1": "Ta notatka pomocy jest wyświetlana, ponieważ ta notatka typu Render HTML nie ma wymaganej relacji do poprawnego działania.",
"note_detail_render_help_2": "Typ notatki Render HTML jest używany do <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">skryptowania</a>. W skrócie, masz notatkę kodu HTML (opcjonalnie z JavaScript) i ta notatka ją wyrenderuje. Aby to zadziałało, musisz zdefiniować <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relację</a> o nazwie \"renderNote\" wskazującą na notatkę HTML do wyrenderowania."
},
"web_view": {
"web_view": "Widok WWW",
"embed_websites": "Notatka typu Widok WWW pozwala na osadzanie stron internetowych w Trilium.",
"create_label": "Aby rozpocząć, utwórz etykietę z adresem URL, który chcesz osadzić, np. #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Odśwież"
},

View File

@@ -1064,15 +1064,6 @@
"default_new_note_title": "nova nota",
"click_on_canvas_to_place_new_note": "Clique no quadro para incluir uma nova nota"
},
"render": {
"note_detail_render_help_1": "Esta nota de ajuda é mostrada porque esta nota do tipo Renderizar HTML não possui a relação necessária para funcionar corretamente.",
"note_detail_render_help_2": "O tipo de nota Renderizar HTML é usado para <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">automação</a>. Em suma, tem uma nota de código HTML (opcionalmente com algum JavaScript) e esta nota irá renderizá-la. Para fazê-lo funcionar, deve definir uma <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relação</a> chamada \"renderNote\" que aponta para a nota HTML a ser renderizada."
},
"web_view": {
"web_view": "Web View",
"embed_websites": "Nota do tipo Visualização Web permite que incorpore sites no Trilium.",
"create_label": "Para começar, crie uma etiqueta com um endereço URL que deseja incorporar, por exemplo, #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Recarregar"
},
@@ -2174,7 +2165,6 @@
"delete_note": "Apagar nota..."
},
"pagination": {
"page_title": "Página {{startIndex}} - {{endIndex}}",
"total_notes": "{{count}} notas"
},
"collections": {

View File

@@ -1271,11 +1271,6 @@
"start_dragging_relations": "Comece arrastando as relações daqui e solte-as em outra nota.",
"cannot_match_transform": "Não foi possível combinar a transformação: {{transform}}"
},
"web_view": {
"web_view": "Web View",
"embed_websites": "Nota do tipo Visualização Web permite que você incorpore sites dentro do Trilium.",
"create_label": "Para começar, crie uma etiqueta com um endereço URL que deseja incorporar, por exemplo, #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Recarregar"
},
@@ -1996,10 +1991,6 @@
"drag_locked_title": "Bloqueado para edição",
"drag_locked_message": "Arrastar não é permitido pois a coleção está bloqueada para edição."
},
"render": {
"note_detail_render_help_1": "Esta nota de ajuda é mostrada porque esta nota do tipo Renderizar HTML não possui a relação necessária para funcionar corretamente.",
"note_detail_render_help_2": "O tipo de nota Renderizar HTML é usado para <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">automação</a>. Em suma, você tem uma nota de código HTML (opcionalmente com algum JavaScript) e esta nota irá renderizá-la. Para fazê-lo funcionar, você precisa definir uma <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relação</a> chamada \"renderNote\" apontando para a nota HTML a ser renderizada."
},
"etapi": {
"title": "ETAPI",
"description": "ETAPI é uma API REST usada para acessar a instância do Trilium programaticamente, sem interface gráfica.",
@@ -2124,7 +2115,6 @@
"shared_locally": "Esta nota é compartilhada localmente em {{- link}}."
},
"pagination": {
"page_title": "Página de {{startIndex}} - {{endIndex}}",
"total_notes": "{{count}} notas"
},
"collections": {

View File

@@ -1094,10 +1094,6 @@
"rename_relation_from": "Redenumește relația din",
"to": "În"
},
"render": {
"note_detail_render_help_1": "Această notă informativă este afișată deoarece această notiță de tip „Randare HTML” nu are relația necesară pentru a funcționa corespunzător.",
"note_detail_render_help_2": "Notița de tipul „Render HTML” este utilizată pentru <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scriptare</a>. Pe scurt, se folosește o notiță de tip cod HTML (opțional cu niște JavaScript) și această notiță o va randa. Pentru a funcționa, trebuie definită o <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relație</a> denumită „renderNote” ce indică notița HTML de randat."
},
"revisions": {
"confirm_delete": "Doriți ștergerea acestei revizii?",
"confirm_delete_all": "Doriți ștergerea tuturor reviziilor acestei notițe?",
@@ -1376,11 +1372,6 @@
"enable_vim_keybindings": "Permite utilizarea combinațiilor de taste în stil Vim pentru notițele de tip cod (fără modul ex)",
"use_vim_keybindings_in_code_notes": "Combinații de taste Vim"
},
"web_view": {
"create_label": "Pentru a începe, creați o etichetă cu adresa URL de încorporat, e.g. #webViewSrc=\"https://www.google.com\"",
"embed_websites": "Notițele de tip „Vizualizare web” permit încorporarea site-urilor web în Trilium.",
"web_view": "Vizualizare web"
},
"wrap_lines": {
"enable_line_wrap": "Activează trecerea automată pe rândul următor (poate necesita o reîncărcare a interfeței pentru a avea efect)",
"wrap_lines_in_code_notes": "Trecerea automată pe rândul următor în notițe de cod"
@@ -2160,7 +2151,6 @@
"percentage": "%"
},
"pagination": {
"page_title": "Pagina pentru {{startIndex}} - {{endIndex}}",
"total_notes": "{{count}} notițe"
},
"collections": {

View File

@@ -668,7 +668,8 @@
"geo-map": {
"unable-to-load-map": "Не удалось загрузить карту.",
"create-child-note-instruction": "Щелкните по карте, чтобы создать новую заметку в этом месте, или нажмите Escape, чтобы закрыть ее.",
"create-child-note-title": "Создать новую дочернюю заметку и добавить ее на карту"
"create-child-note-title": "Создать новую дочернюю заметку и добавить ее на карту",
"create-child-note-text": "Добавить маркер"
},
"note_tooltip": {
"quick-edit": "Быстрое редактирование",
@@ -685,8 +686,8 @@
"electron_integration": {
"zoom-factor": "Коэффициент масштабирования",
"restart-app-button": "Применить изменения и перезапустить приложение",
"background-effects-description": "Эффект Mica добавляет размытый, стильный фон окнам приложений, создавая глубину и современный вид. Опция \"Системная строка заголовка\" должна быть отключена.",
"background-effects": "Включить фоновые эффекты (только Windows 11)",
"background-effects-description": "Добавляет размытый, стильный фон окнам приложений, создавая глубину и современный вид. Опция \"Системная строка заголовка\" должна быть отключена.",
"background-effects": "Включить фоновые эффекты",
"native-title-bar-description": "В Windows и macOS отключение системной строки заголовка делает приложение более компактным. В Linux включение системной строки заголовка улучшает интеграцию с остальной частью системы.",
"native-title-bar": "Системная панель заголовка",
"desktop-application": "Десктопное приложение"
@@ -776,7 +777,11 @@
"refresh-saved-search-results": "Обновить сохраненные результаты поиска",
"automatically-collapse-notes-title": "Заметки будут свернуты после определенного периода бездействия, чтобы навести порядок в дереве.",
"toggle-sidebar": "Переключить боковую панель",
"dropping-not-allowed": "Перетаскивание заметок в эту область не разрешено."
"dropping-not-allowed": "Перетаскивание заметок в эту область не разрешено.",
"shared-indicator-tooltip": "Эта заметка опубликована",
"shared-indicator-tooltip-with-url": "Эта заметка доступно публично по адресу: {{- url}}",
"subtree-hidden-moved-description-other": "В дереве, к которому относится эта заметка, скрыты дочерние заметки.",
"subtree-hidden-moved-description-collection": "Эта коллекция скрывает свои дочерние заметки в дереве."
},
"quick-search": {
"no-results": "Результаты не найдены",
@@ -856,7 +861,10 @@
"convert-to-attachment-confirm": "Вы уверены, что хотите преобразовать выбранные заметки во вложения их родительских заметок? Эта операция применяется только к заметкам в виде изображений; другие заметки будут пропущены.",
"converted-to-attachments": "{{count}} заметок были преобразованы во вложения.",
"archive": "Архивировать",
"unarchive": "Разархивировать"
"unarchive": "Разархивировать",
"open-in-a-new-window": "Открыть в новом окне",
"hide-subtree": "Скрыть поддерево",
"show-subtree": "Показать поддерево"
},
"info": {
"closeButton": "Закрыть",
@@ -1000,7 +1008,8 @@
"switch_to_mobile_version": "Перейти на мобильную версию",
"switch_to_desktop_version": "Переключиться на версию для ПК",
"new-version-available": "Доступно обновление",
"download-update": "Обновить до {{latestVersion}}"
"download-update": "Обновить до {{latestVersion}}",
"search_notes": "Поиск заметок"
},
"zpetne_odkazy": {
"relation": "отношение",
@@ -1047,7 +1056,8 @@
"expand_all_levels": "Развернуть все вложенные уровни",
"expand_nth_level": "Развернуть уровни: {{depth}} шт.",
"expand_first_level": "Развернуть прямые дочерние уровни",
"expand_tooltip": "Разщвернуть дочерние элементы этой коллекции (на один уровень вложенности). Для получения дополнительных параметров нажмите стрелку справа."
"expand_tooltip": "Разщвернуть дочерние элементы этой коллекции (на один уровень вложенности). Для получения дополнительных параметров нажмите стрелку справа.",
"hide_child_notes": "Скрыть дочерние заметки в дереве"
},
"edited_notes": {
"deleted": "(удалено)",
@@ -1692,7 +1702,7 @@
"zoom_in_title": "Увеличить масштаб",
"zoom_out_title": "Уменьшить масштаб",
"reset_pan_zoom_title": "Сбросить панорамирование и масштабирование",
"create_child_note_title": "Создать новую дочернюю заметку и добавить ее в эту карту связей"
"create_child_note_title": "Создать дочернюю заметку и добавить ее в карту"
},
"code_auto_read_only_size": {
"unit": "символов",
@@ -1845,7 +1855,8 @@
"error_cannot_get_branch_id": "Невозможно получить branchId для notePath '{{notePath}}'",
"delete_this_note": "Удалить эту заметку",
"insert_child_note": "Вставить дочернюю заметку",
"note_revisions": "История изменений"
"note_revisions": "История изменений",
"content_language_switcher": "Язык содержимого: {{language}}"
},
"svg_export_button": {
"button_title": "Экспортировать диаграмму как SVG"
@@ -1900,7 +1911,7 @@
"dismiss": "Отклонить",
"background_effects_button": "Включить эффекты фона",
"next_theme_button": "Попробовать новую тему",
"background_effects_message": "На устройствах Windows фоновые эффекты теперь полностью стабильны. Они добавляют цвет в пользовательский интерфейс, размывая фон за ним. Этот приём также используется в других приложениях, например, в проводнике Windows.",
"background_effects_message": "На устройствах с ОС Windows или macOS, фоновые эффекты теперь полностью стабильны. Они добавляют цвета в пользовательский интерфейс, размывая фон за ним.",
"background_effects_title": "Фоновые эффекты теперь стабильны",
"next_theme_title": "Попробуйте новую тему Trilium",
"new_layout_button": "Подробнее",
@@ -1988,11 +1999,6 @@
"attachment_deleted": "Это вложение было удалено.",
"you_can_also_open": ", вы также можете открыть "
},
"web_view": {
"web_view": "Веб-страница",
"create_label": "Для начала создайте метку с URL-адресом, который вы хотите встроить, например, #webViewSrc=\"https://www.google.com\"",
"embed_websites": "Заметки типа \"Веб-страница\" позволяет встраивать веб-сайты в Trilium."
},
"ribbon": {
"widgets": "Виджеты ленты",
"promoted_attributes_message": "Вкладка \"Продвигаемые атрибуты\" будет автоматически открыта, если таковые атрибуты установлены у заметки",
@@ -2075,10 +2081,6 @@
"help-button": {
"title": "Открыть соответствующую страницу справки"
},
"render": {
"note_detail_render_help_2": "Тип заметки «Рендер HTML» используется для <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">скриптинга</a>. Если коротко, у вас есть заметка с HTML-кодом (возможно, с добавлением JavaScript), и эта заметка её отобразит. Для этого необходимо определить <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">отношение</a> с именем «renderNote», указывающее на HTML-заметку для отрисовки.",
"note_detail_render_help_1": "Эта справочная заметка отображается, поскольку эта справка типа Render HTML не имеет необходимой связи для правильной работы."
},
"file": {
"too_big": "В целях повышения производительности в режиме предварительного просмотра отображаются только первые {{maxNumChars}} символов файла. Загрузите файл и откройте его во внешнем браузере, чтобы увидеть всё содержимое.",
"file_preview_not_available": "Предварительный просмотр файла недоступен для этого файла."
@@ -2094,7 +2096,11 @@
"ui": "Пользовательский интерфейс"
},
"sql_result": {
"no_rows": "По этому запросу не возвращено ни одной строки"
"no_rows": "По этому запросу не возвращено ни одной строки",
"not_executed": "Запрос еще не выполнен.",
"failed": "Выполнение SQL-запроса завершилось с ошибкой",
"statement_result": "Результат заявления",
"execute_now": "Выполнить сейчас"
},
"editable_code": {
"placeholder": "Введите содержимое для заметки с кодом..."
@@ -2144,8 +2150,7 @@
"rendering_error": "Невозможно отобразить содержимое из-за ошибки."
},
"pagination": {
"total_notes": "{{count}} заметок",
"page_title": "Страница {{startIndex}} - {{endIndex}}"
"total_notes": "{{count}} заметок"
},
"status_bar": {
"attributes_one": "{{count}} атрибут",
@@ -2189,7 +2194,14 @@
"read_only_auto_description": "Эта заметка была автоматически переведена в режим только для чтения по соображениям производительности. Это автоматическое ограничение можно изменить в настройках.\n\nНажмите, чтобы временно отредактировать её.",
"read_only_auto": "Автоматический режим \"только для чтения\"",
"read_only_explicit_description": "Эта заметка была вручную установлена в режим «только для чтения».\nНажмите, чтобы временно отредактировать её.",
"read_only_explicit": "Только для чтения"
"read_only_explicit": "Только для чтения",
"save_status_saving": "Сохранение...",
"save_status_saved": "Сохранение",
"save_status_unsaved": "Не сохранено",
"save_status_error": "Ошибка сохранения",
"save_status_saving_tooltip": "Изменения сохраняются.",
"save_status_unsaved_tooltip": "Есть несохраненные изменения. Они будут сохранены автоматически через некоторое время.",
"save_status_error_tooltip": "Произошла ошибка при сохранении заметки. Если возможно, попробуйте скопировать содержимое заметки в другое место и перезагрузить приложение."
},
"breadcrumb": {
"hoisted_badge_title": "Снять фокус",
@@ -2243,5 +2255,30 @@
},
"attributes_panel": {
"title": "Атрибуты заметки"
},
"bookmark_buttons": {
"bookmarks": "Закладки"
},
"mobile_tab_switcher": {
"more_options": "Показать больше",
"title_one": "{{count}} вкладка",
"title_few": "{{count}} вкладки",
"title_many": "{{count}} вкладок"
},
"pdf": {
"pages_loading": "Загрузка...",
"pages_alt": "Страница {{pageNumber}}",
"pages_one": "{{count}} страница",
"pages_few": "{{count}} страницы",
"pages_many": "{{count}} страниц",
"layers_one": "{{count}} слой",
"layers_few": "{{count}} слоя",
"layers_many": "{{count}} слоев",
"attachments_one": "{{count}} вложение",
"attachments_few": "{{count}} вложения",
"attachments_many": "{{count}} вложений"
},
"platform_indicator": {
"available_on": "Доступно для {{platform}}"
}
}

View File

@@ -662,7 +662,8 @@
"show-cheatsheet": "顯示快捷鍵說明",
"toggle-zen-mode": "禪模式",
"new-version-available": "發現新更新",
"download-update": "取得版本 {{latestVersion}}"
"download-update": "取得版本 {{latestVersion}}",
"search_notes": "搜尋筆記"
},
"sync_status": {
"unknown": "<p>同步狀態將在下一次同步嘗試開始後顯示。</p><p>點擊以立即觸發同步。</p>",
@@ -758,7 +759,8 @@
"error_cannot_get_branch_id": "無法獲取 notePath '{{notePath}}' 的 branchId",
"error_unrecognized_command": "無法識別的命令 {{command}}",
"note_revisions": "筆記歷史版本",
"backlinks": "反向連結"
"backlinks": "反向連結",
"content_language_switcher": "內文語言:{{language}}"
},
"note_icon": {
"change_note_icon": "更改筆記圖標",
@@ -910,7 +912,8 @@
"unknown_search_option": "未知的搜尋選項 {{searchOptionName}}",
"search_note_saved": "搜尋筆記已儲存至 {{- notePathTitle}}",
"actions_executed": "已執行操作。",
"view_options": "查看選項:"
"view_options": "查看選項:",
"option": "選項"
},
"similar_notes": {
"title": "相似筆記",
@@ -1004,7 +1007,7 @@
"no_attachments": "此筆記沒有附件。"
},
"book": {
"no_children_help": "此類型為書籍的筆記沒有任何子筆記,因此沒有內容可顯示。請參閱 <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> 以了解詳情。",
"no_children_help": "此集合沒有任何子筆記,因此沒有內容可顯示。",
"drag_locked_title": "鎖定編輯",
"drag_locked_message": "無法拖曳,因為此集合已被鎖定編輯。"
},
@@ -1060,15 +1063,6 @@
"default_new_note_title": "新筆記",
"click_on_canvas_to_place_new_note": "點擊畫布以放置新筆記"
},
"render": {
"note_detail_render_help_1": "之所以顯示此說明筆記,是因為該類型的渲染 HTML 沒有設定好必須的關聯。",
"note_detail_render_help_2": "渲染筆記類型用於編寫 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">腳本</a>。簡單說就是您可以寫HTML程式碼或者加上一些JavaScript程式碼 然後這個筆記會把頁面渲染出來。要使其正常工作,您需要定義一個名為 \"renderNote\" 的 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">關聯</a> 指向要呈現的 HTML 筆記。"
},
"web_view": {
"web_view": "網頁顯示",
"embed_websites": "網頁顯示類型的筆記允許您將網站嵌入至 Trilium 中。",
"create_label": "首先,請新增一個帶有您要嵌入的 URL 地址的標籤,例如 #webViewSrc=\"https://www.bing.com\""
},
"backend_log": {
"refresh": "重新整理"
},
@@ -1379,7 +1373,8 @@
"description": "描述",
"reload_app": "重新載入應用以套用更改",
"set_all_to_default": "將所有快捷鍵重設為預設值",
"confirm_reset": "您確定要將所有鍵盤快捷鍵重設為預設值嗎?"
"confirm_reset": "您確定要將所有鍵盤快捷鍵重設為預設值嗎?",
"no_results": "未找到符合 '{{filter}}' 的捷徑"
},
"spellcheck": {
"title": "拼寫檢查",
@@ -1583,7 +1578,9 @@
"print_report_collection_content_one": "集合中的 {{count}} 篇筆記無法列印,因為它們不被支援或受到保護。",
"print_report_collection_content_other": "",
"print_report_collection_details_button": "查看詳情",
"print_report_collection_details_ignored_notes": "忽略的筆記"
"print_report_collection_details_ignored_notes": "忽略的筆記",
"print_report_error_title": "列印失敗",
"print_report_stack_trace": "堆棧追蹤"
},
"note_title": {
"placeholder": "請輸入筆記標題...",
@@ -2077,7 +2074,8 @@
"raster": "柵格",
"vector_light": "向量(淺色)",
"vector_dark": "向量(深色)",
"show-scale": "顯示比例尺"
"show-scale": "顯示比例尺",
"show-labels": "顯示標記名稱"
},
"table_context_menu": {
"delete_row": "刪除列"
@@ -2156,7 +2154,6 @@
"app-restart-required": "(需要重啟程式以套用更改)"
},
"pagination": {
"page_title": "第 {{startIndex}} - {{endIndex}} 頁",
"total_notes": "{{count}} 筆記"
},
"collections": {
@@ -2272,9 +2269,57 @@
},
"mobile_tab_switcher": {
"more_options": "更多選項",
"title_one": "{{count}} 個分頁"
"title_one": "{{count}} 個分頁",
"title_other": ""
},
"platform_indicator": {
"available_on": "可於 {{platform}} 使用"
},
"bookmark_buttons": {
"bookmarks": "書籤"
},
"render": {
"setup_title": "在此筆記中顯示自訂 HTML 或 Preact JSX",
"setup_create_sample_preact": "使用 Preact 建立範例筆記",
"setup_create_sample_html": "使用 HTML 建立範例筆記",
"setup_sample_created": "已建立一個範例筆記作為子筆記。",
"disabled_description": "此渲染筆記來自外部來源。為保護您免受惡意內容侵害,此功能預設為停用狀態。啟用前請務必確認來源可信。",
"disabled_button_enable": "啟用渲染筆記"
},
"web_view_setup": {
"title": "將網頁直接匯入 Trilium 建立即時預覽",
"url_placeholder": "輸入或貼上網站網址,例如 https://triliumnotes.org",
"create_button": "建立網頁檢視",
"invalid_url_title": "無效地址",
"invalid_url_message": "請輸入有效的網址,例如 https://triliumnotes.org。",
"disabled_description": "此網頁檢視來自外部來源。為協助保護您免受網路釣魚或惡意內容侵害,內容不會自動載入。若您信任來源,可手動啟用此功能。",
"disabled_button_enable": "啟用網頁檢視"
},
"active_content_badges": {
"type_icon_pack": "圖示包",
"type_backend_script": "後端腳本",
"type_frontend_script": "前端腳本",
"type_widget": "元件",
"type_app_css": "自訂 CSS",
"type_render_note": "渲染筆記",
"type_web_view": "網頁顯示",
"type_app_theme": "自訂主題",
"toggle_tooltip_enable_tooltip": "點擊以啟用此 {{type}}。",
"toggle_tooltip_disable_tooltip": "點擊以停用此 {{type}}。",
"menu_docs": "打開文件",
"menu_execute_now": "立即執行腳本",
"menu_run": "自動執行",
"menu_run_disabled": "手動",
"menu_run_backend_startup": "當後端啟動時",
"menu_run_hourly": "每小時",
"menu_run_daily": "每日",
"menu_run_frontend_startup": "當桌面前端啟動時",
"menu_run_mobile_startup": "當移動前端啟動時",
"menu_change_to_widget": "更改為元件",
"menu_change_to_frontend_script": "更改為前端腳本",
"menu_theme_base": "主題基底"
},
"setup_form": {
"more_info": "了解更多"
}
}

View File

@@ -1130,15 +1130,6 @@
"default_new_note_title": "нова нотатка",
"click_on_canvas_to_place_new_note": "Натисніть на полотно, щоб розмістити нову нотатку"
},
"render": {
"note_detail_render_help_1": "Ця довідка відображається, оскільки ця нотатка типу Render HTML не має необхідного зв'язку для належного функціонування.",
"note_detail_render_help_2": "Тип нотатки Render HTML використовується для <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">скриптів</a>. Коротше кажучи, у вас є нотатка з HTML-кодом (за бажанням з деяким JavaScript), і ця нотатка її відобразить. Щоб це запрацювало, вам потрібно визначити <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">відношення</a> під назвою \"renderNote\", яке вказує на нотатку HTML для відображення."
},
"web_view": {
"web_view": "Веб-перегляд",
"embed_websites": "Нотатка типу Веб-перегляд дозволяє вбудовувати веб-сайти в Trilium.",
"create_label": "Для початку створіть мітку з URL-адресою, яку ви хочете вбудувати, наприклад, #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Оновити"
},
@@ -2072,7 +2063,6 @@
"app-restart-required": "(щоб зміни набули чинності, потрібен перезапуск програми)"
},
"pagination": {
"page_title": "Сторінка {{startIndex}} - {{endIndex}}",
"total_notes": "{{count}} нотаток"
},
"collections": {

View File

@@ -1,4 +1,4 @@
import { BootstrapDefinition } from "@triliumnext/commons";
import { IconRegistry, Locale } from "@triliumnext/commons";
import appContext, { AppContext } from "./components/app_context";
import type FNote from "./entities/fnote";
@@ -15,9 +15,10 @@ interface ElectronProcess {
platform: string;
}
interface CustomGlobals extends BootstrapDefinition {
interface CustomGlobals {
isDesktop: typeof utils.isDesktop;
isMobile: typeof utils.isMobile;
device: "mobile" | "desktop" | "print";
getComponentByEl: typeof appContext.getComponentByEl;
getHeaders: typeof server.getHeaders;
getReferenceLinkTitle: (href: string) => Promise<string>;
@@ -30,7 +31,32 @@ interface CustomGlobals extends BootstrapDefinition {
SEARCH_HELP_TEXT: string;
activeDialog: JQuery<HTMLElement> | null;
componentId: string;
csrfToken: string;
baseApiUrl: string;
isProtectedSessionAvailable: boolean;
isDev: boolean;
isMainWindow: boolean;
maxEntityChangeIdAtLoad: number;
maxEntityChangeSyncIdAtLoad: number;
assetPath: string;
appPath: string;
instanceName: string;
appCssNoteIds: string[];
triliumVersion: string;
TRILIUM_SAFE_MODE: boolean;
platform?: typeof process.platform;
linter: typeof lint;
hasNativeTitleBar: boolean;
hasBackgroundEffects: boolean;
isElectron: boolean;
isRtl: boolean;
iconRegistry: IconRegistry;
themeCssUrl: string;
themeUseNextAsBase?: "next" | "next-light" | "next-dark";
iconPackCss: string;
headingStyle: "plain" | "underline" | "markdown";
layoutOrientation: "vertical" | "horizontal";
currentLocale: Locale;
}
type RequireMethod = (moduleName: string) => any;

View File

@@ -1,6 +1,7 @@
import "./NoteDetail.css";
import clsx from "clsx";
import { note } from "mermaid/dist/rendering-util/rendering-elements/shapes/note.js";
import { isValidElement, VNode } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
@@ -355,6 +356,14 @@ export function checkFullHeight(noteContext: NoteContext | undefined, type: Exte
// https://github.com/zadam/trilium/issues/2522
const isBackendNote = noteContext?.noteId === "_backendLog";
const isFullHeightNoteType = type && TYPE_MAPPINGS[type].isFullHeight;
// Allow vertical centering when there are no results.
if (type === "book" &&
[ "grid", "list" ].includes(noteContext.note?.getLabelValue("viewType") ?? "grid") &&
!noteContext.note?.hasChildren()) {
return true;
}
return (!noteContext?.hasNoteList() && isFullHeightNoteType)
|| noteContext?.viewScope?.viewMode === "attachments"
|| isBackendNote;
@@ -370,7 +379,33 @@ function showToast(type: "printing" | "exporting_pdf", progress: number = 0) {
}
function handlePrintReport(printReport?: PrintReport) {
if (printReport?.type === "collection" && printReport.ignoredNoteIds.length > 0) {
if (!printReport) return;
if (printReport.type === "error") {
toast.showPersistent({
id: "print-error",
icon: "bx bx-error-circle",
title: t("note_detail.print_report_error_title"),
message: printReport.message,
buttons: printReport.stack ? [
{
text: t("note_detail.print_report_collection_details_button"),
onClick(api) {
api.dismissToast();
dialog.info(<>
<p>{printReport.message}</p>
<details>
<summary>{t("note_detail.print_report_stack_trace")}</summary>
<pre style="font-size: 0.85em; overflow-x: auto;">{printReport.stack}</pre>
</details>
</>, {
title: t("note_detail.print_report_error_title")
});
}
}
] : undefined
});
} else if (printReport.type === "collection" && printReport.ignoredNoteIds.length > 0) {
toast.showPersistent({
id: "print-report",
icon: "bx bx-collection",

View File

@@ -16,6 +16,10 @@ body.mobile .promoted-attributes-widget {
display: table;
}
body.experimental-feature-new-layout .promoted-attributes-container {
max-height: unset;
}
.promoted-attribute-cell {
display: flex;
align-items: center;
@@ -94,4 +98,4 @@ body.mobile .promoted-attributes-widget {
background: rgba(0, 0, 0, 0.5);
transform: rotate(45deg);
pointer-events: none;
}
}

View File

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

View File

@@ -8,7 +8,7 @@ import { CommandNames } from "../../components/app_context";
import Component from "../../components/component";
import { ExperimentalFeature, ExperimentalFeatureId, experimentalFeatures, isExperimentalFeatureEnabled, toggleExperimentalFeature } from "../../services/experimental_features";
import { t } from "../../services/i18n";
import utils, { dynamicRequire, isElectron, isMobile, isStandalone, reloadFrontendApp } from "../../services/utils";
import utils, { dynamicRequire, isElectron, isMobile, reloadFrontendApp } from "../../services/utils";
import Dropdown from "../react/Dropdown";
import { FormDropdownDivider, FormDropdownSubmenu, FormListHeader, FormListItem } from "../react/FormList";
import { useStaticTooltip, useStaticTooltipWithKeyboardShortcut, useTriliumOption, useTriliumOptionBool, useTriliumOptionInt } from "../react/hooks";
@@ -250,7 +250,7 @@ function ToggleWindowOnTop() {
function useTriliumUpdateStatus() {
const [ latestVersion, setLatestVersion ] = useState<string>();
const [ checkForUpdates ] = useTriliumOptionBool("checkForUpdates");
const isUpdateAvailable = utils.isUpdateAvailable(latestVersion, window.glob.triliumVersion);
const isUpdateAvailable = utils.isUpdateAvailable(latestVersion, glob.triliumVersion);
async function updateVersionStatus() {
const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Trilium/releases/latest";
@@ -268,7 +268,7 @@ function useTriliumUpdateStatus() {
}
useEffect(() => {
if (!checkForUpdates || !isStandalone) {
if (!checkForUpdates) {
setLatestVersion(undefined);
return;
}

View File

@@ -2,8 +2,12 @@
min-height: 0;
max-width: var(--max-content-width); /* Inherited from .note-split */
overflow: auto;
overflow: visible;
contain: none !important;
&.full-height {
overflow: auto;
}
}
body.prefers-centered-content .note-list-widget:not(.full-height) {
@@ -19,14 +23,3 @@ body.prefers-centered-content .note-list-widget:not(.full-height) {
.note-list-widget video {
height: 100%;
}
/* #region Pagination */
.note-list-pager {
font-size: 1rem;
span.current-page {
text-decoration: underline;
font-weight: bold;
}
}
/* #endregion */

View File

@@ -0,0 +1,88 @@
:where(.note-list-pager) {
--note-list-pager-page-button-width: 40px;
--note-list-pager-page-button-gap: 3px;
--note-list-pager-ellipsis-width: 20px;
--note-list-pager-justify-content: flex-end;
--note-list-pager-current-page-button-background-color: var(--button-group-active-button-background);
--note-list-pager-current-page-button-text-color: var(--button-group-active-button-text-color);
}
.note-list-pager-container {
display: flex;
flex-direction: column;
width: 100%;
container: note-list-pager / inline-size;
}
.note-list-pager {
display: flex;
align-items: center;
font-size: .8rem;
align-self: var(--note-list-pager-justify-content);
.note-list-pager-nav-button {
--icon-button-icon-ratio: .75;
}
.note-list-pager-page-button-container {
display: flex;
align-items: baseline;
justify-content: space-around;
gap: var(--note-list-pager-page-button-gap);
&.note-list-pager-ellipsis-present {
/* Prevent the prev/next buttons from shifting when ellipses appear or disappear */
--_gap-max-width: calc((var(--note-list-pager-page-button-count) + 2) * var(--note-list-pager-page-button-gap));
min-width: calc(var(--note-list-pager-page-button-count) * var(--note-list-pager-page-button-width)
+ (var(--note-list-pager-ellipsis-width) * 2)
+ var(--_gap-max-width));
}
.note-list-pager-page-button {
min-width: var(--note-list-pager-page-button-width);
padding-inline: 0;
padding-block: 4px;
&.note-list-pager-page-button-current {
background: var(--note-list-pager-current-page-button-background-color);
color: var(--note-list-pager-current-page-button-text-color);
font-weight: bold;
opacity: unset;
}
}
.note-list-pager-ellipsis {
display: inline-block;
width: var(--note-list-pager-ellipsis-width);
text-align: center;
opacity: .5;
}
}
.note-list-pager-narrow-counter {
display: none;
min-width: 60px;
text-align: center;
white-space: nowrap;
}
.note-list-pager-total-count {
margin-inline-start: 8px;
opacity: .5;
white-space: nowrap;
}
@container note-list-pager (max-width: 550px) {
.note-list-pager-page-button-container,
.note-list-pager-total-count {
display: none;
}
.note-list-pager-narrow-counter {
display: block;
}
}
}

View File

@@ -4,6 +4,10 @@ import FNote from "../../entities/fnote";
import froca from "../../services/froca";
import { useNoteLabelInt } from "../react/hooks";
import { t } from "../../services/i18n";
import ActionButton from "../react/ActionButton";
import Button from "../react/Button";
import "./Pagination.css";
import clsx from "clsx";
interface PaginationContext {
page: number;
@@ -17,46 +21,106 @@ interface PaginationContext {
export function Pager({ page, pageSize, setPage, pageCount, totalNotes }: Omit<PaginationContext, "pageNotes">) {
if (pageCount < 2) return;
let lastPrinted = false;
let children: ComponentChildren[] = [];
for (let i = 1; i <= pageCount; i++) {
if (pageCount < 20 || i <= 5 || pageCount - i <= 5 || Math.abs(page - i) <= 2) {
lastPrinted = true;
const startIndex = (i - 1) * pageSize + 1;
const endIndex = Math.min(totalNotes, i * pageSize);
if (i !== page) {
children.push((
<a
href="javascript:"
title={t("pagination.page_title", { startIndex, endIndex })}
onClick={() => setPage(i)}
>
{i}
</a>
))
} else {
// Current page
children.push(<span className="current-page">{i}</span>)
}
children.push(<>{" "}&nbsp;{" "}</>);
} else if (lastPrinted) {
children.push(<>{"... "}&nbsp;{" "}</>);
lastPrinted = false;
}
}
return (
<div class="note-list-pager">
{children}
<div className="note-list-pager-container">
<div className="note-list-pager">
<ActionButton
icon="bx bx-chevron-left"
className="note-list-pager-nav-button"
disabled={(page === 1)}
text={t("pagination.prev_page")}
onClick={() => setPage(page - 1)}
/>
<span className="note-list-pager-total-count">({t("pagination.total_notes", { count: totalNotes })})</span>
<PageButtons page={page} setPage={setPage} pageCount={pageCount} />
<div className="note-list-pager-narrow-counter">
<strong>{page}</strong> / <strong>{pageCount}</strong>
</div>
<ActionButton
icon="bx bx-chevron-right"
className="note-list-pager-nav-button"
disabled={(page === pageCount)}
text={t("pagination.next_page")}
onClick={() => setPage(page + 1)}
/>
<div className="note-list-pager-total-count">
{t("pagination.total_notes", { count: totalNotes })}
</div>
</div>
</div>
)
}
interface PageButtonsProps {
page: number;
setPage: Dispatch<StateUpdater<number>>;
pageCount: number;
}
function PageButtons(props: PageButtonsProps) {
const maxButtonCount = 9;
const maxLeftRightSegmentLength = 2;
// The left-side segment
const leftLength = Math.min(props.pageCount, maxLeftRightSegmentLength);
const leftStart = 1;
// The middle segment
const middleMaxLength = maxButtonCount - maxLeftRightSegmentLength * 2;
const middleLength = Math.min(props.pageCount - leftLength, middleMaxLength);
let middleStart = props.page - Math.floor(middleLength / 2);
middleStart = Math.max(middleStart, leftLength + 1);
// The right-side segment
const rightLength = Math.min(props.pageCount - (middleLength + leftLength), maxLeftRightSegmentLength);
const rightStart = props.pageCount - rightLength + 1;
middleStart = Math.min(middleStart, rightStart - middleLength);
const totalButtonCount = leftLength + middleLength + rightLength;
const hasLeadingEllipsis = (middleStart - leftLength > 1);
const hasTrailingEllipsis = (rightStart - (middleStart + middleLength - 1) > 1);
return <div className={clsx("note-list-pager-page-button-container", {
"note-list-pager-ellipsis-present": (totalButtonCount === maxButtonCount)
})}
style={{"--note-list-pager-page-button-count": totalButtonCount}}>
{[
...createSegment(leftStart, leftLength, props.page, props.setPage, false),
...createSegment(middleStart, middleLength, props.page, props.setPage, hasLeadingEllipsis),
...createSegment(rightStart, rightLength, props.page, props.setPage, hasTrailingEllipsis),
]}
</div>;
}
function createSegment(start: number, length: number, currentPage: number, setPage: Dispatch<StateUpdater<number>>, prependEllipsis: boolean): ComponentChildren[] {
const children: ComponentChildren[] = [];
if (prependEllipsis) {
children.push(<span className="note-list-pager-ellipsis">...</span>);
}
for (let i = 0; i < length; i++) {
const pageNum = start + i;
const isCurrent = (pageNum === currentPage);
children.push((
<Button
text={pageNum.toString()}
kind="lowProfile"
className={clsx(
"note-list-pager-page-button",
{"note-list-pager-page-button-current": isCurrent}
)}
disabled={isCurrent}
onClick={() => setPage(pageNum)}
/>
));
}
return children;
}
export function usePagination(note: FNote, noteIds: string[]): PaginationContext {
const [ page, setPage ] = useState(1);
const [ pageNotes, setPageNotes ] = useState<FNote[]>();

View File

@@ -7,13 +7,21 @@
> .collection-properties {
position: relative;
z-index: 2000;
z-index: 998;
}
}
body.mobile .geo-view > .collection-properties {
z-index: 2500;
}
.geo-map-container {
height: 100%;
overflow: hidden;
.maplibregl-canvas-container {
position: relative;
}
}
.leaflet-pane {
@@ -22,7 +30,7 @@
.leaflet-top,
.leaflet-bottom {
z-index: 997;
z-index: 997 !important;
}
.geo-view.placing-note .geo-map-container {

View File

@@ -22,7 +22,7 @@ import { ViewModeProps } from "../interface";
import { createNewNote, moveMarker } from "./api";
import openContextMenu, { openMapContextMenu } from "./context_menu";
import Map from "./map";
import { DEFAULT_MAP_LAYER_NAME } from "./map_layer";
import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS, MapLayer } from "./map_layer";
import Marker, { GpxTrack } from "./marker";
const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659];
@@ -45,10 +45,11 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
const [ state, setState ] = useState(State.Normal);
const [ coordinates, setCoordinates ] = useState(viewConfig?.view?.center);
const [ zoom, setZoom ] = useState(viewConfig?.view?.zoom);
const [ layerName ] = useNoteLabel(note, "map:style");
const [ hasScale ] = useNoteLabelBoolean(note, "map:scale");
const [ hideLabels ] = useNoteLabelBoolean(note, "map:hideLabels");
const [ isReadOnly ] = useNoteLabelBoolean(note, "readOnly");
const [ notes, setNotes ] = useState<FNote[]>([]);
const layerData = useLayerData(note);
const spacedUpdate = useSpacedUpdate(() => {
if (viewConfig) {
saveConfig(viewConfig);
@@ -152,7 +153,7 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
apiRef={apiRef} containerRef={containerRef}
coordinates={coordinates}
zoom={zoom}
layerName={layerName ?? DEFAULT_MAP_LAYER_NAME}
layerData={layerData}
viewportChanged={(coordinates, zoom) => {
if (!viewConfig) viewConfig = {};
viewConfig.view = { center: coordinates, zoom };
@@ -162,13 +163,35 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
onContextMenu={onContextMenu}
scale={hasScale}
>
{notes.map(note => <NoteWrapper note={note} isReadOnly={isReadOnly} />)}
{notes.map(note => <NoteWrapper note={note} isReadOnly={isReadOnly} hideLabels={hideLabels} />)}
</Map>}
<GeoMapTouchBar state={state} map={apiRef.current} />
</div>
);
}
function useLayerData(note: FNote) {
const [ layerName ] = useNoteLabel(note, "map:style");
// Memo is needed because it would generate unnecessary reloads due to layer change.
const layerData = useMemo(() => {
// Custom layers.
if (layerName?.startsWith("http")) {
return {
name: "Custom",
type: "raster",
url: layerName,
attribution: ""
} satisfies MapLayer;
}
// Built-in layers.
const layerData = MAP_LAYERS[layerName ?? ""] ?? MAP_LAYERS[DEFAULT_MAP_LAYER_NAME];
return layerData;
}, [ layerName ]);
return layerData;
}
function ToggleReadOnlyButton({ note }: { note: FNote }) {
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
@@ -179,22 +202,26 @@ function ToggleReadOnlyButton({ note }: { note: FNote }) {
/>;
}
function NoteWrapper({ note, isReadOnly }: { note: FNote, isReadOnly: boolean }) {
function NoteWrapper({ note, isReadOnly, hideLabels }: {
note: FNote,
isReadOnly: boolean,
hideLabels: boolean
}) {
const mime = useNoteProperty(note, "mime");
const [ location ] = useNoteLabel(note, LOCATION_ATTRIBUTE);
if (mime === "application/gpx+xml") {
return <NoteGpxTrack note={note} />;
return <NoteGpxTrack note={note} hideLabels={hideLabels} />;
}
if (location) {
const latLng = location?.split(",", 2).map((el) => parseFloat(el)) as [ number, number ] | undefined;
if (!latLng) return;
return <NoteMarker note={note} editable={!isReadOnly} latLng={latLng} />;
return <NoteMarker note={note} editable={!isReadOnly} latLng={latLng} hideLabels={hideLabels} />;
}
}
function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean, latLng: [number, number] }) {
function NoteMarker({ note, editable, latLng, hideLabels }: { note: FNote, editable: boolean, latLng: [number, number], hideLabels: boolean }) {
// React to changes
const [ color ] = useNoteLabel(note, "color");
const [ iconClass ] = useNoteLabel(note, "iconClass");
@@ -202,8 +229,9 @@ function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean
const title = useNoteProperty(note, "title");
const icon = useMemo(() => {
return buildIcon(note.getIcon(), note.getColorClass() ?? undefined, title, note.noteId, archived);
}, [ iconClass, color, title, note.noteId, archived]);
const titleOrNone = hideLabels ? undefined : title;
return buildIcon(note.getIcon(), note.getColorClass() ?? undefined, titleOrNone, note.noteId, archived);
}, [ iconClass, color, title, note.noteId, archived, hideLabels ]);
const onClick = useCallback(() => {
appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId });
@@ -235,7 +263,7 @@ function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean
/>;
}
function NoteGpxTrack({ note }: { note: FNote }) {
function NoteGpxTrack({ note, hideLabels }: { note: FNote, hideLabels?: boolean }) {
const [ xmlString, setXmlString ] = useState<string>();
const blob = useNoteBlob(note);
@@ -256,7 +284,7 @@ function NoteGpxTrack({ note }: { note: FNote }) {
const options = useMemo<GPXOptions>(() => ({
markers: {
startIcon: buildIcon(note.getIcon(), note.getColorClass(), note.title),
startIcon: buildIcon(note.getIcon(), note.getColorClass(), hideLabels ? undefined : note.title),
endIcon: buildIcon("bxs-flag-checkered"),
wptIcons: {
"": buildIcon("bx bx-pin")
@@ -265,7 +293,7 @@ function NoteGpxTrack({ note }: { note: FNote }) {
polyline_options: {
color: note.getLabelValue("color") ?? "blue"
}
}), [ color, iconClass ]);
}), [ color, iconClass, hideLabels ]);
return xmlString && <GpxTrack gpxXmlString={xmlString} options={options} />;
}

View File

@@ -1,7 +1,7 @@
import { useEffect, useImperativeHandle, useRef, useState } from "preact/hooks";
import L, { control, LatLng, Layer, LeafletMouseEvent } from "leaflet";
import "leaflet/dist/leaflet.css";
import { MAP_LAYERS } from "./map_layer";
import { MAP_LAYERS, type MapLayer } from "./map_layer";
import { ComponentChildren, createContext, RefObject } from "preact";
import { useElementSize, useSyncedRef } from "../../react/hooks";
@@ -12,7 +12,7 @@ interface MapProps {
containerRef?: RefObject<HTMLDivElement>;
coordinates: LatLng | [number, number];
zoom: number;
layerName: string;
layerData: MapLayer;
viewportChanged: (coordinates: LatLng, zoom: number) => void;
children: ComponentChildren;
onClick?: (e: LeafletMouseEvent) => void;
@@ -21,7 +21,7 @@ interface MapProps {
scale: boolean;
}
export default function Map({ coordinates, zoom, layerName, viewportChanged, children, onClick, onContextMenu, scale, apiRef, containerRef: _containerRef, onZoom }: MapProps) {
export default function Map({ coordinates, zoom, layerData, viewportChanged, children, onClick, onContextMenu, scale, apiRef, containerRef: _containerRef, onZoom }: MapProps) {
const mapRef = useRef<L.Map>(null);
const containerRef = useSyncedRef<HTMLDivElement>(_containerRef);
@@ -49,8 +49,6 @@ export default function Map({ coordinates, zoom, layerName, viewportChanged, chi
const [ layer, setLayer ] = useState<Layer>();
useEffect(() => {
async function load() {
const layerData = MAP_LAYERS[layerName];
if (layerData.type === "vector") {
const style = (typeof layerData.style === "string" ? layerData.style : await layerData.style());
await import("@maplibre/maplibre-gl-leaflet");
@@ -68,7 +66,7 @@ export default function Map({ coordinates, zoom, layerName, viewportChanged, chi
}
load();
}, [ layerName ]);
}, [ layerData ]);
// Attach layer to the map.
useEffect(() => {
@@ -139,7 +137,7 @@ export default function Map({ coordinates, zoom, layerName, viewportChanged, chi
return (
<div
ref={containerRef}
className={`geo-map-container ${MAP_LAYERS[layerName].isDarkTheme ? "dark" : ""}`}
className={`geo-map-container ${layerData.isDarkTheme ? "dark" : ""}`}
>
<ParentMap.Provider value={mapRef.current}>
{children}

View File

@@ -1,20 +1,17 @@
export interface MapLayer {
name: string;
isDarkTheme?: boolean;
}
interface VectorLayer extends MapLayer {
export type MapLayer = ({
type: "vector";
style: string | (() => Promise<{}>)
}
interface RasterLayer extends MapLayer {
} | {
type: "raster";
url: string;
attribution: string;
}
}) & {
// Common properties
name: string;
isDarkTheme?: boolean;
};
export const MAP_LAYERS: Record<string, VectorLayer | RasterLayer> = {
export const MAP_LAYERS: Record<string, MapLayer> = {
"openstreetmap": {
name: "OpenStreetMap",
type: "raster",

View File

@@ -1,5 +1,5 @@
.note-list {
overflow: hidden;
overflow: visible;
position: relative;
height: 100%;
}
@@ -8,7 +8,6 @@
border-radius: 10px;
background-color: var(--accented-background-color);
padding: 10px 15px 15px 8px;
margin: 5px 5px 5px 5px;
overflow: hidden;
display: flex;
flex-direction: column;
@@ -100,27 +99,211 @@
overflow: auto;
}
.note-expander {
font-size: x-large;
position: relative;
top: 3px;
cursor: pointer;
/* #region List view */
@keyframes note-preview-show {
from {
opacity: 0;
} to {
opacity: 1;
}
}
.note-list-pager {
text-align: center;
.nested-note-list {
--card-nested-section-indent: 25px;
&.search-results {
--card-nested-section-indent: 32px;
}
}
.note-list.list-view .note-path {
margin-left: 0.5em;
vertical-align: middle;
opacity: 0.5;
/* List item */
.nested-note-list-item {
h5 {
display: flex;
align-items: center;
font-size: 1em;
font-weight: normal;
margin: 0;
}
.note-expander {
margin-inline-end: 4px;
font-size: x-large;
cursor: pointer;
}
.tn-icon {
margin-inline-end: 8px;
color: var(--note-list-view-icon-color);
font-size: 1.2em;
}
.note-book-title {
--link-hover-background: transparent;
--link-hover-color: currentColor;
color: inherit;
font-weight: normal;
}
.note-path {
margin-left: 0.5em;
vertical-align: middle;
opacity: 0.5;
}
.note-list-attributes {
flex-grow: 1;
margin-inline-start: 1em;
text-align: right;
font-size: .75em;
opacity: .75;
}
.nested-note-list-item-menu {
margin-inline-start: 8px;
flex-shrink: 0;
}
&.archived {
span.tn-icon + span,
.tn-icon {
opacity: .6;
}
}
&.use-note-color {
span.tn-icon + span,
.nested-note-list:not(.search-results) & .tn-icon,
.rendered-note-attributes {
color: var(--custom-color);
}
}
}
.nested-note-list:not(.search-results) h5 {
span.tn-icon + span,
.note-list-attributes {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
/* List item (search results view) */
.nested-note-list.search-results .nested-note-list-item {
span.tn-icon + span > span {
display: flex;
flex-direction: column-reverse;
align-items: flex-start;
}
small {
line-height: .85em;
}
.note-path {
margin-left: 0;
font-size: .85em;
line-height: .85em;
font-weight: 500;
letter-spacing: .5pt;
}
.tn-icon {
display: flex;
flex-shrink: 0;
justify-content: center;
align-items: center;
width: 1.75em;
height: 1.75em;
margin-inline-end: 12px;
border-radius: 50%;
background: var(--note-icon-custom-background-color, var(--note-list-view-large-icon-background));
font-size: 1.2em;
color: var(--note-icon-custom-color, var(--note-list-view-large-icon-color));
}
h5 .ck-find-result {
background: var(--note-list-view-search-result-highlight-background);
color: var(--note-list-view-search-result-highlight-color);
font-weight: 600;
text-decoration: underline;
}
}
/* Note content preview */
.nested-note-list .note-book-content {
display: none;
outline: 1px solid var(--note-list-view-content-background);
border-radius: 8px;
background-color: var(--note-list-view-content-background);
overflow: hidden;
user-select: text;
font-size: .85rem;
animation: note-preview-show .25s ease-out;
will-change: opacity;
&.note-book-content-ready {
display: block;
}
> .rendered-content > *:last-child {
margin-bottom: 0;
}
&.type-text {
padding: 8px 24px;
.ck-content > *:last-child {
margin-bottom: 0;
}
}
&.type-protectedSession {
padding: 20px;
}
&.type-image {
padding: 0;
}
&.type-pdf {
iframe {
height: 50vh;
}
.file-footer {
padding: 8px;
}
}
&.type-webView {
display: flex;
flex-direction: column;
justify-content: center;
min-height: 50vh;
}
.ck-find-result {
outline: 2px solid var(--note-list-view-content-search-result-highlight-background);
border-radius: 4px;
background: var(--note-list-view-content-search-result-highlight-background);
color: var(--note-list-view-content-search-result-highlight-color);
}
}
.note-content-preview:has(.note-book-content:empty) {
display: none;
}
/* #endregion */
/* #region Grid view */
.note-list.grid-view .note-list-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.note-list.grid-view .note-book-card {
@@ -128,6 +311,10 @@
border: 1px solid transparent;
}
body.mobile .note-list.grid-view .note-book-card {
flex-basis: 150px;
}
.note-list.grid-view .note-book-card {
max-height: 300px;
}

View File

@@ -1,4 +1,5 @@
import "./ListOrGridView.css";
import { Card, CardSection } from "../../react/Card";
import { useEffect, useRef, useState } from "preact/hooks";
@@ -14,6 +15,11 @@ import NoteLink from "../../react/NoteLink";
import { ViewModeProps } from "../interface";
import { Pager, usePagination } from "../Pagination";
import { filterChildNotes, useFilteredNoteIds } from "./utils";
import { JSX } from "preact/jsx-runtime";
import { clsx } from "clsx";
import ActionButton from "../../react/ActionButton";
import linkContextMenuService from "../../../menus/link_context_menu";
import { TargetedMouseEvent } from "preact";
export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
const expandDepth = useExpansionDepth(note);
@@ -33,7 +39,7 @@ export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
{ noteIds.length > 0 && <div class="note-list-wrapper">
{!hasCollectionProperties && <Pager {...pagination} />}
<div class="note-list-container use-tn-links">
<Card className={clsx("nested-note-list", {"search-results": (noteType === "search")})}>
{pageNotes?.map(childNote => (
<ListNoteCard
key={childNote.noteId}
@@ -41,7 +47,7 @@ export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
expandDepth={expandDepth} highlightedTokens={highlightedTokens}
currentLevel={1} includeArchived={includeArchived} />
))}
</div>
</Card>
<Pager {...pagination} />
</div>}
@@ -93,27 +99,52 @@ function ListNoteCard({ note, parentNote, highlightedTokens, currentLevel, expan
// Reset expand state if switching to another note, or if user manually toggled expansion state.
useEffect(() => setExpanded(currentLevel <= expandDepth), [ note, currentLevel, expandDepth ]);
let subSections: JSX.Element | undefined = undefined;
if (isExpanded) {
subSections = <>
<CardSection className="note-content-preview">
<NoteContent note={note}
highlightedTokens={highlightedTokens}
noChildrenList
includeArchivedNotes={includeArchived} />
</CardSection>
<NoteChildren note={note}
parentNote={parentNote}
highlightedTokens={highlightedTokens}
currentLevel={currentLevel}
expandDepth={expandDepth}
includeArchived={includeArchived} />
</>
}
return (
<div
className={`note-book-card no-tooltip-preview ${isExpanded ? "expanded" : ""} ${note.isArchived ? "archived" : ""}`}
<CardSection
className={clsx("nested-note-list-item", "no-tooltip-preview", note.getColorClass(), {
"expanded": isExpanded,
"archived": note.isArchived
})}
subSections={subSections}
subSectionsVisible={isExpanded}
highlightOnHover
data-note-id={note.noteId}
>
<h5 className="note-book-header">
<span
className={`note-expander ${isExpanded ? "bx bx-chevron-down" : "bx bx-chevron-right"}`}
onClick={() => setExpanded(!isExpanded)}
/>
<h5>
<span className={`note-expander ${isExpanded ? "bx bx-chevron-down" : "bx bx-chevron-right"}`}
onClick={() => setExpanded(!isExpanded)}/>
<Icon className="note-icon" icon={note.getIcon()} />
<NoteLink className="note-book-title" notePath={notePath} noPreview showNotePath={parentNote.type === "search"} highlightedTokens={highlightedTokens} />
<NoteLink className="note-book-title"
notePath={notePath}
noPreview
showNotePath={parentNote.type === "search"}
highlightedTokens={highlightedTokens} />
<NoteAttributes note={note} />
<ActionButton className="nested-note-list-item-menu"
icon="bx bx-dots-vertical-rounded" text=""
onClick={(e) => openNoteMenu(notePath, e)}
/>
</h5>
{isExpanded && <>
<NoteContent note={note} highlightedTokens={highlightedTokens} noChildrenList includeArchivedNotes={includeArchived} />
<NoteChildren note={note} parentNote={parentNote} highlightedTokens={highlightedTokens} currentLevel={currentLevel} expandDepth={expandDepth} includeArchived={includeArchived} />
</>}
</div>
</CardSection>
);
}
@@ -165,6 +196,9 @@ export function NoteContent({ note, trim, noChildrenList, highlightedTokens, inc
const contentRef = useRef<HTMLDivElement>(null);
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
const [ready, setReady] = useState(false);
const [noteType, setNoteType] = useState<string>("none");
useEffect(() => {
content_renderer.getRenderedContent(note, {
trim,
@@ -179,17 +213,19 @@ export function NoteContent({ note, trim, noChildrenList, highlightedTokens, inc
} else {
contentRef.current.replaceChildren();
}
contentRef.current.classList.add(`type-${type}`);
highlightSearch(contentRef.current);
setNoteType(type);
setReady(true);
})
.catch(e => {
console.warn(`Caught error while rendering note '${note.noteId}' of type '${note.type}'`);
console.error(e);
contentRef.current?.replaceChildren(t("collections.rendering_error"));
setReady(true);
});
}, [ note, highlightedTokens ]);
return <div ref={contentRef} className="note-book-content" />;
return <div ref={contentRef} className={clsx("note-book-content", `type-${noteType}`, {"note-book-content-ready": ready})} />;
}
function NoteChildren({ note, parentNote, highlightedTokens, currentLevel, expandDepth, includeArchived }: {
@@ -238,3 +274,8 @@ function useExpansionDepth(note: FNote) {
return parseInt(expandDepth, 10);
}
function openNoteMenu(notePath, e: TargetedMouseEvent<HTMLElement>) {
linkContextMenuService.openContextMenu(notePath, e);
e.stopPropagation()
}

View File

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

View File

@@ -19,6 +19,10 @@
}
}
body.mobile .scrolling-container {
--content-margin-inline: 8px;
}
.note-split.type-code:not(.mime-text-x-sqlite) {
&> .scrolling-container {
background-color: var(--code-background-color);

View File

@@ -32,6 +32,12 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
display: flex;
align-items: center;
margin-block: 0;
.note-icon-widget {
display: flex;
align-items: center;
margin-inline-start: 0;
}
}
.modal.popup-editor-dialog .modal-header .note-title-widget {

View File

@@ -67,10 +67,7 @@ export default function PopupEditor() {
<NoteContextContext.Provider value={noteContext}>
<DialogWrapper>
<Modal
title={<>
<TitleRow />
{isNewLayout && <NoteBadges />}
</>}
title={<TitleRow />}
customTitleBarButtons={[{
iconClassName: "bx-expand-alt",
title: t("popup-editor.maximize"),
@@ -123,6 +120,7 @@ export function TitleRow() {
<div className="title-row">
<NoteIcon />
<NoteTitleWidget />
{isNewLayout && <NoteBadges />}
</div>
);
}

View File

@@ -1,14 +1,13 @@
import type { AppInfo } from "@triliumnext/commons";
import type { CSSProperties } from "preact/compat";
import { useState } from "preact/hooks";
import Modal from "../react/Modal.js";
import { t } from "../../services/i18n.js";
import openService from "../../services/open.js";
import { formatDateTime } from "../../utils/formatters.js";
import server from "../../services/server.js";
import utils from "../../services/utils.js";
import { formatDateTime } from "../../utils/formatters.js";
import openService from "../../services/open.js";
import { useState } from "preact/hooks";
import type { CSSProperties } from "preact/compat";
import type { AppInfo } from "@triliumnext/commons";
import { useTriliumEvent } from "../react/hooks.jsx";
import Modal from "../react/Modal.js";
export default function AboutDialog() {
const [appInfo, setAppInfo] = useState<AppInfo | null>(null);
@@ -55,15 +54,15 @@ export default function AboutDialog() {
<tr>
<th>{t("about.build_revision")}</th>
<td className="selectable-text">
{appInfo?.buildRevision && <a className="tn-link build-revision external" href={`https://github.com/TriliumNext/Trilium/commit/${appInfo.buildRevision}`} target="_blank" style={forceWordBreak} rel="noreferrer">{appInfo.buildRevision}</a>}
{appInfo?.buildRevision && <a className="tn-link build-revision external" href={`https://github.com/TriliumNext/Trilium/commit/${appInfo.buildRevision}`} target="_blank" style={forceWordBreak}>{appInfo.buildRevision}</a>}
</td>
</tr>
{ appInfo?.dataDirectory && <tr>
<tr>
<th>{t("about.data_directory")}</th>
<td className="data-directory">
{appInfo?.dataDirectory && (<DirectoryLink directory={appInfo.dataDirectory} style={forceWordBreak} />)}
</td>
</tr>}
</tr>
</tbody>
</table>
</Modal>
@@ -77,8 +76,8 @@ function DirectoryLink({ directory, style }: { directory: string, style?: CSSPro
openService.openDirectory(directory);
};
return <a className="tn-link selectable-text" href="#" onClick={onClick} style={style}>{directory}</a>;
}
return <span className="selectable-text" style={style}>{directory}</span>;
return <a className="tn-link selectable-text" href="#" onClick={onClick} style={style}>{directory}</a>
} else {
return <span className="selectable-text" style={style}>{directory}</span>;
}
}

View File

@@ -57,7 +57,7 @@ export default function BulkActionsDialog() {
className="bulk-actions-dialog"
size="xl"
title={t("bulk_actions.bulk_actions")}
footer={<Button text={t("bulk_actions.execute_bulk_actions")} primary />}
footer={<Button text={t("bulk_actions.execute_bulk_actions")} kind="primary" />}
show={shown}
onSubmit={async () => {
await server.post("bulk-action/execute", {

View File

@@ -72,7 +72,7 @@ export default function DeleteNotesDialog() {
footer={<>
<Button text={t("delete_notes.cancel")}
onClick={() => setShown(false)} />
<Button text={t("delete_notes.ok")} primary
<Button text={t("delete_notes.ok")} kind="primary"
buttonRef={okButtonRef}
onClick={() => {
opts.callback?.({ proceed: true, deleteAllClones, eraseNotes });

View File

@@ -58,7 +58,7 @@ export default function ExportDialog() {
setShown(false);
}}
onHidden={() => setShown(false)}
footer={<Button className="export-button" text={t("export.export")} primary />}
footer={<Button className="export-button" text={t("export.export")} kind="primary" />}
show={shown}
>

View File

@@ -55,7 +55,7 @@ export default function ImportDialog() {
setShown(false);
setFiles(null);
}}
footer={<Button text={t("import.import")} primary disabled={!files} />}
footer={<Button text={t("import.import")} kind="primary" disabled={!files} />}
show={shown}
>
<FormGroup name="files" label={t("import.chooseImportFile")} description={

View File

@@ -69,7 +69,7 @@ export default function PromptDialog() {
submitValue.current = null;
opts.current = undefined;
}}
footer={<Button text={t("prompt.ok")} keyboardShortcut="Enter" primary />}
footer={<Button text={t("prompt.ok")} keyboardShortcut="Enter" kind="primary" />}
show={shown}
stackable
>

View File

@@ -27,7 +27,6 @@ export default function RecentChangesDialog() {
});
useEffect(() => {
if (!ancestorNoteId) return;
server.get<RecentChangeRow[]>(`recent-changes/${ancestorNoteId}`)
.then(async (recentChanges) => {
// preload all notes into cache

View File

@@ -203,7 +203,7 @@ function RevisionPreview({noteContent, revisionItem, showDiff, setShown, onRevis
}} />
&nbsp;
<Button
primary
kind="primary"
icon="bx bx-download"
text={t("revisions.download_button")}
onClick={() => {

View File

@@ -35,7 +35,7 @@ export default function UploadAttachmentsDialog() {
className="upload-attachments-dialog"
size="lg"
title={t("upload_attachments.upload_attachments_to_note")}
footer={<Button text={t("upload_attachments.upload")} primary disabled={!files || isUploading} />}
footer={<Button text={t("upload_attachments.upload")} kind="primary" disabled={!files || isUploading} />}
onSubmit={async () => {
if (!files || !parentNoteId) {
return;

View File

@@ -0,0 +1,282 @@
import { BUILTIN_ATTRIBUTES } from "@triliumnext/commons";
import clsx from "clsx";
import { useEffect, useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import { t } from "../../services/i18n";
import { openInAppHelpFromUrl } from "../../services/utils";
import { BadgeWithDropdown } from "../react/Badge";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import FormToggle from "../react/FormToggle";
import { useNoteContext, useTriliumEvent } from "../react/hooks";
import { BookProperty, ViewProperty } from "../react/NotePropertyMenu";
const NON_DANGEROUS_ACTIVE_CONTENT = [ "appCss", "appTheme" ];
const DANGEROUS_ATTRIBUTES = BUILTIN_ATTRIBUTES.filter(a => a.isDangerous || NON_DANGEROUS_ACTIVE_CONTENT.includes(a.name));
const activeContentLabels = [ "iconPack", "widget", "appCss", "appTheme" ] as const;
interface ActiveContentInfo {
type: "iconPack" | "backendScript" | "frontendScript" | "widget" | "appCss" | "renderNote" | "webView" | "appTheme";
isEnabled: boolean;
canToggleEnabled: boolean;
}
const executeOption: BookProperty = {
type: "button",
icon: "bx bx-play",
label: t("active_content_badges.menu_execute_now"),
onClick: context => context.triggerCommand("runActiveNote")
};
const typeMappings: Record<ActiveContentInfo["type"], {
title: string;
icon: string;
helpPage: string;
apiDocsPage?: string;
isExecutable?: boolean;
additionalOptions?: BookProperty[];
}> = {
iconPack: {
title: t("active_content_badges.type_icon_pack"),
icon: "bx bx-package",
helpPage: "g1mlRoU8CsqC",
},
backendScript: {
title: t("active_content_badges.type_backend_script"),
icon: "bx bx-server",
helpPage: "SPirpZypehBG",
apiDocsPage: "MEtfsqa5VwNi",
isExecutable: true,
additionalOptions: [
executeOption,
{
type: "combobox",
bindToLabel: "run",
label: t("active_content_badges.menu_run"),
icon: "bx bx-rss",
dropStart: true,
options: [
{ value: null, label: t("active_content_badges.menu_run_disabled") },
{ value: "backendStartup", label: t("active_content_badges.menu_run_backend_startup") },
{ value: "daily", label: t("active_content_badges.menu_run_daily") },
{ value: "hourly", label: t("active_content_badges.menu_run_hourly") }
]
}
]
},
frontendScript: {
title: t("active_content_badges.type_frontend_script"),
icon: "bx bx-window",
helpPage: "yIhgI5H7A2Sm",
apiDocsPage: "Q2z6av6JZVWm",
isExecutable: true,
additionalOptions: [
executeOption,
{
type: "combobox",
bindToLabel: "run",
label: t("active_content_badges.menu_run"),
icon: "bx bx-rss",
dropStart: true,
options: [
{ value: null, label: t("active_content_badges.menu_run_disabled") },
{ value: "frontendStartup", label: t("active_content_badges.menu_run_frontend_startup") },
{ value: "mobileStartup", label: t("active_content_badges.menu_run_mobile_startup") },
]
},
{ type: "separator" },
{
type: "button",
label: t("active_content_badges.menu_change_to_widget"),
icon: "bx bxs-widget",
onClick: ({ note }) => attributes.setLabel(note.noteId, "widget")
}
]
},
widget: {
title: t("active_content_badges.type_widget"),
icon: "bx bxs-widget",
helpPage: "MgibgPcfeuGz",
additionalOptions: [
{
type: "button",
label: t("active_content_badges.menu_change_to_frontend_script"),
icon: "bx bx-window",
onClick: ({ note }) => {
attributes.removeOwnedLabelByName(note, "widget");
attributes.removeOwnedLabelByName(note, "disabled:widget");
}
}
]
},
appCss: {
title: t("active_content_badges.type_app_css"),
icon: "bx bxs-file-css",
helpPage: "AlhDUqhENtH7"
},
renderNote: {
title: t("active_content_badges.type_render_note"),
icon: "bx bx-extension",
helpPage: "HcABDtFCkbFN"
},
webView: {
title: t("active_content_badges.type_web_view"),
icon: "bx bx-globe",
helpPage: "1vHRoWCEjj0L"
},
appTheme: {
title :t("active_content_badges.type_app_theme"),
icon: "bx bx-palette",
helpPage: "7NfNr5pZpVKV",
additionalOptions: [
{
type: "combobox",
bindToLabel: "appThemeBase",
label: t("active_content_badges.menu_theme_base"),
icon: "bx bx-layer",
dropStart: true,
options: [
{ label: t("theme.auto_theme"), value: null },
{ type: "separator" },
{ label: t("theme.triliumnext"), value: "next" },
{ label: t("theme.triliumnext-light"), value: "next-light" },
{ label: t("theme.triliumnext-dark"), value: "next-dark" }
]
}
]
}
};
export function ActiveContentBadges() {
const { note } = useNoteContext();
const info = useActiveContentInfo(note);
return (note && info &&
<>
{info.canToggleEnabled && <ActiveContentToggle info={info} note={note} />}
<ActiveContentBadge info={info} note={note} />
</>
);
}
function ActiveContentBadge({ info, note }: { note: FNote, info: ActiveContentInfo }) {
const { title, icon, helpPage, apiDocsPage, additionalOptions } = typeMappings[info.type];
return (
<BadgeWithDropdown
className={clsx("active-content-badge", info.canToggleEnabled && !info.isEnabled && "disabled")}
icon={icon}
text={title}
dropdownOptions={{
dropdownContainerClassName: "mobile-bottom-menu",
mobileBackdrop: true
}}
>
{additionalOptions?.length && (
<>
{additionalOptions?.map((property, i) => (
<ViewProperty key={i} note={note} property={property} />
))}
<FormDropdownDivider />
</>
)}
<FormListItem
icon="bx bx-help-circle"
onClick={() => openInAppHelpFromUrl(helpPage)}
>{t("active_content_badges.menu_docs")}</FormListItem>
{apiDocsPage && <FormListItem
icon="bx bx-book-content"
onClick={() => openInAppHelpFromUrl(apiDocsPage)}
>{t("code_buttons.trilium_api_docs_button_title")}</FormListItem>}
</BadgeWithDropdown>
);
}
function ActiveContentToggle({ note, info }: { note: FNote, info: ActiveContentInfo }) {
const { title } = typeMappings[info.type];
return info && <FormToggle
switchOnName="" switchOffName=""
currentValue={info.isEnabled}
switchOffTooltip={t("active_content_badges.toggle_tooltip_disable_tooltip", { type: title })}
switchOnTooltip={t("active_content_badges.toggle_tooltip_enable_tooltip", { type: title })}
onChange={async (willEnable) => {
await Promise.all(note.getOwnedAttributes()
.map(attr => ({ name: attributes.getNameWithoutDangerousPrefix(attr.name), type: attr.type }))
.filter(({ name, type }) => DANGEROUS_ATTRIBUTES.some(item => item.name === name && item.type === type))
.map(({ name, type }) => attributes.toggleDangerousAttribute(note, type, name, willEnable)));
}}
/>;
}
function useActiveContentInfo(note: FNote | null | undefined) {
const [ info, setInfo ] = useState<ActiveContentInfo | null>(null);
function refresh() {
let type: ActiveContentInfo["type"] | null = null;
let isEnabled = false;
let canToggleEnabled = false;
if (!note) {
setInfo(null);
return;
}
if (note.type === "render") {
type = "renderNote";
isEnabled = note.hasRelation("renderNote");
} else if (note.type === "webView") {
type = "webView";
isEnabled = note.hasLabel("webViewSrc");
} else if (note.type === "code" && note.mime === "application/javascript;env=backend") {
type = "backendScript";
for (const backendLabel of [ "run", "customRequestHandler", "customResourceProvider" ]) {
isEnabled ||= note.hasLabel(backendLabel);
if (!canToggleEnabled && note.hasLabelOrDisabled(backendLabel)) {
canToggleEnabled = true;
}
}
} else if (note.type === "code" && note.mime === "application/javascript;env=frontend") {
type = "frontendScript";
isEnabled = note.hasLabel("widget") || note.hasLabel("run");
canToggleEnabled = note.hasLabelOrDisabled("widget") || note.hasLabelOrDisabled("run");
} else if (note.type === "code" && note.hasLabelOrDisabled("appTheme")) {
isEnabled = note.hasLabel("appTheme");
canToggleEnabled = true;
}
for (const labelToCheck of activeContentLabels) {
if (note.hasLabel(labelToCheck)) {
type = labelToCheck;
isEnabled = true;
canToggleEnabled = true;
break;
} else if (note.hasLabel(`disabled:${labelToCheck}`)) {
type = labelToCheck;
isEnabled = false;
canToggleEnabled = true;
break;
}
}
if (type) {
setInfo({ type, isEnabled, canToggleEnabled });
} else {
setInfo(null);
}
}
// Refresh on note change.
useEffect(refresh, [ note ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
refresh();
}
});
return info;
}

View File

@@ -37,6 +37,10 @@
pointer-events: none;
}
}
&.active-content-badge { --color: var(--badge-active-content-background-color); }
&.active-content-badge.disabled {
opacity: 0.5;
}
min-width: 0;
@@ -45,6 +49,11 @@
text-overflow: ellipsis;
min-width: 0;
}
.switch-button {
--switch-track-height: 8px;
--switch-track-width: 30px;
}
}
.dropdown-badge {

View File

@@ -10,6 +10,7 @@ import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { useGetContextData, useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
import { useShareState } from "../ribbon/BasicPropertiesTab";
import { useShareInfo } from "../shared_info";
import { ActiveContentBadges } from "./ActiveContentBadges";
export default function NoteBadges() {
return (
@@ -19,6 +20,7 @@ export default function NoteBadges() {
<ShareBadge />
<ClippedNoteBadge />
<ExecuteBadge />
<ActiveContentBadges />
</div>
);
}

View File

@@ -1,26 +0,0 @@
import { isMobile } from "../../services/utils";
import Admonition from "../react/Admonition";
export default function StandaloneWarningBar() {
return (
<div
className="standalone-warning-bar"
style={{
contain: "none"
}}
>
<Admonition
type="caution"
style={{
margin: 0,
fontSize: "0.8em"
}}
>
{isMobile()
? "Running Trilium standalone. Beware of data loss and other issues."
: "You are running Trilium in standalone mode. Some features are not available, and you may experience issues or data loss. Use the desktop application or self-hosted server for the best experience."
}
</Admonition>
</div>
);
}

View File

@@ -37,6 +37,10 @@
&:hover {
background: var(--input-background-color);
}
.text {
white-space: nowrap;
}
}
.status-bar-dropdown-button {

View File

@@ -35,11 +35,6 @@
display: flex;
flex-direction: column;
&.with-hue {
background-color: hsl(var(--bg-hue), 8.8%, 11.2%);
border-color: hsl(var(--bg-hue), 9.4%, 25.1%);
}
&.active {
outline: 4px solid var(--more-accented-background-color);
background: var(--card-background-hover-color);

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