mirror of
https://github.com/zadam/trilium.git
synced 2025-12-17 21:59:55 +01:00
Compare commits
393 Commits
feature/el
...
fix/try-to
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
055556891d | ||
|
|
a58cfbec05 | ||
|
|
3795be4750 | ||
|
|
3111738700 | ||
|
|
1fa0bada23 | ||
|
|
11eca7e58b | ||
|
|
4b50e2f14d | ||
|
|
63faba9603 | ||
|
|
c88ff07691 | ||
|
|
b53aa5cf6e | ||
|
|
2641b9b3fe | ||
|
|
3a2a73992c | ||
|
|
b82b17a701 | ||
|
|
a6202edcd1 | ||
|
|
6eac0cb75d | ||
|
|
83672d6138 | ||
|
|
51dadf72d0 | ||
|
|
0cbf61acb3 | ||
|
|
b192f43187 | ||
|
|
8cb8d1303c | ||
|
|
5237348975 | ||
|
|
72e2f6757e | ||
|
|
cf059e7f86 | ||
|
|
44d69216b6 | ||
|
|
9373d47e86 | ||
|
|
29350628c3 | ||
|
|
83d1a68879 | ||
|
|
f188408099 | ||
|
|
449ab3a798 | ||
|
|
21504d1417 | ||
|
|
3060b496e3 | ||
|
|
bd35539fa1 | ||
|
|
df6447e3ad | ||
|
|
24fd898f0d | ||
|
|
1aa6238288 | ||
|
|
c16c4788da | ||
|
|
0c35daab85 | ||
|
|
4a19639e92 | ||
|
|
36cceea677 | ||
|
|
b32a344a21 | ||
|
|
3896ab822f | ||
|
|
cfa4ba57d4 | ||
|
|
da051e0269 | ||
|
|
3eda77a91f | ||
|
|
5c2f4be5dd | ||
|
|
435b501db9 | ||
|
|
5a27ffef5f | ||
|
|
02256d9a45 | ||
|
|
7e069009d6 | ||
|
|
3c25cda4c0 | ||
|
|
70d7ad0b1a | ||
|
|
e793b2f661 | ||
|
|
a6e7dff61e | ||
|
|
86d1bbe8ff | ||
|
|
a10cb06f14 | ||
|
|
dd9a62818b | ||
|
|
c0e936675c | ||
|
|
b0b788b7dc | ||
|
|
18f0f3ecac | ||
|
|
e7d745ac94 | ||
|
|
24abf7f0ed | ||
|
|
9a08f6534b | ||
|
|
93c5413790 | ||
|
|
c97c66ed8a | ||
|
|
b581025bbe | ||
|
|
7bc5331747 | ||
|
|
2415976475 | ||
|
|
8d0d0f0449 | ||
|
|
16b00ed160 | ||
|
|
df73a420f9 | ||
|
|
1e4d57f275 | ||
|
|
19a238c8d3 | ||
|
|
5ffd8a79eb | ||
|
|
58e58c192f | ||
|
|
5939344378 | ||
|
|
349f19fef7 | ||
|
|
d5777a024e | ||
|
|
b7f4ee6171 | ||
|
|
a83c4e3970 | ||
|
|
5a767dae34 | ||
|
|
9f93d30b99 | ||
|
|
dff525edc6 | ||
|
|
26da431320 | ||
|
|
cde4622693 | ||
|
|
5ede7ecc69 | ||
|
|
513878dfef | ||
|
|
753d5529b2 | ||
|
|
4e755dc537 | ||
|
|
5351310a38 | ||
|
|
211ca43a82 | ||
|
|
e5235e7f22 | ||
|
|
e72298f0b4 | ||
|
|
3abf5c65c6 | ||
|
|
268acb0b88 | ||
|
|
196b3b873f | ||
|
|
4d9801a372 | ||
|
|
bd710ba665 | ||
|
|
afe369c876 | ||
|
|
206007bbce | ||
|
|
8ad05b92c0 | ||
|
|
735da2a855 | ||
|
|
980077f559 | ||
|
|
5daca270e4 | ||
|
|
e18813a4bf | ||
|
|
4aa7e211f3 | ||
|
|
419dc7edfb | ||
|
|
eaa84a6b39 | ||
|
|
1d0503d0e4 | ||
|
|
f7f98aa9a3 | ||
|
|
575d14261a | ||
|
|
9aab606deb | ||
|
|
2e11681b52 | ||
|
|
8cca6637f7 | ||
|
|
82e076378c | ||
|
|
94ddad3c49 | ||
|
|
d35dbca18b | ||
|
|
7468d6147a | ||
|
|
7c78d749de | ||
|
|
85dd99a3c4 | ||
|
|
0a9c0234e2 | ||
|
|
fad77ba5a0 | ||
|
|
12723f3216 | ||
|
|
a43140515f | ||
|
|
3e3cc8c541 | ||
|
|
d1538508e8 | ||
|
|
9b1da8c311 | ||
|
|
e4a8258acf | ||
|
|
5e88043c7b | ||
|
|
bedf9112fb | ||
|
|
03681d23c5 | ||
|
|
aa191e110c | ||
|
|
dd09907925 | ||
|
|
35e9508bde | ||
|
|
4c8da70ef3 | ||
|
|
ed5da5cd4a | ||
|
|
dc5fccdbcd | ||
|
|
91aea333c7 | ||
|
|
a0de01cff1 | ||
|
|
a41ed34193 | ||
|
|
49e8811c18 | ||
|
|
488563a82e | ||
|
|
a1b18c7f97 | ||
|
|
9958a6e1bf | ||
|
|
1fc6d8aca7 | ||
|
|
3e9ec2d943 | ||
|
|
1420def1c3 | ||
|
|
3b4184e765 | ||
|
|
4ce9102f93 | ||
|
|
eb27ec2234 | ||
|
|
b70e25d348 | ||
|
|
772c0bbe1a | ||
|
|
144021c053 | ||
|
|
8abd3ed3f1 | ||
|
|
53ed510c92 | ||
|
|
4ec46a2ebd | ||
|
|
db6f948499 | ||
|
|
05c73011f5 | ||
|
|
3b733d01f1 | ||
|
|
ebf21296d4 | ||
|
|
6f83ac4822 | ||
|
|
d358924324 | ||
|
|
f9a3606ca2 | ||
|
|
33299ad51e | ||
|
|
8752182e7e | ||
|
|
0551ac8ead | ||
|
|
6d5a11bd4d | ||
|
|
ce19d84247 | ||
|
|
f24aa45a3b | ||
|
|
64a28a7e75 | ||
|
|
249a755312 | ||
|
|
a3d51a013c | ||
|
|
839def9959 | ||
|
|
fd432a7100 | ||
|
|
60a07ce1e7 | ||
|
|
88c5700d87 | ||
|
|
d59993abf6 | ||
|
|
0754011909 | ||
|
|
376bb66cab | ||
|
|
588e15c633 | ||
|
|
93b8ad20d7 | ||
|
|
e51b3d760d | ||
|
|
91f3bc4488 | ||
|
|
3e80a99bbf | ||
|
|
37cdb55e79 | ||
|
|
58b66c0c95 | ||
|
|
e5f9db86a1 | ||
|
|
f138f99356 | ||
|
|
c42f4b9814 | ||
|
|
0a9fb886e3 | ||
|
|
3c4577201f | ||
|
|
816421188f | ||
|
|
5b15d2c4c6 | ||
|
|
4bc7165452 | ||
|
|
82d6531e8c | ||
|
|
d6209035c3 | ||
|
|
1d7799f981 | ||
|
|
51291a61e6 | ||
|
|
0841603be0 | ||
|
|
59ba6a0b1e | ||
|
|
53eda46043 | ||
|
|
cbc9fb7d08 | ||
|
|
1f479b20be | ||
|
|
f00b8e9522 | ||
|
|
c7dd271516 | ||
|
|
a947a61d65 | ||
|
|
0122f1cc5e | ||
|
|
acb905a3e6 | ||
|
|
7422eb5598 | ||
|
|
e721166f95 | ||
|
|
5a48130fa4 | ||
|
|
b60fe1ad10 | ||
|
|
1405b0147c | ||
|
|
222a7a57bc | ||
|
|
cddf9f0242 | ||
|
|
3e17ff5e7b | ||
|
|
04973094f2 | ||
|
|
018a6cb84a | ||
|
|
44825af0c0 | ||
|
|
cfb3607052 | ||
|
|
c5ec928aac | ||
|
|
8d0183a9fb | ||
|
|
ecd4079871 | ||
|
|
3ed975f2e6 | ||
|
|
c6deb537d5 | ||
|
|
e7b3d806a7 | ||
|
|
d1a0778b48 | ||
|
|
378634567f | ||
|
|
ed56ed2be0 | ||
|
|
648aa7e3b0 | ||
|
|
73ff41f2b2 | ||
|
|
3837466cb3 | ||
|
|
b97a5ef888 | ||
|
|
2ff1276ebb | ||
|
|
227cf5de85 | ||
|
|
ccf52be431 | ||
|
|
07713e988c | ||
|
|
f934318625 | ||
|
|
6fb90abd75 | ||
|
|
27cc33888a | ||
|
|
95af901808 | ||
|
|
c5a7f84250 | ||
|
|
a71d28500d | ||
|
|
436fd16f3a | ||
|
|
ca34bf42f6 | ||
|
|
fbf2315f57 | ||
|
|
72f50dcb6b | ||
|
|
fd4c2f79a7 | ||
|
|
72f9335213 | ||
|
|
53d97047a3 | ||
|
|
2ba3666e23 | ||
|
|
4a1d379ab4 | ||
|
|
73167e1e30 | ||
|
|
ffc13f5de3 | ||
|
|
9ba23d49d8 | ||
|
|
222a6c48a7 | ||
|
|
e33208e6ec | ||
|
|
af8781eaa7 | ||
|
|
167b1a8d2e | ||
|
|
0a7aff507c | ||
|
|
103532aad9 | ||
|
|
16939e9fd5 | ||
|
|
4ef6169041 | ||
|
|
9ebee42118 | ||
|
|
234d3997b1 | ||
|
|
3ba0bcea4e | ||
|
|
701855344e | ||
|
|
71b627fbc7 | ||
|
|
5a4fc2c690 | ||
|
|
0d67db52a2 | ||
|
|
d971554201 | ||
|
|
8fd7d7176e | ||
|
|
675575eed9 | ||
|
|
2122cde293 | ||
|
|
b68a554bba | ||
|
|
33043c7133 | ||
|
|
2e0f606a7a | ||
|
|
87878dd6a7 | ||
|
|
5296e073cc | ||
|
|
7bfb7d6f6e | ||
|
|
b5069cc7c2 | ||
|
|
3b6791f51a | ||
|
|
0b0be77e02 | ||
|
|
60db10559e | ||
|
|
76b066ba4a | ||
|
|
a28db32369 | ||
|
|
2523632391 | ||
|
|
53548c356a | ||
|
|
565904ff5d | ||
|
|
e0c5545f8c | ||
|
|
bc21285289 | ||
|
|
bbf8d757cd | ||
|
|
318d504fad | ||
|
|
fd5038148c | ||
|
|
693ca9291e | ||
|
|
cfd8afc226 | ||
|
|
3e52ca7600 | ||
|
|
482522e802 | ||
|
|
8b5b6a01c6 | ||
|
|
5614891d92 | ||
|
|
b9b4961f3c | ||
|
|
7b83b20339 | ||
|
|
e4403dd316 | ||
|
|
3f267fe6c9 | ||
|
|
3229471485 | ||
|
|
62bac1adf9 | ||
|
|
82becfd52a | ||
|
|
92f035545b | ||
|
|
74d8ea7dcb | ||
|
|
ac3f087279 | ||
|
|
1cc4eb98c1 | ||
|
|
e99bdf8f24 | ||
|
|
b4f521a141 | ||
|
|
1e23bc09f1 | ||
|
|
e3ec90405d | ||
|
|
41c87794a4 | ||
|
|
e62d2d4fda | ||
|
|
93adaa0f52 | ||
|
|
263a5d2067 | ||
|
|
f0a5005794 | ||
|
|
577457c8ab | ||
|
|
c0c450c444 | ||
|
|
1e1e0b0f51 | ||
|
|
a19204a1d5 | ||
|
|
1d139bfdfe | ||
|
|
75072decec | ||
|
|
0cf2ad6901 | ||
|
|
ccbd57a0c0 | ||
|
|
92e6c8c445 | ||
|
|
1e966f1d59 | ||
|
|
6872c2194e | ||
|
|
5b6a0216db | ||
|
|
e9a7194cd6 | ||
|
|
26898b9122 | ||
|
|
3e00e490cf | ||
|
|
c02ed17ebc | ||
|
|
fb559d66fe | ||
|
|
25dce64c3b | ||
|
|
6f19fde76e | ||
|
|
33ae91f49c | ||
|
|
99c179e29a | ||
|
|
1dbcb5a027 | ||
|
|
54d613e00e | ||
|
|
1f8aa90482 | ||
|
|
c9dcbef014 | ||
|
|
68086ec3f1 | ||
|
|
f62078d02b | ||
|
|
ab1d8594ea | ||
|
|
c368ec3c38 | ||
|
|
1a15782686 | ||
|
|
3bd0aeef77 | ||
|
|
b463baedd2 | ||
|
|
ae77c41dab | ||
|
|
807d909acd | ||
|
|
fa4f5f526e | ||
|
|
edff43cdb3 | ||
|
|
46fe45528c | ||
|
|
b4b53da6a4 | ||
|
|
41fd270080 | ||
|
|
410bb3cdca | ||
|
|
bc6fc24fbd | ||
|
|
c039f06c2b | ||
|
|
520effbbb7 | ||
|
|
a42d780724 | ||
|
|
da92255dd6 | ||
|
|
cce3d3bce8 | ||
|
|
f524e99290 | ||
|
|
ba19fc7cf3 | ||
|
|
22c3de582f | ||
|
|
48896e67cb | ||
|
|
16cd91eb02 | ||
|
|
7e03774b8e | ||
|
|
a04f6e3858 | ||
|
|
96eb1be556 | ||
|
|
f8e20a1405 | ||
|
|
c67c3a6861 | ||
|
|
d04897e011 | ||
|
|
558ae1a2ea | ||
|
|
64bffb82b1 | ||
|
|
81ac390eab | ||
|
|
0db556fac2 | ||
|
|
2793df06c4 | ||
|
|
e7b448e2bc | ||
|
|
d2bc72d54f | ||
|
|
83b22b4861 | ||
|
|
d42a949602 | ||
|
|
83e1512b59 | ||
|
|
ba6a1ec584 | ||
|
|
6685e583f2 | ||
|
|
d6032c912e | ||
|
|
25527ecc21 | ||
|
|
e0e7bd42cc | ||
|
|
fbc1af56ed | ||
|
|
8ff108db9e |
1
.github/workflows/checks.yml
vendored
1
.github/workflows/checks.yml
vendored
@@ -12,6 +12,7 @@ jobs:
|
||||
steps:
|
||||
- name: Check if PRs have conflicts
|
||||
uses: eps1lon/actions-label-merge-conflict@v3
|
||||
if: github.repository == ${{ vars.REPO_MAIN }}
|
||||
with:
|
||||
dirtyLabel: "merge-conflicts"
|
||||
repoToken: "${{ secrets.MERGE_CONFLICT_LABEL_PAT }}"
|
||||
|
||||
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
@@ -27,7 +27,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
nightly-electron:
|
||||
if: github.repository == 'TriliumNext/Trilium'
|
||||
if: github.repository == ${{ vars.REPO_MAIN }}
|
||||
name: Deploy nightly
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
path: apps/desktop/upload
|
||||
|
||||
nightly-server:
|
||||
if: github.repository == 'TriliumNext/Trilium'
|
||||
if: github.repository == ${{ vars.REPO_MAIN }}
|
||||
name: Deploy server nightly
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
30
README.md
30
README.md
@@ -1,8 +1,8 @@
|
||||
# Trilium Notes
|
||||
|
||||
 
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) [](https://hosted.weblate.org/engage/trilium/)
|
||||
|
||||
[English](./README.md) | [Chinese (Simplified)](./docs/README-ZH_CN.md) | [Chinese (Traditional)](./docs/README-ZH_TW.md) | [Russian](./docs/README.ru.md) | [Japanese](./docs/README.ja.md) | [Italian](./docs/README.it.md) | [Spanish](./docs/README.es.md)
|
||||
@@ -46,15 +46,15 @@ See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for q
|
||||
- [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party themes, scripts, plugins and more.
|
||||
- [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more.
|
||||
|
||||
## ⚠️ Why TriliumNext?
|
||||
## ❓Why TriliumNext?
|
||||
|
||||
[The original Trilium project is in maintenance mode](https://github.com/zadam/trilium/issues/4620).
|
||||
The original Trilium developer ([Zadam](https://github.com/zadam)) has graciously given the Trilium repository to the community project which resides at https://github.com/TriliumNext
|
||||
|
||||
### Migrating from Trilium?
|
||||
### ⬆️Migrating from Zadam/Trilium?
|
||||
|
||||
There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Notes instance. Simply [install TriliumNext/Notes](#-installation) as usual and it will use your existing database.
|
||||
There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Trilium instance. Simply [install TriliumNext/Trilium](#-installation) as usual and it will use your existing database.
|
||||
|
||||
Versions up to and including [v0.90.4](https://github.com/TriliumNext/Notes/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext have their sync versions incremented.
|
||||
Versions up to and including [v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext/Trilium have their sync versions incremented which prevents direct migration.
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
@@ -75,8 +75,8 @@ Feel free to join our official conversations. We would love to hear what feature
|
||||
|
||||
- [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous discussions.)
|
||||
- The `General` Matrix room is also bridged to [XMPP](xmpp:discuss@trilium.thisgreat.party?join)
|
||||
- [Github Discussions](https://github.com/TriliumNext/Notes/discussions) (For asynchronous discussions.)
|
||||
- [Github Issues](https://github.com/TriliumNext/Notes/issues) (For bug reports and feature requests.)
|
||||
- [Github Discussions](https://github.com/TriliumNext/Trilium/discussions) (For asynchronous discussions.)
|
||||
- [Github Issues](https://github.com/TriliumNext/Trilium/issues) (For bug reports and feature requests.)
|
||||
|
||||
## 🏗 Installation
|
||||
|
||||
@@ -104,9 +104,11 @@ Currently only the latest versions of Chrome & Firefox are supported (and tested
|
||||
|
||||
To use TriliumNext on a mobile device, you can use a mobile web browser to access the mobile interface of a server installation (see below).
|
||||
|
||||
If you prefer a native Android app, you can use [TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid). Report bugs and missing features at [their repository](https://github.com/FliegendeWurst/TriliumDroid).
|
||||
See issue https://github.com/TriliumNext/Trilium/issues/4962 for more information on mobile app support.
|
||||
|
||||
See issue https://github.com/TriliumNext/Notes/issues/72 for more information on mobile app support.
|
||||
If you prefer a native Android app, you can use [TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid).
|
||||
Report bugs and missing features at [their repository](https://github.com/FliegendeWurst/TriliumDroid).
|
||||
Note: It is best to disable automatic updates on your server installation (see below) when using TriliumDroid since the sync version must match between Trilium and TriliumDroid.
|
||||
|
||||
### Server
|
||||
|
||||
@@ -152,11 +154,11 @@ pnpm install
|
||||
pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32
|
||||
```
|
||||
|
||||
For more details, see the [development docs](https://github.com/TriliumNext/Notes/blob/develop/docs/Developer%20Guide/Developer%20Guide/Building%20and%20deployment/Running%20a%20development%20build.md).
|
||||
For more details, see the [development docs](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide).
|
||||
|
||||
### Developer Documentation
|
||||
|
||||
Please view the [documentation guide](./docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md) for details. If you have more questions, feel free to reach out via the links described in the "Discuss with us" section above.
|
||||
Please view the [documentation guide](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md) for details. If you have more questions, feel free to reach out via the links described in the "Discuss with us" section above.
|
||||
|
||||
## 👏 Shoutouts
|
||||
|
||||
@@ -168,7 +170,7 @@ Please view the [documentation guide](./docs/Developer%20Guide/Developer%20Guide
|
||||
## 🤝 Support
|
||||
|
||||
Support for the TriliumNext organization will be possible in the near future. For now, you can:
|
||||
- Support continued development on TriliumNext by supporting our developers: [eliandoran](https://github.com/sponsors/eliandoran) (See the [repository insights]([developers]([url](https://github.com/TriliumNext/Notes/graphs/contributors))) for a full list)
|
||||
- Support continued development on TriliumNext by supporting our developers: [eliandoran](https://github.com/sponsors/eliandoran) (See the [repository insights]([developers]([url](https://github.com/TriliumNext/trilium/graphs/contributors))) for a full list)
|
||||
- Show a token of gratitude to the original Trilium developer ([zadam](https://github.com/sponsors/zadam)) via [PayPal](https://paypal.me/za4am) or Bitcoin (bitcoin:bc1qv3svjn40v89mnkre5vyvs2xw6y8phaltl385d2).
|
||||
|
||||
|
||||
|
||||
@@ -35,13 +35,13 @@
|
||||
"chore:generate-openapi": "tsx bin/generate-openapi.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.54.2",
|
||||
"@playwright/test": "1.55.0",
|
||||
"@stylistic/eslint-plugin": "5.2.3",
|
||||
"@types/express": "5.0.3",
|
||||
"@types/node": "22.17.1",
|
||||
"@types/node": "22.18.0",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"eslint": "9.33.0",
|
||||
"eslint": "9.34.0",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"esm": "3.2.25",
|
||||
"jsdoc": "4.0.4",
|
||||
@@ -49,7 +49,7 @@
|
||||
"rcedit": "4.0.1",
|
||||
"rimraf": "6.0.1",
|
||||
"tslib": "2.8.1",
|
||||
"typedoc": "0.28.10",
|
||||
"typedoc": "0.28.11",
|
||||
"typedoc-plugin-missing-exports": "4.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@triliumnext/client",
|
||||
"version": "0.97.2",
|
||||
"version": "0.98.1",
|
||||
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
@@ -10,7 +10,7 @@
|
||||
"url": "https://github.com/TriliumNext/Notes"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint/js": "9.33.0",
|
||||
"@eslint/js": "9.34.0",
|
||||
"@excalidraw/excalidraw": "0.18.0",
|
||||
"@fullcalendar/core": "6.1.19",
|
||||
"@fullcalendar/daygrid": "6.1.19",
|
||||
@@ -19,7 +19,7 @@
|
||||
"@fullcalendar/multimonth": "6.1.19",
|
||||
"@fullcalendar/timegrid": "6.1.19",
|
||||
"@maplibre/maplibre-gl-leaflet": "0.1.3",
|
||||
"@mermaid-js/layout-elk": "0.1.8",
|
||||
"@mermaid-js/layout-elk": "0.1.9",
|
||||
"@mind-elixir/node-menu": "5.0.0",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@triliumnext/ckeditor5": "workspace:*",
|
||||
@@ -28,7 +28,7 @@
|
||||
"@triliumnext/highlightjs": "workspace:*",
|
||||
"@triliumnext/share-theme": "workspace:*",
|
||||
"autocomplete.js": "0.38.1",
|
||||
"bootstrap": "5.3.7",
|
||||
"bootstrap": "5.3.8",
|
||||
"boxicons": "2.1.4",
|
||||
"dayjs": "1.11.13",
|
||||
"dayjs-plugin-utc": "0.1.2",
|
||||
@@ -36,7 +36,7 @@
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.50.1",
|
||||
"globals": "16.3.0",
|
||||
"i18next": "25.3.4",
|
||||
"i18next": "25.4.2",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "3.7.1",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
@@ -46,12 +46,13 @@
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-gpx": "2.2.0",
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "16.1.2",
|
||||
"mermaid": "11.9.0",
|
||||
"mind-elixir": "5.0.5",
|
||||
"marked": "16.2.0",
|
||||
"mermaid": "11.10.1",
|
||||
"mind-elixir": "5.0.6",
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.27.0",
|
||||
"preact": "10.27.1",
|
||||
"react-i18next": "15.7.2",
|
||||
"split.js": "1.6.5",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
"tabulator-tables": "6.3.1",
|
||||
@@ -61,7 +62,7 @@
|
||||
"@ckeditor/ckeditor5-inspector": "5.0.0",
|
||||
"@preact/preset-vite": "2.10.2",
|
||||
"@types/bootstrap": "5.2.10",
|
||||
"@types/jquery": "3.5.32",
|
||||
"@types/jquery": "3.5.33",
|
||||
"@types/leaflet": "1.9.20",
|
||||
"@types/leaflet-gpx": "1.3.7",
|
||||
"@types/mark.js": "8.11.12",
|
||||
@@ -69,7 +70,7 @@
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"happy-dom": "18.0.1",
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "3.1.1"
|
||||
"vite-plugin-static-copy": "3.1.2"
|
||||
},
|
||||
"nx": {
|
||||
"name": "client",
|
||||
|
||||
@@ -35,8 +35,10 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
|
||||
loadResults.addOption(attributeEntity.name);
|
||||
} else if (ec.entityName === "attachments") {
|
||||
processAttachment(loadResults, ec);
|
||||
} else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens") {
|
||||
} else if (ec.entityName === "blobs") {
|
||||
// NOOP - these entities are handled at the backend level and don't require frontend processing
|
||||
} else if (ec.entityName === "etapi_tokens") {
|
||||
loadResults.hasEtapiTokenChanges = true;
|
||||
} else {
|
||||
throw new Error(`Unknown entityName '${ec.entityName}'`);
|
||||
}
|
||||
@@ -77,9 +79,7 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
|
||||
noteAttributeCache.invalidate();
|
||||
}
|
||||
|
||||
// TODO: Remove after porting the file
|
||||
// @ts-ignore
|
||||
const appContext = (await import("../components/app_context.js")).default as any;
|
||||
const appContext = (await import("../components/app_context.js")).default;
|
||||
await appContext.triggerEvent("entitiesReloaded", { loadResults });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import i18next from "i18next";
|
||||
import i18nextHttpBackend from "i18next-http-backend";
|
||||
import server from "./server.js";
|
||||
import type { Locale } from "@triliumnext/commons";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
let locales: Locale[] | null;
|
||||
|
||||
@@ -16,6 +17,7 @@ export async function initLocale() {
|
||||
|
||||
locales = await server.get<Locale[]>("options/locales");
|
||||
|
||||
i18next.use(initReactI18next);
|
||||
await i18next.use(i18nextHttpBackend).init({
|
||||
lng: locale,
|
||||
fallbackLng: "en",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AttachmentRow } from "@triliumnext/commons";
|
||||
import type { AttachmentRow, EtapiTokenRow } from "@triliumnext/commons";
|
||||
import type { AttributeType } from "../entities/fattribute.js";
|
||||
import type { EntityChange } from "../server_types.js";
|
||||
|
||||
@@ -53,6 +53,7 @@ type EntityRowMappings = {
|
||||
options: OptionRow;
|
||||
revisions: RevisionRow;
|
||||
note_reordering: NoteReorderingRow;
|
||||
etapi_tokens: EtapiTokenRow;
|
||||
};
|
||||
|
||||
export type EntityRowNames = keyof EntityRowMappings;
|
||||
@@ -68,6 +69,7 @@ export default class LoadResults {
|
||||
private contentNoteIdToComponentId: ContentNoteIdToComponentIdRow[];
|
||||
private optionNames: string[];
|
||||
private attachmentRows: AttachmentRow[];
|
||||
public hasEtapiTokenChanges: boolean = false;
|
||||
|
||||
constructor(entityChanges: EntityChange[]) {
|
||||
const entities: Record<string, Record<string, any>> = {};
|
||||
@@ -215,7 +217,8 @@ export default class LoadResults {
|
||||
this.revisionRows.length === 0 &&
|
||||
this.contentNoteIdToComponentId.length === 0 &&
|
||||
this.optionNames.length === 0 &&
|
||||
this.attachmentRows.length === 0
|
||||
this.attachmentRows.length === 0 &&
|
||||
!this.hasEtapiTokenChanges
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ export interface Suggestion {
|
||||
commandId?: string;
|
||||
commandDescription?: string;
|
||||
commandShortcut?: string;
|
||||
attributeSnippet?: string;
|
||||
highlightedAttributeSnippet?: string;
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
@@ -323,7 +325,33 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
return `<span class="${suggestion.icon ?? "bx bx-note"}"></span> ${suggestion.highlightedNotePathTitle}`;
|
||||
// Add special class for search-notes action
|
||||
const actionClass = suggestion.action === "search-notes" ? "search-notes-action" : "";
|
||||
|
||||
// Choose appropriate icon based on action
|
||||
let iconClass = suggestion.icon ?? "bx bx-note";
|
||||
if (suggestion.action === "search-notes") {
|
||||
iconClass = "bx bx-search";
|
||||
} else if (suggestion.action === "create-note") {
|
||||
iconClass = "bx bx-plus";
|
||||
} else if (suggestion.action === "external-link") {
|
||||
iconClass = "bx bx-link-external";
|
||||
}
|
||||
|
||||
// Simplified HTML structure without nested divs
|
||||
let html = `<div class="note-suggestion ${actionClass}">`;
|
||||
html += `<span class="icon ${iconClass}"></span>`;
|
||||
html += `<span class="text">`;
|
||||
html += `<span class="search-result-title">${suggestion.highlightedNotePathTitle}</span>`;
|
||||
|
||||
// Add attribute snippet inline if available
|
||||
if (suggestion.highlightedAttributeSnippet) {
|
||||
html += `<span class="search-result-attributes">${suggestion.highlightedAttributeSnippet}</span>`;
|
||||
}
|
||||
|
||||
html += `</span>`;
|
||||
html += `</div>`;
|
||||
return html;
|
||||
}
|
||||
},
|
||||
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { OptionNames } from "@triliumnext/commons";
|
||||
import server from "./server.js";
|
||||
import { isShare } from "./utils.js";
|
||||
|
||||
type OptionValue = number | string;
|
||||
export type OptionValue = number | string;
|
||||
|
||||
class Options {
|
||||
initializedPromise: Promise<void>;
|
||||
@@ -76,6 +77,14 @@ class Options {
|
||||
await server.put(`options`, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves multiple options at once, by supplying a record where the keys are the option names and the values represent the stringified value to set.
|
||||
* @param newValues the record of keys and values.
|
||||
*/
|
||||
async saveMany<T extends OptionNames>(newValues: Record<T, OptionValue>) {
|
||||
await server.put<void>("options", newValues);
|
||||
}
|
||||
|
||||
async toggle(key: string) {
|
||||
await this.save(key, (!this.is(key)).toString());
|
||||
}
|
||||
|
||||
@@ -14,6 +14,32 @@ interface ShortcutBinding {
|
||||
// Store all active shortcut bindings for management
|
||||
const activeBindings: Map<string, ShortcutBinding[]> = new Map();
|
||||
|
||||
// Handle special key mappings and aliases
|
||||
const keyMap: { [key: string]: string[] } = {
|
||||
'return': ['Enter'],
|
||||
'enter': ['Enter'], // alias for return
|
||||
'del': ['Delete'],
|
||||
'delete': ['Delete'], // alias for del
|
||||
'esc': ['Escape'],
|
||||
'escape': ['Escape'], // alias for esc
|
||||
'space': [' ', 'Space'],
|
||||
'tab': ['Tab'],
|
||||
'backspace': ['Backspace'],
|
||||
'home': ['Home'],
|
||||
'end': ['End'],
|
||||
'pageup': ['PageUp'],
|
||||
'pagedown': ['PageDown'],
|
||||
'up': ['ArrowUp'],
|
||||
'down': ['ArrowDown'],
|
||||
'left': ['ArrowLeft'],
|
||||
'right': ['ArrowRight']
|
||||
};
|
||||
|
||||
// Function keys
|
||||
for (let i = 1; i <= 19; i++) {
|
||||
keyMap[`f${i}`] = [`F${i}`];
|
||||
}
|
||||
|
||||
function removeGlobalShortcut(namespace: string) {
|
||||
bindGlobalShortcut("", null, namespace);
|
||||
}
|
||||
@@ -124,32 +150,6 @@ export function keyMatches(e: KeyboardEvent, key: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle special key mappings and aliases
|
||||
const keyMap: { [key: string]: string[] } = {
|
||||
'return': ['Enter'],
|
||||
'enter': ['Enter'], // alias for return
|
||||
'del': ['Delete'],
|
||||
'delete': ['Delete'], // alias for del
|
||||
'esc': ['Escape'],
|
||||
'escape': ['Escape'], // alias for esc
|
||||
'space': [' ', 'Space'],
|
||||
'tab': ['Tab'],
|
||||
'backspace': ['Backspace'],
|
||||
'home': ['Home'],
|
||||
'end': ['End'],
|
||||
'pageup': ['PageUp'],
|
||||
'pagedown': ['PageDown'],
|
||||
'up': ['ArrowUp'],
|
||||
'down': ['ArrowDown'],
|
||||
'left': ['ArrowLeft'],
|
||||
'right': ['ArrowRight']
|
||||
};
|
||||
|
||||
// Function keys
|
||||
for (let i = 1; i <= 19; i++) {
|
||||
keyMap[`f${i}`] = [`F${i}`];
|
||||
}
|
||||
|
||||
const mappedKeys = keyMap[key.toLowerCase()];
|
||||
if (mappedKeys) {
|
||||
return mappedKeys.includes(e.key) || mappedKeys.includes(e.code);
|
||||
@@ -163,7 +163,7 @@ export function keyMatches(e: KeyboardEvent, key: string): boolean {
|
||||
|
||||
// For letter keys, use the physical key code for consistency
|
||||
if (key.length === 1 && key >= 'a' && key <= 'z') {
|
||||
return e.code === `Key${key.toUpperCase()}`;
|
||||
return e.key.toLowerCase() === key.toLowerCase();
|
||||
}
|
||||
|
||||
// For regular keys, check both key and code as fallback
|
||||
|
||||
@@ -5,7 +5,7 @@ const SVG_MIME = "image/svg+xml";
|
||||
|
||||
export const isShare = !window.glob;
|
||||
|
||||
function reloadFrontendApp(reason?: string) {
|
||||
export function reloadFrontendApp(reason?: string) {
|
||||
if (reason) {
|
||||
logInfo(`Frontend app reload: ${reason}`);
|
||||
}
|
||||
@@ -13,7 +13,7 @@ function reloadFrontendApp(reason?: string) {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function restartDesktopApp() {
|
||||
export function restartDesktopApp() {
|
||||
if (!isElectron()) {
|
||||
reloadFrontendApp();
|
||||
return;
|
||||
@@ -125,7 +125,7 @@ function formatDateISO(date: Date) {
|
||||
return `${date.getFullYear()}-${padNum(date.getMonth() + 1)}-${padNum(date.getDate())}`;
|
||||
}
|
||||
|
||||
function formatDateTime(date: Date, userSuppliedFormat?: string): string {
|
||||
export function formatDateTime(date: Date, userSuppliedFormat?: string): string {
|
||||
if (userSuppliedFormat?.trim()) {
|
||||
return dayjs(date).format(userSuppliedFormat);
|
||||
} else {
|
||||
@@ -144,7 +144,7 @@ function now() {
|
||||
/**
|
||||
* Returns `true` if the client is currently running under Electron, or `false` if running in a web browser.
|
||||
*/
|
||||
function isElectron() {
|
||||
export function isElectron() {
|
||||
return !!(window && window.process && window.process.type);
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ function randomString(len: number) {
|
||||
return text;
|
||||
}
|
||||
|
||||
function isMobile() {
|
||||
export function isMobile() {
|
||||
return (
|
||||
window.glob?.device === "mobile" ||
|
||||
// window.glob.device is not available in setup
|
||||
@@ -306,7 +306,7 @@ function copySelectionToClipboard() {
|
||||
}
|
||||
}
|
||||
|
||||
function dynamicRequire(moduleName: string) {
|
||||
export function dynamicRequire(moduleName: string) {
|
||||
if (typeof __non_webpack_require__ !== "undefined") {
|
||||
return __non_webpack_require__(moduleName);
|
||||
} else {
|
||||
@@ -374,33 +374,42 @@ async function openInAppHelp($button: JQuery<HTMLElement>) {
|
||||
|
||||
const inAppHelpPage = $button.attr("data-in-app-help");
|
||||
if (inAppHelpPage) {
|
||||
// Dynamic import to avoid import issues in tests.
|
||||
const appContext = (await import("../components/app_context.js")).default;
|
||||
const activeContext = appContext.tabManager.getActiveContext();
|
||||
if (!activeContext) {
|
||||
return;
|
||||
}
|
||||
const subContexts = activeContext.getSubContexts();
|
||||
const targetNote = `_help_${inAppHelpPage}`;
|
||||
const helpSubcontext = subContexts.find((s) => s.viewScope?.viewMode === "contextual-help");
|
||||
const viewScope: ViewScope = {
|
||||
viewMode: "contextual-help",
|
||||
};
|
||||
if (!helpSubcontext) {
|
||||
// The help is not already open, open a new split with it.
|
||||
const { ntxId } = subContexts[subContexts.length - 1];
|
||||
appContext.triggerCommand("openNewNoteSplit", {
|
||||
ntxId,
|
||||
notePath: targetNote,
|
||||
hoistedNoteId: "_help",
|
||||
viewScope
|
||||
})
|
||||
} else {
|
||||
// There is already a help window open, make sure it opens on the right note.
|
||||
helpSubcontext.setNote(targetNote, { viewScope });
|
||||
}
|
||||
openInAppHelpFromUrl(inAppHelpPage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the in-app help at the given page in a split note. If there already is a split note open with a help page, it will be replaced by this one.
|
||||
*
|
||||
* @param inAppHelpPage the ID of the help note (excluding the `_help_` prefix).
|
||||
* @returns a promise that resolves once the help has been opened.
|
||||
*/
|
||||
export async function openInAppHelpFromUrl(inAppHelpPage: string) {
|
||||
// Dynamic import to avoid import issues in tests.
|
||||
const appContext = (await import("../components/app_context.js")).default;
|
||||
const activeContext = appContext.tabManager.getActiveContext();
|
||||
if (!activeContext) {
|
||||
return;
|
||||
}
|
||||
const subContexts = activeContext.getSubContexts();
|
||||
const targetNote = `_help_${inAppHelpPage}`;
|
||||
const helpSubcontext = subContexts.find((s) => s.viewScope?.viewMode === "contextual-help");
|
||||
const viewScope: ViewScope = {
|
||||
viewMode: "contextual-help",
|
||||
};
|
||||
if (!helpSubcontext) {
|
||||
// The help is not already open, open a new split with it.
|
||||
const { ntxId } = subContexts[subContexts.length - 1];
|
||||
appContext.triggerCommand("openNewNoteSplit", {
|
||||
ntxId,
|
||||
notePath: targetNote,
|
||||
hoistedNoteId: "_help",
|
||||
viewScope
|
||||
})
|
||||
} else {
|
||||
// There is already a help window open, make sure it opens on the right note.
|
||||
helpSubcontext.setNote(targetNote, { viewScope });
|
||||
}
|
||||
}
|
||||
|
||||
function initHelpButtons($el: JQuery<HTMLElement> | JQuery<Window>) {
|
||||
@@ -735,6 +744,50 @@ function isLaunchBarConfig(noteId: string) {
|
||||
return ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers", "_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(noteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a class to the <body> of the page, where the class name is formed via a prefix and a value.
|
||||
* Useful for configurable options such as `heading-style-markdown`, where `heading-style` is the prefix and `markdown` is the dynamic value.
|
||||
* There is no separator between the prefix and the value, if needed it has to be supplied manually to the prefix.
|
||||
*
|
||||
* @param prefix the prefix.
|
||||
* @param value the value to be appended to the prefix.
|
||||
*/
|
||||
export function toggleBodyClass(prefix: string, value: string) {
|
||||
const $body = $("body");
|
||||
for (const clazz of Array.from($body[0].classList)) {
|
||||
// create copy to safely iterate over while removing classes
|
||||
if (clazz.startsWith(prefix)) {
|
||||
$body.removeClass(clazz);
|
||||
}
|
||||
}
|
||||
|
||||
$body.addClass(prefix + value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic comparison for equality between the two arrays. The values are strictly checked via `===`.
|
||||
*
|
||||
* @param a the first array to compare.
|
||||
* @param b the second array to compare.
|
||||
* @returns `true` if both arrays are equals, `false` otherwise.
|
||||
*/
|
||||
export function arrayEqual<T>(a: T[], b: T[]) {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i=0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default {
|
||||
reloadFrontendApp,
|
||||
restartDesktopApp,
|
||||
|
||||
@@ -28,6 +28,28 @@
|
||||
--ck-mention-list-max-height: 500px;
|
||||
}
|
||||
|
||||
body#trilium-app.motion-disabled *,
|
||||
body#trilium-app.motion-disabled *::before,
|
||||
body#trilium-app.motion-disabled *::after {
|
||||
/* Disable transitions and animations */
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
body#trilium-app.shadows-disabled *,
|
||||
body#trilium-app.shadows-disabled *::before,
|
||||
body#trilium-app.shadows-disabled *::after {
|
||||
/* Disable shadows */
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
body#trilium-app.backdrop-effects-disabled *,
|
||||
body#trilium-app.backdrop-effects-disabled *::before,
|
||||
body#trilium-app.backdrop-effects-disabled *::after {
|
||||
/* Disable backdrop effects */
|
||||
backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
.table {
|
||||
--bs-table-bg: transparent !important;
|
||||
}
|
||||
@@ -139,6 +161,15 @@ textarea,
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.form-group.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* Add a gap between consecutive radios / check boxes */
|
||||
label.tn-radio + label.tn-radio,
|
||||
label.tn-checkbox + label.tn-checkbox {
|
||||
@@ -346,7 +377,7 @@ body.desktop .tabulator-popup-container {
|
||||
|
||||
@supports (animation-fill-mode: forwards) {
|
||||
/* Delay the opening of submenus */
|
||||
body.desktop .dropdown-submenu .dropdown-menu {
|
||||
body.desktop:not(.motion-disabled) .dropdown-submenu .dropdown-menu {
|
||||
opacity: 0;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: var(--submenu-opening-delay);
|
||||
@@ -831,10 +862,34 @@ table.promoted-attributes-in-tooltip th {
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion {
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
padding: 6px 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion .icon {
|
||||
display: inline-block;
|
||||
line-height: inherit;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion .text {
|
||||
display: inline-block;
|
||||
width: calc(100% - 20px);
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion .search-result-title {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion .search-result-attributes {
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
color: var(--muted-text-color);
|
||||
opacity: 0.6;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@@ -1738,16 +1793,12 @@ button.close:hover {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.options-number-input {
|
||||
.options-section input[type="number"] {
|
||||
/* overriding settings from .form-control */
|
||||
width: 10em !important;
|
||||
flex-grow: 0 !important;
|
||||
}
|
||||
|
||||
.options-mime-types {
|
||||
column-width: 250px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
cursor: auto;
|
||||
}
|
||||
@@ -1768,20 +1819,42 @@ textarea {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.jump-to-note-dialog .modal-dialog {
|
||||
max-width: 900px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.jump-to-note-dialog .modal-header {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.jump-to-note-dialog .modal-body {
|
||||
padding: 0;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.jump-to-note-results .aa-dropdown-menu {
|
||||
max-height: 40vh;
|
||||
max-height: calc(80vh - 200px);
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.jump-to-note-results {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.jump-to-note-results .aa-suggestions {
|
||||
padding: 1rem;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.jump-to-note-results .aa-dropdown-menu .aa-suggestion:hover,
|
||||
.jump-to-note-results .aa-dropdown-menu .aa-cursor {
|
||||
background-color: var(--hover-item-background-color, #f8f9fa);
|
||||
}
|
||||
|
||||
/* Command palette styling */
|
||||
@@ -1799,8 +1872,24 @@ textarea {
|
||||
|
||||
.jump-to-note-dialog .aa-cursor .command-suggestion,
|
||||
.jump-to-note-dialog .aa-suggestion:hover .command-suggestion {
|
||||
border-left-color: var(--link-color);
|
||||
background-color: var(--hover-background-color);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.jump-to-note-dialog .show-in-full-search,
|
||||
.jump-to-note-results .show-in-full-search {
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
padding-top: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.jump-to-note-results .aa-suggestion .search-notes-action {
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.jump-to-note-results .aa-suggestion:has(.search-notes-action)::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.jump-to-note-dialog .command-icon {
|
||||
@@ -2257,7 +2346,8 @@ footer.webview-footer button {
|
||||
|
||||
/* Search result highlighting */
|
||||
.search-result-title b,
|
||||
.search-result-content b {
|
||||
.search-result-content b,
|
||||
.search-result-attributes b {
|
||||
font-weight: 900;
|
||||
color: var(--admonition-warning-accent-color);
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
|
||||
--menu-text-color: #e3e3e3;
|
||||
--menu-background-color: #222222d9;
|
||||
--menu-background-color-no-backdrop: #1b1b1b;
|
||||
--menu-item-icon-color: #8c8c8c;
|
||||
--menu-item-disabled-opacity: 0.5;
|
||||
--menu-item-keyboard-shortcut-color: #ffffff8f;
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
|
||||
--menu-text-color: #272727;
|
||||
--menu-background-color: #ffffffd9;
|
||||
--menu-background-color-no-backdrop: #fdfdfd;
|
||||
--menu-item-icon-color: #727272;
|
||||
--menu-item-disabled-opacity: 0.6;
|
||||
--menu-item-keyboard-shortcut-color: #666666a8;
|
||||
|
||||
@@ -83,6 +83,12 @@
|
||||
--tab-note-icons: true;
|
||||
}
|
||||
|
||||
body.backdrop-effects-disabled {
|
||||
/* Backdrop effects are disabled, replace the menu background color with the
|
||||
* no-backdrop fallback color */
|
||||
--menu-background-color: var(--menu-background-color-no-backdrop);
|
||||
}
|
||||
|
||||
/*
|
||||
* MENUS
|
||||
*
|
||||
@@ -530,10 +536,9 @@ body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after {
|
||||
}
|
||||
|
||||
/* List item */
|
||||
.jump-to-note-dialog .aa-suggestions div,
|
||||
.note-detail-empty .aa-suggestions div {
|
||||
.jump-to-note-dialog .aa-suggestion,
|
||||
.note-detail-empty .aa-suggestion {
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px;
|
||||
color: var(--menu-text-color);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -181,9 +181,7 @@ div.note-detail-empty {
|
||||
}
|
||||
|
||||
.options-section:not(.tn-no-card) {
|
||||
margin: auto;
|
||||
min-width: var(--options-card-min-width);
|
||||
max-width: var(--options-card-max-width);
|
||||
margin: auto;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--card-border-color) !important;
|
||||
box-shadow: var(--card-box-shadow);
|
||||
@@ -192,6 +190,11 @@ div.note-detail-empty {
|
||||
margin-bottom: calc(var(--options-title-offset) + 26px) !important;
|
||||
}
|
||||
|
||||
body.desktop .option-section:not(.tn-no-card) {
|
||||
min-width: var(--options-card-min-width);
|
||||
max-width: var(--options-card-max-width);
|
||||
}
|
||||
|
||||
.note-detail-content-widget-content.options {
|
||||
--default-padding: 15px;
|
||||
padding-top: calc(var(--default-padding) + var(--options-title-offset) + var(--options-title-font-size));
|
||||
@@ -233,11 +236,6 @@ div.note-detail-empty {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.options-section .options-mime-types {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.options-section .form-group {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
@@ -1165,7 +1165,7 @@
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "笔记修订快照间隔",
|
||||
"note_revisions_snapshot_description": "笔记修订快照间隔是创建新笔记修订的时间。有关更多信息,请参见 <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a>。",
|
||||
"note_revisions_snapshot_description": "笔记修订快照间隔是创建新笔记修订的时间。有关更多信息,请参见 <doc>wiki</doc>。",
|
||||
"snapshot_time_interval_label": "笔记修订快照时间间隔:"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
@@ -1871,14 +1871,19 @@
|
||||
"selected_provider": "已选提供商",
|
||||
"selected_provider_description": "选择用于聊天和补全功能的AI提供商",
|
||||
"select_model": "选择模型...",
|
||||
"select_provider": "选择提供商..."
|
||||
"select_provider": "选择提供商...",
|
||||
"ai_enabled": "已启用 AI 功能",
|
||||
"ai_disabled": "已禁用 AI 功能",
|
||||
"no_models_found_online": "找不到模型。请检查您的 API 密钥及设置。",
|
||||
"no_models_found_ollama": "找不到 Ollama 模型。请确认 Ollama 是否正在运行。",
|
||||
"error_fetching": "获取模型失败:{{error}}"
|
||||
},
|
||||
"code-editor-options": {
|
||||
"title": "编辑器"
|
||||
},
|
||||
"custom_date_time_format": {
|
||||
"title": "自定义日期/时间格式",
|
||||
"description": "通过<kbd></kbd>或工具栏的方式可自定义日期和时间格式,有关日期/时间格式字符串中各个字符的含义,请参阅<a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">Day.js docs</a>。",
|
||||
"description": "通过<shortcut />或工具栏的方式可自定义日期和时间格式,有关日期/时间格式字符串中各个字符的含义,请参阅<doc>Day.js docs</doc>。",
|
||||
"format_string": "日期/时间格式字符串:",
|
||||
"formatted_time": "格式化后日期/时间:"
|
||||
},
|
||||
@@ -1992,11 +1997,28 @@
|
||||
"help_title": "显示关于此画面的更多信息"
|
||||
},
|
||||
"call_to_action": {
|
||||
"next_theme_title": "新的 Trilium 主题已进入稳定版",
|
||||
"next_theme_message": "有一段时间,我们一直在设计新的主题,为了让应用程序看起来更加现代。",
|
||||
"next_theme_button": "切换至新的 Trilium 主题",
|
||||
"background_effects_title": "背景效果现已推出稳定版本",
|
||||
"background_effects_message": "在 Windows 装置上,背景效果现在已完全稳定。背景效果通过模糊背后的背景,为使用者界面增添一抹色彩。此技术也用于其他应用程序,例如 Windows 资源管理器。",
|
||||
"background_effects_button": "启用背景效果"
|
||||
"background_effects_button": "启用背景效果",
|
||||
"next_theme_title": "试用新 Trilium 主题",
|
||||
"next_theme_message": "当前使用旧版主题,要试用新主题吗?",
|
||||
"next_theme_button": "试用新主题",
|
||||
"dismiss": "关闭"
|
||||
},
|
||||
"settings": {
|
||||
"related_settings": "相关设置"
|
||||
},
|
||||
"settings_appearance": {
|
||||
"related_code_blocks": "文本笔记中代码块的色彩方案",
|
||||
"related_code_notes": "代码笔记的色彩方案"
|
||||
},
|
||||
"units": {
|
||||
"percentage": "%"
|
||||
},
|
||||
"ui-performance": {
|
||||
"title": "性能",
|
||||
"enable-motion": "启用过渡和动画",
|
||||
"enable-shadows": "启用阴影",
|
||||
"enable-backdrop-effects": "启用菜单、弹窗和面板的背景效果"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1113,6 +1113,12 @@
|
||||
"layout-vertical-description": "launcher bar is on the left (default)",
|
||||
"layout-horizontal-description": "launcher bar is underneath the tab bar, the tab bar is now full width."
|
||||
},
|
||||
"ui-performance": {
|
||||
"title": "Performance",
|
||||
"enable-motion": "Enable transitions and animations",
|
||||
"enable-shadows": "Enable shadows",
|
||||
"enable-backdrop-effects": "Enable background effects for menus, popups and panels"
|
||||
},
|
||||
"ai_llm": {
|
||||
"not_started": "Not started",
|
||||
"title": "AI Settings",
|
||||
@@ -1253,7 +1259,12 @@
|
||||
"selected_provider": "Selected Provider",
|
||||
"selected_provider_description": "Choose the AI provider for chat and completion features",
|
||||
"select_model": "Select model...",
|
||||
"select_provider": "Select provider..."
|
||||
"select_provider": "Select provider...",
|
||||
"ai_enabled": "AI features enabled",
|
||||
"ai_disabled": "AI features disabled",
|
||||
"no_models_found_online": "No models found. Please check your API key and settings.",
|
||||
"no_models_found_ollama": "No Ollama models found. Please check if Ollama is running.",
|
||||
"error_fetching": "Error fetching models: {{error}}"
|
||||
},
|
||||
"zoom_factor": {
|
||||
"title": "Zoom Factor (desktop build only)",
|
||||
@@ -1310,7 +1321,7 @@
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "Note Revision Snapshot Interval",
|
||||
"note_revisions_snapshot_description": "The Note revision snapshot interval is the time after which a new note revision will be created for the note. See <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a> for more info.",
|
||||
"note_revisions_snapshot_description": "The Note revision snapshot interval is the time after which a new note revision will be created for the note. See <doc>wiki</doc> for more info.",
|
||||
"snapshot_time_interval_label": "Note revision snapshot time interval:"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
@@ -1372,7 +1383,7 @@
|
||||
},
|
||||
"custom_date_time_format": {
|
||||
"title": "Custom Date/Time Format",
|
||||
"description": "Customize the format of the date and time inserted via <kbd></kbd> or the toolbar. See <a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">Day.js docs</a> for available format tokens.",
|
||||
"description": "Customize the format of the date and time inserted via <shortcut /> or the toolbar. See <doc>Day.js docs</doc> for available format tokens.",
|
||||
"format_string": "Format string:",
|
||||
"formatted_time": "Formatted date/time:"
|
||||
},
|
||||
@@ -1994,11 +2005,22 @@
|
||||
"help_title": "Display more information about this screen"
|
||||
},
|
||||
"call_to_action": {
|
||||
"next_theme_title": "The new Trilium theme is now stable",
|
||||
"next_theme_message": "For a while now, we've been working on a new theme to give the application a more modern look.",
|
||||
"next_theme_button": "Switch to the new Trilium theme",
|
||||
"next_theme_title": "Try the new Trilium theme",
|
||||
"next_theme_message": "You are currently using the legacy theme, would you like to try the new theme?",
|
||||
"next_theme_button": "Try the new theme",
|
||||
"background_effects_title": "Background effects are now stable",
|
||||
"background_effects_message": "On Windows devices, background effects are now fully stable. The background effects adds a touch of color to the user interface by blurring the background behind it. This technique is also used in other applications such as Windows Explorer.",
|
||||
"background_effects_button": "Enable background effects"
|
||||
"background_effects_button": "Enable background effects",
|
||||
"dismiss": "Dismiss"
|
||||
},
|
||||
"settings": {
|
||||
"related_settings": "Related settings"
|
||||
},
|
||||
"settings_appearance": {
|
||||
"related_code_blocks": "Color scheme for code blocks in text notes",
|
||||
"related_code_notes": "Color scheme for code notes"
|
||||
},
|
||||
"units": {
|
||||
"percentage": "%"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
22
apps/client/src/translations/fa/translation.json
Normal file
22
apps/client/src/translations/fa/translation.json
Normal 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": "یادداشت"
|
||||
}
|
||||
}
|
||||
147
apps/client/src/translations/fi/translation.json
Normal file
147
apps/client/src/translations/fi/translation.json
Normal 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ää"
|
||||
}
|
||||
}
|
||||
@@ -202,7 +202,8 @@
|
||||
"okButton": "OK"
|
||||
},
|
||||
"jump_to_note": {
|
||||
"search_button": "Rechercher dans le texte intégral"
|
||||
"search_button": "Rechercher dans le texte intégral",
|
||||
"search_placeholder": "Rechercher une note par son nom ou saisir ‘>’ pour les commandes…"
|
||||
},
|
||||
"markdown_import": {
|
||||
"dialog_title": "Importation Markdown",
|
||||
@@ -222,11 +223,16 @@
|
||||
"note_type_chooser": {
|
||||
"modal_title": "Choisissez le type de note",
|
||||
"modal_body": "Choisissez le type de note/le modèle de la nouvelle note :",
|
||||
"templates": "Modèles"
|
||||
"templates": "Modèles",
|
||||
"change_path_prompt": "Modifier l’emplacement de création de la nouvelle note :",
|
||||
"search_placeholder": "Rechercher le chemin par nom (par défaut si vide)",
|
||||
"builtin_templates": "Modèles intégrés"
|
||||
},
|
||||
"password_not_set": {
|
||||
"title": "Le mot de passe n'est pas défini",
|
||||
"body1": "Les notes protégées sont cryptées à l'aide d'un mot de passe utilisateur, mais le mot de passe n'a pas encore été défini."
|
||||
"body1": "Les notes protégées sont cryptées à l'aide d'un mot de passe utilisateur, mais le mot de passe n'a pas encore été défini.",
|
||||
"body2": "Pour pouvoir protéger les notes, cliquez sur le bouton ci-dessous pour ouvrir la boîte de dialogue Options et définir votre mot de passe.",
|
||||
"go_to_password_options": "Accéder aux options de mot de passe"
|
||||
},
|
||||
"prompt": {
|
||||
"title": "Prompt",
|
||||
@@ -268,7 +274,9 @@
|
||||
"mime": "MIME : ",
|
||||
"file_size": "Taille du fichier :",
|
||||
"preview": "Aperçu :",
|
||||
"preview_not_available": "L'aperçu n'est pas disponible pour ce type de note."
|
||||
"preview_not_available": "L'aperçu n'est pas disponible pour ce type de note.",
|
||||
"restore_button": "Restaurer",
|
||||
"delete_button": "Supprimer"
|
||||
},
|
||||
"sort_child_notes": {
|
||||
"sort_children_by": "Trier les enfants par...",
|
||||
@@ -384,7 +392,7 @@
|
||||
"share_index": "la note avec ce label listera toutes les racines des notes partagées",
|
||||
"display_relations": "noms des relations délimités par des virgules qui doivent être affichés. Tous les autres seront masqués.",
|
||||
"hide_relations": "noms de relations délimités par des virgules qui doivent être masqués. Tous les autres seront affichés.",
|
||||
"title_template": "titre par défaut des notes créées en tant qu'enfants de cette note. La valeur est évaluée sous forme de chaîne JavaScript \n et peut ainsi être enrichi de contenu dynamique via les variables injectées <code>now</code> et <code>parentNote</code>. Exemples :\n \n <ul>\n <li><code>Œuvres littéraires de ${parentNote.getLabelValue('authorName')}</code></li>\n <li><code>Connectez-vous pour ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n Consultez le <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">wiki avec plus de détails</a>, la documentation sur l'API pour <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> et <a href=\"https://day.js.org/docs/en/display/format\">maintenant< /a> pour plus de détails.",
|
||||
"title_template": "titre par défaut des notes créées en tant qu'enfants de cette note. La valeur est évaluée sous forme de chaîne JavaScript \n et peut ainsi être enrichi de contenu dynamique via les variables injectées <code>now</code> et <code>parentNote</code>. Exemples :\n \n <ul>\n <li><code>Œuvres littéraires de ${parentNote.getLabelValue('authorName')}</code></li>\n <li><code>Connectez-vous pour ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n Consultez le <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">wiki avec plus de détails</a>, la documentation sur l'API pour <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> et <a href=\"https://day.js.org/docs/en/display/format\">maintenant</a> pour plus de détails.",
|
||||
"template": "Cette note apparaîtra parmi les modèles disponibles lors de la création d'une nouvelle note",
|
||||
"toc": "<code>#toc</code> ou <code>#toc=show</code> forcera l'affichage de la table des matières, <code>#toc=hide</code> force qu'elle soit masquée. Si le label n'existe pas, le paramètre global est utilisé",
|
||||
"color": "définit la couleur de la note dans l'arborescence des notes, les liens, etc. Utilisez n'importe quelle valeur de couleur CSS valide comme « rouge » ou #a13d5f",
|
||||
@@ -418,7 +426,8 @@
|
||||
"other_notes_with_name": "Autres notes portant le nom {{attributeType}} \"{{attributeName}}\"",
|
||||
"and_more": "... et {{count}} plus.",
|
||||
"print_landscape": "Lors de l'exportation en PDF, change l'orientation de la page en paysage au lieu de portrait.",
|
||||
"print_page_size": "Lors de l'exportation en PDF, change la taille de la page. Valeurs supportées : <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>."
|
||||
"print_page_size": "Lors de l'exportation en PDF, change la taille de la page. Valeurs supportées : <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.",
|
||||
"color_type": "Couleur"
|
||||
},
|
||||
"attribute_editor": {
|
||||
"help_text_body1": "Pour ajouter un label, tapez simplement par ex. <code>#rock</code>, ou si vous souhaitez également ajouter une valeur, tapez par ex. <code>#année = 2020</code>",
|
||||
@@ -577,7 +586,8 @@
|
||||
"september": "Septembre",
|
||||
"october": "Octobre",
|
||||
"november": "Novembre",
|
||||
"december": "Décembre"
|
||||
"december": "Décembre",
|
||||
"cannot_find_week_note": "Impossible de trouver la note de la semaine"
|
||||
},
|
||||
"close_pane_button": {
|
||||
"close_this_pane": "Fermer ce volet"
|
||||
@@ -721,7 +731,8 @@
|
||||
"basic_properties": {
|
||||
"note_type": "Type de note",
|
||||
"editable": "Modifiable",
|
||||
"basic_properties": "Propriétés de base"
|
||||
"basic_properties": "Propriétés de base",
|
||||
"language": "Langage"
|
||||
},
|
||||
"book_properties": {
|
||||
"view_type": "Type d'affichage",
|
||||
@@ -732,7 +743,11 @@
|
||||
"collapse": "Réduire",
|
||||
"expand": "Développer",
|
||||
"invalid_view_type": "Type de vue non valide '{{type}}'",
|
||||
"calendar": "Calendrier"
|
||||
"calendar": "Calendrier",
|
||||
"book_properties": "Propriétés de la collection",
|
||||
"table": "Tableau",
|
||||
"geo-map": "Carte géographique",
|
||||
"board": "Tableau de bord"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "Aucune note modifiée ce jour-là...",
|
||||
@@ -809,7 +824,8 @@
|
||||
"unknown_label_type": "Type de label inconnu '{{type}}'",
|
||||
"unknown_attribute_type": "Type d'attribut inconnu '{{type}}'",
|
||||
"add_new_attribute": "Ajouter un nouvel attribut",
|
||||
"remove_this_attribute": "Supprimer cet attribut"
|
||||
"remove_this_attribute": "Supprimer cet attribut",
|
||||
"remove_color": "Supprimer l’étiquette de couleur"
|
||||
},
|
||||
"script_executor": {
|
||||
"query": "Requête",
|
||||
@@ -899,7 +915,7 @@
|
||||
"description1": "Le script de recherche permet de définir les résultats de la recherche en exécutant un script. Cela offre une flexibilité maximale lorsque la recherche standard ne suffit pas.",
|
||||
"description2": "Le script de recherche doit être de type \"code\" et sous-type \"backend JavaScript\". Le script doit retourner un tableau de noteIds ou de notes.",
|
||||
"example_title": "Voir cet exemple :",
|
||||
"example_code": "// 1. préfiltrage à l'aide de la recherche standard\nconst candidateNotes = api.searchForNotes(\"#journal\"); \n\n// 2. application de critères de recherche personnalisés\nconst matchedNotes = candidateNotes\n .filter(note => note.title.match(/[0-9]{1,2}\\. ?[0-9]{1,2}\\. ?[0-9]{4}/));\n\nreturn matchedNotes;",
|
||||
"example_code": "// 1. préfiltrage à l'aide de la recherche standard\nconst candidateNotes = api.searchForNotes(\"#journal\"); \n\n// 2. application de critères de recherche personnalisés\nconst matchedNotes = candidateNotes\n .filter(note => note.title.match(/[0-9]{1,2}\\. ?[0-9]{1,2}\\. ?[0-9]{4}/));\n\nreturn matchedNotes;",
|
||||
"note": "Notez que le script de recherche et la l'expression à rechercher standard ne peuvent pas être combinés."
|
||||
},
|
||||
"search_string": {
|
||||
@@ -1068,7 +1084,8 @@
|
||||
"max_width_label": "Largeur maximale du contenu en pixels",
|
||||
"apply_changes_description": "Pour appliquer les modifications de largeur du contenu, cliquez sur",
|
||||
"reload_button": "recharger l'interface",
|
||||
"reload_description": "changements par rapport aux options d'apparence"
|
||||
"reload_description": "changements par rapport aux options d'apparence",
|
||||
"max_width_unit": "Pixels"
|
||||
},
|
||||
"native_title_bar": {
|
||||
"title": "Barre de titre native (nécessite le redémarrage de l'application)",
|
||||
@@ -1091,7 +1108,10 @@
|
||||
"layout-vertical-title": "Vertical",
|
||||
"layout-horizontal-title": "Horizontal",
|
||||
"layout-vertical-description": "la barre de raccourcis est à gauche (défaut)",
|
||||
"layout-horizontal-description": "la barre de raccourcis est sous la barre des onglets, cette-dernière est s'affiche en pleine largeur."
|
||||
"layout-horizontal-description": "la barre de raccourcis est sous la barre des onglets, cette-dernière est s'affiche en pleine largeur.",
|
||||
"auto_theme": "Hérité (suivre le schéma de couleurs du système)",
|
||||
"light_theme": "Hérité (clair)",
|
||||
"dark_theme": "Hérité (foncé)"
|
||||
},
|
||||
"zoom_factor": {
|
||||
"title": "Facteur de zoom (version bureau uniquement)",
|
||||
@@ -1136,14 +1156,14 @@
|
||||
"note_erasure_timeout": {
|
||||
"note_erasure_timeout_title": "Délai d'effacement des notes",
|
||||
"note_erasure_description": "Les notes supprimées (et les attributs, versions...) sont seulement marquées comme supprimées et il est possible de les récupérer à partir de la boîte de dialogue Notes récentes. Après un certain temps, les notes supprimées sont « effacées », ce qui signifie que leur contenu n'est plus récupérable. Ce paramètre vous permet de configurer la durée entre la suppression et l'effacement de la note.",
|
||||
"erase_notes_after": "Effacer les notes après:",
|
||||
"erase_notes_after": "Effacer les notes après :",
|
||||
"manual_erasing_description": "Vous pouvez également déclencher l'effacement manuellement (sans tenir compte de la durée définie ci-dessus) :",
|
||||
"erase_deleted_notes_now": "Effacer les notes supprimées maintenant",
|
||||
"deleted_notes_erased": "Les notes supprimées ont été effacées."
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "Délai d'enregistrement automatique d'une version de note",
|
||||
"note_revisions_snapshot_description": "Le délai d'enregistrement automatique des versions de note définit le temps avant la création automatique d'une nouvelle version de note. Consultez le <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a> pour plus d'informations.",
|
||||
"note_revisions_snapshot_description": "Le délai d'enregistrement automatique des versions de note définit le temps avant la création automatique d'une nouvelle version de note. Consultez le <doc>wiki</doc> pour plus d'informations.",
|
||||
"snapshot_time_interval_label": "Délai d'enregistrement automatique de version de note :"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
@@ -1650,5 +1670,26 @@
|
||||
},
|
||||
"modal": {
|
||||
"close": "Fermer"
|
||||
},
|
||||
"ai_llm": {
|
||||
"not_started": "Non démarré",
|
||||
"title": "Paramètres IA",
|
||||
"processed_notes": "Notes traitées",
|
||||
"n_notes_queued_0": "{{ count }} note en attente d’indexation",
|
||||
"n_notes_queued_1": "{{ count }} notes en attente d’indexation",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
1
apps/client/src/translations/hu/translation.json
Normal file
1
apps/client/src/translations/hu/translation.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -35,7 +35,11 @@
|
||||
"link_title_arbitrary": "リンクタイトルは自由に変更可能"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"save": "保存"
|
||||
"save": "保存",
|
||||
"edit_branch_prefix": "ブランチ接頭辞の編集",
|
||||
"help_on_tree_prefix": "ツリー接頭辞に関するヘルプ",
|
||||
"prefix": "接頭辞: ",
|
||||
"branch_prefix_saved": "ブランチ接頭辞が保存されました。"
|
||||
},
|
||||
"global_menu": {
|
||||
"menu": "メニュー",
|
||||
@@ -59,7 +63,10 @@
|
||||
"zoom_out": "ズームアウト",
|
||||
"zoom_in": "ズームイン",
|
||||
"advanced": "高度",
|
||||
"toggle-zen-mode": "禅モード"
|
||||
"toggle-zen-mode": "禅モード",
|
||||
"switch_to_mobile_version": "モバイル版に切り替え",
|
||||
"switch_to_desktop_version": "デスクトップ版に切り替え",
|
||||
"configure_launchbar": "ランチャーバーの設定"
|
||||
},
|
||||
"left_pane_toggle": {
|
||||
"show_panel": "パネルを表示",
|
||||
@@ -82,14 +89,17 @@
|
||||
"help_on_links": "ヘルプへのリンク"
|
||||
},
|
||||
"delete_notes": {
|
||||
"delete_all_clones_description": "すべてのクローンも削除する(最近の変更では元に戻すことができる)",
|
||||
"delete_all_clones_description": "すべてのクローンも削除(最近の変更では元に戻すことができる)",
|
||||
"erase_notes_description": "通常の(ソフト)削除では、ノートは削除されたものとしてマークされ、一定期間内に(最近の変更で)削除を取り消すことができます。このオプションをオンにすると、ノートは即座に削除され、削除を取り消すことはできません。",
|
||||
"erase_notes_warning": "すべてのクローンを含め、ノートを完全に消去します(元に戻せません)。これにより、アプリケーションは強制的にリロードされます。",
|
||||
"notes_to_be_deleted": "以下のノートが削除されます ({{notesCount}})",
|
||||
"no_note_to_delete": "ノートは削除されません(クローンのみ)。",
|
||||
"cancel": "キャンセル",
|
||||
"ok": "OK",
|
||||
"close": "閉じる"
|
||||
"close": "閉じる",
|
||||
"delete_notes_preview": "ノートのプレビューを削除",
|
||||
"broken_relations_to_be_deleted": "次のリレーション ({{relationCount}})は壊れているので消去されます",
|
||||
"deleted_relation_text": "削除予定のノート{{- note}}は{{- source}}からリレーション{{- relation}}によって参照されています."
|
||||
},
|
||||
"calendar": {
|
||||
"mon": "月",
|
||||
@@ -141,14 +151,15 @@
|
||||
},
|
||||
"tab_row": {
|
||||
"close_tab": "タブを閉じる",
|
||||
"add_new_tab": "新しいタブ",
|
||||
"add_new_tab": "新しいタブを追加",
|
||||
"close": "閉じる",
|
||||
"close_other_tabs": "他のタブを閉じる",
|
||||
"close_right_tabs": "右側のタブをすべて閉じる",
|
||||
"close_all_tabs": "すべてのタブを閉じる",
|
||||
"reopen_last_tab": "最後に閉じたタブを開く",
|
||||
"move_tab_to_new_window": "このタブを新しいウィンドウに移動する",
|
||||
"copy_tab_to_new_window": "このタブを新しいウィンドウにコピーする"
|
||||
"move_tab_to_new_window": "このタブを新しいウィンドウに移動",
|
||||
"copy_tab_to_new_window": "このタブを新しいウィンドウにコピーする",
|
||||
"new_tab": "新しいタブ"
|
||||
},
|
||||
"tasks": {
|
||||
"due": {
|
||||
@@ -198,7 +209,10 @@
|
||||
"confirm": {
|
||||
"confirmation": "確認",
|
||||
"cancel": "キャンセル",
|
||||
"ok": "OK"
|
||||
"ok": "OK",
|
||||
"also_delete_note": "同時にノートを削除",
|
||||
"are_you_sure_remove_note": "本当に\"{{title}}\"をリレーションマップから除きたいですか? ",
|
||||
"if_you_dont_check": "これをチェックしないと、このノートはリレーションマップからのみ除かれます。"
|
||||
},
|
||||
"export": {
|
||||
"export_note_title": "ノートをエクスポート",
|
||||
@@ -265,7 +279,12 @@
|
||||
"showSQLConsole": "SQLコンソールを表示",
|
||||
"other": "その他",
|
||||
"quickSearch": "クイックサーチにフォーカス",
|
||||
"inPageSearch": "ページ内検索"
|
||||
"inPageSearch": "ページ内検索",
|
||||
"showJumpToNoteDialog": "<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">「ジャンプ先」ダイアログ</a>を表示",
|
||||
"moveNoteUpDown": "ノートリストでノートを上/下に移動",
|
||||
"notSet": "未設定",
|
||||
"goUpDown": "ノートのリストで上下する",
|
||||
"editBranchPrefix": "アクティブノートのクローンの <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/tree-concepts.html#prefix\">プレフィックス</a> を編集する"
|
||||
},
|
||||
"import": {
|
||||
"importIntoNote": "ノートにインポート",
|
||||
@@ -340,7 +359,9 @@
|
||||
},
|
||||
"relation_map_buttons": {
|
||||
"zoom_out_title": "ズームアウト",
|
||||
"zoom_in_title": "ズームイン"
|
||||
"zoom_in_title": "ズームイン",
|
||||
"create_child_note_title": "新しい子ノートを作成し、関連マップに追加",
|
||||
"reset_pan_zoom_title": "パンとズームを初期座標と倍率にリセット"
|
||||
},
|
||||
"tree-context-menu": {
|
||||
"advanced": "高度",
|
||||
@@ -392,19 +413,38 @@
|
||||
"open_note_custom": "プログラムからノートを開く",
|
||||
"import_files": "ファイルをインポート",
|
||||
"export_note": "ノートをエクスポート",
|
||||
"delete_note": "ノートを削除する",
|
||||
"delete_note": "ノートを削除",
|
||||
"print_note": "ノートを印刷",
|
||||
"print_pdf": "PDFとしてエクスポート..."
|
||||
"print_pdf": "PDFとしてエクスポート...",
|
||||
"convert_into_attachment": "添付ファイルに変換",
|
||||
"save_revision": "変更履歴を保存",
|
||||
"convert_into_attachment_failed": "ノート '{{title}}' の変換に失敗しました。",
|
||||
"convert_into_attachment_successful": "ノート '{{title}}' は添付ファイルに変換されました。",
|
||||
"convert_into_attachment_prompt": "本当にノート '{{title}}' を親ノートの添付ファイルに変換しますか?",
|
||||
"note_attachments": "ノートの添付ファイル"
|
||||
},
|
||||
"command_palette": {
|
||||
"export_note_title": "ノートをエクスポート",
|
||||
"search_subtree_title": "サブツリー内を検索"
|
||||
"search_subtree_title": "サブツリー内を検索",
|
||||
"tree-action-name": "ツリー: {{name}}",
|
||||
"export_note_description": "現在のノートをエクスポート",
|
||||
"search_notes_title": "ノートを検索",
|
||||
"search_notes_description": "高度な検索を開く",
|
||||
"search_subtree_description": "現在のサブツリー内を検索",
|
||||
"search_history_title": "検索履歴を表示",
|
||||
"search_history_description": "過去の検索結果を見る"
|
||||
},
|
||||
"delete_note": {
|
||||
"delete_note": "ノートを削除する"
|
||||
"delete_note": "ノートを削除"
|
||||
},
|
||||
"board_view": {
|
||||
"delete-note": "ノートを削除する"
|
||||
"delete-note": "ノートを削除",
|
||||
"insert-above": "上に挿入",
|
||||
"insert-below": "下に挿入",
|
||||
"delete-column": "列を削除",
|
||||
"delete-column-confirmation": "本当にこの列を削除しますか?対応する属性は、この列の下のノートでも削除されます。",
|
||||
"new-item": "新しいアイテム",
|
||||
"add-column": "列を追加"
|
||||
},
|
||||
"code_buttons": {
|
||||
"execute_button_title": "スクリプトを実行",
|
||||
@@ -482,7 +522,8 @@
|
||||
"file_size": "ファイルサイズ",
|
||||
"download": "ダウンロード",
|
||||
"open": "開く",
|
||||
"title": "ファイル"
|
||||
"title": "ファイル",
|
||||
"upload_new_revision": "編集履歴をアップロード"
|
||||
},
|
||||
"note_info_widget": {
|
||||
"note_id": "ノート ID",
|
||||
@@ -492,21 +533,54 @@
|
||||
"note_size": "ノートサイズ",
|
||||
"calculate": "計算",
|
||||
"subtree_size": "(サブツリーサイズ: {{size}}、ノード数: {{count}})",
|
||||
"title": "ノート情報"
|
||||
"title": "ノート情報",
|
||||
"note_size_info": "ノートのサイズは、このノートに必要なストレージの概算を示します。これは、ノートの内容とそのノートの編集履歴の内容を考慮したものです。"
|
||||
},
|
||||
"image_properties": {
|
||||
"file_type": "ファイルタイプ",
|
||||
"file_size": "ファイルサイズ",
|
||||
"download": "ダウンロード",
|
||||
"open": "開く",
|
||||
"title": "画像"
|
||||
"title": "画像",
|
||||
"upload_new_revision": "編集履歴をアップロード"
|
||||
},
|
||||
"revisions": {
|
||||
"download_button": "ダウンロード",
|
||||
"delete_button": "削除"
|
||||
"delete_button": "削除",
|
||||
"note_revisions": "ノートの変更履歴",
|
||||
"delete_all_revisions": "このノートの変更履歴をすべて削除",
|
||||
"delete_all_button": "変更履歴をすべて削除",
|
||||
"help_title": "変更履歴のヘルプ",
|
||||
"revision_last_edited": "この変更は{{date}}に行われました",
|
||||
"confirm_delete_all": "このノートのすべての変更履歴を削除しますか?",
|
||||
"no_revisions": "このノートに変更履歴はまだありません...",
|
||||
"restore_button": "復元",
|
||||
"confirm_restore": "この変更を復元しますか?現在のノートのタイトルとコンテンツはこの変更で上書きされます。",
|
||||
"confirm_delete": "この変更履歴を削除しますか?",
|
||||
"revisions_deleted": "ノートの変更履歴は削除されました。",
|
||||
"revision_restored": "ノートの変更が復元されました。",
|
||||
"revision_deleted": "ノートの変更履歴は削除されました。",
|
||||
"settings": "ノートの変更履歴の設定",
|
||||
"file_size": "ファイルサイズ:",
|
||||
"preview": "プレビュー:",
|
||||
"preview_not_available": "このノートタイプではプレビューは利用できません。"
|
||||
},
|
||||
"attachments_actions": {
|
||||
"download": "ダウンロード"
|
||||
"download": "ダウンロード",
|
||||
"open_externally": "外部で開く",
|
||||
"open_externally_title": "ファイルを外部アプリケーションで開き、変更を監視します。その後、変更されたバージョンをTriliumにアップロードできるようになります。",
|
||||
"open_custom": "プログラムから開く",
|
||||
"open_custom_title": "ファイルを外部アプリケーションで開き、変更を監視します。その後、変更されたバージョンをTriliumにアップロードできるようになります。",
|
||||
"rename_attachment": "添付ファイルの名前を変更",
|
||||
"copy_link_to_clipboard": "リンクをクリップボードにコピー",
|
||||
"convert_attachment_into_note": "添付ファイルをノートに変換",
|
||||
"delete_attachment": "添付ファイルを削除",
|
||||
"open_externally_detail_page": "添付ファイルを外部で開くは、詳細ページからのみ可能です。まず添付ファイル詳細をクリックしてから、再度操作を行ってください。",
|
||||
"open_custom_client_only": "添付ファイルをプログラムから開くは、デスクトップクライアントからのみ可能です。",
|
||||
"delete_confirm": "本当に添付ファイル '{{title}}' を削除しますか?",
|
||||
"delete_success": "添付ファイル '{{title}}' は削除されました。",
|
||||
"enter_new_name": "新しい添付ファイルの名前を入力してください",
|
||||
"upload_new_revision": "編集履歴をアップロード"
|
||||
},
|
||||
"etapi": {
|
||||
"created": "作成日時",
|
||||
@@ -578,21 +652,48 @@
|
||||
"parent_count": "クローンの数",
|
||||
"random": "ランダムな順番",
|
||||
"asc": "昇順(デフォルト)",
|
||||
"desc": "降順"
|
||||
"desc": "降順",
|
||||
"content_and_attachments_size": "添付ファイルを含むノート内容のサイズ",
|
||||
"content_and_attachments_and_revisions_size": "添付ファイルと編集履歴を含むノート内容のサイズ",
|
||||
"revision_count": "編集履歴の回数",
|
||||
"owned_label_count": "ラベルの数",
|
||||
"owned_relation_count": "関係の数"
|
||||
},
|
||||
"table_view": {
|
||||
"sort-column-descending": "降順",
|
||||
"sort-column-ascending": "昇順"
|
||||
"sort-column-ascending": "昇順",
|
||||
"new-row": "新しい行",
|
||||
"new-column": "新しい列",
|
||||
"sort-column-by": "\"{{title}}\" で並べ替え",
|
||||
"sort-column-clear": "並べ替えをクリア",
|
||||
"hide-column": "列 \"{{title}}\" を隠す",
|
||||
"show-hide-columns": "列を表示/非表示",
|
||||
"row-insert-above": "上に行を挿入",
|
||||
"row-insert-below": "下に行を挿入",
|
||||
"row-insert-child": "子ノートを挿入",
|
||||
"add-column-to-the-left": "列を左に追加",
|
||||
"add-column-to-the-right": "列を右に追加",
|
||||
"edit-column": "列を編集",
|
||||
"delete_column_confirmation": "この列を本当に削除してもよろしいですか?対応する属性はすべてのノートから削除されます。",
|
||||
"delete-column": "列を削除",
|
||||
"new-column-label": "ラベル",
|
||||
"new-column-relation": "関係"
|
||||
},
|
||||
"search_script": {
|
||||
"title": "検索スクリプト:",
|
||||
"placeholder": "ノート名で検索"
|
||||
"placeholder": "ノート名で検索",
|
||||
"description1": "検索スクリプトは、スクリプトを実行することによって検索結果を定義することができます。標準の検索では不十分な場合に、最大限の柔軟性を提供します。",
|
||||
"description2": "検索スクリプトはノートタイプが \"code\" かつ \"JavaScript backend\" でなければならない。スクリプトは、 noteIds または note の配列を返す必要があります。",
|
||||
"example_title": "例は以下です:",
|
||||
"example_code": "// 1. 標準検索によるプレフィルタリング\nconst candidateNotes = api.searchForNotes(\"#journal\"); \n\n// 2. カスタム検索条件の適用\nconst matchedNotes = candidateNotes\n .filter(note => note.title.match(/[0-9]{1,2}\\. ?[0-9]{1,2}\\. ?[0-9]{4}/));\n\nreturn matchedNotes;",
|
||||
"note": "検索スクリプトと文字列検索は、互いに組み合わせることはできません。"
|
||||
},
|
||||
"include_note": {
|
||||
"placeholder_search": "ノート名で検索",
|
||||
"dialog_title": "埋め込みノート",
|
||||
"box_size_prompt": "埋め込みノート枠のサイズ:",
|
||||
"button_include": "埋め込みノート"
|
||||
"button_include": "埋め込みノート",
|
||||
"label_note": "ノート"
|
||||
},
|
||||
"ancestor": {
|
||||
"placeholder": "ノート名で検索"
|
||||
@@ -634,7 +735,12 @@
|
||||
"system-default": "システムのデフォルト"
|
||||
},
|
||||
"max_content_width": {
|
||||
"reload_button": "フロントエンドをリロード"
|
||||
"reload_button": "フロントエンドをリロード",
|
||||
"title": "コンテンツ幅",
|
||||
"default_description": "Triliumは、ワイドスクリーンで最大化された画面での可読性を向上させるために、デフォルトでコンテンツの最大幅を制限しています。",
|
||||
"max_width_label": "最大コンテンツ幅",
|
||||
"max_width_unit": "ピクセル",
|
||||
"apply_changes_description": "コンテンツ幅の変更を適用するには、クリックしてください"
|
||||
},
|
||||
"theme": {
|
||||
"title": "アプリのテーマ",
|
||||
@@ -713,7 +819,8 @@
|
||||
"shortcut_info": "オプション -> ショートカット('右ペイン切り替え')で、右ペイン(目次を含む)を素早く切り替えるキーボードショートカットを設定できます。"
|
||||
},
|
||||
"toc": {
|
||||
"table_of_contents": "目次"
|
||||
"table_of_contents": "目次",
|
||||
"options": "オプション"
|
||||
},
|
||||
"text_auto_read_only_size": {
|
||||
"title": "自動読み取り専用のサイズ",
|
||||
@@ -724,11 +831,12 @@
|
||||
"code_auto_read_only_size": {
|
||||
"title": "自動読み取り専用のサイズ",
|
||||
"description": "自動読み取り専用のノートサイズは、ノートが読み取り専用モード(パフォーマンス上の理由)で表示されるようになるサイズです。",
|
||||
"unit": "文字"
|
||||
"unit": "文字",
|
||||
"label": "自動読み取り専用のサイズ(コードノート)"
|
||||
},
|
||||
"custom_date_time_format": {
|
||||
"title": "日付/時刻フォーマットのカスタム",
|
||||
"description": "<kbd></kbd>またはツールバーから挿入される日付と時刻のフォーマットをカスタマイズする。 利用可能なトークンについては <a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">Day.js ドキュメント</a> を参照してください。",
|
||||
"description": "<shortcut />またはツールバーから挿入される日付と時刻のフォーマットをカスタマイズする。 利用可能なトークンについては <doc>Day.js ドキュメント</doc> を参照してください。",
|
||||
"format_string": "文字列形式:",
|
||||
"formatted_time": "日付/時刻形式:"
|
||||
},
|
||||
@@ -804,7 +912,8 @@
|
||||
"close_label": "閉じる"
|
||||
},
|
||||
"modal": {
|
||||
"close": "閉じる"
|
||||
"close": "閉じる",
|
||||
"help_title": "この画面に関する詳細情報を表示"
|
||||
},
|
||||
"bookmark_switch": {
|
||||
"bookmark": "ブックマーク",
|
||||
@@ -812,10 +921,39 @@
|
||||
"remove_bookmark": "ブックマークを削除"
|
||||
},
|
||||
"attribute_detail": {
|
||||
"delete": "削除"
|
||||
"delete": "削除",
|
||||
"close_button_title": "変更を破棄して閉じる",
|
||||
"attr_is_owned_by": "属性の所有者",
|
||||
"attr_name_title": "属性の名前は英数字、コロン、アンダーバーのみで構成します",
|
||||
"name": "名前",
|
||||
"value": "値",
|
||||
"promoted_alias": "エイリアス",
|
||||
"multiplicity_title": "多重性とは、同じ名前を持つ属性をいくつ作成できるかを定義するもので、1つまたはそれ以上です。",
|
||||
"multiplicity": "多重性",
|
||||
"single_value": "単一の値",
|
||||
"multi_value": "複数の値",
|
||||
"label_type_title": "ラベルのタイプは、Triliumがラベルの値を入力するのに適したインターフェースを選択するのに役立ちます。",
|
||||
"label_type": "タイプ",
|
||||
"text": "テキスト",
|
||||
"number": "数値",
|
||||
"boolean": "真偽値",
|
||||
"date": "日付",
|
||||
"date_time": "日付と時間",
|
||||
"time": "時間",
|
||||
"url": "URL",
|
||||
"save_and_close": "保存して閉じる <kbd>Ctrl+Enter</kbd>",
|
||||
"precision_title": "値設定インターフェースで、小数点以下何桁まで利用可能にするか。",
|
||||
"precision": "精度",
|
||||
"digits": "桁",
|
||||
"inheritable_title": "継承属性は、このツリー配下のすべての子孫に継承されます。",
|
||||
"inheritable": "継承",
|
||||
"related_notes_title": "このラベルが付いた他のノート"
|
||||
},
|
||||
"link_context_menu": {
|
||||
"open_note_in_popup": "クイックエディット"
|
||||
"open_note_in_popup": "クイックエディット",
|
||||
"open_note_in_new_tab": "新しいタブでノートを開く",
|
||||
"open_note_in_new_split": "新しく分割してノートを開く",
|
||||
"open_note_in_new_window": "新しいウィンドウでノートを開く"
|
||||
},
|
||||
"note_tooltip": {
|
||||
"quick-edit": "クイックエディット"
|
||||
@@ -861,6 +999,400 @@
|
||||
"hide-archived-notes": "アーカイブノートを隠す",
|
||||
"automatically-collapse-notes": "ノートを自動的に折りたたむ",
|
||||
"automatically-collapse-notes-title": "一定期間使用されないと、ツリーを整理するためにノートは折りたたまれます。",
|
||||
"save-changes": "変更を保存して適用"
|
||||
"save-changes": "変更を保存して適用",
|
||||
"auto-collapsing-notes-after-inactivity": "非アクティブ状態が続いたためノートが自動で折りたたまれます...",
|
||||
"create-child-note": "子ノートを作成"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "一括操作",
|
||||
"affected_notes": "影響されたノート",
|
||||
"include_descendants": "選択したノートの子ノートを含む",
|
||||
"available_actions": "利用可能なアクション",
|
||||
"chosen_actions": "選択されたアクション",
|
||||
"execute_bulk_actions": "一括操作を実行",
|
||||
"bulk_actions_executed": "一括操作が成功に実行されました。",
|
||||
"labels": "ラベル",
|
||||
"relations": "関係",
|
||||
"notes": "ノート",
|
||||
"other": "その他",
|
||||
"none_yet": "アクションを上のリストからクリックして追加。"
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "ここにノートのタイトルを入力..."
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "指定された検索パラメータに該当するノートは見つかりませんでした。",
|
||||
"search_not_executed": "検索はまだ実行されていません。上の「検索」ボタンをクリックすると、検索結果が表示されます。"
|
||||
},
|
||||
"sql_result": {
|
||||
"no_rows": "このクエリでは行が返されませんでした"
|
||||
},
|
||||
"sql_table_schemas": {
|
||||
"tables": "テーブル"
|
||||
},
|
||||
"app_context": {
|
||||
"please_wait_for_save": "保存が完了するまで数秒お待ちください。その後、もう一度お試しいただけます。"
|
||||
},
|
||||
"note_create": {
|
||||
"duplicated": "ノート \"{{title}}\" は複製されました。"
|
||||
},
|
||||
"clipboard": {
|
||||
"cut": "ノートはクリップボードにカットされました。",
|
||||
"copied": "ノートはクリップボードにコピーされました。",
|
||||
"copy_failed": "権限の問題で、クリップボードにコピーできません。",
|
||||
"copy_success": "クリップボードにコピーしました。"
|
||||
},
|
||||
"launcher_context_menu": {
|
||||
"add-note-launcher": "ノートランチャーを追加",
|
||||
"add-script-launcher": "スクリプトランチャーを追加",
|
||||
"add-custom-widget": "カスタムウィジェットを追加",
|
||||
"add-spacer": "スペーサーを追加",
|
||||
"delete": "削除 <kbd data-command=\"deleteNotes\"></kbd>",
|
||||
"reset": "リセット",
|
||||
"move-to-visible-launchers": "可視ランチャーに移動",
|
||||
"move-to-available-launchers": "利用可能なランチャーに移動",
|
||||
"duplicate-launcher": "ランチャーの複製 <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "自動検出"
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "コードブロック",
|
||||
"description": "テキストノート内のコードブロックのシンタックスハイライトを制御します。コードノートには影響しません。",
|
||||
"color-scheme": "カラースキーム"
|
||||
},
|
||||
"code_block": {
|
||||
"word_wrapping": "単語の折り返し",
|
||||
"theme_none": "シンタックスハイライトなし",
|
||||
"theme_group_light": "ライトテーマ",
|
||||
"theme_group_dark": "ダークテーマ",
|
||||
"copy_title": "クリップボードにコピー"
|
||||
},
|
||||
"editor": {
|
||||
"title": "エディター"
|
||||
},
|
||||
"editing": {
|
||||
"editor_type": {
|
||||
"label": "書式設定ツールバー",
|
||||
"floating": {
|
||||
"description": "編集ツールがカーソル付近に表示されます;"
|
||||
},
|
||||
"fixed": {
|
||||
"title": "固定",
|
||||
"description": "編集ツールは「書式設定」のリボンタブに表示されます。"
|
||||
},
|
||||
"multiline-toolbar": "ツールバーが収まりきらない場合は、複数行で表示する。"
|
||||
}
|
||||
},
|
||||
"electron_context_menu": {
|
||||
"add-term-to-dictionary": "辞書に \"{{term}}\" を追加",
|
||||
"cut": "カット",
|
||||
"copy": "コピー",
|
||||
"copy-link": "リンクをコピー",
|
||||
"paste": "ペースト",
|
||||
"paste-as-plain-text": "プレーンテキストで貼り付け",
|
||||
"search_online": "{{searchEngine}} で \"{{term}}\" を検索"
|
||||
},
|
||||
"duration": {
|
||||
"seconds": "秒",
|
||||
"minutes": "分",
|
||||
"hours": "時間",
|
||||
"days": "日"
|
||||
},
|
||||
"share": {
|
||||
"title": "共有設定",
|
||||
"redirect_bare_domain": "ネイキッドドメインを共有ページにリダイレクト",
|
||||
"redirect_bare_domain_description": "匿名のユーザーをログイン画面ではなく共有ページにリダイレクトします",
|
||||
"show_login_link_description": "共有ページの下部にログインリンクを追加",
|
||||
"share_root_found": "共有ルートノート '{{noteTitle}}' の準備が完了",
|
||||
"share_root_not_found": "#shareRoot のラベルが付いたノートが見つかりません",
|
||||
"share_root_not_shared": "ノート '{{noteTitle}}' は #shareRoot のラベルを持っていますが、共有されていません"
|
||||
},
|
||||
"time_selector": {
|
||||
"invalid_input": "入力された時間値が有効な数値ではありません。",
|
||||
"minimum_input": "入力された時間値は {{minimumSeconds}} 秒以上である必要があります。"
|
||||
},
|
||||
"note_language": {
|
||||
"not_set": "未設定",
|
||||
"configure-languages": "言語を設定..."
|
||||
},
|
||||
"content_language": {
|
||||
"title": "コンテンツの言語",
|
||||
"description": "読み取り専用または編集可能なテキストノートの基本プロパティセクションの言語選択に表示する言語を 1 つ以上選択します。これにより、スペルチェックや右から左へのサポートなどの機能が利用できるようになります。"
|
||||
},
|
||||
"png_export_button": {
|
||||
"button_title": "図をPNG形式でエクスポート"
|
||||
},
|
||||
"svg": {
|
||||
"export_to_png": "図をPNG形式でエクスポートできませんでした。"
|
||||
},
|
||||
"code_theme": {
|
||||
"title": "外観",
|
||||
"word_wrapping": "単語の折り返し",
|
||||
"color-scheme": "カラースキーム"
|
||||
},
|
||||
"cpu_arch_warning": {
|
||||
"title": "ARM64版をダウンロードしてください",
|
||||
"dont_show_again": "この警告を二度と表示しない",
|
||||
"continue_anyway": "とにかく続ける"
|
||||
},
|
||||
"editorfeatures": {
|
||||
"emoji_completion_enabled": "絵文字のオートコンプリートを有効",
|
||||
"note_completion_enabled": "ノートのオートコンプリートを有効"
|
||||
},
|
||||
"table_context_menu": {
|
||||
"delete_row": "行を削除"
|
||||
},
|
||||
"content_renderer": {
|
||||
"open_externally": "外部で開く"
|
||||
},
|
||||
"ai_llm": {
|
||||
"title": "AI 設定",
|
||||
"enable_ai_features": "AI/LLM 機能を有効化",
|
||||
"enable_ai_description": "ノートの要約、コンテンツ生成、その他のLLM機能などのAI機能を有効にする",
|
||||
"openai_tab": "OpenAI",
|
||||
"anthropic_tab": "Anthropic",
|
||||
"voyage_tab": "Voyage AI",
|
||||
"ollama_tab": "Ollama",
|
||||
"enable_ai": "AI/LLM 機能を有効化",
|
||||
"enable_ai_desc": "ノートの要約、コンテンツ生成、その他のLLM機能などのAI機能を有効にする",
|
||||
"provider_configuration": "AI プロバイダーの設定",
|
||||
"provider_precedence": "プロバイダーの優先順位",
|
||||
"provider_precedence_description": "カンマで区切られたプロバイダーの優先順位リスト(例: 'openai,anthropic,ollama')",
|
||||
"temperature_description": "応答のランダム性を制御する(0 = 決定的、2 = 最大ランダム性)",
|
||||
"system_prompt_description": "すべてのAIとの対話に使用されるデフォルトのシステムプロンプト",
|
||||
"system_prompt": "システムプロンプト",
|
||||
"openai_configuration": "OpenAIの設定",
|
||||
"openai_settings": "OpenAIの設定",
|
||||
"api_key": "APIキー",
|
||||
"model": "モデル",
|
||||
"openai_api_key_description": "OpenAIのAIサービスにアクセスするためのAPIキー",
|
||||
"anthropic_api_key_description": "AnthropicのClaudeモデルにアクセスするためのAPIキー",
|
||||
"default_model": "デフォルトモデル",
|
||||
"openai_model_description": "例: gpt-4o, gpt-4-turbo, gpt-3.5-turbo",
|
||||
"openai_url_description": "デフォルト: https://api.openai.com/v1",
|
||||
"anthropic_settings": "Anthropicの設定",
|
||||
"anthropic_model_description": "チャット補完用のAnthropic Claudeモデル",
|
||||
"voyage_settings": "Voyage AIの設定",
|
||||
"ollama_settings": "Ollamaの設定",
|
||||
"ollama_url_description": "Ollama API の URL(デフォルト: http://localhost:11434)",
|
||||
"anthropic_url_description": "Anthropic APIのベースURL(デフォルト: https://api.anthropic.com)",
|
||||
"ollama_model_description": "チャット補完用のOllamaモデル",
|
||||
"anthropic_configuration": "Anthropicの設定",
|
||||
"voyage_configuration": "Voyage AIの設定",
|
||||
"voyage_url_description": "デフォルト: https://api.voyageai.com/v1",
|
||||
"ollama_configuration": "Ollamaの設定",
|
||||
"enable_ollama": "Ollamaを有効",
|
||||
"enable_ollama_description": "ローカルAIモデルを利用するためOllamaを有効にする",
|
||||
"ollama_url": "Ollama URL",
|
||||
"ollama_model": "Ollamaモデル",
|
||||
"refresh_models": "モデルを更新",
|
||||
"refreshing_models": "更新中...",
|
||||
"error": "エラー",
|
||||
"retry": "再試行",
|
||||
"partial": "{{ percentage }}%完了",
|
||||
"processing": "処理中({{percentage}}%)",
|
||||
"complete": "完了(100%)",
|
||||
"refreshing": "更新中...",
|
||||
"auto_refresh_notice": "{{seconds}}秒ごとに自動更新",
|
||||
"ai_settings": "AI 設定",
|
||||
"api_key_tooltip": "サービスにアクセスするためのAPIキー",
|
||||
"empty_key_warning": {
|
||||
"anthropic": "Anthropic APIキーが空です。有効な APIキーを入力してください。",
|
||||
"openai": "OpenAI APIキーが空です。有効なAPIキーを入力してください。",
|
||||
"voyage": "Voyage APIキーが空です。有効なAPIキーを入力してください。",
|
||||
"ollama": "Ollama APIキーが空です。有効なAPIキーを入力してください。"
|
||||
},
|
||||
"agent": {
|
||||
"processing": "処理中...",
|
||||
"loading": "読み込み中...",
|
||||
"generating": "生成中..."
|
||||
},
|
||||
"name": "AI",
|
||||
"openai": "OpenAI",
|
||||
"error_contacting_provider": "AIプロバイダーへの接続中にエラーが発生しました。設定とインターネット接続をご確認ください。",
|
||||
"ollama_no_url": "Ollamaは設定されていません。有効なURLを入力してください。",
|
||||
"chat": {
|
||||
"root_note_title": "AIチャット",
|
||||
"root_note_content": "このメモには、保存されたAIチャットの会話が含まれています。",
|
||||
"new_chat_title": "新しいチャット",
|
||||
"create_new_ai_chat": "新しいAIチャットを作成"
|
||||
},
|
||||
"create_new_ai_chat": "新しいAIチャットを作成",
|
||||
"configuration_warnings": "AIの設定に問題があります。設定を確認してください。",
|
||||
"experimental_warning": "LLM機能は現在実験的です - ご注意ください。",
|
||||
"selected_provider_description": "チャットおよび補完機能のAIプロバイダーを選択する",
|
||||
"selected_provider": "プロバイダーを選択",
|
||||
"select_model": "モデルを選択...",
|
||||
"select_provider": "プロバイダーを選択..."
|
||||
},
|
||||
"add_label": {
|
||||
"add_label": "ラベルを追加",
|
||||
"label_name_placeholder": "ラベル名",
|
||||
"label_name_title": "英数字、アンダーバー、コロンが使用可能な文字です。",
|
||||
"new_value_placeholder": "新しい値"
|
||||
},
|
||||
"delete_label": {
|
||||
"delete_label": "ラベルを削除",
|
||||
"label_name_placeholder": "ラベル名",
|
||||
"label_name_title": "英数字、アンダーバー、コロンが使用可能な文字です。"
|
||||
},
|
||||
"rename_label": {
|
||||
"rename_label": "ラベル名の変更",
|
||||
"rename_label_from": "ラベル名を",
|
||||
"old_name_placeholder": "古い名前",
|
||||
"to": "から",
|
||||
"new_name_placeholder": "新しい名前",
|
||||
"name_title": "英数字、アンダーバー、コロンが使用可能な文字です。"
|
||||
},
|
||||
"update_label_value": {
|
||||
"update_label_value": "ラベルの値を更新",
|
||||
"label_name_placeholder": "ラベル名",
|
||||
"label_name_title": "英数字、アンダーバー、コロンが使用可能な文字です。",
|
||||
"new_value_placeholder": "新しい値",
|
||||
"help_text_note": "このメソッドは値なしで呼び出すこともできます。その場合、値なしでラベルがノートに割り当てられます。"
|
||||
},
|
||||
"add_relation": {
|
||||
"add_relation": "関係を追加",
|
||||
"relation_name": "関係の名前",
|
||||
"allowed_characters": "英数字、アンダーバー、コロンが使用可能な文字です。"
|
||||
},
|
||||
"delete_relation": {
|
||||
"delete_relation": "関係を削除",
|
||||
"relation_name": "関係の名前",
|
||||
"allowed_characters": "英数字、アンダーバー、コロンが使用可能な文字です。"
|
||||
},
|
||||
"rename_relation": {
|
||||
"rename_relation": "関係の名前を変更",
|
||||
"rename_relation_from": "関係の名前を",
|
||||
"old_name": "古い名前",
|
||||
"to": "から",
|
||||
"new_name": "新しい名前",
|
||||
"allowed_characters": "英数字、アンダーバー、コロンが使用可能な文字です。"
|
||||
},
|
||||
"update_relation_target": {
|
||||
"update_relation": "関係の更新",
|
||||
"relation_name": "関係の名前",
|
||||
"allowed_characters": "英数字、アンダーバー、コロンが使用可能な文字です。"
|
||||
},
|
||||
"revisions_button": {
|
||||
"note_revisions": "ノートの変更履歴"
|
||||
},
|
||||
"note_launcher": {
|
||||
"this_launcher_doesnt_define_target_note": "このランチャーはターゲットノートを定義していません。"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} バックリンク",
|
||||
"backlinks": "{{count}} バックリンク",
|
||||
"relation": "関係"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"delete_this_note": "このノートを削除",
|
||||
"error_unrecognized_command": "認識されないコマンド {{command}}"
|
||||
},
|
||||
"inherited_attribute_list": {
|
||||
"title": "継承属性",
|
||||
"no_inherited_attributes": "継承属性ではありません。"
|
||||
},
|
||||
"note_map": {
|
||||
"open_full": "拡大表示",
|
||||
"collapse": "通常サイズに折りたたむ",
|
||||
"title": "ノートマップ",
|
||||
"link-distance": "リンク距離"
|
||||
},
|
||||
"owned_attribute_list": {
|
||||
"owned_attributes": "所有属性"
|
||||
},
|
||||
"promoted_attributes": {
|
||||
"unset-field-placeholder": "未設定",
|
||||
"open_external_link": "外部でリンクを開く",
|
||||
"unknown_label_type": "不明なラベルタイプ '{{type}}'",
|
||||
"unknown_attribute_type": "不明な属性タイプ '{{type}}'",
|
||||
"add_new_attribute": "新しい属性を追加",
|
||||
"remove_this_attribute": "この属性を削除",
|
||||
"remove_color": "このカラーラベルを削除"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "新しいタブで開く",
|
||||
"remove_note": "ノートを削除",
|
||||
"edit_title": "タイトルを編集",
|
||||
"rename_note": "ノート名を変更",
|
||||
"enter_new_title": "新しいノート名を入力:",
|
||||
"remove_relation": "関係を削除",
|
||||
"confirm_remove_relation": "本当にこの関係を削除しますか?",
|
||||
"specify_new_relation_name": "新しい関係の名前(使用可能な文字: 英数字、コロン、アンダースコア)を指定:",
|
||||
"note_not_found": "ノート {{noteId}} が見つかりません!",
|
||||
"enter_title_of_new_note": "新しいノートのタイトルを入力",
|
||||
"default_new_note_title": "新しいノート"
|
||||
},
|
||||
"database_anonymization": {
|
||||
"title": "データベースの匿名化",
|
||||
"full_anonymization": "完全匿名化",
|
||||
"full_anonymization_description": "この操作により、データベースの新しいコピーが作成され、匿名化されます(すべてのノートの内容を削除し、構造と一部の非機密メタデータのみを残します)。これにより、個人データが漏洩する心配なく、デバッグ目的でオンライン共有できます。",
|
||||
"save_fully_anonymized_database": "完全に匿名化されたデータベースを保存",
|
||||
"light_anonymization": "軽い匿名化",
|
||||
"light_anonymization_description": "この操作により、データベースの新しいコピーが作成され、軽い匿名化が適用されます。具体的には、すべてのノートの内容のみが削除され、タイトルと属性はそのまま残ります。さらに、カスタムJSフロントエンド/バックエンドスクリプトノートとカスタムウィジェットもそのまま残ります。これにより、問題のデバッグのためのコンテキストがより多く提供されます。",
|
||||
"choose_anonymization": "完全に匿名化したデータベースを提供するか、軽く匿名化したデータベースを提供するかは、あなた自身が決めることができます。完全に匿名化されたDBであっても非常に有用ですが、場合によっては軽く匿名化されたDBの方がバグの特定と修正のプロセスを速めることができます。",
|
||||
"save_lightly_anonymized_database": "軽く匿名化されたデータベースを保存",
|
||||
"existing_anonymized_databases": "既存の匿名化データベース",
|
||||
"creating_fully_anonymized_database": "完全に匿名化されたデータベースを作成中...",
|
||||
"creating_lightly_anonymized_database": "軽く匿名化されたデータベースを作成中...",
|
||||
"error_creating_anonymized_database": "匿名化データベースの作成に失敗しました。詳細はバックエンドログを確認してください",
|
||||
"successfully_created_fully_anonymized_database": "完全に匿名化されたデータベースを {{anonymizedFilePath}} に作成",
|
||||
"successfully_created_lightly_anonymized_database": "軽く匿名化されたデータベースを {{anonymizedFilePath}} に作成",
|
||||
"no_anonymized_database_yet": "匿名化されたデータベースはまだありません。"
|
||||
},
|
||||
"database_integrity_check": {
|
||||
"title": "データベースの整合性チェック",
|
||||
"description": "これは、データベースがSQLiteレベルで破損がないかをチェックします。DBのサイズによっては時間がかかる場合があります。",
|
||||
"check_button": "データベースの整合性をチェック",
|
||||
"checking_integrity": "データベースの整合性をチェックしています...",
|
||||
"integrity_check_succeeded": "整合性チェックに成功 - 問題は見つかりませんでした。",
|
||||
"integrity_check_failed": "整合性チェックに失敗: {{results}}"
|
||||
},
|
||||
"code-editor-options": {
|
||||
"title": "エディター"
|
||||
},
|
||||
"search_string": {
|
||||
"title_column": "文字列検索:",
|
||||
"search_syntax": "検索構文",
|
||||
"also_see": "詳しくは",
|
||||
"complete_help": "検索構文に関する完全なヘルプ",
|
||||
"full_text_search": "テキストを入力すると全文検索が可能",
|
||||
"label_abc": "ラベルabcを持つノートを返す",
|
||||
"label_year": "ラベル「year」の値が「2019」と一致するノート",
|
||||
"label_rock_pop": "rock と pop のラベルを持つノート",
|
||||
"label_rock_or_pop": "どれかのラベルが存在すること",
|
||||
"label_year_comparison": "数値比較(>、>=、<も含む)。",
|
||||
"label_date_created": "過去1ヶ月以内に作成されたノート",
|
||||
"error": "検索エラー: {{error}}",
|
||||
"search_prefix": "検索:"
|
||||
},
|
||||
"delete_revisions": {
|
||||
"delete_note_revisions": "ノートの変更履歴を削除",
|
||||
"all_past_note_revisions": "一致したノートの過去の変更履歴がすべて削除されます。ノート自体は完全に保持されます。言い換えると、ノートのリビジョンが削除されます。"
|
||||
},
|
||||
"rename_note": {
|
||||
"rename_note": "ノート名を変更",
|
||||
"new_note_title": "新しいノート名",
|
||||
"rename_note_title_to": "ノート名を変更",
|
||||
"example_note": "<code>Note</code> - マッチしたノートの名前をすべて'Note'に変更",
|
||||
"example_new_title": "<code>NEW: ${note.title}</code> - 一致したノートの名前の前に 'NEW: ' を付ける",
|
||||
"example_date_prefix": "<code>${note.dateCreatedObj.format('MM-DD:')}: ${note.title}</code> - マッチしたノートの前にノートの作成月日を付ける",
|
||||
"api_docs": "詳細については、 <a href='https://zadam.github.io/trilium/backend_api/Note.html'>note</a> および <a href='https://day.js.org/docs/en/display/format'>dateCreatedObj / utcDateCreatedObj properties</a> の API ドキュメントを参照してください。",
|
||||
"evaluated_as_js_string": "与えられた値はJavaScript文字列として評価されるため、注入された<code>note</code>変数(noteは名前が変更されます)を介して動的なコンテンツで強化できます。例:"
|
||||
},
|
||||
"electron_integration": {
|
||||
"desktop-application": "デスクトップアプリケーション",
|
||||
"native-title-bar": "ネイティブタイトルバー",
|
||||
"native-title-bar-description": "WindowsとmacOSでは、ネイティブタイトルバーをオフにしておくと、アプリケーションがよりコンパクトに見えます。Linuxでは、ネイティブタイトルバーを表示したままの方が、他のシステムとの統一性が高まります。",
|
||||
"background-effects": "背景効果を有効化(Windows 11のみ)",
|
||||
"background-effects-description": "Mica効果は、アプリのウィンドウにぼかされたスタイリッシュな背景を追加し、奥行きとモダンな外観を作り出します。",
|
||||
"restart-app-button": "アプリケーションを再起動して変更を反映",
|
||||
"zoom-factor": "ズーム倍率"
|
||||
},
|
||||
"zoom_factor": {
|
||||
"description": "ズームは CTRL+- と CTRL+= のショートカットでも操作可能。",
|
||||
"title": "ズーム倍率(デスクトップビルドのみ)"
|
||||
}
|
||||
}
|
||||
|
||||
29
apps/client/src/translations/ko/translation.json
Normal file
29
apps/client/src/translations/ko/translation.json
Normal 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": "저장"
|
||||
}
|
||||
}
|
||||
9
apps/client/src/translations/nl/translation.json
Normal file
9
apps/client/src/translations/nl/translation.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"about": {
|
||||
"title": "Over Trilium Notes",
|
||||
"homepage": "Homepagina:",
|
||||
"app_version": "App versie:",
|
||||
"db_version": "DB Versie:",
|
||||
"sync_version": "Sync Versie:"
|
||||
}
|
||||
}
|
||||
41
apps/client/src/translations/pl/translation.json
Normal file
41
apps/client/src/translations/pl/translation.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,121 @@
|
||||
"n_notes_queued_2": "{{ count }} notas enfileiradas para indexação",
|
||||
"notes_indexed_0": "{{ count }} nota indexada",
|
||||
"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": {
|
||||
"confirmation": "Confirmação",
|
||||
@@ -405,10 +519,723 @@
|
||||
"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.",
|
||||
"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",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"add_relation": {
|
||||
"add_relation": "Adaugă relație",
|
||||
"allowed_characters": "Sunt permise doar caractere alfanumerice, underline și două puncte.",
|
||||
"create_relation_on_all_matched_notes": "Crează relația pentru toate notițele găsite",
|
||||
"create_relation_on_all_matched_notes": "Creează relația pentru toate notițele găsite.",
|
||||
"relation_name": "denumirea relației",
|
||||
"target_note": "notița destinație",
|
||||
"to": "către"
|
||||
@@ -76,9 +76,9 @@
|
||||
"attachment_erasure_timeout": {
|
||||
"attachment_auto_deletion_description": "Atașamentele se șterg automat (permanent) dacă nu sunt referențiate de către notița lor părinte după un timp prestabilit de timp.",
|
||||
"attachment_erasure_timeout": "Perioadă de ștergere a atașamentelor",
|
||||
"erase_attachments_after": "Erase unused attachments after:",
|
||||
"erase_attachments_after": "Șterge atașamentele neutilizate după:",
|
||||
"erase_unused_attachments_now": "Elimină atașamentele șterse acum",
|
||||
"manual_erasing_description": "Șterge acum toate atașamentele nefolosite din notițe",
|
||||
"manual_erasing_description": "Puteți șterge atașamentele nefolosite manual (fără a lua în considerare timpul de mai sus):",
|
||||
"unused_attachments_erased": "Atașamentele nefolosite au fost șterse."
|
||||
},
|
||||
"attachment_list": {
|
||||
@@ -141,7 +141,7 @@
|
||||
"hide_promoted_attributes": "Ascunde lista atributelor promovate pentru această notiță",
|
||||
"hide_relations": "lista denumirilor relațiilor ce trebuie ascunse, delimitate prin virgulă. Toate celelalte vor fi afișate.",
|
||||
"icon_class": "valoarea acestei etichete este adăugată ca o clasă CSS la iconița notiței din ierarhia notițelor, fapt ce poate ajuta la identificarea vizuală mai rapidă a notițelor. Un exemplu ar fi „bx bx-home” pentru iconițe preluate din boxicons. Poate fi folosită în notițe de tip șablon.",
|
||||
"inbox": "locația implicită în care vor apărea noile notițe atunci când se crează o noitiță utilizând butonul „Crează notiță” din bara laterală, notițele vor fi create în interiorul notiței cu această etichetă.",
|
||||
"inbox": "locația implicită în care vor apărea noile notițe atunci când se crează o noitiță utilizând butonul „Crează notiță” din bara laterală, notițele vor fi create în interiorul notiței marcată cu eticheta <code>#inbox</code>.",
|
||||
"inherit": "atributele acestei notițe vor fi moștenite chiar dacă nu există o relație părinte-copil între notițe. A se vedea relația de tip șablon pentru un concept similar. De asemenea, a se vedea moștenirea atributelor în documentație.",
|
||||
"inheritable": "Moștenibilă",
|
||||
"inheritable_title": "Atributele moștenibile vor fi moștenite de către toți descendenții acestei notițe.",
|
||||
@@ -177,7 +177,7 @@
|
||||
"render_note": "relație ce definește notița (de tip notiță de cod HTML sau script) ce trebuie randată pentru notițele de tip „Randare notiță HTML”",
|
||||
"run": "definește evenimentele la care să ruleze scriptul. Valori acceptate:\n<ul>\n<li>frontendStartup - când pornește interfața Trilium (sau este reîncărcată), dar nu pe mobil.</li>\n<li>mobileStartup - când pornește interfața Trilium (sau este reîncărcată), doar pe mobil.</li>\n<li>backendStartup - când pornește serverul Trilium</li>\n<li>hourly - o dată pe oră. Se poate utiliza adițional eticheta <code>runAtHour</code> pentru a specifica ora.</li>\n<li>daily - o dată pe zi</li>\n</ul>",
|
||||
"run_at_hour": "La ce oră ar trebui să ruleze. Trebuie folosit împreună cu <code>#run=hourly</code>. Poate fi definit de mai multe ori pentru a rula de mai multe ori în cadrul aceleași zile.",
|
||||
"run_on_attribute_change": "se execută atunci când atributele unei notițe care definește această relație se schimbă. Se apelează și atunci când un atribut este șters",
|
||||
"run_on_attribute_change": " se execută atunci când atributele unei notițe care definește această relație se schimbă. Se apelează și atunci când un atribut este șters",
|
||||
"run_on_attribute_creation": "se execută atunci când un nou atribut este creat pentru notița care definește această relație",
|
||||
"run_on_branch_change": "se execută atunci când o ramură este actualizată.",
|
||||
"run_on_branch_creation": "se execută când o ramură este creată. O ramură este o legătură dintre o notiță părinte și o notiță copil și este creată, spre exemplu, la clonarea sau mutarea unei notițe.",
|
||||
@@ -198,7 +198,7 @@
|
||||
"share_disallow_robot_indexing": "împiedică indexarea conținutului de către roboți utilizând antetul <code>X-Robots-Tag: noindex</code>",
|
||||
"share_external_link": "notița va funcționa drept o legătură către un site web extern în ierarhia de partajare",
|
||||
"share_favicon": "Notiță ce conține pictograma favicon pentru a fi setată în paginile partajate. De obicei se poate seta în rădăcina ierarhiei de partajare și se poate face moștenibilă. Notița ce conține favicon-ul trebuie să fie și ea în ierarhia de partajare. Considerați și utilizarea „share_hidden_from_tree”.",
|
||||
"share_hidden_from_tree": "notița este ascunsă din arborele de navigație din stânga, dar încă este accesibilă prin intermediul unui URL.",
|
||||
"share_hidden_from_tree": "notița este ascunsă din arborele de navigație din stânga, dar încă este accesibilă prin intermediul unui URL",
|
||||
"share_index": "notițele cu această etichetă vor afișa lista tuturor rădăcilor notițelor partajate",
|
||||
"share_js": "Notiță JavaScript ce va fi injectată în pagina de partajare. Notița respectivă trebuie să fie și ea în ierarhia de partajare. Considerați utilizarea 'share_hidden_from_tree'.",
|
||||
"share_omit_default_css": "CSS-ul implicit pentru pagina de partajare va fi omis. Se poate folosi atunci când se fac schimbări majore de stil la pagină.",
|
||||
@@ -214,7 +214,7 @@
|
||||
"target_note_title": "Relația este o conexiune numită dintre o notiță sursă și o notiță țintă.",
|
||||
"template": "Șablon",
|
||||
"text": "Text",
|
||||
"title_template": "titlul implicit al notițelor create în interiorul acestei notițe. Valoarea este evaluată ca un șir de caractere JavaScript\n și poate fi astfel îmbogățită cu un conținut dinamic prin intermediul variabilelow <code>now</code> și <code>parentNote</code>. Exemple:\n \n <ul>\n <li><code>Lucrările lui ${parentNote.getLabelValue('autor')}</code></li>\n <li><code>Jurnal pentru ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n A se vedea <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">wiki-ul pentru detalii</a>, documentația API pentru <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> și <a href=\"https://day.js.org/docs/en/display/format\">now</a> pentru mai multe informații",
|
||||
"title_template": "titlul implicit al notițelor create în interiorul acestei notițe. Valoarea este evaluată ca un șir de caractere JavaScript\n și poate fi astfel îmbogățită cu un conținut dinamic prin intermediul variabilelor <code>now</code> și <code>parentNote</code>. Exemple:\n \n <ul>\n <li><code>Lucrările lui ${parentNote.getLabelValue('autor')}</code></li>\n <li><code>Jurnal pentru ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n A se vedea <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">wiki-ul pentru detalii</a>, documentația API pentru <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> și <a href=\"https://day.js.org/docs/en/display/format\">now</a> pentru mai multe informații.",
|
||||
"toc": "<code>#toc</code> sau <code>#toc=show</code> forțează afișarea tabelei de conținut, <code>#toc=hide</code> forțează ascunderea ei. Dacă eticheta nu există, se utilizează setările globale",
|
||||
"top": "păstrează notița la începutul listei (se aplică doar pentru notițe sortate automat)",
|
||||
"url": "URL",
|
||||
@@ -369,7 +369,7 @@
|
||||
},
|
||||
"confirm": {
|
||||
"also_delete_note": "Șterge și notița",
|
||||
"are_you_sure_remove_note": "Doriți ștergerea notiței „{{title}}” din harta de relații?",
|
||||
"are_you_sure_remove_note": "Doriți ștergerea notiței „{{title}}” din harta de relații? ",
|
||||
"cancel": "Anulează",
|
||||
"confirmation": "Confirm",
|
||||
"if_you_dont_check": "Dacă această opțiune nu este bifată, notița va fi ștearsă doar din harta de relații.",
|
||||
@@ -519,8 +519,8 @@
|
||||
"export_status": "Starea exportului",
|
||||
"export_type_single": "Doar această notiță fără descendenții ei",
|
||||
"export_type_subtree": "Această notiță și toți descendenții ei",
|
||||
"format_html_zip": "HTML în arhivă ZIP - recomandat deoarece păstrează toată formatarea",
|
||||
"format_markdown": "Markdown - păstrează majoritatea formatării",
|
||||
"format_html_zip": "HTML în arhivă ZIP - recomandat deoarece păstrează toată formatarea.",
|
||||
"format_markdown": "Markdown - păstrează majoritatea formatării.",
|
||||
"format_opml": "OPML - format de interschimbare pentru editoare cu structură ierarhică (outline). Formatarea, imaginile și fișierele nu vor fi incluse.",
|
||||
"opml_version_1": "OPML v1.0 - text simplu",
|
||||
"opml_version_2": "OPML v2.0 - permite și HTML",
|
||||
@@ -640,7 +640,7 @@
|
||||
"newTabNoteLink": "pe o legătură către o notiță va deschide notița într-un tab nou",
|
||||
"notSet": "nesetat",
|
||||
"noteNavigation": "Navigarea printre notițe",
|
||||
"numberedList": "<kbd>1.</code> sau <code>1)</code> urmat de spațiu pentru o listă numerotată",
|
||||
"numberedList": "<code>1.</code> sau <code>1)</code> urmat de spațiu pentru o listă numerotată",
|
||||
"onlyInDesktop": "Doar pentru desktop (aplicația Electron)",
|
||||
"openEmptyTab": "deschide un tab nou",
|
||||
"other": "Altele",
|
||||
@@ -655,7 +655,8 @@
|
||||
"showSQLConsole": "afișează consola SQL",
|
||||
"tabShortcuts": "Scurtături pentru tab-uri",
|
||||
"troubleshooting": "Unelte pentru depanare",
|
||||
"newTabWithActivationNoteLink": "pe o legătură către o notiță deschide și activează notița într-un tab nou"
|
||||
"newTabWithActivationNoteLink": "pe o legătură către o notiță deschide și activează notița într-un tab nou",
|
||||
"title": "Ghid rapid"
|
||||
},
|
||||
"hide_floating_buttons_button": {
|
||||
"button_title": "Ascunde butoanele"
|
||||
@@ -806,7 +807,7 @@
|
||||
"dialog_title": "Mută notițele în...",
|
||||
"error_no_path": "Nicio cale la care să poată fi mutate.",
|
||||
"move_button": "Mută la notița selectată",
|
||||
"move_success_message": "Notițele selectate au fost mutate în",
|
||||
"move_success_message": "Notițele selectate au fost mutate în ",
|
||||
"notes_to_move": "Notițe de mutat",
|
||||
"search_placeholder": "căutați notița după denumirea ei",
|
||||
"target_parent_note": "Notița părinte destinație"
|
||||
@@ -886,7 +887,8 @@
|
||||
"modal_title": "Selectați tipul notiței",
|
||||
"templates": "Șabloane",
|
||||
"change_path_prompt": "Selectați locul unde să se creeze noua notiță:",
|
||||
"search_placeholder": "căutare cale notiță după nume (cea implicită dacă este necompletat)"
|
||||
"search_placeholder": "căutare cale notiță după nume (cea implicită dacă este necompletat)",
|
||||
"builtin_templates": "Șabloane predefinite"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "Butonul „{{componentId}}” nu are nicio acțiune la clic definită"
|
||||
@@ -940,7 +942,9 @@
|
||||
},
|
||||
"password_not_set": {
|
||||
"body1": "Notițele protejate sunt criptate utilizând parola de utilizator, dar nu a fost setată nicio parolă.",
|
||||
"title": "Parola nu este setată"
|
||||
"title": "Parola nu este setată",
|
||||
"body2": "Pentru a putea proteja notițe, clic pe butonul de mai jos pentru a deschide fereastra de opțiuni și pentru a seta parola.",
|
||||
"go_to_password_options": "Mergi la setările de parolă"
|
||||
},
|
||||
"promoted_attributes": {
|
||||
"add_new_attribute": "Adaugă un nou atribut",
|
||||
@@ -1054,7 +1058,7 @@
|
||||
"download_button": "Descarcă",
|
||||
"file_size": "Dimensiune fișier:",
|
||||
"help_title": "Informații despre reviziile notițelor",
|
||||
"mime": "MIME:",
|
||||
"mime": "MIME: ",
|
||||
"no_revisions": "Nu există încă nicio revizie pentru această notiță...",
|
||||
"note_revisions": "Revizii ale notiței",
|
||||
"preview": "Previzualizare:",
|
||||
@@ -1072,7 +1076,7 @@
|
||||
"note_revisions": "Revizii ale notiței"
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_description": "Intervalul de salvare a reviziilor este timpul după care se crează o nouă revizie a unei notițe. Vedeți <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki-ul</a> pentru mai multe informații.",
|
||||
"note_revisions_snapshot_description": "Intervalul de salvare a reviziilor este timpul după care se crează o nouă revizie a unei notițe. Vedeți <doc>wiki-ul</doc> pentru mai multe informații.",
|
||||
"note_revisions_snapshot_interval_title": "Intervalul de salvare a reviziilor",
|
||||
"snapshot_time_interval_label": "Intervalul de salvare a reviziilor:"
|
||||
},
|
||||
@@ -1189,7 +1193,7 @@
|
||||
"enable": "Activează corectorul ortografic",
|
||||
"language_code_label": "Codurile de limbă",
|
||||
"language_code_placeholder": "de exemplu „en-US”, „de-AT”",
|
||||
"multiple_languages_info": "Mai multe limbi pot fi separate prin virgulă, e.g. \"en-US, de-DE, cs\".",
|
||||
"multiple_languages_info": "Mai multe limbi pot fi separate prin virgulă, e.g. \"en-US, de-DE, cs\". ",
|
||||
"title": "Corector ortografic",
|
||||
"restart-required": "Schimbările asupra setărilor corectorului ortografic vor fi aplicate după restartarea aplicației."
|
||||
},
|
||||
@@ -1243,7 +1247,10 @@
|
||||
"layout-horizontal-description": "bara de lansare se află sub bara de taburi, bara de taburi este pe toată lungimea.",
|
||||
"layout-horizontal-title": "Orizontal",
|
||||
"layout-vertical-title": "Vertical",
|
||||
"layout-vertical-description": "bara de lansare se află pe stânga (implicit)"
|
||||
"layout-vertical-description": "bara de lansare se află pe stânga (implicit)",
|
||||
"auto_theme": "Tema clasică (se adaptează la schema de culori a sistemului)",
|
||||
"light_theme": "Tema clasică (luminoasă)",
|
||||
"dark_theme": "Tema clasică (întunecată)"
|
||||
},
|
||||
"toast": {
|
||||
"critical-error": {
|
||||
@@ -1279,7 +1286,7 @@
|
||||
"update_relation_target": {
|
||||
"allowed_characters": "Sunt permise doar caractere alfanumerice, underline și două puncte.",
|
||||
"change_target_note": "schimbă notița-țintă a unei relații existente",
|
||||
"on_all_matched_notes": "Pentru toate notițele găsite:",
|
||||
"on_all_matched_notes": "Pentru toate notițele găsite",
|
||||
"relation_name": "denumirea relației",
|
||||
"target_note": "notița destinație",
|
||||
"to": "la",
|
||||
@@ -1307,7 +1314,7 @@
|
||||
"use_vim_keybindings_in_code_notes": "Combinații de taste Vim"
|
||||
},
|
||||
"web_view": {
|
||||
"create_label": "Pentru a începe, creați o etichetă cu adresa URL de încorporat, e.g. #webViewSrc=\"https://www.google.com\"",
|
||||
"create_label": "Pentru a începe, creați o etichetă cu adresa URL de încorporat, e.g. #webViewSrc=\"https://www.google.com\"",
|
||||
"embed_websites": "Notițele de tip „Vizualizare web” permit încorporarea site-urilor web în Trilium.",
|
||||
"web_view": "Vizualizare web"
|
||||
},
|
||||
@@ -1856,15 +1863,20 @@
|
||||
},
|
||||
"create_new_ai_chat": "Crează o nouă discuție cu AI-ul",
|
||||
"configuration_warnings": "Sunt câteva probleme la configurația AI-ului. Verificați setările.",
|
||||
"experimental_warning": "Funcția LLM este experimentală!",
|
||||
"experimental_warning": "Funcția LLM este experimentală.",
|
||||
"selected_provider": "Furnizor selectat",
|
||||
"selected_provider_description": "Selectați furnizorul de AI pentru funcțiile de discuție și completare",
|
||||
"select_model": "Selectați modelul...",
|
||||
"select_provider": "Selectați furnizorul..."
|
||||
"select_provider": "Selectați furnizorul...",
|
||||
"ai_enabled": "Funcționalitățile AI au fost activate",
|
||||
"ai_disabled": "Funcționalitățile AI au fost dezactivate",
|
||||
"no_models_found_online": "Nu s-a găsit niciun model. Verificați cheia API și configurația.",
|
||||
"no_models_found_ollama": "Nu s-a găsit niciun model Ollama. Verificați dacă Ollama rulează.",
|
||||
"error_fetching": "Eroare la obținerea modelelor: {{error}}"
|
||||
},
|
||||
"custom_date_time_format": {
|
||||
"title": "Format dată/timp personalizat",
|
||||
"description": "Personalizați formatul de dată și timp inserat prin <kbd></kbd> sau din bara de unelte. Vedeți <a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">Documentația Day.js</a> pentru câmpurile de formatare disponibile.",
|
||||
"description": "Personalizați formatul de dată și timp inserat prin <shortcut /> sau din bara de unelte. Vedeți <doc>Documentația Day.js</doc> pentru câmpurile de formatare disponibile.",
|
||||
"format_string": "Șir de formatare:",
|
||||
"formatted_time": "Data și ora formatate:"
|
||||
},
|
||||
@@ -1985,6 +1997,32 @@
|
||||
"open_externally": "Deschide în afara programului"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Închide"
|
||||
"close": "Închide",
|
||||
"help_title": "Afișează mai multe informații despre acest ecran"
|
||||
},
|
||||
"call_to_action": {
|
||||
"background_effects_title": "Efectele de fundal sunt acum stabile",
|
||||
"background_effects_message": "Pe dispozitive cu Windows, efectele de fundal sunt complet stabile. Acestea adaugă un strop de culoare interfeței grafice prin estomparea fundalului din spatele ferestrei. Această tehnică este folosită și în alte aplicații precum Windows Explorer.",
|
||||
"background_effects_button": "Activează efectele de fundal",
|
||||
"next_theme_title": "Încercați noua temă Trilium",
|
||||
"next_theme_message": "Utilizați tema clasică, doriți să încercați noua temă?",
|
||||
"next_theme_button": "Testează noua temă",
|
||||
"dismiss": "Treci peste"
|
||||
},
|
||||
"ui-performance": {
|
||||
"title": "Setări de performanță",
|
||||
"enable-motion": "Activează tranzițiile și animațiile",
|
||||
"enable-shadows": "Activează umbrirea elementelor",
|
||||
"enable-backdrop-effects": "Activează efectele de fundal pentru meniuri, popup-uri și panouri"
|
||||
},
|
||||
"settings": {
|
||||
"related_settings": "Setări similare"
|
||||
},
|
||||
"settings_appearance": {
|
||||
"related_code_blocks": "Tema de culori pentru blocuri de cod în notițe de tip text",
|
||||
"related_code_notes": "Tema de culori pentru notițele de tip cod"
|
||||
},
|
||||
"units": {
|
||||
"percentage": "%"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2022
apps/client/src/translations/uk/translation.json
Normal file
2022
apps/client/src/translations/uk/translation.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,69 +1,80 @@
|
||||
{
|
||||
"about": {
|
||||
"homepage": "Trang chủ:",
|
||||
"title": "Về Trilium Notes"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Thêm liên kết",
|
||||
"button_add_link": "Thêm liên kết"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"other": "Khác"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"save": "Lưu"
|
||||
},
|
||||
"confirm": {
|
||||
"ok": "OK",
|
||||
"cancel": "Huỷ"
|
||||
},
|
||||
"delete_notes": {
|
||||
"close": "Đóng",
|
||||
"ok": "OK",
|
||||
"cancel": "Huỷ"
|
||||
},
|
||||
"export": {
|
||||
"close": "Đóng"
|
||||
},
|
||||
"help": {
|
||||
"other": "Khác"
|
||||
},
|
||||
"toast": {
|
||||
"critical-error": {
|
||||
"title": "Lỗi nghiêm trọng"
|
||||
}
|
||||
},
|
||||
"import": {
|
||||
"options": "Tuỳ chọn"
|
||||
},
|
||||
"info": {
|
||||
"okButton": "OK",
|
||||
"closeButton": "Đóng"
|
||||
},
|
||||
"move_to": {
|
||||
"dialog_title": "Chuyển ghi chép tới..."
|
||||
},
|
||||
"prompt": {
|
||||
"ok": "OK"
|
||||
},
|
||||
"protected_session_password": {
|
||||
"close_label": "Đóng"
|
||||
},
|
||||
"revisions": {
|
||||
"restore_button": "Khôi phục",
|
||||
"delete_button": "Xoá"
|
||||
},
|
||||
"upload_attachments": {
|
||||
"options": "Tuỳ chọn"
|
||||
},
|
||||
"attribute_detail": {
|
||||
"name": "Tên",
|
||||
"value": "Giá trị",
|
||||
"text": "Văn bản",
|
||||
"number": "Số",
|
||||
"delete": "Xoá"
|
||||
},
|
||||
"rename_note": {
|
||||
"rename_note": "Đổi tên ghi chép"
|
||||
"about": {
|
||||
"homepage": "Trang chủ:",
|
||||
"title": "Về Trilium Notes"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Thêm liên kết",
|
||||
"button_add_link": "Thêm liên kết"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"other": "Khác"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"save": "Lưu"
|
||||
},
|
||||
"confirm": {
|
||||
"ok": "OK",
|
||||
"cancel": "Huỷ"
|
||||
},
|
||||
"delete_notes": {
|
||||
"close": "Đóng",
|
||||
"ok": "OK",
|
||||
"cancel": "Huỷ"
|
||||
},
|
||||
"export": {
|
||||
"close": "Đóng"
|
||||
},
|
||||
"help": {
|
||||
"other": "Khác"
|
||||
},
|
||||
"toast": {
|
||||
"critical-error": {
|
||||
"title": "Lỗi nghiêm trọng"
|
||||
}
|
||||
},
|
||||
"import": {
|
||||
"options": "Tuỳ chọn"
|
||||
},
|
||||
"info": {
|
||||
"okButton": "OK",
|
||||
"closeButton": "Đóng"
|
||||
},
|
||||
"move_to": {
|
||||
"dialog_title": "Chuyển ghi chép tới..."
|
||||
},
|
||||
"prompt": {
|
||||
"ok": "OK"
|
||||
},
|
||||
"protected_session_password": {
|
||||
"close_label": "Đóng"
|
||||
},
|
||||
"revisions": {
|
||||
"restore_button": "Khôi phục",
|
||||
"delete_button": "Xoá"
|
||||
},
|
||||
"upload_attachments": {
|
||||
"options": "Tuỳ chọn"
|
||||
},
|
||||
"attribute_detail": {
|
||||
"name": "Tên",
|
||||
"value": "Giá trị",
|
||||
"text": "Văn bản",
|
||||
"number": "Số",
|
||||
"delete": "Xoá"
|
||||
},
|
||||
"rename_note": {
|
||||
"rename_note": "Đổi tên ghi chép"
|
||||
},
|
||||
"add_label": {
|
||||
"add_label": "Thêm nhãn",
|
||||
"label_name_placeholder": "tên nhãn",
|
||||
"help_text_item2": "hoặc thay đổi giá trị của nhãn có sẵn"
|
||||
},
|
||||
"rename_label": {
|
||||
"rename_label": "Đặt lại tên nhãn"
|
||||
},
|
||||
"call_to_action": {
|
||||
"dismiss": "Bỏ qua"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,7 +414,7 @@ export default class GlobalMenuWidget extends BasicWidget {
|
||||
}
|
||||
|
||||
async fetchLatestVersion() {
|
||||
const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Notes/releases/latest";
|
||||
const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Trilium/releases/latest";
|
||||
|
||||
const resp = await fetch(RELEASES_API_URL);
|
||||
const data = await resp.json();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import utils from "../../services/utils.js";
|
||||
import type BasicWidget from "../basic_widget.js";
|
||||
import { EventData } from "../../components/app_context.js";
|
||||
import FlexContainer from "./flex_container.js";
|
||||
import options from "../../services/options.js";
|
||||
import type BasicWidget from "../basic_widget.js";
|
||||
import utils from "../../services/utils.js";
|
||||
|
||||
/**
|
||||
* The root container is the top-most widget/container, from which the entire layout derives.
|
||||
@@ -27,15 +29,45 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
window.visualViewport?.addEventListener("resize", () => this.#onMobileResize());
|
||||
}
|
||||
|
||||
this.#setMotion(options.is("motionEnabled"));
|
||||
this.#setShadows(options.is("shadowsEnabled"));
|
||||
this.#setBackdropEffects(options.is("backdropEffectsEnabled"));
|
||||
|
||||
return super.render();
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (loadResults.isOptionReloaded("motionEnabled")) {
|
||||
this.#setMotion(options.is("motionEnabled"));
|
||||
}
|
||||
|
||||
if (loadResults.isOptionReloaded("shadowsEnabled")) {
|
||||
this.#setShadows(options.is("shadowsEnabled"));
|
||||
}
|
||||
|
||||
if (loadResults.isOptionReloaded("backdropEffectsEnabled")) {
|
||||
this.#setBackdropEffects(options.is("backdropEffectsEnabled"));
|
||||
}
|
||||
}
|
||||
|
||||
#onMobileResize() {
|
||||
const currentViewportHeight = getViewportHeight();
|
||||
const isKeyboardOpened = (currentViewportHeight < this.originalViewportHeight);
|
||||
this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened);
|
||||
}
|
||||
|
||||
#setMotion(enabled: boolean) {
|
||||
document.body.classList.toggle("motion-disabled", !enabled);
|
||||
jQuery.fx.off = !enabled;
|
||||
}
|
||||
|
||||
#setShadows(enabled: boolean) {
|
||||
document.body.classList.toggle("shadows-disabled", !enabled);
|
||||
}
|
||||
|
||||
#setBackdropEffects(enabled: boolean) {
|
||||
document.body.classList.toggle("backdrop-effects-disabled", !enabled);
|
||||
}
|
||||
}
|
||||
|
||||
function getViewportHeight() {
|
||||
|
||||
@@ -2,6 +2,7 @@ import FlexContainer from "./flex_container.js";
|
||||
import appContext, { type CommandData, type CommandListenerData, type EventData, type EventNames, type NoteSwitchedContext } from "../../components/app_context.js";
|
||||
import type BasicWidget from "../basic_widget.js";
|
||||
import type NoteContext from "../../components/note_context.js";
|
||||
import Component from "../../components/component.js";
|
||||
|
||||
interface NoteContextEvent {
|
||||
noteContext: NoteContext;
|
||||
@@ -152,6 +153,8 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
|
||||
for (const ntxId of ntxIds) {
|
||||
this.$widget.find(`[data-ntx-id="${ntxId}"]`).remove();
|
||||
|
||||
const widget = this.widgets[ntxId];
|
||||
recursiveCleanup(widget);
|
||||
delete this.widgets[ntxId];
|
||||
}
|
||||
}
|
||||
@@ -237,3 +240,12 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
|
||||
return Promise.all(promises);
|
||||
}
|
||||
}
|
||||
|
||||
function recursiveCleanup(widget: Component) {
|
||||
for (const child of widget.children) {
|
||||
recursiveCleanup(child);
|
||||
}
|
||||
if ("cleanup" in widget && typeof widget.cleanup === "function") {
|
||||
widget.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ function DirectoryLink({ directory, style }: { directory: string, style?: CSSPro
|
||||
openService.openDirectory(directory);
|
||||
};
|
||||
|
||||
return <a className="tn-link" href="#" onClick={onClick} style={style}></a>
|
||||
return <a className="tn-link" href="#" onClick={onClick} style={style}>{directory}</a>
|
||||
} else {
|
||||
return <span style={style}>{directory}</span>;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,14 @@ function AddLinkDialogComponent() {
|
||||
setShown(true);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (hasSelection) {
|
||||
setLinkType("hyper-link");
|
||||
} else {
|
||||
setLinkType("reference-link");
|
||||
}
|
||||
}, [ hasSelection ])
|
||||
|
||||
async function setDefaultLinkTitle(noteId: string) {
|
||||
const noteTitle = await tree.getNoteTitle(noteId);
|
||||
setLinkTitle(noteTitle);
|
||||
@@ -107,7 +115,7 @@ function AddLinkDialogComponent() {
|
||||
}}
|
||||
show={shown}
|
||||
>
|
||||
<FormGroup label={t("add_link.note")}>
|
||||
<FormGroup label={t("add_link.note")} name="note">
|
||||
<NoteAutocomplete
|
||||
inputRef={autocompleteRef}
|
||||
onChange={setSuggestion}
|
||||
|
||||
@@ -64,7 +64,7 @@ function BranchPrefixDialogComponent() {
|
||||
footer={<Button text={t("branch_prefix.save")} />}
|
||||
show={shown}
|
||||
>
|
||||
<FormGroup label={t("branch_prefix.prefix")}>
|
||||
<FormGroup label={t("branch_prefix.prefix")} name="prefix">
|
||||
<div class="input-group">
|
||||
<input class="branch-prefix-input form-control" value={prefix} ref={branchInput}
|
||||
onChange={(e) => setPrefix((e.target as HTMLInputElement).value)} />
|
||||
|
||||
@@ -94,7 +94,8 @@ function AvailableActionsList() {
|
||||
<td>{ actionGroup.title }:</td>
|
||||
{actionGroup.actions.map(({ actionName, actionTitle }) =>
|
||||
<Button
|
||||
small text={actionTitle}
|
||||
size="small"
|
||||
text={actionTitle}
|
||||
onClick={() => bulk_action.addAction("_bulkAction", actionName)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import Button from "../react/Button";
|
||||
import Modal from "../react/Modal";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import { CallToAction, dismissCallToAction, getCallToActions } from "./call_to_action_definitions";
|
||||
import { t } from "../../services/i18n";
|
||||
|
||||
function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActions: CallToAction[] }) {
|
||||
if (!activeCallToActions.length) {
|
||||
@@ -25,12 +26,12 @@ function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActi
|
||||
<Modal
|
||||
className="call-to-action"
|
||||
size="md"
|
||||
title="New features"
|
||||
title={activeItem.title}
|
||||
show={shown}
|
||||
onHidden={() => setShown(false)}
|
||||
footerAlignment="between"
|
||||
footer={<>
|
||||
<Button text="Dismiss" onClick={async () => {
|
||||
<Button text={t("call_to_action.dismiss")} onClick={async () => {
|
||||
await dismissCallToAction(activeItem.id);
|
||||
goToNext();
|
||||
}} />
|
||||
@@ -43,7 +44,6 @@ function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActi
|
||||
)}
|
||||
</>}
|
||||
>
|
||||
<h4>{activeItem.title}</h4>
|
||||
<p>{activeItem.message}</p>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -65,7 +65,7 @@ const CALL_TO_ACTIONS: CallToAction[] = [
|
||||
id: "background_effects",
|
||||
title: t("call_to_action.background_effects_title"),
|
||||
message: t("call_to_action.background_effects_message"),
|
||||
enabled: () => utils.isElectron() && window.glob.platform === "win32" && isNextTheme() && !options.is("backgroundEffects"),
|
||||
enabled: () => false,
|
||||
buttons: [
|
||||
{
|
||||
text: t("call_to_action.background_effects_button"),
|
||||
|
||||
@@ -69,15 +69,15 @@ function CloneToDialogComponent() {
|
||||
>
|
||||
<h5>{t("clone_to.notes_to_clone")}</h5>
|
||||
<NoteList style={{ maxHeight: "200px", overflow: "auto" }} noteIds={clonedNoteIds} />
|
||||
<FormGroup label={t("clone_to.target_parent_note")}>
|
||||
<FormGroup name="target-parent-note" label={t("clone_to.target_parent_note")}>
|
||||
<NoteAutocomplete
|
||||
placeholder={t("clone_to.search_for_note_by_its_name")}
|
||||
onChange={setSuggestion}
|
||||
inputRef={autoCompleteRef}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={t("clone_to.prefix_optional")} title={t("clone_to.cloned_note_prefix_title")}>
|
||||
<FormTextBox name="clone-prefix" onChange={setPrefix} />
|
||||
<FormGroup name="clone-prefix" label={t("clone_to.prefix_optional")} title={t("clone_to.cloned_note_prefix_title")}>
|
||||
<FormTextBox onChange={setPrefix} />
|
||||
</FormGroup>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import tree from "../../services/tree";
|
||||
import Button from "../react/Button";
|
||||
import FormCheckbox from "../react/FormCheckbox";
|
||||
import FormFileUpload from "../react/FormFileUpload";
|
||||
import FormGroup from "../react/FormGroup";
|
||||
import FormGroup, { FormMultiGroup } from "../react/FormGroup";
|
||||
import Modal from "../react/Modal";
|
||||
import RawHtml from "../react/RawHtml";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
@@ -55,11 +55,11 @@ function ImportDialogComponent() {
|
||||
footer={<Button text={t("import.import")} primary disabled={!files} />}
|
||||
show={shown}
|
||||
>
|
||||
<FormGroup label={t("import.chooseImportFile")} description={<>{t("import.importDescription")} <strong>{ noteTitle }</strong></>}>
|
||||
<FormGroup name="files" label={t("import.chooseImportFile")} description={<>{t("import.importDescription")} <strong>{ noteTitle }</strong></>}>
|
||||
<FormFileUpload multiple onChange={setFiles} />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup label={t("import.options")}>
|
||||
<FormMultiGroup label={t("import.options")}>
|
||||
<FormCheckbox
|
||||
name="safe-import" hint={t("import.safeImportTooltip")} label={t("import.safeImport")}
|
||||
currentValue={safeImport} onChange={setSafeImport}
|
||||
@@ -84,7 +84,7 @@ function ImportDialogComponent() {
|
||||
name="replace-underscores-with-spaces" label={t("import.replaceUnderscoresWithSpaces")}
|
||||
currentValue={replaceUnderscoresWithSpaces} onChange={setReplaceUnderscoresWithSpaces}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormMultiGroup>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ function IncludeNoteDialogComponent() {
|
||||
footer={<Button text={t("include_note.button_include")} keyboardShortcut="Enter" />}
|
||||
show={shown}
|
||||
>
|
||||
<FormGroup label={t("include_note.label_note")}>
|
||||
<FormGroup name="note" label={t("include_note.label_note")}>
|
||||
<NoteAutocomplete
|
||||
placeholder={t("include_note.placeholder_search")}
|
||||
onChange={setSuggestion}
|
||||
@@ -55,8 +55,9 @@ function IncludeNoteDialogComponent() {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup label={t("include_note.box_size_prompt")}>
|
||||
<FormRadioGroup name="include-note-box-size"
|
||||
<FormGroup name="include-note-box-size" label={t("include_note.box_size_prompt")}>
|
||||
<FormRadioGroup
|
||||
name="include-note-box-size"
|
||||
currentValue={boxSize} onChange={setBoxSize}
|
||||
values={[
|
||||
{ label: t("include_note.box_size_small"), value: "small" },
|
||||
|
||||
@@ -9,6 +9,7 @@ import appContext from "../../components/app_context";
|
||||
import commandRegistry from "../../services/command_registry";
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
import shortcutService from "../../services/shortcuts";
|
||||
|
||||
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
|
||||
|
||||
@@ -83,6 +84,27 @@ function JumpToNoteDialogComponent() {
|
||||
$autoComplete
|
||||
.trigger("focus")
|
||||
.trigger("select");
|
||||
|
||||
// Add keyboard shortcut for full search
|
||||
shortcutService.bindElShortcut($autoComplete, "ctrl+return", () => {
|
||||
if (!isCommandMode) {
|
||||
showInFullSearch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function showInFullSearch() {
|
||||
try {
|
||||
setShown(false);
|
||||
const searchString = actualText.current?.trim();
|
||||
if (searchString && !searchString.startsWith(">")) {
|
||||
await appContext.triggerCommand("searchNotes", {
|
||||
searchString
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to trigger full search:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -108,7 +130,12 @@ function JumpToNoteDialogComponent() {
|
||||
/>}
|
||||
onShown={onShown}
|
||||
onHidden={() => setShown(false)}
|
||||
footer={!isCommandMode && <Button className="show-in-full-text-button" text={t("jump_to_note.search_button")} keyboardShortcut="Ctrl+Enter" />}
|
||||
footer={!isCommandMode && <Button
|
||||
className="show-in-full-text-button"
|
||||
text={t("jump_to_note.search_button")}
|
||||
keyboardShortcut="Ctrl+Enter"
|
||||
onClick={showInFullSearch}
|
||||
/>}
|
||||
show={shown}
|
||||
>
|
||||
<div className="algolia-autocomplete-container jump-to-note-results" ref={containerRef}></div>
|
||||
|
||||
@@ -57,7 +57,7 @@ function MoveToDialogComponent() {
|
||||
<h5>{t("move_to.notes_to_move")}</h5>
|
||||
<NoteList branchIds={movedBranchIds} />
|
||||
|
||||
<FormGroup label={t("move_to.target_parent_note")}>
|
||||
<FormGroup name="parent-note" label={t("move_to.target_parent_note")}>
|
||||
<NoteAutocomplete
|
||||
onChange={setSuggestion}
|
||||
inputRef={autoCompleteRef}
|
||||
|
||||
@@ -83,7 +83,7 @@ function NoteTypeChooserDialogComponent() {
|
||||
show={shown}
|
||||
stackable
|
||||
>
|
||||
<FormGroup label={t("note_type_chooser.change_path_prompt")}>
|
||||
<FormGroup name="parent-note" label={t("note_type_chooser.change_path_prompt")}>
|
||||
<NoteAutocomplete
|
||||
onChange={setParentNote}
|
||||
placeholder={t("note_type_chooser.search_placeholder")}
|
||||
@@ -95,7 +95,7 @@ function NoteTypeChooserDialogComponent() {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup label={t("note_type_chooser.modal_body")}>
|
||||
<FormGroup name="note-type" label={t("note_type_chooser.modal_body")}>
|
||||
<FormList onSelect={onNoteTypeSelected}>
|
||||
{noteTypes.map((_item) => {
|
||||
if (_item.title === "----") {
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface PromptDialogOptions {
|
||||
defaultValue?: string;
|
||||
shown?: PromptShownDialogCallback;
|
||||
callback?: (value: string | null) => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
function PromptDialogComponent() {
|
||||
@@ -32,24 +33,26 @@ function PromptDialogComponent() {
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const labelRef = useRef<HTMLLabelElement>(null);
|
||||
const answerRef = useRef<HTMLInputElement>(null);
|
||||
const [ opts, setOpts ] = useState<PromptDialogOptions>();
|
||||
const [ value, setValue ] = useState("");
|
||||
const opts = useRef<PromptDialogOptions>();
|
||||
const [ value, setValue ] = useState("");
|
||||
const [ shown, setShown ] = useState(false);
|
||||
const submitValue = useRef<string>(null);
|
||||
|
||||
useTriliumEvent("showPromptDialog", (opts) => {
|
||||
setOpts(opts);
|
||||
useTriliumEvent("showPromptDialog", (newOpts) => {
|
||||
opts.current = newOpts;
|
||||
setValue(newOpts.defaultValue ?? "");
|
||||
setShown(true);
|
||||
})
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="prompt-dialog"
|
||||
title={opts?.title ?? t("prompt.title")}
|
||||
title={opts.current?.title ?? t("prompt.title")}
|
||||
size="lg"
|
||||
zIndex={2000}
|
||||
modalRef={modalRef} formRef={formRef}
|
||||
onShown={() => {
|
||||
opts?.shown?.({
|
||||
opts.current?.shown?.({
|
||||
$dialog: refToJQuerySelector(modalRef),
|
||||
$question: refToJQuerySelector(labelRef),
|
||||
$answer: refToJQuerySelector(answerRef),
|
||||
@@ -58,24 +61,25 @@ function PromptDialogComponent() {
|
||||
answerRef.current?.focus();
|
||||
}}
|
||||
onSubmit={() => {
|
||||
const modal = BootstrapModal.getOrCreateInstance(modalRef.current!);
|
||||
modal.hide();
|
||||
|
||||
opts?.callback?.(value);
|
||||
submitValue.current = value;
|
||||
setShown(false);
|
||||
}}
|
||||
onHidden={() => {
|
||||
opts?.callback?.(null);
|
||||
setShown(false);
|
||||
opts.current?.callback?.(submitValue.current);
|
||||
submitValue.current = null;
|
||||
opts.current = undefined;
|
||||
}}
|
||||
footer={<Button text={t("prompt.ok")} keyboardShortcut="Enter" primary />}
|
||||
show={shown}
|
||||
stackable
|
||||
>
|
||||
<FormGroup label={opts?.message} labelRef={labelRef}>
|
||||
<FormGroup name="prompt-dialog-answer" label={opts.current?.message} labelRef={labelRef}>
|
||||
<FormTextBox
|
||||
name="prompt-dialog-answer"
|
||||
inputRef={answerRef}
|
||||
currentValue={value} onChange={setValue} />
|
||||
currentValue={value} onChange={setValue}
|
||||
readOnly={opts.current?.readOnly}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -57,7 +57,8 @@ function RecentChangesDialogComponent() {
|
||||
header={
|
||||
<Button
|
||||
text={t("recent_changes.erase_notes_button")}
|
||||
small style={{ padding: "0 10px" }}
|
||||
size="small"
|
||||
style={{ padding: "0 10px" }}
|
||||
onClick={() => {
|
||||
server.post("notes/erase-deleted-notes-now").then(() => {
|
||||
setNeedsRefresh(true);
|
||||
|
||||
@@ -55,7 +55,7 @@ function RevisionsDialogComponent() {
|
||||
helpPageId="vZWERwf8U3nx"
|
||||
bodyStyle={{ display: "flex", height: "80vh" }}
|
||||
header={
|
||||
(!!revisions?.length && <Button text={t("revisions.delete_all_revisions")} small style={{ padding: "0 10px" }}
|
||||
(!!revisions?.length && <Button text={t("revisions.delete_all_revisions")} size="small" style={{ padding: "0 10px" }}
|
||||
onClick={async () => {
|
||||
const text = t("revisions.confirm_delete_all");
|
||||
|
||||
|
||||
@@ -83,11 +83,8 @@ function SortChildNotesDialogComponent() {
|
||||
label={t("sort_child_notes.sort_with_respect_to_different_character_sorting")}
|
||||
currentValue={sortNatural} onChange={setSortNatural}
|
||||
/>
|
||||
<FormGroup className="form-check" label={t("sort_child_notes.natural_sort_language")} description={t("sort_child_notes.the_language_code_for_natural_sort")}>
|
||||
<FormTextBox
|
||||
name="sort-locale"
|
||||
currentValue={sortLocale} onChange={setSortLocale}
|
||||
/>
|
||||
<FormGroup name="sort-locale" className="form-check" label={t("sort_child_notes.natural_sort_language")} description={t("sort_child_notes.the_language_code_for_natural_sort")}>
|
||||
<FormTextBox currentValue={sortLocale} onChange={setSortLocale} />
|
||||
</FormGroup>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -51,13 +51,12 @@ function UploadAttachmentsDialogComponent() {
|
||||
onHidden={() => setShown(false)}
|
||||
show={shown}
|
||||
>
|
||||
<FormGroup label={t("upload_attachments.choose_files")} description={description}>
|
||||
<FormGroup name="files" label={t("upload_attachments.choose_files")} description={description}>
|
||||
<FormFileUpload onChange={setFiles} multiple />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup label={t("upload_attachments.options")}>
|
||||
<FormCheckbox
|
||||
name="shrink-images"
|
||||
<FormGroup name="shrink-images" label={t("upload_attachments.options")}>
|
||||
<FormCheckbox
|
||||
hint={t("upload_attachments.tooltip")} label={t("upload_attachments.shrink_images")}
|
||||
currentValue={shrinkImages} onChange={setShrinkImages}
|
||||
/>
|
||||
|
||||
@@ -152,7 +152,7 @@ const TPL = /*html*/`
|
||||
const MAX_SEARCH_RESULTS_IN_TREE = 100;
|
||||
|
||||
// this has to be hanged on the actual elements to effectively intercept and stop click event
|
||||
const cancelClickPropagation: JQuery.TypeEventHandler<unknown, unknown, unknown, unknown, any> = (e) => e.stopPropagation();
|
||||
const cancelClickPropagation: (e: JQuery.ClickEvent) => void = (e) => e.stopPropagation();
|
||||
|
||||
// TODO: Fix once we remove Node.js API from public
|
||||
type Timeout = NodeJS.Timeout | string | number | undefined;
|
||||
|
||||
@@ -93,6 +93,8 @@ interface QuickSearchResponse {
|
||||
highlightedNotePathTitle: string;
|
||||
contentSnippet?: string;
|
||||
highlightedContentSnippet?: string;
|
||||
attributeSnippet?: string;
|
||||
highlightedAttributeSnippet?: string;
|
||||
icon: string;
|
||||
}>;
|
||||
error: string;
|
||||
@@ -241,7 +243,12 @@ export default class QuickSearchWidget extends BasicWidget {
|
||||
<span style="flex: 1;" class="search-result-title">${result.highlightedNotePathTitle}</span>
|
||||
</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) {
|
||||
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>`;
|
||||
}
|
||||
|
||||
14
apps/client/src/widgets/react/Admonition.tsx
Normal file
14
apps/client/src/widgets/react/Admonition.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ComponentChildren } from "preact";
|
||||
|
||||
interface AlertProps {
|
||||
type: "info" | "danger";
|
||||
type: "info" | "danger" | "warning";
|
||||
title?: string;
|
||||
children: ComponentChildren;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRef, useMemo } from "preact/hooks";
|
||||
import { memo } from "preact/compat";
|
||||
|
||||
interface ButtonProps {
|
||||
name?: string;
|
||||
/** Reference to the button element. Mostly useful for requesting focus. */
|
||||
buttonRef?: RefObject<HTMLButtonElement>;
|
||||
text: string;
|
||||
@@ -14,11 +15,11 @@ interface ButtonProps {
|
||||
onClick?: () => void;
|
||||
primary?: boolean;
|
||||
disabled?: boolean;
|
||||
small?: boolean;
|
||||
size?: "normal" | "small" | "micro";
|
||||
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
|
||||
const classes = useMemo(() => {
|
||||
const classList: string[] = ["btn"];
|
||||
@@ -30,11 +31,13 @@ const Button = memo(({ buttonRef: _buttonRef, className, text, onClick, keyboard
|
||||
if (className) {
|
||||
classList.push(className);
|
||||
}
|
||||
if (small) {
|
||||
if (size === "small") {
|
||||
classList.push("btn-sm");
|
||||
} else if (size === "micro") {
|
||||
classList.push("btn-micro");
|
||||
}
|
||||
return classList.join(" ");
|
||||
}, [primary, className, small]);
|
||||
}, [primary, className, size]);
|
||||
|
||||
const buttonRef = _buttonRef ?? useRef<HTMLButtonElement>(null);
|
||||
|
||||
@@ -52,6 +55,7 @@ const Button = memo(({ buttonRef: _buttonRef, className, text, onClick, keyboard
|
||||
|
||||
return (
|
||||
<button
|
||||
name={name}
|
||||
className={classes}
|
||||
type={onClick ? "button" : "submit"}
|
||||
onClick={onClick}
|
||||
|
||||
17
apps/client/src/widgets/react/Column.tsx
Normal file
17
apps/client/src/widgets/react/Column.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -2,10 +2,12 @@ import { Tooltip } from "bootstrap";
|
||||
import { useEffect, useRef, useMemo, useCallback } from "preact/hooks";
|
||||
import { escapeQuotes } from "../../services/utils";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { memo } from "preact/compat";
|
||||
import { CSSProperties, memo } from "preact/compat";
|
||||
import { useUniqueName } from "./hooks";
|
||||
|
||||
interface FormCheckboxProps {
|
||||
name: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
label: string | ComponentChildren;
|
||||
/**
|
||||
* 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;
|
||||
disabled?: boolean;
|
||||
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);
|
||||
|
||||
// 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]);
|
||||
|
||||
return (
|
||||
<div className="form-checkbox">
|
||||
<div className="form-checkbox" style={containerStyle}>
|
||||
<label
|
||||
className="form-check-label tn-checkbox"
|
||||
style={labelStyle}
|
||||
@@ -54,9 +58,10 @@ const FormCheckbox = memo(({ name, disabled, label, currentValue, onChange, hint
|
||||
ref={labelRef}
|
||||
>
|
||||
<input
|
||||
id={id}
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
name={name}
|
||||
name={id}
|
||||
checked={currentValue || false}
|
||||
value="1"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -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 {
|
||||
name: string;
|
||||
labelRef?: RefObject<HTMLLabelElement>;
|
||||
label?: string;
|
||||
title?: string;
|
||||
className?: string;
|
||||
children: ComponentChildren;
|
||||
children: VNode<any>;
|
||||
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 (
|
||||
<div className={`form-group ${className}`} title={title}
|
||||
style={{ "margin-bottom": "15px" }}>
|
||||
<label style={{ width: "100%" }} ref={labelRef}>
|
||||
{label && <div style={{ "margin-bottom": "10px" }}>{label}</div> }
|
||||
{children}
|
||||
</label>
|
||||
<div className={`form-group ${className} ${disabled ? "disabled" : ""}`} title={title} style={style}>
|
||||
{ label &&
|
||||
<label style={{ width: "100%" }} ref={labelRef} htmlFor={id}>{label}</label>}
|
||||
|
||||
{childWithId}
|
||||
|
||||
{description && <small className="form-text">{description}</small>}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +1,56 @@
|
||||
import type { ComponentChildren } from "preact";
|
||||
import { useUniqueName } from "./hooks";
|
||||
|
||||
interface FormRadioProps {
|
||||
name: string;
|
||||
currentValue?: string;
|
||||
values: {
|
||||
value: string;
|
||||
label: string;
|
||||
label: string | ComponentChildren;
|
||||
inlineDescription?: string | ComponentChildren;
|
||||
}[];
|
||||
onChange(newValue: string): void;
|
||||
}
|
||||
|
||||
export default function FormRadioGroup({ name, values, currentValue, onChange }: FormRadioProps) {
|
||||
export default function FormRadioGroup({ values, ...restProps }: FormRadioProps) {
|
||||
return (
|
||||
<>
|
||||
{(values || []).map(({ value, label }) => (
|
||||
<div className="form-check">
|
||||
<label className="form-check-label tn-radio">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="radio"
|
||||
name={name}
|
||||
value={value}
|
||||
checked={value === currentValue}
|
||||
onChange={e => onChange((e.target as HTMLInputElement).value)} />
|
||||
{label}
|
||||
</label>
|
||||
<div role="group">
|
||||
{(values || []).map(({ value, label, inlineDescription }) => (
|
||||
<div className="form-checkbox">
|
||||
<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
|
||||
className="form-check-input"
|
||||
type="radio"
|
||||
name={useUniqueName(name)}
|
||||
value={value}
|
||||
checked={value === currentValue}
|
||||
onChange={e => onChange((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
{inlineDescription ?
|
||||
<><strong>{label}</strong> - {inlineDescription}</>
|
||||
: label}
|
||||
</label>
|
||||
)
|
||||
}
|
||||
79
apps/client/src/widgets/react/FormSelect.tsx
Normal file
79
apps/client/src/widgets/react/FormSelect.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
}
|
||||
5
apps/client/src/widgets/react/FormText.tsx
Normal file
5
apps/client/src/widgets/react/FormText.tsx
Normal 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>
|
||||
}
|
||||
18
apps/client/src/widgets/react/FormTextArea.tsx
Normal file
18
apps/client/src/widgets/react/FormTextArea.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +1,48 @@
|
||||
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;
|
||||
currentValue?: string;
|
||||
onChange?(newValue: string): void;
|
||||
onChange?(newValue: string, validity: ValidityState): void;
|
||||
onBlur?(newValue: string): void;
|
||||
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 (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type={type ?? "text"}
|
||||
className={`form-control ${className ?? ""}`}
|
||||
id={id}
|
||||
name={name}
|
||||
type={type ?? "text"}
|
||||
value={currentValue}
|
||||
autoComplete={autoComplete}
|
||||
placeholder={placeholder}
|
||||
title={title}
|
||||
pattern={pattern}
|
||||
onInput={e => onChange?.(e.currentTarget.value)}
|
||||
style={style}
|
||||
onInput={onChange && (e => {
|
||||
const target = e.currentTarget;
|
||||
onChange?.(target.value, target.validity);
|
||||
})}
|
||||
onBlur={onBlur && (e => {
|
||||
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>
|
||||
)
|
||||
}
|
||||
33
apps/client/src/widgets/react/KeyboardShortcut.tsx
Normal file
33
apps/client/src/widgets/react/KeyboardShortcut.tsx
Normal 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]), [])}
|
||||
</>
|
||||
);
|
||||
}
|
||||
17
apps/client/src/widgets/react/LinkButton.tsx
Normal file
17
apps/client/src/widgets/react/LinkButton.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import type { RefObject } from "preact";
|
||||
import type { CSSProperties } from "preact/compat";
|
||||
|
||||
interface NoteAutocompleteProps {
|
||||
id?: string;
|
||||
inputRef?: RefObject<HTMLInputElement>;
|
||||
text?: string;
|
||||
placeholder?: string;
|
||||
@@ -18,7 +19,7 @@ interface NoteAutocompleteProps {
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -74,6 +75,7 @@ export default function NoteAutocomplete({ inputRef: _ref, text, placeholder, on
|
||||
return (
|
||||
<div className="input-group" style={containerStyle}>
|
||||
<input
|
||||
id={id}
|
||||
ref={ref}
|
||||
className="note-autocomplete form-control"
|
||||
placeholder={placeholder ?? t("add_link.search_note")} />
|
||||
|
||||
@@ -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) {
|
||||
html = html[0];
|
||||
}
|
||||
|
||||
@@ -22,11 +22,18 @@ export default abstract class ReactBasicWidget extends BasicWidget {
|
||||
* @returns the rendered wrapped DOM 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((
|
||||
<ParentComponent.Provider value={parentComponent}>
|
||||
{el}
|
||||
</ParentComponent.Provider>
|
||||
), renderContainer);
|
||||
return $(renderContainer.firstChild as HTMLElement);
|
||||
), container);
|
||||
return $(container) as JQuery<HTMLElement>;
|
||||
}
|
||||
|
||||
export function disposeReactWidget(container: Element) {
|
||||
render(null, container);
|
||||
}
|
||||
@@ -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 { ParentComponent } from "./ReactBasicWidget";
|
||||
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.
|
||||
@@ -12,32 +20,67 @@ import SpacedUpdate from "../../services/spaced_update";
|
||||
* @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).
|
||||
*/
|
||||
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);
|
||||
useEffect(() => {
|
||||
if (!parentWidget || !enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a unique handler name for this specific event listener
|
||||
const handlerName = `${eventName}Event`;
|
||||
const originalHandler = parentWidget[handlerName];
|
||||
|
||||
// Override the event handler to call our handler
|
||||
parentWidget[handlerName] = async function(data: EventData<T>) {
|
||||
// Call original handler if it exists
|
||||
if (originalHandler) {
|
||||
await originalHandler.call(parentWidget, data);
|
||||
if (!parentWidget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlerName = `${eventName}Event`;
|
||||
const customHandler = useMemo(() => {
|
||||
return async (data: EventData<T>) => {
|
||||
// Inform the attached event listeners.
|
||||
const eventHandlers = registeredHandlers.get(parentWidget)?.get(eventName) ?? [];
|
||||
for (const eventHandler of eventHandlers) {
|
||||
eventHandler(data);
|
||||
}
|
||||
// Call our React component's handler
|
||||
handler(data);
|
||||
};
|
||||
}
|
||||
}, [ eventName, parentWidget ]);
|
||||
|
||||
// Cleanup: restore original handler on unmount or when disabled
|
||||
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;
|
||||
|
||||
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) {
|
||||
@@ -63,4 +106,116 @@ export function useSpacedUpdate(callback: () => Promise<void>, interval = 1000)
|
||||
}, [interval]);
|
||||
|
||||
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 ]);
|
||||
}
|
||||
@@ -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 }));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
137
apps/client/src/widgets/type_widgets/content_widget.tsx
Normal file
137
apps/client/src/widgets/type_widgets/content_widget.tsx
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
175
apps/client/src/widgets/type_widgets/options/advanced.tsx
Normal file
175
apps/client/src/widgets/type_widgets/options/advanced.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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"));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import AiSettingsWidget from './ai_settings/index.js';
|
||||
export default AiSettingsWidget;
|
||||
236
apps/client/src/widgets/type_widgets/options/ai_settings.tsx
Normal file
236
apps/client/src/widgets/type_widgets/options/ai_settings.tsx
Normal 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" }}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import AiSettingsWidget from './ai_settings_widget.js';
|
||||
export default AiSettingsWidget;
|
||||
@@ -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;
|
||||
}>;
|
||||
}
|
||||
@@ -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>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
`;
|
||||
295
apps/client/src/widgets/type_widgets/options/appearance.tsx
Normal file
295
apps/client/src/widgets/type_widgets/options/appearance.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
119
apps/client/src/widgets/type_widgets/options/backup.tsx
Normal file
119
apps/client/src/widgets/type_widgets/options/backup.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
164
apps/client/src/widgets/type_widgets/options/code_notes.tsx
Normal file
164
apps/client/src/widgets/type_widgets/options/code_notes.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -1,33 +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("code_auto_read_only_size.title")}</h4>
|
||||
|
||||
<p class="form-text">${t("code_auto_read_only_size.description")}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="auto-readonly-size-code">${t("code_auto_read_only_size.label")}</label>
|
||||
<label class="input-group tn-number-unit-pair">
|
||||
<input id="auto-readonly-size-code" class="auto-readonly-size-code form-control options-number-input" type="number" min="0">
|
||||
<span class="input-group-text">${t("code_auto_read_only_size.unit")}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class CodeAutoReadOnlySizeOptions extends OptionsWidget {
|
||||
|
||||
private $autoReadonlySizeCode!: JQuery<HTMLElement>;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$autoReadonlySizeCode = this.$widget.find(".auto-readonly-size-code");
|
||||
this.$autoReadonlySizeCode.on("change", () => this.updateOption("autoReadonlySizeCode", this.$autoReadonlySizeCode.val()));
|
||||
}
|
||||
|
||||
async optionsLoaded(options: OptionMap) {
|
||||
this.$autoReadonlySizeCode.val(options.autoReadonlySizeCode);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
import type { OptionMap } from "@triliumnext/commons";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="options-section">
|
||||
<h4>${t("code-editor-options.title")}</h4>
|
||||
<label class="tn-checkbox">
|
||||
<input type="checkbox" class="vim-keymap-enabled form-check-input">
|
||||
${t("vim_key_bindings.use_vim_keybindings_in_code_notes")}
|
||||
</label>
|
||||
<p class="form-text">${t("vim_key_bindings.enable_vim_keybindings")}</p>
|
||||
</div>`;
|
||||
|
||||
export default class CodeEditorOptions extends OptionsWidget {
|
||||
|
||||
private $vimKeymapEnabled!: JQuery<HTMLElement>;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$vimKeymapEnabled = this.$widget.find(".vim-keymap-enabled");
|
||||
this.$vimKeymapEnabled.on("change", () => this.updateCheckboxOption("vimKeymapEnabled", this.$vimKeymapEnabled));
|
||||
}
|
||||
|
||||
async optionsLoaded(options: OptionMap) {
|
||||
this.setCheckboxState(this.$vimKeymapEnabled, options.vimKeymapEnabled);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user