Compare commits

...

431 Commits

Author SHA1 Message Date
perf3ct
055556891d fix(docs): try to fix swagger ui api pages, take 1 2025-08-27 20:05:52 +00:00
perf3ct
a58cfbec05 fix(docs): try to fix swagger ui api pages, take 1 2025-08-27 19:41:51 +00:00
Elian Doran
3795be4750 Translations update from Hosted Weblate (#6802) 2025-08-27 18:29:13 +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
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
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
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
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
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
Sky Swimmer
9a08f6534b Fix casing and formatting in i18n.ts which was causing compile errors 2025-08-24 23:14:45 +02: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
c97c66ed8a Translations update from Hosted Weblate (#6767) 2025-08-24 17:30:55 +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
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
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
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
Микола Копитін
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
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
Adorian Doran
d1538508e8 client/settings/disable motion: turn off jQuery animations if motion is disabled 2025-08-22 22:20:57 +03:00
Adorian Doran
9b1da8c311 Settings/Appearance: improve CSS selector specificity 2025-08-22 21:37:56 +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
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
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
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
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
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
Elian Doran
1dfcf960d3 fix(client): missing calendar view language support 2025-08-14 15:20:08 +03:00
Elian Doran
9bdc51a3fb feat(i18n): add Japanese language 2025-08-14 14:51:57 +03:00
Elian Doran
dbf3bcfacf Merge remote-tracking branch 'weblate/main' 2025-08-14 14:34:30 +03:00
Elian Doran
3d5b269315 chore(docs): fix file 2025-08-14 14:23:37 +03:00
Elian Doran
48f97da9cc chore(forge/rpm): rename key properly 2025-08-14 14:10:33 +03:00
Elian Doran
9c954fbd81 Create CNAME 2025-08-14 13:53:25 +03:00
Elian Doran
c6bd41654f chore(forge/rpm): add public key 2025-08-14 13:40:23 +03:00
Francis C
d65a74bb23 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-14 12:30:37 +02:00
acwr47
ff08bca042 Translated using Weblate (Japanese)
Currently translated at 41.6% (646 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-08-14 12:30:36 +02:00
Francis C
a5d3d2e3b4 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-14 12:30:35 +02:00
Bruno MARGUERIN
496a0667ee 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-14 12:30:34 +02:00
Bruno MARGUERIN
9be688b667 Translated using Weblate (French)
Currently translated at 80.7% (1252 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fr/
2025-08-14 12:30:33 +02:00
Elian Doran
f3d9008c61 feat(forge): rpm signing (#6646) 2025-08-14 13:30:26 +03:00
Elian Doran
649a43c978 fix(forge): RPM signing not done on the right file 2025-08-14 12:45:18 +03:00
Elian Doran
50568704ca feat(forge): minor improvements to RPM signing 2025-08-14 12:40:19 +03:00
Elian Doran
b66b4dec83 feat(forge): proper rpm signing 2025-08-14 12:04:12 +03:00
Francis C
8d0e807435 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-14 09:02:31 +00:00
acwr47
bf05ed7caf 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-14 09:02:29 +00:00
Elian Doran
b5080eff00 Translated using Weblate (Russian)
Currently translated at 55.6% (863 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ru/
2025-08-14 09:02:28 +00:00
Francis C
c474769dd6 Translated using Weblate (Japanese)
Currently translated at 35.4% (550 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-08-14 09:02:27 +00:00
acwr47
a6ae01da0b Translated using Weblate (Japanese)
Currently translated at 35.4% (550 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-08-14 09:02:25 +00:00
Francis C
2bf4c44dbf 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-14 09:02:24 +00:00
Francis C
5ca0fbba13 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-14 09:02:22 +00:00
Elian Doran
4cd84b2019 Translated using Weblate (Romanian)
Currently translated at 99.0% (1536 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ro/
2025-08-14 09:02:19 +00:00
Marcelo Popper Costa
c502a45cf5 Translated using Weblate (Portuguese (Brazil))
Currently translated at 22.3% (346 of 1550 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pt_BR/
2025-08-14 09:02:17 +00:00
Francis C
9e66914306 Translated using Weblate (Chinese (Simplified 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_Hans/
2025-08-14 09:02:15 +00:00
Elian Doran
d33d27ee82 feat(forge): validate rpm signing 2025-08-14 11:45:59 +03:00
Elian Doran
e2b13573ae feat(forge): rpm signing 2025-08-14 10:43:38 +03:00
Elian Doran
ec74f5f1de feat(logs): provide an option to keep all logs (#6644) 2025-08-14 08:51:46 +03:00
Elian Doran
5dee56debc Add Traditional Chinese translation for README file & fix Docker Hub URL (#6645) 2025-08-14 08:43:08 +03:00
Francis C.
5623fc992d Update README-ZH_TW.md (tiny fix) 2025-08-14 12:04:45 +08:00
Francis C.
1d28bfc570 Update README-ZH_TW.md (tiny fix) 2025-08-14 11:25:53 +08:00
Francis C.
084327e973 Revise some words for Simplified Chinese translation 2025-08-14 11:20:32 +08:00
Francis C.
b2885efdc1 Update README-ZH_CN.md 2025-08-14 10:56:54 +08:00
Francis C.
b65a75f138 fix relative path for URLs 2025-08-14 10:27:41 +08:00
Francis C.
0ee7f50bb4 Move readme to docs folder 2025-08-14 10:15:03 +08:00
Francis C.
02ce21bc18 Add readme file translation for Traditional Chinese & fix Docker Hub URL 2025-08-14 10:12:14 +08:00
Romain DEP.
3ba487bb00 feat(logs): provide an option to keep all logs 2025-08-13 23:35:31 +02:00
236 changed files with 30648 additions and 18002 deletions

View File

@@ -162,3 +162,25 @@ runs:
echo "Found ZIP: $zip_file" echo "Found ZIP: $zip_file"
echo "Note: ZIP files are not code signed, but their contents should be" echo "Note: ZIP files are not code signed, but their contents should be"
fi fi
- name: Sign the RPM
if: inputs.os == 'linux'
shell: ${{ inputs.shell }}
run: |
echo -n "$GPG_SIGNING_KEY" | base64 --decode | gpg --import
# Import the key into RPM for verification
gpg --export -a > pubkey
rpm --import pubkey
rm pubkey
# Sign the RPM
rpm_file=$(find ./apps/desktop/upload -name "*.rpm" -print -quit)
rpmsign --define "_gpg_name Trilium Notes Signing Key <triliumnotes@outlook.com>" --addsign "$rpm_file"
rpm -Kv "$rpm_file"
# Validate code signing
if ! rpm -K "$rpm_file" | grep -q "digests signatures OK"; then
echo .rpm file not signed
exit 1
fi

View File

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

View File

@@ -27,7 +27,7 @@ permissions:
jobs: jobs:
nightly-electron: nightly-electron:
if: github.repository == 'TriliumNext/Trilium' if: github.repository == ${{ vars.REPO_MAIN }}
name: Deploy nightly name: Deploy nightly
strategy: strategy:
fail-fast: false fail-fast: false
@@ -76,6 +76,7 @@ jobs:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
WINDOWS_SIGN_EXECUTABLE: ${{ vars.WINDOWS_SIGN_EXECUTABLE }} WINDOWS_SIGN_EXECUTABLE: ${{ vars.WINDOWS_SIGN_EXECUTABLE }}
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
- name: Publish release - name: Publish release
uses: softprops/action-gh-release@v2.3.2 uses: softprops/action-gh-release@v2.3.2
@@ -97,7 +98,7 @@ jobs:
path: apps/desktop/upload path: apps/desktop/upload
nightly-server: nightly-server:
if: github.repository == 'TriliumNext/Trilium' if: github.repository == ${{ vars.REPO_MAIN }}
name: Deploy server nightly name: Deploy server nightly
strategy: strategy:
fail-fast: false fail-fast: false

View File

@@ -58,6 +58,7 @@ jobs:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
WINDOWS_SIGN_EXECUTABLE: ${{ vars.WINDOWS_SIGN_EXECUTABLE }} WINDOWS_SIGN_EXECUTABLE: ${{ vars.WINDOWS_SIGN_EXECUTABLE }}
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
- name: Upload the artifact - name: Upload the artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

View File

@@ -1,11 +1,11 @@
# Trilium Notes # Trilium Notes
![GitHub Sponsors](https://img.shields.io/github/sponsors/eliandoran) ![LiberaPay patrons](https://img.shields.io/liberapay/patrons/ElianDoran) ![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) ![Docker Pulls](https://img.shields.io/docker/pulls/triliumnext/trilium)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/triliumnext/notes/total) ![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/) [![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](./docs/README-ZH_CN.md) | [Russian](./docs/README.ru.md) | [Japanese](./docs/README.ja.md) | [Italian](./docs/README.it.md) | [Spanish](./docs/README.es.md) [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)
Trilium Notes is a free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases. Trilium Notes is a free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases.
@@ -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. - [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. - [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 ## 📖 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.) - [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) - 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 Discussions](https://github.com/TriliumNext/Trilium/discussions) (For asynchronous discussions.)
- [Github Issues](https://github.com/TriliumNext/Notes/issues) (For bug reports and feature requests.) - [Github Issues](https://github.com/TriliumNext/Trilium/issues) (For bug reports and feature requests.)
## 🏗 Installation ## 🏗 Installation
@@ -104,13 +104,15 @@ 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). 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 ### Server
To install TriliumNext on your own server (including via Docker from [Dockerhub](https://hub.docker.com/r/triliumnext/notes)) follow [the server installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation). To install TriliumNext on your own server (including via Docker from [Dockerhub](https://hub.docker.com/r/triliumnext/trilium)) follow [the server installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation).
## 💻 Contribute ## 💻 Contribute
@@ -152,11 +154,11 @@ pnpm install
pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32 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 ### 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 ## 👏 Shoutouts
@@ -168,7 +170,7 @@ Please view the [documentation guide](./docs/Developer%20Guide/Developer%20Guide
## 🤝 Support ## 🤝 Support
Support for the TriliumNext organization will be possible in the near future. For now, you can: 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). - 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" "chore:generate-openapi": "tsx bin/generate-openapi.js"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.54.2", "@playwright/test": "1.55.0",
"@stylistic/eslint-plugin": "5.2.3", "@stylistic/eslint-plugin": "5.2.3",
"@types/express": "5.0.3", "@types/express": "5.0.3",
"@types/node": "22.17.1", "@types/node": "22.18.0",
"@types/yargs": "17.0.33", "@types/yargs": "17.0.33",
"@vitest/coverage-v8": "3.2.4", "@vitest/coverage-v8": "3.2.4",
"eslint": "9.33.0", "eslint": "9.34.0",
"eslint-plugin-simple-import-sort": "12.1.1", "eslint-plugin-simple-import-sort": "12.1.1",
"esm": "3.2.25", "esm": "3.2.25",
"jsdoc": "4.0.4", "jsdoc": "4.0.4",
@@ -49,7 +49,7 @@
"rcedit": "4.0.1", "rcedit": "4.0.1",
"rimraf": "6.0.1", "rimraf": "6.0.1",
"tslib": "2.8.1", "tslib": "2.8.1",
"typedoc": "0.28.10", "typedoc": "0.28.11",
"typedoc-plugin-missing-exports": "4.1.0" "typedoc-plugin-missing-exports": "4.1.0"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

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

View File

@@ -35,8 +35,10 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
loadResults.addOption(attributeEntity.name); loadResults.addOption(attributeEntity.name);
} else if (ec.entityName === "attachments") { } else if (ec.entityName === "attachments") {
processAttachment(loadResults, ec); 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 // 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 { } else {
throw new Error(`Unknown entityName '${ec.entityName}'`); throw new Error(`Unknown entityName '${ec.entityName}'`);
} }
@@ -77,9 +79,7 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
noteAttributeCache.invalidate(); noteAttributeCache.invalidate();
} }
// TODO: Remove after porting the file const appContext = (await import("../components/app_context.js")).default;
// @ts-ignore
const appContext = (await import("../components/app_context.js")).default as any;
await appContext.triggerEvent("entitiesReloaded", { loadResults }); await appContext.triggerEvent("entitiesReloaded", { loadResults });
} }
} }

View File

@@ -3,6 +3,7 @@ import i18next from "i18next";
import i18nextHttpBackend from "i18next-http-backend"; import i18nextHttpBackend from "i18next-http-backend";
import server from "./server.js"; import server from "./server.js";
import type { Locale } from "@triliumnext/commons"; import type { Locale } from "@triliumnext/commons";
import { initReactI18next } from "react-i18next";
let locales: Locale[] | null; let locales: Locale[] | null;
@@ -16,6 +17,7 @@ export async function initLocale() {
locales = await server.get<Locale[]>("options/locales"); locales = await server.get<Locale[]>("options/locales");
i18next.use(initReactI18next);
await i18next.use(i18nextHttpBackend).init({ await i18next.use(i18nextHttpBackend).init({
lng: locale, lng: locale,
fallbackLng: "en", 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 { AttributeType } from "../entities/fattribute.js";
import type { EntityChange } from "../server_types.js"; import type { EntityChange } from "../server_types.js";
@@ -53,6 +53,7 @@ type EntityRowMappings = {
options: OptionRow; options: OptionRow;
revisions: RevisionRow; revisions: RevisionRow;
note_reordering: NoteReorderingRow; note_reordering: NoteReorderingRow;
etapi_tokens: EtapiTokenRow;
}; };
export type EntityRowNames = keyof EntityRowMappings; export type EntityRowNames = keyof EntityRowMappings;
@@ -68,6 +69,7 @@ export default class LoadResults {
private contentNoteIdToComponentId: ContentNoteIdToComponentIdRow[]; private contentNoteIdToComponentId: ContentNoteIdToComponentIdRow[];
private optionNames: string[]; private optionNames: string[];
private attachmentRows: AttachmentRow[]; private attachmentRows: AttachmentRow[];
public hasEtapiTokenChanges: boolean = false;
constructor(entityChanges: EntityChange[]) { constructor(entityChanges: EntityChange[]) {
const entities: Record<string, Record<string, any>> = {}; const entities: Record<string, Record<string, any>> = {};
@@ -215,7 +217,8 @@ export default class LoadResults {
this.revisionRows.length === 0 && this.revisionRows.length === 0 &&
this.contentNoteIdToComponentId.length === 0 && this.contentNoteIdToComponentId.length === 0 &&
this.optionNames.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; commandId?: string;
commandDescription?: string; commandDescription?: string;
commandShortcut?: string; commandShortcut?: string;
attributeSnippet?: string;
highlightedAttributeSnippet?: string;
} }
export interface Options { export interface Options {
@@ -323,7 +325,33 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
html += '</div>'; html += '</div>';
return html; 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 // we can't cache identical searches because notes can be created / renamed, new recent notes can be added

View File

@@ -1,7 +1,8 @@
import { OptionNames } from "@triliumnext/commons";
import server from "./server.js"; import server from "./server.js";
import { isShare } from "./utils.js"; import { isShare } from "./utils.js";
type OptionValue = number | string; export type OptionValue = number | string;
class Options { class Options {
initializedPromise: Promise<void>; initializedPromise: Promise<void>;
@@ -76,6 +77,14 @@ class Options {
await server.put(`options`, payload); 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) { async toggle(key: string) {
await this.save(key, (!this.is(key)).toString()); await this.save(key, (!this.is(key)).toString());
} }

View File

@@ -14,6 +14,32 @@ interface ShortcutBinding {
// Store all active shortcut bindings for management // Store all active shortcut bindings for management
const activeBindings: Map<string, ShortcutBinding[]> = new Map(); 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) { function removeGlobalShortcut(namespace: string) {
bindGlobalShortcut("", null, namespace); bindGlobalShortcut("", null, namespace);
} }
@@ -124,32 +150,6 @@ export function keyMatches(e: KeyboardEvent, key: string): boolean {
return false; 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()]; const mappedKeys = keyMap[key.toLowerCase()];
if (mappedKeys) { if (mappedKeys) {
return mappedKeys.includes(e.key) || mappedKeys.includes(e.code); 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 // For letter keys, use the physical key code for consistency
if (key.length === 1 && key >= 'a' && key <= 'z') { 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 // 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; export const isShare = !window.glob;
function reloadFrontendApp(reason?: string) { export function reloadFrontendApp(reason?: string) {
if (reason) { if (reason) {
logInfo(`Frontend app reload: ${reason}`); logInfo(`Frontend app reload: ${reason}`);
} }
@@ -13,7 +13,7 @@ function reloadFrontendApp(reason?: string) {
window.location.reload(); window.location.reload();
} }
function restartDesktopApp() { export function restartDesktopApp() {
if (!isElectron()) { if (!isElectron()) {
reloadFrontendApp(); reloadFrontendApp();
return; return;
@@ -125,7 +125,7 @@ function formatDateISO(date: Date) {
return `${date.getFullYear()}-${padNum(date.getMonth() + 1)}-${padNum(date.getDate())}`; 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()) { if (userSuppliedFormat?.trim()) {
return dayjs(date).format(userSuppliedFormat); return dayjs(date).format(userSuppliedFormat);
} else { } else {
@@ -144,7 +144,7 @@ function now() {
/** /**
* Returns `true` if the client is currently running under Electron, or `false` if running in a web browser. * 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); return !!(window && window.process && window.process.type);
} }
@@ -218,7 +218,7 @@ function randomString(len: number) {
return text; return text;
} }
function isMobile() { export function isMobile() {
return ( return (
window.glob?.device === "mobile" || window.glob?.device === "mobile" ||
// window.glob.device is not available in setup // window.glob.device is not available in setup
@@ -306,7 +306,7 @@ function copySelectionToClipboard() {
} }
} }
function dynamicRequire(moduleName: string) { export function dynamicRequire(moduleName: string) {
if (typeof __non_webpack_require__ !== "undefined") { if (typeof __non_webpack_require__ !== "undefined") {
return __non_webpack_require__(moduleName); return __non_webpack_require__(moduleName);
} else { } else {
@@ -374,6 +374,17 @@ async function openInAppHelp($button: JQuery<HTMLElement>) {
const inAppHelpPage = $button.attr("data-in-app-help"); const inAppHelpPage = $button.attr("data-in-app-help");
if (inAppHelpPage) { if (inAppHelpPage) {
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. // Dynamic import to avoid import issues in tests.
const appContext = (await import("../components/app_context.js")).default; const appContext = (await import("../components/app_context.js")).default;
const activeContext = appContext.tabManager.getActiveContext(); const activeContext = appContext.tabManager.getActiveContext();
@@ -399,8 +410,6 @@ async function openInAppHelp($button: JQuery<HTMLElement>) {
// There is already a help window open, make sure it opens on the right note. // There is already a help window open, make sure it opens on the right note.
helpSubcontext.setNote(targetNote, { viewScope }); helpSubcontext.setNote(targetNote, { viewScope });
} }
return;
}
} }
function initHelpButtons($el: JQuery<HTMLElement> | JQuery<Window>) { function initHelpButtons($el: JQuery<HTMLElement> | JQuery<Window>) {
@@ -735,6 +744,50 @@ function isLaunchBarConfig(noteId: string) {
return ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers", "_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(noteId); 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;
}
export default { export default {
reloadFrontendApp, reloadFrontendApp,
restartDesktopApp, restartDesktopApp,

View File

@@ -28,6 +28,28 @@
--ck-mention-list-max-height: 500px; --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 { .table {
--bs-table-bg: transparent !important; --bs-table-bg: transparent !important;
} }
@@ -139,6 +161,15 @@ textarea,
color: var(--muted-text-color); 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 */ /* Add a gap between consecutive radios / check boxes */
label.tn-radio + label.tn-radio, label.tn-radio + label.tn-radio,
label.tn-checkbox + label.tn-checkbox { label.tn-checkbox + label.tn-checkbox {
@@ -346,7 +377,7 @@ body.desktop .tabulator-popup-container {
@supports (animation-fill-mode: forwards) { @supports (animation-fill-mode: forwards) {
/* Delay the opening of submenus */ /* Delay the opening of submenus */
body.desktop .dropdown-submenu .dropdown-menu { body.desktop:not(.motion-disabled) .dropdown-submenu .dropdown-menu {
opacity: 0; opacity: 0;
animation-fill-mode: forwards; animation-fill-mode: forwards;
animation-delay: var(--submenu-opening-delay); animation-delay: var(--submenu-opening-delay);
@@ -831,10 +862,34 @@ table.promoted-attributes-in-tooltip th {
.aa-dropdown-menu .aa-suggestion { .aa-dropdown-menu .aa-suggestion {
cursor: pointer; cursor: pointer;
padding: 5px; padding: 6px 16px;
margin: 0; 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 { .aa-dropdown-menu .aa-suggestion p {
padding: 0; padding: 0;
margin: 0; margin: 0;
@@ -1738,16 +1793,12 @@ button.close:hover {
margin-bottom: 10px; margin-bottom: 10px;
} }
.options-number-input { .options-section input[type="number"] {
/* overriding settings from .form-control */ /* overriding settings from .form-control */
width: 10em !important; width: 10em !important;
flex-grow: 0 !important; flex-grow: 0 !important;
} }
.options-mime-types {
column-width: 250px;
}
textarea { textarea {
cursor: auto; cursor: auto;
} }
@@ -1768,20 +1819,42 @@ textarea {
font-size: 1em; font-size: 1em;
} }
.jump-to-note-dialog .modal-dialog {
max-width: 900px;
width: 90%;
}
.jump-to-note-dialog .modal-header { .jump-to-note-dialog .modal-header {
align-items: center; align-items: center;
} }
.jump-to-note-dialog .modal-body { .jump-to-note-dialog .modal-body {
padding: 0; padding: 0;
min-height: 200px;
} }
.jump-to-note-results .aa-dropdown-menu { .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 { .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 */ /* Command palette styling */
@@ -1799,8 +1872,24 @@ textarea {
.jump-to-note-dialog .aa-cursor .command-suggestion, .jump-to-note-dialog .aa-cursor .command-suggestion,
.jump-to-note-dialog .aa-suggestion:hover .command-suggestion { .jump-to-note-dialog .aa-suggestion:hover .command-suggestion {
border-left-color: var(--link-color); background-color: transparent;
background-color: var(--hover-background-color); }
.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 { .jump-to-note-dialog .command-icon {
@@ -2257,7 +2346,8 @@ footer.webview-footer button {
/* Search result highlighting */ /* Search result highlighting */
.search-result-title b, .search-result-title b,
.search-result-content b { .search-result-content b,
.search-result-attributes b {
font-weight: 900; font-weight: 900;
color: var(--admonition-warning-accent-color); color: var(--admonition-warning-accent-color);
} }

View File

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

View File

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

View File

@@ -83,6 +83,12 @@
--tab-note-icons: true; --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 * MENUS
* *
@@ -530,10 +536,9 @@ body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after {
} }
/* List item */ /* List item */
.jump-to-note-dialog .aa-suggestions div, .jump-to-note-dialog .aa-suggestion,
.note-detail-empty .aa-suggestions div { .note-detail-empty .aa-suggestion {
border-radius: 6px; border-radius: 6px;
padding: 6px 12px;
color: var(--menu-text-color); color: var(--menu-text-color);
cursor: default; cursor: default;
} }

View File

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

View File

@@ -967,7 +967,7 @@
}, },
"protected_session": { "protected_session": {
"enter_password_instruction": "显示受保护的笔记需要输入您的密码:", "enter_password_instruction": "显示受保护的笔记需要输入您的密码:",
"start_session_button": "开始受保护的会话", "start_session_button": "开始受保护的会话 <kbd>Enter</kbd>",
"started": "受保护的会话已启动。", "started": "受保护的会话已启动。",
"wrong_password": "密码错误。", "wrong_password": "密码错误。",
"protecting-finished-successfully": "保护操作已成功完成。", "protecting-finished-successfully": "保护操作已成功完成。",
@@ -1028,7 +1028,7 @@
"error_creating_anonymized_database": "无法创建匿名化数据库,请检查后端日志以获取详细信息", "error_creating_anonymized_database": "无法创建匿名化数据库,请检查后端日志以获取详细信息",
"successfully_created_fully_anonymized_database": "成功创建完全匿名化的数据库,路径为 {{anonymizedFilePath}}", "successfully_created_fully_anonymized_database": "成功创建完全匿名化的数据库,路径为 {{anonymizedFilePath}}",
"successfully_created_lightly_anonymized_database": "成功创建轻度匿名化的数据库,路径为 {{anonymizedFilePath}}", "successfully_created_lightly_anonymized_database": "成功创建轻度匿名化的数据库,路径为 {{anonymizedFilePath}}",
"no_anonymized_database_yet": "尚无匿名化数据库" "no_anonymized_database_yet": "尚无匿名化数据库"
}, },
"database_integrity_check": { "database_integrity_check": {
"title": "数据库完整性检查", "title": "数据库完整性检查",
@@ -1165,7 +1165,7 @@
}, },
"revisions_snapshot_interval": { "revisions_snapshot_interval": {
"note_revisions_snapshot_interval_title": "笔记修订快照间隔", "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": "笔记修订快照时间间隔:" "snapshot_time_interval_label": "笔记修订快照时间间隔:"
}, },
"revisions_snapshot_limit": { "revisions_snapshot_limit": {
@@ -1333,7 +1333,7 @@
"oauth_title": "OAuth/OpenID 认证", "oauth_title": "OAuth/OpenID 认证",
"oauth_description": "OpenID 是一种标准化方式,允许您使用其他服务(如 Google的账号登录网站来验证您的身份。默认的身份提供者是 Google但您可以更改为任何其他 OpenID 提供者。点击<a href=\"#root/_hidden/_help/_help_Otzi9La2YAUX/_help_WOcw2SLH6tbX/_help_7DAiwaf8Z7Rz\">这里</a>了解更多信息。请参阅这些 <a href=\"https://developers.google.com/identity/openid-connect/openid-connect\">指南</a> 通过 Google 设置 OpenID 服务。", "oauth_description": "OpenID 是一种标准化方式,允许您使用其他服务(如 Google的账号登录网站来验证您的身份。默认的身份提供者是 Google但您可以更改为任何其他 OpenID 提供者。点击<a href=\"#root/_hidden/_help/_help_Otzi9La2YAUX/_help_WOcw2SLH6tbX/_help_7DAiwaf8Z7Rz\">这里</a>了解更多信息。请参阅这些 <a href=\"https://developers.google.com/identity/openid-connect/openid-connect\">指南</a> 通过 Google 设置 OpenID 服务。",
"oauth_description_warning": "要启用 OAuth/OpenID您需要设置 config.ini 文件中的 OAuth/OpenID 基础 URL、客户端 ID 和客户端密钥,并重新启动应用程序。如果要从环境变量设置,请设置 TRILIUM_OAUTH_BASE_URL、TRILIUM_OAUTH_CLIENT_ID 和 TRILIUM_OAUTH_CLIENT_SECRET 环境变量。", "oauth_description_warning": "要启用 OAuth/OpenID您需要设置 config.ini 文件中的 OAuth/OpenID 基础 URL、客户端 ID 和客户端密钥,并重新启动应用程序。如果要从环境变量设置,请设置 TRILIUM_OAUTH_BASE_URL、TRILIUM_OAUTH_CLIENT_ID 和 TRILIUM_OAUTH_CLIENT_SECRET 环境变量。",
"oauth_missing_vars": "缺少以下设置项: {{missingVars}}", "oauth_missing_vars": "缺少以下设置项{{variables}}",
"oauth_user_account": "用户账号: ", "oauth_user_account": "用户账号: ",
"oauth_user_email": "用户邮箱: ", "oauth_user_email": "用户邮箱: ",
"oauth_user_not_logged_in": "未登录!" "oauth_user_not_logged_in": "未登录!"
@@ -1871,14 +1871,19 @@
"selected_provider": "已选提供商", "selected_provider": "已选提供商",
"selected_provider_description": "选择用于聊天和补全功能的AI提供商", "selected_provider_description": "选择用于聊天和补全功能的AI提供商",
"select_model": "选择模型...", "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": { "code-editor-options": {
"title": "编辑器" "title": "编辑器"
}, },
"custom_date_time_format": { "custom_date_time_format": {
"title": "自定义日期/时间格式", "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": "日期/时间格式字符串:", "format_string": "日期/时间格式字符串:",
"formatted_time": "格式化后日期/时间:" "formatted_time": "格式化后日期/时间:"
}, },
@@ -1992,11 +1997,28 @@
"help_title": "显示关于此画面的更多信息" "help_title": "显示关于此画面的更多信息"
}, },
"call_to_action": { "call_to_action": {
"next_theme_title": "新的 Trilium 主题已进入稳定版",
"next_theme_message": "有一段时间,我们一直在设计新的主题,为了让应用程序看起来更加现代。",
"next_theme_button": "切换至新的 Trilium 主题",
"background_effects_title": "背景效果现已推出稳定版本", "background_effects_title": "背景效果现已推出稳定版本",
"background_effects_message": "在 Windows 装置上,背景效果现在已完全稳定。背景效果通过模糊背后的背景,为使用者界面增添一抹色彩。此技术也用于其他应用程序,例如 Windows 资源管理器。", "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": "启用菜单、弹窗和面板的背景效果"
} }
} }

View File

@@ -202,11 +202,12 @@
"okButton": "OK" "okButton": "OK"
}, },
"jump_to_note": { "jump_to_note": {
"search_button": "Suche im Volltext" "search_button": "Suche im Volltext",
"search_placeholder": "Suche nach Notiz anhand ihres Titels oder gib > ein für Kommandos..."
}, },
"markdown_import": { "markdown_import": {
"dialog_title": "Markdown-Import", "dialog_title": "Markdown-Import",
"modal_body_text": "Aufgrund der Browser-Sandbox ist es nicht möglich, die Zwischenablage direkt aus JavaScript zu lesen. Bitte füge den zu importierenden Markdown in den Textbereich unten ein und klicke auf die Schaltfläche „Importieren“.", "modal_body_text": "Aufgrund der Browser-Sandbox ist es nicht möglich, die Zwischenablage direkt aus JavaScript zu lesen. Bitte füge den zu importierenden Markdown in den Textbereich unten ein und klicke auf die Schaltfläche „Importieren“",
"import_button": "Importieren", "import_button": "Importieren",
"import_success": "Markdown-Inhalt wurde in das Dokument importiert." "import_success": "Markdown-Inhalt wurde in das Dokument importiert."
}, },
@@ -217,21 +218,26 @@
"search_placeholder": "Suche nach einer Notiz anhand ihres Namens", "search_placeholder": "Suche nach einer Notiz anhand ihres Namens",
"move_button": "Zur ausgewählten Notiz wechseln", "move_button": "Zur ausgewählten Notiz wechseln",
"error_no_path": "Kein Weg, auf den man sich bewegen kann.", "error_no_path": "Kein Weg, auf den man sich bewegen kann.",
"move_success_message": "Ausgewählte Notizen wurden verschoben" "move_success_message": "Ausgewählte Notizen wurden verschoben in "
}, },
"note_type_chooser": { "note_type_chooser": {
"modal_title": "Wähle den Notiztyp aus", "modal_title": "Wähle den Notiztyp aus",
"modal_body": "Wähle den Notiztyp / die Vorlage der neuen Notiz:", "modal_body": "Wähle den Notiztyp / die Vorlage der neuen Notiz:",
"templates": "Vorlagen" "templates": "Vorlagen",
"change_path_prompt": "Ändern wo die neue Notiz erzeugt wird:",
"search_placeholder": "Durchsuche Pfad nach Namen (Standard falls leer)",
"builtin_templates": "Eingebaute Vorlage"
}, },
"password_not_set": { "password_not_set": {
"title": "Das Passwort ist nicht festgelegt", "title": "Das Passwort ist nicht festgelegt",
"body1": "Geschützte Notizen werden mit einem Benutzerpasswort verschlüsselt, es wurde jedoch noch kein Passwort festgelegt." "body1": "Geschützte Notizen werden mit einem Benutzerpasswort verschlüsselt, es wurde jedoch noch kein Passwort festgelegt.",
"body2": "Um Notizen zu schützen, klicke den unteren Button um den Optionsdialog zu öffnen und dein Passwort festzulegen.",
"go_to_password_options": "Gehe zu Passwortoptionen"
}, },
"prompt": { "prompt": {
"title": "Prompt", "title": "Eingabeaufforderung",
"ok": "OK", "ok": "OK",
"defaultTitle": "Prompt" "defaultTitle": "Eingabeaufforderung"
}, },
"protected_session_password": { "protected_session_password": {
"modal_title": "Geschützte Sitzung", "modal_title": "Geschützte Sitzung",
@@ -268,7 +274,9 @@
"mime": "MIME: ", "mime": "MIME: ",
"file_size": "Dateigröße:", "file_size": "Dateigröße:",
"preview": "Vorschau:", "preview": "Vorschau:",
"preview_not_available": "Für diesen Notiztyp ist keine Vorschau verfügbar." "preview_not_available": "Für diesen Notiztyp ist keine Vorschau verfügbar.",
"restore_button": "Wiederherstellen",
"delete_button": "Löschen"
}, },
"sort_child_notes": { "sort_child_notes": {
"sort_children_by": "Unternotizen sortieren nach...", "sort_children_by": "Unternotizen sortieren nach...",
@@ -348,7 +356,7 @@
"sorted": "Hält untergeordnete Notizen alphabetisch nach Titel sortiert", "sorted": "Hält untergeordnete Notizen alphabetisch nach Titel sortiert",
"sort_direction": "ASC (Standard) oder DESC", "sort_direction": "ASC (Standard) oder DESC",
"sort_folders_first": "Ordner (Notizen mit Unternotizen) sollten oben sortiert werden", "sort_folders_first": "Ordner (Notizen mit Unternotizen) sollten oben sortiert werden",
"top": "Behalte die angegebene Notiz oben in der übergeordneten Notiz (gilt nur für sortierte übergeordnete Notizen).", "top": "Behalte die angegebene Notiz oben in der übergeordneten Notiz (gilt nur für sortierte übergeordnete Notizen)",
"hide_promoted_attributes": "Heraufgestufte Attribute für diese Notiz ausblenden", "hide_promoted_attributes": "Heraufgestufte Attribute für diese Notiz ausblenden",
"read_only": "Der Editor befindet sich im schreibgeschützten Modus. Funktioniert nur für Text- und Codenotizen.", "read_only": "Der Editor befindet sich im schreibgeschützten Modus. Funktioniert nur für Text- und Codenotizen.",
"auto_read_only_disabled": "Text-/Codenotizen können automatisch in den Lesemodus versetzt werden, wenn sie zu groß sind. Du kannst dieses Verhalten für jede einzelne Notiz deaktivieren, indem du diese Beschriftung zur Notiz hinzufügst", "auto_read_only_disabled": "Text-/Codenotizen können automatisch in den Lesemodus versetzt werden, wenn sie zu groß sind. Du kannst dieses Verhalten für jede einzelne Notiz deaktivieren, indem du diese Beschriftung zur Notiz hinzufügst",
@@ -371,10 +379,10 @@
"inbox": "Standard-Inbox-Position für neue Notizen wenn du eine Notiz über den \"Neue Notiz\"-Button in der Seitenleiste erstellst, wird die Notiz als untergeordnete Notiz der Notiz erstellt, die mit dem <code>#inbox</code>-Label markiert ist.", "inbox": "Standard-Inbox-Position für neue Notizen wenn du eine Notiz über den \"Neue Notiz\"-Button in der Seitenleiste erstellst, wird die Notiz als untergeordnete Notiz der Notiz erstellt, die mit dem <code>#inbox</code>-Label markiert ist.",
"workspace_inbox": "Standard-Posteingangsspeicherort für neue Notizen, wenn sie zu einem Vorgänger dieser Arbeitsbereichsnotiz verschoben werden", "workspace_inbox": "Standard-Posteingangsspeicherort für neue Notizen, wenn sie zu einem Vorgänger dieser Arbeitsbereichsnotiz verschoben werden",
"sql_console_home": "Standardspeicherort der SQL-Konsolennotizen", "sql_console_home": "Standardspeicherort der SQL-Konsolennotizen",
"bookmark_folder": "Notizen mit dieser Bezeichnung werden in den Lesezeichen als Ordner angezeigt (und ermöglichen den Zugriff auf ihre untergeordneten Ordner).", "bookmark_folder": "Notizen mit dieser Bezeichnung werden in den Lesezeichen als Ordner angezeigt (und ermöglichen den Zugriff auf ihre untergeordneten Ordner)",
"share_hidden_from_tree": "Diese Notiz ist im linken Navigationsbaum ausgeblendet, kann aber weiterhin über ihre URL aufgerufen werden", "share_hidden_from_tree": "Diese Notiz ist im linken Navigationsbaum ausgeblendet, kann aber weiterhin über ihre URL aufgerufen werden",
"share_external_link": "Die Notiz dient als Link zu einer externen Website im Freigabebaum", "share_external_link": "Die Notiz dient als Link zu einer externen Website im Freigabebaum",
"share_alias": "Lege einen Alias fest, unter dem die Notiz unter https://your_trilium_host/share/[dein_alias] verfügbar sein wird.", "share_alias": "Lege einen Alias fest, mit dem die Notiz unter https://your_trilium_host/share/[dein_alias] verfügbar sein wird",
"share_omit_default_css": "Das Standard-CSS für die Freigabeseite wird weggelassen. Verwende es, wenn du umfangreiche Stylingänderungen vornimmst.", "share_omit_default_css": "Das Standard-CSS für die Freigabeseite wird weggelassen. Verwende es, wenn du umfangreiche Stylingänderungen vornimmst.",
"share_root": "Markiert eine Notiz, die im /share-Root bereitgestellt wird.", "share_root": "Markiert eine Notiz, die im /share-Root bereitgestellt wird.",
"share_description": "Definiere Text, der dem HTML-Meta-Tag zur Beschreibung hinzugefügt werden soll", "share_description": "Definiere Text, der dem HTML-Meta-Tag zur Beschreibung hinzugefügt werden soll",
@@ -390,7 +398,7 @@
"color": "Definiert die Farbe der Notiz im Notizbaum, in Links usw. Verwende einen beliebigen gültigen CSS-Farbwert wie „rot“ oder #a13d5f", "color": "Definiert die Farbe der Notiz im Notizbaum, in Links usw. Verwende einen beliebigen gültigen CSS-Farbwert wie „rot“ oder #a13d5f",
"keyboard_shortcut": "Definiert eine Tastenkombination, die sofort zu dieser Notiz springt. Beispiel: „Strg+Alt+E“. Erfordert ein Neuladen des Frontends, damit die Änderung wirksam wird.", "keyboard_shortcut": "Definiert eine Tastenkombination, die sofort zu dieser Notiz springt. Beispiel: „Strg+Alt+E“. Erfordert ein Neuladen des Frontends, damit die Änderung wirksam wird.",
"keep_current_hoisting": "Das Öffnen dieses Links ändert das Hochziehen nicht, selbst wenn die Notiz im aktuell hochgezogenen Unterbaum nicht angezeigt werden kann.", "keep_current_hoisting": "Das Öffnen dieses Links ändert das Hochziehen nicht, selbst wenn die Notiz im aktuell hochgezogenen Unterbaum nicht angezeigt werden kann.",
"execute_button": "Titel der Schaltfläche, die die aktuelle Codenotiz ausführt", "execute_button": "Titel der Schaltfläche, welche die aktuelle Codenotiz ausführt",
"execute_description": "Längere Beschreibung der aktuellen Codenotiz, die zusammen mit der Schaltfläche „Ausführen“ angezeigt wird", "execute_description": "Längere Beschreibung der aktuellen Codenotiz, die zusammen mit der Schaltfläche „Ausführen“ angezeigt wird",
"exclude_from_note_map": "Notizen mit dieser Bezeichnung werden in der Notizenkarte ausgeblendet", "exclude_from_note_map": "Notizen mit dieser Bezeichnung werden in der Notizenkarte ausgeblendet",
"new_notes_on_top": "Neue Notizen werden oben in der übergeordneten Notiz erstellt, nicht unten.", "new_notes_on_top": "Neue Notizen werden oben in der übergeordneten Notiz erstellt, nicht unten.",
@@ -408,7 +416,7 @@
"run_on_attribute_change": " wird ausgeführt, wenn das Attribut einer Notiz geändert wird, die diese Beziehung definiert. Dies wird auch ausgelöst, wenn das Attribut gelöscht wird", "run_on_attribute_change": " wird ausgeführt, wenn das Attribut einer Notiz geändert wird, die diese Beziehung definiert. Dies wird auch ausgelöst, wenn das Attribut gelöscht wird",
"relation_template": "Die Attribute der Notiz werden auch ohne eine Eltern-Kind-Beziehung vererbt. Der Inhalt und der Unterbaum der Notiz werden den Instanznotizen hinzugefügt, wenn sie leer sind. Einzelheiten findest du in der Dokumentation.", "relation_template": "Die Attribute der Notiz werden auch ohne eine Eltern-Kind-Beziehung vererbt. Der Inhalt und der Unterbaum der Notiz werden den Instanznotizen hinzugefügt, wenn sie leer sind. Einzelheiten findest du in der Dokumentation.",
"inherit": "Die Attribute einer Notiz werden auch ohne eine Eltern-Kind-Beziehung vererbt. Ein ähnliches Konzept findest du unter Vorlagenbeziehung. Siehe Attributvererbung in der Dokumentation.", "inherit": "Die Attribute einer Notiz werden auch ohne eine Eltern-Kind-Beziehung vererbt. Ein ähnliches Konzept findest du unter Vorlagenbeziehung. Siehe Attributvererbung in der Dokumentation.",
"render_note": "Notizen vom Typ \"HTML-Notiz rendern\" werden mit einer Code-Notiz (HTML oder Skript) gerendert, und es ist notwendig, über diese Beziehung anzugeben, welche Notiz gerendert werden soll.", "render_note": "Notizen vom Typ \"HTML-Notiz rendern\" werden mit einer Code-Notiz (HTML oder Skript) gerendert, und es ist notwendig, über diese Beziehung anzugeben, welche Notiz gerendert werden soll",
"widget_relation": "Das Ziel dieser Beziehung wird ausgeführt und als Widget in der Seitenleiste gerendert", "widget_relation": "Das Ziel dieser Beziehung wird ausgeführt und als Widget in der Seitenleiste gerendert",
"share_css": "CSS-Hinweis, der in die Freigabeseite eingefügt wird. Die CSS-Notiz muss sich ebenfalls im gemeinsamen Unterbaum befinden. Erwäge auch die Verwendung von „share_hidden_from_tree“ und „share_omit_default_css“.", "share_css": "CSS-Hinweis, der in die Freigabeseite eingefügt wird. Die CSS-Notiz muss sich ebenfalls im gemeinsamen Unterbaum befinden. Erwäge auch die Verwendung von „share_hidden_from_tree“ und „share_omit_default_css“.",
"share_js": "JavaScript-Hinweis, der in die Freigabeseite eingefügt wird. Die JS-Notiz muss sich ebenfalls im gemeinsamen Unterbaum befinden. Erwäge die Verwendung von „share_hidden_from_tree“.", "share_js": "JavaScript-Hinweis, der in die Freigabeseite eingefügt wird. Die JS-Notiz muss sich ebenfalls im gemeinsamen Unterbaum befinden. Erwäge die Verwendung von „share_hidden_from_tree“.",
@@ -418,7 +426,8 @@
"other_notes_with_name": "Other notes with {{attributeType}} name \"{{attributeName}}\"", "other_notes_with_name": "Other notes with {{attributeType}} name \"{{attributeName}}\"",
"and_more": "... und {{count}} mehr.", "and_more": "... und {{count}} mehr.",
"print_landscape": "Beim Export als PDF, wird die Seitenausrichtung Querformat anstatt Hochformat verwendet.", "print_landscape": "Beim Export als PDF, wird die Seitenausrichtung Querformat anstatt Hochformat verwendet.",
"print_page_size": "Beim Export als PDF, wird die Größe der Seite angepasst. Unterstützte Größen: <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": "Beim Export als PDF, wird die Größe der Seite angepasst. Unterstützte Größen: <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": "Farbe"
}, },
"attribute_editor": { "attribute_editor": {
"help_text_body1": "Um ein Label hinzuzufügen, gebe einfach z.B. ein. <code>#rock</code> oder wenn du auch einen Wert hinzufügen möchten, dann z.B. <code>#year = 2024</code>", "help_text_body1": "Um ein Label hinzuzufügen, gebe einfach z.B. ein. <code>#rock</code> oder wenn du auch einen Wert hinzufügen möchten, dann z.B. <code>#year = 2024</code>",
@@ -490,9 +499,9 @@
"to": "nach", "to": "nach",
"target_parent_note": "Ziel-Übergeordnetenotiz", "target_parent_note": "Ziel-Übergeordnetenotiz",
"on_all_matched_notes": "Auf allen übereinstimmenden Notizen", "on_all_matched_notes": "Auf allen übereinstimmenden Notizen",
"move_note_new_parent": "Verschiebe die Notiz in die neue übergeordnete Notiz, wenn die Notiz nur eine übergeordnete Notiz hat (d. h. der alte Zweig wird entfernt und ein neuer Zweig in die neue übergeordnete Notiz erstellt).", "move_note_new_parent": "Verschiebe die Notiz in die neue übergeordnete Notiz, wenn die Notiz nur eine übergeordnete Notiz hat (d. h. der alte Zweig wird entfernt und ein neuer Zweig in die neue übergeordnete Notiz erstellt)",
"clone_note_new_parent": "Notiz auf die neue übergeordnete Notiz klonen, wenn die Notiz mehrere Klone/Zweige hat (es ist nicht klar, welcher Zweig entfernt werden soll)", "clone_note_new_parent": "Notiz auf die neue übergeordnete Notiz klonen, wenn die Notiz mehrere Klone/Zweige hat (es ist nicht klar, welcher Zweig entfernt werden soll)",
"nothing_will_happen": "Es passiert nichts, wenn die Notiz nicht zur Zielnotiz verschoben werden kann (d. h. dies würde einen Baumzyklus erzeugen)." "nothing_will_happen": "Es passiert nichts, wenn die Notiz nicht zur Zielnotiz verschoben werden kann (z.B. wenn dies einen Kreislauf in der Baumstruktur erzeugen würde)"
}, },
"rename_note": { "rename_note": {
"rename_note": "Notiz umbenennen", "rename_note": "Notiz umbenennen",
@@ -500,10 +509,10 @@
"new_note_title": "neuer Notiztitel", "new_note_title": "neuer Notiztitel",
"click_help_icon": "Klicke rechts auf das Hilfesymbol, um alle Optionen anzuzeigen", "click_help_icon": "Klicke rechts auf das Hilfesymbol, um alle Optionen anzuzeigen",
"evaluated_as_js_string": "Der angegebene Wert wird als JavaScript-String ausgewertet und kann somit über die injizierte <code>note</code>-Variable mit dynamischem Inhalt angereichert werden (Notiz wird umbenannt). Beispiele:", "evaluated_as_js_string": "Der angegebene Wert wird als JavaScript-String ausgewertet und kann somit über die injizierte <code>note</code>-Variable mit dynamischem Inhalt angereichert werden (Notiz wird umbenannt). Beispiele:",
"example_note": "<code>Notiz</code> alle übereinstimmenden Notizen werden in „Notiz“ umbenannt.", "example_note": "<code>Notiz</code> alle übereinstimmenden Notizen werden in „Notiz“ umbenannt",
"example_new_title": "<code>NEU: ${note.title}</code> Übereinstimmende Notiztitel erhalten das Präfix „NEU:“", "example_new_title": "<code>NEU: ${note.title}</code> Übereinstimmende Notiztitel erhalten das Präfix „NEU:“",
"example_date_prefix": "<code>${note.dateCreatedObj.format('MM-DD:')}: ${note.title}</code> übereinstimmende Notizen werden mit dem Erstellungsmonat und -datum der Notiz vorangestellt", "example_date_prefix": "<code>${note.dateCreatedObj.format('MM-DD:')}: ${note.title}</code> übereinstimmende Notizen werden mit dem Erstellungsmonat und -datum der Notiz vorangestellt",
"api_docs": "Siehe API-Dokumente für <a hrefu003d'https://zadam.github.io/trilium/backend_api/Note.html'>Notiz</a> und seinen <a hrefu003d'https://day.js.org/ docs/en/display/format'>dateCreatedObj / utcDateCreatedObj-Eigenschaften</a> für Details." "api_docs": "Siehe API-Dokumente für <a href='https://zadam.github.io/trilium/backend_api/Note.html'>Notiz</a> und seinen <a href='https://day.js.org/ docs/en/display/format'>dateCreatedObj / utcDateCreatedObj properties</a> für Details."
}, },
"add_relation": { "add_relation": {
"add_relation": "Beziehung hinzufügen", "add_relation": "Beziehung hinzufügen",
@@ -577,7 +586,8 @@
"september": "September", "september": "September",
"october": "Oktober", "october": "Oktober",
"november": "November", "november": "November",
"december": "Dezember" "december": "Dezember",
"cannot_find_week_note": "Wochennotiz kann nicht gefunden werden"
}, },
"close_pane_button": { "close_pane_button": {
"close_this_pane": "Schließe diesen Bereich" "close_this_pane": "Schließe diesen Bereich"
@@ -699,14 +709,14 @@
"zoom_out_title": "Herauszoomen" "zoom_out_title": "Herauszoomen"
}, },
"zpetne_odkazy": { "zpetne_odkazy": {
"backlink": "{{count}} Backlink", "backlink": "{{count}} Rückverlinkung",
"backlinks": "{{count}} Backlinks", "backlinks": "{{count}} Rückverlinkungen",
"relation": "Beziehung" "relation": "Beziehung"
}, },
"mobile_detail_menu": { "mobile_detail_menu": {
"insert_child_note": "Untergeordnete Notiz einfügen", "insert_child_note": "Untergeordnete Notiz einfügen",
"delete_this_note": "Diese Notiz löschen", "delete_this_note": "Diese Notiz löschen",
"error_cannot_get_branch_id": "BranchId für notePath „{{notePath}}“ kann nicht abgerufen werden.", "error_cannot_get_branch_id": "BranchId für notePath „{{notePath}}“ kann nicht abgerufen werden",
"error_unrecognized_command": "Unbekannter Befehl {{command}}" "error_unrecognized_command": "Unbekannter Befehl {{command}}"
}, },
"note_icon": { "note_icon": {
@@ -718,7 +728,8 @@
"basic_properties": { "basic_properties": {
"note_type": "Notiztyp", "note_type": "Notiztyp",
"editable": "Bearbeitbar", "editable": "Bearbeitbar",
"basic_properties": "Grundlegende Eigenschaften" "basic_properties": "Grundlegende Eigenschaften",
"language": "Sprache"
}, },
"book_properties": { "book_properties": {
"view_type": "Ansichtstyp", "view_type": "Ansichtstyp",
@@ -729,7 +740,11 @@
"collapse": "Einklappen", "collapse": "Einklappen",
"expand": "Ausklappen", "expand": "Ausklappen",
"invalid_view_type": "Ungültiger Ansichtstyp „{{type}}“", "invalid_view_type": "Ungültiger Ansichtstyp „{{type}}“",
"calendar": "Kalender" "calendar": "Kalender",
"book_properties": "Sammlungseigenschaften",
"table": "Tabelle",
"geo-map": "Weltkarte",
"board": "Tafel"
}, },
"edited_notes": { "edited_notes": {
"no_edited_notes_found": "An diesem Tag wurden noch keine Notizen bearbeitet...", "no_edited_notes_found": "An diesem Tag wurden noch keine Notizen bearbeitet...",
@@ -805,7 +820,9 @@
"unknown_label_type": "Unbekannter Labeltyp „{{type}}“", "unknown_label_type": "Unbekannter Labeltyp „{{type}}“",
"unknown_attribute_type": "Unbekannter Attributtyp „{{type}}“", "unknown_attribute_type": "Unbekannter Attributtyp „{{type}}“",
"add_new_attribute": "Neues Attribut hinzufügen", "add_new_attribute": "Neues Attribut hinzufügen",
"remove_this_attribute": "Entferne dieses Attribut" "remove_this_attribute": "Entferne dieses Attribut",
"unset-field-placeholder": "nicht gesetzt",
"remove_color": "Entferne Farblabel"
}, },
"script_executor": { "script_executor": {
"query": "Abfrage", "query": "Abfrage",
@@ -867,7 +884,7 @@
"include_archived_notes": "Füge archivierte Notizen hinzu" "include_archived_notes": "Füge archivierte Notizen hinzu"
}, },
"limit": { "limit": {
"limit": "Limit", "limit": "Limitierung",
"take_first_x_results": "Nehmen Sie nur die ersten X angegebenen Ergebnisse." "take_first_x_results": "Nehmen Sie nur die ersten X angegebenen Ergebnisse."
}, },
"order_by": { "order_by": {
@@ -942,7 +959,8 @@
"enter_workspace": "Betrete den Arbeitsbereich {{title}}" "enter_workspace": "Betrete den Arbeitsbereich {{title}}"
}, },
"file": { "file": {
"file_preview_not_available": "Für dieses Dateiformat ist keine Dateivorschau verfügbar." "file_preview_not_available": "Für dieses Dateiformat ist keine Dateivorschau verfügbar.",
"too_big": "Die Vorschau zeigt aus Effizienzgründen nur die ersten {{maxNumChars}} Zeichen der Datei an. Lade die Datei herunter und öffne sie extern um den gesamten Inhalt zu sehen."
}, },
"protected_session": { "protected_session": {
"enter_password_instruction": "Um die geschützte Notiz anzuzeigen, musst du dein Passwort eingeben:", "enter_password_instruction": "Um die geschützte Notiz anzuzeigen, musst du dein Passwort eingeben:",
@@ -981,7 +999,7 @@
"web_view": { "web_view": {
"web_view": "Webansicht", "web_view": "Webansicht",
"embed_websites": "Notiz vom Typ Web View ermöglicht das Einbetten von Websites in Trilium.", "embed_websites": "Notiz vom Typ Web View ermöglicht das Einbetten von Websites in Trilium.",
"create_label": "To start, please create a label with a URL address you want to embed, e.g. #webViewSrc=\"https://www.google.com\"" "create_label": "Um zu beginnen, erstelle bitte ein Label mit einer URL-Adresse, die eingebettet werden soll, z. B. #webViewSrc=\"https://www.google.com\""
}, },
"backend_log": { "backend_log": {
"refresh": "Aktualisieren" "refresh": "Aktualisieren"
@@ -1007,7 +1025,7 @@
"error_creating_anonymized_database": "Die anonymisierte Datenbank konnte nicht erstellt werden. Überprüfe die Backend-Protokolle auf Details", "error_creating_anonymized_database": "Die anonymisierte Datenbank konnte nicht erstellt werden. Überprüfe die Backend-Protokolle auf Details",
"successfully_created_fully_anonymized_database": "Vollständig anonymisierte Datenbank in {{anonymizedFilePath}} erstellt", "successfully_created_fully_anonymized_database": "Vollständig anonymisierte Datenbank in {{anonymizedFilePath}} erstellt",
"successfully_created_lightly_anonymized_database": "Leicht anonymisierte Datenbank in {{anonymizedFilePath}} erstellt", "successfully_created_lightly_anonymized_database": "Leicht anonymisierte Datenbank in {{anonymizedFilePath}} erstellt",
"no_anonymized_database_yet": "Noch keine anonymisierte Datenbank" "no_anonymized_database_yet": "Noch keine anonymisierte Datenbank."
}, },
"database_integrity_check": { "database_integrity_check": {
"title": "Datenbankintegritätsprüfung", "title": "Datenbankintegritätsprüfung",
@@ -1028,7 +1046,7 @@
"failed": "Synchronisierung fehlgeschlagen: {{message}}" "failed": "Synchronisierung fehlgeschlagen: {{message}}"
}, },
"vacuum_database": { "vacuum_database": {
"title": "Vakuumdatenbank", "title": "Datenbank aufräumen",
"description": "Dadurch wird die Datenbank neu erstellt, was normalerweise zu einer kleineren Datenbankdatei führt. Es werden keine Daten tatsächlich geändert.", "description": "Dadurch wird die Datenbank neu erstellt, was normalerweise zu einer kleineren Datenbankdatei führt. Es werden keine Daten tatsächlich geändert.",
"button_text": "Vakuumdatenbank", "button_text": "Vakuumdatenbank",
"vacuuming_database": "Datenbank wird geleert...", "vacuuming_database": "Datenbank wird geleert...",
@@ -1063,7 +1081,8 @@
"max_width_label": "Maximale Inhaltsbreite in Pixel", "max_width_label": "Maximale Inhaltsbreite in Pixel",
"apply_changes_description": "Um Änderungen an der Inhaltsbreite anzuwenden, klicke auf", "apply_changes_description": "Um Änderungen an der Inhaltsbreite anzuwenden, klicke auf",
"reload_button": "Frontend neu laden", "reload_button": "Frontend neu laden",
"reload_description": "Änderungen an den Darstellungsoptionen" "reload_description": "Änderungen an den Darstellungsoptionen",
"max_width_unit": "Pixel"
}, },
"native_title_bar": { "native_title_bar": {
"title": "Native Titelleiste (App-Neustart erforderlich)", "title": "Native Titelleiste (App-Neustart erforderlich)",
@@ -1086,7 +1105,10 @@
"layout-vertical-title": "Vertikal", "layout-vertical-title": "Vertikal",
"layout-horizontal-title": "Horizontal", "layout-horizontal-title": "Horizontal",
"layout-vertical-description": "Startleiste ist auf der linken Seite (standard)", "layout-vertical-description": "Startleiste ist auf der linken Seite (standard)",
"layout-horizontal-description": "Startleiste ist unter der Tableiste. Die Tableiste wird dadurch auf die ganze Breite erweitert." "layout-horizontal-description": "Startleiste ist unter der Tableiste. Die Tableiste wird dadurch auf die ganze Breite erweitert.",
"auto_theme": "Alt (Folge dem Farbschema des Systems)",
"light_theme": "Alt (Hell)",
"dark_theme": "Alt (Dunkel)"
}, },
"zoom_factor": { "zoom_factor": {
"title": "Zoomfaktor (nur Desktop-Build)", "title": "Zoomfaktor (nur Desktop-Build)",
@@ -1095,7 +1117,8 @@
"code_auto_read_only_size": { "code_auto_read_only_size": {
"title": "Automatische schreibgeschützte Größe", "title": "Automatische schreibgeschützte Größe",
"description": "Die automatische schreibgeschützte Notizgröße ist die Größe, ab der Notizen im schreibgeschützten Modus angezeigt werden (aus Leistungsgründen).", "description": "Die automatische schreibgeschützte Notizgröße ist die Größe, ab der Notizen im schreibgeschützten Modus angezeigt werden (aus Leistungsgründen).",
"label": "Automatische schreibgeschützte Größe (Codenotizen)" "label": "Automatische schreibgeschützte Größe (Codenotizen)",
"unit": "Zeichen"
}, },
"code_mime_types": { "code_mime_types": {
"title": "Verfügbare MIME-Typen im Dropdown-Menü" "title": "Verfügbare MIME-Typen im Dropdown-Menü"
@@ -1114,12 +1137,13 @@
"download_images_description": "Eingefügter HTML-Code kann Verweise auf Online-Bilder enthalten. Trilium findet diese Verweise und lädt die Bilder herunter, sodass sie offline verfügbar sind.", "download_images_description": "Eingefügter HTML-Code kann Verweise auf Online-Bilder enthalten. Trilium findet diese Verweise und lädt die Bilder herunter, sodass sie offline verfügbar sind.",
"enable_image_compression": "Bildkomprimierung aktivieren", "enable_image_compression": "Bildkomprimierung aktivieren",
"max_image_dimensions": "Maximale Breite/Höhe eines Bildes in Pixel (die Größe des Bildes wird geändert, wenn es diese Einstellung überschreitet).", "max_image_dimensions": "Maximale Breite/Höhe eines Bildes in Pixel (die Größe des Bildes wird geändert, wenn es diese Einstellung überschreitet).",
"jpeg_quality_description": "JPEG-Qualität (10 schlechteste Qualität, 100 beste Qualität, 50 85 wird empfohlen)" "jpeg_quality_description": "JPEG-Qualität (10 schlechteste Qualität, 100 beste Qualität, 50 85 wird empfohlen)",
"max_image_dimensions_unit": "Pixel"
}, },
"attachment_erasure_timeout": { "attachment_erasure_timeout": {
"attachment_erasure_timeout": "Zeitüberschreitung beim Löschen von Anhängen", "attachment_erasure_timeout": "Zeitüberschreitung beim Löschen von Anhängen",
"attachment_auto_deletion_description": "Anhänge werden automatisch gelöscht (und gelöscht), wenn sie nach einer definierten Zeitspanne nicht mehr in ihrer Notiz referenziert werden.", "attachment_auto_deletion_description": "Anhänge werden automatisch gelöscht (und gelöscht), wenn sie nach einer definierten Zeitspanne nicht mehr in ihrer Notiz referenziert werden.",
"erase_attachments_after": "Erase unused attachments after:", "erase_attachments_after": "Nicht verwendete Anhänge löschen nach:",
"manual_erasing_description": "Du kannst das Löschen auch manuell auslösen (ohne Berücksichtigung des oben definierten Timeouts):", "manual_erasing_description": "Du kannst das Löschen auch manuell auslösen (ohne Berücksichtigung des oben definierten Timeouts):",
"erase_unused_attachments_now": "Lösche jetzt nicht verwendete Anhangnotizen", "erase_unused_attachments_now": "Lösche jetzt nicht verwendete Anhangnotizen",
"unused_attachments_erased": "Nicht verwendete Anhänge wurden gelöscht." "unused_attachments_erased": "Nicht verwendete Anhänge wurden gelöscht."
@@ -1130,7 +1154,7 @@
}, },
"note_erasure_timeout": { "note_erasure_timeout": {
"note_erasure_timeout_title": "Beachte das Zeitlimit für die Löschung", "note_erasure_timeout_title": "Beachte das Zeitlimit für die Löschung",
"note_erasure_description": "Deleted notes (and attributes, revisions...) are at first only marked as deleted and it is possible to recover them from Recent Notes dialog. After a period of time, deleted notes are \"erased\" which means their content is not recoverable anymore. This setting allows you to configure the length of the period between deleting and erasing the note.", "note_erasure_description": "Gelöschte Notizen (und Attribute, Notizrevisionen...) werden zunächst nur als gelöscht markiert und können über den Dialog „Zuletzt verwendete Notizen” wiederhergestellt werden. Nach einer bestimmten Zeit werden gelöschte Notizen „gelöscht”, was bedeutet, dass ihr Inhalt nicht mehr wiederhergestellt werden kann. Mit dieser Einstellung können Sie die Zeitspanne zwischen dem Löschen und dem endgültigen Löschen der Notiz festlegen.",
"erase_notes_after": "Notizen löschen nach:", "erase_notes_after": "Notizen löschen nach:",
"manual_erasing_description": "Du kannst das Löschen auch manuell auslösen (ohne Berücksichtigung des oben definierten Timeouts):", "manual_erasing_description": "Du kannst das Löschen auch manuell auslösen (ohne Berücksichtigung des oben definierten Timeouts):",
"erase_deleted_notes_now": "Jetzt gelöschte Notizen löschen", "erase_deleted_notes_now": "Jetzt gelöschte Notizen löschen",
@@ -1138,7 +1162,7 @@
}, },
"revisions_snapshot_interval": { "revisions_snapshot_interval": {
"note_revisions_snapshot_interval_title": "Snapshot-Intervall für Notizrevisionen", "note_revisions_snapshot_interval_title": "Snapshot-Intervall für Notizrevisionen",
"note_revisions_snapshot_description": "Das Snapshot-Zeitintervall für Notizrevisionen ist die Zeit, nach der eine neue Notizrevision erstellt wird. Weitere Informationen findest du im <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">Wiki</a>.", "note_revisions_snapshot_description": "Das Snapshot-Zeitintervall für Notizrevisionen ist die Zeit, nach der eine neue Notizrevision erstellt wird. Weitere Informationen findest du im <doc>Wiki</doc>.",
"snapshot_time_interval_label": "Zeitintervall für Notiz-Revisions-Snapshot:" "snapshot_time_interval_label": "Zeitintervall für Notiz-Revisions-Snapshot:"
}, },
"revisions_snapshot_limit": { "revisions_snapshot_limit": {
@@ -1146,7 +1170,8 @@
"note_revisions_snapshot_limit_description": "Das Limit für Notizrevision-Snapshots bezieht sich auf die maximale Anzahl von Revisionen, die für jede Notiz gespeichert werden können. Dabei bedeutet -1, dass es kein Limit gibt, und 0 bedeutet, dass alle Revisionen gelöscht werden. Du kannst das maximale Limit für Revisionen einer einzelnen Notiz über das Label #versioningLimit festlegen.", "note_revisions_snapshot_limit_description": "Das Limit für Notizrevision-Snapshots bezieht sich auf die maximale Anzahl von Revisionen, die für jede Notiz gespeichert werden können. Dabei bedeutet -1, dass es kein Limit gibt, und 0 bedeutet, dass alle Revisionen gelöscht werden. Du kannst das maximale Limit für Revisionen einer einzelnen Notiz über das Label #versioningLimit festlegen.",
"snapshot_number_limit_label": "Limit der Notizrevision-Snapshots:", "snapshot_number_limit_label": "Limit der Notizrevision-Snapshots:",
"erase_excess_revision_snapshots": "Überschüssige Revision-Snapshots jetzt löschen", "erase_excess_revision_snapshots": "Überschüssige Revision-Snapshots jetzt löschen",
"erase_excess_revision_snapshots_prompt": "Überschüssige Revision-Snapshots wurden gelöscht." "erase_excess_revision_snapshots_prompt": "Überschüssige Revision-Snapshots wurden gelöscht.",
"snapshot_number_limit_unit": "Momentaufnahmen"
}, },
"search_engine": { "search_engine": {
"title": "Suchmaschine", "title": "Suchmaschine",
@@ -1188,12 +1213,14 @@
"title": "Inhaltsverzeichnis", "title": "Inhaltsverzeichnis",
"description": "Das Inhaltsverzeichnis wird in Textnotizen angezeigt, wenn die Notiz mehr als eine definierte Anzahl von Überschriften enthält. Du kannst diese Nummer anpassen:", "description": "Das Inhaltsverzeichnis wird in Textnotizen angezeigt, wenn die Notiz mehr als eine definierte Anzahl von Überschriften enthält. Du kannst diese Nummer anpassen:",
"disable_info": "Du kannst diese Option auch verwenden, um TOC effektiv zu deaktivieren, indem du eine sehr hohe Zahl festlegst.", "disable_info": "Du kannst diese Option auch verwenden, um TOC effektiv zu deaktivieren, indem du eine sehr hohe Zahl festlegst.",
"shortcut_info": "Du kannst eine Tastenkombination zum schnellen Umschalten des rechten Bereichs (einschließlich Inhaltsverzeichnis) unter Optionen -> Tastenkombinationen konfigurieren (Name „toggleRightPane“)." "shortcut_info": "Du kannst eine Tastenkombination zum schnellen Umschalten des rechten Bereichs (einschließlich Inhaltsverzeichnis) unter Optionen -> Tastenkombinationen konfigurieren (Name „toggleRightPane“).",
"unit": "Überschriften"
}, },
"text_auto_read_only_size": { "text_auto_read_only_size": {
"title": "Automatische schreibgeschützte Größe", "title": "Automatische schreibgeschützte Größe",
"description": "Die automatische schreibgeschützte Notizgröße ist die Größe, ab der Notizen im schreibgeschützten Modus angezeigt werden (aus Leistungsgründen).", "description": "Die automatische schreibgeschützte Notizgröße ist die Größe, ab der Notizen im schreibgeschützten Modus angezeigt werden (aus Leistungsgründen).",
"label": "Automatische schreibgeschützte Größe (Textnotizen)" "label": "Automatische schreibgeschützte Größe (Textnotizen)",
"unit": "Zeichen"
}, },
"i18n": { "i18n": {
"title": "Lokalisierung", "title": "Lokalisierung",
@@ -1361,7 +1388,7 @@
"duplicate": "Duplizieren", "duplicate": "Duplizieren",
"export": "Exportieren", "export": "Exportieren",
"import-into-note": "In Notiz importieren", "import-into-note": "In Notiz importieren",
"apply-bulk-actions": "Massenaktionen ausführen", "apply-bulk-actions": "Massenaktionen anwenden",
"converted-to-attachments": "{{count}} Notizen wurden als Anhang konvertiert.", "converted-to-attachments": "{{count}} Notizen wurden als Anhang konvertiert.",
"convert-to-attachment-confirm": "Bist du sicher, dass du die ausgewählten Notizen in Anhänge ihrer übergeordneten Notizen umwandeln möchtest?" "convert-to-attachment-confirm": "Bist du sicher, dass du die ausgewählten Notizen in Anhänge ihrer übergeordneten Notizen umwandeln möchtest?"
}, },
@@ -1377,7 +1404,7 @@
"relation-map": "Beziehungskarte", "relation-map": "Beziehungskarte",
"note-map": "Notizkarte", "note-map": "Notizkarte",
"render-note": "Render Notiz", "render-note": "Render Notiz",
"mermaid-diagram": "Mermaid Diagram", "mermaid-diagram": "Mermaid Diagramm",
"canvas": "Canvas", "canvas": "Canvas",
"web-view": "Webansicht", "web-view": "Webansicht",
"mind-map": "Mind Map", "mind-map": "Mind Map",
@@ -1387,7 +1414,7 @@
"doc": "Dokument", "doc": "Dokument",
"widget": "Widget", "widget": "Widget",
"confirm-change": "Es is nicht empfehlenswert den Notiz-Typ zu ändern, wenn der Inhalt der Notiz nicht leer ist. Möchtest du dennoch fortfahren?", "confirm-change": "Es is nicht empfehlenswert den Notiz-Typ zu ändern, wenn der Inhalt der Notiz nicht leer ist. Möchtest du dennoch fortfahren?",
"geo-map": "Geo Map", "geo-map": "Geo-Karte",
"beta-feature": "Beta" "beta-feature": "Beta"
}, },
"protect_note": { "protect_note": {
@@ -1561,11 +1588,11 @@
"label": "Format Toolbar", "label": "Format Toolbar",
"floating": { "floating": {
"title": "Schwebend", "title": "Schwebend",
"description": "Werkzeuge erscheinen in Cursornähe" "description": "Werkzeuge erscheinen in Cursornähe;"
}, },
"fixed": { "fixed": {
"title": "Fixiert", "title": "Fixiert",
"description": "Werkzeuge erscheinen im \"Format\" Tab" "description": "Werkzeuge erscheinen im \"Format\" Tab."
}, },
"multiline-toolbar": "Toolbar wenn nötig in mehreren Zeilen darstellen." "multiline-toolbar": "Toolbar wenn nötig in mehreren Zeilen darstellen."
} }
@@ -1631,5 +1658,170 @@
}, },
"modal": { "modal": {
"close": "Schließen" "close": "Schließen"
},
"ai_llm": {
"n_notes_queued": "{{ count }} Notiz zur Indizierung vorgemerkt",
"n_notes_queued_plural": "{{ count }} Notizen zur Indizierung vorgemerkt",
"notes_indexed": "{{ count }} Notiz indiziert",
"notes_indexed_plural": "{{ count }} Notizen indiziert",
"not_started": "Nicht gestartet",
"title": "KI Einstellungen",
"processed_notes": "Verarbeitete Notizen",
"total_notes": "Gesamt Notizen",
"progress": "Fortschritt",
"queued_notes": "Eingereihte Notizen",
"failed_notes": "Fehlgeschlagenen Notizen",
"last_processed": "Zuletzt verarbeitet",
"refresh_stats": "Statistiken neu laden",
"enable_ai_features": "Aktiviere KI/LLM Funktionen",
"enable_ai_description": "Aktiviere KI-Funktionen wie Notizzusammenfassungen, Inhaltserzeugung und andere LLM-Funktionen",
"openai_tab": "OpenAI",
"anthropic_tab": "Anthropic",
"voyage_tab": "Voyage AI",
"ollama_tab": "Ollama",
"enable_ai": "Aktiviere KI/LLM Funktionen",
"enable_ai_desc": "Aktiviere KI-Funktionen wie Notizzusammenfassungen, Inhaltserzeugung und andere LLM-Funktionen",
"provider_configuration": "KI-Anbieterkonfiguration",
"provider_precedence": "Anbieter Priorität",
"provider_precedence_description": "Komma-getrennte Liste von Anbieter in der Reihenfolge ihrer Priorität (z.B. 'openai, anthropic,ollama')",
"temperature": "Temperatur",
"temperature_description": "Regelt die Zufälligkeit in Antworten (0 = deterministisch, 2 = maximale Zufälligkeit)",
"system_prompt": "Systemaufforderung",
"system_prompt_description": "Standard Systemaufforderung für alle KI-Interaktionen",
"openai_configuration": "OpenAI Konfiguration",
"openai_settings": "OpenAI Einstellungen",
"api_key": "API Schlüssel",
"url": "Basis-URL",
"model": "Modell",
"anthropic_settings": "Anthropic Einstellungen",
"partial": "{{ percentage }}% verarbeitet",
"anthropic_api_key_description": "Dein Anthropic API-Key für den Zugriff auf Claude Modelle",
"anthropic_model_description": "Anthropic Claude Modell für Chat-Vervollständigung",
"voyage_settings": "Einstellungen für Voyage AI",
"ollama_url_description": "URL für die Ollama API (Standard: http://localhost:11434)",
"ollama_model_description": "Ollama Modell für Chat-Vervollständigung",
"anthropic_configuration": "Anthropic Konfiguration",
"voyage_configuration": "Voyage AI Konfiguration",
"voyage_url_description": "Standard: https://api.voyageai.com/v1",
"ollama_configuration": "Ollama Konfiguration",
"enable_ollama": "Aktiviere Ollama",
"enable_ollama_description": "Aktiviere Ollama für lokale KI Modell Nutzung",
"ollama_url": "Ollama URL",
"ollama_model": "Ollama Modell",
"refresh_models": "Aktualisiere Modelle",
"refreshing_models": "Aktualisiere...",
"enable_automatic_indexing": "Aktiviere automatische Indizierung",
"rebuild_index": "Index neu aufbauen",
"rebuild_index_error": "Fehler beim Neuaufbau des Index. Prüfe Log für mehr Informationen.",
"retry_failed": "Fehler: Notiz konnte nicht erneut eingereiht werden",
"max_notes_per_llm_query": "Max. Notizen je Abfrage",
"max_notes_per_llm_query_description": "Maximale Anzahl ähnlicher Notizen zum Einbinden als KI Kontext",
"active_providers": "Aktive Anbieter",
"disabled_providers": "Inaktive Anbieter",
"remove_provider": "Entferne Anbieter von Suche",
"restore_provider": "Anbieter zur Suche wiederherstellen",
"similarity_threshold": "Ähnlichkeitsschwelle",
"similarity_threshold_description": "Mindestähnlichkeitswert (0-1) für Notizen, die im Kontext für LLM-Abfragen berücksichtigt werden sollen",
"reprocess_index": "Suchindex neu erstellen",
"reprocessing_index": "Neuerstellung...",
"reprocess_index_started": "Suchindex-Optimierung wurde im Hintergrund gestartet",
"reprocess_index_error": "Fehler beim Wiederaufbau des Suchindex",
"index_rebuild_progress": "Fortschritt der Index-Neuerstellung",
"index_rebuilding": "Optimierung Index ({{percentage}}%)",
"index_rebuild_complete": "Index Optimierung abgeschlossen",
"index_rebuild_status_error": "Fehler bei Überprüfung Status Index Neuerstellung",
"never": "Niemals",
"processing": "Verarbeitung ({{percentage}}%)",
"refreshing": "Aktualisiere...",
"incomplete": "Unvollständig ({{percentage}}%)",
"complete": "Abgeschlossen (100%)",
"auto_refresh_notice": "Auto-Aktualisierung alle {{seconds}} Sekunden",
"note_queued_for_retry": "Notiz in Warteschlange für erneuten Versuch hinzugefügt",
"failed_to_retry_note": "Wiederholungsversuch fehlgeschlagen für Notiz",
"ai_settings": "KI Einstellungen",
"agent": {
"processing": "Verarbeite...",
"thinking": "Nachdenken...",
"loading": "Lade...",
"generating": "Generiere..."
},
"name": "KI",
"openai": "OpenAI",
"use_enhanced_context": "Benutze verbesserten Kontext",
"openai_api_key_description": "Dein OpenAPI-Key für den Zugriff auf den KI-Dienst",
"default_model": "Standardmodell",
"openai_model_description": "Beispiele: gpt-4o, gpt-4-turbo, gpt-3.5-turbo",
"base_url": "Basis URL",
"openai_url_description": "Standard: https://api.openai.com/v1",
"anthropic_url_description": "Basis URL für Anthropic API (Standard: https://api.anthropic.com)",
"ollama_settings": "Ollama Einstellungen",
"note_title": "Notiz Titel",
"error": "Fehler",
"last_attempt": "Letzter Versuch",
"actions": "Aktionen",
"retry": "Erneut versuchen",
"retry_queued": "Notiz für weiteren Versuch eingereiht",
"empty_key_warning": {
"anthropic": "Anthropic API-Key ist leer. Bitte gültigen API-Key eingeben.",
"openai": "OpenAI API-Key ist leer. Bitte gültigen API-Key eingeben.",
"voyage": "Voyage API-Key ist leer. Bitte gültigen API-Key eingeben.",
"ollama": "Ollama API-Key ist leer. Bitte gültigen API-Key eingeben."
},
"api_key_tooltip": "API-Key für den Zugriff auf den Dienst",
"failed_to_retry_all": "Wiederholungsversuch für Notizen fehlgeschlagen",
"all_notes_queued_for_retry": "Alle fehlgeschlagenen Notizen wurden zur Wiederholung in die Warteschlange gestellt",
"enhanced_context_description": "Versorgt die KI mit mehr Kontext aus der Notiz und den zugehörigen Notizen, um bessere Antworten zu ermöglichen",
"show_thinking": "Zeige Denkprozess",
"show_thinking_description": "Zeige den Denkprozess der KI",
"enter_message": "Geben Sie Ihre Nachricht ein...",
"error_contacting_provider": "Fehler beim Kontaktieren des KI-Anbieters. Bitte überprüfe die Einstellungen und die Internetverbindung.",
"error_generating_response": "Fehler beim Generieren der KI Antwort",
"index_all_notes": "Indiziere alle Notizen",
"index_status": "Indizierungsstatus",
"indexed_notes": "Indizierte Notizen",
"indexing_stopped": "Indizierung gestoppt",
"indexing_in_progress": "Indizierung in Bearbeitung...",
"last_indexed": "Zuletzt Indiziert",
"note_chat": "Notizen-Chat",
"sources": "Quellen",
"start_indexing": "Starte Indizierung",
"use_advanced_context": "Benutze erweiterten Kontext",
"ollama_no_url": "Ollama ist nicht konfiguriert. Bitte trage eine gültige URL ein.",
"chat": {
"root_note_title": "KI Chats",
"root_note_content": "Diese Notiz enthält gespeicherte KI-Chat-Unterhaltungen.",
"new_chat_title": "Neuer Chat",
"create_new_ai_chat": "Erstelle neuen KI Chat"
},
"create_new_ai_chat": "Erstelle neuen KI Chat",
"configuration_warnings": "Es wurden Probleme mit der KI Konfiguration festgestellt. Bitte überprüfe die Einstellungen.",
"experimental_warning": "Die LLM-Funktionen sind aktuell experimentell - sei an dieser Stelle gewarnt.",
"selected_provider": "Ausgewählter Anbieter",
"selected_provider_description": "Wähle einen KI-Anbieter für Chat- und Vervollständigungsfunktionen",
"select_model": "Wähle Modell...",
"select_provider": "Wähle Anbieter...",
"ai_enabled": "KI Funktionen aktiviert",
"ai_disabled": "KI Funktionen deaktiviert",
"no_models_found_online": "Keine Modelle gefunden. Bitte überprüfe den API-Key und die Einstellungen.",
"no_models_found_ollama": "Kein Ollama Modell gefunden. Bitte prüfe, ob Ollama gerade läuft.",
"error_fetching": "Fehler beim Abrufen der Modelle: {{error}}"
},
"zen_mode": {
"button_exit": "Verlasse Zen Modus"
},
"ui-performance": {
"title": "Leistung",
"enable-motion": "Aktiviere Übergänge und Animationen",
"enable-shadows": "Aktiviere Schatten",
"enable-backdrop-effects": "Aktiviere Hintergrundeffekte für Menüs, Pop-up Fenster und Panele"
},
"code-editor-options": {
"title": "Editor"
},
"custom_date_time_format": {
"title": "Benutzerdefiniertes Datums-/Zeitformat",
"description": "Passe das Format des Datums und der Uhrzeit an, die über <shortcut /> oder die Symbolleiste eingefügt werden. Die verfügbaren Format-Tokens sind unter <doc>Day.js docs</doc> zu finden.",
"format_string": "Format Zeichenfolge:",
"formatted_time": "Formatiertes Datum/Uhrzeit:"
} }
} }

View File

@@ -1113,6 +1113,12 @@
"layout-vertical-description": "launcher bar is on the left (default)", "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." "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": { "ai_llm": {
"not_started": "Not started", "not_started": "Not started",
"title": "AI Settings", "title": "AI Settings",
@@ -1253,7 +1259,12 @@
"selected_provider": "Selected Provider", "selected_provider": "Selected Provider",
"selected_provider_description": "Choose the AI provider for chat and completion features", "selected_provider_description": "Choose the AI provider for chat and completion features",
"select_model": "Select model...", "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": { "zoom_factor": {
"title": "Zoom Factor (desktop build only)", "title": "Zoom Factor (desktop build only)",
@@ -1310,7 +1321,7 @@
}, },
"revisions_snapshot_interval": { "revisions_snapshot_interval": {
"note_revisions_snapshot_interval_title": "Note Revision 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:" "snapshot_time_interval_label": "Note revision snapshot time interval:"
}, },
"revisions_snapshot_limit": { "revisions_snapshot_limit": {
@@ -1372,7 +1383,7 @@
}, },
"custom_date_time_format": { "custom_date_time_format": {
"title": "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:", "format_string": "Format string:",
"formatted_time": "Formatted date/time:" "formatted_time": "Formatted date/time:"
}, },
@@ -1994,11 +2005,22 @@
"help_title": "Display more information about this screen" "help_title": "Display more information about this screen"
}, },
"call_to_action": { "call_to_action": {
"next_theme_title": "The new Trilium theme is now stable", "next_theme_title": "Try the new Trilium theme",
"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_message": "You are currently using the legacy theme, would you like to try the new theme?",
"next_theme_button": "Switch to the new Trilium theme", "next_theme_button": "Try the new theme",
"background_effects_title": "Background effects are now stable", "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_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": "%"
} }
} }

View File

@@ -157,7 +157,8 @@
"showSQLConsole": "mostrar consola SQL", "showSQLConsole": "mostrar consola SQL",
"other": "Otro", "other": "Otro",
"quickSearch": "centrarse en la entrada de búsqueda rápida", "quickSearch": "centrarse en la entrada de búsqueda rápida",
"inPageSearch": "búsqueda en la página" "inPageSearch": "búsqueda en la página",
"title": "Hoja de ayuda"
}, },
"import": { "import": {
"importIntoNote": "Importar a nota", "importIntoNote": "Importar a nota",
@@ -224,11 +225,14 @@
"search_placeholder": "ruta de búsqueda por nombre (por defecto si está vacío)", "search_placeholder": "ruta de búsqueda por nombre (por defecto si está vacío)",
"modal_title": "Elija el tipo de nota", "modal_title": "Elija el tipo de nota",
"modal_body": "Elija el tipo de nota/plantilla de la nueva nota:", "modal_body": "Elija el tipo de nota/plantilla de la nueva nota:",
"templates": "Plantillas" "templates": "Plantillas",
"builtin_templates": "Plantillas incluidas"
}, },
"password_not_set": { "password_not_set": {
"title": "La contraseña no está establecida", "title": "La contraseña no está establecida",
"body1": "Las notas protegidas se cifran mediante una contraseña de usuario, pero la contraseña aún no se ha establecido." "body1": "Las notas protegidas se cifran mediante una contraseña de usuario, pero la contraseña aún no se ha establecido.",
"go_to_password_options": "Ir a opciones de contraseña",
"body2": "Para poder proteger las notas, haz click en el botón inferior para abrir la pantalla de Opciones y establecer tu contraseña."
}, },
"prompt": { "prompt": {
"title": "Aviso", "title": "Aviso",
@@ -1104,7 +1108,10 @@
"layout-vertical-title": "Vertical", "layout-vertical-title": "Vertical",
"layout-horizontal-title": "Horizontal", "layout-horizontal-title": "Horizontal",
"layout-vertical-description": "la barra del lanzador está en la izquierda (por defecto)", "layout-vertical-description": "la barra del lanzador está en la izquierda (por defecto)",
"layout-horizontal-description": "la barra de lanzamiento está debajo de la barra de pestañas, la barra de pestañas ahora tiene ancho completo." "layout-horizontal-description": "la barra de lanzamiento está debajo de la barra de pestañas, la barra de pestañas ahora tiene ancho completo.",
"auto_theme": "Heredado (Sigue el esquema de colores del sistema)",
"light_theme": "Heredado (Claro)",
"dark_theme": "Heredado (Oscuro)"
}, },
"ai_llm": { "ai_llm": {
"not_started": "No iniciado", "not_started": "No iniciado",
@@ -1303,7 +1310,7 @@
}, },
"revisions_snapshot_interval": { "revisions_snapshot_interval": {
"note_revisions_snapshot_interval_title": "Intervalo de instantáneas de revisiones de notas", "note_revisions_snapshot_interval_title": "Intervalo de instantáneas de revisiones de notas",
"note_revisions_snapshot_description": "El intervalo de tiempo de la instantánea de revisión de nota es el tiempo después de lo cual se creará una nueva revisión para la nota. Ver <a href=\"https://triliumnext.github.io/docs/wiki/note-revisions.html\" class=\"external\"> wiki </a> para obtener más información.", "note_revisions_snapshot_description": "El intervalo de tiempo de la instantánea de revisión de nota es el tiempo después de lo cual se creará una nueva revisión para la nota. Ver <doc>wiki</doc> para obtener más información.",
"snapshot_time_interval_label": "Intervalo de tiempo de la instantánea de revisión de notas:" "snapshot_time_interval_label": "Intervalo de tiempo de la instantánea de revisión de notas:"
}, },
"revisions_snapshot_limit": { "revisions_snapshot_limit": {
@@ -1365,7 +1372,7 @@
}, },
"custom_date_time_format": { "custom_date_time_format": {
"title": "Formato de fecha/hora personalizada", "title": "Formato de fecha/hora personalizada",
"description": "Personalizar el formado de fecha y la hora insertada vía <kbd></kbd> o la barra de herramientas. Véa la <a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">documentación de Day.js</a> para más tokens de formato disponibles.", "description": "Personalizar el formado de fecha y la hora insertada vía <shortcut /> o la barra de herramientas. Véa la <doc>documentación de Day.js</doc> para más tokens de formato disponibles.",
"format_string": "Cadena de formato:", "format_string": "Cadena de formato:",
"formatted_time": "Fecha/hora personalizada:" "formatted_time": "Fecha/hora personalizada:"
}, },
@@ -1983,6 +1990,16 @@
"configure_launch_bar_description": "Abrir la configuración de la barra de inicio, para agregar o quitar elementos." "configure_launch_bar_description": "Abrir la configuración de la barra de inicio, para agregar o quitar elementos."
}, },
"modal": { "modal": {
"close": "Cerrar" "close": "Cerrar",
"help_title": "Mostrar más información sobre esta pantalla"
},
"call_to_action": {
"next_theme_title": "Prueba el nuevo tema de Trilium",
"next_theme_message": "Estas usando actualmente el tema heredado, ¿Te gustaría probar el nuevo tema?",
"next_theme_button": "Prueba el nuevo tema",
"background_effects_title": "Los efectos de fondo son ahora estables",
"background_effects_message": "En los dispositivos Windows, los efectos de fondo ya son totalmente estables. Los efectos de fondo añaden un toque de color a la interfaz de usuario difuminando el fondo que hay detrás. Esta técnica también se utiliza en otras aplicaciones como el Explorador de Windows.",
"background_effects_button": "Activar efectos de fondo",
"dismiss": "Desestimar"
} }
} }

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

@@ -156,7 +156,9 @@
"showSQLConsole": "afficher la console SQL", "showSQLConsole": "afficher la console SQL",
"other": "Autre", "other": "Autre",
"quickSearch": "aller à la recherche rapide", "quickSearch": "aller à la recherche rapide",
"inPageSearch": "recherche sur la page" "inPageSearch": "recherche sur la page",
"title": "Aide-mémoire",
"newTabWithActivationNoteLink": "Lorsquon clique sur un lien de note, celle-ci souvre et devient active dans un nouvel onglet"
}, },
"import": { "import": {
"importIntoNote": "Importer dans la note", "importIntoNote": "Importer dans la note",
@@ -200,7 +202,8 @@
"okButton": "OK" "okButton": "OK"
}, },
"jump_to_note": { "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": { "markdown_import": {
"dialog_title": "Importation Markdown", "dialog_title": "Importation Markdown",
@@ -220,11 +223,16 @@
"note_type_chooser": { "note_type_chooser": {
"modal_title": "Choisissez le type de note", "modal_title": "Choisissez le type de note",
"modal_body": "Choisissez le type de note/le modèle de la nouvelle 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": { "password_not_set": {
"title": "Le mot de passe n'est pas défini", "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": { "prompt": {
"title": "Prompt", "title": "Prompt",
@@ -266,7 +274,9 @@
"mime": "MIME : ", "mime": "MIME : ",
"file_size": "Taille du fichier :", "file_size": "Taille du fichier :",
"preview": "Aperçu :", "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_child_notes": {
"sort_children_by": "Trier les enfants par...", "sort_children_by": "Trier les enfants par...",
@@ -377,7 +387,7 @@
"share_root": "partage cette note à l'adresse racine /share.", "share_root": "partage cette note à l'adresse racine /share.",
"share_description": "définir le texte à ajouter à la balise méta HTML pour la description", "share_description": "définir le texte à ajouter à la balise méta HTML pour la description",
"share_raw": "la note sera servie dans son format brut, sans wrapper HTML", "share_raw": "la note sera servie dans son format brut, sans wrapper HTML",
"share_disallow_robot_indexing": "interdira l'indexation par robot de cette note via l'en-tête <code>X-Robots-Tag: noindex</code>", "share_disallow_robot_indexing": "Interdira l'indexation par robot de cette note via l'en-tête <code>X-Robots-Tag: noindex</code>",
"share_credentials": "exiger des informations didentification pour accéder à cette note partagée. La valeur devrait être au format « nom d'utilisateur : mot de passe ». N'oubliez pas de rendre cela héritable pour l'appliquer aux notes/images enfants.", "share_credentials": "exiger des informations didentification pour accéder à cette note partagée. La valeur devrait être au format « nom d'utilisateur : mot de passe ». N'oubliez pas de rendre cela héritable pour l'appliquer aux notes/images enfants.",
"share_index": "la note avec ce label listera toutes les racines des notes partagées", "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.", "display_relations": "noms des relations délimités par des virgules qui doivent être affichés. Tous les autres seront masqués.",
@@ -416,7 +426,8 @@
"other_notes_with_name": "Autres notes portant le nom {{attributeType}} \"{{attributeName}}\"", "other_notes_with_name": "Autres notes portant le nom {{attributeType}} \"{{attributeName}}\"",
"and_more": "... et {{count}} plus.", "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_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": { "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>", "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>",
@@ -536,7 +547,7 @@
}, },
"attachments_actions": { "attachments_actions": {
"open_externally": "Ouverture externe", "open_externally": "Ouverture externe",
"open_externally_title": "Le fichier sera ouvert dans une application externe et les modifications apportées seront surveillées. \nVous pourrez ensuite téléverser la version modifiée dans Trilium.", "open_externally_title": "Le fichier sera ouvert dans une application externe et surveillé pour détecter les modifications. Vous pourrez ensuite téléverser la version modifiée dans Trilium.",
"open_custom": "Ouvrir avec", "open_custom": "Ouvrir avec",
"open_custom_title": "Le fichier sera ouvert dans une application externe et surveillé pour les modifications. Vous pourrez ensuite téléverser la version modifiée sur Trilium.", "open_custom_title": "Le fichier sera ouvert dans une application externe et surveillé pour les modifications. Vous pourrez ensuite téléverser la version modifiée sur Trilium.",
"download": "Télécharger", "download": "Télécharger",
@@ -575,7 +586,8 @@
"september": "Septembre", "september": "Septembre",
"october": "Octobre", "october": "Octobre",
"november": "Novembre", "november": "Novembre",
"december": "Décembre" "december": "Décembre",
"cannot_find_week_note": "Impossible de trouver la note de la semaine"
}, },
"close_pane_button": { "close_pane_button": {
"close_this_pane": "Fermer ce volet" "close_this_pane": "Fermer ce volet"
@@ -719,7 +731,8 @@
"basic_properties": { "basic_properties": {
"note_type": "Type de note", "note_type": "Type de note",
"editable": "Modifiable", "editable": "Modifiable",
"basic_properties": "Propriétés de base" "basic_properties": "Propriétés de base",
"language": "Langage"
}, },
"book_properties": { "book_properties": {
"view_type": "Type d'affichage", "view_type": "Type d'affichage",
@@ -730,7 +743,11 @@
"collapse": "Réduire", "collapse": "Réduire",
"expand": "Développer", "expand": "Développer",
"invalid_view_type": "Type de vue non valide '{{type}}'", "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": { "edited_notes": {
"no_edited_notes_found": "Aucune note modifiée ce jour-là...", "no_edited_notes_found": "Aucune note modifiée ce jour-là...",
@@ -807,7 +824,8 @@
"unknown_label_type": "Type de label inconnu '{{type}}'", "unknown_label_type": "Type de label inconnu '{{type}}'",
"unknown_attribute_type": "Type d'attribut inconnu '{{type}}'", "unknown_attribute_type": "Type d'attribut inconnu '{{type}}'",
"add_new_attribute": "Ajouter un nouvel attribut", "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": { "script_executor": {
"query": "Requête", "query": "Requête",
@@ -897,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.", "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.", "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_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." "note": "Notez que le script de recherche et la l'expression à rechercher standard ne peuvent pas être combinés."
}, },
"search_string": { "search_string": {
@@ -1066,7 +1084,8 @@
"max_width_label": "Largeur maximale du contenu en pixels", "max_width_label": "Largeur maximale du contenu en pixels",
"apply_changes_description": "Pour appliquer les modifications de largeur du contenu, cliquez sur", "apply_changes_description": "Pour appliquer les modifications de largeur du contenu, cliquez sur",
"reload_button": "recharger l'interface", "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": { "native_title_bar": {
"title": "Barre de titre native (nécessite le redémarrage de l'application)", "title": "Barre de titre native (nécessite le redémarrage de l'application)",
@@ -1089,7 +1108,10 @@
"layout-vertical-title": "Vertical", "layout-vertical-title": "Vertical",
"layout-horizontal-title": "Horizontal", "layout-horizontal-title": "Horizontal",
"layout-vertical-description": "la barre de raccourcis est à gauche (défaut)", "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": { "zoom_factor": {
"title": "Facteur de zoom (version bureau uniquement)", "title": "Facteur de zoom (version bureau uniquement)",
@@ -1134,14 +1156,14 @@
"note_erasure_timeout": { "note_erasure_timeout": {
"note_erasure_timeout_title": "Délai d'effacement des notes", "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.", "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) :", "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", "erase_deleted_notes_now": "Effacer les notes supprimées maintenant",
"deleted_notes_erased": "Les notes supprimées ont été effacées." "deleted_notes_erased": "Les notes supprimées ont été effacées."
}, },
"revisions_snapshot_interval": { "revisions_snapshot_interval": {
"note_revisions_snapshot_interval_title": "Délai d'enregistrement automatique d'une version de note", "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 :" "snapshot_time_interval_label": "Délai d'enregistrement automatique de version de note :"
}, },
"revisions_snapshot_limit": { "revisions_snapshot_limit": {
@@ -1648,5 +1670,26 @@
}, },
"modal": { "modal": {
"close": "Fermer" "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 @@
{}

File diff suppressed because it is too large Load Diff

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"
}
}

View File

@@ -78,7 +78,121 @@
"n_notes_queued_2": "{{ count }} notas enfileiradas para indexação", "n_notes_queued_2": "{{ count }} notas enfileiradas para indexação",
"notes_indexed_0": "{{ count }} nota indexada", "notes_indexed_0": "{{ count }} nota indexada",
"notes_indexed_1": "{{ count }} notas indexadas", "notes_indexed_1": "{{ count }} notas indexadas",
"notes_indexed_2": "{{ count }} notas indexadas" "notes_indexed_2": "{{ count }} notas indexadas",
"temperature": "Temperatura",
"retry_queued": "Nota enfileirada para nova tentativa",
"queued_notes": "Notas Enfileiradas",
"empty_key_warning": {
"voyage": "A chave de API da Voyage API está vazia. Por favor, digite uma chave de API válida.",
"ollama": "A chave de API da Ollama API está vazia. Por favor, digite uma chave de API válida.",
"anthropic": "A chave de API Anthropic está vazia. Por favor, digite uma chave de API válida.",
"openai": "A chave de API OpenAI está vazia. Por favor, digite uma chave de API válida."
},
"not_started": "Não iniciado",
"title": "Configurações de IA",
"processed_notes": "Notas Processadas",
"total_notes": "Total de Notas",
"progress": "Andamento",
"failed_notes": "Notas com Falha",
"last_processed": "Últimas Processadas",
"refresh_stats": "Atualizar Estatísticas",
"enable_ai_features": "Ativar recurso IA/LLM",
"enable_ai_description": "Ativar recursos IA como sumarização de notas, geração de conteúdo, e outras capacidades de LLM",
"openai_tab": "OpenAI",
"anthropic_tab": "Anthropic",
"voyage_tab": "Voyage AI",
"enable_ai": "Ativar recursos IA/LLM",
"provider_configuration": "Configuração de Provedor de IA",
"system_prompt": "Prompt de Sistema",
"system_prompt_description": "Prompt padrão de sistema usado para todas as interações de IA",
"openai_configuration": "Configuração OpenAI",
"openai_settings": "Opções OpenAI",
"api_key": "Chave de API",
"url": "URL Base",
"model": "Modelo",
"openai_api_key_description": "Sua API Key da OpenAI para acessar os serviços de IA",
"anthropic_api_key_description": "Sua API Key da Anthropic para acessar os modelos Claude",
"default_model": "Modelo Padrão",
"openai_model_description": "Exemplos: gpt-4o, gpt-4-turbo, gpt-3.5-turbo",
"base_url": "URL Base",
"openai_url_description": "Padrão: https://api.openai.com/v1",
"anthropic_settings": "Configurações Anthropic",
"anthropic_url_description": "URL Base da API Anthropic (padrão: https://api.anthropic.com)",
"anthropic_model_description": "Modelos Claude da Anthropic para completar conversas",
"voyage_settings": "Configurações Voyage AI",
"retry": "Tentar novamente",
"retry_failed": "Falha ao enfileirar nota para nova tentativa",
"max_notes_per_llm_query": "Máximo de Notas por Consulta",
"max_notes_per_llm_query_description": "Número máximo de notas similares para incluir no contexto da IA",
"active_providers": "Provedores Ativos",
"disabled_providers": "Provedores Desativados",
"remove_provider": "Remover provedor da pesquisa",
"restore_provider": "Restaurar provedor na pesquisa",
"similarity_threshold": "Tolerância de Similaridade",
"similarity_threshold_description": "Pontuação máxima de similaridade (0-1) para notas a serem incluídas no contexto das consultas de LLM",
"reprocess_index": "Reconstruir Índice de Pesquisa",
"reprocessing_index": "Reconstruindo…",
"reprocess_index_started": "Otimiação do índice de pesquisa iniciado em plano de fundo",
"reprocess_index_error": "Erro ao reconstruir índice de pesquisa",
"index_rebuild_progress": "Andamento da Reconstrução do Índice",
"index_rebuilding": "Otimizando índice ({{percentage}}%)",
"index_rebuild_complete": "Otimização de índice finalizada",
"index_rebuild_status_error": "Erro ao verificar o estado da reconstrução do índice",
"never": "Nunca",
"processing": "Processando ({{percentage}}%)",
"incomplete": "Incompleto ({{percentage}}%)",
"complete": "Completo (100%)",
"refreshing": "Atualizando…",
"auto_refresh_notice": "Atualizando automaticamente a cada {{seconds}} segundos",
"note_queued_for_retry": "Nota enfileirada para nova tentativa",
"failed_to_retry_note": "Falha ao retentar nota",
"all_notes_queued_for_retry": "Todas as notas com falha enfileiradas para nova tentativa",
"failed_to_retry_all": "Falha ao retentar notas",
"ai_settings": "Configurações IA",
"api_key_tooltip": "Chave de API para acessar o serviço",
"agent": {
"processing": "Processando…",
"thinking": "Pensando…",
"loading": "Carregando…",
"generating": "Gerando…"
},
"name": "IA",
"openai": "OpenAI",
"use_enhanced_context": "Usar contexto aprimorado",
"enhanced_context_description": "Alimentar IA com mais contexto sobre a nota e suas notas relacionadas para melhores respostas",
"show_thinking": "Exibir pensamento",
"enter_message": "Digite sua mensagem…",
"error_contacting_provider": "Erro ao contatar o provedor de IA. Por favor, verifique suas configurações e sua conexão de internet.",
"error_generating_response": "Erro ao gerar resposta da IA",
"index_all_notes": "Indexar Todas as Notas",
"index_status": "Estado do Índice",
"indexed_notes": "Notas Indexadas",
"indexing_stopped": "Indexação interrompida",
"indexing_in_progress": "Indexação em andamento…",
"last_indexed": "Última Indexada",
"note_chat": "Conversa de Nota",
"sources": "Origens",
"start_indexing": "Iniciar Indexação",
"use_advanced_context": "Usar Contexto Avançado",
"ollama_no_url": "Ollama não está configurado. Por favor, digite uma URL válida.",
"chat": {
"root_note_title": "Conversas IA",
"root_note_content": "Esta nota contém suas conversas com IA salvas.",
"new_chat_title": "Nova Conversa",
"create_new_ai_chat": "Criar nova Conversa IA"
},
"create_new_ai_chat": "Criar nova Conversa IA",
"configuration_warnings": "Existem alguns problemas com sua configuração de IA. Por fovor, verifique suas configurações.",
"experimental_warning": "O recurso de LLM atualmente é experimental - você foi avisado.",
"selected_provider": "Provedor Selecionado",
"selected_provider_description": "Escolha o provedor de IA para conversas e recursos de completar",
"select_model": "Selecionar modelo…",
"select_provider": "Selecionar provedor…",
"ai_enabled": "Recursos de IA habilitados",
"ai_disabled": "Recursos de IA desabilitados",
"no_models_found_online": "Nenhum modelo encontrado. Por favor, verifique sua chave de API e as configurações.",
"no_models_found_ollama": "Nenhum modelo Ollama encontrado. Por favor, verifique se o Ollama está em execução.",
"error_fetching": "Erro ao obter modelos: {{error}}"
}, },
"confirm": { "confirm": {
"confirmation": "Confirmação", "confirmation": "Confirmação",
@@ -249,7 +363,7 @@
}, },
"prompt": { "prompt": {
"title": "Prompt", "title": "Prompt",
"ok": "OK <kbd>enter</kbd>", "ok": "OK",
"defaultTitle": "Prompt" "defaultTitle": "Prompt"
}, },
"protected_session_password": { "protected_session_password": {
@@ -257,7 +371,7 @@
"help_title": "Ajuda sobre notas protegidas", "help_title": "Ajuda sobre notas protegidas",
"close_label": "Fechar", "close_label": "Fechar",
"form_label": "Para prosseguir com a ação solicitada, você precisa iniciar uma sessão protegida digitando a senha:", "form_label": "Para prosseguir com a ação solicitada, você precisa iniciar uma sessão protegida digitando a senha:",
"start_button": "Iniciar sessão protegida <kbd>enter</kbd>" "start_button": "Iniciar sessão protegida"
}, },
"recent_changes": { "recent_changes": {
"title": "Alterações recentes", "title": "Alterações recentes",
@@ -306,12 +420,12 @@
"sort_with_respect_to_different_character_sorting": "classificar de acordo com diferentes regras de ordenação de caracteres e colação em diferentes idiomas ou regiões.", "sort_with_respect_to_different_character_sorting": "classificar de acordo com diferentes regras de ordenação de caracteres e colação em diferentes idiomas ou regiões.",
"natural_sort_language": "Linguagem da ordenação natural", "natural_sort_language": "Linguagem da ordenação natural",
"the_language_code_for_natural_sort": "O código do idioma para ordenação natural, por exemplo, \"zh-CN\" para chinês.", "the_language_code_for_natural_sort": "O código do idioma para ordenação natural, por exemplo, \"zh-CN\" para chinês.",
"sort": "Ordenar <kbd>enter</kbd>" "sort": "Ordenar"
}, },
"upload_attachments": { "upload_attachments": {
"upload_attachments_to_note": "Enviar anexos para a nota", "upload_attachments_to_note": "Enviar anexos para a nota",
"choose_files": "Escolher arquivos", "choose_files": "Escolher arquivos",
"files_will_be_uploaded": "Os arquivos serão enviados como anexos para", "files_will_be_uploaded": "Os arquivos serão enviados como anexos para {{noteTitle}}",
"options": "Opções", "options": "Opções",
"shrink_images": "Reduzir imagens", "shrink_images": "Reduzir imagens",
"upload": "Enviar", "upload": "Enviar",
@@ -405,10 +519,723 @@
"share_index": "notas com este rótulo irão listar todas as raízes das notas compartilhadas", "share_index": "notas com este rótulo irão listar todas as raízes das notas compartilhadas",
"display_relations": "nomes das relações separados por vírgula que devem ser exibidos. Todas as outras serão ocultadas.", "display_relations": "nomes das relações separados por vírgula que devem ser exibidos. Todas as outras serão ocultadas.",
"hide_relations": "nomes das relações separados por vírgula que devem ser ocultados. Todas as outras serão exibidas.", "hide_relations": "nomes das relações separados por vírgula que devem ser ocultados. Todas as outras serão exibidas.",
"title_template": "Título padrão das notas criadas como filhas desta nota. O valor é avaliado como uma string JavaScript e pode ser enriquecido com conteúdo dinâmico usando as variáveis injetadas <code>now</code> e <code>parentNote</code>. Exemplos:\n\n<ul>\n <li><code>${parentNote.getLabelValue('authorName')}'s literary works</code></li>\n <li><code>Log for ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n</ul>\n\nVeja a <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">wiki com detalhes</a>, a documentação da API para <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> e para <a href=\"https://day.js.org/docs/en/display/format\">now</a> para mais informações.", "title_template": "título padrão das notas criadas como filhas desta nota. O valor é avaliado como uma string JavaScript \n e pode ser enriquecido com conteúdo dinâmico usando as variáveis injetadas <code>now</code> e <code>parentNote</code>. Exemplos:\n \n <ul>\n <li><code>${parentNote.getLabelValue('authorName')}'s literary works</code></li>\n <li><code>Log for ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n Veja a <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">wiki com detalhes</a>, a documentação da API para <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> e para <a href=\"https://day.js.org/docs/en/display/format\">now</a> para mais informações.",
"template": "Esta nota aparecerá na seleção de modelos disponíveis ao criar uma nova nota", "template": "Esta nota aparecerá na seleção de modelos disponíveis ao criar uma nova nota",
"toc": "<code>#toc</code> ou <code>#toc=show</code> irá forçar a exibição do Sumário, <code>#toc=hide</code> irá forçar que ele fique oculto. Se o rótulo não existir, será considerado o ajuste global", "toc": "<code>#toc</code> ou <code>#toc=show</code> irá forçar a exibição do Sumário, <code>#toc=hide</code> irá forçar que ele fique oculto. Se o rótulo não existir, será considerado o ajuste global",
"color": "define a cor da nota na árvore de notas, links etc. Use qualquer valor de cor CSS válido, como 'red' ou #a13d5f", "color": "define a cor da nota na árvore de notas, links etc. Use qualquer valor de cor CSS válido, como 'red' ou #a13d5f",
"keyboard_shortcut": "Define um atalho de teclado que irá pular imediatamente para esta nota. Exemplo: 'ctrl+alt+e'. É necessário recarregar o frontend para que a alteração tenha efeito." "keyboard_shortcut": "Define um atalho de teclado que irá pular imediatamente para esta nota. Exemplo: 'ctrl+alt+e'. É necessário recarregar o frontend para que a alteração tenha efeito.",
"hide_highlight_widget": "Ocultar o widget da lista de destaques",
"keep_current_hoisting": "Abrir este link não alterará o destaque, mesmo que a nota não seja exibível na subárvore destacada atual.",
"execute_button": "Titulo do botão que executará a nota de código atual",
"exclude_from_note_map": "Notas com este rótulo ficarão ocultas no Mapa de Notas",
"new_notes_on_top": "Novas notas serão criadas no topo da nota raiz, não na parte inferior.",
"execute_description": "Descrição longa da nota de código atualmente exibida junto ao botão executar",
"print_page_size": "Quando exportando para PDF, altera o tamanho da página. Valores suportados: <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>.",
"and_more": "... e {{count}} mais.",
"other_notes_with_name": "Outras notas com {{attributeType}} igual a \"{{attributeName}}\"",
"color_type": "Cor",
"run_on_note_creation": "executa quando a nota é criada no backend. Use esta relação se quiser executar o script para todas as notas criadas em uma subárvore específica. Neste caso, crie-a na nota raiz da subárvore e torne-a herdável. Uma nova nota criada dentro da subárvore (qualquer profundidade) irá acionar o script.",
"run_on_child_note_creation": "executa quando uma nova nota é criada sob a nota onde esta relação está definida",
"run_on_note_title_change": "executa quando o título da nota é alterado (inclusive na criação de nota)",
"run_on_note_content_change": "executa quando o conteúdo da nota é alterado (inclusive na criação de nota).",
"run_on_note_change": "executa quando a nota é alterada (inclusive na criação de nota). Não incluí alterações no conteúdo",
"run_on_note_deletion": "executa quando a nota está sendo excluída",
"run_on_branch_creation": "executa quando uma ramificação é criada. Ramificação é uma ligação entre nota pai e nota filha e é criado, por exemplo, ao clonar ou mover uma nota.",
"run_on_branch_change": "executa quando uma remificação é atualizada.",
"run_on_attribute_creation": "executa quando um novo atributo é criado para a nota que define esta relação",
"run_on_attribute_change": " executa quando o atributo é alterado na nota que define esta relação. Também é disparado quando o atributo é excluído",
"widget_relation": "o destino desta relação será executado e renderizado como um widget na barra lateral",
"run_on_branch_deletion": "executa quando uma ramificação é excluída. Ramificação é um link entre a nota pai e a nota filha e é excluído, por exemplo, ao mover a nota (a ramificação/link antiga é excluída).",
"relation_template": "os atributos da nota serão herdados mesmo sem um relacionamento pai-filho, o conteúdo e subárvore da nota serão adicionados às notas da instância se vazias. Veja a documentação para detalhes.",
"inherit": "os atributos da nota serão herdados mesmo sem um relacionamento pai-filho. Veja relação de modelos para um conceito semelhante. Veja a herança de atributos na documentação.",
"render_note": "notas do tipo \"nota de renderização HTML\" serão renderizadas usando uma nota de código (HTML ou script) e é necessário apontar usando esta relação qual nota deve ser renderizada",
"share_css": "Nota CSS que será injetada na página de compartilhamento. A nota CSS também deve estar na subárvore compartilhada. Considere usar também 'share_hidden_from_tree' e 'share_omit_default_css'.",
"share_js": "Nota JavaScript que será injetada na página de compartilhamento. A nota JS também deve estar na subárvore compartilhada. Considere usar 'share_hidden_from_tree'.",
"share_template": "Nota JavaScript incorporada que será usada como modelo para exibir a nota compartilhada. Retorna ao modelo padrão. Considere usar 'share_hidden_from_tree'.",
"share_favicon": "Nota Favicon que será usada na página compartilhada. Tipicamente você quer defini-la na raiz do compartilhamento e torná-lo herdável. A nota de Favicon também deve estar na subárvore compartilhada. Considere usar 'share_hidden_from_tree'.",
"is_owned_by_note": "é propriedade da nota",
"print_landscape": "Ao exportar para PDF, muda a orientação da página para paisagem em vez de retrato."
},
"attachments_actions": {
"delete_attachment": "Excluir anexo",
"open_externally": "Abrir externamente",
"open_custom": "Abrir customizado",
"download": "Baixar",
"rename_attachment": "Renomear anexo",
"upload_new_revision": "Enviar nova revisão",
"copy_link_to_clipboard": "Copiar link para a área de transferência",
"convert_attachment_into_note": "Converter anexo para nota",
"upload_success": "Uma nova revisão de anexo foi enviada.",
"upload_failed": "O envio de uma nova revisão de anexo falhou.",
"delete_success": "O anexo '{{title}}' foi excluído.",
"convert_success": "O anexo '{{title}}' foi convertido para uma nota.",
"enter_new_name": "Por favor, digite o novo nome do anexo",
"delete_confirm": "Tem certeza que deseja excluir o anexo '{{title}}'?",
"convert_confirm": "Tem certeza que deseja converter o anexo '{{title}}' em uma nota separada?",
"open_externally_title": "O arquivo será aberto em uma aplicação externa e monitorado por alterações. Você então poderá enviar a versão modificada de volta para o Trilium.",
"open_custom_title": "O arquivo será aberto em uma aplicação externa e monitorado por alterações. Você então poderá enviar a versão modificada de volta para o Trilium.",
"open_externally_detail_page": "A abertura de anexo externamente só está disponível através da página de detalhes. Por favor, primeiro clique nos detalhes do anexo e repita a ação.",
"open_custom_client_only": "A abertura customizada de anexos só pode ser feita usando o cliente de desktop."
},
"attachment_detail": {
"you_can_also_open": ", você também pode abrir o(a) ",
"open_help_page": "Abrir página de ajuda nos anexos",
"list_of_all_attachments": "Lista de todos os anexos",
"attachment_deleted": "Este anexo foi excluído."
},
"ancestor": {
"depth_gt": "é maior que {{count}}",
"label": "Ancestral",
"placeholder": "buscar notas pelo nome",
"depth_label": "profundidade",
"depth_doesnt_matter": "não importa",
"depth_eq": "é exatamente {{count}}",
"direct_children": "filho direto",
"depth_lt": "é menor que {{count}}"
},
"add_relation": {
"add_relation": "Adicionar relação",
"allowed_characters": "Caracteres alfanuméricos, underscore e vírgula são permitidos.",
"relation_name": "nome da relação",
"to": "para",
"target_note": "nota destino",
"create_relation_on_all_matched_notes": "Crie a relação informada em todas as notas correspondentes."
},
"delete_label": {
"label_name_placeholder": "nome da etiqueta",
"label_name_title": "Caracteres alfanuméricos, underscore e vírgula são permitidos.",
"delete_label": "Excluir etiqueta"
},
"rename_label": {
"rename_label": "Renomear etiqueta",
"rename_label_from": "Renomear etiqueta de",
"old_name_placeholder": "nome antigo",
"to": "Para",
"new_name_placeholder": "novo nome",
"name_title": "Caracteres alfanuméricos, underscore e vírgula são permitidos."
},
"execute_script": {
"example_1": "Por exemplo para anexar um texto ao título de uma nota, use este pequeno script:",
"execute_script": "Executar script",
"help_text": "Você pode executar scripts simples nas notas correspondentes.",
"example_2": "Um exemplo mais complexo seria excluir todos os atributos das notas correspondentes:"
},
"attribute_editor": {
"help_text_body1": "Para adicionar uma etiqueta, digite por exemplo <code>#rock</code> ou se você também quer adicionar um valor então por exemplo <code>#year = 2020</code>",
"help_text_body2": "Para relação, digite <code>~author = @</code>, que deve ser exibido um autocompletar onde você pode encontrar a nota desejada.",
"help_text_body3": "Alternativamente, você pode adicionar etiqueta e relação usando o botão <code>+</code> no lado direito.",
"save_attributes": "Salvar atributos <enter>",
"add_a_new_attribute": "Adicionar um novo atributo",
"add_new_label": "Adicionar nova etiqueta <kbd data-command=\"addNewLabel\"></kbd>",
"add_new_relation": "Adicionar nova relação <kbd data-command=\"addNewRelation\"></kbd>",
"add_new_label_definition": "Adicionar nova definição de etiqueta",
"add_new_relation_definition": "Adicionar nova definição de relação",
"placeholder": "Digite as etiquetas e relações aqui"
},
"abstract_bulk_action": {
"remove_this_search_action": "Remover esta ação de busca"
},
"add_label": {
"add_label": "Adicionar etiqueta",
"label_name_placeholder": "nome da etiqueta",
"label_name_title": "Caracteres alfanuméricos, underscore e vírgula são permitidos.",
"to_value": "para o valor",
"new_value_placeholder": "novo valor",
"help_text": "Em todas as notas correspondentes:",
"help_text_item1": "criar a etiqueta indicada se a nota ainda não tiver uma",
"help_text_item2": "ou altere o valor da etiqueta existente",
"help_text_note": "Você também pode chamar este método sem valor, neste caso a etiqueta será atribuída à nota sem valor."
},
"update_label_value": {
"update_label_value": "Atualizar valor da etiqueta",
"label_name_placeholder": "nome da etiqueta",
"label_name_title": "Caracteres alfanuméricos, underscore e vírgula são permitidos.",
"new_value_placeholder": "novo valor",
"to_value": "para o valor",
"help_text": "Em todas as notas correspondentes, altera o valor da etiqueta existente.",
"help_text_note": "Você também pode chamar este método sem um valor, neste caso a etiqueta será à nota sem um valor."
},
"delete_relation": {
"allowed_characters": "Caracteres alfanuméricos, underscore e vírgula são permitidos.",
"delete_relation": "Excluir relação",
"relation_name": "nome da relação"
},
"rename_relation": {
"allowed_characters": "Caracteres alfanuméricos, underscore e vírgula são permitidos.",
"rename_relation": "Renomar relação",
"rename_relation_from": "Renomear relação de",
"old_name": "nome antigo",
"to": "Para",
"new_name": "novo nome"
},
"update_relation_target": {
"allowed_characters": "Caracteres alfanuméricos, underscore e vírgula são permitidos.",
"to": "para",
"target_note": "nota destino",
"on_all_matched_notes": "Em todas as notas correspondentes",
"change_target_note": "alterar nota destino da relação existente",
"update_relation_target": "Atualizar destino da relação",
"update_relation": "Atualizar relação",
"relation_name": "nome da relação"
},
"content_renderer": {
"open_externally": "Abrir externamente"
},
"modal": {
"close": "Fechar"
},
"api_log": {
"close": "Fechar"
},
"attachment_detail_2": {
"will_be_deleted_in": "Este anexo será excluído automaticamente em {{time}}",
"will_be_deleted_soon": "Este anexo será excluído automaticamente em breve",
"deletion_reason": ", porque o anexo não está associado ao conteúdo da nota. Para evitar a exclusão, adicione o anexo novamente ao conteúdo ou converta o anexo em uma nota.",
"role_and_size": "Regra: {{role}}, Tamanho: {{size}}",
"link_copied": "Link do anexo copiado para a área de transferência.",
"unrecognized_role": "Regra desconhecida de anexo '{{role}}'."
},
"bookmark_switch": {
"bookmark": "Favorito",
"bookmark_this_note": "Favoritar esta nota no painel da esquerda",
"remove_bookmark": "Remover favorito"
},
"editability_select": {
"auto": "Auto",
"read_only": "Somente leitura",
"always_editable": "Sempre Editável",
"note_is_editable": "A nota é editável se não for muito longa.",
"note_is_read_only": "A nota é somente leitura, mas pode ser editada com um clique no botão.",
"note_is_always_editable": "A nota é sempre editável, independentemente do seu tamanho."
},
"note-map": {
"button-link-map": "Mapa de Links",
"button-tree-map": "Mapa em Árvore"
},
"tree-context-menu": {
"open-in-a-new-tab": "Abrir em uma nova aba <kbd>Ctrl+Click</kbd>",
"open-in-a-new-split": "Abrir em um novo painel dividido",
"insert-note-after": "Inserir nota após",
"insert-child-note": "Inserir nota filha",
"delete": "Excluir",
"search-in-subtree": "Buscar na subárvore"
},
"command_palette": {
"search_subtree_title": "Buscar na Subárvore",
"search_subtree_description": "Buscar dentro da subárvore atual",
"search_history_title": "Exibir Histórico de Busca",
"search_history_description": "Visualizar buscas anteriores",
"configure_launch_bar_title": "Configurar Barra de Execução"
},
"delete_note": {
"delete_note": "Excluir nota",
"delete_matched_notes": "Excluir notas correspondentes",
"delete_matched_notes_description": "Isso irá excluir as notas correspondentes.",
"undelete_notes_instruction": "Depois da exclusão, é possível desfazer através da janela de Alterações Recentes.",
"erase_notes_instruction": "Para apagar notas permanentemente, você pode fazer isso depois da exclusão indo em Opções -> Outros e clicar no botão \"Apagar notas excluídas agora\"."
},
"delete_revisions": {
"delete_note_revisions": "Excluir revisões da nota",
"all_past_note_revisions": "Todas as revisões anteriores das notas correspondentes serão excluídas. A nota em si será perservada. Ou seja, o histórico da nota será removido."
},
"move_note": {
"move_note": "Mover nota",
"to": "para",
"target_parent_note": "nota pai destino",
"on_all_matched_notes": "Em todas as notas correspondentes",
"move_note_new_parent": "move a nota para o novo pai se a nota tem apenas um pai (ou seja, a antiga ramificação é removida e uma nova ramificação é criada para o novo pai)",
"clone_note_new_parent": "clona a nota para o novo pai se a nota tem vários clones / ramificações (não é claro qual ramificação deve ser removida)",
"nothing_will_happen": "nada acontecerá se a nota não puder ser movida para a nota de destino (por exemplo, se criaria um ciclo de árvore)"
},
"rename_note": {
"rename_note": "Renomear nota",
"rename_note_title_to": "Renomear título da nota para",
"new_note_title": "novo título da nota",
"click_help_icon": "Clique no ícone de ajuda a direita para ver todas as opções",
"example_note": "<code>Nota</code> - todas as notas correspondentes serão renomeadas para 'Nota'",
"example_new_title": "<code>NOVO: ${note.title}</code> - o título das notas correspondentes receberá o prefixo 'NOVO: '",
"example_date_prefix": "<code>${note.dateCreatedObj.format('MM-DD:')}: ${note.title}</code> - notas correspondentes receberão um prefixo com o mês-dia da data de criação da nota",
"api_docs": "Veja da documentação da API para <a href='https://zadam.github.io/trilium/backend_api/Note.html'>nota</a> e suas <a href='https://day.js.org/docs/en/display/format'>propriedades dateCreatedObj / utcDateCreatedObj</a> para detalhes.",
"evaluated_as_js_string": "O valor digitado é avaliado como string JavaScript e, portanto, pode ser enriquecido com conteúdo dinâmico através da variável injetada <code>note</code> (nota sendo renomeada). Exemplos:"
},
"calendar": {
"mon": "Seg",
"tue": "Ter",
"wed": "Qua",
"thu": "Qui",
"fri": "Sex",
"sat": "Sáb",
"sun": "Dom",
"cannot_find_day_note": "Nota do dia não encontrada",
"cannot_find_week_note": "Nota semanal não encontrada",
"january": "Janeiro",
"febuary": "Fevereiro",
"march": "Março",
"april": "Abril",
"may": "Maio",
"june": "Junho",
"july": "Julho",
"august": "Agosto",
"september": "Setembro",
"october": "Outubro",
"november": "Novembro",
"december": "Dezembro"
},
"close_pane_button": {
"close_this_pane": "Fechar este painel"
},
"create_pane_button": {
"create_new_split": "Criar nova divisão"
},
"edit_button": {
"edit_this_note": "Editar esta nota"
},
"show_toc_widget_button": {
"show_toc": "Mostrar Tabela de Conteúdo"
},
"show_highlights_list_widget_button": {
"show_highlights_list": "Mostrar Lista de Destaques"
},
"global_menu": {
"menu": "Menu",
"options": "Opções",
"open_new_window": "Abrir Nova Janela",
"switch_to_mobile_version": "Alternar para Versão Mobile",
"switch_to_desktop_version": "Alternar para Versão Desktop",
"zoom": "Zoom",
"toggle_fullscreen": "Alternar Tela Cheia",
"zoom_out": "Reduzir",
"reset_zoom_level": "Redefinir Zoom",
"zoom_in": "Aumentar",
"configure_launchbar": "Configurar Barra de Lançamento",
"show_shared_notes_subtree": "Exibir Subárvore de Notas Compartilhadas",
"advanced": "Avançado",
"open_dev_tools": "Abrir Ferramentas de Desenvolvedor",
"open_sql_console": "Abrir Console SQL",
"open_sql_console_history": "Abrir Histórico de Console SQL",
"open_search_history": "Abrir Histórico de Busca",
"show_backend_log": "Abrir Log do Servidor",
"reload_frontend": "Recarregar Frontend",
"show_hidden_subtree": "Exibir Subárvore Oculta",
"show_help": "Exibir Ajuda",
"about": "Sobre o Trilium Notes",
"logout": "Sair",
"show-cheatsheet": "Exibir Cheatsheet",
"toggle-zen-mode": "Modo Zen",
"reload_hint": "Recarregar pode ajudar com alguns problemas visuais sem reiniciar toda a aplicação."
},
"zen_mode": {
"button_exit": "Sair do Modo Zen"
},
"sync_status": {
"in_progress": "Sincronização com o servidor em andamento.",
"unknown": "<p>O estado da sincronização será conhecido assim que a próxima tentativa começar.</p><p>Clique para iniciar a sincronização agora.</p>",
"connected_with_changes": "<p>Conectado ao servidor de sincronização.<br>Existem algumas alterações esperando para serem sincronizadas.</p><p>Clique para sincronizar.</p>",
"connected_no_changes": "<p>Conectado ao servidor de sincronização.<br>Todas as alterações já foram sincronizadas.</p><p>Clique para sincronizar.</p>",
"disconnected_with_changes": "<p>A conexão ao servidor de sincronização falhou.<br>Existem algumas alterações esperando para serem sincronizadas.</p><p>Clique para sincronizar.</p>",
"disconnected_no_changes": "<p>A conexão ao servidor de sincronização falhou.<br>Todas as alterações já foram sincronizadas.</p><p>Clique para sincronizar.</p>"
},
"left_pane_toggle": {
"show_panel": "Exibir painel",
"hide_panel": "Esconder painel"
},
"move_pane_button": {
"move_left": "Mover para a esquerda",
"move_right": "Mover para a direita"
},
"note_actions": {
"convert_into_attachment": "Converter para anexo",
"re_render_note": "Renderizar nota novamente",
"search_in_note": "Buscar na nota",
"note_source": "Código Fonte da nota",
"note_attachments": "Anexos da nota",
"open_note_externally": "Abrir nota externamente",
"open_note_custom": "Abrir nota de forma customizada",
"import_files": "Importar arquivos",
"export_note": "Exportar nota",
"delete_note": "Excluir nota",
"print_note": "Imprimir nota",
"save_revision": "Salvar revisão",
"convert_into_attachment_failed": "A conversão da nota '{{title}}' falhou.",
"convert_into_attachment_successful": "A nota '{{title}}' foi convertida para anexo.",
"print_pdf": "Exportar como PDF…",
"open_note_externally_title": "O arquivo será aberto em uma aplicação externa e monitorado por alterações. Você então poderá enviar a versão modificada de volta para o Trilium.",
"convert_into_attachment_prompt": "Você tem certeza que quer converter a nota '{{title}}' em um anexo da nota pai?"
},
"protected_session_status": {
"inactive": "Clique para entrar na sessão protegida",
"active": "Sessão protegida está ativada. Clique para deixar a sessão protegida."
},
"revisions_button": {
"note_revisions": "Revisões da Nota"
},
"update_available": {
"update_available": "Atualização disponível"
},
"code_buttons": {
"execute_button_title": "Executar script",
"trilium_api_docs_button_title": "Abrir documentação da Trilium API",
"save_to_note_button_title": "Salvar para uma nota",
"opening_api_docs_message": "Abrindo documentação da API…",
"sql_console_saved_message": "Nota do Console SQL foi salva no caminho {{note_path}}"
},
"hide_floating_buttons_button": {
"button_title": "Esconder botões"
},
"show_floating_buttons_button": {
"button_title": "Exibir botões"
},
"svg_export_button": {
"button_title": "Exportar diagrama como SVG"
},
"relation_map_buttons": {
"zoom_in_title": "Aumentar",
"zoom_out_title": "Reduzir",
"create_child_note_title": "Criar nova nota filha e adicione neste mapa de relação",
"reset_pan_zoom_title": "Redefinir pan & zoom para coordenadas e ampliação iniciais"
},
"zpetne_odkazy": {
"backlink": "{{count}} Links Reversos",
"backlinks": "{{count}} Links Reversos",
"relation": "relação"
},
"mobile_detail_menu": {
"insert_child_note": "Inserir nota filha",
"delete_this_note": "Excluir essa nota",
"error_unrecognized_command": "Comando não reconhecido {{command}}",
"error_cannot_get_branch_id": "Não foi possível obter o branchId para o notePath '{{notePath}} '"
},
"note_icon": {
"change_note_icon": "Alterar ícone da nota",
"category": "Categoria:",
"search": "Busca:",
"reset-default": "Redefinir para o ícone padrão"
},
"basic_properties": {
"note_type": "Tipo da nota",
"editable": "Editável",
"basic_properties": "Propriedades Básicas",
"language": "Idioma"
},
"book_properties": {
"view_type": "Tipo de visualização",
"grid": "Grade",
"list": "Lista",
"collapse_all_notes": "Recolher todas as notas",
"expand_all_children": "Expandir todos os filhos",
"collapse": "Recolher",
"expand": "Expandir",
"book_properties": "Propriedades da Coleção",
"invalid_view_type": "Tipo de visualização inválido '{{type}}'",
"calendar": "Calendário",
"table": "Tabela",
"geo-map": "Geo Map",
"board": "Quadro"
},
"edited_notes": {
"no_edited_notes_found": "Ainda não há nenhuma nota editada neste dia…",
"title": "Notas Editadas",
"deleted": "(excluído)"
},
"file_properties": {
"note_id": "ID da Nota",
"original_file_name": "Nome original do arquivo",
"file_type": "Tipo do arquivo",
"file_size": "Tamanho do arquivo",
"download": "Baixar",
"open": "Abrir",
"upload_new_revision": "Enviar nova revisão",
"upload_success": "Uma nova revisão de arquivo foi enviada.",
"upload_failed": "O envio de uma nova revisão de arquivo falhou.",
"title": "Arquivo"
},
"image_properties": {
"original_file_name": "Nome original do arquivo",
"file_type": "Tipo do arquivo",
"file_size": "Tamanho do arquivo",
"download": "Baixar",
"open": "Abrir",
"copy_reference_to_clipboard": "Copiar referência para a área de transferência",
"upload_new_revision": "Enviar nova revisão",
"upload_success": "Uma nova revisão de imagem foi enviado.",
"upload_failed": "O envio de uma nova revisão de imagem falhou: {{message}}",
"title": "Imagem"
},
"inherited_attribute_list": {
"title": "Atributos Herdados",
"no_inherited_attributes": "Nenhum atributo herdado."
},
"note_info_widget": {
"note_id": "ID da Nota",
"created": "Criado",
"modified": "Editado",
"type": "Tipo",
"note_size": "Tamanho da nota",
"calculate": "calcular",
"title": "Informações da nota",
"subtree_size": "(tamanho da subárvore: {{size}} em {{count}} notas)",
"note_size_info": "O tamanho da nota fornece uma estimativa aproximada dos requisitos de armazenamento para esta nota. Leva em conta o conteúdo e o conteúdo de suas revisões de nota."
},
"note_map": {
"open_full": "Expandir completamente",
"collapse": "Recolher para tamanho normal",
"title": "Mapa de Notas",
"fix-nodes": "Fixar nós",
"link-distance": "Distância do Link"
},
"note_paths": {
"title": "Caminho das Notas",
"clone_button": "Clonar nota para novo local…",
"intro_placed": "Esta nova está localizada nos caminhos:",
"intro_not_placed": "Esta nota ainda não está em nenhuma árvore de notas.",
"archived": "Arquivado",
"search": "Pesquisar",
"outside_hoisted": "Este caminho está fora de uma nota fixada e você teria que desafixar."
},
"note_properties": {
"this_note_was_originally_taken_from": "Esta nota foi originalmente obtida de:",
"info": "Informações"
},
"promoted_attributes": {
"promoted_attributes": "Atributos Promovidos",
"unset-field-placeholder": "não atribuído",
"open_external_link": "Abrir link externo",
"unknown_label_type": "Tipo de etiqueta desconhecido '{{type}}'",
"unknown_attribute_type": "Tipo de atributo desconhecido '{{type}}'",
"add_new_attribute": "Adicionar novo atributo",
"remove_this_attribute": "Remover este atributo",
"remove_color": "Remover a etiqueta de cor",
"url_placeholder": "http://website..."
},
"script_executor": {
"query": "Consulta",
"script": "Script",
"execute_query": "Executar Consulta",
"execute_script": "Executar Script"
},
"search_definition": {
"add_search_option": "Adicionar opção de pesquisa:",
"search_string": "pesquisa de texto",
"search_script": "pesquisa de script",
"ancestor": "ancestral",
"fast_search": "pesquisa rápida",
"include_archived": "incluir arquivados",
"order_by": "ordenar por",
"limit": "limite",
"limit_description": "Limitar número de resultados",
"debug": "depurar",
"action": "ação",
"search_button": "Pesquisar <kbd>enter</kbd>",
"search_execute": "Pesquisar & Executar ações",
"save_to_note": "Salvar para nota",
"search_parameters": "Parâmetros de Pesquisa",
"unknown_search_option": "Opção de pesquisa desconhecida {{searchOptionName}}",
"actions_executed": "As ações foram executadas.",
"search_note_saved": "Nota de pesquisa foi salva em {{- notePathTitle}}",
"fast_search_description": "A opção de pesquisa rápida desabilita a pesquisa de texto completo do conteúdo de nota, o que pode acelerar a pesquisa em grandes bancos de dados.",
"include_archived_notes_description": "As notas arquivadas são por padrão excluídas dos resultados da pesquisa, com esta opção elas serão incluídas.",
"debug_description": "A depuração irá imprimir informações adicionais no console para ajudar na depuração de consultas complexas"
},
"similar_notes": {
"title": "Notas Similares",
"no_similar_notes_found": "Nenhum nota similar encontrada."
},
"abstract_search_option": {
"remove_this_search_option": "Remover esta opção de pesquisa",
"failed_rendering": "A renderização da opção de busca falhou: {{dto}} com o erro: {{error}} {{stack}}"
},
"debug": {
"debug": "Depurar",
"debug_info": "A depuração irá imprimir informações adicionais no console para ajudar em depuração de consultas complexas.",
"access_info": "Para acessar as informações de depuração, execute a consulta e clique em \"Exibir log do servidor\" no canto superior esquerdo."
},
"fast_search": {
"fast_search": "Pesquisa rápida",
"description": "A opção de pesquisa rápida desabilita a pesquisa de texto completo do conteúdo de nota, o que pode acelerar a pesquisa em grandes bancos de dados."
},
"include_archived_notes": {
"include_archived_notes": "Incluir notas arquivadas"
},
"limit": {
"limit": "Limite",
"take_first_x_results": "Pegar apenas os X primeiros resultados."
},
"order_by": {
"order_by": "Ordenar por",
"relevancy": "Relevância (padrão)",
"title": "Título",
"date_created": "Data de criação",
"date_modified": "Data da última modificação",
"content_size": "Tamaho do conteúdo da nota",
"content_and_attachments_size": "Tamanho do conteúdo da nota incluindo anexos",
"content_and_attachments_and_revisions_size": "Tamanho do conteúdo da nota incluindo anexos e revisões",
"revision_count": "Número de revisões",
"children_count": "Número de notas filhas",
"parent_count": "Número de clones",
"owned_label_count": "Número de etiquetas",
"owned_relation_count": "Número de relações",
"target_relation_count": "Número de relações para esta nota",
"random": "Ordem aleatória",
"asc": "Crescente (padrão)",
"desc": "Decrescente"
},
"search_script": {
"title": "Buscar script:",
"placeholder": "buscar notas pelo nome",
"example_title": "Veja este exemplo:",
"example_code": "// 1. pré-filtro usando pesquisa padrão\nconst candidateNotes = api.searchForNotes(\"#journal\"); \n\n// 2. aplicando critérios de pesquisa customizados\nconst matchedNotes = candidateNotes\n .filter(note => note.title.match(/[0-9]{1,2}\\. ?[0-9]{1,2}\\. ?[0-9]{4}/));\n\nreturn matchedNotes;",
"description1": "O script de pesquisa permite definir os resultados da pesquisa executando um script. Isso proporciona flexibilidade máxima quando a busca padrão não é suficiente.",
"description2": "O script de pesquisa deve ser do tipo \"código\" e subtipo \"JavaScript no servidor\". O script precisa retornar um array de noteIds ou de notas.",
"note": "Note que o script de pesquisa e a pesquisa de texto não podem ser combinados entre si."
},
"search_string": {
"title_column": "Buscar texto:",
"search_syntax": "Sintaxe de pesquisa",
"also_see": "veja também",
"full_text_search": "Digite qualquer texto para busca por texto completo",
"label_abc": "retorna notas com a etiqueta abc",
"label_year": "corresponde notas com a etiqueta de ano 2019",
"label_rock_pop": "corresponde notas que tenham tanto a etiqueta rock quando pop",
"label_rock_or_pop": "apenas uma das etiquetas deve estar presente",
"label_year_comparison": "comparação numérica (também >, >=, <).",
"label_date_created": "notas criadas no último mês",
"error": "Erro na busca: {{error}}",
"search_prefix": "Busca:",
"placeholder": "palavras-chave fulltext, #tag = valor..."
},
"attachment_list": {
"open_help_page": "Abrir página de ajuda nos anexos",
"upload_attachments": "Enviar anexos",
"no_attachments": "Esta nota não possuí anexos."
},
"editable_code": {
"placeholder": "Digite o conteúdo da sua nota de código aqui…"
},
"editable_text": {
"placeholder": "Digite o conteúdo da sua nota aqui…"
},
"empty": {
"search_placeholder": "buscar uma nota pelo nome",
"enter_workspace": "Entrar no workspace {{title}}"
},
"file": {
"file_preview_not_available": "Prévia não disponível para este formato de arquivo."
},
"protected_session": {
"enter_password_instruction": "É necessário digitar sua senha para mostar notas protegidas:",
"started": "A sessão protegida foi iniciada.",
"wrong_password": "Senha incorreta.",
"protecting-finished-successfully": "A proteção foi finalizada com sucesso.",
"unprotecting-finished-successfully": "A remoção da proteção foi finalizada com sucesso.",
"protecting-in-progress": "Proteções em andamento: {{count}}",
"unprotecting-in-progress-count": "Remoções de proteção em andamento: {{count}}",
"protecting-title": "Estado da proteção",
"unprotecting-title": "Estado da remoção de proteção"
},
"relation_map": {
"open_in_new_tab": "Abrir em nova aba",
"remove_note": "Remover nota",
"edit_title": "Editar título",
"rename_note": "Renomear nota",
"enter_new_title": "Digite o novo título da nota:",
"remove_relation": "Remover relação",
"confirm_remove_relation": "Tem certeza que deseja remover esta relação?",
"connection_exists": "A conexão '{{name}}' já existe entre estas notas.",
"note_not_found": "Nota {{noteId}} não encontrada!",
"note_already_in_diagram": "A nota \"{{title}}\" já está no diagrama.",
"enter_title_of_new_note": "Digite o título da nova nota",
"default_new_note_title": "nova nota",
"click_on_canvas_to_place_new_note": "Clique no quadro para incluir uma nova nota"
},
"web_view": {
"web_view": "Web View"
},
"backend_log": {
"refresh": "Recarregar"
},
"consistency_checks": {
"title": "Chegagem de Consistência",
"find_and_fix_button": "Encontrar e corrigir problemas de consistência",
"finding_and_fixing_message": "Buscando e corrigindo problemas de consistência…",
"issues_fixed_message": "Qualquer problema de consistência encontrado foi corrigido."
},
"database_integrity_check": {
"check_button": "Verificar integridade do banco de dados",
"checking_integrity": "Verificando integridade do banco de dados…",
"integrity_check_succeeded": "Verificação de integridade bem sucedida - nenhum problema encontrado.",
"integrity_check_failed": "Verificação de integridade falhou: {{results}}"
},
"sync": {
"title": "Sincronizar",
"force_full_sync_button": "Forçar sincronização completa",
"full_sync_triggered": "Sincronização completa iniciada",
"finished-successfully": "Sincronização finalizada com sucesso.",
"failed": "Sincronização falhou: {{message}}"
},
"vacuum_database": {
"description": "Isso irá reconstruir o banco de dados, o que normalmente irá resultar em uma redução do arquivo do banco de dados. Nenhum dado será alterado."
},
"fonts": {
"theme_defined": "Tema definido",
"fonts": "Fontes",
"main_font": "Fonte Principal",
"font_family": "Família da fonte",
"size": "Tamanho",
"note_tree_font": "Fonte da Árvore de Notas",
"note_detail_font": "Fonte Padrão da Nota",
"monospace_font": "Fonte Monospace (código)",
"not_all_fonts_available": "Nem todas as fontes listadas podem estar disponíveis em seu sistema.",
"apply_font_changes": "Para aplicar as alterações de fonte, clique em",
"reload_frontend": "recarregar frontend",
"generic-fonts": "Fontes genéricas",
"sans-serif-system-fonts": "Fontes sem serifa de sistema",
"serif-system-fonts": "Fontes serifadas de sistema",
"monospace-system-fonts": "Fontes monospace de sistema",
"handwriting-system-fonts": "Fontes de escrita à mão de sistema",
"serif": "Serifa",
"sans-serif": "Sem Serifa",
"monospace": "Monospace",
"system-default": "Padrão do Sistema"
},
"max_content_width": {
"title": "Largura do Conteúdo",
"max_width_label": "Largura máxima do conteúdo",
"max_width_unit": "pixels",
"apply_changes_description": "Para aplicar as alterações de largura do conteúdo, clique em",
"reload_button": "recarregar frontend",
"reload_description": "alterações de opções de aparência"
},
"native_title_bar": {
"title": "Barra de Título Nativa (requer recarregar o app)",
"enabled": "ativada",
"disabled": "desativada"
},
"theme": {
"title": "Tema da Aplicação",
"theme_label": "Tema",
"override_theme_fonts_label": "Sobrepor fontes do tema",
"auto_theme": "Legado (Seguir esquema de cor do sistema)",
"light_theme": "Legado (Claro)",
"dark_theme": "Legado (Escuro)",
"triliumnext": "Trilium (Seguir esquema de cor do sistema)",
"triliumnext-light": "Trilium (Claro)",
"triliumnext-dark": "Trilium (Escuro)",
"layout": "Layout",
"layout-vertical-title": "Vertical",
"layout-horizontal-title": "Horizontal",
"layout-vertical-description": "barra de lançamento está a esquerda (padrão)",
"layout-horizontal-description": "barra de lançamento está abaixo da barra de abas, a barra de abas agora tem a largura total."
},
"note_launcher": {
"this_launcher_doesnt_define_target_note": "Este lançador não define uma nota destino."
},
"copy_image_reference_button": {
"button_title": "Copiar referência da imagem para a área de transferência, pode ser colado em uma nota de texto."
},
"onclick_button": {
"no_click_handler": "Componente de botão '{{componentId}}' não possui manipulador de clique definido"
},
"owned_attribute_list": {
"owned_attributes": "Atributos próprios"
} }
} }

View File

@@ -40,7 +40,7 @@
"add_relation": { "add_relation": {
"add_relation": "Adaugă relație", "add_relation": "Adaugă relație",
"allowed_characters": "Sunt permise doar caractere alfanumerice, underline și două puncte.", "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", "relation_name": "denumirea relației",
"target_note": "notița destinație", "target_note": "notița destinație",
"to": "către" "to": "către"
@@ -76,9 +76,9 @@
"attachment_erasure_timeout": { "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_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", "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", "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." "unused_attachments_erased": "Atașamentele nefolosite au fost șterse."
}, },
"attachment_list": { "attachment_list": {
@@ -141,7 +141,7 @@
"hide_promoted_attributes": "Ascunde lista atributelor promovate pentru această notiță", "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.", "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.", "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.", "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": "Moștenibilă",
"inheritable_title": "Atributele moștenibile vor fi moștenite de către toți descendenții acestei notițe.", "inheritable_title": "Atributele moștenibile vor fi moștenite de către toți descendenții acestei 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_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_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_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_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_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ă.", "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ă.", "target_note_title": "Relația este o conexiune numită dintre o notiță sursă și o notiță țintă.",
"template": "Șablon", "template": "Șablon",
"text": "Text", "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", "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)", "top": "păstrează notița la începutul listei (se aplică doar pentru notițe sortate automat)",
"url": "URL", "url": "URL",
@@ -519,8 +519,8 @@
"export_status": "Starea exportului", "export_status": "Starea exportului",
"export_type_single": "Doar această notiță fără descendenții ei", "export_type_single": "Doar această notiță fără descendenții ei",
"export_type_subtree": "Această notiță și toți 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_html_zip": "HTML în arhivă ZIP - recomandat deoarece păstrează toată formatarea.",
"format_markdown": "Markdown - păstrează majoritatea formatării", "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.", "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_1": "OPML v1.0 - text simplu",
"opml_version_2": "OPML v2.0 - permite și HTML", "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", "newTabNoteLink": "pe o legătură către o notiță va deschide notița într-un tab nou",
"notSet": "nesetat", "notSet": "nesetat",
"noteNavigation": "Navigarea printre notițe", "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)", "onlyInDesktop": "Doar pentru desktop (aplicația Electron)",
"openEmptyTab": "deschide un tab nou", "openEmptyTab": "deschide un tab nou",
"other": "Altele", "other": "Altele",
@@ -655,7 +655,8 @@
"showSQLConsole": "afișează consola SQL", "showSQLConsole": "afișează consola SQL",
"tabShortcuts": "Scurtături pentru tab-uri", "tabShortcuts": "Scurtături pentru tab-uri",
"troubleshooting": "Unelte pentru depanare", "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": { "hide_floating_buttons_button": {
"button_title": "Ascunde butoanele" "button_title": "Ascunde butoanele"
@@ -886,7 +887,8 @@
"modal_title": "Selectați tipul notiței", "modal_title": "Selectați tipul notiței",
"templates": "Șabloane", "templates": "Șabloane",
"change_path_prompt": "Selectați locul unde să se creeze noua notiță:", "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": { "onclick_button": {
"no_click_handler": "Butonul „{{componentId}}” nu are nicio acțiune la clic definită" "no_click_handler": "Butonul „{{componentId}}” nu are nicio acțiune la clic definită"
@@ -940,7 +942,9 @@
}, },
"password_not_set": { "password_not_set": {
"body1": "Notițele protejate sunt criptate utilizând parola de utilizator, dar nu a fost setată nicio parolă.", "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": { "promoted_attributes": {
"add_new_attribute": "Adaugă un nou atribut", "add_new_attribute": "Adaugă un nou atribut",
@@ -1072,7 +1076,7 @@
"note_revisions": "Revizii ale notiței" "note_revisions": "Revizii ale notiței"
}, },
"revisions_snapshot_interval": { "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", "note_revisions_snapshot_interval_title": "Intervalul de salvare a reviziilor",
"snapshot_time_interval_label": "Intervalul de salvare a reviziilor:" "snapshot_time_interval_label": "Intervalul de salvare a reviziilor:"
}, },
@@ -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-description": "bara de lansare se află sub bara de taburi, bara de taburi este pe toată lungimea.",
"layout-horizontal-title": "Orizontal", "layout-horizontal-title": "Orizontal",
"layout-vertical-title": "Vertical", "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": { "toast": {
"critical-error": { "critical-error": {
@@ -1279,7 +1286,7 @@
"update_relation_target": { "update_relation_target": {
"allowed_characters": "Sunt permise doar caractere alfanumerice, underline și două puncte.", "allowed_characters": "Sunt permise doar caractere alfanumerice, underline și două puncte.",
"change_target_note": "schimbă notița-țintă a unei relații existente", "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", "relation_name": "denumirea relației",
"target_note": "notița destinație", "target_note": "notița destinație",
"to": "la", "to": "la",
@@ -1856,15 +1863,20 @@
}, },
"create_new_ai_chat": "Crează o nouă discuție cu AI-ul", "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.", "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": "Furnizor selectat",
"selected_provider_description": "Selectați furnizorul de AI pentru funcțiile de discuție și completare", "selected_provider_description": "Selectați furnizorul de AI pentru funcțiile de discuție și completare",
"select_model": "Selectați modelul...", "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": { "custom_date_time_format": {
"title": "Format dată/timp personalizat", "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:", "format_string": "Șir de formatare:",
"formatted_time": "Data și ora formatate:" "formatted_time": "Data și ora formatate:"
}, },
@@ -1985,6 +1997,32 @@
"open_externally": "Deschide în afara programului" "open_externally": "Deschide în afara programului"
}, },
"modal": { "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

@@ -65,5 +65,16 @@
}, },
"rename_note": { "rename_note": {
"rename_note": "Đổi tên ghi chép" "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

@@ -414,7 +414,7 @@ export default class GlobalMenuWidget extends BasicWidget {
} }
async fetchLatestVersion() { 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 resp = await fetch(RELEASES_API_URL);
const data = await resp.json(); const data = await resp.json();

View File

@@ -1,6 +1,8 @@
import utils from "../../services/utils.js"; import { EventData } from "../../components/app_context.js";
import type BasicWidget from "../basic_widget.js";
import FlexContainer from "./flex_container.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. * 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()); window.visualViewport?.addEventListener("resize", () => this.#onMobileResize());
} }
this.#setMotion(options.is("motionEnabled"));
this.#setShadows(options.is("shadowsEnabled"));
this.#setBackdropEffects(options.is("backdropEffectsEnabled"));
return super.render(); 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() { #onMobileResize() {
const currentViewportHeight = getViewportHeight(); const currentViewportHeight = getViewportHeight();
const isKeyboardOpened = (currentViewportHeight < this.originalViewportHeight); const isKeyboardOpened = (currentViewportHeight < this.originalViewportHeight);
this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened); 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() { 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 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 BasicWidget from "../basic_widget.js";
import type NoteContext from "../../components/note_context.js"; import type NoteContext from "../../components/note_context.js";
import Component from "../../components/component.js";
interface NoteContextEvent { interface NoteContextEvent {
noteContext: NoteContext; noteContext: NoteContext;
@@ -152,6 +153,8 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
for (const ntxId of ntxIds) { for (const ntxId of ntxIds) {
this.$widget.find(`[data-ntx-id="${ntxId}"]`).remove(); this.$widget.find(`[data-ntx-id="${ntxId}"]`).remove();
const widget = this.widgets[ntxId];
recursiveCleanup(widget);
delete this.widgets[ntxId]; delete this.widgets[ntxId];
} }
} }
@@ -237,3 +240,12 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
return Promise.all(promises); 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

@@ -77,7 +77,7 @@ function DirectoryLink({ directory, style }: { directory: string, style?: CSSPro
openService.openDirectory(directory); 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 { } else {
return <span style={style}>{directory}</span>; return <span style={style}>{directory}</span>;
} }

View File

@@ -30,6 +30,14 @@ function AddLinkDialogComponent() {
setShown(true); setShown(true);
}); });
useEffect(() => {
if (hasSelection) {
setLinkType("hyper-link");
} else {
setLinkType("reference-link");
}
}, [ hasSelection ])
async function setDefaultLinkTitle(noteId: string) { async function setDefaultLinkTitle(noteId: string) {
const noteTitle = await tree.getNoteTitle(noteId); const noteTitle = await tree.getNoteTitle(noteId);
setLinkTitle(noteTitle); setLinkTitle(noteTitle);
@@ -107,7 +115,7 @@ function AddLinkDialogComponent() {
}} }}
show={shown} show={shown}
> >
<FormGroup label={t("add_link.note")}> <FormGroup label={t("add_link.note")} name="note">
<NoteAutocomplete <NoteAutocomplete
inputRef={autocompleteRef} inputRef={autocompleteRef}
onChange={setSuggestion} onChange={setSuggestion}

View File

@@ -64,7 +64,7 @@ function BranchPrefixDialogComponent() {
footer={<Button text={t("branch_prefix.save")} />} footer={<Button text={t("branch_prefix.save")} />}
show={shown} show={shown}
> >
<FormGroup label={t("branch_prefix.prefix")}> <FormGroup label={t("branch_prefix.prefix")} name="prefix">
<div class="input-group"> <div class="input-group">
<input class="branch-prefix-input form-control" value={prefix} ref={branchInput} <input class="branch-prefix-input form-control" value={prefix} ref={branchInput}
onChange={(e) => setPrefix((e.target as HTMLInputElement).value)} /> onChange={(e) => setPrefix((e.target as HTMLInputElement).value)} />

View File

@@ -94,7 +94,8 @@ function AvailableActionsList() {
<td>{ actionGroup.title }:</td> <td>{ actionGroup.title }:</td>
{actionGroup.actions.map(({ actionName, actionTitle }) => {actionGroup.actions.map(({ actionName, actionTitle }) =>
<Button <Button
small text={actionTitle} size="small"
text={actionTitle}
onClick={() => bulk_action.addAction("_bulkAction", actionName)} onClick={() => bulk_action.addAction("_bulkAction", actionName)}
/> />
)} )}

View File

@@ -3,6 +3,7 @@ import Button from "../react/Button";
import Modal from "../react/Modal"; import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget"; import ReactBasicWidget from "../react/ReactBasicWidget";
import { CallToAction, dismissCallToAction, getCallToActions } from "./call_to_action_definitions"; import { CallToAction, dismissCallToAction, getCallToActions } from "./call_to_action_definitions";
import { t } from "../../services/i18n";
function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActions: CallToAction[] }) { function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActions: CallToAction[] }) {
if (!activeCallToActions.length) { if (!activeCallToActions.length) {
@@ -25,12 +26,12 @@ function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActi
<Modal <Modal
className="call-to-action" className="call-to-action"
size="md" size="md"
title="New features" title={activeItem.title}
show={shown} show={shown}
onHidden={() => setShown(false)} onHidden={() => setShown(false)}
footerAlignment="between" footerAlignment="between"
footer={<> footer={<>
<Button text="Dismiss" onClick={async () => { <Button text={t("call_to_action.dismiss")} onClick={async () => {
await dismissCallToAction(activeItem.id); await dismissCallToAction(activeItem.id);
goToNext(); goToNext();
}} /> }} />
@@ -43,7 +44,6 @@ function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActi
)} )}
</>} </>}
> >
<h4>{activeItem.title}</h4>
<p>{activeItem.message}</p> <p>{activeItem.message}</p>
</Modal> </Modal>
) )

View File

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

View File

@@ -69,15 +69,15 @@ function CloneToDialogComponent() {
> >
<h5>{t("clone_to.notes_to_clone")}</h5> <h5>{t("clone_to.notes_to_clone")}</h5>
<NoteList style={{ maxHeight: "200px", overflow: "auto" }} noteIds={clonedNoteIds} /> <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 <NoteAutocomplete
placeholder={t("clone_to.search_for_note_by_its_name")} placeholder={t("clone_to.search_for_note_by_its_name")}
onChange={setSuggestion} onChange={setSuggestion}
inputRef={autoCompleteRef} inputRef={autoCompleteRef}
/> />
</FormGroup> </FormGroup>
<FormGroup label={t("clone_to.prefix_optional")} title={t("clone_to.cloned_note_prefix_title")}> <FormGroup name="clone-prefix" label={t("clone_to.prefix_optional")} title={t("clone_to.cloned_note_prefix_title")}>
<FormTextBox name="clone-prefix" onChange={setPrefix} /> <FormTextBox onChange={setPrefix} />
</FormGroup> </FormGroup>
</Modal> </Modal>
) )

View File

@@ -4,7 +4,7 @@ import tree from "../../services/tree";
import Button from "../react/Button"; import Button from "../react/Button";
import FormCheckbox from "../react/FormCheckbox"; import FormCheckbox from "../react/FormCheckbox";
import FormFileUpload from "../react/FormFileUpload"; import FormFileUpload from "../react/FormFileUpload";
import FormGroup from "../react/FormGroup"; import FormGroup, { FormMultiGroup } from "../react/FormGroup";
import Modal from "../react/Modal"; import Modal from "../react/Modal";
import RawHtml from "../react/RawHtml"; import RawHtml from "../react/RawHtml";
import ReactBasicWidget from "../react/ReactBasicWidget"; import ReactBasicWidget from "../react/ReactBasicWidget";
@@ -55,11 +55,11 @@ function ImportDialogComponent() {
footer={<Button text={t("import.import")} primary disabled={!files} />} footer={<Button text={t("import.import")} primary disabled={!files} />}
show={shown} 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} /> <FormFileUpload multiple onChange={setFiles} />
</FormGroup> </FormGroup>
<FormGroup label={t("import.options")}> <FormMultiGroup label={t("import.options")}>
<FormCheckbox <FormCheckbox
name="safe-import" hint={t("import.safeImportTooltip")} label={t("import.safeImport")} name="safe-import" hint={t("import.safeImportTooltip")} label={t("import.safeImport")}
currentValue={safeImport} onChange={setSafeImport} currentValue={safeImport} onChange={setSafeImport}
@@ -84,7 +84,7 @@ function ImportDialogComponent() {
name="replace-underscores-with-spaces" label={t("import.replaceUnderscoresWithSpaces")} name="replace-underscores-with-spaces" label={t("import.replaceUnderscoresWithSpaces")}
currentValue={replaceUnderscoresWithSpaces} onChange={setReplaceUnderscoresWithSpaces} currentValue={replaceUnderscoresWithSpaces} onChange={setReplaceUnderscoresWithSpaces}
/> />
</FormGroup> </FormMultiGroup>
</Modal> </Modal>
); );
} }

View File

@@ -43,7 +43,7 @@ function IncludeNoteDialogComponent() {
footer={<Button text={t("include_note.button_include")} keyboardShortcut="Enter" />} footer={<Button text={t("include_note.button_include")} keyboardShortcut="Enter" />}
show={shown} show={shown}
> >
<FormGroup label={t("include_note.label_note")}> <FormGroup name="note" label={t("include_note.label_note")}>
<NoteAutocomplete <NoteAutocomplete
placeholder={t("include_note.placeholder_search")} placeholder={t("include_note.placeholder_search")}
onChange={setSuggestion} onChange={setSuggestion}
@@ -55,8 +55,9 @@ function IncludeNoteDialogComponent() {
/> />
</FormGroup> </FormGroup>
<FormGroup label={t("include_note.box_size_prompt")}> <FormGroup name="include-note-box-size" label={t("include_note.box_size_prompt")}>
<FormRadioGroup name="include-note-box-size" <FormRadioGroup
name="include-note-box-size"
currentValue={boxSize} onChange={setBoxSize} currentValue={boxSize} onChange={setBoxSize}
values={[ values={[
{ label: t("include_note.box_size_small"), value: "small" }, { label: t("include_note.box_size_small"), value: "small" },

View File

@@ -9,6 +9,7 @@ import appContext from "../../components/app_context";
import commandRegistry from "../../services/command_registry"; import commandRegistry from "../../services/command_registry";
import { refToJQuerySelector } from "../react/react_utils"; 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; const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
@@ -83,6 +84,27 @@ function JumpToNoteDialogComponent() {
$autoComplete $autoComplete
.trigger("focus") .trigger("focus")
.trigger("select"); .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 ( return (
@@ -108,7 +130,12 @@ function JumpToNoteDialogComponent() {
/>} />}
onShown={onShown} onShown={onShown}
onHidden={() => setShown(false)} 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} show={shown}
> >
<div className="algolia-autocomplete-container jump-to-note-results" ref={containerRef}></div> <div className="algolia-autocomplete-container jump-to-note-results" ref={containerRef}></div>

View File

@@ -57,7 +57,7 @@ function MoveToDialogComponent() {
<h5>{t("move_to.notes_to_move")}</h5> <h5>{t("move_to.notes_to_move")}</h5>
<NoteList branchIds={movedBranchIds} /> <NoteList branchIds={movedBranchIds} />
<FormGroup label={t("move_to.target_parent_note")}> <FormGroup name="parent-note" label={t("move_to.target_parent_note")}>
<NoteAutocomplete <NoteAutocomplete
onChange={setSuggestion} onChange={setSuggestion}
inputRef={autoCompleteRef} inputRef={autoCompleteRef}

View File

@@ -83,7 +83,7 @@ function NoteTypeChooserDialogComponent() {
show={shown} show={shown}
stackable stackable
> >
<FormGroup label={t("note_type_chooser.change_path_prompt")}> <FormGroup name="parent-note" label={t("note_type_chooser.change_path_prompt")}>
<NoteAutocomplete <NoteAutocomplete
onChange={setParentNote} onChange={setParentNote}
placeholder={t("note_type_chooser.search_placeholder")} placeholder={t("note_type_chooser.search_placeholder")}
@@ -95,7 +95,7 @@ function NoteTypeChooserDialogComponent() {
/> />
</FormGroup> </FormGroup>
<FormGroup label={t("note_type_chooser.modal_body")}> <FormGroup name="note-type" label={t("note_type_chooser.modal_body")}>
<FormList onSelect={onNoteTypeSelected}> <FormList onSelect={onNoteTypeSelected}>
{noteTypes.map((_item) => { {noteTypes.map((_item) => {
if (_item.title === "----") { if (_item.title === "----") {

View File

@@ -25,6 +25,7 @@ export interface PromptDialogOptions {
defaultValue?: string; defaultValue?: string;
shown?: PromptShownDialogCallback; shown?: PromptShownDialogCallback;
callback?: (value: string | null) => void; callback?: (value: string | null) => void;
readOnly?: boolean;
} }
function PromptDialogComponent() { function PromptDialogComponent() {
@@ -32,24 +33,26 @@ function PromptDialogComponent() {
const formRef = useRef<HTMLFormElement>(null); const formRef = useRef<HTMLFormElement>(null);
const labelRef = useRef<HTMLLabelElement>(null); const labelRef = useRef<HTMLLabelElement>(null);
const answerRef = useRef<HTMLInputElement>(null); const answerRef = useRef<HTMLInputElement>(null);
const [ opts, setOpts ] = useState<PromptDialogOptions>(); const opts = useRef<PromptDialogOptions>();
const [ value, setValue ] = useState(""); const [ value, setValue ] = useState("");
const [ shown, setShown ] = useState(false); const [ shown, setShown ] = useState(false);
const submitValue = useRef<string>(null);
useTriliumEvent("showPromptDialog", (opts) => { useTriliumEvent("showPromptDialog", (newOpts) => {
setOpts(opts); opts.current = newOpts;
setValue(newOpts.defaultValue ?? "");
setShown(true); setShown(true);
}) })
return ( return (
<Modal <Modal
className="prompt-dialog" className="prompt-dialog"
title={opts?.title ?? t("prompt.title")} title={opts.current?.title ?? t("prompt.title")}
size="lg" size="lg"
zIndex={2000} zIndex={2000}
modalRef={modalRef} formRef={formRef} modalRef={modalRef} formRef={formRef}
onShown={() => { onShown={() => {
opts?.shown?.({ opts.current?.shown?.({
$dialog: refToJQuerySelector(modalRef), $dialog: refToJQuerySelector(modalRef),
$question: refToJQuerySelector(labelRef), $question: refToJQuerySelector(labelRef),
$answer: refToJQuerySelector(answerRef), $answer: refToJQuerySelector(answerRef),
@@ -58,24 +61,25 @@ function PromptDialogComponent() {
answerRef.current?.focus(); answerRef.current?.focus();
}} }}
onSubmit={() => { onSubmit={() => {
const modal = BootstrapModal.getOrCreateInstance(modalRef.current!); submitValue.current = value;
modal.hide(); setShown(false);
opts?.callback?.(value);
}} }}
onHidden={() => { onHidden={() => {
opts?.callback?.(null);
setShown(false); setShown(false);
opts.current?.callback?.(submitValue.current);
submitValue.current = null;
opts.current = undefined;
}} }}
footer={<Button text={t("prompt.ok")} keyboardShortcut="Enter" primary />} footer={<Button text={t("prompt.ok")} keyboardShortcut="Enter" primary />}
show={shown} show={shown}
stackable stackable
> >
<FormGroup label={opts?.message} labelRef={labelRef}> <FormGroup name="prompt-dialog-answer" label={opts.current?.message} labelRef={labelRef}>
<FormTextBox <FormTextBox
name="prompt-dialog-answer"
inputRef={answerRef} inputRef={answerRef}
currentValue={value} onChange={setValue} /> currentValue={value} onChange={setValue}
readOnly={opts.current?.readOnly}
/>
</FormGroup> </FormGroup>
</Modal> </Modal>
); );

View File

@@ -57,7 +57,8 @@ function RecentChangesDialogComponent() {
header={ header={
<Button <Button
text={t("recent_changes.erase_notes_button")} text={t("recent_changes.erase_notes_button")}
small style={{ padding: "0 10px" }} size="small"
style={{ padding: "0 10px" }}
onClick={() => { onClick={() => {
server.post("notes/erase-deleted-notes-now").then(() => { server.post("notes/erase-deleted-notes-now").then(() => {
setNeedsRefresh(true); setNeedsRefresh(true);

View File

@@ -55,7 +55,7 @@ function RevisionsDialogComponent() {
helpPageId="vZWERwf8U3nx" helpPageId="vZWERwf8U3nx"
bodyStyle={{ display: "flex", height: "80vh" }} bodyStyle={{ display: "flex", height: "80vh" }}
header={ 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 () => { onClick={async () => {
const text = t("revisions.confirm_delete_all"); const text = t("revisions.confirm_delete_all");

View File

@@ -83,11 +83,8 @@ function SortChildNotesDialogComponent() {
label={t("sort_child_notes.sort_with_respect_to_different_character_sorting")} label={t("sort_child_notes.sort_with_respect_to_different_character_sorting")}
currentValue={sortNatural} onChange={setSortNatural} 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")}> <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 <FormTextBox currentValue={sortLocale} onChange={setSortLocale} />
name="sort-locale"
currentValue={sortLocale} onChange={setSortLocale}
/>
</FormGroup> </FormGroup>
</Modal> </Modal>
) )

View File

@@ -51,13 +51,12 @@ function UploadAttachmentsDialogComponent() {
onHidden={() => setShown(false)} onHidden={() => setShown(false)}
show={shown} 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 /> <FormFileUpload onChange={setFiles} multiple />
</FormGroup> </FormGroup>
<FormGroup label={t("upload_attachments.options")}> <FormGroup name="shrink-images" label={t("upload_attachments.options")}>
<FormCheckbox <FormCheckbox
name="shrink-images"
hint={t("upload_attachments.tooltip")} label={t("upload_attachments.shrink_images")} hint={t("upload_attachments.tooltip")} label={t("upload_attachments.shrink_images")}
currentValue={shrinkImages} onChange={setShrinkImages} currentValue={shrinkImages} onChange={setShrinkImages}
/> />

View File

@@ -152,7 +152,7 @@ const TPL = /*html*/`
const MAX_SEARCH_RESULTS_IN_TREE = 100; const MAX_SEARCH_RESULTS_IN_TREE = 100;
// this has to be hanged on the actual elements to effectively intercept and stop click event // 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 // TODO: Fix once we remove Node.js API from public
type Timeout = NodeJS.Timeout | string | number | undefined; type Timeout = NodeJS.Timeout | string | number | undefined;

View File

@@ -93,6 +93,8 @@ interface QuickSearchResponse {
highlightedNotePathTitle: string; highlightedNotePathTitle: string;
contentSnippet?: string; contentSnippet?: string;
highlightedContentSnippet?: string; highlightedContentSnippet?: string;
attributeSnippet?: string;
highlightedAttributeSnippet?: string;
icon: string; icon: string;
}>; }>;
error: string; error: string;
@@ -241,7 +243,12 @@ export default class QuickSearchWidget extends BasicWidget {
<span style="flex: 1;" class="search-result-title">${result.highlightedNotePathTitle}</span> <span style="flex: 1;" class="search-result-title">${result.highlightedNotePathTitle}</span>
</div>`; </div>`;
// Add content snippet below the title if available // Add attribute snippet (tags/attributes) below the title if available
if (result.highlightedAttributeSnippet) {
itemHtml += `<div style="font-size: 0.75em; color: var(--muted-text-color); opacity: 0.5; margin-left: 20px; margin-top: 2px; line-height: 1.2;" class="search-result-attributes">${result.highlightedAttributeSnippet}</div>`;
}
// Add content snippet below the attributes if available
if (result.highlightedContentSnippet) { if (result.highlightedContentSnippet) {
itemHtml += `<div style="font-size: 0.85em; color: var(--main-text-color); opacity: 0.7; margin-left: 20px; margin-top: 4px; line-height: 1.3;" class="search-result-content">${result.highlightedContentSnippet}</div>`; itemHtml += `<div style="font-size: 0.85em; color: var(--main-text-color); opacity: 0.7; margin-left: 20px; margin-top: 4px; line-height: 1.3;" class="search-result-content">${result.highlightedContentSnippet}</div>`;
} }

View File

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

View File

@@ -1,7 +1,7 @@
import { ComponentChildren } from "preact"; import { ComponentChildren } from "preact";
interface AlertProps { interface AlertProps {
type: "info" | "danger"; type: "info" | "danger" | "warning";
title?: string; title?: string;
children: ComponentChildren; children: ComponentChildren;
} }

View File

@@ -4,6 +4,7 @@ import { useRef, useMemo } from "preact/hooks";
import { memo } from "preact/compat"; import { memo } from "preact/compat";
interface ButtonProps { interface ButtonProps {
name?: string;
/** Reference to the button element. Mostly useful for requesting focus. */ /** Reference to the button element. Mostly useful for requesting focus. */
buttonRef?: RefObject<HTMLButtonElement>; buttonRef?: RefObject<HTMLButtonElement>;
text: string; text: string;
@@ -14,11 +15,11 @@ interface ButtonProps {
onClick?: () => void; onClick?: () => void;
primary?: boolean; primary?: boolean;
disabled?: boolean; disabled?: boolean;
small?: boolean; size?: "normal" | "small" | "micro";
style?: CSSProperties; style?: CSSProperties;
} }
const Button = memo(({ buttonRef: _buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled, small, style }: ButtonProps) => { const Button = memo(({ name, buttonRef: _buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled, size, style }: ButtonProps) => {
// Memoize classes array to prevent recreation // Memoize classes array to prevent recreation
const classes = useMemo(() => { const classes = useMemo(() => {
const classList: string[] = ["btn"]; const classList: string[] = ["btn"];
@@ -30,11 +31,13 @@ const Button = memo(({ buttonRef: _buttonRef, className, text, onClick, keyboard
if (className) { if (className) {
classList.push(className); classList.push(className);
} }
if (small) { if (size === "small") {
classList.push("btn-sm"); classList.push("btn-sm");
} else if (size === "micro") {
classList.push("btn-micro");
} }
return classList.join(" "); return classList.join(" ");
}, [primary, className, small]); }, [primary, className, size]);
const buttonRef = _buttonRef ?? useRef<HTMLButtonElement>(null); const buttonRef = _buttonRef ?? useRef<HTMLButtonElement>(null);
@@ -52,6 +55,7 @@ const Button = memo(({ buttonRef: _buttonRef, className, text, onClick, keyboard
return ( return (
<button <button
name={name}
className={classes} className={classes}
type={onClick ? "button" : "submit"} type={onClick ? "button" : "submit"}
onClick={onClick} onClick={onClick}

View File

@@ -0,0 +1,17 @@
import type { ComponentChildren } from "preact";
import { CSSProperties } from "preact/compat";
interface ColumnProps {
md?: number;
children: ComponentChildren;
className?: string;
style?: CSSProperties;
}
export default function Column({ md, children, className, style }: ColumnProps) {
return (
<div className={`col-md-${md ?? 6} ${className ?? ""}`} style={style}>
{children}
</div>
)
}

View File

@@ -2,10 +2,12 @@ import { Tooltip } from "bootstrap";
import { useEffect, useRef, useMemo, useCallback } from "preact/hooks"; import { useEffect, useRef, useMemo, useCallback } from "preact/hooks";
import { escapeQuotes } from "../../services/utils"; import { escapeQuotes } from "../../services/utils";
import { ComponentChildren } from "preact"; import { ComponentChildren } from "preact";
import { memo } from "preact/compat"; import { CSSProperties, memo } from "preact/compat";
import { useUniqueName } from "./hooks";
interface FormCheckboxProps { interface FormCheckboxProps {
name: string; id?: string;
name?: string;
label: string | ComponentChildren; label: string | ComponentChildren;
/** /**
* If set, the checkbox label will be underlined and dotted, indicating a hint. When hovered, it will show the hint text. * If set, the checkbox label will be underlined and dotted, indicating a hint. When hovered, it will show the hint text.
@@ -14,9 +16,11 @@ interface FormCheckboxProps {
currentValue: boolean; currentValue: boolean;
disabled?: boolean; disabled?: boolean;
onChange(newValue: boolean): void; onChange(newValue: boolean): void;
containerStyle?: CSSProperties;
} }
const FormCheckbox = memo(({ name, disabled, label, currentValue, onChange, hint }: FormCheckboxProps) => { const FormCheckbox = memo(({ name, id: _id, disabled, label, currentValue, onChange, hint, containerStyle }: FormCheckboxProps) => {
const id = _id ?? useUniqueName(name);
const labelRef = useRef<HTMLLabelElement>(null); const labelRef = useRef<HTMLLabelElement>(null);
// Fix: Move useEffect outside conditional // Fix: Move useEffect outside conditional
@@ -46,7 +50,7 @@ const FormCheckbox = memo(({ name, disabled, label, currentValue, onChange, hint
const titleText = useMemo(() => hint ? escapeQuotes(hint) : undefined, [hint]); const titleText = useMemo(() => hint ? escapeQuotes(hint) : undefined, [hint]);
return ( return (
<div className="form-checkbox"> <div className="form-checkbox" style={containerStyle}>
<label <label
className="form-check-label tn-checkbox" className="form-check-label tn-checkbox"
style={labelStyle} style={labelStyle}
@@ -54,9 +58,10 @@ const FormCheckbox = memo(({ name, disabled, label, currentValue, onChange, hint
ref={labelRef} ref={labelRef}
> >
<input <input
id={id}
className="form-check-input" className="form-check-input"
type="checkbox" type="checkbox"
name={name} name={id}
checked={currentValue || false} checked={currentValue || false}
value="1" value="1"
disabled={disabled} disabled={disabled}

View File

@@ -1,24 +1,43 @@
import { ComponentChildren, RefObject } from "preact"; import { cloneElement, ComponentChildren, RefObject, VNode } from "preact";
import { CSSProperties } from "preact/compat";
import { useUniqueName } from "./hooks";
interface FormGroupProps { interface FormGroupProps {
name: string;
labelRef?: RefObject<HTMLLabelElement>; labelRef?: RefObject<HTMLLabelElement>;
label?: string; label?: string;
title?: string; title?: string;
className?: string; className?: string;
children: ComponentChildren; children: VNode<any>;
description?: string | ComponentChildren; description?: string | ComponentChildren;
disabled?: boolean;
style?: CSSProperties;
} }
export default function FormGroup({ label, title, className, children, description, labelRef }: FormGroupProps) { export default function FormGroup({ name, label, title, className, children, description, labelRef, disabled, style }: FormGroupProps) {
const id = useUniqueName(name);
const childWithId = cloneElement(children, { id });
return ( return (
<div className={`form-group ${className}`} title={title} <div className={`form-group ${className} ${disabled ? "disabled" : ""}`} title={title} style={style}>
style={{ "margin-bottom": "15px" }}> { label &&
<label style={{ width: "100%" }} ref={labelRef}> <label style={{ width: "100%" }} ref={labelRef} htmlFor={id}>{label}</label>}
{label && <div style={{ "margin-bottom": "10px" }}>{label}</div> }
{children} {childWithId}
</label>
{description && <small className="form-text">{description}</small>} {description && <small className="form-text">{description}</small>}
</div> </div>
); );
} }
/**
* Similar to {@link FormGroup} but allows more than one child. Due to this behaviour, there is no automatic ID assignment.
*/
export function FormMultiGroup({ label, children }: { label: string, children: ComponentChildren }) {
return (
<div className={`form-group`}>
{label && <label>{label}</label>}
{children}
</div>
);
}

View File

@@ -1,30 +1,56 @@
import type { ComponentChildren } from "preact";
import { useUniqueName } from "./hooks";
interface FormRadioProps { interface FormRadioProps {
name: string; name: string;
currentValue?: string; currentValue?: string;
values: { values: {
value: string; value: string;
label: string; label: string | ComponentChildren;
inlineDescription?: string | ComponentChildren;
}[]; }[];
onChange(newValue: string): void; onChange(newValue: string): void;
} }
export default function FormRadioGroup({ name, values, currentValue, onChange }: FormRadioProps) { export default function FormRadioGroup({ values, ...restProps }: FormRadioProps) {
return ( return (
<> <div role="group">
{(values || []).map(({ value, label }) => ( {(values || []).map(({ value, label, inlineDescription }) => (
<div className="form-check"> <div className="form-checkbox">
<label className="form-check-label tn-radio"> <FormRadio
value={value}
label={label} inlineDescription={inlineDescription}
labelClassName="form-check-label"
{...restProps}
/>
</div>
))}
</div>
);
}
export function FormInlineRadioGroup({ values, ...restProps }: FormRadioProps) {
return (
<div role="group">
{values.map(({ value, label }) => (<FormRadio value={value} label={label} {...restProps} />))}
</div>
)
}
function FormRadio({ name, value, label, currentValue, onChange, labelClassName, inlineDescription }: Omit<FormRadioProps, "values"> & { value: string, label: ComponentChildren, inlineDescription?: ComponentChildren, labelClassName?: string }) {
return (
<label className={`tn-radio ${labelClassName ?? ""}`}>
<input <input
className="form-check-input" className="form-check-input"
type="radio" type="radio"
name={name} name={useUniqueName(name)}
value={value} value={value}
checked={value === currentValue} checked={value === currentValue}
onChange={e => onChange((e.target as HTMLInputElement).value)} /> onChange={e => onChange((e.target as HTMLInputElement).value)}
{label} />
{inlineDescription ?
<><strong>{label}</strong> - {inlineDescription}</>
: label}
</label> </label>
</div> )
))}
</>
);
} }

View File

@@ -0,0 +1,79 @@
import type { ComponentChildren } from "preact";
import { CSSProperties } from "preact/compat";
type OnChangeListener = (newValue: string) => void;
export interface FormSelectGroup<T> {
title: string;
items: T[];
}
interface ValueConfig<T, Q> {
values: Q[];
/** The property of an item of {@link values} to be used as the key, uniquely identifying it. The key will be passed to the change listener. */
keyProperty: keyof T;
/** The property of an item of {@link values} to be used as the label, representing a human-readable version of the key. If missing, {@link keyProperty} will be used instead. */
titleProperty?: keyof T;
/** The current value of the combobox. The value will be looked up by going through {@link values} and looking an item whose {@link #keyProperty} value matches this one */
currentValue?: string;
}
interface FormSelectProps<T, Q> extends ValueConfig<T, Q> {
id?: string;
onChange: OnChangeListener;
style?: CSSProperties;
}
/**
* Combobox component that takes in any object array as data. Each item of the array is rendered as an item, and the key and values are obtained by looking into the object by a specified key.
*/
export default function FormSelect<T>({ id, onChange, style, ...restProps }: FormSelectProps<T, T>) {
return (
<FormSelectBody id={id} onChange={onChange} style={style}>
<FormSelectGroup {...restProps} />
</FormSelectBody>
);
}
/**
* Similar to {@link FormSelect}, but the top-level elements are actually groups.
*/
export function FormSelectWithGroups<T>({ id, values, keyProperty, titleProperty, currentValue, onChange }: FormSelectProps<T, FormSelectGroup<T>>) {
return (
<FormSelectBody id={id} onChange={onChange}>
{values.map(({ title, items }) => {
return (
<optgroup label={title}>
<FormSelectGroup values={items} keyProperty={keyProperty} titleProperty={titleProperty} currentValue={currentValue} />
</optgroup>
);
})}
</FormSelectBody>
)
}
function FormSelectBody({ id, children, onChange, style }: { id?: string, children: ComponentChildren, onChange: OnChangeListener, style?: CSSProperties }) {
return (
<select
id={id}
class="form-select"
onChange={e => onChange((e.target as HTMLInputElement).value)}
style={style}
>
{children}
</select>
)
}
function FormSelectGroup<T>({ values, keyProperty, titleProperty, currentValue }: ValueConfig<T, T>) {
return values.map(item => {
return (
<option
value={item[keyProperty] as any}
selected={item[keyProperty] === currentValue}
>
{item[titleProperty ?? keyProperty] ?? item[keyProperty] as any}
</option>
);
});
}

View File

@@ -0,0 +1,5 @@
import { ComponentChildren } from "preact";
export default function FormText({ children }: { children: ComponentChildren }) {
return <p className="form-text use-tn-links">{children}</p>
}

View File

@@ -0,0 +1,18 @@
interface FormTextAreaProps {
id?: string;
currentValue: string;
onBlur?(newValue: string): void;
rows: number;
}
export default function FormTextArea({ id, onBlur, rows, currentValue }: FormTextAreaProps) {
return (
<textarea
id={id}
rows={rows}
onBlur={(e) => {
onBlur?.(e.currentTarget.value);
}}
style={{ width: "100%" }}
>{currentValue}</textarea>
)
}

View File

@@ -1,27 +1,48 @@
import type { InputHTMLAttributes, RefObject } from "preact/compat"; import type { InputHTMLAttributes, RefObject } from "preact/compat";
interface FormTextBoxProps extends Pick<InputHTMLAttributes<HTMLInputElement>, "placeholder" | "autoComplete" | "className" | "type" | "name" | "pattern" | "title" | "style"> { interface FormTextBoxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "onBlur" | "value"> {
id?: string; id?: string;
currentValue?: string; currentValue?: string;
onChange?(newValue: string): void; onChange?(newValue: string, validity: ValidityState): void;
onBlur?(newValue: string): void;
inputRef?: RefObject<HTMLInputElement>; inputRef?: RefObject<HTMLInputElement>;
} }
export default function FormTextBox({ id, type, name, className, currentValue, onChange, autoComplete, inputRef, placeholder, title, pattern, style }: FormTextBoxProps) { export default function FormTextBox({ inputRef, className, type, currentValue, onChange, onBlur,...rest}: FormTextBoxProps) {
if (type === "number" && currentValue) {
const { min, max } = rest;
const currentValueNum = parseInt(currentValue, 10);
if (min && currentValueNum < parseInt(String(min), 10)) {
currentValue = String(min);
} else if (max && currentValueNum > parseInt(String(max), 10)) {
currentValue = String(max);
}
}
return ( return (
<input <input
ref={inputRef} ref={inputRef}
type={type ?? "text"}
className={`form-control ${className ?? ""}`} className={`form-control ${className ?? ""}`}
id={id} type={type ?? "text"}
name={name}
value={currentValue} value={currentValue}
autoComplete={autoComplete} onInput={onChange && (e => {
placeholder={placeholder} const target = e.currentTarget;
title={title} onChange?.(target.value, target.validity);
pattern={pattern} })}
onInput={e => onChange?.(e.currentTarget.value)} onBlur={onBlur && (e => {
style={style} const target = e.currentTarget;
onBlur(target.value);
})}
{...rest}
/> />
); );
} }
export function FormTextBoxWithUnit(props: FormTextBoxProps & { unit: string }) {
return (
<label class="input-group tn-number-unit-pair">
<FormTextBox {...props} />
<span class="input-group-text">{props.unit}</span>
</label>
)
}

View File

@@ -0,0 +1,33 @@
import { ActionKeyboardShortcut, KeyboardActionNames } from "@triliumnext/commons";
import { useEffect, useState } from "preact/hooks";
import keyboard_actions from "../../services/keyboard_actions";
interface KeyboardShortcutProps {
actionName: KeyboardActionNames;
}
export default function KeyboardShortcut({ actionName }: KeyboardShortcutProps) {
const [ action, setAction ] = useState<ActionKeyboardShortcut>();
useEffect(() => {
keyboard_actions.getAction(actionName).then(setAction);
}, []);
if (!action) {
return <></>;
}
return (
<>
{action.effectiveShortcuts?.map((shortcut, i) => {
const keys = shortcut.split("+");
return keys
.map((key, i) => (
<>
<kbd>{key}</kbd> {i + 1 < keys.length && "+ "}
</>
))
}).reduce<any>((acc, item) => (acc.length ? [...acc, ", ", item] : [item]), [])}
</>
);
}

View File

@@ -0,0 +1,17 @@
import { ComponentChild } from "preact";
interface LinkButtonProps {
onClick: () => void;
text: ComponentChild;
}
export default function LinkButton({ onClick, text }: LinkButtonProps) {
return (
<a class="tn-link" href="javascript:" onClick={(e) => {
e.preventDefault();
onClick();
}}>
{text}
</a>
)
}

View File

@@ -6,6 +6,7 @@ import type { RefObject } from "preact";
import type { CSSProperties } from "preact/compat"; import type { CSSProperties } from "preact/compat";
interface NoteAutocompleteProps { interface NoteAutocompleteProps {
id?: string;
inputRef?: RefObject<HTMLInputElement>; inputRef?: RefObject<HTMLInputElement>;
text?: string; text?: string;
placeholder?: string; placeholder?: string;
@@ -18,7 +19,7 @@ interface NoteAutocompleteProps {
noteId?: string; noteId?: string;
} }
export default function NoteAutocomplete({ inputRef: _ref, text, placeholder, onChange, onTextChange, container, containerStyle, opts, noteId, noteIdChanged }: NoteAutocompleteProps) { export default function NoteAutocomplete({ id, inputRef: _ref, text, placeholder, onChange, onTextChange, container, containerStyle, opts, noteId, noteIdChanged }: NoteAutocompleteProps) {
const ref = _ref ?? useRef<HTMLInputElement>(null); const ref = _ref ?? useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
@@ -74,6 +75,7 @@ export default function NoteAutocomplete({ inputRef: _ref, text, placeholder, on
return ( return (
<div className="input-group" style={containerStyle}> <div className="input-group" style={containerStyle}>
<input <input
id={id}
ref={ref} ref={ref}
className="note-autocomplete form-control" className="note-autocomplete form-control"
placeholder={placeholder ?? t("add_link.search_note")} /> placeholder={placeholder ?? t("add_link.search_note")} />

View File

@@ -24,7 +24,7 @@ function getProps({ className, html, style }: RawHtmlProps) {
} }
} }
function getHtml(html: string | HTMLElement | JQuery<HTMLElement>) { export function getHtml(html: string | HTMLElement | JQuery<HTMLElement>) {
if (typeof html === "object" && "length" in html) { if (typeof html === "object" && "length" in html) {
html = html[0]; html = html[0];
} }

View File

@@ -22,11 +22,18 @@ export default abstract class ReactBasicWidget extends BasicWidget {
* @returns the rendered wrapped DOM element. * @returns the rendered wrapped DOM element.
*/ */
export function renderReactWidget(parentComponent: Component, el: JSX.Element) { export function renderReactWidget(parentComponent: Component, el: JSX.Element) {
const renderContainer = new DocumentFragment(); return renderReactWidgetAtElement(parentComponent, el, new DocumentFragment()).children();
}
export function renderReactWidgetAtElement(parentComponent: Component, el: JSX.Element, container: Element | DocumentFragment) {
render(( render((
<ParentComponent.Provider value={parentComponent}> <ParentComponent.Provider value={parentComponent}>
{el} {el}
</ParentComponent.Provider> </ParentComponent.Provider>
), renderContainer); ), container);
return $(renderContainer.firstChild as HTMLElement); return $(container) as JQuery<HTMLElement>;
}
export function disposeReactWidget(container: Element) {
render(null, container);
} }

View File

@@ -1,7 +1,15 @@
import { useContext, useEffect, useRef } from "preact/hooks"; import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { EventData, EventNames } from "../../components/app_context"; import { EventData, EventNames } from "../../components/app_context";
import { ParentComponent } from "./ReactBasicWidget"; import { ParentComponent } from "./ReactBasicWidget";
import SpacedUpdate from "../../services/spaced_update"; import SpacedUpdate from "../../services/spaced_update";
import { OptionNames } from "@triliumnext/commons";
import options, { type OptionValue } from "../../services/options";
import utils, { reloadFrontendApp } from "../../services/utils";
import Component from "../../components/component";
import server from "../../services/server";
type TriliumEventHandler<T extends EventNames> = (data: EventData<T>) => void;
const registeredHandlers: Map<Component, Map<EventNames, TriliumEventHandler<any>[]>> = new Map();
/** /**
* Allows a React component to react to Trilium events (e.g. `entitiesReloaded`). When the desired event is triggered, the handler is invoked with the event parameters. * Allows a React component to react to Trilium events (e.g. `entitiesReloaded`). When the desired event is triggered, the handler is invoked with the event parameters.
@@ -12,32 +20,67 @@ import SpacedUpdate from "../../services/spaced_update";
* @param handler the handler to be invoked when the event is triggered. * @param handler the handler to be invoked when the event is triggered.
* @param enabled determines whether the event should be listened to or not. Useful to conditionally limit the listener based on a state (e.g. a modal being displayed). * @param enabled determines whether the event should be listened to or not. Useful to conditionally limit the listener based on a state (e.g. a modal being displayed).
*/ */
export default function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void, enabled = true) { export default function useTriliumEvent<T extends EventNames>(eventName: T, handler: TriliumEventHandler<T>, enabled = true) {
const parentWidget = useContext(ParentComponent); const parentWidget = useContext(ParentComponent);
useEffect(() => { if (!parentWidget) {
if (!parentWidget || !enabled) {
return; return;
} }
// Create a unique handler name for this specific event listener
const handlerName = `${eventName}Event`; const handlerName = `${eventName}Event`;
const originalHandler = parentWidget[handlerName]; const customHandler = useMemo(() => {
return async (data: EventData<T>) => {
// Override the event handler to call our handler // Inform the attached event listeners.
parentWidget[handlerName] = async function(data: EventData<T>) { const eventHandlers = registeredHandlers.get(parentWidget)?.get(eventName) ?? [];
// Call original handler if it exists for (const eventHandler of eventHandlers) {
if (originalHandler) { eventHandler(data);
await originalHandler.call(parentWidget, data);
} }
// Call our React component's handler }
handler(data); }, [ eventName, parentWidget ]);
};
useEffect(() => {
// Attach to the list of handlers.
let handlersByWidget = registeredHandlers.get(parentWidget);
if (!handlersByWidget) {
handlersByWidget = new Map();
registeredHandlers.set(parentWidget, handlersByWidget);
}
let handlersByWidgetAndEventName = handlersByWidget.get(eventName);
if (!handlersByWidgetAndEventName) {
handlersByWidgetAndEventName = [];
handlersByWidget.set(eventName, handlersByWidgetAndEventName);
}
if (!handlersByWidgetAndEventName.includes(handler)) {
handlersByWidgetAndEventName.push(handler);
}
// Apply the custom event handler.
if (parentWidget[handlerName] && parentWidget[handlerName] !== customHandler) {
console.warn(`Widget ${parentWidget.componentId} already had an event listener and it was replaced by the React one.`);
}
parentWidget[handlerName] = customHandler;
// Cleanup: restore original handler on unmount or when disabled
return () => { return () => {
parentWidget[handlerName] = originalHandler; const eventHandlers = registeredHandlers.get(parentWidget)?.get(eventName);
if (!eventHandlers || !eventHandlers.includes(handler)) {
return;
}
// Remove the event handler from the array.
const newEventHandlers = eventHandlers.filter(e => e !== handler);
if (newEventHandlers.length) {
registeredHandlers.get(parentWidget)?.set(eventName, newEventHandlers);
} else {
registeredHandlers.get(parentWidget)?.delete(eventName);
}
if (!registeredHandlers.get(parentWidget)?.size) {
registeredHandlers.delete(parentWidget);
}
}; };
}, [parentWidget, enabled, eventName, handler]); }, [ eventName, parentWidget, handler ]);
} }
export function useSpacedUpdate(callback: () => Promise<void>, interval = 1000) { export function useSpacedUpdate(callback: () => Promise<void>, interval = 1000) {
@@ -64,3 +107,115 @@ export function useSpacedUpdate(callback: () => Promise<void>, interval = 1000)
return spacedUpdateRef.current; return spacedUpdateRef.current;
} }
/**
* Allows a React component to read and write a Trilium option, while also watching for external changes.
*
* Conceptually, `useTriliumOption` works just like `useState`, but the value is also automatically updated if
* the option is changed somewhere else in the client.
*
* @param name the name of the option to listen for.
* @param needsRefresh whether to reload the frontend whenever the value is changed.
* @returns an array where the first value is the current option value and the second value is the setter.
*/
export function useTriliumOption(name: OptionNames, needsRefresh?: boolean): [string, (newValue: OptionValue) => Promise<void>] {
const initialValue = options.get(name);
const [ value, setValue ] = useState(initialValue);
const wrappedSetValue = useMemo(() => {
return async (newValue: OptionValue) => {
await options.save(name, newValue);
if (needsRefresh) {
reloadFrontendApp(`option change: ${name}`);
}
}
}, [ name, needsRefresh ]);
useTriliumEvent("entitiesReloaded", useCallback(({ loadResults }) => {
if (loadResults.getOptionNames().includes(name)) {
const newValue = options.get(name);
setValue(newValue);
}
}, [ name ]));
return [
value,
wrappedSetValue
]
}
/**
* Similar to {@link useTriliumOption}, but the value is converted to and from a boolean instead of a string.
*
* @param name the name of the option to listen for.
* @param needsRefresh whether to reload the frontend whenever the value is changed.
* @returns an array where the first value is the current option value and the second value is the setter.
*/
export function useTriliumOptionBool(name: OptionNames, needsRefresh?: boolean): [boolean, (newValue: boolean) => Promise<void>] {
const [ value, setValue ] = useTriliumOption(name, needsRefresh);
return [
(value === "true"),
(newValue) => setValue(newValue ? "true" : "false")
]
}
/**
* Similar to {@link useTriliumOption}, but the value is converted to and from a int instead of a string.
*
* @param name the name of the option to listen for.
* @param needsRefresh whether to reload the frontend whenever the value is changed.
* @returns an array where the first value is the current option value and the second value is the setter.
*/
export function useTriliumOptionInt(name: OptionNames): [number, (newValue: number) => Promise<void>] {
const [ value, setValue ] = useTriliumOption(name);
return [
(parseInt(value, 10)),
(newValue) => setValue(newValue)
]
}
/**
* Similar to {@link useTriliumOption}, but the object value is parsed to and from a JSON instead of a string.
*
* @param name the name of the option to listen for.
* @returns an array where the first value is the current option value and the second value is the setter.
*/
export function useTriliumOptionJson<T>(name: OptionNames): [ T, (newValue: T) => Promise<void> ] {
const [ value, setValue ] = useTriliumOption(name);
return [
(JSON.parse(value) as T),
(newValue => setValue(JSON.stringify(newValue)))
];
}
/**
* Similar to {@link useTriliumOption}, but operates with multiple options at once.
*
* @param names the name of the option to listen for.
* @returns an array where the first value is a map where the keys are the option names and the values, and the second value is the setter which takes in the same type of map and saves them all at once.
*/
export function useTriliumOptions<T extends OptionNames>(...names: T[]) {
const values: Record<string, string> = {};
for (const name of names) {
values[name] = options.get(name);
}
return [
values as Record<T, string>,
options.saveMany
] as const;
}
/**
* Generates a unique name via a random alphanumeric string of a fixed length.
*
* <p>
* Generally used to assign names to inputs that are unique, especially useful for widgets inside tabs.
*
* @param prefix a prefix to add to the unique name.
* @returns a name with the given prefix and a random alpanumeric string appended to it.
*/
export function useUniqueName(prefix?: string) {
return useMemo(() => (prefix ? prefix + "-" : "") + utils.randomString(10), [ prefix ]);
}

View File

@@ -1,199 +0,0 @@
import TypeWidget from "./type_widget.js";
import ElectronIntegrationOptions from "./options/appearance/electron_integration.js";
import ThemeOptions from "./options/appearance/theme.js";
import FontsOptions from "./options/appearance/fonts.js";
import MaxContentWidthOptions from "./options/appearance/max_content_width.js";
import KeyboardShortcutsOptions from "./options/shortcuts.js";
import HeadingStyleOptions from "./options/text_notes/heading_style.js";
import TableOfContentsOptions from "./options/text_notes/table_of_contents.js";
import HighlightsListOptions from "./options/text_notes/highlights_list.js";
import TextAutoReadOnlySizeOptions from "./options/text_notes/text_auto_read_only_size.js";
import DateTimeFormatOptions from "./options/text_notes/date_time_format.js";
import CodeEditorOptions from "./options/code_notes/code_editor.js";
import CodeAutoReadOnlySizeOptions from "./options/code_notes/code_auto_read_only_size.js";
import CodeMimeTypesOptions from "./options/code_notes/code_mime_types.js";
import ImageOptions from "./options/images/images.js";
import SpellcheckOptions from "./options/spellcheck.js";
import PasswordOptions from "./options/password/password.js";
import ProtectedSessionTimeoutOptions from "./options/password/protected_session_timeout.js";
import EtapiOptions from "./options/etapi.js";
import BackupOptions from "./options/backup.js";
import SyncOptions from "./options/sync.js";
import SearchEngineOptions from "./options/other/search_engine.js";
import TrayOptions from "./options/other/tray.js";
import NoteErasureTimeoutOptions from "./options/other/note_erasure_timeout.js";
import RevisionsSnapshotIntervalOptions from "./options/other/revisions_snapshot_interval.js";
import RevisionSnapshotsLimitOptions from "./options/other/revision_snapshots_limit.js";
import NetworkConnectionsOptions from "./options/other/network_connections.js";
import HtmlImportTagsOptions from "./options/other/html_import_tags.js";
import AdvancedSyncOptions from "./options/advanced/sync.js";
import DatabaseIntegrityCheckOptions from "./options/advanced/database_integrity_check.js";
import VacuumDatabaseOptions from "./options/advanced/vacuum_database.js";
import DatabaseAnonymizationOptions from "./options/advanced/database_anonymization.js";
import BackendLogWidget from "./content/backend_log.js";
import AttachmentErasureTimeoutOptions from "./options/other/attachment_erasure_timeout.js";
import RibbonOptions from "./options/appearance/ribbon.js";
import MultiFactorAuthenticationOptions from './options/multi_factor_authentication.js';
import LocalizationOptions from "./options/i18n/i18n.js";
import CodeBlockOptions from "./options/text_notes/code_block.js";
import EditorOptions from "./options/text_notes/editor.js";
import ShareSettingsOptions from "./options/other/share_settings.js";
import AiSettingsOptions from "./options/ai_settings.js";
import type FNote from "../../entities/fnote.js";
import type NoteContextAwareWidget from "../note_context_aware_widget.js";
import { t } from "../../services/i18n.js";
import LanguageOptions from "./options/i18n/language.js";
import type BasicWidget from "../basic_widget.js";
import CodeTheme from "./options/code_notes/code_theme.js";
import RelatedSettings from "./options/appearance/related_settings.js";
import EditorFeaturesOptions from "./options/text_notes/features.js";
const TPL = /*html*/`<div class="note-detail-content-widget note-detail-printable">
<style>
.type-contentWidget .note-detail {
height: 100%;
}
.note-detail-content-widget {
height: 100%;
}
.note-detail-content-widget-content {
padding: 15px;
height: 100%;
}
.note-detail.full-height .note-detail-content-widget-content {
padding: 0;
}
</style>
<div class="note-detail-content-widget-content"></div>
</div>`;
export type OptionPages = "_optionsAppearance" | "_optionsShortcuts" | "_optionsTextNotes" | "_optionsCodeNotes" | "_optionsImages" | "_optionsSpellcheck" | "_optionsPassword" | "_optionsMFA" | "_optionsEtapi" | "_optionsBackup" | "_optionsSync" | "_optionsAi" | "_optionsOther" | "_optionsLocalization" | "_optionsAdvanced";
const CONTENT_WIDGETS: Record<OptionPages | "_backendLog", (typeof NoteContextAwareWidget)[]> = {
_optionsAppearance: [
ThemeOptions,
FontsOptions,
ElectronIntegrationOptions,
MaxContentWidthOptions,
RibbonOptions
],
_optionsShortcuts: [
KeyboardShortcutsOptions
],
_optionsTextNotes: [
EditorOptions,
EditorFeaturesOptions,
HeadingStyleOptions,
CodeBlockOptions,
TableOfContentsOptions,
HighlightsListOptions,
TextAutoReadOnlySizeOptions,
DateTimeFormatOptions
],
_optionsCodeNotes: [
CodeEditorOptions,
CodeTheme,
CodeMimeTypesOptions,
CodeAutoReadOnlySizeOptions
],
_optionsImages: [
ImageOptions
],
_optionsSpellcheck: [
SpellcheckOptions
],
_optionsPassword: [
PasswordOptions,
ProtectedSessionTimeoutOptions
],
_optionsMFA: [MultiFactorAuthenticationOptions],
_optionsEtapi: [
EtapiOptions
],
_optionsBackup: [
BackupOptions
],
_optionsSync: [
SyncOptions
],
_optionsAi: [AiSettingsOptions],
_optionsOther: [
SearchEngineOptions,
TrayOptions,
NoteErasureTimeoutOptions,
AttachmentErasureTimeoutOptions,
RevisionsSnapshotIntervalOptions,
RevisionSnapshotsLimitOptions,
HtmlImportTagsOptions,
ShareSettingsOptions,
NetworkConnectionsOptions
],
_optionsLocalization: [
LocalizationOptions,
LanguageOptions
],
_optionsAdvanced: [
AdvancedSyncOptions,
DatabaseIntegrityCheckOptions,
DatabaseAnonymizationOptions,
VacuumDatabaseOptions
],
_backendLog: [
BackendLogWidget
]
};
/**
* Type widget that displays one or more widgets based on the type of note, generally used for options and other interactive notes such as the backend log.
*
* One important aspect is that, like its parent {@link TypeWidget}, the content widgets don't receive all events by default and they must be manually added
* to the propagation list in {@link TypeWidget.handleEventInChildren}.
*/
export default class ContentWidgetTypeWidget extends TypeWidget {
private $content!: JQuery<HTMLElement>;
private widget?: BasicWidget;
static getType() {
return "contentWidget";
}
doRender() {
this.$widget = $(TPL);
this.$content = this.$widget.find(".note-detail-content-widget-content");
super.doRender();
}
async doRefresh(note: FNote) {
this.$content.empty();
this.children = [];
const contentWidgets = [
...((CONTENT_WIDGETS as Record<string, typeof NoteContextAwareWidget[]>)[note.noteId]),
RelatedSettings
];
this.$content.toggleClass("options", note.noteId.startsWith("_options"));
if (contentWidgets) {
for (const clazz of contentWidgets) {
const widget = new clazz();
if (this.noteContext) {
await widget.handleEvent("setNoteContext", { noteContext: this.noteContext });
}
this.child(widget);
this.$content.append(widget.render());
this.widget = widget;
await widget.refresh();
}
} else {
this.$content.append(t("content_widget.unknown_widget", { id: note.noteId }));
}
}
}

View File

@@ -0,0 +1,137 @@
import TypeWidget from "./type_widget.js";
import type FNote from "../../entities/fnote.js";
import type NoteContextAwareWidget from "../note_context_aware_widget.js";
import { t } from "../../services/i18n.js";
import type BasicWidget from "../basic_widget.js";
import type { JSX } from "preact/jsx-runtime";
import AppearanceSettings from "./options/appearance.jsx";
import { disposeReactWidget, renderReactWidget, renderReactWidgetAtElement } from "../react/ReactBasicWidget.jsx";
import ImageSettings from "./options/images.jsx";
import AdvancedSettings from "./options/advanced.jsx";
import InternationalizationOptions from "./options/i18n.jsx";
import SyncOptions from "./options/sync.jsx";
import EtapiSettings from "./options/etapi.js";
import BackupSettings from "./options/backup.js";
import SpellcheckSettings from "./options/spellcheck.js";
import PasswordSettings from "./options/password.jsx";
import ShortcutSettings from "./options/shortcuts.js";
import TextNoteSettings from "./options/text_notes.jsx";
import CodeNoteSettings from "./options/code_notes.jsx";
import OtherSettings from "./options/other.jsx";
import BackendLogWidget from "./content/backend_log.js";
import MultiFactorAuthenticationSettings from "./options/multi_factor_authentication.js";
import AiSettings from "./options/ai_settings.jsx";
const TPL = /*html*/`<div class="note-detail-content-widget note-detail-printable">
<style>
.type-contentWidget .note-detail {
height: 100%;
}
.note-detail-content-widget {
height: 100%;
}
.note-detail-content-widget-content {
padding: 15px;
height: 100%;
}
.note-detail.full-height .note-detail-content-widget-content {
padding: 0;
}
</style>
<div class="note-detail-content-widget-content"></div>
</div>`;
export type OptionPages = "_optionsAppearance" | "_optionsShortcuts" | "_optionsTextNotes" | "_optionsCodeNotes" | "_optionsImages" | "_optionsSpellcheck" | "_optionsPassword" | "_optionsMFA" | "_optionsEtapi" | "_optionsBackup" | "_optionsSync" | "_optionsAi" | "_optionsOther" | "_optionsLocalization" | "_optionsAdvanced";
const CONTENT_WIDGETS: Record<OptionPages | "_backendLog", ((typeof NoteContextAwareWidget)[] | JSX.Element)> = {
_optionsAppearance: <AppearanceSettings />,
_optionsShortcuts: <ShortcutSettings />,
_optionsTextNotes: <TextNoteSettings />,
_optionsCodeNotes: <CodeNoteSettings />,
_optionsImages: <ImageSettings />,
_optionsSpellcheck: <SpellcheckSettings />,
_optionsPassword: <PasswordSettings />,
_optionsMFA: <MultiFactorAuthenticationSettings />,
_optionsEtapi: <EtapiSettings />,
_optionsBackup: <BackupSettings />,
_optionsSync: <SyncOptions />,
_optionsAi: <AiSettings />,
_optionsOther: <OtherSettings />,
_optionsLocalization: <InternationalizationOptions />,
_optionsAdvanced: <AdvancedSettings />,
_backendLog: [
BackendLogWidget
]
};
/**
* Type widget that displays one or more widgets based on the type of note, generally used for options and other interactive notes such as the backend log.
*
* One important aspect is that, like its parent {@link TypeWidget}, the content widgets don't receive all events by default and they must be manually added
* to the propagation list in {@link TypeWidget.handleEventInChildren}.
*/
export default class ContentWidgetTypeWidget extends TypeWidget {
private $content!: JQuery<HTMLElement>;
private widget?: BasicWidget;
static getType() {
return "contentWidget";
}
doRender() {
this.$widget = $(TPL);
this.$content = this.$widget.find(".note-detail-content-widget-content");
super.doRender();
}
async doRefresh(note: FNote) {
this.$content.empty();
this.children = [];
const contentWidgets = (CONTENT_WIDGETS as Record<string, (typeof NoteContextAwareWidget[] | JSX.Element)>)[note.noteId];
this.$content.toggleClass("options", note.noteId.startsWith("_options"));
// Unknown widget.
if (!contentWidgets) {
this.$content.append(t("content_widget.unknown_widget", { id: note.noteId }));
return;
}
// Legacy widget.
if (Array.isArray(contentWidgets)) {
for (const clazz of contentWidgets) {
const widget = new clazz();
if (this.noteContext) {
await widget.handleEvent("setNoteContext", { noteContext: this.noteContext });
}
this.child(widget);
this.$content.append(widget.render());
this.widget = widget;
await widget.refresh();
}
return;
}
// React widget.
renderReactWidgetAtElement(this, contentWidgets, this.$content[0]);
}
cleanup(): void {
if (this.noteId) {
const contentWidgets = (CONTENT_WIDGETS as Record<string, (typeof NoteContextAwareWidget[] | JSX.Element)>)[this.noteId];
if (contentWidgets && !Array.isArray(contentWidgets)) {
disposeReactWidget(this.$content[0]);
}
}
super.cleanup();
}
}

View File

@@ -0,0 +1,175 @@
import { AnonymizedDbResponse, DatabaseAnonymizeResponse, DatabaseCheckIntegrityResponse } from "@triliumnext/commons";
import { t } from "../../../services/i18n";
import server from "../../../services/server";
import toast from "../../../services/toast";
import Button from "../../react/Button";
import FormText from "../../react/FormText";
import OptionsSection from "./components/OptionsSection"
import Column from "../../react/Column";
import { useEffect, useState } from "preact/hooks";
export default function AdvancedSettings() {
return <>
<AdvancedSyncOptions />
<DatabaseIntegrityOptions />
<DatabaseAnonymizationOptions />
<VacuumDatabaseOptions />
</>;
}
function AdvancedSyncOptions() {
return (
<OptionsSection title={t("sync.title")}>
<Button
text={t("sync.force_full_sync_button")}
onClick={async () => {
await server.post("sync/force-full-sync");
toast.showMessage(t("sync.full_sync_triggered"));
}}
/>
<Button
text={t("sync.fill_entity_changes_button")}
onClick={async () => {
toast.showMessage(t("sync.filling_entity_changes"));
await server.post("sync/fill-entity-changes");
toast.showMessage(t("sync.sync_rows_filled_successfully"));
}}
/>
</OptionsSection>
);
}
function DatabaseIntegrityOptions() {
return (
<OptionsSection title={t("database_integrity_check.title")}>
<FormText>{t("database_integrity_check.description")}</FormText>
<Button
text={t("database_integrity_check.check_button")}
onClick={async () => {
toast.showMessage(t("database_integrity_check.checking_integrity"));
const { results } = await server.get<DatabaseCheckIntegrityResponse>("database/check-integrity");
if (results.length === 1 && results[0].integrity_check === "ok") {
toast.showMessage(t("database_integrity_check.integrity_check_succeeded"));
} else {
toast.showMessage(t("database_integrity_check.integrity_check_failed", { results: JSON.stringify(results, null, 2) }), 15000);
}
}}
/>
<Button
text={t("consistency_checks.find_and_fix_button")}
onClick={async () => {
toast.showMessage(t("consistency_checks.finding_and_fixing_message"));
await server.post("database/find-and-fix-consistency-issues");
toast.showMessage(t("consistency_checks.issues_fixed_message"));
}}
/>
</OptionsSection>
)
}
function DatabaseAnonymizationOptions() {
const [ existingAnonymizedDatabases, setExistingAnonymizedDatabases ] = useState<AnonymizedDbResponse[]>([]);
function refreshAnonymizedDatabase() {
server.get<AnonymizedDbResponse[]>("database/anonymized-databases").then(setExistingAnonymizedDatabases);
}
useEffect(refreshAnonymizedDatabase, []);
return (
<OptionsSection title={t("database_anonymization.title")}>
<FormText>{t("database_anonymization.choose_anonymization")}</FormText>
<div className="row">
<DatabaseAnonymizationOption
title={t("database_anonymization.full_anonymization")}
description={t("database_anonymization.full_anonymization_description")}
buttonText={t("database_anonymization.save_fully_anonymized_database")}
buttonClick={async () => {
toast.showMessage(t("database_anonymization.creating_fully_anonymized_database"));
const resp = await server.post<DatabaseAnonymizeResponse>("database/anonymize/full");
if (!resp.success) {
toast.showError(t("database_anonymization.error_creating_anonymized_database"));
} else {
toast.showMessage(t("database_anonymization.successfully_created_fully_anonymized_database", { anonymizedFilePath: resp.anonymizedFilePath }), 10000);
refreshAnonymizedDatabase();
}
}}
/>
<DatabaseAnonymizationOption
title={t("database_anonymization.light_anonymization")}
description={t("database_anonymization.light_anonymization_description")}
buttonText={t("database_anonymization.save_lightly_anonymized_database")}
buttonClick={async () => {
toast.showMessage(t("database_anonymization.creating_lightly_anonymized_database"));
const resp = await server.post<DatabaseAnonymizeResponse>("database/anonymize/light");
if (!resp.success) {
toast.showError(t("database_anonymization.error_creating_anonymized_database"));
} else {
toast.showMessage(t("database_anonymization.successfully_created_lightly_anonymized_database", { anonymizedFilePath: resp.anonymizedFilePath }), 10000);
refreshAnonymizedDatabase();
}
}}
/>
</div>
<hr />
<ExistingAnonymizedDatabases databases={existingAnonymizedDatabases} />
</OptionsSection>
)
}
function DatabaseAnonymizationOption({ title, description, buttonText, buttonClick }: { title: string, description: string, buttonText: string, buttonClick: () => void }) {
return (
<Column md={6} style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", marginTop: "1em" }}>
<h5>{title}</h5>
<FormText>{description}</FormText>
<Button text={buttonText} onClick={buttonClick} />
</Column>
)
}
function ExistingAnonymizedDatabases({ databases }: { databases: AnonymizedDbResponse[] }) {
if (!databases.length) {
return <FormText>{t("database_anonymization.no_anonymized_database_yet")}</FormText>
}
return (
<table className="table table-stripped">
<thead>
<th>{t("database_anonymization.existing_anonymized_databases")}</th>
</thead>
<tbody>
{databases.map(({ filePath }) => (
<tr>
<td>{filePath}</td>
</tr>
))}
</tbody>
</table>
)
}
function VacuumDatabaseOptions() {
return (
<OptionsSection title={t("vacuum_database.title")}>
<FormText>{t("vacuum_database.description")}</FormText>
<Button
text={t("vacuum_database.button_text")}
onClick={async () => {
toast.showMessage(t("vacuum_database.vacuuming_database"));
await server.post("database/vacuum-database");
toast.showMessage(t("vacuum_database.database_vacuumed"));
}}
/>
</OptionsSection>
)
}

View File

@@ -1,119 +0,0 @@
import OptionsWidget from "../options_widget.js";
import toastService from "../../../../services/toast.js";
import server from "../../../../services/server.js";
import { t } from "../../../../services/i18n.js";
import type { OptionMap } from "@triliumnext/commons";
const TPL = /*html*/`
<div class="options-section">
<style>
.database-database-anonymization-option {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-top: 1em;
}
.database-database-anonymization-option p {
margin-top: .75em;
flex-grow: 1;
}
</style>
<h4>${t("database_anonymization.title")}</h4>
<div class="row">
<p class="form-text">${t("database_anonymization.choose_anonymization")}</p>
<div class="col-md-6 database-database-anonymization-option">
<h5>${t("database_anonymization.full_anonymization")}</h5>
<p class="form-text">${t("database_anonymization.full_anonymization_description")}</p>
<button class="anonymize-full-button btn btn-secondary">${t("database_anonymization.save_fully_anonymized_database")}</button>
</div>
<div class="col-md-6 database-database-anonymization-option">
<h5>${t("database_anonymization.light_anonymization")}</h5>
<p class="form-text">${t("database_anonymization.light_anonymization_description")}</p>
<button class="anonymize-light-button btn btn-secondary">${t("database_anonymization.save_lightly_anonymized_database")}</button>
</div>
</div>
<hr />
<table class="existing-anonymized-databases-table table table-stripped">
<thead>
<th>${t("database_anonymization.existing_anonymized_databases")}</th>
</thead>
<tbody class="existing-anonymized-databases">
</tbody>
</table>
</div>`;
// TODO: Deduplicate with server
interface AnonymizeResponse {
success: boolean;
anonymizedFilePath: string;
}
interface AnonymizedDbResponse {
filePath: string;
}
export default class DatabaseAnonymizationOptions extends OptionsWidget {
private $anonymizeFullButton!: JQuery<HTMLElement>;
private $anonymizeLightButton!: JQuery<HTMLElement>;
private $existingAnonymizedDatabases!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.$anonymizeFullButton = this.$widget.find(".anonymize-full-button");
this.$anonymizeLightButton = this.$widget.find(".anonymize-light-button");
this.$anonymizeFullButton.on("click", async () => {
toastService.showMessage(t("database_anonymization.creating_fully_anonymized_database"));
const resp = await server.post<AnonymizeResponse>("database/anonymize/full");
if (!resp.success) {
toastService.showError(t("database_anonymization.error_creating_anonymized_database"));
} else {
toastService.showMessage(t("database_anonymization.successfully_created_fully_anonymized_database", { anonymizedFilePath: resp.anonymizedFilePath }), 10000);
}
this.refresh();
});
this.$anonymizeLightButton.on("click", async () => {
toastService.showMessage(t("database_anonymization.creating_lightly_anonymized_database"));
const resp = await server.post<AnonymizeResponse>("database/anonymize/light");
if (!resp.success) {
toastService.showError(t("database_anonymization.error_creating_anonymized_database"));
} else {
toastService.showMessage(t("database_anonymization.successfully_created_lightly_anonymized_database", { anonymizedFilePath: resp.anonymizedFilePath }), 10000);
}
this.refresh();
});
this.$existingAnonymizedDatabases = this.$widget.find(".existing-anonymized-databases");
}
optionsLoaded(options: OptionMap) {
server.get<AnonymizedDbResponse[]>("database/anonymized-databases").then((anonymizedDatabases) => {
this.$existingAnonymizedDatabases.empty();
if (!anonymizedDatabases.length) {
anonymizedDatabases = [{ filePath: t("database_anonymization.no_anonymized_database_yet") }];
}
for (const { filePath } of anonymizedDatabases) {
this.$existingAnonymizedDatabases.append($("<tr>").append($("<td>").text(filePath)));
}
});
}
}

View File

@@ -1,53 +0,0 @@
import OptionsWidget from "../options_widget.js";
import toastService from "../../../../services/toast.js";
import server from "../../../../services/server.js";
import { t } from "../../../../services/i18n.js";
const TPL = /*html*/`
<div class="options-section">
<h4>${t("database_integrity_check.title")}</h4>
<p class="form-text">${t("database_integrity_check.description")}</p>
<button class="check-integrity-button btn btn-secondary">${t("database_integrity_check.check_button")}</button>
<button class="find-and-fix-consistency-issues-button btn btn-secondary">${t("consistency_checks.find_and_fix_button")}</button>
</div>
`;
// TODO: Deduplicate with server
interface Response {
results: {
integrity_check: string;
}[];
}
export default class DatabaseIntegrityCheckOptions extends OptionsWidget {
private $checkIntegrityButton!: JQuery<HTMLElement>;
private $findAndFixConsistencyIssuesButton!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.$checkIntegrityButton = this.$widget.find(".check-integrity-button");
this.$checkIntegrityButton.on("click", async () => {
toastService.showMessage(t("database_integrity_check.checking_integrity"));
const { results } = await server.get<Response>("database/check-integrity");
if (results.length === 1 && results[0].integrity_check === "ok") {
toastService.showMessage(t("database_integrity_check.integrity_check_succeeded"));
} else {
toastService.showMessage(t("database_integrity_check.integrity_check_failed", { results: JSON.stringify(results, null, 2) }), 15000);
}
});
this.$findAndFixConsistencyIssuesButton = this.$widget.find(".find-and-fix-consistency-issues-button");
this.$findAndFixConsistencyIssuesButton.on("click", async () => {
toastService.showMessage(t("consistency_checks.finding_and_fixing_message"));
await server.post("database/find-and-fix-consistency-issues");
toastService.showMessage(t("consistency_checks.issues_fixed_message"));
});
}
}

View File

@@ -1,40 +0,0 @@
import OptionsWidget from "../options_widget.js";
import server from "../../../../services/server.js";
import toastService from "../../../../services/toast.js";
import { t } from "../../../../services/i18n.js";
import type { OptionMap } from "@triliumnext/commons";
const TPL = /*html*/`
<div class="options-section">
<h4>${t("sync.title")}</h4>
<button class="force-full-sync-button btn btn-secondary">${t("sync.force_full_sync_button")}</button>
<button class="fill-entity-changes-button btn btn-secondary">${t("sync.fill_entity_changes_button")}</button>
</div>`;
export default class AdvancedSyncOptions extends OptionsWidget {
private $forceFullSyncButton!: JQuery<HTMLElement>;
private $fillEntityChangesButton!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.$forceFullSyncButton = this.$widget.find(".force-full-sync-button");
this.$fillEntityChangesButton = this.$widget.find(".fill-entity-changes-button");
this.$forceFullSyncButton.on("click", async () => {
await server.post("sync/force-full-sync");
toastService.showMessage(t("sync.full_sync_triggered"));
});
this.$fillEntityChangesButton.on("click", async () => {
toastService.showMessage(t("sync.filling_entity_changes"));
await server.post("sync/fill-entity-changes");
toastService.showMessage(t("sync.sync_rows_filled_successfully"));
});
}
async optionsLoaded(options: OptionMap) {}
}

View File

@@ -1,29 +0,0 @@
import OptionsWidget from "../options_widget.js";
import toastService from "../../../../services/toast.js";
import server from "../../../../services/server.js";
import { t } from "../../../../services/i18n.js";
const TPL = /*html*/`
<div class="options-section">
<h4>${t("vacuum_database.title")}</h4>
<p class="form-text">${t("vacuum_database.description")}</p>
<button class="vacuum-database-button btn btn-secondary">${t("vacuum_database.button_text")}</button>
</div>`;
export default class VacuumDatabaseOptions extends OptionsWidget {
private $vacuumDatabaseButton!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.$vacuumDatabaseButton = this.$widget.find(".vacuum-database-button");
this.$vacuumDatabaseButton.on("click", async () => {
toastService.showMessage(t("vacuum_database.vacuuming_database"));
await server.post("database/vacuum-database");
toastService.showMessage(t("vacuum_database.database_vacuumed"));
});
}
}

View File

@@ -1,2 +0,0 @@
import AiSettingsWidget from './ai_settings/index.js';
export default AiSettingsWidget;

View File

@@ -0,0 +1,236 @@
import { useCallback, useEffect, useState } from "preact/hooks";
import { t } from "../../../services/i18n";
import toast from "../../../services/toast";
import FormCheckbox from "../../react/FormCheckbox";
import FormGroup from "../../react/FormGroup";
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
import OptionsSection from "./components/OptionsSection";
import Admonition from "../../react/Admonition";
import FormSelect from "../../react/FormSelect";
import FormTextBox from "../../react/FormTextBox";
import type { OllamaModelResponse, OpenAiOrAnthropicModelResponse, OptionNames } from "@triliumnext/commons";
import server from "../../../services/server";
import Button from "../../react/Button";
import FormTextArea from "../../react/FormTextArea";
export default function AiSettings() {
return (
<>
<EnableAiSettings />
<ProviderSettings />
</>
);
}
function EnableAiSettings() {
const [ aiEnabled, setAiEnabled ] = useTriliumOptionBool("aiEnabled");
return (
<>
<OptionsSection title={t("ai_llm.title")}>
<FormGroup name="ai-enabled" description={t("ai_llm.enable_ai_description")}>
<FormCheckbox
label={t("ai_llm.enable_ai_features")}
currentValue={aiEnabled} onChange={(isEnabled) => {
if (isEnabled) {
toast.showMessage(t("ai_llm.ai_enabled"));
} else {
toast.showMessage(t("ai_llm.ai_disabled"));
}
setAiEnabled(isEnabled);
}}
/>
</FormGroup>
{aiEnabled && <Admonition type="warning">{t("ai_llm.experimental_warning")}</Admonition>}
</OptionsSection>
</>
);
}
function ProviderSettings() {
const [ aiSelectedProvider, setAiSelectedProvider ] = useTriliumOption("aiSelectedProvider");
const [ aiTemperature, setAiTemperature ] = useTriliumOption("aiTemperature");
const [ aiSystemPrompt, setAiSystemPrompt ] = useTriliumOption("aiSystemPrompt");
return (
<OptionsSection title={t("ai_llm.provider_configuration")}>
<FormGroup name="selected-provider" label={t("ai_llm.selected_provider")} description={t("ai_llm.selected_provider_description")}>
<FormSelect
values={[
{ value: "", text: t("ai_llm.select_provider") },
{ value: "openai", text: "OpenAI" },
{ value: "anthropic", text: "Anthropic" },
{ value: "ollama", text: "Ollama" }
]}
currentValue={aiSelectedProvider} onChange={setAiSelectedProvider}
keyProperty="value" titleProperty="text"
/>
</FormGroup>
{
aiSelectedProvider === "openai" ?
<SingleProviderSettings
title={t("ai_llm.openai_settings")}
apiKeyDescription={t("ai_llm.openai_api_key_description")}
baseUrlDescription={t("ai_llm.openai_url_description")}
modelDescription={t("ai_llm.openai_model_description")}
validationErrorMessage={t("ai_llm.empty_key_warning.openai")}
apiKeyOption="openaiApiKey" baseUrlOption="openaiBaseUrl" modelOption="openaiDefaultModel"
provider={aiSelectedProvider}
/>
: aiSelectedProvider === "anthropic" ?
<SingleProviderSettings
title={t("ai_llm.anthropic_settings")}
apiKeyDescription={t("ai_llm.anthropic_api_key_description")}
modelDescription={t("ai_llm.anthropic_model_description")}
baseUrlDescription={t("ai_llm.anthropic_url_description")}
validationErrorMessage={t("ai_llm.empty_key_warning.anthropic")}
apiKeyOption="anthropicApiKey" baseUrlOption="anthropicBaseUrl" modelOption="anthropicDefaultModel"
provider={aiSelectedProvider}
/>
: aiSelectedProvider === "ollama" ?
<SingleProviderSettings
title={t("ai_llm.ollama_settings")}
baseUrlDescription={t("ai_llm.ollama_url_description")}
modelDescription={t("ai_llm.ollama_model_description")}
validationErrorMessage={t("ai_llm.ollama_no_url")}
baseUrlOption="ollamaBaseUrl"
provider={aiSelectedProvider} modelOption="ollamaDefaultModel"
/>
:
<></>
}
<FormGroup name="ai-temperature" label={t("ai_llm.temperature")} description={t("ai_llm.temperature_description")}>
<FormTextBox
type="number" min="0" max="2" step="0.1"
currentValue={aiTemperature} onChange={setAiTemperature}
/>
</FormGroup>
<FormGroup name="system-prompt" label={t("ai_llm.system_prompt")} description={t("ai_llm.system_prompt_description")}>
<FormTextArea
rows={3}
currentValue={aiSystemPrompt} onBlur={setAiSystemPrompt}
/>
</FormGroup>
</OptionsSection>
)
}
interface SingleProviderSettingsProps {
provider: string;
title: string;
apiKeyDescription?: string;
baseUrlDescription: string;
modelDescription: string;
validationErrorMessage: string;
apiKeyOption?: OptionNames;
baseUrlOption: OptionNames;
modelOption: OptionNames;
}
function SingleProviderSettings({ provider, title, apiKeyDescription, baseUrlDescription, modelDescription, validationErrorMessage, apiKeyOption, baseUrlOption, modelOption }: SingleProviderSettingsProps) {
const [ apiKey, setApiKey ] = apiKeyOption ? useTriliumOption(apiKeyOption) : [];
const [ baseUrl, setBaseUrl ] = useTriliumOption(baseUrlOption);
const isValid = (apiKeyOption ? !!apiKey : !!baseUrl);
return (
<div class="provider-settings">
<div class="card mt-3">
<div class="card-header">
<h5>{title}</h5>
</div>
<div class="card-body">
{!isValid && <Admonition type="caution">{validationErrorMessage}</Admonition> }
{apiKeyOption && (
<FormGroup name="api-key" label={t("ai_llm.api_key")} description={apiKeyDescription}>
<FormTextBox
type="password" autoComplete="off"
currentValue={apiKey} onChange={setApiKey}
/>
</FormGroup>
)}
<FormGroup name="base-url" label={t("ai_llm.url")} description={baseUrlDescription}>
<FormTextBox
currentValue={baseUrl ?? "https://api.openai.com/v1"} onChange={setBaseUrl}
/>
</FormGroup>
{isValid &&
<FormGroup name="model" label={t("ai_llm.model")} description={modelDescription}>
<ModelSelector provider={provider} baseUrl={baseUrl} modelOption={modelOption} />
</FormGroup>
}
</div>
</div>
</div>
)
}
function ModelSelector({ provider, baseUrl, modelOption }: { provider: string; baseUrl: string, modelOption: OptionNames }) {
const [ model, setModel ] = useTriliumOption(modelOption);
const [ models, setModels ] = useState<{ name: string, id: string }[]>([]);
const loadProviders = useCallback(async () => {
switch (provider) {
case "openai":
case "anthropic": {
try {
const response = await server.get<OpenAiOrAnthropicModelResponse>(`llm/providers/${provider}/models?baseUrl=${encodeURIComponent(baseUrl)}`);
if (response.success) {
setModels(response.chatModels.toSorted((a, b) => a.name.localeCompare(b.name)));
} else {
toast.showError(t("ai_llm.no_models_found_online"));
}
} catch (e) {
toast.showError(t("ai_llm.error_fetching", { error: e }));
}
break;
}
case "ollama": {
try {
const response = await server.get<OllamaModelResponse>(`llm/providers/ollama/models?baseUrl=${encodeURIComponent(baseUrl)}`);
if (response.success) {
setModels(response.models
.map(model => ({
name: model.name,
id: model.model
}))
.toSorted((a, b) => a.name.localeCompare(b.name)));
} else {
toast.showError(t("ai_llm.no_models_found_ollama"));
}
} catch (e) {
toast.showError(t("ai_llm.error_fetching", { error: e }));
}
break;
}
}
}, [provider]);
useEffect(() => {
loadProviders();
}, [provider]);
return (
<>
<FormSelect
values={models}
keyProperty="id" titleProperty="name"
currentValue={model} onChange={setModel}
/>
<Button
text={t("ai_llm.refresh_models")}
onClick={loadProviders}
size="small"
style={{ marginTop: "0.5em" }}
/>
</>
)
}

View File

@@ -1,362 +0,0 @@
import OptionsWidget from "../options_widget.js";
import { TPL } from "./template.js";
import { t } from "../../../../services/i18n.js";
import type { OptionDefinitions, OptionMap } from "@triliumnext/commons";
import server from "../../../../services/server.js";
import toastService from "../../../../services/toast.js";
import { ProviderService } from "./providers.js";
export default class AiSettingsWidget extends OptionsWidget {
private ollamaModelsRefreshed = false;
private openaiModelsRefreshed = false;
private anthropicModelsRefreshed = false;
private providerService: ProviderService | null = null;
doRender() {
this.$widget = $(TPL);
this.providerService = new ProviderService(this.$widget);
// Setup event handlers for options
this.setupEventHandlers();
return this.$widget;
}
/**
* Helper method to set up a change event handler for an option
* @param selector The jQuery selector for the element
* @param optionName The name of the option to update
* @param validateAfter Whether to run validation after the update
* @param isCheckbox Whether the element is a checkbox
*/
setupChangeHandler(selector: string, optionName: keyof OptionDefinitions, validateAfter: boolean = false, isCheckbox: boolean = false) {
if (!this.$widget) return;
const $element = this.$widget.find(selector);
$element.on('change', async () => {
let value: string;
if (isCheckbox) {
value = $element.prop('checked') ? 'true' : 'false';
} else {
value = $element.val() as string;
}
await this.updateOption(optionName, value);
// Special handling for aiEnabled option
if (optionName === 'aiEnabled') {
try {
const isEnabled = value === 'true';
if (isEnabled) {
toastService.showMessage(t("ai_llm.ai_enabled") || "AI features enabled");
} else {
toastService.showMessage(t("ai_llm.ai_disabled") || "AI features disabled");
}
} catch (error) {
console.error('Error toggling AI:', error);
toastService.showError(t("ai_llm.ai_toggle_error") || "Error toggling AI features");
}
}
if (validateAfter) {
await this.displayValidationWarnings();
}
});
}
/**
* Set up all event handlers for options
*/
setupEventHandlers() {
if (!this.$widget) return;
// Core AI options
this.setupChangeHandler('.ai-enabled', 'aiEnabled', true, true);
this.setupChangeHandler('.ai-selected-provider', 'aiSelectedProvider', true);
this.setupChangeHandler('.ai-temperature', 'aiTemperature');
this.setupChangeHandler('.ai-system-prompt', 'aiSystemPrompt');
// OpenAI options
this.setupChangeHandler('.openai-api-key', 'openaiApiKey', true);
this.setupChangeHandler('.openai-base-url', 'openaiBaseUrl', true);
this.setupChangeHandler('.openai-default-model', 'openaiDefaultModel');
// Anthropic options
this.setupChangeHandler('.anthropic-api-key', 'anthropicApiKey', true);
this.setupChangeHandler('.anthropic-default-model', 'anthropicDefaultModel');
this.setupChangeHandler('.anthropic-base-url', 'anthropicBaseUrl');
// Voyage options
this.setupChangeHandler('.voyage-api-key', 'voyageApiKey');
// Ollama options
this.setupChangeHandler('.ollama-base-url', 'ollamaBaseUrl');
this.setupChangeHandler('.ollama-default-model', 'ollamaDefaultModel');
const $refreshModels = this.$widget.find('.refresh-models');
$refreshModels.on('click', async () => {
this.ollamaModelsRefreshed = await this.providerService?.refreshOllamaModels(true, this.ollamaModelsRefreshed) || false;
});
// Add tab change handler for Ollama tab
const $ollamaTab = this.$widget.find('#nav-ollama-tab');
$ollamaTab.on('shown.bs.tab', async () => {
// Only refresh the models if we haven't done it before
this.ollamaModelsRefreshed = await this.providerService?.refreshOllamaModels(false, this.ollamaModelsRefreshed) || false;
});
// OpenAI models refresh button
const $refreshOpenAIModels = this.$widget.find('.refresh-openai-models');
$refreshOpenAIModels.on('click', async () => {
this.openaiModelsRefreshed = await this.providerService?.refreshOpenAIModels(true, this.openaiModelsRefreshed) || false;
});
// Add tab change handler for OpenAI tab
const $openaiTab = this.$widget.find('#nav-openai-tab');
$openaiTab.on('shown.bs.tab', async () => {
// Only refresh the models if we haven't done it before
this.openaiModelsRefreshed = await this.providerService?.refreshOpenAIModels(false, this.openaiModelsRefreshed) || false;
});
// Anthropic models refresh button
const $refreshAnthropicModels = this.$widget.find('.refresh-anthropic-models');
$refreshAnthropicModels.on('click', async () => {
this.anthropicModelsRefreshed = await this.providerService?.refreshAnthropicModels(true, this.anthropicModelsRefreshed) || false;
});
// Add tab change handler for Anthropic tab
const $anthropicTab = this.$widget.find('#nav-anthropic-tab');
$anthropicTab.on('shown.bs.tab', async () => {
// Only refresh the models if we haven't done it before
this.anthropicModelsRefreshed = await this.providerService?.refreshAnthropicModels(false, this.anthropicModelsRefreshed) || false;
});
// Add provider selection change handlers for dynamic settings visibility
this.$widget.find('.ai-selected-provider').on('change', async () => {
const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string;
this.$widget.find('.provider-settings').hide();
if (selectedProvider) {
this.$widget.find(`.${selectedProvider}-provider-settings`).show();
// Automatically fetch models for the newly selected provider
await this.fetchModelsForProvider(selectedProvider, 'chat');
}
});
// Add base URL change handlers to trigger model fetching
this.$widget.find('.openai-base-url').on('change', async () => {
const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string;
if (selectedProvider === 'openai') {
await this.fetchModelsForProvider('openai', 'chat');
}
});
this.$widget.find('.anthropic-base-url').on('change', async () => {
const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string;
if (selectedProvider === 'anthropic') {
await this.fetchModelsForProvider('anthropic', 'chat');
}
});
this.$widget.find('.ollama-base-url').on('change', async () => {
const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string;
if (selectedProvider === 'ollama') {
await this.fetchModelsForProvider('ollama', 'chat');
}
});
// Add API key change handlers to trigger model fetching
this.$widget.find('.openai-api-key').on('change', async () => {
const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string;
if (selectedProvider === 'openai') {
await this.fetchModelsForProvider('openai', 'chat');
}
});
this.$widget.find('.anthropic-api-key').on('change', async () => {
const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string;
if (selectedProvider === 'anthropic') {
await this.fetchModelsForProvider('anthropic', 'chat');
}
});
}
/**
* Display warnings for validation issues with providers
*/
async displayValidationWarnings() {
if (!this.$widget) return;
const $warningDiv = this.$widget.find('.provider-validation-warning');
// Check if AI is enabled
const aiEnabled = this.$widget.find('.ai-enabled').prop('checked');
if (!aiEnabled) {
$warningDiv.hide();
return;
}
// Get selected provider
const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string;
// Start with experimental warning
const allWarnings = [
t("ai_llm.experimental_warning")
];
// Check for selected provider configuration
const providerWarnings: string[] = [];
if (selectedProvider === 'openai') {
const openaiApiKey = this.$widget.find('.openai-api-key').val();
if (!openaiApiKey) {
providerWarnings.push(t("ai_llm.empty_key_warning.openai"));
}
} else if (selectedProvider === 'anthropic') {
const anthropicApiKey = this.$widget.find('.anthropic-api-key').val();
if (!anthropicApiKey) {
providerWarnings.push(t("ai_llm.empty_key_warning.anthropic"));
}
} else if (selectedProvider === 'ollama') {
const ollamaBaseUrl = this.$widget.find('.ollama-base-url').val();
if (!ollamaBaseUrl) {
providerWarnings.push(t("ai_llm.ollama_no_url"));
}
}
// Add provider warnings to all warnings
allWarnings.push(...providerWarnings);
// Show or hide warnings
if (allWarnings.length > 0) {
const warningHtml = '<strong>' + t("ai_llm.configuration_warnings") + '</strong><ul>' +
allWarnings.map(warning => `<li>${warning}</li>`).join('') + '</ul>';
$warningDiv.html(warningHtml).show();
} else {
$warningDiv.hide();
}
}
/**
* Helper to get display name for providers
*/
getProviderDisplayName(provider: string): string {
switch(provider) {
case 'openai': return 'OpenAI';
case 'anthropic': return 'Anthropic';
case 'ollama': return 'Ollama';
case 'voyage': return 'Voyage';
case 'local': return 'Local';
default: return provider.charAt(0).toUpperCase() + provider.slice(1);
}
}
/**
* Set model dropdown value, adding the option if it doesn't exist
*/
setModelDropdownValue(selector: string, value: string | undefined) {
if (!this.$widget || !value) return;
const $dropdown = this.$widget.find(selector);
// Check if the value already exists as an option
if ($dropdown.find(`option[value="${value}"]`).length === 0) {
// Add the custom value as an option
$dropdown.append(`<option value="${value}">${value} (current)</option>`);
}
// Set the value
$dropdown.val(value);
}
/**
* Fetch models for a specific provider and model type
*/
async fetchModelsForProvider(provider: string, modelType: 'chat') {
if (!this.providerService) return;
try {
switch (provider) {
case 'openai':
this.openaiModelsRefreshed = await this.providerService.refreshOpenAIModels(false, this.openaiModelsRefreshed);
break;
case 'anthropic':
this.anthropicModelsRefreshed = await this.providerService.refreshAnthropicModels(false, this.anthropicModelsRefreshed);
break;
case 'ollama':
this.ollamaModelsRefreshed = await this.providerService.refreshOllamaModels(false, this.ollamaModelsRefreshed);
break;
default:
console.log(`Model fetching not implemented for provider: ${provider}`);
}
} catch (error) {
console.error(`Error fetching models for ${provider}:`, error);
}
}
/**
* Update provider settings visibility based on selected providers
*/
updateProviderSettingsVisibility() {
if (!this.$widget) return;
// Update AI provider settings visibility
const selectedAiProvider = this.$widget.find('.ai-selected-provider').val() as string;
this.$widget.find('.provider-settings').hide();
if (selectedAiProvider) {
this.$widget.find(`.${selectedAiProvider}-provider-settings`).show();
}
}
/**
* Called when the options have been loaded from the server
*/
async optionsLoaded(options: OptionMap) {
if (!this.$widget) return;
// AI Options
this.$widget.find('.ai-enabled').prop('checked', options.aiEnabled !== 'false');
this.$widget.find('.ai-temperature').val(options.aiTemperature || '0.7');
this.$widget.find('.ai-system-prompt').val(options.aiSystemPrompt || '');
this.$widget.find('.ai-selected-provider').val(options.aiSelectedProvider || 'openai');
// OpenAI Section
this.$widget.find('.openai-api-key').val(options.openaiApiKey || '');
this.$widget.find('.openai-base-url').val(options.openaiBaseUrl || 'https://api.openai.com/v1');
this.setModelDropdownValue('.openai-default-model', options.openaiDefaultModel);
// Anthropic Section
this.$widget.find('.anthropic-api-key').val(options.anthropicApiKey || '');
this.$widget.find('.anthropic-base-url').val(options.anthropicBaseUrl || 'https://api.anthropic.com');
this.setModelDropdownValue('.anthropic-default-model', options.anthropicDefaultModel);
// Voyage Section
this.$widget.find('.voyage-api-key').val(options.voyageApiKey || '');
// Ollama Section
this.$widget.find('.ollama-base-url').val(options.ollamaBaseUrl || 'http://localhost:11434');
this.setModelDropdownValue('.ollama-default-model', options.ollamaDefaultModel);
// Show/hide provider settings based on selected providers
this.updateProviderSettingsVisibility();
// Automatically fetch models for currently selected providers
const selectedAiProvider = this.$widget.find('.ai-selected-provider').val() as string;
if (selectedAiProvider) {
await this.fetchModelsForProvider(selectedAiProvider, 'chat');
}
// Display validation warnings
this.displayValidationWarnings();
}
cleanup() {
// Cleanup method for widget
}
}

View File

@@ -1,2 +0,0 @@
import AiSettingsWidget from './ai_settings_widget.js';
export default AiSettingsWidget;

View File

@@ -1,31 +0,0 @@
// Interface for the Ollama model response
export interface OllamaModelResponse {
success: boolean;
models: Array<{
name: string;
model: string;
details?: {
family?: string;
parameter_size?: string;
}
}>;
}
export interface OpenAIModelResponse {
success: boolean;
chatModels: Array<{
id: string;
name: string;
type: string;
}>;
}
export interface AnthropicModelResponse {
success: boolean;
chatModels: Array<{
id: string;
name: string;
type: string;
}>;
}

View File

@@ -1,252 +0,0 @@
import server from "../../../../services/server.js";
import toastService from "../../../../services/toast.js";
import { t } from "../../../../services/i18n.js";
import options from "../../../../services/options.js";
import type { OpenAIModelResponse, AnthropicModelResponse, OllamaModelResponse } from "./interfaces.js";
export class ProviderService {
constructor(private $widget: JQuery<HTMLElement>) {
// AI provider settings
}
/**
* Ensures the dropdown has the correct value set, prioritizing:
* 1. Current UI value if present
* 2. Value from database options if available
* 3. Falling back to first option if neither is available
*/
private ensureSelectedValue($select: JQuery<HTMLElement>, currentValue: string | number | string[] | undefined | null, optionName: string) {
if (currentValue) {
$select.val(currentValue);
// If the value doesn't exist anymore, select the first option
if (!$select.val()) {
$select.prop('selectedIndex', 0);
}
} else {
// If no current value exists in the dropdown but there's a default in the database
const savedModel = options.get(optionName);
if (savedModel) {
$select.val(savedModel);
// If the saved model isn't in the dropdown, select the first option
if (!$select.val()) {
$select.prop('selectedIndex', 0);
}
}
}
}
/**
* Refreshes the list of OpenAI models
* @param showLoading Whether to show loading indicators and toasts
* @param openaiModelsRefreshed Reference to track if models have been refreshed
* @returns Promise that resolves when the refresh is complete
*/
async refreshOpenAIModels(showLoading: boolean, openaiModelsRefreshed: boolean): Promise<boolean> {
if (!this.$widget) return false;
const $refreshOpenAIModels = this.$widget.find('.refresh-openai-models');
// If we've already refreshed and we're not forcing a refresh, don't do it again
if (openaiModelsRefreshed && !showLoading) {
return openaiModelsRefreshed;
}
if (showLoading) {
$refreshOpenAIModels.prop('disabled', true);
$refreshOpenAIModels.html(`<i class="spinner-border spinner-border-sm"></i>`);
}
try {
const openaiBaseUrl = this.$widget.find('.openai-base-url').val() as string;
const response = await server.get<OpenAIModelResponse>(`llm/providers/openai/models?baseUrl=${encodeURIComponent(openaiBaseUrl)}`);
if (response && response.success) {
// Update the chat models dropdown
if (response.chatModels?.length > 0) {
const $chatModelSelect = this.$widget.find('.openai-default-model');
const currentChatValue = $chatModelSelect.val();
// Clear existing options
$chatModelSelect.empty();
// Sort models by name
const sortedChatModels = [...response.chatModels].sort((a, b) => a.name.localeCompare(b.name));
// Add models to the dropdown
sortedChatModels.forEach(model => {
$chatModelSelect.append(`<option value="${model.id}">${model.name}</option>`);
});
// Try to restore the previously selected value
this.ensureSelectedValue($chatModelSelect, currentChatValue, 'openaiDefaultModel');
}
if (showLoading) {
// Show success message
const totalModels = (response.chatModels?.length || 0);
toastService.showMessage(`${totalModels} OpenAI models found.`);
}
return true;
} else if (showLoading) {
toastService.showError(`No OpenAI models found. Please check your API key and settings.`);
}
return openaiModelsRefreshed;
} catch (e) {
console.error(`Error fetching OpenAI models:`, e);
if (showLoading) {
toastService.showError(`Error fetching OpenAI models: ${e}`);
}
return openaiModelsRefreshed;
} finally {
if (showLoading) {
$refreshOpenAIModels.prop('disabled', false);
$refreshOpenAIModels.html(`<span class="bx bx-refresh"></span>`);
}
}
}
/**
* Refreshes the list of Anthropic models
* @param showLoading Whether to show loading indicators and toasts
* @param anthropicModelsRefreshed Reference to track if models have been refreshed
* @returns Promise that resolves when the refresh is complete
*/
async refreshAnthropicModels(showLoading: boolean, anthropicModelsRefreshed: boolean): Promise<boolean> {
if (!this.$widget) return false;
const $refreshAnthropicModels = this.$widget.find('.refresh-anthropic-models');
// If we've already refreshed and we're not forcing a refresh, don't do it again
if (anthropicModelsRefreshed && !showLoading) {
return anthropicModelsRefreshed;
}
if (showLoading) {
$refreshAnthropicModels.prop('disabled', true);
$refreshAnthropicModels.html(`<i class="spinner-border spinner-border-sm"></i>`);
}
try {
const anthropicBaseUrl = this.$widget.find('.anthropic-base-url').val() as string;
const response = await server.get<AnthropicModelResponse>(`llm/providers/anthropic/models?baseUrl=${encodeURIComponent(anthropicBaseUrl)}`);
if (response && response.success) {
// Update the chat models dropdown
if (response.chatModels?.length > 0) {
const $chatModelSelect = this.$widget.find('.anthropic-default-model');
const currentChatValue = $chatModelSelect.val();
// Clear existing options
$chatModelSelect.empty();
// Sort models by name
const sortedChatModels = [...response.chatModels].sort((a, b) => a.name.localeCompare(b.name));
// Add models to the dropdown
sortedChatModels.forEach(model => {
$chatModelSelect.append(`<option value="${model.id}">${model.name}</option>`);
});
// Try to restore the previously selected value
this.ensureSelectedValue($chatModelSelect, currentChatValue, 'anthropicDefaultModel');
}
if (showLoading) {
// Show success message
const totalModels = (response.chatModels?.length || 0);
toastService.showMessage(`${totalModels} Anthropic models found.`);
}
return true;
} else if (showLoading) {
toastService.showError(`No Anthropic models found. Please check your API key and settings.`);
}
return anthropicModelsRefreshed;
} catch (e) {
console.error(`Error fetching Anthropic models:`, e);
if (showLoading) {
toastService.showError(`Error fetching Anthropic models: ${e}`);
}
return anthropicModelsRefreshed;
} finally {
if (showLoading) {
$refreshAnthropicModels.prop('disabled', false);
$refreshAnthropicModels.html(`<span class="bx bx-refresh"></span>`);
}
}
}
/**
* Refreshes the list of Ollama models
* @param showLoading Whether to show loading indicators and toasts
* @param ollamaModelsRefreshed Reference to track if models have been refreshed
* @returns Promise that resolves when the refresh is complete
*/
async refreshOllamaModels(showLoading: boolean, ollamaModelsRefreshed: boolean): Promise<boolean> {
if (!this.$widget) return false;
const $refreshModels = this.$widget.find('.refresh-models');
// If we've already refreshed and we're not forcing a refresh, don't do it again
if (ollamaModelsRefreshed && !showLoading) {
return ollamaModelsRefreshed;
}
if (showLoading) {
$refreshModels.prop('disabled', true);
$refreshModels.text(t("ai_llm.refreshing_models"));
}
try {
// Use the general Ollama base URL
const ollamaBaseUrl = this.$widget.find('.ollama-base-url').val() as string;
const response = await server.get<OllamaModelResponse>(`llm/providers/ollama/models?baseUrl=${encodeURIComponent(ollamaBaseUrl)}`);
if (response && response.success && response.models && response.models.length > 0) {
// Update the LLM model dropdown
const $modelSelect = this.$widget.find('.ollama-default-model');
const currentModelValue = $modelSelect.val();
// Clear existing options
$modelSelect.empty();
// Sort models by name to make them easier to find
const sortedModels = [...response.models].sort((a, b) => a.name.localeCompare(b.name));
// Add all models to the dropdown
sortedModels.forEach(model => {
$modelSelect.append(`<option value="${model.name}">${model.name}</option>`);
});
// Try to restore the previously selected value
this.ensureSelectedValue($modelSelect, currentModelValue, 'ollamaDefaultModel');
if (showLoading) {
toastService.showMessage(`${response.models.length} Ollama models found.`);
}
return true;
} else if (showLoading) {
toastService.showError(`No Ollama models found. Please check if Ollama is running.`);
}
return ollamaModelsRefreshed;
} catch (e) {
console.error(`Error fetching Ollama models:`, e);
if (showLoading) {
toastService.showError(`Error fetching Ollama models: ${e}`);
}
return ollamaModelsRefreshed;
} finally {
if (showLoading) {
$refreshModels.prop('disabled', false);
$refreshModels.html(`<span class="bx bx-refresh"></span>`);
}
}
}
}

View File

@@ -1,135 +0,0 @@
import { t } from "../../../../services/i18n.js";
export const TPL = `
<div class="options-section">
<h4>${t("ai_llm.title")}</h4>
<!-- Add warning alert div -->
<div class="provider-validation-warning alert alert-warning" style="display: none;"></div>
<div class="form-group">
<label class="tn-checkbox">
<input class="ai-enabled form-check-input" type="checkbox">
${t("ai_llm.enable_ai_features")}
</label>
<div class="form-text">${t("ai_llm.enable_ai_description")}</div>
</div>
</div>
<!-- AI settings template -->
<div class="ai-providers-section options-section">
<h4>${t("ai_llm.provider_configuration")}</h4>
<div class="form-group">
<label>${t("ai_llm.selected_provider")}</label>
<select class="ai-selected-provider form-control">
<option value="">${t("ai_llm.select_provider")}</option>
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="ollama">Ollama</option>
</select>
<div class="form-text">${t("ai_llm.selected_provider_description")}</div>
</div>
<!-- OpenAI Provider Settings -->
<div class="provider-settings openai-provider-settings" style="display: none;">
<div class="card mt-3">
<div class="card-header">
<h5>${t("ai_llm.openai_settings")}</h5>
</div>
<div class="card-body">
<div class="form-group">
<label>${t("ai_llm.api_key")}</label>
<input type="password" class="openai-api-key form-control" autocomplete="off" />
<div class="form-text">${t("ai_llm.openai_api_key_description")}</div>
</div>
<div class="form-group">
<label>${t("ai_llm.url")}</label>
<input type="text" class="openai-base-url form-control" />
<div class="form-text">${t("ai_llm.openai_url_description")}</div>
</div>
<div class="form-group">
<label>${t("ai_llm.model")}</label>
<select class="openai-default-model form-control">
<option value="">${t("ai_llm.select_model")}</option>
</select>
<div class="form-text">${t("ai_llm.openai_model_description")}</div>
<button class="btn btn-sm btn-outline-secondary refresh-openai-models">${t("ai_llm.refresh_models")}</button>
</div>
</div>
</div>
</div>
<!-- Anthropic Provider Settings -->
<div class="provider-settings anthropic-provider-settings" style="display: none;">
<div class="card mt-3">
<div class="card-header">
<h5>${t("ai_llm.anthropic_settings")}</h5>
</div>
<div class="card-body">
<div class="form-group">
<label>${t("ai_llm.api_key")}</label>
<input type="password" class="anthropic-api-key form-control" autocomplete="off" />
<div class="form-text">${t("ai_llm.anthropic_api_key_description")}</div>
</div>
<div class="form-group">
<label>${t("ai_llm.url")}</label>
<input type="text" class="anthropic-base-url form-control" />
<div class="form-text">${t("ai_llm.anthropic_url_description")}</div>
</div>
<div class="form-group">
<label>${t("ai_llm.model")}</label>
<select class="anthropic-default-model form-control">
<option value="">${t("ai_llm.select_model")}</option>
</select>
<div class="form-text">${t("ai_llm.anthropic_model_description")}</div>
<button class="btn btn-sm btn-outline-secondary refresh-anthropic-models">${t("ai_llm.refresh_models")}</button>
</div>
</div>
</div>
</div>
<!-- Ollama Provider Settings -->
<div class="provider-settings ollama-provider-settings" style="display: none;">
<div class="card mt-3">
<div class="card-header">
<h5>${t("ai_llm.ollama_settings")}</h5>
</div>
<div class="card-body">
<div class="form-group">
<label>${t("ai_llm.url")}</label>
<input type="text" class="ollama-base-url form-control" />
<div class="form-text">${t("ai_llm.ollama_url_description")}</div>
</div>
<div class="form-group">
<label>${t("ai_llm.model")}</label>
<select class="ollama-default-model form-control">
<option value="">${t("ai_llm.select_model")}</option>
</select>
<div class="form-text">${t("ai_llm.ollama_model_description")}</div>
<button class="btn btn-sm btn-outline-secondary refresh-models"><span class="bx bx-refresh"></span></button>
</div>
</div>
</div>
</div>
<div class="form-group">
<label>${t("ai_llm.temperature")}</label>
<input class="ai-temperature form-control" type="number" min="0" max="2" step="0.1">
<div class="form-text">${t("ai_llm.temperature_description")}</div>
</div>
<div class="form-group">
<label>${t("ai_llm.system_prompt")}</label>
<textarea class="ai-system-prompt form-control" rows="3"></textarea>
<div class="form-text">${t("ai_llm.system_prompt_description")}</div>
</div>
</div>
`;

View File

@@ -0,0 +1,295 @@
import { useEffect, useState } from "preact/hooks";
import { t } from "../../../services/i18n";
import { isElectron, isMobile, reloadFrontendApp, restartDesktopApp } from "../../../services/utils";
import Column from "../../react/Column";
import FormRadioGroup from "../../react/FormRadioGroup";
import FormSelect, { FormSelectWithGroups } from "../../react/FormSelect";
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
import OptionsSection from "./components/OptionsSection";
import server from "../../../services/server";
import FormCheckbox from "../../react/FormCheckbox";
import FormGroup from "../../react/FormGroup";
import { FontFamily, OptionNames } from "@triliumnext/commons";
import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox";
import FormText from "../../react/FormText";
import Button from "../../react/Button";
import RelatedSettings from "./components/RelatedSettings";
const MIN_CONTENT_WIDTH = 640;
interface Theme {
val: string;
title: string;
noteId?: string;
}
const BUILTIN_THEMES: Theme[] = [
{ val: "next", title: t("theme.triliumnext") },
{ val: "next-light", title: t("theme.triliumnext-light") },
{ val: "next-dark", title: t("theme.triliumnext-dark") },
{ val: "auto", title: t("theme.auto_theme") },
{ val: "light", title: t("theme.light_theme") },
{ val: "dark", title: t("theme.dark_theme") }
]
interface FontFamilyEntry {
value: FontFamily;
label?: string;
}
interface FontGroup {
title: string;
items: FontFamilyEntry[];
}
const FONT_FAMILIES: FontGroup[] = [
{
title: t("fonts.generic-fonts"),
items: [
{ value: "theme", label: t("fonts.theme_defined") },
{ value: "system", label: t("fonts.system-default") },
{ value: "serif", label: t("fonts.serif") },
{ value: "sans-serif", label: t("fonts.sans-serif") },
{ value: "monospace", label: t("fonts.monospace") }
]
},
{
title: t("fonts.sans-serif-system-fonts"),
items: [{ value: "Arial" }, { value: "Verdana" }, { value: "Helvetica" }, { value: "Tahoma" }, { value: "Trebuchet MS" }, { value: "Microsoft YaHei" }]
},
{
title: t("fonts.serif-system-fonts"),
items: [{ value: "Times New Roman" }, { value: "Georgia" }, { value: "Garamond" }]
},
{
title: t("fonts.monospace-system-fonts"),
items: [
{ value: "Courier New" },
{ value: "Brush Script MT" },
{ value: "Impact" },
{ value: "American Typewriter" },
{ value: "Andalé Mono" },
{ value: "Lucida Console" },
{ value: "Monaco" }
]
},
{
title: t("fonts.handwriting-system-fonts"),
items: [{ value: "Bradley Hand" }, { value: "Luminari" }, { value: "Comic Sans MS" }]
}
];
export default function AppearanceSettings() {
const [ overrideThemeFonts ] = useTriliumOption("overrideThemeFonts");
return (
<div>
{!isMobile() && <LayoutOrientation />}
<ApplicationTheme />
{overrideThemeFonts === "true" && <Fonts />}
{isElectron() && <ElectronIntegration /> }
<Performance />
<MaxContentWidth />
<RelatedSettings items={[
{
title: t("settings_appearance.related_code_blocks"),
targetPage: "_optionsTextNotes"
},
{
title: t("settings_appearance.related_code_notes"),
targetPage: "_optionsCodeNotes"
}
]} />
</div>
)
}
function LayoutOrientation() {
const [ layoutOrientation, setLayoutOrientation ] = useTriliumOption("layoutOrientation", true);
return (
<OptionsSection title={t("theme.layout")}>
<FormRadioGroup
name="layout-orientation"
values={[
{
label: t("theme.layout-vertical-title"),
inlineDescription: t("theme.layout-vertical-description"),
value: "vertical"
},
{
label: t("theme.layout-horizontal-title"),
inlineDescription: t("theme.layout-horizontal-description"),
value: "horizontal"
}
]}
currentValue={layoutOrientation} onChange={setLayoutOrientation}
/>
</OptionsSection>
);
}
function ApplicationTheme() {
const [ theme, setTheme ] = useTriliumOption("theme", true);
const [ overrideThemeFonts, setOverrideThemeFonts ] = useTriliumOptionBool("overrideThemeFonts");
const [ themes, setThemes ] = useState<Theme[]>([]);
useEffect(() => {
server.get<Theme[]>("options/user-themes").then((userThemes) => {
setThemes([
...BUILTIN_THEMES,
...userThemes
])
});
}, []);
return (
<OptionsSection title={t("theme.title")}>
<div className="row">
<FormGroup name="theme" label={t("theme.theme_label")} className="col-md-6" style={{ marginBottom: 0 }}>
<FormSelect
values={themes} currentValue={theme} onChange={setTheme}
keyProperty="val" titleProperty="title"
/>
</FormGroup>
<FormGroup className="side-checkbox col-md-6" name="override-theme-fonts">
<FormCheckbox
label={t("theme.override_theme_fonts_label")}
currentValue={overrideThemeFonts} onChange={setOverrideThemeFonts} />
</FormGroup>
</div>
</OptionsSection>
)
}
function Fonts() {
return (
<OptionsSection title={t("fonts.fonts")}>
<Font title={t("fonts.main_font")} fontFamilyOption="mainFontFamily" fontSizeOption="mainFontSize" />
<Font title={t("fonts.note_tree_font")} fontFamilyOption="treeFontFamily" fontSizeOption="treeFontSize" />
<Font title={t("fonts.note_detail_font")} fontFamilyOption="detailFontFamily" fontSizeOption="detailFontSize" />
<Font title={t("fonts.monospace_font")} fontFamilyOption="monospaceFontFamily" fontSizeOption="monospaceFontSize" />
<FormText>{t("fonts.note_tree_and_detail_font_sizing")}</FormText>
<FormText>{t("fonts.not_all_fonts_available")}</FormText>
<p>
{t("fonts.apply_font_changes")} <Button text={t("fonts.reload_frontend")} size="micro" onClick={reloadFrontendApp} />
</p>
</OptionsSection>
);
}
function Font({ title, fontFamilyOption, fontSizeOption }: { title: string, fontFamilyOption: OptionNames, fontSizeOption: OptionNames }) {
const [ fontFamily, setFontFamily ] = useTriliumOption(fontFamilyOption);
const [ fontSize, setFontSize ] = useTriliumOption(fontSizeOption);
return (
<>
<h5>{title}</h5>
<div className="row">
<FormGroup name="font-family" className="col-md-4" label={t("fonts.font_family")}>
<FormSelectWithGroups
values={FONT_FAMILIES}
currentValue={fontFamily} onChange={setFontFamily}
keyProperty="value" titleProperty="label"
/>
</FormGroup>
<FormGroup name="font-size" className="col-md-6" label={t("fonts.size")}>
<FormTextBoxWithUnit
name="tree-font-size"
type="number" min={50} max={200} step={10}
currentValue={fontSize} onChange={setFontSize}
unit={t("units.percentage")}
/>
</FormGroup>
</div>
</>
);
}
function ElectronIntegration() {
const [ zoomFactor, setZoomFactor ] = useTriliumOption("zoomFactor");
const [ nativeTitleBarVisible, setNativeTitleBarVisible ] = useTriliumOptionBool("nativeTitleBarVisible");
const [ backgroundEffects, setBackgroundEffects ] = useTriliumOptionBool("backgroundEffects");
return (
<OptionsSection title={t("electron_integration.desktop-application")}>
<FormGroup name="zoom-factor" label={t("electron_integration.zoom-factor")} description={t("zoom_factor.description")}>
<FormTextBox
type="number"
min="0.3" max="2.0" step="0.1"
currentValue={zoomFactor} onChange={setZoomFactor}
/>
</FormGroup>
<hr/>
<FormGroup name="native-title-bar" description={t("electron_integration.native-title-bar-description")}>
<FormCheckbox
label={t("electron_integration.native-title-bar")}
currentValue={nativeTitleBarVisible} onChange={setNativeTitleBarVisible}
/>
</FormGroup>
<FormGroup name="background-effects" description={t("electron_integration.background-effects-description")}>
<FormCheckbox
label={t("electron_integration.background-effects")}
currentValue={backgroundEffects} onChange={setBackgroundEffects}
/>
</FormGroup>
<Button text={t("electron_integration.restart-app-button")} onClick={restartDesktopApp} />
</OptionsSection>
)
}
function Performance() {
const [ motionEnabled, setMotionEnabled ] = useTriliumOptionBool("motionEnabled");
const [ shadowsEnabled, setShadowsEnabled ] = useTriliumOptionBool("shadowsEnabled");
const [ backdropEffectsEnabled, setBackdropEffectsEnabled ] = useTriliumOptionBool("backdropEffectsEnabled");
return <OptionsSection title={t("ui-performance.title")}>
<FormCheckbox
label={t("ui-performance.enable-motion")}
currentValue={motionEnabled} onChange={setMotionEnabled}
/>
<FormCheckbox
label={t("ui-performance.enable-shadows")}
currentValue={shadowsEnabled} onChange={setShadowsEnabled}
/>
<FormCheckbox
label={t("ui-performance.enable-backdrop-effects")}
currentValue={backdropEffectsEnabled} onChange={setBackdropEffectsEnabled}
/>
</OptionsSection>
}
function MaxContentWidth() {
const [ maxContentWidth, setMaxContentWidth ] = useTriliumOption("maxContentWidth");
return (
<OptionsSection title={t("max_content_width.title")}>
<FormText>{t("max_content_width.default_description")}</FormText>
<Column md={6}>
<FormGroup name="max-content-width" label={t("max_content_width.max_width_label")}>
<FormTextBoxWithUnit
type="number" min={MIN_CONTENT_WIDTH} step="10"
currentValue={maxContentWidth} onChange={setMaxContentWidth}
unit={t("max_content_width.max_width_unit")}
/>
</FormGroup>
</Column>
<p>
{t("max_content_width.apply_changes_description")} <Button text={t("max_content_width.reload_button")} size="micro" onClick={reloadFrontendApp} />
</p>
</OptionsSection>
)
}

View File

@@ -1,76 +0,0 @@
import OptionsWidget from "../options_widget.js";
import { t } from "../../../../services/i18n.js";
import utils from "../../../../services/utils.js";
import type { OptionMap } from "@triliumnext/commons";
const TPL = /*html*/`
<div class="options-section">
<h4>${t("electron_integration.desktop-application")}</h4>
<div class="form-group row">
<div class="col-12">
<label for="zoom-factor-select">${t("electron_integration.zoom-factor")}</label>
<input id="zoom-factor-select" type="number" class="zoom-factor-select form-control options-number-input" min="0.3" max="2.0" step="0.1"/>
<p class="form-text">${t("zoom_factor.description")}</p>
</div>
</div>
<hr />
<div>
<label class="form-check tn-checkbox">
<input type="checkbox" class="native-title-bar form-check-input" />
${t("electron_integration.native-title-bar")}
</label>
<p class="form-text">
${t("electron_integration.native-title-bar-description")}
</p>
</div>
<div>
<label class="form-check tn-checkbox">
<input type="checkbox" class="background-effects form-check-input" />
${t("electron_integration.background-effects")}
</label>
<p class="form-text">
${t("electron_integration.background-effects-description")}
</p>
</div>
<button class="btn btn-secondary btn-micro restart-app-button">${t("electron_integration.restart-app-button")}</button>
</div>
`;
export default class ElectronIntegrationOptions extends OptionsWidget {
private $zoomFactorSelect!: JQuery<HTMLElement>;
private $nativeTitleBar!: JQuery<HTMLElement>;
private $backgroundEffects!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.$zoomFactorSelect = this.$widget.find(".zoom-factor-select");
this.$zoomFactorSelect.on("change", () => {
this.triggerCommand("setZoomFactorAndSave", { zoomFactor: String(this.$zoomFactorSelect.val()) });
});
this.$nativeTitleBar = this.$widget.find("input.native-title-bar");
this.$nativeTitleBar.on("change", () => this.updateCheckboxOption("nativeTitleBarVisible", this.$nativeTitleBar));
this.$backgroundEffects = this.$widget.find("input.background-effects");
this.$backgroundEffects.on("change", () => this.updateCheckboxOption("backgroundEffects", this.$backgroundEffects));
const restartAppButton = this.$widget.find(".restart-app-button");
restartAppButton.on("click", utils.restartDesktopApp);
}
isEnabled() {
return utils.isElectron();
}
async optionsLoaded(options: OptionMap) {
this.$zoomFactorSelect.val(options.zoomFactor);
this.setCheckboxState(this.$nativeTitleBar, options.nativeTitleBarVisible);
this.setCheckboxState(this.$backgroundEffects, options.backgroundEffects);
}
}

View File

@@ -1,218 +0,0 @@
import OptionsWidget from "../options_widget.js";
import utils from "../../../../services/utils.js";
import { t } from "../../../../services/i18n.js";
import type { FontFamily, OptionMap, OptionNames } from "@triliumnext/commons";
interface FontFamilyEntry {
value: FontFamily;
label?: string;
}
interface FontGroup {
title: string;
items: FontFamilyEntry[];
}
const FONT_FAMILIES: FontGroup[] = [
{
title: t("fonts.generic-fonts"),
items: [
{ value: "theme", label: t("fonts.theme_defined") },
{ value: "system", label: t("fonts.system-default") },
{ value: "serif", label: t("fonts.serif") },
{ value: "sans-serif", label: t("fonts.sans-serif") },
{ value: "monospace", label: t("fonts.monospace") }
]
},
{
title: t("fonts.sans-serif-system-fonts"),
items: [{ value: "Arial" }, { value: "Verdana" }, { value: "Helvetica" }, { value: "Tahoma" }, { value: "Trebuchet MS" }, { value: "Microsoft YaHei" }]
},
{
title: t("fonts.serif-system-fonts"),
items: [{ value: "Times New Roman" }, { value: "Georgia" }, { value: "Garamond" }]
},
{
title: t("fonts.monospace-system-fonts"),
items: [
{ value: "Courier New" },
{ value: "Brush Script MT" },
{ value: "Impact" },
{ value: "American Typewriter" },
{ value: "Andalé Mono" },
{ value: "Lucida Console" },
{ value: "Monaco" }
]
},
{
title: t("fonts.handwriting-system-fonts"),
items: [{ value: "Bradley Hand" }, { value: "Luminari" }, { value: "Comic Sans MS" }]
}
];
const TPL = /*html*/`
<div class="options-section">
<h4>${t("fonts.fonts")}</h4>
<h5>${t("fonts.main_font")}</h5>
<div class="form-group row">
<div class="col-4">
<label for="main-font-family">${t("fonts.font_family")}</label>
<select id="main-font-family" class="main-font-family form-select"></select>
</div>
<div class="col-6">
<label for="main-font-size">${t("fonts.size")}</label>
<label class="input-group tn-number-unit-pair main-font-size-input-group">
<input id="main-font-size" type="number" class="main-font-size form-control options-number-input" min="50" max="200" step="10"/>
<span class="input-group-text">%</span>
</label>
</div>
</div>
<h5>${t("fonts.note_tree_font")}</h5>
<div class="form-group row">
<div class="col-4">
<label for="tree-font-family">${t("fonts.font_family")}</label>
<select id="tree-font-family" class="tree-font-family form-select"></select>
</div>
<div class="col-6">
<label for="tree-font-size">${t("fonts.size")}</label>
<label class="input-group tn-number-unit-pair tree-font-size-input-group">
<input id="tree-font-size" type="number" class="tree-font-size form-control options-number-input" min="50" max="200" step="10"/>
<span class="input-group-text">%</span>
</label>
</div>
</div>
<h5>${t("fonts.note_detail_font")}</h5>
<div class="form-group row">
<div class="col-4">
<label for="detail-font-family">${t("fonts.font_family")}</label>
<select id="detail-font-family" class="detail-font-family form-select"></select>
</div>
<div class="col-6">
<label for="detail-font-size">${t("fonts.size")}</label>
<label class="input-group tn-number-unit-pair detail-font-size-input-group">
<input id="detail-font-size" type="number" class="detail-font-size form-control options-number-input" min="50" max="200" step="10"/>
<span class="input-group-text">%</span>
</label>
</div>
</div>
<h5>${t("fonts.monospace_font")}</h5>
<div class="form-group row">
<div class="col-4">
<label for="monospace-font-family">${t("fonts.font_family")}</label>
<select id="monospace-font-family" class="monospace-font-family form-select"></select>
</div>
<div class="col-6">
<label for="monospace-font-size">${t("fonts.size")}</label>
<label class="input-group tn-number-unit-pair monospace-font-size-input-group">
<input id="monospace-font-size" type="number" class="monospace-font-size form-control options-number-input" min="50" max="200" step="10"/>
<span class="input-group-text">%</span>
</label>
</div>
</div>
<p class="form-text">${t("fonts.note_tree_and_detail_font_sizing")}</p>
<p class="form-text">${t("fonts.not_all_fonts_available")}</p>
<p>
${t("fonts.apply_font_changes")}
<button class="btn btn-secondary btn-micro reload-frontend-button">${t("fonts.reload_frontend")}</button>
</p>
</div>`;
export default class FontsOptions extends OptionsWidget {
private $mainFontSize!: JQuery<HTMLElement>;
private $mainFontFamily!: JQuery<HTMLElement>;
private $treeFontSize!: JQuery<HTMLElement>;
private $treeFontFamily!: JQuery<HTMLElement>;
private $detailFontSize!: JQuery<HTMLElement>;
private $detailFontFamily!: JQuery<HTMLElement>;
private $monospaceFontSize!: JQuery<HTMLElement>;
private $monospaceFontFamily!: JQuery<HTMLElement>;
private _isEnabled?: boolean;
doRender() {
this.$widget = $(TPL);
this.$mainFontSize = this.$widget.find(".main-font-size");
this.$mainFontFamily = this.$widget.find(".main-font-family");
this.$treeFontSize = this.$widget.find(".tree-font-size");
this.$treeFontFamily = this.$widget.find(".tree-font-family");
this.$detailFontSize = this.$widget.find(".detail-font-size");
this.$detailFontFamily = this.$widget.find(".detail-font-family");
this.$monospaceFontSize = this.$widget.find(".monospace-font-size");
this.$monospaceFontFamily = this.$widget.find(".monospace-font-family");
this.$widget.find(".reload-frontend-button").on("click", () => utils.reloadFrontendApp("changes from appearance options"));
}
isEnabled() {
return !!this._isEnabled;
}
async optionsLoaded(options: OptionMap) {
this._isEnabled = options.overrideThemeFonts === "true";
this.toggleInt(this._isEnabled);
if (!this._isEnabled) {
return;
}
this.$mainFontSize.val(options.mainFontSize);
this.fillFontFamilyOptions(this.$mainFontFamily, options.mainFontFamily);
this.$treeFontSize.val(options.treeFontSize);
this.fillFontFamilyOptions(this.$treeFontFamily, options.treeFontFamily);
this.$detailFontSize.val(options.detailFontSize);
this.fillFontFamilyOptions(this.$detailFontFamily, options.detailFontFamily);
this.$monospaceFontSize.val(options.monospaceFontSize);
this.fillFontFamilyOptions(this.$monospaceFontFamily, options.monospaceFontFamily);
const optionsToSave: OptionNames[] = ["mainFontFamily", "mainFontSize", "treeFontFamily", "treeFontSize", "detailFontFamily", "detailFontSize", "monospaceFontFamily", "monospaceFontSize"];
for (const optionName of optionsToSave) {
const $el = (this as any)[`$${optionName}`];
$el.on("change", () => this.updateOption(optionName, $el.val()));
}
}
fillFontFamilyOptions($select: JQuery<HTMLElement>, currentValue: string) {
$select.empty();
for (const { title, items } of Object.values(FONT_FAMILIES)) {
const $group = $("<optgroup>").attr("label", title);
for (const { value, label } of items) {
$group.append(
$("<option>")
.attr("value", value)
.prop("selected", value === currentValue)
.text(label ?? value)
);
}
$select.append($group);
}
}
}

View File

@@ -1,47 +0,0 @@
import OptionsWidget from "../options_widget.js";
import utils from "../../../../services/utils.js";
import { t } from "../../../../services/i18n.js";
import type { OptionMap } from "@triliumnext/commons";
const MIN_VALUE = 640;
const TPL = /*html*/`
<div class="options-section">
<h4>${t("max_content_width.title")}</h4>
<p class="form-text">${t("max_content_width.default_description")}</p>
<div class="form-group row">
<div class="col-md-6">
<label for="max-content-width">${t("max_content_width.max_width_label")}</label>
<label class="input-group tn-number-unit-pair">
<input id="max-content-width" type="number" min="${MIN_VALUE}" step="10" class="max-content-width form-control options-number-input">
<span class="input-group-text">${t("max_content_width.max_width_unit")}</span>
</label>
</div>
</div>
<p>
${t("max_content_width.apply_changes_description")}
<button class="btn btn-secondary btn-micro reload-frontend-button">${t("max_content_width.reload_button")}</button>
</p>
</div>`;
export default class MaxContentWidthOptions extends OptionsWidget {
private $maxContentWidth!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.$maxContentWidth = this.$widget.find(".max-content-width");
this.$maxContentWidth.on("change", async () => this.updateOption("maxContentWidth", String(this.$maxContentWidth.val())));
this.$widget.find(".reload-frontend-button").on("click", () => utils.reloadFrontendApp(t("max_content_width.reload_description")));
}
async optionsLoaded(options: OptionMap) {
this.$maxContentWidth.val(Math.max(MIN_VALUE, parseInt(options.maxContentWidth, 10)));
}
}

View File

@@ -1,71 +0,0 @@
import type { OptionPages } from "../../content_widget";
import OptionsWidget from "../options_widget";
const TPL = `\
<div class="options-section">
<h4>Related settings</h4>
<nav class="related-settings use-tn-links">
<li>Color scheme for code blocks in text notes</li>
<li>Color scheme for code notes</li>
</nav>
<style>
.related-settings {
padding: 0;
margin: 0;
list-style-type: none;
}
</style>
</div>
`;
interface RelatedSettingsConfig {
items: {
title: string;
targetPage: OptionPages;
}[];
}
const RELATED_SETTINGS: Record<string, RelatedSettingsConfig> = {
"_optionsAppearance": {
items: [
{
title: "Color scheme for code blocks in text notes",
targetPage: "_optionsTextNotes"
},
{
title: "Color scheme for code notes",
targetPage: "_optionsCodeNotes"
}
]
}
};
export default class RelatedSettings extends OptionsWidget {
doRender() {
this.$widget = $(TPL);
const config = this.noteId && RELATED_SETTINGS[this.noteId];
if (!config) {
return;
}
const $relatedSettings = this.$widget.find(".related-settings");
$relatedSettings.empty();
for (const item of config.items) {
const $item = $("<li>");
const $link = $("<a>").text(item.title);
$item.append($link);
$link.attr("href", `#root/_hidden/_options/${item.targetPage}`);
$relatedSettings.append($item);
}
}
isEnabled() {
return (!!this.noteId && this.noteId in RELATED_SETTINGS);
}
}

View File

@@ -1,42 +0,0 @@
import type { OptionMap } from "@triliumnext/commons";
import { t } from "../../../../services/i18n.js";
import OptionsWidget from "../options_widget.js";
const TPL = /*html*/`
<div class="options-section">
<h4>${t("ribbon.widgets")}</h4>
<div>
<label class="tn-checkbox">
<input type="checkbox" class="promoted-attributes-open-in-ribbon form-check-input">
${t("ribbon.promoted_attributes_message")}
</label>
</div>
<div>
<label class="tn-checkbox">
<input type="checkbox" class="edited-notes-open-in-ribbon form-check-input">
${t("ribbon.edited_notes_message")}
</label>
</div>
</div>`;
export default class RibbonOptions extends OptionsWidget {
private $promotedAttributesOpenInRibbon!: JQuery<HTMLElement>;
private $editedNotesOpenInRibbon!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.$promotedAttributesOpenInRibbon = this.$widget.find(".promoted-attributes-open-in-ribbon");
this.$promotedAttributesOpenInRibbon.on("change", () => this.updateCheckboxOption("promotedAttributesOpenInRibbon", this.$promotedAttributesOpenInRibbon));
this.$editedNotesOpenInRibbon = this.$widget.find(".edited-notes-open-in-ribbon");
this.$editedNotesOpenInRibbon.on("change", () => this.updateCheckboxOption("editedNotesOpenInRibbon", this.$editedNotesOpenInRibbon));
}
async optionsLoaded(options: OptionMap) {
this.setCheckboxState(this.$promotedAttributesOpenInRibbon, options.promotedAttributesOpenInRibbon);
this.setCheckboxState(this.$editedNotesOpenInRibbon, options.editedNotesOpenInRibbon);
}
}

View File

@@ -1,112 +0,0 @@
import OptionsWidget from "../options_widget.js";
import server from "../../../../services/server.js";
import utils from "../../../../services/utils.js";
import { t } from "../../../../services/i18n.js";
import type { OptionMap } from "@triliumnext/commons";
const TPL = /*html*/`
<div class="options-section">
<h4>${t("theme.layout")}</h4>
<div class="form-group row">
<div>
<label class="tn-radio">
<input type="radio" name="layout-orientation" value="vertical" />
<strong>${t("theme.layout-vertical-title")}</strong>
- ${t("theme.layout-vertical-description")}
</label>
</div>
<div>
<label class="tn-radio">
<input type="radio" name="layout-orientation" value="horizontal" />
<strong>${t("theme.layout-horizontal-title")}</strong>
- ${t("theme.layout-horizontal-description")}
</label>
</div>
</div>
</div>
<div class="options-section">
<h4>${t("theme.title")}</h4>
<div class="form-group row">
<div class="col-md-6">
<label for="theme-select">${t("theme.theme_label")}</label>
<select id="theme-select" class="theme-select form-select"></select>
</div>
<div class="col-md-6 side-checkbox">
<label class="form-check tn-checkbox">
<input type="checkbox" class="override-theme-fonts form-check-input">
${t("theme.override_theme_fonts_label")}
</label>
</div>
</div>
</div>
`;
interface Theme {
val: string;
title: string;
noteId?: string;
}
export default class ThemeOptions extends OptionsWidget {
private $themeSelect!: JQuery<HTMLElement>;
private $overrideThemeFonts!: JQuery<HTMLElement>;
private $layoutOrientation!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.$themeSelect = this.$widget.find(".theme-select");
this.$overrideThemeFonts = this.$widget.find(".override-theme-fonts");
this.$layoutOrientation = this.$widget.find(`input[name="layout-orientation"]`).on("change", async () => {
const newLayoutOrientation = String(this.$widget.find(`input[name="layout-orientation"]:checked`).val());
await this.updateOption("layoutOrientation", newLayoutOrientation);
utils.reloadFrontendApp("layout orientation change");
});
const $layoutOrientationSection = $(this.$widget[0]);
$layoutOrientationSection.toggleClass("hidden-ext", utils.isMobile());
this.$themeSelect.on("change", async () => {
const newTheme = this.$themeSelect.val();
await server.put(`options/theme/${newTheme}`);
utils.reloadFrontendApp("theme change");
});
this.$overrideThemeFonts.on("change", () => this.updateCheckboxOption("overrideThemeFonts", this.$overrideThemeFonts));
}
async optionsLoaded(options: OptionMap) {
const themes: Theme[] = [
{ val: "next", title: t("theme.triliumnext") },
{ val: "next-light", title: t("theme.triliumnext-light") },
{ val: "next-dark", title: t("theme.triliumnext-dark") },
{ val: "auto", title: t("theme.auto_theme") },
{ val: "light", title: t("theme.light_theme") },
{ val: "dark", title: t("theme.dark_theme") }
].concat(await server.get<Theme[]>("options/user-themes"));
this.$themeSelect.empty();
for (const theme of themes) {
this.$themeSelect.append(
$("<option>")
.attr("value", theme.val)
.attr("data-note-id", theme.noteId || "")
.text(theme.title)
);
}
this.$themeSelect.val(options.theme);
this.setCheckboxState(this.$overrideThemeFonts, options.overrideThemeFonts);
this.$widget.find(`input[name="layout-orientation"][value="${options.layoutOrientation}"]`).prop("checked", "true");
}
}

View File

@@ -1,149 +0,0 @@
import { formatDateTime } from "../../../utils/formatters.js";
import { t } from "../../../services/i18n.js";
import OptionsWidget from "./options_widget.js";
import server from "../../../services/server.js";
import toastService from "../../../services/toast.js";
import type { OptionMap } from "@triliumnext/commons";
const TPL = /*html*/`
<div class="options-section">
<h4>${t("backup.automatic_backup")}</h4>
<p>${t("backup.automatic_backup_description")}</p>
<ul style="list-style: none">
<li>
<label class="tn-checkbox">
<input type="checkbox" class="daily-backup-enabled form-check-input">
${t("backup.enable_daily_backup")}
</label>
</li>
<li>
<label class="tn-checkbox">
<input type="checkbox" class="weekly-backup-enabled form-check-input">
${t("backup.enable_weekly_backup")}
</label>
</li>
<li>
<label class="tn-checkbox">
<input type="checkbox" class="monthly-backup-enabled form-check-input">
${t("backup.enable_monthly_backup")}
</label>
</li>
</ul>
<p class="form-text">${t("backup.backup_recommendation")}</p>
</div>
<div class="options-section">
<h4>${t("backup.backup_now")}</h4>
<button class="backup-database-button btn btn-secondary">${t("backup.backup_database_now")}</button>
</div>
<div class="options-section">
<h4>${t("backup.existing_backups")}</h4>
<table class="table table-stripped">
<colgroup>
<col width="33%" />
<col />
</colgroup>
<thead>
<tr>
<th>${t("backup.date-and-time")}</th>
<th>${t("backup.path")}</th>
</tr>
</thead>
<tbody class="existing-backup-list-items">
</tbody>
</table>
</div>
`;
// TODO: Deduplicate.
interface PostDatabaseResponse {
backupFile: string;
}
// TODO: Deduplicate
interface Backup {
filePath: string;
mtime: number;
}
export default class BackupOptions extends OptionsWidget {
private $backupDatabaseButton!: JQuery<HTMLElement>;
private $dailyBackupEnabled!: JQuery<HTMLElement>;
private $weeklyBackupEnabled!: JQuery<HTMLElement>;
private $monthlyBackupEnabled!: JQuery<HTMLElement>;
private $existingBackupList!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.$backupDatabaseButton = this.$widget.find(".backup-database-button");
this.$backupDatabaseButton.on("click", async () => {
const { backupFile } = await server.post<PostDatabaseResponse>("database/backup-database");
toastService.showMessage(t("backup.database_backed_up_to", { backupFilePath: backupFile }), 10000);
this.refresh();
});
this.$dailyBackupEnabled = this.$widget.find(".daily-backup-enabled");
this.$weeklyBackupEnabled = this.$widget.find(".weekly-backup-enabled");
this.$monthlyBackupEnabled = this.$widget.find(".monthly-backup-enabled");
this.$dailyBackupEnabled.on("change", () => this.updateCheckboxOption("dailyBackupEnabled", this.$dailyBackupEnabled));
this.$weeklyBackupEnabled.on("change", () => this.updateCheckboxOption("weeklyBackupEnabled", this.$weeklyBackupEnabled));
this.$monthlyBackupEnabled.on("change", () => this.updateCheckboxOption("monthlyBackupEnabled", this.$monthlyBackupEnabled));
this.$existingBackupList = this.$widget.find(".existing-backup-list-items");
}
optionsLoaded(options: OptionMap) {
this.setCheckboxState(this.$dailyBackupEnabled, options.dailyBackupEnabled);
this.setCheckboxState(this.$weeklyBackupEnabled, options.weeklyBackupEnabled);
this.setCheckboxState(this.$monthlyBackupEnabled, options.monthlyBackupEnabled);
server.get<Backup[]>("database/backups").then((backupFiles) => {
this.$existingBackupList.empty();
if (!backupFiles.length) {
this.$existingBackupList.append(
$(`
<tr>
<td class="empty-table-placeholder" colspan="2">${t("backup.no_backup_yet")}</td>
</tr>
`)
);
return;
}
// Sort the backup files by modification date & time in a desceding order
backupFiles.sort((a, b) => {
if (a.mtime < b.mtime) return 1;
if (a.mtime > b.mtime) return -1;
return 0;
});
for (const { filePath, mtime } of backupFiles) {
this.$existingBackupList.append(
$(`
<tr>
<td>${mtime ? formatDateTime(mtime) : "-"}</td>
<td>${filePath}</td>
</tr>
`)
);
}
});
}
}

View File

@@ -0,0 +1,119 @@
import { BackupDatabaseNowResponse, DatabaseBackup } from "@triliumnext/commons";
import { t } from "../../../services/i18n";
import server from "../../../services/server";
import toast from "../../../services/toast";
import Button from "../../react/Button";
import FormCheckbox from "../../react/FormCheckbox";
import FormGroup, { FormMultiGroup } from "../../react/FormGroup";
import FormText from "../../react/FormText";
import { useTriliumOptionBool } from "../../react/hooks";
import OptionsSection from "./components/OptionsSection";
import { useCallback, useEffect, useState } from "preact/hooks";
import { formatDateTime } from "../../../utils/formatters";
export default function BackupSettings() {
const [ backups, setBackups ] = useState<DatabaseBackup[]>([]);
const refreshBackups = useCallback(() => {
server.get<DatabaseBackup[]>("database/backups").then((backupFiles) => {
// Sort the backup files by modification date & time in a desceding order
backupFiles.sort((a, b) => {
if (a.mtime < b.mtime) return 1;
if (a.mtime > b.mtime) return -1;
return 0;
});
setBackups(backupFiles);
});
}, [ setBackups ]);
useEffect(refreshBackups, []);
return (
<>
<AutomaticBackup />
<BackupNow refreshCallback={refreshBackups} />
<BackupList backups={backups} />
</>
)
}
export function AutomaticBackup() {
const [ dailyBackupEnabled, setDailyBackupEnabled ] = useTriliumOptionBool("dailyBackupEnabled");
const [ weeklyBackupEnabled, setWeeklyBackupEnabled ] = useTriliumOptionBool("weeklyBackupEnabled");
const [ monthlyBackupEnabled, setMonthlyBackupEnabled ] = useTriliumOptionBool("monthlyBackupEnabled");
return (
<OptionsSection title={t("backup.automatic_backup")}>
<FormMultiGroup label={t("backup.automatic_backup_description")}>
<FormCheckbox
name="daily-backup-enabled"
label={t("backup.enable_daily_backup")}
currentValue={dailyBackupEnabled} onChange={setDailyBackupEnabled}
/>
<FormCheckbox
name="weekly-backup-enabled"
label={t("backup.enable_weekly_backup")}
currentValue={weeklyBackupEnabled} onChange={setWeeklyBackupEnabled}
/>
<FormCheckbox
name="monthly-backup-enabled"
label={t("backup.enable_monthly_backup")}
currentValue={monthlyBackupEnabled} onChange={setMonthlyBackupEnabled}
/>
</FormMultiGroup>
<FormText>{t("backup.backup_recommendation")}</FormText>
</OptionsSection>
)
}
export function BackupNow({ refreshCallback }: { refreshCallback: () => void }) {
return (
<OptionsSection title={t("backup.backup_now")}>
<Button
text={t("backup.backup_database_now")}
onClick={async () => {
const { backupFile } = await server.post<BackupDatabaseNowResponse>("database/backup-database");
toast.showMessage(t("backup.database_backed_up_to", { backupFilePath: backupFile }), 10000);
refreshCallback();
}}
/>
</OptionsSection>
)
}
export function BackupList({ backups }: { backups: DatabaseBackup[] }) {
return (
<OptionsSection title={t("backup.existing_backups")}>
<table class="table table-stripped">
<colgroup>
<col width="33%" />
<col />
</colgroup>
<thead>
<tr>
<th>{t("backup.date-and-time")}</th>
<th>{t("backup.path")}</th>
</tr>
</thead>
<tbody>
{ backups.length > 0 ? (
backups.map(({ mtime, filePath }) => (
<tr>
<td>{mtime ? formatDateTime(mtime) : "-"}</td>
<td>{filePath}</td>
</tr>
))
) : (
<tr>
<td className="empty-table-placeholder" colspan={2}>{t("backup.no_backup_yet")}</td>
</tr>
)}
</tbody>
</table>
</OptionsSection>
);
}

View File

@@ -0,0 +1,164 @@
import CodeMirror, { ColorThemes, getThemeById } from "@triliumnext/codemirror";
import { t } from "../../../services/i18n";
import Column from "../../react/Column";
import FormCheckbox from "../../react/FormCheckbox";
import FormGroup from "../../react/FormGroup";
import FormSelect from "../../react/FormSelect";
import { useTriliumOption, useTriliumOptionBool, useTriliumOptionJson } from "../../react/hooks";
import OptionsSection from "./components/OptionsSection";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import codeNoteSample from "./samples/code_note.txt?raw";
import { DEFAULT_PREFIX } from "../abstract_code_type_widget";
import { MimeType } from "@triliumnext/commons";
import mime_types from "../../../services/mime_types";
import CheckboxList from "./components/CheckboxList";
import AutoReadOnlySize from "./components/AutoReadOnlySize";
const SAMPLE_MIME = "application/typescript";
export default function CodeNoteSettings() {
return (
<>
<Editor />
<Appearance />
<CodeMimeTypes />
<AutoReadOnlySize option="autoReadonlySizeCode" label={t("code_auto_read_only_size.label")} />
</>
)
}
function Editor() {
const [ vimKeymapEnabled, setVimKeymapEnabled ] = useTriliumOptionBool("vimKeymapEnabled");
return (
<OptionsSection title={t("code-editor-options.title")}>
<FormGroup name="vim-keymap-enabled" description={t("vim_key_bindings.enable_vim_keybindings")}>
<FormCheckbox
label={t("vim_key_bindings.use_vim_keybindings_in_code_notes")}
currentValue={vimKeymapEnabled} onChange={setVimKeymapEnabled}
/>
</FormGroup>
</OptionsSection>
)
}
function Appearance() {
const [ codeNoteTheme, setCodeNoteTheme ] = useTriliumOption("codeNoteTheme");
const [ codeLineWrapEnabled, setCodeLineWrapEnabled ] = useTriliumOptionBool("codeLineWrapEnabled");
const themes = useMemo(() => {
return ColorThemes.map(({ id, name }) => ({
id: "default:" + id,
name
}));
}, []);
return (
<OptionsSection title={t("code_theme.title")}>
<div className="row" style={{ marginBottom: "15px" }}>
<FormGroup name="color-scheme" label={t("code_theme.color-scheme")} className="col-md-6" style={{ marginBottom: 0 }}>
<FormSelect
values={themes}
keyProperty="id" titleProperty="name"
currentValue={codeNoteTheme} onChange={setCodeNoteTheme}
/>
</FormGroup>
<Column className="side-checkbox">
<FormCheckbox
name="word-wrap"
label={t("code_theme.word_wrapping")}
currentValue={codeLineWrapEnabled} onChange={setCodeLineWrapEnabled}
/>
</Column>
</div>
<CodeNotePreview wordWrapping={codeLineWrapEnabled} themeName={codeNoteTheme} />
</OptionsSection>
);
}
function CodeNotePreview({ themeName, wordWrapping }: { themeName: string, wordWrapping: boolean }) {
const editorRef = useRef<CodeMirror>(null);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) {
return;
}
// Clean up previous instance.
editorRef.current?.destroy();
containerRef.current.innerHTML = "";
// Set up a new instance.
const editor = new CodeMirror({
parent: containerRef.current
});
editor.setText(codeNoteSample);
editor.setMimeType(SAMPLE_MIME);
editorRef.current = editor;
}, []);
useEffect(() => {
editorRef.current?.setLineWrapping(wordWrapping);
}, [ wordWrapping ]);
useEffect(() => {
if (themeName?.startsWith(DEFAULT_PREFIX)) {
const theme = getThemeById(themeName.substring(DEFAULT_PREFIX.length));
if (theme) {
editorRef.current?.setTheme(theme);
}
}
}, [ themeName ]);
return (
<div
ref={containerRef}
class="note-detail-readonly-code-content"
style={{ margin: 0, height: "200px" }}
/>
);
}
function CodeMimeTypes() {
const [ codeNotesMimeTypes, setCodeNotesMimeTypes ] = useTriliumOptionJson<string[]>("codeNotesMimeTypes");
const sectionStyle = useMemo(() => ({ marginBottom: "1em", breakInside: "avoid-column" }), []);
const groupedMimeTypes: Record<string, MimeType[]> = useMemo(() => {
mime_types.loadMimeTypes();
const ungroupedMimeTypes = Array.from(mime_types.getMimeTypes());
const plainTextMimeType = ungroupedMimeTypes.shift();
const result: Record<string, MimeType[]> = {};
ungroupedMimeTypes.sort((a, b) => a.title.localeCompare(b.title));
result[""] = [ plainTextMimeType! ];
for (const mimeType of ungroupedMimeTypes) {
const initial = mimeType.title.charAt(0).toUpperCase();
if (!result[initial]) {
result[initial] = [];
}
result[initial].push(mimeType);
}
return result;
}, []);
return (
<OptionsSection title={t("code_mime_types.title")}>
<ul class="options-mime-types" style={{ listStyleType: "none", columnWidth: "250px" }}>
{Object.entries(groupedMimeTypes).map(([ initial, mimeTypes ]) => (
<section style={sectionStyle}>
{ initial && <h5>{initial}</h5> }
<CheckboxList
values={mimeTypes}
keyProperty="mime" titleProperty="title"
currentValue={codeNotesMimeTypes} onChange={setCodeNotesMimeTypes}
columnWidth="inherit"
/>
</section>
))}
</ul>
</OptionsSection>
)
}

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