Compare commits

...

594 Commits

Author SHA1 Message Date
perf3ct
912bc61730 feat(search): also limit note content that can be searched, but keep searchability of titles 2025-08-28 18:56:06 +00:00
perf3ct
93e8459d4b feat(quick_search): remove some old variables that are no longer used now 2025-08-27 22:33:38 +00:00
perf3ct
6c26fa709e feat(quick_search): just fuzzy match note titles for larger notes, while still matching on exact strings 2025-08-27 21:11:44 +00:00
Elian Doran
f3416fa03e Translations update from Hosted Weblate (#6807) 2025-08-27 20:31:34 +03:00
rodrigomescua
3e5ab2b1e1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 82.0% (1284 of 1564 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pt_BR/
2025-08-27 17:15:24 +00:00
Newcomer1989
5c0bc9a7c2 Translated using Weblate (German)
Currently translated at 91.8% (1437 of 1564 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2025-08-27 17:15:23 +00:00
Elian Doran
2adfa55acd React context-aware widgets (ribbon, note actions, note title) (#6731) 2025-08-27 20:15:04 +03:00
Elian Doran
8125e8afcd fix(react/options): plain text not disabled 2025-08-27 20:14:33 +03:00
Elian Doran
e328f18558 feat(react/ribbon): edit content languages in modal 2025-08-27 19:56:14 +03:00
Elian Doran
cbdfa9079c fix(react/ribbon): bad right margin of note type 2025-08-27 19:51:24 +03:00
Elian Doran
e08c4515a7 fix(react/ribbon): note type not always updating 2025-08-27 19:49:22 +03:00
Elian Doran
650aa16b89 feat(react/ribbon): improve style of note type modal 2025-08-27 19:46:26 +03:00
Elian Doran
11d908218b feat(react/ribbon): allow editing code types directly 2025-08-27 19:42:20 +03:00
Elian Doran
3627a7dc93 refactor(react/dialogs): allow modals in sub-components 2025-08-27 19:35:47 +03:00
Elian Doran
1e1c8cc4ff fix(ribbon): code note types not refreshing 2025-08-27 19:13:30 +03:00
Elian Doran
d616bc09c9 chore(e2e): remove test that is no longer relevant 2025-08-27 18:55:52 +03:00
Elian Doran
f1cef44d5d Merge remote-tracking branch 'origin/main' into react/note_context_aware
; Conflicts:
;	pnpm-lock.yaml
2025-08-27 18:30:19 +03:00
Elian Doran
3795be4750 Translations update from Hosted Weblate (#6802) 2025-08-27 18:29:13 +03:00
Elian Doran
2bb66a7526 chore(react): fix some more type errors 2025-08-27 18:27:47 +03:00
Elian Doran
28a472782f chore(react): remove debug log 2025-08-27 18:26:27 +03:00
Elian Doran
a4da002352 fix(react/ribbon): saving from attribute dialog not working 2025-08-27 18:25:54 +03:00
Elian Doran
94fdc2beee fix(react/dialogs): formatting toolbar shown in code notes in quick edit 2025-08-27 18:12:39 +03:00
Elian Doran
dd4a01d9f8 fix(react/dialogs): jump to note sometimes showing empty list 2025-08-27 17:59:37 +03:00
Elian Doran
a2a6c67350 fix(react): alignment and size of search/bulk action buttons 2025-08-27 17:53:27 +03:00
Elian Doran
2152ca7ba6 fix(react): search crashing due to bad rendering mechanism 2025-08-27 17:46:20 +03:00
Elian Doran
40e4d236f4 fix(react): owned attributes not showing up the first time 2025-08-27 17:42:38 +03:00
Elian Doran
0450cd080d fix(react): note context sometimes not working on mobile 2025-08-27 17:37:28 +03:00
Elian Doran
1eaac79d63 fix(react/ribbon): note context menu button looking off 2025-08-27 16:58:01 +03:00
Elian Doran
19c0305ed9 fix(react/revisions): revision list overflowing when too many 2025-08-27 16:41:42 +03:00
Elian Doran
f0d14a966a fix(react/revisions): wrong selection when navigating between notes 2025-08-27 16:39:48 +03:00
Elian Doran
37e6ccdc1a fix(react/revisions): selection not possible due to new hierarchy 2025-08-27 16:37:07 +03:00
Elian Doran
06cea99b40 fix(react): note title not selecting text 2025-08-27 16:28:07 +03:00
Elian Doran
1851336862 fix(react/ribbon): solve some type errors 2025-08-27 16:17:19 +03:00
Elian Doran
461eb273d9 fix(react/ribbon): attribute editor sometimes not clearing between notes 2025-08-27 15:53:27 +03:00
Elian Doran
470edc4d70 fix(react/ribbon): attribute editor saving unnecessarily 2025-08-27 15:35:44 +03:00
Elian Doran
26132a2a56 fix(react/ribbon): crash due to misuse of component rendering 2025-08-27 15:19:31 +03:00
Elian Doran
d92bd16042 chore(react/ribbon): react to note type changes 2025-08-27 14:56:18 +03:00
Elian Doran
1c7dfa6c91 chore(react/ribbon): fix activation of add new label to all tabs 2025-08-27 13:19:52 +03:00
Elian Doran
3a3fed4314 chore(react/ribbon): fix 3px height of the ribbon when collapsed 2025-08-27 13:13:02 +03:00
Elian Doran
82bdb76d75 chore(react/ribbon): simplify useNoteContext & handle setNoteContext 2025-08-27 13:06:57 +03:00
Elian Doran
066f3ea078 chore(react/ribbon): react to add new label/relation even when not shown 2025-08-27 13:05:51 +03:00
Elian Doran
9d760a21d5 chore(react/ribbon): disable when view mode is not good 2025-08-27 12:20:11 +03:00
Elian Doran
976c795ac6 chore(react/ribbon): add tooltip with keyboard shortcut 2025-08-27 12:16:40 +03:00
Elian Doran
ed320e4e24 chore(client): fix type error due to React integration 2025-08-27 11:59:07 +03:00
Newcomer1989
3111738700 Translated using Weblate (German)
Currently translated at 100.0% (378 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/de/
2025-08-27 10:28:44 +02:00
rodrigomescua
1fa0bada23 Translated using Weblate (Portuguese (Brazil))
Currently translated at 62.5% (978 of 1564 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pt_BR/
2025-08-27 10:28:44 +02:00
Newcomer1989
11eca7e58b Translated using Weblate (German)
Currently translated at 90.3% (1413 of 1564 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2025-08-27 10:28:44 +02:00
toaik
4b50e2f14d Translated using Weblate (German)
Currently translated at 90.3% (1413 of 1564 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2025-08-27 10:28:43 +02:00
Newcomer1989
63faba9603 Translated using Weblate (German)
Currently translated at 90.3% (1413 of 1564 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2025-08-27 10:28:43 +02:00
Elian Doran
3e213699e0 chore(client): fix type error due to React integration 2025-08-27 11:27:49 +03:00
Elian Doran
c88ff07691 chore(deps): update dependency typedoc to v0.28.11 (#6792) 2025-08-27 11:25:52 +03:00
Elian Doran
b53aa5cf6e chore(deps): update svelte monorepo (#6793) 2025-08-27 11:25:44 +03:00
Elian Doran
2641b9b3fe fix(deps): update dependency bootstrap to v5.3.8 (#6794) 2025-08-27 11:25:35 +03:00
Elian Doran
3a2a73992c chore(deps): update dependency @types/node to v22.18.0 (#6795) 2025-08-27 11:24:39 +03:00
Elian Doran
b82b17a701 chore(deps): update typescript-eslint monorepo to v8.41.0 (#6796) 2025-08-27 11:24:27 +03:00
renovate[bot]
a6202edcd1 chore(deps): update typescript-eslint monorepo to v8.41.0 2025-08-27 02:40:21 +00:00
renovate[bot]
6eac0cb75d chore(deps): update dependency @types/node to v22.18.0 2025-08-27 02:38:43 +00:00
renovate[bot]
83672d6138 fix(deps): update dependency bootstrap to v5.3.8 2025-08-27 02:38:15 +00:00
renovate[bot]
51dadf72d0 chore(deps): update svelte monorepo 2025-08-27 02:37:45 +00:00
renovate[bot]
0cbf61acb3 chore(deps): update dependency typedoc to v0.28.11 2025-08-27 02:37:12 +00:00
Elian Doran
399c7435ac Merge remote-tracking branch 'origin/main' into react/note_context_aware 2025-08-26 23:49:00 +03:00
Elian Doran
d51fae7878 fix(mobile): file properties not displayed 2025-08-26 23:23:45 +03:00
Elian Doran
9750e25ad5 fix(mobile): note title not working 2025-08-26 22:21:42 +03:00
Elian Doran
2f9b2f0e8f refactor(react): rename formatting toolbar for clarity 2025-08-26 21:57:28 +03:00
Elian Doran
8deaf22544 fix(quick_edit): classic toolbar not shown 2025-08-26 21:45:22 +03:00
Elian Doran
b192f43187 chore(release): prepare for 0.98.1 2025-08-26 20:35:41 +03:00
Elian Doran
8cb8d1303c docs(release): v0.98.1 2025-08-26 20:34:36 +03:00
Elian Doran
5237348975 chore(docs): fix quick search documentation not in meta 2025-08-26 19:44:01 +03:00
Elian Doran
72e2f6757e fix(client): autocomplete looking off in new tab 2025-08-26 19:15:36 +03:00
Elian Doran
cf059e7f86 Translations update from Hosted Weblate (#6787) 2025-08-26 15:36:21 +03:00
Elian Doran
44d69216b6 Translated using Weblate (Romanian)
Currently translated at 100.0% (1564 of 1564 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ro/
2025-08-26 14:35:50 +02:00
Elian Doran
c38bf09af0 chore(client): fix a table nesting issue 2025-08-26 11:53:58 +03:00
Elian Doran
9373d47e86 fix: log same error message on api 401 as on login error to allow fail2ban blocking (#6782) 2025-08-26 08:53:55 +03:00
hulmgulm
29350628c3 Merge branch 'main' into main 2025-08-26 06:26:41 +02:00
Elian Doran
83d1a68879 fix(i18n): pt_BR still not working 2025-08-25 23:32:20 +03:00
hulmgulm
f188408099 Merge branch 'main' into main 2025-08-25 21:22:07 +02:00
Elian Doran
449ab3a798 fix(client/about): directory not displayed on desktop 2025-08-25 22:19:17 +03:00
hulmgulm
21504d1417 Logout same error message on api 401 as on login error 2025-08-25 21:18:01 +02:00
Elian Doran
3060b496e3 chore(client): remove redundant log 2025-08-25 22:09:29 +03:00
Elian Doran
bd35539fa1 fix(i18n): electron locale for pt_BR 2025-08-25 21:34:59 +03:00
Elian Doran
df6447e3ad feat(quick_search): also allow for the equals operator in note title's quick search (#6769) 2025-08-25 20:49:45 +03:00
Elian Doran
24fd898f0d fix(add_link): inserting link to selection not working properly (closes #6776) 2025-08-25 20:48:15 +03:00
Elian Doran
1aa6238288 chore(deps): update dependency @types/jquery to v3.5.33 (#6748) 2025-08-25 20:22:33 +03:00
Elian Doran
c16c4788da fix(client): type error 2025-08-25 20:18:51 +03:00
Elian Doran
0c35daab85 Merge remote-tracking branch 'origin/main' into renovate/jquery-3.x 2025-08-25 20:01:59 +03:00
Elian Doran
4a19639e92 fix(deps): update dependency tsx to v4.20.5 (#6775) 2025-08-25 20:00:55 +03:00
renovate[bot]
36cceea677 fix(deps): update dependency tsx to v4.20.5 2025-08-25 16:33:16 +00:00
Elian Doran
4dbc76790a Merge remote-tracking branch 'origin/main' into react/note_context_aware 2025-08-25 19:33:14 +03:00
Elian Doran
b32a344a21 chore(deps): update dependency tsx to v4.20.5 (#6772) 2025-08-25 19:31:04 +03:00
Elian Doran
3896ab822f Translations update from Hosted Weblate (#6778) 2025-08-25 19:30:49 +03:00
OKiU Network
cfa4ba57d4 Translated using Weblate (Dutch)
Currently translated at 1.8% (7 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/nl/
2025-08-25 18:29:51 +02:00
OKiU Network
da051e0269 Translated using Weblate (Dutch)
Currently translated at 0.3% (5 of 1564 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/nl/
2025-08-25 18:29:50 +02:00
Cédric MARCOUX
3eda77a91f Translated using Weblate (French)
Currently translated at 88.3% (334 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/fr/
2025-08-25 18:29:49 +02:00
OKiU Network
5c2f4be5dd Added translation using Weblate (Dutch) 2025-08-25 18:29:49 +02:00
OKiU Network
435b501db9 Added translation using Weblate (Dutch) 2025-08-25 18:29:48 +02:00
Newcomer1989
5a27ffef5f Translated using Weblate (German)
Currently translated at 100.0% (378 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/de/
2025-08-25 18:29:47 +02:00
Luis Rebhan
02256d9a45 Translated using Weblate (German)
Currently translated at 82.9% (1297 of 1564 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2025-08-25 18:29:47 +02:00
Newcomer1989
7e069009d6 Translated using Weblate (German)
Currently translated at 82.9% (1297 of 1564 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2025-08-25 18:29:46 +02:00
Elian Doran
3c25cda4c0 feat(docs): also improve how environment variables are shown in docs (#6727) 2025-08-25 19:29:40 +03:00
Elian Doran
70d7ad0b1a fix(jump_to): fix issue where attributes/tags don't show in results (#6752) 2025-08-25 19:26:48 +03:00
Elian Doran
e793b2f661 feat(config): fix previously documented env var formula not working (#6726) 2025-08-25 19:23:35 +03:00
Elian Doran
917ea3e401 feat(react): set up ESLint 2025-08-25 18:42:07 +03:00
Elian Doran
5a54dd666f refactor(react): fix a few more rules of hooks violations 2025-08-25 18:41:48 +03:00
Elian Doran
733ec2c145 refactor(react): fix a few rules of hooks violations 2025-08-25 18:00:10 +03:00
Elian Doran
e386b03b90 refactor(react): fix all eslint issues in .tsx files 2025-08-25 17:20:47 +03:00
Elian Doran
25d5d51085 chore(react): fix note path when empty 2025-08-25 16:15:13 +03:00
Elian Doran
50c4301a34 chore(react): fix height of title bar 2025-08-25 16:05:09 +03:00
Elian Doran
0f60c0696b chore(react): fix leak in content widget 2025-08-25 15:53:21 +03:00
Elian Doran
3ec2947c4f chore(react): fix configure languages open in background 2025-08-25 14:48:13 +03:00
Elian Doran
e89162838e chore(react): fix events not updating properly 2025-08-25 14:48:00 +03:00
Elian Doran
72181090a5 refactor(react): get rid of ReactBasicWidget 2025-08-25 14:36:17 +03:00
Elian Doran
72b2a5cc0d chore(react): use effects for event handlers to prevent leaks 2025-08-25 14:27:32 +03:00
Elian Doran
1eaeec8100 Revert "chore(react): prototype for note context"
This reverts commit 660db3b3ab.
2025-08-25 13:51:43 +03:00
Elian Doran
660db3b3ab chore(react): prototype for note context 2025-08-25 11:48:56 +03:00
Elian Doran
89d2fcb81e refactor(react): add debug information for devtools 2025-08-25 11:01:12 +03:00
Elian Doran
ccda623840 refactor(react/ribbon): remove unnecessary hook 2025-08-25 10:32:11 +03:00
renovate[bot]
a6e7dff61e chore(deps): update dependency tsx to v4.20.5 2025-08-25 06:59:19 +00:00
Elian Doran
86d1bbe8ff chore(deps): update dependency webdriverio to v9.19.2 (#6773) 2025-08-25 09:54:52 +03:00
Elian Doran
a10cb06f14 fix(deps): update dependency i18next to v25.4.2 (#6774) 2025-08-25 09:54:41 +03:00
renovate[bot]
dd9a62818b fix(deps): update dependency i18next to v25.4.2 2025-08-25 00:34:50 +00:00
renovate[bot]
c0e936675c chore(deps): update dependency webdriverio to v9.19.2 2025-08-25 00:34:18 +00:00
Jon Fuller
b0b788b7dc Merge branch 'main' into fix/quick-search-equals-operator 2025-08-24 14:47:31 -07:00
Jon Fuller
18f0f3ecac Fix for casing and formatting in i18n.ts which was causing compile errors (#6770) 2025-08-24 14:47:18 -07:00
Sky Swimmer
e7d745ac94 Update calendar_view.ts
Same as the previous, another casing error
2025-08-24 23:22:54 +02:00
Sky Swimmer
24abf7f0ed Update i18n.ts
Another casing error throwing off tests
2025-08-24 23:22:24 +02:00
Elian Doran
36fb097d1d chore(react/ribbon): add CSS for context menu 2025-08-25 00:21:44 +03:00
Elian Doran
35ef5fd0d3 chore(react/ribbon): add disable rules for context menu 2025-08-25 00:19:12 +03:00
Sky Swimmer
9a08f6534b Fix casing and formatting in i18n.ts which was causing compile errors 2025-08-24 23:14:45 +02:00
Elian Doran
885dd2053b chore(react/ribbon): add rest of the note action items 2025-08-25 00:08:40 +03:00
Elian Doran
6f6f280bdd chore(react/ribbon): add part of the note actions menu 2025-08-24 23:56:05 +03:00
Elian Doran
a3e8fd374f chore(react/ribbon): port convert to attachment 2025-08-24 23:21:28 +03:00
Elian Doran
f91c1f4180 chore(react/ribbon): port revisions button 2025-08-24 22:56:47 +03:00
Elian Doran
d85746c1b9 Revert "refactor(react/ribbon): use effects for event handling"
This reverts commit 5a17075eef.
2025-08-24 22:43:20 +03:00
Elian Doran
5a17075eef refactor(react/ribbon): use effects for event handling 2025-08-24 22:21:11 +03:00
Elian Doran
6cab47fb55 feat(react/ribbon): bring back toggling tabs via keyboard shortcut 2025-08-24 22:14:42 +03:00
perf3ct
93c5413790 feat(quick_search): also allow for the equals operator in note title's quick search 2025-08-24 18:53:05 +00:00
Elian Doran
f2db7baeba refactor(react): use beta approach for handling events everywhere 2025-08-24 21:18:48 +03:00
Elian Doran
a507991808 refactor(react/modals): use classless components 2025-08-24 20:57:23 +03:00
Elian Doran
7c86f90ac6 chore(react/ribbon): fix some more crashes when rapidly switching tabs 2025-08-24 20:31:39 +03:00
Elian Doran
1e9b772692 chore(react/ribbon): fix cannot set style when switching attributes 2025-08-24 20:25:11 +03:00
Elian Doran
096ab52216 refactor(react/ribbon): solve type errors 2025-08-24 20:23:00 +03:00
Elian Doran
88c3cd5cdd refactor(react/ribbon): move files around & remove imports 2025-08-24 20:16:58 +03:00
Elian Doran
99a911a220 chore(react/ribbon): port bulk actions for search 2025-08-24 20:12:22 +03:00
Elian Doran
3218ab971b chore(react/ribbon): fix alignment of help/close buttons 2025-08-24 19:03:19 +03:00
Elian Doran
274e3c1f7f refactor(react/ribbon): split into two files 2025-08-24 18:40:05 +03:00
Elian Doran
f8916a6e35 chore(react/ribbon): port limit 2025-08-24 18:34:29 +03:00
Elian Doran
73f20d01e4 chore(react/ribbon): port order by 2025-08-24 18:29:47 +03:00
Elian Doran
2fd3a875b6 chore(react/ribbon): port include archived notes 2025-08-24 18:11:29 +03:00
Elian Doran
68cba8d3b2 chore(react/ribbon): port debug 2025-08-24 18:09:33 +03:00
Elian Doran
6b28fd405e chore(react/ribbon): port fast search 2025-08-24 18:07:16 +03:00
Elian Doran
3bccbabe53 chore(react/ribbon): port ancestor depth 2025-08-24 18:02:18 +03:00
Elian Doran
c97c66ed8a Translations update from Hosted Weblate (#6767) 2025-08-24 17:30:55 +03:00
Elian Doran
4b212232c8 chore(react/ribbon): port ancestor (without depth) 2025-08-24 17:29:48 +03:00
Elian Doran
ac3a8edf2b chore(react/ribbon): add search script 2025-08-24 17:20:40 +03:00
Sleepy0Duck5
b581025bbe Translated using Weblate (Korean)
Currently translated at 10.0% (38 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ko/
2025-08-24 16:02:09 +02:00
Sleepy0Duck5
7bc5331747 Translated using Weblate (Korean)
Currently translated at 0.9% (15 of 1564 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ko/
2025-08-24 16:02:07 +02:00
Kuzma Simonov
2415976475 Translated using Weblate (Russian)
Currently translated at 100.0% (1564 of 1564 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ru/
2025-08-24 16:02:06 +02:00
Francis C
8d0d0f0449 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (378 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/zh_Hant/
2025-08-24 16:02:02 +02:00
Francis C
16b00ed160 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1564 of 1564 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2025-08-24 16:02:01 +02:00
chdagenais
df73a420f9 Translated using Weblate (French)
Currently translated at 83.5% (316 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/fr/
2025-08-24 16:01:59 +02:00
Newcomer1989
1e4d57f275 Translated using Weblate (German)
Currently translated at 92.8% (351 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/de/
2025-08-24 16:01:56 +02:00
chdagenais
19a238c8d3 Translated using Weblate (French)
Currently translated at 82.2% (1287 of 1564 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fr/
2025-08-24 16:01:54 +02:00
Francis C
5ffd8a79eb Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1564 of 1564 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2025-08-24 16:01:53 +02:00
Elian Doran
04fbc82d7c chore(react/ribbon): save search to note 2025-08-24 16:58:10 +03:00
Elian Doran
3f105f7b8b chore(react/ribbon): focus on search textbox 2025-08-24 16:45:17 +03:00
Elian Doran
b9193a5562 chore(react/ribbon): handle search error 2025-08-24 16:41:44 +03:00
Elian Doran
e1fa188244 chore(react/ribbon): refresh options 2025-08-24 16:17:10 +03:00
Elian Doran
80ad87671a chore(react/ribbon): search & execute button 2025-08-24 16:02:15 +03:00
Elian Doran
b6d5a6ec2e chore(react/ribbon): dynamic rendering of search options 2025-08-24 15:59:22 +03:00
Elian Doran
759398d804 chore(react/ribbon): working execute search button 2025-08-24 15:48:53 +03:00
Elian Doran
c1b30db3d1 chore(react/ribbon): port search string 2025-08-24 15:29:07 +03:00
Elian Doran
0c8bfc39ef refactor(react/ribbon): bring back tab activation 2025-08-24 12:23:25 +03:00
Elian Doran
3815fddb27 Merge branch 'react/note_context_aware' of https://github.com/TriliumNext/trilium into react/note_context_aware 2025-08-24 12:22:16 +03:00
Elian Doran
b585a64a38 Merge remote-tracking branch 'origin/main' into react/note_context_aware 2025-08-24 12:05:05 +03:00
Elian Doran
58e58c192f Merge branch 'main' of https://github.com/TriliumNext/trilium 2025-08-24 11:58:56 +03:00
Elian Doran
5939344378 fix(deps): update dependency i18next to v25.4.1 (#6754) 2025-08-24 11:49:48 +03:00
Elian Doran
ad85ee3531 feat(react/ribbon): start porting search definitions (buttons) 2025-08-24 11:42:25 +03:00
renovate[bot]
349f19fef7 fix(deps): update dependency i18next to v25.4.1 2025-08-24 08:18:50 +00:00
Elian Doran
d5777a024e chore(deps): update svelte monorepo (#6753) 2025-08-24 11:17:52 +03:00
Elian Doran
b7f4ee6171 fix(deps): update dependency react-i18next to v15.7.2 (#6755) 2025-08-24 11:17:05 +03:00
Elian Doran
a83c4e3970 fix(deps): update dependency eslint-linter-browserify to v9.34.0 (#6756) 2025-08-24 11:16:53 +03:00
Elian Doran
5a767dae34 feat(i18n): add support for Brazilian Portuguese 2025-08-24 11:05:52 +03:00
Elian Doran
9f93d30b99 feat(i18n): add support for Ukrainian 2025-08-24 10:53:21 +03:00
Elian Doran
dff525edc6 Translations update from Hosted Weblate (#6743) 2025-08-24 10:52:42 +03:00
renovate[bot]
26da431320 fix(deps): update dependency react-i18next to v15.7.2 2025-08-24 07:32:43 +00:00
renovate[bot]
cde4622693 fix(deps): update dependency eslint-linter-browserify to v9.34.0 2025-08-24 07:30:44 +00:00
renovate[bot]
5ede7ecc69 chore(deps): update svelte monorepo 2025-08-24 01:36:13 +00:00
Elian Doran
b607d1e628 refactor(react/ribbon): shared component for labelled entry 2025-08-23 23:59:41 +03:00
Elian Doran
d7e36bdf93 feat(react/ribbon): reintroduce combobox collection properties 2025-08-23 23:54:14 +03:00
Elian Doran
2b8b185b5b feat(react/ribbon): reintroduce number collection properties 2025-08-23 23:39:47 +03:00
Elian Doran
927ebcbec9 feat(react/ribbon): reintroduce checkbox collection properties 2025-08-23 23:32:12 +03:00
Elian Doran
ea1397de63 feat(react/ribbon): reintroduce button collection properties 2025-08-23 23:25:25 +03:00
Elian Doran
ce1f5c6204 feat(react/ribbon): port view type 2025-08-23 22:49:32 +03:00
Elian Doran
652114c7b5 feat(react/ribbon): finalize port of inherited attributes tab 2025-08-23 22:18:04 +03:00
Elian Doran
17cd2128fd chore(react): add editorconfig for .tsx 2025-08-23 22:02:41 +03:00
Elian Doran
bc4378cb3e chore(react/ribbon): port inherited attributes partially 2025-08-23 22:02:33 +03:00
perf3ct
513878dfef feat(jump_to): got the styling to look exactly how we were hoping for 2025-08-23 18:58:18 +00:00
perf3ct
753d5529b2 feat(jump_to): get the styling very close to what we want it to look like... 2025-08-23 18:40:11 +00:00
Elian Doran
9f217b88e4 refactor(react/ribbon): set up keyboard shortcuts 2025-08-23 20:59:21 +03:00
Elian Doran
d53faa8c01 refactor(react/ribbon): imperative api for saving, reloading, updating attributes 2025-08-23 20:49:54 +03:00
Elian Doran
a934760960 refactor(react/ribbon): use custom method for injecting handlers 2025-08-23 20:44:03 +03:00
Elian Doran
82914fc2aa chore(react/ribbon): unable to create notes in attribute editor 2025-08-23 20:35:19 +03:00
Elian Doran
db687197de chore(react/ribbon): add focus to attribute editor 2025-08-23 20:31:00 +03:00
Elian Doran
efd713dc61 chore(react/ribbon): add blur & keydown events 2025-08-23 19:54:02 +03:00
Elian Doran
3f3c7cfe88 chore(react/ribbon): add menu 2025-08-23 19:48:01 +03:00
Elian Doran
73ca285b7a chore(react/ribbon): support reference links in attributes 2025-08-23 19:26:23 +03:00
Elian Doran
168d25c020 chore(react/ribbon): fix save icon displayed when it shouldn't 2025-08-23 19:13:48 +03:00
Elian Doran
e8ae5486c8 chore(react/ribbon): display attribute errors 2025-08-23 18:28:42 +03:00
Elian Doran
f049b8b915 chore(react/ribbon): save attribute changes 2025-08-23 18:23:38 +03:00
Микола Копитін
4e755dc537 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1560 of 1560 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/uk/
2025-08-23 14:16:31 +02:00
Francis C
5351310a38 Translated using Weblate (Japanese)
Currently translated at 67.9% (1060 of 1560 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-08-23 14:16:31 +02:00
Микола Копитін
211ca43a82 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (378 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/uk/
2025-08-23 14:16:31 +02:00
Микола Копитін
e5235e7f22 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1560 of 1560 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/uk/
2025-08-23 14:16:31 +02:00
Astryd Park
e72298f0b4 Translated using Weblate (Korean)
Currently translated at 0.2% (1 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ko/
2025-08-23 14:16:31 +02:00
Микола Копитін
3abf5c65c6 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (378 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/uk/
2025-08-23 14:16:31 +02:00
Микола Копитін
268acb0b88 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1560 of 1560 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/uk/
2025-08-23 14:16:30 +02:00
Kuzma Simonov
196b3b873f Translated using Weblate (Russian)
Currently translated at 100.0% (378 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ru/
2025-08-23 14:16:30 +02:00
Микола Копитін
4d9801a372 Translated using Weblate (Russian)
Currently translated at 100.0% (1560 of 1560 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ru/
2025-08-23 14:16:30 +02:00
Kuzma Simonov
bd710ba665 Translated using Weblate (Russian)
Currently translated at 100.0% (1560 of 1560 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ru/
2025-08-23 14:16:30 +02:00
Francis C
afe369c876 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1560 of 1560 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2025-08-23 14:16:30 +02:00
Newcomer1989
206007bbce Translated using Weblate (German)
Currently translated at 81.7% (309 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/de/
2025-08-23 14:16:30 +02:00
rodrigomescua
8ad05b92c0 Translated using Weblate (Portuguese (Brazil))
Currently translated at 60.8% (950 of 1560 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pt_BR/
2025-08-23 14:16:30 +02:00
Francis C
735da2a855 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1560 of 1560 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2025-08-23 14:16:30 +02:00
Elian Doran
980077f559 Add UI performance-related settings (#6747) 2025-08-23 15:16:22 +03:00
Elian Doran
12053e75bb chore(react/ribbon): fix size of attribute widget 2025-08-23 13:13:01 +03:00
Elian Doran
62372ed4c5 chore(react/ribbon): add logic for displaying attribute detail 2025-08-23 12:55:48 +03:00
Elian Doran
e5caf37697 chore(react/ribbon): load current attributes in editor 2025-08-23 12:39:49 +03:00
Elian Doran
befc5a9530 feat(react/ribbon): display help tooltip in attribute editor 2025-08-23 12:31:54 +03:00
Elian Doran
1e00407864 chore(react/ribbon): use separate component for editor 2025-08-23 12:05:03 +03:00
Elian Doran
73038efccf chore(react/ribbon): add some CKEditor events 2025-08-23 11:52:40 +03:00
Elian Doran
6d37e19b40 chore(react/ribbon): start implementing attribute editor 2025-08-23 11:44:51 +03:00
Elian Doran
2c33ef2b0d chore(react/ribbon): similar notes style 2025-08-23 11:28:10 +03:00
Elian Doran
6c30e0836f chore(react/ribbon): also react to width, not just height 2025-08-23 11:21:05 +03:00
Elian Doran
5f77ca31bd chore(react/ribbon): react note map to height changes 2025-08-23 11:12:14 +03:00
Elian Doran
f7c82d6b09 chore(react/ribbon): watch note map size 2025-08-23 11:00:25 +03:00
Elian Doran
86dd9aa42a chore(react/ribbon): integrate expand/collapse button 2025-08-23 10:47:46 +03:00
Elian Doran
5daca270e4 fix(deps): update dependency mermaid to v11.10.1 (#6749) 2025-08-23 08:47:35 +03:00
Elian Doran
e18813a4bf fix(deps): update eslint monorepo to v9.34.0 (#6750) 2025-08-23 08:47:16 +03:00
renovate[bot]
4aa7e211f3 fix(deps): update eslint monorepo to v9.34.0 2025-08-23 00:25:46 +00:00
renovate[bot]
419dc7edfb fix(deps): update dependency mermaid to v11.10.1 2025-08-23 00:24:33 +00:00
renovate[bot]
eaa84a6b39 chore(deps): update dependency @types/jquery to v3.5.33 2025-08-23 00:23:57 +00:00
Adorian Doran
1d0503d0e4 client/settings/ui-performance-settings: remove form groups 2025-08-23 03:22:59 +03:00
Adorian Doran
f7f98aa9a3 client/settings/ui-performance-settings: improve code formatting 2025-08-23 03:10:51 +03:00
Adorian Doran
575d14261a Merge branch 'main' of https://github.com/TriliumNext/Trilium into client/settings/ui-performance-settings 2025-08-23 02:20:37 +03:00
Adorian Doran
9aab606deb style: improve the support of disabled backdrop effects 2025-08-23 02:15:06 +03:00
Adorian Doran
2e11681b52 client/settings/disable backdrop effects: add the CSS implementation 2025-08-23 01:26:21 +03:00
Adorian Doran
8cca6637f7 client/settings/disable backdrop effects: react to the option change 2025-08-23 01:23:20 +03:00
Adorian Doran
82e076378c client/settings/disable backdrop effects: add the corresponding checkbox in the Appearance settings page 2025-08-23 01:20:54 +03:00
Adorian Doran
94ddad3c49 client/settings/disable backdrop effects: add an option to enable or disable backdrop effects 2025-08-23 01:15:00 +03:00
Adorian Doran
d35dbca18b client/settings/disable shadows: add the CSS implementation 2025-08-23 00:58:50 +03:00
Adorian Doran
7468d6147a client/settings/disable shadows: react to the option change 2025-08-23 00:55:46 +03:00
Adorian Doran
7c78d749de client/settings/disable shadows: add the corresponding checkbox in the Appearance settings page 2025-08-23 00:49:35 +03:00
Adorian Doran
85dd99a3c4 client/settings/disable shadows: add an option to enable or disable shadows 2025-08-23 00:43:49 +03:00
Adorian Doran
0a9c0234e2 client/settings/disable motion: update translation 2025-08-23 00:38:06 +03:00
JYC333
fad77ba5a0 chore(deps): update dependency vite-plugin-static-copy to v3.1.2 [security] (#6735) 2025-08-22 23:30:13 +02:00
JYC333
12723f3216 fix(deps): update dependency react-i18next to v15.7.1 (#6739) 2025-08-22 23:29:38 +02:00
JYC333
a43140515f chore(deps): update ckeditor5 config packages to v12.1.1 (#6738) 2025-08-22 23:29:26 +02:00
Adorian Doran
3e3cc8c541 client/settings/disable motion: refactor 2025-08-23 00:19:26 +03:00
Elian Doran
a85141ace2 feat(react/ribbon): port note map partially 2025-08-22 23:47:02 +03:00
Elian Doran
c33280bbb2 chore(react): fix leak & adjustable class name 2025-08-22 23:45:31 +03:00
Elian Doran
df3aa04787 chore(react): proper legacy widget injection & event handling 2025-08-22 23:33:02 +03:00
Elian Doran
4bd25a0d4a chore(react): use different injection mechanism 2025-08-22 23:24:00 +03:00
Elian Doran
7fadf4c6e1 chore(react): prototype hook to render legacy widgets 2025-08-22 23:12:14 +03:00
Adorian Doran
d1538508e8 client/settings/disable motion: turn off jQuery animations if motion is disabled 2025-08-22 22:20:57 +03:00
Elian Doran
b24d786933 chore(react/ribbon): fix a few type errors 2025-08-22 21:58:35 +03:00
Elian Doran
8f69b87dd1 feat(react/ribbon): port note paths tab 2025-08-22 21:45:03 +03:00
Adorian Doran
9b1da8c311 Settings/Appearance: improve CSS selector specificity 2025-08-22 21:37:56 +03:00
Elian Doran
8287063aab feat(react/ribbon): port image properties 2025-08-22 21:09:51 +03:00
Adorian Doran
e4a8258acf client/settings/disable motion: fix submenus not opening 2025-08-22 20:52:31 +03:00
Adorian Doran
5e88043c7b client/settings/disable motion: add the CSS implementation 2025-08-22 20:48:26 +03:00
Adorian Doran
bedf9112fb client/settings/disable motion: add localization support 2025-08-22 20:42:17 +03:00
Adorian Doran
03681d23c5 client/settings/disable motion: add an option to allow transitions and animations to be disabled 2025-08-22 20:32:08 +03:00
Elian Doran
21683db0b8 refactor(react/ribbon): dedicated component for file upload 2025-08-22 20:25:15 +03:00
Elian Doran
978d829150 feat(react/ribbon): port file notes 2025-08-22 20:17:00 +03:00
Elian Doran
cc05572a35 feat(react/ribbon): port similar notes 2025-08-22 19:27:58 +03:00
Elian Doran
c5bb310613 chore(react/ribbon): bring back note info auto-refresh 2025-08-22 19:07:04 +03:00
Elian Doran
77551b1fed feat(react/ribbon): port note info tab 2025-08-22 18:23:54 +03:00
Elian Doran
70728c274e feat(react/ribbon): port note properties 2025-08-22 17:41:45 +03:00
Elian Doran
cee4714665 feat(react/ribbon): port edited notes 2025-08-22 17:31:15 +03:00
Elian Doran
c3eca3b626 feat(react/ribbon): port script tab 2025-08-22 16:58:44 +03:00
Elian Doran
01e4cd2e78 chore(react/ribbon): set up formatting toolbar 2025-08-22 16:34:16 +03:00
Elian Doran
b99d01ad7b chore(react/ribbon): bring back tab filtering 2025-08-22 16:07:30 +03:00
Elian Doran
bf0213907e chore(react/ribbon): finalize language switcher 2025-08-22 15:40:36 +03:00
Elian Doran
eff5b6459d chore(react/ribbon): fix event 2025-08-22 15:11:12 +03:00
Elian Doran
8e29b5eed6 feat(react/ribbon): port note language 2025-08-22 12:34:21 +03:00
Elian Doran
c91748da15 feat(react/ribbon): port template switch 2025-08-22 12:15:03 +03:00
Elian Doran
f04f9dc262 feat(react/ribbon): port shared switch 2025-08-22 11:57:45 +03:00
Elian Doran
e873cdab7e chore(react/ribbon): fix width of editability select 2025-08-22 11:42:07 +03:00
Elian Doran
f9b6fd6ac5 feat(react): port bookmark switch 2025-08-22 11:40:27 +03:00
renovate[bot]
aa191e110c fix(deps): update dependency react-i18next to v15.7.1 2025-08-22 02:44:08 +00:00
renovate[bot]
dd09907925 chore(deps): update ckeditor5 config packages to v12.1.1 2025-08-22 02:43:31 +00:00
Elian Doran
da4810672d feat(react/ribbon): improve editability select 2025-08-21 22:24:35 +03:00
Elian Doran
f772f59d7c feat(react/ribbon): port editability select 2025-08-21 22:19:26 +03:00
Elian Doran
1964fb90d5 feat(react/ribbon): port toggle protected note 2025-08-21 21:26:45 +03:00
Elian Doran
5945f2860a chore(react/ribbon): finalize note type selection 2025-08-21 21:01:57 +03:00
Elian Doran
f45da049b9 chore(react/ribbon): change note type 2025-08-21 20:56:37 +03:00
Elian Doran
c0beab8a5d chore(react/ribbon): display current note type 2025-08-21 20:38:19 +03:00
Elian Doran
cabeb13adb chore(react/ribbon): add note types 2025-08-21 20:30:12 +03:00
Elian Doran
e2e9721d5f chore(react/ribbon): add basic note types & badges 2025-08-21 20:16:06 +03:00
Elian Doran
4e9deab605 chore(react/ribbon): make content work 2025-08-21 19:59:56 +03:00
Elian Doran
9bb048fb01 chore(react/ribbon): toggleable tabs 2025-08-21 19:48:07 +03:00
Elian Doran
6849f80506 chore(react/ribbon): make tabs activable 2025-08-21 19:35:21 +03:00
Elian Doran
5597f4e2e0 chore(react/ribbon): add all ribbon tab titles & icon 2025-08-21 19:27:18 +03:00
renovate[bot]
35e9508bde chore(deps): update dependency vite-plugin-static-copy to v3.1.2 [security] 2025-08-21 15:41:25 +00:00
Elian Doran
45fbcec805 feat(react/ribbon): port base structure 2025-08-21 18:29:13 +03:00
Elian Doran
4c8da70ef3 chore(deps): update dependency rollup-plugin-webpack-stats to v2.1.4 (#6721) 2025-08-21 17:49:28 +03:00
Elian Doran
ed5da5cd4a Translations update from Hosted Weblate (#6732) 2025-08-21 17:49:10 +03:00
Astryd Park
dc5fccdbcd Added translation using Weblate (Korean) 2025-08-21 16:32:31 +02:00
Astryd Park
91aea333c7 Added translation using Weblate (Korean) 2025-08-21 16:32:31 +02:00
Микола Копитін
a0de01cff1 Translated using Weblate (Ukrainian)
Currently translated at 99.4% (376 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/uk/
2025-08-21 16:32:30 +02:00
Микола Копитін
a41ed34193 Translated using Weblate (Ukrainian)
Currently translated at 37.0% (578 of 1560 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/uk/
2025-08-21 16:32:30 +02:00
Newcomer1989
49e8811c18 Translated using Weblate (German)
Currently translated at 77.7% (294 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/de/
2025-08-21 16:32:29 +02:00
Lucas Fernandes de Camargo
488563a82e Translated using Weblate (Portuguese (Brazil))
Currently translated at 22.5% (351 of 1560 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pt_BR/
2025-08-21 16:32:29 +02:00
Elian Doran
d76d50f30e chore(react): fix a type error 2025-08-21 17:16:03 +03:00
Elian Doran
4685aef88d chore(react/note_icon): reset to default icon 2025-08-21 16:19:18 +03:00
Elian Doran
a106510924 chore(react/note_icon): react to icon changes 2025-08-21 15:50:14 +03:00
Elian Doran
9d54503ef7 chore(react/note_icon): reintroduce setting the icon 2025-08-21 15:21:32 +03:00
Elian Doran
b1449eebf3 chore(react/note_icon): reintroduce read-only 2025-08-21 15:14:19 +03:00
Elian Doran
b213453062 refactor(react/note_icon): introduce autofocus at text box level 2025-08-21 15:11:08 +03:00
Elian Doran
076c0321cf chore(react/note_icon): focus search by default 2025-08-21 15:09:25 +03:00
Elian Doran
4d71b73f38 chore(react/note_icon): case insensitive search 2025-08-21 15:07:41 +03:00
Elian Doran
b20ffdf7db chore(react/note_icon): sort by count 2025-08-21 15:05:55 +03:00
Elian Doran
ef018e22d6 feat(react/note_icon): render dropdown only when needed 2025-08-21 15:03:54 +03:00
Elian Doran
3fa290a257 chore(react/note_icon): add back filter by category 2025-08-21 14:57:54 +03:00
Elian Doran
cdde530b60 chore(react/note_icon): add back filter by text 2025-08-21 14:45:19 +03:00
Elian Doran
aa608510d0 feat(react): port note icon 2025-08-21 14:41:59 +03:00
Elian Doran
009fd63ce9 chore(react): finalize note title porting 2025-08-21 13:18:39 +03:00
Elian Doran
bea352855a refactor(react): allow binding multiple events at once 2025-08-21 13:17:28 +03:00
Elian Doran
51e8a80ca3 chore(react/note_title): delete new notes on escape 2025-08-21 13:13:48 +03:00
Elian Doran
8a543d4513 chore(react/note_title): focus content on enter 2025-08-21 13:00:08 +03:00
Elian Doran
945e180a6f chore(react/note_title): add before unload listener 2025-08-21 12:55:33 +03:00
Elian Doran
b93fa332d3 fix(client): please wait for save showing up multiple times 2025-08-21 12:49:03 +03:00
Elian Doran
9e947f742d fix(react/note_title): title shown on empty widget pane 2025-08-21 12:15:12 +03:00
Elian Doran
033e90f8b7 fix(react/note_title): not refreshing on protected session 2025-08-21 12:13:30 +03:00
Elian Doran
be576176c5 feat(react/note_title): bring back navigation title 2025-08-21 11:08:33 +03:00
Elian Doran
4da3e8a4d8 refactor(react/note_title): use note property for title as well 2025-08-21 10:54:38 +03:00
Elian Doran
db2bf537ea refactor(react/note_title): use hook for listening to note property 2025-08-21 10:44:58 +03:00
Elian Doran
9a4fdcaef2 chore(react/note_title): bring back styles 2025-08-21 10:34:30 +03:00
Elian Doran
ca40360f7d feat(react): basic implementation of note title 2025-08-21 10:08:49 +03:00
Elian Doran
799e705ff8 fix(react): note context not always updated 2025-08-21 09:18:52 +03:00
Elian Doran
a1b18c7f97 chore(deps): update dependency @sveltejs/kit to v2.36.1 (#6723) 2025-08-21 08:22:09 +03:00
Elian Doran
9958a6e1bf fix(deps): update dependency i18next to v25.4.0 (#6724) 2025-08-21 08:21:24 +03:00
renovate[bot]
1fc6d8aca7 fix(deps): update dependency i18next to v25.4.0 2025-08-21 05:21:13 +00:00
Elian Doran
3e9ec2d943 chore(deps): update dependency @playwright/test to v1.55.0 (#6722) 2025-08-21 08:20:47 +03:00
Elian Doran
1420def1c3 fix(deps): update dependency react-i18next to v15.7.0 (#6725) 2025-08-21 08:19:38 +03:00
renovate[bot]
3b4184e765 chore(deps): update dependency @sveltejs/kit to v2.36.1 2025-08-21 02:22:23 +00:00
perf3ct
4ce9102f93 feat(docs): try to also improve how environment variables are shown in docs 2025-08-21 02:21:00 +00:00
perf3ct
eb27ec2234 feat(config): fix previously documented env var formula not working
asdf
2025-08-21 02:18:08 +00:00
renovate[bot]
b70e25d348 fix(deps): update dependency react-i18next to v15.7.0 2025-08-21 00:05:32 +00:00
renovate[bot]
772c0bbe1a chore(deps): update dependency @playwright/test to v1.55.0 2025-08-21 00:04:01 +00:00
renovate[bot]
144021c053 chore(deps): update dependency rollup-plugin-webpack-stats to v2.1.4 2025-08-21 00:03:29 +00:00
Elian Doran
59486cd55d feat(react): basic handling of note context aware 2025-08-20 23:53:13 +03:00
Elian Doran
afe3904ea3 feat(react): render raw react components 2025-08-20 22:13:52 +03:00
Elian Doran
8abd3ed3f1 feat(docs): implement swagger ui endpoint for internal api (#6719) 2025-08-20 21:36:21 +03:00
perf3ct
53ed510c92 feat(docs): remove old json api docs 2025-08-20 17:36:22 +00:00
Elian Doran
4ec46a2ebd chore(client): add some documentation 2025-08-20 20:34:00 +03:00
Elian Doran
db6f948499 Translations update from Hosted Weblate (#6720) 2025-08-20 20:33:26 +03:00
perf3ct
05c73011f5 feat(docs): add additional api routes 2025-08-20 17:30:26 +00:00
Микола Копитін
3b733d01f1 Translated using Weblate (Ukrainian)
Currently translated at 44.1% (167 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/uk/
2025-08-20 17:29:08 +00:00
Микола Копитін
ebf21296d4 Translated using Weblate (Ukrainian)
Currently translated at 11.8% (184 of 1551 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/uk/
2025-08-20 17:29:07 +00:00
Elian Doran
6f83ac4822 Port settings to React (#6660) 2025-08-20 20:28:54 +03:00
perf3ct
d358924324 feat(docs): implement swagger ui endpoint for internal api 2025-08-20 17:14:44 +00:00
perf3ct
f9a3606ca2 feat(docs): implement swagger ui endpoint for internal api 2025-08-20 17:11:54 +00:00
Elian Doran
33299ad51e chore(deps): update package lock 2025-08-20 19:45:49 +03:00
Elian Doran
8752182e7e Bump mermaid from 11.9.0 to 11.10.0 in /apps/client (#6717) 2025-08-20 19:22:44 +03:00
Elian Doran
0551ac8ead feat(ci): don't run checks outside main repo 2025-08-20 19:22:21 +03:00
Elian Doran
6d5a11bd4d Translations update from Hosted Weblate (#6718) 2025-08-20 19:14:25 +03:00
Elian Doran
ce19d84247 test(e2e): i18n test broken due to button 2025-08-20 19:10:41 +03:00
Микола Копитін
f24aa45a3b Translated using Weblate (Ukrainian)
Currently translated at 39.6% (150 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/uk/
2025-08-20 18:02:18 +02:00
Микола Копитін
64a28a7e75 Translated using Weblate (Ukrainian)
Currently translated at 10.8% (168 of 1551 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/uk/
2025-08-20 18:02:16 +02:00
Flowerlywind
249a755312 Translated using Weblate (Vietnamese)
Currently translated at 2.1% (34 of 1551 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/vi/
2025-08-20 18:02:13 +02:00
Kuzma Simonov
a3d51a013c Translated using Weblate (Russian)
Currently translated at 88.6% (335 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ru/
2025-08-20 18:02:12 +02:00
repilac
839def9959 Translated using Weblate (Japanese)
Currently translated at 97.6% (369 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ja/
2025-08-20 18:02:10 +02:00
Kuzma Simonov
fd432a7100 Translated using Weblate (Russian)
Currently translated at 81.9% (1271 of 1551 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ru/
2025-08-20 18:02:08 +02:00
repilac
60a07ce1e7 Translated using Weblate (Japanese)
Currently translated at 68.2% (1059 of 1551 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-08-20 18:02:07 +02:00
diego diaz
88c5700d87 Translated using Weblate (Spanish)
Currently translated at 100.0% (1551 of 1551 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2025-08-20 18:02:05 +02:00
Elian Doran
d59993abf6 fix(server): potential race condition when rotating logs 2025-08-20 19:00:41 +03:00
Elian Doran
0754011909 chore(react): fix type errors 2025-08-20 18:52:14 +03:00
Elian Doran
376bb66cab Merge remote-tracking branch 'origin/main' into react/settings
; Conflicts:
;	pnpm-lock.yaml
2025-08-20 18:32:57 +03:00
Elian Doran
588e15c633 fix(settings): not fitting properly on mobile 2025-08-20 18:17:52 +03:00
Elian Doran
93b8ad20d7 fix(react/settings): settings displayed inline 2025-08-20 18:17:42 +03:00
dependabot[bot]
e51b3d760d Bump mermaid from 11.9.0 to 11.10.0 in /apps/client
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 11.9.0 to 11.10.0.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Commits](https://github.com/mermaid-js/mermaid/compare/mermaid@11.9.0...mermaid@11.10.0)

---
updated-dependencies:
- dependency-name: mermaid
  dependency-version: 11.10.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-20 14:44:27 +00:00
Elian Doran
91f3bc4488 chore(deps): update svelte monorepo (#6707) 2025-08-20 08:41:54 +03:00
Elian Doran
3e80a99bbf fix(deps): update dependency mermaid to v11.10.0 [security] (#6700) 2025-08-20 08:41:11 +03:00
renovate[bot]
37cdb55e79 chore(deps): update svelte monorepo 2025-08-20 05:37:42 +00:00
renovate[bot]
58b66c0c95 fix(deps): update dependency mermaid to v11.10.0 [security] 2025-08-20 05:36:02 +00:00
Elian Doran
e5f9db86a1 fix(deps): update ckeditor monorepo to v46.0.2 (#6704) 2025-08-20 08:32:25 +03:00
Elian Doran
f138f99356 chore(deps): update pnpm to v10.15.0 (#6706) 2025-08-20 08:31:17 +03:00
renovate[bot]
c42f4b9814 fix(deps): update ckeditor monorepo to v46.0.2 2025-08-20 05:31:11 +00:00
Elian Doran
0a9fb886e3 fix(deps): update dependency @mermaid-js/layout-elk to v0.1.9 (#6705) 2025-08-20 08:31:04 +03:00
Elian Doran
3c4577201f chore(deps): update dependency vite to v7.1.3 (#6703) 2025-08-20 08:30:16 +03:00
Elian Doran
816421188f chore(deps): update dependency electron to v37.3.1 (#6702) 2025-08-20 08:29:36 +03:00
renovate[bot]
5b15d2c4c6 chore(deps): update pnpm to v10.15.0 2025-08-20 01:18:37 +00:00
renovate[bot]
4bc7165452 fix(deps): update dependency @mermaid-js/layout-elk to v0.1.9 2025-08-20 01:18:28 +00:00
renovate[bot]
82d6531e8c chore(deps): update dependency vite to v7.1.3 2025-08-20 01:17:10 +00:00
renovate[bot]
d6209035c3 chore(deps): update dependency electron to v37.3.1 2025-08-20 01:16:36 +00:00
Elian Doran
1d7799f981 refactor(react/settings): add names to all form groups 2025-08-19 23:34:25 +03:00
Elian Doran
51291a61e6 refactor(react/settings): associate IDs for labels 2025-08-19 22:54:23 +03:00
Elian Doran
0841603be0 refactor(react/settings): use FormGroup for time selector 2025-08-19 22:36:47 +03:00
Elian Doran
59ba6a0b1e chore(react/settings): use FormGroup for labels 2025-08-19 22:32:20 +03:00
Elian Doran
53eda46043 chore(react/settings): use translation for all units 2025-08-19 21:50:29 +03:00
Elian Doran
cbc9fb7d08 chore(react/settings): solve type errors 2025-08-19 21:41:05 +03:00
Elian Doran
1f479b20be chore(react/settings): use onBlur instead of onChange 2025-08-19 20:09:56 +03:00
Elian Doran
f00b8e9522 chore(react/settings): set 100% width for textarea 2025-08-19 17:39:41 +03:00
Elian Doran
c7dd271516 Translations update from Hosted Weblate (#6697) 2025-08-19 17:07:49 +03:00
Hosted Weblate
a947a61d65 Update translation files
Updated by "Remove blank strings" add-on in Weblate.

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/
2025-08-19 16:02:30 +02:00
Székely Miklós
0122f1cc5e Translated using Weblate (Hungarian)
Currently translated at 6.3% (24 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hu/
2025-08-19 16:02:27 +02:00
Wojciech O
acb905a3e6 Translated using Weblate (Polish)
Currently translated at 37.0% (140 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/pl/
2025-08-19 16:02:26 +02:00
Wojciech O
7422eb5598 Translated using Weblate (Polish)
Currently translated at 1.3% (21 of 1551 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pl/
2025-08-19 16:02:22 +02:00
Kuzma Simonov
e721166f95 Translated using Weblate (Russian)
Currently translated at 50.2% (190 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ru/
2025-08-19 16:02:18 +02:00
acwr47
5a48130fa4 Translated using Weblate (Japanese)
Currently translated at 96.8% (366 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ja/
2025-08-19 16:02:16 +02:00
Kuzma Simonov
b60fe1ad10 Translated using Weblate (Russian)
Currently translated at 81.8% (1269 of 1551 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ru/
2025-08-19 16:02:15 +02:00
gri-gri
1405b0147c Translated using Weblate (Russian)
Currently translated at 81.8% (1269 of 1551 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ru/
2025-08-19 16:02:13 +02:00
acwr47
222a7a57bc Translated using Weblate (Japanese)
Currently translated at 67.8% (1052 of 1551 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-08-19 16:02:12 +02:00
Elian Doran
cddf9f0242 Merge remote-tracking branch 'origin/main' into react/settings
; Conflicts:
;	apps/client/package.json
;	apps/client/src/translations/en/translation.json
;	apps/client/src/translations/tw/translation.json
;	pnpm-lock.yaml
2025-08-19 13:50:27 +03:00
Elian Doran
3e17ff5e7b chore(react/settings): clean up options widget 2025-08-19 13:47:15 +03:00
Elian Doran
04973094f2 feat(react/settings): port LLM settings 2025-08-19 13:46:13 +03:00
Elian Doran
018a6cb84a chore(react/settings): add back some checks for MFA 2025-08-19 10:54:02 +03:00
Elian Doran
44825af0c0 feat(react/settings): port OAuth settings 2025-08-19 10:51:05 +03:00
Elian Doran
cfb3607052 feat(react/settings): port totp settings 2025-08-19 10:37:14 +03:00
Elian Doran
c5ec928aac fix(deps): update dependency preact to v10.27.1 (#6690) 2025-08-19 08:40:36 +03:00
Elian Doran
8d0183a9fb chore(deps): update svelte monorepo (#6691) 2025-08-19 08:38:55 +03:00
Elian Doran
ecd4079871 chore(deps): update typescript-eslint monorepo to v8.40.0 (#6692) 2025-08-19 08:38:24 +03:00
Elian Doran
3ed975f2e6 fix(deps): update dependency marked to v16.2.0 (#6693) 2025-08-19 08:35:28 +03:00
renovate[bot]
c6deb537d5 fix(deps): update dependency marked to v16.2.0 2025-08-19 01:29:40 +00:00
renovate[bot]
e7b3d806a7 chore(deps): update typescript-eslint monorepo to v8.40.0 2025-08-19 01:28:54 +00:00
renovate[bot]
d1a0778b48 chore(deps): update svelte monorepo 2025-08-19 01:27:47 +00:00
renovate[bot]
378634567f fix(deps): update dependency preact to v10.27.1 2025-08-19 01:27:13 +00:00
Elian Doran
ed56ed2be0 feat(quick_search): format multi-line results better (#6672) 2025-08-18 23:14:15 +03:00
Elian Doran
648aa7e3b0 fix(hotkeys): interpret shortcut in the user's locale (#6681) 2025-08-18 23:10:59 +03:00
Elian Doran
73ff41f2b2 fix(react/settings): hook leak after closing tabs 2025-08-18 22:15:47 +03:00
Elian Doran
3837466cb3 feat(react/settings): react to external changes 2025-08-18 20:41:33 +03:00
Elian Doran
b97a5ef888 chore(react/settings): reimplement reset shortcuts 2025-08-18 19:47:40 +03:00
Elian Doran
2ff1276ebb Translations update from Hosted Weblate (#6686) 2025-08-18 19:06:42 +03:00
Elian Doran
227cf5de85 feat(react/settings): port protected session timeout 2025-08-18 19:00:42 +03:00
Elian Doran
ccf52be431 feat(react/settings): port tray options 2025-08-18 18:47:18 +03:00
Elian Doran
07713e988c feat(react/settings): port search engine settings 2025-08-18 18:43:27 +03:00
Elian Doran
f934318625 feat(react/settings): port revision snapshot list 2025-08-18 18:27:12 +03:00
Elian Doran
6fb90abd75 feat(react/settings): port network connections 2025-08-18 18:22:07 +03:00
Elian Doran
27cc33888a feat(react/settings): port share settings 2025-08-18 18:19:48 +03:00
Elian Doran
95af901808 feat(react/settings): port HTML import tags 2025-08-18 18:07:58 +03:00
Elian Doran
c5a7f84250 feat(react/settings): port note revision snapshot interval 2025-08-18 17:51:45 +03:00
Elian Doran
a71d28500d feat(react/settings): port attachment erasure timeout 2025-08-18 17:42:39 +03:00
Elian Doran
436fd16f3a feat(react/settings): port note erasure timeout 2025-08-18 17:37:20 +03:00
Székely Miklós
ca34bf42f6 Added translation using Weblate (Hungarian) 2025-08-18 15:38:27 +02:00
Székely Miklós
fbf2315f57 Added translation using Weblate (Hungarian) 2025-08-18 15:38:26 +02:00
tomek7667
72f50dcb6b Translated using Weblate (Polish)
Currently translated at 16.4% (62 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/pl/
2025-08-18 15:38:25 +02:00
tomek7667
fd4c2f79a7 Translated using Weblate (Polish)
Currently translated at 0.6% (10 of 1551 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pl/
2025-08-18 15:38:25 +02:00
acwr47
72f9335213 Translated using Weblate (Japanese)
Currently translated at 66.9% (1038 of 1551 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-08-18 15:38:25 +02:00
Elian Doran
53d97047a3 feat(react/settings): port code read-only size 2025-08-18 16:10:05 +03:00
Elian Doran
2ba3666e23 feat(react/settings): port code mime types 2025-08-18 15:55:18 +03:00
Elian Doran
4a1d379ab4 feat(react/settings): port code editor appearance 2025-08-18 15:02:58 +03:00
Elian Doran
73167e1e30 feat(react/settings): port code editor settings 2025-08-18 14:27:45 +03:00
Elian Doran
ffc13f5de3 feat(react/settings): port date time format 2025-08-18 14:19:38 +03:00
tomek7667
9ba23d49d8 Added translation using Weblate (Polish) 2025-08-18 12:32:13 +02:00
tomek7667
222a6c48a7 Added translation using Weblate (Polish) 2025-08-18 12:32:13 +02:00
VortexP
e33208e6ec Translated using Weblate (Finnish)
Currently translated at 6.1% (95 of 1551 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fi/
2025-08-18 12:32:12 +02:00
acwr47
af8781eaa7 Translated using Weblate (Japanese)
Currently translated at 65.5% (1016 of 1551 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-08-18 12:32:12 +02:00
Francis C
167b1a8d2e Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (378 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/zh_Hant/
2025-08-18 12:32:12 +02:00
Francis C
0a7aff507c Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1551 of 1551 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2025-08-18 12:32:12 +02:00
Francis C
103532aad9 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1551 of 1551 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2025-08-18 12:32:11 +02:00
Elian Doran
16939e9fd5 feat(react/settings): port auto read-only size 2025-08-18 12:14:38 +03:00
Elian Doran
4ef6169041 feat(react/settings): port highlight list settings 2025-08-18 12:11:29 +03:00
Elian Doran
9ebee42118 feat(react/settings): port TOC settings 2025-08-18 11:26:58 +03:00
Elian Doran
234d3997b1 feat(react/settings): port code block settings 2025-08-18 11:21:09 +03:00
Elian Doran
3ba0bcea4e feat(react/settings): port heading style 2025-08-18 09:47:18 +03:00
Elian Doran
701855344e feat(react/settings): port text features 2025-08-18 09:40:36 +03:00
Elian Doran
71b627fbc7 feat(react/settings): port text formatting toolbar 2025-08-18 09:34:16 +03:00
Elian Doran
5a4fc2c690 fix(deps): update dependency mind-elixir to v5.0.6 (#6682) 2025-08-18 09:07:10 +03:00
Elian Doran
0d67db52a2 chore(deps): update dependency chalk to v5.6.0 (#6683) 2025-08-18 09:06:25 +03:00
perf3ct
d971554201 feat(quick_search): also show the tags/attributes in quick search results 2025-08-18 03:20:04 +00:00
perf3ct
8fd7d7176e Merge branch 'main' into feat/quick-search-multiline-results 2025-08-18 00:30:58 +00:00
renovate[bot]
675575eed9 chore(deps): update dependency chalk to v5.6.0 2025-08-18 00:29:14 +00:00
renovate[bot]
2122cde293 fix(deps): update dependency mind-elixir to v5.0.6 2025-08-18 00:28:33 +00:00
Romain DEP.
b68a554bba fix(hotkeys): interpret shortcut in the user's locale
fixes #6547
2025-08-17 23:48:48 +02:00
Elian Doran
33043c7133 chore(call_to_action): add missing translation 2025-08-17 23:37:33 +03:00
Elian Doran
2e0f606a7a chore(release): prepare for v0.98.0 2025-08-17 22:46:16 +03:00
Elian Doran
87878dd6a7 Translations update from Hosted Weblate (#6679) 2025-08-17 22:20:39 +03:00
morteza rahvard
5296e073cc Translated using Weblate (Persian)
Currently translated at 0.7% (12 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fa/
2025-08-17 19:17:36 +00:00
acwr47
7bfb7d6f6e Translated using Weblate (Japanese)
Currently translated at 65.4% (1014 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-08-17 19:17:35 +00:00
Elian Doran
b5069cc7c2 chore(call_to_action): rephrase 2025-08-17 22:17:21 +03:00
Elian Doran
3b6791f51a chore(call_to_action): disable background effects for now 2025-08-17 21:23:22 +03:00
Elian Doran
0b0be77e02 chore(deps): update dependency @sveltejs/kit to v2.31.1 (#6676) 2025-08-17 08:23:45 +03:00
renovate[bot]
60db10559e chore(deps): update dependency @sveltejs/kit to v2.31.1 2025-08-17 02:31:17 +00:00
Elian Doran
76b066ba4a Translations update from Hosted Weblate (#6673) 2025-08-16 23:27:47 +03:00
ali mohammadi
a28db32369 Translated using Weblate (Persian)
Currently translated at 1.5% (6 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/fa/
2025-08-16 20:02:07 +00:00
ali mohammadi
2523632391 Translated using Weblate (Persian)
Currently translated at 0.1% (3 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fa/
2025-08-16 20:02:05 +00:00
neketos851
53548c356a Translated using Weblate (Ukrainian)
Currently translated at 2.1% (8 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/uk/
2025-08-16 20:02:03 +00:00
neketos851
565904ff5d Translated using Weblate (Ukrainian)
Currently translated at 3.1% (49 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/uk/
2025-08-16 20:02:01 +00:00
acwr47
e0c5545f8c Translated using Weblate (Japanese)
Currently translated at 65.2% (1012 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-08-16 20:01:59 +00:00
Aristide Bauchart
bc21285289 Translated using Weblate (French)
Currently translated at 81.7% (309 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/fr/
2025-08-16 20:01:57 +00:00
perf3ct
bbf8d757cd feat(quick_search): format multi-line results better 2025-08-16 19:16:27 +00:00
Elian Doran
318d504fad chore(deps): update dependency @types/node to v22.17.2 (#6665) 2025-08-16 08:55:02 +03:00
Elian Doran
fd5038148c Merge branch 'main' into renovate/node-22.x 2025-08-16 08:55:00 +03:00
renovate[bot]
693ca9291e chore(deps): update dependency @types/node to v22.17.2 2025-08-16 05:45:48 +00:00
Elian Doran
cfd8afc226 chore(deps): update dependency @sveltejs/kit to v2.31.0 (#6666) 2025-08-16 08:44:44 +03:00
Elian Doran
3e52ca7600 chore(deps): update dependency electron to v37.3.0 (#6667) 2025-08-16 08:44:09 +03:00
renovate[bot]
482522e802 chore(deps): update dependency electron to v37.3.0 2025-08-16 02:10:14 +00:00
renovate[bot]
8b5b6a01c6 chore(deps): update dependency @sveltejs/kit to v2.31.0 2025-08-16 02:09:37 +00:00
Elian Doran
5614891d92 fix(react/settings): unnecessary top margin 2025-08-16 00:22:18 +03:00
Elian Doran
b9b4961f3c fix(react/settings): shortcuts saved upon render 2025-08-16 00:13:18 +03:00
Elian Doran
7b83b20339 feat(react/settings): port shortcuts 2025-08-16 00:08:51 +03:00
Elian Doran
e4403dd316 Translations update from Hosted Weblate (#6664) 2025-08-15 23:04:33 +03:00
ali mohammadi
3f267fe6c9 Added translation using Weblate (Persian) 2025-08-15 21:31:45 +02:00
ali mohammadi
3229471485 Added translation using Weblate (Persian) 2025-08-15 21:31:44 +02:00
neketos851
62bac1adf9 Translated using Weblate (Ukrainian)
Currently translated at 0.7% (3 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/uk/
2025-08-15 21:31:43 +02:00
neketos851
82becfd52a Translated using Weblate (Ukrainian)
Currently translated at 0.5% (9 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/uk/
2025-08-15 21:31:43 +02:00
Elian Doran
92f035545b Translations update from Hosted Weblate (#6663) 2025-08-15 21:49:49 +03:00
neketos851
74d8ea7dcb Added translation using Weblate (Ukrainian) 2025-08-15 20:45:02 +02:00
neketos851
ac3f087279 Added translation using Weblate (Ukrainian) 2025-08-15 20:45:02 +02:00
VortexP
1cc4eb98c1 Translated using Weblate (Finnish)
Currently translated at 1.5% (6 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/fi/
2025-08-15 20:45:01 +02:00
VortexP
e99bdf8f24 Translated using Weblate (Finnish)
Currently translated at 6.0% (94 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fi/
2025-08-15 20:45:01 +02:00
acwr47
b4f521a141 Translated using Weblate (Japanese)
Currently translated at 60.8% (943 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-08-15 20:45:01 +02:00
VortexP
1e23bc09f1 Translated using Weblate (Finnish)
Currently translated at 1.3% (5 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/fi/
2025-08-15 16:07:51 +00:00
VortexP
e3ec90405d Translated using Weblate (Finnish)
Currently translated at 1.6% (25 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fi/
2025-08-15 16:07:51 +00:00
acwr47
41c87794a4 Translated using Weblate (Japanese)
Currently translated at 60.7% (942 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-08-15 16:07:50 +00:00
VortexP
e62d2d4fda Added translation using Weblate (Finnish) 2025-08-15 16:07:50 +00:00
VortexP
93adaa0f52 Added translation using Weblate (Finnish) 2025-08-15 16:07:49 +00:00
Excal
263a5d2067 Translated using Weblate (Russian)
Currently translated at 7.9% (30 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ru/
2025-08-15 16:07:48 +00:00
acwr47
f0a5005794 Translated using Weblate (Japanese)
Currently translated at 60.3% (936 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-08-15 16:07:48 +00:00
Elian Doran
577457c8ab fix(docs): Update links to Trilium repo files in advanced config docs (#6662) 2025-08-15 19:07:39 +03:00
Jon Fuller
c0c450c444 fix(docs): Update links to Trilium repo files in advanced config docs 2025-08-15 08:39:40 -07:00
Elian Doran
1e1e0b0f51 Fix (Update): No update notification in the global menu (#6657) 2025-08-15 16:56:12 +03:00
Elian Doran
a19204a1d5 Translations update from Hosted Weblate (#6661) 2025-08-15 16:55:35 +03:00
Flowerlywind
1d139bfdfe Translated using Weblate (Vietnamese)
Currently translated at 2.1% (33 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/vi/
2025-08-15 14:02:12 +02:00
Excal
75072decec Translated using Weblate (Russian)
Currently translated at 1.8% (7 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ru/
2025-08-15 14:02:09 +02:00
Francis C
0cf2ad6901 Translated using Weblate (Japanese)
Currently translated at 96.8% (366 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ja/
2025-08-15 14:02:08 +02:00
acwr47
ccbd57a0c0 Translated using Weblate (Japanese)
Currently translated at 54.5% (845 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-08-15 14:02:07 +02:00
Francis C
92e6c8c445 Translated using Weblate (Japanese)
Currently translated at 54.5% (845 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-08-15 14:02:06 +02:00
Francis C
1e966f1d59 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (378 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/zh_Hant/
2025-08-15 14:02:05 +02:00
Francis C
6872c2194e Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1550 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2025-08-15 14:02:03 +02:00
Bruno MARGUERIN
5b6a0216db Translated using Weblate (French)
Currently translated at 71.1% (269 of 378 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/fr/
2025-08-15 14:02:01 +02:00
Elian Doran
e9a7194cd6 Translated using Weblate (Romanian)
Currently translated at 100.0% (1550 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ro/
2025-08-15 14:01:59 +02:00
Bruno MARGUERIN
26898b9122 Translated using Weblate (French)
Currently translated at 82.3% (1277 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fr/
2025-08-15 14:01:58 +02:00
Elian Doran
3e00e490cf chore(react/settings): start porting protected session timeout 2025-08-15 14:23:54 +03:00
Elian Doran
c02ed17ebc feat(react/settings): port change password 2025-08-15 14:19:49 +03:00
Elian Doran
fb559d66fe feat(react/settings): port spellcheck 2025-08-15 13:52:52 +03:00
Elian Doran
25dce64c3b feat(react/settings): port backup DB list 2025-08-15 13:16:57 +03:00
Elian Doran
6f19fde76e feat(react/settings): port backup DB now 2025-08-15 12:52:59 +03:00
Elian Doran
33ae91f49c feat(backup): display full path to the database 2025-08-15 12:52:50 +03:00
Elian Doran
99c179e29a feat(react/settings): port automatic backup 2025-08-15 12:47:35 +03:00
Elian Doran
1dbcb5a027 fix(react/dialogs): unable to chain prompts 2025-08-15 12:33:23 +03:00
Elian Doran
54d613e00e fix(react/dialogs): prompt not setting default value properly 2025-08-15 12:15:29 +03:00
Elian Doran
1f8aa90482 fix(react/settings): etapi list not always reacting to changes 2025-08-15 12:11:40 +03:00
Elian Doran
c9dcbef014 feat(react/settings): port etapi tokens 2025-08-15 12:00:11 +03:00
Elian Doran
68086ec3f1 feat(react/settings): port sync test 2025-08-15 11:30:48 +03:00
Elian Doran
f62078d02b feat(react/settings): port sync options 2025-08-15 11:21:19 +03:00
SiriusXT
ab1d8594ea Fix (Update): No update notification in the global menu 2025-08-15 16:04:59 +08:00
Elian Doran
c368ec3c38 feat(react/settings): port content languages 2025-08-15 10:26:25 +03:00
Elian Doran
1a15782686 fix(deps): update dependency i18next to v25.3.6 (#6652) 2025-08-15 09:05:12 +03:00
Elian Doran
3bd0aeef77 chore(deps): update svelte monorepo (#6654) 2025-08-15 09:04:48 +03:00
Elian Doran
b463baedd2 chore(deps): update dependency tsx to v4.20.4 (#6632) 2025-08-15 09:04:22 +03:00
Elian Doran
ae77c41dab chore(deps): update tailwindcss monorepo to v4.1.12 (#6651) 2025-08-15 09:03:58 +03:00
Elian Doran
807d909acd chore(deps): update dependency @anthropic-ai/sdk to v0.60.0 (#6653) 2025-08-15 09:03:36 +03:00
Elian Doran
fa4f5f526e chore(deps): update dependency turndown to v7.2.1 (#6650) 2025-08-15 09:03:13 +03:00
renovate[bot]
edff43cdb3 chore(deps): update dependency tsx to v4.20.4 2025-08-15 05:51:01 +00:00
renovate[bot]
46fe45528c chore(deps): update svelte monorepo 2025-08-15 02:17:16 +00:00
renovate[bot]
b4b53da6a4 chore(deps): update dependency @anthropic-ai/sdk to v0.60.0 2025-08-15 02:16:43 +00:00
renovate[bot]
41fd270080 fix(deps): update dependency i18next to v25.3.6 2025-08-15 02:16:06 +00:00
renovate[bot]
410bb3cdca chore(deps): update tailwindcss monorepo to v4.1.12 2025-08-15 02:15:22 +00:00
renovate[bot]
bc6fc24fbd chore(deps): update dependency turndown to v7.2.1 2025-08-15 02:14:46 +00:00
Elian Doran
c039f06c2b chore(react/settings): fix a margin between radios 2025-08-15 00:19:37 +03:00
Elian Doran
520effbbb7 chore(react/settings): bring back style 2025-08-15 00:08:43 +03:00
Elian Doran
a42d780724 refactor(react/settings): fix type errors 2025-08-14 23:58:58 +03:00
Elian Doran
da92255dd6 refactor(react/settings): use better option mechanism 2025-08-14 23:54:32 +03:00
Elian Doran
cce3d3bce8 chore(react/settings): port date settings 2025-08-14 23:51:27 +03:00
Elian Doran
f524e99290 chore(react/settings): port first day of the week 2025-08-14 23:37:25 +03:00
Elian Doran
ba19fc7cf3 chore(react/settings): port formatting locale 2025-08-14 23:32:44 +03:00
Elian Doran
22c3de582f chore(react/settings): port language selection 2025-08-14 23:25:44 +03:00
Elian Doran
48896e67cb chore(react/settings): remove unnecessary ribbon settings 2025-08-14 23:11:13 +03:00
Elian Doran
16cd91eb02 feat(react/settings): port database anonymization 2025-08-14 23:10:53 +03:00
Elian Doran
7e03774b8e feat(react/settings): port vacuum database 2025-08-14 22:42:49 +03:00
Elian Doran
a04f6e3858 feat(react/settings): port integrity check 2025-08-14 22:40:54 +03:00
Elian Doran
96eb1be556 feat(react/settings): port advanced sync options 2025-08-14 22:35:58 +03:00
Jon Fuller
f8e20a1405 Update README.md (#6648) 2025-08-14 12:31:35 -07:00
Elian Doran
c67c3a6861 feat(react/settings): port images 2025-08-14 22:27:07 +03:00
Elian Doran
d04897e011 feat(react/settings): port related settings 2025-08-14 22:05:45 +03:00
MeIchthys
558ae1a2ea Update README.md
- Update links to point to new TriliumNext/Trilium repo
- Update a couple broken links
2025-08-14 14:59:08 -04:00
Elian Doran
64bffb82b1 feat(react/settings): port max content width 2025-08-14 21:55:16 +03:00
Elian Doran
81ac390eab feat(react/settings): port electron integration 2025-08-14 21:44:30 +03:00
Elian Doran
0db556fac2 feat(react/settings): port font size 2025-08-14 21:31:09 +03:00
Elian Doran
2793df06c4 fix(react/settings): useTriliumEvent not cleaning up properly 2025-08-14 21:05:24 +03:00
Elian Doran
e7b448e2bc fix(react/settings): event leak in useOption 2025-08-14 19:55:45 +03:00
Elian Doran
d2bc72d54f chore(react/settings): port font family settings 2025-08-14 19:25:22 +03:00
Elian Doran
83b22b4861 chore(react/settings): allow combo to have any possible object structure 2025-08-14 18:51:32 +03:00
Elian Doran
d42a949602 refactor(react/settings): use separate components inside same file 2025-08-14 18:29:08 +03:00
Elian Doran
83e1512b59 feat(react/settings): port override theme fonts 2025-08-14 18:26:40 +03:00
Elian Doran
ba6a1ec584 feat(react/settings): port theme switch 2025-08-14 18:18:45 +03:00
Elian Doran
6685e583f2 chore(react/settings): make layout switch functional 2025-08-14 17:59:17 +03:00
Elian Doran
d6032c912e chore(react/settings): improve layout 2025-08-14 17:54:52 +03:00
Elian Doran
25527ecc21 fix(react/settings): not working properly when side-by-side in split 2025-08-14 17:53:24 +03:00
Elian Doran
e0e7bd42cc feat(react/settings): react to property change 2025-08-14 17:47:45 +03:00
Elian Doran
fbc1af56ed feat(react/settings): basic hook to read Trilium option 2025-08-14 17:36:11 +03:00
Elian Doran
8ff108db9e feat(react/settings): basic rendering of React content widgets 2025-08-14 17:19:38 +03:00
355 changed files with 29224 additions and 18102 deletions

View File

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

View File

@@ -12,6 +12,7 @@ jobs:
steps:
- name: Check if PRs have conflicts
uses: eps1lon/actions-label-merge-conflict@v3
if: github.repository == ${{ vars.REPO_MAIN }}
with:
dirtyLabel: "merge-conflicts"
repoToken: "${{ secrets.MERGE_CONFLICT_LABEL_PAT }}"

View File

@@ -27,7 +27,7 @@ permissions:
jobs:
nightly-electron:
if: github.repository == 'TriliumNext/Trilium'
if: github.repository == ${{ vars.REPO_MAIN }}
name: Deploy nightly
strategy:
fail-fast: false
@@ -98,7 +98,7 @@ jobs:
path: apps/desktop/upload
nightly-server:
if: github.repository == 'TriliumNext/Trilium'
if: github.repository == ${{ vars.REPO_MAIN }}
name: Deploy server nightly
strategy:
fail-fast: false

View File

@@ -20,5 +20,10 @@
"scope": "typescript",
"prefix": "jqf",
"body": ["private $${1:name}!: JQuery<HTMLElement>;"]
},
"region": {
"scope": "css",
"prefix": "region",
"body": ["/* #region ${1:name} */\n$0\n/* #endregion */"]
}
}

View File

@@ -1,8 +1,8 @@
# Trilium Notes
![GitHub Sponsors](https://img.shields.io/github/sponsors/eliandoran) ![LiberaPay patrons](https://img.shields.io/liberapay/patrons/ElianDoran)
![Docker Pulls](https://img.shields.io/docker/pulls/triliumnext/notes)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/triliumnext/notes/total)
![Docker Pulls](https://img.shields.io/docker/pulls/triliumnext/trilium)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/triliumnext/trilium/total)
[![RelativeCI](https://badges.relative-ci.com/badges/Di5q7dz9daNDZ9UXi0Bp?branch=develop)](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) [![Translation status](https://hosted.weblate.org/widget/trilium/svg-badge.svg)](https://hosted.weblate.org/engage/trilium/)
[English](./README.md) | [Chinese (Simplified)](./docs/README-ZH_CN.md) | [Chinese (Traditional)](./docs/README-ZH_TW.md) | [Russian](./docs/README.ru.md) | [Japanese](./docs/README.ja.md) | [Italian](./docs/README.it.md) | [Spanish](./docs/README.es.md)
@@ -46,15 +46,15 @@ See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for q
- [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party themes, scripts, plugins and more.
- [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more.
## ⚠️ Why TriliumNext?
## Why TriliumNext?
[The original Trilium project is in maintenance mode](https://github.com/zadam/trilium/issues/4620).
The original Trilium developer ([Zadam](https://github.com/zadam)) has graciously given the Trilium repository to the community project which resides at https://github.com/TriliumNext
### Migrating from Trilium?
### ⬆️Migrating from Zadam/Trilium?
There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Notes instance. Simply [install TriliumNext/Notes](#-installation) as usual and it will use your existing database.
There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Trilium instance. Simply [install TriliumNext/Trilium](#-installation) as usual and it will use your existing database.
Versions up to and including [v0.90.4](https://github.com/TriliumNext/Notes/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext have their sync versions incremented.
Versions up to and including [v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext/Trilium have their sync versions incremented which prevents direct migration.
## 📖 Documentation
@@ -75,8 +75,8 @@ Feel free to join our official conversations. We would love to hear what feature
- [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous discussions.)
- The `General` Matrix room is also bridged to [XMPP](xmpp:discuss@trilium.thisgreat.party?join)
- [Github Discussions](https://github.com/TriliumNext/Notes/discussions) (For asynchronous discussions.)
- [Github Issues](https://github.com/TriliumNext/Notes/issues) (For bug reports and feature requests.)
- [Github Discussions](https://github.com/TriliumNext/Trilium/discussions) (For asynchronous discussions.)
- [Github Issues](https://github.com/TriliumNext/Trilium/issues) (For bug reports and feature requests.)
## 🏗 Installation
@@ -104,9 +104,11 @@ Currently only the latest versions of Chrome & Firefox are supported (and tested
To use TriliumNext on a mobile device, you can use a mobile web browser to access the mobile interface of a server installation (see below).
If you prefer a native Android app, you can use [TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid). Report bugs and missing features at [their repository](https://github.com/FliegendeWurst/TriliumDroid).
See issue https://github.com/TriliumNext/Trilium/issues/4962 for more information on mobile app support.
See issue https://github.com/TriliumNext/Notes/issues/72 for more information on mobile app support.
If you prefer a native Android app, you can use [TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid).
Report bugs and missing features at [their repository](https://github.com/FliegendeWurst/TriliumDroid).
Note: It is best to disable automatic updates on your server installation (see below) when using TriliumDroid since the sync version must match between Trilium and TriliumDroid.
### Server
@@ -152,11 +154,11 @@ pnpm install
pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32
```
For more details, see the [development docs](https://github.com/TriliumNext/Notes/blob/develop/docs/Developer%20Guide/Developer%20Guide/Building%20and%20deployment/Running%20a%20development%20build.md).
For more details, see the [development docs](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide).
### Developer Documentation
Please view the [documentation guide](./docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md) for details. If you have more questions, feel free to reach out via the links described in the "Discuss with us" section above.
Please view the [documentation guide](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md) for details. If you have more questions, feel free to reach out via the links described in the "Discuss with us" section above.
## 👏 Shoutouts
@@ -168,7 +170,7 @@ Please view the [documentation guide](./docs/Developer%20Guide/Developer%20Guide
## 🤝 Support
Support for the TriliumNext organization will be possible in the near future. For now, you can:
- Support continued development on TriliumNext by supporting our developers: [eliandoran](https://github.com/sponsors/eliandoran) (See the [repository insights]([developers]([url](https://github.com/TriliumNext/Notes/graphs/contributors))) for a full list)
- Support continued development on TriliumNext by supporting our developers: [eliandoran](https://github.com/sponsors/eliandoran) (See the [repository insights]([developers]([url](https://github.com/TriliumNext/trilium/graphs/contributors))) for a full list)
- Show a token of gratitude to the original Trilium developer ([zadam](https://github.com/sponsors/zadam)) via [PayPal](https://paypal.me/za4am) or Bitcoin (bitcoin:bc1qv3svjn40v89mnkre5vyvs2xw6y8phaltl385d2).

View File

@@ -35,13 +35,13 @@
"chore:generate-openapi": "tsx bin/generate-openapi.js"
},
"devDependencies": {
"@playwright/test": "1.54.2",
"@playwright/test": "1.55.0",
"@stylistic/eslint-plugin": "5.2.3",
"@types/express": "5.0.3",
"@types/node": "22.17.1",
"@types/node": "22.18.0",
"@types/yargs": "17.0.33",
"@vitest/coverage-v8": "3.2.4",
"eslint": "9.33.0",
"eslint": "9.34.0",
"eslint-plugin-simple-import-sort": "12.1.1",
"esm": "3.2.25",
"jsdoc": "4.0.4",
@@ -49,7 +49,7 @@
"rcedit": "4.0.1",
"rimraf": "6.0.1",
"tslib": "2.8.1",
"typedoc": "0.28.10",
"typedoc": "0.28.11",
"typedoc-plugin-missing-exports": "4.1.0"
},
"optionalDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/client",
"version": "0.97.2",
"version": "0.98.1",
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
"private": true,
"license": "AGPL-3.0-only",
@@ -10,7 +10,7 @@
"url": "https://github.com/TriliumNext/Notes"
},
"dependencies": {
"@eslint/js": "9.33.0",
"@eslint/js": "9.34.0",
"@excalidraw/excalidraw": "0.18.0",
"@fullcalendar/core": "6.1.19",
"@fullcalendar/daygrid": "6.1.19",
@@ -19,7 +19,7 @@
"@fullcalendar/multimonth": "6.1.19",
"@fullcalendar/timegrid": "6.1.19",
"@maplibre/maplibre-gl-leaflet": "0.1.3",
"@mermaid-js/layout-elk": "0.1.8",
"@mermaid-js/layout-elk": "0.1.9",
"@mind-elixir/node-menu": "5.0.0",
"@popperjs/core": "2.11.8",
"@triliumnext/ckeditor5": "workspace:*",
@@ -28,7 +28,7 @@
"@triliumnext/highlightjs": "workspace:*",
"@triliumnext/share-theme": "workspace:*",
"autocomplete.js": "0.38.1",
"bootstrap": "5.3.7",
"bootstrap": "5.3.8",
"boxicons": "2.1.4",
"dayjs": "1.11.13",
"dayjs-plugin-utc": "0.1.2",
@@ -36,7 +36,7 @@
"draggabilly": "3.0.0",
"force-graph": "1.50.1",
"globals": "16.3.0",
"i18next": "25.3.4",
"i18next": "25.4.2",
"i18next-http-backend": "3.0.2",
"jquery": "3.7.1",
"jquery.fancytree": "2.38.5",
@@ -46,12 +46,13 @@
"leaflet": "1.9.4",
"leaflet-gpx": "2.2.0",
"mark.js": "8.11.1",
"marked": "16.1.2",
"mermaid": "11.9.0",
"mind-elixir": "5.0.5",
"marked": "16.2.0",
"mermaid": "11.10.1",
"mind-elixir": "5.0.6",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.27.0",
"preact": "10.27.1",
"react-i18next": "15.7.2",
"split.js": "1.6.5",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",
@@ -61,7 +62,7 @@
"@ckeditor/ckeditor5-inspector": "5.0.0",
"@preact/preset-vite": "2.10.2",
"@types/bootstrap": "5.2.10",
"@types/jquery": "3.5.32",
"@types/jquery": "3.5.33",
"@types/leaflet": "1.9.20",
"@types/leaflet-gpx": "1.3.7",
"@types/mark.js": "8.11.12",
@@ -69,7 +70,7 @@
"copy-webpack-plugin": "13.0.1",
"happy-dom": "18.0.1",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "3.1.1"
"vite-plugin-static-copy": "3.1.2"
},
"nx": {
"name": "client",

View File

@@ -31,16 +31,13 @@ import { StartupChecks } from "./startup_checks.js";
import type { CreateNoteOpts } from "../services/note_create.js";
import { ColumnComponent } from "tabulator-tables";
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
import type RootContainer from "../widgets/containers/root_container.js";
interface Layout {
getRootWidget: (appContext: AppContext) => RootWidget;
getRootWidget: (appContext: AppContext) => RootContainer;
}
interface RootWidget extends Component {
render: () => JQuery<HTMLElement>;
}
interface BeforeUploadListener extends Component {
export interface BeforeUploadListener extends Component {
beforeUnloadEvent(): boolean;
}
@@ -85,7 +82,6 @@ export type CommandMappings = {
focusTree: CommandData;
focusOnTitle: CommandData;
focusOnDetail: CommandData;
focusOnSearchDefinition: Required<CommandData>;
searchNotes: CommandData & {
searchString?: string;
ancestorNoteId?: string | null;
@@ -323,6 +319,7 @@ export type CommandMappings = {
printActiveNote: CommandData;
exportAsPdf: CommandData;
openNoteExternally: CommandData;
openNoteCustom: CommandData;
renderActiveNote: CommandData;
unhoist: CommandData;
reloadFrontendApp: CommandData;
@@ -526,7 +523,7 @@ export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMapp
export class AppContext extends Component {
isMainWindow: boolean;
components: Component[];
beforeUnloadListeners: WeakRef<BeforeUploadListener>[];
beforeUnloadListeners: (WeakRef<BeforeUploadListener> | (() => boolean))[];
tabManager!: TabManager;
layout?: Layout;
noteTreeWidget?: NoteTreeWidget;
@@ -619,7 +616,7 @@ export class AppContext extends Component {
component.triggerCommand(commandName, { $el: $(this) });
});
this.child(rootWidget);
this.child(rootWidget as Component);
this.triggerEvent("initialRenderComplete", {});
}
@@ -649,13 +646,17 @@ export class AppContext extends Component {
return $(el).closest(".component").prop("component");
}
addBeforeUnloadListener(obj: BeforeUploadListener) {
addBeforeUnloadListener(obj: BeforeUploadListener | (() => boolean)) {
if (typeof WeakRef !== "function") {
// older browsers don't support WeakRef
return;
}
this.beforeUnloadListeners.push(new WeakRef<BeforeUploadListener>(obj));
if (typeof obj === "object") {
this.beforeUnloadListeners.push(new WeakRef<BeforeUploadListener>(obj));
} else {
this.beforeUnloadListeners.push(obj);
}
}
}
@@ -665,25 +666,29 @@ const appContext = new AppContext(window.glob.isMainWindow);
$(window).on("beforeunload", () => {
let allSaved = true;
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => !!wr.deref());
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => typeof wr === "function" || !!wr.deref());
for (const weakRef of appContext.beforeUnloadListeners) {
const component = weakRef.deref();
for (const listener of appContext.beforeUnloadListeners) {
if (typeof listener === "object") {
const component = listener.deref();
if (!component) {
continue;
}
if (!component) {
continue;
}
if (!component.beforeUnloadEvent()) {
console.log(`Component ${component.componentId} is not finished saving its state.`);
toast.showMessage(t("app_context.please_wait_for_save"), 10000);
allSaved = false;
if (!component.beforeUnloadEvent()) {
console.log(`Component ${component.componentId} is not finished saving its state.`);
allSaved = false;
}
} else {
if (!listener()) {
allSaved = false;
}
}
}
if (!allSaved) {
toast.showMessage(t("app_context.please_wait_for_save"), 10000);
return "some string";
}
});

View File

@@ -1,6 +1,8 @@
import utils from "../services/utils.js";
import type { CommandMappings, CommandNames, EventData, EventNames } from "./app_context.js";
type EventHandler = ((data: any) => void);
/**
* Abstract class for all components in the Trilium's frontend.
*
@@ -19,6 +21,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
initialized: Promise<void> | null;
parent?: TypedComponent<any>;
_position!: number;
private listeners: Record<string, EventHandler[]> | null = {};
constructor() {
this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`;
@@ -76,6 +79,14 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
const promises: Promise<unknown>[] = [];
// Handle React children.
if (this.listeners?.[name]) {
for (const listener of this.listeners[name]) {
listener(data);
}
}
// Handle legacy children.
for (const child of this.children) {
const ret = child.handleEvent(name, data) as Promise<void>;
@@ -120,6 +131,35 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
return promise;
}
registerHandler<T extends EventNames>(name: T, handler: EventHandler) {
if (!this.listeners) {
this.listeners = {};
}
if (!this.listeners[name]) {
this.listeners[name] = [];
}
if (this.listeners[name].includes(handler)) {
return;
}
this.listeners[name].push(handler);
}
removeHandler<T extends EventNames>(name: T, handler: EventHandler) {
if (!this.listeners?.[name]?.includes(handler)) {
return;
}
this.listeners[name] = this.listeners[name]
.filter(listener => listener !== handler);
if (!this.listeners[name].length) {
delete this.listeners[name];
}
}
}
export default class Component extends TypedComponent<Component> {}

View File

@@ -43,8 +43,6 @@ export default class RootCommandExecutor extends Component {
const noteContext = await appContext.tabManager.openTabWithNoteWithHoisting(searchNote.noteId, {
activate: true
});
appContext.triggerCommand("focusOnSearchDefinition", { ntxId: noteContext.ntxId });
}
async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) {

View File

@@ -8,7 +8,6 @@ import electronContextMenu from "./menus/electron_context_menu.js";
import glob from "./services/glob.js";
import { t } from "./services/i18n.js";
import options from "./services/options.js";
import server from "./services/server.js";
import type ElectronRemote from "@electron/remote";
import type Electron from "electron";
import "./stylesheets/bootstrap.scss";

View File

@@ -1020,6 +1020,14 @@ class FNote {
return this.noteId.startsWith("_options");
}
isTriliumSqlite() {
return this.mime === "text/x-sqlite;schema=trilium";
}
isTriliumScript() {
return this.mime.startsWith("application/javascript");
}
/**
* Provides note's date metadata.
*/

View File

@@ -4,21 +4,13 @@ import TabRowWidget from "../widgets/tab_row.js";
import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js";
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteTitleWidget from "../widgets/note_title.js";
import OwnedAttributeListWidget from "../widgets/ribbon_widgets/owned_attribute_list.js";
import NoteActionsWidget from "../widgets/buttons/note_actions.js";
import NoteTitleWidget from "../widgets/note_title.jsx";
import NoteDetailWidget from "../widgets/note_detail.js";
import RibbonContainer from "../widgets/containers/ribbon_container.js";
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
import InheritedAttributesWidget from "../widgets/ribbon_widgets/inherited_attribute_list.js";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import NoteListWidget from "../widgets/note_list.js";
import SearchDefinitionWidget from "../widgets/ribbon_widgets/search_definition.js";
import SqlResultWidget from "../widgets/sql_result.js";
import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js";
import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js";
import ImagePropertiesWidget from "../widgets/ribbon_widgets/image_properties.js";
import NotePropertiesWidget from "../widgets/ribbon_widgets/note_properties.js";
import NoteIconWidget from "../widgets/note_icon.js";
import NoteIconWidget from "../widgets/note_icon.jsx";
import SearchResultWidget from "../widgets/search_result.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import RootContainer from "../widgets/containers/root_container.js";
@@ -29,15 +21,8 @@ import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import LeftPaneToggleWidget from "../widgets/buttons/left_pane_toggle.js";
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
import BasicPropertiesWidget from "../widgets/ribbon_widgets/basic_properties.js";
import NoteInfoWidget from "../widgets/ribbon_widgets/note_info_widget.js";
import BookPropertiesWidget from "../widgets/ribbon_widgets/book_properties.js";
import NoteMapRibbonWidget from "../widgets/ribbon_widgets/note_map.js";
import NotePathsWidget from "../widgets/ribbon_widgets/note_paths.js";
import SimilarNotesWidget from "../widgets/ribbon_widgets/similar_notes.js";
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
import EditButton from "../widgets/floating_buttons/edit_button.js";
import EditedNotesWidget from "../widgets/ribbon_widgets/edited_notes.js";
import ShowTocWidgetButton from "../widgets/buttons/show_toc_widget_button.js";
import ShowHighlightsListWidgetButton from "../widgets/buttons/show_highlights_list_widget_button.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
@@ -51,16 +36,13 @@ import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js";
import LauncherContainer from "../widgets/containers/launcher_container.js";
import RevisionsButton from "../widgets/buttons/revisions_button.js";
import CodeButtonsWidget from "../widgets/floating_buttons/code_buttons.js";
import ApiLogWidget from "../widgets/api_log.js";
import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js";
import ScriptExecutorWidget from "../widgets/ribbon_widgets/script_executor.js";
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js";
import ScrollPaddingWidget from "../widgets/scroll_padding.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
import options from "../services/options.js";
import utils from "../services/utils.js";
import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js";
@@ -73,6 +55,7 @@ import ToggleReadOnlyButton from "../widgets/floating_buttons/toggle_read_only_b
import PngExportButton from "../widgets/floating_buttons/png_export_button.js";
import RefreshButton from "../widgets/floating_buttons/refresh_button.js";
import { applyModals } from "./layout_commons.js";
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
export default class DesktopLayout {
@@ -151,37 +134,15 @@ export default class DesktopLayout {
.css("min-height", "50px")
.css("align-items", "center")
.cssBlock(".title-row > * { margin: 5px; }")
.child(new NoteIconWidget())
.child(new NoteTitleWidget())
.child(<NoteIconWidget />)
.child(<NoteTitleWidget />)
.child(new SpacerWidget(0, 1))
.child(new MovePaneButton(true))
.child(new MovePaneButton(false))
.child(new ClosePaneButton())
.child(new CreatePaneButton())
)
.child(
new RibbonContainer()
// the order of the widgets matter. Some of these want to "activate" themselves
// when visible. When this happens to multiple of them, the first one "wins".
// promoted attributes should always win.
.ribbon(new ClassicEditorToolbar())
.ribbon(new ScriptExecutorWidget())
.ribbon(new SearchDefinitionWidget())
.ribbon(new EditedNotesWidget())
.ribbon(new BookPropertiesWidget())
.ribbon(new NotePropertiesWidget())
.ribbon(new FilePropertiesWidget())
.ribbon(new ImagePropertiesWidget())
.ribbon(new BasicPropertiesWidget())
.ribbon(new OwnedAttributeListWidget())
.ribbon(new InheritedAttributesWidget())
.ribbon(new NotePathsWidget())
.ribbon(new NoteMapRibbonWidget())
.ribbon(new SimilarNotesWidget())
.ribbon(new NoteInfoWidget())
.button(new RevisionsButton())
.button(new NoteActionsWidget())
)
.child(<Ribbon />)
.child(new SharedInfoWidget())
.child(new WatchedFileUpdateStatusWidget())
.child(
@@ -235,8 +196,8 @@ export default class DesktopLayout {
.child(new CloseZenButton())
// Desktop-specific dialogs.
.child(new PasswordNoteSetDialog())
.child(new UploadAttachmentsDialog());
.child(<PasswordNoteSetDialog />)
.child(<UploadAttachmentsDialog />);
applyModals(rootContainer);
return rootContainer;

View File

@@ -24,48 +24,48 @@ import InfoDialog from "../widgets/dialogs/info.js";
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
import PopupEditorDialog from "../widgets/dialogs/popup_editor.js";
import FlexContainer from "../widgets/containers/flex_container.js";
import NoteIconWidget from "../widgets/note_icon.js";
import NoteTitleWidget from "../widgets/note_title.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
import NoteIconWidget from "../widgets/note_icon";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import NoteDetailWidget from "../widgets/note_detail.js";
import NoteListWidget from "../widgets/note_list.js";
import { CallToActionDialog } from "../widgets/dialogs/call_to_action.jsx";
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
import NoteTitleWidget from "../widgets/note_title.jsx";
import { PopupEditorFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.js";
export function applyModals(rootContainer: RootContainer) {
rootContainer
.child(new BulkActionsDialog())
.child(new AboutDialog())
.child(new HelpDialog())
.child(new RecentChangesDialog())
.child(new BranchPrefixDialog())
.child(new SortChildNotesDialog())
.child(new IncludeNoteDialog())
.child(new NoteTypeChooserDialog())
.child(new JumpToNoteDialog())
.child(new AddLinkDialog())
.child(new CloneToDialog())
.child(new MoveToDialog())
.child(new ImportDialog())
.child(new ExportDialog())
.child(new MarkdownImportDialog())
.child(new ProtectedSessionPasswordDialog())
.child(new RevisionsDialog())
.child(new DeleteNotesDialog())
.child(new InfoDialog())
.child(new ConfirmDialog())
.child(new PromptDialog())
.child(new IncorrectCpuArchDialog())
.child(<BulkActionsDialog />)
.child(<AboutDialog />)
.child(<HelpDialog />)
.child(<RecentChangesDialog />)
.child(<BranchPrefixDialog />)
.child(<SortChildNotesDialog />)
.child(<IncludeNoteDialog />)
.child(<NoteTypeChooserDialog />)
.child(<JumpToNoteDialog />)
.child(<AddLinkDialog />)
.child(<CloneToDialog />)
.child(<MoveToDialog />)
.child(<ImportDialog />)
.child(<ExportDialog />)
.child(<MarkdownImportDialog />)
.child(<ProtectedSessionPasswordDialog />)
.child(<RevisionsDialog />)
.child(<DeleteNotesDialog />)
.child(<InfoDialog />)
.child(<ConfirmDialog />)
.child(<PromptDialog />)
.child(<IncorrectCpuArchDialog />)
.child(new PopupEditorDialog()
.child(new FlexContainer("row")
.class("title-row")
.css("align-items", "center")
.cssBlock(".title-row > * { margin: 5px; }")
.child(new NoteIconWidget())
.child(new NoteTitleWidget()))
.child(new ClassicEditorToolbar())
.child(<NoteIconWidget />)
.child(<NoteTitleWidget />))
.child(<PopupEditorFormattingToolbar />)
.child(new PromotedAttributesWidget())
.child(new NoteDetailWidget())
.child(new NoteListWidget(true)))
.child(new CallToActionDialog());
.child(<CallToActionDialog />);
}

View File

@@ -7,7 +7,6 @@ import ToggleSidebarButtonWidget from "../widgets/mobile_widgets/toggle_sidebar_
import MobileDetailMenuWidget from "../widgets/mobile_widgets/mobile_detail_menu.js";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js";
import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
import EditButton from "../widgets/floating_buttons/edit_button.js";
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
@@ -19,14 +18,18 @@ import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
import LauncherContainer from "../widgets/containers/launcher_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import SharedInfoWidget from "../widgets/shared_info.js";
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
import type AppContext from "../components/app_context.js";
import TabRowWidget from "../widgets/tab_row.js";
import RefreshButton from "../widgets/floating_buttons/refresh_button.js";
import MobileEditorToolbar from "../widgets/ribbon_widgets/mobile_editor_toolbar.js";
import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js";
import { applyModals } from "./layout_commons.js";
import CloseZenButton from "../widgets/close_zen_button.js";
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
import { useNoteContext } from "../widgets/react/hooks.jsx";
import { useContext } from "preact/hooks";
import { ParentComponent } from "../widgets/react/react_utils.jsx";
const MOBILE_CSS = `
<style>
@@ -144,7 +147,7 @@ export default class MobileLayout {
.css("font-size", "larger")
.css("align-items", "center")
.child(new ToggleSidebarButtonWidget().contentSized())
.child(new NoteTitleWidget().contentSized().css("position", "relative").css("padding-left", "0.5em"))
.child(<NoteTitleWidget />)
.child(new MobileDetailMenuWidget(true).contentSized())
)
.child(new SharedInfoWidget())
@@ -164,7 +167,7 @@ export default class MobileLayout {
.contentSized()
.child(new NoteDetailWidget())
.child(new NoteListWidget(false))
.child(new FilePropertiesWidget().css("font-size", "smaller"))
.child(<FilePropertiesWrapper />)
)
.child(new MobileEditorToolbar())
)
@@ -181,3 +184,13 @@ export default class MobileLayout {
return rootContainer;
}
}
function FilePropertiesWrapper() {
const { note } = useNoteContext();
return (
<div>
{note?.type === "file" && <FilePropertiesTab note={note} />}
</div>
);
}

View File

@@ -2,6 +2,7 @@ import server from "./server.js";
import froca from "./froca.js";
import type FNote from "../entities/fnote.js";
import type { AttributeRow } from "./load_results.js";
import { AttributeType } from "@triliumnext/commons";
async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
await server.put(`notes/${noteId}/attribute`, {
@@ -25,6 +26,14 @@ async function removeAttributeById(noteId: string, attributeId: string) {
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
}
export async function removeOwnedAttributesByNameOrType(note: FNote, type: AttributeType, name: string) {
for (const attr of note.getOwnedAttributes()) {
if (attr.type === type && attr.name === name) {
await server.remove(`notes/${note.noteId}/attributes/${attr.attributeId}`);
}
}
}
/**
* Removes a label identified by its name from the given note, if it exists. Note that the label must be owned, i.e.
* it will not remove inherited attributes.
@@ -52,7 +61,7 @@ function removeOwnedLabelByName(note: FNote, labelName: string) {
* @param value the value of the attribute to set.
*/
export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
if (value) {
if (value !== null && value !== undefined) {
// Create or update the attribute.
await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value });
} else {

View File

@@ -18,7 +18,7 @@ import type FNote from "../entities/fnote.js";
import toast from "./toast.js";
import { BulkAction } from "@triliumnext/commons";
const ACTION_GROUPS = [
export const ACTION_GROUPS = [
{
title: t("bulk_actions.labels"),
actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction]

View File

@@ -35,8 +35,10 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
loadResults.addOption(attributeEntity.name);
} else if (ec.entityName === "attachments") {
processAttachment(loadResults, ec);
} else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens") {
} else if (ec.entityName === "blobs") {
// NOOP - these entities are handled at the backend level and don't require frontend processing
} else if (ec.entityName === "etapi_tokens") {
loadResults.hasEtapiTokenChanges = true;
} else {
throw new Error(`Unknown entityName '${ec.entityName}'`);
}
@@ -77,9 +79,7 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
noteAttributeCache.invalidate();
}
// TODO: Remove after porting the file
// @ts-ignore
const appContext = (await import("../components/app_context.js")).default as any;
const appContext = (await import("../components/app_context.js")).default;
await appContext.triggerEvent("entitiesReloaded", { loadResults });
}
}

View File

@@ -3,6 +3,7 @@ import i18next from "i18next";
import i18nextHttpBackend from "i18next-http-backend";
import server from "./server.js";
import type { Locale } from "@triliumnext/commons";
import { initReactI18next } from "react-i18next";
let locales: Locale[] | null;
@@ -16,6 +17,7 @@ export async function initLocale() {
locales = await server.get<Locale[]>("options/locales");
i18next.use(initReactI18next);
await i18next.use(i18nextHttpBackend).init({
lng: locale,
fallbackLng: "en",

View File

@@ -1,4 +1,4 @@
import type { AttachmentRow } from "@triliumnext/commons";
import type { AttachmentRow, EtapiTokenRow } from "@triliumnext/commons";
import type { AttributeType } from "../entities/fattribute.js";
import type { EntityChange } from "../server_types.js";
@@ -53,6 +53,7 @@ type EntityRowMappings = {
options: OptionRow;
revisions: RevisionRow;
note_reordering: NoteReorderingRow;
etapi_tokens: EtapiTokenRow;
};
export type EntityRowNames = keyof EntityRowMappings;
@@ -68,6 +69,7 @@ export default class LoadResults {
private contentNoteIdToComponentId: ContentNoteIdToComponentIdRow[];
private optionNames: string[];
private attachmentRows: AttachmentRow[];
public hasEtapiTokenChanges: boolean = false;
constructor(entityChanges: EntityChange[]) {
const entities: Record<string, Record<string, any>> = {};
@@ -215,7 +217,8 @@ export default class LoadResults {
this.revisionRows.length === 0 &&
this.contentNoteIdToComponentId.length === 0 &&
this.optionNames.length === 0 &&
this.attachmentRows.length === 0
this.attachmentRows.length === 0 &&
!this.hasEtapiTokenChanges
);
}

View File

@@ -36,6 +36,8 @@ export interface Suggestion {
commandId?: string;
commandDescription?: string;
commandShortcut?: string;
attributeSnippet?: string;
highlightedAttributeSnippet?: string;
}
export interface Options {
@@ -323,7 +325,33 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
html += '</div>';
return html;
}
return `<span class="${suggestion.icon ?? "bx bx-note"}"></span> ${suggestion.highlightedNotePathTitle}`;
// Add special class for search-notes action
const actionClass = suggestion.action === "search-notes" ? "search-notes-action" : "";
// Choose appropriate icon based on action
let iconClass = suggestion.icon ?? "bx bx-note";
if (suggestion.action === "search-notes") {
iconClass = "bx bx-search";
} else if (suggestion.action === "create-note") {
iconClass = "bx bx-plus";
} else if (suggestion.action === "external-link") {
iconClass = "bx bx-link-external";
}
// Simplified HTML structure without nested divs
let html = `<div class="note-suggestion ${actionClass}">`;
html += `<span class="icon ${iconClass}"></span>`;
html += `<span class="text">`;
html += `<span class="search-result-title">${suggestion.highlightedNotePathTitle}</span>`;
// Add attribute snippet inline if available
if (suggestion.highlightedAttributeSnippet) {
html += `<span class="search-result-attributes">${suggestion.highlightedAttributeSnippet}</span>`;
}
html += `</span>`;
html += `</div>`;
return html;
}
},
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added

View File

@@ -35,7 +35,7 @@ function download(url: string) {
}
}
function downloadFileNote(noteId: string) {
export function downloadFileNote(noteId: string) {
const url = `${getFileUrl("notes", noteId)}?${Date.now()}`; // don't use cache
download(url);
@@ -163,7 +163,7 @@ async function openExternally(type: string, entityId: string, mime: string) {
}
}
const openNoteExternally = async (noteId: string, mime: string) => await openExternally("notes", noteId, mime);
export const openNoteExternally = async (noteId: string, mime: string) => await openExternally("notes", noteId, mime);
const openAttachmentExternally = async (attachmentId: string, mime: string) => await openExternally("attachments", attachmentId, mime);
function getHost() {

View File

@@ -1,7 +1,8 @@
import { OptionNames } from "@triliumnext/commons";
import server from "./server.js";
import { isShare } from "./utils.js";
type OptionValue = number | string;
export type OptionValue = number | string;
class Options {
initializedPromise: Promise<void>;
@@ -76,6 +77,14 @@ class Options {
await server.put(`options`, payload);
}
/**
* Saves multiple options at once, by supplying a record where the keys are the option names and the values represent the stringified value to set.
* @param newValues the record of keys and values.
*/
async saveMany<T extends OptionNames>(newValues: Record<T, OptionValue>) {
await server.put<void>("options", newValues);
}
async toggle(key: string) {
await this.save(key, (!this.is(key)).toString());
}

View File

@@ -14,6 +14,32 @@ interface ShortcutBinding {
// Store all active shortcut bindings for management
const activeBindings: Map<string, ShortcutBinding[]> = new Map();
// Handle special key mappings and aliases
const keyMap: { [key: string]: string[] } = {
'return': ['Enter'],
'enter': ['Enter'], // alias for return
'del': ['Delete'],
'delete': ['Delete'], // alias for del
'esc': ['Escape'],
'escape': ['Escape'], // alias for esc
'space': [' ', 'Space'],
'tab': ['Tab'],
'backspace': ['Backspace'],
'home': ['Home'],
'end': ['End'],
'pageup': ['PageUp'],
'pagedown': ['PageDown'],
'up': ['ArrowUp'],
'down': ['ArrowDown'],
'left': ['ArrowLeft'],
'right': ['ArrowRight']
};
// Function keys
for (let i = 1; i <= 19; i++) {
keyMap[`f${i}`] = [`F${i}`];
}
function removeGlobalShortcut(namespace: string) {
bindGlobalShortcut("", null, namespace);
}
@@ -124,32 +150,6 @@ export function keyMatches(e: KeyboardEvent, key: string): boolean {
return false;
}
// Handle special key mappings and aliases
const keyMap: { [key: string]: string[] } = {
'return': ['Enter'],
'enter': ['Enter'], // alias for return
'del': ['Delete'],
'delete': ['Delete'], // alias for del
'esc': ['Escape'],
'escape': ['Escape'], // alias for esc
'space': [' ', 'Space'],
'tab': ['Tab'],
'backspace': ['Backspace'],
'home': ['Home'],
'end': ['End'],
'pageup': ['PageUp'],
'pagedown': ['PageDown'],
'up': ['ArrowUp'],
'down': ['ArrowDown'],
'left': ['ArrowLeft'],
'right': ['ArrowRight']
};
// Function keys
for (let i = 1; i <= 19; i++) {
keyMap[`f${i}`] = [`F${i}`];
}
const mappedKeys = keyMap[key.toLowerCase()];
if (mappedKeys) {
return mappedKeys.includes(e.key) || mappedKeys.includes(e.code);
@@ -163,7 +163,7 @@ export function keyMatches(e: KeyboardEvent, key: string): boolean {
// For letter keys, use the physical key code for consistency
if (key.length === 1 && key >= 'a' && key <= 'z') {
return e.code === `Key${key.toUpperCase()}`;
return e.key.toLowerCase() === key.toLowerCase();
}
// For regular keys, check both key and code as fallback

View File

@@ -5,7 +5,7 @@ const SVG_MIME = "image/svg+xml";
export const isShare = !window.glob;
function reloadFrontendApp(reason?: string) {
export function reloadFrontendApp(reason?: string) {
if (reason) {
logInfo(`Frontend app reload: ${reason}`);
}
@@ -13,7 +13,7 @@ function reloadFrontendApp(reason?: string) {
window.location.reload();
}
function restartDesktopApp() {
export function restartDesktopApp() {
if (!isElectron()) {
reloadFrontendApp();
return;
@@ -125,7 +125,7 @@ function formatDateISO(date: Date) {
return `${date.getFullYear()}-${padNum(date.getMonth() + 1)}-${padNum(date.getDate())}`;
}
function formatDateTime(date: Date, userSuppliedFormat?: string): string {
export function formatDateTime(date: Date, userSuppliedFormat?: string): string {
if (userSuppliedFormat?.trim()) {
return dayjs(date).format(userSuppliedFormat);
} else {
@@ -144,11 +144,11 @@ function now() {
/**
* Returns `true` if the client is currently running under Electron, or `false` if running in a web browser.
*/
function isElectron() {
export function isElectron() {
return !!(window && window.process && window.process.type);
}
function isMac() {
export function isMac() {
return navigator.platform.indexOf("Mac") > -1;
}
@@ -185,7 +185,11 @@ export function escapeQuotes(value: string) {
return value.replaceAll('"', "&quot;");
}
function formatSize(size: number) {
export function formatSize(size: number | null | undefined) {
if (size === null || size === undefined) {
return "";
}
size = Math.max(Math.round(size / 1024), 1);
if (size < 1024) {
@@ -218,7 +222,7 @@ function randomString(len: number) {
return text;
}
function isMobile() {
export function isMobile() {
return (
window.glob?.device === "mobile" ||
// window.glob.device is not available in setup
@@ -292,7 +296,7 @@ function isHtmlEmpty(html: string) {
);
}
async function clearBrowserCache() {
export async function clearBrowserCache() {
if (isElectron()) {
const win = dynamicRequire("@electron/remote").getCurrentWindow();
await win.webContents.session.clearCache();
@@ -306,7 +310,7 @@ function copySelectionToClipboard() {
}
}
function dynamicRequire(moduleName: string) {
export function dynamicRequire(moduleName: string) {
if (typeof __non_webpack_require__ !== "undefined") {
return __non_webpack_require__(moduleName);
} else {
@@ -374,33 +378,42 @@ async function openInAppHelp($button: JQuery<HTMLElement>) {
const inAppHelpPage = $button.attr("data-in-app-help");
if (inAppHelpPage) {
// Dynamic import to avoid import issues in tests.
const appContext = (await import("../components/app_context.js")).default;
const activeContext = appContext.tabManager.getActiveContext();
if (!activeContext) {
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 { ntxId } = subContexts[subContexts.length - 1];
appContext.triggerCommand("openNewNoteSplit", {
ntxId,
notePath: targetNote,
hoistedNoteId: "_help",
viewScope
})
} else {
// There is already a help window open, make sure it opens on the right note.
helpSubcontext.setNote(targetNote, { viewScope });
}
openInAppHelpFromUrl(inAppHelpPage);
}
}
/**
* Opens the in-app help at the given page in a split note. If there already is a split note open with a help page, it will be replaced by this one.
*
* @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) {
// Dynamic import to avoid import issues in tests.
const appContext = (await import("../components/app_context.js")).default;
const activeContext = appContext.tabManager.getActiveContext();
if (!activeContext) {
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 { ntxId } = subContexts[subContexts.length - 1];
appContext.triggerCommand("openNewNoteSplit", {
ntxId,
notePath: targetNote,
hoistedNoteId: "_help",
viewScope
})
} else {
// There is already a help window open, make sure it opens on the right note.
helpSubcontext.setNote(targetNote, { viewScope });
}
}
function initHelpButtons($el: JQuery<HTMLElement> | JQuery<Window>) {
@@ -731,10 +744,86 @@ function isUpdateAvailable(latestVersion: string | null | undefined, currentVers
return compareVersions(latestVersion, currentVersion) > 0;
}
function isLaunchBarConfig(noteId: string) {
export function isLaunchBarConfig(noteId: string) {
return ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers", "_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(noteId);
}
/**
* Adds a class to the <body> of the page, where the class name is formed via a prefix and a value.
* Useful for configurable options such as `heading-style-markdown`, where `heading-style` is the prefix and `markdown` is the dynamic value.
* There is no separator between the prefix and the value, if needed it has to be supplied manually to the prefix.
*
* @param prefix the prefix.
* @param value the value to be appended to the prefix.
*/
export function toggleBodyClass(prefix: string, value: string) {
const $body = $("body");
for (const clazz of Array.from($body[0].classList)) {
// create copy to safely iterate over while removing classes
if (clazz.startsWith(prefix)) {
$body.removeClass(clazz);
}
}
$body.addClass(prefix + value);
}
/**
* Basic comparison for equality between the two arrays. The values are strictly checked via `===`.
*
* @param a the first array to compare.
* @param b the second array to compare.
* @returns `true` if both arrays are equals, `false` otherwise.
*/
export function arrayEqual<T>(a: T[], b: T[]) {
if (a === b) {
return true;
}
if (a.length !== b.length) {
return false;
}
for (let i=0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
type Indexed<T extends object> = T & { index: number };
/**
* Given an object array, alters every object in the array to have an index field assigned to it.
*
* @param items the objects to be numbered.
* @returns the same object for convenience, with the type changed to indicate the new index field.
*/
export function numberObjectsInPlace<T extends object>(items: T[]): Indexed<T>[] {
let index = 0;
for (const item of items) {
(item as Indexed<T>).index = index++;
}
return items as Indexed<T>[];
}
export function mapToKeyValueArray<K extends string | number | symbol, V>(map: Record<K, V>) {
const values: { key: K, value: V }[] = [];
for (const [ key, value ] of Object.entries(map)) {
values.push({ key: key as K, value: value as V });
}
return values;
}
export function getErrorMessage(e: unknown) {
if (e && typeof e === "object" && "message" in e && typeof e.message === "string") {
return e.message;
} else {
return "Unknown error";
}
}
export default {
reloadFrontendApp,
restartDesktopApp,

View File

@@ -28,6 +28,28 @@
--ck-mention-list-max-height: 500px;
}
body#trilium-app.motion-disabled *,
body#trilium-app.motion-disabled *::before,
body#trilium-app.motion-disabled *::after {
/* Disable transitions and animations */
transition: none !important;
animation: none !important;
}
body#trilium-app.shadows-disabled *,
body#trilium-app.shadows-disabled *::before,
body#trilium-app.shadows-disabled *::after {
/* Disable shadows */
box-shadow: none !important;
}
body#trilium-app.backdrop-effects-disabled *,
body#trilium-app.backdrop-effects-disabled *::before,
body#trilium-app.backdrop-effects-disabled *::after {
/* Disable backdrop effects */
backdrop-filter: none !important;
}
.table {
--bs-table-bg: transparent !important;
}
@@ -139,6 +161,15 @@ textarea,
color: var(--muted-text-color);
}
.form-group.disabled {
opacity: 0.5;
pointer-events: none;
}
.form-group {
margin-bottom: 15px;
}
/* Add a gap between consecutive radios / check boxes */
label.tn-radio + label.tn-radio,
label.tn-checkbox + label.tn-checkbox {
@@ -346,7 +377,7 @@ body.desktop .tabulator-popup-container {
@supports (animation-fill-mode: forwards) {
/* Delay the opening of submenus */
body.desktop .dropdown-submenu .dropdown-menu {
body.desktop:not(.motion-disabled) .dropdown-submenu .dropdown-menu {
opacity: 0;
animation-fill-mode: forwards;
animation-delay: var(--submenu-opening-delay);
@@ -831,10 +862,34 @@ table.promoted-attributes-in-tooltip th {
.aa-dropdown-menu .aa-suggestion {
cursor: pointer;
padding: 5px;
padding: 6px 16px;
margin: 0;
}
.aa-dropdown-menu .aa-suggestion .icon {
display: inline-block;
line-height: inherit;
vertical-align: top;
}
.aa-dropdown-menu .aa-suggestion .text {
display: inline-block;
width: calc(100% - 20px);
padding-left: 4px;
}
.aa-dropdown-menu .aa-suggestion .search-result-title {
display: block;
}
.aa-dropdown-menu .aa-suggestion .search-result-attributes {
display: block;
font-size: 0.8em;
color: var(--muted-text-color);
opacity: 0.6;
line-height: 1;
}
.aa-dropdown-menu .aa-suggestion p {
padding: 0;
margin: 0;
@@ -1738,16 +1793,12 @@ button.close:hover {
margin-bottom: 10px;
}
.options-number-input {
.options-section input[type="number"] {
/* overriding settings from .form-control */
width: 10em !important;
flex-grow: 0 !important;
}
.options-mime-types {
column-width: 250px;
}
textarea {
cursor: auto;
}
@@ -1768,20 +1819,42 @@ textarea {
font-size: 1em;
}
.jump-to-note-dialog .modal-dialog {
max-width: 900px;
width: 90%;
}
.jump-to-note-dialog .modal-header {
align-items: center;
}
.jump-to-note-dialog .modal-body {
padding: 0;
min-height: 200px;
}
.jump-to-note-results .aa-dropdown-menu {
max-height: 40vh;
max-height: calc(80vh - 200px);
width: 100%;
max-width: none;
overflow-y: auto;
overflow-x: hidden;
text-overflow: ellipsis;
box-shadow: none;
}
.jump-to-note-results {
width: 100%;
}
.jump-to-note-results .aa-suggestions {
padding: 1rem;
padding: 0;
width: 100%;
}
.jump-to-note-results .aa-dropdown-menu .aa-suggestion:hover,
.jump-to-note-results .aa-dropdown-menu .aa-cursor {
background-color: var(--hover-item-background-color, #f8f9fa);
}
/* Command palette styling */
@@ -1799,8 +1872,24 @@ textarea {
.jump-to-note-dialog .aa-cursor .command-suggestion,
.jump-to-note-dialog .aa-suggestion:hover .command-suggestion {
border-left-color: var(--link-color);
background-color: var(--hover-background-color);
background-color: transparent;
}
.jump-to-note-dialog .show-in-full-search,
.jump-to-note-results .show-in-full-search {
border-top: 1px solid var(--main-border-color);
padding-top: 12px;
margin-top: 12px;
}
.jump-to-note-results .aa-suggestion .search-notes-action {
border-top: 1px solid var(--main-border-color);
margin-top: 8px;
padding-top: 8px;
}
.jump-to-note-results .aa-suggestion:has(.search-notes-action)::after {
display: none;
}
.jump-to-note-dialog .command-icon {
@@ -2257,7 +2346,8 @@ footer.webview-footer button {
/* Search result highlighting */
.search-result-title b,
.search-result-content b {
.search-result-content b,
.search-result-attributes b {
font-weight: 900;
color: var(--admonition-warning-accent-color);
}

View File

@@ -89,6 +89,7 @@
--menu-text-color: #e3e3e3;
--menu-background-color: #222222d9;
--menu-background-color-no-backdrop: #1b1b1b;
--menu-item-icon-color: #8c8c8c;
--menu-item-disabled-opacity: 0.5;
--menu-item-keyboard-shortcut-color: #ffffff8f;

View File

@@ -83,6 +83,7 @@
--menu-text-color: #272727;
--menu-background-color: #ffffffd9;
--menu-background-color-no-backdrop: #fdfdfd;
--menu-item-icon-color: #727272;
--menu-item-disabled-opacity: 0.6;
--menu-item-keyboard-shortcut-color: #666666a8;

View File

@@ -83,6 +83,12 @@
--tab-note-icons: true;
}
body.backdrop-effects-disabled {
/* Backdrop effects are disabled, replace the menu background color with the
* no-backdrop fallback color */
--menu-background-color: var(--menu-background-color-no-backdrop);
}
/*
* MENUS
*
@@ -530,10 +536,9 @@ body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after {
}
/* List item */
.jump-to-note-dialog .aa-suggestions div,
.note-detail-empty .aa-suggestions div {
.jump-to-note-dialog .aa-suggestion,
.note-detail-empty .aa-suggestion {
border-radius: 6px;
padding: 6px 12px;
color: var(--menu-text-color);
cursor: default;
}

View File

@@ -181,9 +181,7 @@ div.note-detail-empty {
}
.options-section:not(.tn-no-card) {
margin: auto;
min-width: var(--options-card-min-width);
max-width: var(--options-card-max-width);
margin: auto;
border-radius: 12px;
border: 1px solid var(--card-border-color) !important;
box-shadow: var(--card-box-shadow);
@@ -192,6 +190,11 @@ div.note-detail-empty {
margin-bottom: calc(var(--options-title-offset) + 26px) !important;
}
body.desktop .option-section:not(.tn-no-card) {
min-width: var(--options-card-min-width);
max-width: var(--options-card-max-width);
}
.note-detail-content-widget-content.options {
--default-padding: 15px;
padding-top: calc(var(--default-padding) + var(--options-title-offset) + var(--options-title-font-size));
@@ -233,11 +236,6 @@ div.note-detail-empty {
margin-bottom: 0;
}
.options-section .options-mime-types {
padding: 0;
margin: 0;
}
.options-section .form-group {
margin-bottom: 1em;
}

View File

@@ -848,7 +848,7 @@
"debug": "调试",
"debug_description": "调试将打印额外的调试信息到控制台,以帮助调试复杂查询",
"action": "操作",
"search_button": "搜索 <kbd>回车</kbd>",
"search_button": "搜索",
"search_execute": "搜索并执行操作",
"save_to_note": "保存到笔记",
"search_parameters": "搜索参数",
@@ -1165,7 +1165,7 @@
},
"revisions_snapshot_interval": {
"note_revisions_snapshot_interval_title": "笔记修订快照间隔",
"note_revisions_snapshot_description": "笔记修订快照间隔是创建新笔记修订的时间。有关更多信息,请参见 <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a>。",
"note_revisions_snapshot_description": "笔记修订快照间隔是创建新笔记修订的时间。有关更多信息,请参见 <doc>wiki</doc>。",
"snapshot_time_interval_label": "笔记修订快照时间间隔:"
},
"revisions_snapshot_limit": {
@@ -1871,14 +1871,19 @@
"selected_provider": "已选提供商",
"selected_provider_description": "选择用于聊天和补全功能的AI提供商",
"select_model": "选择模型...",
"select_provider": "选择提供商..."
"select_provider": "选择提供商...",
"ai_enabled": "已启用 AI 功能",
"ai_disabled": "已禁用 AI 功能",
"no_models_found_online": "找不到模型。请检查您的 API 密钥及设置。",
"no_models_found_ollama": "找不到 Ollama 模型。请确认 Ollama 是否正在运行。",
"error_fetching": "获取模型失败:{{error}}"
},
"code-editor-options": {
"title": "编辑器"
},
"custom_date_time_format": {
"title": "自定义日期/时间格式",
"description": "通过<kbd></kbd>或工具栏的方式可自定义日期和时间格式,有关日期/时间格式字符串中各个字符的含义,请参阅<a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">Day.js docs</a>。",
"description": "通过<shortcut />或工具栏的方式可自定义日期和时间格式,有关日期/时间格式字符串中各个字符的含义,请参阅<doc>Day.js docs</doc>。",
"format_string": "日期/时间格式字符串:",
"formatted_time": "格式化后日期/时间:"
},
@@ -1992,11 +1997,28 @@
"help_title": "显示关于此画面的更多信息"
},
"call_to_action": {
"next_theme_title": "新的 Trilium 主题已进入稳定版",
"next_theme_message": "有一段时间,我们一直在设计新的主题,为了让应用程序看起来更加现代。",
"next_theme_button": "切换至新的 Trilium 主题",
"background_effects_title": "背景效果现已推出稳定版本",
"background_effects_message": "在 Windows 装置上,背景效果现在已完全稳定。背景效果通过模糊背后的背景,为使用者界面增添一抹色彩。此技术也用于其他应用程序,例如 Windows 资源管理器。",
"background_effects_button": "启用背景效果"
"background_effects_button": "启用背景效果",
"next_theme_title": "试用新 Trilium 主题",
"next_theme_message": "当前使用旧版主题,要试用新主题吗?",
"next_theme_button": "试用新主题",
"dismiss": "关闭"
},
"settings": {
"related_settings": "相关设置"
},
"settings_appearance": {
"related_code_blocks": "文本笔记中代码块的色彩方案",
"related_code_notes": "代码笔记的色彩方案"
},
"units": {
"percentage": "%"
},
"ui-performance": {
"title": "性能",
"enable-motion": "启用过渡和动画",
"enable-shadows": "启用阴影",
"enable-backdrop-effects": "启用菜单、弹窗和面板的背景效果"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -732,7 +732,8 @@
"note_type": "Note type",
"editable": "Editable",
"basic_properties": "Basic Properties",
"language": "Language"
"language": "Language",
"configure_code_notes": "Configure code notes..."
},
"book_properties": {
"view_type": "View type",
@@ -848,7 +849,7 @@
"debug": "debug",
"debug_description": "Debug will print extra debugging information into the console to aid in debugging complex queries",
"action": "action",
"search_button": "Search <kbd>enter</kbd>",
"search_button": "Search",
"search_execute": "Search & Execute actions",
"save_to_note": "Save to note",
"search_parameters": "Search Parameters",
@@ -1113,6 +1114,12 @@
"layout-vertical-description": "launcher bar is on the left (default)",
"layout-horizontal-description": "launcher bar is underneath the tab bar, the tab bar is now full width."
},
"ui-performance": {
"title": "Performance",
"enable-motion": "Enable transitions and animations",
"enable-shadows": "Enable shadows",
"enable-backdrop-effects": "Enable background effects for menus, popups and panels"
},
"ai_llm": {
"not_started": "Not started",
"title": "AI Settings",
@@ -1253,7 +1260,12 @@
"selected_provider": "Selected Provider",
"selected_provider_description": "Choose the AI provider for chat and completion features",
"select_model": "Select model...",
"select_provider": "Select provider..."
"select_provider": "Select provider...",
"ai_enabled": "AI features enabled",
"ai_disabled": "AI features disabled",
"no_models_found_online": "No models found. Please check your API key and settings.",
"no_models_found_ollama": "No Ollama models found. Please check if Ollama is running.",
"error_fetching": "Error fetching models: {{error}}"
},
"zoom_factor": {
"title": "Zoom Factor (desktop build only)",
@@ -1310,7 +1322,7 @@
},
"revisions_snapshot_interval": {
"note_revisions_snapshot_interval_title": "Note Revision Snapshot Interval",
"note_revisions_snapshot_description": "The Note revision snapshot interval is the time after which a new note revision will be created for the note. See <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a> for more info.",
"note_revisions_snapshot_description": "The Note revision snapshot interval is the time after which a new note revision will be created for the note. See <doc>wiki</doc> for more info.",
"snapshot_time_interval_label": "Note revision snapshot time interval:"
},
"revisions_snapshot_limit": {
@@ -1372,7 +1384,7 @@
},
"custom_date_time_format": {
"title": "Custom Date/Time Format",
"description": "Customize the format of the date and time inserted via <kbd></kbd> or the toolbar. See <a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">Day.js docs</a> for available format tokens.",
"description": "Customize the format of the date and time inserted via <shortcut /> or the toolbar. See <doc>Day.js docs</doc> for available format tokens.",
"format_string": "Format string:",
"formatted_time": "Formatted date/time:"
},
@@ -1994,11 +2006,22 @@
"help_title": "Display more information about this screen"
},
"call_to_action": {
"next_theme_title": "The new Trilium theme is now stable",
"next_theme_message": "For a while now, we've been working on a new theme to give the application a more modern look.",
"next_theme_button": "Switch to the new Trilium theme",
"next_theme_title": "Try the new Trilium theme",
"next_theme_message": "You are currently using the legacy theme, would you like to try the new theme?",
"next_theme_button": "Try the new theme",
"background_effects_title": "Background effects are now stable",
"background_effects_message": "On Windows devices, background effects are now fully stable. The background effects adds a touch of color to the user interface by blurring the background behind it. This technique is also used in other applications such as Windows Explorer.",
"background_effects_button": "Enable background effects"
"background_effects_button": "Enable background effects",
"dismiss": "Dismiss"
},
"settings": {
"related_settings": "Related settings"
},
"settings_appearance": {
"related_code_blocks": "Color scheme for code blocks in text notes",
"related_code_notes": "Color scheme for code notes"
},
"units": {
"percentage": "%"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
{
"about": {
"title": "درباره Trilium Notes",
"homepage": "صفحه اصلی:",
"app_version": "نسخه برنامه:",
"db_version": "نسخه پایگاه داده:",
"sync_version": "نسخه منطبق:",
"build_date": "تاریخ ساخت:",
"build_revision": "نسخه بازنگری شده:",
"data_directory": "دایرکتوری داده:"
},
"toast": {
"critical-error": {
"title": "خطای بحرانی",
"message": "خطای بحرانی رخ داده که مانع از اجرای برنامه می شود\n\n {{message}}\n\nبه احتمال زیاد ناشی از خطای غیرمنتظره در اجرای ناموفق یک اسکریپت است. برنامه را در مد ایمن اجرا کنید و خطا را بررسی نمایید."
}
},
"add_link": {
"add_link": "افزودن لینک",
"note": "یادداشت"
}
}

View File

@@ -0,0 +1,147 @@
{
"about": {
"title": "Lisätietoja Trilium Notes:ista",
"homepage": "Kotisivu:",
"app_version": "Sovelluksen versio:",
"db_version": "Tietokannan versio:",
"build_date": "Koontipäivämäärä:",
"data_directory": "Datakansio:",
"sync_version": "Synkronoinnin versio:",
"build_revision": "Sovelluksen versio:"
},
"toast": {
"critical-error": {
"title": "Kriittinen virhe"
},
"widget-error": {
"title": "Widgetin luonti epäonnistui"
}
},
"add_link": {
"add_link": "Lisää linkki",
"link_title": "Linkin otsikko",
"button_add_link": "Lisää linkki",
"note": "Muistio",
"search_note": "etsi muistiota sen nimellä"
},
"branch_prefix": {
"prefix": "Etuliite: ",
"save": "Tallenna"
},
"bulk_actions": {
"bulk_actions": "Massatoiminnot",
"available_actions": "Saatavilla olevat toiminnot",
"chosen_actions": "Valitut toiminnot",
"execute_bulk_actions": "Toteuta massatoiminnot",
"bulk_actions_executed": "Massatoiminnot on toteutettu onnistuneesti.",
"none_yet": "Ei vielä... lisää toiminto klikkaamalla jotiain yllä saatavilla olevaa yltä.",
"labels": "Merkit",
"relations": "Suhteet",
"notes": "Muistiot",
"other": "Muut",
"affected_notes": "Vaikuttaa muistioihin"
},
"clone_to": {
"clone_notes_to": "Kopioi muistiot...",
"help_on_links": "Apua linkkeihin",
"notes_to_clone": "Kopioitavat muistiot",
"target_parent_note": "Kohteen päämuistio",
"search_for_note_by_its_name": "ensi muistiota sen nimellä",
"cloned_note_prefix_title": "Kopioitu muistia näytetään puussa annetulla etuliitteellä",
"prefix_optional": "Etuliite (valinnainen)",
"clone_to_selected_note": "Kopioi valittuun muistioon",
"note_cloned": "Muistio \"{{clonedTitle}}\" on kopioitu \"{{targetTitle}}\""
},
"confirm": {
"confirmation": "Vahvistus",
"cancel": "Peruuta",
"ok": "OK",
"also_delete_note": "Poista myös muistio"
},
"delete_notes": {
"delete_notes_preview": "Poista muistion esikatselu",
"close": "Sulje",
"notes_to_be_deleted": "Seuraavat muistiot tullaan poistamaan ({{notesCount}})",
"no_note_to_delete": "Muistioita ei poisteta (vain kopiot).",
"cancel": "Peruuta",
"ok": "OK"
},
"export": {
"export_note_title": "Vie muistio",
"close": "Sulje",
"format_html": "HTML - suositeltu, sillä se säilyttää kaikki formatoinnit",
"format_markdown": "Markdown - tämä säilyttää suurimman osan formatoinneista.",
"opml_version_1": "OPML v1.0 - pelkkä teksti",
"opml_version_2": "OPML v2.0 - sallii myös HTML:n",
"export": "Vie",
"choose_export_type": "Valitse ensin viennin tyyppi",
"export_status": "Viennin tila",
"export_in_progress": "Vienti käynnissä: {{progressCount}}",
"export_finished_successfully": "Vienti valmistui onnistuneesti.",
"format_pdf": "PDF - tulostukseen ja jakamiseen."
},
"help": {
"title": "Lunttilappu",
"noteNavigation": "Muistion navigointi",
"goUpDown": "mene ylös/alas muistioiden listassa",
"collapseExpand": "pienennä/suurenna solmu",
"notSet": "ei asetettu",
"goBackForwards": "mene taaksepäin/eteenpäin historiassa",
"jumpToParentNote": "Hyppää ylempään muistioon",
"collapseWholeTree": "pienennä koko muistio puu",
"onlyInDesktop": "Vain työpöytänäkymässä (Electron build)",
"openEmptyTab": "Avaa tyhjä välilehti",
"closeActiveTab": "sulje aktiivinen välilehti",
"activateNextTab": "aktivoi seuraava välilehti",
"activatePreviousTab": "aktivoi edellinen välilehti",
"creatingNotes": "Luo muistiota",
"movingCloningNotes": "Siirrä / kopioi muistioita",
"moveNoteUpHierarchy": "siirrä muistio ylöspäin listassa",
"selectNote": "valitse muistio",
"editingNotes": "Muokkaa solmua",
"createEditLink": "luo / muokkaa ulkoista linkkiä",
"createInternalLink": "luo sisäinen linkki",
"insertDateTime": "lisää nykyinen päivämäärä ja aika hiiren kohdalle",
"troubleshooting": "Vianmääritys",
"reloadFrontend": "lataa Trilium:in käyttöliittymä",
"showDevTools": "näytä kehittäjätyökalut",
"showSQLConsole": "näytä SQL konsoli",
"other": "Muut"
},
"import": {
"importIntoNote": "Tuo muistioon",
"chooseImportFile": "Valitse tuonnin tiedosto",
"options": "Valinnat",
"safeImport": "Turvallinen tuonti",
"shrinkImages": "Kutista kuvat",
"replaceUnderscoresWithSpaces": "Korvaa alaviivat väleillä tuotujen muistioiden tiedostonimissä",
"import": "Tuo",
"failed": "Tuonti epäonnistui: {{message}}.",
"html_import_tags": {
"title": "HTML Tuonnin Tunnisteet",
"placeholder": "Lisää HTML tunnisteet, yksi per rivi"
},
"import-status": "Tuonnin tila",
"in-progress": "Tuonti vaiheessa: {{progress}}",
"successful": "Tuonti valmistui onnistuneesti."
},
"include_note": {
"dialog_title": "Sisällytä muistio",
"label_note": "Muistio",
"placeholder_search": "etsi muistiota sen nimellä",
"box_size_small": "pieni (~ 10 riviä)",
"box_size_medium": "keskisuuri (~ 30 riviä)",
"button_include": "Sisällytä muistio"
},
"info": {
"modalTitle": "Info viesti",
"closeButton": "Sulje",
"okButton": "OK"
},
"jump_to_note": {
"search_button": "Etsi koko tekstistä"
},
"call_to_action": {
"dismiss": "Hylkää"
}
}

View File

@@ -202,7 +202,8 @@
"okButton": "OK"
},
"jump_to_note": {
"search_button": "Rechercher dans le texte intégral"
"search_button": "Rechercher dans le texte intégral",
"search_placeholder": "Rechercher une note par son nom ou saisir > pour les commandes…"
},
"markdown_import": {
"dialog_title": "Importation Markdown",
@@ -222,11 +223,16 @@
"note_type_chooser": {
"modal_title": "Choisissez le type de note",
"modal_body": "Choisissez le type de note/le modèle de la nouvelle note :",
"templates": "Modèles"
"templates": "Modèles",
"change_path_prompt": "Modifier lemplacement de création de la nouvelle note :",
"search_placeholder": "Rechercher le chemin par nom (par défaut si vide)",
"builtin_templates": "Modèles intégrés"
},
"password_not_set": {
"title": "Le mot de passe n'est pas défini",
"body1": "Les notes protégées sont cryptées à l'aide d'un mot de passe utilisateur, mais le mot de passe n'a pas encore été défini."
"body1": "Les notes protégées sont cryptées à l'aide d'un mot de passe utilisateur, mais le mot de passe n'a pas encore été défini.",
"body2": "Pour pouvoir protéger les notes, cliquez sur le bouton ci-dessous pour ouvrir la boîte de dialogue Options et définir votre mot de passe.",
"go_to_password_options": "Accéder aux options de mot de passe"
},
"prompt": {
"title": "Prompt",
@@ -268,7 +274,9 @@
"mime": "MIME : ",
"file_size": "Taille du fichier :",
"preview": "Aperçu :",
"preview_not_available": "L'aperçu n'est pas disponible pour ce type de note."
"preview_not_available": "L'aperçu n'est pas disponible pour ce type de note.",
"restore_button": "Restaurer",
"delete_button": "Supprimer"
},
"sort_child_notes": {
"sort_children_by": "Trier les enfants par...",
@@ -384,7 +392,7 @@
"share_index": "la note avec ce label listera toutes les racines des notes partagées",
"display_relations": "noms des relations délimités par des virgules qui doivent être affichés. Tous les autres seront masqués.",
"hide_relations": "noms de relations délimités par des virgules qui doivent être masqués. Tous les autres seront affichés.",
"title_template": "titre par défaut des notes créées en tant qu'enfants de cette note. La valeur est évaluée sous forme de chaîne JavaScript \n et peut ainsi être enrichi de contenu dynamique via les variables injectées <code>now</code> et <code>parentNote</code>. Exemples :\n \n <ul>\n <li><code>Œuvres littéraires de ${parentNote.getLabelValue('authorName')}</code></li>\n <li><code>Connectez-vous pour ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n Consultez le <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">wiki avec plus de détails</a>, la documentation sur l'API pour <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> et <a href=\"https://day.js.org/docs/en/display/format\">maintenant< /a> pour plus de détails.",
"title_template": "titre par défaut des notes créées en tant qu'enfants de cette note. La valeur est évaluée sous forme de chaîne JavaScript \n et peut ainsi être enrichi de contenu dynamique via les variables injectées <code>now</code> et <code>parentNote</code>. Exemples :\n \n <ul>\n <li><code>Œuvres littéraires de ${parentNote.getLabelValue('authorName')}</code></li>\n <li><code>Connectez-vous pour ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n Consultez le <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">wiki avec plus de détails</a>, la documentation sur l'API pour <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> et <a href=\"https://day.js.org/docs/en/display/format\">maintenant</a> pour plus de détails.",
"template": "Cette note apparaîtra parmi les modèles disponibles lors de la création d'une nouvelle note",
"toc": "<code>#toc</code> ou <code>#toc=show</code> forcera l'affichage de la table des matières, <code>#toc=hide</code> force qu'elle soit masquée. Si le label n'existe pas, le paramètre global est utilisé",
"color": "définit la couleur de la note dans l'arborescence des notes, les liens, etc. Utilisez n'importe quelle valeur de couleur CSS valide comme « rouge » ou #a13d5f",
@@ -418,7 +426,8 @@
"other_notes_with_name": "Autres notes portant le nom {{attributeType}} \"{{attributeName}}\"",
"and_more": "... et {{count}} plus.",
"print_landscape": "Lors de l'exportation en PDF, change l'orientation de la page en paysage au lieu de portrait.",
"print_page_size": "Lors de l'exportation en PDF, change la taille de la page. Valeurs supportées : <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>."
"print_page_size": "Lors de l'exportation en PDF, change la taille de la page. Valeurs supportées : <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.",
"color_type": "Couleur"
},
"attribute_editor": {
"help_text_body1": "Pour ajouter un label, tapez simplement par ex. <code>#rock</code>, ou si vous souhaitez également ajouter une valeur, tapez par ex. <code>#année = 2020</code>",
@@ -577,7 +586,8 @@
"september": "Septembre",
"october": "Octobre",
"november": "Novembre",
"december": "Décembre"
"december": "Décembre",
"cannot_find_week_note": "Impossible de trouver la note de la semaine"
},
"close_pane_button": {
"close_this_pane": "Fermer ce volet"
@@ -721,7 +731,8 @@
"basic_properties": {
"note_type": "Type de note",
"editable": "Modifiable",
"basic_properties": "Propriétés de base"
"basic_properties": "Propriétés de base",
"language": "Langage"
},
"book_properties": {
"view_type": "Type d'affichage",
@@ -732,7 +743,11 @@
"collapse": "Réduire",
"expand": "Développer",
"invalid_view_type": "Type de vue non valide '{{type}}'",
"calendar": "Calendrier"
"calendar": "Calendrier",
"book_properties": "Propriétés de la collection",
"table": "Tableau",
"geo-map": "Carte géographique",
"board": "Tableau de bord"
},
"edited_notes": {
"no_edited_notes_found": "Aucune note modifiée ce jour-là...",
@@ -809,7 +824,8 @@
"unknown_label_type": "Type de label inconnu '{{type}}'",
"unknown_attribute_type": "Type d'attribut inconnu '{{type}}'",
"add_new_attribute": "Ajouter un nouvel attribut",
"remove_this_attribute": "Supprimer cet attribut"
"remove_this_attribute": "Supprimer cet attribut",
"remove_color": "Supprimer létiquette de couleur"
},
"script_executor": {
"query": "Requête",
@@ -832,7 +848,7 @@
"debug": "debug",
"debug_description": "Debug imprimera des informations supplémentaires dans la console pour faciliter le débogage des requêtes complexes",
"action": "action",
"search_button": "Recherche <kbd>Entrée</kbd>",
"search_button": "Recherche",
"search_execute": "Rechercher et exécuter des actions",
"save_to_note": "Enregistrer dans la note",
"search_parameters": "Paramètres de recherche",
@@ -899,7 +915,7 @@
"description1": "Le script de recherche permet de définir les résultats de la recherche en exécutant un script. Cela offre une flexibilité maximale lorsque la recherche standard ne suffit pas.",
"description2": "Le script de recherche doit être de type \"code\" et sous-type \"backend JavaScript\". Le script doit retourner un tableau de noteIds ou de notes.",
"example_title": "Voir cet exemple :",
"example_code": "// 1. préfiltrage à l'aide de la recherche standard\nconst candidateNotes = api.searchForNotes(\"#journal\"); \n\n// 2. application de critères de recherche personnalisés\nconst matchedNotes = candidateNotes\n .filter(note => note.title.match(/[0-9]{1,2}\\. ?[0-9]{1,2}\\. ?[0-9]{4}/));\n\nreturn matchedNotes;",
"example_code": "// 1. préfiltrage à l'aide de la recherche standard\nconst candidateNotes = api.searchForNotes(\"#journal\"); \n\n// 2. application de critères de recherche personnalisés\nconst matchedNotes = candidateNotes\n .filter(note => note.title.match(/[0-9]{1,2}\\.?[0-9]{1,2}\\.?[0-9]{4}/));\n\nreturn matchedNotes;",
"note": "Notez que le script de recherche et la l'expression à rechercher standard ne peuvent pas être combinés."
},
"search_string": {
@@ -1068,7 +1084,8 @@
"max_width_label": "Largeur maximale du contenu en pixels",
"apply_changes_description": "Pour appliquer les modifications de largeur du contenu, cliquez sur",
"reload_button": "recharger l'interface",
"reload_description": "changements par rapport aux options d'apparence"
"reload_description": "changements par rapport aux options d'apparence",
"max_width_unit": "Pixels"
},
"native_title_bar": {
"title": "Barre de titre native (nécessite le redémarrage de l'application)",
@@ -1091,7 +1108,10 @@
"layout-vertical-title": "Vertical",
"layout-horizontal-title": "Horizontal",
"layout-vertical-description": "la barre de raccourcis est à gauche (défaut)",
"layout-horizontal-description": "la barre de raccourcis est sous la barre des onglets, cette-dernière est s'affiche en pleine largeur."
"layout-horizontal-description": "la barre de raccourcis est sous la barre des onglets, cette-dernière est s'affiche en pleine largeur.",
"auto_theme": "Hérité (suivre le schéma de couleurs du système)",
"light_theme": "Hérité (clair)",
"dark_theme": "Hérité (foncé)"
},
"zoom_factor": {
"title": "Facteur de zoom (version bureau uniquement)",
@@ -1136,14 +1156,14 @@
"note_erasure_timeout": {
"note_erasure_timeout_title": "Délai d'effacement des notes",
"note_erasure_description": "Les notes supprimées (et les attributs, versions...) sont seulement marquées comme supprimées et il est possible de les récupérer à partir de la boîte de dialogue Notes récentes. Après un certain temps, les notes supprimées sont « effacées », ce qui signifie que leur contenu n'est plus récupérable. Ce paramètre vous permet de configurer la durée entre la suppression et l'effacement de la note.",
"erase_notes_after": "Effacer les notes après:",
"erase_notes_after": "Effacer les notes après :",
"manual_erasing_description": "Vous pouvez également déclencher l'effacement manuellement (sans tenir compte de la durée définie ci-dessus) :",
"erase_deleted_notes_now": "Effacer les notes supprimées maintenant",
"deleted_notes_erased": "Les notes supprimées ont été effacées."
},
"revisions_snapshot_interval": {
"note_revisions_snapshot_interval_title": "Délai d'enregistrement automatique d'une version de note",
"note_revisions_snapshot_description": "Le délai d'enregistrement automatique des versions de note définit le temps avant la création automatique d'une nouvelle version de note. Consultez le <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a> pour plus d'informations.",
"note_revisions_snapshot_description": "Le délai d'enregistrement automatique des versions de note définit le temps avant la création automatique d'une nouvelle version de note. Consultez le <doc>wiki</doc> pour plus d'informations.",
"snapshot_time_interval_label": "Délai d'enregistrement automatique de version de note :"
},
"revisions_snapshot_limit": {
@@ -1650,5 +1670,26 @@
},
"modal": {
"close": "Fermer"
},
"ai_llm": {
"not_started": "Non démarré",
"title": "Paramètres IA",
"processed_notes": "Notes traitées",
"n_notes_queued_0": "{{ count }} note en attente dindexation",
"n_notes_queued_1": "{{ count }} notes en attente dindexation",
"n_notes_queued_2": "",
"notes_indexed_0": "{{ count }} note indexée",
"notes_indexed_1": "{{ count }} notes indexées",
"notes_indexed_2": "",
"anthropic_url_description": "URL de base pour l'API Anthropic (par défaut : https ://api.anthropic.com)",
"anthropic_model_description": "Modèles Anthropic Claude pour la complétion",
"voyage_settings": "Réglages d'IA Voyage",
"ollama_settings": "Réglages Ollama",
"ollama_url_description": "URL pour l'API Ollama (par défaut: http://localhost:11434)",
"ollama_model_description": "Model Ollama utilisé pour la complétion",
"anthropic_configuration": "Configuration Anthropic",
"voyage_configuration": "Configuration IA Voyage",
"voyage_url_description": "Défaut: https://api.voyageai.com/v1",
"ollama_configuration": "Configuration Ollama"
}
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -35,7 +35,11 @@
"link_title_arbitrary": "リンクタイトルは自由に変更可能"
},
"branch_prefix": {
"save": "保存"
"save": "保存",
"edit_branch_prefix": "ブランチ接頭辞の編集",
"help_on_tree_prefix": "ツリー接頭辞に関するヘルプ",
"prefix": "接頭辞: ",
"branch_prefix_saved": "ブランチ接頭辞が保存されました。"
},
"global_menu": {
"menu": "メニュー",
@@ -59,7 +63,10 @@
"zoom_out": "ズームアウト",
"zoom_in": "ズームイン",
"advanced": "高度",
"toggle-zen-mode": "禅モード"
"toggle-zen-mode": "禅モード",
"switch_to_mobile_version": "モバイル版に切り替え",
"switch_to_desktop_version": "デスクトップ版に切り替え",
"configure_launchbar": "ランチャーバーの設定"
},
"left_pane_toggle": {
"show_panel": "パネルを表示",
@@ -82,14 +89,17 @@
"help_on_links": "ヘルプへのリンク"
},
"delete_notes": {
"delete_all_clones_description": "すべてのクローンも削除する(最近の変更では元に戻すことができる)",
"delete_all_clones_description": "すべてのクローンも削除(最近の変更では元に戻すことができる)",
"erase_notes_description": "通常の(ソフト)削除では、ノートは削除されたものとしてマークされ、一定期間内に(最近の変更で)削除を取り消すことができます。このオプションをオンにすると、ノートは即座に削除され、削除を取り消すことはできません。",
"erase_notes_warning": "すべてのクローンを含め、ノートを完全に消去します(元に戻せません)。これにより、アプリケーションは強制的にリロードされます。",
"notes_to_be_deleted": "以下のノートが削除されます ({{notesCount}})",
"no_note_to_delete": "ノートは削除されません(クローンのみ)。",
"cancel": "キャンセル",
"ok": "OK",
"close": "閉じる"
"close": "閉じる",
"delete_notes_preview": "ノートのプレビューを削除",
"broken_relations_to_be_deleted": "次のリレーション ({{relationCount}})は壊れているので消去されます",
"deleted_relation_text": "削除予定のノート{{- note}}は{{- source}}からリレーション{{- relation}}によって参照されています."
},
"calendar": {
"mon": "月",
@@ -141,14 +151,15 @@
},
"tab_row": {
"close_tab": "タブを閉じる",
"add_new_tab": "新しいタブ",
"add_new_tab": "新しいタブを追加",
"close": "閉じる",
"close_other_tabs": "他のタブを閉じる",
"close_right_tabs": "右側のタブをすべて閉じる",
"close_all_tabs": "すべてのタブを閉じる",
"reopen_last_tab": "最後に閉じたタブを開く",
"move_tab_to_new_window": "このタブを新しいウィンドウに移動する",
"copy_tab_to_new_window": "このタブを新しいウィンドウにコピーする"
"move_tab_to_new_window": "このタブを新しいウィンドウに移動",
"copy_tab_to_new_window": "このタブを新しいウィンドウにコピーする",
"new_tab": "新しいタブ"
},
"tasks": {
"due": {
@@ -174,7 +185,7 @@
"debug": "デバッグ",
"debug_description": "デバッグは複雑なクエリのデバッグを支援するために、追加のデバッグ情報をコンソールに出力します",
"action": "アクション",
"search_button": "検索 <kbd>Enter</kbd>",
"search_button": "検索",
"search_execute": "検索とアクションの実行",
"save_to_note": "ノートに保存",
"search_parameters": "検索パラメータ",
@@ -198,7 +209,10 @@
"confirm": {
"confirmation": "確認",
"cancel": "キャンセル",
"ok": "OK"
"ok": "OK",
"also_delete_note": "同時にノートを削除",
"are_you_sure_remove_note": "本当に\"{{title}}\"をリレーションマップから除きたいですか? ",
"if_you_dont_check": "これをチェックしないと、このノートはリレーションマップからのみ除かれます。"
},
"export": {
"export_note_title": "ノートをエクスポート",
@@ -265,7 +279,12 @@
"showSQLConsole": "SQLコンソールを表示",
"other": "その他",
"quickSearch": "クイックサーチにフォーカス",
"inPageSearch": "ページ内検索"
"inPageSearch": "ページ内検索",
"showJumpToNoteDialog": "<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">「ジャンプ先」ダイアログ</a>を表示",
"moveNoteUpDown": "ノートリストでノートを上/下に移動",
"notSet": "未設定",
"goUpDown": "ノートのリストで上下する",
"editBranchPrefix": "アクティブノートのクローンの <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/tree-concepts.html#prefix\">プレフィックス</a> を編集する"
},
"import": {
"importIntoNote": "ノートにインポート",
@@ -340,7 +359,9 @@
},
"relation_map_buttons": {
"zoom_out_title": "ズームアウト",
"zoom_in_title": "ズームイン"
"zoom_in_title": "ズームイン",
"create_child_note_title": "新しい子ノートを作成し、関連マップに追加",
"reset_pan_zoom_title": "パンとズームを初期座標と倍率にリセット"
},
"tree-context-menu": {
"advanced": "高度",
@@ -392,19 +413,38 @@
"open_note_custom": "プログラムからノートを開く",
"import_files": "ファイルをインポート",
"export_note": "ノートをエクスポート",
"delete_note": "ノートを削除する",
"delete_note": "ノートを削除",
"print_note": "ノートを印刷",
"print_pdf": "PDFとしてエクスポート..."
"print_pdf": "PDFとしてエクスポート...",
"convert_into_attachment": "添付ファイルに変換",
"save_revision": "変更履歴を保存",
"convert_into_attachment_failed": "ノート '{{title}}' の変換に失敗しました。",
"convert_into_attachment_successful": "ノート '{{title}}' は添付ファイルに変換されました。",
"convert_into_attachment_prompt": "本当にノート '{{title}}' を親ノートの添付ファイルに変換しますか?",
"note_attachments": "ノートの添付ファイル"
},
"command_palette": {
"export_note_title": "ノートをエクスポート",
"search_subtree_title": "サブツリー内を検索"
"search_subtree_title": "サブツリー内を検索",
"tree-action-name": "ツリー: {{name}}",
"export_note_description": "現在のノートをエクスポート",
"search_notes_title": "ノートを検索",
"search_notes_description": "高度な検索を開く",
"search_subtree_description": "現在のサブツリー内を検索",
"search_history_title": "検索履歴を表示",
"search_history_description": "過去の検索結果を見る"
},
"delete_note": {
"delete_note": "ノートを削除する"
"delete_note": "ノートを削除"
},
"board_view": {
"delete-note": "ノートを削除する"
"delete-note": "ノートを削除",
"insert-above": "上に挿入",
"insert-below": "下に挿入",
"delete-column": "列を削除",
"delete-column-confirmation": "本当にこの列を削除しますか?対応する属性は、この列の下のノートでも削除されます。",
"new-item": "新しいアイテム",
"add-column": "列を追加"
},
"code_buttons": {
"execute_button_title": "スクリプトを実行",
@@ -482,7 +522,8 @@
"file_size": "ファイルサイズ",
"download": "ダウンロード",
"open": "開く",
"title": "ファイル"
"title": "ファイル",
"upload_new_revision": "編集履歴をアップロード"
},
"note_info_widget": {
"note_id": "ノート ID",
@@ -492,21 +533,54 @@
"note_size": "ノートサイズ",
"calculate": "計算",
"subtree_size": "(サブツリーサイズ: {{size}}、ノード数: {{count}}",
"title": "ノート情報"
"title": "ノート情報",
"note_size_info": "ノートのサイズは、このノートに必要なストレージの概算を示します。これは、ノートの内容とそのノートの編集履歴の内容を考慮したものです。"
},
"image_properties": {
"file_type": "ファイルタイプ",
"file_size": "ファイルサイズ",
"download": "ダウンロード",
"open": "開く",
"title": "画像"
"title": "画像",
"upload_new_revision": "編集履歴をアップロード"
},
"revisions": {
"download_button": "ダウンロード",
"delete_button": "削除"
"delete_button": "削除",
"note_revisions": "ノートの変更履歴",
"delete_all_revisions": "このノートの変更履歴をすべて削除",
"delete_all_button": "変更履歴をすべて削除",
"help_title": "変更履歴のヘルプ",
"revision_last_edited": "この変更は{{date}}に行われました",
"confirm_delete_all": "このノートのすべての変更履歴を削除しますか?",
"no_revisions": "このノートに変更履歴はまだありません...",
"restore_button": "復元",
"confirm_restore": "この変更を復元しますか?現在のノートのタイトルとコンテンツはこの変更で上書きされます。",
"confirm_delete": "この変更履歴を削除しますか?",
"revisions_deleted": "ノートの変更履歴は削除されました。",
"revision_restored": "ノートの変更が復元されました。",
"revision_deleted": "ノートの変更履歴は削除されました。",
"settings": "ノートの変更履歴の設定",
"file_size": "ファイルサイズ:",
"preview": "プレビュー:",
"preview_not_available": "このノートタイプではプレビューは利用できません。"
},
"attachments_actions": {
"download": "ダウンロード"
"download": "ダウンロード",
"open_externally": "外部で開く",
"open_externally_title": "ファイルを外部アプリケーションで開き、変更を監視します。その後、変更されたバージョンをTriliumにアップロードできるようになります。",
"open_custom": "プログラムから開く",
"open_custom_title": "ファイルを外部アプリケーションで開き、変更を監視します。その後、変更されたバージョンをTriliumにアップロードできるようになります。",
"rename_attachment": "添付ファイルの名前を変更",
"copy_link_to_clipboard": "リンクをクリップボードにコピー",
"convert_attachment_into_note": "添付ファイルをノートに変換",
"delete_attachment": "添付ファイルを削除",
"open_externally_detail_page": "添付ファイルを外部で開くは、詳細ページからのみ可能です。まず添付ファイル詳細をクリックしてから、再度操作を行ってください。",
"open_custom_client_only": "添付ファイルをプログラムから開くは、デスクトップクライアントからのみ可能です。",
"delete_confirm": "本当に添付ファイル '{{title}}' を削除しますか?",
"delete_success": "添付ファイル '{{title}}' は削除されました。",
"enter_new_name": "新しい添付ファイルの名前を入力してください",
"upload_new_revision": "編集履歴をアップロード"
},
"etapi": {
"created": "作成日時",
@@ -578,21 +652,48 @@
"parent_count": "クローンの数",
"random": "ランダムな順番",
"asc": "昇順(デフォルト)",
"desc": "降順"
"desc": "降順",
"content_and_attachments_size": "添付ファイルを含むノート内容のサイズ",
"content_and_attachments_and_revisions_size": "添付ファイルと編集履歴を含むノート内容のサイズ",
"revision_count": "編集履歴の回数",
"owned_label_count": "ラベルの数",
"owned_relation_count": "関係の数"
},
"table_view": {
"sort-column-descending": "降順",
"sort-column-ascending": "昇順"
"sort-column-ascending": "昇順",
"new-row": "新しい行",
"new-column": "新しい列",
"sort-column-by": "\"{{title}}\" で並べ替え",
"sort-column-clear": "並べ替えをクリア",
"hide-column": "列 \"{{title}}\" を隠す",
"show-hide-columns": "列を表示/非表示",
"row-insert-above": "上に行を挿入",
"row-insert-below": "下に行を挿入",
"row-insert-child": "子ノートを挿入",
"add-column-to-the-left": "列を左に追加",
"add-column-to-the-right": "列を右に追加",
"edit-column": "列を編集",
"delete_column_confirmation": "この列を本当に削除してもよろしいですか?対応する属性はすべてのノートから削除されます。",
"delete-column": "列を削除",
"new-column-label": "ラベル",
"new-column-relation": "関係"
},
"search_script": {
"title": "検索スクリプト:",
"placeholder": "ノート名で検索"
"placeholder": "ノート名で検索",
"description1": "検索スクリプトは、スクリプトを実行することによって検索結果を定義することができます。標準の検索では不十分な場合に、最大限の柔軟性を提供します。",
"description2": "検索スクリプトはノートタイプが \"code\" かつ \"JavaScript backend\" でなければならない。スクリプトは、 noteIds または note の配列を返す必要があります。",
"example_title": "例は以下です:",
"example_code": "// 1. 標準検索によるプレフィルタリング\nconst candidateNotes = api.searchForNotes(\"#journal\"); \n\n// 2. カスタム検索条件の適用\nconst matchedNotes = candidateNotes\n .filter(note => note.title.match(/[0-9]{1,2}\\. ?[0-9]{1,2}\\. ?[0-9]{4}/));\n\nreturn matchedNotes;",
"note": "検索スクリプトと文字列検索は、互いに組み合わせることはできません。"
},
"include_note": {
"placeholder_search": "ノート名で検索",
"dialog_title": "埋め込みノート",
"box_size_prompt": "埋め込みノート枠のサイズ:",
"button_include": "埋め込みノート"
"button_include": "埋め込みノート",
"label_note": "ノート"
},
"ancestor": {
"placeholder": "ノート名で検索"
@@ -634,7 +735,12 @@
"system-default": "システムのデフォルト"
},
"max_content_width": {
"reload_button": "フロントエンドをリロード"
"reload_button": "フロントエンドをリロード",
"title": "コンテンツ幅",
"default_description": "Triliumは、ワイドスクリーンで最大化された画面での可読性を向上させるために、デフォルトでコンテンツの最大幅を制限しています。",
"max_width_label": "最大コンテンツ幅",
"max_width_unit": "ピクセル",
"apply_changes_description": "コンテンツ幅の変更を適用するには、クリックしてください"
},
"theme": {
"title": "アプリのテーマ",
@@ -713,7 +819,8 @@
"shortcut_info": "オプション -> ショートカット('右ペイン切り替え')で、右ペイン(目次を含む)を素早く切り替えるキーボードショートカットを設定できます。"
},
"toc": {
"table_of_contents": "目次"
"table_of_contents": "目次",
"options": "オプション"
},
"text_auto_read_only_size": {
"title": "自動読み取り専用のサイズ",
@@ -724,11 +831,12 @@
"code_auto_read_only_size": {
"title": "自動読み取り専用のサイズ",
"description": "自動読み取り専用のノートサイズは、ノートが読み取り専用モード(パフォーマンス上の理由)で表示されるようになるサイズです。",
"unit": "文字"
"unit": "文字",
"label": "自動読み取り専用のサイズ(コードノート)"
},
"custom_date_time_format": {
"title": "日付/時刻フォーマットのカスタム",
"description": "<kbd></kbd>またはツールバーから挿入される日付と時刻のフォーマットをカスタマイズする。 利用可能なトークンについては <a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">Day.js ドキュメント</a> を参照してください。",
"description": "<shortcut />またはツールバーから挿入される日付と時刻のフォーマットをカスタマイズする。 利用可能なトークンについては <doc>Day.js ドキュメント</doc> を参照してください。",
"format_string": "文字列形式:",
"formatted_time": "日付/時刻形式:"
},
@@ -804,7 +912,8 @@
"close_label": "閉じる"
},
"modal": {
"close": "閉じる"
"close": "閉じる",
"help_title": "この画面に関する詳細情報を表示"
},
"bookmark_switch": {
"bookmark": "ブックマーク",
@@ -812,10 +921,39 @@
"remove_bookmark": "ブックマークを削除"
},
"attribute_detail": {
"delete": "削除"
"delete": "削除",
"close_button_title": "変更を破棄して閉じる",
"attr_is_owned_by": "属性の所有者",
"attr_name_title": "属性の名前は英数字、コロン、アンダーバーのみで構成します",
"name": "名前",
"value": "値",
"promoted_alias": "エイリアス",
"multiplicity_title": "多重性とは、同じ名前を持つ属性をいくつ作成できるかを定義するもので、1つまたはそれ以上です。",
"multiplicity": "多重性",
"single_value": "単一の値",
"multi_value": "複数の値",
"label_type_title": "ラベルのタイプは、Triliumがラベルの値を入力するのに適したインターフェースを選択するのに役立ちます。",
"label_type": "タイプ",
"text": "テキスト",
"number": "数値",
"boolean": "真偽値",
"date": "日付",
"date_time": "日付と時間",
"time": "時間",
"url": "URL",
"save_and_close": "保存して閉じる <kbd>Ctrl+Enter</kbd>",
"precision_title": "値設定インターフェースで、小数点以下何桁まで利用可能にするか。",
"precision": "精度",
"digits": "桁",
"inheritable_title": "継承属性は、このツリー配下のすべての子孫に継承されます。",
"inheritable": "継承",
"related_notes_title": "このラベルが付いた他のノート"
},
"link_context_menu": {
"open_note_in_popup": "クイックエディット"
"open_note_in_popup": "クイックエディット",
"open_note_in_new_tab": "新しいタブでノートを開く",
"open_note_in_new_split": "新しく分割してノートを開く",
"open_note_in_new_window": "新しいウィンドウでノートを開く"
},
"note_tooltip": {
"quick-edit": "クイックエディット"
@@ -861,6 +999,400 @@
"hide-archived-notes": "アーカイブノートを隠す",
"automatically-collapse-notes": "ノートを自動的に折りたたむ",
"automatically-collapse-notes-title": "一定期間使用されないと、ツリーを整理するためにノートは折りたたまれます。",
"save-changes": "変更を保存して適用"
"save-changes": "変更を保存して適用",
"auto-collapsing-notes-after-inactivity": "非アクティブ状態が続いたためノートが自動で折りたたまれます...",
"create-child-note": "子ノートを作成"
},
"bulk_actions": {
"bulk_actions": "一括操作",
"affected_notes": "影響されたノート",
"include_descendants": "選択したノートの子ノートを含む",
"available_actions": "利用可能なアクション",
"chosen_actions": "選択されたアクション",
"execute_bulk_actions": "一括操作を実行",
"bulk_actions_executed": "一括操作が成功に実行されました。",
"labels": "ラベル",
"relations": "関係",
"notes": "ノート",
"other": "その他",
"none_yet": "アクションを上のリストからクリックして追加。"
},
"note_title": {
"placeholder": "ここにノートのタイトルを入力..."
},
"search_result": {
"no_notes_found": "指定された検索パラメータに該当するノートは見つかりませんでした。",
"search_not_executed": "検索はまだ実行されていません。上の「検索」ボタンをクリックすると、検索結果が表示されます。"
},
"sql_result": {
"no_rows": "このクエリでは行が返されませんでした"
},
"sql_table_schemas": {
"tables": "テーブル"
},
"app_context": {
"please_wait_for_save": "保存が完了するまで数秒お待ちください。その後、もう一度お試しいただけます。"
},
"note_create": {
"duplicated": "ノート \"{{title}}\" は複製されました。"
},
"clipboard": {
"cut": "ノートはクリップボードにカットされました。",
"copied": "ノートはクリップボードにコピーされました。",
"copy_failed": "権限の問題で、クリップボードにコピーできません。",
"copy_success": "クリップボードにコピーしました。"
},
"launcher_context_menu": {
"add-note-launcher": "ノートランチャーを追加",
"add-script-launcher": "スクリプトランチャーを追加",
"add-custom-widget": "カスタムウィジェットを追加",
"add-spacer": "スペーサーを追加",
"delete": "削除 <kbd data-command=\"deleteNotes\"></kbd>",
"reset": "リセット",
"move-to-visible-launchers": "可視ランチャーに移動",
"move-to-available-launchers": "利用可能なランチャーに移動",
"duplicate-launcher": "ランチャーの複製 <kbd data-command=\"duplicateSubtree\">"
},
"editable-text": {
"auto-detect-language": "自動検出"
},
"highlighting": {
"title": "コードブロック",
"description": "テキストノート内のコードブロックのシンタックスハイライトを制御します。コードノートには影響しません。",
"color-scheme": "カラースキーム"
},
"code_block": {
"word_wrapping": "単語の折り返し",
"theme_none": "シンタックスハイライトなし",
"theme_group_light": "ライトテーマ",
"theme_group_dark": "ダークテーマ",
"copy_title": "クリップボードにコピー"
},
"editor": {
"title": "エディター"
},
"editing": {
"editor_type": {
"label": "書式設定ツールバー",
"floating": {
"description": "編集ツールがカーソル付近に表示されます;"
},
"fixed": {
"title": "固定",
"description": "編集ツールは「書式設定」のリボンタブに表示されます。"
},
"multiline-toolbar": "ツールバーが収まりきらない場合は、複数行で表示する。"
}
},
"electron_context_menu": {
"add-term-to-dictionary": "辞書に \"{{term}}\" を追加",
"cut": "カット",
"copy": "コピー",
"copy-link": "リンクをコピー",
"paste": "ペースト",
"paste-as-plain-text": "プレーンテキストで貼り付け",
"search_online": "{{searchEngine}} で \"{{term}}\" を検索"
},
"duration": {
"seconds": "秒",
"minutes": "分",
"hours": "時間",
"days": "日"
},
"share": {
"title": "共有設定",
"redirect_bare_domain": "ネイキッドドメインを共有ページにリダイレクト",
"redirect_bare_domain_description": "匿名のユーザーをログイン画面ではなく共有ページにリダイレクトします",
"show_login_link_description": "共有ページの下部にログインリンクを追加",
"share_root_found": "共有ルートノート '{{noteTitle}}' の準備が完了",
"share_root_not_found": "#shareRoot のラベルが付いたノートが見つかりません",
"share_root_not_shared": "ノート '{{noteTitle}}' は #shareRoot のラベルを持っていますが、共有されていません"
},
"time_selector": {
"invalid_input": "入力された時間値が有効な数値ではありません。",
"minimum_input": "入力された時間値は {{minimumSeconds}} 秒以上である必要があります。"
},
"note_language": {
"not_set": "未設定",
"configure-languages": "言語を設定..."
},
"content_language": {
"title": "コンテンツの言語",
"description": "読み取り専用または編集可能なテキストノートの基本プロパティセクションの言語選択に表示する言語を 1 つ以上選択します。これにより、スペルチェックや右から左へのサポートなどの機能が利用できるようになります。"
},
"png_export_button": {
"button_title": "図をPNG形式でエクスポート"
},
"svg": {
"export_to_png": "図をPNG形式でエクスポートできませんでした。"
},
"code_theme": {
"title": "外観",
"word_wrapping": "単語の折り返し",
"color-scheme": "カラースキーム"
},
"cpu_arch_warning": {
"title": "ARM64版をダウンロードしてください",
"dont_show_again": "この警告を二度と表示しない",
"continue_anyway": "とにかく続ける"
},
"editorfeatures": {
"emoji_completion_enabled": "絵文字のオートコンプリートを有効",
"note_completion_enabled": "ノートのオートコンプリートを有効"
},
"table_context_menu": {
"delete_row": "行を削除"
},
"content_renderer": {
"open_externally": "外部で開く"
},
"ai_llm": {
"title": "AI 設定",
"enable_ai_features": "AI/LLM 機能を有効化",
"enable_ai_description": "ートの要約、コンテンツ生成、その他のLLM機能などのAI機能を有効にする",
"openai_tab": "OpenAI",
"anthropic_tab": "Anthropic",
"voyage_tab": "Voyage AI",
"ollama_tab": "Ollama",
"enable_ai": "AI/LLM 機能を有効化",
"enable_ai_desc": "ートの要約、コンテンツ生成、その他のLLM機能などのAI機能を有効にする",
"provider_configuration": "AI プロバイダーの設定",
"provider_precedence": "プロバイダーの優先順位",
"provider_precedence_description": "カンマで区切られたプロバイダーの優先順位リスト(例: 'openai,anthropic,ollama'",
"temperature_description": "応答のランダム性を制御する0 = 決定的、2 = 最大ランダム性)",
"system_prompt_description": "すべてのAIとの対話に使用されるデフォルトのシステムプロンプト",
"system_prompt": "システムプロンプト",
"openai_configuration": "OpenAIの設定",
"openai_settings": "OpenAIの設定",
"api_key": "APIキー",
"model": "モデル",
"openai_api_key_description": "OpenAIのAIサービスにアクセスするためのAPIキー",
"anthropic_api_key_description": "AnthropicのClaudeモデルにアクセスするためのAPIキー",
"default_model": "デフォルトモデル",
"openai_model_description": "例: gpt-4o, gpt-4-turbo, gpt-3.5-turbo",
"openai_url_description": "デフォルト: https://api.openai.com/v1",
"anthropic_settings": "Anthropicの設定",
"anthropic_model_description": "チャット補完用のAnthropic Claudeモデル",
"voyage_settings": "Voyage AIの設定",
"ollama_settings": "Ollamaの設定",
"ollama_url_description": "Ollama API の URLデフォルト: http://localhost:11434",
"anthropic_url_description": "Anthropic APIのベースURLデフォルト: https://api.anthropic.com",
"ollama_model_description": "チャット補完用のOllamaモデル",
"anthropic_configuration": "Anthropicの設定",
"voyage_configuration": "Voyage AIの設定",
"voyage_url_description": "デフォルト: https://api.voyageai.com/v1",
"ollama_configuration": "Ollamaの設定",
"enable_ollama": "Ollamaを有効",
"enable_ollama_description": "ローカルAIモデルを利用するためOllamaを有効にする",
"ollama_url": "Ollama URL",
"ollama_model": "Ollamaモデル",
"refresh_models": "モデルを更新",
"refreshing_models": "更新中...",
"error": "エラー",
"retry": "再試行",
"partial": "{{ percentage }}%完了",
"processing": "処理中({{percentage}}%",
"complete": "完了100%",
"refreshing": "更新中...",
"auto_refresh_notice": "{{seconds}}秒ごとに自動更新",
"ai_settings": "AI 設定",
"api_key_tooltip": "サービスにアクセスするためのAPIキー",
"empty_key_warning": {
"anthropic": "Anthropic APIキーが空です。有効な APIキーを入力してください。",
"openai": "OpenAI APIキーが空です。有効なAPIキーを入力してください。",
"voyage": "Voyage APIキーが空です。有効なAPIキーを入力してください。",
"ollama": "Ollama APIキーが空です。有効なAPIキーを入力してください。"
},
"agent": {
"processing": "処理中...",
"loading": "読み込み中...",
"generating": "生成中..."
},
"name": "AI",
"openai": "OpenAI",
"error_contacting_provider": "AIプロバイダーへの接続中にエラーが発生しました。設定とインターネット接続をご確認ください。",
"ollama_no_url": "Ollamaは設定されていません。有効なURLを入力してください。",
"chat": {
"root_note_title": "AIチャット",
"root_note_content": "このメモには、保存されたAIチャットの会話が含まれています。",
"new_chat_title": "新しいチャット",
"create_new_ai_chat": "新しいAIチャットを作成"
},
"create_new_ai_chat": "新しいAIチャットを作成",
"configuration_warnings": "AIの設定に問題があります。設定を確認してください。",
"experimental_warning": "LLM機能は現在実験的です - ご注意ください。",
"selected_provider_description": "チャットおよび補完機能のAIプロバイダーを選択する",
"selected_provider": "プロバイダーを選択",
"select_model": "モデルを選択...",
"select_provider": "プロバイダーを選択..."
},
"add_label": {
"add_label": "ラベルを追加",
"label_name_placeholder": "ラベル名",
"label_name_title": "英数字、アンダーバー、コロンが使用可能な文字です。",
"new_value_placeholder": "新しい値"
},
"delete_label": {
"delete_label": "ラベルを削除",
"label_name_placeholder": "ラベル名",
"label_name_title": "英数字、アンダーバー、コロンが使用可能な文字です。"
},
"rename_label": {
"rename_label": "ラベル名の変更",
"rename_label_from": "ラベル名を",
"old_name_placeholder": "古い名前",
"to": "から",
"new_name_placeholder": "新しい名前",
"name_title": "英数字、アンダーバー、コロンが使用可能な文字です。"
},
"update_label_value": {
"update_label_value": "ラベルの値を更新",
"label_name_placeholder": "ラベル名",
"label_name_title": "英数字、アンダーバー、コロンが使用可能な文字です。",
"new_value_placeholder": "新しい値",
"help_text_note": "このメソッドは値なしで呼び出すこともできます。その場合、値なしでラベルがノートに割り当てられます。"
},
"add_relation": {
"add_relation": "関係を追加",
"relation_name": "関係の名前",
"allowed_characters": "英数字、アンダーバー、コロンが使用可能な文字です。"
},
"delete_relation": {
"delete_relation": "関係を削除",
"relation_name": "関係の名前",
"allowed_characters": "英数字、アンダーバー、コロンが使用可能な文字です。"
},
"rename_relation": {
"rename_relation": "関係の名前を変更",
"rename_relation_from": "関係の名前を",
"old_name": "古い名前",
"to": "から",
"new_name": "新しい名前",
"allowed_characters": "英数字、アンダーバー、コロンが使用可能な文字です。"
},
"update_relation_target": {
"update_relation": "関係の更新",
"relation_name": "関係の名前",
"allowed_characters": "英数字、アンダーバー、コロンが使用可能な文字です。"
},
"revisions_button": {
"note_revisions": "ノートの変更履歴"
},
"note_launcher": {
"this_launcher_doesnt_define_target_note": "このランチャーはターゲットノートを定義していません。"
},
"zpetne_odkazy": {
"backlink": "{{count}} バックリンク",
"backlinks": "{{count}} バックリンク",
"relation": "関係"
},
"mobile_detail_menu": {
"delete_this_note": "このノートを削除",
"error_unrecognized_command": "認識されないコマンド {{command}}"
},
"inherited_attribute_list": {
"title": "継承属性",
"no_inherited_attributes": "継承属性ではありません。"
},
"note_map": {
"open_full": "拡大表示",
"collapse": "通常サイズに折りたたむ",
"title": "ノートマップ",
"link-distance": "リンク距離"
},
"owned_attribute_list": {
"owned_attributes": "所有属性"
},
"promoted_attributes": {
"unset-field-placeholder": "未設定",
"open_external_link": "外部でリンクを開く",
"unknown_label_type": "不明なラベルタイプ '{{type}}'",
"unknown_attribute_type": "不明な属性タイプ '{{type}}'",
"add_new_attribute": "新しい属性を追加",
"remove_this_attribute": "この属性を削除",
"remove_color": "このカラーラベルを削除"
},
"relation_map": {
"open_in_new_tab": "新しいタブで開く",
"remove_note": "ノートを削除",
"edit_title": "タイトルを編集",
"rename_note": "ノート名を変更",
"enter_new_title": "新しいノート名を入力:",
"remove_relation": "関係を削除",
"confirm_remove_relation": "本当にこの関係を削除しますか?",
"specify_new_relation_name": "新しい関係の名前(使用可能な文字: 英数字、コロン、アンダースコア)を指定:",
"note_not_found": "ノート {{noteId}} が見つかりません!",
"enter_title_of_new_note": "新しいノートのタイトルを入力",
"default_new_note_title": "新しいノート"
},
"database_anonymization": {
"title": "データベースの匿名化",
"full_anonymization": "完全匿名化",
"full_anonymization_description": "この操作により、データベースの新しいコピーが作成され、匿名化されます(すべてのノートの内容を削除し、構造と一部の非機密メタデータのみを残します)。これにより、個人データが漏洩する心配なく、デバッグ目的でオンライン共有できます。",
"save_fully_anonymized_database": "完全に匿名化されたデータベースを保存",
"light_anonymization": "軽い匿名化",
"light_anonymization_description": "この操作により、データベースの新しいコピーが作成され、軽い匿名化が適用されます。具体的には、すべてのートの内容のみが削除され、タイトルと属性はそのまま残ります。さらに、カスタムJSフロントエンド/バックエンドスクリプトノートとカスタムウィジェットもそのまま残ります。これにより、問題のデバッグのためのコンテキストがより多く提供されます。",
"choose_anonymization": "完全に匿名化したデータベースを提供するか、軽く匿名化したデータベースを提供するかは、あなた自身が決めることができます。完全に匿名化されたDBであっても非常に有用ですが、場合によっては軽く匿名化されたDBの方がバグの特定と修正のプロセスを速めることができます。",
"save_lightly_anonymized_database": "軽く匿名化されたデータベースを保存",
"existing_anonymized_databases": "既存の匿名化データベース",
"creating_fully_anonymized_database": "完全に匿名化されたデータベースを作成中...",
"creating_lightly_anonymized_database": "軽く匿名化されたデータベースを作成中...",
"error_creating_anonymized_database": "匿名化データベースの作成に失敗しました。詳細はバックエンドログを確認してください",
"successfully_created_fully_anonymized_database": "完全に匿名化されたデータベースを {{anonymizedFilePath}} に作成",
"successfully_created_lightly_anonymized_database": "軽く匿名化されたデータベースを {{anonymizedFilePath}} に作成",
"no_anonymized_database_yet": "匿名化されたデータベースはまだありません。"
},
"database_integrity_check": {
"title": "データベースの整合性チェック",
"description": "これは、データベースがSQLiteレベルで破損がないかをチェックします。DBのサイズによっては時間がかかる場合があります。",
"check_button": "データベースの整合性をチェック",
"checking_integrity": "データベースの整合性をチェックしています...",
"integrity_check_succeeded": "整合性チェックに成功 - 問題は見つかりませんでした。",
"integrity_check_failed": "整合性チェックに失敗: {{results}}"
},
"code-editor-options": {
"title": "エディター"
},
"search_string": {
"title_column": "文字列検索:",
"search_syntax": "検索構文",
"also_see": "詳しくは",
"complete_help": "検索構文に関する完全なヘルプ",
"full_text_search": "テキストを入力すると全文検索が可能",
"label_abc": "ラベルabcを持つートを返す",
"label_year": "ラベル「year」の値が「2019」と一致するート",
"label_rock_pop": "rock と pop のラベルを持つノート",
"label_rock_or_pop": "どれかのラベルが存在すること",
"label_year_comparison": "数値比較(>、>=、<も含む)。",
"label_date_created": "過去1ヶ月以内に作成されたート",
"error": "検索エラー: {{error}}",
"search_prefix": "検索:"
},
"delete_revisions": {
"delete_note_revisions": "ノートの変更履歴を削除",
"all_past_note_revisions": "一致したノートの過去の変更履歴がすべて削除されます。ノート自体は完全に保持されます。言い換えると、ノートのリビジョンが削除されます。"
},
"rename_note": {
"rename_note": "ノート名を変更",
"new_note_title": "新しいノート名",
"rename_note_title_to": "ノート名を変更",
"example_note": "<code>Note</code> - マッチしたノートの名前をすべて'Note'に変更",
"example_new_title": "<code>NEW: ${note.title}</code> - 一致したノートの名前の前に 'NEW: ' を付ける",
"example_date_prefix": "<code>${note.dateCreatedObj.format('MM-DD:')}: ${note.title}</code> - マッチしたノートの前にノートの作成月日を付ける",
"api_docs": "詳細については、 <a href='https://zadam.github.io/trilium/backend_api/Note.html'>note</a> および <a href='https://day.js.org/docs/en/display/format'>dateCreatedObj / utcDateCreatedObj properties</a> の API ドキュメントを参照してください。",
"evaluated_as_js_string": "与えられた値はJavaScript文字列として評価されるため、注入された<code>note</code>変数noteは名前が変更されますを介して動的なコンテンツで強化できます。例:"
},
"electron_integration": {
"desktop-application": "デスクトップアプリケーション",
"native-title-bar": "ネイティブタイトルバー",
"native-title-bar-description": "WindowsとmacOSでは、ネイティブタイトルバーをオフにしておくと、アプリケーションがよりコンパクトに見えます。Linuxでは、ネイティブタイトルバーを表示したままの方が、他のシステムとの統一性が高まります。",
"background-effects": "背景効果を有効化Windows 11のみ",
"background-effects-description": "Mica効果は、アプリのウィンドウにぼかされたスタイリッシュな背景を追加し、奥行きとモダンな外観を作り出します。",
"restart-app-button": "アプリケーションを再起動して変更を反映",
"zoom-factor": "ズーム倍率"
},
"zoom_factor": {
"description": "ズームは CTRL+- と CTRL+= のショートカットでも操作可能。",
"title": "ズーム倍率(デスクトップビルドのみ)"
}
}

View File

@@ -0,0 +1,29 @@
{
"about": {
"title": "Trilium Notes에 대해서",
"homepage": "홈페이지:",
"app_version": "앱 버전:",
"db_version": "DB 버전:",
"sync_version": "동기화 버전:",
"build_date": "빌드 날짜:",
"build_revision": "빌드 리비전:",
"data_directory": "데이터 경로:"
},
"toast": {
"critical-error": {
"title": "심각한 오류",
"message": "클라이언트 애플리케이션 시작 도중 심각한 오류가 발생했습니다:\n\n{{message}}\n\n이는 스크립트가 예기치 않게 실패하면서 발생한 것일 수 있습니다. 애플리케이션을 안전 모드로 시작한 뒤 문제를 해결해 보세요."
},
"widget-error": {
"title": "위젯 초기화 실패"
}
},
"add_link": {
"add_link": "링크 추가",
"note": "노트",
"search_note": "이름으로 노트 검색하기"
},
"branch_prefix": {
"save": "저장"
}
}

View File

@@ -0,0 +1,9 @@
{
"about": {
"title": "Over Trilium Notes",
"homepage": "Homepagina:",
"app_version": "App versie:",
"db_version": "DB Versie:",
"sync_version": "Sync Versie:"
}
}

View File

@@ -0,0 +1,41 @@
{
"about": {
"title": "O notatkach Trilium",
"homepage": "Strona główna:",
"app_version": "Wersja aplikacji:",
"db_version": "Wersja bazy danych:",
"sync_version": "Wersja synchronizacji:",
"build_date": "Zbudowano:",
"build_revision": "Rewizja zbudowania:",
"data_directory": "Katalog z danymi:"
},
"toast": {
"critical-error": {
"title": "Błąd krytyczny",
"message": "Wystąpił krytyczny błąd uniemożliwiający uruchomienie aplikacji:\n\n{{message}}\n\nJest to spowodowane najprawdopodobniej niespodziewanym błędem skryptu. Spróbuj uruchomić aplikację ponownie w trybie bezpiecznym i zaadresuj problem."
}
},
"add_link": {
"add_link": "Dodaj link"
},
"branch_prefix": {
"save": "Zapisz"
},
"bulk_actions": {
"labels": "Etykiety",
"notes": "Notatki",
"other": "Inne",
"relations": "Powiązania"
},
"confirm": {
"ok": "OK",
"cancel": "Anuluj"
},
"delete_notes": {
"cancel": "Anuluj",
"close": "Zamknij"
},
"export": {
"close": "Zamknij"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -40,7 +40,7 @@
"add_relation": {
"add_relation": "Adaugă relație",
"allowed_characters": "Sunt permise doar caractere alfanumerice, underline și două puncte.",
"create_relation_on_all_matched_notes": "Crează relația pentru toate notițele găsite",
"create_relation_on_all_matched_notes": "Creează relația pentru toate notițele găsite.",
"relation_name": "denumirea relației",
"target_note": "notița destinație",
"to": "către"
@@ -76,9 +76,9 @@
"attachment_erasure_timeout": {
"attachment_auto_deletion_description": "Atașamentele se șterg automat (permanent) dacă nu sunt referențiate de către notița lor părinte după un timp prestabilit de timp.",
"attachment_erasure_timeout": "Perioadă de ștergere a atașamentelor",
"erase_attachments_after": "Erase unused attachments after:",
"erase_attachments_after": "Șterge atașamentele neutilizate după:",
"erase_unused_attachments_now": "Elimină atașamentele șterse acum",
"manual_erasing_description": "Șterge acum toate atașamentele nefolosite din notițe",
"manual_erasing_description": "Puteți șterge atașamentele nefolosite manual (fără a lua în considerare timpul de mai sus):",
"unused_attachments_erased": "Atașamentele nefolosite au fost șterse."
},
"attachment_list": {
@@ -141,7 +141,7 @@
"hide_promoted_attributes": "Ascunde lista atributelor promovate pentru această notiță",
"hide_relations": "lista denumirilor relațiilor ce trebuie ascunse, delimitate prin virgulă. Toate celelalte vor fi afișate.",
"icon_class": "valoarea acestei etichete este adăugată ca o clasă CSS la iconița notiței din ierarhia notițelor, fapt ce poate ajuta la identificarea vizuală mai rapidă a notițelor. Un exemplu ar fi „bx bx-home” pentru iconițe preluate din boxicons. Poate fi folosită în notițe de tip șablon.",
"inbox": "locația implicită în care vor apărea noile notițe atunci când se crează o noitiță utilizând butonul „Crează notiță” din bara laterală, notițele vor fi create în interiorul notiței cu această etichetă.",
"inbox": "locația implicită în care vor apărea noile notițe atunci când se crează o noitiță utilizând butonul „Crează notiță” din bara laterală, notițele vor fi create în interiorul notiței marcată cu eticheta <code>#inbox</code>.",
"inherit": "atributele acestei notițe vor fi moștenite chiar dacă nu există o relație părinte-copil între notițe. A se vedea relația de tip șablon pentru un concept similar. De asemenea, a se vedea moștenirea atributelor în documentație.",
"inheritable": "Moștenibilă",
"inheritable_title": "Atributele moștenibile vor fi moștenite de către toți descendenții acestei notițe.",
@@ -177,7 +177,7 @@
"render_note": "relație ce definește notița (de tip notiță de cod HTML sau script) ce trebuie randată pentru notițele de tip „Randare notiță HTML”",
"run": "definește evenimentele la care să ruleze scriptul. Valori acceptate:\n<ul>\n<li>frontendStartup - când pornește interfața Trilium (sau este reîncărcată), dar nu pe mobil.</li>\n<li>mobileStartup - când pornește interfața Trilium (sau este reîncărcată), doar pe mobil.</li>\n<li>backendStartup - când pornește serverul Trilium</li>\n<li>hourly - o dată pe oră. Se poate utiliza adițional eticheta <code>runAtHour</code> pentru a specifica ora.</li>\n<li>daily - o dată pe zi</li>\n</ul>",
"run_at_hour": "La ce oră ar trebui să ruleze. Trebuie folosit împreună cu <code>#run=hourly</code>. Poate fi definit de mai multe ori pentru a rula de mai multe ori în cadrul aceleași zile.",
"run_on_attribute_change": "se execută atunci când atributele unei notițe care definește această relație se schimbă. Se apelează și atunci când un atribut este șters",
"run_on_attribute_change": " se execută atunci când atributele unei notițe care definește această relație se schimbă. Se apelează și atunci când un atribut este șters",
"run_on_attribute_creation": "se execută atunci când un nou atribut este creat pentru notița care definește această relație",
"run_on_branch_change": "se execută atunci când o ramură este actualizată.",
"run_on_branch_creation": "se execută când o ramură este creată. O ramură este o legătură dintre o notiță părinte și o notiță copil și este creată, spre exemplu, la clonarea sau mutarea unei notițe.",
@@ -198,7 +198,7 @@
"share_disallow_robot_indexing": "împiedică indexarea conținutului de către roboți utilizând antetul <code>X-Robots-Tag: noindex</code>",
"share_external_link": "notița va funcționa drept o legătură către un site web extern în ierarhia de partajare",
"share_favicon": "Notiță ce conține pictograma favicon pentru a fi setată în paginile partajate. De obicei se poate seta în rădăcina ierarhiei de partajare și se poate face moștenibilă. Notița ce conține favicon-ul trebuie să fie și ea în ierarhia de partajare. Considerați și utilizarea „share_hidden_from_tree”.",
"share_hidden_from_tree": "notița este ascunsă din arborele de navigație din stânga, dar încă este accesibilă prin intermediul unui URL.",
"share_hidden_from_tree": "notița este ascunsă din arborele de navigație din stânga, dar încă este accesibilă prin intermediul unui URL",
"share_index": "notițele cu această etichetă vor afișa lista tuturor rădăcilor notițelor partajate",
"share_js": "Notiță JavaScript ce va fi injectată în pagina de partajare. Notița respectivă trebuie să fie și ea în ierarhia de partajare. Considerați utilizarea 'share_hidden_from_tree'.",
"share_omit_default_css": "CSS-ul implicit pentru pagina de partajare va fi omis. Se poate folosi atunci când se fac schimbări majore de stil la pagină.",
@@ -214,7 +214,7 @@
"target_note_title": "Relația este o conexiune numită dintre o notiță sursă și o notiță țintă.",
"template": "Șablon",
"text": "Text",
"title_template": "titlul implicit al notițelor create în interiorul acestei notițe. Valoarea este evaluată ca un șir de caractere JavaScript\n și poate fi astfel îmbogățită cu un conținut dinamic prin intermediul variabilelow <code>now</code> și <code>parentNote</code>. Exemple:\n \n <ul>\n <li><code>Lucrările lui ${parentNote.getLabelValue('autor')}</code></li>\n <li><code>Jurnal pentru ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n A se vedea <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">wiki-ul pentru detalii</a>, documentația API pentru <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> și <a href=\"https://day.js.org/docs/en/display/format\">now</a> pentru mai multe informații",
"title_template": "titlul implicit al notițelor create în interiorul acestei notițe. Valoarea este evaluată ca un șir de caractere JavaScript\n și poate fi astfel îmbogățită cu un conținut dinamic prin intermediul variabilelor <code>now</code> și <code>parentNote</code>. Exemple:\n \n <ul>\n <li><code>Lucrările lui ${parentNote.getLabelValue('autor')}</code></li>\n <li><code>Jurnal pentru ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n A se vedea <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">wiki-ul pentru detalii</a>, documentația API pentru <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> și <a href=\"https://day.js.org/docs/en/display/format\">now</a> pentru mai multe informații.",
"toc": "<code>#toc</code> sau <code>#toc=show</code> forțează afișarea tabelei de conținut, <code>#toc=hide</code> forțează ascunderea ei. Dacă eticheta nu există, se utilizează setările globale",
"top": "păstrează notița la începutul listei (se aplică doar pentru notițe sortate automat)",
"url": "URL",
@@ -369,7 +369,7 @@
},
"confirm": {
"also_delete_note": "Șterge și notița",
"are_you_sure_remove_note": "Doriți ștergerea notiței „{{title}}” din harta de relații?",
"are_you_sure_remove_note": "Doriți ștergerea notiței „{{title}}” din harta de relații? ",
"cancel": "Anulează",
"confirmation": "Confirm",
"if_you_dont_check": "Dacă această opțiune nu este bifată, notița va fi ștearsă doar din harta de relații.",
@@ -519,8 +519,8 @@
"export_status": "Starea exportului",
"export_type_single": "Doar această notiță fără descendenții ei",
"export_type_subtree": "Această notiță și toți descendenții ei",
"format_html_zip": "HTML în arhivă ZIP - recomandat deoarece păstrează toată formatarea",
"format_markdown": "Markdown - păstrează majoritatea formatării",
"format_html_zip": "HTML în arhivă ZIP - recomandat deoarece păstrează toată formatarea.",
"format_markdown": "Markdown - păstrează majoritatea formatării.",
"format_opml": "OPML - format de interschimbare pentru editoare cu structură ierarhică (outline). Formatarea, imaginile și fișierele nu vor fi incluse.",
"opml_version_1": "OPML v1.0 - text simplu",
"opml_version_2": "OPML v2.0 - permite și HTML",
@@ -640,7 +640,7 @@
"newTabNoteLink": "pe o legătură către o notiță va deschide notița într-un tab nou",
"notSet": "nesetat",
"noteNavigation": "Navigarea printre notițe",
"numberedList": "<kbd>1.</code> sau <code>1)</code> urmat de spațiu pentru o listă numerotată",
"numberedList": "<code>1.</code> sau <code>1)</code> urmat de spațiu pentru o listă numerotată",
"onlyInDesktop": "Doar pentru desktop (aplicația Electron)",
"openEmptyTab": "deschide un tab nou",
"other": "Altele",
@@ -655,7 +655,8 @@
"showSQLConsole": "afișează consola SQL",
"tabShortcuts": "Scurtături pentru tab-uri",
"troubleshooting": "Unelte pentru depanare",
"newTabWithActivationNoteLink": "pe o legătură către o notiță deschide și activează notița într-un tab nou"
"newTabWithActivationNoteLink": "pe o legătură către o notiță deschide și activează notița într-un tab nou",
"title": "Ghid rapid"
},
"hide_floating_buttons_button": {
"button_title": "Ascunde butoanele"
@@ -806,7 +807,7 @@
"dialog_title": "Mută notițele în...",
"error_no_path": "Nicio cale la care să poată fi mutate.",
"move_button": "Mută la notița selectată",
"move_success_message": "Notițele selectate au fost mutate în",
"move_success_message": "Notițele selectate au fost mutate în ",
"notes_to_move": "Notițe de mutat",
"search_placeholder": "căutați notița după denumirea ei",
"target_parent_note": "Notița părinte destinație"
@@ -886,7 +887,8 @@
"modal_title": "Selectați tipul notiței",
"templates": "Șabloane",
"change_path_prompt": "Selectați locul unde să se creeze noua notiță:",
"search_placeholder": "căutare cale notiță după nume (cea implicită dacă este necompletat)"
"search_placeholder": "căutare cale notiță după nume (cea implicită dacă este necompletat)",
"builtin_templates": "Șabloane predefinite"
},
"onclick_button": {
"no_click_handler": "Butonul „{{componentId}}” nu are nicio acțiune la clic definită"
@@ -940,7 +942,9 @@
},
"password_not_set": {
"body1": "Notițele protejate sunt criptate utilizând parola de utilizator, dar nu a fost setată nicio parolă.",
"title": "Parola nu este setată"
"title": "Parola nu este setată",
"body2": "Pentru a putea proteja notițe, clic pe butonul de mai jos pentru a deschide fereastra de opțiuni și pentru a seta parola.",
"go_to_password_options": "Mergi la setările de parolă"
},
"promoted_attributes": {
"add_new_attribute": "Adaugă un nou atribut",
@@ -1054,7 +1058,7 @@
"download_button": "Descarcă",
"file_size": "Dimensiune fișier:",
"help_title": "Informații despre reviziile notițelor",
"mime": "MIME:",
"mime": "MIME: ",
"no_revisions": "Nu există încă nicio revizie pentru această notiță...",
"note_revisions": "Revizii ale notiței",
"preview": "Previzualizare:",
@@ -1072,7 +1076,7 @@
"note_revisions": "Revizii ale notiței"
},
"revisions_snapshot_interval": {
"note_revisions_snapshot_description": "Intervalul de salvare a reviziilor este timpul după care se crează o nouă revizie a unei notițe. Vedeți <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki-ul</a> pentru mai multe informații.",
"note_revisions_snapshot_description": "Intervalul de salvare a reviziilor este timpul după care se crează o nouă revizie a unei notițe. Vedeți <doc>wiki-ul</doc> pentru mai multe informații.",
"note_revisions_snapshot_interval_title": "Intervalul de salvare a reviziilor",
"snapshot_time_interval_label": "Intervalul de salvare a reviziilor:"
},
@@ -1102,7 +1106,7 @@
"limit_description": "Limitează numărul de rezultate",
"order_by": "ordonează după",
"save_to_note": "Salvează în notiță",
"search_button": "Căutare <kbd>Enter</kbd>",
"search_button": "Căutare",
"search_execute": "Caută și execută acțiunile",
"search_note_saved": "Notița de căutare a fost salvată în {{- notePathTitle}}",
"search_parameters": "Parametrii de căutare",
@@ -1189,7 +1193,7 @@
"enable": "Activează corectorul ortografic",
"language_code_label": "Codurile de limbă",
"language_code_placeholder": "de exemplu „en-US”, „de-AT”",
"multiple_languages_info": "Mai multe limbi pot fi separate prin virgulă, e.g. \"en-US, de-DE, cs\".",
"multiple_languages_info": "Mai multe limbi pot fi separate prin virgulă, e.g. \"en-US, de-DE, cs\". ",
"title": "Corector ortografic",
"restart-required": "Schimbările asupra setărilor corectorului ortografic vor fi aplicate după restartarea aplicației."
},
@@ -1243,7 +1247,10 @@
"layout-horizontal-description": "bara de lansare se află sub bara de taburi, bara de taburi este pe toată lungimea.",
"layout-horizontal-title": "Orizontal",
"layout-vertical-title": "Vertical",
"layout-vertical-description": "bara de lansare se află pe stânga (implicit)"
"layout-vertical-description": "bara de lansare se află pe stânga (implicit)",
"auto_theme": "Tema clasică (se adaptează la schema de culori a sistemului)",
"light_theme": "Tema clasică (luminoasă)",
"dark_theme": "Tema clasică (întunecată)"
},
"toast": {
"critical-error": {
@@ -1279,7 +1286,7 @@
"update_relation_target": {
"allowed_characters": "Sunt permise doar caractere alfanumerice, underline și două puncte.",
"change_target_note": "schimbă notița-țintă a unei relații existente",
"on_all_matched_notes": "Pentru toate notițele găsite:",
"on_all_matched_notes": "Pentru toate notițele găsite",
"relation_name": "denumirea relației",
"target_note": "notița destinație",
"to": "la",
@@ -1307,7 +1314,7 @@
"use_vim_keybindings_in_code_notes": "Combinații de taste Vim"
},
"web_view": {
"create_label": "Pentru a începe, creați o etichetă cu adresa URL de încorporat, e.g. #webViewSrc=\"https://www.google.com\"",
"create_label": "Pentru a începe, creați o etichetă cu adresa URL de încorporat, e.g. #webViewSrc=\"https://www.google.com\"",
"embed_websites": "Notițele de tip „Vizualizare web” permit încorporarea site-urilor web în Trilium.",
"web_view": "Vizualizare web"
},
@@ -1856,15 +1863,20 @@
},
"create_new_ai_chat": "Crează o nouă discuție cu AI-ul",
"configuration_warnings": "Sunt câteva probleme la configurația AI-ului. Verificați setările.",
"experimental_warning": "Funcția LLM este experimentală!",
"experimental_warning": "Funcția LLM este experimentală.",
"selected_provider": "Furnizor selectat",
"selected_provider_description": "Selectați furnizorul de AI pentru funcțiile de discuție și completare",
"select_model": "Selectați modelul...",
"select_provider": "Selectați furnizorul..."
"select_provider": "Selectați furnizorul...",
"ai_enabled": "Funcționalitățile AI au fost activate",
"ai_disabled": "Funcționalitățile AI au fost dezactivate",
"no_models_found_online": "Nu s-a găsit niciun model. Verificați cheia API și configurația.",
"no_models_found_ollama": "Nu s-a găsit niciun model Ollama. Verificați dacă Ollama rulează.",
"error_fetching": "Eroare la obținerea modelelor: {{error}}"
},
"custom_date_time_format": {
"title": "Format dată/timp personalizat",
"description": "Personalizați formatul de dată și timp inserat prin <kbd></kbd> sau din bara de unelte. Vedeți <a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">Documentația Day.js</a> pentru câmpurile de formatare disponibile.",
"description": "Personalizați formatul de dată și timp inserat prin <shortcut /> sau din bara de unelte. Vedeți <doc>Documentația Day.js</doc> pentru câmpurile de formatare disponibile.",
"format_string": "Șir de formatare:",
"formatted_time": "Data și ora formatate:"
},
@@ -1985,6 +1997,32 @@
"open_externally": "Deschide în afara programului"
},
"modal": {
"close": "Închide"
"close": "Închide",
"help_title": "Afișează mai multe informații despre acest ecran"
},
"call_to_action": {
"background_effects_title": "Efectele de fundal sunt acum stabile",
"background_effects_message": "Pe dispozitive cu Windows, efectele de fundal sunt complet stabile. Acestea adaugă un strop de culoare interfeței grafice prin estomparea fundalului din spatele ferestrei. Această tehnică este folosită și în alte aplicații precum Windows Explorer.",
"background_effects_button": "Activează efectele de fundal",
"next_theme_title": "Încercați noua temă Trilium",
"next_theme_message": "Utilizați tema clasică, doriți să încercați noua temă?",
"next_theme_button": "Testează noua temă",
"dismiss": "Treci peste"
},
"ui-performance": {
"title": "Setări de performanță",
"enable-motion": "Activează tranzițiile și animațiile",
"enable-shadows": "Activează umbrirea elementelor",
"enable-backdrop-effects": "Activează efectele de fundal pentru meniuri, popup-uri și panouri"
},
"settings": {
"related_settings": "Setări similare"
},
"settings_appearance": {
"related_code_blocks": "Tema de culori pentru blocuri de cod în notițe de tip text",
"related_code_notes": "Tema de culori pentru notițele de tip cod"
},
"units": {
"percentage": "%"
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,69 +1,80 @@
{
"about": {
"homepage": "Trang chủ:",
"title": "Về Trilium Notes"
},
"add_link": {
"add_link": "Thêm liên kết",
"button_add_link": "Thêm liên kết"
},
"bulk_actions": {
"other": "Khác"
},
"branch_prefix": {
"save": "Lưu"
},
"confirm": {
"ok": "OK",
"cancel": "Huỷ"
},
"delete_notes": {
"close": "Đóng",
"ok": "OK",
"cancel": "Huỷ"
},
"export": {
"close": "Đóng"
},
"help": {
"other": "Khác"
},
"toast": {
"critical-error": {
"title": "Lỗi nghiêm trọng"
}
},
"import": {
"options": "Tuỳ chọn"
},
"info": {
"okButton": "OK",
"closeButton": "Đóng"
},
"move_to": {
"dialog_title": "Chuyển ghi chép tới..."
},
"prompt": {
"ok": "OK"
},
"protected_session_password": {
"close_label": "Đóng"
},
"revisions": {
"restore_button": "Khôi phục",
"delete_button": "Xoá"
},
"upload_attachments": {
"options": "Tuỳ chọn"
},
"attribute_detail": {
"name": "Tên",
"value": "Giá trị",
"text": "Văn bản",
"number": "Số",
"delete": "Xoá"
},
"rename_note": {
"rename_note": "Đổi tên ghi chép"
"about": {
"homepage": "Trang chủ:",
"title": "Về Trilium Notes"
},
"add_link": {
"add_link": "Thêm liên kết",
"button_add_link": "Thêm liên kết"
},
"bulk_actions": {
"other": "Khác"
},
"branch_prefix": {
"save": "Lưu"
},
"confirm": {
"ok": "OK",
"cancel": "Huỷ"
},
"delete_notes": {
"close": "Đóng",
"ok": "OK",
"cancel": "Huỷ"
},
"export": {
"close": "Đóng"
},
"help": {
"other": "Khác"
},
"toast": {
"critical-error": {
"title": "Lỗi nghiêm trọng"
}
},
"import": {
"options": "Tuỳ chọn"
},
"info": {
"okButton": "OK",
"closeButton": "Đóng"
},
"move_to": {
"dialog_title": "Chuyển ghi chép tới..."
},
"prompt": {
"ok": "OK"
},
"protected_session_password": {
"close_label": "Đóng"
},
"revisions": {
"restore_button": "Khôi phục",
"delete_button": "Xoá"
},
"upload_attachments": {
"options": "Tuỳ chọn"
},
"attribute_detail": {
"name": "Tên",
"value": "Giá trị",
"text": "Văn bản",
"number": "Số",
"delete": "Xoá"
},
"rename_note": {
"rename_note": "Đổi tên ghi chép"
},
"add_label": {
"add_label": "Thêm nhãn",
"label_name_placeholder": "tên nhãn",
"help_text_item2": "hoặc thay đổi giá trị của nhãn có sẵn"
},
"rename_label": {
"rename_label": "Đặt lại tên nhãn"
},
"call_to_action": {
"dismiss": "Bỏ qua"
}
}

View File

@@ -3,7 +3,11 @@ type DateTimeStyle = "full" | "long" | "medium" | "short" | "none" | undefined;
/**
* Formats the given date and time to a string based on the current locale.
*/
export function formatDateTime(date: string | Date | number, dateStyle: DateTimeStyle = "medium", timeStyle: DateTimeStyle = "medium") {
export function formatDateTime(date: string | Date | number | null | undefined, dateStyle: DateTimeStyle = "medium", timeStyle: DateTimeStyle = "medium") {
if (!date) {
return "";
}
const locale = navigator.language;
let parsedDate;

View File

@@ -1,504 +0,0 @@
import { t } from "../../services/i18n.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import noteAutocompleteService, { type Suggestion } from "../../services/note_autocomplete.js";
import server from "../../services/server.js";
import contextMenuService from "../../menus/context_menu.js";
import attributeParser, { type Attribute } from "../../services/attribute_parser.js";
import { AttributeEditor, type EditorConfig, type ModelElement, type MentionFeed, type ModelNode, type ModelPosition } from "@triliumnext/ckeditor5";
import froca from "../../services/froca.js";
import attributeRenderer from "../../services/attribute_renderer.js";
import noteCreateService from "../../services/note_create.js";
import attributeService from "../../services/attributes.js";
import linkService from "../../services/link.js";
import type AttributeDetailWidget from "./attribute_detail.js";
import type { CommandData, EventData, EventListener, FilteredCommandNames } from "../../components/app_context.js";
import type { default as FAttribute, AttributeType } from "../../entities/fattribute.js";
import type FNote from "../../entities/fnote.js";
import { escapeQuotes } from "../../services/utils.js";
const HELP_TEXT = `
<p>${t("attribute_editor.help_text_body1")}</p>
<p>${t("attribute_editor.help_text_body2")}</p>
<p>${t("attribute_editor.help_text_body3")}</p>`;
const TPL = /*html*/`
<div style="position: relative; padding-top: 10px; padding-bottom: 10px">
<style>
.attribute-list-editor {
border: 0 !important;
outline: 0 !important;
box-shadow: none !important;
padding: 0 0 0 5px !important;
margin: 0 !important;
max-height: 100px;
overflow: auto;
transition: opacity .1s linear;
}
.attribute-list-editor.ck-content .mention {
color: var(--muted-text-color) !important;
background: transparent !important;
}
.save-attributes-button {
color: var(--muted-text-color);
position: absolute;
bottom: 14px;
right: 25px;
cursor: pointer;
border: 1px solid transparent;
font-size: 130%;
}
.add-new-attribute-button {
color: var(--muted-text-color);
position: absolute;
bottom: 13px;
right: 0;
cursor: pointer;
border: 1px solid transparent;
font-size: 130%;
}
.add-new-attribute-button:hover, .save-attributes-button:hover {
border: 1px solid var(--button-border-color);
border-radius: var(--button-border-radius);
background: var(--button-background-color);
color: var(--button-text-color);
}
.attribute-errors {
color: red;
padding: 5px 50px 0px 5px; /* large right padding to avoid buttons */
}
</style>
<div class="attribute-list-editor" tabindex="200"></div>
<div class="bx bx-save save-attributes-button tn-tool-button" title="${escapeQuotes(t("attribute_editor.save_attributes"))}"></div>
<div class="bx bx-plus add-new-attribute-button tn-tool-button" title="${escapeQuotes(t("attribute_editor.add_a_new_attribute"))}"></div>
<div class="attribute-errors" style="display: none;"></div>
</div>
`;
const mentionSetup: MentionFeed[] = [
{
marker: "@",
feed: (queryText) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
itemRenderer: (_item) => {
const item = _item as Suggestion;
const itemElement = document.createElement("button");
itemElement.innerHTML = `${item.highlightedNotePathTitle} `;
return itemElement;
},
minimumCharacters: 0
},
{
marker: "#",
feed: async (queryText) => {
const names = await server.get<string[]>(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`);
return names.map((name) => {
return {
id: `#${name}`,
name: name
};
});
},
minimumCharacters: 0
},
{
marker: "~",
feed: async (queryText) => {
const names = await server.get<string[]>(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`);
return names.map((name) => {
return {
id: `~${name}`,
name: name
};
});
},
minimumCharacters: 0
}
];
const editorConfig: EditorConfig = {
toolbar: {
items: []
},
placeholder: t("attribute_editor.placeholder"),
mention: {
feeds: mentionSetup
},
licenseKey: "GPL"
};
type AttributeCommandNames = FilteredCommandNames<CommandData>;
export default class AttributeEditorWidget extends NoteContextAwareWidget implements EventListener<"entitiesReloaded">, EventListener<"addNewLabel">, EventListener<"addNewRelation"> {
private attributeDetailWidget: AttributeDetailWidget;
private $editor!: JQuery<HTMLElement>;
private $addNewAttributeButton!: JQuery<HTMLElement>;
private $saveAttributesButton!: JQuery<HTMLElement>;
private $errors!: JQuery<HTMLElement>;
private textEditor!: AttributeEditor;
private lastUpdatedNoteId!: string | undefined;
private lastSavedContent!: string;
constructor(attributeDetailWidget: AttributeDetailWidget) {
super();
this.attributeDetailWidget = attributeDetailWidget;
}
doRender() {
this.$widget = $(TPL);
this.$editor = this.$widget.find(".attribute-list-editor");
this.initialized = this.initEditor();
this.$editor.on("keydown", async (e) => {
if (e.which === 13) {
// allow autocomplete to fill the result textarea
setTimeout(() => this.save(), 100);
}
this.attributeDetailWidget.hide();
});
this.$editor.on("blur", () => setTimeout(() => this.save(), 100)); // Timeout to fix https://github.com/zadam/trilium/issues/4160
this.$addNewAttributeButton = this.$widget.find(".add-new-attribute-button");
this.$addNewAttributeButton.on("click", (e) => this.addNewAttribute(e));
this.$saveAttributesButton = this.$widget.find(".save-attributes-button");
this.$saveAttributesButton.on("click", () => this.save());
this.$errors = this.$widget.find(".attribute-errors");
}
addNewAttribute(e: JQuery.ClickEvent) {
contextMenuService.show<AttributeCommandNames>({
x: e.pageX,
y: e.pageY,
orientation: "left",
items: [
{ title: t("attribute_editor.add_new_label"), command: "addNewLabel", uiIcon: "bx bx-hash" },
{ title: t("attribute_editor.add_new_relation"), command: "addNewRelation", uiIcon: "bx bx-transfer" },
{ title: "----" },
{ title: t("attribute_editor.add_new_label_definition"), command: "addNewLabelDefinition", uiIcon: "bx bx-empty" },
{ title: t("attribute_editor.add_new_relation_definition"), command: "addNewRelationDefinition", uiIcon: "bx bx-empty" }
],
selectMenuItemHandler: ({ command }) => this.handleAddNewAttributeCommand(command)
});
// Prevent automatic hiding of the context menu due to the button being clicked.
e.stopPropagation();
}
// triggered from keyboard shortcut
async addNewLabelEvent({ ntxId }: EventData<"addNewLabel">) {
if (this.isNoteContext(ntxId)) {
await this.refresh();
this.handleAddNewAttributeCommand("addNewLabel");
}
}
// triggered from keyboard shortcut
async addNewRelationEvent({ ntxId }: EventData<"addNewRelation">) {
if (this.isNoteContext(ntxId)) {
await this.refresh();
this.handleAddNewAttributeCommand("addNewRelation");
}
}
async handleAddNewAttributeCommand(command: AttributeCommandNames | undefined) {
// TODO: Not sure what the relation between FAttribute[] and Attribute[] is.
const attrs = this.parseAttributes() as FAttribute[];
if (!attrs) {
return;
}
let type: AttributeType;
let name;
let value;
if (command === "addNewLabel") {
type = "label";
name = "myLabel";
value = "";
} else if (command === "addNewRelation") {
type = "relation";
name = "myRelation";
value = "";
} else if (command === "addNewLabelDefinition") {
type = "label";
name = "label:myLabel";
value = "promoted,single,text";
} else if (command === "addNewRelationDefinition") {
type = "label";
name = "relation:myRelation";
value = "promoted,single";
} else {
return;
}
// TODO: Incomplete type
//@ts-ignore
attrs.push({
type,
name,
value,
isInheritable: false
});
await this.renderOwnedAttributes(attrs, false);
this.$editor.scrollTop(this.$editor[0].scrollHeight);
const rect = this.$editor[0].getBoundingClientRect();
setTimeout(() => {
// showing a little bit later because there's a conflict with outside click closing the attr detail
this.attributeDetailWidget.showAttributeDetail({
allAttributes: attrs,
attribute: attrs[attrs.length - 1],
isOwned: true,
x: (rect.left + rect.right) / 2,
y: rect.bottom,
focus: "name"
});
}, 100);
}
async save() {
if (this.lastUpdatedNoteId !== this.noteId) {
// https://github.com/zadam/trilium/issues/3090
console.warn("Ignoring blur event because a different note is loaded.");
return;
}
const attributes = this.parseAttributes();
if (attributes) {
await server.put(`notes/${this.noteId}/attributes`, attributes, this.componentId);
this.$saveAttributesButton.fadeOut();
// blink the attribute text to give a visual hint that save has been executed
this.$editor.css("opacity", 0);
// revert back
setTimeout(() => this.$editor.css("opacity", 1), 100);
}
}
parseAttributes() {
try {
return attributeParser.lexAndParse(this.getPreprocessedData());
} catch (e: any) {
this.$errors.text(e.message).slideDown();
}
}
getPreprocessedData() {
const str = this.textEditor
.getData()
.replace(/<a[^>]+href="(#[A-Za-z0-9_/]*)"[^>]*>[^<]*<\/a>/g, "$1")
.replace(/&nbsp;/g, " "); // otherwise .text() below outputs non-breaking space in unicode
return $("<div>").html(str).text();
}
async initEditor() {
this.$widget.show();
this.$editor.on("click", (e) => this.handleEditorClick(e));
this.textEditor = await AttributeEditor.create(this.$editor[0], editorConfig);
this.textEditor.model.document.on("change:data", () => this.dataChanged());
this.textEditor.editing.view.document.on(
"enter",
(event, data) => {
// disable entering new line - see https://github.com/ckeditor/ckeditor5/issues/9422
data.preventDefault();
event.stop();
},
{ priority: "high" }
);
// disable spellcheck for attribute editor
const documentRoot = this.textEditor.editing.view.document.getRoot();
if (documentRoot) {
this.textEditor.editing.view.change((writer) => writer.setAttribute("spellcheck", "false", documentRoot));
}
}
dataChanged() {
this.lastUpdatedNoteId = this.noteId;
if (this.lastSavedContent === this.textEditor.getData()) {
this.$saveAttributesButton.fadeOut();
} else {
this.$saveAttributesButton.fadeIn();
}
if (this.$errors.is(":visible")) {
// using .hide() instead of .slideUp() since this will also hide the error after confirming
// mention for relation name which suits up. When using.slideUp() error will appear and the slideUp which is weird
this.$errors.hide();
}
}
async handleEditorClick(e: JQuery.ClickEvent) {
const pos = this.textEditor.model.document.selection.getFirstPosition();
if (pos && pos.textNode && pos.textNode.data) {
const clickIndex = this.getClickIndex(pos);
let parsedAttrs;
try {
parsedAttrs = attributeParser.lexAndParse(this.getPreprocessedData(), true);
} catch (e) {
// the input is incorrect because the user messed up with it and now needs to fix it manually
return null;
}
let matchedAttr: Attribute | null = null;
for (const attr of parsedAttrs) {
if (attr.startIndex && clickIndex > attr.startIndex && attr.endIndex && clickIndex <= attr.endIndex) {
matchedAttr = attr;
break;
}
}
setTimeout(() => {
if (matchedAttr) {
this.$editor.tooltip("hide");
this.attributeDetailWidget.showAttributeDetail({
allAttributes: parsedAttrs,
attribute: matchedAttr,
isOwned: true,
x: e.pageX,
y: e.pageY
});
} else {
this.showHelpTooltip();
}
}, 100);
} else {
this.showHelpTooltip();
}
}
showHelpTooltip() {
this.attributeDetailWidget.hide();
this.$editor.tooltip({
trigger: "focus",
html: true,
title: HELP_TEXT,
placement: "bottom",
offset: "0,30"
});
this.$editor.tooltip("show");
}
getClickIndex(pos: ModelPosition) {
let clickIndex = pos.offset - (pos.textNode?.startOffset ?? 0);
let curNode: ModelNode | Text | ModelElement | null = pos.textNode;
while (curNode?.previousSibling) {
curNode = curNode.previousSibling;
if ((curNode as ModelElement).name === "reference") {
clickIndex += (curNode.getAttribute("href") as string).length + 1;
} else if ("data" in curNode) {
clickIndex += (curNode.data as string).length;
}
}
return clickIndex;
}
async loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string) {
const { noteId } = linkService.parseNavigationStateFromUrl(href);
const note = noteId ? await froca.getNote(noteId, true) : null;
const title = note ? note.title : "[missing]";
$el.text(title);
}
async refreshWithNote(note: FNote) {
await this.renderOwnedAttributes(note.getOwnedAttributes(), true);
}
async renderOwnedAttributes(ownedAttributes: FAttribute[], saved: boolean) {
// attrs are not resorted if position changes after the initial load
ownedAttributes.sort((a, b) => a.position - b.position);
let htmlAttrs = (await attributeRenderer.renderAttributes(ownedAttributes, true)).html();
if (htmlAttrs.length > 0) {
htmlAttrs += "&nbsp;";
}
this.textEditor.setData(htmlAttrs);
if (saved) {
this.lastSavedContent = this.textEditor.getData();
this.$saveAttributesButton.fadeOut(0);
}
}
async createNoteForReferenceLink(title: string) {
let result;
if (this.notePath) {
result = await noteCreateService.createNoteWithTypePrompt(this.notePath, {
activate: false,
title: title
});
}
return result?.note?.getBestNotePathString();
}
async updateAttributeList(attributes: FAttribute[]) {
await this.renderOwnedAttributes(attributes, false);
}
focus() {
this.$editor.trigger("focus");
this.textEditor.model.change((writer) => {
const documentRoot = this.textEditor.editing.model.document.getRoot();
if (!documentRoot) {
return;
}
const positionAt = writer.createPositionAt(documentRoot, "end");
writer.setSelection(positionAt);
});
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) {
this.refresh();
}
}
}

View File

@@ -1,7 +1,11 @@
import { isValidElement, VNode } from "preact";
import Component, { TypedComponent } from "../components/component.js";
import froca from "../services/froca.js";
import { t } from "../services/i18n.js";
import toastService from "../services/toast.js";
import { renderReactWidget } from "./react/react_utils.jsx";
import { EventNames, EventData } from "../components/app_context.js";
import { Handler } from "leaflet";
export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedComponent<T> {
protected attrs: Record<string, string>;
@@ -22,11 +26,14 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
this.childPositionCounter = 10;
}
child(...components: T[]) {
if (!components) {
child(..._components: (T | VNode)[]) {
if (!_components) {
return this;
}
// Convert any React components to legacy wrapped components.
const components = wrapReactWidgets(_components);
super.child(...components);
for (const component of components) {
@@ -258,3 +265,30 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
* For information on using widgets, see the tutorial {@tutorial widget_basics}.
*/
export default class BasicWidget extends TypedBasicWidget<Component> {}
export function wrapReactWidgets<T extends TypedComponent<any>>(components: (T | VNode)[]) {
const wrappedResult: T[] = [];
for (const component of components) {
if (isValidElement(component)) {
wrappedResult.push(new ReactWrappedWidget(component) as unknown as T);
} else {
wrappedResult.push(component);
}
}
return wrappedResult;
}
export class ReactWrappedWidget extends BasicWidget {
private el: VNode;
constructor(el: VNode) {
super();
this.el = el;
}
doRender() {
this.$widget = renderReactWidget(this, this.el);
}
}

View File

@@ -1,54 +0,0 @@
import SwitchWidget from "./switch.js";
import server from "../services/server.js";
import toastService from "../services/toast.js";
import { t } from "../services/i18n.js";
import type FNote from "../entities/fnote.js";
import type { EventData } from "../components/app_context.js";
// TODO: Deduplicate
type Response = {
success: true;
} | {
success: false;
message: string;
}
export default class BookmarkSwitchWidget extends SwitchWidget {
isEnabled() {
return (
super.isEnabled() &&
// it's not possible to bookmark root because that would clone it under bookmarks and thus create a cycle
!["root", "_hidden"].includes(this.noteId ?? "")
);
}
doRender() {
super.doRender();
this.switchOnName = t("bookmark_switch.bookmark");
this.switchOnTooltip = t("bookmark_switch.bookmark_this_note");
this.switchOffName = t("bookmark_switch.bookmark");
this.switchOffTooltip = t("bookmark_switch.remove_bookmark");
}
async toggle(state: boolean | null | undefined) {
const resp = await server.put<Response>(`notes/${this.noteId}/toggle-in-parent/_lbBookmarks/${!!state}`);
if (!resp.success && "message" in resp) {
toastService.showError(resp.message);
}
}
async refreshWithNote(note: FNote) {
const isBookmarked = !!note.getParentBranches().find((b) => b.parentNoteId === "_lbBookmarks");
this.isToggled = isBookmarked;
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.getBranchRows().find((b) => b.noteId === this.noteId)) {
this.refresh();
}
}
}

View File

@@ -1,6 +1,7 @@
import { ComponentChildren } from "preact";
import { memo } from "preact/compat";
import AbstractBulkAction from "./abstract_bulk_action";
import HelpRemoveButtons from "../react/HelpRemoveButtons";
interface BulkActionProps {
label: string | ComponentChildren;
@@ -24,19 +25,11 @@ const BulkAction = memo(({ label, children, helpText, bulkAction }: BulkActionPr
{children}
</div>
</td>
<td className="button-column">
{helpText && <div className="dropdown help-dropdown">
<span className="bx bx-help-circle icon-action" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<div className="dropdown-menu dropdown-menu-right p-4">
{helpText}
</div>
</div>}
<span
className="bx bx-x icon-action action-conf-del"
onClick={() => bulkAction?.deleteAction()}
/>
</td>
<HelpRemoveButtons
help={helpText}
removeText="Delete"
onRemove={() => bulkAction?.deleteAction()}
/>
</tr>
);
});

View File

@@ -1,11 +1,11 @@
import { t } from "../../../services/i18n.js";
import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action.js";
import AbstractBulkAction from "../abstract_bulk_action.js";
import BulkAction, { BulkActionText } from "../BulkAction.jsx";
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";
import { useEffect, useState } from "preact/hooks";
import { useSpacedUpdate } from "../../react/hooks.jsx";
function MoveNoteBulkActionComponent({ bulkAction, actionDef }: { bulkAction: AbstractBulkAction, actionDef: ActionDefinition }) {
function MoveNoteBulkActionComponent({ bulkAction }: { bulkAction: AbstractBulkAction }) {
const [ targetParentNoteId, setTargetParentNoteId ] = useState<string>();
const spacedUpdate = useSpacedUpdate(() => {
return bulkAction.saveAction({ targetParentNoteId: targetParentNoteId })
@@ -45,6 +45,6 @@ export default class MoveNoteBulkAction extends AbstractBulkAction {
}
doRender() {
return <MoveNoteBulkActionComponent bulkAction={this} actionDef={this.actionDef} />
return <MoveNoteBulkActionComponent bulkAction={this} />
}
}

View File

@@ -1,4 +1,3 @@
import SpacedUpdate from "../../../services/spaced_update.js";
import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action.js";
import { t } from "../../../services/i18n.js";
import BulkAction from "../BulkAction.jsx";

View File

@@ -1,6 +1,4 @@
import SpacedUpdate from "../../../services/spaced_update.js";
import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action.js";
import noteAutocompleteService from "../../../services/note_autocomplete.js";
import { t } from "../../../services/i18n.js";
import BulkAction, { BulkActionText } from "../BulkAction.jsx";
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";

View File

@@ -414,7 +414,7 @@ export default class GlobalMenuWidget extends BasicWidget {
}
async fetchLatestVersion() {
const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Notes/releases/latest";
const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Trilium/releases/latest";
const resp = await fetch(RELEASES_API_URL);
const data = await resp.json();

View File

@@ -1,252 +0,0 @@
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import utils from "../../services/utils.js";
import branchService from "../../services/branches.js";
import dialogService from "../../services/dialog.js";
import server from "../../services/server.js";
import toastService from "../../services/toast.js";
import ws from "../../services/ws.js";
import appContext, { type EventData } from "../../components/app_context.js";
import { t } from "../../services/i18n.js";
import type FNote from "../../entities/fnote.js";
import type { FAttachmentRow } from "../../entities/fattachment.js";
// TODO: Deduplicate with server
interface ConvertToAttachmentResponse {
attachment: FAttachmentRow;
}
const TPL = /*html*/`
<div class="dropdown note-actions">
<style>
.note-actions {
width: 35px;
height: 35px;
}
.note-actions .dropdown-menu {
min-width: 15em;
}
.note-actions .dropdown-item .bx {
position: relative;
top: 3px;
font-size: 120%;
margin-right: 5px;
}
.note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover {
color: var(--muted-text-color) !important;
background-color: transparent !important;
pointer-events: none; /* makes it unclickable */
}
</style>
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
class="icon-action bx bx-dots-vertical-rounded"></button>
<div class="dropdown-menu dropdown-menu-right">
<li data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">
<span class="bx bx-paperclip"></span> ${t("note_actions.convert_into_attachment")}
</li>
<li data-trigger-command="renderActiveNote" class="dropdown-item render-note-button">
<span class="bx bx-extension"></span> ${t("note_actions.re_render_note")}<kbd data-command="renderActiveNote"></kbd>
</li>
<li data-trigger-command="findInText" class="dropdown-item find-in-text-button">
<span class='bx bx-search'></span> ${t("note_actions.search_in_note")}<kbd data-command="findInText"></kbd>
</li>
<li data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button">
<span class="bx bx-printer"></span> ${t("note_actions.print_note")}<kbd data-command="printActiveNote"></kbd>
</li>
<li data-trigger-command="exportAsPdf" class="dropdown-item export-as-pdf-button">
<span class="bx bxs-file-pdf"></span> ${t("note_actions.print_pdf")}<kbd data-command="exportAsPdf"></kbd>
</li>
<div class="dropdown-divider"></div>
<li class="dropdown-item import-files-button"><span class="bx bx-import"></span> ${t("note_actions.import_files")}</li>
<li class="dropdown-item export-note-button"><span class="bx bx-export"></span> ${t("note_actions.export_note")}</li>
<div class="dropdown-divider"></div>
<li data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button" title="${t("note_actions.open_note_externally_title")}">
<span class="bx bx-file-find"></span> ${t("note_actions.open_note_externally")}<kbd data-command="openNoteExternally"></kbd>
</li>
<li data-trigger-command="openNoteCustom" class="dropdown-item open-note-custom-button">
<span class="bx bx-customize"></span> ${t("note_actions.open_note_custom")}<kbd data-command="openNoteCustom"></kbd>
</li>
<li data-trigger-command="showNoteSource" class="dropdown-item show-source-button">
<span class="bx bx-code"></span> ${t("note_actions.note_source")}<kbd data-command="showNoteSource"></kbd>
</li>
<div class="dropdown-divider"></div>
<li data-trigger-command="forceSaveRevision" class="dropdown-item save-revision-button">
<span class="bx bx-save"></span> ${t("note_actions.save_revision")}<kbd data-command="forceSaveRevision"></kbd>
</li>
<li class="dropdown-item delete-note-button"><span class="bx bx-trash destructive-action-icon"></span> ${t("note_actions.delete_note")}</li>
<div class="dropdown-divider"></div>
<li data-trigger-command="showAttachments" class="dropdown-item show-attachments-button">
<span class="bx bx-paperclip"></span> ${t("note_actions.note_attachments")}<kbd data-command="showAttachments"></kbd>
</li>
</div>
</div>`;
export default class NoteActionsWidget extends NoteContextAwareWidget {
private $convertNoteIntoAttachmentButton!: JQuery<HTMLElement>;
private $findInTextButton!: JQuery<HTMLElement>;
private $printActiveNoteButton!: JQuery<HTMLElement>;
private $exportAsPdfButton!: JQuery<HTMLElement>;
private $showSourceButton!: JQuery<HTMLElement>;
private $showAttachmentsButton!: JQuery<HTMLElement>;
private $renderNoteButton!: JQuery<HTMLElement>;
private $saveRevisionButton!: JQuery<HTMLElement>;
private $exportNoteButton!: JQuery<HTMLElement>;
private $importNoteButton!: JQuery<HTMLElement>;
private $openNoteExternallyButton!: JQuery<HTMLElement>;
private $openNoteCustomButton!: JQuery<HTMLElement>;
private $deleteNoteButton!: JQuery<HTMLElement>;
isEnabled() {
return this.note?.type !== "launcher";
}
doRender() {
this.$widget = $(TPL);
this.$widget.on("show.bs.dropdown", () => {
if (this.note) {
this.refreshVisibility(this.note);
}
});
this.$convertNoteIntoAttachmentButton = this.$widget.find("[data-trigger-command='convertNoteIntoAttachment']");
this.$findInTextButton = this.$widget.find(".find-in-text-button");
this.$printActiveNoteButton = this.$widget.find(".print-active-note-button");
this.$exportAsPdfButton = this.$widget.find(".export-as-pdf-button");
this.$showSourceButton = this.$widget.find(".show-source-button");
this.$showAttachmentsButton = this.$widget.find(".show-attachments-button");
this.$renderNoteButton = this.$widget.find(".render-note-button");
this.$saveRevisionButton = this.$widget.find(".save-revision-button");
this.$exportNoteButton = this.$widget.find(".export-note-button");
this.$exportNoteButton.on("click", () => {
if (this.$exportNoteButton.hasClass("disabled") || !this.noteContext?.notePath) {
return;
}
this.triggerCommand("showExportDialog", {
notePath: this.noteContext.notePath,
defaultType: "single"
});
});
this.$importNoteButton = this.$widget.find(".import-files-button");
this.$importNoteButton.on("click", () => {
if (this.noteId) {
this.triggerCommand("showImportDialog", { noteId: this.noteId });
}
});
this.$widget.on("click", ".dropdown-item", () => this.$widget.find("[data-bs-toggle='dropdown']").dropdown("toggle"));
this.$openNoteExternallyButton = this.$widget.find(".open-note-externally-button");
this.$openNoteCustomButton = this.$widget.find(".open-note-custom-button");
this.$deleteNoteButton = this.$widget.find(".delete-note-button");
this.$deleteNoteButton.on("click", () => {
if (!this.note || this.note.noteId === "root") {
return;
}
branchService.deleteNotes([this.note.getParentBranches()[0].branchId], true);
});
}
async refreshVisibility(note: FNote) {
const isInOptions = note.noteId.startsWith("_options");
this.$convertNoteIntoAttachmentButton.toggle(note.isEligibleForConversionToAttachment());
this.toggleDisabled(this.$findInTextButton, ["text", "code", "book", "mindMap", "doc"].includes(note.type));
this.toggleDisabled(this.$showAttachmentsButton, !isInOptions);
this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type));
const canPrint = ["text", "code"].includes(note.type);
this.toggleDisabled(this.$printActiveNoteButton, canPrint);
this.toggleDisabled(this.$exportAsPdfButton, canPrint);
this.$exportAsPdfButton.toggleClass("hidden-ext", !utils.isElectron());
this.$renderNoteButton.toggle(note.type === "render");
this.toggleDisabled(this.$openNoteExternallyButton, utils.isElectron() && !["search", "book"].includes(note.type));
this.toggleDisabled(
this.$openNoteCustomButton,
utils.isElectron() &&
!utils.isMac() && // no implementation for Mac yet
!["search", "book"].includes(note.type)
);
// I don't want to handle all special notes like this, but intuitively user might want to export content of backend log
this.toggleDisabled(this.$exportNoteButton, !["_backendLog"].includes(note.noteId) && !isInOptions);
this.toggleDisabled(this.$importNoteButton, !["search"].includes(note.type) && !isInOptions);
this.toggleDisabled(this.$deleteNoteButton, !isInOptions);
this.toggleDisabled(this.$saveRevisionButton, !isInOptions);
}
async convertNoteIntoAttachmentCommand() {
if (!this.note || !(await dialogService.confirm(t("note_actions.convert_into_attachment_prompt", { title: this.note.title })))) {
return;
}
const { attachment: newAttachment } = await server.post<ConvertToAttachmentResponse>(`notes/${this.noteId}/convert-to-attachment`);
if (!newAttachment) {
toastService.showMessage(t("note_actions.convert_into_attachment_failed", { title: this.note.title }));
return;
}
toastService.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
}
});
}
toggleDisabled($el: JQuery<HTMLElement>, enable: boolean) {
if (enable) {
$el.removeAttr("disabled");
} else {
$el.attr("disabled", "disabled");
}
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isNoteReloaded(this.noteId)) {
this.refresh();
}
}
}

View File

@@ -1,14 +0,0 @@
import { t } from "../../services/i18n.js";
import CommandButtonWidget from "./command_button.js";
export default class RevisionsButton extends CommandButtonWidget {
constructor() {
super();
this.icon("bx-history").title(t("revisions_button.note_revisions")).command("showRevisions").titlePlacement("bottom").class("icon-action");
}
isEnabled() {
return super.isEnabled() && !["launcher", "doc"].includes(this.note?.type ?? "");
}
}

View File

@@ -1,388 +0,0 @@
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import keyboardActionsService from "../../services/keyboard_actions.js";
import attributeService from "../../services/attributes.js";
import type CommandButtonWidget from "../buttons/command_button.js";
import type FNote from "../../entities/fnote.js";
import type { NoteType } from "../../entities/fnote.js";
import type { EventData, EventNames } from "../../components/app_context.js";
import type NoteActionsWidget from "../buttons/note_actions.js";
const TPL = /*html*/`
<div class="ribbon-container">
<style>
.ribbon-container {
margin-bottom: 5px;
}
.ribbon-top-row {
display: flex;
}
.ribbon-tab-container {
display: flex;
flex-direction: row;
justify-content: center;
margin-left: 10px;
flex-grow: 1;
flex-flow: row wrap;
}
.ribbon-tab-title {
color: var(--muted-text-color);
border-bottom: 1px solid var(--main-border-color);
min-width: 24px;
flex-basis: 24px;
max-width: max-content;
flex-grow: 10;
}
.ribbon-tab-title .bx {
font-size: 150%;
position: relative;
top: 3px;
}
.ribbon-tab-title.active {
color: var(--main-text-color);
border-bottom: 3px solid var(--main-text-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ribbon-tab-title:hover {
cursor: pointer;
}
.ribbon-tab-title:hover {
color: var(--main-text-color);
}
.ribbon-tab-title:first-of-type {
padding-left: 10px;
}
.ribbon-tab-spacer {
flex-basis: 0;
min-width: 0;
max-width: 35px;
flex-grow: 1;
border-bottom: 1px solid var(--main-border-color);
}
.ribbon-tab-spacer:last-of-type {
flex-grow: 1;
flex-basis: 0;
min-width: 0;
max-width: 10000px;
}
.ribbon-button-container {
display: flex;
border-bottom: 1px solid var(--main-border-color);
margin-right: 5px;
}
.ribbon-button-container > * {
position: relative;
top: -3px;
margin-left: 10px;
}
.ribbon-body {
display: none;
border-bottom: 1px solid var(--main-border-color);
margin-left: 10px;
margin-right: 5px; /* needs to have this value so that the bottom border is the same width as the top one */
}
.ribbon-body.active {
display: block;
}
.ribbon-tab-title-label {
display: none;
}
.ribbon-tab-title.active .ribbon-tab-title-label {
display: inline;
}
</style>
<div class="ribbon-top-row">
<div class="ribbon-tab-container"></div>
<div class="ribbon-button-container"></div>
</div>
<div class="ribbon-body-container"></div>
</div>`;
type ButtonWidget = (CommandButtonWidget | NoteActionsWidget);
export default class RibbonContainer extends NoteContextAwareWidget {
private lastActiveComponentId?: string | null;
private lastNoteType?: NoteType;
private ribbonWidgets: NoteContextAwareWidget[];
private buttonWidgets: ButtonWidget[];
private $tabContainer!: JQuery<HTMLElement>;
private $buttonContainer!: JQuery<HTMLElement>;
private $bodyContainer!: JQuery<HTMLElement>;
constructor() {
super();
this.contentSized();
this.ribbonWidgets = [];
this.buttonWidgets = [];
}
isEnabled() {
return super.isEnabled() && this.noteContext?.viewScope?.viewMode === "default";
}
ribbon(widget: NoteContextAwareWidget) {
// TODO: Base class
super.child(widget);
this.ribbonWidgets.push(widget);
return this;
}
button(widget: ButtonWidget) {
super.child(widget);
this.buttonWidgets.push(widget);
return this;
}
doRender() {
this.$widget = $(TPL);
this.$tabContainer = this.$widget.find(".ribbon-tab-container");
this.$buttonContainer = this.$widget.find(".ribbon-button-container");
this.$bodyContainer = this.$widget.find(".ribbon-body-container");
for (const ribbonWidget of this.ribbonWidgets) {
this.$bodyContainer.append($('<div class="ribbon-body">').attr("data-ribbon-component-id", ribbonWidget.componentId).append(ribbonWidget.render()));
}
for (const buttonWidget of this.buttonWidgets) {
this.$buttonContainer.append(buttonWidget.render());
}
this.$tabContainer.on("click", ".ribbon-tab-title", (e) => {
const $ribbonTitle = $(e.target).closest(".ribbon-tab-title");
this.toggleRibbonTab($ribbonTitle);
});
}
toggleRibbonTab($ribbonTitle: JQuery<HTMLElement>, refreshActiveTab = true) {
const activate = !$ribbonTitle.hasClass("active");
this.$tabContainer.find(".ribbon-tab-title").removeClass("active");
this.$bodyContainer.find(".ribbon-body").removeClass("active");
if (activate) {
const ribbonComponendId = $ribbonTitle.attr("data-ribbon-component-id");
const wasAlreadyActive = this.lastActiveComponentId === ribbonComponendId;
this.lastActiveComponentId = ribbonComponendId;
this.$tabContainer.find(`.ribbon-tab-title[data-ribbon-component-id="${ribbonComponendId}"]`).addClass("active");
this.$bodyContainer.find(`.ribbon-body[data-ribbon-component-id="${ribbonComponendId}"]`).addClass("active");
const activeChild = this.getActiveRibbonWidget();
if (activeChild && (refreshActiveTab || !wasAlreadyActive) && this.noteContext && this.notePath) {
const handleEventPromise = activeChild.handleEvent("noteSwitched", { noteContext: this.noteContext, notePath: this.notePath });
if (refreshActiveTab) {
if (handleEventPromise) {
handleEventPromise.then(() => (activeChild as any).focus?.()); // TODO: Base class
} else {
// TODO: Base class
(activeChild as any).focus?.();
}
}
}
} else {
this.lastActiveComponentId = null;
}
}
async noteSwitched() {
this.lastActiveComponentId = null;
await super.noteSwitched();
}
async refreshWithNote(note: FNote, noExplicitActivation = false) {
this.lastNoteType = note.type;
let $ribbonTabToActivate, $lastActiveRibbon;
this.$tabContainer.empty();
for (const ribbonWidget of this.ribbonWidgets) {
// TODO: Base class for ribbon widget
const ret = await (ribbonWidget as any).getTitle(note);
if (!ret.show) {
continue;
}
const $ribbonTitle = $('<div class="ribbon-tab-title">')
.attr("data-ribbon-component-id", ribbonWidget.componentId)
.attr("data-ribbon-component-name", (ribbonWidget as any).name as string) // TODO: base class for ribbon widgets
.append(
$('<span class="ribbon-tab-title-icon">')
.addClass(ret.icon)
.attr("title", ret.title)
.attr("data-toggle-command", (ribbonWidget as any).toggleCommand)
) // TODO: base class
.append(" ")
.append($('<span class="ribbon-tab-title-label">').text(ret.title));
this.$tabContainer.append($ribbonTitle);
this.$tabContainer.append('<div class="ribbon-tab-spacer">');
if (ret.activate && !this.lastActiveComponentId && !$ribbonTabToActivate && !noExplicitActivation) {
$ribbonTabToActivate = $ribbonTitle;
}
if (this.lastActiveComponentId === ribbonWidget.componentId) {
$lastActiveRibbon = $ribbonTitle;
}
}
keyboardActionsService.getActions().then((actions) => {
this.$tabContainer.find(".ribbon-tab-title-icon").tooltip({
title: () => {
const toggleCommandName = $(this).attr("data-toggle-command");
const action = actions.find((act) => act.actionName === toggleCommandName);
const title = $(this).attr("data-title");
if (action?.effectiveShortcuts && action.effectiveShortcuts.length > 0) {
return `${title} (${action.effectiveShortcuts.join(", ")})`;
} else {
return title ?? "";
}
}
});
});
if (!$ribbonTabToActivate) {
$ribbonTabToActivate = $lastActiveRibbon;
}
if ($ribbonTabToActivate) {
this.toggleRibbonTab($ribbonTabToActivate, false);
} else {
this.$bodyContainer.find(".ribbon-body").removeClass("active");
}
}
isRibbonTabActive(name: string) {
const $ribbonComponent = this.$widget.find(`.ribbon-tab-title[data-ribbon-component-name='${name}']`);
return $ribbonComponent.hasClass("active");
}
ensureOwnedAttributesAreOpen(ntxId: string | null | undefined) {
if (ntxId && this.isNoteContext(ntxId) && !this.isRibbonTabActive("ownedAttributes")) {
this.toggleRibbonTabWithName("ownedAttributes", ntxId);
}
}
addNewLabelEvent({ ntxId }: EventData<"addNewLabel">) {
this.ensureOwnedAttributesAreOpen(ntxId);
}
addNewRelationEvent({ ntxId }: EventData<"addNewRelation">) {
this.ensureOwnedAttributesAreOpen(ntxId);
}
toggleRibbonTabWithName(name: string, ntxId?: string) {
if (!this.isNoteContext(ntxId)) {
return false;
}
const $ribbonComponent = this.$widget.find(`.ribbon-tab-title[data-ribbon-component-name='${name}']`);
if ($ribbonComponent) {
this.toggleRibbonTab($ribbonComponent);
}
}
handleEvent<T extends EventNames>(name: T, data: EventData<T>) {
const PREFIX = "toggleRibbonTab";
if (name.startsWith(PREFIX)) {
let componentName = name.substr(PREFIX.length);
componentName = componentName[0].toLowerCase() + componentName.substr(1);
this.toggleRibbonTabWithName(componentName, (data as any).ntxId);
} else {
return super.handleEvent(name, data);
}
}
async handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>) {
if (["activeContextChanged", "setNoteContext"].includes(name)) {
// won't trigger .refresh();
await super.handleEventInChildren("setNoteContext", data as EventData<"activeContextChanged" | "setNoteContext">);
} else if (this.isEnabled() || name === "initialRenderComplete") {
const activeRibbonWidget = this.getActiveRibbonWidget();
// forward events only to active ribbon tab, inactive ones don't need to be updated
if (activeRibbonWidget) {
await activeRibbonWidget.handleEvent(name, data);
}
for (const buttonWidget of this.buttonWidgets) {
await buttonWidget.handleEvent(name, data);
}
}
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (!this.note) {
return;
}
if (this.noteId && loadResults.isNoteReloaded(this.noteId) && this.lastNoteType !== this.note.type) {
// note type influences the list of available ribbon tabs the most
// check for the type is so that we don't update on each title rename
this.lastNoteType = this.note.type;
this.refresh();
} else if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) {
this.refreshWithNote(this.note, true);
}
}
async noteTypeMimeChangedEvent() {
// We are ignoring the event which triggers a refresh since it is usually already done by a different
// event and causing a race condition in which the items appear twice.
}
/**
* Executed as soon as the user presses the "Edit" floating button in a read-only text note.
*
* <p>
* We need to refresh the ribbon for cases such as the classic editor which relies on the read-only state.
*/
readOnlyTemporarilyDisabledEvent() {
this.refresh();
}
getActiveRibbonWidget() {
return this.ribbonWidgets.find((ch) => ch.componentId === this.lastActiveComponentId);
}
}

View File

@@ -1,6 +1,8 @@
import utils from "../../services/utils.js";
import type BasicWidget from "../basic_widget.js";
import { EventData } from "../../components/app_context.js";
import FlexContainer from "./flex_container.js";
import options from "../../services/options.js";
import type BasicWidget from "../basic_widget.js";
import utils from "../../services/utils.js";
/**
* The root container is the top-most widget/container, from which the entire layout derives.
@@ -27,15 +29,45 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
window.visualViewport?.addEventListener("resize", () => this.#onMobileResize());
}
this.#setMotion(options.is("motionEnabled"));
this.#setShadows(options.is("shadowsEnabled"));
this.#setBackdropEffects(options.is("backdropEffectsEnabled"));
return super.render();
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isOptionReloaded("motionEnabled")) {
this.#setMotion(options.is("motionEnabled"));
}
if (loadResults.isOptionReloaded("shadowsEnabled")) {
this.#setShadows(options.is("shadowsEnabled"));
}
if (loadResults.isOptionReloaded("backdropEffectsEnabled")) {
this.#setBackdropEffects(options.is("backdropEffectsEnabled"));
}
}
#onMobileResize() {
const currentViewportHeight = getViewportHeight();
const isKeyboardOpened = (currentViewportHeight < this.originalViewportHeight);
this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened);
}
#setMotion(enabled: boolean) {
document.body.classList.toggle("motion-disabled", !enabled);
jQuery.fx.off = !enabled;
}
#setShadows(enabled: boolean) {
document.body.classList.toggle("shadows-disabled", !enabled);
}
#setBackdropEffects(enabled: boolean) {
document.body.classList.toggle("backdrop-effects-disabled", !enabled);
}
}
function getViewportHeight() {

View File

@@ -2,6 +2,7 @@ import FlexContainer from "./flex_container.js";
import appContext, { type CommandData, type CommandListenerData, type EventData, type EventNames, type NoteSwitchedContext } from "../../components/app_context.js";
import type BasicWidget from "../basic_widget.js";
import type NoteContext from "../../components/note_context.js";
import Component from "../../components/component.js";
interface NoteContextEvent {
noteContext: NoteContext;
@@ -152,6 +153,8 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
for (const ntxId of ntxIds) {
this.$widget.find(`[data-ntx-id="${ntxId}"]`).remove();
const widget = this.widgets[ntxId];
recursiveCleanup(widget);
delete this.widgets[ntxId];
}
}
@@ -237,3 +240,12 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
return Promise.all(promises);
}
}
function recursiveCleanup(widget: Component) {
for (const child of widget.children) {
recursiveCleanup(child);
}
if ("cleanup" in widget && typeof widget.cleanup === "function") {
widget.cleanup();
}
}

View File

@@ -1,4 +1,3 @@
import ReactBasicWidget from "../react/ReactBasicWidget.js";
import Modal from "../react/Modal.js";
import { t } from "../../services/i18n.js";
import { formatDateTime } from "../../utils/formatters.js";
@@ -8,11 +7,11 @@ import openService from "../../services/open.js";
import { useState } from "preact/hooks";
import type { CSSProperties } from "preact/compat";
import type { AppInfo } from "@triliumnext/commons";
import useTriliumEvent from "../react/hooks.jsx";
import { useTriliumEvent } from "../react/hooks.jsx";
function AboutDialogComponent() {
let [appInfo, setAppInfo] = useState<AppInfo | null>(null);
let [shown, setShown] = useState(false);
export default function AboutDialog() {
const [appInfo, setAppInfo] = useState<AppInfo | null>(null);
const [shown, setShown] = useState(false);
const forceWordBreak: CSSProperties = { wordBreak: "break-all" };
useTriliumEvent("openAboutDialog", () => setShown(true));
@@ -77,16 +76,8 @@ function DirectoryLink({ directory, style }: { directory: string, style?: CSSPro
openService.openDirectory(directory);
};
return <a className="tn-link" href="#" onClick={onClick} style={style}></a>
return <a className="tn-link" href="#" onClick={onClick} style={style}>{directory}</a>
} else {
return <span style={style}>{directory}</span>;
}
}
export default class AboutDialog extends ReactBasicWidget {
get component() {
return <AboutDialogComponent />;
}
}

View File

@@ -1,6 +1,5 @@
import { t } from "../../services/i18n";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import Button from "../react/Button";
import FormRadioGroup from "../react/FormRadioGroup";
import NoteAutocomplete from "../react/NoteAutocomplete";
@@ -11,11 +10,11 @@ import { default as TextTypeWidget } from "../type_widgets/editable_text.js";
import { logError } from "../../services/ws";
import FormGroup from "../react/FormGroup.js";
import { refToJQuerySelector } from "../react/react_utils";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
type LinkType = "reference-link" | "external-link" | "hyper-link";
function AddLinkDialogComponent() {
export default function AddLinkDialog() {
const [ textTypeWidget, setTextTypeWidget ] = useState<TextTypeWidget>();
const initialText = useRef<string>();
const [ linkTitle, setLinkTitle ] = useState("");
@@ -30,6 +29,14 @@ function AddLinkDialogComponent() {
setShown(true);
});
useEffect(() => {
if (hasSelection) {
setLinkType("hyper-link");
} else {
setLinkType("reference-link");
}
}, [ hasSelection ])
async function setDefaultLinkTitle(noteId: string) {
const noteTitle = await tree.getNoteTitle(noteId);
setLinkTitle(noteTitle);
@@ -107,7 +114,7 @@ function AddLinkDialogComponent() {
}}
show={shown}
>
<FormGroup label={t("add_link.note")}>
<FormGroup label={t("add_link.note")} name="note">
<NoteAutocomplete
inputRef={autocompleteRef}
onChange={setSuggestion}
@@ -152,11 +159,3 @@ function AddLinkDialogComponent() {
</Modal>
);
}
export default class AddLinkDialog extends ReactBasicWidget {
get component() {
return <AddLinkDialogComponent />;
}
}

View File

@@ -4,15 +4,14 @@ import { t } from "../../services/i18n.js";
import server from "../../services/server.js";
import toast from "../../services/toast.js";
import Modal from "../react/Modal.jsx";
import ReactBasicWidget from "../react/ReactBasicWidget.js";
import froca from "../../services/froca.js";
import tree from "../../services/tree.js";
import Button from "../react/Button.jsx";
import FormGroup from "../react/FormGroup.js";
import useTriliumEvent from "../react/hooks.jsx";
import { useTriliumEvent } from "../react/hooks.jsx";
import FBranch from "../../entities/fbranch.js";
function BranchPrefixDialogComponent() {
export default function BranchPrefixDialog() {
const [ shown, setShown ] = useState(false);
const [ branch, setBranch ] = useState<FBranch>();
const [ prefix, setPrefix ] = useState(branch?.prefix ?? "");
@@ -64,7 +63,7 @@ function BranchPrefixDialogComponent() {
footer={<Button text={t("branch_prefix.save")} />}
show={shown}
>
<FormGroup label={t("branch_prefix.prefix")}>
<FormGroup label={t("branch_prefix.prefix")} name="prefix">
<div class="input-group">
<input class="branch-prefix-input form-control" value={prefix} ref={branchInput}
onChange={(e) => setPrefix((e.target as HTMLInputElement).value)} />
@@ -75,14 +74,6 @@ function BranchPrefixDialogComponent() {
);
}
export default class BranchPrefixDialog extends ReactBasicWidget {
get component() {
return <BranchPrefixDialogComponent />;
}
}
async function savePrefix(branchId: string, prefix: string) {
await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix });
toast.showMessage(t("branch_prefix.branch_prefix_saved"));

View File

@@ -1,7 +1,6 @@
import { useEffect, useState, useCallback } from "preact/hooks";
import { t } from "../../services/i18n";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import "./bulk_actions.css";
import { BulkActionAffectedNotes } from "@triliumnext/commons";
import server from "../../services/server";
@@ -12,9 +11,9 @@ import toast from "../../services/toast";
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
import FNote from "../../entities/fnote";
import froca from "../../services/froca";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
function BulkActionComponent() {
export default function BulkActionsDialog() {
const [ selectedOrActiveNoteIds, setSelectedOrActiveNoteIds ] = useState<string[]>();
const [ bulkActionNote, setBulkActionNote ] = useState<FNote | null>();
const [ includeDescendants, setIncludeDescendants ] = useState(false);
@@ -51,7 +50,7 @@ function BulkActionComponent() {
row.type === "label" && row.name === "action" && row.noteId === "_bulkAction")) {
refreshExistingActions();
}
}, shown);
});
return (
<Modal
@@ -94,7 +93,8 @@ function AvailableActionsList() {
<td>{ actionGroup.title }:</td>
{actionGroup.actions.map(({ actionName, actionTitle }) =>
<Button
small text={actionTitle}
size="small"
text={actionTitle}
onClick={() => bulk_action.addAction("_bulkAction", actionName)}
/>
)}
@@ -116,11 +116,3 @@ function ExistingActionsList({ existingActions }: { existingActions?: RenameNote
</table>
);
}
export default class BulkActionsDialog extends ReactBasicWidget {
get component() {
return <BulkActionComponent />
}
}

View File

@@ -1,14 +1,11 @@
import { useState } from "preact/hooks";
import { useMemo, useState } from "preact/hooks";
import Button from "../react/Button";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import { CallToAction, dismissCallToAction, getCallToActions } from "./call_to_action_definitions";
import { dismissCallToAction, getCallToActions } from "./call_to_action_definitions";
import { t } from "../../services/i18n";
function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActions: CallToAction[] }) {
if (!activeCallToActions.length) {
return <></>;
}
export default function CallToActionDialog() {
const activeCallToActions = useMemo(() => getCallToActions(), []);
const [ activeIndex, setActiveIndex ] = useState(0);
const [ shown, setShown ] = useState(true);
const activeItem = activeCallToActions[activeIndex];
@@ -21,16 +18,16 @@ function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActi
}
}
return (
return (activeCallToActions.length &&
<Modal
className="call-to-action"
size="md"
title="New features"
title={activeItem.title}
show={shown}
onHidden={() => setShown(false)}
footerAlignment="between"
footer={<>
<Button text="Dismiss" onClick={async () => {
<Button text={t("call_to_action.dismiss")} onClick={async () => {
await dismissCallToAction(activeItem.id);
goToNext();
}} />
@@ -43,16 +40,7 @@ function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActi
)}
</>}
>
<h4>{activeItem.title}</h4>
<p>{activeItem.message}</p>
</Modal>
)
}
export class CallToActionDialog extends ReactBasicWidget {
get component() {
return <CallToActionDialogComponent activeCallToActions={getCallToActions()} />
}
}

View File

@@ -65,7 +65,7 @@ const CALL_TO_ACTIONS: CallToAction[] = [
id: "background_effects",
title: t("call_to_action.background_effects_title"),
message: t("call_to_action.background_effects_message"),
enabled: () => utils.isElectron() && window.glob.platform === "win32" && isNextTheme() && !options.is("backgroundEffects"),
enabled: () => false,
buttons: [
{
text: t("call_to_action.background_effects_button"),

View File

@@ -2,7 +2,6 @@ import { useRef, useState } from "preact/hooks";
import appContext from "../../components/app_context";
import { t } from "../../services/i18n";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import NoteAutocomplete from "../react/NoteAutocomplete";
import froca from "../../services/froca";
import FormGroup from "../react/FormGroup";
@@ -14,9 +13,9 @@ import tree from "../../services/tree";
import branches from "../../services/branches";
import toast from "../../services/toast";
import NoteList from "../react/NoteList";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
function CloneToDialogComponent() {
export default function CloneToDialog() {
const [ clonedNoteIds, setClonedNoteIds ] = useState<string[]>();
const [ prefix, setPrefix ] = useState("");
const [ suggestion, setSuggestion ] = useState<Suggestion | null>(null);
@@ -69,28 +68,20 @@ function CloneToDialogComponent() {
>
<h5>{t("clone_to.notes_to_clone")}</h5>
<NoteList style={{ maxHeight: "200px", overflow: "auto" }} noteIds={clonedNoteIds} />
<FormGroup label={t("clone_to.target_parent_note")}>
<FormGroup name="target-parent-note" label={t("clone_to.target_parent_note")}>
<NoteAutocomplete
placeholder={t("clone_to.search_for_note_by_its_name")}
onChange={setSuggestion}
inputRef={autoCompleteRef}
/>
</FormGroup>
<FormGroup label={t("clone_to.prefix_optional")} title={t("clone_to.cloned_note_prefix_title")}>
<FormTextBox name="clone-prefix" onChange={setPrefix} />
<FormGroup name="clone-prefix" label={t("clone_to.prefix_optional")} title={t("clone_to.cloned_note_prefix_title")}>
<FormTextBox onChange={setPrefix} />
</FormGroup>
</Modal>
)
}
export default class CloneToDialog extends ReactBasicWidget {
get component() {
return <CloneToDialogComponent />;
}
}
async function cloneNotesTo(notePath: string, clonedNoteIds: string[], prefix?: string) {
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
if (!noteId || !parentNoteId) {

View File

@@ -1,10 +1,9 @@
import ReactBasicWidget from "../react/ReactBasicWidget";
import Modal from "../react/Modal";
import Button from "../react/Button";
import { t } from "../../services/i18n";
import { useState } from "preact/hooks";
import FormCheckbox from "../react/FormCheckbox";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
interface ConfirmDialogProps {
title?: string;
@@ -13,7 +12,7 @@ interface ConfirmDialogProps {
isConfirmDeleteNoteBox?: boolean;
}
function ConfirmDialogComponent() {
export default function ConfirmDialog() {
const [ opts, setOpts ] = useState<ConfirmDialogProps>();
const [ isDeleteNoteChecked, setIsDeleteNoteChecked ] = useState(false);
const [ shown, setShown ] = useState(false);
@@ -92,11 +91,3 @@ export interface ConfirmWithTitleOptions {
title: string;
callback: ConfirmDialogCallback;
}
export default class ConfirmDialog extends ReactBasicWidget {
get component() {
return <ConfirmDialogComponent />;
}
}

View File

@@ -2,7 +2,6 @@ import { useRef, useState, useEffect } from "preact/hooks";
import { t } from "../../services/i18n.js";
import FormCheckbox from "../react/FormCheckbox.js";
import Modal from "../react/Modal.js";
import ReactBasicWidget from "../react/ReactBasicWidget.js";
import type { DeleteNotesPreview } from "@triliumnext/commons";
import server from "../../services/server.js";
import froca from "../../services/froca.js";
@@ -10,7 +9,7 @@ import FNote from "../../entities/fnote.js";
import link from "../../services/link.js";
import Button from "../react/Button.jsx";
import Alert from "../react/Alert.jsx";
import useTriliumEvent from "../react/hooks.jsx";
import { useTriliumEvent } from "../react/hooks.jsx";
export interface ResolveOptions {
proceed: boolean;
@@ -30,7 +29,7 @@ interface BrokenRelationData {
source: string;
}
function DeleteNotesDialogComponent() {
export default function DeleteNotesDialog() {
const [ opts, setOpts ] = useState<ShowDeleteNotesDialogOpts>({});
const [ deleteAllClones, setDeleteAllClones ] = useState(false);
const [ eraseNotes, setEraseNotes ] = useState(!!opts.forceDeleteAllClones);
@@ -140,7 +139,7 @@ function BrokenRelations({ brokenRelations }: { brokenRelations: DeleteNotesPrev
const noteIds = brokenRelations
.map(relation => relation.noteId)
.filter(noteId => noteId) as string[];
froca.getNotes(noteIds).then(async (notes) => {
froca.getNotes(noteIds).then(async () => {
const notesWithBrokenRelations: BrokenRelationData[] = [];
for (const attr of brokenRelations) {
notesWithBrokenRelations.push({
@@ -171,11 +170,3 @@ function BrokenRelations({ brokenRelations }: { brokenRelations: DeleteNotesPrev
return <></>;
}
}
export default class DeleteNotesDialog extends ReactBasicWidget {
get component() {
return <DeleteNotesDialogComponent />;
}
}

View File

@@ -4,14 +4,13 @@ import tree from "../../services/tree";
import Button from "../react/Button";
import FormRadioGroup from "../react/FormRadioGroup";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import "./export.css";
import ws from "../../services/ws";
import toastService, { ToastOptions } from "../../services/toast";
import utils from "../../services/utils";
import open from "../../services/open";
import froca from "../../services/froca";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
interface ExportDialogProps {
branchId?: string | null;
@@ -19,7 +18,7 @@ interface ExportDialogProps {
defaultType?: "subtree" | "single";
}
function ExportDialogComponent() {
export default function ExportDialog() {
const [ opts, setOpts ] = useState<ExportDialogProps>();
const [ exportType, setExportType ] = useState<string>(opts?.defaultType ?? "subtree");
const [ subtreeFormat, setSubtreeFormat ] = useState("html");
@@ -125,14 +124,6 @@ function ExportDialogComponent() {
);
}
export default class ExportDialog extends ReactBasicWidget {
get component() {
return <ExportDialogComponent />
}
}
function exportBranch(branchId: string, type: string, format: string, version: string) {
const taskId = utils.randomString(10);
const url = open.getUrlForDownload(`api/branches/${branchId}/export/${type}/${format}/${version}/${taskId}`);

View File

@@ -1,4 +1,3 @@
import ReactBasicWidget from "../react/ReactBasicWidget.js";
import Modal from "../react/Modal.jsx";
import { t } from "../../services/i18n.js";
import { ComponentChildren } from "preact";
@@ -6,9 +5,9 @@ import { CommandNames } from "../../components/app_context.js";
import RawHtml from "../react/RawHtml.jsx";
import { useEffect, useState } from "preact/hooks";
import keyboard_actions from "../../services/keyboard_actions.js";
import useTriliumEvent from "../react/hooks.jsx";
import { useTriliumEvent } from "../react/hooks.jsx";
function HelpDialogComponent() {
export default function HelpDialog() {
const [ shown, setShown ] = useState(false);
useTriliumEvent("showCheatsheet", () => setShown(true));
@@ -161,11 +160,3 @@ function Card({ title, children }: { title: string, children: ComponentChildren
</div>
)
}
export default class HelpDialog extends ReactBasicWidget {
get component() {
return <HelpDialogComponent />;
}
}

View File

@@ -4,14 +4,13 @@ import tree from "../../services/tree";
import Button from "../react/Button";
import FormCheckbox from "../react/FormCheckbox";
import FormFileUpload from "../react/FormFileUpload";
import FormGroup from "../react/FormGroup";
import FormGroup, { FormMultiGroup } from "../react/FormGroup";
import Modal from "../react/Modal";
import RawHtml from "../react/RawHtml";
import ReactBasicWidget from "../react/ReactBasicWidget";
import importService, { UploadFilesOptions } from "../../services/import";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
function ImportDialogComponent() {
export default function ImportDialog() {
const [ parentNoteId, setParentNoteId ] = useState<string>();
const [ noteTitle, setNoteTitle ] = useState<string>();
const [ files, setFiles ] = useState<FileList | null>(null);
@@ -55,11 +54,11 @@ function ImportDialogComponent() {
footer={<Button text={t("import.import")} primary disabled={!files} />}
show={shown}
>
<FormGroup label={t("import.chooseImportFile")} description={<>{t("import.importDescription")} <strong>{ noteTitle }</strong></>}>
<FormGroup name="files" label={t("import.chooseImportFile")} description={<>{t("import.importDescription")} <strong>{ noteTitle }</strong></>}>
<FormFileUpload multiple onChange={setFiles} />
</FormGroup>
<FormGroup label={t("import.options")}>
<FormMultiGroup label={t("import.options")}>
<FormCheckbox
name="safe-import" hint={t("import.safeImportTooltip")} label={t("import.safeImport")}
currentValue={safeImport} onChange={setSafeImport}
@@ -84,19 +83,11 @@ function ImportDialogComponent() {
name="replace-underscores-with-spaces" label={t("import.replaceUnderscoresWithSpaces")}
currentValue={replaceUnderscoresWithSpaces} onChange={setReplaceUnderscoresWithSpaces}
/>
</FormGroup>
</FormMultiGroup>
</Modal>
);
}
export default class ImportDialog extends ReactBasicWidget {
get component() {
return <ImportDialogComponent />
}
}
function boolToString(value: boolean) {
return value ? "true" : "false";
}

View File

@@ -4,15 +4,14 @@ import FormGroup from "../react/FormGroup";
import FormRadioGroup from "../react/FormRadioGroup";
import Modal from "../react/Modal";
import NoteAutocomplete from "../react/NoteAutocomplete";
import ReactBasicWidget from "../react/ReactBasicWidget";
import Button from "../react/Button";
import { Suggestion, triggerRecentNotes } from "../../services/note_autocomplete";
import tree from "../../services/tree";
import froca from "../../services/froca";
import EditableTextTypeWidget from "../type_widgets/editable_text";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
function IncludeNoteDialogComponent() {
export default function IncludeNoteDialog() {
const [textTypeWidget, setTextTypeWidget] = useState<EditableTextTypeWidget>();
const [suggestion, setSuggestion] = useState<Suggestion | null>(null);
const [boxSize, setBoxSize] = useState("medium");
@@ -43,7 +42,7 @@ function IncludeNoteDialogComponent() {
footer={<Button text={t("include_note.button_include")} keyboardShortcut="Enter" />}
show={shown}
>
<FormGroup label={t("include_note.label_note")}>
<FormGroup name="note" label={t("include_note.label_note")}>
<NoteAutocomplete
placeholder={t("include_note.placeholder_search")}
onChange={setSuggestion}
@@ -55,8 +54,9 @@ function IncludeNoteDialogComponent() {
/>
</FormGroup>
<FormGroup label={t("include_note.box_size_prompt")}>
<FormRadioGroup name="include-note-box-size"
<FormGroup name="include-note-box-size" label={t("include_note.box_size_prompt")}>
<FormRadioGroup
name="include-note-box-size"
currentValue={boxSize} onChange={setBoxSize}
values={[
{ label: t("include_note.box_size_small"), value: "small" },
@@ -69,14 +69,6 @@ function IncludeNoteDialogComponent() {
)
}
export default class IncludeNoteDialog extends ReactBasicWidget {
get component() {
return <IncludeNoteDialogComponent />;
}
}
async function includeNote(notePath: string, textTypeWidget: EditableTextTypeWidget) {
const noteId = tree.getNoteIdFromUrl(notePath);
if (!noteId) {

View File

@@ -3,11 +3,10 @@ import { t } from "../../services/i18n.js";
import utils from "../../services/utils.js";
import Button from "../react/Button.js";
import Modal from "../react/Modal.js";
import ReactBasicWidget from "../react/ReactBasicWidget.js";
import { useState } from "preact/hooks";
import useTriliumEvent from "../react/hooks.jsx";
import { useTriliumEvent } from "../react/hooks.jsx";
function IncorrectCpuArchDialogComponent() {
export default function IncorrectCpuArchDialogComponent() {
const [ shown, setShown ] = useState(false);
const downloadButtonRef = useRef<HTMLButtonElement>(null);
useTriliumEvent("showCpuArchWarning", () => setShown(true));
@@ -44,11 +43,3 @@ function IncorrectCpuArchDialogComponent() {
</Modal>
)
}
export default class IncorrectCpuArchDialog extends ReactBasicWidget {
get component() {
return <IncorrectCpuArchDialogComponent />
}
}

View File

@@ -1,13 +1,12 @@
import { EventData } from "../../components/app_context";
import ReactBasicWidget from "../react/ReactBasicWidget";
import Modal from "../react/Modal";
import { t } from "../../services/i18n";
import Button from "../react/Button";
import { useRef, useState } from "preact/hooks";
import { RawHtmlBlock } from "../react/RawHtml";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
function ShowInfoDialogComponent() {
export default function InfoDialog() {
const [ opts, setOpts ] = useState<EventData<"showInfoDialog">>();
const [ shown, setShown ] = useState(false);
const okButtonRef = useRef<HTMLButtonElement>(null);
@@ -37,11 +36,3 @@ function ShowInfoDialogComponent() {
<RawHtmlBlock className="info-dialog-content" html={opts?.message ?? ""} />
</Modal>);
}
export default class InfoDialog extends ReactBasicWidget {
get component() {
return <ShowInfoDialogComponent />;
}
}

View File

@@ -1,4 +1,3 @@
import ReactBasicWidget from "../react/ReactBasicWidget";
import Modal from "../react/Modal";
import Button from "../react/Button";
import NoteAutocomplete from "../react/NoteAutocomplete";
@@ -8,13 +7,14 @@ import note_autocomplete, { Suggestion } from "../../services/note_autocomplete"
import appContext from "../../components/app_context";
import commandRegistry from "../../services/command_registry";
import { refToJQuerySelector } from "../react/react_utils";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
import shortcutService from "../../services/shortcuts";
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
type Mode = "last-search" | "recent-notes" | "commands";
function JumpToNoteDialogComponent() {
export default function JumpToNoteDialogComponent() {
const [ mode, setMode ] = useState<Mode>();
const [ lastOpenedTs, setLastOpenedTs ] = useState<number>(0);
const containerRef = useRef<HTMLDivElement>(null);
@@ -26,12 +26,12 @@ function JumpToNoteDialogComponent() {
async function openDialog(commandMode: boolean) {
let newMode: Mode;
let initialText: string = "";
let initialText = "";
if (commandMode) {
newMode = "commands";
initialText = ">";
} else if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText) {
} else if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText.current) {
// if you open the Jump To dialog soon after using it previously, it can often mean that you
// actually want to search for the same thing (e.g., you opened the wrong note at first try)
// so we'll keep the content.
@@ -83,6 +83,27 @@ function JumpToNoteDialogComponent() {
$autoComplete
.trigger("focus")
.trigger("select");
// Add keyboard shortcut for full search
shortcutService.bindElShortcut($autoComplete, "ctrl+return", () => {
if (!isCommandMode) {
showInFullSearch();
}
});
}
async function showInFullSearch() {
try {
setShown(false);
const searchString = actualText.current?.trim();
if (searchString && !searchString.startsWith(">")) {
await appContext.triggerCommand("searchNotes", {
searchString
});
}
} catch (error) {
console.error("Failed to trigger full search:", error);
}
}
return (
@@ -108,18 +129,15 @@ function JumpToNoteDialogComponent() {
/>}
onShown={onShown}
onHidden={() => setShown(false)}
footer={!isCommandMode && <Button className="show-in-full-text-button" text={t("jump_to_note.search_button")} keyboardShortcut="Ctrl+Enter" />}
footer={!isCommandMode && <Button
className="show-in-full-text-button"
text={t("jump_to_note.search_button")}
keyboardShortcut="Ctrl+Enter"
onClick={showInFullSearch}
/>}
show={shown}
>
<div className="algolia-autocomplete-container jump-to-note-results" ref={containerRef}></div>
</Modal>
);
}
export default class JumpToNoteDialog extends ReactBasicWidget {
get component() {
return <JumpToNoteDialogComponent />;
}
}

View File

@@ -5,18 +5,17 @@ import server from "../../services/server";
import toast from "../../services/toast";
import utils from "../../services/utils";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import Button from "../react/Button";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
interface RenderMarkdownResponse {
htmlContent: string;
}
function MarkdownImportDialogComponent() {
export default function MarkdownImportDialog() {
const markdownImportTextArea = useRef<HTMLTextAreaElement>(null);
let [ text, setText ] = useState("");
let [ shown, setShown ] = useState(false);
const [ text, setText ] = useState("");
const [ shown, setShown ] = useState(false);
const triggerImport = useCallback(() => {
if (appContext.tabManager.getActiveContextNoteType() !== "text") {
@@ -64,14 +63,6 @@ function MarkdownImportDialogComponent() {
)
}
export default class MarkdownImportDialog extends ReactBasicWidget {
get component() {
return <MarkdownImportDialogComponent />;
}
}
async function convertMarkdownToHtml(markdownContent: string) {
const { htmlContent } = await server.post<RenderMarkdownResponse>("other/render-markdown", { markdownContent });

View File

@@ -1,4 +1,3 @@
import ReactBasicWidget from "../react/ReactBasicWidget";
import Modal from "../react/Modal";
import { t } from "../../services/i18n";
import NoteList from "../react/NoteList";
@@ -11,9 +10,9 @@ import tree from "../../services/tree";
import froca from "../../services/froca";
import branches from "../../services/branches";
import toast from "../../services/toast";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
function MoveToDialogComponent() {
export default function MoveToDialog() {
const [ movedBranchIds, setMovedBranchIds ] = useState<string[]>();
const [ suggestion, setSuggestion ] = useState<Suggestion | null>(null);
const [ shown, setShown ] = useState(false);
@@ -57,7 +56,7 @@ function MoveToDialogComponent() {
<h5>{t("move_to.notes_to_move")}</h5>
<NoteList branchIds={movedBranchIds} />
<FormGroup label={t("move_to.target_parent_note")}>
<FormGroup name="parent-note" label={t("move_to.target_parent_note")}>
<NoteAutocomplete
onChange={setSuggestion}
inputRef={autoCompleteRef}
@@ -67,14 +66,6 @@ function MoveToDialogComponent() {
)
}
export default class MoveToDialog extends ReactBasicWidget {
get component() {
return <MoveToDialogComponent />;
}
}
async function moveNotesTo(movedBranchIds: string[] | undefined, parentBranchId: string) {
if (movedBranchIds) {
await branches.moveToParentNote(movedBranchIds, parentBranchId);

View File

@@ -1,4 +1,3 @@
import ReactBasicWidget from "../react/ReactBasicWidget";
import Modal from "../react/Modal";
import { t } from "../../services/i18n";
import FormGroup from "../react/FormGroup";
@@ -10,7 +9,7 @@ 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 useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
export interface ChooseNoteTypeResponse {
success: boolean;
@@ -26,7 +25,7 @@ const SEPARATOR_TITLE_REPLACEMENTS = [
t("note_type_chooser.templates")
];
function NoteTypeChooserDialogComponent() {
export default function NoteTypeChooserDialogComponent() {
const [ callback, setCallback ] = useState<ChooseNoteTypeCallback>();
const [ shown, setShown ] = useState(false);
const [ parentNote, setParentNote ] = useState<Suggestion | null>();
@@ -37,25 +36,23 @@ function NoteTypeChooserDialogComponent() {
setShown(true);
});
if (!noteTypes.length) {
useEffect(() => {
note_types.getNoteTypeItems().then(noteTypes => {
let index = -1;
useEffect(() => {
note_types.getNoteTypeItems().then(noteTypes => {
let index = -1;
setNoteTypes((noteTypes ?? []).map((item, _index) => {
if (item.title === "----") {
index++;
return {
title: SEPARATOR_TITLE_REPLACEMENTS[index],
enabled: false
}
setNoteTypes((noteTypes ?? []).map((item) => {
if (item.title === "----") {
index++;
return {
title: SEPARATOR_TITLE_REPLACEMENTS[index],
enabled: false
}
}
return item;
}));
});
return item;
}));
});
}
}, []);
function onNoteTypeSelected(value: string) {
const [ noteType, templateNoteId ] = value.split(",");
@@ -83,7 +80,7 @@ function NoteTypeChooserDialogComponent() {
show={shown}
stackable
>
<FormGroup label={t("note_type_chooser.change_path_prompt")}>
<FormGroup name="parent-note" label={t("note_type_chooser.change_path_prompt")}>
<NoteAutocomplete
onChange={setParentNote}
placeholder={t("note_type_chooser.search_placeholder")}
@@ -95,7 +92,7 @@ function NoteTypeChooserDialogComponent() {
/>
</FormGroup>
<FormGroup label={t("note_type_chooser.modal_body")}>
<FormGroup name="note-type" label={t("note_type_chooser.modal_body")}>
<FormList onSelect={onNoteTypeSelected}>
{noteTypes.map((_item) => {
if (_item.title === "----") {
@@ -120,11 +117,3 @@ function NoteTypeChooserDialogComponent() {
</Modal>
);
}
export default class NoteTypeChooserDialog extends ReactBasicWidget {
get component() {
return <NoteTypeChooserDialogComponent />
}
}

View File

@@ -1,12 +1,11 @@
import ReactBasicWidget from "../react/ReactBasicWidget";
import Modal from "../react/Modal";
import { t } from "../../services/i18n";
import Button from "../react/Button";
import appContext from "../../components/app_context";
import { useState } from "preact/hooks";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
function PasswordNotSetDialogComponent() {
export default function PasswordNotSetDialog() {
const [ shown, setShown ] = useState(false);
useTriliumEvent("showPasswordNotSet", () => setShown(true));
@@ -27,10 +26,3 @@ function PasswordNotSetDialogComponent() {
);
}
export default class PasswordNotSetDialog extends ReactBasicWidget {
get component() {
return <PasswordNotSetDialogComponent />;
}
}

View File

@@ -2,12 +2,10 @@ import { useRef, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import Button from "../react/Button";
import Modal from "../react/Modal";
import { Modal as BootstrapModal } from "bootstrap";
import ReactBasicWidget from "../react/ReactBasicWidget";
import FormTextBox from "../react/FormTextBox";
import FormGroup from "../react/FormGroup";
import { refToJQuerySelector } from "../react/react_utils";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
// JQuery here is maintained for compatibility with existing code.
interface ShownCallbackData {
@@ -25,31 +23,34 @@ export interface PromptDialogOptions {
defaultValue?: string;
shown?: PromptShownDialogCallback;
callback?: (value: string | null) => void;
readOnly?: boolean;
}
function PromptDialogComponent() {
export default function PromptDialog() {
const modalRef = useRef<HTMLDivElement>(null);
const formRef = useRef<HTMLFormElement>(null);
const labelRef = useRef<HTMLLabelElement>(null);
const answerRef = useRef<HTMLInputElement>(null);
const [ opts, setOpts ] = useState<PromptDialogOptions>();
const [ value, setValue ] = useState("");
const opts = useRef<PromptDialogOptions>();
const [ value, setValue ] = useState("");
const [ shown, setShown ] = useState(false);
const submitValue = useRef<string>(null);
useTriliumEvent("showPromptDialog", (opts) => {
setOpts(opts);
useTriliumEvent("showPromptDialog", (newOpts) => {
opts.current = newOpts;
setValue(newOpts.defaultValue ?? "");
setShown(true);
})
return (
<Modal
className="prompt-dialog"
title={opts?.title ?? t("prompt.title")}
title={opts.current?.title ?? t("prompt.title")}
size="lg"
zIndex={2000}
modalRef={modalRef} formRef={formRef}
onShown={() => {
opts?.shown?.({
opts.current?.shown?.({
$dialog: refToJQuerySelector(modalRef),
$question: refToJQuerySelector(labelRef),
$answer: refToJQuerySelector(answerRef),
@@ -58,33 +59,26 @@ function PromptDialogComponent() {
answerRef.current?.focus();
}}
onSubmit={() => {
const modal = BootstrapModal.getOrCreateInstance(modalRef.current!);
modal.hide();
opts?.callback?.(value);
submitValue.current = value;
setShown(false);
}}
onHidden={() => {
opts?.callback?.(null);
setShown(false);
opts.current?.callback?.(submitValue.current);
submitValue.current = null;
opts.current = undefined;
}}
footer={<Button text={t("prompt.ok")} keyboardShortcut="Enter" primary />}
show={shown}
stackable
>
<FormGroup label={opts?.message} labelRef={labelRef}>
<FormGroup name="prompt-dialog-answer" label={opts.current?.message} labelRef={labelRef}>
<FormTextBox
name="prompt-dialog-answer"
inputRef={answerRef}
currentValue={value} onChange={setValue} />
currentValue={value} onChange={setValue}
readOnly={opts.current?.readOnly}
/>
</FormGroup>
</Modal>
);
}
export default class PromptDialog extends ReactBasicWidget {
get component() {
return <PromptDialogComponent />;
}
}

View File

@@ -3,11 +3,10 @@ import { t } from "../../services/i18n";
import Button from "../react/Button";
import FormTextBox from "../react/FormTextBox";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import protected_session from "../../services/protected_session";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
function ProtectedSessionPasswordDialogComponent() {
export default function ProtectedSessionPasswordDialog() {
const [ shown, setShown ] = useState(false);
const [ password, setPassword ] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
@@ -38,11 +37,3 @@ function ProtectedSessionPasswordDialogComponent() {
</Modal>
)
}
export default class ProtectedSessionPasswordDialog extends ReactBasicWidget {
get component() {
return <ProtectedSessionPasswordDialogComponent />;
}
}

View File

@@ -6,7 +6,6 @@ import server from "../../services/server";
import toast from "../../services/toast";
import Button from "../react/Button";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import hoisted_note from "../../services/hoisted_note";
import type { RecentChangeRow } from "@triliumnext/commons";
import froca from "../../services/froca";
@@ -14,39 +13,32 @@ import { formatDateTime } from "../../utils/formatters";
import link from "../../services/link";
import RawHtml from "../react/RawHtml";
import ws from "../../services/ws";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
function RecentChangesDialogComponent() {
export default function RecentChangesDialog() {
const [ ancestorNoteId, setAncestorNoteId ] = useState<string>();
const [ groupedByDate, setGroupedByDate ] = useState<Map<String, RecentChangeRow[]>>();
const [ needsRefresh, setNeedsRefresh ] = useState(false);
const [ groupedByDate, setGroupedByDate ] = useState<Map<string, RecentChangeRow[]>>();
const [ refreshCounter, setRefreshCounter ] = useState(0);
const [ shown, setShown ] = useState(false);
useTriliumEvent("showRecentChanges", ({ ancestorNoteId }) => {
setNeedsRefresh(true);
useTriliumEvent("showRecentChanges", ({ ancestorNoteId }) => {
setAncestorNoteId(ancestorNoteId ?? hoisted_note.getHoistedNoteId());
setShown(true);
});
if (!groupedByDate || needsRefresh) {
useEffect(() => {
if (needsRefresh) {
setNeedsRefresh(false);
}
useEffect(() => {
server.get<RecentChangeRow[]>(`recent-changes/${ancestorNoteId}`)
.then(async (recentChanges) => {
// preload all notes into cache
await froca.getNotes(
recentChanges.map((r) => r.noteId),
true
);
server.get<RecentChangeRow[]>(`recent-changes/${ancestorNoteId}`)
.then(async (recentChanges) => {
// preload all notes into cache
await froca.getNotes(
recentChanges.map((r) => r.noteId),
true
);
const groupedByDate = groupByDate(recentChanges);
setGroupedByDate(groupedByDate);
});
})
}
const groupedByDate = groupByDate(recentChanges);
setGroupedByDate(groupedByDate);
});
}, [ shown, refreshCounter ])
return (
<Modal
@@ -57,10 +49,11 @@ function RecentChangesDialogComponent() {
header={
<Button
text={t("recent_changes.erase_notes_button")}
small style={{ padding: "0 10px" }}
size="small"
style={{ padding: "0 10px" }}
onClick={() => {
server.post("notes/erase-deleted-notes-now").then(() => {
setNeedsRefresh(true);
setRefreshCounter(refreshCounter + 1);
toast.showMessage(t("recent_changes.deleted_notes_message"));
});
}}
@@ -78,7 +71,7 @@ function RecentChangesDialogComponent() {
)
}
function RecentChangesTimeline({ groupedByDate, setShown }: { groupedByDate: Map<String, RecentChangeRow[]>, setShown: Dispatch<StateUpdater<boolean>> }) {
function RecentChangesTimeline({ groupedByDate, setShown }: { groupedByDate: Map<string, RecentChangeRow[]>, setShown: Dispatch<StateUpdater<boolean>> }) {
return (
<>
{ Array.from(groupedByDate.entries()).map(([dateDay, dayChanges]) => {
@@ -113,10 +106,6 @@ function RecentChangesTimeline({ groupedByDate, setShown }: { groupedByDate: Map
}
function NoteLink({ notePath, title }: { notePath: string, title: string }) {
if (!notePath || !title) {
return null;
}
const [ noteLink, setNoteLink ] = useState<JQuery<HTMLElement> | null>(null);
useEffect(() => {
link.createLink(notePath, {
@@ -155,25 +144,19 @@ function DeletedNoteLink({ change, setShown }: { change: RecentChangeRow, setSho
);
}
export default class RecentChangesDialog extends ReactBasicWidget {
get component() {
return <RecentChangesDialogComponent />
}
}
function groupByDate(rows: RecentChangeRow[]) {
const groupedByDate = new Map<String, RecentChangeRow[]>();
const groupedByDate = new Map<string, RecentChangeRow[]>();
for (const row of rows) {
const dateDay = row.date.substr(0, 10);
if (!groupedByDate.has(dateDay)) {
groupedByDate.set(dateDay, []);
let dateDayArray = groupedByDate.get(dateDay);
if (!dateDayArray) {
dateDayArray = [];
groupedByDate.set(dateDay, dateDayArray);
}
groupedByDate.get(dateDay)!.push(row);
dateDayArray.push(row);
}
return groupedByDate;

View File

@@ -8,7 +8,6 @@ import server from "../../services/server";
import toast from "../../services/toast";
import Button from "../react/Button";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import FormList, { FormListItem } from "../react/FormList";
import utils from "../../services/utils";
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
@@ -18,9 +17,9 @@ import type { CSSProperties } from "preact/compat";
import open from "../../services/open";
import ActionButton from "../react/ActionButton";
import options from "../../services/options";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
function RevisionsDialogComponent() {
export default function RevisionsDialog() {
const [ note, setNote ] = useState<FNote>();
const [ revisions, setRevisions ] = useState<RevisionItem[]>();
const [ currentRevision, setCurrentRevision ] = useState<RevisionItem>();
@@ -55,7 +54,7 @@ function RevisionsDialogComponent() {
helpPageId="vZWERwf8U3nx"
bodyStyle={{ display: "flex", height: "80vh" }}
header={
(!!revisions?.length && <Button text={t("revisions.delete_all_revisions")} small style={{ padding: "0 10px" }}
(!!revisions?.length && <Button text={t("revisions.delete_all_revisions")} size="small" style={{ padding: "0 10px" }}
onClick={async () => {
const text = t("revisions.confirm_delete_all");
@@ -72,6 +71,8 @@ function RevisionsDialogComponent() {
onHidden={() => {
setShown(false);
setNote(undefined);
setCurrentRevision(undefined);
setRevisions(undefined);
}}
show={shown}
>
@@ -202,17 +203,9 @@ function RevisionContent({ revisionItem, fullRevision }: { revisionItem?: Revisi
return <></>;
}
switch (revisionItem.type) {
case "text": {
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (contentRef.current?.querySelector("span.math-tex")) {
renderMathInElement(contentRef.current, { trust: true });
}
});
return <div ref={contentRef} className="ck-content" dangerouslySetInnerHTML={{ __html: content as string }}></div>
}
case "text":
return <RevisionContentText content={content} />
case "code":
return <pre style={CODE_STYLE}>{content}</pre>;
case "image":
@@ -264,6 +257,16 @@ function RevisionContent({ revisionItem, fullRevision }: { revisionItem?: Revisi
}
}
function RevisionContentText({ content }: { content: string | Buffer<ArrayBufferLike> | undefined }) {
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (contentRef.current?.querySelector("span.math-tex")) {
renderMathInElement(contentRef.current, { trust: true });
}
}, [content]);
return <div ref={contentRef} className="ck-content" dangerouslySetInnerHTML={{ __html: content as string }}></div>
}
function RevisionFooter({ note }: { note?: FNote }) {
if (!note) {
return <></>;
@@ -291,14 +294,6 @@ function RevisionFooter({ note }: { note?: FNote }) {
</>;
}
export default class RevisionsDialog extends ReactBasicWidget {
get component() {
return <RevisionsDialogComponent />
}
}
async function getNote(noteId?: string | null) {
if (noteId) {
return await froca.getNote(noteId);

View File

@@ -5,12 +5,11 @@ import FormCheckbox from "../react/FormCheckbox";
import FormRadioGroup from "../react/FormRadioGroup";
import FormTextBox from "../react/FormTextBox";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import server from "../../services/server";
import FormGroup from "../react/FormGroup";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
function SortChildNotesDialogComponent() {
export default function SortChildNotesDialog() {
const [ parentNoteId, setParentNoteId ] = useState<string>();
const [ sortBy, setSortBy ] = useState("title");
const [ sortDirection, setSortDirection ] = useState("asc");
@@ -83,20 +82,9 @@ function SortChildNotesDialogComponent() {
label={t("sort_child_notes.sort_with_respect_to_different_character_sorting")}
currentValue={sortNatural} onChange={setSortNatural}
/>
<FormGroup className="form-check" label={t("sort_child_notes.natural_sort_language")} description={t("sort_child_notes.the_language_code_for_natural_sort")}>
<FormTextBox
name="sort-locale"
currentValue={sortLocale} onChange={setSortLocale}
/>
<FormGroup name="sort-locale" className="form-check" label={t("sort_child_notes.natural_sort_language")} description={t("sort_child_notes.the_language_code_for_natural_sort")}>
<FormTextBox currentValue={sortLocale} onChange={setSortLocale} />
</FormGroup>
</Modal>
)
}
export default class SortChildNotesDialog extends ReactBasicWidget {
get component() {
return <SortChildNotesDialogComponent />;
}
}

View File

@@ -5,13 +5,12 @@ import FormCheckbox from "../react/FormCheckbox";
import FormFileUpload from "../react/FormFileUpload";
import FormGroup from "../react/FormGroup";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import options from "../../services/options";
import importService from "../../services/import.js";
import tree from "../../services/tree";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
function UploadAttachmentsDialogComponent() {
export default function UploadAttachmentsDialog() {
const [ parentNoteId, setParentNoteId ] = useState<string>();
const [ files, setFiles ] = useState<FileList | null>(null);
const [ shrinkImages, setShrinkImages ] = useState(options.is("compressImages"));
@@ -24,12 +23,12 @@ function UploadAttachmentsDialogComponent() {
setShown(true);
});
if (parentNoteId) {
useEffect(() => {
tree.getNoteTitle(parentNoteId).then((noteTitle) =>
setDescription(t("upload_attachments.files_will_be_uploaded", { noteTitle })));
}, [parentNoteId]);
}
useEffect(() => {
if (!parentNoteId) return;
tree.getNoteTitle(parentNoteId).then((noteTitle) =>
setDescription(t("upload_attachments.files_will_be_uploaded", { noteTitle })));
}, [parentNoteId]);
return (
<Modal
@@ -51,13 +50,12 @@ function UploadAttachmentsDialogComponent() {
onHidden={() => setShown(false)}
show={shown}
>
<FormGroup label={t("upload_attachments.choose_files")} description={description}>
<FormGroup name="files" label={t("upload_attachments.choose_files")} description={description}>
<FormFileUpload onChange={setFiles} multiple />
</FormGroup>
<FormGroup label={t("upload_attachments.options")}>
<FormCheckbox
name="shrink-images"
<FormGroup name="shrink-images" label={t("upload_attachments.options")}>
<FormCheckbox
hint={t("upload_attachments.tooltip")} label={t("upload_attachments.shrink_images")}
currentValue={shrinkImages} onChange={setShrinkImages}
/>
@@ -65,11 +63,3 @@ function UploadAttachmentsDialogComponent() {
</Modal>
);
}
export default class UploadAttachmentsDialog extends ReactBasicWidget {
get component() {
return <UploadAttachmentsDialogComponent />;
}
}

View File

@@ -1,120 +0,0 @@
import attributeService from "../services/attributes.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import { t } from "../services/i18n.js";
import type FNote from "../entities/fnote.js";
import type { EventData } from "../components/app_context.js";
import { Dropdown } from "bootstrap";
type Editability = "auto" | "readOnly" | "autoReadOnlyDisabled";
const TPL = /*html*/`
<div class="dropdown editability-select-widget">
<style>
.editability-dropdown {
width: 300px;
}
.editability-dropdown .dropdown-item {
display: flex !importamt;
}
.editability-dropdown .dropdown-item > div {
margin-left: 10px;
}
.editability-dropdown .description {
font-size: small;
color: var(--muted-text-color);
white-space: normal;
}
</style>
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm select-button dropdown-toggle editability-button">
<span class="editability-active-desc">${t("editability_select.auto")}</span>
<span class="caret"></span>
</button>
<div class="editability-dropdown dropdown-menu dropdown-menu-right tn-dropdown-list">
<a class="dropdown-item" href="#" data-editability="auto">
<span class="check">&check;</span>
<div>
${t("editability_select.auto")}
<div class="description">${t("editability_select.note_is_editable")}</div>
</div>
</a>
<a class="dropdown-item" href="#" data-editability="readOnly">
<span class="check">&check;</span>
<div>
${t("editability_select.read_only")}
<div class="description">${t("editability_select.note_is_read_only")}</div>
</div>
</a>
<a class="dropdown-item" href="#" data-editability="autoReadOnlyDisabled">
<span class="check">&check;</span>
<div>
${t("editability_select.always_editable")}
<div class="description">${t("editability_select.note_is_always_editable")}</div>
</div>
</a>
</div>
</div>
`;
export default class EditabilitySelectWidget extends NoteContextAwareWidget {
private dropdown!: Dropdown;
private $editabilityActiveDesc!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]);
this.$editabilityActiveDesc = this.$widget.find(".editability-active-desc");
this.$widget.on("click", ".dropdown-item", async (e) => {
this.dropdown.toggle();
const editability = $(e.target).closest("[data-editability]").attr("data-editability");
if (!this.note || !this.noteId) {
return;
}
for (const ownedAttr of this.note.getOwnedLabels()) {
if (["readOnly", "autoReadOnlyDisabled"].includes(ownedAttr.name)) {
await attributeService.removeAttributeById(this.noteId, ownedAttr.attributeId);
}
}
if (editability && editability !== "auto") {
await attributeService.addLabel(this.noteId, editability);
}
});
}
async refreshWithNote(note: FNote) {
let editability: Editability = "auto";
if (this.note?.isLabelTruthy("readOnly")) {
editability = "readOnly";
} else if (this.note?.isLabelTruthy("autoReadOnlyDisabled")) {
editability = "autoReadOnlyDisabled";
}
const labels = {
auto: t("editability_select.auto"),
readOnly: t("editability_select.read_only"),
autoReadOnlyDisabled: t("editability_select.always_editable")
};
this.$widget.find(".dropdown-item").removeClass("selected");
this.$widget.find(`.dropdown-item[data-editability='${editability}']`).addClass("selected");
this.$editabilityActiveDesc.text(labels[editability]);
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId)) {
this.refresh();
}
}
}

View File

@@ -1,6 +1,6 @@
// taken from the HTML source of https://boxicons.com/
interface Category {
export interface Category {
name: string;
id: number;
}

View File

@@ -0,0 +1,59 @@
.note-icon-widget {
padding-top: 3px;
padding-left: 7px;
margin-right: 0;
width: 50px;
height: 50px;
}
.note-icon-widget button.note-icon {
font-size: 180%;
background-color: transparent;
border: 1px solid transparent;
cursor: pointer;
padding: 6px;
color: var(--main-text-color);
}
.note-icon-widget button.note-icon:hover {
border: 1px solid var(--main-border-color);
}
.note-icon-widget .dropdown-menu {
border-radius: 10px;
border-width: 2px;
box-shadow: 10px 10px 93px -25px black;
padding: 10px 15px 10px 15px !important;
}
.note-icon-widget .filter-row {
padding-top: 10px;
padding-bottom: 10px;
padding-right: 20px;
display: flex;
align-items: baseline;
}
.note-icon-widget .filter-row span {
display: block;
padding-left: 15px;
padding-right: 15px;
font-weight: bold;
}
.note-icon-widget .icon-list {
height: 500px;
overflow: auto;
}
.note-icon-widget .icon-list span {
display: inline-block;
padding: 10px;
cursor: pointer;
border: 1px solid transparent;
font-size: 180%;
}
.note-icon-widget .icon-list span:hover {
border: 1px solid var(--main-border-color);
}

View File

@@ -1,229 +0,0 @@
import { t } from "../services/i18n.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import attributeService from "../services/attributes.js";
import server from "../services/server.js";
import type FNote from "../entities/fnote.js";
import type { EventData } from "../components/app_context.js";
import type { Icon } from "./icon_list.js";
import { Dropdown } from "bootstrap";
const TPL = /*html*/`
<div class="note-icon-widget dropdown">
<style>
.note-icon-widget {
padding-top: 3px;
padding-left: 7px;
margin-right: 0;
width: 50px;
height: 50px;
}
.note-icon-widget button.note-icon {
font-size: 180%;
background-color: transparent;
border: 1px solid transparent;
cursor: pointer;
padding: 6px;
color: var(--main-text-color);
}
.note-icon-widget button.note-icon:hover {
border: 1px solid var(--main-border-color);
}
.note-icon-widget .dropdown-menu {
border-radius: 10px;
border-width: 2px;
box-shadow: 10px 10px 93px -25px black;
padding: 10px 15px 10px 15px !important;
}
.note-icon-widget .filter-row {
padding-top: 10px;
padding-bottom: 10px;
padding-right: 20px;
display: flex;
align-items: baseline;
}
.note-icon-widget .filter-row span {
display: block;
padding-left: 15px;
padding-right: 15px;
font-weight: bold;
}
.note-icon-widget .icon-list {
height: 500px;
overflow: auto;
}
.note-icon-widget .icon-list span {
display: inline-block;
padding: 10px;
cursor: pointer;
border: 1px solid transparent;
font-size: 180%;
}
.note-icon-widget .icon-list span:hover {
border: 1px solid var(--main-border-color);
}
</style>
<button class="btn dropdown-toggle note-icon" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="${t("note_icon.change_note_icon")}"></button>
<div class="dropdown-menu" aria-labelledby="note-path-list-button" style="width: 610px;">
<div class="filter-row">
<span>${t("note_icon.category")}</span> <select name="icon-category" class="form-select"></select>
<span>${t("note_icon.search")}</span> <input type="text" name="icon-search" class="form-control" />
</div>
<div class="icon-list"></div>
</div>
</div>`;
interface IconToCountCache {
iconClassToCountMap: Record<string, number>;
}
export default class NoteIconWidget extends NoteContextAwareWidget {
private dropdown!: bootstrap.Dropdown;
private $icon!: JQuery<HTMLElement>;
private $iconList!: JQuery<HTMLElement>;
private $iconCategory!: JQuery<HTMLElement>;
private $iconSearch!: JQuery<HTMLElement>;
private iconToCountCache!: Promise<IconToCountCache | null> | null;
doRender() {
this.$widget = $(TPL);
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]);
this.$icon = this.$widget.find("button.note-icon");
this.$iconList = this.$widget.find(".icon-list");
this.$iconList.on("click", "span", async (e) => {
const clazz = $(e.target).attr("class");
if (this.noteId && this.note) {
await attributeService.setLabel(this.noteId, this.note.hasOwnedLabel("workspace") ? "workspaceIconClass" : "iconClass", clazz);
}
});
this.$iconCategory = this.$widget.find("select[name='icon-category']");
this.$iconCategory.on("change", () => this.renderDropdown());
this.$iconCategory.on("click", (e) => e.stopPropagation());
this.$iconSearch = this.$widget.find("input[name='icon-search']");
this.$iconSearch.on("input", () => this.renderDropdown());
this.$widget.on("show.bs.dropdown", async () => {
const { categories } = (await import("./icon_list.js")).default;
this.$iconCategory.empty();
for (const category of categories) {
this.$iconCategory.append($("<option>").text(category.name).attr("value", category.id));
}
this.$iconSearch.val("");
this.renderDropdown();
});
}
async refreshWithNote(note: FNote) {
this.$icon.removeClass().addClass(`${note.getIcon()} note-icon`);
this.$icon.prop("disabled", !!(this.noteContext?.viewScope?.viewMode !== "default"));
this.dropdown.hide();
}
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (this.noteId && loadResults.isNoteReloaded(this.noteId)) {
this.refresh();
return;
}
for (const attr of loadResults.getAttributeRows()) {
if (attr.type === "label" && ["iconClass", "workspaceIconClass"].includes(attr.name ?? "") && attributeService.isAffecting(attr, this.note)) {
this.refresh();
break;
}
}
}
async renderDropdown() {
const iconToCount = await this.getIconToCountMap();
const { icons } = (await import("./icon_list.js")).default;
this.$iconList.empty();
if (this.getIconLabels().length > 0) {
this.$iconList.append(
$(`<div style="text-align: center">`).append(
$(`<button class="btn btn-sm">${t("note_icon.reset-default")}</button>`).on("click", () =>
this.getIconLabels().forEach((label) => {
if (this.noteId) {
attributeService.removeAttributeById(this.noteId, label.attributeId);
}
})
)
)
);
}
const categoryId = parseInt(String(this.$iconCategory.find("option:selected")?.val()));
const search = String(this.$iconSearch.val())?.trim()?.toLowerCase();
const filteredIcons = icons.filter((icon) => {
if (categoryId && icon.category_id !== categoryId) {
return false;
}
if (search) {
if (!icon.name.includes(search) && !icon.term?.find((t) => t.includes(search))) {
return false;
}
}
return true;
});
if (iconToCount) {
filteredIcons.sort((a, b) => {
const countA = iconToCount[a.className ?? ""] || 0;
const countB = iconToCount[b.className ?? ""] || 0;
return countB - countA;
});
}
for (const icon of filteredIcons) {
this.$iconList.append(this.renderIcon(icon));
}
this.$iconSearch.focus();
}
async getIconToCountMap() {
if (!this.iconToCountCache) {
this.iconToCountCache = server.get<IconToCountCache>("other/icon-usage");
setTimeout(() => (this.iconToCountCache = null), 20000); // invalidate cache after 20 seconds
}
return (await this.iconToCountCache)?.iconClassToCountMap;
}
renderIcon(icon: Icon) {
return $("<span>")
.addClass("bx " + icon.className)
.attr("title", icon.name);
}
getIconLabels() {
if (!this.note) {
return [];
}
return this.note.getOwnedLabels().filter((label) => ["workspaceIconClass", "iconClass"].includes(label.name));
}
}

View File

@@ -0,0 +1,184 @@
import Dropdown from "./react/Dropdown";
import "./note_icon.css";
import { t } from "i18next";
import { useNoteContext, useNoteLabel } from "./react/hooks";
import { useEffect, useRef, useState } from "preact/hooks";
import server from "../services/server";
import type { Category, Icon } from "./icon_list";
import FormTextBox from "./react/FormTextBox";
import FormSelect from "./react/FormSelect";
import FNote from "../entities/fnote";
import attributes from "../services/attributes";
import Button from "./react/Button";
interface IconToCountCache {
iconClassToCountMap: Record<string, number>;
}
interface IconData {
iconToCount: Record<string, number>;
categories: Category[];
icons: Icon[];
}
let fullIconData: {
categories: Category[];
icons: Icon[];
};
let iconToCountCache!: Promise<IconToCountCache> | null;
export default function NoteIcon() {
const { note, viewScope } = useNoteContext();
const [ icon, setIcon ] = useState<string | null | undefined>();
const [ iconClass ] = useNoteLabel(note, "iconClass");
const [ workspaceIconClass ] = useNoteLabel(note, "workspaceIconClass");
useEffect(() => {
setIcon(note?.getIcon());
}, [ note, iconClass, workspaceIconClass ]);
return (
<Dropdown
className="note-icon-widget"
title={t("note_icon.change_note_icon")}
dropdownContainerStyle={{ width: "610px" }}
buttonClassName={`note-icon ${icon ?? "bx bx-empty"}`}
hideToggleArrow
disabled={viewScope?.viewMode !== "default"}
>
{ note && <NoteIconList note={note} /> }
</Dropdown>
)
}
function NoteIconList({ note }: { note: FNote }) {
const searchBoxRef = useRef<HTMLInputElement>(null);
const [ search, setSearch ] = useState<string>();
const [ categoryId, setCategoryId ] = useState<string>("0");
const [ iconData, setIconData ] = useState<IconData>();
useEffect(() => {
async function loadIcons() {
if (!fullIconData) {
fullIconData = (await import("./icon_list.js")).default;
}
// Filter by text and/or category.
let icons: Icon[] = fullIconData.icons;
const processedSearch = search?.trim()?.toLowerCase();
if (processedSearch || categoryId) {
icons = icons.filter((icon) => {
if (categoryId !== "0" && String(icon.category_id) !== categoryId) {
return false;
}
if (processedSearch) {
if (!icon.name.includes(processedSearch) &&
!icon.term?.find((t) => t.includes(processedSearch))) {
return false;
}
}
return true;
});
}
// Sort by count.
const iconToCount = await getIconToCountMap();
if (iconToCount) {
icons.sort((a, b) => {
const countA = iconToCount[a.className ?? ""] || 0;
const countB = iconToCount[b.className ?? ""] || 0;
return countB - countA;
});
}
setIconData({
iconToCount,
icons,
categories: fullIconData.categories
})
}
loadIcons();
}, [ search, categoryId ]);
return (
<>
<div class="filter-row">
<span>{t("note_icon.category")}</span>
<FormSelect
name="icon-category"
values={fullIconData?.categories ?? []}
currentValue={categoryId} onChange={setCategoryId}
keyProperty="id" titleProperty="name"
/>
<span>{t("note_icon.search")}</span>
<FormTextBox
inputRef={searchBoxRef}
type="text"
name="icon-search"
currentValue={search} onChange={setSearch}
autoFocus
/>
</div>
<div
class="icon-list"
onClick={(e) => {
const clickedTarget = e.target as HTMLElement;
if (!clickedTarget.classList.contains("bx")) {
return;
}
const iconClass = Array.from(clickedTarget.classList.values()).join(" ");
if (note) {
const attributeToSet = note.hasOwnedLabel("workspace") ? "workspaceIconClass" : "iconClass";
attributes.setLabel(note.noteId, attributeToSet, iconClass);
}
}}
>
{getIconLabels(note).length > 0 && (
<div style={{ textAlign: "center" }}>
<Button
text={t("note_icon.reset-default")}
onClick={() => {
if (!note) {
return;
}
for (const label of getIconLabels(note)) {
attributes.removeAttributeById(note.noteId, label.attributeId);
}
}}
/>
</div>
)}
{(iconData?.icons ?? []).map(({className, name}) => (
<span class={`bx ${className}`} title={name} />
))}
</div>
</>
);
}
async function getIconToCountMap() {
if (!iconToCountCache) {
iconToCountCache = server.get<IconToCountCache>("other/icon-usage");
setTimeout(() => (iconToCountCache = null), 20000); // invalidate cache after 20 seconds
}
return (await iconToCountCache).iconClassToCountMap;
}
function getIconLabels(note: FNote) {
if (!note) {
return [];
}
return note.getOwnedLabels()
.filter((label) => ["workspaceIconClass", "iconClass"]
.includes(label.name));
}

View File

@@ -1,166 +0,0 @@
import { Dropdown } from "bootstrap";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import { getAvailableLocales, getLocaleById, t } from "../services/i18n.js";
import type { EventData } from "../components/app_context.js";
import type FNote from "../entities/fnote.js";
import attributes from "../services/attributes.js";
import type { Locale } from "@triliumnext/commons";
import options from "../services/options.js";
import appContext from "../components/app_context.js";
const TPL = /*html*/`\
<div class="dropdown note-language-widget">
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle select-button note-language-button">
<span class="note-language-desc"></span>
<span class="caret"></span>
</button>
<div class="note-language-dropdown dropdown-menu dropdown-menu-left tn-dropdown-list"></div>
<button class="language-help-button icon-action bx bx-help-circle" type="button" data-in-app-help="B0lcI9xz1r8K" title="${t("open-help-page")}"></button>
<style>
.note-language-widget {
display: flex;
align-items: center;
}
.language-help-button {
margin-left: 4px;
}
.note-language-dropdown [dir=rtl] {
text-align: right;
}
.dropdown-item.rtl > .check {
order: 1;
}
</style>
</div>
`;
const DEFAULT_LOCALE: Locale = {
id: "",
name: t("note_language.not_set")
};
export default class NoteLanguageWidget extends NoteContextAwareWidget {
private dropdown!: Dropdown;
private $noteLanguageDropdown!: JQuery<HTMLElement>;
private $noteLanguageDesc!: JQuery<HTMLElement>;
private locales: (Locale | "---")[];
private currentLanguageId?: string;
constructor() {
super();
this.locales = NoteLanguageWidget.#buildLocales();
}
doRender() {
this.$widget = $(TPL);
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]);
this.$widget.on("show.bs.dropdown", () => this.renderDropdown());
this.$noteLanguageDropdown = this.$widget.find(".note-language-dropdown")
this.$noteLanguageDesc = this.$widget.find(".note-language-desc");
}
renderDropdown() {
this.$noteLanguageDropdown.empty();
if (!this.note) {
return;
}
for (const locale of this.locales) {
if (typeof locale === "object") {
const $title = $("<span>").text(locale.name);
const $link = $('<a class="dropdown-item">')
.attr("data-language", locale.id)
.append('<span class="check">&check;</span> ')
.append($title)
.on("click", () => {
const languageId = $link.attr("data-language") ?? "";
this.save(languageId);
});
if (locale.rtl) {
$link.attr("dir", "rtl");
}
if (locale.id === this.currentLanguageId) {
$link.addClass("selected");
}
this.$noteLanguageDropdown.append($link);
} else {
this.$noteLanguageDropdown.append('<div class="dropdown-divider"></div>');
}
}
const $configureLink = $('<a class="dropdown-item">')
.append(`<span>${t("note_language.configure-languages")}</span>`)
.on("click", () => appContext.tabManager.openContextWithNote("_optionsLocalization", { activate: true }));
this.$noteLanguageDropdown.append($configureLink);
}
async save(languageId: string) {
if (!this.note) {
return;
}
attributes.setAttribute(this.note, "label", "language", languageId);
}
async refreshWithNote(note: FNote) {
const currentLanguageId = note.getLabelValue("language") ?? "";
const language = getLocaleById(currentLanguageId) ?? DEFAULT_LOCALE;
this.currentLanguageId = currentLanguageId;
this.$noteLanguageDesc.text(language.name);
this.dropdown.hide();
}
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isOptionReloaded("languages")) {
this.locales = NoteLanguageWidget.#buildLocales();
}
if (loadResults.getAttributeRows().find((a) => a.noteId === this.noteId && a.name === "language")) {
this.refresh();
}
}
static #buildLocales() {
const enabledLanguages = JSON.parse(options.get("languages") ?? "[]") as string[];
const filteredLanguages = getAvailableLocales().filter((l) => typeof l !== "object" || enabledLanguages.includes(l.id));
const leftToRightLanguages = filteredLanguages.filter((l) => !l.rtl);
const rightToLeftLanguages = filteredLanguages.filter((l) => l.rtl);
let locales: ("---" | Locale)[] = [
DEFAULT_LOCALE
];
if (leftToRightLanguages.length > 0) {
locales = [
...locales,
"---",
...leftToRightLanguages
];
}
if (rightToLeftLanguages.length > 0) {
locales = [
...locales,
"---",
...rightToLeftLanguages
];
}
// This will separate the list of languages from the "Configure languages" button.
// If there is at least one language.
locales.push("---");
return locales;
}
}

View File

@@ -0,0 +1,25 @@
.note-title-widget {
flex-grow: 1000;
height: 100%;
}
.note-title-widget input.note-title {
font-size: 110%;
border: 0;
margin: 2px 0px;
min-width: 5em;
width: 100%;
padding: 1px 12px;
}
.note-title-widget input.note-title.protected {
text-shadow: 4px 4px 4px var(--muted-text-color);
}
body.mobile .note-title-widget input.note-title {
padding: 0;
}
body.desktop .note-title-widget input.note-title {
font-size: 180%;
}

View File

@@ -1,147 +0,0 @@
import { t } from "../services/i18n.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import protectedSessionHolder from "../services/protected_session_holder.js";
import server from "../services/server.js";
import SpacedUpdate from "../services/spaced_update.js";
import appContext, { type EventData } from "../components/app_context.js";
import branchService from "../services/branches.js";
import shortcutService from "../services/shortcuts.js";
import utils from "../services/utils.js";
import type FNote from "../entities/fnote.js";
const TPL = /*html*/`
<div class="note-title-widget">
<style>
.note-title-widget {
flex-grow: 1000;
height: 100%;
}
.note-title-widget input.note-title {
font-size: 110%;
border: 0;
margin: 2px 0px;
min-width: 5em;
width: 100%;
}
body.mobile .note-title-widget input.note-title {
padding: 0;
}
body.desktop .note-title-widget input.note-title {
font-size: 180%;
}
.note-title-widget input.note-title.protected {
text-shadow: 4px 4px 4px var(--muted-text-color);
}
</style>
<input autocomplete="off" value="" placeholder="${t("note_title.placeholder")}" class="note-title" tabindex="100">
</div>`;
export default class NoteTitleWidget extends NoteContextAwareWidget {
private $noteTitle!: JQuery<HTMLElement>;
private deleteNoteOnEscape: boolean;
private spacedUpdate: SpacedUpdate;
constructor() {
super();
this.spacedUpdate = new SpacedUpdate(async () => {
const title = this.$noteTitle.val();
if (this.note) {
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
}
await server.put(`notes/${this.noteId}/title`, { title }, this.componentId);
});
this.deleteNoteOnEscape = false;
appContext.addBeforeUnloadListener(this);
}
doRender() {
this.$widget = $(TPL);
this.$noteTitle = this.$widget.find(".note-title");
this.$noteTitle.on("input", () => this.spacedUpdate.scheduleUpdate());
this.$noteTitle.on("blur", () => {
this.spacedUpdate.updateNowIfNecessary();
this.deleteNoteOnEscape = false;
});
shortcutService.bindElShortcut(this.$noteTitle, "esc", () => {
if (this.deleteNoteOnEscape && this.noteContext?.isActive() && this.noteContext?.note) {
branchService.deleteNotes(Object.values(this.noteContext.note.parentToBranch));
}
});
shortcutService.bindElShortcut(this.$noteTitle, "return", () => {
this.triggerCommand("focusOnDetail", { ntxId: this.noteContext?.ntxId });
});
}
async refreshWithNote(note: FNote) {
const isReadOnly =
(note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable())
|| utils.isLaunchBarConfig(note.noteId)
|| this.noteContext?.viewScope?.viewMode !== "default";
this.$noteTitle.val(isReadOnly ? (await this.noteContext?.getNavigationTitle()) || "" : note.title);
this.$noteTitle.prop("readonly", isReadOnly);
this.setProtectedStatus(note);
}
setProtectedStatus(note: FNote) {
this.$noteTitle.toggleClass("protected", !!note.isProtected);
}
async beforeNoteSwitchEvent({ noteContext }: EventData<"beforeNoteSwitch">) {
if (this.isNoteContext(noteContext.ntxId)) {
await this.spacedUpdate.updateNowIfNecessary();
}
}
async beforeNoteContextRemoveEvent({ ntxIds }: EventData<"beforeNoteContextRemove">) {
if (this.isNoteContext(ntxIds)) {
await this.spacedUpdate.updateNowIfNecessary();
}
}
focusOnTitleEvent() {
if (this.noteContext && this.noteContext.isActive()) {
this.$noteTitle.trigger("focus");
}
}
focusAndSelectTitleEvent({ isNewNote } = { isNewNote: false }) {
if (this.noteContext && this.noteContext.isActive()) {
this.$noteTitle.trigger("focus").trigger("select");
this.deleteNoteOnEscape = isNewNote;
}
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isNoteReloaded(this.noteId) && this.note) {
// not updating the title specifically since the synced title might be older than what the user is currently typing
this.setProtectedStatus(this.note);
}
if (loadResults.isNoteReloaded(this.noteId, this.componentId)) {
this.refresh();
}
}
beforeUnloadEvent() {
return this.spacedUpdate.isAllSavedAndTriggerUpdate();
}
}

View File

@@ -0,0 +1,99 @@
import { useEffect, useRef, useState } from "preact/hooks";
import { t } from "../services/i18n";
import FormTextBox from "./react/FormTextBox";
import { useNoteContext, useNoteProperty, useSpacedUpdate, useTriliumEvent, useTriliumEvents } from "./react/hooks";
import protected_session_holder from "../services/protected_session_holder";
import server from "../services/server";
import "./note_title.css";
import { isLaunchBarConfig } from "../services/utils";
import appContext from "../components/app_context";
import branches from "../services/branches";
export default function NoteTitleWidget() {
const { note, noteId, componentId, viewScope, noteContext, parentComponent } = useNoteContext();
const title = useNoteProperty(note, "title", componentId);
const isProtected = useNoteProperty(note, "isProtected");
const newTitle = useRef("");
const [ isReadOnly, setReadOnly ] = useState<boolean>(false);
const [ navigationTitle, setNavigationTitle ] = useState<string | null>(null);
// Manage read-only
useEffect(() => {
const isReadOnly = note === null
|| note === undefined
|| (note.isProtected && !protected_session_holder.isProtectedSessionAvailable())
|| isLaunchBarConfig(note.noteId)
|| viewScope?.viewMode !== "default";
setReadOnly(isReadOnly);
}, [ note, note?.noteId, note?.isProtected, viewScope?.viewMode ]);
// Manage the title for read-only notes
useEffect(() => {
if (isReadOnly) {
noteContext?.getNavigationTitle().then(setNavigationTitle);
}
}, [isReadOnly]);
// Save changes to title.
const spacedUpdate = useSpacedUpdate(async () => {
if (!note) {
return;
}
protected_session_holder.touchProtectedSessionIfNecessary(note);
await server.put<void>(`notes/${noteId}/title`, { title: newTitle.current }, componentId);
});
// Prevent user from navigating away if the spaced update is not done.
useEffect(() => {
appContext.addBeforeUnloadListener(() => spacedUpdate.isAllSavedAndTriggerUpdate());
}, []);
useTriliumEvents([ "beforeNoteSwitch", "beforeNoteContextRemove" ], () => spacedUpdate.updateNowIfNecessary());
// Manage focus.
const textBoxRef = useRef<HTMLInputElement>(null);
const isNewNote = useRef<boolean>();
useTriliumEvents([ "focusOnTitle", "focusAndSelectTitle" ], (e, eventName) => {
if (noteContext?.isActive() && textBoxRef.current) {
textBoxRef.current.focus();
if (eventName === "focusAndSelectTitle") {
textBoxRef.current.select();
}
isNewNote.current = ("isNewNote" in e ? e.isNewNote : false);
}
});
return (
<div className="note-title-widget">
{note && <FormTextBox
inputRef={textBoxRef}
autocomplete="off"
currentValue={(!isReadOnly ? title : navigationTitle) ?? ""}
placeholder={t("note_title.placeholder")}
className={`note-title ${isProtected ? "protected" : ""}`}
tabIndex={100}
readOnly={isReadOnly}
onChange={(newValue) => {
newTitle.current = newValue;
spacedUpdate.scheduleUpdate();
}}
onKeyDown={(e) => {
// Focus on the note content when pressing enter.
if (e.key === "Enter") {
e.preventDefault();
parentComponent.triggerCommand("focusOnDetail", { ntxId: noteContext?.ntxId });
return;
}
if (e.key === "Escape" && isNewNote.current && noteContext?.isActive() && note) {
branches.deleteNotes(Object.values(note.parentToBranch));
}
}}
onBlur={() => {
spacedUpdate.updateNowIfNecessary();
isNewNote.current = false;
}}
/>}
</div>
);
}

View File

@@ -152,7 +152,7 @@ const TPL = /*html*/`
const MAX_SEARCH_RESULTS_IN_TREE = 100;
// this has to be hanged on the actual elements to effectively intercept and stop click event
const cancelClickPropagation: JQuery.TypeEventHandler<unknown, unknown, unknown, unknown, any> = (e) => e.stopPropagation();
const cancelClickPropagation: (e: JQuery.ClickEvent) => void = (e) => e.stopPropagation();
// TODO: Fix once we remove Node.js API from public
type Timeout = NodeJS.Timeout | string | number | undefined;

View File

@@ -1,175 +0,0 @@
import { Dropdown } from "bootstrap";
import { NOTE_TYPES } from "../services/note_types.js";
import { t } from "../services/i18n.js";
import dialogService from "../services/dialog.js";
import mimeTypesService from "../services/mime_types.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import server from "../services/server.js";
import type { EventData } from "../components/app_context.js";
import type { NoteType } from "../entities/fnote.js";
import type FNote from "../entities/fnote.js";
const NOT_SELECTABLE_NOTE_TYPES = NOTE_TYPES.filter((nt) => nt.reserved || nt.static).map((nt) => nt.type);
const TPL = /*html*/`
<div class="dropdown note-type-widget">
<style>
.note-type-dropdown {
max-height: 500px;
overflow-y: auto;
overflow-x: hidden;
}
</style>
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle select-button note-type-button">
<span class="note-type-desc"></span>
<span class="caret"></span>
</button>
<div class="note-type-dropdown dropdown-menu dropdown-menu-left tn-dropdown-list"></div>
</div>
`;
export default class NoteTypeWidget extends NoteContextAwareWidget {
private dropdown!: Dropdown;
private $noteTypeDropdown!: JQuery<HTMLElement>;
private $noteTypeButton!: JQuery<HTMLElement>;
private $noteTypeDesc!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]);
this.$widget.on("show.bs.dropdown", () => this.renderDropdown());
this.$noteTypeDropdown = this.$widget.find(".note-type-dropdown");
this.$noteTypeButton = this.$widget.find(".note-type-button");
this.$noteTypeDesc = this.$widget.find(".note-type-desc");
this.$widget.on("click", ".dropdown-item", () => this.dropdown.toggle());
}
async refreshWithNote(note: FNote) {
this.$noteTypeButton.prop("disabled", () => NOT_SELECTABLE_NOTE_TYPES.includes(note.type));
this.$noteTypeDesc.text(await this.findTypeTitle(note.type, note.mime));
this.dropdown.hide();
}
/** the actual body is rendered lazily on note-type button click */
async renderDropdown() {
this.$noteTypeDropdown.empty();
if (!this.note) {
return;
}
for (const noteType of NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static)) {
let $typeLink: JQuery<HTMLElement>;
const $title = $("<span>").text(noteType.title);
if (noteType.isNew) {
$title.append($(`<span class="badge new-note-type-badge">`).text(t("note_types.new-feature")));
}
if (noteType.isBeta) {
$title.append($(`<span class="badge">`).text(t("note_types.beta-feature")));
}
if (noteType.type !== "code") {
$typeLink = $('<a class="dropdown-item">')
.attr("data-note-type", noteType.type)
.append('<span class="check">&check;</span> ')
.append($title)
.on("click", (e) => {
const type = $typeLink.attr("data-note-type");
const noteType = NOTE_TYPES.find((nt) => nt.type === type);
if (noteType) {
this.save(noteType.type, noteType.mime);
}
});
} else {
this.$noteTypeDropdown.append('<div class="dropdown-divider"></div>');
$typeLink = $('<a class="dropdown-item disabled">').attr("data-note-type", noteType.type).append('<span class="check">&check;</span> ').append($("<strong>").text(noteType.title));
}
if (this.note.type === noteType.type) {
$typeLink.addClass("selected");
}
this.$noteTypeDropdown.append($typeLink);
}
for (const mimeType of mimeTypesService.getMimeTypes()) {
if (!mimeType.enabled) {
continue;
}
const $mimeLink = $('<a class="dropdown-item">')
.attr("data-mime-type", mimeType.mime)
.append('<span class="check">&check;</span> ')
.append($("<span>").text(mimeType.title))
.on("click", (e) => {
const $link = $(e.target).closest(".dropdown-item");
this.save("code", $link.attr("data-mime-type") ?? "");
});
if (this.note.type === "code" && this.note.mime === mimeType.mime) {
$mimeLink.addClass("selected");
this.$noteTypeDesc.text(mimeType.title);
}
this.$noteTypeDropdown.append($mimeLink);
}
}
async findTypeTitle(type: NoteType, mime: string) {
if (type === "code") {
const mimeTypes = mimeTypesService.getMimeTypes();
const found = mimeTypes.find((mt) => mt.mime === mime);
return found ? found.title : mime;
} else {
const noteType = NOTE_TYPES.find((nt) => nt.type === type);
return noteType ? noteType.title : type;
}
}
async save(type: NoteType, mime?: string) {
if (type === this.note?.type && mime === this.note?.mime) {
return;
}
if (type !== this.note?.type && !(await this.confirmChangeIfContent())) {
return;
}
await server.put(`notes/${this.noteId}/type`, { type, mime });
}
async confirmChangeIfContent() {
if (!this.note) {
return;
}
const blob = await this.note.getBlob();
if (!blob?.content || !blob.content.trim().length) {
return true;
}
return await dialogService.confirm(t("note_types.confirm-change"));
}
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isNoteReloaded(this.noteId)) {
this.refresh();
}
}
}

View File

@@ -1,16 +1,16 @@
import { t } from "../../services/i18n.js";
import server from "../../services/server.js";
import ws from "../../services/ws.js";
import treeService from "../../services/tree.js";
import noteAutocompleteService from "../../services/note_autocomplete.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import attributeService from "../../services/attributes.js";
import options from "../../services/options.js";
import utils from "../../services/utils.js";
import type FNote from "../../entities/fnote.js";
import type { Attribute } from "../../services/attribute_parser.js";
import type FAttribute from "../../entities/fattribute.js";
import type { EventData } from "../../components/app_context.js";
import { t } from "../services/i18n.js";
import server from "../services/server.js";
import ws from "../services/ws.js";
import treeService from "../services/tree.js";
import noteAutocompleteService from "../services/note_autocomplete.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import attributeService from "../services/attributes.js";
import options from "../services/options.js";
import utils from "../services/utils.js";
import type FNote from "../entities/fnote.js";
import type { Attribute } from "../services/attribute_parser.js";
import type FAttribute from "../entities/fattribute.js";
import type { EventData } from "../components/app_context.js";
const TPL = /*html*/`
<div class="promoted-attributes-widget">

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