Compare commits

..

399 Commits

Author SHA1 Message Date
Elian Doran
58e2111a8f chore(nix): update flake lock 2025-12-14 11:01:25 +02:00
Elian Doran
c5e4c484dc chore(deps): update react monorepo to v19.2.3 (#8033) 2025-12-14 10:58:02 +02:00
Elian Doran
75a6dece7a Translations update from Hosted Weblate (#8052) 2025-12-14 10:57:37 +02:00
Hosted Weblate
5c0e7736d6 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2025-12-14 09:52:47 +01:00
Elian Doran
2562ecd055 feat(renovate): enable nix 2025-12-14 10:52:29 +02:00
Elian Doran
aaaa47b575 Translations update from Hosted Weblate (#8049) 2025-12-14 10:33:29 +02:00
green
21d82ec1d7 Translated using Weblate (Japanese)
Currently translated at 100.0% (1686 of 1686 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-12-14 09:21:46 +01:00
Luk On
5af8444cac Translated using Weblate (Polish)
Currently translated at 100.0% (116 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/pl/
2025-12-14 09:21:46 +01:00
Wojciech O
cd82c34b93 Translated using Weblate (Polish)
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/pl/
2025-12-14 09:21:45 +01:00
noobhjy
d182659d62 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1686 of 1686 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2025-12-14 09:21:44 +01:00
Mik Piet
171f428b9d Translated using Weblate (Polish)
Currently translated at 100.0% (1686 of 1686 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pl/
2025-12-14 09:21:43 +01:00
kamykO
da4ca9c804 Translated using Weblate (Polish)
Currently translated at 100.0% (1686 of 1686 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pl/
2025-12-14 09:21:43 +01:00
Luk On
c019341503 Translated using Weblate (Polish)
Currently translated at 100.0% (1686 of 1686 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pl/
2025-12-14 09:21:42 +01:00
Hosted Weblate
7234f04b56 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2025-12-14 09:21:42 +01:00
Elian Doran
1998cbc005 chore(deps): update dependency @types/node to v24.10.4 (#8050) 2025-12-14 10:21:26 +02:00
renovate[bot]
5914073c3f chore(deps): update dependency @types/node to v24.10.4 2025-12-14 02:09:59 +00:00
Elian Doran
d5aadf2604 chore: add dev shell and direnv support (#8011) 2025-12-13 22:49:35 +02:00
renovate[bot]
1fe22f940b chore(deps): update react monorepo to v19.2.3 2025-12-13 20:46:52 +00:00
Elian Doran
0cdaf70efe chore(deps): update dependency electron to v39.2.7 (#8030) 2025-12-13 22:46:30 +02:00
Elian Doran
8174c65243 chore(deps): update dependency eslint to v9.39.2 (#8031) 2025-12-13 22:45:31 +02:00
Elian Doran
2645801277 chore(deps): update dependency vite to v7.2.7 (#8032) 2025-12-13 22:44:45 +02:00
Elian Doran
fb8c31cb9c fix(deps): update dependency i18next to v25.7.2 (#8035) 2025-12-13 22:44:05 +02:00
Elian Doran
7287dbd64f fix(deps): update dependency preact-render-to-string to v6.6.4 (#8036) 2025-12-13 22:43:19 +02:00
renovate[bot]
6569d64931 fix(deps): update dependency preact-render-to-string to v6.6.4 2025-12-13 19:24:56 +00:00
renovate[bot]
e9f3216926 fix(deps): update dependency i18next to v25.7.2 2025-12-13 19:24:28 +00:00
renovate[bot]
ca0af9646d chore(deps): update dependency vite to v7.2.7 2025-12-13 19:23:33 +00:00
renovate[bot]
92dfafd1ff chore(deps): update dependency eslint to v9.39.2 2025-12-13 19:23:05 +00:00
renovate[bot]
d04dde3b97 chore(deps): update dependency electron to v39.2.7 2025-12-13 19:22:36 +00:00
Elian Doran
4c520c6df3 e2e(server): broken test after submenu 2025-12-13 21:10:59 +02:00
Adorian Doran
65d6ed1cdc Merge branch 'main' of https://github.com/TriliumNext/Trilium 2025-12-13 20:42:24 +02:00
Adorian Doran
3352a92445 style/new layout/inline title: tweak 2025-12-13 20:42:16 +02:00
Elian Doran
bc8c55b8fb Translations update from Hosted Weblate (#8047) 2025-12-13 20:37:28 +02:00
Elian Doran
7660914eb8 Merge branch 'main' into weblate-trilium-client 2025-12-13 20:34:04 +02:00
Elian Doran
869aec778c New layout: Shared formatting toolbar (#8046) 2025-12-13 20:15:43 +02:00
noobhjy
255726dcc4 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1685 of 1685 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2025-12-13 18:09:10 +00:00
Adorian Doran
9969000807 style/new layout/note title: tweak 2025-12-13 20:08:57 +02:00
Elian Doran
3b909fd739 chore(layout/formatting_toolbar): address requested changes 2025-12-13 19:59:45 +02:00
Elian Doran
ad08fb8132 chore(formatting_toolbar): address self-review 2025-12-13 19:32:44 +02:00
Elian Doran
8d536a6040 fix(formatting_toolbar): view mode check not working in multi-split 2025-12-13 19:29:13 +02:00
Elian Doran
2b1bc8e2b9 feat(inline_title): in split, avoid layout shift by maintaining the toolbar 2025-12-13 16:54:04 +02:00
Adorian Doran
563194ff6c client/note menu: localize string 2025-12-13 16:45:09 +02:00
Elian Doran
0c9ff4dae4 chore(inline_title): fix type error 2025-12-13 16:43:27 +02:00
Elian Doran
b10e7f1811 fix(inline_title): some badges not visible in split 2025-12-13 16:42:06 +02:00
Elian Doran
f93ad499e2 feat(layout/formatting_toolbar): move above sidebar 2025-12-13 16:35:48 +02:00
Adorian Doran
87a51251ca client/layout/status bar: replace some icons 2025-12-13 16:33:33 +02:00
Elian Doran
b56e5b2483 fix(inline_title): note type switcher visible for options 2025-12-13 16:33:33 +02:00
Elian Doran
476c162016 fix(layout/formatting_toolbar): memory leak for closed tabs 2025-12-13 16:31:19 +02:00
Elian Doran
4182f6043a feat(layout/formatting_toolbar): render cached components 2025-12-13 16:26:01 +02:00
Elian Doran
aa528c65b7 chore(layout/formatting_toolbar): render without adapter 2025-12-13 16:05:11 +02:00
Elian Doran
4998560e31 chore(layout/inline_title): show note type switcher for code notes as well 2025-12-13 15:55:56 +02:00
Elian Doran
86f36922c4 Translations update from Hosted Weblate (#8025) 2025-12-13 15:42:54 +02:00
Hosted Weblate
4f617b86d3 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/
2025-12-13 13:34:47 +00:00
noobhjy
b28527e10d Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1683 of 1683 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2025-12-13 13:34:46 +00:00
green
fbb8924ebf Translated using Weblate (Japanese)
Currently translated at 100.0% (1683 of 1683 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-12-13 13:34:45 +00:00
Hosted Weblate
f68c9b751f 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/
2025-12-13 13:34:45 +00:00
Luk On
8091f02b16 Translated using Weblate (Polish)
Currently translated at 100.0% (1677 of 1677 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pl/
2025-12-13 13:34:44 +00:00
noobhjy
f4c68d115b Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1677 of 1677 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2025-12-13 13:34:43 +00:00
Luk On
6c70d6b9ae Translated using Weblate (Polish)
Currently translated at 100.0% (152 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/pl/
2025-12-13 13:34:43 +00:00
Luk On
1ea12567a3 Translated using Weblate (Polish)
Currently translated at 100.0% (116 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/pl/
2025-12-13 13:34:42 +00:00
Luk On
2d16ab7a70 Translated using Weblate (Polish)
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/pl/
2025-12-13 13:34:41 +00:00
Luk On
a228ba5273 Translated using Weblate (Polish)
Currently translated at 100.0% (1677 of 1677 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pl/
2025-12-13 13:34:41 +00:00
Luk On
d0477e9ebf Translated using Weblate (Polish)
Currently translated at 100.0% (152 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/pl/
2025-12-13 13:34:40 +00:00
Luk On
c99907972d Translated using Weblate (Polish)
Currently translated at 11.2% (13 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/pl/
2025-12-13 13:34:39 +00:00
Luk On
b9ebc7d7ea Translated using Weblate (Polish)
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/pl/
2025-12-13 13:34:39 +00:00
green
4f9e2c5eca Translated using Weblate (Japanese)
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ja/
2025-12-13 13:34:38 +00:00
green
ab1f8ee5ae Translated using Weblate (Japanese)
Currently translated at 100.0% (152 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ja/
2025-12-13 13:34:37 +00:00
green
89276ad51a Translated using Weblate (Japanese)
Currently translated at 100.0% (1677 of 1677 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-12-13 13:34:36 +00:00
green
eca533a517 Translated using Weblate (Japanese)
Currently translated at 100.0% (116 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/ja/
2025-12-13 13:34:36 +00:00
Elian Doran
0be578c517 chore(deps): update dependency @electron/rebuild to v4.0.2 (#8028) 2025-12-13 15:34:09 +02:00
Elian Doran
198b315602 chore(deps): update dependency @redocly/cli to v2.12.6 (#8029) 2025-12-13 15:33:28 +02:00
Elian Doran
6474abc983 fix(deps): update dependency eslint-linter-browserify to v9.39.2 (#8034) 2025-12-13 15:32:33 +02:00
Elian Doran
2137dbe849 chore(deps): update node.js to v24.12.0 (#8037) 2025-12-13 15:32:07 +02:00
Elian Doran
b7b46703d9 chore(deps): update pnpm to v10.25.0 (#8038) 2025-12-13 15:31:13 +02:00
Elian Doran
d2d96a1421 chore(deps): update typescript-eslint monorepo to v8.49.0 (#8039) 2025-12-13 15:30:26 +02:00
Elian Doran
cfcc309e5a fix(deps): update dependency @codemirror/view to v6.39.4 (#8040) 2025-12-13 15:28:35 +02:00
Elian Doran
7d87ec942e fix(deps): update dependency react-i18next to v16.5.0 (#8041) 2025-12-13 15:26:40 +02:00
Elian Doran
4def13272f chore(deps): update github artifact actions (major) (#8042) 2025-12-13 15:25:59 +02:00
Elian Doran
c4f914bb7b New layout: Title bar & inline title (#8044) 2025-12-13 15:09:30 +02:00
Elian Doran
6bf213a0b0 fix(layout/status_bar): some popups not dismissing 2025-12-13 15:02:16 +02:00
Elian Doran
694cd2bc7c chore(layout/title_bar): address LLM review 2025-12-13 14:58:11 +02:00
Elian Doran
3851a94400 fix(layout/title_bar): badges not collapsing 2025-12-13 14:51:58 +02:00
Elian Doran
e296416a54 fix(layout/inline-title): title not shown when switching to other types of notes 2025-12-13 14:38:58 +02:00
Elian Doran
0bd89a659c chore(layout/inline-title): disable pointer events while hidden 2025-12-13 14:00:27 +02:00
Elian Doran
0ada6523a8 feat(layout/inline-title): add transition 2025-12-13 13:58:20 +02:00
Elian Doran
56570d7ba1 fix(layout/inline-title): text displayed even when note is not empty 2025-12-13 13:46:28 +02:00
Elian Doran
0ffdedcfa6 feat(layout/inline-title): dropdown for collections 2025-12-13 13:45:34 +02:00
Elian Doran
f391bb8eec feat(layout/inline-title): support built-in templates 2025-12-13 13:35:32 +02:00
Elian Doran
7000076961 feat(layout/inline-title): react to template add/remove 2025-12-13 13:26:48 +02:00
Elian Doran
e0f6ba808c feat(layout/inline-title): template switcher 2025-12-13 13:24:32 +02:00
Elian Doran
4c2fe8a846 feat(layout/inline-title): group some note types 2025-12-13 13:12:03 +02:00
Elian Doran
2ea23368bc feat(vscode): eslint on save 2025-12-13 12:59:32 +02:00
Elian Doran
87666005a6 feat(layout/inline-title): add an intro text 2025-12-13 12:57:33 +02:00
Elian Doran
7666f44b7a fix(layout/inline-title): hide note type switcher on other note types 2025-12-13 12:49:51 +02:00
Elian Doran
470f6e5334 feat(layout/inline-title): hide note type switcher when empty 2025-12-13 12:48:20 +02:00
Elian Doran
a2b007874b feat(layout/inline-title): not reacting to note type changes 2025-12-13 12:43:15 +02:00
Elian Doran
9946d8c6b9 fix(layout/statusbar): code note switcher displayed for other note types 2025-12-13 12:29:59 +02:00
Elian Doran
02fab16475 feat(layout/inline-title): add icons 2025-12-13 12:28:22 +02:00
Elian Doran
5145ce2d23 feat(layout/inline-title): horizontal scroll via wheel 2025-12-13 12:27:45 +02:00
Elian Doran
e06abe6e5b fix(layout/inline-title): current note type displayed in switcher 2025-12-13 12:26:02 +02:00
Elian Doran
50a847777e feat(layout/inline-title): basic note type switcher 2025-12-13 12:25:01 +02:00
Elian Doran
4473f80d73 refactor(layout): remove floating title bar experiment 2025-12-13 12:02:17 +02:00
Elian Doran
70c918c9c6 feat(layout/inline-title): support in options as well 2025-12-13 12:01:06 +02:00
Elian Doran
0939975631 style(layout/inline-title): use muted text color 2025-12-13 11:58:35 +02:00
Elian Doran
0ef90c6165 fix(layout/inline-title): hide in attachments and other view scopes 2025-12-13 11:57:53 +02:00
Elian Doran
cef14a3b19 feat(layout/inline-title): support code 2025-12-13 11:51:57 +02:00
Elian Doran
61d3141bce refactor(layout/inline-title): extract specific styles 2025-12-13 11:49:05 +02:00
Elian Doran
f040a0b6d1 refactor(layout/inline-title): separate old title details into title actions 2025-12-13 11:46:42 +02:00
Elian Doran
e9dfec88c9 feat(layout/inline-title): bring back creation and modification date 2025-12-13 11:43:27 +02:00
Elian Doran
6fa97c845a fix(layout/inline-title): still visible in other note types 2025-12-13 11:37:56 +02:00
Elian Doran
f686d9ecd0 feat(layout/inline-title): keep header bar visible 2025-12-13 11:34:29 +02:00
Elian Doran
621ebe4396 feat(layout/inline-title): title and icon 2025-12-13 11:33:02 +02:00
Adorian Doran
ac2a566685 client/note menu: reorganize menu items 2025-12-13 11:29:39 +02:00
Elian Doran
ac3d57d5da chore(layout): remove ribbon border 2025-12-13 11:28:52 +02:00
Elian Doran
9ab5eef984 feat(layout/inline-title): intersection observer 2025-12-13 11:26:42 +02:00
Elian Doran
912f90accf feat(layout/inline-title): collapse title for text notes 2025-12-13 11:17:39 +02:00
Elian Doran
6463b0dcaa chore(layout/inline-title): placeholder for the title 2025-12-13 11:08:34 +02:00
Elian Doran
0b45fb6764 feat(layout/title): hide note badges while editing title 2025-12-13 10:57:34 +02:00
Elian Doran
330d71847b refactor(layout/title): rename to note badges 2025-12-13 10:54:19 +02:00
Elian Doran
60c8f0c78b refactor(layout/title): relocate badges to layouts dir 2025-12-13 10:47:46 +02:00
Elian Doran
fcbd1ab0b1 chore(layout/title): remove spacer 2025-12-13 10:44:33 +02:00
Elian Doran
3549bfb328 feat(layout/title): collapse badges and note title while constrained in size 2025-12-13 10:43:32 +02:00
Elian Doran
c97038fffd chore(layout): revert breadcrumb row 2025-12-13 10:26:25 +02:00
Elian Doran
15b5885982 New layout: status bar (#8021) 2025-12-13 10:23:12 +02:00
renovate[bot]
6aa8d9fbf9 chore(deps): update github artifact actions 2025-12-13 01:19:13 +00:00
renovate[bot]
eccf4620ac fix(deps): update dependency react-i18next to v16.5.0 2025-12-13 01:19:08 +00:00
renovate[bot]
f08fbe9bb2 fix(deps): update dependency @codemirror/view to v6.39.4 2025-12-13 01:18:39 +00:00
renovate[bot]
bfa87af489 chore(deps): update typescript-eslint monorepo to v8.49.0 2025-12-13 01:18:08 +00:00
renovate[bot]
a7899b7505 chore(deps): update pnpm to v10.25.0 2025-12-13 01:17:37 +00:00
renovate[bot]
e80b5cddcd chore(deps): update node.js to v24.12.0 2025-12-13 01:17:27 +00:00
renovate[bot]
db12f9b8dc fix(deps): update dependency eslint-linter-browserify to v9.39.2 2025-12-13 01:15:42 +00:00
renovate[bot]
f4c95195c9 chore(deps): update dependency @redocly/cli to v2.12.6 2025-12-13 01:12:39 +00:00
renovate[bot]
e2cbff7b3a chore(deps): update dependency @electron/rebuild to v4.0.2 2025-12-13 01:11:53 +00:00
Elian Doran
98a3c8150c feat(breadcrumb): replace title editing with jump to top 2025-12-13 01:45:02 +02:00
Elian Doran
447e09fec1 feat(note_actions): hide code notes from new layout 2025-12-13 01:24:37 +02:00
Elian Doran
7d2a1bb2e5 feat(status_bar): modal for configuring list of code languages 2025-12-13 01:19:20 +02:00
Elian Doran
40fcf79778 fix(status_bar): code mime not updating between notes 2025-12-13 01:14:50 +02:00
Elian Doran
88a779bbdb feat(status_bar): indicate selected code mime in menu 2025-12-13 01:10:38 +02:00
Elian Doran
db04514769 fix(status_bar): code mime switcher is clipped 2025-12-13 01:07:30 +02:00
Elian Doran
23062470f5 feat(status_bar): code mime switcher 2025-12-13 01:03:57 +02:00
Elian Doran
5bad043ed5 chore(status_bar): address requested changes 2025-12-13 00:43:00 +02:00
Elian Doran
4ab8af0995 feat(status_bar): keep button active when dropdown is shown 2025-12-13 00:37:29 +02:00
Elian Doran
1a65c5e13e feat(status_bar): hide note paths in hidden notes 2025-12-13 00:30:22 +02:00
Elian Doran
fc08946038 chore(status_bar): avoid shifting due to language switcher 2025-12-13 00:27:51 +02:00
Elian Doran
4d6dba06ad refactor(ribbon): remove left-over logic for calculating ribbon height 2025-12-13 00:24:05 +02:00
Elian Doran
d7887fe25f chore(layout): hide ribbon in new layout 2025-12-13 00:22:34 +02:00
Elian Doran
81dd50e752 fix(status_bar): wrong font size for language selector text 2025-12-13 00:13:06 +02:00
Elian Doran
fe13065ef8 lint: status bar 2025-12-13 00:11:28 +02:00
Elian Doran
eb02330fdf feat(status_bar): integrate note paths widget 2025-12-13 00:05:33 +02:00
Elian Doran
738fa6fd0e lint: note paths 2025-12-12 23:51:03 +02:00
Elian Doran
0c1c7e4f8e feat(status_bar): note paths (no interaction yet) 2025-12-12 23:47:31 +02:00
Elian Doran
9eb9b66398 fix(status_bar): keyboard shortcuts to add attributes not working 2025-12-12 23:34:26 +02:00
Elian Doran
9db046b401 fix(status_bar): attributes not editable from modal 2025-12-12 23:29:59 +02:00
Elian Doran
914272eee0 style(status_bar): improve layout and spacing slightly 2025-12-12 23:13:31 +02:00
Elian Doran
2b7e203bcc refactor(status_bar): remove wrapper container for breadcrumb 2025-12-12 23:07:57 +02:00
Elian Doran
a61ddedc0b refactor(status_bar): remove old breadcrumb styles 2025-12-12 23:06:47 +02:00
Elian Doran
60fc34ffac feat(status_bar): functional attribute toggle button 2025-12-12 21:57:42 +02:00
Elian Doran
685109556c chore(ribbon): hide inherited & owned attributes on new layout 2025-12-12 21:49:42 +02:00
Elian Doran
45927053f3 fix(ribbon): links in inherited attributes not visible 2025-12-12 21:48:11 +02:00
Elian Doran
5d438a877b feat(status_bar): improve alignment of attribute editor 2025-12-12 21:44:27 +02:00
Elian Doran
870499bc3a feat(status_bar): basic integration of inherited attributes 2025-12-12 21:41:05 +02:00
Elian Doran
c6d97e3d4b feat(status_bar): basic integration of attribute editor 2025-12-12 21:30:32 +02:00
Elian Doran
efff38b116 feat(status_bar): attribute button (not yet interactive) 2025-12-12 21:19:23 +02:00
Elian Doran
1b725175c6 refactor(status_bar): solve warnings 2025-12-12 20:59:57 +02:00
Elian Doran
6eff62f73f feat(status_bar): add new attachment count 2025-12-12 20:55:54 +02:00
Elian Doran
95d2160c76 feat(status_bar): integrate backlinks 2025-12-12 20:31:19 +02:00
Elian Doran
2b195155ed fix(note_details): appearing in options 2025-12-12 20:21:55 +02:00
Elian Doran
28e9abc8bb chore(status_bar): re-order icons to avoid layout shifting 2025-12-12 20:19:37 +02:00
Elian Doran
0162b9d441 fix(status_bar): language selector appearing for non-text notes 2025-12-12 20:18:50 +02:00
Elian Doran
0545b929e1 fix(status_bar): react to active note context 2025-12-12 20:17:53 +02:00
Elian Doran
d2b32ff5af feat(status_bar): relocate to outside split area 2025-12-12 19:47:47 +02:00
Elian Doran
2d3776cd5f feat(status_bar): integrate note info badge 2025-12-12 19:31:00 +02:00
Elian Doran
2638963171 feat(status_bar/language): add tooltip 2025-12-12 18:58:54 +02:00
Elian Doran
24ed97f65d feat(status_bar/language): improve display of more languages 2025-12-12 18:53:54 +02:00
Elian Doran
c099634e39 feat(status_bar/language): improve display of Asian languages 2025-12-12 18:50:48 +02:00
Elian Doran
12be14e6cf feat(status_bar/language): display icon 2025-12-12 18:47:34 +02:00
Elian Doran
4dc773c1a3 refactor(status_bar/language): stop reusing UI for greater customisibility 2025-12-12 18:29:40 +02:00
Elian Doran
31c5323fd9 feat(status_bar/language): compact locale name 2025-12-12 18:05:10 +02:00
Elian Doran
74b6e7bf63 fix(breadcrumb): some dropdowns not visible 2025-12-12 17:55:22 +02:00
Elian Doran
34025fa646 fix(global_menu): dev menu wrongly positioned on horizontal layout 2025-12-12 14:41:10 +02:00
Elian Doran
df9554194a feat(layout/status_bar): integrate language selector basically 2025-12-12 00:34:47 +02:00
Elian Doran
4e1188484d refactor(layout/status_bar): move breadcrumbs into layout dir 2025-12-12 00:24:30 +02:00
Elian Doran
2f44b9dc59 feat(layout/status_bar): integrate breadcrumbs 2025-12-12 00:21:40 +02:00
Elian Doran
9ee3c48485 chore(layout): relocate ribbon on the top temporarily 2025-12-12 00:15:58 +02:00
Elian Doran
78b9c94829 chore(layout/status_bar): create empty component 2025-12-12 00:13:38 +02:00
Elian Doran
4c8225ed73 Translations update from Hosted Weblate (#8020) 2025-12-11 23:58:41 +02:00
noobhjy
88aad6d351 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1668 of 1668 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2025-12-11 20:38:41 +00:00
Elian Doran
d99d701095 feat(global_menu): add support for all experimental options 2025-12-11 22:38:22 +02:00
Elian Doran
61fe27abbe feat(layout): extract floating titlebar into its own experimental feature 2025-12-11 22:29:22 +02:00
Elian Doran
24cd5006d5 chore(note_map): open in reusable split 2025-12-11 22:14:08 +02:00
Elian Doran
726d6aad65 feat(layout): integrate note map 2025-12-11 22:01:22 +02:00
Elian Doran
bd9fe14a6c chore(layout): remove title extra spacing for now 2025-12-11 21:08:36 +02:00
Elian Doran
792a10ace5 New layout: Integrate small ribbon categories + collection properties (#8018) 2025-12-11 20:59:31 +02:00
Elian Doran
e9ac69b8e5 chore(note_bars/collection): address change request 2025-12-11 20:33:52 +02:00
Elian Doran
c76ff2d371 feat(note_bars/collection): add a help button 2025-12-11 20:19:06 +02:00
Elian Doran
8ab9e30404 chore(note_bars/collection): disable ribbon tab 2025-12-11 20:13:04 +02:00
Elian Doran
53b7d93efb feat(note_bars/collection): support comboboxes 2025-12-11 20:09:25 +02:00
Elian Doran
00df3c3d1f feat(note_bars/collection): support number fields 2025-12-11 19:51:40 +02:00
Elian Doran
e766b82418 feat(note_bars/collection): add icon to checkboxes 2025-12-11 19:44:22 +02:00
Elian Doran
9f4757af5b chore(note_bars/collection): put archived notes at the end 2025-12-11 19:39:07 +02:00
Elian Doran
1a9fb34a6e feat(note_bars/collection): support dropdown menu click action 2025-12-11 19:37:04 +02:00
Elian Doran
a1513a3567 feat(note_bars/collection): support split button properties 2025-12-11 19:34:22 +02:00
Elian Doran
0de67b6a69 feat(note_bars/collection): support button properties 2025-12-11 19:29:27 +02:00
Elian Doran
fec5ee9335 feat(note_bars/collection): integrate show archived notes 2025-12-11 19:21:51 +02:00
Elian Doran
b540111fa4 feat(note_bars): add icons to view type switcher 2025-12-11 18:59:28 +02:00
Elian Doran
0eed72b888 feat(note_bars): view type switcher 2025-12-11 18:53:48 +02:00
Elian Doran
0856d3dbdf fix(layout): note title padding on full-height note 2025-12-11 18:02:52 +02:00
Elian Doran
a9b453c27a feat(breadcrumb_badges): integrate query/script tab 2025-12-11 17:43:00 +02:00
Elian Doran
fa8287269f feat(breadcrumb_badges): integrate note properties tab 2025-12-11 17:34:04 +02:00
Elian Doran
1eee471018 fix(breadcrumb_badges): temporarily editable showing up always in popup editor 2025-12-11 17:20:28 +02:00
Elian Doran
c3829f82ab New layout: Note info (#8015) 2025-12-11 17:18:19 +02:00
Elian Doran
a51820f5df chore(note_info): address requested changes 2025-12-11 16:57:04 +02:00
Elian Doran
68591fb511 feat(note_info): hide ribbon on new layout 2025-12-11 16:48:49 +02:00
Elian Doran
3795ce2143 feat(note_info): integrate near the note title 2025-12-11 16:47:44 +02:00
Elian Doran
3561a4f14d feat(note_info): add back tooltip for note size 2025-12-11 16:38:31 +02:00
Elian Doran
84cda001aa feat(note_info): improve layout slightly 2025-12-11 16:33:18 +02:00
Elian Doran
481127a560 docs(user): mention version for board custom attributes 2025-12-11 10:57:30 +02:00
Elian Doran
c708e7cd61 Translations update from Hosted Weblate (#8016) 2025-12-11 10:49:33 +02:00
Elian Doran
fee0268792 Translated using Weblate (Romanian)
Currently translated at 100.0% (116 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/ro/
2025-12-11 09:47:52 +01:00
green
953593c9d4 Translated using Weblate (Japanese)
Currently translated at 100.0% (1668 of 1668 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-12-11 09:47:52 +01:00
Elian Doran
5ff60e53cb Translated using Weblate (Romanian)
Currently translated at 100.0% (1668 of 1668 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ro/
2025-12-11 09:47:51 +01:00
Giovi
b38ee36fae Translated using Weblate (Italian)
Currently translated at 100.0% (1668 of 1668 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2025-12-11 09:47:49 +01:00
Hosted Weblate
38a415faf0 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2025-12-11 09:32:25 +01:00
Elian Doran
1e26864842 Translations update from Hosted Weblate (#8006) 2025-12-11 10:32:09 +02:00
Elian Doran
4b74ad5577 feat(breadcrumb/note_info): note size 2025-12-11 00:34:25 +02:00
Elian Doran
e5696713de feat(breadcrumb/note_info): modification/creation date 2025-12-11 00:23:32 +02:00
Elian Doran
2e44397c88 feat(breadcrumb/note_info): get basic dropdown 2025-12-11 00:18:56 +02:00
Francis C.
5d19881981 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1658 of 1658 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2025-12-10 22:38:11 +01:00
Tomas Adamek
1711384eaa Translated using Weblate (Czech)
Currently translated at 35.3% (41 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/cs/
2025-12-10 22:38:11 +01:00
Tomas Adamek
9897efe4af Translated using Weblate (Czech)
Currently translated at 5.3% (88 of 1658 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/cs/
2025-12-10 22:38:10 +01:00
Francis C.
884578ea95 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (116 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/zh_Hans/
2025-12-10 22:38:09 +01:00
Francis C.
e404e76299 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (116 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/zh_Hant/
2025-12-10 22:38:09 +01:00
green
1db54cba3e Translated using Weblate (Japanese)
Currently translated at 100.0% (1658 of 1658 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-12-10 22:38:08 +01:00
Francis C.
77e3cc4021 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1658 of 1658 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2025-12-10 22:38:07 +01:00
pythaac
242c63dfb4 Translated using Weblate (Korean)
Currently translated at 65.1% (99 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ko/
2025-12-10 22:38:06 +01:00
Abdulmajeed Alaskar
f5440576b5 Translated using Weblate (Arabic)
Currently translated at 53.9% (82 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ar/
2025-12-10 22:38:06 +01:00
Abdulmajeed Alaskar
b020365af4 Translated using Weblate (Arabic)
Currently translated at 29.3% (34 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/ar/
2025-12-10 22:38:05 +01:00
green
25e5bf0b86 Translated using Weblate (Japanese)
Currently translated at 100.0% (1648 of 1648 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-12-10 22:38:05 +01:00
Elian Doran
19b32dd3a6 New layout: Integrate Basic properties (#8014) 2025-12-10 23:37:54 +02:00
Elian Doran
1ab89d0db0 fix(status_bar): language selector not updating properly 2025-12-10 23:36:47 +02:00
Elian Doran
6e8e10323f chore(client): address requested changes 2025-12-10 23:19:17 +02:00
Elian Doran
58bc5dc66a chore(ribbon): hide basic properties from the ribbon on new layout 2025-12-10 23:07:35 +02:00
Elian Doran
db42bb603b feat(status_bar): add help item 2025-12-10 23:04:32 +02:00
Elian Doran
cb382c9537 fix(global_menu): layout switcher sometimes fails 2025-12-10 22:57:52 +02:00
Elian Doran
a4b79a2dc9 fix(ribbon): content code modal hidden behind backdrop 2025-12-10 22:56:29 +02:00
Elian Doran
0f867e02c4 fix(ribbon): content languages modal hidden behind backdrop 2025-12-10 22:52:13 +02:00
Elian Doran
ab1b4b37f4 feat(global_menu): add an option to switch layouts 2025-12-10 22:51:47 +02:00
Elian Doran
5a1d138f29 feat(status_bar): language selector 2025-12-10 22:39:07 +02:00
Elian Doran
06a5298efa feat(note_actions): hide options in attachments 2025-12-10 22:27:56 +02:00
Elian Doran
db720acc18 feat(note_actions): hide options in help pages 2025-12-10 22:25:09 +02:00
Elian Doran
8d8ff25bae feat(note_actions): reintroduce help pages 2025-12-10 22:21:15 +02:00
Elian Doran
6f85b7cc09 feat(note_actions): integrate note type 2025-12-10 21:54:17 +02:00
Elian Doran
77f5770bff feat(note_actions): protect note switch 2025-12-10 20:57:15 +02:00
Elian Doran
14cda5b921 fix(note_actions): editability context menu is too narrow 2025-12-10 20:46:58 +02:00
Elian Doran
36b1182565 feat(widgets/toggle): disable if going too fast 2025-12-10 20:33:30 +02:00
Elian Doran
483327c808 fix(widgets/toggle): double event triggering when in menu 2025-12-10 20:30:55 +02:00
Elian Doran
efb2f9a048 chore(note_actions): reintroduce disabled logic for toggles 2025-12-10 20:20:21 +02:00
Elian Doran
01978dabf0 fix(breadcrumb_badges): doesn't refresh when switching editability 2025-12-10 19:05:33 +02:00
Elian Doran
cfbd2bf53a feat(note_actions): integrate editability menu into new layout 2025-12-10 18:58:46 +02:00
Elian Doran
9262f94190 feat(note_actions): integrate template switch into new layout 2025-12-10 18:43:34 +02:00
Elian Doran
b36a0bd10b feat(note_actions): integrate shared switch into new layout 2025-12-10 18:40:56 +02:00
Elian Doran
2dc8948f33 chore(breadcrumb_badges): chagne icon for shared locally 2025-12-10 18:35:29 +02:00
Elian Doran
9f2ed2f9d4 feat(widgets/toggle): disable transitions on first render 2025-12-10 18:33:29 +02:00
Elian Doran
e0f7d65f77 feat(widgets): toggle from label 2025-12-10 18:24:31 +02:00
Elian Doran
f18ac3a923 feat(note_actions): integrate bookmark into new layout 2025-12-10 18:20:36 +02:00
Elian Doran
b39a6bcc97 feat(widgets): prevent clicks in toggle from dismissing menu 2025-12-10 18:17:39 +02:00
Elian Doran
8fa9c25f2a feat(widgets): menu item with toggle 2025-12-10 18:07:38 +02:00
Elian Doran
84bde62e05 New layout improvements (#8012) 2025-12-10 17:50:31 +02:00
Elian Doran
5bb4621097 chore(layout): address requested changes 2025-12-10 17:42:08 +02:00
Elian Doran
f1edf84f4d fix(layout): title background for code notes 2025-12-10 17:13:52 +02:00
Elian Doran
f7955a9040 fix(client/dropdown): tooltip flickering due to child elements 2025-12-10 17:02:11 +02:00
Elian Doran
7c5df21685 feat(note_actions): group development options 2025-12-10 16:51:07 +02:00
Elian Doran
2060bb8cdd feat(breadcrumb): show note preview 2025-12-10 16:14:40 +02:00
Elian Doran
a9b4e7b1e2 style(layout): apply heavy padding to title only in normal view 2025-12-10 16:11:17 +02:00
Elian Doran
82528c4478 style(layout): slightly smaller note title in full-height note type 2025-12-10 15:23:42 +02:00
Zexin Yuan
4dcfc3e0bc chore: add dev shell and direnv support 2025-12-10 21:17:11 +08:00
Elian Doran
999315d3c6 feat(breadcrumb): basic rename note support 2025-12-10 15:16:45 +02:00
Elian Doran
aef0b03c34 feat(breadcrumb_row): collapse badges sooner 2025-12-10 13:38:15 +02:00
Elian Doran
49f008c46f feat(breadcrumb_row): improve button fit on constrained width 2025-12-10 13:37:07 +02:00
Elian Doran
bd81db4117 feat(breadcrumb_row): improve badge fit on constrained width 2025-12-10 13:33:33 +02:00
Elian Doran
9f274883e3 feat(breadcrumb_badges): basic shrink support 2025-12-10 13:06:25 +02:00
Elian Doran
07b76b80f4 feat(layout): hide note details in attachment view 2025-12-10 12:52:03 +02:00
Elian Doran
0014f0a88d feat(layout): minor improvements to title/icon alignment 2025-12-10 12:50:05 +02:00
Elian Doran
63f7a78d31 chore(note_actions): use dedicated translation for note revisions 2025-12-10 12:46:23 +02:00
Elian Doran
e556c090ff fix(ribbon): attribute details not shown in new layout 2025-12-10 12:40:06 +02:00
Elian Doran
c4f483c250 feat(options/advanced): automatically refresh 2025-12-10 12:29:12 +02:00
Elian Doran
4031332b98 feat(note_title_details): tooltips for values 2025-12-10 12:25:38 +02:00
Elian Doran
10cb7c8d6a feat(note_title_details): hide creation dates on hidden notes 2025-12-10 12:10:32 +02:00
Elian Doran
be190bfe33 feat(layout): improve layout for full-height notes 2025-12-10 12:06:05 +02:00
Elian Doran
4d7d642952 fix(layout): floating toolbar displayed in attachments 2025-12-10 11:58:17 +02:00
Elian Doran
737711e5eb fix(layout): weird title in full-width & attachments 2025-12-10 11:56:34 +02:00
Elian Doran
42fc128f97 chore(breadcrumb_badges/backlinks): display actual count of backlinks 2025-12-10 11:51:09 +02:00
Elian Doran
b03e6c3b19 chore(breadcrumb_badges/backlinks): display list of backlinks on click 2025-12-10 11:41:14 +02:00
Elian Doran
66008489c4 chore(breadcrumb_badges): fake backlink widget 2025-12-10 11:21:06 +02:00
Elian Doran
3262e3490a feat(breadcrumb_badges): integrate into quick edit 2025-12-10 11:10:26 +02:00
Elian Doran
16a73b0848 fix(popup_editor): wrong margin for title 2025-12-10 11:03:12 +02:00
Elian Doran
52bb4d7a0e feat(breadcrumb_badges): make badge not wrap-around 2025-12-10 09:52:46 +02:00
Elian Doran
40b5e4d549 feat(breadcrumb_badges): proper link handling support 2025-12-10 09:47:05 +02:00
Elian Doran
b014ea8950 feat(breadcrumb_badges): add colors to the badges 2025-12-10 09:38:55 +02:00
Elian Doran
61592716f9 feat(breadcrumb_badges): add tooltips for the badges 2025-12-10 09:27:44 +02:00
Elian Doran
efe7fc0ee7 chore(layout): hide breadcrumb badges if not on new layout 2025-12-10 09:12:57 +02:00
Elian Doran
a810db3641 feat(breadcrumb_badges): display badge when editing is unlocked 2025-12-10 09:11:28 +02:00
Elian Doran
f8b292dfa3 Experimental layout (#8005) 2025-12-09 23:35:15 +02:00
Elian Doran
fc2ab91280 feat(options/advanced): add description for experimental 2025-12-09 23:16:30 +02:00
Elian Doran
668ee219c6 chore(layout): use translation for badges 2025-12-09 23:02:21 +02:00
Elian Doran
ee6512a1a6 refactor(layout): align name for breadcrumb badges 2025-12-09 23:00:41 +02:00
Elian Doran
fe1f590286 chore(layout): use translations for note title details 2025-12-09 23:00:02 +02:00
Elian Doran
876e8f843a chore(layout): use i18n for options 2025-12-09 22:58:16 +02:00
Elian Doran
a45c1a1dc8 chore(layout): fix regressions after merge 2025-12-09 22:57:36 +02:00
Elian Doran
f8377169e6 Merge remote-tracking branch 'origin/main' into feature/new_layout 2025-12-09 22:46:18 +02:00
Elian Doran
a197a33d35 chore(experimental_features): address review 2025-12-09 22:43:39 +02:00
Elian Doran
3060207d04 feat(layout): created & modification date 2025-12-09 22:22:28 +02:00
Elian Doran
28c1d0b3f5 feat(layout): indicate clickable badges 2025-12-09 21:50:38 +02:00
Elian Doran
644d051477 feat(layout): add shared badge 2025-12-09 21:44:39 +02:00
Elian Doran
f42031c8de feat(layout): add icon to the badge 2025-12-09 21:11:30 +02:00
Elian Doran
6b50d9b087 feat(layout): implement read-only badge 2025-12-09 21:06:28 +02:00
Elian Doran
a0f0da64b4 feat(layout): new icon for note actions & fix padding 2025-12-09 20:59:28 +02:00
Elian Doran
1e72ebd104 feat(layout): move revisions button to note actions 2025-12-09 20:48:54 +02:00
Elian Doran
1184a95697 style(layout): missed bottom border in ribbon buttons 2025-12-09 20:44:58 +02:00
Elian Doran
cd0e4a5678 feat(layout): move fixed formatting toolbar above 2025-12-09 20:36:48 +02:00
Elian Doran
394f6c3110 feat(layout): respect content width for title 2025-12-09 20:22:31 +02:00
Elian Doran
e2b6d0c256 feat(layout): move the note title into the scrollable region 2025-12-09 20:09:06 +02:00
Elian Doran
fe7ca210dd feat(layout): move the note actions into the breadcrumb area 2025-12-09 20:05:42 +02:00
Elian Doran
e58d6bf2a3 feat(layout): reverse the layout of the ribbon 2025-12-09 19:51:53 +02:00
Elian Doran
460d20d6b2 feat(layout): move ribbon to the bottom as experimental layout 2025-12-09 19:42:37 +02:00
Elian Doran
ae154212fe feat(client/server): basic support for experimental features 2025-12-09 19:34:03 +02:00
Elian Doran
28bb4edbac chore(layout): revert work on floating panel 2025-12-09 19:18:48 +02:00
Elian Doran
1ceed1b47b chore(layout): revert work on floating panel 2025-12-09 19:11:27 +02:00
Elian Doran
9445e64c2e Translations update from Hosted Weblate (#8004) 2025-12-09 17:49:32 +02:00
Matheus Fongaro (MatioZG)
e6fba03ba7 Translated using Weblate (Portuguese (Brazil))
Currently translated at 3.2% (5 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/pt_BR/
2025-12-09 16:16:40 +01:00
green
b027ca5c09 Translated using Weblate (Japanese)
Currently translated at 100.0% (1646 of 1646 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-12-09 16:16:39 +01:00
Elian Doran
e98df30500 fix(popup-editor): broken title bar layout 2025-12-09 17:16:29 +02:00
Elian Doran
111c44dadf fix(content_header): z-index issues 2025-12-09 17:11:28 +02:00
Elian Doran
cb31c25e6c fix(content_header): note icon dropdown broken when scrolling 2025-12-09 17:08:03 +02:00
Elian Doran
5d59c953c2 fix(layout): title row background in settings 2025-12-09 16:39:31 +02:00
Elian Doran
a2cff42981 fix(layout): scrollbar design in code note 2025-12-09 16:31:47 +02:00
Elian Doran
cae892a971 fix(layout): title not visible on dark code theme 2025-12-09 16:27:19 +02:00
Elian Doran
f8447d923e feat(ribbon): hide when in options 2025-12-09 16:22:03 +02:00
Elian Doran
3b8dabc9d2 Back/forward navigation in tab bar (#8003) 2025-12-09 16:11:17 +02:00
Elian Doran
cda39e967c chore(tab_navigation): address requested changes 2025-12-09 16:02:24 +02:00
Elian Doran
7da9367dc9 fix(tab_navigation): affecting server and mobile views 2025-12-09 15:59:15 +02:00
Elian Doran
82d97ef26f feat(tab_navigation): hide buttons if launcher ones are used 2025-12-09 15:30:46 +02:00
Elian Doran
9e094f1d96 feat(tab_navigation): add it to horizontal layout as well 2025-12-09 15:14:13 +02:00
Elian Doran
da7e15c268 refactor(tab_navigation): sort imports 2025-12-09 15:06:54 +02:00
Elian Doran
24806a810c feat(tab_navigation): display note icon in history navigation 2025-12-09 15:02:52 +02:00
Elian Doran
a2ace4510a Translations update from Hosted Weblate (#8000) 2025-12-09 15:00:26 +02:00
Elian Doran
5c8132088f feat(client): use chevrons to display note path 2025-12-09 14:49:19 +02:00
Elian Doran
7ee060b228 feat(tab_navigation): improve indicator for current item in context menu 2025-12-09 14:41:17 +02:00
Elian Doran
4b2a4b8f7b feat(tab_navigation): reflect state of history by disabling the buttons 2025-12-09 14:29:45 +02:00
Elian Doran
5a668ede01 chore(tab_navigation): reintroduce history cleaning 2025-12-09 14:24:39 +02:00
Elian Doran
9e099444b6 feat(tab_navigation): functional context menu 2025-12-09 14:23:06 +02:00
Elian Doran
e3f5b3535a feat(tab_navigation): functional back/forward buttons 2025-12-09 14:10:11 +02:00
Elian Doran
346ad1e8a3 feat(tab_navigation): add the buttons on vertical layout 2025-12-09 14:06:57 +02:00
Elian Doran
2a9558e9c5 style(ribbon): make icons slightly bigger 2025-12-09 13:18:25 +02:00
Hosted Weblate
c324f66aef Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2025-12-09 12:15:20 +01:00
Elian Doran
e688f2cdb6 Add breadcrumbs to navigation (#7995) 2025-12-09 13:15:03 +02:00
Elian Doran
2ff8762a22 chore(client): fix typecheck 2025-12-09 13:07:41 +02:00
Elian Doran
4d75221938 chore(breadcrumbs): address requested changes 2025-12-09 12:41:54 +02:00
Elian Doran
658b699b71 fix(collections/geomap): fake floating buttons mispositioned 2025-12-09 12:24:28 +02:00
Elian Doran
72b0d03546 chore(layout): remove some title margins 2025-12-09 12:23:05 +02:00
Elian Doran
19980807f2 style(ribbon): fix some alignment issues & decrease button size 2025-12-09 12:20:27 +02:00
Elian Doran
3514e3d057 fix(floating_buttons): wrong position when at the top of the note 2025-12-09 11:46:16 +02:00
Elian Doran
fb6c82740c chore(ribbon): remove top transition completely 2025-12-09 11:38:04 +02:00
Elian Doran
8df5a010c9 fix(ribbon): note buttons cut off 2025-12-09 11:36:00 +02:00
Elian Doran
895e9b8bf0 chore(client/layout): reduce transitions 2025-12-09 11:30:18 +02:00
Elian Doran
bfcf85e0d2 fix(client/layout): content header height not properly resized when switching notes 2025-12-09 11:27:05 +02:00
Elian Doran
5770222304 fix(client/floating_buttons): clipped by floating content header 2025-12-09 11:08:28 +02:00
Elian Doran
d5d2815bdf fix(client/floating_buttons): clipped by ribbon 2025-12-09 09:20:31 +02:00
Elian Doran
7fc3d413e5 fix(client): 1px scroll in full-height note 2025-12-09 09:13:18 +02:00
Elian Doran
474228b630 style(client): remove bottom border & box-shadow for content header 2025-12-09 08:22:51 +02:00
Elian Doran
0805e077a1 feat(ribbon): basic implementation for scroll pinning 2025-12-09 08:18:27 +02:00
Elian Doran
6b059a9a75 feat(ribbon): context menu for root item 2025-12-09 08:01:52 +02:00
Elian Doran
7377e4e34d chore(ribbon): improve paddings slightly 2025-12-09 07:58:59 +02:00
Elian Doran
6fac947d9c chore(ribbon): address requested changes 2025-12-09 07:50:21 +02:00
Elian Doran
5973e5ca26 chore(ribbon): remove label for the root entirely 2025-12-09 07:46:46 +02:00
Elian Doran
608ab53933 chore(ribbon): reduce note title padding 2025-12-08 23:41:17 +02:00
Elian Doran
05679f7a8d feat(ribbon): prototype sticky ribbon 2025-12-08 23:33:55 +02:00
Elian Doran
fcf51ec6da chore(eslint): apply to .tsx as well 2025-12-08 23:14:09 +02:00
Elian Doran
d15b5f8cbc style(next): basic styling of ribbon as a floating toolbar 2025-12-08 23:13:58 +02:00
Elian Doran
ef3cbcac6d refactor(breadcrumb): fix eslint issues 2025-12-08 23:09:00 +02:00
Elian Doran
b16893c4d2 feat(breadcrumb): collapse items if the list is too big 2025-12-08 23:03:31 +02:00
Elian Doran
a365814aaa feat(breadcrumb): improve overflow slightly 2025-12-08 22:54:31 +02:00
Elian Doran
eca2116adc feat(breadcrumb): make the root note clickable 2025-12-08 22:46:04 +02:00
Elian Doran
4cfa403657 feat(breadcrumb): apply ellipsis to dropdown 2025-12-08 22:40:37 +02:00
Elian Doran
70ded4c2cd chore(breadcrumb): use bold for highlighting active entry 2025-12-08 22:38:06 +02:00
Elian Doran
3fe45db6ef feat(breadcrumb): improve overflow support 2025-12-08 22:17:49 +02:00
Elian Doran
11467775b6 feat(breadcrumb): basic overflow support 2025-12-08 22:09:46 +02:00
Elian Doran
1e5fcf635e feat(breadcrumb): show root title if it's the one active 2025-12-08 22:05:09 +02:00
Elian Doran
223ba4643f fix(breadcrumb): breadcrumb shown if there are no children 2025-12-08 21:57:51 +02:00
Elian Doran
200fd76929 feat(breadcrumb): display a checkbox for the current note 2025-12-08 21:52:36 +02:00
Elian Doran
c5c4ecd6e6 feat(breadcrumb): show current item 2025-12-08 17:04:08 +02:00
Elian Doran
bedca9f82c feat(breadcrumb): hide root icon 2025-12-08 16:45:26 +02:00
Elian Doran
adc356eff3 fix(breadcrumb): navigation on first level not working 2025-12-08 16:41:06 +02:00
Elian Doran
c4285772b3 feat(breadcrumb): basic navigation in separator 2025-12-08 16:34:40 +02:00
Elian Doran
a02235f2bd feat(breadcrumb): set up dropdown 2025-12-08 16:03:12 +02:00
Elian Doran
5f215b14c2 feat(breadcrumb): start implementing interactive breadcrumbs 2025-12-08 16:01:34 +02:00
Elian Doran
6e29fe8d58 feat(breadcrumb): hide preview 2025-12-08 15:56:03 +02:00
Elian Doran
43ceb1982d feat(breadcrumb): hide last note 2025-12-08 15:53:17 +02:00
Elian Doran
d02ec47d77 feat(breadcrumb): get breadcrumb to render 2025-12-08 15:16:06 +02:00
Elian Doran
9942950710 feat(layout): relocate title into scrollable region 2025-12-08 14:54:57 +02:00
145 changed files with 6402 additions and 3099 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

View File

@@ -86,12 +86,12 @@ jobs:
- name: Upload Playwright trace
if: failure()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: Playwright trace (${{ matrix.dockerfile }})
path: test-output/playwright/output
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v6
if: ${{ !cancelled() }}
with:
name: Playwright report (${{ matrix.dockerfile }})
@@ -213,7 +213,7 @@ jobs:
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: digests-${{ env.PLATFORM_PAIR }}-${{ matrix.dockerfile }}
path: /tmp/digests/*
@@ -227,7 +227,7 @@ jobs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
path: /tmp/digests
pattern: digests-*

View File

@@ -102,7 +102,7 @@ jobs:
name: Nightly Build
- name: Publish artifacts
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
if: ${{ github.event_name == 'pull_request' }}
with:
name: TriliumNotes ${{ matrix.os.name }} ${{ matrix.arch }}

View File

@@ -77,7 +77,7 @@ jobs:
- name: Upload test report
if: failure()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: e2e report ${{ matrix.arch }}
path: apps/server-e2e/test-output

View File

@@ -73,7 +73,7 @@ jobs:
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
- name: Upload the artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: release-desktop-${{ matrix.os.name }}-${{ matrix.arch }}
path: apps/desktop/upload/*.*
@@ -100,7 +100,7 @@ jobs:
arch: ${{ matrix.arch }}
- name: Upload the artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: release-server-linux-${{ matrix.arch }}
path: upload/*.*
@@ -120,7 +120,7 @@ jobs:
docs/Release Notes
- name: Download all artifacts
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
merge-multiple: true
pattern: release-*

3
.gitignore vendored
View File

@@ -44,9 +44,10 @@ upload
.rollup.cache
*.tsbuildinfo
/.direnv
/result
.svelte-kit
# docs
site/
apps/*/coverage
apps/*/coverage

2
.nvmrc
View File

@@ -1 +1 @@
24.11.1
24.12.0

View File

@@ -37,6 +37,9 @@
"apps/server/src/assets/doc_notes/**": true,
"apps/edit-docs/demo/**": true
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.rules.customizations": [
{ "rule": "*", "severity": "warn" }
]

View File

@@ -9,13 +9,13 @@
"keywords": [],
"author": "Elian Doran <contact@eliandoran.me>",
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.24.0",
"packageManager": "pnpm@10.25.0",
"devDependencies": {
"@redocly/cli": "2.12.3",
"@redocly/cli": "2.12.6",
"archiver": "7.0.1",
"fs-extra": "11.3.2",
"react": "19.2.1",
"react-dom": "19.2.1",
"react": "19.2.3",
"react-dom": "19.2.3",
"typedoc": "0.28.15",
"typedoc-plugin-missing-exports": "4.1.2"
}

View File

@@ -44,7 +44,7 @@
"draggabilly": "3.0.0",
"force-graph": "1.51.0",
"globals": "16.5.0",
"i18next": "25.7.1",
"i18next": "25.7.2",
"i18next-http-backend": "3.0.2",
"jquery": "3.7.1",
"jquery.fancytree": "2.38.5",
@@ -60,7 +60,7 @@
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.28.0",
"react-i18next": "16.4.0",
"react-i18next": "16.5.0",
"reveal.js": "5.2.1",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",

View File

@@ -265,7 +265,7 @@ export type CommandMappings = {
reEvaluateRightPaneVisibility: CommandData;
runActiveNote: CommandData;
scrollContainerToCommand: CommandData & {
scrollContainerTo: CommandData & {
position: number;
};
scrollToEnd: CommandData;

View File

@@ -1,18 +1,19 @@
import protectedSessionHolder from "../services/protected_session_holder.js";
import server from "../services/server.js";
import utils from "../services/utils.js";
import appContext, { type EventData, type EventListener } from "./app_context.js";
import treeService from "../services/tree.js";
import Component from "./component.js";
import froca from "../services/froca.js";
import hoistedNoteService from "../services/hoisted_note.js";
import options from "../services/options.js";
import type { ViewScope } from "../services/link.js";
import type FNote from "../entities/fnote.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror";
import type FNote from "../entities/fnote.js";
import { closeActiveDialog } from "../services/dialog.js";
import froca from "../services/froca.js";
import hoistedNoteService from "../services/hoisted_note.js";
import type { ViewScope } from "../services/link.js";
import options from "../services/options.js";
import protectedSessionHolder from "../services/protected_session_holder.js";
import server from "../services/server.js";
import treeService from "../services/tree.js";
import utils from "../services/utils.js";
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
import appContext, { type EventData, type EventListener } from "./app_context.js";
import Component from "./component.js";
export interface SetNoteOpts {
triggerSwitchEvent?: unknown;

View File

@@ -6,7 +6,7 @@ import openService from "../services/open.js";
import protectedSessionService from "../services/protected_session.js";
import options from "../services/options.js";
import froca from "../services/froca.js";
import utils from "../services/utils.js";
import utils, { openInReusableSplit } from "../services/utils.js";
import toastService from "../services/toast.js";
import noteCreateService from "../services/note_create.js";
@@ -193,6 +193,16 @@ export default class RootCommandExecutor extends Component {
appContext.triggerEvent("zenModeChanged", { isEnabled });
}
async toggleRibbonTabNoteMapCommand() {
const { isExperimentalFeatureEnabled } = await import("../services/experimental_features.js");
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
if (!isNewLayout) return;
const activeContext = appContext.tabManager.getActiveContext();
if (!activeContext?.notePath) return;
openInReusableSplit(activeContext.notePath, "note-map");
}
firstTabCommand() {
this.#goToTab(1);
}

View File

@@ -64,6 +64,9 @@ function initOnElectron() {
if (options.get("nativeTitleBarVisible") !== "true") {
initTitleBarButtons(style, currentWindow);
}
// Clear navigation history on frontend refresh.
currentWindow.webContents.navigationHistory.clear();
}
function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {

View File

@@ -1,49 +1,57 @@
import { applyModals } from "./layout_commons.js";
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import type { AppContext } from "../components/app_context.js";
import type { WidgetsByParent } from "../services/bundle.js";
import { isExperimentalFeatureEnabled } from "../services/experimental_features.js";
import options from "../services/options.js";
import utils from "../services/utils.js";
import ApiLog from "../widgets/api_log.jsx";
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
import ContentHeader from "../widgets/containers/content_header.js";
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
import FindWidget from "../widgets/find.js";
import FlexContainer from "../widgets/containers/flex_container.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
import HighlightsListWidget from "../widgets/highlights_list.js";
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
import NoteIconWidget from "../widgets/note_icon.jsx";
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
import NoteList from "../widgets/collections/NoteList.jsx";
import NoteTitleWidget from "../widgets/note_title.jsx";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import options from "../services/options.js";
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
import QuickSearchWidget from "../widgets/quick_search.js";
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
import ContentHeader from "../widgets/containers/content_header.js";
import FlexContainer from "../widgets/containers/flex_container.js";
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import FindWidget from "../widgets/find.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import HighlightsListWidget from "../widgets/highlights_list.js";
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
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 StatusBar from "../widgets/layout/StatusBar.jsx";
import NoteIconWidget from "../widgets/note_icon.jsx";
import NoteTitleWidget from "../widgets/note_title.jsx";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import QuickSearchWidget from "../widgets/quick_search.js";
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
import { FixedFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.jsx";
import NoteActions from "../widgets/ribbon/NoteActions.jsx";
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
import ScrollPadding from "../widgets/scroll_padding.js";
import SearchResult from "../widgets/search_result.jsx";
import SharedInfo from "../widgets/shared_info.jsx";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import SqlResults from "../widgets/sql_result.js";
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
import TabRowWidget from "../widgets/tab_row.js";
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
import TocWidget from "../widgets/toc.js";
import type { AppContext } from "../components/app_context.js";
import type { WidgetsByParent } from "../services/bundle.js";
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import utils from "../services/utils.js";
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
import { applyModals } from "./layout_commons.js";
export default class DesktopLayout {
@@ -69,16 +77,31 @@ export default class DesktopLayout {
*/
const fullWidthTabBar = launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac);
const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows;
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
const titleRow = new FlexContainer("row")
.class("title-row")
.cssBlock(".title-row > * { margin: 5px; }")
.child(<NoteIconWidget />)
.child(<NoteTitleWidget />)
.optChild(isNewLayout, <NoteBadges />)
.optChild(!isNewLayout, <SpacerWidget baseSize={0} growthFactor={1} />)
.child(<MovePaneButton direction="left" />)
.child(<MovePaneButton direction="right" />)
.child(<ClosePaneButton />)
.child(<CreatePaneButton />)
.optChild(isNewLayout, <NoteActions />);
const rootContainer = new RootContainer(true)
.setParent(appContext)
.class((launcherPaneIsHorizontal ? "horizontal" : "vertical") + "-layout")
.class(`${launcherPaneIsHorizontal ? "horizontal" : "vertical" }-layout`)
.optChild(
fullWidthTabBar,
new FlexContainer("row")
.class("tab-row-container")
.child(new FlexContainer("row").id("tab-row-left-spacer"))
.optChild(launcherPaneIsHorizontal, <LeftPaneToggle isHorizontalLayout={true} />)
.child(<TabHistoryNavigationButtons />)
.child(new TabRowWidget().class("full-width"))
.optChild(customTitleBarButtons, <TitleBarButtons />)
.css("height", "40px")
@@ -101,7 +124,13 @@ export default class DesktopLayout {
new FlexContainer("column")
.id("rest-pane")
.css("flex-grow", "1")
.optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, <TitleBarButtons />).css("height", "40px"))
.optChild(!fullWidthTabBar,
new FlexContainer("row")
.child(<TabHistoryNavigationButtons />)
.child(new TabRowWidget())
.optChild(customTitleBarButtons, <TitleBarButtons />)
.css("height", "40px"))
.optChild(isNewLayout, <FixedFormattingToolbar />)
.child(
new FlexContainer("row")
.filling()
@@ -115,28 +144,17 @@ export default class DesktopLayout {
.child(
new SplitNoteContainer(() =>
new NoteWrapperWidget()
.child(
new FlexContainer("row")
.class("title-row")
.css("height", "50px")
.css("min-height", "50px")
.css("align-items", "center")
.cssBlock(".title-row > * { margin: 5px; }")
.child(<NoteIconWidget />)
.child(<NoteTitleWidget />)
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
.child(<MovePaneButton direction="left" />)
.child(<MovePaneButton direction="right" />)
.child(<ClosePaneButton />)
.child(<CreatePaneButton />)
)
.child(<Ribbon />)
.child(titleRow)
.optChild(!isNewLayout, <Ribbon><NoteActions /></Ribbon>)
.optChild(isNewLayout, <Ribbon />)
.child(new WatchedFileUpdateStatusWidget())
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
.child(
new ScrollingContainer()
.filling()
.child(new ContentHeader()
.optChild(isNewLayout, <InlineTitle />)
.optChild(isNewLayout, <NoteTitleActions />)
.optChild(!isNewLayout, new ContentHeader()
.child(<ReadOnlyNoteInfoBar />)
.child(<SharedInfo />)
)
@@ -157,6 +175,7 @@ export default class DesktopLayout {
)
)
.child(...this.customWidgets.get("center-pane"))
)
.child(
new RightPaneContainer()
@@ -165,8 +184,10 @@ export default class DesktopLayout {
.child(...this.customWidgets.get("right-pane"))
)
)
.optChild(!launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
)
)
.optChild(launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
.child(<CloseZenModeButton />)
// Desktop-specific dialogs.

View File

@@ -52,5 +52,5 @@ export function applyModals(rootContainer: RootContainer) {
.child(<IncorrectCpuArchDialog />)
.child(<PopupEditorDialog />)
.child(<CallToActionDialog />)
.child(<ToastContainer />)
.child(<ToastContainer />);
}

View File

@@ -0,0 +1,51 @@
import { t } from "./i18n";
import options from "./options";
export interface ExperimentalFeature {
id: string;
name: string;
description: string;
}
export const experimentalFeatures = [
{
id: "new-layout",
name: t("experimental_features.new_layout_name"),
description: t("experimental_features.new_layout_description"),
}
] as const satisfies ExperimentalFeature[];
export type ExperimentalFeatureId = typeof experimentalFeatures[number]["id"];
let enabledFeatures: Set<ExperimentalFeatureId> | null = null;
export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId): boolean {
return getEnabledFeatures().has(featureId);
}
export function getEnabledExperimentalFeatureIds() {
return getEnabledFeatures().values();
}
export async function toggleExperimentalFeature(featureId: ExperimentalFeatureId, enable: boolean) {
const features = new Set(getEnabledFeatures());
if (enable) {
features.add(featureId);
} else {
features.delete(featureId);
}
await options.save("experimentalFeatures", JSON.stringify(Array.from(features)));
}
function getEnabledFeatures() {
if (!enabledFeatures) {
let features: ExperimentalFeatureId[] = [];
try {
features = JSON.parse(options.get("experimentalFeatures")) as ExperimentalFeatureId[];
} catch (e) {
console.warn("Failed to parse experimental features from options:", e);
}
enabledFeatures = new Set(features);
}
return enabledFeatures;
}

View File

@@ -27,7 +27,7 @@ async function getLinkIcon(noteId: string, viewMode: ViewMode | undefined) {
return icon;
}
export type ViewMode = "default" | "source" | "attachments" | "contextual-help";
export type ViewMode = "default" | "source" | "attachments" | "contextual-help" | "note-map";
export interface ViewScope {
/**
@@ -99,7 +99,7 @@ async function createLink(notePath: string | undefined, options: CreateLinkOptio
const viewMode = viewScope.viewMode || "default";
let linkTitle = options.title;
if (!linkTitle) {
if (linkTitle === undefined) {
if (viewMode === "attachments" && viewScope.attachmentId) {
const attachment = await froca.getAttachment(viewScope.attachmentId);

View File

@@ -4,6 +4,8 @@ import froca from "./froca.js";
import hoistedNoteService from "./hoisted_note.js";
import appContext from "../components/app_context.js";
export const NOTE_PATH_TITLE_SEPARATOR = " ";
async function resolveNotePath(notePath: string, hoistedNoteId = "root") {
const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId);
@@ -254,7 +256,7 @@ async function getNotePathTitle(notePath: string) {
const titlePath = await getNotePathTitleComponents(notePath);
return titlePath.join(" / ");
return titlePath.join(NOTE_PATH_TITLE_SEPARATOR);
}
async function getNoteTitleWithPathAsSuffix(notePath: string) {

View File

@@ -1,5 +1,5 @@
import { dayjs } from "@triliumnext/commons";
import type { ViewScope } from "./link.js";
import type { ViewMode, ViewScope } from "./link.js";
import FNote from "../entities/fnote";
import { snapdom } from "@zumer/snapdom";
@@ -439,7 +439,20 @@ async function openInAppHelp($button: JQuery<HTMLElement>) {
* @param inAppHelpPage the ID of the help note (excluding the `_help_` prefix).
* @returns a promise that resolves once the help has been opened.
*/
export async function openInAppHelpFromUrl(inAppHelpPage: string) {
export function openInAppHelpFromUrl(inAppHelpPage: string) {
return openInReusableSplit(`_help_${inAppHelpPage}`, "contextual-help");
}
/**
* Similar to opening a new note in a split, but re-uses an existing split if there is already one open with the same view mode.
*
* @param targetNoteId the note ID to open in the split.
* @param targetViewMode the view mode of the split to open the note in.
* @param openOpts additional options for opening the note.
*/
export async function openInReusableSplit(targetNoteId: string, targetViewMode: ViewMode, openOpts: {
hoistedNoteId?: string;
} = {}) {
// Dynamic import to avoid import issues in tests.
const appContext = (await import("../components/app_context.js")).default;
const activeContext = appContext.tabManager.getActiveContext();
@@ -447,23 +460,20 @@ export async function openInAppHelpFromUrl(inAppHelpPage: string) {
return;
}
const subContexts = activeContext.getSubContexts();
const targetNote = `_help_${inAppHelpPage}`;
const helpSubcontext = subContexts.find((s) => s.viewScope?.viewMode === "contextual-help");
const viewScope: ViewScope = {
viewMode: "contextual-help",
};
if (!helpSubcontext) {
// The help is not already open, open a new split with it.
const existingSubcontext = subContexts.find((s) => s.viewScope?.viewMode === targetViewMode);
const viewScope: ViewScope = { viewMode: targetViewMode };
if (!existingSubcontext) {
// The target split is not already open, open a new split with it.
const { ntxId } = subContexts[subContexts.length - 1];
appContext.triggerCommand("openNewNoteSplit", {
ntxId,
notePath: targetNote,
hoistedNoteId: "_help",
notePath: targetNoteId,
hoistedNoteId: openOpts.hoistedNoteId,
viewScope
})
});
} else {
// There is already a help window open, make sure it opens on the right note.
helpSubcontext.setNote(targetNote, { viewScope });
// There is already a target split open, make sure it opens on the right note.
existingSubcontext.setNote(targetNoteId, { viewScope });
}
}

View File

@@ -24,8 +24,8 @@
--bs-body-font-family: var(--main-font-family) !important;
--bs-body-font-weight: var(--main-font-weight) !important;
--bs-body-color: var(--main-text-color) !important;
--bs-body-bg: var(--main-background-color) !important;
--ck-mention-list-max-height: 500px;
--bs-body-bg: var(--main-background-color) !important;
--ck-mention-list-max-height: 500px;
--tn-modal-max-height: 90vh;
--tree-item-light-theme-max-color-lightness: 50;
@@ -423,16 +423,16 @@ body.desktop .tabulator-popup-container,
pointer-events: none;
}
.dropdown-menu .disabled .disabled-tooltip {
.dropdown-menu .disabled .contextual-help {
pointer-events: all;
margin-inline-start: 8px;
font-size: 0.75rem;
color: var(--disabled-tooltip-icon-color);
color: var(--contextual-help-icon-color);
cursor: help;
opacity: 0.75;
}
.dropdown-menu .disabled .disabled-tooltip:hover {
.dropdown-menu .disabled .contextual-help:hover {
opacity: 1;
}
@@ -471,7 +471,7 @@ body.mobile .dropdown .dropdown-submenu > span {
padding-inline-start: 12px;
}
.dropdown-menu kbd {
.dropdown-menu kbd {
color: var(--muted-text-color);
border: none;
background-color: transparent;
@@ -487,7 +487,7 @@ body.mobile .dropdown .dropdown-submenu > span {
border: 1px solid transparent !important;
}
/* This is a workaround for Firefox not supporting break-before / break-after: avoid on columns.
/* This is a workaround for Firefox not supporting break-before / break-after: avoid on columns.
* It usually wraps a menu item followed by a separator / header and another menu item. */
.dropdown-no-break {
break-inside: avoid;
@@ -1315,12 +1315,18 @@ body.desktop li.dropdown-submenu:hover > ul.dropdown-menu {
top: 0;
inset-inline-start: calc(100% - 2px); /* -2px, otherwise there's a small gap between menu and submenu where the hover can disappear */
margin-top: -10px;
min-width: 15rem;
min-width: max-content;
max-width: 300px;
/* to make submenu scrollable https://github.com/zadam/trilium/issues/3136 */
max-height: 600px;
overflow: auto;
}
.dropdown-submenu.dropstart > .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);
}
@@ -1367,6 +1373,10 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
background-color: var(--scrollbar-background-color);
}
::-webkit-scrollbar-button {
display: none;
}
::-webkit-scrollbar-corner {
background-color: inherit;
}
@@ -1591,7 +1601,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
inset-inline-start: 0;
inset-inline-end: 0;
margin: 0 !important;
max-height: 85vh;
max-height: 85vh;
display: flex;
}
@@ -2093,7 +2103,7 @@ body.zen .note-split.type-text .scrolling-container {
body.zen:not(.backdrop-effects-disabled) .note-split.type-text .scrolling-container {
--padding-top: 50px; /* Should be enough to cover the title row */
padding-top: var(--padding-top);
scroll-padding-top: var(--padding-top);
}
@@ -2365,7 +2375,7 @@ footer.webview-footer button {
margin-bottom: 0;
}
.admonition::before {
.admonition::before {
color: var(--accent-color);
font-family: boxicons !important;
position: absolute;
@@ -2391,7 +2401,7 @@ footer.webview-footer button {
.ck-content ul.todo-list li:has(> span.todo-list__label input[type="checkbox"]:checked) > span.todo-list__label span.todo-list__label__description {
text-decoration: line-through;
opacity: 0.6;
opacity: 0.6;
}
.chat-options-container {
@@ -2524,6 +2534,7 @@ iframe.print-iframe {
position: relative;
flex-grow: 1;
width: 100%;
overflow: hidden;
}
/* Calendar collection */
@@ -2538,7 +2549,7 @@ iframe.print-iframe {
body.mobile {
.split-note-container-widget {
flex-direction: column !important;
.note-split {
width: 100%;
}
@@ -2553,4 +2564,10 @@ iframe.print-iframe {
opacity: 0.4;
}
}
}
}
body.desktop .title-row {
height: 50px;
min-height: 50px;
align-items: center;
}

View File

@@ -19,7 +19,7 @@
--dropdown-border-color: #555;
--dropdown-shadow-opacity: 0.4;
--dropdown-item-icon-destructive-color: #de6e5b;
--disabled-tooltip-icon-color: #7fd2ef;
--contextual-help-icon-color: #7fd2ef;
--accented-background-color: #555;
--more-accented-background-color: #777;
@@ -115,7 +115,3 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
.use-note-color {
--custom-color: var(--dark-theme-custom-color);
}
span.fancytree-active {
color: var(--dark-theme-custom-color);
}

View File

@@ -23,7 +23,7 @@ html {
--dropdown-border-color: #ccc;
--dropdown-shadow-opacity: 0.2;
--dropdown-item-icon-destructive-color: #ec5138;
--disabled-tooltip-icon-color: #004382;
--contextual-help-icon-color: #004382;
--accented-background-color: #f5f5f5;
--more-accented-background-color: #ddd;
@@ -99,7 +99,3 @@ html {
.use-note-color {
--custom-color: var(--light-theme-custom-color);
}
span.fancytree-active {
color: var(--light-theme-custom-color);
}

View File

@@ -6,7 +6,7 @@
*/
:root {
/*
/*
* ⚠️ NOTICE: This theme is currently in the beta stage of development.
* The names and purposes of these CSS variables are subject to frequent changes.
*/
@@ -22,7 +22,7 @@
--dropdown-border-color: #404040;
--dropdown-shadow-opacity: 0.6;
--dropdown-item-icon-destructive-color: #de6e5b;
--disabled-tooltip-icon-color: #7fd2ef;
--contextual-help-icon-color: #7fd2ef;
--accented-background-color: #555;
@@ -182,7 +182,7 @@
--tab-close-button-hover-background: #a45353;
--tab-close-button-hover-color: white;
--active-tab-background-color: #ffffff1c;
--active-tab-hover-background-color: var(--active-tab-background-color);
--active-tab-icon-color: #a9a9a9;
@@ -201,7 +201,7 @@
--promoted-attribute-card-background-color: #ffffff21;
--promoted-attribute-card-shadow: none;
--floating-button-shadow-color: #00000080;
--floating-button-background-color: #494949d2;
--floating-button-color: var(--button-text-color);
@@ -226,7 +226,7 @@
--scrollbar-border-color: unset; /* Deprecated */
--selection-background-color: #3399FF70;
--link-color: lightskyblue;
--mermaid-theme: dark;
@@ -320,4 +320,4 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
.use-note-color {
--custom-color: var(--dark-theme-custom-color);
}
}

View File

@@ -6,7 +6,7 @@
*/
:root {
/*
/*
* ⚠️ NOTICE: This theme is currently in the beta stage of development.
* The names and purposes of these CSS variables are subject to frequent changes.
*/
@@ -22,7 +22,7 @@
--dropdown-border-color: #ccc;
--dropdown-shadow-opacity: 0.2;
--dropdown-item-icon-destructive-color: #ec5138;
--disabled-tooltip-icon-color: #004382;
--contextual-help-icon-color: #004382;
--accented-background-color: #f5f5f5;
@@ -138,7 +138,7 @@
/* Deprecated: now local variables in #launcher, with the values dependent on the current layout. */
--launcher-pane-background-color: unset;
--launcher-pane-text-color: unset;
--launcher-pane-vert-background-color: #e8e8e8;
--launcher-pane-vert-text-color: #000000bd;
--launcher-pane-vert-button-hover-color: black;
@@ -174,7 +174,7 @@
--tab-close-button-hover-background: #c95a5a;
--tab-close-button-hover-color: white;
--active-tab-background-color: white;
--active-tab-hover-background-color: var(--active-tab-background-color);
--active-tab-icon-color: gray;
@@ -291,4 +291,4 @@
--modal-background-color: hsl(var(--custom-color-hue), 56%, 96%);
--modal-border-color: hsl(var(--custom-color-hue), 33%, 41%);
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 40%, 88%);
}
}

View File

@@ -89,13 +89,13 @@
* the color is adjusted based on the current color scheme (light or dark). The lightness
* component of the color represented in the CIELAB color space, will be
* constrained to a certain percentage defined below.
*
*
* Note: the tree background may vary when background effects are enabled, so it is recommended
* to maintain a higher contrast margin than on the usual note tree solid background. */
/* The maximum perceptual lightness for the custom color in the light theme (%): */
--tree-item-light-theme-max-color-lightness: 60;
/* The minimum perceptual lightness for the custom color in the dark theme (%): */
--tree-item-dark-theme-min-color-lightness: 65;
}
@@ -165,7 +165,7 @@ body.desktop .dropdown-submenu .dropdown-menu {
--menu-item-start-padding: 8px;
--menu-item-end-padding: 22px;
--menu-item-vertical-padding: 2px;
padding-top: var(--menu-item-vertical-padding) !important;
padding-bottom: var(--menu-item-vertical-padding) !important;
padding-inline-start: var(--menu-item-start-padding) !important;
@@ -176,6 +176,11 @@ body.desktop .dropdown-submenu .dropdown-menu {
cursor: default !important;
}
.dropdown-menu:has(> .dropdown-submenu.dropstart) > .dropdown-item {
padding-inline-end: var(--menu-item-start-padding) !important;
padding-inline-start: var(--menu-item-end-padding) !important;
}
:root .dropdown-item:focus-visible {
outline: 2px solid var(--input-focus-outline-color) !important;
background-color: transparent;
@@ -249,7 +254,7 @@ html body .dropdown-item[disabled] {
}
/* Menu item arrow */
.dropdown-menu .dropdown-toggle::after {
.dropdown-submenu:not(.dropstart) .dropdown-toggle::after {
content: "\ed3b" !important;
position: absolute;
display: flex !important;
@@ -265,6 +270,22 @@ html body .dropdown-item[disabled] {
color: var(--menu-item-arrow-color) !important;
}
.dropdown-submenu.dropstart .dropdown-toggle::before {
content: "\ea4d" !important;
position: absolute;
display: flex !important;
align-items: center;
justify-content: center;
top: 0;
inset-inline-start: 0;
margin: unset !important;
border: unset !important;
padding: 0 4px;
font-family: boxicons;
font-size: 1.2em;
color: var(--menu-item-arrow-color) !important;
}
body[dir=rtl] .dropdown-menu:not([data-popper-placement="bottom-start"]) .dropdown-toggle::after {
content: "\ea4d" !important;
}
@@ -339,7 +360,7 @@ body.mobile .dropdown-menu {
font-size: 1em !important;
backdrop-filter: var(--dropdown-backdrop-filter);
position: relative;
.dropdown-toggle::after {
top: 0.5em;
right: var(--dropdown-menu-padding-horizontal);
@@ -356,7 +377,7 @@ body.mobile .dropdown-menu {
padding: var(--dropdown-menu-padding-vertical) var(--dropdown-menu-padding-horizontal) !important;
background: var(--card-background-color);
border-bottom: 1px solid var(--menu-item-delimiter-color) !important;
border-radius: 0;
border-radius: 0;
}
.dropdown-item:first-of-type,
@@ -367,9 +388,9 @@ body.mobile .dropdown-menu {
border-top-right-radius: 6px;
}
.dropdown-item:last-of-type,
.dropdown-item:last-of-type,
.dropdown-item:has(+ .dropdown-divider),
.dropdown-custom-item:last-of-type,
.dropdown-custom-item:last-of-type,
.dropdown-custom-item:has(+ .dropdown-divider) {
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
@@ -392,10 +413,10 @@ body.mobile .dropdown-menu {
--menu-background-color: --menu-submenu-mobile-background-color;
--bs-dropdown-divider-margin-y: 0.25rem;
border-radius: 0;
max-height: 0;
max-height: 0;
transition: max-height 100ms ease-in;
display: block !important;
display: block !important;
&.show {
max-height: 1000px;
padding: 0.5rem 0.75rem !important;
@@ -405,7 +426,7 @@ body.mobile .dropdown-menu {
&.submenu-open {
.dropdown-toggle {
padding-bottom: var(--dropdown-menu-padding-vertical);
}
}
}
}
@@ -743,4 +764,4 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
.note-detail-empty .aa-suggestions div.aa-cursor {
background: var(--hover-item-background-color);
color: var(--hover-item-text-color);
}
}

View File

@@ -154,7 +154,7 @@ button.btn.btn-success kbd {
color: var(--button-group-active-button-text-color);
}
/*
/*
* Input boxes
*/
@@ -399,7 +399,8 @@ button.select-button.dropdown-toggle.btn:active {
select:focus,
select.form-select:focus,
select.form-control:focus,
.select-button.dropdown-toggle.btn:focus {
.select-button.dropdown-toggle.btn:focus,
.select-button.focus-outline:focus {
box-shadow: unset;
outline: 3px solid var(--input-focus-outline-color);
outline-offset: 0;
@@ -422,7 +423,7 @@ optgroup {
line-height: 40px;
}
/*
/*
* File input
*
* <label class="tn-file-input tn-input-field">
@@ -784,4 +785,4 @@ input[type="range"] {
scrollbar-color: unset;
scrollbar-width: unset;
}
}
}

View File

@@ -164,7 +164,7 @@ ul.editability-dropdown li.dropdown-item > div {
background: var(--cmd-button-hover-background-color);
}
/*
/*
* Note info
*/
@@ -177,4 +177,4 @@ ul.editability-dropdown li.dropdown-item > div {
/* Narrow width layout */
.note-info-widget {
container: info-section / inline-size;
}
}

View File

@@ -502,7 +502,7 @@ div.bookmark-folder-widget .note-link .bx {
font-size: 1.2em;
}
/*
/*
* QUICK SEARCH BOX
*/
@@ -613,7 +613,7 @@ div.quick-search .dropdown-menu {
* As a temporary workaround, the backdrop and transparency are disabled for the
* vertical layout.
*/
body.layout-vertical.background-effects div.quick-search .dropdown-menu {
body.layout-vertical.background-effects div.quick-search .dropdown-menu {
--menu-background-color: var(--menu-background-color-no-backdrop) !important;
}
@@ -945,12 +945,26 @@ body.electron.background-effects.layout-horizontal .tab-row-container .toggle-bu
position: absolute;
bottom: 0;
inset-inline-start: -10px;
inset-inline-end: -10px;
inset-inline-end: -6px;
top: 32px;
height: 1px;
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
}
body.electron.background-effects.layout-horizontal .tab-row-container .tab-history-navigation-buttons {
position: relative;
&:after {
content: "";
position: absolute;
bottom: 0;
inset-inline-start: 0;
inset-inline-end: -7px;
height: 1px;
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
}
}
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-left,
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-right {
position: relative;
@@ -1569,7 +1583,7 @@ div.floating-buttons .show-floating-buttons-button {
div.floating-buttons .show-floating-buttons-button::before {
animation: floating-buttons-show-hide-button-animation 400ms ease-out;
}
div.floating-buttons .show-floating-buttons-button:hover,
div.floating-buttons .show-floating-buttons-button:active {
box-shadow: var(--floating-button-show-button-hover-shadow);
@@ -1831,7 +1845,7 @@ div.find-replace-widget div.find-widget-found-wrapper > span {
.excalidraw .dropdown-menu {
border: unset !important;
box-shadow: unset !important;
box-shadow: unset !important;
background-color: transparent !important;
--island-bg-color: var(--menu-background-color);
--shadow-island: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
@@ -1850,4 +1864,4 @@ div.find-replace-widget div.find-widget-found-wrapper > span {
.excalidraw .dropdown-menu:before {
content: unset !important;
}
}

View File

@@ -1,10 +1,12 @@
import utils from "../services/utils.js";
import { NoteType } from "@triliumnext/commons";
import FAttribute from "../entities/fattribute.js";
import FBlob from "../entities/fblob.js";
import FBranch from "../entities/fbranch.js";
import FNote from "../entities/fnote.js";
import froca from "../services/froca.js";
import FAttribute from "../entities/fattribute.js";
import noteAttributeCache from "../services/note_attribute_cache.js";
import FBranch from "../entities/fbranch.js";
import FBlob from "../entities/fblob.js";
import utils from "../services/utils.js";
type AttributeDefinitions = { [key in `#${string}`]: string; };
type RelationDefinitions = { [key in `~${string}`]: string; };
@@ -12,6 +14,7 @@ type RelationDefinitions = { [key in `~${string}`]: string; };
interface NoteDefinition extends AttributeDefinitions, RelationDefinitions {
id?: string | undefined;
title: string;
type?: NoteType;
children?: NoteDefinition[];
content?: string;
}
@@ -45,7 +48,7 @@ export function buildNote(noteDef: NoteDefinition) {
const note = new FNote(froca, {
noteId: noteDef.id ?? utils.randomString(12),
title: noteDef.title,
type: "text",
type: noteDef.type ?? "text",
mime: "text/html",
isProtected: false,
blobId: ""

View File

@@ -693,7 +693,10 @@
"convert_into_attachment_successful": "笔记 '{{title}}' 已成功转换为附件。",
"convert_into_attachment_prompt": "确定要将笔记 '{{title}}' 转换为父笔记的附件吗?",
"print_pdf": "导出为 PDF...",
"open_note_on_server": "在服务器上打开笔记"
"open_note_on_server": "在服务器上打开笔记",
"view_revisions": "笔记修订...",
"note_map": "笔记地图",
"advanced": "高级"
},
"onclick_button": {
"no_click_handler": "按钮组件'{{componentId}}'没有定义点击处理程序"
@@ -1577,7 +1580,13 @@
"printing_pdf": "正在导出为PDF…"
},
"note_title": {
"placeholder": "请输入笔记标题..."
"placeholder": "请输入笔记标题...",
"created_on": "建立于 <Value />",
"last_modified": "最后修改于 <Value />",
"note_type_switcher_label": "从 {{type}} 切换到:",
"note_type_switcher_others": "更多笔记类型",
"note_type_switcher_templates": "模板",
"note_type_switcher_collection": "集合"
},
"search_result": {
"no_notes_found": "没有找到符合搜索条件的笔记。",
@@ -1939,8 +1948,9 @@
"unknown_widget": "未知组件:\"{{id}}\"."
},
"note_language": {
"not_set": "设置",
"configure-languages": "设置语言..."
"not_set": "设置语言",
"configure-languages": "设置语言...",
"help-on-languages": "内容语言帮助..."
},
"content_language": {
"title": "内容语言",
@@ -2007,7 +2017,7 @@
"book_properties_config": {
"hide-weekends": "隐藏周末",
"display-week-numbers": "显示周数",
"map-style": "地图样式",
"map-style": "地图样式",
"max-nesting-depth": "最大嵌套深度:",
"raster": "栅格",
"vector_light": "矢量(浅色)",
@@ -2116,5 +2126,43 @@
"unknown_http_error_title": "与服务器通讯错误",
"unknown_http_error_content": "状态码: {{statusCode}}\n地址: {{method}} {{url}}\n信息: {{message}}",
"traefik_blocks_requests": "如果您使用 Traefik 反向代理,它引入了一项影响与服务器的通信重大更改。"
},
"experimental_features": {
"title": "实验选项",
"disclaimer": "这些选项处于实验阶段,可能导致系统不稳定。请谨慎使用。",
"new_layout_name": "新布局",
"new_layout_description": "尝试全新布局,呈现更现代的外观并提升易用性。后续版本将进行重大调整。"
},
"tab_history_navigation_buttons": {
"go-back": "返回前一笔记",
"go-forward": "前往下一笔记"
},
"breadcrumb_badges": {
"read_only_explicit": "只读",
"read_only_auto": "自动只读",
"shared_publicly": "公开共享",
"shared_locally": "本地共享",
"read_only_explicit_description": "此笔记已被手动设置为只读。\n点击可临时编辑。",
"read_only_auto_description": "出于性能原因,此笔记已被自动设置为只读模式。此自动限制可以在设置中调整。\n\n点击可临时编辑。",
"read_only_temporarily_disabled": "临时编辑",
"read_only_temporarily_disabled_description": "此笔记当前可编辑,但通常是只读的。一旦你切换到其他笔记,该笔记将恢复为只读模式。\n\n点击以重新启用只读模式。",
"shared_publicly_description": "此笔记已在网上发布,链接为 {{- link}},并且可公开访问。\n\n点击以导航到共享笔记或右键点击查看更多选项。",
"shared_locally_description": "此笔记仅在本地网络共享,链接为 {{- link}}。\n\n点击以导航到共享笔记或右键点击查看更多选项。",
"clipped_note": "网页剪辑",
"clipped_note_description": "此笔记最初来自 {{url}}。\n\n点击即可跳转至源网页。",
"execute_script": "运行脚本",
"execute_script_description": "这是一篇脚本笔记。点击即可执行脚本。",
"execute_sql": "运行SQL",
"execute_sql_description": "这是一篇 SQL 笔记。点击即可执行 SQL 查询。"
},
"status_bar": {
"language_title": "更改所有内容的语言",
"note_info_title": "查看有关此笔记的信息,例如创建/修改日期或笔记大小。",
"backlinks_title_other": "此笔记由 {{count}} 个其他笔记链接而来。\n\n点击查看反向链接列表。",
"attachments_title_other": "此笔记包含 {{count}} 个附件。点击即可在新标签页中打开附件列表。",
"attributes_other": "{{count}} 个属性",
"attributes_title": "单击以打开专用窗格,编辑此笔记拥有的属性,以及查看继承属性列表。",
"note_paths_title": "点击查看此笔记在树状图中的位置路径。",
"code_note_switcher": "更改语言模式"
}
}

View File

@@ -108,6 +108,11 @@
"cloned_note_prefix_title": "Klonovaná poznámka se zobrazí ve stromu poznámek s danou předponou",
"clone_to_selected_note": "Klonovat vybranou poznámku",
"no_path_to_clone_to": "Žádná cest pro klonování.",
"note_cloned": "Poznámka: „{{clonedTitle}}“ bylo naklonováno do „{{targetTitle}}“"
"note_cloned": "Poznámka „{{clonedTitle}}“ bylo naklonována do „{{targetTitle}}“"
},
"zpetne_odkazy": {
"backlink_one": "{{count}} zpětný odkaz",
"backlink_few": "{{count}} zpětné odkazy",
"backlink_other": "{{count}} zpětných odkazů"
}
}

View File

@@ -689,11 +689,14 @@
"export_note": "Export note",
"delete_note": "Delete note",
"print_note": "Print note",
"view_revisions": "Note revisions...",
"save_revision": "Save revision",
"advanced": "Advanced",
"convert_into_attachment_failed": "Converting note '{{title}}' failed.",
"convert_into_attachment_successful": "Note '{{title}}' has been converted to attachment.",
"convert_into_attachment_prompt": "Are you sure you want to convert note '{{title}}' into an attachment of the parent note?",
"print_pdf": "Export as PDF..."
"print_pdf": "Export as PDF...",
"note_map": "Note map"
},
"onclick_button": {
"no_click_handler": "Button widget '{{componentId}}' has no defined click handler"
@@ -1096,6 +1099,12 @@
"vacuuming_database": "Vacuuming database...",
"database_vacuumed": "Database has been vacuumed"
},
"experimental_features": {
"title": "Experimental Options",
"disclaimer": "These options are experimental and may cause instability. Use with caution.",
"new_layout_name": "New Layout",
"new_layout_description": "Try out the new layout for a more modern look and improved usability. Subject to heavy change in the upcoming releases."
},
"fonts": {
"theme_defined": "Theme defined",
"fonts": "Fonts",
@@ -1743,7 +1752,13 @@
"printing_pdf": "Exporting to PDF in progress..."
},
"note_title": {
"placeholder": "type note's title here..."
"placeholder": "type note's title here...",
"created_on": "Created on <Value />",
"last_modified": "Last modified on <Value />",
"note_type_switcher_label": "Switch from {{type}} to:",
"note_type_switcher_others": "More note types",
"note_type_switcher_templates": "Templates",
"note_type_switcher_collection": "Collections"
},
"search_result": {
"no_notes_found": "No notes have been found for given search parameters.",
@@ -1953,8 +1968,9 @@
"unknown_widget": "Unknown widget for \"{{id}}\"."
},
"note_language": {
"not_set": "Not set",
"configure-languages": "Configure languages..."
"not_set": "No language set",
"configure-languages": "Configure languages...",
"help-on-languages": "Help on content languages..."
},
"content_language": {
"title": "Content languages",
@@ -2021,7 +2037,7 @@
"book_properties_config": {
"hide-weekends": "Hide weekends",
"display-week-numbers": "Display week numbers",
"map-style": "Map style:",
"map-style": "Map style",
"max-nesting-depth": "Max nesting depth:",
"raster": "Raster",
"vector_light": "Vector (Light)",
@@ -2117,5 +2133,40 @@
"unknown_http_error_title": "Communication error with the server",
"unknown_http_error_content": "Status code: {{statusCode}}\nURL: {{method}} {{url}}\nMessage: {{message}}",
"traefik_blocks_requests": "If you are using the Traefik reverse proxy, it introduced a breaking change which affects the communication with the server."
},
"tab_history_navigation_buttons": {
"go-back": "Go back to previous note",
"go-forward": "Go forward to next note"
},
"breadcrumb_badges": {
"read_only_explicit": "Read-only",
"read_only_explicit_description": "This note has been manually set to read-only.\nClick to edit it temporarily.",
"read_only_auto": "Auto read-only",
"read_only_auto_description": "This note was set automatically to read-only mode for performance reasons. This automatic limit is adjustable from settings.\n\nClick to edit it temporarily.",
"read_only_temporarily_disabled": "Temporarily editable",
"read_only_temporarily_disabled_description": "This note is currently editable, but it is normally read-only. The note will go back to being read-only as soon as you navigate to another note.\n\nClick to re-enable read-only mode.",
"shared_publicly": "Shared publicly",
"shared_publicly_description": "This note has been published online at {{- link}}, and is publicly accessible.\n\nClick to navigate to the shared note or right click for more options.",
"shared_locally": "Shared locally",
"shared_locally_description": "This note is shared on the local network only at {{- link}}.\n\nClick to navigate to the shared note or right click for more options.",
"clipped_note": "Web clip",
"clipped_note_description": "This note was originally taken from {{url}}.\n\nClick to navigate to the source webpage.",
"execute_script": "Run script",
"execute_script_description": "This note is a script note. Click to execute the script.",
"execute_sql": "Run SQL",
"execute_sql_description": "This note is a SQL note. Click to execute the SQL query."
},
"status_bar": {
"language_title": "Change the language of the entire content",
"note_info_title": "View information about this note such as the creation/modification date or the note size.",
"backlinks_title_one": "This note is linked from {{count}} other note.\n\nClick to view the list of backlinks.",
"backlinks_title_other": "This note is linked from {{count}} other notes.\n\nClick to view the list of backlinks.",
"attachments_title_one": "This note has {{count}} attachment. Click to open the list of attachments in a new tab.",
"attachments_title_other": "This note has {{count}} attachments. Click to open the list of attachments in a new tab.",
"attributes_one": "{{count}} attribute",
"attributes_other": "{{count}} attributes",
"attributes_title": "Click to open a dedicated pane to edit this note's owned attributes, as well as to see the list of inherited attributes.",
"note_paths_title": "Click to see the paths where this note is placed into the tree.",
"code_note_switcher": "Change language mode"
}
}

View File

@@ -94,7 +94,8 @@
"info": {
"okButton": "OK",
"closeButton": "Chiudi",
"modalTitle": "Messaggio informativo"
"modalTitle": "Messaggio informativo",
"copy_to_clipboard": "Copia negli appunti"
},
"export": {
"close": "Chiudi",
@@ -314,7 +315,7 @@
"import-into-note": "Importa nella nota",
"apply-bulk-actions": "Applica azioni in blocco",
"converted-to-attachments": "{{count}} note sono state convertite in allegati.",
"convert-to-attachment-confirm": "Sei sicuro di voler convertire le note selezionate in allegati delle note padre?",
"convert-to-attachment-confirm": "Sei sicuro di voler convertire le note selezionate in allegati delle note principali? Questa operazione si applica solo alle note immagine, le altre note verranno ignorate.",
"open-in-popup": "Modifica rapida"
},
"electron_context_menu": {
@@ -1260,7 +1261,8 @@
"convert_into_attachment_successful": "Nota '{{title}}' è stato convertito in allegato.",
"convert_into_attachment_prompt": "Sei sicuro di voler convertire la nota '{{title}}' in un allegato della nota padre?",
"print_pdf": "Esporta come PDF...",
"open_note_on_server": "Apri una nota sul server"
"open_note_on_server": "Apri una nota sul server",
"view_revisions": "Revisioni..."
},
"onclick_button": {
"no_click_handler": "Il widget pulsante '{{componentId}}' non ha un gestore di clic definito"
@@ -1493,7 +1495,12 @@
"editable_text": {
"placeholder": "Digita qui il contenuto della tua nota...",
"auto-detect-language": "Rilevato automaticamente",
"keeps-crashing": "Il componente di modifica continua a bloccarsi. Prova a riavviare Trilium. Se il problema persiste, valuta la possibilità di creare una segnalazione di bug."
"keeps-crashing": "Il componente di modifica continua a bloccarsi. Prova a riavviare Trilium. Se il problema persiste, valuta la possibilità di creare una segnalazione di bug.",
"editor_crashed_title": "L'editor di testo si è bloccato",
"editor_crashed_content": "I tuoi contenuti sono stati recuperati con successo, ma alcune delle modifiche più recenti potrebbero non essere state salvate.",
"editor_crashed_details_button": "Visualizza ulteriori dettagli...",
"editor_crashed_details_intro": "Se questo errore si verifica più volte, valuta la possibilità di segnalarlo su GitHub incollando le informazioni riportate di seguito.",
"editor_crashed_details_title": "Informazioni tecniche"
},
"empty": {
"open_note_instruction": "Apri una nota digitandone il titolo nel campo sottostante oppure scegli una nota nell'albero.",
@@ -1867,7 +1874,9 @@
"printing_pdf": "Esportazione in PDF in corso..."
},
"note_title": {
"placeholder": "scrivi qui il titolo della nota..."
"placeholder": "scrivi qui il titolo della nota...",
"created_on": "Creato il <Value />",
"last_modified": "Ultima modifica il <Value />"
},
"search_result": {
"no_notes_found": "Non sono state trovate note per i parametri di ricerca specificati.",
@@ -2003,8 +2012,9 @@
"unknown_widget": "Widget sconosciuto per \"{{id}}\"."
},
"note_language": {
"not_set": "Non impostato",
"configure-languages": "Configura le lingue..."
"not_set": "Nessuna lingua impostata",
"configure-languages": "Configura le lingue...",
"help-on-languages": "Aiuto sulle lingue dei contenuti..."
},
"content_language": {
"title": "Lingue dei contenuti",
@@ -2022,7 +2032,8 @@
"button_title": "Esporta diagramma come PNG"
},
"svg": {
"export_to_png": "Non è stato possibile esportare il diagramma in formato PNG."
"export_to_png": "Non è stato possibile esportare il diagramma in formato PNG.",
"export_to_svg": "Il diagramma non può essere esportato in formato SVG."
},
"code_theme": {
"title": "Aspetto",
@@ -2106,5 +2117,32 @@
},
"popup-editor": {
"maximize": "Passa all'editor completo"
},
"experimental_features": {
"title": "Opzioni sperimentali",
"disclaimer": "Queste opzioni sono sperimentali e potrebbero causare instabilità. Usare con cautela.",
"new_layout_name": "Nuovo layout",
"new_layout_description": "Prova il nuovo layout per un look più moderno e una maggiore usabilità. Soggetto a modifiche significative nelle prossime versioni."
},
"server": {
"unknown_http_error_title": "Errore di comunicazione con il server",
"unknown_http_error_content": "Codice di stato: {{statusCode}}\nURL: {{method}} {{url}}\nMessaggio: {{message}}",
"traefik_blocks_requests": "Se si utilizza il proxy inverso Traefik, è stata introdotta una modifica sostanziale che influisce sulla comunicazione con il server."
},
"tab_history_navigation_buttons": {
"go-back": "Torna alla nota precedente",
"go-forward": "Passa alla nota successiva"
},
"breadcrumb_badges": {
"read_only_explicit": "Sola lettura",
"read_only_explicit_description": "Questa nota è stata impostata manualmente come di sola lettura.\nClicca per modificarla temporaneamente.",
"read_only_auto": "Solo lettura automatica",
"read_only_auto_description": "Questa nota è stata impostata automaticamente in modalità di sola lettura per motivi di prestazioni. Questo limite automatico è modificabile dalle impostazioni.\n\nClicca per modificarla temporaneamente.",
"read_only_temporarily_disabled": "Modificabile temporaneamente",
"read_only_temporarily_disabled_description": "Questa nota è attualmente modificabile, ma normalmente è di sola lettura. La nota tornerà ad essere di sola lettura non appena passerai a un'altra nota.\n\nClicca per riattivare la modalità di sola lettura.",
"shared_publicly": "Condiviso pubblicamente",
"shared_publicly_description": "Questa nota è stata pubblicata online all'indirizzo {{- link}} ed è accessibile al pubblico.\n\nClicca per visualizzare la nota condivisa o clicca con il tasto destro del mouse per ulteriori opzioni.",
"shared_locally": "Condiviso localmente",
"shared_locally_description": "Questa nota è condivisa sulla rete locale solo all'indirizzo {{- link}}.\n\nClicca per accedere alla nota condivisa o clicca con il tasto destro del mouse per ulteriori opzioni."
}
}

View File

@@ -258,7 +258,7 @@
"export_in_progress": "エクスポート処理中: {{progressCount}}",
"export_finished_successfully": "エクスポートが正常に完了しました。",
"format_pdf": "PDF - 印刷または共有目的に。",
"share-format": "Web 公開用の HTML - 共有ノートで使用されるのと同じテーマを使用しますが、静的 Web サイトとして公開できます。"
"share-format": "web 公開用の HTML - 共有ノートで使用されるのと同じテーマを使用しますが、静的 web サイトとして公開できます。"
},
"help": {
"title": "チートシート",
@@ -458,7 +458,10 @@
"convert_into_attachment_successful": "ノート '{{title}}' は添付ファイルに変換されました。",
"convert_into_attachment_prompt": "本当にノート '{{title}}' を親ノートの添付ファイルに変換しますか?",
"note_attachments": "ノートの添付ファイル",
"open_note_on_server": "サーバー上のノートを開く"
"open_note_on_server": "サーバー上のノートを開く",
"view_revisions": "ノートの変更履歴...",
"note_map": "ノートマップ",
"advanced": "高度"
},
"command_palette": {
"export_note_title": "ノートをエクスポート",
@@ -799,7 +802,7 @@
},
"web_view": {
"web_view": "Web ビュー",
"embed_websites": "Web ビュータイプでは、ウェブサイトをTriliumに埋め込むことができます。",
"embed_websites": "Web ビュータイプでは、web サイトを Trilium に埋め込むことができます。",
"create_label": "まず始めに、埋め込みたいURLアドレスのラベルを作成してください。例: #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
@@ -961,7 +964,7 @@
"password": {
"wiki": "wiki",
"heading": "パスワード",
"alert_message": "新しいパスワードは大切に保管してください。パスワードはウェブインターフェースへのログインや、保護されたノートの暗号化に使用されます。パスワードを忘れると、保護されたノートはすべて永久に失われます。",
"alert_message": "新しいパスワードは大切に保管してください。パスワードは web インターフェースへのログインや、保護されたノートの暗号化に使用されます。パスワードを忘れると、保護されたノートはすべて永久に失われます。",
"reset_link": "リセットするにはここをクリック。",
"old_password": "旧パスワード",
"new_password": "新パスワード",
@@ -1107,7 +1110,7 @@
"sql_console_home": "SQLコンソールートのデフォルトの場所",
"bookmark_folder": "このラベルの付いたノートは、ブックマークにフォルダとして表示されます(子フォルダへのアクセスを許可します)",
"share_hidden_from_tree": "このートは左側のナビゲーションツリーには表示されていませんが、URL からアクセスできます",
"share_external_link": "ノートは共有ツリー内で外部ウェブサイトへのリンクとして機能します",
"share_external_link": "ノートは共有ツリー内で外部 web サイトへのリンクとして機能します",
"share_alias": "https://your_trilium_host/share/[your_alias] でノートを利用できるようにエイリアスを定義します",
"share_omit_default_css": "デフォルトの共有ページのCSSは省略されます。スタイルを大幅に変更する場合に使用してください。",
"share_root": "/share root で提供されるノートをマークする。",
@@ -1233,7 +1236,13 @@
"none_yet": "アクションを上のリストからクリックして追加。"
},
"note_title": {
"placeholder": "ここにノートのタイトルを入力..."
"placeholder": "ここにノートのタイトルを入力...",
"created_on": "作成日 <Value />",
"last_modified": "最終更新日 <Value />",
"note_type_switcher_label": "{{type}} から切り替え:",
"note_type_switcher_others": "その他のノートタイプ",
"note_type_switcher_templates": "テンプレート",
"note_type_switcher_collection": "コレクション"
},
"search_result": {
"no_notes_found": "指定された検索パラメータに該当するノートは見つかりませんでした。",
@@ -1330,8 +1339,9 @@
"minimum_input": "入力された時間値は {{minimumSeconds}} 秒以上である必要があります。"
},
"note_language": {
"not_set": "未設定",
"configure-languages": "言語を設定..."
"not_set": "言語が設定されていません",
"configure-languages": "言語を設定...",
"help-on-languages": "コンテンツの言語に関するヘルプ..."
},
"content_language": {
"title": "コンテンツの言語",
@@ -1620,7 +1630,7 @@
"remove_this_attribute": "この属性を削除",
"remove_color": "このカラーラベルを削除",
"promoted_attributes": "プロモート属性",
"url_placeholder": "http://ウェブサイト..."
"url_placeholder": "http://web サイト..."
},
"relation_map": {
"open_in_new_tab": "新しいタブで開く",
@@ -1974,7 +1984,7 @@
"book_properties_config": {
"hide-weekends": "週末を非表示",
"display-week-numbers": "週番号を表示",
"map-style": "マップスタイル:",
"map-style": "マップスタイル",
"max-nesting-depth": "最大階層の深さ:",
"show-scale": "スケールを表示",
"raster": "Raster",
@@ -2069,7 +2079,7 @@
"recovery_keys_used": "使用日: {{date}}",
"recovery_keys_unused": "回復コード {{index}} は未使用です",
"oauth_title": "OAuth/OpenID",
"oauth_description": "OpenIDは、Googleなどの他のサービスのアカウントを使用してウェブサイトにログインし、本人確認を行うための標準化された方法です。デフォルトの発行者はGoogleですが、他のOpenIDプロバイダに変更できます。詳しくは<a href=\"#root/_hidden/_help/_help_Otzi9La2YAUX/_help_WOcw2SLH6tbX/_help_7DAiwaf8Z7Rz\">こちら</a>をご覧ください。Google経由でOpenIDサービスを設定するには、<a href=\"https://developers.google.com/identity/openid-connect/openid-connect\">こちらの手順</a>に従ってください。",
"oauth_description": "OpenIDは、Googleなどの他のサービスのアカウントを使用して web サイトにログインし、本人確認を行うための標準化された方法です。デフォルトの発行者はGoogleですが、他のOpenIDプロバイダに変更できます。詳しくは<a href=\"#root/_hidden/_help/_help_Otzi9La2YAUX/_help_WOcw2SLH6tbX/_help_7DAiwaf8Z7Rz\">こちら</a>をご覧ください。Google経由でOpenIDサービスを設定するには、<a href=\"https://developers.google.com/identity/openid-connect/openid-connect\">こちらの手順</a>に従ってください。",
"oauth_description_warning": "OAuth/OpenIDを有効にするには、config.iniファイルにOAuth/OpenIDのベースURL、クライアントID、クライアントシークレットを設定し、アプリケーションを再起動する必要があります。環境変数から設定する場合は、TRILIUM_OAUTH_BASE_URL, TRILIUM_OAUTH_CLIENT_ID and TRILIUM_OAUTH_CLIENT_SECRET を設定してください。",
"oauth_missing_vars": "設定がありません: {{-variables}}",
"oauth_user_account": "ユーザーアカウント: ",
@@ -2116,5 +2126,43 @@
"unknown_http_error_title": "サーバーとの通信エラー",
"unknown_http_error_content": "ステータスコード: {{statusCode}}\nURL: {{method}} {{url}}\nメッセージ: {{message}}",
"traefik_blocks_requests": "Traefik リバース プロキシを使用している場合、サーバーとの通信に影響する重大な変更が導入されました。"
},
"tab_history_navigation_buttons": {
"go-back": "前のノートに戻る",
"go-forward": "次のノートに進む"
},
"experimental_features": {
"title": "実験オプション",
"disclaimer": "これらのオプションは試験的なもので、動作が不安定になる可能性があります。注意してご使用ください。",
"new_layout_name": "新しいレイアウト",
"new_layout_description": "よりモダンな外観と使いやすさが向上した新しいレイアウトをお試しください。今後のリリースで大幅な変更が加えられる可能性があります。"
},
"breadcrumb_badges": {
"read_only_explicit": "読み取り専用",
"read_only_auto": "自動的に読み取り専用",
"shared_publicly": "公開で共有",
"shared_locally": "ローカルで共有",
"read_only_explicit_description": "このノートは手動で読み取り専用に設定されています。\nクリックすると一時的に編集できます。",
"read_only_temporarily_disabled": "一時的に編集可能",
"read_only_auto_description": "このノートはパフォーマンス上の理由により、自動的に読み取り専用モードに設定されました。この自動制限は設定から調整できます。\n\n一時的に編集するにはクリックしてください。",
"read_only_temporarily_disabled_description": "このノートは現在編集可能ですが、通常は読み取り専用です。別のノートに移動すると読み取り専用に戻ります。\n\nクリックすると読み取り専用モードが再度有効になります。",
"shared_publicly_description": "このノートは {{- link}} でオンライン公開されており、誰でも閲覧可能です。\n\n共有ートに移動するにはクリックするか、右クリックしてその他のオプションを選択してください。",
"shared_locally_description": "このノートは、{{- link}} でローカルネットワークのみで共有されています。\n\nクリックして共有ートに移動するか、右クリックしてその他のオプションを選択してください。",
"clipped_note": "Web クリップ",
"clipped_note_description": "このノートは {{url}} から取得されました。\n\nクリックすると元の web ページに移動します。",
"execute_script": "スクリプトを実行",
"execute_script_description": "このノートはスクリプトノートです。クリックするとスクリプトが実行されます。",
"execute_sql": "SQL を実行",
"execute_sql_description": "このノートは SQL ノートです。クリックすると SQL クエリが実行されます。"
},
"status_bar": {
"language_title": "コンテンツ全体の言語を変更",
"note_info_title": "作成日/変更日やノートのサイズなど、このノートに関する情報を表示する。",
"backlinks_title_other": "このノートは {{count}} 件の他のノートからリンクされています。\n\nクリックするとバックリンクのリストが表示されます。",
"attachments_title_other": "このノートには {{count}} 件の添付ファイルがあります。クリックすると、添付ファイルのリストが新しいタブで開きます。",
"attributes_other": "{{count}} 個の属性",
"attributes_title": "クリックすると専用のペインが開き、このノートの所有属性を編集したり、継承された属性のリストを表示できます。",
"note_paths_title": "クリックすると、このノートがツリー内に配置されているパスが表示されます。",
"code_note_switcher": "言語モードを変更"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -493,7 +493,12 @@
"editable_text": {
"placeholder": "Scrieți conținutul notiței aici...",
"auto-detect-language": "Automat",
"keeps-crashing": "Componenta de editare se blochează în continuu. Încercați să reporniți Trilium. Dacă problema persistă, luați în considerare să raportați această problemă."
"keeps-crashing": "Componenta de editare se blochează în continuu. Încercați să reporniți Trilium. Dacă problema persistă, luați în considerare să raportați această problemă.",
"editor_crashed_title": "Editorul text a avut o eroare",
"editor_crashed_content": "Conținutul a fost recuperat cu succes, dar este posibil ca o parte din cele mai recente modificări ale dvs. să nu se fi salvat.",
"editor_crashed_details_button": "Mai multe detalii...",
"editor_crashed_details_intro": "Dacă întâmpinați frecvent această eroare, considerați să o raportați pe GitHub copiând informația de mai jos.",
"editor_crashed_details_title": "Informații tehnice"
},
"edited_notes": {
"deleted": "(șters)",
@@ -785,7 +790,8 @@
"info": {
"closeButton": "Închide",
"modalTitle": "Mesaj informativ",
"okButton": "OK"
"okButton": "OK",
"copy_to_clipboard": "Copiază în clipboard"
},
"inherited_attribute_list": {
"no_inherited_attributes": "Niciun atribut moștenit.",
@@ -867,12 +873,14 @@
"print_note": "Imprimare notiță",
"re_render_note": "Reinterpretare notiță",
"save_revision": "Salvează o nouă revizie",
"advanced": "Advansat",
"search_in_note": "Caută în notiță",
"convert_into_attachment_failed": "Nu s-a putut converti notița „{{title}}”.",
"convert_into_attachment_successful": "Notița „{{title}}” a fost convertită în atașament.",
"convert_into_attachment_prompt": "Doriți convertirea notiței „{{title}}” într-un atașament al notiței părinte?",
"print_pdf": "Exportare ca PDF...",
"open_note_on_server": "Deschide notița pe server"
"open_note_on_server": "Deschide notița pe server",
"view_revisions": "Revizii ale notițelor..."
},
"note_erasure_timeout": {
"deleted_notes_erased": "Notițele șterse au fost eliminate permanent.",
@@ -1407,7 +1415,7 @@
"hoist-note": "Focalizează notița",
"unhoist-note": "Defocalizează notița",
"converted-to-attachments": "{{count}} notițe au fost convertite în atașamente.",
"convert-to-attachment-confirm": "Doriți convertirea notițelor selectate în atașamente ale notiței părinte?",
"convert-to-attachment-confirm": "Doriți convertirea notițelor selectate în atașamente ale notiței părinte? Această operațiune se aplică doar notițelor de tip imagine, celelalte vor fi ignorate.",
"open-in-popup": "Editare rapidă",
"archive": "Arhivează",
"unarchive": "Dezarhivează"
@@ -1526,7 +1534,9 @@
"printing_pdf": "Exportare ca PDF în curs..."
},
"note_title": {
"placeholder": "introduceți titlul notiței aici..."
"placeholder": "introduceți titlul notiței aici...",
"created_on": "Creată la <Value />",
"last_modified": "Modificată la <Value />"
},
"revisions_snapshot_limit": {
"erase_excess_revision_snapshots": "Șterge acum reviziile excesive",
@@ -1758,7 +1768,8 @@
},
"note_language": {
"configure-languages": "Configurează limbile...",
"not_set": "Nedefinită"
"not_set": "Nicio limbă setată",
"help-on-languages": "Informații despre limba conținutului..."
},
"png_export_button": {
"button_title": "Exportă diagrama ca PNG"
@@ -1954,7 +1965,8 @@
"oauth_user_not_logged_in": "Neautentificat!"
},
"svg": {
"export_to_png": "Diagrama nu a putut fi exportată în PNG."
"export_to_png": "Diagrama nu a putut fi exportată în PNG.",
"export_to_svg": "Diagrama nu a putut fi exportată în SVG."
},
"code_theme": {
"title": "Afișare",
@@ -2106,5 +2118,32 @@
},
"popup-editor": {
"maximize": "Comută la editorul principal"
},
"experimental_features": {
"title": "Opțiuni experimentale",
"disclaimer": "Aceste opțiuni sunt experimentale și pot cauza instabilitate. Folosiți cu prudență.",
"new_layout_name": "Aspect nou",
"new_layout_description": "Încercați noul aspect pentru un design mai modern și mai ușor de utilizat. Poate surveni modificări semnificative în următoarele release-uri."
},
"server": {
"unknown_http_error_title": "Eroare de comunicare cu server-ul",
"unknown_http_error_content": "Cod: {{statusCode}}\nURL: {{method}} {{url}}\nMesaj: {{message}}",
"traefik_blocks_requests": "Dacă utilizați reverse proxy-ul Traefik, acesta a introdus o schimbare majoră ce afectează comunicarea cu server-ul."
},
"tab_history_navigation_buttons": {
"go-back": "Înapoi la notița anterioară",
"go-forward": "Înainte către notița următoare"
},
"breadcrumb_badges": {
"read_only_explicit": "Mod citire",
"read_only_explicit_description": "Această notiță a fost setată explicit să fie doar în citire.\nClick pentru a o edita temporar.",
"read_only_auto": "Mod citire auto",
"read_only_auto_description": "Această notița a fost setată automată să fie în mod doar de citire din motive de performanță. Această limită automată este ajustabilă din setări.\n\nClick pentru a o edita temporar.",
"read_only_temporarily_disabled": "Editabilă temporar",
"read_only_temporarily_disabled_description": "Această notiță se poate modifica, deși în mod normal ea este doar în citire. Notița va reveni la modul doar în citire imediat ce navigați către altă notiță.\n\nClick pentru a re-activa modul doar în citire.",
"shared_publicly": "Partajată public",
"shared_publicly_description": "Această notiță este publicată online la {{- link}} și este acesibilă public.\n\nClic pentru a naviga la pagina partajată sau click dreapta pentru mai multe opțiuni.",
"shared_locally": "Partajată local",
"shared_locally_description": "Această notiță este partajată doar pe rețeaua locală la {{- link}}.\n\nClic pentru a naviga la pagina partajată sau click dreapta pentru mai multe opțiuni."
}
}

View File

@@ -205,7 +205,8 @@
"info": {
"modalTitle": "資訊消息",
"closeButton": "關閉",
"okButton": "確定"
"okButton": "確定",
"copy_to_clipboard": "複製到剪貼簿"
},
"jump_to_note": {
"search_button": "全文搜尋",
@@ -986,7 +987,12 @@
"editable_text": {
"placeholder": "在這裡輸入您的筆記內容…",
"auto-detect-language": "自動檢測",
"keeps-crashing": "編輯元件持續發生崩潰。請嘗試重新啟動 Trilium。若問題仍存在請考慮提交錯誤報告。"
"keeps-crashing": "編輯元件持續發生崩潰。請嘗試重新啟動 Trilium。若問題仍存在請考慮提交錯誤報告。",
"editor_crashed_title": "文字編輯器崩潰",
"editor_crashed_content": "您的內容已成功恢復,但最近的幾項變更可能未被儲存。",
"editor_crashed_details_button": "檢視更多資訊⋯",
"editor_crashed_details_intro": "若您多次遇到此錯誤,請考慮在 GitHub 回報以下資訊。",
"editor_crashed_details_title": "技術資訊"
},
"empty": {
"open_note_instruction": "透過在下面的輸入框中輸入筆記標題或在樹中選擇筆記來打開筆記。",
@@ -1531,7 +1537,9 @@
"printing_pdf": "正在匯出為 PDF…"
},
"note_title": {
"placeholder": "請輸入筆記標題..."
"placeholder": "請輸入筆記標題...",
"created_on": "建立於 {{date}}",
"last_modified": "最後修改於 {{date}}"
},
"search_result": {
"no_notes_found": "沒有找到符合搜尋條件的筆記。",
@@ -2106,5 +2114,26 @@
},
"popup-editor": {
"maximize": "切換至完整編輯器"
},
"experimental_features": {
"title": "實驗性選項",
"disclaimer": "這些選項屬實驗性質,可能導致系統不穩定。請謹慎使用。",
"new_layout_name": "新版面配置",
"new_layout_description": "體驗全新版面配置,呈現更現代的外觀與更佳的使用體驗。在未來版本將進行大幅調整。"
},
"server": {
"unknown_http_error_title": "與伺服器通訊錯誤",
"unknown_http_error_content": "狀態碼:{{statusCode}}\n網址{{method}} {{url}}\n訊息{{message}}",
"traefik_blocks_requests": "若您正在使用 Traefik 反向代理,該代理已引入一項重大變更影響與伺服器的通訊。"
},
"tab_history_navigation_buttons": {
"go-back": "返回前一筆記",
"go-forward": "前往下一筆記"
},
"breadcrumb_badges": {
"read_only_explicit": "唯讀",
"read_only_auto": "自動唯讀",
"shared_publicly": "公開分享",
"shared_locally": "本地分享"
}
}

View File

@@ -5,7 +5,7 @@ import NoteContext from "../components/note_context";
import FNote from "../entities/fnote";
import ActionButton, { ActionButtonProps } from "./react/ActionButton";
import { useIsNoteReadOnly, useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils";
import server from "../services/server";
import { BacklinkCountResponse, BacklinksResponse, SaveSqlConsoleResponse } from "@triliumnext/commons";
@@ -20,6 +20,7 @@ import RawHtml from "./react/RawHtml";
import { ViewTypeOptions } from "./collections/interface";
import attributes from "../services/attributes";
import LoadResults from "../services/load_results";
import { isExperimentalFeatureEnabled } from "../services/experimental_features";
export interface FloatingButtonContext {
parentComponent: Component;
@@ -76,6 +77,8 @@ export const POPUP_HIDDEN_FLOATING_BUTTONS: FloatingButtonsList = [
ToggleReadOnlyButton
];
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
function RefreshBackendLogButton({ note, parentComponent, noteContext, isDefaultViewMode }: FloatingButtonContext) {
const isEnabled = (note.noteId === "_backendLog" || note.type === "render") && isDefaultViewMode;
return isEnabled && <FloatingButton
@@ -297,8 +300,9 @@ function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingB
function InAppHelpButton({ note }: FloatingButtonContext) {
const helpUrl = getHelpUrlForNote(note);
const isEnabled = !!helpUrl && (!isNewLayout || (note?.type !== "book"));
return !!helpUrl && (
return isEnabled && (
<FloatingButton
icon="bx bx-help-circle"
text={t("help-button.title")}
@@ -308,22 +312,9 @@ function InAppHelpButton({ note }: FloatingButtonContext) {
}
function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
let [ backlinkCount, setBacklinkCount ] = useState(0);
let [ popupOpen, setPopupOpen ] = useState(false);
const [ popupOpen, setPopupOpen ] = useState(false);
const backlinksContainerRef = useRef<HTMLDivElement>(null);
function refresh() {
if (!isDefaultViewMode) return;
server.get<BacklinkCountResponse>(`note-map/${note.noteId}/backlink-count`).then(resp => {
setBacklinkCount(resp.count);
});
}
useEffect(() => refresh(), [ note ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (needsRefresh(note, loadResults)) refresh();
});
const backlinkCount = useBacklinkCount(note, isDefaultViewMode);
// Determine the max height of the container.
const { windowHeight } = useWindowSize();
@@ -336,7 +327,7 @@ function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
}
}, [ popupOpen, windowHeight ]);
const isEnabled = isDefaultViewMode && backlinkCount > 0;
const isEnabled = !isNewLayout && isDefaultViewMode && backlinkCount > 0;
return (isEnabled &&
<div className="backlinks-widget has-overflow">
<div
@@ -355,15 +346,34 @@ function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
);
}
function BacklinksList({ note }: { note: FNote }) {
export function useBacklinkCount(note: FNote | null | undefined, isDefaultViewMode: boolean) {
const [ backlinkCount, setBacklinkCount ] = useState(0);
const refresh = useCallback(() => {
if (!note || !isDefaultViewMode) return;
server.get<BacklinkCountResponse>(`note-map/${note.noteId}/backlink-count`).then(resp => {
setBacklinkCount(resp.count);
});
}, [ isDefaultViewMode, note ]);
useEffect(() => refresh(), [ refresh ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (note && needsRefresh(note, loadResults)) refresh();
});
return backlinkCount;
}
export function BacklinksList({ note }: { note: FNote }) {
const [ backlinks, setBacklinks ] = useState<BacklinksResponse>([]);
function refresh() {
server.get<BacklinksResponse>(`note-map/${note.noteId}/backlinks`).then(async (backlinks) => {
// prefetch all
const noteIds = backlinks
.filter(bl => "noteId" in bl)
.map((bl) => bl.noteId);
.filter(bl => "noteId" in bl)
.map((bl) => bl.noteId);
await froca.getNotes(noteIds);
setBacklinks(backlinks);
});

View File

@@ -299,8 +299,10 @@ async function getWidgetType(note: FNote | null | undefined, noteContext: NoteCo
if (noteContext?.viewScope?.viewMode === "source") {
resultingType = "readOnlyCode";
} else if (noteContext?.viewScope && noteContext.viewScope.viewMode === "attachments") {
} else if (noteContext.viewScope?.viewMode === "attachments") {
resultingType = noteContext.viewScope.attachmentId ? "attachmentDetail" : "attachmentList";
} else if (noteContext.viewScope?.viewMode === "note-map") {
resultingType = "noteMap";
} else if (type === "text" && (await noteContext?.isReadOnly())) {
resultingType = "readOnlyText";
} else if ((type === "code" || type === "mermaid") && (await noteContext?.isReadOnly())) {

View File

@@ -0,0 +1,7 @@
.component.tab-history-navigation-buttons {
contain: none;
flex-shrink: 0;
display: flex;
align-items: center;
margin-inline-end: 0.5em;
}

View File

@@ -0,0 +1,64 @@
import "./TabHistoryNavigationButtons.css";
import { useEffect, useMemo, useState } from "preact/hooks";
import { t } from "../services/i18n";
import { dynamicRequire, isElectron } from "../services/utils";
import { handleHistoryContextMenu } from "./launch_bar/HistoryNavigation";
import ActionButton from "./react/ActionButton";
import { useLauncherVisibility } from "./react/hooks";
export default function TabHistoryNavigationButtons() {
const webContents = useMemo(() => isElectron() ? dynamicRequire("@electron/remote").getCurrentWebContents() : undefined, []);
const onContextMenu = webContents ? handleHistoryContextMenu(webContents) : undefined;
const { canGoBack, canGoForward } = useBackForwardState(webContents);
const legacyBackVisible = useLauncherVisibility("_lbBackInHistory");
const legacyForwardVisible = useLauncherVisibility("_lbForwardInHistory");
return (isElectron() &&
<div className="tab-history-navigation-buttons">
{!legacyBackVisible && <ActionButton
icon="bx bx-left-arrow-alt"
text={t("tab_history_navigation_buttons.go-back")}
triggerCommand="backInNoteHistory"
onContextMenu={onContextMenu}
disabled={!canGoBack}
/>}
{!legacyForwardVisible && <ActionButton
icon="bx bx-right-arrow-alt"
text={t("tab_history_navigation_buttons.go-forward")}
triggerCommand="forwardInNoteHistory"
onContextMenu={onContextMenu}
disabled={!canGoForward}
/>}
</div>
);
}
function useBackForwardState(webContents: Electron.WebContents | undefined) {
const [ canGoBack, setCanGoBack ] = useState(webContents?.navigationHistory.canGoBack());
const [ canGoForward, setCanGoForward ] = useState(webContents?.navigationHistory.canGoForward());
useEffect(() => {
if (!webContents) return;
const updateNavigationState = () => {
setCanGoBack(webContents.navigationHistory.canGoBack());
setCanGoForward(webContents.navigationHistory.canGoForward());
};
webContents.on("did-navigate", updateNavigationState);
webContents.on("did-navigate-in-page", updateNavigationState);
return () => {
webContents.removeListener("did-navigate", updateNavigationState);
webContents.removeListener("did-navigate-in-page", updateNavigationState);
};
}, [ webContents ]);
if (!webContents) {
return { canGoBack: true, canGoForward: true };
}
return { canGoBack, canGoForward };
}

View File

@@ -12,6 +12,7 @@ import shortcutService from "../../services/shortcuts.js";
import appContext from "../../components/app_context.js";
import type { Attribute } from "../../services/attribute_parser.js";
import { focusSavedElement, saveFocusedElement } from "../../services/focus.js";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features.js";
const TPL = /*html*/`
<div class="attr-detail tn-tool-dialog">
@@ -309,6 +310,8 @@ interface SearchRelatedResponse {
count: number;
}
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
export default class AttributeDetailWidget extends NoteContextAwareWidget {
private $title!: JQuery<HTMLElement>;
private $inputName!: JQuery<HTMLElement>;
@@ -579,6 +582,13 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
.css("top", y - offset.top + 70)
.css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000);
if (isNewLayout) {
this.$widget
.css("top", "unset")
.css("bottom", 70)
.css("max-height", "80vh");
}
if (focus === "name") {
this.$inputName.trigger("focus").trigger("select");
}

View File

@@ -1,16 +1,19 @@
import Dropdown from "../react/Dropdown";
import "./global_menu.css";
import { useStaticTooltip, useStaticTooltipWithKeyboardShortcut, useTriliumOption, useTriliumOptionBool, useTriliumOptionInt } from "../react/hooks";
import { useContext, useEffect, useRef, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import { FormDropdownDivider, FormDropdownSubmenu, FormListHeader, FormListItem } from "../react/FormList";
import { CommandNames } from "../../components/app_context";
import KeyboardShortcut from "../react/KeyboardShortcut";
import { KeyboardActionNames } from "@triliumnext/commons";
import { ComponentChildren } from "preact";
import { useContext, useEffect, useRef, useState } from "preact/hooks";
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, 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";
import KeyboardShortcut from "../react/KeyboardShortcut";
import { ParentComponent } from "../react/react_utils";
import utils, { dynamicRequire, isElectron, isMobile } from "../../services/utils";
interface MenuItemProps<T> {
icon: string,
@@ -70,8 +73,9 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
</>}
{!isElectron() && <BrowserOnlyOptions />}
{glob.isDev && <DevelopmentOptions />}
</Dropdown>
)
);
}
function AdvancedMenu() {
@@ -89,7 +93,7 @@ function AdvancedMenu() {
{isElectron() && <MenuItem command="openDevTools" icon="bx bx-bug-alt" text={t("global_menu.open_dev_tools")} />}
<KeyboardActionMenuItem command="reloadFrontendApp" icon="bx bx-refresh" text={t("global_menu.reload_frontend")} title={t("global_menu.reload_hint")} />
</FormDropdownSubmenu>
)
);
}
function BrowserOnlyOptions() {
@@ -99,6 +103,35 @@ function BrowserOnlyOptions() {
</>;
}
function DevelopmentOptions() {
const [ layoutOrientation ] = useTriliumOption("layoutOrientation");
return <>
<FormDropdownDivider />
<FormListItem disabled>Development Options</FormListItem>
<FormDropdownSubmenu icon="bx bx-test-tube" title="Experimental features" dropStart={layoutOrientation === "horizontal"}>
{experimentalFeatures.map((feature) => (
<ExperimentalFeatureToggle key={feature.id} experimentalFeature={feature as ExperimentalFeature} />
))}
</FormDropdownSubmenu>
</>;
}
function ExperimentalFeatureToggle({ experimentalFeature }: { experimentalFeature: ExperimentalFeature }) {
const featureEnabled = isExperimentalFeatureEnabled(experimentalFeature.id as ExperimentalFeatureId);
return (
<FormListItem
checked={featureEnabled}
title={experimentalFeature.description}
onClick={async () => {
await toggleExperimentalFeature(experimentalFeature.id as ExperimentalFeatureId, !featureEnabled);
reloadFrontendApp();
}}
>{experimentalFeature.name}</FormListItem>
);
}
function SwitchToOptions() {
if (isElectron()) {
return;

View File

@@ -243,7 +243,7 @@ function AddNewColumn({ api, isInRelationMode }: { api: BoardApi, isInRelationMo
export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, isNewItem }: {
currentValue?: string;
placeholder?: string;
save: (newValue: string) => void;
save: (newValue: string) => void | Promise<void>;
dismiss: () => void;
isNewItem?: boolean;
mode?: "normal" | "multiline" | "relation";

View File

@@ -2,9 +2,13 @@
position: absolute;
top: 1em;
right: 1em;
.floating-buttons-children {
top: 0;
}
}
.presentation-container {
width: 100%;
height: 100%;
}
}

View File

@@ -0,0 +1 @@
/** Intentionally left empty for now **/

View File

@@ -5,6 +5,7 @@ import FlexContainer from "./flex_container.js";
import options from "../../services/options.js";
import type BasicWidget from "../basic_widget.js";
import utils from "../../services/utils.js";
import { getEnabledExperimentalFeatureIds } from "../../services/experimental_features.js";
/**
* The root container is the top-most widget/container, from which the entire layout derives.
@@ -37,6 +38,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
this.#setBackdropEffects();
this.#setThemeCapabilities();
this.#setLocaleAndDirection(options.get("locale"));
this.#setExperimentalFeatures();
return super.render();
}
@@ -56,7 +58,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
if (loadResults.isOptionReloaded("maxContentWidth")
|| loadResults.isOptionReloaded("centerContent")) {
this.#setMaxContentWidth();
}
}
@@ -99,6 +101,12 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
document.body.classList.toggle("theme-supports-background-effects", useBgfx);
}
#setExperimentalFeatures() {
for (const featureId of getEnabledExperimentalFeatureIds()) {
document.body.classList.add(`experimental-feature-${featureId}`);
}
}
#setLocaleAndDirection(locale: string) {
const correspondingLocale = LOCALES.find(l => l.id === locale);
document.body.lang = locale;

View File

@@ -6,4 +6,5 @@
.note-split.type-code:not(.mime-text-x-sqlite) > .scrolling-container {
background-color: var(--code-background-color);
}
--scrollbar-background-color: var(--main-background-color);
}

View File

@@ -49,7 +49,7 @@ export default class ScrollingContainer extends Container<BasicWidget> {
}
}
scrollContainerToCommand({ position }: CommandListenerData<"scrollContainerToCommand">) {
scrollContainerToCommand({ position }: CommandListenerData<"scrollContainerTo">) {
this.$widget.scrollTop(position);
}
}

View File

@@ -31,10 +31,12 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
flex-grow: 1;
display: flex;
align-items: center;
margin-block: 0;
}
.modal.popup-editor-dialog .modal-header .note-title-widget {
margin-top: 8px;
display: flex;
align-items: center;
}
.modal.popup-editor-dialog .modal-body {
@@ -42,13 +44,14 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
height: 75vh;
overflow: auto;
display: flex;
flex-direction: column;
flex-direction: column;
}
.modal.popup-editor-dialog .title-row,
.modal.popup-editor-dialog .modal-title,
.modal.popup-editor-dialog .note-icon-widget {
height: 32px;
min-height: unset;
}
.modal.popup-editor-dialog .note-icon-widget {
@@ -99,7 +102,7 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
}
.modal.popup-editor-dialog .note-detail-code-editor {
padding: 0;
padding: 0;
& .cm-editor {
margin: 0;

View File

@@ -1,26 +1,32 @@
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import Modal from "../react/Modal";
import "./PopupEditor.css";
import { useNoteContext, useNoteLabel, useTriliumEvent } from "../react/hooks";
import NoteTitleWidget from "../note_title";
import NoteIcon from "../note_icon";
import NoteContext from "../../components/note_context";
import { NoteContextContext, ParentComponent } from "../react/react_utils";
import NoteDetail from "../NoteDetail";
import { ComponentChildren } from "preact";
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import appContext from "../../components/app_context";
import NoteContext from "../../components/note_context";
import froca from "../../services/froca";
import { t } from "../../services/i18n";
import tree from "../../services/tree";
import utils from "../../services/utils";
import NoteList from "../collections/NoteList";
import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter";
import FormattingToolbar from "../ribbon/FormattingToolbar";
import PromotedAttributes from "../PromotedAttributes";
import FloatingButtons from "../FloatingButtons";
import { DESKTOP_FLOATING_BUTTONS, MOBILE_FLOATING_BUTTONS, POPUP_HIDDEN_FLOATING_BUTTONS } from "../FloatingButtonsDefinitions";
import utils from "../../services/utils";
import tree from "../../services/tree";
import froca from "../../services/froca";
import NoteIcon from "../note_icon";
import NoteTitleWidget from "../note_title";
import NoteDetail from "../NoteDetail";
import PromotedAttributes from "../PromotedAttributes";
import { useNoteContext, useNoteLabel, useTriliumEvent } from "../react/hooks";
import Modal from "../react/Modal";
import { NoteContextContext, ParentComponent } from "../react/react_utils";
import ReadOnlyNoteInfoBar from "../ReadOnlyNoteInfoBar";
import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter";
import FormattingToolbar from "../ribbon/FormattingToolbar";
import MobileEditorToolbar from "../type_widgets/text/mobile_editor_toolbar";
import { t } from "../../services/i18n";
import appContext from "../../components/app_context";
import NoteBadges from "../layout/NoteBadges";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
export default function PopupEditor() {
const [ shown, setShown ] = useState(false);
@@ -61,7 +67,10 @@ export default function PopupEditor() {
<NoteContextContext.Provider value={noteContext}>
<DialogWrapper>
<Modal
title={<TitleRow />}
title={<>
<TitleRow />
{isNewLayout && <NoteBadges />}
</>}
customTitleBarButtons={[{
iconClassName: "bx-expand-alt",
title: t("popup-editor.maximize"),
@@ -75,19 +84,17 @@ export default function PopupEditor() {
className="popup-editor-dialog"
size="lg"
show={shown}
onShown={() => {
parentComponent?.handleEvent("focusOnDetail", { ntxId: noteContext.ntxId });
}}
onShown={() => parentComponent?.handleEvent("focusOnDetail", { ntxId: noteContext.ntxId })}
onHidden={() => setShown(false)}
keepInDom // needed for faster loading
noFocus // automatic focus breaks block popup
>
<ReadOnlyNoteInfoBar />
{!isNewLayout && <ReadOnlyNoteInfoBar />}
<PromotedAttributes />
{isMobile
? <MobileEditorToolbar inPopupEditor />
: <StandaloneRibbonAdapter component={FormattingToolbar} />}
? <MobileEditorToolbar inPopupEditor />
: <StandaloneRibbonAdapter component={FormattingToolbar} />}
<FloatingButtons items={items} />
<NoteDetail />
@@ -95,7 +102,7 @@ export default function PopupEditor() {
</Modal>
</DialogWrapper>
</NoteContextContext.Provider>
)
);
}
export function DialogWrapper({ children }: { children: ComponentChildren }) {
@@ -107,7 +114,7 @@ export function DialogWrapper({ children }: { children: ComponentChildren }) {
<div ref={wrapperRef} class={`quick-edit-dialog-wrapper ${note?.getColorClass() ?? ""}`}>
{children}
</div>
)
);
}
export function TitleRow() {
@@ -116,5 +123,5 @@ export function TitleRow() {
<NoteIcon />
<NoteTitleWidget />
</div>
)
);
}

View File

@@ -8,7 +8,7 @@ import note_types from "../../services/note_types";
import { MenuCommandItem, MenuItem } from "../../menus/context_menu";
import { TreeCommandNames } from "../../menus/tree_context_menu";
import { Suggestion } from "../../services/note_autocomplete";
import Badge from "../react/Badge";
import SimpleBadge from "../react/Badge";
import { useTriliumEvent } from "../react/hooks";
export interface ChooseNoteTypeResponse {
@@ -108,7 +108,7 @@ export default function NoteTypeChooserDialogComponent() {
value={[ item.type, item.templateNoteId ].join(",") }
icon={item.uiIcon}>
{item.title}
{item.badges && item.badges.map((badge) => <Badge {...badge} />)}
{item.badges && item.badges.map((badge) => <SimpleBadge {...badge} />)}
</FormListItem>;
}
})}

View File

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

View File

@@ -0,0 +1,62 @@
.breadcrumb {
position: relative;
align-items: center;
display: flex;
margin: 0;
align-items: center;
font-size: 0.9em;
gap: 0.25em;
flex-wrap: nowrap;
overflow: hidden;
> span,
> span > span {
display: flex;
align-items: center;
min-width: 0;
a {
color: inherit;
text-decoration: none;
min-width: 0;
max-width: 150px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
display: block;
flex-shrink: 2;
}
}
> span:last-of-type a {
max-width: 300px;
flex-shrink: 1;
}
ul {
flex-direction: column;
list-style-type: none;
margin: 0;
padding: 0;
}
.dropdown-item span,
.dropdown-item strong,
.breadcrumb-last-item {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
display: block;
max-width: 300px;
}
.breadcrumb-last-item {
text-decoration: none;
color: unset;
}
input {
padding: 0 10px;
width: 200px;
}
}

View File

@@ -0,0 +1,198 @@
import "./Breadcrumb.css";
import { useMemo, useState } from "preact/hooks";
import { Fragment } from "preact/jsx-runtime";
import appContext from "../../components/app_context";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
import link_context_menu from "../../menus/link_context_menu";
import froca from "../../services/froca";
import ActionButton from "../react/ActionButton";
import Dropdown from "../react/Dropdown";
import { FormListItem } from "../react/FormList";
import { useChildNotes, useNoteLabel, useNoteProperty } from "../react/hooks";
import Icon from "../react/Icon";
import NoteLink from "../react/NoteLink";
const COLLAPSE_THRESHOLD = 5;
const INITIAL_ITEMS = 2;
const FINAL_ITEMS = 2;
export default function Breadcrumb({ note, noteContext }: { note: FNote, noteContext: NoteContext }) {
const notePath = buildNotePaths(noteContext?.notePathArray);
return (
<div className="breadcrumb">
{notePath.length > COLLAPSE_THRESHOLD ? (
<>
{notePath.slice(0, INITIAL_ITEMS).map((item, index) => (
<Fragment key={item}>
<BreadcrumbItem index={index} notePath={item} notePathLength={notePath.length} noteContext={noteContext} />
<BreadcrumbSeparator notePath={item} activeNotePath={notePath[index + 1]} noteContext={noteContext} />
</Fragment>
))}
<BreadcrumbCollapsed items={notePath.slice(INITIAL_ITEMS, -FINAL_ITEMS)} noteContext={noteContext} />
{notePath.slice(-FINAL_ITEMS).map((item, index) => (
<Fragment key={item}>
<BreadcrumbSeparator notePath={notePath[notePath.length - FINAL_ITEMS - (1 - index)]} activeNotePath={item} noteContext={noteContext} />
<BreadcrumbItem index={notePath.length - FINAL_ITEMS + index} notePath={item} notePathLength={notePath.length} noteContext={noteContext} />
</Fragment>
))}
</>
) : (
notePath.map((item, index) => (
<Fragment key={item}>
{index === 0
? <BreadcrumbRoot noteContext={noteContext} />
: <BreadcrumbItem index={index} notePath={item} notePathLength={notePath.length} noteContext={noteContext} />
}
{(index < notePath.length - 1 || note?.hasChildren()) &&
<BreadcrumbSeparator notePath={item} activeNotePath={notePath[index + 1]} noteContext={noteContext} />}
</Fragment>
))
)}
</div>
);
}
function BreadcrumbRoot({ noteContext }: { noteContext: NoteContext | undefined }) {
const note = useMemo(() => froca.getNoteFromCache("root"), []);
useNoteLabel(note, "iconClass");
const title = useNoteProperty(note, "title");
return (note &&
<ActionButton
className="root-note"
icon={note.getIcon()}
text={title ?? ""}
onClick={() => noteContext?.setNote("root")}
onContextMenu={(e) => {
e.preventDefault();
link_context_menu.openContextMenu(note.noteId, e);
}}
/>
);
}
function BreadcrumbLink({ notePath }: { notePath: string }) {
return (
<NoteLink
notePath={notePath}
/>
);
}
function BreadcrumbLastItem({ notePath }: { notePath: string }) {
const noteId = notePath.split("/").at(-1);
const [ note ] = useState(() => froca.getNoteFromCache(noteId!));
const title = useNoteProperty(note, "title");
if (!note) return null;
return (
<a
href="#"
className="breadcrumb-last-item tn-link"
onClick={() => {
const activeNtxId = appContext.tabManager.activeNtxId;
const scrollingContainer = document.querySelector(`[data-ntx-id="${activeNtxId}"] .scrolling-container`);
scrollingContainer?.scrollTo({ top: 0, behavior: "smooth" });
}}
>{title}</a>
);
}
function BreadcrumbItem({ index, notePath, noteContext, notePathLength }: { index: number, notePathLength: number, notePath: string, noteContext: NoteContext | undefined }) {
if (index === 0) {
return <BreadcrumbRoot noteContext={noteContext} />;
}
if (index === notePathLength - 1) {
return <>
<BreadcrumbLastItem notePath={notePath} />
</>;
}
return <BreadcrumbLink notePath={notePath} />;
}
function BreadcrumbSeparator({ notePath, noteContext, activeNotePath }: { notePath: string, activeNotePath: string, noteContext: NoteContext | undefined }) {
return (
<Dropdown
text={<Icon icon="bx bx-chevron-right" />}
noSelectButtonStyle
buttonClassName="icon-action"
hideToggleArrow
dropdownOptions={{ popperConfig: { strategy: "fixed", placement: "top" } }}
>
<BreadcrumbSeparatorDropdownContent notePath={notePath} noteContext={noteContext} activeNotePath={activeNotePath} />
</Dropdown>
);
}
function BreadcrumbSeparatorDropdownContent({ notePath, noteContext, activeNotePath }: { notePath: string, activeNotePath: string, noteContext: NoteContext | undefined }) {
const notePathComponents = notePath.split("/");
const parentNoteId = notePathComponents.at(-1);
const childNotes = useChildNotes(parentNoteId);
return (
<ul className="breadcrumb-child-list">
{childNotes.map((note) => {
const childNotePath = `${notePath}/${note.noteId}`;
return <li key={note.noteId}>
<FormListItem
icon={note.getIcon()}
onClick={() => noteContext?.setNote(childNotePath)}
>
{childNotePath !== activeNotePath
? <span>{note.title}</span>
: <strong>{note.title}</strong>}
</FormListItem>
</li>;
})}
</ul>
);
}
function BreadcrumbCollapsed({ items, noteContext }: { items: string[], noteContext: NoteContext | undefined }) {
return (
<Dropdown
text={<Icon icon="bx bx-dots-horizontal-rounded" />}
noSelectButtonStyle
buttonClassName="icon-action"
hideToggleArrow
dropdownOptions={{ popperConfig: { strategy: "fixed" } }}
>
<ul className="breadcrumb-child-list">
{items.map((notePath) => {
const notePathComponents = notePath.split("/");
const noteId = notePathComponents[notePathComponents.length - 1];
const note = froca.getNoteFromCache(noteId);
if (!note) return null;
return <li key={note.noteId}>
<FormListItem
icon={note.getIcon()}
onClick={() => noteContext?.setNote(notePath)}
>
<span>{note.title}</span>
</FormListItem>
</li>;
})}
</ul>
</Dropdown>
);
}
function buildNotePaths(notePathArray: string[] | undefined) {
if (!notePathArray) return [];
let prefix = "";
const output: string[] = [];
for (const notePath of notePathArray) {
output.push(`${prefix}${notePath}`);
prefix += `${notePath}/`;
}
return output;
}

View File

@@ -0,0 +1,91 @@
:root {
--title-transition: opacity 200ms ease-in;
}
.component.inline-title {
contain: none;
}
.inline-title {
max-width: var(--max-content-width);
padding-inline-start: 24px;
& > .inline-title-row {
display: flex;
align-items: center;
transition: var(--title-transition);
&.hidden {
opacity: 0;
pointer-events: none;
}
}
&.hidden {
display: none;
}
.note-icon-widget {
padding: 0;
}
.inline-title-row {
border-bottom: 2px solid gray;
}
}
.title-row {
&.note-icon-widget,
&.note-title-widget {
transition: var(--title-transition);
}
&.hide-title .note-icon-widget,
&.hide-title .note-title-widget {
opacity: 0;
pointer-events: none;
}
}
.note-split.type-code:not(.mime-text-x-sqlite) .inline-title {
background-color: var(--main-background-color);
}
body.prefers-centered-content .inline-title {
margin-inline: auto;
}
.title-details {
display: flex;
gap: 0.25em;
margin: 0;
margin-top: 4px;
list-style-type: none;
opacity: .5;
span.value {
font-weight: 500;
}
}
.note-type-switcher {
padding: .25em 0;
display: flex;
align-items: center;
overflow-x: auto;
min-width: 0;
gap: 5px;
min-height: 40px;
--badge-radius: 12px;
>* {
flex-shrink: 0;
}
.ext-badge {
--color: var(--input-background-color);
color: var(--main-text-color);
font-size: 0.9rem;
flex-shrink: 0;
}
}

View File

@@ -0,0 +1,303 @@
import "./InlineTitle.css";
import { NoteType } from "@triliumnext/commons";
import clsx from "clsx";
import { ComponentChild } from "preact";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
import { Trans } from "react-i18next";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import froca from "../../services/froca";
import { t } from "../../services/i18n";
import { ViewScope } from "../../services/link";
import { NOTE_TYPES, NoteTypeMapping } from "../../services/note_types";
import server from "../../services/server";
import { formatDateTime } from "../../utils/formatters";
import NoteIcon from "../note_icon";
import NoteTitleWidget from "../note_title";
import { Badge, BadgeWithDropdown } from "../react/Badge";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { useNoteBlob, useNoteContext, useNoteProperty, useStaticTooltip, useTriliumEvent } from "../react/hooks";
import { joinElements } from "../react/react_utils";
import { useNoteMetadata } from "../ribbon/NoteInfoTab";
import { onWheelHorizontalScroll } from "../widget_utils";
const supportedNoteTypes = new Set<NoteType>([
"text", "code"
]);
export default function InlineTitle() {
const { note, parentComponent, viewScope } = useNoteContext();
const type = useNoteProperty(note, "type");
const [ shown, setShown ] = useState(shouldShow(note?.noteId, type, viewScope));
const containerRef = useRef<HTMLDivElement>(null);
const [ titleHidden, setTitleHidden ] = useState(false);
useLayoutEffect(() => {
setShown(shouldShow(note?.noteId, type, viewScope));
}, [ note, type, viewScope ]);
useLayoutEffect(() => {
if (!shown) return;
const titleRow = parentComponent.$widget[0].closest(".note-split")?.querySelector(":scope > .title-row");
if (!titleRow) return;
titleRow.classList.toggle("hide-title", true);
const observer = new IntersectionObserver((entries) => {
titleRow.classList.toggle("hide-title", entries[0].isIntersecting);
setTitleHidden(!entries[0].isIntersecting);
}, {
threshold: 0.85
});
if (containerRef.current) {
observer.observe(containerRef.current);
}
return () => {
titleRow.classList.remove("hide-title");
observer.disconnect();
};
}, [ shown, parentComponent ]);
return (
<div
ref={containerRef}
className={clsx("inline-title", !shown && "hidden")}
>
<div class={clsx("inline-title-row", titleHidden && "hidden")}>
<NoteIcon />
<NoteTitleWidget />
</div>
<NoteTitleDetails />
<NoteTypeSwitcher />
</div>
);
}
function shouldShow(noteId: string | undefined, type: NoteType | undefined, viewScope: ViewScope | undefined) {
if (viewScope?.viewMode !== "default") return false;
if (noteId?.startsWith("_options")) return true;
return type && supportedNoteTypes.has(type);
}
//#region Title details
export function NoteTitleDetails() {
const { note } = useNoteContext();
const { metadata } = useNoteMetadata(note);
const isHiddenNote = note?.noteId.startsWith("_");
const items: ComponentChild[] = [
(!isHiddenNote && metadata?.dateCreated &&
<TextWithValue
i18nKey="note_title.created_on"
value={formatDateTime(metadata.dateCreated, "medium", "none")}
valueTooltip={formatDateTime(metadata.dateCreated, "full", "long")}
/>),
(!isHiddenNote && metadata?.dateModified &&
<TextWithValue
i18nKey="note_title.last_modified"
value={formatDateTime(metadata.dateModified, "medium", "none")}
valueTooltip={formatDateTime(metadata.dateModified, "full", "long")}
/>)
].filter(item => !!item);
return items.length > 0 && (
<div className="title-details">
{joinElements(items, " • ")}
</div>
);
}
function TextWithValue({ i18nKey, value, valueTooltip }: {
i18nKey: string;
value: string;
valueTooltip: string;
}) {
const listItemRef = useRef<HTMLLIElement>(null);
useStaticTooltip(listItemRef, {
selector: "span.value",
title: valueTooltip,
popperConfig: { placement: "bottom" }
});
return (
<li ref={listItemRef}>
<Trans
i18nKey={i18nKey}
components={{
Value: <span className="value">{value}</span> as React.ReactElement
}}
/>
</li>
);
}
//#endregion
//#region Note type switcher
const SWITCHER_PINNED_NOTE_TYPES = new Set<NoteType>([ "text", "code", "book", "canvas" ]);
function NoteTypeSwitcher() {
const { note } = useNoteContext();
const blob = useNoteBlob(note);
const currentNoteType = useNoteProperty(note, "type");
const { pinnedNoteTypes, restNoteTypes } = useMemo(() => {
const pinnedNoteTypes: NoteTypeMapping[] = [];
const restNoteTypes: NoteTypeMapping[] = [];
for (const noteType of NOTE_TYPES) {
if (noteType.reserved || noteType.static || noteType.type === "book") continue;
if (SWITCHER_PINNED_NOTE_TYPES.has(noteType.type)) {
pinnedNoteTypes.push(noteType);
} else {
restNoteTypes.push(noteType);
}
}
return { pinnedNoteTypes, restNoteTypes };
}, []);
const currentNoteTypeData = useMemo(() => NOTE_TYPES.find(t => t.type === currentNoteType), [ currentNoteType ]);
const { builtinTemplates, collectionTemplates } = useBuiltinTemplates();
return (currentNoteType && supportedNoteTypes.has(currentNoteType) &&
<div
className="note-type-switcher"
onWheel={onWheelHorizontalScroll}
>
{note && blob?.contentLength === 0 && (
<>
<div className="intro">{t("note_title.note_type_switcher_label", { type: currentNoteTypeData?.title.toLocaleLowerCase() })}</div>
{pinnedNoteTypes.map(noteType => noteType.type !== currentNoteType && (
<Badge
key={noteType.type}
text={noteType.title}
icon={`bx ${noteType.icon}`}
onClick={() => switchNoteType(note.noteId, noteType)}
/>
))}
{collectionTemplates.length > 0 && <CollectionNoteTypes noteId={note.noteId} collectionTemplates={collectionTemplates} />}
{builtinTemplates.length > 0 && <TemplateNoteTypes noteId={note.noteId} builtinTemplates={builtinTemplates} />}
{restNoteTypes.length > 0 && <MoreNoteTypes noteId={note.noteId} restNoteTypes={restNoteTypes} />}
</>
)}
</div>
);
}
function MoreNoteTypes({ noteId, restNoteTypes }: { noteId: string, restNoteTypes: NoteTypeMapping[] }) {
return (
<BadgeWithDropdown
text={t("note_title.note_type_switcher_others")}
icon="bx bx-dots-vertical-rounded"
>
{restNoteTypes.map(noteType => (
<FormListItem
key={noteType.type}
icon={`bx ${noteType.icon}`}
onClick={() => switchNoteType(noteId, noteType)}
>{noteType.title}</FormListItem>
))}
</BadgeWithDropdown>
);
}
function CollectionNoteTypes({ noteId, collectionTemplates }: { noteId: string, collectionTemplates: FNote[] }) {
return (
<BadgeWithDropdown
text={t("note_title.note_type_switcher_collection")}
icon="bx bx-book"
>
{collectionTemplates.map(collectionTemplate => (
<FormListItem
key={collectionTemplate.noteId}
icon={collectionTemplate.getIcon()}
onClick={() => setTemplate(noteId, collectionTemplate.noteId)}
>{collectionTemplate.title}</FormListItem>
))}
</BadgeWithDropdown>
);
}
function TemplateNoteTypes({ noteId, builtinTemplates }: { noteId: string, builtinTemplates: FNote[] }) {
const [ userTemplates, setUserTemplates ] = useState<FNote[]>([]);
async function refreshTemplates() {
const templateNoteIds = await server.get<string[]>("search-templates");
const templateNotes = await froca.getNotes(templateNoteIds);
setUserTemplates(templateNotes);
}
// First load.
useEffect(() => {
refreshTemplates();
}, []);
// React to external changes.
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().some(attr => attr.type === "label" && attr.name === "template")) {
refreshTemplates();
}
});
return (
<BadgeWithDropdown
text={t("note_title.note_type_switcher_templates")}
icon="bx bx-copy-alt"
>
{userTemplates.map(template => <TemplateItem key={template.noteId} noteId={noteId} template={template} />)}
{userTemplates.length > 0 && <FormDropdownDivider />}
{builtinTemplates.map(template => <TemplateItem key={template.noteId} noteId={noteId} template={template} />)}
</BadgeWithDropdown>
);
}
function TemplateItem({ noteId, template }: { noteId: string, template: FNote }) {
return (
<FormListItem
icon={template.getIcon()}
onClick={() => setTemplate(noteId, template.noteId)}
>{template.title}</FormListItem>
);
}
function switchNoteType(noteId: string, { type, mime }: NoteTypeMapping) {
return server.put(`notes/${noteId}/type`, { type, mime });
}
function setTemplate(noteId: string, templateId: string) {
return attributes.setRelation(noteId, "template", templateId);
}
function useBuiltinTemplates() {
const [ templates, setTemplates ] = useState<{
builtinTemplates: FNote[];
collectionTemplates: FNote[];
}>({
builtinTemplates: [],
collectionTemplates: []
});
async function loadBuiltinTemplates() {
const templatesRoot = await froca.getNote("_templates");
if (!templatesRoot) return;
const childNotes = await templatesRoot.getChildNotes();
const builtinTemplates: FNote[] = [];
const collectionTemplates: FNote[] = [];
for (const childNote of childNotes) {
if (!childNote.hasLabel("template")) continue;
if (childNote.hasLabel("collection")) {
collectionTemplates.push(childNote);
} else {
builtinTemplates.push(childNote);
}
}
setTemplates({ builtinTemplates, collectionTemplates });
}
useEffect(() => {
loadBuiltinTemplates();
}, []);
return templates;
}
//#endregion

View File

@@ -0,0 +1,27 @@
.component.note-badges {
contain: none;
}
.note-badges {
display: flex;
gap: 5px;
min-width: 0;
flex-shrink: 1;
overflow: hidden;
--badge-radius: 12px;
.ext-badge {
&.temporarily-editable-badge { --color: #4fa52b; }
&.read-only-badge { --color: #e33f3b; }
&.share-badge { --color: #3b82f6; }
&.clipped-note-badge { --color: #57a2a5; }
&.execute-badge { --color: #f59e0b; }
}
.dropdown-badge {
&.dropdown-backlinks-badge .dropdown-menu {
min-width: 500px;
}
}
}

View File

@@ -0,0 +1,100 @@
import "./NoteBadges.css";
import clsx from "clsx";
import { ComponentChildren, MouseEventHandler } from "preact";
import { useRef } from "preact/hooks";
import { t } from "../../services/i18n";
import Dropdown, { DropdownProps } from "../react/Dropdown";
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useStaticTooltip } from "../react/hooks";
import Icon from "../react/Icon";
import { useShareInfo } from "../shared_info";
import { Badge } from "../react/Badge";
export default function NoteBadges() {
return (
<div className="note-badges">
<ReadOnlyBadge />
<ShareBadge />
<ClippedNoteBadge />
<ExecuteBadge />
</div>
);
}
function ReadOnlyBadge() {
const { note, noteContext } = useNoteContext();
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
const isExplicitReadOnly = note?.isLabelTruthy("readOnly");
const isTemporarilyEditable = noteContext?.ntxId !== "_popup-editor" && noteContext?.viewScope?.readOnlyTemporarilyDisabled;
if (isTemporarilyEditable) {
return <Badge
icon="bx bx-lock-open-alt"
text={t("breadcrumb_badges.read_only_temporarily_disabled")}
tooltip={t("breadcrumb_badges.read_only_temporarily_disabled_description")}
className="temporarily-editable-badge"
onClick={() => enableEditing(false)}
/>;
} else if (isReadOnly) {
return <Badge
icon="bx bx-lock-alt"
text={isExplicitReadOnly ? t("breadcrumb_badges.read_only_explicit") : t("breadcrumb_badges.read_only_auto")}
tooltip={isExplicitReadOnly ? t("breadcrumb_badges.read_only_explicit_description") : t("breadcrumb_badges.read_only_auto_description")}
className="read-only-badge"
onClick={() => enableEditing()}
/>;
}
}
function ShareBadge() {
const { note } = useNoteContext();
const { isSharedExternally, link, linkHref } = useShareInfo(note);
return (link &&
<Badge
icon={isSharedExternally ? "bx bx-world" : "bx bx-share-alt"}
text={isSharedExternally ? t("breadcrumb_badges.shared_publicly") : t("breadcrumb_badges.shared_locally")}
tooltip={isSharedExternally ?
t("breadcrumb_badges.shared_publicly_description", { link }) :
t("breadcrumb_badges.shared_locally_description", { link })
}
className="share-badge"
href={linkHref}
/>
);
}
function ClippedNoteBadge() {
const { note } = useNoteContext();
const [ pageUrl ] = useNoteLabel(note, "pageUrl");
return (pageUrl &&
<Badge
className="clipped-note-badge"
icon="bx bx-globe"
text={t("breadcrumb_badges.clipped_note")}
tooltip={t("breadcrumb_badges.clipped_note_description", { url: pageUrl })}
href={pageUrl}
/>
);
}
function ExecuteBadge() {
const { note, parentComponent } = useNoteContext();
const isScript = note?.isTriliumScript();
const isSql = note?.isTriliumSqlite();
const isExecutable = isScript || isSql;
const [ executeDescription ] = useNoteLabel(note, "executeDescription");
const [ executeButton ] = useNoteLabelBoolean(note, "executeButton");
return (note && isExecutable && (executeDescription || executeButton) &&
<Badge
className="execute-badge"
icon="bx bx-play"
text={isScript ? t("breadcrumb_badges.execute_script") : t("breadcrumb_badges.execute_sql")}
tooltip={executeDescription || (isScript ? t("breadcrumb_badges.execute_script_description") : t("breadcrumb_badges.execute_sql_description"))}
onClick={() => parentComponent.triggerCommand("runActiveNote")}
/>
);
}

View File

@@ -0,0 +1,28 @@
body.experimental-feature-new-layout {
.component.title-actions {
contain: none;
}
.title-actions {
padding: 0;
display: flex;
gap: 0.25em;
align-items: center;
width: 100%;
max-width: unset;
padding-inline-start: 15px;
padding-bottom: 0.2em;
font-size: 0.8em;
.dropdown-menu {
input.form-control {
padding: 2px 8px;
margin-left: 1em;
}
}
.spacer {
flex-grow: 1;
}
}
}

View File

@@ -0,0 +1,15 @@
import CollectionProperties from "../note_bars/CollectionProperties";
import { useNoteContext, useNoteProperty } from "../react/hooks";
import "./NoteTitleActions.css";
export default function NoteTitleActions() {
const { note } = useNoteContext();
const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_");
const noteType = useNoteProperty(note, "type");
return (
<div className="title-actions">
{note && !isHiddenNote && noteType === "book" && <CollectionProperties note={note} />}
</div>
);
}

View File

@@ -0,0 +1,114 @@
.component.status-bar {
contain: none;
border-top: 1px solid var(--main-border-color);
background-color: var(--left-pane-background-color);
> .status-bar-main-row {
min-height: 28px;
display: flex;
align-items: center;
padding-inline: 0.25em;
font-size: 0.85em;
> .breadcrumb {
flex-grow: 1;
--icon-button-size: 23px;
}
> .actions-row {
padding: 0.1em;
display: flex;
gap: 0.1em;
.btn {
padding: 0 0.5em !important;
background: transparent;
display: flex;
align-items: center;
border: 0;
span:first-of-type {
font-size: 1rem;
}
&.active,
&.dropdown-toggle.show,
&:focus,
&:hover {
background: var(--input-background-color);
}
}
.status-bar-dropdown-button {
&:after {
content: unset;
}
}
}
.dropdown {
.dropdown-toggle {
padding: 0.1em 0.25em;
}
.dropdown-menu {
width: max-content;
}
}
.dropdown-note-info {
ul {
list-style-type: none;
padding: 0.5em;
margin: 0;
display: table;
li {
display: table-row;
> strong {
display: table-cell;
padding: 0.2em 0;
}
> span {
display: table-cell;
user-select: text;
padding-left: 2em;
}
}
}
}
.dropdown-note-paths {
.note-paths-widget {
padding: 0.5em;
}
.note-path-list {
margin: 1em;
padding: 0;
}
}
.dropdown-code-note-switcher {
max-height: 90vh;
overflow: scroll;
}
}
> .attribute-list {
font-size: 0.9em;
padding: 0.5em 0.75em;
.inherited-attributes-widget > div {
padding: 0;
font-size: 0.9em;
}
.attribute-list-editor {
padding: 0 !important;
}
}
}

View File

@@ -0,0 +1,381 @@
import "./StatusBar.css";
import { Locale } from "@triliumnext/commons";
import clsx from "clsx";
import { type ComponentChildren } from "preact";
import { createPortal } from "preact/compat";
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { CommandNames } from "../../components/app_context";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import { t } from "../../services/i18n";
import { ViewScope } from "../../services/link";
import server from "../../services/server";
import { openInAppHelpFromUrl } from "../../services/utils";
import { formatDateTime } from "../../utils/formatters";
import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions";
import Dropdown, { DropdownProps } from "../react/Dropdown";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { useActiveNoteContext, useLegacyImperativeHandlers, useNoteProperty, useStaticTooltip, useTriliumEvent, useTriliumEvents } from "../react/hooks";
import Icon from "../react/Icon";
import { ParentComponent } from "../react/react_utils";
import { ContentLanguagesModal, NoteTypeCodeNoteList, NoteTypeOptionsModal, useLanguageSwitcher, useMimeTypes } from "../ribbon/BasicPropertiesTab";
import AttributeEditor, { AttributeEditorImperativeHandlers } from "../ribbon/components/AttributeEditor";
import InheritedAttributesTab from "../ribbon/InheritedAttributesTab";
import { NoteSizeWidget, useNoteMetadata } from "../ribbon/NoteInfoTab";
import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab";
import { useAttachments } from "../type_widgets/Attachment";
import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector";
import Breadcrumb from "./Breadcrumb";
interface StatusBarContext {
note: FNote;
notePath: string | null | undefined;
noteContext: NoteContext;
viewScope?: ViewScope;
hoistedNoteId?: string;
}
export default function StatusBar() {
const { note, notePath, noteContext, viewScope, hoistedNoteId } = useActiveNoteContext();
const [ attributesShown, setAttributesShown ] = useState(false);
const context: StatusBarContext | undefined | null = note && noteContext && { note, notePath, noteContext, viewScope, hoistedNoteId };
const attributesContext: AttributesProps | undefined | null = context && { ...context, attributesShown, setAttributesShown };
const isHiddenNote = note?.isInHiddenSubtree();
return (
<div className="status-bar">
{attributesContext && <AttributesPane {...attributesContext} />}
<div className="status-bar-main-row">
{context && attributesContext && <>
<Breadcrumb {...context} />
<div className="actions-row">
<CodeNoteSwitcher {...context} />
<LanguageSwitcher {...context} />
{!isHiddenNote && <NotePaths {...context} />}
<AttributesButton {...attributesContext} />
<AttachmentCount {...context} />
<BacklinksBadge {...context} />
<NoteInfoBadge {...context} />
</div>
</>}
</div>
</div>
);
}
function StatusBarDropdown({ children, icon, text, buttonClassName, titleOptions, dropdownOptions, ...dropdownProps }: Omit<DropdownProps, "hideToggleArrow" | "title" | "titlePosition"> & {
title: string;
icon?: string;
}) {
return (
<Dropdown
buttonClassName={clsx("status-bar-dropdown-button", buttonClassName)}
titlePosition="top"
titleOptions={{
popperConfig: {
...titleOptions?.popperConfig,
strategy: "fixed"
},
...titleOptions
}}
dropdownOptions={{
popperConfig: {
strategy: "fixed",
placement: "top"
},
...dropdownOptions
}}
text={<>
{icon && (<><Icon icon={icon} />&nbsp;</>)}
{text}
</>}
{...dropdownProps}
>
{children}
</Dropdown>
);
}
interface StatusBarButtonBaseProps {
className?: string;
icon: string;
title: string;
text?: string | number;
disabled?: boolean;
active?: boolean;
}
type StatusBarButtonWithCommand = StatusBarButtonBaseProps & { triggerCommand: CommandNames; };
type StatusBarButtonWithClick = StatusBarButtonBaseProps & { onClick: () => void; };
function StatusBarButton({ className, icon, text, title, active, ...restProps }: StatusBarButtonWithCommand | StatusBarButtonWithClick) {
const parentComponent = useContext(ParentComponent);
const buttonRef = useRef<HTMLButtonElement>(null);
useStaticTooltip(buttonRef, {
placement: "top",
fallbackPlacements: [ "top" ],
popperConfig: { strategy: "fixed" },
title
});
return (
<button
ref={buttonRef}
className={clsx("btn select-button focus-outline", className, active && "active")}
type="button"
onClick={() => {
if ("triggerCommand" in restProps) {
parentComponent?.triggerCommand(restProps.triggerCommand);
} else {
restProps.onClick();
}
}}
>
<Icon icon={icon} />&nbsp;{text}
</button>
);
}
//#region Language Switcher
function LanguageSwitcher({ note }: StatusBarContext) {
const [ modalShown, setModalShown ] = useState(false);
const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note);
const { activeLocale, processedLocales } = useProcessedLocales(locales, DEFAULT_LOCALE, currentNoteLanguage ?? DEFAULT_LOCALE.id);
return (
<>
{note.type === "text" && <StatusBarDropdown
icon="bx bx-globe"
title={t("status_bar.language_title")}
text={<span dir={activeLocale?.rtl ? "rtl" : "ltr"}>{getLocaleName(activeLocale)}</span>}
>
{processedLocales.map((locale, index) =>
(typeof locale === "object") ? (
<FormListItem
key={locale.id}
rtl={locale.rtl}
checked={locale.id === currentNoteLanguage}
onClick={() => setCurrentNoteLanguage(locale.id)}
>{locale.name}</FormListItem>
) : (
<FormDropdownDivider key={`divider-${index}`} />
)
)}
<FormDropdownDivider />
<FormListItem
onClick={() => openInAppHelpFromUrl("veGu4faJErEM")}
icon="bx bx-help-circle"
>{t("note_language.help-on-languages")}</FormListItem>
<FormListItem
onClick={() => setModalShown(true)}
icon="bx bx-cog"
>{t("note_language.configure-languages")}</FormListItem>
</StatusBarDropdown>}
{createPortal(
<ContentLanguagesModal modalShown={modalShown} setModalShown={setModalShown} />,
document.body
)}
</>
);
}
export function getLocaleName(locale: Locale | null | undefined) {
if (!locale) return "";
if (!locale.id) return "-";
if (locale.name.length <= 4 || locale.rtl) return locale.name; // Some locales like Japanese and Chinese look better than their ID.
return locale.id
.replace("_", "-")
.toLocaleUpperCase();
}
//#endregion
//#region Note info
export function NoteInfoBadge({ note }: { note: FNote | null | undefined }) {
const { metadata, ...sizeProps } = useNoteMetadata(note);
return (note &&
<StatusBarDropdown
icon="bx bx-info-circle"
title={t("status_bar.note_info_title")}
dropdownContainerClassName="dropdown-note-info"
dropdownOptions={{ autoClose: "outside" }}
>
<ul>
<NoteInfoValue text={t("note_info_widget.created")} value={formatDateTime(metadata?.dateCreated)} />
<NoteInfoValue text={t("note_info_widget.modified")} value={formatDateTime(metadata?.dateModified)} />
<NoteInfoValue text={t("note_info_widget.type")} value={<span>{note.type} {note.mime && <span>({note.mime})</span>}</span>} />
<NoteInfoValue text={t("note_info_widget.note_id")} value={<code>{note.noteId}</code>} />
<NoteInfoValue text={t("note_info_widget.note_size")} title={t("note_info_widget.note_size_info")} value={<NoteSizeWidget {...sizeProps} />} />
</ul>
</StatusBarDropdown>
);
}
function NoteInfoValue({ text, title, value }: { text: string; title?: string, value: ComponentChildren }) {
return (
<li>
<strong title={title}>{text}{": "}</strong>
<span>{value}</span>
</li>
);
}
//#endregion
//#region Backlinks
function BacklinksBadge({ note, viewScope }: StatusBarContext) {
const count = useBacklinkCount(note, viewScope?.viewMode === "default");
return (note && count > 0 &&
<StatusBarDropdown
className="backlinks-badge backlinks-widget"
icon="bx bx-link"
text={count}
title={t("status_bar.backlinks_title", { count })}
dropdownContainerClassName="backlinks-items"
>
<BacklinksList note={note} />
</StatusBarDropdown>
);
}
//#endregion
//#region Attachment count
function AttachmentCount({ note }: StatusBarContext) {
const attachments = useAttachments(note);
const count = attachments.length;
return (note && count > 0 &&
<StatusBarButton
className="attachment-count-button"
icon="bx bx-paperclip"
text={count}
title={t("status_bar.attachments_title", { count })}
triggerCommand="showAttachments"
/>
);
}
//#endregion
//#region Attributes
interface AttributesProps extends StatusBarContext {
attributesShown: boolean;
setAttributesShown: (shown: boolean) => void;
}
function AttributesButton({ note, attributesShown, setAttributesShown }: AttributesProps) {
const [ count, setCount ] = useState(note.attributes.length);
// React to note changes.
useEffect(() => {
setCount(note.attributes.length);
}, [ note ]);
// React to changes in count.
useTriliumEvent("entitiesReloaded", (({loadResults}) => {
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
setCount(note.attributes.length);
}
}));
return (
<StatusBarButton
className="attributes-button"
icon="bx bx-list-check"
title={t("status_bar.attributes_title")}
text={t("status_bar.attributes", { count })}
active={attributesShown}
onClick={() => setAttributesShown(!attributesShown)}
/>
);
}
function AttributesPane({ note, noteContext, attributesShown, setAttributesShown }: AttributesProps) {
const parentComponent = useContext(ParentComponent);
const api = useRef<AttributeEditorImperativeHandlers>(null);
const context = parentComponent && {
componentId: parentComponent.componentId,
note,
hidden: !note
};
// Show on keyboard shortcuts.
useTriliumEvents([ "addNewLabel", "addNewRelation" ], () => setAttributesShown(true));
// Interaction with the attribute editor.
useLegacyImperativeHandlers(useMemo(() => ({
saveAttributesCommand: () => api.current?.save(),
reloadAttributesCommand: () => api.current?.refresh(),
updateAttributeListCommand: ({ attributes }) => api.current?.renderOwnedAttributes(attributes)
}), [ api ]));
return (context &&
<div className={clsx("attribute-list", !attributesShown && "hidden-ext")}>
<InheritedAttributesTab {...context} />
<AttributeEditor
{...context}
api={api}
ntxId={noteContext.ntxId}
/>
</div>
);
}
//#endregion
//#region Note paths
function NotePaths({ note, hoistedNoteId, notePath }: StatusBarContext) {
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
return (
<StatusBarDropdown
title={t("status_bar.note_paths_title")}
dropdownContainerClassName="dropdown-note-paths"
icon="bx bx-directions"
text={sortedNotePaths?.length}
>
<NotePathsWidget
sortedNotePaths={sortedNotePaths}
currentNotePath={notePath}
/>
</StatusBarDropdown>
);
}
//#endregion
//#region Code note switcher
function CodeNoteSwitcher({ note }: StatusBarContext) {
const [ modalShown, setModalShown ] = useState(false);
const currentNoteMime = useNoteProperty(note, "mime");
const mimeTypes = useMimeTypes();
const correspondingMimeType = useMemo(() => (
mimeTypes.find(m => m.mime === currentNoteMime)
), [ mimeTypes, currentNoteMime ]);
return (note.type === "code" &&
<>
<StatusBarDropdown
icon="bx bx-code-curly"
text={correspondingMimeType?.title}
title={t("status_bar.code_note_switcher")}
dropdownContainerClassName="dropdown-code-note-switcher"
>
<NoteTypeCodeNoteList
currentMimeType={currentNoteMime}
mimeTypes={mimeTypes}
changeNoteType={(type, mime) => server.put(`notes/${note.noteId}/type`, { type, mime })}
setModalShown={() => setModalShown(true)}
/>
</StatusBarDropdown>
{createPortal(
<NoteTypeOptionsModal modalShown={modalShown} setModalShown={setModalShown} />,
document.body
)}
</>
);
}
//#endregion

View File

@@ -0,0 +1,220 @@
import { t } from "i18next";
import { useContext } from "preact/hooks";
import { Fragment } from "preact/jsx-runtime";
import FNote from "../../entities/fnote";
import { ViewTypeOptions } from "../collections/interface";
import Dropdown from "../react/Dropdown";
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
import FormTextBox from "../react/FormTextBox";
import { useNoteLabel, useNoteLabelBoolean, useNoteLabelWithDefault } from "../react/hooks";
import Icon from "../react/Icon";
import { ParentComponent } from "../react/react_utils";
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config";
import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab";
import ActionButton from "../react/ActionButton";
import { getHelpUrlForNote } from "../../services/in_app_help";
import { openInAppHelpFromUrl } from "../../services/utils";
const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
grid: "bx bxs-grid",
list: "bx bx-list-ul",
calendar: "bx bx-calendar",
table: "bx bx-table",
geoMap: "bx bx-map-alt",
board: "bx bx-columns",
presentation: "bx bx-rectangle"
};
export default function CollectionProperties({ note }: { note: FNote }) {
const [ viewType, setViewType ] = useViewType(note);
return (
<>
<ViewTypeSwitcher viewType={viewType} setViewType={setViewType} />
<ViewOptions note={note} viewType={viewType} />
<div className="spacer" />
<HelpButton note={note} />
</>
);
}
function ViewTypeSwitcher({ viewType, setViewType }: { viewType: ViewTypeOptions, setViewType: (newValue: ViewTypeOptions) => void }) {
return (
<Dropdown
text={<>
<Icon icon={ICON_MAPPINGS[viewType]} />&nbsp;
{VIEW_TYPE_MAPPINGS[viewType]}
</>}
>
{Object.entries(VIEW_TYPE_MAPPINGS).map(([ key, label ]) => (
<FormListItem
key={key}
onClick={() => setViewType(key as ViewTypeOptions)}
selected={viewType === key}
disabled={viewType === key}
icon={ICON_MAPPINGS[key as ViewTypeOptions]}
>{label}</FormListItem>
))}
</Dropdown>
);
}
function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOptions }) {
const properties = bookPropertiesConfig[viewType].properties;
return (
<Dropdown
buttonClassName="bx bx-cog icon-action"
hideToggleArrow
>
{properties.map(property => (
<ViewProperty key={property.label} note={note} property={property} />
))}
{properties.length > 0 && <FormDropdownDivider />}
<ViewProperty note={note} property={{
type: "checkbox",
icon: "bx bx-archive",
label: t("book_properties.include_archived_notes"),
bindToLabel: "includeArchived"
} as CheckBoxProperty} />
</Dropdown>
);
}
function ViewProperty({ note, property }: { note: FNote, property: BookProperty }) {
switch (property.type) {
case "button":
return <ButtonPropertyView note={note} property={property} />;
case "split-button":
return <SplitButtonPropertyView note={note} property={property} />;
case "checkbox":
return <CheckBoxPropertyView note={note} property={property} />;
case "number":
return <NumberPropertyView note={note} property={property} />;
case "combobox":
return <ComboBoxPropertyView note={note} property={property} />;
}
}
function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) {
const parentComponent = useContext(ParentComponent);
return (
<FormListItem
icon={property.icon}
title={property.title}
onClick={() => {
if (!parentComponent) return;
property.onClick({
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
});
}}
>{property.label}</FormListItem>
);
}
function SplitButtonPropertyView({ note, property }: { note: FNote, property: SplitButtonProperty }) {
const parentComponent = useContext(ParentComponent);
const ItemsComponent = property.items;
const clickContext = parentComponent && {
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
};
return (parentComponent &&
<FormDropdownSubmenu
icon={property.icon ?? "bx bx-empty"}
title={property.label}
onDropdownToggleClicked={() => clickContext && property.onClick(clickContext)}
>
<ItemsComponent note={note} parentComponent={parentComponent} />
</FormDropdownSubmenu>
);
}
function NumberPropertyView({ note, property }: { note: FNote, property: NumberProperty }) {
//@ts-expect-error Interop with text box which takes in string values even for numbers.
const [ value, setValue ] = useNoteLabel(note, property.bindToLabel);
const disabled = property.disabled?.(note);
return (
<FormListItem
icon={property.icon}
disabled={disabled}
onClick={(e) => e.stopPropagation()}
>
{property.label}
<FormTextBox
type="number"
currentValue={value ?? ""} onChange={setValue}
style={{ width: (property.width ?? 100) }}
min={property.min ?? 0}
disabled={disabled}
/>
</FormListItem>
);
}
function ComboBoxPropertyView({ note, property }: { note: FNote, property: ComboBoxProperty }) {
const [ value, setValue ] = useNoteLabelWithDefault(note, property.bindToLabel, property.defaultValue ?? "");
function renderItem(option: ComboBoxItem) {
return (
<FormListItem
key={option.value}
checked={value === option.value}
onClick={() => setValue(option.value)}
>
{option.label}
</FormListItem>
);
}
return (
<FormDropdownSubmenu
title={property.label}
icon={property.icon ?? "bx bx-empty"}
>
{(property.options).map((option, index) => {
if ("items" in option) {
return (
<Fragment key={option.title}>
<FormListItem key={option.title} disabled>{option.title}</FormListItem>
{option.items.map(renderItem)}
{index < property.options.length - 1 && <FormDropdownDivider />}
</Fragment>
);
} else {
return renderItem(option);
}
})}
</FormDropdownSubmenu>
);
}
function CheckBoxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) {
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
return (
<FormListToggleableItem
icon={property.icon}
title={property.label}
currentValue={value}
onChange={setValue}
/>
);
}
function HelpButton({ note }: { note: FNote }) {
const helpUrl = getHelpUrlForNote(note);
return (helpUrl && (
<ActionButton
icon="bx bx-help-circle"
onClick={(() => openInAppHelpFromUrl(helpUrl))}
text={t("help-button.title")}
/>
));
}

View File

@@ -1,5 +1,5 @@
.note-icon-widget {
padding-inline-start: 7px;
padding-inline-start: 10px;
margin-inline-end: 0;
width: 50px;
height: 50px;
@@ -13,7 +13,7 @@
cursor: pointer;
color: var(--muted-text-color);
}
.note-icon-widget button.note-icon:disabled {
cursor: default;
opacity: .75;
@@ -68,4 +68,4 @@
border: 1px dashed var(--muted-text-color);
width: 1em;
height: 1em;
}
}

View File

@@ -27,4 +27,46 @@ body.mobile .note-title-widget input.note-title {
body.desktop .note-title-widget input.note-title {
font-size: 180%;
}
}
body.experimental-feature-new-layout {
.title-row {
container-type: size;
border-bottom: 1px solid var(--main-border-color);
transition: border 400ms ease-out;
&.hide-title {
border-bottom-color: transparent;
transition: none;
}
@container (max-width: 700px) {
.note-icon-widget .note-icon {
font-size: 1.3em;
}
.note-title-widget {
display: flex;
align-items: center;
.note-title {
font-size: 1em;
}
}
.note-title-widget:focus-within + .note-badges,
.ext-badge .text {
display: none;
}
}
}
.note-title-widget {
input.note-title {
--input-focus-background: transparent;
--input-focus-outline-color: transparent;
--input-hover-background: transparent;
--input-hover-color: initial;
--input-focus-color: initial;
}
}
}

View File

@@ -62,6 +62,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
this.$widget.addClass(utils.getNoteTypeClass(note.type));
this.$widget.addClass(utils.getMimeTypeClass(note.mime));
this.$widget.addClass(`view-mode-${this.noteContext?.viewScope?.viewMode ?? "default"}`);
this.$widget.toggleClass(["bgfx", "options"], note.isOptions());
this.$widget.toggleClass("protected", note.isProtected);

View File

@@ -0,0 +1,49 @@
.ext-badge {
display: flex;
align-items: center;
padding: 2px 6px;
border-radius: var(--badge-radius);
font-size: 0.75em;
background-color: var(--color, transparent);
color: white;
min-width: 0;
flex-shrink: 1;
&.clickable {
cursor: pointer;
&:hover {
background-color: color-mix(in srgb, var(--color, --badge-background-color) 80%, black);
}
}
a {
color: inherit !important;
text-decoration: none;
}
> * {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.dropdown-badge {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border-radius: var(--badge-radius);
.ext-badge {
border-radius: 0;
}
.btn {
border: 0;
margin: 0;
padding: 0;
}
}

View File

@@ -1,8 +1,78 @@
interface BadgeProps {
import "./Badge.css";
import clsx from "clsx";
import { ComponentChildren, MouseEventHandler } from "preact";
import { useRef } from "preact/hooks";
import Dropdown, { DropdownProps } from "./Dropdown";
import { useStaticTooltip } from "./hooks";
import Icon from "./Icon";
interface SimpleBadgeProps {
className?: string;
title: string;
}
export default function Badge({ title, className }: BadgeProps) {
return <span class={`badge ${className ?? ""}`}>{title}</span>
}
interface BadgeProps {
text?: string;
icon?: string;
className?: string;
tooltip?: string;
onClick?: MouseEventHandler<HTMLDivElement>;
href?: string;
}
export default function SimpleBadge({ title, className }: SimpleBadgeProps) {
return <span class={`badge ${className ?? ""}`}>{title}</span>;
}
export function Badge({ icon, className, text, tooltip, onClick, href }: BadgeProps) {
const containerRef = useRef<HTMLDivElement>(null);
useStaticTooltip(containerRef, {
placement: "bottom",
fallbackPlacements: [ "bottom" ],
animation: false,
html: true,
title: tooltip
});
const content = <>
{icon && <><Icon icon={icon} />&nbsp;</>}
<span class="text">{text}</span>
</>;
return (
<div
ref={containerRef}
className={clsx("ext-badge", className, { "clickable": !!onClick })}
onClick={onClick}
>
{href ? <a href={href}>{content}</a> : <span>{content}</span>}
</div>
);
}
export function BadgeWithDropdown({ children, tooltip, className, dropdownOptions, ...props }: BadgeProps & {
children: ComponentChildren,
dropdownOptions?: Partial<DropdownProps>
}) {
return (
<Dropdown
className={`dropdown-badge dropdown-${className}`}
text={<Badge className={className} {...props} />}
noDropdownListStyle
noSelectButtonStyle
hideToggleArrow
title={tooltip}
titlePosition="bottom"
{...dropdownOptions}
dropdownOptions={{
...dropdownOptions?.dropdownOptions,
popperConfig: {
...dropdownOptions?.dropdownOptions?.popperConfig,
placement: "bottom", strategy: "fixed"
}
}}
>{children}</Dropdown>
);
}

View File

@@ -117,8 +117,8 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi
aria-expanded="false"
id={id ?? ariaId}
disabled={disabled}
onMouseOver={() => showTooltip()}
onMouseLeave={() => hideTooltip()}
onMouseEnter={showTooltip}
onMouseLeave={hideTooltip}
{...buttonProps}
>
{text}

View File

@@ -1,9 +1,29 @@
.dropdown-item .description {
font-size: small;
color: var(--muted-text-color);
white-space: normal;
}
.dropdown-item {
.description {
font-size: small;
color: var(--muted-text-color);
white-space: normal;
}
.dropdown-item span.bx {
flex-shrink: 0;
}
span.bx {
flex-shrink: 0;
}
.switch-widget {
flex-grow: 1;
width: 100%;
--switch-track-width: 40px;
--switch-track-height: 20px;
--switch-thumb-width: 12px;
--switch-thumb-height: var(--switch-thumb-width);
.contextual-help {
margin-inline-start: 0.25em;
cursor: pointer;
}
.switch-spacer {
flex-grow: 1;
}
}
}

View File

@@ -5,8 +5,9 @@ import { useEffect, useMemo, useRef, useState, type CSSProperties } from "preact
import "./FormList.css";
import { CommandNames } from "../../components/app_context";
import { useStaticTooltip } from "./hooks";
import { handleRightToLeftPlacement, isMobile } from "../../services/utils";
import { handleRightToLeftPlacement, isMobile, openInAppHelpFromUrl } from "../../services/utils";
import clsx from "clsx";
import FormToggle from "./FormToggle";
interface FormListOpts {
children: ComponentChildren;
@@ -94,12 +95,13 @@ interface FormListItemOpts {
description?: string;
className?: string;
rtl?: boolean;
postContent?: ComponentChildren;
}
const TOOLTIP_CONFIG: Partial<Tooltip.Options> = {
placement: handleRightToLeftPlacement("right"),
fallbackPlacements: [ handleRightToLeftPlacement("right") ]
}
};
export function FormListItem({ className, icon, value, title, active, disabled, checked, container, onClick, selected, rtl, triggerCommand, description, ...contentProps }: FormListItemOpts) {
const itemRef = useRef<HTMLLIElement>(null);
@@ -132,6 +134,49 @@ export function FormListItem({ className, icon, value, title, active, disabled,
);
}
export function FormListToggleableItem({ title, currentValue, onChange, disabled, helpPage, ...props }: Omit<FormListItemOpts, "onClick" | "children"> & {
title: string;
currentValue: boolean;
helpPage?: string;
onChange(newValue: boolean): void | Promise<void>;
}) {
const isWaiting = useRef(false);
return (
<FormListItem
{...props}
disabled={disabled}
onClick={async (e) => {
if ((e.target as HTMLElement | null)?.classList.contains("contextual-help")) {
return;
}
e.stopPropagation();
if (!disabled && !isWaiting.current) {
isWaiting.current = true;
await onChange(!currentValue);
isWaiting.current = false;
}
}}>
<FormToggle
switchOnName={title}
switchOffName={title}
currentValue={currentValue}
onChange={() => {}}
afterName={<>
{helpPage && (
<span
class="bx bx-help-circle contextual-help"
onClick={() => openInAppHelpFromUrl(helpPage)}
/>
)}
<span class="switch-spacer" />
</>}
/>
</FormListItem>
);
}
function FormListContent({ children, badges, description, disabled, disabledTooltip }: Pick<FormListItemOpts, "children" | "badges" | "description" | "disabled" | "disabledTooltip">) {
return <>
{children}
@@ -139,7 +184,7 @@ function FormListContent({ children, badges, description, disabled, disabledTool
<span className={`badge ${className ?? ""}`}>{text}</span>
))}
{disabled && disabledTooltip && (
<span class="bx bx-info-circle disabled-tooltip" title={disabledTooltip} />
<span class="bx bx-info-circle contextual-help" title={disabledTooltip} />
)}
{description && <div className="description">{description}</div>}
</>;
@@ -161,11 +206,17 @@ export function FormDropdownDivider() {
return <div className="dropdown-divider" />;
}
export function FormDropdownSubmenu({ icon, title, children }: { icon: string, title: ComponentChildren, children: ComponentChildren }) {
export function FormDropdownSubmenu({ icon, title, children, dropStart, onDropdownToggleClicked }: {
icon: string,
title: ComponentChildren,
children: ComponentChildren,
onDropdownToggleClicked?: () => void,
dropStart?: boolean
}) {
const [ openOnMobile, setOpenOnMobile ] = useState(false);
return (
<li className={`dropdown-item dropdown-submenu ${openOnMobile ? "submenu-open" : ""}`}>
<li className={clsx("dropdown-item dropdown-submenu", { "submenu-open": openOnMobile, "dropstart": dropStart })}>
<span
className="dropdown-toggle"
onClick={(e) => {
@@ -174,6 +225,10 @@ export function FormDropdownSubmenu({ icon, title, children }: { icon: string, t
if (isMobile()) {
setOpenOnMobile(!openOnMobile);
}
if (onDropdownToggleClicked) {
onDropdownToggleClicked();
}
}}
>
<Icon icon={icon} />{" "}
@@ -184,5 +239,5 @@ export function FormDropdownSubmenu({ icon, title, children }: { icon: string, t
{children}
</ul>
</li>
)
);
}

View File

@@ -24,6 +24,14 @@
border-radius: 24px;
background-color: var(--switch-off-track-background);
transition: background 200ms ease-in;
&.disable-transitions {
transition: none !important;
&:after {
transition: none !important;
}
}
}
.switch-widget .switch-button.on {
@@ -103,4 +111,4 @@ body[dir=rtl] .switch-widget .switch-button.on:after {
.switch-widget .switch-help-button:hover {
color: var(--main-text-color);
}
}

View File

@@ -1,25 +1,39 @@
import clsx from "clsx";
import "./FormToggle.css";
import HelpButton from "./HelpButton";
import { useEffect, useState } from "preact/hooks";
import { ComponentChildren } from "preact";
interface FormToggleProps {
currentValue: boolean | null;
onChange(newValue: boolean): void;
switchOnName: string;
switchOnTooltip: string;
switchOnTooltip?: string;
switchOffName: string;
switchOffTooltip: string;
switchOffTooltip?: string;
helpPage?: string;
disabled?: boolean;
afterName?: ComponentChildren;
}
export default function FormToggle({ currentValue, helpPage, switchOnName, switchOnTooltip, switchOffName, switchOffTooltip, onChange, disabled }: FormToggleProps) {
export default function FormToggle({ currentValue, helpPage, switchOnName, switchOnTooltip, switchOffName, switchOffTooltip, onChange, disabled, afterName }: FormToggleProps) {
const [ disableTransition, setDisableTransition ] = useState(true);
useEffect(() => {
const timeout = setTimeout(() => {
setDisableTransition(false);
}, 100);
return () => clearTimeout(timeout);
}, []);
return (
<div className="switch-widget">
<span className="switch-name">{ currentValue ? switchOffName : switchOnName }</span>
{ afterName }
<label>
<div
className={`switch-button ${currentValue ? "on" : ""} ${disabled ? "disabled" : ""}`}
className={clsx("switch-button", { "on": currentValue, disabled, "disable-transitions": disableTransition })}
title={currentValue ? switchOffTooltip : switchOnTooltip }
>
<input
@@ -37,5 +51,5 @@ export default function FormToggle({ currentValue, helpPage, switchOnName, switc
{ helpPage && <HelpButton className="switch-help-button" helpPage={helpPage} />}
</div>
)
}
);
}

View File

@@ -201,7 +201,7 @@ export function useTriliumOptionBool(name: OptionNames, needsRefresh?: boolean):
return [
(value === "true"),
(newValue) => setValue(newValue ? "true" : "false")
]
];
}
/**
@@ -217,17 +217,18 @@ export function useTriliumOptionInt(name: OptionNames): [number, (newValue: numb
return [
(parseInt(value, 10)),
(newValue) => setValue(newValue)
]
];
}
/**
* Similar to {@link useTriliumOption}, but the object value is parsed to and from a JSON instead of a string.
*
* @param name the name of the option to listen for.
* @param needsRefresh whether to reload the frontend whenever the value is changed.
* @returns an array where the first value is the current option value and the second value is the setter.
*/
export function useTriliumOptionJson<T>(name: OptionNames): [ T, (newValue: T) => Promise<void> ] {
const [ value, setValue ] = useTriliumOption(name);
export function useTriliumOptionJson<T>(name: OptionNames, needsRefresh?: boolean): [ T, (newValue: T) => Promise<void> ] {
const [ value, setValue ] = useTriliumOption(name, needsRefresh);
useDebugValue(name);
return [
(JSON.parse(value) as T),
@@ -315,7 +316,7 @@ export function useNoteContext() {
useDebugValue(() => `notePath=${notePath}, ntxId=${noteContext?.ntxId}`);
return {
note: note,
note,
noteId: noteContext?.note?.noteId,
notePath: noteContext?.notePath,
hoistedNoteId: noteContext?.hoistedNoteId,
@@ -326,7 +327,65 @@ export function useNoteContext() {
parentComponent,
isReadOnlyTemporarilyDisabled
};
}
/**
* Similar to {@link useNoteContext}, but instead of using the note context from the split container that the component is part of, it uses the active note context instead
* (the note currently focused by the user).
*/
export function useActiveNoteContext() {
const [ noteContext, setNoteContext ] = useState<NoteContext | undefined>(appContext.tabManager.getActiveContext() ?? undefined);
const [ notePath, setNotePath ] = useState<string | null | undefined>();
const [ note, setNote ] = useState<FNote | null | undefined>();
const [ , setViewScope ] = useState<ViewScope>();
const [ isReadOnlyTemporarilyDisabled, setIsReadOnlyTemporarilyDisabled ] = useState<boolean | null | undefined>(noteContext?.viewScope?.isReadOnly);
const [ refreshCounter, setRefreshCounter ] = useState(0);
useEffect(() => {
if (!noteContext) {
setNoteContext(appContext.tabManager.getActiveContext() ?? undefined);
}
}, [ noteContext ]);
useEffect(() => {
setNote(noteContext?.note);
}, [ notePath ]);
useTriliumEvents([ "setNoteContext", "activeContextChanged", "noteSwitchedAndActivated", "noteSwitched" ], () => {
const noteContext = appContext.tabManager.getActiveContext() ?? undefined;
setNoteContext(noteContext);
setNotePath(noteContext?.notePath);
setViewScope(noteContext?.viewScope);
});
useTriliumEvent("frocaReloaded", () => {
setNote(noteContext?.note);
});
useTriliumEvent("noteTypeMimeChanged", ({ noteId }) => {
if (noteId === note?.noteId) {
setRefreshCounter(refreshCounter + 1);
}
});
useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => {
if (eventNoteContext.ntxId === noteContext?.ntxId) {
setIsReadOnlyTemporarilyDisabled(eventNoteContext?.viewScope?.readOnlyTemporarilyDisabled);
}
});
const parentComponent = useContext(ParentComponent) as ReactWrappedWidget;
useDebugValue(() => `notePath=${notePath}, ntxId=${noteContext?.ntxId}`);
return {
note,
noteId: noteContext?.note?.noteId,
notePath: noteContext?.notePath,
hoistedNoteId: noteContext?.hoistedNoteId,
ntxId: noteContext?.ntxId,
viewScope: noteContext?.viewScope,
componentId: parentComponent.componentId,
noteContext,
parentComponent,
isReadOnlyTemporarilyDisabled
};
}
/**
@@ -844,10 +903,12 @@ export function useGlobalShortcut(keyboardShortcut: string | null | undefined, h
*/
export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: NoteContext | undefined) {
const [ isReadOnly, setIsReadOnly ] = useState<boolean | undefined>(undefined);
const [ readOnlyAttr ] = useNoteLabelBoolean(note, "readOnly");
const [ autoReadOnlyDisabledAttr ] = useNoteLabelBoolean(note, "autoReadOnlyDisabled");
const enableEditing = useCallback(() => {
const enableEditing = useCallback((enabled = true) => {
if (noteContext?.viewScope) {
noteContext.viewScope.readOnlyTemporarilyDisabled = true;
noteContext.viewScope.readOnlyTemporarilyDisabled = enabled;
appContext.triggerEvent("readOnlyTemporarilyDisabled", {noteContext});
}
}, [noteContext]);
@@ -858,11 +919,11 @@ export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: N
setIsReadOnly(readOnly);
});
}
}, [ note, noteContext, noteContext?.viewScope ]);
}, [ note, noteContext, noteContext?.viewScope, readOnlyAttr, autoReadOnlyDisabledAttr ]);
useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => {
if (noteContext?.ntxId === eventNoteContext.ntxId) {
setIsReadOnly(false);
setIsReadOnly(!noteContext.viewScope?.readOnlyTemporarilyDisabled);
}
});
@@ -886,15 +947,42 @@ async function isNoteReadOnly(note: FNote, noteContext: NoteContext) {
return true;
}
export function useChildNotes(parentNoteId: string) {
export function useChildNotes(parentNoteId: string | undefined) {
const [ childNotes, setChildNotes ] = useState<FNote[]>([]);
useEffect(() => {
(async function() {
const parentNote = await froca.getNote(parentNoteId);
const childNotes = await parentNote?.getChildNotes();
let childNotes: FNote[] | undefined;
if (parentNoteId) {
const parentNote = await froca.getNote(parentNoteId);
childNotes = await parentNote?.getChildNotes();
}
setChildNotes(childNotes ?? []);
})();
}, [ parentNoteId ]);
}, [ parentNoteId ]);
return childNotes;
}
export function useLauncherVisibility(launchNoteId: string) {
const checkIfVisible = useCallback(() => {
const note = froca.getNoteFromCache(launchNoteId);
return note?.getParentBranches().some(branch =>
[ "_lbVisibleLaunchers", "_lbMobileVisibleLaunchers" ].includes(branch.parentNoteId)) ?? false;
}, [ launchNoteId ]);
const [ isVisible, setIsVisible ] = useState<boolean>(checkIfVisible());
// React to note not being available in the cache.
useEffect(() => {
froca.getNote(launchNoteId).then(() => setIsVisible(checkIfVisible()));
}, [ launchNoteId, checkIfVisible ]);
// React to changes.
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getBranchRows().some(branch => branch.noteId === launchNoteId)) {
setIsVisible(checkIfVisible());
}
});
return isVisible;
}

View File

@@ -44,7 +44,7 @@ export function disposeReactWidget(container: Element) {
render(null, container);
}
export function joinElements(components: ComponentChild[] | undefined, separator = ", ") {
export function joinElements(components: ComponentChild[] | undefined, separator: ComponentChild = ", ") {
if (!components) return <></>;
const joinedComponents: ComponentChild[] = [];

View File

@@ -1,26 +1,29 @@
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
import Dropdown from "../react/Dropdown";
import { NOTE_TYPES } from "../../services/note_types";
import { FormDropdownDivider, FormListBadge, FormListItem } from "../react/FormList";
import { getAvailableLocales, t } from "../../services/i18n";
import { useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumOption } from "../react/hooks";
import mime_types from "../../services/mime_types";
import { Locale, LOCALES, NoteType, ToggleInParentResponse } from "@triliumnext/commons";
import server from "../../services/server";
import dialog from "../../services/dialog";
import FormToggle from "../react/FormToggle";
import { MimeType, NoteType, ToggleInParentResponse } from "@triliumnext/commons";
import { ComponentChildren } from "preact";
import { createPortal } from "preact/compat";
import { Dispatch, StateUpdater, useCallback, useEffect, useMemo, useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import protected_session from "../../services/protected_session";
import FormDropdownList from "../react/FormDropdownList";
import toast from "../../services/toast";
import branches from "../../services/branches";
import dialog from "../../services/dialog";
import { getAvailableLocales, t } from "../../services/i18n";
import mime_types from "../../services/mime_types";
import { NOTE_TYPES } from "../../services/note_types";
import protected_session from "../../services/protected_session";
import server from "../../services/server";
import sync from "../../services/sync";
import toast from "../../services/toast";
import Dropdown from "../react/Dropdown";
import FormDropdownList from "../react/FormDropdownList";
import { FormDropdownDivider, FormListBadge, FormListItem } from "../react/FormList";
import FormToggle from "../react/FormToggle";
import HelpButton from "../react/HelpButton";
import { TabContext } from "./ribbon-interface";
import { useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumOption } from "../react/hooks";
import Modal from "../react/Modal";
import { CodeMimeTypesList } from "../type_widgets/options/code_notes";
import { ContentLanguagesList } from "../type_widgets/options/i18n";
import { LocaleSelector } from "../type_widgets/options/components/LocaleSelector";
import { ContentLanguagesList } from "../type_widgets/options/i18n";
import { TabContext } from "./ribbon-interface";
export default function BasicPropertiesTab({ note }: TabContext) {
return (
@@ -37,18 +40,40 @@ export default function BasicPropertiesTab({ note }: TabContext) {
}
function NoteTypeWidget({ note }: { note?: FNote | null }) {
const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []);
const [ codeNotesMimeTypes ] = useTriliumOption("codeNotesMimeTypes");
const mimeTypes = useMemo(() => {
mime_types.loadMimeTypes();
return mime_types.getMimeTypes().filter(mimeType => mimeType.enabled)
}, [ codeNotesMimeTypes ]);
const notSelectableNoteTypes = useMemo(() => NOTE_TYPES.filter((nt) => nt.reserved || nt.static).map((nt) => nt.type), []);
const currentNoteType = useNoteProperty(note, "type") ?? undefined;
const currentNoteMime = useNoteProperty(note, "mime");
const [ modalShown, setModalShown ] = useState(false);
return (
<div className="note-type-container">
<span>{t("basic_properties.note_type")}:</span> &nbsp;
<Dropdown
dropdownContainerClassName="note-type-dropdown"
text={<span className="note-type-desc">{findTypeTitle(currentNoteType, currentNoteMime)}</span>}
disabled={notSelectableNoteTypes.includes(currentNoteType ?? "text")}
>
<NoteTypeDropdownContent currentNoteType={currentNoteType} currentNoteMime={currentNoteMime} note={note} setModalShown={setModalShown} />
</Dropdown>
{createPortal(
<NoteTypeOptionsModal modalShown={modalShown} setModalShown={setModalShown} />,
document.body
)}
</div>
);
}
export function NoteTypeDropdownContent({ currentNoteType, currentNoteMime, note, setModalShown, noCodeNotes }: {
currentNoteType?: NoteType;
currentNoteMime?: string | null;
note?: FNote | null;
setModalShown: Dispatch<StateUpdater<boolean>>;
noCodeNotes?: boolean;
}) {
const mimeTypes = useMimeTypes();
const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []);
const changeNoteType = useCallback(async (type: NoteType, mime?: string) => {
if (!note || (type === currentNoteType && mime === currentNoteMime)) {
return;
@@ -68,71 +93,94 @@ function NoteTypeWidget({ note }: { note?: FNote | null }) {
}, [ note, currentNoteType, currentNoteMime ]);
return (
<div className="note-type-container">
<span>{t("basic_properties.note_type")}:</span> &nbsp;
<Dropdown
dropdownContainerClassName="note-type-dropdown"
text={<span className="note-type-desc">{findTypeTitle(currentNoteType, currentNoteMime)}</span>}
disabled={notSelectableNoteTypes.includes(currentNoteType ?? "text")}
>
{noteTypes.map(({ isNew, isBeta, type, mime, title }) => {
const badges: FormListBadge[] = [];
if (isNew) {
badges.push({
className: "new-note-type-badge",
text: t("note_types.new-feature")
});
}
if (isBeta) {
badges.push({
text: t("note_types.beta-feature")
});
}
<>
{noteTypes.map(({ isNew, isBeta, type, mime, title }) => {
const badges: FormListBadge[] = [];
if (isNew) {
badges.push({
className: "new-note-type-badge",
text: t("note_types.new-feature")
});
}
if (isBeta) {
badges.push({
text: t("note_types.beta-feature")
});
}
const checked = (type === currentNoteType);
if (type !== "code") {
return (
const checked = (type === currentNoteType);
if (noCodeNotes || type !== "code") {
return (
<FormListItem
checked={checked}
badges={badges}
onClick={() => changeNoteType(type, mime)}
>{title}</FormListItem>
);
} else {
return (
<>
<FormDropdownDivider />
<FormListItem
checked={checked}
badges={badges}
onClick={() => changeNoteType(type, mime)}
>{title}</FormListItem>
);
} else {
return (
<>
<FormDropdownDivider />
<FormListItem
checked={checked}
disabled
>
<strong>{title}</strong>
</FormListItem>
</>
)
}
})}
disabled
>
<strong>{title}</strong>
</FormListItem>
</>
);
}
})}
{mimeTypes.map(({ title, mime }) => (
<FormListItem onClick={() => changeNoteType("code", mime)}>
{title}
</FormListItem>
))}
{!noCodeNotes && <NoteTypeCodeNoteList mimeTypes={mimeTypes} changeNoteType={changeNoteType} setModalShown={setModalShown} />}
</>
);
}
<FormDropdownDivider />
<FormListItem icon="bx bx-cog" onClick={() => setModalShown(true)}>{t("basic_properties.configure_code_notes")}</FormListItem>
</Dropdown>
export function NoteTypeCodeNoteList({ currentMimeType, mimeTypes, changeNoteType, setModalShown }: {
currentMimeType?: string;
mimeTypes: MimeType[];
changeNoteType(type: NoteType, mime: string): void;
setModalShown(shown: boolean): void;
}) {
return (
<>
{mimeTypes.map(({ title, mime }) => (
<FormListItem
key={mime}
checked={mime === currentMimeType}
onClick={() => changeNoteType("code", mime)}
>
{title}
</FormListItem>
))}
<Modal
className="code-mime-types-modal"
title={t("code_mime_types.title")}
show={modalShown} onHidden={() => setModalShown(false)}
size="xl" scrollable
>
<CodeMimeTypesList />
</Modal>
</div>
)
<FormDropdownDivider />
<FormListItem icon="bx bx-cog" onClick={() => setModalShown(true)}>{t("basic_properties.configure_code_notes")}</FormListItem>
</>
);
}
export function useMimeTypes() {
const [ codeNotesMimeTypes ] = useTriliumOption("codeNotesMimeTypes");
const mimeTypes = useMemo(() => {
mime_types.loadMimeTypes();
return mime_types.getMimeTypes().filter(mimeType => mimeType.enabled);
}, [ codeNotesMimeTypes ]); // eslint-disable-line react-hooks/exhaustive-deps
return mimeTypes;
}
export function NoteTypeOptionsModal({ modalShown, setModalShown }: { modalShown: boolean, setModalShown: (shown: boolean) => void }) {
return (
<Modal
className="code-mime-types-modal"
title={t("code_mime_types.title")}
show={modalShown} onHidden={() => setModalShown(false)}
size="xl" scrollable
>
<CodeMimeTypesList />
</Modal>
);
}
function ProtectedNoteSwitch({ note }: { note?: FNote | null }) {
@@ -187,22 +235,11 @@ function EditabilitySelect({ note }: { note?: FNote | null }) {
}}
/>
</div>
)
);
}
function BookmarkSwitch({ note }: { note?: FNote | null }) {
const [ isBookmarked, setIsBookmarked ] = useState<boolean>(false);
const refreshState = useCallback(() => {
const isBookmarked = note && !!note.getParentBranches().find((b) => b.parentNoteId === "_lbBookmarks");
setIsBookmarked(!!isBookmarked);
}, [ note ]);
useEffect(() => refreshState(), [ note ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (note && loadResults.getBranchRows().find((b) => b.noteId === note.noteId)) {
refreshState();
}
});
const [ isBookmarked, setIsBookmarked ] = useNoteBookmarkState(note);
return (
<div className="bookmark-switch-container">
@@ -210,18 +247,36 @@ function BookmarkSwitch({ note }: { note?: FNote | null }) {
switchOnName={t("bookmark_switch.bookmark")} switchOnTooltip={t("bookmark_switch.bookmark_this_note")}
switchOffName={t("bookmark_switch.bookmark")} switchOffTooltip={t("bookmark_switch.remove_bookmark")}
currentValue={isBookmarked}
onChange={async (shouldBookmark) => {
if (!note) return;
const resp = await server.put<ToggleInParentResponse>(`notes/${note.noteId}/toggle-in-parent/_lbBookmarks/${shouldBookmark}`);
if (!resp.success && "message" in resp) {
toast.showError(resp.message);
}
}}
onChange={setIsBookmarked}
disabled={["root", "_hidden"].includes(note?.noteId ?? "")}
/>
</div>
)
);
}
export function useNoteBookmarkState(note: FNote | null | undefined) {
const [ isBookmarked, setIsBookmarked ] = useState<boolean>(false);
const refreshState = useCallback(() => {
const isBookmarked = note && !!note.getParentBranches().find((b) => b.parentNoteId === "_lbBookmarks");
setIsBookmarked(!!isBookmarked);
}, [ note ]);
const changeHandler = useCallback(async (shouldBookmark: boolean) => {
if (!note) return;
const resp = await server.put<ToggleInParentResponse>(`notes/${note.noteId}/toggle-in-parent/_lbBookmarks/${shouldBookmark}`);
if (!resp.success && "message" in resp) {
toast.showError(resp.message);
}
}, [ note ]);
useEffect(() => refreshState(), [ refreshState ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (note && loadResults.getBranchRows().find((b) => b.noteId === note.noteId)) {
refreshState();
}
});
return [ isBookmarked, changeHandler ] as const;
}
function TemplateSwitch({ note }: { note?: FNote | null }) {
@@ -237,16 +292,33 @@ function TemplateSwitch({ note }: { note?: FNote | null }) {
currentValue={isTemplate} onChange={setIsTemplate}
/>
</div>
)
);
}
function SharedSwitch({ note }: { note?: FNote | null }) {
const [ isShared, switchShareState ] = useShareState(note);
return (
<div className="shared-switch-container">
<FormToggle
currentValue={isShared}
onChange={switchShareState}
switchOnName={t("shared_switch.shared")} switchOnTooltip={t("shared_switch.toggle-on-title")}
switchOffName={t("shared_switch.shared")} switchOffTooltip={t("shared_switch.toggle-off-title")}
helpPage="R9pX4DGra2Vt"
disabled={["root", "_share", "_hidden"].includes(note?.noteId ?? "") || note?.noteId.startsWith("_options")}
/>
</div>
);
}
export function useShareState(note: FNote | null | undefined) {
const [ isShared, setIsShared ] = useState(false);
const refreshState = useCallback(() => {
setIsShared(!!note?.hasAncestor("_share"));
}, [ note ]);
useEffect(() => refreshState(), [ note ]);
useEffect(() => refreshState(), [ refreshState ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (note && loadResults.getBranchRows().find((b) => b.noteId === note.noteId)) {
refreshState();
@@ -271,63 +343,71 @@ function SharedSwitch({ note }: { note?: FNote | null }) {
sync.syncNow(true);
}, [ note ]);
return (
<div className="shared-switch-container">
<FormToggle
currentValue={isShared}
onChange={switchShareState}
switchOnName={t("shared_switch.shared")} switchOnTooltip={t("shared_switch.toggle-on-title")}
switchOffName={t("shared_switch.shared")} switchOffTooltip={t("shared_switch.toggle-off-title")}
helpPage="R9pX4DGra2Vt"
disabled={["root", "_share", "_hidden"].includes(note?.noteId ?? "") || note?.noteId.startsWith("_options")}
/>
</div>
)
return [ isShared, switchShareState ] as const;
}
function NoteLanguageSwitch({ note }: { note?: FNote | null }) {
return (
<div className="note-language-container">
<span>{t("basic_properties.language")}:</span>
&nbsp;
<NoteLanguageSelector note={note} />
<HelpButton helpPage="veGu4faJErEM" style={{ marginInlineStart: "4px" }} />
</div>
);
}
export function NoteLanguageSelector({ note }: { note: FNote | null | undefined }) {
const [ modalShown, setModalShown ] = useState(false);
const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note);
return (
<>
<LocaleSelector
locales={locales}
defaultLocale={DEFAULT_LOCALE}
currentValue={currentNoteLanguage} onChange={setCurrentNoteLanguage}
extraChildren={<>
<FormListItem
onClick={() => setModalShown(true)}
icon="bx bx-cog"
>{t("note_language.configure-languages")}</FormListItem>
</>}
/>
{createPortal(
<ContentLanguagesModal modalShown={modalShown} setModalShown={setModalShown} />,
document.body
)}
</>
);
}
export function useLanguageSwitcher(note: FNote | null | undefined) {
const [ languages ] = useTriliumOption("languages");
const DEFAULT_LOCALE = {
id: "",
name: t("note_language.not_set")
};
const [ currentNoteLanguage, setCurrentNoteLanguage ] = useNoteLabel(note, "language");
const [ modalShown, setModalShown ] = useState(false);
const locales = useMemo(() => {
const enabledLanguages = JSON.parse(languages ?? "[]") as string[];
const filteredLanguages = getAvailableLocales().filter((l) => typeof l !== "object" || enabledLanguages.includes(l.id));
return filteredLanguages;
}, [ languages ]);
return { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage };
}
export function ContentLanguagesModal({ modalShown, setModalShown }: { modalShown: boolean, setModalShown: (shown: boolean) => void }) {
return (
<div className="note-language-container">
<span>{t("basic_properties.language")}:</span>
&nbsp;
<LocaleSelector
locales={locales}
defaultLocale={DEFAULT_LOCALE}
currentValue={currentNoteLanguage ?? ""} onChange={setCurrentNoteLanguage}
extraChildren={(
<FormListItem
onClick={() => setModalShown(true)}
icon="bx bx-cog"
>{t("note_language.configure-languages")}</FormListItem>
)}
>
</LocaleSelector>
<HelpButton helpPage="B0lcI9xz1r8K" style={{ marginInlineStart: "4px" }} />
<Modal
className="content-languages-modal"
title={t("content_language.title")}
show={modalShown} onHidden={() => setModalShown(false)}
size="lg" scrollable
>
<ContentLanguagesList />
</Modal>
</div>
<Modal
className="content-languages-modal"
title={t("content_language.title")}
show={modalShown} onHidden={() => setModalShown(false)}
size="lg" scrollable
>
<ContentLanguagesList />
</Modal>
);
}

View File

@@ -12,34 +12,41 @@ import FormCheckbox from "../react/FormCheckbox";
import FormTextBox from "../react/FormTextBox";
import { ComponentChildren } from "preact";
import { ViewTypeOptions } from "../collections/interface";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
const VIEW_TYPE_MAPPINGS: Record<ViewTypeOptions, string> = {
grid: t("book_properties.grid"),
list: t("book_properties.list"),
calendar: t("book_properties.calendar"),
table: t("book_properties.table"),
geoMap: t("book_properties.geo-map"),
board: t("book_properties.board"),
presentation: t("book_properties.presentation")
export const VIEW_TYPE_MAPPINGS: Record<ViewTypeOptions, string> = {
grid: t("book_properties.grid"),
list: t("book_properties.list"),
calendar: t("book_properties.calendar"),
table: t("book_properties.table"),
geoMap: t("book_properties.geo-map"),
board: t("book_properties.board"),
presentation: t("book_properties.presentation")
};
export default function CollectionPropertiesTab({ note }: TabContext) {
const [ viewType, setViewType ] = useNoteLabel(note, "viewType");
const defaultViewType = (note?.type === "search" ? "list" : "grid");
const viewTypeWithDefault = (viewType ?? defaultViewType) as ViewTypeOptions;
const properties = bookPropertiesConfig[viewTypeWithDefault].properties;
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
return (
<div className="book-properties-widget">
{note && (
<>
<CollectionTypeSwitcher viewType={viewTypeWithDefault} setViewType={setViewType} />
<BookProperties viewType={viewTypeWithDefault} note={note} properties={properties} />
</>
)}
</div>
);
export default function CollectionPropertiesTab({ note }: TabContext) {
const [viewType, setViewType] = useViewType(note);
const properties = bookPropertiesConfig[viewType].properties;
return (
<div className="book-properties-widget">
{note && (
<>
{!isNewLayout && <CollectionTypeSwitcher viewType={viewType} setViewType={setViewType} />}
<BookProperties viewType={viewType} note={note} properties={properties} />
</>
)}
</div>
);
}
export function useViewType(note: FNote | null | undefined) {
const [ viewType, setViewType ] = useNoteLabel(note, "viewType");
const defaultViewType = (note?.type === "search" ? "list" : "grid");
const viewTypeWithDefault = (viewType ?? defaultViewType) as ViewTypeOptions;
return [ viewTypeWithDefault, setViewType ] as const;
}
function CollectionTypeSwitcher({ viewType, setViewType }: { viewType: string, setViewType: (newValue: string) => void }) {
@@ -148,7 +155,7 @@ function NumberPropertyView({ note, property }: { note: FNote, property: NumberP
<FormTextBox
type="number"
currentValue={value ?? ""} onChange={setValue}
style={{ width: (property.width ?? 100) + "px" }}
style={{ width: (property.width ?? 100) }}
min={property.min ?? 0}
disabled={disabled}
/>

View File

@@ -0,0 +1,151 @@
import { NoteType } from "@triliumnext/commons";
import { beforeAll, describe, expect, it, vi } from "vitest";
import NoteContext from "../../components/note_context";
import { ViewMode } from "../../services/link";
import { randomString } from "../../services/utils";
import { buildNote } from "../../test/easy-froca";
import { getFormattingToolbarState } from "./FormattingToolbar";
interface NoteContextInfo {
type: NoteType;
viewScope?: ViewMode;
isReadOnly?: boolean;
}
describe("Formatting toolbar logic", () => {
beforeAll(() => {
vi.mock("../../services/tree.ts", () => ({
default: {
getActiveContextNotePath() {
return "root";
},
resolveNotePath(inputNotePath: string) {
return inputNotePath;
},
getNoteIdFromUrl(url) {
return url.split("/").at(-1);
}
}
}));
buildNote({
id: "root",
title: "Root"
});
});
async function buildConfig(noteContextInfos: NoteContextInfo[], activeIndex: number = 0) {
const noteContexts: NoteContext[] = [];
for (const noteContextData of noteContextInfos) {
const noteContext = new NoteContext(randomString(10));
const note = buildNote({
title: randomString(5),
type: noteContextData.type
});
noteContext.noteId = note.noteId;
expect(noteContext.note).toBe(note);
noteContext.viewScope = {
viewMode: noteContextData.viewScope ?? "default"
};
noteContext.isReadOnly = async () => !!noteContextData.isReadOnly;
noteContext.getSubContexts = () => [];
noteContexts.push(noteContext);
};
const mainNoteContext = noteContexts[0];
for (const noteContext of noteContexts) {
noteContext.getMainContext = () => mainNoteContext;
}
mainNoteContext.getSubContexts = () => noteContexts;
return noteContexts[activeIndex];
}
async function testSplit(noteContextInfos: NoteContextInfo[], activeIndex: number = 0, editor = "ckeditor-classic") {
const noteContext = await buildConfig(noteContextInfos, activeIndex);
return await getFormattingToolbarState(noteContext, noteContext.note, editor);
}
describe("Single split", () => {
it("should be hidden for floating toolbar", async () => {
expect(await testSplit([ { type: "text" } ], 0, "ckeditor-balloon")).toBe("hidden");
});
it("should be visible for single text note", async () => {
expect(await testSplit([ { type: "text" } ])).toBe("visible");
});
it("should be hidden for read-only text note", async () => {
expect(await testSplit([ { type: "text", isReadOnly: true } ])).toBe("hidden");
});
it("should be hidden for non-text note", async () => {
expect(await testSplit([ { type: "code" } ])).toBe("hidden");
});
it("should be hidden for wrong view mode", async () => {
expect(await testSplit([ { type: "text", viewScope: "attachments" } ])).toBe("hidden");
});
});
describe("Multi split", () => {
it("should be hidden for floating toolbar", async () => {
expect(await testSplit([
{ type: "text" },
{ type: "text" },
], 0, "ckeditor-balloon")).toBe("hidden");
});
it("should be visible for two text notes", async () => {
expect(await testSplit([
{ type: "text" },
{ type: "text" },
])).toBe("visible");
});
it("should be disabled if on a non-text note", async () => {
expect(await testSplit([
{ type: "text" },
{ type: "code" },
], 1)).toBe("disabled");
});
it("should be hidden for all non-text notes", async () => {
expect(await testSplit([
{ type: "code" },
{ type: "canvas" },
])).toBe("hidden");
});
it("should be hidden for all read-only text notes", async () => {
expect(await testSplit([
{ type: "text", isReadOnly: true },
{ type: "text", isReadOnly: true },
])).toBe("hidden");
});
it("should be visible for mixed view mode", async () => {
expect(await testSplit([
{ type: "text" },
{ type: "text", viewScope: "attachments" }
])).toBe("visible");
});
it("should be hidden for all wrong view mode", async () => {
expect(await testSplit([
{ type: "text", viewScope: "attachments" },
{ type: "text", viewScope: "attachments" }
])).toBe("hidden");
});
it("should be disabled for wrong view mode", async () => {
expect(await testSplit([
{ type: "text" },
{ type: "text", viewScope: "attachments" }
], 1)).toBe("disabled");
});
});
});

View File

@@ -1,5 +1,9 @@
import { useRef } from "preact/hooks";
import { useTriliumEvent, useTriliumOption } from "../react/hooks";
import clsx from "clsx";
import { useEffect, useRef, useState } from "preact/hooks";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
import { useActiveNoteContext, useNoteProperty, useTriliumEvent, useTriliumEvents, useTriliumOption } from "../react/hooks";
import { TabContext } from "./ribbon-interface";
/**
@@ -33,5 +37,116 @@ export default function FormattingToolbar({ hidden, ntxId }: TabContext) {
ref={containerRef}
className={`classic-toolbar-widget ${hidden ? "hidden-ext" : ""}`}
/>
)
);
};
const toolbarCache = new Map<string, HTMLElement | null | undefined>();
export function FixedFormattingToolbar() {
const containerRef = useRef<HTMLDivElement>(null);
const { note, noteContext, ntxId } = useActiveNoteContext();
const noteType = useNoteProperty(note, "type");
const renderState = useRenderState(noteContext, note);
const [ toolbarToRender, setToolbarToRender ] = useState<HTMLElement | null | undefined>();
// Populate the cache with the toolbar of every note context.
useTriliumEvent("textEditorRefreshed", ({ ntxId: eventNtxId, editor }) => {
if (!eventNtxId) return;
const toolbar = editor.ui.view.toolbar?.element;
toolbarCache.set(eventNtxId, toolbar);
// Replace on the spot if the editor crashed.
if (eventNtxId === ntxId) {
setToolbarToRender(toolbar);
}
});
// Clean the cache when tabs are closed.
useTriliumEvent("noteContextRemoved", ({ ntxIds: eventNtxIds }) => {
for (const eventNtxId of eventNtxIds) {
toolbarCache.delete(eventNtxId);
}
});
// Switch between the cached toolbar when user navigates to a different note context.
useEffect(() => {
if (!ntxId) return;
const toolbar = toolbarCache.get(ntxId);
if (toolbar) {
setToolbarToRender(toolbar);
}
}, [ ntxId, noteType, noteContext ]);
// Render the toolbar.
useEffect(() => {
if (toolbarToRender) {
containerRef.current?.replaceChildren(toolbarToRender);
} else {
containerRef.current?.replaceChildren();
}
}, [ toolbarToRender ]);
return (
<div
ref={containerRef}
className={clsx("classic-toolbar-widget", {
"hidden-ext": renderState === "hidden",
"disabled": renderState === "disabled"
})}
/>
);
}
function useRenderState(activeNoteContext: NoteContext | undefined, activeNote: FNote | null | undefined) {
const [ textNoteEditorType ] = useTriliumOption("textNoteEditorType");
const [ state, setState ] = useState("hidden");
useTriliumEvents([ "newNoteContextCreated", "noteContextRemoved" ], () => {
getFormattingToolbarState(activeNoteContext, activeNote, textNoteEditorType).then(setState);
});
useEffect(() => {
getFormattingToolbarState(activeNoteContext, activeNote, textNoteEditorType).then(setState);
}, [ activeNoteContext, activeNote, textNoteEditorType ]);
return state;
}
export async function getFormattingToolbarState(activeNoteContext: NoteContext | undefined, activeNote: FNote | null | undefined, textNoteEditorType: string) {
if (!activeNoteContext || textNoteEditorType !== "ckeditor-classic") {
return "hidden";
}
const subContexts = activeNoteContext?.getMainContext().getSubContexts() ?? [];
if (subContexts.length === 1) {
if (activeNote?.type !== "text" || activeNoteContext.viewScope?.viewMode !== "default") {
return "hidden";
}
const isReadOnly = await activeNoteContext.isReadOnly();
if (isReadOnly) {
return "hidden";
}
return "visible";
}
// If there are multiple note contexts (e.g. splits), the logic is slightly different.
const textNoteContexts = subContexts.filter(s => s.note?.type === "text" && s.viewScope?.viewMode === "default");
const textNoteContextsReadOnly = await Promise.all(textNoteContexts.map(sc => sc.isReadOnly()));
// If all text notes are hidden, no need to display the toolbar at all.
if (textNoteContextsReadOnly.indexOf(false) === -1) {
return "hidden";
}
// If the current subcontext is not a text note, but there is at least an editable text then it must be disabled.
if (activeNote?.type !== "text") return "disabled";
// If the current subcontext is a text note, it must not be read-only.
const subContextIndex = textNoteContexts.indexOf(activeNoteContext);
if (subContextIndex !== -1) {
if (textNoteContextsReadOnly[subContextIndex]) return "disabled";
}
if (activeNoteContext.viewScope?.viewMode !== "default") return "disabled";
return "visible";
}

View File

@@ -9,7 +9,7 @@ import RawHtml from "../react/RawHtml";
import { joinElements } from "../react/react_utils";
import AttributeDetailWidget from "../attribute_widgets/attribute_detail";
export default function InheritedAttributesTab({ note, componentId }: TabContext) {
export default function InheritedAttributesTab({ note, componentId }: Pick<TabContext, "note" | "componentId">) {
const [ inheritedAttributes, setInheritedAttributes ] = useState<FAttribute[]>();
const [ attributeDetailWidgetEl, attributeDetailWidget ] = useLegacyWidget(() => new AttributeDetailWidget());
@@ -34,7 +34,7 @@ export default function InheritedAttributesTab({ note, componentId }: TabContext
refresh();
}
});
return (
<div className="inherited-attributes-widget">
<div className="inherited-attributes-container selectable-text">
@@ -83,4 +83,4 @@ function InheritedAttribute({ attribute, onClick }: { attribute: FAttribute, onC
onClick={onClick}
/>
);
}
}

View File

@@ -1,174 +1,269 @@
import { ConvertToAttachmentResponse } from "@triliumnext/commons";
import { FormDropdownDivider, FormListHeader, FormListItem } from "../react/FormList";
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
import { ParentComponent } from "../react/react_utils";
import { t } from "../../services/i18n"
import { useContext } from "preact/hooks";
import { useIsNoteReadOnly, useNoteLabel, useNoteProperty } from "../react/hooks";
import { useTriliumOption } from "../react/hooks";
import ActionButton from "../react/ActionButton"
import { useContext, useState } from "preact/hooks";
import appContext, { CommandNames } from "../../components/app_context";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
import branches from "../../services/branches";
import dialog from "../../services/dialog";
import Dropdown from "../react/Dropdown";
import FNote from "../../entities/fnote"
import NoteContext from "../../components/note_context";
import { t } from "../../services/i18n";
import server from "../../services/server";
import toast from "../../services/toast";
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
import ws from "../../services/ws";
import ActionButton from "../react/ActionButton";
import Dropdown from "../react/Dropdown";
import { FormDropdownDivider, FormDropdownSubmenu, FormListHeader, FormListItem, FormListToggleableItem } from "../react/FormList";
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumOption } from "../react/hooks";
import { ParentComponent } from "../react/react_utils";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import { NoteTypeDropdownContent, useNoteBookmarkState, useShareState } from "./BasicPropertiesTab";
import protected_session from "../../services/protected_session";
interface NoteActionsProps {
note?: FNote;
noteContext?: NoteContext;
}
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
export default function NoteActions({ note, noteContext }: NoteActionsProps) {
return (
<>
{note && <RevisionsButton note={note} />}
{note && note.type !== "launcher" && <NoteContextMenu note={note as FNote} noteContext={noteContext}/>}
</>
);
export default function NoteActions() {
const { note, noteContext } = useNoteContext();
return (
<div className="ribbon-button-container" style={{ contain: "none" }}>
{note && !isNewLayout && <RevisionsButton note={note} />}
{note && note.type !== "launcher" && <NoteContextMenu note={note as FNote} noteContext={noteContext} />}
</div>
);
}
function RevisionsButton({ note }: { note: FNote }) {
const isEnabled = !["launcher", "doc"].includes(note?.type ?? "");
const isEnabled = !["launcher", "doc"].includes(note?.type ?? "");
return (isEnabled &&
<ActionButton
icon="bx bx-history"
text={t("revisions_button.note_revisions")}
triggerCommand="showRevisions"
titlePosition="bottom"
/>
);
return (isEnabled &&
<ActionButton
icon="bx bx-history"
text={t("revisions_button.note_revisions")}
triggerCommand="showRevisions"
titlePosition="bottom"
/>
);
}
function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
const parentComponent = useContext(ParentComponent);
const noteType = useNoteProperty(note, "type") ?? "";
const [ viewType ] = useNoteLabel(note, "viewType");
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType);
const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
const isPrintable = ["text", "code"].includes(noteType) || (noteType === "book" && ["presentation", "list", "table"].includes(viewType ?? ""));
const isElectron = getIsElectron();
const isMac = getIsMac();
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(noteType);
const isSearchOrBook = ["search", "book"].includes(noteType);
const [ syncServerHost ] = useTriliumOption("syncServerHost");
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext);
const parentComponent = useContext(ParentComponent);
const noteType = useNoteProperty(note, "type") ?? "";
const [viewType] = useNoteLabel(note, "viewType");
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType);
const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
const isPrintable = ["text", "code"].includes(noteType) || (noteType === "book" && ["presentation", "list", "table"].includes(viewType ?? ""));
const isElectron = getIsElectron();
const isMac = getIsMac();
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(noteType);
const isSearchOrBook = ["search", "book"].includes(noteType);
const isHelpPage = note.noteId.startsWith("_help");
const [syncServerHost] = useTriliumOption("syncServerHost");
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
const isNormalViewMode = noteContext?.viewScope?.viewMode === "default";
return (
<Dropdown
buttonClassName="bx bx-dots-vertical-rounded"
className="note-actions"
hideToggleArrow
noSelectButtonStyle
iconAction>
return (
<Dropdown
buttonClassName={ isNewLayout ? "bx bx-dots-horizontal-rounded" : "bx bx-dots-vertical-rounded" }
className="note-actions"
hideToggleArrow
noSelectButtonStyle
iconAction>
{isReadOnly && <>
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
command={() => enableEditing()} />
<FormDropdownDivider />
</>}
<CommandItem command="findInText" icon="bx bx-search" disabled={!isSearchable} text={t("note_actions.search_in_note")} />
<CommandItem command="showAttachments" icon="bx bx-paperclip" disabled={isInOptionsOrHelp} text={t("note_actions.note_attachments")} />
{isNewLayout && <CommandItem command="toggleRibbonTabNoteMap" icon="bx bxs-network-chart" disabled={isInOptionsOrHelp} text={t("note_actions.note_map")} />}
<FormDropdownDivider />
{isNewLayout && isNormalViewMode && !isHelpPage && <>
<NoteBasicProperties note={note} />
<FormDropdownDivider />
</>}
<CommandItem icon="bx bx-import" text={t("note_actions.import_files")}
disabled={isInOptionsOrHelp || note.type === "search"}
command={() => parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} />
<CommandItem icon="bx bx-export" text={t("note_actions.export_note")}
disabled={isInOptionsOrHelp || note.noteId === "_backendLog"}
command={() => noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", {
notePath: noteContext.notePath,
defaultType: "single"
})} />
{isElectron && <CommandItem command="exportAsPdf" icon="bx bxs-file-pdf" disabled={!isPrintable} text={t("note_actions.print_pdf")} />}
<CommandItem command="printActiveNote" icon="bx bx-printer" disabled={!isPrintable} text={t("note_actions.print_note")} />
<FormDropdownDivider />
<CommandItem command="showRevisions" icon="bx bx-history" text={t("note_actions.view_revisions")} />
<CommandItem command="forceSaveRevision" icon="bx bx-save" disabled={isInOptionsOrHelp} text={t("note_actions.save_revision")} />
<FormDropdownDivider />
{canBeConvertedToAttachment && <ConvertToAttachment note={note} />}
{note.type === "render" && <CommandItem command="renderActiveNote" icon="bx bx-extension" text={t("note_actions.re_render_note")}
/>}
<FormDropdownSubmenu icon="bx bx-wrench" title={t("note_actions.advanced")} dropStart>
<CommandItem command="openNoteExternally" icon="bx bx-file-find" disabled={isSearchOrBook || !isElectron} text={t("note_actions.open_note_externally")} title={t("note_actions.open_note_externally_title")} />
<CommandItem command="openNoteCustom" icon="bx bx-customize" disabled={isSearchOrBook || isMac || !isElectron} text={t("note_actions.open_note_custom")} />
<CommandItem command="showNoteSource" icon="bx bx-code" disabled={!hasSource} text={t("note_actions.note_source")} />
{(syncServerHost && isElectron) &&
<CommandItem command="openNoteOnServer" icon="bx bx-world" disabled={!syncServerHost} text={t("note_actions.open_note_on_server")} />
}
{glob.isDev && <DevelopmentActions note={note} noteContext={noteContext} />}
</FormDropdownSubmenu>
<FormDropdownDivider />
<CommandItem icon="bx bx-trash destructive-action-icon" text={t("note_actions.delete_note")} destructive
disabled={isInOptionsOrHelp}
command={() => branches.deleteNotes([note.getParentBranches()[0].branchId])}
/>
</Dropdown>
);
}
function NoteBasicProperties({ note }: { note: FNote }) {
const [ isBookmarked, setIsBookmarked ] = useNoteBookmarkState(note);
const [ isShared, switchShareState ] = useShareState(note);
const [ isTemplate, setIsTemplate ] = useNoteLabelBoolean(note, "template");
const isProtected = useNoteProperty(note, "isProtected");
return <>
<FormListToggleableItem
icon="bx bx-share-alt"
title={t("shared_switch.shared")}
currentValue={isShared} onChange={switchShareState}
helpPage="R9pX4DGra2Vt"
disabled={["root", "_share", "_hidden"].includes(note?.noteId ?? "") || note?.noteId.startsWith("_options")}
/>
<FormListToggleableItem
icon="bx bx-lock-alt"
title={t("protect_note.toggle-on")}
currentValue={!!isProtected} onChange={shouldProtect => protected_session.protectNote(note.noteId, shouldProtect, false)}
/>
<FormListToggleableItem
icon="bx bx-bookmark"
title={t("bookmark_switch.bookmark")}
currentValue={isBookmarked} onChange={setIsBookmarked}
disabled={["root", "_hidden"].includes(note?.noteId ?? "")}
/>
{isReadOnly && <>
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
command={() => enableEditing()} />
<FormDropdownDivider />
</>}
{canBeConvertedToAttachment && <ConvertToAttachment note={note} /> }
{note.type === "render" && <CommandItem command="renderActiveNote" icon="bx bx-extension" text={t("note_actions.re_render_note")} />}
<CommandItem command="findInText" icon="bx bx-search" disabled={!isSearchable} text={t("note_actions.search_in_note")} />
<CommandItem command="printActiveNote" icon="bx bx-printer" disabled={!isPrintable} text={t("note_actions.print_note")} />
{isElectron && <CommandItem command="exportAsPdf" icon="bx bxs-file-pdf" disabled={!isPrintable} text={t("note_actions.print_pdf")} />}
<FormDropdownDivider />
<NoteTypeDropdown note={note} />
<EditabilityDropdown note={note} />
<CommandItem icon="bx bx-import" text={t("note_actions.import_files")}
disabled={isInOptionsOrHelp || note.type === "search"}
command={() => parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} />
<CommandItem icon="bx bx-export" text={t("note_actions.export_note")}
disabled={isInOptionsOrHelp || note.noteId === "_backendLog"}
command={() => noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", {
notePath: noteContext.notePath,
defaultType: "single"
})} />
<FormDropdownDivider />
<FormListToggleableItem
icon="bx bx-copy-alt"
title={t("template_switch.template")}
currentValue={isTemplate} onChange={setIsTemplate}
helpPage="KC1HB96bqqHX"
disabled={note?.noteId.startsWith("_options")}
/>
</>;
}
<CommandItem command="openNoteExternally" icon="bx bx-file-find" disabled={isSearchOrBook || !isElectron} text={t("note_actions.open_note_externally")} title={t("note_actions.open_note_externally_title")} />
<CommandItem command="openNoteCustom" icon="bx bx-customize" disabled={isSearchOrBook || isMac || !isElectron} text={t("note_actions.open_note_custom")} />
<CommandItem command="showNoteSource" icon="bx bx-code" disabled={!hasSource} text={t("note_actions.note_source")} />
{(syncServerHost && isElectron) &&
<CommandItem command="openNoteOnServer" icon="bx bx-world" disabled={!syncServerHost} text={t("note_actions.open_note_on_server")} />
}
<FormDropdownDivider />
function EditabilityDropdown({ note }: { note: FNote }) {
const [ readOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
const [ autoReadOnlyDisabled, setAutoReadOnlyDisabled ] = useNoteLabelBoolean(note, "autoReadOnlyDisabled");
<CommandItem command="forceSaveRevision" icon="bx bx-save" disabled={isInOptionsOrHelp} text={t("note_actions.save_revision")} />
<CommandItem icon="bx bx-trash destructive-action-icon" text={t("note_actions.delete_note")} destructive
disabled={isInOptionsOrHelp}
command={() => branches.deleteNotes([note.getParentBranches()[0].branchId])}
/>
<FormDropdownDivider />
function setState(readOnly: boolean, autoReadOnlyDisabled: boolean) {
setReadOnly(readOnly);
setAutoReadOnlyDisabled(autoReadOnlyDisabled);
}
<CommandItem command="showAttachments" icon="bx bx-paperclip" disabled={isInOptionsOrHelp} text={t("note_actions.note_attachments")} />
{glob.isDev && <DevelopmentActions note={note} noteContext={noteContext} />}
</Dropdown>
);
return (
<FormDropdownSubmenu title={t("basic_properties.editable")} icon="bx bx-edit-alt" dropStart>
<FormListItem checked={!readOnly && !autoReadOnlyDisabled} onClick={() => setState(false, false)} description={t("editability_select.note_is_editable")}>{t("editability_select.auto")}</FormListItem>
<FormListItem checked={readOnly && !autoReadOnlyDisabled} onClick={() => setState(true, false)} description={t("editability_select.note_is_read_only")}>{t("editability_select.read_only")}</FormListItem>
<FormListItem checked={!readOnly && autoReadOnlyDisabled} onClick={() => setState(false, true)} description={t("editability_select.note_is_always_editable")}>{t("editability_select.always_editable")}</FormListItem>
</FormDropdownSubmenu>
);
}
function NoteTypeDropdown({ note }: { note: FNote }) {
const currentNoteType = useNoteProperty(note, "type") ?? undefined;
const currentNoteMime = useNoteProperty(note, "mime");
return (
<FormDropdownSubmenu title={t("basic_properties.note_type")} icon="bx bx-file" dropStart>
<NoteTypeDropdownContent
currentNoteType={currentNoteType}
currentNoteMime={currentNoteMime}
note={note}
setModalShown={() => { /* no-op since no code notes are displayed here */ }}
noCodeNotes
/>
</FormDropdownSubmenu>
);
}
function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
return (
<>
<FormListHeader text="Development-only Actions" />
<FormListHeader text="Development Actions" />
<FormListItem
icon="bx bx-printer"
onClick={() => window.open(`/?print=#root/${note.noteId}`, "_blank")}
>Open print page</FormListItem>
{note.type === "text" && (
<FormListItem
icon="bx bx-error"
onClick={() => {
noteContext?.getTextEditor(editor => {
editor.editing.view.change(() => {
throw new Error("Editor crashed.");
});
<FormListItem
icon="bx bx-error"
disabled={note.type !== "text"}
onClick={() => {
noteContext?.getTextEditor(editor => {
editor.editing.view.change(() => {
throw new Error("Editor crashed.");
});
}}>Crash editor</FormListItem>)}
});
}}>Crash editor</FormListItem>
</>
)
);
}
function CommandItem({ icon, text, title, command, disabled }: { icon: string, text: string, title?: string, command: CommandNames | (() => void), disabled?: boolean, destructive?: boolean }) {
return <FormListItem
icon={icon}
title={title}
triggerCommand={typeof command === "string" ? command : undefined}
onClick={typeof command === "function" ? command : undefined}
disabled={disabled}
>{text}</FormListItem>
return <FormListItem
icon={icon}
title={title}
triggerCommand={typeof command === "string" ? command : undefined}
onClick={typeof command === "function" ? command : undefined}
disabled={disabled}
>{text}</FormListItem>;
}
function ConvertToAttachment({ note }: { note: FNote }) {
return (
<FormListItem
icon="bx bx-paperclip"
onClick={async () => {
if (!note || !(await dialog.confirm(t("note_actions.convert_into_attachment_prompt", { title: note.title })))) {
return;
}
return (
<FormListItem
icon="bx bx-paperclip"
onClick={async () => {
if (!note || !(await dialog.confirm(t("note_actions.convert_into_attachment_prompt", { title: note.title })))) {
return;
}
const { attachment: newAttachment } = await server.post<ConvertToAttachmentResponse>(`notes/${note.noteId}/convert-to-attachment`);
const { attachment: newAttachment } = await server.post<ConvertToAttachmentResponse>(`notes/${note.noteId}/convert-to-attachment`);
if (!newAttachment) {
toast.showMessage(t("note_actions.convert_into_attachment_failed", { title: note.title }));
return;
}
if (!newAttachment) {
toast.showMessage(t("note_actions.convert_into_attachment_failed", { title: note.title }));
return;
}
toast.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title }));
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, {
viewScope: {
viewMode: "attachments",
attachmentId: newAttachment.attachmentId
}
});
}}
>{t("note_actions.convert_into_attachment")}</FormListItem>
)
toast.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title }));
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, {
viewScope: {
viewMode: "attachments",
attachmentId: newAttachment.attachmentId
}
});
}}
>{t("note_actions.convert_into_attachment")}</FormListItem>
);
}

View File

@@ -1,6 +1,5 @@
import { useEffect, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import { TabContext } from "./ribbon-interface";
import { MetadataResponse, NoteSizeResponse, SubtreeSizeResponse } from "@triliumnext/commons";
import server from "../../services/server";
import Button from "../react/Button";
@@ -8,12 +7,76 @@ import { formatDateTime } from "../../utils/formatters";
import { formatSize } from "../../services/utils";
import LoadingSpinner from "../react/LoadingSpinner";
import { useTriliumEvent } from "../react/hooks";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import FNote from "../../entities/fnote";
export default function NoteInfoTab({ note }: TabContext) {
const [ metadata, setMetadata ] = useState<MetadataResponse>();
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
export default function NoteInfoTab({ note }: { note: FNote | null | undefined }) {
const { metadata, ...sizeProps } = useNoteMetadata(note);
return (
<div className="note-info-widget">
{note && (
<>
<div className="note-info-item">
<span>{t("note_info_widget.note_id")}:</span>
<span className="note-info-id selectable-text">{note.noteId}</span>
</div>
{!isNewLayout && <div className="note-info-item">
<span>{t("note_info_widget.created")}:</span>
<span className="selectable-text">{formatDateTime(metadata?.dateCreated)}</span>
</div>}
{!isNewLayout && <div className="note-info-item">
<span>{t("note_info_widget.modified")}:</span>
<span className="selectable-text">{formatDateTime(metadata?.dateModified)}</span>
</div>}
<div className="note-info-item">
<span>{t("note_info_widget.type")}:</span>
<span>
<span className="note-info-type">{note.type}</span>{' '}
{note.mime && <span className="note-info-mime selectable-text">({note.mime})</span>}
</span>
</div>
<div className="note-info-item">
<span title={t("note_info_widget.note_size_info")}>{t("note_info_widget.note_size")}:</span>
<span className="note-info-size-col-span">
<NoteSizeWidget {...sizeProps} />
</span>
</div>
</>
)}
</div>
);
}
export function NoteSizeWidget({ isLoading, noteSizeResponse, subtreeSizeResponse, requestSizeInfo }: Omit<ReturnType<typeof useNoteMetadata>, "metadata">) {
return <>
{!isLoading && !noteSizeResponse && !subtreeSizeResponse && (
<Button
className="calculate-button"
icon="bx bx-calculator"
text={t("note_info_widget.calculate")}
onClick={requestSizeInfo}
/>
)}
<span className="note-sizes-wrapper selectable-text">
<span className="note-size">{formatSize(noteSizeResponse?.noteSize)}</span>
{" "}
{subtreeSizeResponse && subtreeSizeResponse.subTreeNoteCount > 1 &&
<span className="subtree-size">{t("note_info_widget.subtree_size", { size: formatSize(subtreeSizeResponse.subTreeSize), count: subtreeSizeResponse.subTreeNoteCount })}</span>
}
{isLoading && <LoadingSpinner />}
</span>
</>;
}
export function useNoteMetadata(note: FNote | null | undefined) {
const [ isLoading, setIsLoading ] = useState(false);
const [ noteSizeResponse, setNoteSizeResponse ] = useState<NoteSizeResponse>();
const [ subtreeSizeResponse, setSubtreeSizeResponse ] = useState<SubtreeSizeResponse>();
const [ metadata, setMetadata ] = useState<MetadataResponse>();
function refresh() {
if (note) {
@@ -25,7 +88,20 @@ export default function NoteInfoTab({ note }: TabContext) {
setIsLoading(false);
}
useEffect(refresh, [ note?.noteId ]);
function requestSizeInfo() {
if (!note) return;
setIsLoading(true);
setTimeout(async () => {
await Promise.allSettled([
server.get<NoteSizeResponse>(`stats/note-size/${note.noteId}`).then(setNoteSizeResponse),
server.get<SubtreeSizeResponse>(`stats/subtree-size/${note.noteId}`).then(setSubtreeSizeResponse)
]);
setIsLoading(false);
}, 0);
}
useEffect(refresh, [ note ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
const noteId = note?.noteId;
if (noteId && (loadResults.isNoteReloaded(noteId) || loadResults.isNoteContentReloaded(noteId))) {
@@ -33,62 +109,5 @@ export default function NoteInfoTab({ note }: TabContext) {
}
});
return (
<div className="note-info-widget">
{note && (
<>
<div className="note-info-item">
<span>{t("note_info_widget.note_id")}:</span>
<span className="note-info-id selectable-text">{note.noteId}</span>
</div>
<div className="note-info-item">
<span>{t("note_info_widget.created")}:</span>
<span className="selectable-text">{formatDateTime(metadata?.dateCreated)}</span>
</div>
<div className="note-info-item">
<span>{t("note_info_widget.modified")}:</span>
<span className="selectable-text">{formatDateTime(metadata?.dateModified)}</span>
</div>
<div className="note-info-item">
<span>{t("note_info_widget.type")}:</span>
<span>
<span className="note-info-type">{note.type}</span>{' '}
{note.mime && <span className="note-info-mime selectable-text">({note.mime})</span>}
</span>
</div>
<div className="note-info-item">
<span title={t("note_info_widget.note_size_info")}>{t("note_info_widget.note_size")}:</span>
<span className="note-info-size-col-span">
{!isLoading && !noteSizeResponse && !subtreeSizeResponse && (
<Button
className="calculate-button"
icon="bx bx-calculator"
text={t("note_info_widget.calculate")}
onClick={() => {
setIsLoading(true);
setTimeout(async () => {
await Promise.allSettled([
server.get<NoteSizeResponse>(`stats/note-size/${note.noteId}`).then(setNoteSizeResponse),
server.get<SubtreeSizeResponse>(`stats/subtree-size/${note.noteId}`).then(setSubtreeSizeResponse)
]);
setIsLoading(false);
}, 0);
}}
/>
)}
<span className="note-sizes-wrapper selectable-text">
<span className="note-size">{formatSize(noteSizeResponse?.noteSize)}</span>
{" "}
{subtreeSizeResponse && subtreeSizeResponse.subTreeNoteCount > 1 &&
<span className="subtree-size">{t("note_info_widget.subtree_size", { size: formatSize(subtreeSizeResponse.subTreeSize), count: subtreeSizeResponse.subTreeNoteCount })}</span>
}
{isLoading && <LoadingSpinner />}
</span>
</span>
</div>
</>
)}
</div>
)
return { isLoading, metadata, noteSizeResponse, subtreeSizeResponse, requestSizeInfo };
}

View File

@@ -1,13 +1,50 @@
import { TabContext } from "./ribbon-interface";
import { useEffect, useMemo, useState } from "preact/hooks";
import FNote, { NotePathRecord } from "../../entities/fnote";
import { t } from "../../services/i18n";
import { NOTE_PATH_TITLE_SEPARATOR } from "../../services/tree";
import Button from "../react/Button";
import { useTriliumEvent } from "../react/hooks";
import { useEffect, useMemo, useState } from "preact/hooks";
import { NotePathRecord } from "../../entities/fnote";
import NoteLink from "../react/NoteLink";
import { joinElements } from "../react/react_utils";
import { TabContext } from "./ribbon-interface";
export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabContext) {
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
return <NotePathsWidget sortedNotePaths={sortedNotePaths} currentNotePath={notePath} />;
}
export function NotePathsWidget({ sortedNotePaths, currentNotePath }: {
sortedNotePaths: NotePathRecord[] | undefined;
currentNotePath?: string | null | undefined;
}) {
return (
<div class="note-paths-widget">
<>
<div className="note-path-intro">
{sortedNotePaths?.length ? t("note_paths.intro_placed") : t("note_paths.intro_not_placed")}
</div>
<ul className="note-path-list">
{sortedNotePaths?.length ? sortedNotePaths.map(sortedNotePath => (
<NotePath
key={sortedNotePath.notePath}
currentNotePath={currentNotePath}
notePathRecord={sortedNotePath}
/>
)) : undefined}
</ul>
<Button
triggerCommand="cloneNoteIdsTo"
text={t("note_paths.clone_button")}
/>
</>
</div>
);
}
export function useSortedNotePaths(note: FNote | null | undefined, hoistedNoteId?: string) {
const [ sortedNotePaths, setSortedNotePaths ] = useState<NotePathRecord[]>();
function refresh() {
@@ -17,7 +54,7 @@ export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabConte
.filter((notePath) => !notePath.isHidden));
}
useEffect(refresh, [ note?.noteId ]);
useEffect(refresh, [ note, hoistedNoteId ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
const noteId = note?.noteId;
if (!noteId) return;
@@ -27,35 +64,13 @@ export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabConte
}
});
return (
<div class="note-paths-widget">
<>
<div className="note-path-intro">
{sortedNotePaths?.length ? t("note_paths.intro_placed") : t("note_paths.intro_not_placed")}
</div>
<ul className="note-path-list">
{sortedNotePaths?.length ? sortedNotePaths.map(sortedNotePath => (
<NotePath
currentNotePath={notePath}
notePathRecord={sortedNotePath}
/>
)) : undefined}
</ul>
<Button
triggerCommand="cloneNoteIdsTo"
text={t("note_paths.clone_button")}
/>
</>
</div>
)
return sortedNotePaths;
}
function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: string | null, notePathRecord?: NotePathRecord }) {
const notePath = notePathRecord?.notePath ?? [];
const notePathString = useMemo(() => notePath.join("/"), [ notePath ]);
const notePath = notePathRecord?.notePath;
const notePathString = useMemo(() => (notePath ?? []).join("/"), [ notePath ]);
const [ classes, icons ] = useMemo(() => {
const classes: string[] = [];
const icons: { icon: string, title: string }[] = [];
@@ -67,17 +82,17 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
if (!notePathRecord || notePathRecord.isInHoistedSubTree) {
classes.push("path-in-hoisted-subtree");
} else {
icons.push({ icon: "bx bx-trending-up", title: t("note_paths.outside_hoisted") })
icons.push({ icon: "bx bx-trending-up", title: t("note_paths.outside_hoisted") });
}
if (notePathRecord?.isArchived) {
classes.push("path-archived");
icons.push({ icon: "bx bx-archive", title: t("note_paths.archived") })
icons.push({ icon: "bx bx-archive", title: t("note_paths.archived") });
}
if (notePathRecord?.isSearch) {
classes.push("path-search");
icons.push({ icon: "bx bx-search", title: t("note_paths.search") })
icons.push({ icon: "bx bx-search", title: t("note_paths.search") });
}
return [ classes.join(" "), icons ];
@@ -86,7 +101,7 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
// Determine the full note path (for the links) of every component of the current note path.
const pathSegments: string[] = [];
const fullNotePaths: string[] = [];
for (const noteId of notePath) {
for (const noteId of notePath ?? []) {
pathSegments.push(noteId);
fullNotePaths.push(pathSegments.join("/"));
}
@@ -94,12 +109,12 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
return (
<li class={classes}>
{joinElements(fullNotePaths.map(notePath => (
<NoteLink notePath={notePath} noPreview />
)), " / ")}
<NoteLink key={notePath} notePath={notePath} noPreview />
)), NOTE_PATH_TITLE_SEPARATOR)}
{icons.map(({ icon, title }) => (
<span class={icon} title={title} />
<span key={title} class={icon} title={title} />
))}
</li>
)
}
);
}

View File

@@ -1,4 +1,5 @@
import { useMemo, useRef } from "preact/hooks";
import { useLegacyImperativeHandlers, useTriliumEvents } from "../react/hooks";
import AttributeEditor, { AttributeEditorImperativeHandlers } from "./components/AttributeEditor";
import { TabContext } from "./ribbon-interface";
@@ -25,5 +26,5 @@ export default function OwnedAttributesTab({ note, hidden, activate, ntxId, ...r
<AttributeEditor api={api} ntxId={ntxId} note={note} {...restProps} hidden={hidden} />
)}
</div>
)
);
}

View File

@@ -1,14 +1,15 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
import "./style.css";
import { Indexed, numberObjectsInPlace } from "../../services/utils";
import { EventNames } from "../../components/app_context";
import NoteActions from "./NoteActions";
import { KeyboardActionNames } from "@triliumnext/commons";
import { RIBBON_TAB_DEFINITIONS } from "./RibbonDefinition";
import { TabConfiguration, TitleContext } from "./ribbon-interface";
import clsx from "clsx";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { EventNames } from "../../components/app_context";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import { Indexed, numberObjectsInPlace } from "../../services/utils";
import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
import { TabConfiguration, TitleContext } from "./ribbon-interface";
import { RIBBON_TAB_DEFINITIONS } from "./RibbonDefinition";
const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>(RIBBON_TAB_DEFINITIONS);
@@ -16,7 +17,9 @@ interface ComputedTab extends Indexed<TabConfiguration> {
shouldShow: boolean;
}
export default function Ribbon() {
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
export default function Ribbon({ children }: { children?: preact.ComponentChildren }) {
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId, isReadOnlyTemporarilyDisabled } = useNoteContext();
const noteType = useNoteProperty(note, "type");
const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>();
@@ -29,7 +32,8 @@ export default function Ribbon() {
async function refresh() {
const computedTabs: ComputedTab[] = [];
for (const tab of TAB_CONFIGURATION) {
const shouldShow = await shouldShowTab(tab.show, titleContext);
const shouldAvoid = (isNewLayout && tab.avoidInNewLayout);
const shouldShow = !shouldAvoid && await shouldShowTab(tab.show, titleContext);
computedTabs.push({
...tab,
shouldShow: !!shouldShow
@@ -54,7 +58,7 @@ export default function Ribbon() {
useTriliumEvents(eventsToListenTo, useCallback((e, toggleCommand) => {
if (!computedTabs) return;
const correspondingTab = computedTabs.find(tab => tab.toggleCommand === toggleCommand);
if (correspondingTab) {
if (correspondingTab?.shouldShow) {
if (activeTabIndex !== correspondingTab.index) {
setActiveTabIndex(correspondingTab.index);
} else {
@@ -63,9 +67,10 @@ export default function Ribbon() {
}
}, [ computedTabs, activeTabIndex ]));
const shouldShowRibbon = (noteContext?.viewScope?.viewMode === "default" && !noteContext.noteId?.startsWith("_options"));
return (
<div
className={clsx("ribbon-container", noteContext?.viewScope?.viewMode !== "default" && "hidden-ext")}
className={clsx("ribbon-container", !shouldShowRibbon && "hidden-ext")}
style={{ contain: "none" }}
>
<div className="ribbon-top-row">
@@ -87,9 +92,7 @@ export default function Ribbon() {
/>
))}
</div>
<div className="ribbon-button-container">
{ note && <NoteActions note={note} noteContext={noteContext} /> }
</div>
{children}
</div>
<div className="ribbon-body-container">
@@ -120,7 +123,7 @@ export default function Ribbon() {
})}
</div>
</div>
)
);
}
function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: string; title: string; active: boolean, onClick: () => void, toggleCommand?: KeyboardActionNames }) {
@@ -143,7 +146,7 @@ function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: stri
<div class="ribbon-tab-spacer" />
</>
)
);
}
export async function shouldShowTab(showConfig: boolean | ((context: TitleContext) => Promise<boolean | null | undefined> | boolean | null | undefined), context: TitleContext) {

View File

@@ -1,40 +1,44 @@
import ScriptTab from "./ScriptTab";
import EditedNotesTab from "./EditedNotesTab";
import NotePropertiesTab from "./NotePropertiesTab";
import NoteInfoTab from "./NoteInfoTab";
import SimilarNotesTab from "./SimilarNotesTab";
import FilePropertiesTab from "./FilePropertiesTab";
import ImagePropertiesTab from "./ImagePropertiesTab";
import NotePathsTab from "./NotePathsTab";
import NoteMapTab from "./NoteMapTab";
import OwnedAttributesTab from "./OwnedAttributesTab";
import InheritedAttributesTab from "./InheritedAttributesTab";
import CollectionPropertiesTab from "./CollectionPropertiesTab";
import SearchDefinitionTab from "./SearchDefinitionTab";
import BasicPropertiesTab from "./BasicPropertiesTab";
import FormattingToolbar from "./FormattingToolbar";
import options from "../../services/options";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import { t } from "../../services/i18n";
import options from "../../services/options";
import BasicPropertiesTab from "./BasicPropertiesTab";
import CollectionPropertiesTab from "./CollectionPropertiesTab";
import EditedNotesTab from "./EditedNotesTab";
import FilePropertiesTab from "./FilePropertiesTab";
import FormattingToolbar from "./FormattingToolbar";
import ImagePropertiesTab from "./ImagePropertiesTab";
import InheritedAttributesTab from "./InheritedAttributesTab";
import NoteInfoTab from "./NoteInfoTab";
import NoteMapTab from "./NoteMapTab";
import NotePathsTab from "./NotePathsTab";
import NotePropertiesTab from "./NotePropertiesTab";
import OwnedAttributesTab from "./OwnedAttributesTab";
import { TabConfiguration } from "./ribbon-interface";
import ScriptTab from "./ScriptTab";
import SearchDefinitionTab from "./SearchDefinitionTab";
import SimilarNotesTab from "./SimilarNotesTab";
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
{
title: t("classic_editor_toolbar.title"),
icon: "bx bx-text",
show: async ({ note, noteContext }) => note?.type === "text"
show: async ({ note, noteContext }) => note?.type === "text" && noteContext?.viewScope?.viewMode === "default"
&& options.get("textNoteEditorType") === "ckeditor-classic"
&& !(await noteContext?.isReadOnly()),
toggleCommand: "toggleRibbonTabClassicEditor",
content: FormattingToolbar,
activate: ({ note }) => !options.is("editedNotesOpenInRibbon") || !note?.hasOwnedLabel("dateNote"),
stayInDom: true
stayInDom: !isNewLayout,
avoidInNewLayout: true
},
{
title: ({ note }) => note?.isTriliumSqlite() ? t("script_executor.query") : t("script_executor.script"),
icon: "bx bx-play",
content: ScriptTab,
activate: true,
show: ({ note }) => note &&
show: ({ note }) => note && !isNewLayout &&
(note.isTriliumScript() || note.isTriliumSqlite()) &&
(note.hasLabel("executeDescription") || note.hasLabel("executeButton"))
},
@@ -56,14 +60,14 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
title: t("book_properties.book_properties"),
icon: "bx bx-book",
content: CollectionPropertiesTab,
show: ({ note }) => note?.type === "book" || note?.type === "search",
show: ({ note }) => !isNewLayout && note?.type === "book" || note?.type === "search",
toggleCommand: "toggleRibbonTabBookProperties"
},
{
title: t("note_properties.info"),
icon: "bx bx-info-square",
content: NotePropertiesTab,
show: ({ note }) => !!note?.getLabelValue("pageUrl"),
show: ({ note }) => !isNewLayout && !!note?.getLabelValue("pageUrl"),
activate: true
},
{
@@ -83,53 +87,52 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
activate: true,
},
{
// BasicProperties
title: t("basic_properties.basic_properties"),
icon: "bx bx-slider",
content: BasicPropertiesTab,
show: ({note}) => !note?.isLaunchBarConfig(),
show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(),
toggleCommand: "toggleRibbonTabBasicProperties"
},
{
title: t("owned_attribute_list.owned_attributes"),
icon: "bx bx-list-check",
content: OwnedAttributesTab,
show: ({note}) => !note?.isLaunchBarConfig(),
show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(),
toggleCommand: "toggleRibbonTabOwnedAttributes",
stayInDom: true
stayInDom: !isNewLayout
},
{
title: t("inherited_attribute_list.title"),
icon: "bx bx-list-plus",
content: InheritedAttributesTab,
show: ({note}) => !note?.isLaunchBarConfig(),
show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(),
toggleCommand: "toggleRibbonTabInheritedAttributes"
},
{
title: t("note_paths.title"),
icon: "bx bx-collection",
content: NotePathsTab,
show: true,
show: !isNewLayout,
toggleCommand: "toggleRibbonTabNotePaths"
},
{
title: t("note_map.title"),
icon: "bx bxs-network-chart",
content: NoteMapTab,
show: true,
show: !isNewLayout,
toggleCommand: "toggleRibbonTabNoteMap"
},
{
title: t("similar_notes.title"),
icon: "bx bx-bar-chart",
show: ({ note }) => note?.type !== "search" && !note?.isLabelTruthy("similarNotesWidgetDisabled"),
show: ({ note }) => !isNewLayout && note?.type !== "search" && !note?.isLabelTruthy("similarNotesWidgetDisabled"),
content: SimilarNotesTab,
toggleCommand: "toggleRibbonTabSimilarNotes"
},
{
title: t("note_info_widget.title"),
icon: "bx bx-info-circle",
show: ({ note }) => !!note,
show: ({ note }) => !isNewLayout && !!note,
content: NoteInfoTab,
toggleCommand: "toggleRibbonTabNoteInfo"
}

View File

@@ -18,7 +18,8 @@ interface BookConfig {
export interface CheckBoxProperty {
type: "checkbox",
label: string;
bindToLabel: FilterLabelsByType<boolean>
bindToLabel: FilterLabelsByType<boolean>;
icon?: string;
}
export interface ButtonProperty {
@@ -40,10 +41,11 @@ export interface NumberProperty {
bindToLabel: FilterLabelsByType<number>;
width?: number;
min?: number;
icon?: string;
disabled?: (note: FNote) => boolean;
}
interface ComboBoxItem {
export interface ComboBoxItem {
value: string;
label: string;
}
@@ -56,6 +58,7 @@ interface ComboBoxGroup {
export interface ComboBoxProperty {
type: "combobox",
label: string;
icon?: string;
bindToLabel: FilterLabelsByType<string>;
/**
* The default value is used when the label is not set.
@@ -107,11 +110,13 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
properties: [
{
label: t("book_properties_config.hide-weekends"),
icon: "bx bx-calendar-week",
type: "checkbox",
bindToLabel: "calendar:hideWeekends"
},
{
label: t("book_properties_config.display-week-numbers"),
icon: "bx bx-hash",
type: "checkbox",
bindToLabel: "calendar:weekNumbers"
}
@@ -121,6 +126,7 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
properties: [
{
label: t("book_properties_config.map-style"),
icon: "bx bx-palette",
type: "combobox",
bindToLabel: "map:style",
defaultValue: DEFAULT_MAP_LAYER_NAME,
@@ -147,6 +153,7 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
},
{
label: t("book_properties_config.show-scale"),
icon: "bx bx-ruler",
type: "checkbox",
bindToLabel: "map:scale"
}
@@ -156,6 +163,7 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
properties: [
{
label: t("book_properties_config.max-nesting-depth"),
icon: "bx bx-subdirectory-right",
type: "number",
bindToLabel: "maxNestingDepth",
width: 65,
@@ -171,6 +179,7 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
{
label: "Theme",
type: "combobox",
icon: "bx bx-palette",
bindToLabel: "presentation:theme",
defaultValue: DEFAULT_THEME,
options: getPresentationThemes().map(theme => ({

View File

@@ -30,4 +30,5 @@ export interface TabConfiguration {
* By default the tab content will not be rendered unless the tab is active (i.e. selected by the user). Setting to `true` will ensure that the tab is rendered even when inactive, for cases where the tab needs to be accessible at all times (e.g. for the detached editor toolbar) or if event handling is needed.
*/
stayInDom?: boolean;
avoidInNewLayout?: boolean;
}

View File

@@ -1,5 +1,13 @@
.ribbon-container {
margin-bottom: 5px;
position: relative;
z-index: 998;
}
/* When content header is floating, ribbon sticks below it */
.scrolling-container:has(.content-header-widget.floating) .ribbon-container {
position: sticky;
top: var(--content-header-height, 100px);
}
.ribbon-top-row {
@@ -24,12 +32,15 @@
max-width: max-content;
flex-grow: 10;
user-select: none;
display: flex;
align-items: center;
font-size: 0.9em;
padding-top: 2px;
}
.ribbon-tab-title .bx {
font-size: 150%;
position: relative;
top: 3px;
}
.ribbon-tab-title.active {
@@ -71,12 +82,9 @@
display: flex;
border-bottom: 1px solid var(--main-border-color);
margin-inline-end: 5px;
}
.ribbon-button-container > * {
position: relative;
top: -3px;
margin-inline-start: 10px;
align-items: center;
height: 36px;
gap: 10px;
}
.ribbon-body {
@@ -144,6 +152,15 @@
opacity: 0.3;
}
body.experimental-feature-new-layout .classic-toolbar-widget {
transition: opacity 250ms ease-in;
&.disabled {
opacity: 0.3;
pointer-events: none;
}
}
/* #endregion */
/* #region Script Tab */
@@ -347,6 +364,10 @@ body[dir=rtl] .attribute-list-editor {
max-height: 200px;
overflow: auto;
padding: 14px 12px 13px 12px;
a.reference-link {
text-decoration: underline;
}
}
/* #endregion */
@@ -386,6 +407,8 @@ body[dir=rtl] .attribute-list-editor {
.note-actions {
width: 35px;
height: 35px;
display: flex;
align-items: center;
}
.note-actions .dropdown-menu {
@@ -404,4 +427,29 @@ body[dir=rtl] .attribute-list-editor {
background-color: transparent !important;
pointer-events: none; /* makes it unclickable */
}
/* #endregion */
/* #endregion */
/* #region Experimental layout */
body.experimental-feature-new-layout {
.ribbon-top-row {
min-height: 0;
}
.ribbon-container {
display: flex;
flex-direction: column-reverse;
border: 0;
.ribbon-tab-spacer,
.ribbon-tab-title,
.ribbon-body {
border-bottom: 0 !important;
}
}
.ribbon-button-container {
border-bottom: 0 !important;
margin: 0;
}
}
/* #endregion */

View File

@@ -10,8 +10,24 @@ import RawHtml from "./react/RawHtml";
export default function SharedInfo() {
const { note } = useNoteContext();
const [ syncServerHost ] = useTriliumOption("syncServerHost");
const { isSharedExternally, link } = useShareInfo(note);
return (
<InfoBar className="shared-info-widget" type="subtle" style={{display: (!link) ? "none" : undefined}}>
{link && (
<RawHtml html={isSharedExternally
? t("shared_info.shared_publicly", { link })
: t("shared_info.shared_locally", { link })} />
)}
<HelpButton helpPage="R9pX4DGra2Vt" style={{ width: "24px", height: "24px" }} />
</InfoBar>
);
}
export function useShareInfo(note: FNote | null | undefined) {
const [ link, setLink ] = useState<string>();
const [ linkHref, setLinkHref ] = useState<string>();
const [ syncServerHost ] = useTriliumOption("syncServerHost");
function refresh() {
if (!note) return;
@@ -37,9 +53,10 @@ export default function SharedInfo() {
}
setLink(`<a href="${link}" class="external tn-link">${link}</a>`);
setLinkHref(link);
}
useEffect(refresh, [ note ]);
useEffect(refresh, [ note, syncServerHost ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().find((attr) => attr.name?.startsWith("_share") && attributes.isAffecting(attr, note))) {
refresh();
@@ -48,16 +65,7 @@ export default function SharedInfo() {
}
});
return (
<InfoBar className="shared-info-widget" type="subtle" style={{display: (!link) ? "none" : undefined}}>
{link && (
<RawHtml html={syncServerHost
? t("shared_info.shared_publicly", { link })
: t("shared_info.shared_locally", { link })} />
)}
<HelpButton helpPage="R9pX4DGra2Vt" style={{ width: "24px", height: "24px" }} />
</InfoBar>
)
return { link, linkHref, isSharedExternally: !!syncServerHost };
}
function getShareId(note: FNote) {
@@ -66,4 +74,4 @@ function getShareId(note: FNote) {
}
return note.getOwnedLabelValue("shareAlias") || note.noteId;
}
}

View File

@@ -26,24 +26,13 @@ import ws from "../../services/ws";
import appContext from "../../components/app_context";
import { ConvertAttachmentToNoteResponse } from "@triliumnext/commons";
import options from "../../services/options";
import FNote from "../../entities/fnote";
/**
* Displays the full list of attachments of a note and allows the user to interact with them.
*/
export function AttachmentList({ note }: TypeWidgetProps) {
const [ attachments, setAttachments ] = useState<FAttachment[]>([]);
function refresh() {
note.getAttachments().then(attachments => setAttachments(Array.from(attachments)));
}
useEffect(refresh, [ note ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttachmentRows().some((att) => att.attachmentId && att.ownerId === note.noteId)) {
refresh();
}
});
const attachments = useAttachments(note);
return (
<>
@@ -59,7 +48,25 @@ export function AttachmentList({ note }: TypeWidgetProps) {
)}
</div>
</>
)
);
}
export function useAttachments(note: FNote) {
const [ attachments, setAttachments ] = useState<FAttachment[]>([]);
function refresh() {
note.getAttachments().then(attachments => setAttachments(Array.from(attachments)));
}
useEffect(refresh, [ note ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttachmentRows().some((att) => att.attachmentId && att.ownerId === note.noteId)) {
refresh();
}
});
return attachments;
}
function AttachmentListHeader({ noteId }: { noteId: string }) {

View File

@@ -3,12 +3,15 @@ import NoteMapEl from "../note_map/NoteMap";
import { useRef } from "preact/hooks";
import "./NoteMap.css";
export default function NoteMap({ note }: TypeWidgetProps) {
export default function NoteMap({ note, noteContext }: TypeWidgetProps) {
const containerRef = useRef<HTMLDivElement>(null);
return (
<div ref={containerRef}>
<NoteMapEl parentRef={containerRef} note={note} widgetMode="type" />
<NoteMapEl
parentRef={containerRef}
note={note}
widgetMode={noteContext?.viewScope?.viewMode === "note-map" ? "ribbon" : "type"} />
</div>
);
}

View File

@@ -7,6 +7,9 @@ import FormText from "../../react/FormText";
import OptionsSection from "./components/OptionsSection"
import Column from "../../react/Column";
import { useEffect, useState } from "preact/hooks";
import CheckboxList from "./components/CheckboxList";
import { experimentalFeatures } from "../../../services/experimental_features";
import { useTriliumOptionJson } from "../../react/hooks";
export default function AdvancedSettings() {
return <>
@@ -14,6 +17,7 @@ export default function AdvancedSettings() {
<DatabaseIntegrityOptions />
<DatabaseAnonymizationOptions />
<VacuumDatabaseOptions />
<ExperimentalOptions />
</>;
}
@@ -44,14 +48,14 @@ function DatabaseIntegrityOptions() {
return (
<OptionsSection title={t("database_integrity_check.title")}>
<FormText>{t("database_integrity_check.description")}</FormText>
<Button
text={t("database_integrity_check.check_button")}
onClick={async () => {
toast.showMessage(t("database_integrity_check.checking_integrity"));
const { results } = await server.get<DatabaseCheckIntegrityResponse>("database/check-integrity");
if (results.length === 1 && results[0].integrity_check === "ok") {
toast.showMessage(t("database_integrity_check.integrity_check_succeeded"));
} else {
@@ -93,7 +97,7 @@ function DatabaseAnonymizationOptions() {
buttonClick={async () => {
toast.showMessage(t("database_anonymization.creating_fully_anonymized_database"));
const resp = await server.post<DatabaseAnonymizeResponse>("database/anonymize/full");
if (!resp.success) {
toast.showError(t("database_anonymization.error_creating_anonymized_database"));
} else {
@@ -141,7 +145,7 @@ function ExistingAnonymizedDatabases({ databases }: { databases: AnonymizedDbRes
return <FormText>{t("database_anonymization.no_anonymized_database_yet")}</FormText>
}
return (
return (
<table className="table table-stripped">
<thead>
<th>{t("database_anonymization.existing_anonymized_databases")}</th>
@@ -154,7 +158,7 @@ function ExistingAnonymizedDatabases({ databases }: { databases: AnonymizedDbRes
))}
</tbody>
</table>
)
);
}
function VacuumDatabaseOptions() {
@@ -171,5 +175,23 @@ function VacuumDatabaseOptions() {
}}
/>
</OptionsSection>
)
}
);
}
function ExperimentalOptions() {
const [ enabledExperimentalFeatures, setEnabledExperimentalFeatures ] = useTriliumOptionJson<string[]>("experimentalFeatures", true);
return (
<OptionsSection title={t("experimental_features.title")}>
<FormText>{t("experimental_features.disclaimer")}</FormText>
<CheckboxList
values={experimentalFeatures}
keyProperty="id"
titleProperty="name"
descriptionProperty="description"
currentValue={enabledExperimentalFeatures} onChange={setEnabledExperimentalFeatures}
/>
</OptionsSection>
);
}

View File

@@ -1,14 +1,17 @@
import FormCheckbox from "../../../react/FormCheckbox";
interface CheckboxListProps<T> {
values: T[];
keyProperty: keyof T;
titleProperty?: keyof T;
disabledProperty?: keyof T;
descriptionProperty?: keyof T;
currentValue: string[];
onChange: (newValues: string[]) => void;
columnWidth?: string;
}
export default function CheckboxList<T>({ values, keyProperty, titleProperty, disabledProperty, currentValue, onChange, columnWidth }: CheckboxListProps<T>) {
export default function CheckboxList<T>({ values, keyProperty, titleProperty, disabledProperty, descriptionProperty, currentValue, onChange, columnWidth }: CheckboxListProps<T>) {
function toggleValue(value: string) {
if (currentValue.includes(value)) {
// Already there, needs removing.
@@ -22,20 +25,17 @@ export default function CheckboxList<T>({ values, keyProperty, titleProperty, di
return (
<ul style={{ listStyleType: "none", marginBottom: 0, columnWidth: columnWidth ?? "400px" }}>
{values.map(value => (
<li>
<label className="tn-checkbox">
<input
type="checkbox"
className="form-check-input"
value={String(value[keyProperty])}
checked={currentValue.includes(String(value[keyProperty]))}
disabled={!!(disabledProperty && value[disabledProperty])}
onChange={e => toggleValue((e.target as HTMLInputElement).value)}
/>
{String(value[titleProperty ?? keyProperty] ?? value[keyProperty])}
</label>
<li key={String(value[keyProperty])}>
<FormCheckbox
label={String(value[titleProperty ?? keyProperty] ?? value[keyProperty])}
name={String(value[keyProperty])}
currentValue={currentValue.includes(String(value[keyProperty]))}
disabled={!!(disabledProperty && value[disabledProperty])}
hint={value && (descriptionProperty ? String(value[descriptionProperty]) : undefined)}
onChange={() => toggleValue(String(value[keyProperty]))}
/>
</li>
))}
</ul>
)
}
);
}

View File

@@ -1,18 +1,48 @@
import { Locale } from "@triliumnext/commons";
import { ComponentChildren } from "preact";
import { useMemo } from "preact/hooks";
import Dropdown from "../../../react/Dropdown";
import { FormDropdownDivider, FormListItem } from "../../../react/FormList";
import { ComponentChildren } from "preact";
import { useMemo, useState } from "preact/hooks";
export function LocaleSelector({ id, locales, currentValue, onChange, defaultLocale, extraChildren }: {
id?: string;
locales: Locale[],
currentValue: string,
currentValue: string | null | undefined,
onChange: (newLocale: string) => void,
defaultLocale?: Locale,
extraChildren?: ComponentChildren
extraChildren?: ComponentChildren,
}) {
const [ activeLocale, setActiveLocale ] = useState(defaultLocale?.id === currentValue ? defaultLocale : locales.find(l => l.id === currentValue));
const currentValueWithDefault = currentValue ?? defaultLocale?.id ?? "";
const { activeLocale, processedLocales } = useProcessedLocales(locales, defaultLocale, currentValueWithDefault);
return (
<Dropdown id={id} text={activeLocale?.name}>
{processedLocales.map((locale, index) => (
(typeof locale === "object") ? (
<FormListItem
key={locale.id}
rtl={locale.rtl}
checked={locale.id === currentValue}
onClick={() => {
onChange(locale.id);
}}
>{locale.name}</FormListItem>
) : (
<FormDropdownDivider key={`divider-${index}`} />
)
))}
{extraChildren && (
<>
<FormDropdownDivider />
{extraChildren}
</>
)}
</Dropdown>
);
}
export function useProcessedLocales(locales: Locale[], defaultLocale: Locale | undefined, currentValue: string) {
const activeLocale = defaultLocale?.id === currentValue ? defaultLocale : locales.find(l => l.id === currentValue);
const processedLocales = useMemo(() => {
const leftToRightLanguages = locales.filter((l) => !l.rtl);
@@ -34,29 +64,8 @@ export function LocaleSelector({ id, locales, currentValue, onChange, defaultLoc
];
}
if (extraChildren) {
items.push("---");
}
return items;
}, [ locales ]);
}, [ locales, defaultLocale ]);
return (
<Dropdown id={id} text={activeLocale?.name}>
{processedLocales.map(locale => {
if (typeof locale === "object") {
return <FormListItem
rtl={locale.rtl}
checked={locale.id === currentValue}
onClick={() => {
setActiveLocale(locale);
onChange(locale.id);
}}
>{locale.name}</FormListItem>
} else {
return <FormDropdownDivider />
}
})}
{extraChildren}
</Dropdown>
)
return { activeLocale, processedLocales };
}

View File

@@ -35,7 +35,7 @@
"@triliumnext/commons": "workspace:*",
"@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "13.0.1",
"electron": "39.2.6",
"electron": "39.2.7",
"@electron-forge/cli": "7.10.2",
"@electron-forge/maker-deb": "7.10.2",
"@electron-forge/maker-dmg": "7.10.2",

View File

@@ -12,7 +12,7 @@
"@triliumnext/desktop": "workspace:*",
"@types/fs-extra": "11.0.4",
"copy-webpack-plugin": "13.0.1",
"electron": "39.2.6",
"electron": "39.2.7",
"fs-extra": "11.3.2"
},
"scripts": {

View File

@@ -1,5 +1,5 @@
import { expect, Locator, Page } from "@playwright/test";
import type { BrowserContext } from "@playwright/test";
import { expect, Locator, Page } from "@playwright/test";
interface GotoOpts {
url?: string;
@@ -123,7 +123,7 @@ export default class App {
const noteActionsButton = this.currentNoteSplit.locator(".note-actions");
await noteActionsButton.click();
const dropdownMenu = noteActionsButton.locator(".dropdown-menu");
const dropdownMenu = noteActionsButton.locator(".dropdown-menu").first();
await this.page.waitForTimeout(100);
await expect(dropdownMenu).toBeVisible();
dropdownMenu.getByText(itemToFind).click();
@@ -163,7 +163,7 @@ export default class App {
}
dropdown(_locator: Locator): DropdownLocator {
let locator = _locator as DropdownLocator;
const locator = _locator as DropdownLocator;
locator.selectOptionByText = async (text: string) => {
await locator.locator(".dropdown-toggle").click();
await locator.locator(".dropdown-item", { hasText: text }).click();

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