mirror of
https://github.com/zadam/trilium.git
synced 2025-12-16 21:29:56 +01:00
Compare commits
778 Commits
feature/el
...
feat/push-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53f31f9c78 | ||
|
|
22c5651eeb | ||
|
|
68a10a9813 | ||
|
|
b204964e57 | ||
|
|
b2c869d7ab | ||
|
|
307e17f9c8 | ||
|
|
ee7052ebc2 | ||
|
|
9059642738 | ||
|
|
fe8e3b4489 | ||
|
|
4e00e5b995 | ||
|
|
05ae0ca9d7 | ||
|
|
ac0116109b | ||
|
|
710ed9dd0e | ||
|
|
267f5105b2 | ||
|
|
a9564f8f38 | ||
|
|
9c5a130ab4 | ||
|
|
c40398df5d | ||
|
|
27fdd9e715 | ||
|
|
59697095b1 | ||
|
|
a16f5f5505 | ||
|
|
922d484a33 | ||
|
|
e1b4a0b720 | ||
|
|
991399fe4f | ||
|
|
27855456a0 | ||
|
|
0687ed9ec4 | ||
|
|
632976e71f | ||
|
|
98addef614 | ||
|
|
c72c9934b5 | ||
|
|
6362f24ae9 | ||
|
|
dc2d2fe25b | ||
|
|
bbc007e6cf | ||
|
|
3dfd195630 | ||
|
|
e3f72baab3 | ||
|
|
1bb19d0d9e | ||
|
|
bc1b69a836 | ||
|
|
0bfa9f0c58 | ||
|
|
498ffa806d | ||
|
|
9bfed2a80d | ||
|
|
ec902c5762 | ||
|
|
bb1d31f877 | ||
|
|
09f938fb72 | ||
|
|
a3e9192998 | ||
|
|
80b7c0b4c9 | ||
|
|
f01d6938f3 | ||
|
|
ab95d707a3 | ||
|
|
6475b4029a | ||
|
|
f646b3dc5c | ||
|
|
bcef0802e4 | ||
|
|
47099cc77b | ||
|
|
83be42f4ea | ||
|
|
ab9fec0186 | ||
|
|
c6dd32ea7b | ||
|
|
1d4cd538ac | ||
|
|
dc99f725f9 | ||
|
|
2f804f3eac | ||
|
|
9d6bb306e7 | ||
|
|
03ab912495 | ||
|
|
d12dfabd0b | ||
|
|
ed748bbebd | ||
|
|
e85858d22d | ||
|
|
0afa9717e5 | ||
|
|
1e2e3498c6 | ||
|
|
59a01b816c | ||
|
|
508f46af42 | ||
|
|
1af865a577 | ||
|
|
74834af222 | ||
|
|
f55e33f303 | ||
|
|
fcb77360e1 | ||
|
|
3d285e105e | ||
|
|
bb55544f25 | ||
|
|
2e9e9a60bf | ||
|
|
f7e77cd6cb | ||
|
|
a7a94789e6 | ||
|
|
864ac1a270 | ||
|
|
fbec6d8873 | ||
|
|
5f647a932d | ||
|
|
6e5046c0d4 | ||
|
|
3c9a8e38d3 | ||
|
|
b3a3196136 | ||
|
|
3229b7d106 | ||
|
|
4213c377f8 | ||
|
|
86365ebd44 | ||
|
|
20cf685174 | ||
|
|
aeb5a7b251 | ||
|
|
47a50bb449 | ||
|
|
a2a5b67496 | ||
|
|
a94cc5bdab | ||
|
|
526c5a6dd8 | ||
|
|
3f5239706f | ||
|
|
d2761abd04 | ||
|
|
57983b54d2 | ||
|
|
703cf8434a | ||
|
|
aa4375e25f | ||
|
|
7d3a672b55 | ||
|
|
c08b30a060 | ||
|
|
d579e39b40 | ||
|
|
b147d4bdeb | ||
|
|
48faa8a813 | ||
|
|
ec646809dd | ||
|
|
ab48a28635 | ||
|
|
3fd7afbb57 | ||
|
|
4074929c6b | ||
|
|
d73e84ea6c | ||
|
|
753f1dc7b6 | ||
|
|
9464667323 | ||
|
|
4d82f2f22d | ||
|
|
e3d28e703f | ||
|
|
5f39a314b5 | ||
|
|
43caadc472 | ||
|
|
f2ce8b9f3c | ||
|
|
735e91e636 | ||
|
|
4df94d1f20 | ||
|
|
70440520e1 | ||
|
|
e49e2d5093 | ||
|
|
f0ac301417 | ||
|
|
168ff90e38 | ||
|
|
5e4f529b26 | ||
|
|
0d1bd3e298 | ||
|
|
70f826b737 | ||
|
|
8bd5af3fd2 | ||
|
|
dbbae87cd3 | ||
|
|
83fd42aff2 | ||
|
|
93c9383a92 | ||
|
|
7c490d8b72 | ||
|
|
b4b5e86a14 | ||
|
|
e166b97b8f | ||
|
|
829f382726 | ||
|
|
4ef103063d | ||
|
|
fa66e50193 | ||
|
|
255ad96c8b | ||
|
|
a12fa1177b | ||
|
|
6fa7cc8201 | ||
|
|
2fff5418a9 | ||
|
|
2e805cd5a3 | ||
|
|
61eaa89de6 | ||
|
|
aa0c021f8b | ||
|
|
4fd02db079 | ||
|
|
88bbc7e8c1 | ||
|
|
af0ba32dd9 | ||
|
|
938d295bf3 | ||
|
|
f82667066f | ||
|
|
03a7fe1282 | ||
|
|
0c0504ffd1 | ||
|
|
e4900ce87b | ||
|
|
04de87722b | ||
|
|
a95e28c085 | ||
|
|
9fbcfb0f0f | ||
|
|
f24a3442fb | ||
|
|
918a945e3b | ||
|
|
3b96b5779b | ||
|
|
a93b20428e | ||
|
|
0522024f6d | ||
|
|
f27f135a61 | ||
|
|
6a76136878 | ||
|
|
1766d28fc2 | ||
|
|
f51d944bb3 | ||
|
|
cabe240e7e | ||
|
|
e72fb39c4d | ||
|
|
0ca30e0e87 | ||
|
|
cc362393be | ||
|
|
40bfd827d2 | ||
|
|
a4046fbf6e | ||
|
|
28605f2687 | ||
|
|
2085d1bbba | ||
|
|
08db03800e | ||
|
|
04b7e0cde9 | ||
|
|
401260d3ca | ||
|
|
53e0c05290 | ||
|
|
cdbb89482e | ||
|
|
e290635ba5 | ||
|
|
e340e6f5e3 | ||
|
|
2d950e8f3a | ||
|
|
4c70d72ba2 | ||
|
|
80edc4c4e0 | ||
|
|
4d07a1aab6 | ||
|
|
e2cd357319 | ||
|
|
620b57bfa6 | ||
|
|
934c9d3df8 | ||
|
|
f034e8bb37 | ||
|
|
93f80c6837 | ||
|
|
a54177fee0 | ||
|
|
0e6ad42923 | ||
|
|
b0beb74011 | ||
|
|
4857fecc41 | ||
|
|
8405d960be | ||
|
|
b9101c9fb2 | ||
|
|
e457e6a2f2 | ||
|
|
f3416fa03e | ||
|
|
3e5ab2b1e1 | ||
|
|
5c0bc9a7c2 | ||
|
|
2adfa55acd | ||
|
|
8125e8afcd | ||
|
|
e328f18558 | ||
|
|
cbdfa9079c | ||
|
|
e08c4515a7 | ||
|
|
650aa16b89 | ||
|
|
11d908218b | ||
|
|
3627a7dc93 | ||
|
|
1e1c8cc4ff | ||
|
|
d616bc09c9 | ||
|
|
f1cef44d5d | ||
|
|
3795be4750 | ||
|
|
2bb66a7526 | ||
|
|
28a472782f | ||
|
|
a4da002352 | ||
|
|
94fdc2beee | ||
|
|
dd4a01d9f8 | ||
|
|
a2a6c67350 | ||
|
|
2152ca7ba6 | ||
|
|
40e4d236f4 | ||
|
|
0450cd080d | ||
|
|
1eaac79d63 | ||
|
|
19c0305ed9 | ||
|
|
f0d14a966a | ||
|
|
37e6ccdc1a | ||
|
|
06cea99b40 | ||
|
|
1851336862 | ||
|
|
461eb273d9 | ||
|
|
470edc4d70 | ||
|
|
26132a2a56 | ||
|
|
d92bd16042 | ||
|
|
1c7dfa6c91 | ||
|
|
3a3fed4314 | ||
|
|
82bdb76d75 | ||
|
|
066f3ea078 | ||
|
|
9d760a21d5 | ||
|
|
976c795ac6 | ||
|
|
ed320e4e24 | ||
|
|
3111738700 | ||
|
|
1fa0bada23 | ||
|
|
11eca7e58b | ||
|
|
4b50e2f14d | ||
|
|
63faba9603 | ||
|
|
3e213699e0 | ||
|
|
c88ff07691 | ||
|
|
b53aa5cf6e | ||
|
|
2641b9b3fe | ||
|
|
3a2a73992c | ||
|
|
b82b17a701 | ||
|
|
a6202edcd1 | ||
|
|
6eac0cb75d | ||
|
|
83672d6138 | ||
|
|
51dadf72d0 | ||
|
|
0cbf61acb3 | ||
|
|
399c7435ac | ||
|
|
d51fae7878 | ||
|
|
9750e25ad5 | ||
|
|
2f9b2f0e8f | ||
|
|
8deaf22544 | ||
|
|
b192f43187 | ||
|
|
8cb8d1303c | ||
|
|
5237348975 | ||
|
|
72e2f6757e | ||
|
|
cf059e7f86 | ||
|
|
44d69216b6 | ||
|
|
c38bf09af0 | ||
|
|
9373d47e86 | ||
|
|
29350628c3 | ||
|
|
83d1a68879 | ||
|
|
f188408099 | ||
|
|
449ab3a798 | ||
|
|
21504d1417 | ||
|
|
3060b496e3 | ||
|
|
bd35539fa1 | ||
|
|
df6447e3ad | ||
|
|
24fd898f0d | ||
|
|
1aa6238288 | ||
|
|
c16c4788da | ||
|
|
0c35daab85 | ||
|
|
4a19639e92 | ||
|
|
36cceea677 | ||
|
|
4dbc76790a | ||
|
|
b32a344a21 | ||
|
|
3896ab822f | ||
|
|
cfa4ba57d4 | ||
|
|
da051e0269 | ||
|
|
3eda77a91f | ||
|
|
5c2f4be5dd | ||
|
|
435b501db9 | ||
|
|
5a27ffef5f | ||
|
|
02256d9a45 | ||
|
|
7e069009d6 | ||
|
|
3c25cda4c0 | ||
|
|
70d7ad0b1a | ||
|
|
e793b2f661 | ||
|
|
917ea3e401 | ||
|
|
5a54dd666f | ||
|
|
733ec2c145 | ||
|
|
e386b03b90 | ||
|
|
25d5d51085 | ||
|
|
50c4301a34 | ||
|
|
0f60c0696b | ||
|
|
3ec2947c4f | ||
|
|
e89162838e | ||
|
|
72181090a5 | ||
|
|
72b2a5cc0d | ||
|
|
1eaeec8100 | ||
|
|
660db3b3ab | ||
|
|
89d2fcb81e | ||
|
|
ccda623840 | ||
|
|
a6e7dff61e | ||
|
|
86d1bbe8ff | ||
|
|
a10cb06f14 | ||
|
|
dd9a62818b | ||
|
|
c0e936675c | ||
|
|
b0b788b7dc | ||
|
|
18f0f3ecac | ||
|
|
e7d745ac94 | ||
|
|
24abf7f0ed | ||
|
|
36fb097d1d | ||
|
|
35ef5fd0d3 | ||
|
|
9a08f6534b | ||
|
|
885dd2053b | ||
|
|
6f6f280bdd | ||
|
|
a3e8fd374f | ||
|
|
f91c1f4180 | ||
|
|
d85746c1b9 | ||
|
|
5a17075eef | ||
|
|
6cab47fb55 | ||
|
|
93c5413790 | ||
|
|
f2db7baeba | ||
|
|
a507991808 | ||
|
|
7c86f90ac6 | ||
|
|
1e9b772692 | ||
|
|
096ab52216 | ||
|
|
88c3cd5cdd | ||
|
|
99a911a220 | ||
|
|
3218ab971b | ||
|
|
274e3c1f7f | ||
|
|
f8916a6e35 | ||
|
|
73f20d01e4 | ||
|
|
2fd3a875b6 | ||
|
|
68cba8d3b2 | ||
|
|
6b28fd405e | ||
|
|
3bccbabe53 | ||
|
|
c97c66ed8a | ||
|
|
4b212232c8 | ||
|
|
ac3a8edf2b | ||
|
|
b581025bbe | ||
|
|
7bc5331747 | ||
|
|
2415976475 | ||
|
|
8d0d0f0449 | ||
|
|
16b00ed160 | ||
|
|
df73a420f9 | ||
|
|
1e4d57f275 | ||
|
|
19a238c8d3 | ||
|
|
5ffd8a79eb | ||
|
|
04fbc82d7c | ||
|
|
3f105f7b8b | ||
|
|
b9193a5562 | ||
|
|
e1fa188244 | ||
|
|
80ad87671a | ||
|
|
b6d5a6ec2e | ||
|
|
759398d804 | ||
|
|
c1b30db3d1 | ||
|
|
0c8bfc39ef | ||
|
|
3815fddb27 | ||
|
|
b585a64a38 | ||
|
|
58e58c192f | ||
|
|
5939344378 | ||
|
|
ad85ee3531 | ||
|
|
349f19fef7 | ||
|
|
d5777a024e | ||
|
|
b7f4ee6171 | ||
|
|
a83c4e3970 | ||
|
|
5a767dae34 | ||
|
|
9f93d30b99 | ||
|
|
dff525edc6 | ||
|
|
26da431320 | ||
|
|
cde4622693 | ||
|
|
5ede7ecc69 | ||
|
|
b607d1e628 | ||
|
|
d7e36bdf93 | ||
|
|
2b8b185b5b | ||
|
|
927ebcbec9 | ||
|
|
ea1397de63 | ||
|
|
ce1f5c6204 | ||
|
|
652114c7b5 | ||
|
|
17cd2128fd | ||
|
|
bc4378cb3e | ||
|
|
513878dfef | ||
|
|
753d5529b2 | ||
|
|
9f217b88e4 | ||
|
|
d53faa8c01 | ||
|
|
a934760960 | ||
|
|
82914fc2aa | ||
|
|
db687197de | ||
|
|
efd713dc61 | ||
|
|
3f3c7cfe88 | ||
|
|
73ca285b7a | ||
|
|
168d25c020 | ||
|
|
e8ae5486c8 | ||
|
|
f049b8b915 | ||
|
|
4e755dc537 | ||
|
|
5351310a38 | ||
|
|
211ca43a82 | ||
|
|
e5235e7f22 | ||
|
|
e72298f0b4 | ||
|
|
3abf5c65c6 | ||
|
|
268acb0b88 | ||
|
|
196b3b873f | ||
|
|
4d9801a372 | ||
|
|
bd710ba665 | ||
|
|
afe369c876 | ||
|
|
206007bbce | ||
|
|
8ad05b92c0 | ||
|
|
735da2a855 | ||
|
|
980077f559 | ||
|
|
12053e75bb | ||
|
|
62372ed4c5 | ||
|
|
e5caf37697 | ||
|
|
befc5a9530 | ||
|
|
1e00407864 | ||
|
|
73038efccf | ||
|
|
6d37e19b40 | ||
|
|
2c33ef2b0d | ||
|
|
6c30e0836f | ||
|
|
5f77ca31bd | ||
|
|
f7c82d6b09 | ||
|
|
86dd9aa42a | ||
|
|
5daca270e4 | ||
|
|
e18813a4bf | ||
|
|
4aa7e211f3 | ||
|
|
419dc7edfb | ||
|
|
eaa84a6b39 | ||
|
|
1d0503d0e4 | ||
|
|
f7f98aa9a3 | ||
|
|
575d14261a | ||
|
|
9aab606deb | ||
|
|
2e11681b52 | ||
|
|
8cca6637f7 | ||
|
|
82e076378c | ||
|
|
94ddad3c49 | ||
|
|
d35dbca18b | ||
|
|
7468d6147a | ||
|
|
7c78d749de | ||
|
|
85dd99a3c4 | ||
|
|
0a9c0234e2 | ||
|
|
fad77ba5a0 | ||
|
|
12723f3216 | ||
|
|
a43140515f | ||
|
|
3e3cc8c541 | ||
|
|
a85141ace2 | ||
|
|
c33280bbb2 | ||
|
|
df3aa04787 | ||
|
|
4bd25a0d4a | ||
|
|
7fadf4c6e1 | ||
|
|
d1538508e8 | ||
|
|
b24d786933 | ||
|
|
8f69b87dd1 | ||
|
|
9b1da8c311 | ||
|
|
8287063aab | ||
|
|
e4a8258acf | ||
|
|
5e88043c7b | ||
|
|
bedf9112fb | ||
|
|
03681d23c5 | ||
|
|
21683db0b8 | ||
|
|
978d829150 | ||
|
|
cc05572a35 | ||
|
|
c5bb310613 | ||
|
|
77551b1fed | ||
|
|
70728c274e | ||
|
|
cee4714665 | ||
|
|
c3eca3b626 | ||
|
|
01e4cd2e78 | ||
|
|
b99d01ad7b | ||
|
|
bf0213907e | ||
|
|
eff5b6459d | ||
|
|
8e29b5eed6 | ||
|
|
c91748da15 | ||
|
|
f04f9dc262 | ||
|
|
e873cdab7e | ||
|
|
f9b6fd6ac5 | ||
|
|
aa191e110c | ||
|
|
dd09907925 | ||
|
|
da4810672d | ||
|
|
f772f59d7c | ||
|
|
1964fb90d5 | ||
|
|
5945f2860a | ||
|
|
f45da049b9 | ||
|
|
c0beab8a5d | ||
|
|
cabeb13adb | ||
|
|
e2e9721d5f | ||
|
|
4e9deab605 | ||
|
|
9bb048fb01 | ||
|
|
6849f80506 | ||
|
|
5597f4e2e0 | ||
|
|
35e9508bde | ||
|
|
45fbcec805 | ||
|
|
4c8da70ef3 | ||
|
|
ed5da5cd4a | ||
|
|
dc5fccdbcd | ||
|
|
91aea333c7 | ||
|
|
a0de01cff1 | ||
|
|
a41ed34193 | ||
|
|
49e8811c18 | ||
|
|
488563a82e | ||
|
|
d76d50f30e | ||
|
|
4685aef88d | ||
|
|
a106510924 | ||
|
|
9d54503ef7 | ||
|
|
b1449eebf3 | ||
|
|
b213453062 | ||
|
|
076c0321cf | ||
|
|
4d71b73f38 | ||
|
|
b20ffdf7db | ||
|
|
ef018e22d6 | ||
|
|
3fa290a257 | ||
|
|
cdde530b60 | ||
|
|
aa608510d0 | ||
|
|
009fd63ce9 | ||
|
|
bea352855a | ||
|
|
51e8a80ca3 | ||
|
|
8a543d4513 | ||
|
|
945e180a6f | ||
|
|
b93fa332d3 | ||
|
|
9e947f742d | ||
|
|
033e90f8b7 | ||
|
|
be576176c5 | ||
|
|
4da3e8a4d8 | ||
|
|
db2bf537ea | ||
|
|
9a4fdcaef2 | ||
|
|
ca40360f7d | ||
|
|
799e705ff8 | ||
|
|
a1b18c7f97 | ||
|
|
9958a6e1bf | ||
|
|
1fc6d8aca7 | ||
|
|
3e9ec2d943 | ||
|
|
1420def1c3 | ||
|
|
3b4184e765 | ||
|
|
4ce9102f93 | ||
|
|
eb27ec2234 | ||
|
|
b70e25d348 | ||
|
|
772c0bbe1a | ||
|
|
144021c053 | ||
|
|
59486cd55d | ||
|
|
afe3904ea3 | ||
|
|
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,6 +1,6 @@
|
||||
root = true
|
||||
|
||||
[*.{js,ts}]
|
||||
[*.{js,ts,.tsx}]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
|
||||
453
.github/scripts/sync-docs-to-wiki-with-wiki-syntax.ts
vendored
Normal file
453
.github/scripts/sync-docs-to-wiki-with-wiki-syntax.ts
vendored
Normal file
@@ -0,0 +1,453 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { Dirent } from 'fs';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Configuration
|
||||
const FILE_EXTENSIONS = ['.md', '.png', '.jpg', '.jpeg', '.gif', '.svg'] as const;
|
||||
const README_PATTERN = /^README(?:[-.](.+))?\.md$/;
|
||||
|
||||
interface SyncConfig {
|
||||
mainRepoPath: string;
|
||||
wikiPath: string;
|
||||
docsPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert markdown to GitHub Wiki format
|
||||
* - Images:  → [[image.png]]
|
||||
* - Links: [text](page.md) → [[text|page]]
|
||||
*/
|
||||
async function convertToWikiFormat(wikiDir: string): Promise<void> {
|
||||
console.log('Converting to GitHub Wiki format...');
|
||||
const mdFiles = await findFiles(wikiDir, ['.md']);
|
||||
let convertedCount = 0;
|
||||
|
||||
for (const file of mdFiles) {
|
||||
let content = await fs.readFile(file, 'utf-8');
|
||||
const originalContent = content;
|
||||
|
||||
// Convert image references to wiki format
|
||||
//  → [[image.png]]
|
||||
content = content.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
||||
// Skip external URLs
|
||||
if (src.startsWith('http://') || src.startsWith('https://')) {
|
||||
return match;
|
||||
}
|
||||
|
||||
// Decode URL encoding
|
||||
let imagePath = src;
|
||||
if (src.includes('%')) {
|
||||
try {
|
||||
imagePath = decodeURIComponent(src);
|
||||
} catch {
|
||||
imagePath = src;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract just the filename for wiki syntax
|
||||
const filename = path.basename(imagePath);
|
||||
|
||||
// Use wiki syntax for images
|
||||
// If alt text exists, add it after pipe
|
||||
if (alt && alt.trim()) {
|
||||
return `[[${filename}|alt=${alt}]]`;
|
||||
} else {
|
||||
return `[[${filename}]]`;
|
||||
}
|
||||
});
|
||||
|
||||
// Convert internal markdown links to wiki format
|
||||
// [text](../path/to/Page.md) → [[text|Page]]
|
||||
content = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, href) => {
|
||||
// Skip external URLs, anchors, and images
|
||||
if (href.startsWith('http://') ||
|
||||
href.startsWith('https://') ||
|
||||
href.startsWith('#') ||
|
||||
href.match(/\.(png|jpg|jpeg|gif|svg)$/i)) {
|
||||
return match;
|
||||
}
|
||||
|
||||
// Check if it's a markdown file link
|
||||
if (href.endsWith('.md') || href.includes('.md#')) {
|
||||
// Decode URL encoding
|
||||
let decodedHref = href;
|
||||
if (href.includes('%')) {
|
||||
try {
|
||||
decodedHref = decodeURIComponent(href);
|
||||
} catch {
|
||||
decodedHref = href;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract page name without extension and path
|
||||
let pageName = decodedHref
|
||||
.replace(/\.md(#.*)?$/, '') // Remove .md and anchor
|
||||
.split('/') // Split by path
|
||||
.pop() || ''; // Get last part (filename)
|
||||
|
||||
// Convert spaces to hyphens (GitHub wiki convention)
|
||||
pageName = pageName.replace(/ /g, '-');
|
||||
|
||||
// Use wiki link syntax
|
||||
if (text === pageName || text === pageName.replace(/-/g, ' ')) {
|
||||
return `[[${pageName}]]`;
|
||||
} else {
|
||||
return `[[${text}|${pageName}]]`;
|
||||
}
|
||||
}
|
||||
|
||||
// For other internal links, just decode URL encoding
|
||||
if (href.includes('%') && !href.startsWith('http')) {
|
||||
try {
|
||||
const decodedHref = decodeURIComponent(href);
|
||||
return `[${text}](${decodedHref})`;
|
||||
} catch {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
// Save if modified
|
||||
if (content !== originalContent) {
|
||||
await fs.writeFile(file, content, 'utf-8');
|
||||
const relativePath = path.relative(wikiDir, file);
|
||||
console.log(` Converted: ${relativePath}`);
|
||||
convertedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (convertedCount > 0) {
|
||||
console.log(`Converted ${convertedCount} files to wiki format`);
|
||||
} else {
|
||||
console.log('No files needed conversion');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find all files matching the given extensions
|
||||
*/
|
||||
async function findFiles(dir: string, extensions: readonly string[]): Promise<string[]> {
|
||||
const files: string[] = [];
|
||||
|
||||
async function walk(currentDir: string): Promise<void> {
|
||||
const entries: Dirent[] = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await walk(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (extensions.includes(ext)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files in a directory recursively
|
||||
*/
|
||||
async function getAllFiles(dir: string): Promise<Set<string>> {
|
||||
const files = new Set<string>();
|
||||
|
||||
async function walk(currentDir: string): Promise<void> {
|
||||
try {
|
||||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
const relativePath = path.relative(dir, fullPath);
|
||||
|
||||
// Skip .git directory
|
||||
if (entry.name === '.git' || relativePath.startsWith('.git')) continue;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await walk(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
files.add(relativePath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Directory might not exist yet
|
||||
if ((error as any).code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten directory structure - move all files to root
|
||||
* GitHub Wiki prefers flat structure
|
||||
*/
|
||||
async function flattenStructure(wikiDir: string): Promise<void> {
|
||||
console.log('Flattening directory structure for wiki...');
|
||||
const allFiles = await getAllFiles(wikiDir);
|
||||
let movedCount = 0;
|
||||
|
||||
for (const file of allFiles) {
|
||||
// Skip if already at root
|
||||
if (!file.includes('/')) continue;
|
||||
|
||||
const oldPath = path.join(wikiDir, file);
|
||||
const basename = path.basename(file);
|
||||
|
||||
// Create unique name if file already exists at root
|
||||
let newName = basename;
|
||||
let counter = 1;
|
||||
while (await fileExists(path.join(wikiDir, newName))) {
|
||||
const ext = path.extname(basename);
|
||||
const nameWithoutExt = basename.slice(0, -ext.length);
|
||||
newName = `${nameWithoutExt}-${counter}${ext}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
const newPath = path.join(wikiDir, newName);
|
||||
|
||||
// Move file to root
|
||||
await fs.rename(oldPath, newPath);
|
||||
console.log(` Moved: ${file} → ${newName}`);
|
||||
movedCount++;
|
||||
}
|
||||
|
||||
if (movedCount > 0) {
|
||||
console.log(`Moved ${movedCount} files to root`);
|
||||
|
||||
// Clean up empty directories
|
||||
await cleanEmptyDirectories(wikiDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function fileExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove empty directories recursively
|
||||
*/
|
||||
async function cleanEmptyDirectories(dir: string): Promise<void> {
|
||||
const allDirs = await getAllDirectories(dir);
|
||||
|
||||
for (const subDir of allDirs) {
|
||||
try {
|
||||
const entries = await fs.readdir(subDir);
|
||||
if (entries.length === 0 || (entries.length === 1 && entries[0] === '.git')) {
|
||||
await fs.rmdir(subDir);
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all directories recursively
|
||||
*/
|
||||
async function getAllDirectories(dir: string): Promise<string[]> {
|
||||
const dirs: string[] = [];
|
||||
|
||||
async function walk(currentDir: string): Promise<void> {
|
||||
try {
|
||||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name !== '.git') {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
dirs.push(fullPath);
|
||||
await walk(fullPath);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return dirs.sort((a, b) => b.length - a.length); // Sort longest first for cleanup
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync files from source to wiki
|
||||
*/
|
||||
async function syncFiles(sourceDir: string, wikiDir: string): Promise<void> {
|
||||
console.log('Syncing files to wiki...');
|
||||
|
||||
// Get all valid source files
|
||||
const sourceFiles = await findFiles(sourceDir, FILE_EXTENSIONS);
|
||||
const sourceRelativePaths = new Set<string>();
|
||||
|
||||
// Copy all source files
|
||||
console.log(`Found ${sourceFiles.length} files to sync`);
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
const relativePath = path.relative(sourceDir, file);
|
||||
sourceRelativePaths.add(relativePath);
|
||||
|
||||
const targetPath = path.join(wikiDir, relativePath);
|
||||
const targetDir = path.dirname(targetPath);
|
||||
|
||||
// Create directory structure
|
||||
await fs.mkdir(targetDir, { recursive: true });
|
||||
|
||||
// Copy file
|
||||
await fs.copyFile(file, targetPath);
|
||||
}
|
||||
|
||||
// Remove orphaned files
|
||||
const wikiFiles = await getAllFiles(wikiDir);
|
||||
for (const wikiFile of wikiFiles) {
|
||||
if (!sourceRelativePaths.has(wikiFile) && !wikiFile.startsWith('Home')) {
|
||||
const fullPath = path.join(wikiDir, wikiFile);
|
||||
await fs.unlink(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy root README.md to wiki as Home.md if it exists
|
||||
*/
|
||||
async function copyRootReadme(mainRepoPath: string, wikiPath: string): Promise<void> {
|
||||
const rootReadmePath = path.join(mainRepoPath, 'README.md');
|
||||
const wikiHomePath = path.join(wikiPath, 'Home.md');
|
||||
|
||||
try {
|
||||
await fs.access(rootReadmePath);
|
||||
await fs.copyFile(rootReadmePath, wikiHomePath);
|
||||
console.log(' Copied root README.md as Home.md');
|
||||
} catch (error) {
|
||||
console.log(' No root README.md found to use as Home page');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename README files to wiki-compatible names
|
||||
*/
|
||||
async function renameReadmeFiles(wikiDir: string): Promise<void> {
|
||||
console.log('Converting README files for wiki compatibility...');
|
||||
const files = await fs.readdir(wikiDir);
|
||||
|
||||
for (const file of files) {
|
||||
const match = file.match(README_PATTERN);
|
||||
if (match) {
|
||||
const oldPath = path.join(wikiDir, file);
|
||||
let newName: string;
|
||||
|
||||
if (match[1]) {
|
||||
// Language-specific README
|
||||
newName = `Home-${match[1]}.md`;
|
||||
} else {
|
||||
// Main README
|
||||
newName = 'Home.md';
|
||||
}
|
||||
|
||||
const newPath = path.join(wikiDir, newName);
|
||||
await fs.rename(oldPath, newPath);
|
||||
console.log(` Renamed: ${file} → ${newName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any changes in the wiki
|
||||
*/
|
||||
async function hasChanges(wikiDir: string): Promise<boolean> {
|
||||
try {
|
||||
const { stdout } = await execAsync('git status --porcelain', { cwd: wikiDir });
|
||||
return stdout.trim().length > 0;
|
||||
} catch (error) {
|
||||
console.error('Error checking git status:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration from environment variables
|
||||
*/
|
||||
function getConfig(): SyncConfig {
|
||||
const mainRepoPath = process.env.MAIN_REPO_PATH || 'main-repo';
|
||||
const wikiPath = process.env.WIKI_PATH || 'wiki';
|
||||
const docsPath = path.join(mainRepoPath, 'docs');
|
||||
|
||||
return { mainRepoPath, wikiPath, docsPath };
|
||||
}
|
||||
|
||||
/**
|
||||
* Main sync function
|
||||
*/
|
||||
async function syncDocsToWiki(): Promise<void> {
|
||||
const config = getConfig();
|
||||
const flattenWiki = process.env.FLATTEN_WIKI === 'true';
|
||||
|
||||
console.log('Starting documentation sync to wiki...');
|
||||
console.log(`Source: ${config.docsPath}`);
|
||||
console.log(`Target: ${config.wikiPath}`);
|
||||
console.log(`Flatten structure: ${flattenWiki}`);
|
||||
|
||||
try {
|
||||
// Verify paths exist
|
||||
await fs.access(config.docsPath);
|
||||
await fs.access(config.wikiPath);
|
||||
|
||||
// Sync files
|
||||
await syncFiles(config.docsPath, config.wikiPath);
|
||||
|
||||
// Copy root README.md as Home.md
|
||||
await copyRootReadme(config.mainRepoPath, config.wikiPath);
|
||||
|
||||
// Convert to wiki format
|
||||
await convertToWikiFormat(config.wikiPath);
|
||||
|
||||
// Optionally flatten directory structure
|
||||
if (flattenWiki) {
|
||||
await flattenStructure(config.wikiPath);
|
||||
}
|
||||
|
||||
// Rename README files to wiki-compatible names
|
||||
await renameReadmeFiles(config.wikiPath);
|
||||
|
||||
// Check for changes
|
||||
const changed = await hasChanges(config.wikiPath);
|
||||
|
||||
if (changed) {
|
||||
console.log('\nChanges detected in wiki');
|
||||
process.stdout.write('::set-output name=changes::true\n');
|
||||
} else {
|
||||
console.log('\nNo changes detected in wiki');
|
||||
process.stdout.write('::set-output name=changes::false\n');
|
||||
}
|
||||
|
||||
console.log('Sync completed successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error during sync:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
syncDocsToWiki();
|
||||
}
|
||||
|
||||
export { syncDocsToWiki };
|
||||
437
.github/scripts/sync-docs-to-wiki.ts
vendored
Executable file
437
.github/scripts/sync-docs-to-wiki.ts
vendored
Executable file
@@ -0,0 +1,437 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { Dirent } from 'fs';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Configuration
|
||||
const FILE_EXTENSIONS = ['.md', '.png', '.jpg', '.jpeg', '.gif', '.svg'] as const;
|
||||
const README_PATTERN = /^README(?:[-.](.+))?\.md$/;
|
||||
|
||||
interface SyncConfig {
|
||||
mainRepoPath: string;
|
||||
wikiPath: string;
|
||||
docsPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find all files matching the given extensions
|
||||
*/
|
||||
async function findFiles(dir: string, extensions: readonly string[]): Promise<string[]> {
|
||||
const files: string[] = [];
|
||||
|
||||
async function walk(currentDir: string): Promise<void> {
|
||||
const entries: Dirent[] = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await walk(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (extensions.includes(ext)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files in a directory recursively
|
||||
*/
|
||||
async function getAllFiles(dir: string): Promise<Set<string>> {
|
||||
const files = new Set<string>();
|
||||
|
||||
async function walk(currentDir: string): Promise<void> {
|
||||
try {
|
||||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
const relativePath = path.relative(dir, fullPath);
|
||||
|
||||
// Skip .git directory
|
||||
if (entry.name === '.git' || relativePath.startsWith('.git')) continue;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await walk(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
files.add(relativePath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Directory might not exist yet
|
||||
if ((error as any).code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync files from source to wiki, preserving directory structure and removing orphaned files
|
||||
*/
|
||||
async function syncFiles(sourceDir: string, wikiDir: string): Promise<void> {
|
||||
console.log('Analyzing files to sync...');
|
||||
|
||||
// Get all valid source files
|
||||
const sourceFiles = await findFiles(sourceDir, FILE_EXTENSIONS);
|
||||
const sourceRelativePaths = new Set<string>();
|
||||
|
||||
// Copy all source files and track their paths
|
||||
console.log(`Found ${sourceFiles.length} files to sync`);
|
||||
let copiedCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
const relativePath = path.relative(sourceDir, file);
|
||||
sourceRelativePaths.add(relativePath);
|
||||
|
||||
const targetPath = path.join(wikiDir, relativePath);
|
||||
const targetDir = path.dirname(targetPath);
|
||||
|
||||
// Create directory structure
|
||||
await fs.mkdir(targetDir, { recursive: true });
|
||||
|
||||
// Check if file needs updating (compare modification times)
|
||||
let needsCopy = true;
|
||||
try {
|
||||
const sourceStat = await fs.stat(file);
|
||||
const targetStat = await fs.stat(targetPath);
|
||||
// Only copy if source is newer or sizes differ
|
||||
needsCopy = sourceStat.mtime > targetStat.mtime || sourceStat.size !== targetStat.size;
|
||||
} catch {
|
||||
// Target doesn't exist, needs copy
|
||||
needsCopy = true;
|
||||
}
|
||||
|
||||
if (needsCopy) {
|
||||
await fs.copyFile(file, targetPath);
|
||||
console.log(` Updated: ${relativePath}`);
|
||||
copiedCount++;
|
||||
} else {
|
||||
skippedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Updated ${copiedCount} files, ${skippedCount} unchanged`);
|
||||
|
||||
// Find and remove files that don't exist in source
|
||||
console.log('Checking for orphaned files in wiki...');
|
||||
const wikiFiles = await getAllFiles(wikiDir);
|
||||
let removedCount = 0;
|
||||
|
||||
for (const wikiFile of wikiFiles) {
|
||||
// Check if this file should exist (either as-is or will be renamed)
|
||||
let shouldExist = sourceRelativePaths.has(wikiFile);
|
||||
|
||||
// Special handling for Home files that will be created from READMEs
|
||||
if (wikiFile.startsWith('Home')) {
|
||||
const readmeVariant1 = wikiFile.replace(/^Home(-.*)?\.md$/, 'README$1.md');
|
||||
const readmeVariant2 = wikiFile.replace(/^Home-(.+)\.md$/, 'README.$1.md');
|
||||
shouldExist = sourceRelativePaths.has(readmeVariant1) || sourceRelativePaths.has(readmeVariant2) || sourceRelativePaths.has('README.md');
|
||||
}
|
||||
|
||||
if (!shouldExist) {
|
||||
const fullPath = path.join(wikiDir, wikiFile);
|
||||
await fs.unlink(fullPath);
|
||||
console.log(` Removed: ${wikiFile}`);
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removedCount > 0) {
|
||||
console.log(`Removed ${removedCount} orphaned files`);
|
||||
|
||||
// Clean up empty directories
|
||||
await cleanEmptyDirectories(wikiDir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove empty directories recursively
|
||||
*/
|
||||
async function cleanEmptyDirectories(dir: string): Promise<void> {
|
||||
async function removeEmptyDirs(currentDir: string): Promise<boolean> {
|
||||
if (currentDir === dir) return false; // Don't remove root
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
// Skip .git directory
|
||||
const filteredEntries = entries.filter(e => e.name !== '.git');
|
||||
|
||||
if (filteredEntries.length === 0) {
|
||||
await fs.rmdir(currentDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check subdirectories
|
||||
for (const entry of filteredEntries) {
|
||||
if (entry.isDirectory()) {
|
||||
const subDir = path.join(currentDir, entry.name);
|
||||
await removeEmptyDirs(subDir);
|
||||
}
|
||||
}
|
||||
|
||||
// Check again after cleaning subdirectories
|
||||
const remainingEntries = await fs.readdir(currentDir);
|
||||
const filteredRemaining = remainingEntries.filter(e => e !== '.git');
|
||||
|
||||
if (filteredRemaining.length === 0 && currentDir !== dir) {
|
||||
await fs.rmdir(currentDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get all directories and process them
|
||||
const allDirs = await getAllDirectories(dir);
|
||||
for (const subDir of allDirs) {
|
||||
await removeEmptyDirs(subDir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all directories recursively
|
||||
*/
|
||||
async function getAllDirectories(dir: string): Promise<string[]> {
|
||||
const dirs: string[] = [];
|
||||
|
||||
async function walk(currentDir: string): Promise<void> {
|
||||
try {
|
||||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name !== '.git') {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
dirs.push(fullPath);
|
||||
await walk(fullPath);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return dirs.sort((a, b) => b.length - a.length); // Sort longest first for cleanup
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix references in markdown files for wiki compatibility
|
||||
*
|
||||
* Issues fixed:
|
||||
* 1. URL-encoded image references (spaces as %20) need to match actual filenames
|
||||
* 2. Internal markdown links need to be converted to wiki syntax
|
||||
* 3. Images can optionally use wiki syntax [[image.png]] for better compatibility
|
||||
*/
|
||||
async function fixImageReferences(wikiDir: string): Promise<void> {
|
||||
console.log('Fixing references for GitHub Wiki compatibility...');
|
||||
const mdFiles = await findFiles(wikiDir, ['.md']);
|
||||
let fixedCount = 0;
|
||||
|
||||
for (const file of mdFiles) {
|
||||
let content = await fs.readFile(file, 'utf-8');
|
||||
let modified = false;
|
||||
const originalContent = content;
|
||||
|
||||
// Step 1: Fix URL-encoded image references
|
||||
// Convert  to 
|
||||
content = content.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
||||
// Skip external URLs
|
||||
if (src.startsWith('http://') || src.startsWith('https://')) {
|
||||
return match;
|
||||
}
|
||||
|
||||
// Decode URL encoding if present
|
||||
if (src.includes('%')) {
|
||||
try {
|
||||
const decodedSrc = decodeURIComponent(src);
|
||||
return ``;
|
||||
} catch {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
// Step 2: Fix internal links - decode URL encoding but keep standard markdown format
|
||||
// GitHub Wiki actually supports standard markdown links with relative paths
|
||||
content = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, href) => {
|
||||
// Skip external URLs, anchors, and images
|
||||
if (href.startsWith('http://') ||
|
||||
href.startsWith('https://') ||
|
||||
href.startsWith('#') ||
|
||||
href.match(/\.(png|jpg|jpeg|gif|svg)$/i)) {
|
||||
return match;
|
||||
}
|
||||
|
||||
// Decode URL encoding for all internal links
|
||||
if (href.includes('%')) {
|
||||
try {
|
||||
const decodedHref = decodeURIComponent(href);
|
||||
return `[${text}](${decodedHref})`;
|
||||
} catch {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
// Check if content was modified
|
||||
if (content !== originalContent) {
|
||||
modified = true;
|
||||
await fs.writeFile(file, content, 'utf-8');
|
||||
const relativePath = path.relative(wikiDir, file);
|
||||
console.log(` Fixed references in: ${relativePath}`);
|
||||
fixedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (fixedCount > 0) {
|
||||
console.log(`Fixed references in ${fixedCount} files`);
|
||||
} else {
|
||||
console.log('No references needed fixing');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename README files to wiki-compatible names
|
||||
*/
|
||||
async function renameReadmeFiles(wikiDir: string): Promise<void> {
|
||||
console.log('Converting README files for wiki compatibility...');
|
||||
const files = await fs.readdir(wikiDir);
|
||||
|
||||
for (const file of files) {
|
||||
const match = file.match(README_PATTERN);
|
||||
if (match) {
|
||||
const oldPath = path.join(wikiDir, file);
|
||||
let newName: string;
|
||||
|
||||
if (match[1]) {
|
||||
// Language-specific README (e.g., README-ZH_CN.md or README.es.md)
|
||||
newName = `Home-${match[1]}.md`;
|
||||
} else {
|
||||
// Main README
|
||||
newName = 'Home.md';
|
||||
}
|
||||
|
||||
const newPath = path.join(wikiDir, newName);
|
||||
await fs.rename(oldPath, newPath);
|
||||
console.log(` Renamed: ${file} → ${newName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any changes in the wiki
|
||||
*/
|
||||
async function hasChanges(wikiDir: string): Promise<boolean> {
|
||||
try {
|
||||
const { stdout } = await execAsync('git status --porcelain', { cwd: wikiDir });
|
||||
return stdout.trim().length > 0;
|
||||
} catch (error) {
|
||||
console.error('Error checking git status:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy root README.md to wiki as Home.md if it exists
|
||||
*/
|
||||
async function copyRootReadme(mainRepoPath: string, wikiPath: string): Promise<void> {
|
||||
const rootReadmePath = path.join(mainRepoPath, 'README.md');
|
||||
const wikiHomePath = path.join(wikiPath, 'Home.md');
|
||||
|
||||
try {
|
||||
await fs.access(rootReadmePath);
|
||||
await fs.copyFile(rootReadmePath, wikiHomePath);
|
||||
console.log(' Copied root README.md as Home.md');
|
||||
} catch (error) {
|
||||
// Root README doesn't exist or can't be accessed
|
||||
console.log(' No root README.md found to use as Home page');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration from environment variables
|
||||
*/
|
||||
function getConfig(): SyncConfig {
|
||||
const mainRepoPath = process.env.MAIN_REPO_PATH || 'main-repo';
|
||||
const wikiPath = process.env.WIKI_PATH || 'wiki';
|
||||
const docsPath = path.join(mainRepoPath, 'docs');
|
||||
|
||||
return { mainRepoPath, wikiPath, docsPath };
|
||||
}
|
||||
|
||||
/**
|
||||
* Main sync function
|
||||
*/
|
||||
async function syncDocsToWiki(): Promise<void> {
|
||||
const config = getConfig();
|
||||
|
||||
console.log('Starting documentation sync to wiki...');
|
||||
console.log(`Source: ${config.docsPath}`);
|
||||
console.log(`Target: ${config.wikiPath}`);
|
||||
|
||||
try {
|
||||
// Verify paths exist
|
||||
await fs.access(config.docsPath);
|
||||
await fs.access(config.wikiPath);
|
||||
|
||||
// Sync files (copy new/updated, remove orphaned)
|
||||
await syncFiles(config.docsPath, config.wikiPath);
|
||||
|
||||
// Copy root README.md as Home.md
|
||||
await copyRootReadme(config.mainRepoPath, config.wikiPath);
|
||||
|
||||
// Fix image and link references for wiki compatibility
|
||||
await fixImageReferences(config.wikiPath);
|
||||
|
||||
// Rename README files to wiki-compatible names
|
||||
await renameReadmeFiles(config.wikiPath);
|
||||
|
||||
// Check for changes
|
||||
const changed = await hasChanges(config.wikiPath);
|
||||
|
||||
if (changed) {
|
||||
console.log('\nChanges detected in wiki');
|
||||
// GitHub Actions output format
|
||||
process.stdout.write('::set-output name=changes::true\n');
|
||||
} else {
|
||||
console.log('\nNo changes detected in wiki');
|
||||
process.stdout.write('::set-output name=changes::false\n');
|
||||
}
|
||||
|
||||
console.log('Sync completed successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error during sync:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
syncDocsToWiki();
|
||||
}
|
||||
|
||||
export { syncDocsToWiki };
|
||||
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
|
||||
|
||||
67
.github/workflows/sync-docs-to-wiki.yml
vendored
Normal file
67
.github/workflows/sync-docs-to-wiki.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Sync Docs to Wiki
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'docs/**'
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
|
||||
permissions:
|
||||
contents: read # Read access to repository contents
|
||||
# Note: Writing to wiki requires a PAT or GITHUB_TOKEN with additional permissions
|
||||
# The default GITHUB_TOKEN cannot write to wikis, so we need to:
|
||||
# 1. Create a Personal Access Token (PAT) with 'repo' scope
|
||||
# 2. Add it as a repository secret named WIKI_TOKEN
|
||||
|
||||
jobs:
|
||||
sync-wiki:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout main repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: main-repo
|
||||
|
||||
- name: Checkout wiki repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: TriliumNext/Trilium.wiki
|
||||
path: wiki
|
||||
token: ${{ secrets.WIKI_TOKEN }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install tsx for TypeScript execution
|
||||
run: npm install -g tsx
|
||||
|
||||
- name: Setup Git
|
||||
run: |
|
||||
git config --global user.email "action@github.com"
|
||||
git config --global user.name "GitHub Action"
|
||||
|
||||
- name: Sync documentation to wiki
|
||||
id: sync
|
||||
run: |
|
||||
tsx main-repo/.github/scripts/sync-docs-to-wiki.ts
|
||||
env:
|
||||
MAIN_REPO_PATH: main-repo
|
||||
WIKI_PATH: wiki
|
||||
|
||||
- name: Commit and push changes
|
||||
if: contains(steps.sync.outputs.changes, 'true')
|
||||
run: |
|
||||
cd wiki
|
||||
git add .
|
||||
git commit -m "Sync documentation from main repository
|
||||
|
||||
Source commit: ${{ github.sha }}
|
||||
Triggered by: ${{ github.event.head_commit.message }}"
|
||||
git push
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WIKI_TOKEN }}
|
||||
5
.vscode/snippets.code-snippets
vendored
5
.vscode/snippets.code-snippets
vendored
@@ -20,5 +20,10 @@
|
||||
"scope": "typescript",
|
||||
"prefix": "jqf",
|
||||
"body": ["private $${1:name}!: JQuery<HTMLElement>;"]
|
||||
},
|
||||
"region": {
|
||||
"scope": "css",
|
||||
"prefix": "region",
|
||||
"body": ["/* #region ${1:name} */\n$0\n/* #endregion */"]
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
"@stylistic/eslint-plugin": "5.2.3",
|
||||
"@playwright/test": "1.55.0",
|
||||
"@stylistic/eslint-plugin": "5.3.1",
|
||||
"@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.12",
|
||||
"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,15 +28,15 @@
|
||||
"@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": "1.11.18",
|
||||
"dayjs-plugin-utc": "0.1.2",
|
||||
"debounce": "2.2.0",
|
||||
"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.1",
|
||||
"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.3",
|
||||
"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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import froca from "../services/froca.js";
|
||||
import RootCommandExecutor from "./root_command_executor.js";
|
||||
import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js";
|
||||
import Entrypoints from "./entrypoints.js";
|
||||
import options from "../services/options.js";
|
||||
import utils, { hasTouchBar } from "../services/utils.js";
|
||||
import zoomComponent from "./zoom.js";
|
||||
@@ -31,16 +31,14 @@ import { StartupChecks } from "./startup_checks.js";
|
||||
import type { CreateNoteOpts } from "../services/note_create.js";
|
||||
import { ColumnComponent } from "tabulator-tables";
|
||||
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
||||
import type RootContainer from "../widgets/containers/root_container.js";
|
||||
import { SqlExecuteResults } from "@triliumnext/commons";
|
||||
|
||||
interface Layout {
|
||||
getRootWidget: (appContext: AppContext) => RootWidget;
|
||||
getRootWidget: (appContext: AppContext) => RootContainer;
|
||||
}
|
||||
|
||||
interface RootWidget extends Component {
|
||||
render: () => JQuery<HTMLElement>;
|
||||
}
|
||||
|
||||
interface BeforeUploadListener extends Component {
|
||||
export interface BeforeUploadListener extends Component {
|
||||
beforeUnloadEvent(): boolean;
|
||||
}
|
||||
|
||||
@@ -85,7 +83,6 @@ export type CommandMappings = {
|
||||
focusTree: CommandData;
|
||||
focusOnTitle: CommandData;
|
||||
focusOnDetail: CommandData;
|
||||
focusOnSearchDefinition: Required<CommandData>;
|
||||
searchNotes: CommandData & {
|
||||
searchString?: string;
|
||||
ancestorNoteId?: string | null;
|
||||
@@ -93,6 +90,11 @@ export type CommandMappings = {
|
||||
closeTocCommand: CommandData;
|
||||
closeHlt: CommandData;
|
||||
showLaunchBarSubtree: CommandData;
|
||||
showHiddenSubtree: CommandData;
|
||||
showSQLConsoleHistory: CommandData;
|
||||
logout: CommandData;
|
||||
switchToMobileVersion: CommandData;
|
||||
switchToDesktopVersion: CommandData;
|
||||
showRevisions: CommandData & {
|
||||
noteId?: string | null;
|
||||
};
|
||||
@@ -138,6 +140,7 @@ export type CommandMappings = {
|
||||
showLeftPane: CommandData;
|
||||
showAttachments: CommandData;
|
||||
showSearchHistory: CommandData;
|
||||
showShareSubtree: CommandData;
|
||||
hoistNote: CommandData & { noteId: string };
|
||||
leaveProtectedSession: CommandData;
|
||||
enterProtectedSession: CommandData;
|
||||
@@ -323,6 +326,7 @@ export type CommandMappings = {
|
||||
printActiveNote: CommandData;
|
||||
exportAsPdf: CommandData;
|
||||
openNoteExternally: CommandData;
|
||||
openNoteCustom: CommandData;
|
||||
renderActiveNote: CommandData;
|
||||
unhoist: CommandData;
|
||||
reloadFrontendApp: CommandData;
|
||||
@@ -526,7 +530,7 @@ export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMapp
|
||||
export class AppContext extends Component {
|
||||
isMainWindow: boolean;
|
||||
components: Component[];
|
||||
beforeUnloadListeners: WeakRef<BeforeUploadListener>[];
|
||||
beforeUnloadListeners: (WeakRef<BeforeUploadListener> | (() => boolean))[];
|
||||
tabManager!: TabManager;
|
||||
layout?: Layout;
|
||||
noteTreeWidget?: NoteTreeWidget;
|
||||
@@ -619,7 +623,7 @@ export class AppContext extends Component {
|
||||
component.triggerCommand(commandName, { $el: $(this) });
|
||||
});
|
||||
|
||||
this.child(rootWidget);
|
||||
this.child(rootWidget as Component);
|
||||
|
||||
this.triggerEvent("initialRenderComplete", {});
|
||||
}
|
||||
@@ -649,13 +653,17 @@ export class AppContext extends Component {
|
||||
return $(el).closest(".component").prop("component");
|
||||
}
|
||||
|
||||
addBeforeUnloadListener(obj: BeforeUploadListener) {
|
||||
addBeforeUnloadListener(obj: BeforeUploadListener | (() => boolean)) {
|
||||
if (typeof WeakRef !== "function") {
|
||||
// older browsers don't support WeakRef
|
||||
return;
|
||||
}
|
||||
|
||||
this.beforeUnloadListeners.push(new WeakRef<BeforeUploadListener>(obj));
|
||||
if (typeof obj === "object") {
|
||||
this.beforeUnloadListeners.push(new WeakRef<BeforeUploadListener>(obj));
|
||||
} else {
|
||||
this.beforeUnloadListeners.push(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,25 +673,29 @@ const appContext = new AppContext(window.glob.isMainWindow);
|
||||
$(window).on("beforeunload", () => {
|
||||
let allSaved = true;
|
||||
|
||||
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => !!wr.deref());
|
||||
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => typeof wr === "function" || !!wr.deref());
|
||||
|
||||
for (const weakRef of appContext.beforeUnloadListeners) {
|
||||
const component = weakRef.deref();
|
||||
for (const listener of appContext.beforeUnloadListeners) {
|
||||
if (typeof listener === "object") {
|
||||
const component = listener.deref();
|
||||
|
||||
if (!component) {
|
||||
continue;
|
||||
}
|
||||
if (!component) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!component.beforeUnloadEvent()) {
|
||||
console.log(`Component ${component.componentId} is not finished saving its state.`);
|
||||
|
||||
toast.showMessage(t("app_context.please_wait_for_save"), 10000);
|
||||
|
||||
allSaved = false;
|
||||
if (!component.beforeUnloadEvent()) {
|
||||
console.log(`Component ${component.componentId} is not finished saving its state.`);
|
||||
allSaved = false;
|
||||
}
|
||||
} else {
|
||||
if (!listener()) {
|
||||
allSaved = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!allSaved) {
|
||||
toast.showMessage(t("app_context.please_wait_for_save"), 10000);
|
||||
return "some string";
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import utils from "../services/utils.js";
|
||||
import type { CommandMappings, CommandNames, EventData, EventNames } from "./app_context.js";
|
||||
|
||||
type EventHandler = ((data: any) => void);
|
||||
|
||||
/**
|
||||
* Abstract class for all components in the Trilium's frontend.
|
||||
*
|
||||
@@ -19,6 +21,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
||||
initialized: Promise<void> | null;
|
||||
parent?: TypedComponent<any>;
|
||||
_position!: number;
|
||||
private listeners: Record<string, EventHandler[]> | null = {};
|
||||
|
||||
constructor() {
|
||||
this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`;
|
||||
@@ -76,6 +79,14 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
||||
handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
|
||||
const promises: Promise<unknown>[] = [];
|
||||
|
||||
// Handle React children.
|
||||
if (this.listeners?.[name]) {
|
||||
for (const listener of this.listeners[name]) {
|
||||
listener(data);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle legacy children.
|
||||
for (const child of this.children) {
|
||||
const ret = child.handleEvent(name, data) as Promise<void>;
|
||||
|
||||
@@ -120,6 +131,35 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
registerHandler<T extends EventNames>(name: T, handler: EventHandler) {
|
||||
if (!this.listeners) {
|
||||
this.listeners = {};
|
||||
}
|
||||
|
||||
if (!this.listeners[name]) {
|
||||
this.listeners[name] = [];
|
||||
}
|
||||
|
||||
if (this.listeners[name].includes(handler)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.listeners[name].push(handler);
|
||||
}
|
||||
|
||||
removeHandler<T extends EventNames>(name: T, handler: EventHandler) {
|
||||
if (!this.listeners?.[name]?.includes(handler)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.listeners[name] = this.listeners[name]
|
||||
.filter(listener => listener !== handler);
|
||||
|
||||
if (!this.listeners[name].length) {
|
||||
delete this.listeners[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class Component extends TypedComponent<Component> {}
|
||||
|
||||
@@ -10,22 +10,7 @@ import bundleService from "../services/bundle.js";
|
||||
import froca from "../services/froca.js";
|
||||
import linkService from "../services/link.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
|
||||
// TODO: Move somewhere else nicer.
|
||||
export type SqlExecuteResults = string[][][];
|
||||
|
||||
// TODO: Deduplicate with server.
|
||||
interface SqlExecuteResponse {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
results: SqlExecuteResults;
|
||||
}
|
||||
|
||||
// TODO: Deduplicate with server.
|
||||
interface CreateChildrenResponse {
|
||||
note: FNote;
|
||||
}
|
||||
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
|
||||
|
||||
export default class Entrypoints extends Component {
|
||||
constructor() {
|
||||
@@ -34,7 +19,7 @@ export default class Entrypoints extends Component {
|
||||
|
||||
openDevToolsCommand() {
|
||||
if (utils.isElectron()) {
|
||||
utils.dynamicRequire("@electron/remote").getCurrentWindow().toggleDevTools();
|
||||
utils.dynamicRequire("@electron/remote").getCurrentWindow().webContents.toggleDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +109,7 @@ export default class Entrypoints extends Component {
|
||||
if (utils.isElectron()) {
|
||||
// standard JS version does not work completely correctly in electron
|
||||
const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents();
|
||||
const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex());
|
||||
const activeIndex = webContents.navigationHistory.getActiveIndex();
|
||||
|
||||
webContents.goToIndex(activeIndex - 1);
|
||||
} else {
|
||||
@@ -136,7 +121,7 @@ export default class Entrypoints extends Component {
|
||||
if (utils.isElectron()) {
|
||||
// standard JS version does not work completely correctly in electron
|
||||
const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents();
|
||||
const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex());
|
||||
const activeIndex = webContents.navigationHistory.getActiveIndex();
|
||||
|
||||
webContents.goToIndex(activeIndex + 1);
|
||||
} else {
|
||||
|
||||
@@ -43,8 +43,6 @@ export default class RootCommandExecutor extends Component {
|
||||
const noteContext = await appContext.tabManager.openTabWithNoteWithHoisting(searchNote.noteId, {
|
||||
activate: true
|
||||
});
|
||||
|
||||
appContext.triggerCommand("focusOnSearchDefinition", { ntxId: noteContext.ntxId });
|
||||
}
|
||||
|
||||
async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import electronContextMenu from "./menus/electron_context_menu.js";
|
||||
import glob from "./services/glob.js";
|
||||
import { t } from "./services/i18n.js";
|
||||
import options from "./services/options.js";
|
||||
import server from "./services/server.js";
|
||||
import type ElectronRemote from "@electron/remote";
|
||||
import type Electron from "electron";
|
||||
import "./stylesheets/bootstrap.scss";
|
||||
|
||||
@@ -64,7 +64,7 @@ export interface NoteMetaData {
|
||||
/**
|
||||
* Note is the main node and concept in Trilium.
|
||||
*/
|
||||
class FNote {
|
||||
export default class FNote {
|
||||
private froca: Froca;
|
||||
|
||||
noteId!: string;
|
||||
@@ -1020,6 +1020,14 @@ class FNote {
|
||||
return this.noteId.startsWith("_options");
|
||||
}
|
||||
|
||||
isTriliumSqlite() {
|
||||
return this.mime === "text/x-sqlite;schema=trilium";
|
||||
}
|
||||
|
||||
isTriliumScript() {
|
||||
return this.mime.startsWith("application/javascript");
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides note's date metadata.
|
||||
*/
|
||||
@@ -1027,5 +1035,3 @@ class FNote {
|
||||
return await server.get<NoteMetaData>(`notes/${this.noteId}/metadata`);
|
||||
}
|
||||
}
|
||||
|
||||
export default FNote;
|
||||
|
||||
@@ -1,78 +1,47 @@
|
||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
||||
import TabRowWidget from "../widgets/tab_row.js";
|
||||
import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js";
|
||||
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import NoteTitleWidget from "../widgets/note_title.js";
|
||||
import OwnedAttributeListWidget from "../widgets/ribbon_widgets/owned_attribute_list.js";
|
||||
import NoteActionsWidget from "../widgets/buttons/note_actions.js";
|
||||
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||
import RibbonContainer from "../widgets/containers/ribbon_container.js";
|
||||
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
|
||||
import InheritedAttributesWidget from "../widgets/ribbon_widgets/inherited_attribute_list.js";
|
||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||
import NoteListWidget from "../widgets/note_list.js";
|
||||
import SearchDefinitionWidget from "../widgets/ribbon_widgets/search_definition.js";
|
||||
import SqlResultWidget from "../widgets/sql_result.js";
|
||||
import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js";
|
||||
import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js";
|
||||
import ImagePropertiesWidget from "../widgets/ribbon_widgets/image_properties.js";
|
||||
import NotePropertiesWidget from "../widgets/ribbon_widgets/note_properties.js";
|
||||
import NoteIconWidget from "../widgets/note_icon.js";
|
||||
import SearchResultWidget from "../widgets/search_result.js";
|
||||
import NoteIconWidget from "../widgets/note_icon.jsx";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
||||
import SpacerWidget from "../widgets/spacer.js";
|
||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||
import LeftPaneToggleWidget from "../widgets/buttons/left_pane_toggle.js";
|
||||
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
||||
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
||||
import BasicPropertiesWidget from "../widgets/ribbon_widgets/basic_properties.js";
|
||||
import NoteInfoWidget from "../widgets/ribbon_widgets/note_info_widget.js";
|
||||
import BookPropertiesWidget from "../widgets/ribbon_widgets/book_properties.js";
|
||||
import NoteMapRibbonWidget from "../widgets/ribbon_widgets/note_map.js";
|
||||
import NotePathsWidget from "../widgets/ribbon_widgets/note_paths.js";
|
||||
import SimilarNotesWidget from "../widgets/ribbon_widgets/similar_notes.js";
|
||||
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
||||
import EditButton from "../widgets/floating_buttons/edit_button.js";
|
||||
import EditedNotesWidget from "../widgets/ribbon_widgets/edited_notes.js";
|
||||
import ShowTocWidgetButton from "../widgets/buttons/show_toc_widget_button.js";
|
||||
import ShowHighlightsListWidgetButton from "../widgets/buttons/show_highlights_list_widget_button.js";
|
||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js";
|
||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||
import FindWidget from "../widgets/find.js";
|
||||
import TocWidget from "../widgets/toc.js";
|
||||
import HighlightsListWidget from "../widgets/highlights_list.js";
|
||||
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
||||
import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
|
||||
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
|
||||
import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js";
|
||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||
import RevisionsButton from "../widgets/buttons/revisions_button.js";
|
||||
import CodeButtonsWidget from "../widgets/floating_buttons/code_buttons.js";
|
||||
import ApiLogWidget from "../widgets/api_log.js";
|
||||
import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js";
|
||||
import ScriptExecutorWidget from "../widgets/ribbon_widgets/script_executor.js";
|
||||
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
||||
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
||||
import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js";
|
||||
import ScrollPaddingWidget from "../widgets/scroll_padding.js";
|
||||
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
|
||||
import ScrollPadding from "../widgets/scroll_padding.js";
|
||||
import options from "../services/options.js";
|
||||
import utils from "../services/utils.js";
|
||||
import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js";
|
||||
import ContextualHelpButton from "../widgets/floating_buttons/help_button.js";
|
||||
import CloseZenButton from "../widgets/close_zen_button.js";
|
||||
import type { AppContext } from "../components/app_context.js";
|
||||
import type { WidgetsByParent } from "../services/bundle.js";
|
||||
import SwitchSplitOrientationButton from "../widgets/floating_buttons/switch_layout_button.js";
|
||||
import ToggleReadOnlyButton from "../widgets/floating_buttons/toggle_read_only_button.js";
|
||||
import PngExportButton from "../widgets/floating_buttons/png_export_button.js";
|
||||
import RefreshButton from "../widgets/floating_buttons/refresh_button.js";
|
||||
import { applyModals } from "./layout_commons.js";
|
||||
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||
import SearchResult from "../widgets/search_result.jsx";
|
||||
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
|
||||
import SqlResults from "../widgets/sql_result.js";
|
||||
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
|
||||
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
|
||||
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
|
||||
import ApiLog from "../widgets/api_log.jsx";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
||||
import SharedInfo from "../widgets/shared_info.jsx";
|
||||
|
||||
export default class DesktopLayout {
|
||||
|
||||
@@ -107,9 +76,9 @@ export default class DesktopLayout {
|
||||
new FlexContainer("row")
|
||||
.class("tab-row-container")
|
||||
.child(new FlexContainer("row").id("tab-row-left-spacer"))
|
||||
.optChild(launcherPaneIsHorizontal, new LeftPaneToggleWidget(true))
|
||||
.optChild(launcherPaneIsHorizontal, <LeftPaneToggle isHorizontalLayout={true} />)
|
||||
.child(new TabRowWidget().class("full-width"))
|
||||
.optChild(customTitleBarButtons, new TitleBarButtonsWidget())
|
||||
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
||||
.css("height", "40px")
|
||||
.css("background-color", "var(--launcher-pane-background-color)")
|
||||
.setParent(appContext)
|
||||
@@ -130,7 +99,7 @@ export default class DesktopLayout {
|
||||
new FlexContainer("column")
|
||||
.id("rest-pane")
|
||||
.css("flex-grow", "1")
|
||||
.optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, new TitleBarButtonsWidget()).css("height", "40px"))
|
||||
.optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, <TitleBarButtons />).css("height", "40px"))
|
||||
.child(
|
||||
new FlexContainer("row")
|
||||
.filling()
|
||||
@@ -151,69 +120,30 @@ export default class DesktopLayout {
|
||||
.css("min-height", "50px")
|
||||
.css("align-items", "center")
|
||||
.cssBlock(".title-row > * { margin: 5px; }")
|
||||
.child(new NoteIconWidget())
|
||||
.child(new NoteTitleWidget())
|
||||
.child(<NoteIconWidget />)
|
||||
.child(<NoteTitleWidget />)
|
||||
.child(new SpacerWidget(0, 1))
|
||||
.child(new MovePaneButton(true))
|
||||
.child(new MovePaneButton(false))
|
||||
.child(new ClosePaneButton())
|
||||
.child(new CreatePaneButton())
|
||||
)
|
||||
.child(
|
||||
new RibbonContainer()
|
||||
// the order of the widgets matter. Some of these want to "activate" themselves
|
||||
// when visible. When this happens to multiple of them, the first one "wins".
|
||||
// promoted attributes should always win.
|
||||
.ribbon(new ClassicEditorToolbar())
|
||||
.ribbon(new ScriptExecutorWidget())
|
||||
.ribbon(new SearchDefinitionWidget())
|
||||
.ribbon(new EditedNotesWidget())
|
||||
.ribbon(new BookPropertiesWidget())
|
||||
.ribbon(new NotePropertiesWidget())
|
||||
.ribbon(new FilePropertiesWidget())
|
||||
.ribbon(new ImagePropertiesWidget())
|
||||
.ribbon(new BasicPropertiesWidget())
|
||||
.ribbon(new OwnedAttributeListWidget())
|
||||
.ribbon(new InheritedAttributesWidget())
|
||||
.ribbon(new NotePathsWidget())
|
||||
.ribbon(new NoteMapRibbonWidget())
|
||||
.ribbon(new SimilarNotesWidget())
|
||||
.ribbon(new NoteInfoWidget())
|
||||
.button(new RevisionsButton())
|
||||
.button(new NoteActionsWidget())
|
||||
)
|
||||
.child(new SharedInfoWidget())
|
||||
.child(<Ribbon />)
|
||||
.child(<SharedInfo />)
|
||||
.child(new WatchedFileUpdateStatusWidget())
|
||||
.child(
|
||||
new FloatingButtons()
|
||||
.child(new RefreshButton())
|
||||
.child(new SwitchSplitOrientationButton())
|
||||
.child(new ToggleReadOnlyButton())
|
||||
.child(new EditButton())
|
||||
.child(new ShowTocWidgetButton())
|
||||
.child(new ShowHighlightsListWidgetButton())
|
||||
.child(new CodeButtonsWidget())
|
||||
.child(new RelationMapButtons())
|
||||
.child(new GeoMapButtons())
|
||||
.child(new CopyImageReferenceButton())
|
||||
.child(new SvgExportButton())
|
||||
.child(new PngExportButton())
|
||||
.child(new BacklinksWidget())
|
||||
.child(new ContextualHelpButton())
|
||||
.child(new HideFloatingButtonsButton())
|
||||
)
|
||||
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
||||
.child(
|
||||
new ScrollingContainer()
|
||||
.filling()
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(new SqlTableSchemasWidget())
|
||||
.child(<SqlTableSchemas />)
|
||||
.child(new NoteDetailWidget())
|
||||
.child(new NoteListWidget(false))
|
||||
.child(new SearchResultWidget())
|
||||
.child(new SqlResultWidget())
|
||||
.child(new ScrollPaddingWidget())
|
||||
.child(<SearchResult />)
|
||||
.child(<SqlResults />)
|
||||
.child(<ScrollPadding />)
|
||||
)
|
||||
.child(new ApiLogWidget())
|
||||
.child(<ApiLog />)
|
||||
.child(new FindWidget())
|
||||
.child(
|
||||
...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC
|
||||
@@ -232,11 +162,11 @@ export default class DesktopLayout {
|
||||
)
|
||||
)
|
||||
)
|
||||
.child(new CloseZenButton())
|
||||
.child(<CloseZenModeButton />)
|
||||
|
||||
// Desktop-specific dialogs.
|
||||
.child(new PasswordNoteSetDialog())
|
||||
.child(new UploadAttachmentsDialog());
|
||||
.child(<PasswordNoteSetDialog />)
|
||||
.child(<UploadAttachmentsDialog />);
|
||||
|
||||
applyModals(rootContainer);
|
||||
return rootContainer;
|
||||
@@ -246,14 +176,18 @@ export default class DesktopLayout {
|
||||
let launcherPane;
|
||||
|
||||
if (isHorizontal) {
|
||||
launcherPane = new FlexContainer("row").css("height", "53px").class("horizontal").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true));
|
||||
launcherPane = new FlexContainer("row")
|
||||
.css("height", "53px")
|
||||
.class("horizontal")
|
||||
.child(new LauncherContainer(true))
|
||||
.child(<GlobalMenu isHorizontalLayout={true} />);
|
||||
} else {
|
||||
launcherPane = new FlexContainer("column")
|
||||
.css("width", "53px")
|
||||
.class("vertical")
|
||||
.child(new GlobalMenuWidget(false))
|
||||
.child(<GlobalMenu isHorizontalLayout={false} />)
|
||||
.child(new LauncherContainer(false))
|
||||
.child(new LeftPaneToggleWidget(false));
|
||||
.child(<LeftPaneToggle isHorizontalLayout={false} />);
|
||||
}
|
||||
|
||||
launcherPane.id("launcher-pane");
|
||||
@@ -24,48 +24,48 @@ import InfoDialog from "../widgets/dialogs/info.js";
|
||||
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
|
||||
import PopupEditorDialog from "../widgets/dialogs/popup_editor.js";
|
||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import NoteIconWidget from "../widgets/note_icon.js";
|
||||
import NoteTitleWidget from "../widgets/note_title.js";
|
||||
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
|
||||
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
|
||||
import NoteIconWidget from "../widgets/note_icon";
|
||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||
import NoteListWidget from "../widgets/note_list.js";
|
||||
import { CallToActionDialog } from "../widgets/dialogs/call_to_action.jsx";
|
||||
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
|
||||
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||
import { PopupEditorFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.js";
|
||||
|
||||
export function applyModals(rootContainer: RootContainer) {
|
||||
rootContainer
|
||||
.child(new BulkActionsDialog())
|
||||
.child(new AboutDialog())
|
||||
.child(new HelpDialog())
|
||||
.child(new RecentChangesDialog())
|
||||
.child(new BranchPrefixDialog())
|
||||
.child(new SortChildNotesDialog())
|
||||
.child(new IncludeNoteDialog())
|
||||
.child(new NoteTypeChooserDialog())
|
||||
.child(new JumpToNoteDialog())
|
||||
.child(new AddLinkDialog())
|
||||
.child(new CloneToDialog())
|
||||
.child(new MoveToDialog())
|
||||
.child(new ImportDialog())
|
||||
.child(new ExportDialog())
|
||||
.child(new MarkdownImportDialog())
|
||||
.child(new ProtectedSessionPasswordDialog())
|
||||
.child(new RevisionsDialog())
|
||||
.child(new DeleteNotesDialog())
|
||||
.child(new InfoDialog())
|
||||
.child(new ConfirmDialog())
|
||||
.child(new PromptDialog())
|
||||
.child(new IncorrectCpuArchDialog())
|
||||
.child(<BulkActionsDialog />)
|
||||
.child(<AboutDialog />)
|
||||
.child(<HelpDialog />)
|
||||
.child(<RecentChangesDialog />)
|
||||
.child(<BranchPrefixDialog />)
|
||||
.child(<SortChildNotesDialog />)
|
||||
.child(<IncludeNoteDialog />)
|
||||
.child(<NoteTypeChooserDialog />)
|
||||
.child(<JumpToNoteDialog />)
|
||||
.child(<AddLinkDialog />)
|
||||
.child(<CloneToDialog />)
|
||||
.child(<MoveToDialog />)
|
||||
.child(<ImportDialog />)
|
||||
.child(<ExportDialog />)
|
||||
.child(<MarkdownImportDialog />)
|
||||
.child(<ProtectedSessionPasswordDialog />)
|
||||
.child(<RevisionsDialog />)
|
||||
.child(<DeleteNotesDialog />)
|
||||
.child(<InfoDialog />)
|
||||
.child(<ConfirmDialog />)
|
||||
.child(<PromptDialog />)
|
||||
.child(<IncorrectCpuArchDialog />)
|
||||
.child(new PopupEditorDialog()
|
||||
.child(new FlexContainer("row")
|
||||
.class("title-row")
|
||||
.css("align-items", "center")
|
||||
.cssBlock(".title-row > * { margin: 5px; }")
|
||||
.child(new NoteIconWidget())
|
||||
.child(new NoteTitleWidget()))
|
||||
.child(new ClassicEditorToolbar())
|
||||
.child(<NoteIconWidget />)
|
||||
.child(<NoteTitleWidget />))
|
||||
.child(<PopupEditorFormattingToolbar />)
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(new NoteDetailWidget())
|
||||
.child(new NoteListWidget(true)))
|
||||
.child(new CallToActionDialog());
|
||||
.child(<CallToActionDialog />);
|
||||
}
|
||||
@@ -3,30 +3,26 @@ import NoteTitleWidget from "../widgets/note_title.js";
|
||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import ToggleSidebarButtonWidget from "../widgets/mobile_widgets/toggle_sidebar_button.js";
|
||||
import MobileDetailMenuWidget from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js";
|
||||
import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
|
||||
import EditButton from "../widgets/floating_buttons/edit_button.js";
|
||||
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
|
||||
import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js";
|
||||
import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js";
|
||||
import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js";
|
||||
import NoteListWidget from "../widgets/note_list.js";
|
||||
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
|
||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
|
||||
import type AppContext from "../components/app_context.js";
|
||||
import TabRowWidget from "../widgets/tab_row.js";
|
||||
import RefreshButton from "../widgets/floating_buttons/refresh_button.js";
|
||||
import MobileEditorToolbar from "../widgets/ribbon_widgets/mobile_editor_toolbar.js";
|
||||
import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js";
|
||||
import { applyModals } from "./layout_commons.js";
|
||||
import CloseZenButton from "../widgets/close_zen_button.js";
|
||||
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
||||
import { useNoteContext } from "../widgets/react/hooks.jsx";
|
||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.js";
|
||||
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||
|
||||
const MOBILE_CSS = `
|
||||
<style>
|
||||
@@ -143,20 +139,12 @@ export default class MobileLayout {
|
||||
.contentSized()
|
||||
.css("font-size", "larger")
|
||||
.css("align-items", "center")
|
||||
.child(new ToggleSidebarButtonWidget().contentSized())
|
||||
.child(new NoteTitleWidget().contentSized().css("position", "relative").css("padding-left", "0.5em"))
|
||||
.child(new MobileDetailMenuWidget(true).contentSized())
|
||||
)
|
||||
.child(new SharedInfoWidget())
|
||||
.child(
|
||||
new FloatingButtons()
|
||||
.child(new RefreshButton())
|
||||
.child(new EditButton())
|
||||
.child(new RelationMapButtons())
|
||||
.child(new SvgExportButton())
|
||||
.child(new BacklinksWidget())
|
||||
.child(new HideFloatingButtonsButton())
|
||||
.child(<ToggleSidebarButton />)
|
||||
.child(<NoteTitleWidget />)
|
||||
.child(<MobileDetailMenu />)
|
||||
)
|
||||
.child(<SharedInfoWidget />)
|
||||
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(
|
||||
new ScrollingContainer()
|
||||
@@ -164,9 +152,9 @@ export default class MobileLayout {
|
||||
.contentSized()
|
||||
.child(new NoteDetailWidget())
|
||||
.child(new NoteListWidget(false))
|
||||
.child(new FilePropertiesWidget().css("font-size", "smaller"))
|
||||
.child(<FilePropertiesWrapper />)
|
||||
)
|
||||
.child(new MobileEditorToolbar())
|
||||
.child(<MobileEditorToolbar />)
|
||||
)
|
||||
)
|
||||
.child(
|
||||
@@ -174,10 +162,25 @@ export default class MobileLayout {
|
||||
.contentSized()
|
||||
.id("mobile-bottom-bar")
|
||||
.child(new TabRowWidget().css("height", "40px"))
|
||||
.child(new FlexContainer("row").class("horizontal").css("height", "53px").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true)).id("launcher-pane"))
|
||||
.child(new FlexContainer("row")
|
||||
.class("horizontal")
|
||||
.css("height", "53px")
|
||||
.child(new LauncherContainer(true))
|
||||
.child(<GlobalMenuWidget isHorizontalLayout />)
|
||||
.id("launcher-pane"))
|
||||
)
|
||||
.child(new CloseZenButton());
|
||||
.child(<CloseZenModeButton />);
|
||||
applyModals(rootContainer);
|
||||
return rootContainer;
|
||||
}
|
||||
}
|
||||
|
||||
function FilePropertiesWrapper() {
|
||||
const { note } = useNoteContext();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{note?.type === "file" && <FilePropertiesTab note={note} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import server from "./server.js";
|
||||
import froca from "./froca.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { AttributeRow } from "./load_results.js";
|
||||
import { AttributeType } from "@triliumnext/commons";
|
||||
|
||||
async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||
await server.put(`notes/${noteId}/attribute`, {
|
||||
@@ -25,6 +26,14 @@ async function removeAttributeById(noteId: string, attributeId: string) {
|
||||
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
|
||||
}
|
||||
|
||||
export async function removeOwnedAttributesByNameOrType(note: FNote, type: AttributeType, name: string) {
|
||||
for (const attr of note.getOwnedAttributes()) {
|
||||
if (attr.type === type && attr.name === name) {
|
||||
await server.remove(`notes/${note.noteId}/attributes/${attr.attributeId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a label identified by its name from the given note, if it exists. Note that the label must be owned, i.e.
|
||||
* it will not remove inherited attributes.
|
||||
@@ -52,7 +61,7 @@ function removeOwnedLabelByName(note: FNote, labelName: string) {
|
||||
* @param value the value of the attribute to set.
|
||||
*/
|
||||
export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
|
||||
if (value) {
|
||||
if (value !== null && value !== undefined) {
|
||||
// Create or update the attribute.
|
||||
await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value });
|
||||
} else {
|
||||
|
||||
@@ -18,7 +18,7 @@ import type FNote from "../entities/fnote.js";
|
||||
import toast from "./toast.js";
|
||||
import { BulkAction } from "@triliumnext/commons";
|
||||
|
||||
const ACTION_GROUPS = [
|
||||
export const ACTION_GROUPS = [
|
||||
{
|
||||
title: t("bulk_actions.labels"),
|
||||
actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction]
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
import { t } from "./i18n.js";
|
||||
import toastService, { showError } from "./toast.js";
|
||||
|
||||
function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
|
||||
export function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
|
||||
try {
|
||||
$imageWrapper.attr("contenteditable", "true");
|
||||
selectImage($imageWrapper.get(0));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { byBookType, byNoteType } from "./help_button.js";
|
||||
import { byBookType, byNoteType } from "./in_app_help.js";
|
||||
import fs from "fs";
|
||||
import type { HiddenSubtreeItem } from "@triliumnext/commons";
|
||||
import path from "path";
|
||||
@@ -25,7 +25,7 @@ describe("Help button", () => {
|
||||
...Object.values(byBookType)
|
||||
].filter((noteId) => noteId) as string[];
|
||||
|
||||
const metaPath = path.resolve(path.join(__dirname, "../../../../server/src/assets/doc_notes/en/User Guide/!!!meta.json"));
|
||||
const metaPath = path.resolve(path.join(__dirname, "../../../server/src/assets/doc_notes/en/User Guide/!!!meta.json"));
|
||||
const meta: HiddenSubtreeItem[] = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
||||
const allNoteIds = new Set(getNoteIds(meta));
|
||||
|
||||
43
apps/client/src/services/in_app_help.ts
Normal file
43
apps/client/src/services/in_app_help.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { NoteType } from "@triliumnext/commons";
|
||||
import { ViewTypeOptions } from "./note_list_renderer";
|
||||
import FNote from "../entities/fnote";
|
||||
|
||||
export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
|
||||
canvas: null,
|
||||
code: null,
|
||||
contentWidget: null,
|
||||
doc: null,
|
||||
file: null,
|
||||
image: null,
|
||||
launcher: null,
|
||||
mermaid: null,
|
||||
mindMap: null,
|
||||
noteMap: null,
|
||||
relationMap: null,
|
||||
render: null,
|
||||
search: null,
|
||||
text: null,
|
||||
webView: null,
|
||||
aiChat: null
|
||||
};
|
||||
|
||||
export const byBookType: Record<ViewTypeOptions, string | null> = {
|
||||
list: "mULW0Q3VojwY",
|
||||
grid: "8QqnMzx393bx",
|
||||
calendar: "xWbu3jpNWapp",
|
||||
table: "2FvYrpmOXm29",
|
||||
geoMap: "81SGnPGMk7Xc",
|
||||
board: "CtBQqbwXDx1w"
|
||||
};
|
||||
|
||||
export function getHelpUrlForNote(note: FNote | null | undefined) {
|
||||
if (note && note.type !== "book" && byNoteType[note.type]) {
|
||||
return byNoteType[note.type];
|
||||
} else if (note?.hasLabel("calendarRoot")) {
|
||||
return "l0tKav7yLHGF";
|
||||
} else if (note?.hasLabel("textSnippet")) {
|
||||
return "pwc194wlRzcH";
|
||||
} else if (note && note.type === "book") {
|
||||
return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""]
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,10 @@ async function getAction(actionName: string, silent = false) {
|
||||
return action;
|
||||
}
|
||||
|
||||
export function getActionSync(actionName: string) {
|
||||
return keyboardActionRepo[actionName];
|
||||
}
|
||||
|
||||
function updateDisplayedShortcuts($container: JQuery<HTMLElement>) {
|
||||
//@ts-ignore
|
||||
//TODO: each() does not support async callbacks.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,7 +35,7 @@ function download(url: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function downloadFileNote(noteId: string) {
|
||||
export function downloadFileNote(noteId: string) {
|
||||
const url = `${getFileUrl("notes", noteId)}?${Date.now()}`; // don't use cache
|
||||
|
||||
download(url);
|
||||
@@ -163,7 +163,7 @@ async function openExternally(type: string, entityId: string, mime: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const openNoteExternally = async (noteId: string, mime: string) => await openExternally("notes", noteId, mime);
|
||||
export const openNoteExternally = async (noteId: string, mime: string) => await openExternally("notes", noteId, mime);
|
||||
const openAttachmentExternally = async (attachmentId: string, mime: string) => await openExternally("attachments", attachmentId, mime);
|
||||
|
||||
function getHost() {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, sile
|
||||
if (utils.isElectron()) {
|
||||
const ipc = utils.dynamicRequire("electron").ipcRenderer;
|
||||
|
||||
ipc.on("server-response", async (event: string, arg: Arg) => {
|
||||
ipc.on("server-response", async (_, arg: Arg) => {
|
||||
if (arg.statusCode >= 200 && arg.statusCode < 300) {
|
||||
handleSuccessfulResponse(arg);
|
||||
} else {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import dayjs from "dayjs";
|
||||
import type { ViewScope } from "./link.js";
|
||||
import FNote from "../entities/fnote";
|
||||
|
||||
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 +14,7 @@ function reloadFrontendApp(reason?: string) {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function restartDesktopApp() {
|
||||
export function restartDesktopApp() {
|
||||
if (!isElectron()) {
|
||||
reloadFrontendApp();
|
||||
return;
|
||||
@@ -125,7 +126,7 @@ function formatDateISO(date: Date) {
|
||||
return `${date.getFullYear()}-${padNum(date.getMonth() + 1)}-${padNum(date.getDate())}`;
|
||||
}
|
||||
|
||||
function formatDateTime(date: Date, userSuppliedFormat?: string): string {
|
||||
export function formatDateTime(date: Date, userSuppliedFormat?: string): string {
|
||||
if (userSuppliedFormat?.trim()) {
|
||||
return dayjs(date).format(userSuppliedFormat);
|
||||
} else {
|
||||
@@ -144,11 +145,11 @@ function now() {
|
||||
/**
|
||||
* Returns `true` if the client is currently running under Electron, or `false` if running in a web browser.
|
||||
*/
|
||||
function isElectron() {
|
||||
export function isElectron() {
|
||||
return !!(window && window.process && window.process.type);
|
||||
}
|
||||
|
||||
function isMac() {
|
||||
export function isMac() {
|
||||
return navigator.platform.indexOf("Mac") > -1;
|
||||
}
|
||||
|
||||
@@ -185,7 +186,11 @@ export function escapeQuotes(value: string) {
|
||||
return value.replaceAll('"', """);
|
||||
}
|
||||
|
||||
function formatSize(size: number) {
|
||||
export function formatSize(size: number | null | undefined) {
|
||||
if (size === null || size === undefined) {
|
||||
return "";
|
||||
}
|
||||
|
||||
size = Math.max(Math.round(size / 1024), 1);
|
||||
|
||||
if (size < 1024) {
|
||||
@@ -218,7 +223,7 @@ function randomString(len: number) {
|
||||
return text;
|
||||
}
|
||||
|
||||
function isMobile() {
|
||||
export function isMobile() {
|
||||
return (
|
||||
window.glob?.device === "mobile" ||
|
||||
// window.glob.device is not available in setup
|
||||
@@ -292,7 +297,7 @@ function isHtmlEmpty(html: string) {
|
||||
);
|
||||
}
|
||||
|
||||
async function clearBrowserCache() {
|
||||
export async function clearBrowserCache() {
|
||||
if (isElectron()) {
|
||||
const win = dynamicRequire("@electron/remote").getCurrentWindow();
|
||||
await win.webContents.session.clearCache();
|
||||
@@ -306,7 +311,13 @@ function copySelectionToClipboard() {
|
||||
}
|
||||
}
|
||||
|
||||
function dynamicRequire(moduleName: string) {
|
||||
type dynamicRequireMappings = {
|
||||
"@electron/remote": typeof import("@electron/remote"),
|
||||
"electron": typeof import("electron"),
|
||||
"child_process": typeof import("child_process")
|
||||
};
|
||||
|
||||
export function dynamicRequire<T extends keyof dynamicRequireMappings>(moduleName: T): Awaited<dynamicRequireMappings[T]>{
|
||||
if (typeof __non_webpack_require__ !== "undefined") {
|
||||
return __non_webpack_require__(moduleName);
|
||||
} else {
|
||||
@@ -374,33 +385,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>) {
|
||||
@@ -561,8 +581,7 @@ function copyHtmlToClipboard(content: string) {
|
||||
document.removeEventListener("copy", listener);
|
||||
}
|
||||
|
||||
// TODO: Set to FNote once the file is ported.
|
||||
function createImageSrcUrl(note: { noteId: string; title: string }) {
|
||||
export function createImageSrcUrl(note: FNote) {
|
||||
return `api/images/${note.noteId}/${encodeURIComponent(note.title)}?timestamp=${Date.now()}`;
|
||||
}
|
||||
|
||||
@@ -731,10 +750,86 @@ function isUpdateAvailable(latestVersion: string | null | undefined, currentVers
|
||||
return compareVersions(latestVersion, currentVersion) > 0;
|
||||
}
|
||||
|
||||
function isLaunchBarConfig(noteId: string) {
|
||||
export function isLaunchBarConfig(noteId: string) {
|
||||
return ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers", "_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(noteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a class to the <body> of the page, where the class name is formed via a prefix and a value.
|
||||
* Useful for configurable options such as `heading-style-markdown`, where `heading-style` is the prefix and `markdown` is the dynamic value.
|
||||
* There is no separator between the prefix and the value, if needed it has to be supplied manually to the prefix.
|
||||
*
|
||||
* @param prefix the prefix.
|
||||
* @param value the value to be appended to the prefix.
|
||||
*/
|
||||
export function toggleBodyClass(prefix: string, value: string) {
|
||||
const $body = $("body");
|
||||
for (const clazz of Array.from($body[0].classList)) {
|
||||
// create copy to safely iterate over while removing classes
|
||||
if (clazz.startsWith(prefix)) {
|
||||
$body.removeClass(clazz);
|
||||
}
|
||||
}
|
||||
|
||||
$body.addClass(prefix + value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic comparison for equality between the two arrays. The values are strictly checked via `===`.
|
||||
*
|
||||
* @param a the first array to compare.
|
||||
* @param b the second array to compare.
|
||||
* @returns `true` if both arrays are equals, `false` otherwise.
|
||||
*/
|
||||
export function arrayEqual<T>(a: T[], b: T[]) {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i=0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
type Indexed<T extends object> = T & { index: number };
|
||||
|
||||
/**
|
||||
* Given an object array, alters every object in the array to have an index field assigned to it.
|
||||
*
|
||||
* @param items the objects to be numbered.
|
||||
* @returns the same object for convenience, with the type changed to indicate the new index field.
|
||||
*/
|
||||
export function numberObjectsInPlace<T extends object>(items: T[]): Indexed<T>[] {
|
||||
let index = 0;
|
||||
for (const item of items) {
|
||||
(item as Indexed<T>).index = index++;
|
||||
}
|
||||
return items as Indexed<T>[];
|
||||
}
|
||||
|
||||
export function mapToKeyValueArray<K extends string | number | symbol, V>(map: Record<K, V>) {
|
||||
const values: { key: K, value: V }[] = [];
|
||||
for (const [ key, value ] of Object.entries(map)) {
|
||||
values.push({ key: key as K, value: value as V });
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
export function getErrorMessage(e: unknown) {
|
||||
if (e && typeof e === "object" && "message" in e && typeof e.message === "string") {
|
||||
return e.message;
|
||||
} else {
|
||||
return "Unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
reloadFrontendApp,
|
||||
restartDesktopApp,
|
||||
|
||||
@@ -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,12 +161,26 @@ 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 {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
label.tn-radio input[type="radio"],
|
||||
label.tn-checkbox input[type="checkbox"] {
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
#left-pane input,
|
||||
#left-pane select,
|
||||
#left-pane textarea {
|
||||
@@ -346,7 +382,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);
|
||||
@@ -406,14 +442,20 @@ body #context-menu-container .dropdown-item > span {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dropdown-menu kbd {
|
||||
.dropdown-item span.keyboard-shortcut {
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dropdown-menu kbd {
|
||||
color: var(--muted-text-color);
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
padding-bottom: 0;
|
||||
padding: 0;
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dropdown-item,
|
||||
@@ -831,10 +873,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;
|
||||
@@ -922,6 +988,11 @@ div[data-notify="container"] {
|
||||
font-family: var(--monospace-font-family);
|
||||
}
|
||||
|
||||
svg.ck-icon .note-icon {
|
||||
color: var(--main-text-color);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.ck-content {
|
||||
--ck-content-font-family: var(--detail-font-family);
|
||||
--ck-content-font-size: 1.1em;
|
||||
@@ -1392,7 +1463,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
color: var(--launcher-pane-text-color);
|
||||
background-color: var(--launcher-pane-background-color);
|
||||
background: transparent;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1738,16 +1809,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 +1835,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 +1888,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 {
|
||||
@@ -2255,16 +2360,18 @@ footer.webview-footer button {
|
||||
padding: 1px 10px 1px 10px;
|
||||
}
|
||||
|
||||
/* Search result highlighting */
|
||||
.search-result-title b,
|
||||
.search-result-content b {
|
||||
font-weight: 900;
|
||||
color: var(--admonition-warning-accent-color);
|
||||
}
|
||||
|
||||
/* Customized icons */
|
||||
|
||||
.bx-tn-toc::before {
|
||||
content: "\ec24";
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* CK Edito */
|
||||
|
||||
/* Insert text snippet: limit the width of the listed items to avoid overly long names */
|
||||
:root body.desktop div.ck-template-form li.ck-list__item .ck-template-form__text-part > span {
|
||||
max-width: 25vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
--main-text-color: #ccc;
|
||||
--main-border-color: #454545;
|
||||
--subtle-border-color: #313131;
|
||||
--dropdown-border-color: #292929;
|
||||
--dropdown-border-color: #404040;
|
||||
--dropdown-shadow-opacity: 0.6;
|
||||
--dropdown-item-icon-destructive-color: #de6e5b;
|
||||
--disabled-tooltip-icon-color: #7fd2ef;
|
||||
@@ -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;
|
||||
@@ -120,6 +121,8 @@
|
||||
--quick-search-focus-border: #80808095;
|
||||
--quick-search-focus-background: #ffffff1f;
|
||||
--quick-search-focus-color: white;
|
||||
--quick-search-result-content-background: #0000004d;
|
||||
--quick-search-result-highlight-color: #a4d995;
|
||||
|
||||
--left-pane-collapsed-border-color: #0009;
|
||||
--left-pane-background-color: #1f1f1f;
|
||||
|
||||
@@ -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;
|
||||
@@ -114,6 +115,8 @@
|
||||
--quick-search-focus-border: #00000029;
|
||||
--quick-search-focus-background: #ffffff80;
|
||||
--quick-search-focus-color: #000;
|
||||
--quick-search-result-content-background: #0000000f;
|
||||
--quick-search-result-highlight-color: #c65050;
|
||||
|
||||
--left-pane-collapsed-border-color: #0000000d;
|
||||
--left-pane-background-color: #f2f2f2;
|
||||
|
||||
@@ -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
|
||||
*
|
||||
@@ -191,13 +197,17 @@ html body .dropdown-item[disabled] {
|
||||
|
||||
/* Menu item keyboard shortcut */
|
||||
.dropdown-item kbd {
|
||||
margin-left: 16px;
|
||||
font-family: unset !important;
|
||||
font-size: unset !important;
|
||||
color: var(--menu-item-keyboard-shortcut-color) !important;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.dropdown-item span.keyboard-shortcut {
|
||||
color: var(--menu-item-keyboard-shortcut-color) !important;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
position: relative;
|
||||
border-color: transparent !important;
|
||||
@@ -319,6 +329,8 @@ body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after {
|
||||
|
||||
#toast-container .toast .toast-body {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -530,10 +542,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;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
button.btn.btn-primary,
|
||||
button.btn.btn-secondary,
|
||||
button.btn.btn-sm:not(.select-button),
|
||||
button.btn.btn-success {
|
||||
button.btn.btn-success,
|
||||
button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel, .ck-button-replaceall, .ck-button-replace).ck-button_with-text {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -21,7 +22,8 @@ button.btn.btn-success {
|
||||
button.btn.btn-primary:hover,
|
||||
button.btn.btn-secondary:hover,
|
||||
button.btn.btn-sm:not(.select-button):hover,
|
||||
button.btn.btn-success:hover {
|
||||
button.btn.btn-success:hover,
|
||||
button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel, .ck-button-replaceall, .ck-button-replace).ck-button_with-text:not(.ck-disabled):hover {
|
||||
background: var(--cmd-button-hover-background-color);
|
||||
color: var(--cmd-button-hover-text-color);
|
||||
}
|
||||
@@ -29,7 +31,8 @@ button.btn.btn-success:hover {
|
||||
button.btn.btn-primary:active,
|
||||
button.btn.btn-secondary:active,
|
||||
button.btn.btn-sm:not(.select-button):active,
|
||||
button.btn.btn-success:active {
|
||||
button.btn.btn-success:active,
|
||||
button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel, .ck-button-replaceall, .ck-button-replace).ck-button_with-text:not(.ck-disabled):active {
|
||||
opacity: 0.85;
|
||||
box-shadow: unset;
|
||||
background: var(--cmd-button-background-color) !important;
|
||||
@@ -40,14 +43,16 @@ button.btn.btn-success:active {
|
||||
button.btn.btn-primary:disabled,
|
||||
button.btn.btn-secondary:disabled,
|
||||
button.btn.btn-sm:not(.select-button):disabled,
|
||||
button.btn.btn-success:disabled {
|
||||
button.btn.btn-success:disabled,
|
||||
button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel, .ck-button-replaceall, .ck-button-replace).ck-button_with-text.ck-disabled {
|
||||
opacity: var(--cmd-button-disabled-opacity);
|
||||
}
|
||||
|
||||
button.btn.btn-primary:focus-visible,
|
||||
button.btn.btn-secondary:focus-visible,
|
||||
button.btn.btn-sm:not(.select-button):focus-visible,
|
||||
button.btn.btn-success:focus-visible {
|
||||
button.btn.btn-success:focus-visible,
|
||||
button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel, .ck-button-replaceall, .ck-button-replace).ck-button_with-text:not(.ck-disabled):focus-visible {
|
||||
outline: 2px solid var(--input-focus-outline-color);
|
||||
}
|
||||
|
||||
@@ -149,8 +154,11 @@ input[type="password"],
|
||||
input[type="date"],
|
||||
input[type="time"],
|
||||
input[type="datetime-local"],
|
||||
:root input.ck.ck-input-text,
|
||||
:root input.ck.ck-input-number,
|
||||
textarea.form-control,
|
||||
textarea,
|
||||
:root textarea.ck.ck-textarea,
|
||||
.tn-input-field {
|
||||
outline: 3px solid transparent;
|
||||
outline-offset: 6px;
|
||||
@@ -167,8 +175,11 @@ input[type="password"]:hover,
|
||||
input[type="date"]:hover,
|
||||
input[type="time"]:hover,
|
||||
input[type="datetime-local"]:hover,
|
||||
:root input.ck.ck-input-text:not([readonly="true"]):hover,
|
||||
:root input.ck.ck-input-number:not([readonly="true"]):hover,
|
||||
textarea.form-control:hover,
|
||||
textarea:hover,
|
||||
:root textarea.ck.ck-textarea:hover,
|
||||
.tn-input-field:hover {
|
||||
background: var(--input-hover-background);
|
||||
color: var(--input-hover-color);
|
||||
@@ -181,8 +192,11 @@ input[type="password"]:focus,
|
||||
input[type="date"]:focus,
|
||||
input[type="time"]:focus,
|
||||
input[type="datetime-local"]:focus,
|
||||
:root input.ck.ck-input-text:focus,
|
||||
:root input.ck.ck-input-number:focus,
|
||||
textarea.form-control:focus,
|
||||
textarea:focus,
|
||||
:root textarea.ck.ck-textarea:focus,
|
||||
.tn-input-field:focus,
|
||||
.tn-input-field:focus-within {
|
||||
box-shadow: unset;
|
||||
@@ -455,6 +469,7 @@ optgroup {
|
||||
left: 0;
|
||||
width: var(--box-size);
|
||||
height: 100%;
|
||||
margin: unset;
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
:root {
|
||||
--ck-font-face: var(--main-font-family);
|
||||
--ck-input-label-height: 1.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -307,6 +308,11 @@
|
||||
fill: black !important;
|
||||
}
|
||||
|
||||
/* Hex color input box prefix */
|
||||
:root .ck.ck-color-selector .ck-color-picker__hash-view {
|
||||
margin-top: var(--ck-input-label-height);
|
||||
}
|
||||
|
||||
/* Numbered list */
|
||||
|
||||
:root .ck.ck-list-properties_with-numbered-properties .ck.ck-list-styles-list {
|
||||
@@ -363,19 +369,86 @@
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
/* Text snippet dropdown */
|
||||
|
||||
:root .ck-link-actions button.ck-button,
|
||||
:root .ck-link-form button.ck-button {
|
||||
--ck-border-radius: 6px;
|
||||
|
||||
background: transparent;
|
||||
box-shadow: unset;
|
||||
div.ck-template-form {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
:root .ck-link-actions button.ck-button:hover,
|
||||
:root .ck-link-form button.ck-button:hover {
|
||||
background: var(--hover-item-background-color);
|
||||
div.ck-template-form .ck-labeled-field-view {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Template item */
|
||||
|
||||
:root div.ck-template-form li.ck-list__item button.ck-template-button {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
/* Template icon */
|
||||
:root .ck-template-form .ck-button__icon {
|
||||
--ck-spacing-medium: 2px;
|
||||
}
|
||||
|
||||
:root div.ck-template-form .note-icon {
|
||||
color: var(--menu-item-icon-color);
|
||||
}
|
||||
|
||||
/* Template name */
|
||||
div.ck-template-form .ck-template-form__text-part {
|
||||
color: var(--hover-item-text-color);
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
div.ck-template-form .ck-template-form__text-part mark {
|
||||
background: unset;
|
||||
color: var(--quick-search-result-highlight-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Template description */
|
||||
:root div.ck-template-form .ck-template-form__description {
|
||||
opacity: .5;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
div.ck-template-form .ck-search__info > span {
|
||||
line-height: initial;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
div.ck-template-form .ck-search__info span:nth-child(2) {
|
||||
display: block;
|
||||
opacity: .5;
|
||||
margin-top: 8px;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
/* Link dropdown */
|
||||
|
||||
:root .ck.ck-form.ck-link-form ul.ck-link-form__providers-list {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
/* Math popup */
|
||||
|
||||
.ck-math-form .ck-labeled-field-view {
|
||||
--ck-input-label-height: 0;
|
||||
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
|
||||
/* Emoji dropdown */
|
||||
|
||||
.ck-emoji-picker-form .ck-emoji__search .ck-button_with-text:not(.ck-list-item-button) {
|
||||
margin-top: var(--ck-input-label-height);
|
||||
}
|
||||
|
||||
/* Find and replace dialog */
|
||||
|
||||
.ck-find-and-replace-form .ck-find-and-replace-form__inputs button {
|
||||
margin-top: var(--ck-input-label-height);
|
||||
}
|
||||
|
||||
/* Mention list (the autocompletion list for emojis, labels and relations) */
|
||||
@@ -392,6 +465,58 @@
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/*
|
||||
* FORMS
|
||||
*/
|
||||
|
||||
/*
|
||||
* Buttons
|
||||
*/
|
||||
|
||||
button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel).ck-button_with-text {
|
||||
--ck-color-text: var(--cmd-button-text-color);
|
||||
|
||||
min-width: 60px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/*
|
||||
* Text boxes
|
||||
*/
|
||||
|
||||
.ck.ck-labeled-field-view {
|
||||
padding-top: var(--ck-input-label-height) !important; /* Create space for the label */
|
||||
}
|
||||
|
||||
.ck.ck-labeled-field-view > .ck.ck-labeled-field-view__input-wrapper > label.ck.ck-label {
|
||||
/* Move the label above the text box regardless of the text box state */
|
||||
transform: translate(0, calc(-.2em - var(--ck-input-label-height))) !important;
|
||||
|
||||
padding-left: 0 !important;
|
||||
background: transparent;
|
||||
font-size: .85em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:root input.ck.ck-input-text[readonly="true"] {
|
||||
cursor: not-allowed;
|
||||
background: var(--input-background-color);
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
|
||||
:root .ck.ck-form__row.ck-form__row_with-submit > :not(:first-child) {
|
||||
margin-inline-start: 16px;
|
||||
}
|
||||
|
||||
.ck.ck-form__row_with-submit button {
|
||||
margin-top: var(--ck-input-label-height);
|
||||
}
|
||||
|
||||
.ck.ck-form__header {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* EDITOR'S CONTENT
|
||||
*/
|
||||
|
||||
@@ -96,7 +96,6 @@
|
||||
background: var(--background) !important;
|
||||
color: var(--color) !important;
|
||||
line-height: unset;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.sql-table-schemas-widget .sql-table-schemas button:hover,
|
||||
@@ -106,18 +105,6 @@
|
||||
--color: var(--main-text-color);
|
||||
}
|
||||
|
||||
/* Tooltip */
|
||||
|
||||
.tooltip .table-schema {
|
||||
font-family: var(--monospace-font-family);
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
/* Data type */
|
||||
.tooltip .table-schema td:nth-child(2) {
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
/*
|
||||
* NOTE MAP
|
||||
*/
|
||||
@@ -181,9 +168,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 +177,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 +223,6 @@ div.note-detail-empty {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.options-section .options-mime-types {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.options-section .form-group {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
@@ -59,8 +59,7 @@ body.background-effects.platform-win32.layout-vertical {
|
||||
}
|
||||
|
||||
body.background-effects.platform-win32,
|
||||
body.background-effects.platform-win32 #root-widget,
|
||||
body.background-effects.platform-win32 #launcher-pane .launcher-button {
|
||||
body.background-effects.platform-win32 #root-widget {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
@@ -330,7 +329,6 @@ body.layout-horizontal > .horizontal {
|
||||
*/
|
||||
|
||||
.calendar-dropdown-widget {
|
||||
width: unset !important;
|
||||
padding: 12px;
|
||||
color: var(--calendar-color);
|
||||
user-select: none;
|
||||
@@ -576,25 +574,18 @@ div.quick-search .search-button.show {
|
||||
* Quick search results
|
||||
*/
|
||||
|
||||
div.quick-search .dropdown-menu {
|
||||
--quick-search-item-delimiter-color: transparent;
|
||||
--menu-item-icon-vert-offset: -.065em;
|
||||
}
|
||||
|
||||
/* Item */
|
||||
.quick-search .dropdown-menu *.dropdown-item {
|
||||
padding: 8px 12px !important;
|
||||
}
|
||||
|
||||
/* Note icon */
|
||||
.quick-search .dropdown-menu .dropdown-item > .bx {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
/* Note title */
|
||||
.quick-search .dropdown-menu .dropdown-item > a {
|
||||
color: var(--menu-text-color);
|
||||
}
|
||||
|
||||
.quick-search .dropdown-menu .dropdown-item > a:hover {
|
||||
--hover-item-background-color: transparent;
|
||||
text-decoration: underline;
|
||||
.quick-search .quick-search-item-icon {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
/* Note path */
|
||||
@@ -605,6 +596,24 @@ div.quick-search .search-button.show {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Note content snippet */
|
||||
:root .quick-search .search-result-content {
|
||||
background-color: var(--quick-search-result-content-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Highlighted search terms */
|
||||
:root .quick-search .search-result-title b,
|
||||
:root .quick-search .search-result-content b,
|
||||
:root .quick-search .search-result-attributes b {
|
||||
color: var(--quick-search-result-highlight-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.quick-search div.dropdown-divider {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* TREE PANE
|
||||
*/
|
||||
@@ -1375,7 +1384,7 @@ div.floating-buttons-children .floating-button:active {
|
||||
}
|
||||
|
||||
/* The first visible floating button */
|
||||
div.floating-buttons-children > *:nth-child(1 of .visible) {
|
||||
div.floating-buttons-children > *:first-child {
|
||||
--border-radius: var(--border-radius-size) 0 0 var(--border-radius-size);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
@@ -1477,13 +1486,6 @@ div.floating-buttons-children .close-floating-buttons:has(.close-floating-button
|
||||
padding-inline-start: 8px;
|
||||
}
|
||||
|
||||
/* Copy image reference */
|
||||
|
||||
.floating-buttons .copy-image-reference-button .hidden-image-copy {
|
||||
/* Take out of the the hidden image from flexbox to prevent the layout being affected */
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* Code, relation map buttons */
|
||||
|
||||
.floating-buttons .code-buttons-widget,
|
||||
|
||||
@@ -732,7 +732,8 @@
|
||||
"note_type": "笔记类型",
|
||||
"editable": "可编辑",
|
||||
"basic_properties": "基本属性",
|
||||
"language": "语言"
|
||||
"language": "语言",
|
||||
"configure_code_notes": "配置代码注释..."
|
||||
},
|
||||
"book_properties": {
|
||||
"view_type": "视图类型",
|
||||
@@ -848,7 +849,7 @@
|
||||
"debug": "调试",
|
||||
"debug_description": "调试将打印额外的调试信息到控制台,以帮助调试复杂查询",
|
||||
"action": "操作",
|
||||
"search_button": "搜索 <kbd>回车</kbd>",
|
||||
"search_button": "搜索",
|
||||
"search_execute": "搜索并执行操作",
|
||||
"save_to_note": "保存到笔记",
|
||||
"search_parameters": "搜索参数",
|
||||
@@ -1165,7 +1166,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": {
|
||||
@@ -1439,9 +1440,9 @@
|
||||
"open-in-popup": "快速编辑"
|
||||
},
|
||||
"shared_info": {
|
||||
"shared_publicly": "此笔记已公开分享于",
|
||||
"shared_locally": "此笔记已在本地分享于",
|
||||
"help_link": "访问 <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a> 获取帮助。"
|
||||
"help_link": "访问 <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a> 获取帮助。",
|
||||
"shared_publicly": "该笔记已在 {{- link}} 上公开分享。",
|
||||
"shared_locally": "此笔记在本地通过 {{- link}} 进行共享。"
|
||||
},
|
||||
"note_types": {
|
||||
"text": "文本",
|
||||
@@ -1519,7 +1520,8 @@
|
||||
"hoist-this-note-workspace": "聚焦此笔记(工作区)",
|
||||
"refresh-saved-search-results": "刷新保存的搜索结果",
|
||||
"create-child-note": "创建子笔记",
|
||||
"unhoist": "取消聚焦"
|
||||
"unhoist": "取消聚焦",
|
||||
"toggle-sidebar": "切换侧边栏"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "保持此窗口置顶"
|
||||
@@ -1871,14 +1873,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 +1999,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
@@ -732,7 +732,8 @@
|
||||
"note_type": "Note type",
|
||||
"editable": "Editable",
|
||||
"basic_properties": "Basic Properties",
|
||||
"language": "Language"
|
||||
"language": "Language",
|
||||
"configure_code_notes": "Configure code notes..."
|
||||
},
|
||||
"book_properties": {
|
||||
"view_type": "View type",
|
||||
@@ -848,7 +849,7 @@
|
||||
"debug": "debug",
|
||||
"debug_description": "Debug will print extra debugging information into the console to aid in debugging complex queries",
|
||||
"action": "action",
|
||||
"search_button": "Search <kbd>enter</kbd>",
|
||||
"search_button": "Search",
|
||||
"search_execute": "Search & Execute actions",
|
||||
"save_to_note": "Save to note",
|
||||
"search_parameters": "Search Parameters",
|
||||
@@ -1113,6 +1114,12 @@
|
||||
"layout-vertical-description": "launcher bar is on the left (default)",
|
||||
"layout-horizontal-description": "launcher bar is underneath the tab bar, the tab bar is now full width."
|
||||
},
|
||||
"ui-performance": {
|
||||
"title": "Performance",
|
||||
"enable-motion": "Enable transitions and animations",
|
||||
"enable-shadows": "Enable shadows",
|
||||
"enable-backdrop-effects": "Enable background effects for menus, popups and panels"
|
||||
},
|
||||
"ai_llm": {
|
||||
"not_started": "Not started",
|
||||
"title": "AI Settings",
|
||||
@@ -1253,7 +1260,12 @@
|
||||
"selected_provider": "Selected Provider",
|
||||
"selected_provider_description": "Choose the AI provider for chat and completion features",
|
||||
"select_model": "Select model...",
|
||||
"select_provider": "Select provider..."
|
||||
"select_provider": "Select provider...",
|
||||
"ai_enabled": "AI features enabled",
|
||||
"ai_disabled": "AI features disabled",
|
||||
"no_models_found_online": "No models found. Please check your API key and settings.",
|
||||
"no_models_found_ollama": "No Ollama models found. Please check if Ollama is running.",
|
||||
"error_fetching": "Error fetching models: {{error}}"
|
||||
},
|
||||
"zoom_factor": {
|
||||
"title": "Zoom Factor (desktop build only)",
|
||||
@@ -1310,7 +1322,7 @@
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "Note Revision Snapshot Interval",
|
||||
"note_revisions_snapshot_description": "The Note revision snapshot interval is the time after which a new note revision will be created for the note. See <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a> for more info.",
|
||||
"note_revisions_snapshot_description": "The Note revision snapshot interval is the time after which a new note revision will be created for the note. See <doc>wiki</doc> for more info.",
|
||||
"snapshot_time_interval_label": "Note revision snapshot time interval:"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
@@ -1372,7 +1384,7 @@
|
||||
},
|
||||
"custom_date_time_format": {
|
||||
"title": "Custom Date/Time Format",
|
||||
"description": "Customize the format of the date and time inserted via <kbd></kbd> or the toolbar. See <a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">Day.js docs</a> for available format tokens.",
|
||||
"description": "Customize the format of the date and time inserted via <shortcut /> or the toolbar. See <doc>Day.js docs</doc> for available format tokens.",
|
||||
"format_string": "Format string:",
|
||||
"formatted_time": "Formatted date/time:"
|
||||
},
|
||||
@@ -1590,8 +1602,8 @@
|
||||
"open-in-popup": "Quick edit"
|
||||
},
|
||||
"shared_info": {
|
||||
"shared_publicly": "This note is shared publicly on",
|
||||
"shared_locally": "This note is shared locally on",
|
||||
"shared_publicly": "This note is shared publicly on {{- link}}.",
|
||||
"shared_locally": "This note is shared locally on {{- link}}.",
|
||||
"help_link": "For help visit <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a>."
|
||||
},
|
||||
"note_types": {
|
||||
@@ -1670,7 +1682,8 @@
|
||||
"hoist-this-note-workspace": "Hoist this note (workspace)",
|
||||
"refresh-saved-search-results": "Refresh saved search results",
|
||||
"create-child-note": "Create child note",
|
||||
"unhoist": "Unhoist"
|
||||
"unhoist": "Unhoist",
|
||||
"toggle-sidebar": "Toggle sidebar"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Keep Window on Top"
|
||||
@@ -1994,11 +2007,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",
|
||||
@@ -832,7 +848,7 @@
|
||||
"debug": "debug",
|
||||
"debug_description": "Debug imprimera des informations supplémentaires dans la console pour faciliter le débogage des requêtes complexes",
|
||||
"action": "action",
|
||||
"search_button": "Recherche <kbd>Entrée</kbd>",
|
||||
"search_button": "Recherche",
|
||||
"search_execute": "Rechercher et exécuter des actions",
|
||||
"save_to_note": "Enregistrer dans la note",
|
||||
"search_parameters": "Paramètres de recherche",
|
||||
@@ -899,7 +915,7 @@
|
||||
"description1": "Le script de recherche permet de définir les résultats de la recherche en exécutant un script. Cela offre une flexibilité maximale lorsque la recherche standard ne suffit pas.",
|
||||
"description2": "Le script de recherche doit être de type \"code\" et sous-type \"backend JavaScript\". Le script doit retourner un tableau de noteIds ou de notes.",
|
||||
"example_title": "Voir cet exemple :",
|
||||
"example_code": "// 1. préfiltrage à l'aide de la recherche standard\nconst candidateNotes = api.searchForNotes(\"#journal\"); \n\n// 2. application de critères de recherche personnalisés\nconst matchedNotes = candidateNotes\n .filter(note => note.title.match(/[0-9]{1,2}\\. ?[0-9]{1,2}\\. ?[0-9]{4}/));\n\nreturn matchedNotes;",
|
||||
"example_code": "// 1. préfiltrage à l'aide de la recherche standard\nconst candidateNotes = api.searchForNotes(\"#journal\"); \n\n// 2. application de critères de recherche personnalisés\nconst matchedNotes = candidateNotes\n .filter(note => note.title.match(/[0-9]{1,2}\\. ?[0-9]{1,2}\\. ?[0-9]{4}/));\n\nreturn matchedNotes;",
|
||||
"note": "Notez que le script de recherche et la l'expression à rechercher standard ne peuvent pas être combinés."
|
||||
},
|
||||
"search_string": {
|
||||
@@ -1068,7 +1084,8 @@
|
||||
"max_width_label": "Largeur maximale du contenu en pixels",
|
||||
"apply_changes_description": "Pour appliquer les modifications de largeur du contenu, cliquez sur",
|
||||
"reload_button": "recharger l'interface",
|
||||
"reload_description": "changements par rapport aux options d'apparence"
|
||||
"reload_description": "changements par rapport aux options d'apparence",
|
||||
"max_width_unit": "Pixels"
|
||||
},
|
||||
"native_title_bar": {
|
||||
"title": "Barre de titre native (nécessite le redémarrage de l'application)",
|
||||
@@ -1091,7 +1108,10 @@
|
||||
"layout-vertical-title": "Vertical",
|
||||
"layout-horizontal-title": "Horizontal",
|
||||
"layout-vertical-description": "la barre de raccourcis est à gauche (défaut)",
|
||||
"layout-horizontal-description": "la barre de raccourcis est sous la barre des onglets, cette-dernière est s'affiche en pleine largeur."
|
||||
"layout-horizontal-description": "la barre de raccourcis est sous la barre des onglets, cette-dernière est s'affiche en pleine largeur.",
|
||||
"auto_theme": "Hérité (suivre le schéma de couleurs du système)",
|
||||
"light_theme": "Hérité (clair)",
|
||||
"dark_theme": "Hérité (foncé)"
|
||||
},
|
||||
"zoom_factor": {
|
||||
"title": "Facteur de zoom (version bureau uniquement)",
|
||||
@@ -1136,14 +1156,14 @@
|
||||
"note_erasure_timeout": {
|
||||
"note_erasure_timeout_title": "Délai d'effacement des notes",
|
||||
"note_erasure_description": "Les notes supprimées (et les attributs, versions...) sont seulement marquées comme supprimées et il est possible de les récupérer à partir de la boîte de dialogue Notes récentes. Après un certain temps, les notes supprimées sont « effacées », ce qui signifie que leur contenu n'est plus récupérable. Ce paramètre vous permet de configurer la durée entre la suppression et l'effacement de la note.",
|
||||
"erase_notes_after": "Effacer les notes après:",
|
||||
"erase_notes_after": "Effacer les notes après :",
|
||||
"manual_erasing_description": "Vous pouvez également déclencher l'effacement manuellement (sans tenir compte de la durée définie ci-dessus) :",
|
||||
"erase_deleted_notes_now": "Effacer les notes supprimées maintenant",
|
||||
"deleted_notes_erased": "Les notes supprimées ont été effacées."
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "Délai d'enregistrement automatique d'une version de note",
|
||||
"note_revisions_snapshot_description": "Le délai d'enregistrement automatique des versions de note définit le temps avant la création automatique d'une nouvelle version de note. Consultez le <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a> pour plus d'informations.",
|
||||
"note_revisions_snapshot_description": "Le délai d'enregistrement automatique des versions de note définit le temps avant la création automatique d'une nouvelle version de note. Consultez le <doc>wiki</doc> pour plus d'informations.",
|
||||
"snapshot_time_interval_label": "Délai d'enregistrement automatique de version de note :"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
@@ -1369,8 +1389,8 @@
|
||||
"convert-to-attachment-confirm": "Êtes-vous sûr de vouloir convertir les notes sélectionnées en pièces jointes de leurs notes parentes ?"
|
||||
},
|
||||
"shared_info": {
|
||||
"shared_publicly": "Cette note est partagée publiquement sur",
|
||||
"shared_locally": "Cette note est partagée localement sur",
|
||||
"shared_publicly": "Cette note est partagée publiquement sur {{- link}}",
|
||||
"shared_locally": "Cette note est partagée localement sur {{- link}}",
|
||||
"help_link": "Pour obtenir de l'aide, visitez le <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a>."
|
||||
},
|
||||
"note_types": {
|
||||
@@ -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 @@
|
||||
{}
|
||||
@@ -1,345 +1,353 @@
|
||||
{
|
||||
"about": {
|
||||
"app_version": "Versione dell'app:",
|
||||
"db_version": "Versione DB:",
|
||||
"sync_version": "Versione Sync:",
|
||||
"data_directory": "Cartella dati:",
|
||||
"title": "Informazioni su Trilium Notes",
|
||||
"build_date": "Data della build:",
|
||||
"build_revision": "Revisione della build:",
|
||||
"homepage": "Homepage:"
|
||||
"about": {
|
||||
"app_version": "Versione dell'app:",
|
||||
"db_version": "Versione DB:",
|
||||
"sync_version": "Versione Sync:",
|
||||
"data_directory": "Cartella dati:",
|
||||
"title": "Informazioni su Trilium Notes",
|
||||
"build_date": "Data della build:",
|
||||
"build_revision": "Revisione della build:",
|
||||
"homepage": "Homepage:"
|
||||
},
|
||||
"toast": {
|
||||
"critical-error": {
|
||||
"title": "Errore critico",
|
||||
"message": "Si è verificato un errore critico che impedisce l'avvio dell'applicazione client:\n\n{{message}}\n\nQuesto è probabilmente causato da un errore di script inaspettato. Prova a avviare l'applicazione in modo sicuro e controlla il problema."
|
||||
},
|
||||
"toast": {
|
||||
"critical-error": {
|
||||
"title": "Errore critico",
|
||||
"message": "Si è verificato un errore critico che impedisce l'avvio dell'applicazione client:\n\n{{message}}\n\nQuesto è probabilmente causato da un errore di script inaspettato. Prova a avviare l'applicazione in modo sicuro e controlla il problema."
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Non si è riusciti a caricare uno script personalizzato",
|
||||
"message": "Lo script della nota con ID \"{{id}}\", dal titolo \"{{title}}\" non è stato inizializzato a causa di:\n\n{{message}}"
|
||||
},
|
||||
"widget-error": {
|
||||
"title": "Impossibile inizializzare un widget",
|
||||
"message-custom": "Il widget personalizzato della nota con ID \"{{id}}\", dal titolo \"{{title}}\" non è stato inizializzato a causa di:\n\n{{message}}",
|
||||
"message-unknown": "Un widget sconosciuto non è stato inizializzato a causa di:\n\n{{message}}"
|
||||
}
|
||||
"bundle-error": {
|
||||
"title": "Non si è riusciti a caricare uno script personalizzato",
|
||||
"message": "Lo script della nota con ID \"{{id}}\", dal titolo \"{{title}}\" non è stato inizializzato a causa di:\n\n{{message}}"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Aggiungi un collegamento",
|
||||
"note": "Nota",
|
||||
"search_note": "cerca una nota per nome",
|
||||
"link_title_mirrors": "il titolo del collegamento rispecchia il titolo della nota corrente",
|
||||
"link_title_arbitrary": "il titolo del collegamento può essere modificato arbitrariamente",
|
||||
"link_title": "Titolo del collegamento",
|
||||
"button_add_link": "Aggiungi il collegamento <kbd>invio</kbd>",
|
||||
"help_on_links": "Aiuto sui collegamenti"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"edit_branch_prefix": "Modifica il prefisso del ramo",
|
||||
"help_on_tree_prefix": "Aiuto sui prefissi dell'Albero",
|
||||
"prefix": "Prefisso: ",
|
||||
"save": "Salva",
|
||||
"branch_prefix_saved": "Il prefisso del ramo è stato salvato."
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "Azioni massive",
|
||||
"affected_notes": "Note influenzate",
|
||||
"include_descendants": "Includi i discendenti della nota selezionata",
|
||||
"available_actions": "Azioni disponibili",
|
||||
"chosen_actions": "Azioni scelte",
|
||||
"execute_bulk_actions": "Esegui le azioni massive",
|
||||
"bulk_actions_executed": "Le azioni massive sono state eseguite con successo.",
|
||||
"none_yet": "Ancora nessuna... aggiungi una azione cliccando su una di quelle disponibili sopra.",
|
||||
"labels": "Etichette",
|
||||
"relations": "Relazioni",
|
||||
"notes": "Note",
|
||||
"other": "Altro"
|
||||
},
|
||||
"clone_to": {
|
||||
"clone_notes_to": "Clona note in...",
|
||||
"help_on_links": "Aiuto sui collegamenti",
|
||||
"notes_to_clone": "Note da clonare",
|
||||
"target_parent_note": "Nodo padre obiettivo",
|
||||
"search_for_note_by_its_name": "cerca una nota per nome",
|
||||
"cloned_note_prefix_title": "Le note clonate saranno mostrate nell'albero delle note con il dato prefisso",
|
||||
"prefix_optional": "Prefisso (opzionale)",
|
||||
"clone_to_selected_note": "Clona sotto la nota selezionata <kbd>invio</kbd>",
|
||||
"no_path_to_clone_to": "Nessun percorso per clonare dentro.",
|
||||
"note_cloned": "La nota \"{{clonedTitle}}\" è stata clonata in \"{{targetTitle}}\""
|
||||
},
|
||||
"confirm": {
|
||||
"cancel": "Annulla",
|
||||
"ok": "OK",
|
||||
"confirmation": "Conferma",
|
||||
"are_you_sure_remove_note": "Sei sicuro di voler rimuovere la nota \"{{title}}\" dalla mappa delle relazioni? ",
|
||||
"if_you_dont_check": "Se non lo selezioni, la nota sarà rimossa solamente dalla mappa delle relazioni.",
|
||||
"also_delete_note": "Rimuove anche la nota"
|
||||
},
|
||||
"delete_notes": {
|
||||
"ok": "OK",
|
||||
"close": "Chiudi",
|
||||
"delete_notes_preview": "Anteprima di eliminazione delle note",
|
||||
"delete_all_clones_description": "Elimina anche tutti i cloni (può essere disfatto tramite i cambiamenti recenti)",
|
||||
"erase_notes_description": "L'eliminazione normale (soft) marca le note come eliminate e potranno essere recuperate entro un certo lasso di tempo (dalla finestra dei cambiamenti recenti). Selezionando questa opzione le note si elimineranno immediatamente e non sarà possibile recuperarle.",
|
||||
"erase_notes_warning": "Elimina le note in modo permanente (non potrà essere disfatto), compresi tutti i cloni. Ciò forzerà un nuovo caricamento dell'applicazione.",
|
||||
"cancel": "Annulla",
|
||||
"notes_to_be_deleted": "Le seguenti note saranno eliminate ({{- noteCount}})",
|
||||
"no_note_to_delete": "Nessuna nota sarà eliminata (solo i cloni).",
|
||||
"broken_relations_to_be_deleted": "Le seguenti relazioni saranno interrotte ed eliminate ({{- relationCount}})",
|
||||
"deleted_relation_text": "La nota {{- note}} (da eliminare) è referenziata dalla relazione {{- relation}} originata da {{- source}}."
|
||||
},
|
||||
"info": {
|
||||
"okButton": "OK",
|
||||
"closeButton": "Chiudi"
|
||||
},
|
||||
"export": {
|
||||
"close": "Chiudi",
|
||||
"export_note_title": "Esporta la nota",
|
||||
"export_status": "Stato dell'esportazione",
|
||||
"export": "Esporta",
|
||||
"choose_export_type": "Scegli prima il tipo di esportazione, per favore",
|
||||
"export_in_progress": "Esportazione in corso: {{progressCount}}",
|
||||
"export_finished_successfully": "Esportazione terminata con successo.",
|
||||
"format_pdf": "PDF- allo scopo di stampa o esportazione.",
|
||||
"export_type_subtree": "Questa nota e tutti i suoi discendenti",
|
||||
"format_html": "HTML - raccomandato in quanto mantiene tutti i formati",
|
||||
"format_html_zip": "HTML in archivio ZIP - questo è raccomandato in quanto conserva tutta la formattazione.",
|
||||
"format_markdown": "MArkdown - questo conserva la maggior parte della formattazione."
|
||||
},
|
||||
"password_not_set": {
|
||||
"body1": "Le note protette sono crittografate utilizzando una password utente, ma la password non è stata ancora impostata.",
|
||||
"body2": "Per proteggere le note, fare clic su <a class=\"open-password-options-button\" href=\"javascript:\">qui</a> per aprire la finestra di dialogo Opzioni e impostare la password."
|
||||
},
|
||||
"protected_session_password": {
|
||||
"close_label": "Chiudi"
|
||||
},
|
||||
"abstract_bulk_action": {
|
||||
"remove_this_search_action": "Rimuovi questa azione di ricerca"
|
||||
},
|
||||
"etapi": {
|
||||
"new_token_title": "Nuovo token ETAPI",
|
||||
"new_token_message": "Inserire il nuovo nome del token"
|
||||
},
|
||||
"electron_integration": {
|
||||
"zoom-factor": "Fattore di ingrandimento",
|
||||
"desktop-application": "Applicazione Desktop"
|
||||
},
|
||||
"note_autocomplete": {
|
||||
"search-for": "Cerca \"{{term}}\"",
|
||||
"create-note": "Crea e collega la nota figlia \"{{term}}\"",
|
||||
"insert-external-link": "Inserisci il collegamento esterno a \"{{term}}\"",
|
||||
"clear-text-field": "Pulisci il campo di testo",
|
||||
"show-recent-notes": "Mostra le note recenti",
|
||||
"full-text-search": "Ricerca full text"
|
||||
},
|
||||
"note_tooltip": {
|
||||
"note-has-been-deleted": "La nota è stata eliminata.",
|
||||
"quick-edit": "Modifica veloce"
|
||||
},
|
||||
"geo-map": {
|
||||
"create-child-note-title": "Crea una nota figlia e aggiungila alla mappa",
|
||||
"create-child-note-instruction": "Clicca sulla mappa per creare una nuova nota qui o premi Escape per uscire.",
|
||||
"unable-to-load-map": "Impossibile caricare la mappa."
|
||||
},
|
||||
"geo-map-context": {
|
||||
"open-location": "Apri la posizione",
|
||||
"remove-from-map": "Rimuovi dalla mappa",
|
||||
"add-note": "Aggiungi un marcatore in questa posizione"
|
||||
},
|
||||
"debug": {
|
||||
"debug": "Debug"
|
||||
},
|
||||
"database_anonymization": {
|
||||
"light_anonymization": "Anonimizzazione parziale",
|
||||
"title": "Anonimizzazione del Database",
|
||||
"full_anonymization": "Anonimizzazione completa",
|
||||
"full_anonymization_description": "Questa azione creerà una nuova copia del database e lo anonimizzerà (rimuove tutti i contenuti delle note, lasciando solo la struttura e qualche metadato non sensibile) per condividerlo online allo scopo di debugging, senza paura di far trapelare i tuoi dati personali.",
|
||||
"save_fully_anonymized_database": "Salva il database completamente anonimizzato",
|
||||
"light_anonymization_description": "Questa azione creerà una nuova copia del database e lo anonimizzerà in parzialmente — in particolare, solo il contenuto delle note sarà rimosso, ma i titoli e gli attributi rimarranno. Inoltre, note con script personalizzati JS di frontend/backend e widget personalizzati lasciando rimarranno. Ciò mette a disposizione più contesto per il debug dei problemi.",
|
||||
"choose_anonymization": "Puoi decidere da solo se fornire un database completamente o parzialmente anonimizzato. Anche un database completamente anonimizzato è molto utile, sebbene in alcuni casi i database parzialmente anonimizzati possono accelerare il processo di identificazione dei bug e la loro correzione.",
|
||||
"no_anonymized_database_yet": "Nessun database ancora anonimizzato.",
|
||||
"save_lightly_anonymized_database": "Salva il database parzialmente anonimizzato",
|
||||
"successfully_created_fully_anonymized_database": "Database completamente anonimizzato creato in {{anonymizedFilePath}}",
|
||||
"successfully_created_lightly_anonymized_database": "Database parzialmente anonimizzato creato in {{anonymizedFilePath}}"
|
||||
},
|
||||
"cpu_arch_warning": {
|
||||
"title": "Per favore scarica la versione ARM64",
|
||||
"continue_anyway": "Continua Comunque",
|
||||
"dont_show_again": "Non mostrare più questo avviso",
|
||||
"download_link": "Scarica la Versione Nativa"
|
||||
},
|
||||
"editorfeatures": {
|
||||
"title": "Caratteristiche",
|
||||
"emoji_completion_enabled": "Abilita il completamento automatico delle Emoji",
|
||||
"note_completion_enabled": "Abilita il completamento automatico delle note"
|
||||
},
|
||||
"table_view": {
|
||||
"new-row": "Nuova riga",
|
||||
"new-column": "Nuova colonna",
|
||||
"sort-column-by": "Ordina per \"{{title}}\"",
|
||||
"sort-column-ascending": "Ascendente",
|
||||
"sort-column-descending": "Discendente",
|
||||
"sort-column-clear": "Cancella l'ordinamento",
|
||||
"hide-column": "Nascondi la colonna \"{{title}}\"",
|
||||
"show-hide-columns": "Mostra/nascondi le colonne",
|
||||
"row-insert-above": "Inserisci una riga sopra",
|
||||
"row-insert-below": "Inserisci una riga sotto"
|
||||
},
|
||||
"abstract_search_option": {
|
||||
"remove_this_search_option": "Rimuovi questa opzione di ricerca",
|
||||
"failed_rendering": "Opzione di ricerca di rendering non riuscita: {{dto}} con errore: {{error}} {{stack}}"
|
||||
},
|
||||
"ancestor": {
|
||||
"label": "Antenato"
|
||||
},
|
||||
"add_label": {
|
||||
"add_label": "Aggiungi etichetta",
|
||||
"label_name_placeholder": "nome dell'etichetta",
|
||||
"new_value_placeholder": "nuovo valore",
|
||||
"to_value": "al valore"
|
||||
},
|
||||
"update_label_value": {
|
||||
"to_value": "al valore",
|
||||
"label_name_placeholder": "nome dell'etichetta"
|
||||
},
|
||||
"delete_label": {
|
||||
"delete_label": "Elimina etichetta",
|
||||
"label_name_placeholder": "nome dell'etichetta",
|
||||
"label_name_title": "Sono ammessi i caratteri alfanumerici, il carattere di sottolineato e i due punti."
|
||||
},
|
||||
"tree-context-menu": {
|
||||
"move-to": "Muovi in...",
|
||||
"cut": "Taglia"
|
||||
},
|
||||
"electron_context_menu": {
|
||||
"cut": "Taglia",
|
||||
"copy": "Copia",
|
||||
"paste": "Incolla",
|
||||
"copy-link": "Copia collegamento",
|
||||
"paste-as-plain-text": "Incolla come testo semplice"
|
||||
},
|
||||
"editing": {
|
||||
"editor_type": {
|
||||
"multiline-toolbar": "Mostra la barra degli strumenti su più linee se non entra."
|
||||
}
|
||||
},
|
||||
"edit_button": {
|
||||
"edit_this_note": "Modifica questa nota"
|
||||
},
|
||||
"shortcuts": {
|
||||
"shortcuts": "Scorciatoie"
|
||||
},
|
||||
"shared_switch": {
|
||||
"toggle-on-title": "Condividi la nota",
|
||||
"toggle-off-title": "Non condividere la nota"
|
||||
},
|
||||
"search_string": {
|
||||
"search_prefix": "Cerca:"
|
||||
},
|
||||
"attachment_detail": {
|
||||
"open_help_page": "Apri la pagina di aiuto sugli allegati"
|
||||
},
|
||||
"search_definition": {
|
||||
"ancestor": "antenato",
|
||||
"debug": "debug",
|
||||
"action": "azione",
|
||||
"add_search_option": "Aggiungi un opzione di ricerca:",
|
||||
"search_string": "cerca la stringa",
|
||||
"limit": "limite"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Chiudi"
|
||||
},
|
||||
"board_view": {
|
||||
"insert-below": "Inserisci sotto",
|
||||
"delete-column": "Elimina la colonna",
|
||||
"delete-column-confirmation": "Sei sicuro di vole eliminare questa colonna? Il corrispondente attributo sarà eliminato anche nelle note sotto questa colonna."
|
||||
},
|
||||
"backup": {
|
||||
"enable_weekly_backup": "Abilita le archiviazioni settimanali",
|
||||
"enable_monthly_backup": "Abilita le archiviazioni mensili",
|
||||
"backup_recommendation": "Si raccomanda di mantenere attive le archiviazioni, sebbene ciò possa rendere l'avvio dell'applicazione lento con database grandi e/o dispositivi di archiviazione lenti.",
|
||||
"backup_now": "Archivia adesso",
|
||||
"backup_database_now": "Archivia il database adesso",
|
||||
"existing_backups": "Backup esistenti",
|
||||
"date-and-time": "Data e ora",
|
||||
"path": "Percorso",
|
||||
"database_backed_up_to": "Il database è stato archiviato in {{backupFilePath}}",
|
||||
"enable_daily_backup": "Abilita le archiviazioni giornaliere",
|
||||
"no_backup_yet": "Ancora nessuna archiviazione"
|
||||
},
|
||||
"backend_log": {
|
||||
"refresh": "Aggiorna"
|
||||
},
|
||||
"consistency_checks": {
|
||||
"find_and_fix_button": "Trova e correggi i problemi di coerenza",
|
||||
"finding_and_fixing_message": "In cerca e correzione dei problemi di coerenza...",
|
||||
"issues_fixed_message": "Qualsiasi problema di coerenza che possa essere stato trovato ora è corretto."
|
||||
},
|
||||
"database_integrity_check": {
|
||||
"check_button": "Controllo dell'integrità del database",
|
||||
"checking_integrity": "Controllo dell'integrità del database in corso...",
|
||||
"title": "Controllo di Integrità del database",
|
||||
"description": "Controllerà che il database non sia corrotto a livello SQLite. Può durare un po' di tempo, a seconda della grandezza del DB.",
|
||||
"integrity_check_failed": "Controllo di integrità fallito: {{results}}"
|
||||
},
|
||||
"sync": {
|
||||
"title": "Sincronizza",
|
||||
"force_full_sync_button": "Forza una sincronizzazione completa",
|
||||
"failed": "Sincronizzazione fallita: {{message}}"
|
||||
},
|
||||
"sync_2": {
|
||||
"config_title": "Configurazione per la Sincronizzazione",
|
||||
"proxy_label": "Server Proxy per la sincronizzazione (opzionale)",
|
||||
"test_title": "Test di sincronizzazione",
|
||||
"timeout": "Timeout per la sincronizzazione",
|
||||
"timeout_unit": "millisecondi",
|
||||
"save": "Salva",
|
||||
"help": "Aiuto"
|
||||
},
|
||||
"search_engine": {
|
||||
"save_button": "Salva"
|
||||
},
|
||||
"sql_table_schemas": {
|
||||
"tables": "Tabelle"
|
||||
},
|
||||
"tab_row": {
|
||||
"close_tab": "Chiudi la scheda",
|
||||
"add_new_tab": "Aggiungi una nuova scheda",
|
||||
"close": "Chiudi",
|
||||
"close_other_tabs": "Chiudi le altre schede",
|
||||
"close_right_tabs": "Chiudi le schede a destra",
|
||||
"close_all_tabs": "Chiudi tutte le schede",
|
||||
"reopen_last_tab": "Riapri l'ultima scheda chiusa",
|
||||
"move_tab_to_new_window": "Sposta questa scheda in una nuova finestra",
|
||||
"copy_tab_to_new_window": "Copia questa scheda in una nuova finestra",
|
||||
"new_tab": "Nuova scheda"
|
||||
},
|
||||
"toc": {
|
||||
"table_of_contents": "Sommario"
|
||||
},
|
||||
"table_of_contents": {
|
||||
"title": "Sommario"
|
||||
},
|
||||
"tray": {
|
||||
"title": "Vassoio di Sistema",
|
||||
"enable_tray": "Abilita il vassoio (Trilium necessita di essere riavviato affinché la modifica abbia effetto)"
|
||||
},
|
||||
"heading_style": {
|
||||
"title": "Stile dell'Intestazione",
|
||||
"plain": "Normale",
|
||||
"underline": "Sottolineato",
|
||||
"markdown": "Stile Markdown"
|
||||
},
|
||||
"highlights_list": {
|
||||
"title": "Punti salienti"
|
||||
},
|
||||
"highlights_list_2": {
|
||||
"title": "Punti salienti",
|
||||
"options": "Opzioni"
|
||||
},
|
||||
"quick-search": {
|
||||
"placeholder": "Ricerca rapida",
|
||||
"searching": "Ricerca in corso..."
|
||||
"widget-error": {
|
||||
"title": "Impossibile inizializzare un widget",
|
||||
"message-custom": "Il widget personalizzato della nota con ID \"{{id}}\", dal titolo \"{{title}}\" non è stato inizializzato a causa di:\n\n{{message}}",
|
||||
"message-unknown": "Un widget sconosciuto non è stato inizializzato a causa di:\n\n{{message}}"
|
||||
}
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Aggiungi un collegamento",
|
||||
"note": "Nota",
|
||||
"search_note": "cerca una nota per nome",
|
||||
"link_title_mirrors": "il titolo del collegamento rispecchia il titolo della nota corrente",
|
||||
"link_title_arbitrary": "il titolo del collegamento può essere modificato arbitrariamente",
|
||||
"link_title": "Titolo del collegamento",
|
||||
"button_add_link": "Aggiungi il collegamento <kbd>invio</kbd>",
|
||||
"help_on_links": "Aiuto sui collegamenti"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"edit_branch_prefix": "Modifica il prefisso del ramo",
|
||||
"help_on_tree_prefix": "Aiuto sui prefissi dell'Albero",
|
||||
"prefix": "Prefisso: ",
|
||||
"save": "Salva",
|
||||
"branch_prefix_saved": "Il prefisso del ramo è stato salvato."
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "Azioni massive",
|
||||
"affected_notes": "Note influenzate",
|
||||
"include_descendants": "Includi i discendenti della nota selezionata",
|
||||
"available_actions": "Azioni disponibili",
|
||||
"chosen_actions": "Azioni scelte",
|
||||
"execute_bulk_actions": "Esegui le azioni massive",
|
||||
"bulk_actions_executed": "Le azioni massive sono state eseguite con successo.",
|
||||
"none_yet": "Ancora nessuna... aggiungi una azione cliccando su una di quelle disponibili sopra.",
|
||||
"labels": "Etichette",
|
||||
"relations": "Relazioni",
|
||||
"notes": "Note",
|
||||
"other": "Altro"
|
||||
},
|
||||
"clone_to": {
|
||||
"clone_notes_to": "Clona note in...",
|
||||
"help_on_links": "Aiuto sui collegamenti",
|
||||
"notes_to_clone": "Note da clonare",
|
||||
"target_parent_note": "Nodo padre obiettivo",
|
||||
"search_for_note_by_its_name": "cerca una nota per nome",
|
||||
"cloned_note_prefix_title": "Le note clonate saranno mostrate nell'albero delle note con il dato prefisso",
|
||||
"prefix_optional": "Prefisso (opzionale)",
|
||||
"clone_to_selected_note": "Clona sotto la nota selezionata <kbd>invio</kbd>",
|
||||
"no_path_to_clone_to": "Nessun percorso per clonare dentro.",
|
||||
"note_cloned": "La nota \"{{clonedTitle}}\" è stata clonata in \"{{targetTitle}}\""
|
||||
},
|
||||
"confirm": {
|
||||
"cancel": "Annulla",
|
||||
"ok": "OK",
|
||||
"confirmation": "Conferma",
|
||||
"are_you_sure_remove_note": "Sei sicuro di voler rimuovere la nota \"{{title}}\" dalla mappa delle relazioni? ",
|
||||
"if_you_dont_check": "Se non lo selezioni, la nota sarà rimossa solamente dalla mappa delle relazioni.",
|
||||
"also_delete_note": "Rimuove anche la nota"
|
||||
},
|
||||
"delete_notes": {
|
||||
"ok": "OK",
|
||||
"close": "Chiudi",
|
||||
"delete_notes_preview": "Anteprima di eliminazione delle note",
|
||||
"delete_all_clones_description": "Elimina anche tutti i cloni (può essere disfatto tramite i cambiamenti recenti)",
|
||||
"erase_notes_description": "L'eliminazione normale (soft) marca le note come eliminate e potranno essere recuperate entro un certo lasso di tempo (dalla finestra dei cambiamenti recenti). Selezionando questa opzione le note si elimineranno immediatamente e non sarà possibile recuperarle.",
|
||||
"erase_notes_warning": "Elimina le note in modo permanente (non potrà essere disfatto), compresi tutti i cloni. Ciò forzerà un nuovo caricamento dell'applicazione.",
|
||||
"cancel": "Annulla",
|
||||
"notes_to_be_deleted": "Le seguenti note saranno eliminate ({{- noteCount}})",
|
||||
"no_note_to_delete": "Nessuna nota sarà eliminata (solo i cloni).",
|
||||
"broken_relations_to_be_deleted": "Le seguenti relazioni saranno interrotte ed eliminate ({{- relationCount}})",
|
||||
"deleted_relation_text": "La nota {{- note}} (da eliminare) è referenziata dalla relazione {{- relation}} originata da {{- source}}."
|
||||
},
|
||||
"info": {
|
||||
"okButton": "OK",
|
||||
"closeButton": "Chiudi"
|
||||
},
|
||||
"export": {
|
||||
"close": "Chiudi",
|
||||
"export_note_title": "Esporta la nota",
|
||||
"export_status": "Stato dell'esportazione",
|
||||
"export": "Esporta",
|
||||
"choose_export_type": "Scegli prima il tipo di esportazione, per favore",
|
||||
"export_in_progress": "Esportazione in corso: {{progressCount}}",
|
||||
"export_finished_successfully": "Esportazione terminata con successo.",
|
||||
"format_pdf": "PDF- allo scopo di stampa o esportazione.",
|
||||
"export_type_subtree": "Questa nota e tutti i suoi discendenti",
|
||||
"format_html": "HTML - raccomandato in quanto mantiene tutti i formati",
|
||||
"format_html_zip": "HTML in archivio ZIP - questo è raccomandato in quanto conserva tutta la formattazione.",
|
||||
"format_markdown": "MArkdown - questo conserva la maggior parte della formattazione.",
|
||||
"export_type_single": "Solo questa nota, senza le sottostanti"
|
||||
},
|
||||
"password_not_set": {
|
||||
"body1": "Le note protette sono crittografate utilizzando una password utente, ma la password non è stata ancora impostata.",
|
||||
"body2": "Per proteggere le note, fare clic su <a class=\"open-password-options-button\" href=\"javascript:\">qui</a> per aprire la finestra di dialogo Opzioni e impostare la password."
|
||||
},
|
||||
"protected_session_password": {
|
||||
"close_label": "Chiudi"
|
||||
},
|
||||
"abstract_bulk_action": {
|
||||
"remove_this_search_action": "Rimuovi questa azione di ricerca"
|
||||
},
|
||||
"etapi": {
|
||||
"new_token_title": "Nuovo token ETAPI",
|
||||
"new_token_message": "Inserire il nuovo nome del token"
|
||||
},
|
||||
"electron_integration": {
|
||||
"zoom-factor": "Fattore di ingrandimento",
|
||||
"desktop-application": "Applicazione Desktop"
|
||||
},
|
||||
"note_autocomplete": {
|
||||
"search-for": "Cerca \"{{term}}\"",
|
||||
"create-note": "Crea e collega la nota figlia \"{{term}}\"",
|
||||
"insert-external-link": "Inserisci il collegamento esterno a \"{{term}}\"",
|
||||
"clear-text-field": "Pulisci il campo di testo",
|
||||
"show-recent-notes": "Mostra le note recenti",
|
||||
"full-text-search": "Ricerca full text"
|
||||
},
|
||||
"note_tooltip": {
|
||||
"note-has-been-deleted": "La nota è stata eliminata.",
|
||||
"quick-edit": "Modifica veloce"
|
||||
},
|
||||
"geo-map": {
|
||||
"create-child-note-title": "Crea una nota figlia e aggiungila alla mappa",
|
||||
"create-child-note-instruction": "Clicca sulla mappa per creare una nuova nota qui o premi Escape per uscire.",
|
||||
"unable-to-load-map": "Impossibile caricare la mappa."
|
||||
},
|
||||
"geo-map-context": {
|
||||
"open-location": "Apri la posizione",
|
||||
"remove-from-map": "Rimuovi dalla mappa",
|
||||
"add-note": "Aggiungi un marcatore in questa posizione"
|
||||
},
|
||||
"debug": {
|
||||
"debug": "Debug"
|
||||
},
|
||||
"database_anonymization": {
|
||||
"light_anonymization": "Anonimizzazione parziale",
|
||||
"title": "Anonimizzazione del Database",
|
||||
"full_anonymization": "Anonimizzazione completa",
|
||||
"full_anonymization_description": "Questa azione creerà una nuova copia del database e lo anonimizzerà (rimuove tutti i contenuti delle note, lasciando solo la struttura e qualche metadato non sensibile) per condividerlo online allo scopo di debugging, senza paura di far trapelare i tuoi dati personali.",
|
||||
"save_fully_anonymized_database": "Salva il database completamente anonimizzato",
|
||||
"light_anonymization_description": "Questa azione creerà una nuova copia del database e lo anonimizzerà in parzialmente — in particolare, solo il contenuto delle note sarà rimosso, ma i titoli e gli attributi rimarranno. Inoltre, note con script personalizzati JS di frontend/backend e widget personalizzati lasciando rimarranno. Ciò mette a disposizione più contesto per il debug dei problemi.",
|
||||
"choose_anonymization": "Puoi decidere da solo se fornire un database completamente o parzialmente anonimizzato. Anche un database completamente anonimizzato è molto utile, sebbene in alcuni casi i database parzialmente anonimizzati possono accelerare il processo di identificazione dei bug e la loro correzione.",
|
||||
"no_anonymized_database_yet": "Nessun database ancora anonimizzato.",
|
||||
"save_lightly_anonymized_database": "Salva il database parzialmente anonimizzato",
|
||||
"successfully_created_fully_anonymized_database": "Database completamente anonimizzato creato in {{anonymizedFilePath}}",
|
||||
"successfully_created_lightly_anonymized_database": "Database parzialmente anonimizzato creato in {{anonymizedFilePath}}"
|
||||
},
|
||||
"cpu_arch_warning": {
|
||||
"title": "Per favore scarica la versione ARM64",
|
||||
"continue_anyway": "Continua Comunque",
|
||||
"dont_show_again": "Non mostrare più questo avviso",
|
||||
"download_link": "Scarica la Versione Nativa"
|
||||
},
|
||||
"editorfeatures": {
|
||||
"title": "Caratteristiche",
|
||||
"emoji_completion_enabled": "Abilita il completamento automatico delle Emoji",
|
||||
"note_completion_enabled": "Abilita il completamento automatico delle note"
|
||||
},
|
||||
"table_view": {
|
||||
"new-row": "Nuova riga",
|
||||
"new-column": "Nuova colonna",
|
||||
"sort-column-by": "Ordina per \"{{title}}\"",
|
||||
"sort-column-ascending": "Ascendente",
|
||||
"sort-column-descending": "Discendente",
|
||||
"sort-column-clear": "Cancella l'ordinamento",
|
||||
"hide-column": "Nascondi la colonna \"{{title}}\"",
|
||||
"show-hide-columns": "Mostra/nascondi le colonne",
|
||||
"row-insert-above": "Inserisci una riga sopra",
|
||||
"row-insert-below": "Inserisci una riga sotto"
|
||||
},
|
||||
"abstract_search_option": {
|
||||
"remove_this_search_option": "Rimuovi questa opzione di ricerca",
|
||||
"failed_rendering": "Opzione di ricerca di rendering non riuscita: {{dto}} con errore: {{error}} {{stack}}"
|
||||
},
|
||||
"ancestor": {
|
||||
"label": "Antenato"
|
||||
},
|
||||
"add_label": {
|
||||
"add_label": "Aggiungi etichetta",
|
||||
"label_name_placeholder": "nome dell'etichetta",
|
||||
"new_value_placeholder": "nuovo valore",
|
||||
"to_value": "al valore"
|
||||
},
|
||||
"update_label_value": {
|
||||
"to_value": "al valore",
|
||||
"label_name_placeholder": "nome dell'etichetta"
|
||||
},
|
||||
"delete_label": {
|
||||
"delete_label": "Elimina etichetta",
|
||||
"label_name_placeholder": "nome dell'etichetta",
|
||||
"label_name_title": "Sono ammessi i caratteri alfanumerici, il carattere di sottolineato e i due punti."
|
||||
},
|
||||
"tree-context-menu": {
|
||||
"move-to": "Muovi in...",
|
||||
"cut": "Taglia"
|
||||
},
|
||||
"electron_context_menu": {
|
||||
"cut": "Taglia",
|
||||
"copy": "Copia",
|
||||
"paste": "Incolla",
|
||||
"copy-link": "Copia collegamento",
|
||||
"paste-as-plain-text": "Incolla come testo semplice"
|
||||
},
|
||||
"editing": {
|
||||
"editor_type": {
|
||||
"multiline-toolbar": "Mostra la barra degli strumenti su più linee se non entra."
|
||||
}
|
||||
},
|
||||
"edit_button": {
|
||||
"edit_this_note": "Modifica questa nota"
|
||||
},
|
||||
"shortcuts": {
|
||||
"shortcuts": "Scorciatoie"
|
||||
},
|
||||
"shared_switch": {
|
||||
"toggle-on-title": "Condividi la nota",
|
||||
"toggle-off-title": "Non condividere la nota"
|
||||
},
|
||||
"search_string": {
|
||||
"search_prefix": "Cerca:"
|
||||
},
|
||||
"attachment_detail": {
|
||||
"open_help_page": "Apri la pagina di aiuto sugli allegati"
|
||||
},
|
||||
"search_definition": {
|
||||
"ancestor": "antenato",
|
||||
"debug": "debug",
|
||||
"action": "azione",
|
||||
"add_search_option": "Aggiungi un opzione di ricerca:",
|
||||
"search_string": "cerca la stringa",
|
||||
"limit": "limite"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Chiudi"
|
||||
},
|
||||
"board_view": {
|
||||
"insert-below": "Inserisci sotto",
|
||||
"delete-column": "Elimina la colonna",
|
||||
"delete-column-confirmation": "Sei sicuro di vole eliminare questa colonna? Il corrispondente attributo sarà eliminato anche nelle note sotto questa colonna."
|
||||
},
|
||||
"backup": {
|
||||
"enable_weekly_backup": "Abilita le archiviazioni settimanali",
|
||||
"enable_monthly_backup": "Abilita le archiviazioni mensili",
|
||||
"backup_recommendation": "Si raccomanda di mantenere attive le archiviazioni, sebbene ciò possa rendere l'avvio dell'applicazione lento con database grandi e/o dispositivi di archiviazione lenti.",
|
||||
"backup_now": "Archivia adesso",
|
||||
"backup_database_now": "Archivia il database adesso",
|
||||
"existing_backups": "Backup esistenti",
|
||||
"date-and-time": "Data e ora",
|
||||
"path": "Percorso",
|
||||
"database_backed_up_to": "Il database è stato archiviato in {{backupFilePath}}",
|
||||
"enable_daily_backup": "Abilita le archiviazioni giornaliere",
|
||||
"no_backup_yet": "Ancora nessuna archiviazione"
|
||||
},
|
||||
"backend_log": {
|
||||
"refresh": "Aggiorna"
|
||||
},
|
||||
"consistency_checks": {
|
||||
"find_and_fix_button": "Trova e correggi i problemi di coerenza",
|
||||
"finding_and_fixing_message": "In cerca e correzione dei problemi di coerenza...",
|
||||
"issues_fixed_message": "Qualsiasi problema di coerenza che possa essere stato trovato ora è corretto."
|
||||
},
|
||||
"database_integrity_check": {
|
||||
"check_button": "Controllo dell'integrità del database",
|
||||
"checking_integrity": "Controllo dell'integrità del database in corso...",
|
||||
"title": "Controllo di Integrità del database",
|
||||
"description": "Controllerà che il database non sia corrotto a livello SQLite. Può durare un po' di tempo, a seconda della grandezza del DB.",
|
||||
"integrity_check_failed": "Controllo di integrità fallito: {{results}}"
|
||||
},
|
||||
"sync": {
|
||||
"title": "Sincronizza",
|
||||
"force_full_sync_button": "Forza una sincronizzazione completa",
|
||||
"failed": "Sincronizzazione fallita: {{message}}"
|
||||
},
|
||||
"sync_2": {
|
||||
"config_title": "Configurazione per la Sincronizzazione",
|
||||
"proxy_label": "Server Proxy per la sincronizzazione (opzionale)",
|
||||
"test_title": "Test di sincronizzazione",
|
||||
"timeout": "Timeout per la sincronizzazione",
|
||||
"timeout_unit": "millisecondi",
|
||||
"save": "Salva",
|
||||
"help": "Aiuto"
|
||||
},
|
||||
"search_engine": {
|
||||
"save_button": "Salva"
|
||||
},
|
||||
"sql_table_schemas": {
|
||||
"tables": "Tabelle"
|
||||
},
|
||||
"tab_row": {
|
||||
"close_tab": "Chiudi la scheda",
|
||||
"add_new_tab": "Aggiungi una nuova scheda",
|
||||
"close": "Chiudi",
|
||||
"close_other_tabs": "Chiudi le altre schede",
|
||||
"close_right_tabs": "Chiudi le schede a destra",
|
||||
"close_all_tabs": "Chiudi tutte le schede",
|
||||
"reopen_last_tab": "Riapri l'ultima scheda chiusa",
|
||||
"move_tab_to_new_window": "Sposta questa scheda in una nuova finestra",
|
||||
"copy_tab_to_new_window": "Copia questa scheda in una nuova finestra",
|
||||
"new_tab": "Nuova scheda"
|
||||
},
|
||||
"toc": {
|
||||
"table_of_contents": "Sommario"
|
||||
},
|
||||
"table_of_contents": {
|
||||
"title": "Sommario"
|
||||
},
|
||||
"tray": {
|
||||
"title": "Vassoio di Sistema",
|
||||
"enable_tray": "Abilita il vassoio (Trilium necessita di essere riavviato affinché la modifica abbia effetto)"
|
||||
},
|
||||
"heading_style": {
|
||||
"title": "Stile dell'Intestazione",
|
||||
"plain": "Normale",
|
||||
"underline": "Sottolineato",
|
||||
"markdown": "Stile Markdown"
|
||||
},
|
||||
"highlights_list": {
|
||||
"title": "Punti salienti"
|
||||
},
|
||||
"highlights_list_2": {
|
||||
"title": "Punti salienti",
|
||||
"options": "Opzioni"
|
||||
},
|
||||
"quick-search": {
|
||||
"placeholder": "Ricerca rapida",
|
||||
"searching": "Ricerca in corso..."
|
||||
},
|
||||
"help": {
|
||||
"goUpDown": "su/giù nella lista delle note",
|
||||
"collapseExpand": "collassa/espande il nodo",
|
||||
"notSet": "non impostato",
|
||||
"goBackForwards": "indietro/avanti nella cronologia",
|
||||
"showJumpToNoteDialog": "mostra <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">la finestra di dialogo \"Salta alla nota\"<a>"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,11 @@
|
||||
"link_title_arbitrary": "リンクタイトルは自由に変更可能"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"save": "保存"
|
||||
"save": "保存",
|
||||
"edit_branch_prefix": "ブランチ接頭辞の編集",
|
||||
"help_on_tree_prefix": "ツリー接頭辞に関するヘルプ",
|
||||
"prefix": "接頭辞: ",
|
||||
"branch_prefix_saved": "ブランチ接頭辞が保存されました。"
|
||||
},
|
||||
"global_menu": {
|
||||
"menu": "メニュー",
|
||||
@@ -59,7 +63,10 @@
|
||||
"zoom_out": "ズームアウト",
|
||||
"zoom_in": "ズームイン",
|
||||
"advanced": "高度",
|
||||
"toggle-zen-mode": "禅モード"
|
||||
"toggle-zen-mode": "禅モード",
|
||||
"switch_to_mobile_version": "モバイル版に切り替え",
|
||||
"switch_to_desktop_version": "デスクトップ版に切り替え",
|
||||
"configure_launchbar": "ランチャーバーの設定"
|
||||
},
|
||||
"left_pane_toggle": {
|
||||
"show_panel": "パネルを表示",
|
||||
@@ -82,14 +89,17 @@
|
||||
"help_on_links": "ヘルプへのリンク"
|
||||
},
|
||||
"delete_notes": {
|
||||
"delete_all_clones_description": "すべてのクローンも削除する(最近の変更では元に戻すことができる)",
|
||||
"delete_all_clones_description": "すべてのクローンも削除(最近の変更では元に戻すことができる)",
|
||||
"erase_notes_description": "通常の(ソフト)削除では、ノートは削除されたものとしてマークされ、一定期間内に(最近の変更で)削除を取り消すことができます。このオプションをオンにすると、ノートは即座に削除され、削除を取り消すことはできません。",
|
||||
"erase_notes_warning": "すべてのクローンを含め、ノートを完全に消去します(元に戻せません)。これにより、アプリケーションは強制的にリロードされます。",
|
||||
"notes_to_be_deleted": "以下のノートが削除されます ({{notesCount}})",
|
||||
"no_note_to_delete": "ノートは削除されません(クローンのみ)。",
|
||||
"cancel": "キャンセル",
|
||||
"ok": "OK",
|
||||
"close": "閉じる"
|
||||
"close": "閉じる",
|
||||
"delete_notes_preview": "ノートのプレビューを削除",
|
||||
"broken_relations_to_be_deleted": "次のリレーション ({{relationCount}})は壊れているので消去されます",
|
||||
"deleted_relation_text": "削除予定のノート{{- note}}は{{- source}}からリレーション{{- relation}}によって参照されています."
|
||||
},
|
||||
"calendar": {
|
||||
"mon": "月",
|
||||
@@ -141,14 +151,15 @@
|
||||
},
|
||||
"tab_row": {
|
||||
"close_tab": "タブを閉じる",
|
||||
"add_new_tab": "新しいタブ",
|
||||
"add_new_tab": "新しいタブを追加",
|
||||
"close": "閉じる",
|
||||
"close_other_tabs": "他のタブを閉じる",
|
||||
"close_right_tabs": "右側のタブをすべて閉じる",
|
||||
"close_all_tabs": "すべてのタブを閉じる",
|
||||
"reopen_last_tab": "最後に閉じたタブを開く",
|
||||
"move_tab_to_new_window": "このタブを新しいウィンドウに移動する",
|
||||
"copy_tab_to_new_window": "このタブを新しいウィンドウにコピーする"
|
||||
"move_tab_to_new_window": "このタブを新しいウィンドウに移動",
|
||||
"copy_tab_to_new_window": "このタブを新しいウィンドウにコピーする",
|
||||
"new_tab": "新しいタブ"
|
||||
},
|
||||
"tasks": {
|
||||
"due": {
|
||||
@@ -174,7 +185,7 @@
|
||||
"debug": "デバッグ",
|
||||
"debug_description": "デバッグは複雑なクエリのデバッグを支援するために、追加のデバッグ情報をコンソールに出力します",
|
||||
"action": "アクション",
|
||||
"search_button": "検索 <kbd>Enter</kbd>",
|
||||
"search_button": "検索",
|
||||
"search_execute": "検索とアクションの実行",
|
||||
"save_to_note": "ノートに保存",
|
||||
"search_parameters": "検索パラメータ",
|
||||
@@ -198,7 +209,10 @@
|
||||
"confirm": {
|
||||
"confirmation": "確認",
|
||||
"cancel": "キャンセル",
|
||||
"ok": "OK"
|
||||
"ok": "OK",
|
||||
"also_delete_note": "同時にノートを削除",
|
||||
"are_you_sure_remove_note": "本当に\"{{title}}\"をリレーションマップから除きたいですか? ",
|
||||
"if_you_dont_check": "これをチェックしないと、このノートはリレーションマップからのみ除かれます。"
|
||||
},
|
||||
"export": {
|
||||
"export_note_title": "ノートをエクスポート",
|
||||
@@ -265,7 +279,12 @@
|
||||
"showSQLConsole": "SQLコンソールを表示",
|
||||
"other": "その他",
|
||||
"quickSearch": "クイックサーチにフォーカス",
|
||||
"inPageSearch": "ページ内検索"
|
||||
"inPageSearch": "ページ内検索",
|
||||
"showJumpToNoteDialog": "<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">「ジャンプ先」ダイアログ</a>を表示",
|
||||
"moveNoteUpDown": "ノートリストでノートを上/下に移動",
|
||||
"notSet": "未設定",
|
||||
"goUpDown": "ノートのリストで上下する",
|
||||
"editBranchPrefix": "アクティブノートのクローンの <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/tree-concepts.html#prefix\">プレフィックス</a> を編集する"
|
||||
},
|
||||
"import": {
|
||||
"importIntoNote": "ノートにインポート",
|
||||
@@ -340,7 +359,9 @@
|
||||
},
|
||||
"relation_map_buttons": {
|
||||
"zoom_out_title": "ズームアウト",
|
||||
"zoom_in_title": "ズームイン"
|
||||
"zoom_in_title": "ズームイン",
|
||||
"create_child_note_title": "新しい子ノートを作成し、関連マップに追加",
|
||||
"reset_pan_zoom_title": "パンとズームを初期座標と倍率にリセット"
|
||||
},
|
||||
"tree-context-menu": {
|
||||
"advanced": "高度",
|
||||
@@ -392,19 +413,38 @@
|
||||
"open_note_custom": "プログラムからノートを開く",
|
||||
"import_files": "ファイルをインポート",
|
||||
"export_note": "ノートをエクスポート",
|
||||
"delete_note": "ノートを削除する",
|
||||
"delete_note": "ノートを削除",
|
||||
"print_note": "ノートを印刷",
|
||||
"print_pdf": "PDFとしてエクスポート..."
|
||||
"print_pdf": "PDFとしてエクスポート...",
|
||||
"convert_into_attachment": "添付ファイルに変換",
|
||||
"save_revision": "変更履歴を保存",
|
||||
"convert_into_attachment_failed": "ノート '{{title}}' の変換に失敗しました。",
|
||||
"convert_into_attachment_successful": "ノート '{{title}}' は添付ファイルに変換されました。",
|
||||
"convert_into_attachment_prompt": "本当にノート '{{title}}' を親ノートの添付ファイルに変換しますか?",
|
||||
"note_attachments": "ノートの添付ファイル"
|
||||
},
|
||||
"command_palette": {
|
||||
"export_note_title": "ノートをエクスポート",
|
||||
"search_subtree_title": "サブツリー内を検索"
|
||||
"search_subtree_title": "サブツリー内を検索",
|
||||
"tree-action-name": "ツリー: {{name}}",
|
||||
"export_note_description": "現在のノートをエクスポート",
|
||||
"search_notes_title": "ノートを検索",
|
||||
"search_notes_description": "高度な検索を開く",
|
||||
"search_subtree_description": "現在のサブツリー内を検索",
|
||||
"search_history_title": "検索履歴を表示",
|
||||
"search_history_description": "過去の検索結果を見る"
|
||||
},
|
||||
"delete_note": {
|
||||
"delete_note": "ノートを削除する"
|
||||
"delete_note": "ノートを削除"
|
||||
},
|
||||
"board_view": {
|
||||
"delete-note": "ノートを削除する"
|
||||
"delete-note": "ノートを削除",
|
||||
"insert-above": "上に挿入",
|
||||
"insert-below": "下に挿入",
|
||||
"delete-column": "列を削除",
|
||||
"delete-column-confirmation": "本当にこの列を削除しますか?対応する属性は、この列の下のノートでも削除されます。",
|
||||
"new-item": "新しいアイテム",
|
||||
"add-column": "列を追加"
|
||||
},
|
||||
"code_buttons": {
|
||||
"execute_button_title": "スクリプトを実行",
|
||||
@@ -482,7 +522,8 @@
|
||||
"file_size": "ファイルサイズ",
|
||||
"download": "ダウンロード",
|
||||
"open": "開く",
|
||||
"title": "ファイル"
|
||||
"title": "ファイル",
|
||||
"upload_new_revision": "編集履歴をアップロード"
|
||||
},
|
||||
"note_info_widget": {
|
||||
"note_id": "ノート ID",
|
||||
@@ -492,21 +533,54 @@
|
||||
"note_size": "ノートサイズ",
|
||||
"calculate": "計算",
|
||||
"subtree_size": "(サブツリーサイズ: {{size}}、ノード数: {{count}})",
|
||||
"title": "ノート情報"
|
||||
"title": "ノート情報",
|
||||
"note_size_info": "ノートのサイズは、このノートに必要なストレージの概算を示します。これは、ノートの内容とそのノートの編集履歴の内容を考慮したものです。"
|
||||
},
|
||||
"image_properties": {
|
||||
"file_type": "ファイルタイプ",
|
||||
"file_size": "ファイルサイズ",
|
||||
"download": "ダウンロード",
|
||||
"open": "開く",
|
||||
"title": "画像"
|
||||
"title": "画像",
|
||||
"upload_new_revision": "編集履歴をアップロード"
|
||||
},
|
||||
"revisions": {
|
||||
"download_button": "ダウンロード",
|
||||
"delete_button": "削除"
|
||||
"delete_button": "削除",
|
||||
"note_revisions": "ノートの変更履歴",
|
||||
"delete_all_revisions": "このノートの変更履歴をすべて削除",
|
||||
"delete_all_button": "変更履歴をすべて削除",
|
||||
"help_title": "変更履歴のヘルプ",
|
||||
"revision_last_edited": "この変更は{{date}}に行われました",
|
||||
"confirm_delete_all": "このノートのすべての変更履歴を削除しますか?",
|
||||
"no_revisions": "このノートに変更履歴はまだありません...",
|
||||
"restore_button": "復元",
|
||||
"confirm_restore": "この変更を復元しますか?現在のノートのタイトルとコンテンツはこの変更で上書きされます。",
|
||||
"confirm_delete": "この変更履歴を削除しますか?",
|
||||
"revisions_deleted": "ノートの変更履歴は削除されました。",
|
||||
"revision_restored": "ノートの変更が復元されました。",
|
||||
"revision_deleted": "ノートの変更履歴は削除されました。",
|
||||
"settings": "ノートの変更履歴の設定",
|
||||
"file_size": "ファイルサイズ:",
|
||||
"preview": "プレビュー:",
|
||||
"preview_not_available": "このノートタイプではプレビューは利用できません。"
|
||||
},
|
||||
"attachments_actions": {
|
||||
"download": "ダウンロード"
|
||||
"download": "ダウンロード",
|
||||
"open_externally": "外部で開く",
|
||||
"open_externally_title": "ファイルを外部アプリケーションで開き、変更を監視します。その後、変更されたバージョンをTriliumにアップロードできるようになります。",
|
||||
"open_custom": "プログラムから開く",
|
||||
"open_custom_title": "ファイルを外部アプリケーションで開き、変更を監視します。その後、変更されたバージョンをTriliumにアップロードできるようになります。",
|
||||
"rename_attachment": "添付ファイルの名前を変更",
|
||||
"copy_link_to_clipboard": "リンクをクリップボードにコピー",
|
||||
"convert_attachment_into_note": "添付ファイルをノートに変換",
|
||||
"delete_attachment": "添付ファイルを削除",
|
||||
"open_externally_detail_page": "添付ファイルを外部で開くは、詳細ページからのみ可能です。まず添付ファイル詳細をクリックしてから、再度操作を行ってください。",
|
||||
"open_custom_client_only": "添付ファイルをプログラムから開くは、デスクトップクライアントからのみ可能です。",
|
||||
"delete_confirm": "本当に添付ファイル '{{title}}' を削除しますか?",
|
||||
"delete_success": "添付ファイル '{{title}}' は削除されました。",
|
||||
"enter_new_name": "新しい添付ファイルの名前を入力してください",
|
||||
"upload_new_revision": "編集履歴をアップロード"
|
||||
},
|
||||
"etapi": {
|
||||
"created": "作成日時",
|
||||
@@ -578,21 +652,48 @@
|
||||
"parent_count": "クローンの数",
|
||||
"random": "ランダムな順番",
|
||||
"asc": "昇順(デフォルト)",
|
||||
"desc": "降順"
|
||||
"desc": "降順",
|
||||
"content_and_attachments_size": "添付ファイルを含むノート内容のサイズ",
|
||||
"content_and_attachments_and_revisions_size": "添付ファイルと編集履歴を含むノート内容のサイズ",
|
||||
"revision_count": "編集履歴の回数",
|
||||
"owned_label_count": "ラベルの数",
|
||||
"owned_relation_count": "関係の数"
|
||||
},
|
||||
"table_view": {
|
||||
"sort-column-descending": "降順",
|
||||
"sort-column-ascending": "昇順"
|
||||
"sort-column-ascending": "昇順",
|
||||
"new-row": "新しい行",
|
||||
"new-column": "新しい列",
|
||||
"sort-column-by": "\"{{title}}\" で並べ替え",
|
||||
"sort-column-clear": "並べ替えをクリア",
|
||||
"hide-column": "列 \"{{title}}\" を隠す",
|
||||
"show-hide-columns": "列を表示/非表示",
|
||||
"row-insert-above": "上に行を挿入",
|
||||
"row-insert-below": "下に行を挿入",
|
||||
"row-insert-child": "子ノートを挿入",
|
||||
"add-column-to-the-left": "列を左に追加",
|
||||
"add-column-to-the-right": "列を右に追加",
|
||||
"edit-column": "列を編集",
|
||||
"delete_column_confirmation": "この列を本当に削除してもよろしいですか?対応する属性はすべてのノートから削除されます。",
|
||||
"delete-column": "列を削除",
|
||||
"new-column-label": "ラベル",
|
||||
"new-column-relation": "関係"
|
||||
},
|
||||
"search_script": {
|
||||
"title": "検索スクリプト:",
|
||||
"placeholder": "ノート名で検索"
|
||||
"placeholder": "ノート名で検索",
|
||||
"description1": "検索スクリプトは、スクリプトを実行することによって検索結果を定義することができます。標準の検索では不十分な場合に、最大限の柔軟性を提供します。",
|
||||
"description2": "検索スクリプトはノートタイプが \"code\" かつ \"JavaScript backend\" でなければならない。スクリプトは、 noteIds または note の配列を返す必要があります。",
|
||||
"example_title": "例は以下です:",
|
||||
"example_code": "// 1. 標準検索によるプレフィルタリング\nconst candidateNotes = api.searchForNotes(\"#journal\"); \n\n// 2. カスタム検索条件の適用\nconst matchedNotes = candidateNotes\n .filter(note => note.title.match(/[0-9]{1,2}\\. ?[0-9]{1,2}\\. ?[0-9]{4}/));\n\nreturn matchedNotes;",
|
||||
"note": "検索スクリプトと文字列検索は、互いに組み合わせることはできません。"
|
||||
},
|
||||
"include_note": {
|
||||
"placeholder_search": "ノート名で検索",
|
||||
"dialog_title": "埋め込みノート",
|
||||
"box_size_prompt": "埋め込みノート枠のサイズ:",
|
||||
"button_include": "埋め込みノート"
|
||||
"button_include": "埋め込みノート",
|
||||
"label_note": "ノート"
|
||||
},
|
||||
"ancestor": {
|
||||
"placeholder": "ノート名で検索"
|
||||
@@ -634,7 +735,12 @@
|
||||
"system-default": "システムのデフォルト"
|
||||
},
|
||||
"max_content_width": {
|
||||
"reload_button": "フロントエンドをリロード"
|
||||
"reload_button": "フロントエンドをリロード",
|
||||
"title": "コンテンツ幅",
|
||||
"default_description": "Triliumは、ワイドスクリーンで最大化された画面での可読性を向上させるために、デフォルトでコンテンツの最大幅を制限しています。",
|
||||
"max_width_label": "最大コンテンツ幅",
|
||||
"max_width_unit": "ピクセル",
|
||||
"apply_changes_description": "コンテンツ幅の変更を適用するには、クリックしてください"
|
||||
},
|
||||
"theme": {
|
||||
"title": "アプリのテーマ",
|
||||
@@ -713,7 +819,8 @@
|
||||
"shortcut_info": "オプション -> ショートカット('右ペイン切り替え')で、右ペイン(目次を含む)を素早く切り替えるキーボードショートカットを設定できます。"
|
||||
},
|
||||
"toc": {
|
||||
"table_of_contents": "目次"
|
||||
"table_of_contents": "目次",
|
||||
"options": "オプション"
|
||||
},
|
||||
"text_auto_read_only_size": {
|
||||
"title": "自動読み取り専用のサイズ",
|
||||
@@ -724,11 +831,12 @@
|
||||
"code_auto_read_only_size": {
|
||||
"title": "自動読み取り専用のサイズ",
|
||||
"description": "自動読み取り専用のノートサイズは、ノートが読み取り専用モード(パフォーマンス上の理由)で表示されるようになるサイズです。",
|
||||
"unit": "文字"
|
||||
"unit": "文字",
|
||||
"label": "自動読み取り専用のサイズ(コードノート)"
|
||||
},
|
||||
"custom_date_time_format": {
|
||||
"title": "日付/時刻フォーマットのカスタム",
|
||||
"description": "<kbd></kbd>またはツールバーから挿入される日付と時刻のフォーマットをカスタマイズする。 利用可能なトークンについては <a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">Day.js ドキュメント</a> を参照してください。",
|
||||
"description": "<shortcut />またはツールバーから挿入される日付と時刻のフォーマットをカスタマイズする。 利用可能なトークンについては <doc>Day.js ドキュメント</doc> を参照してください。",
|
||||
"format_string": "文字列形式:",
|
||||
"formatted_time": "日付/時刻形式:"
|
||||
},
|
||||
@@ -804,7 +912,8 @@
|
||||
"close_label": "閉じる"
|
||||
},
|
||||
"modal": {
|
||||
"close": "閉じる"
|
||||
"close": "閉じる",
|
||||
"help_title": "この画面に関する詳細情報を表示"
|
||||
},
|
||||
"bookmark_switch": {
|
||||
"bookmark": "ブックマーク",
|
||||
@@ -812,10 +921,39 @@
|
||||
"remove_bookmark": "ブックマークを削除"
|
||||
},
|
||||
"attribute_detail": {
|
||||
"delete": "削除"
|
||||
"delete": "削除",
|
||||
"close_button_title": "変更を破棄して閉じる",
|
||||
"attr_is_owned_by": "属性の所有者",
|
||||
"attr_name_title": "属性の名前は英数字、コロン、アンダーバーのみで構成します",
|
||||
"name": "名前",
|
||||
"value": "値",
|
||||
"promoted_alias": "エイリアス",
|
||||
"multiplicity_title": "多重性とは、同じ名前を持つ属性をいくつ作成できるかを定義するもので、1つまたはそれ以上です。",
|
||||
"multiplicity": "多重性",
|
||||
"single_value": "単一の値",
|
||||
"multi_value": "複数の値",
|
||||
"label_type_title": "ラベルのタイプは、Triliumがラベルの値を入力するのに適したインターフェースを選択するのに役立ちます。",
|
||||
"label_type": "タイプ",
|
||||
"text": "テキスト",
|
||||
"number": "数値",
|
||||
"boolean": "真偽値",
|
||||
"date": "日付",
|
||||
"date_time": "日付と時間",
|
||||
"time": "時間",
|
||||
"url": "URL",
|
||||
"save_and_close": "保存して閉じる <kbd>Ctrl+Enter</kbd>",
|
||||
"precision_title": "値設定インターフェースで、小数点以下何桁まで利用可能にするか。",
|
||||
"precision": "精度",
|
||||
"digits": "桁",
|
||||
"inheritable_title": "継承属性は、このツリー配下のすべての子孫に継承されます。",
|
||||
"inheritable": "継承",
|
||||
"related_notes_title": "このラベルが付いた他のノート"
|
||||
},
|
||||
"link_context_menu": {
|
||||
"open_note_in_popup": "クイックエディット"
|
||||
"open_note_in_popup": "クイックエディット",
|
||||
"open_note_in_new_tab": "新しいタブでノートを開く",
|
||||
"open_note_in_new_split": "新しく分割してノートを開く",
|
||||
"open_note_in_new_window": "新しいウィンドウでノートを開く"
|
||||
},
|
||||
"note_tooltip": {
|
||||
"quick-edit": "クイックエディット"
|
||||
@@ -839,8 +977,6 @@
|
||||
},
|
||||
"open-help-page": "ヘルプページを開く",
|
||||
"shared_info": {
|
||||
"shared_publicly": "このノートは一般公開されています",
|
||||
"shared_locally": "このノートはローカルで共有されています",
|
||||
"help_link": "ヘルプについては、<a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a>をご覧ください。"
|
||||
},
|
||||
"highlights_list_2": {
|
||||
@@ -861,6 +997,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:"
|
||||
}
|
||||
}
|
||||
139
apps/client/src/translations/pl/translation.json
Normal file
139
apps/client/src/translations/pl/translation.json
Normal file
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"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."
|
||||
},
|
||||
"widget-error": {
|
||||
"title": "Nie udało się zainicjować widżetu",
|
||||
"message-custom": "Niestandardowy widżet z notatki o identyfikatorze \"{{id}}\", i tytule \"{{title}}\" nie mógł zostać zainicjowany z powodu:\n\n{{message}}",
|
||||
"message-unknown": "Nieznany widżet nie mógł być zainicjowany z powodu:\n\n{{message}}"
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Nie udało się załadować niestandardowego skryptu",
|
||||
"message": "Skrypt z notatki o identyfikatorze \"{{id}}\", tytule \"{{title}}: nie został uruchomiony z powodu:\n\n{{message}}"
|
||||
}
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Dodaj link",
|
||||
"note": "Notatka",
|
||||
"search_note": "Wyszukaj notatkę po nazwie",
|
||||
"link_title_arbitrary": "Tytuł linku można dowolnie zmieniać",
|
||||
"link_title": "Tytuł linku",
|
||||
"button_add_link": "Dodaj link"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"save": "Zapisz",
|
||||
"edit_branch_prefix": "Edytuj prefiks gałęzi",
|
||||
"prefix": "Prefiks: ",
|
||||
"branch_prefix_saved": "Zapisano prefiks gałęzi."
|
||||
},
|
||||
"bulk_actions": {
|
||||
"labels": "Etykiety",
|
||||
"notes": "Notatki",
|
||||
"other": "Inne",
|
||||
"relations": "Powiązania",
|
||||
"bulk_actions": "Działania zbiorcze",
|
||||
"include_descendants": "Uwzględnia rozwinięcia wybranych notatek",
|
||||
"available_actions": "Dostępne działania",
|
||||
"chosen_actions": "Wybrane działania",
|
||||
"execute_bulk_actions": "Wykonaj zbiór działań",
|
||||
"bulk_actions_executed": "Zbiór działań został wykonany prawidłowo.",
|
||||
"none_yet": "Brak zaznaczonych działań... dodaj działanie poprzez kliknięcie jednej z dostępnych opcji powyżej."
|
||||
},
|
||||
"confirm": {
|
||||
"ok": "OK",
|
||||
"cancel": "Anuluj",
|
||||
"confirmation": "Potwierdzenie",
|
||||
"are_you_sure_remove_note": "Czy napewno chcesz usunąć notatkę \"{{title}}\" z mapy powiązań? ",
|
||||
"if_you_dont_check": "Jeśli nie zaznaczysz tej opcji, notatka zostanie usunięta jedynie z mapy powiązań.",
|
||||
"also_delete_note": "Usuń dodatkowo notatkę"
|
||||
},
|
||||
"delete_notes": {
|
||||
"cancel": "Anuluj",
|
||||
"close": "Zamknij",
|
||||
"delete_notes_preview": "Usuń podgląd notatek",
|
||||
"delete_all_clones_description": "Usuń również wszystkie sklonowania (działanie może zostać cofnięte w ostatnich zmianach)",
|
||||
"erase_notes_description": "Normalne (miękkie) usuwanie zaznacza jedynie notatki jako usunięte i można je przywrócić (w oknie dialogowym ostatnich zmian) przez wyznaczony okres czasu. Zaznaczenie tej opcji spowoduje natychmiastowe usunięcie notatek, bez możliwości ich przywrócenia.",
|
||||
"erase_notes_warning": "Usuń notatki permanentnie (bez opcji ich przywrócenia), włączając wszystkie kopie. Działanie to wymaga ponownego uruchomienia aplikacji.",
|
||||
"notes_to_be_deleted": "Następujące notatki zostaną usunięte ({{notesCount}})",
|
||||
"no_note_to_delete": "Żadne notatki nie zostaną usunięte (jedynie kopie).",
|
||||
"broken_relations_to_be_deleted": "Następujące powiązania zostaną uszkodzone i usunięte ({{ relationCount}})",
|
||||
"ok": "OK",
|
||||
"deleted_relation_text": "Notatka {{- note}} (do usunięcia) jest powiązana przez relację {{- relation}} pochodzącą z {{- source}}."
|
||||
},
|
||||
"export": {
|
||||
"close": "Zamknij",
|
||||
"export_note_title": "Eksportuj notatkę",
|
||||
"export_type_subtree": "Ta notatka oraz wszystkie podrzędne",
|
||||
"format_html": "HTML - rekomendowany jako zachowujący całość formatowania",
|
||||
"format_html_zip": "HTML w archiwum ZIP - rekomendowany jako zachowujący całość formatowania.",
|
||||
"format_markdown": "Markdown - zachowuje większość formatowania.",
|
||||
"format_opml": "OPML - format wymiany danych dla outlinerów zawierający tylko tekst. Formatowanie, obrazy i pliki nie są uwzględnione.",
|
||||
"opml_version_1": "OPML v1.0 - tylko zwykły tekst",
|
||||
"opml_version_2": "OPML v2.0 - umożliwia również HTML",
|
||||
"export_type_single": "Tylko ta notatka, bez elementów podrzędnych",
|
||||
"export": "Eksportuj",
|
||||
"choose_export_type": "Wybierz najpierw rodzaj pliku do eksportu",
|
||||
"export_status": "Status eksportu",
|
||||
"export_in_progress": "Postęp eksportowania: {{progressCount}}",
|
||||
"export_finished_successfully": "Eksportowanie zakończone.",
|
||||
"format_pdf": "PDF - w celu drukowania lub udostępniania."
|
||||
},
|
||||
"clone_to": {
|
||||
"clone_notes_to": "Sklonuj notatki do...",
|
||||
"notes_to_clone": "Notatki do sklonowania",
|
||||
"search_for_note_by_its_name": "Wyszukaj notatkę po jej nazwie",
|
||||
"cloned_note_prefix_title": "Sklonowana notatka zostanie wyświetlona w drzewie notatki z podanym prefiksem",
|
||||
"prefix_optional": "Prefiks (opcjonalne)",
|
||||
"clone_to_selected_note": "Sklonuj do wybranej notatki",
|
||||
"no_path_to_clone_to": "Brak ścieżki do sklonowania.",
|
||||
"note_cloned": "Notatka \"{{clonedTitle}}\" została sklonowana do \"{{targetTitle}}\""
|
||||
},
|
||||
"help": {
|
||||
"title": "Ściągawka",
|
||||
"noteNavigation": "Nawigacja po notatkach",
|
||||
"goUpDown": "przewijanie w górę/w dół w liście notatek",
|
||||
"collapseExpand": "zwiń/rozwiń zbiór",
|
||||
"notSet": "niezdefiniowany",
|
||||
"goBackForwards": "przewijaj do tyłu/do przodu w historii",
|
||||
"showJumpToNoteDialog": "pokaż <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">\"przejdź do dialogu</a>",
|
||||
"scrollToActiveNote": "przewiń do aktywnej notatki",
|
||||
"jumpToParentNote": "przejdź do głównej notatki",
|
||||
"collapseWholeTree": "zwiń całe drzewko notatki",
|
||||
"collapseSubTree": "zwiń gałąź notatki",
|
||||
"tabShortcuts": "Skóry kart",
|
||||
"newTabNoteLink": "link notatki otwiera notatkę w nowej karcie",
|
||||
"newTabWithActivationNoteLink": "link notatki otwiera i aktywuje notatkę w nowej karcie",
|
||||
"onlyInDesktop": "Tylko na komputerze stacjonarnym (wersja Electron)",
|
||||
"openEmptyTab": "Otwórz pustą kartę",
|
||||
"closeActiveTab": "zamknij aktywną kartę",
|
||||
"activateNextTab": "aktywuj następną kartę",
|
||||
"activatePreviousTab": "aktywuj poprzednią kartę",
|
||||
"creatingNotes": "Tworzenie notatek",
|
||||
"createNoteAfter": "Utwórz nową notatkę obok obecnie aktywnej",
|
||||
"createNoteInto": "Utwórz nową podnotatkę w obecnie otwartej",
|
||||
"editBranchPrefix": "edytuj <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/tree-concepts.html#prefix\">prefiks</a> aktywnej kopii notatki",
|
||||
"movingCloningNotes": "Przenoszenie / kopiowanie notatek",
|
||||
"moveNoteUpDown": "Przenieś notatkę w górę/w dół na liście notatek",
|
||||
"moveNoteUpHierarchy": "Przenieś notatkę w górę w hierarchii",
|
||||
"multiSelectNote": "Zaznacz wiele notatek powyżej/poniżej",
|
||||
"selectAllNotes": "Wybierz wszystkie notatki na obecnym poziomie",
|
||||
"selectNote": "Wybierz notatkę",
|
||||
"copyNotes": "skopiuj obecną notatkę (lub obecną sekcję) do schowka (zastosowanie dla<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/cloning-notes.html#cloning-notes\">klonowania</a>)",
|
||||
"cutNotes": "przytnij obecną notatkę (lub obecną sekcję) do schowka (zastosowanie dla przenoszenia notatek)",
|
||||
"pasteNotes": "wklej notatkę jako podnotatka w obecnej notatce (rozumiane jako przenieś lub skopiuj, w zależności czy notatka była skopiowana czy wycięta)",
|
||||
"deleteNotes": "usuń notatkę / gałąź",
|
||||
"editingNotes": "Edytowanie notatek"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,7 +40,7 @@
|
||||
"add_relation": {
|
||||
"add_relation": "Adaugă relație",
|
||||
"allowed_characters": "Sunt permise doar caractere alfanumerice, underline și două puncte.",
|
||||
"create_relation_on_all_matched_notes": "Crează relația pentru toate notițele găsite",
|
||||
"create_relation_on_all_matched_notes": "Creează relația pentru toate notițele găsite.",
|
||||
"relation_name": "denumirea relației",
|
||||
"target_note": "notița destinație",
|
||||
"to": "către"
|
||||
@@ -76,9 +76,9 @@
|
||||
"attachment_erasure_timeout": {
|
||||
"attachment_auto_deletion_description": "Atașamentele se șterg automat (permanent) dacă nu sunt referențiate de către notița lor părinte după un timp prestabilit de timp.",
|
||||
"attachment_erasure_timeout": "Perioadă de ștergere a atașamentelor",
|
||||
"erase_attachments_after": "Erase unused attachments after:",
|
||||
"erase_attachments_after": "Șterge atașamentele neutilizate după:",
|
||||
"erase_unused_attachments_now": "Elimină atașamentele șterse acum",
|
||||
"manual_erasing_description": "Șterge acum toate atașamentele nefolosite din notițe",
|
||||
"manual_erasing_description": "Puteți șterge atașamentele nefolosite manual (fără a lua în considerare timpul de mai sus):",
|
||||
"unused_attachments_erased": "Atașamentele nefolosite au fost șterse."
|
||||
},
|
||||
"attachment_list": {
|
||||
@@ -141,7 +141,7 @@
|
||||
"hide_promoted_attributes": "Ascunde lista atributelor promovate pentru această notiță",
|
||||
"hide_relations": "lista denumirilor relațiilor ce trebuie ascunse, delimitate prin virgulă. Toate celelalte vor fi afișate.",
|
||||
"icon_class": "valoarea acestei etichete este adăugată ca o clasă CSS la iconița notiței din ierarhia notițelor, fapt ce poate ajuta la identificarea vizuală mai rapidă a notițelor. Un exemplu ar fi „bx bx-home” pentru iconițe preluate din boxicons. Poate fi folosită în notițe de tip șablon.",
|
||||
"inbox": "locația implicită în care vor apărea noile notițe atunci când se crează o noitiță utilizând butonul „Crează notiță” din bara laterală, notițele vor fi create în interiorul notiței cu această etichetă.",
|
||||
"inbox": "locația implicită în care vor apărea noile notițe atunci când se crează o noitiță utilizând butonul „Crează notiță” din bara laterală, notițele vor fi create în interiorul notiței marcată cu eticheta <code>#inbox</code>.",
|
||||
"inherit": "atributele acestei notițe vor fi moștenite chiar dacă nu există o relație părinte-copil între notițe. A se vedea relația de tip șablon pentru un concept similar. De asemenea, a se vedea moștenirea atributelor în documentație.",
|
||||
"inheritable": "Moștenibilă",
|
||||
"inheritable_title": "Atributele moștenibile vor fi moștenite de către toți descendenții acestei notițe.",
|
||||
@@ -177,7 +177,7 @@
|
||||
"render_note": "relație ce definește notița (de tip notiță de cod HTML sau script) ce trebuie randată pentru notițele de tip „Randare notiță HTML”",
|
||||
"run": "definește evenimentele la care să ruleze scriptul. Valori acceptate:\n<ul>\n<li>frontendStartup - când pornește interfața Trilium (sau este reîncărcată), dar nu pe mobil.</li>\n<li>mobileStartup - când pornește interfața Trilium (sau este reîncărcată), doar pe mobil.</li>\n<li>backendStartup - când pornește serverul Trilium</li>\n<li>hourly - o dată pe oră. Se poate utiliza adițional eticheta <code>runAtHour</code> pentru a specifica ora.</li>\n<li>daily - o dată pe zi</li>\n</ul>",
|
||||
"run_at_hour": "La ce oră ar trebui să ruleze. Trebuie folosit împreună cu <code>#run=hourly</code>. Poate fi definit de mai multe ori pentru a rula de mai multe ori în cadrul aceleași zile.",
|
||||
"run_on_attribute_change": "se execută atunci când atributele unei notițe care definește această relație se schimbă. Se apelează și atunci când un atribut este șters",
|
||||
"run_on_attribute_change": " se execută atunci când atributele unei notițe care definește această relație se schimbă. Se apelează și atunci când un atribut este șters",
|
||||
"run_on_attribute_creation": "se execută atunci când un nou atribut este creat pentru notița care definește această relație",
|
||||
"run_on_branch_change": "se execută atunci când o ramură este actualizată.",
|
||||
"run_on_branch_creation": "se execută când o ramură este creată. O ramură este o legătură dintre o notiță părinte și o notiță copil și este creată, spre exemplu, la clonarea sau mutarea unei notițe.",
|
||||
@@ -198,7 +198,7 @@
|
||||
"share_disallow_robot_indexing": "împiedică indexarea conținutului de către roboți utilizând antetul <code>X-Robots-Tag: noindex</code>",
|
||||
"share_external_link": "notița va funcționa drept o legătură către un site web extern în ierarhia de partajare",
|
||||
"share_favicon": "Notiță ce conține pictograma favicon pentru a fi setată în paginile partajate. De obicei se poate seta în rădăcina ierarhiei de partajare și se poate face moștenibilă. Notița ce conține favicon-ul trebuie să fie și ea în ierarhia de partajare. Considerați și utilizarea „share_hidden_from_tree”.",
|
||||
"share_hidden_from_tree": "notița este ascunsă din arborele de navigație din stânga, dar încă este accesibilă prin intermediul unui URL.",
|
||||
"share_hidden_from_tree": "notița este ascunsă din arborele de navigație din stânga, dar încă este accesibilă prin intermediul unui URL",
|
||||
"share_index": "notițele cu această etichetă vor afișa lista tuturor rădăcilor notițelor partajate",
|
||||
"share_js": "Notiță JavaScript ce va fi injectată în pagina de partajare. Notița respectivă trebuie să fie și ea în ierarhia de partajare. Considerați utilizarea 'share_hidden_from_tree'.",
|
||||
"share_omit_default_css": "CSS-ul implicit pentru pagina de partajare va fi omis. Se poate folosi atunci când se fac schimbări majore de stil la pagină.",
|
||||
@@ -214,7 +214,7 @@
|
||||
"target_note_title": "Relația este o conexiune numită dintre o notiță sursă și o notiță țintă.",
|
||||
"template": "Șablon",
|
||||
"text": "Text",
|
||||
"title_template": "titlul implicit al notițelor create în interiorul acestei notițe. Valoarea este evaluată ca un șir de caractere JavaScript\n și poate fi astfel îmbogățită cu un conținut dinamic prin intermediul variabilelow <code>now</code> și <code>parentNote</code>. Exemple:\n \n <ul>\n <li><code>Lucrările lui ${parentNote.getLabelValue('autor')}</code></li>\n <li><code>Jurnal pentru ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n A se vedea <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">wiki-ul pentru detalii</a>, documentația API pentru <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> și <a href=\"https://day.js.org/docs/en/display/format\">now</a> pentru mai multe informații",
|
||||
"title_template": "titlul implicit al notițelor create în interiorul acestei notițe. Valoarea este evaluată ca un șir de caractere JavaScript\n și poate fi astfel îmbogățită cu un conținut dinamic prin intermediul variabilelor <code>now</code> și <code>parentNote</code>. Exemple:\n \n <ul>\n <li><code>Lucrările lui ${parentNote.getLabelValue('autor')}</code></li>\n <li><code>Jurnal pentru ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n A se vedea <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">wiki-ul pentru detalii</a>, documentația API pentru <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> și <a href=\"https://day.js.org/docs/en/display/format\">now</a> pentru mai multe informații.",
|
||||
"toc": "<code>#toc</code> sau <code>#toc=show</code> forțează afișarea tabelei de conținut, <code>#toc=hide</code> forțează ascunderea ei. Dacă eticheta nu există, se utilizează setările globale",
|
||||
"top": "păstrează notița la începutul listei (se aplică doar pentru notițe sortate automat)",
|
||||
"url": "URL",
|
||||
@@ -369,7 +369,7 @@
|
||||
},
|
||||
"confirm": {
|
||||
"also_delete_note": "Șterge și notița",
|
||||
"are_you_sure_remove_note": "Doriți ștergerea notiței „{{title}}” din harta de relații?",
|
||||
"are_you_sure_remove_note": "Doriți ștergerea notiței „{{title}}” din harta de relații? ",
|
||||
"cancel": "Anulează",
|
||||
"confirmation": "Confirm",
|
||||
"if_you_dont_check": "Dacă această opțiune nu este bifată, notița va fi ștearsă doar din harta de relații.",
|
||||
@@ -519,8 +519,8 @@
|
||||
"export_status": "Starea exportului",
|
||||
"export_type_single": "Doar această notiță fără descendenții ei",
|
||||
"export_type_subtree": "Această notiță și toți descendenții ei",
|
||||
"format_html_zip": "HTML în arhivă ZIP - recomandat deoarece păstrează toată formatarea",
|
||||
"format_markdown": "Markdown - păstrează majoritatea formatării",
|
||||
"format_html_zip": "HTML în arhivă ZIP - recomandat deoarece păstrează toată formatarea.",
|
||||
"format_markdown": "Markdown - păstrează majoritatea formatării.",
|
||||
"format_opml": "OPML - format de interschimbare pentru editoare cu structură ierarhică (outline). Formatarea, imaginile și fișierele nu vor fi incluse.",
|
||||
"opml_version_1": "OPML v1.0 - text simplu",
|
||||
"opml_version_2": "OPML v2.0 - permite și HTML",
|
||||
@@ -640,7 +640,7 @@
|
||||
"newTabNoteLink": "pe o legătură către o notiță va deschide notița într-un tab nou",
|
||||
"notSet": "nesetat",
|
||||
"noteNavigation": "Navigarea printre notițe",
|
||||
"numberedList": "<kbd>1.</code> sau <code>1)</code> urmat de spațiu pentru o listă numerotată",
|
||||
"numberedList": "<code>1.</code> sau <code>1)</code> urmat de spațiu pentru o listă numerotată",
|
||||
"onlyInDesktop": "Doar pentru desktop (aplicația Electron)",
|
||||
"openEmptyTab": "deschide un tab nou",
|
||||
"other": "Altele",
|
||||
@@ -655,7 +655,8 @@
|
||||
"showSQLConsole": "afișează consola SQL",
|
||||
"tabShortcuts": "Scurtături pentru tab-uri",
|
||||
"troubleshooting": "Unelte pentru depanare",
|
||||
"newTabWithActivationNoteLink": "pe o legătură către o notiță deschide și activează notița într-un tab nou"
|
||||
"newTabWithActivationNoteLink": "pe o legătură către o notiță deschide și activează notița într-un tab nou",
|
||||
"title": "Ghid rapid"
|
||||
},
|
||||
"hide_floating_buttons_button": {
|
||||
"button_title": "Ascunde butoanele"
|
||||
@@ -806,7 +807,7 @@
|
||||
"dialog_title": "Mută notițele în...",
|
||||
"error_no_path": "Nicio cale la care să poată fi mutate.",
|
||||
"move_button": "Mută la notița selectată",
|
||||
"move_success_message": "Notițele selectate au fost mutate în",
|
||||
"move_success_message": "Notițele selectate au fost mutate în ",
|
||||
"notes_to_move": "Notițe de mutat",
|
||||
"search_placeholder": "căutați notița după denumirea ei",
|
||||
"target_parent_note": "Notița părinte destinație"
|
||||
@@ -886,7 +887,8 @@
|
||||
"modal_title": "Selectați tipul notiței",
|
||||
"templates": "Șabloane",
|
||||
"change_path_prompt": "Selectați locul unde să se creeze noua notiță:",
|
||||
"search_placeholder": "căutare cale notiță după nume (cea implicită dacă este necompletat)"
|
||||
"search_placeholder": "căutare cale notiță după nume (cea implicită dacă este necompletat)",
|
||||
"builtin_templates": "Șabloane predefinite"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "Butonul „{{componentId}}” nu are nicio acțiune la clic definită"
|
||||
@@ -940,7 +942,9 @@
|
||||
},
|
||||
"password_not_set": {
|
||||
"body1": "Notițele protejate sunt criptate utilizând parola de utilizator, dar nu a fost setată nicio parolă.",
|
||||
"title": "Parola nu este setată"
|
||||
"title": "Parola nu este setată",
|
||||
"body2": "Pentru a putea proteja notițe, clic pe butonul de mai jos pentru a deschide fereastra de opțiuni și pentru a seta parola.",
|
||||
"go_to_password_options": "Mergi la setările de parolă"
|
||||
},
|
||||
"promoted_attributes": {
|
||||
"add_new_attribute": "Adaugă un nou atribut",
|
||||
@@ -1054,7 +1058,7 @@
|
||||
"download_button": "Descarcă",
|
||||
"file_size": "Dimensiune fișier:",
|
||||
"help_title": "Informații despre reviziile notițelor",
|
||||
"mime": "MIME:",
|
||||
"mime": "MIME: ",
|
||||
"no_revisions": "Nu există încă nicio revizie pentru această notiță...",
|
||||
"note_revisions": "Revizii ale notiței",
|
||||
"preview": "Previzualizare:",
|
||||
@@ -1072,7 +1076,7 @@
|
||||
"note_revisions": "Revizii ale notiței"
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_description": "Intervalul de salvare a reviziilor este timpul după care se crează o nouă revizie a unei notițe. Vedeți <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki-ul</a> pentru mai multe informații.",
|
||||
"note_revisions_snapshot_description": "Intervalul de salvare a reviziilor este timpul după care se crează o nouă revizie a unei notițe. Vedeți <doc>wiki-ul</doc> pentru mai multe informații.",
|
||||
"note_revisions_snapshot_interval_title": "Intervalul de salvare a reviziilor",
|
||||
"snapshot_time_interval_label": "Intervalul de salvare a reviziilor:"
|
||||
},
|
||||
@@ -1102,7 +1106,7 @@
|
||||
"limit_description": "Limitează numărul de rezultate",
|
||||
"order_by": "ordonează după",
|
||||
"save_to_note": "Salvează în notiță",
|
||||
"search_button": "Căutare <kbd>Enter</kbd>",
|
||||
"search_button": "Căutare",
|
||||
"search_execute": "Caută și execută acțiunile",
|
||||
"search_note_saved": "Notița de căutare a fost salvată în {{- notePathTitle}}",
|
||||
"search_parameters": "Parametrii de căutare",
|
||||
@@ -1189,7 +1193,7 @@
|
||||
"enable": "Activează corectorul ortografic",
|
||||
"language_code_label": "Codurile de limbă",
|
||||
"language_code_placeholder": "de exemplu „en-US”, „de-AT”",
|
||||
"multiple_languages_info": "Mai multe limbi pot fi separate prin virgulă, e.g. \"en-US, de-DE, cs\".",
|
||||
"multiple_languages_info": "Mai multe limbi pot fi separate prin virgulă, e.g. \"en-US, de-DE, cs\". ",
|
||||
"title": "Corector ortografic",
|
||||
"restart-required": "Schimbările asupra setărilor corectorului ortografic vor fi aplicate după restartarea aplicației."
|
||||
},
|
||||
@@ -1243,7 +1247,10 @@
|
||||
"layout-horizontal-description": "bara de lansare se află sub bara de taburi, bara de taburi este pe toată lungimea.",
|
||||
"layout-horizontal-title": "Orizontal",
|
||||
"layout-vertical-title": "Vertical",
|
||||
"layout-vertical-description": "bara de lansare se află pe stânga (implicit)"
|
||||
"layout-vertical-description": "bara de lansare se află pe stânga (implicit)",
|
||||
"auto_theme": "Tema clasică (se adaptează la schema de culori a sistemului)",
|
||||
"light_theme": "Tema clasică (luminoasă)",
|
||||
"dark_theme": "Tema clasică (întunecată)"
|
||||
},
|
||||
"toast": {
|
||||
"critical-error": {
|
||||
@@ -1279,7 +1286,7 @@
|
||||
"update_relation_target": {
|
||||
"allowed_characters": "Sunt permise doar caractere alfanumerice, underline și două puncte.",
|
||||
"change_target_note": "schimbă notița-țintă a unei relații existente",
|
||||
"on_all_matched_notes": "Pentru toate notițele găsite:",
|
||||
"on_all_matched_notes": "Pentru toate notițele găsite",
|
||||
"relation_name": "denumirea relației",
|
||||
"target_note": "notița destinație",
|
||||
"to": "la",
|
||||
@@ -1307,7 +1314,7 @@
|
||||
"use_vim_keybindings_in_code_notes": "Combinații de taste Vim"
|
||||
},
|
||||
"web_view": {
|
||||
"create_label": "Pentru a începe, creați o etichetă cu adresa URL de încorporat, e.g. #webViewSrc=\"https://www.google.com\"",
|
||||
"create_label": "Pentru a începe, creați o etichetă cu adresa URL de încorporat, e.g. #webViewSrc=\"https://www.google.com\"",
|
||||
"embed_websites": "Notițele de tip „Vizualizare web” permit încorporarea site-urilor web în Trilium.",
|
||||
"web_view": "Vizualizare web"
|
||||
},
|
||||
@@ -1366,8 +1373,8 @@
|
||||
},
|
||||
"shared_info": {
|
||||
"help_link": "Pentru informații vizitați <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki-ul</a>.",
|
||||
"shared_locally": "Această notiță este partajată local la",
|
||||
"shared_publicly": "Această notiță este partajată public la"
|
||||
"shared_locally": "Această notiță este partajată local la {{- link}}",
|
||||
"shared_publicly": "Această notiță este partajată public la {{- link}}"
|
||||
},
|
||||
"note_types": {
|
||||
"book": "Colecție",
|
||||
@@ -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
2030
apps/client/src/translations/uk/translation.json
Normal file
2030
apps/client/src/translations/uk/translation.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,69 +1,88 @@
|
||||
{
|
||||
"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",
|
||||
"app_version": "Phiên bản:",
|
||||
"db_version": "Phiên bản DB:",
|
||||
"sync_version": "Phiên bản liên kết:"
|
||||
},
|
||||
"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",
|
||||
"notSet": "chưa được đặt"
|
||||
},
|
||||
"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",
|
||||
"new_value_placeholder": "giá trị mới"
|
||||
},
|
||||
"rename_label": {
|
||||
"rename_label": "Đặt lại tên nhãn"
|
||||
},
|
||||
"call_to_action": {
|
||||
"dismiss": "Bỏ qua"
|
||||
},
|
||||
"abstract_search_option": {
|
||||
"remove_this_search_option": "Xoá lựa chọn tìm kiếm này"
|
||||
}
|
||||
}
|
||||
|
||||
2
apps/client/src/types-fancytree.d.ts
vendored
2
apps/client/src/types-fancytree.d.ts
vendored
@@ -113,7 +113,7 @@ declare namespace Fancytree {
|
||||
generateFormElements(selected?: boolean, active?: boolean): void;
|
||||
|
||||
/** Return the currently active node or null. */
|
||||
getActiveNode(): FancytreeNode;
|
||||
getActiveNode(): FancytreeNode | null;
|
||||
|
||||
/** Return the first top level node if any (not the invisible root node). */
|
||||
getFirstChild(): FancytreeNode;
|
||||
|
||||
@@ -3,7 +3,11 @@ type DateTimeStyle = "full" | "long" | "medium" | "short" | "none" | undefined;
|
||||
/**
|
||||
* Formats the given date and time to a string based on the current locale.
|
||||
*/
|
||||
export function formatDateTime(date: string | Date | number, dateStyle: DateTimeStyle = "medium", timeStyle: DateTimeStyle = "medium") {
|
||||
export function formatDateTime(date: string | Date | number | null | undefined, dateStyle: DateTimeStyle = "medium", timeStyle: DateTimeStyle = "medium") {
|
||||
if (!date) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const locale = navigator.language;
|
||||
|
||||
let parsedDate;
|
||||
|
||||
163
apps/client/src/widgets/FloatingButtons.css
Normal file
163
apps/client/src/widgets/FloatingButtons.css
Normal file
@@ -0,0 +1,163 @@
|
||||
/* #region Generic floating buttons styles */
|
||||
.floating-buttons {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.floating-buttons-children,
|
||||
.show-floating-buttons {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.note-split.rtl .floating-buttons-children,
|
||||
.note-split.rtl .show-floating-buttons {
|
||||
right: unset;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.note-split.rtl .close-floating-buttons {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.note-split.rtl .close-floating-buttons,
|
||||
.note-split.rtl .show-floating-buttons {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.type-canvas .floating-buttons-children {
|
||||
top: 70px;
|
||||
}
|
||||
|
||||
.type-canvas .floating-buttons-children > * {
|
||||
--border-radius: 0; /* Overridden by themes */
|
||||
}
|
||||
|
||||
.floating-buttons-children > *:not(.hidden-int):not(.no-content-hidden) {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.floating-buttons-children > *:not(.has-overflow) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.floating-buttons-children > button, .floating-buttons-children .floating-button {
|
||||
font-size: 150%;
|
||||
padding: 5px 10px 4px 10px;
|
||||
width: 40px;
|
||||
cursor: pointer;
|
||||
color: var(--button-text-color);
|
||||
background: var(--button-background-color);
|
||||
border-radius: var(--button-border-radius);
|
||||
border: 1px solid transparent;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.floating-buttons-children > button:hover, .floating-buttons-children .floating-button:hover {
|
||||
text-decoration: none;
|
||||
border-color: var(--button-border-color);
|
||||
}
|
||||
|
||||
.floating-buttons .floating-buttons-children.temporarily-hidden {
|
||||
display: none;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region Show floating button */
|
||||
.floating-buttons-children.temporarily-hidden+.show-floating-buttons {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.show-floating-buttons {
|
||||
/* display: none;*/
|
||||
margin-left: 5px !important;
|
||||
}
|
||||
|
||||
.show-floating-buttons-button {
|
||||
border: 1px solid transparent;
|
||||
color: var(--button-text-color);
|
||||
padding: 6px;
|
||||
border-radius: 100px !important;
|
||||
}
|
||||
|
||||
.show-floating-buttons-button:hover {
|
||||
border: 1px solid var(--button-border-color);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region Geo map buttons */
|
||||
.leaflet-pane {
|
||||
z-index: 50;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region Close floating buttons */
|
||||
.close-floating-buttons {
|
||||
margin-left: 5px !important;
|
||||
}
|
||||
|
||||
.close-floating-buttons:first-child {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.close-floating-buttons-button {
|
||||
border: 1px solid transparent;
|
||||
color: var(--button-text-color);
|
||||
padding: 6px;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.close-floating-buttons-button:hover {
|
||||
border: 1px solid var(--button-border-color);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region Backlinks */
|
||||
.backlinks-widget {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.backlinks-ticker {
|
||||
border-radius: 10px;
|
||||
border-color: var(--main-border-color);
|
||||
background-color: var(--more-accented-background-color);
|
||||
padding: 4px 10px 4px 10px;
|
||||
opacity: 90%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.backlinks-count {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.backlinks-items {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 10px;
|
||||
width: 400px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--accented-background-color);
|
||||
color: var(--main-text-color);
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.backlink-excerpt {
|
||||
border-left: 2px solid var(--main-border-color);
|
||||
padding-left: 10px;
|
||||
opacity: 80%;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.backlink-excerpt .backlink-link { /* the actual backlink */
|
||||
font-weight: bold;
|
||||
background-color: yellow;
|
||||
}
|
||||
/* #endregion */
|
||||
94
apps/client/src/widgets/FloatingButtons.tsx
Normal file
94
apps/client/src/widgets/FloatingButtons.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { t } from "i18next";
|
||||
import "./FloatingButtons.css";
|
||||
import { useNoteContext, useNoteLabel, useNoteLabelBoolean } from "./react/hooks";
|
||||
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
||||
import { ParentComponent } from "./react/react_utils";
|
||||
import { EventData, EventNames } from "../components/app_context";
|
||||
import { type FloatingButtonsList, type FloatingButtonContext } from "./FloatingButtonsDefinitions";
|
||||
import ActionButton from "./react/ActionButton";
|
||||
import { ViewTypeOptions } from "../services/note_list_renderer";
|
||||
|
||||
interface FloatingButtonsProps {
|
||||
items: FloatingButtonsList;
|
||||
}
|
||||
|
||||
/*
|
||||
* Note:
|
||||
*
|
||||
* For floating button widgets that require content to overflow, the has-overflow CSS class should
|
||||
* be applied to the root element of the widget. Additionally, this root element may need to
|
||||
* properly handle rounded corners, as defined by the --border-radius CSS variable.
|
||||
*/
|
||||
export default function FloatingButtons({ items }: FloatingButtonsProps) {
|
||||
const { note, noteContext } = useNoteContext();
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const [ viewType ] = useNoteLabel(note, "viewType");
|
||||
const [ isReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const context = useMemo<FloatingButtonContext | null>(() => {
|
||||
if (!note || !noteContext || !parentComponent) return null;
|
||||
|
||||
return {
|
||||
note,
|
||||
noteContext,
|
||||
parentComponent,
|
||||
isDefaultViewMode: noteContext.viewScope?.viewMode === "default",
|
||||
viewType: viewType as ViewTypeOptions,
|
||||
isReadOnly,
|
||||
triggerEvent<T extends EventNames>(name: T, data?: Omit<EventData<T>, "ntxId">) {
|
||||
parentComponent.triggerEvent(name, {
|
||||
ntxId: noteContext.ntxId,
|
||||
...data
|
||||
} as EventData<T>);
|
||||
}
|
||||
};
|
||||
}, [ note, noteContext, parentComponent, viewType, isReadOnly ]);
|
||||
|
||||
// Manage the user-adjustable visibility of the floating buttons.
|
||||
const [ visible, setVisible ] = useState(true);
|
||||
useEffect(() => setVisible(true), [ note ]);
|
||||
|
||||
return (
|
||||
<div className="floating-buttons no-print">
|
||||
<div className={`floating-buttons-children ${!visible ? "temporarily-hidden" : ""}`}>
|
||||
{context && items.map((Component) => (
|
||||
<Component {...context} />
|
||||
))}
|
||||
|
||||
{visible && <CloseFloatingButton setVisible={setVisible} />}
|
||||
</div>
|
||||
|
||||
{!visible && <ShowFloatingButton setVisible={setVisible} /> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show button that displays floating button after click on close button
|
||||
*/
|
||||
function ShowFloatingButton({ setVisible }: { setVisible(visible: boolean): void }) {
|
||||
return (
|
||||
<div className="show-floating-buttons">
|
||||
<ActionButton
|
||||
className="show-floating-buttons-button"
|
||||
icon="bx bx-chevrons-left"
|
||||
text={t("show_floating_buttons_button.button_title")}
|
||||
onClick={() => setVisible(true)}
|
||||
noIconActionClass
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CloseFloatingButton({ setVisible }: { setVisible(visible: boolean): void }) {
|
||||
return (
|
||||
<div className="close-floating-buttons">
|
||||
<ActionButton
|
||||
className="close-floating-buttons-button"
|
||||
icon="bx bx-chevrons-right"
|
||||
text={t("hide_floating_buttons_button.button_title")}
|
||||
onClick={() => setVisible(false)}
|
||||
noIconActionClass
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
398
apps/client/src/widgets/FloatingButtonsDefinitions.tsx
Normal file
398
apps/client/src/widgets/FloatingButtonsDefinitions.tsx
Normal file
@@ -0,0 +1,398 @@
|
||||
import { VNode } from "preact";
|
||||
import appContext, { EventData, EventNames } from "../components/app_context";
|
||||
import Component from "../components/component";
|
||||
import NoteContext from "../components/note_context";
|
||||
import FNote from "../entities/fnote";
|
||||
import ActionButton, { ActionButtonProps } from "./react/ActionButton";
|
||||
import { useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks";
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils";
|
||||
import server from "../services/server";
|
||||
import { BacklinkCountResponse, BacklinksResponse, SaveSqlConsoleResponse } from "@triliumnext/commons";
|
||||
import toast from "../services/toast";
|
||||
import { t } from "../services/i18n";
|
||||
import { copyImageReferenceToClipboard } from "../services/image";
|
||||
import tree from "../services/tree";
|
||||
import protected_session_holder from "../services/protected_session_holder";
|
||||
import options from "../services/options";
|
||||
import { getHelpUrlForNote } from "../services/in_app_help";
|
||||
import froca from "../services/froca";
|
||||
import NoteLink from "./react/NoteLink";
|
||||
import RawHtml from "./react/RawHtml";
|
||||
import { ViewTypeOptions } from "../services/note_list_renderer";
|
||||
|
||||
export interface FloatingButtonContext {
|
||||
parentComponent: Component;
|
||||
note: FNote;
|
||||
noteContext: NoteContext;
|
||||
isDefaultViewMode: boolean;
|
||||
isReadOnly: boolean;
|
||||
/** Shorthand for triggering an event from the parent component. The `ntxId` is automatically handled for convenience. */
|
||||
triggerEvent<T extends EventNames>(name: T, data?: Omit<EventData<T>, "ntxId">): void;
|
||||
viewType?: ViewTypeOptions | null;
|
||||
}
|
||||
|
||||
function FloatingButton({ className, ...props }: ActionButtonProps) {
|
||||
return <ActionButton
|
||||
className={`floating-button ${className ?? ""}`}
|
||||
noIconActionClass
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
|
||||
export type FloatingButtonsList = ((context: FloatingButtonContext) => false | VNode)[];
|
||||
|
||||
export const DESKTOP_FLOATING_BUTTONS: FloatingButtonsList = [
|
||||
RefreshBackendLogButton,
|
||||
SwitchSplitOrientationButton,
|
||||
ToggleReadOnlyButton,
|
||||
EditButton,
|
||||
ShowTocWidgetButton,
|
||||
ShowHighlightsListWidgetButton,
|
||||
RunActiveNoteButton,
|
||||
OpenTriliumApiDocsButton,
|
||||
SaveToNoteButton,
|
||||
RelationMapButtons,
|
||||
GeoMapButtons,
|
||||
CopyImageReferenceButton,
|
||||
ExportImageButtons,
|
||||
InAppHelpButton,
|
||||
Backlinks
|
||||
];
|
||||
|
||||
export const MOBILE_FLOATING_BUTTONS: FloatingButtonsList = [
|
||||
RefreshBackendLogButton,
|
||||
EditButton,
|
||||
RelationMapButtons,
|
||||
ExportImageButtons,
|
||||
Backlinks
|
||||
]
|
||||
|
||||
function RefreshBackendLogButton({ note, parentComponent, noteContext, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const isEnabled = note.noteId === "_backendLog" && isDefaultViewMode;
|
||||
return isEnabled && <FloatingButton
|
||||
text={t("backend_log.refresh")}
|
||||
icon="bx bx-refresh"
|
||||
onClick={() => parentComponent.triggerEvent("refreshData", { ntxId: noteContext.ntxId })}
|
||||
/>
|
||||
}
|
||||
|
||||
function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const isEnabled = note.type === "mermaid" && note.isContentAvailable() && !isReadOnly && isDefaultViewMode;
|
||||
const [ splitEditorOrientation, setSplitEditorOrientation ] = useTriliumOption("splitEditorOrientation");
|
||||
const upcomingOrientation = splitEditorOrientation === "horizontal" ? "vertical" : "horizontal";
|
||||
|
||||
return isEnabled && <FloatingButton
|
||||
text={upcomingOrientation === "vertical" ? t("switch_layout_button.title_vertical") : t("switch_layout_button.title_horizontal")}
|
||||
icon={upcomingOrientation === "vertical" ? "bx bxs-dock-bottom" : "bx bxs-dock-left"}
|
||||
onClick={() => setSplitEditorOrientation(upcomingOrientation)}
|
||||
/>
|
||||
}
|
||||
|
||||
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const isEnabled = (note.type === "mermaid" || viewType === "geoMap")
|
||||
&& note.isContentAvailable() && isDefaultViewMode;
|
||||
|
||||
return isEnabled && <FloatingButton
|
||||
text={isReadOnly ? t("toggle_read_only_button.unlock-editing") : t("toggle_read_only_button.lock-editing")}
|
||||
icon={isReadOnly ? "bx bx-lock-open-alt" : "bx bx-lock-alt"}
|
||||
onClick={() => setReadOnly(!isReadOnly)}
|
||||
/>
|
||||
}
|
||||
|
||||
function EditButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const [ animationClass, setAnimationClass ] = useState("");
|
||||
const [ isEnabled, setIsEnabled ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
noteContext.isReadOnly().then(isReadOnly => {
|
||||
setIsEnabled(
|
||||
isDefaultViewMode
|
||||
&& (!note.isProtected || protected_session_holder.isProtectedSessionAvailable())
|
||||
&& !options.is("databaseReadonly")
|
||||
&& isReadOnly
|
||||
);
|
||||
});
|
||||
}, [ note ]);
|
||||
|
||||
useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => {
|
||||
if (noteContext?.ntxId === eventNoteContext.ntxId) {
|
||||
setIsEnabled(false);
|
||||
}
|
||||
});
|
||||
|
||||
// make the edit button stand out on the first display, otherwise
|
||||
// it's difficult to notice that the note is readonly
|
||||
useEffect(() => {
|
||||
if (isEnabled) {
|
||||
setAnimationClass("bx-tada bx-lg");
|
||||
setTimeout(() => {
|
||||
setAnimationClass("");
|
||||
}, 1700);
|
||||
}
|
||||
}, [ isEnabled ]);
|
||||
|
||||
return isEnabled && <FloatingButton
|
||||
text={t("edit_button.edit_this_note")}
|
||||
icon="bx bx-pencil"
|
||||
className={animationClass}
|
||||
onClick={() => {
|
||||
if (noteContext.viewScope) {
|
||||
noteContext.viewScope.readOnlyTemporarilyDisabled = true;
|
||||
appContext.triggerEvent("readOnlyTemporarilyDisabled", { noteContext });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
function ShowTocWidgetButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const [ isEnabled, setIsEnabled ] = useState(false);
|
||||
useTriliumEvent("reEvaluateTocWidgetVisibility", () => {
|
||||
setIsEnabled(note.type === "text" && isDefaultViewMode && !!noteContext.viewScope?.tocTemporarilyHidden);
|
||||
});
|
||||
|
||||
return isEnabled && <FloatingButton
|
||||
text={t("show_toc_widget_button.show_toc")}
|
||||
icon="bx bx-tn-toc"
|
||||
onClick={() => {
|
||||
if (noteContext?.viewScope && noteContext.noteId) {
|
||||
noteContext.viewScope.tocTemporarilyHidden = false;
|
||||
appContext.triggerEvent("showTocWidget", { noteId: noteContext.noteId });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
function ShowHighlightsListWidgetButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const [ isEnabled, setIsEnabled ] = useState(false);
|
||||
useTriliumEvent("reEvaluateHighlightsListWidgetVisibility", () => {
|
||||
setIsEnabled(note.type === "text" && isDefaultViewMode && !!noteContext.viewScope?.highlightsListTemporarilyHidden);
|
||||
});
|
||||
|
||||
return isEnabled && <FloatingButton
|
||||
text={t("show_highlights_list_widget_button.show_highlights_list")}
|
||||
icon="bx bx-bookmarks"
|
||||
onClick={() => {
|
||||
if (noteContext?.viewScope && noteContext.noteId) {
|
||||
noteContext.viewScope.highlightsListTemporarilyHidden = false;
|
||||
appContext.triggerEvent("showHighlightsListWidget", { noteId: noteContext.noteId });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
function RunActiveNoteButton({ note }: FloatingButtonContext) {
|
||||
const isEnabled = note.mime.startsWith("application/javascript") || note.mime === "text/x-sqlite;schema=trilium";
|
||||
return isEnabled && <FloatingButton
|
||||
icon="bx bx-play"
|
||||
text={t("code_buttons.execute_button_title")}
|
||||
triggerCommand="runActiveNote"
|
||||
/>
|
||||
}
|
||||
|
||||
function OpenTriliumApiDocsButton({ note }: FloatingButtonContext) {
|
||||
const isEnabled = note.mime.startsWith("application/javascript;env=");
|
||||
return isEnabled && <FloatingButton
|
||||
icon="bx bx-help-circle"
|
||||
text={t("code_buttons.trilium_api_docs_button_title")}
|
||||
onClick={() => openInAppHelpFromUrl(note.mime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")}
|
||||
/>
|
||||
}
|
||||
|
||||
function SaveToNoteButton({ note }: FloatingButtonContext) {
|
||||
const isEnabled = note.mime === "text/x-sqlite;schema=trilium" && note.isHiddenCompletely();
|
||||
return isEnabled && <FloatingButton
|
||||
icon="bx bx-save"
|
||||
text={t("code_buttons.save_to_note_button_title")}
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
const { notePath } = await server.post<SaveSqlConsoleResponse>("special-notes/save-sql-console", { sqlConsoleNoteId: note.noteId });
|
||||
if (notePath) {
|
||||
toast.showMessage(t("code_buttons.sql_console_saved_message", { "note_path": await tree.getNotePathTitle(notePath) }));
|
||||
// TODO: This hangs the navigation, for some reason.
|
||||
//await ws.waitForMaxKnownEntityChangeId();
|
||||
await appContext.tabManager.getActiveContext()?.setNote(notePath);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
function RelationMapButtons({ note, triggerEvent }: FloatingButtonContext) {
|
||||
const isEnabled = (note.type === "relationMap");
|
||||
return isEnabled && (
|
||||
<>
|
||||
<FloatingButton
|
||||
icon="bx bx-folder-plus"
|
||||
text={t("relation_map_buttons.create_child_note_title")}
|
||||
onClick={() => triggerEvent("relationMapCreateChildNote")}
|
||||
/>
|
||||
|
||||
<FloatingButton
|
||||
icon="bx bx-crop"
|
||||
text={t("relation_map_buttons.reset_pan_zoom_title")}
|
||||
onClick={() => triggerEvent("relationMapResetPanZoom")}
|
||||
/>
|
||||
|
||||
<div className="btn-group">
|
||||
<FloatingButton
|
||||
icon="bx bx-zoom-in"
|
||||
text={t("relation_map_buttons.zoom_in_title")}
|
||||
onClick={() => triggerEvent("relationMapResetZoomIn")}
|
||||
/>
|
||||
|
||||
<FloatingButton
|
||||
icon="bx bx-zoom-out"
|
||||
text={t("relation_map_buttons.zoom_out_title")}
|
||||
onClick={() => triggerEvent("relationMapResetZoomOut")}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonContext) {
|
||||
const isEnabled = viewType === "geoMap" && !isReadOnly;
|
||||
return isEnabled && (
|
||||
<FloatingButton
|
||||
icon="bx bx-plus-circle"
|
||||
text={t("geo-map.create-child-note-title")}
|
||||
onClick={() => triggerEvent("geoMapCreateChildNote")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CopyImageReferenceButton({ note, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const hiddenImageCopyRef = useRef<HTMLDivElement>(null);
|
||||
const isEnabled = ["mermaid", "canvas", "mindMap"].includes(note?.type ?? "")
|
||||
&& note?.isContentAvailable() && isDefaultViewMode;
|
||||
|
||||
return isEnabled && (
|
||||
<>
|
||||
<FloatingButton
|
||||
icon="bx bx-copy"
|
||||
text={t("copy_image_reference_button.button_title")}
|
||||
onClick={() => {
|
||||
if (!hiddenImageCopyRef.current) return;
|
||||
const imageEl = document.createElement("img");
|
||||
imageEl.src = createImageSrcUrl(note);
|
||||
hiddenImageCopyRef.current.replaceChildren(imageEl);
|
||||
copyImageReferenceToClipboard($(hiddenImageCopyRef.current));
|
||||
hiddenImageCopyRef.current.removeChild(imageEl);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div ref={hiddenImageCopyRef} className="hidden-image-copy" style={{
|
||||
position: "absolute" // Take out of the the hidden image from flexbox to prevent the layout being affected
|
||||
}} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const isEnabled = ["mermaid", "mindMap"].includes(note?.type ?? "")
|
||||
&& note?.isContentAvailable() && isDefaultViewMode;
|
||||
return isEnabled && (
|
||||
<>
|
||||
<FloatingButton
|
||||
icon="bx bxs-file-image"
|
||||
text={t("svg_export_button.button_title")}
|
||||
onClick={() => triggerEvent("exportSvg")}
|
||||
/>
|
||||
|
||||
<FloatingButton
|
||||
icon="bx bxs-file-png"
|
||||
text={t("png_export_button.button_title")}
|
||||
onClick={() => triggerEvent("exportPng")}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function InAppHelpButton({ note }: FloatingButtonContext) {
|
||||
const helpUrl = getHelpUrlForNote(note);
|
||||
|
||||
return !!helpUrl && (
|
||||
<FloatingButton
|
||||
icon="bx bx-help-circle"
|
||||
text={t("help-button.title")}
|
||||
onClick={() => helpUrl && openInAppHelpFromUrl(helpUrl)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
|
||||
let [ backlinkCount, setBacklinkCount ] = useState(0);
|
||||
let [ popupOpen, setPopupOpen ] = useState(false);
|
||||
const backlinksContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefaultViewMode) return;
|
||||
|
||||
server.get<BacklinkCountResponse>(`note-map/${note.noteId}/backlink-count`).then(resp => {
|
||||
setBacklinkCount(resp.count);
|
||||
});
|
||||
}, [ note ]);
|
||||
|
||||
// Determine the max height of the container.
|
||||
const { windowHeight } = useWindowSize();
|
||||
useLayoutEffect(() => {
|
||||
const el = backlinksContainerRef.current;
|
||||
if (popupOpen && el) {
|
||||
const box = el.getBoundingClientRect();
|
||||
const maxHeight = windowHeight - box.top - 10;
|
||||
el.style.maxHeight = `${maxHeight}px`;
|
||||
}
|
||||
}, [ popupOpen, windowHeight ]);
|
||||
|
||||
const isEnabled = isDefaultViewMode && backlinkCount > 0;
|
||||
return (isEnabled &&
|
||||
<div className="backlinks-widget has-overflow">
|
||||
<div
|
||||
className="backlinks-ticker"
|
||||
onClick={() => setPopupOpen(!popupOpen)}
|
||||
>
|
||||
<span className="backlinks-count">{t("zpetne_odkazy.backlink", { count: backlinkCount })}</span>
|
||||
</div>
|
||||
|
||||
{popupOpen && (
|
||||
<div ref={backlinksContainerRef} className="backlinks-items dropdown-menu" style={{ display: "block" }}>
|
||||
<BacklinksList noteId={note.noteId} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BacklinksList({ noteId }: { noteId: string }) {
|
||||
const [ backlinks, setBacklinks ] = useState<BacklinksResponse>([]);
|
||||
|
||||
useEffect(() => {
|
||||
server.get<BacklinksResponse>(`note-map/${noteId}/backlinks`).then(async (backlinks) => {
|
||||
// prefetch all
|
||||
const noteIds = backlinks
|
||||
.filter(bl => "noteId" in bl)
|
||||
.map((bl) => bl.noteId);
|
||||
await froca.getNotes(noteIds);
|
||||
setBacklinks(backlinks);
|
||||
});
|
||||
}, [ noteId ]);
|
||||
|
||||
return backlinks.map(backlink => (
|
||||
<div>
|
||||
<NoteLink
|
||||
notePath={backlink.noteId}
|
||||
showNotePath showNoteIcon
|
||||
noPreview
|
||||
/>
|
||||
|
||||
{"relationName" in backlink ? (
|
||||
<p>{backlink.relationName}</p>
|
||||
) : (
|
||||
backlink.excerpts.map(excerpt => (
|
||||
<RawHtml html={excerpt} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
28
apps/client/src/widgets/api_log.css
Normal file
28
apps/client/src/widgets/api_log.css
Normal file
@@ -0,0 +1,28 @@
|
||||
.api-log-widget {
|
||||
flex-grow: 1;
|
||||
max-height: 40%;
|
||||
position: relative;
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
background-color: var(--accented-background-color);
|
||||
}
|
||||
|
||||
.api-log-container {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
font-family: var(--monospace-font-family);
|
||||
font-size: 0.8em;
|
||||
white-space: pre;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.close-api-log-button {
|
||||
padding: 5px;
|
||||
border: 1px solid var(--button-border-color);
|
||||
background-color: var(--button-background-color);
|
||||
border-radius: var(--button-border-radius);
|
||||
color: var(--button-text-color);
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import type { EventData } from "../components/app_context.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="api-log-widget">
|
||||
<style>
|
||||
.api-log-widget {
|
||||
padding: 15px;
|
||||
flex-grow: 1;
|
||||
max-height: 40%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hidden-api-log {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.api-log-container {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.close-api-log-button {
|
||||
padding: 5px;
|
||||
border: 1px solid var(--button-border-color);
|
||||
background-color: var(--button-background-color);
|
||||
border-radius: var(--button-border-radius);
|
||||
color: var(--button-text-color);
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="bx bx-x close-api-log-button" title="${t("api_log.close")}"></div>
|
||||
|
||||
<div class="api-log-container"></div>
|
||||
</div>`;
|
||||
|
||||
export default class ApiLogWidget extends NoteContextAwareWidget {
|
||||
|
||||
private $logContainer!: JQuery<HTMLElement>;
|
||||
private $closeButton!: JQuery<HTMLElement>;
|
||||
|
||||
isEnabled() {
|
||||
return !!this.note && this.note.mime.startsWith("application/javascript;env=") && super.isEnabled();
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.toggle(false);
|
||||
|
||||
this.$logContainer = this.$widget.find(".api-log-container");
|
||||
this.$closeButton = this.$widget.find(".close-api-log-button");
|
||||
this.$closeButton.on("click", () => this.toggle(false));
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
this.$logContainer.empty();
|
||||
}
|
||||
|
||||
apiLogMessagesEvent({ messages, noteId }: EventData<"apiLogMessages">) {
|
||||
if (!this.isNote(noteId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.toggle(true);
|
||||
|
||||
for (const message of messages) {
|
||||
this.$logContainer.append(message).append($("<br>"));
|
||||
}
|
||||
}
|
||||
|
||||
toggle(show: boolean) {
|
||||
this.$widget.toggleClass("hidden-api-log", !show);
|
||||
}
|
||||
}
|
||||
41
apps/client/src/widgets/api_log.tsx
Normal file
41
apps/client/src/widgets/api_log.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "./api_log.css";
|
||||
import { useNoteContext, useTriliumEvent } from "./react/hooks";
|
||||
import ActionButton from "./react/ActionButton";
|
||||
import { t } from "../services/i18n";
|
||||
|
||||
/**
|
||||
* Displays the messages that are logged by the current note via `api.log`, for frontend and backend scripts.
|
||||
*/
|
||||
export default function ApiLog() {
|
||||
const { note, noteId } = useNoteContext();
|
||||
const [ messages, setMessages ] = useState<string[]>();
|
||||
|
||||
useTriliumEvent("apiLogMessages", ({ messages, noteId: eventNoteId }) => {
|
||||
if (eventNoteId !== noteId) return;
|
||||
setMessages(messages);
|
||||
});
|
||||
|
||||
// Clear when navigating away.
|
||||
useEffect(() => setMessages(undefined), [ note ]);
|
||||
|
||||
const isEnabled = note?.mime.startsWith("application/javascript;env=") && messages?.length;
|
||||
return (
|
||||
<div className={`api-log-widget ${!isEnabled ? "hidden-ext" : ""}`}>
|
||||
{isEnabled && (
|
||||
<>
|
||||
<ActionButton
|
||||
icon="bx bx-x"
|
||||
className="close-api-log-button"
|
||||
text={t("api_log.close")}
|
||||
onClick={() => setMessages(undefined)}
|
||||
/>
|
||||
|
||||
<div className="api-log-container">
|
||||
{messages.join("\n")}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,504 +0,0 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import noteAutocompleteService, { type Suggestion } from "../../services/note_autocomplete.js";
|
||||
import server from "../../services/server.js";
|
||||
import contextMenuService from "../../menus/context_menu.js";
|
||||
import attributeParser, { type Attribute } from "../../services/attribute_parser.js";
|
||||
import { AttributeEditor, type EditorConfig, type ModelElement, type MentionFeed, type ModelNode, type ModelPosition } from "@triliumnext/ckeditor5";
|
||||
import froca from "../../services/froca.js";
|
||||
import attributeRenderer from "../../services/attribute_renderer.js";
|
||||
import noteCreateService from "../../services/note_create.js";
|
||||
import attributeService from "../../services/attributes.js";
|
||||
import linkService from "../../services/link.js";
|
||||
import type AttributeDetailWidget from "./attribute_detail.js";
|
||||
import type { CommandData, EventData, EventListener, FilteredCommandNames } from "../../components/app_context.js";
|
||||
import type { default as FAttribute, AttributeType } from "../../entities/fattribute.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import { escapeQuotes } from "../../services/utils.js";
|
||||
|
||||
const HELP_TEXT = `
|
||||
<p>${t("attribute_editor.help_text_body1")}</p>
|
||||
|
||||
<p>${t("attribute_editor.help_text_body2")}</p>
|
||||
|
||||
<p>${t("attribute_editor.help_text_body3")}</p>`;
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div style="position: relative; padding-top: 10px; padding-bottom: 10px">
|
||||
<style>
|
||||
.attribute-list-editor {
|
||||
border: 0 !important;
|
||||
outline: 0 !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 0 0 5px !important;
|
||||
margin: 0 !important;
|
||||
max-height: 100px;
|
||||
overflow: auto;
|
||||
transition: opacity .1s linear;
|
||||
}
|
||||
|
||||
.attribute-list-editor.ck-content .mention {
|
||||
color: var(--muted-text-color) !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.save-attributes-button {
|
||||
color: var(--muted-text-color);
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
right: 25px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
font-size: 130%;
|
||||
}
|
||||
|
||||
.add-new-attribute-button {
|
||||
color: var(--muted-text-color);
|
||||
position: absolute;
|
||||
bottom: 13px;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
font-size: 130%;
|
||||
}
|
||||
|
||||
.add-new-attribute-button:hover, .save-attributes-button:hover {
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: var(--button-border-radius);
|
||||
background: var(--button-background-color);
|
||||
color: var(--button-text-color);
|
||||
}
|
||||
|
||||
.attribute-errors {
|
||||
color: red;
|
||||
padding: 5px 50px 0px 5px; /* large right padding to avoid buttons */
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="attribute-list-editor" tabindex="200"></div>
|
||||
|
||||
<div class="bx bx-save save-attributes-button tn-tool-button" title="${escapeQuotes(t("attribute_editor.save_attributes"))}"></div>
|
||||
<div class="bx bx-plus add-new-attribute-button tn-tool-button" title="${escapeQuotes(t("attribute_editor.add_a_new_attribute"))}"></div>
|
||||
|
||||
<div class="attribute-errors" style="display: none;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const mentionSetup: MentionFeed[] = [
|
||||
{
|
||||
marker: "@",
|
||||
feed: (queryText) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
|
||||
itemRenderer: (_item) => {
|
||||
const item = _item as Suggestion;
|
||||
const itemElement = document.createElement("button");
|
||||
|
||||
itemElement.innerHTML = `${item.highlightedNotePathTitle} `;
|
||||
|
||||
return itemElement;
|
||||
},
|
||||
minimumCharacters: 0
|
||||
},
|
||||
{
|
||||
marker: "#",
|
||||
feed: async (queryText) => {
|
||||
const names = await server.get<string[]>(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`);
|
||||
|
||||
return names.map((name) => {
|
||||
return {
|
||||
id: `#${name}`,
|
||||
name: name
|
||||
};
|
||||
});
|
||||
},
|
||||
minimumCharacters: 0
|
||||
},
|
||||
{
|
||||
marker: "~",
|
||||
feed: async (queryText) => {
|
||||
const names = await server.get<string[]>(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`);
|
||||
|
||||
return names.map((name) => {
|
||||
return {
|
||||
id: `~${name}`,
|
||||
name: name
|
||||
};
|
||||
});
|
||||
},
|
||||
minimumCharacters: 0
|
||||
}
|
||||
];
|
||||
|
||||
const editorConfig: EditorConfig = {
|
||||
toolbar: {
|
||||
items: []
|
||||
},
|
||||
placeholder: t("attribute_editor.placeholder"),
|
||||
mention: {
|
||||
feeds: mentionSetup
|
||||
},
|
||||
licenseKey: "GPL"
|
||||
};
|
||||
|
||||
type AttributeCommandNames = FilteredCommandNames<CommandData>;
|
||||
|
||||
export default class AttributeEditorWidget extends NoteContextAwareWidget implements EventListener<"entitiesReloaded">, EventListener<"addNewLabel">, EventListener<"addNewRelation"> {
|
||||
private attributeDetailWidget: AttributeDetailWidget;
|
||||
private $editor!: JQuery<HTMLElement>;
|
||||
private $addNewAttributeButton!: JQuery<HTMLElement>;
|
||||
private $saveAttributesButton!: JQuery<HTMLElement>;
|
||||
private $errors!: JQuery<HTMLElement>;
|
||||
|
||||
private textEditor!: AttributeEditor;
|
||||
private lastUpdatedNoteId!: string | undefined;
|
||||
private lastSavedContent!: string;
|
||||
|
||||
constructor(attributeDetailWidget: AttributeDetailWidget) {
|
||||
super();
|
||||
|
||||
this.attributeDetailWidget = attributeDetailWidget;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$editor = this.$widget.find(".attribute-list-editor");
|
||||
|
||||
this.initialized = this.initEditor();
|
||||
|
||||
this.$editor.on("keydown", async (e) => {
|
||||
if (e.which === 13) {
|
||||
// allow autocomplete to fill the result textarea
|
||||
setTimeout(() => this.save(), 100);
|
||||
}
|
||||
|
||||
this.attributeDetailWidget.hide();
|
||||
});
|
||||
|
||||
this.$editor.on("blur", () => setTimeout(() => this.save(), 100)); // Timeout to fix https://github.com/zadam/trilium/issues/4160
|
||||
|
||||
this.$addNewAttributeButton = this.$widget.find(".add-new-attribute-button");
|
||||
this.$addNewAttributeButton.on("click", (e) => this.addNewAttribute(e));
|
||||
|
||||
this.$saveAttributesButton = this.$widget.find(".save-attributes-button");
|
||||
this.$saveAttributesButton.on("click", () => this.save());
|
||||
|
||||
this.$errors = this.$widget.find(".attribute-errors");
|
||||
}
|
||||
|
||||
addNewAttribute(e: JQuery.ClickEvent) {
|
||||
contextMenuService.show<AttributeCommandNames>({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
orientation: "left",
|
||||
items: [
|
||||
{ title: t("attribute_editor.add_new_label"), command: "addNewLabel", uiIcon: "bx bx-hash" },
|
||||
{ title: t("attribute_editor.add_new_relation"), command: "addNewRelation", uiIcon: "bx bx-transfer" },
|
||||
{ title: "----" },
|
||||
{ title: t("attribute_editor.add_new_label_definition"), command: "addNewLabelDefinition", uiIcon: "bx bx-empty" },
|
||||
{ title: t("attribute_editor.add_new_relation_definition"), command: "addNewRelationDefinition", uiIcon: "bx bx-empty" }
|
||||
],
|
||||
selectMenuItemHandler: ({ command }) => this.handleAddNewAttributeCommand(command)
|
||||
});
|
||||
// Prevent automatic hiding of the context menu due to the button being clicked.
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// triggered from keyboard shortcut
|
||||
async addNewLabelEvent({ ntxId }: EventData<"addNewLabel">) {
|
||||
if (this.isNoteContext(ntxId)) {
|
||||
await this.refresh();
|
||||
|
||||
this.handleAddNewAttributeCommand("addNewLabel");
|
||||
}
|
||||
}
|
||||
|
||||
// triggered from keyboard shortcut
|
||||
async addNewRelationEvent({ ntxId }: EventData<"addNewRelation">) {
|
||||
if (this.isNoteContext(ntxId)) {
|
||||
await this.refresh();
|
||||
|
||||
this.handleAddNewAttributeCommand("addNewRelation");
|
||||
}
|
||||
}
|
||||
|
||||
async handleAddNewAttributeCommand(command: AttributeCommandNames | undefined) {
|
||||
// TODO: Not sure what the relation between FAttribute[] and Attribute[] is.
|
||||
const attrs = this.parseAttributes() as FAttribute[];
|
||||
|
||||
if (!attrs) {
|
||||
return;
|
||||
}
|
||||
|
||||
let type: AttributeType;
|
||||
let name;
|
||||
let value;
|
||||
|
||||
if (command === "addNewLabel") {
|
||||
type = "label";
|
||||
name = "myLabel";
|
||||
value = "";
|
||||
} else if (command === "addNewRelation") {
|
||||
type = "relation";
|
||||
name = "myRelation";
|
||||
value = "";
|
||||
} else if (command === "addNewLabelDefinition") {
|
||||
type = "label";
|
||||
name = "label:myLabel";
|
||||
value = "promoted,single,text";
|
||||
} else if (command === "addNewRelationDefinition") {
|
||||
type = "label";
|
||||
name = "relation:myRelation";
|
||||
value = "promoted,single";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Incomplete type
|
||||
//@ts-ignore
|
||||
attrs.push({
|
||||
type,
|
||||
name,
|
||||
value,
|
||||
isInheritable: false
|
||||
});
|
||||
|
||||
await this.renderOwnedAttributes(attrs, false);
|
||||
|
||||
this.$editor.scrollTop(this.$editor[0].scrollHeight);
|
||||
|
||||
const rect = this.$editor[0].getBoundingClientRect();
|
||||
|
||||
setTimeout(() => {
|
||||
// showing a little bit later because there's a conflict with outside click closing the attr detail
|
||||
this.attributeDetailWidget.showAttributeDetail({
|
||||
allAttributes: attrs,
|
||||
attribute: attrs[attrs.length - 1],
|
||||
isOwned: true,
|
||||
x: (rect.left + rect.right) / 2,
|
||||
y: rect.bottom,
|
||||
focus: "name"
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async save() {
|
||||
if (this.lastUpdatedNoteId !== this.noteId) {
|
||||
// https://github.com/zadam/trilium/issues/3090
|
||||
console.warn("Ignoring blur event because a different note is loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
const attributes = this.parseAttributes();
|
||||
|
||||
if (attributes) {
|
||||
await server.put(`notes/${this.noteId}/attributes`, attributes, this.componentId);
|
||||
|
||||
this.$saveAttributesButton.fadeOut();
|
||||
|
||||
// blink the attribute text to give a visual hint that save has been executed
|
||||
this.$editor.css("opacity", 0);
|
||||
|
||||
// revert back
|
||||
setTimeout(() => this.$editor.css("opacity", 1), 100);
|
||||
}
|
||||
}
|
||||
|
||||
parseAttributes() {
|
||||
try {
|
||||
return attributeParser.lexAndParse(this.getPreprocessedData());
|
||||
} catch (e: any) {
|
||||
this.$errors.text(e.message).slideDown();
|
||||
}
|
||||
}
|
||||
|
||||
getPreprocessedData() {
|
||||
const str = this.textEditor
|
||||
.getData()
|
||||
.replace(/<a[^>]+href="(#[A-Za-z0-9_/]*)"[^>]*>[^<]*<\/a>/g, "$1")
|
||||
.replace(/ /g, " "); // otherwise .text() below outputs non-breaking space in unicode
|
||||
|
||||
return $("<div>").html(str).text();
|
||||
}
|
||||
|
||||
async initEditor() {
|
||||
this.$widget.show();
|
||||
|
||||
this.$editor.on("click", (e) => this.handleEditorClick(e));
|
||||
|
||||
this.textEditor = await AttributeEditor.create(this.$editor[0], editorConfig);
|
||||
this.textEditor.model.document.on("change:data", () => this.dataChanged());
|
||||
this.textEditor.editing.view.document.on(
|
||||
"enter",
|
||||
(event, data) => {
|
||||
// disable entering new line - see https://github.com/ckeditor/ckeditor5/issues/9422
|
||||
data.preventDefault();
|
||||
event.stop();
|
||||
},
|
||||
{ priority: "high" }
|
||||
);
|
||||
|
||||
// disable spellcheck for attribute editor
|
||||
const documentRoot = this.textEditor.editing.view.document.getRoot();
|
||||
if (documentRoot) {
|
||||
this.textEditor.editing.view.change((writer) => writer.setAttribute("spellcheck", "false", documentRoot));
|
||||
}
|
||||
}
|
||||
|
||||
dataChanged() {
|
||||
this.lastUpdatedNoteId = this.noteId;
|
||||
|
||||
if (this.lastSavedContent === this.textEditor.getData()) {
|
||||
this.$saveAttributesButton.fadeOut();
|
||||
} else {
|
||||
this.$saveAttributesButton.fadeIn();
|
||||
}
|
||||
|
||||
if (this.$errors.is(":visible")) {
|
||||
// using .hide() instead of .slideUp() since this will also hide the error after confirming
|
||||
// mention for relation name which suits up. When using.slideUp() error will appear and the slideUp which is weird
|
||||
this.$errors.hide();
|
||||
}
|
||||
}
|
||||
|
||||
async handleEditorClick(e: JQuery.ClickEvent) {
|
||||
const pos = this.textEditor.model.document.selection.getFirstPosition();
|
||||
|
||||
if (pos && pos.textNode && pos.textNode.data) {
|
||||
const clickIndex = this.getClickIndex(pos);
|
||||
|
||||
let parsedAttrs;
|
||||
|
||||
try {
|
||||
parsedAttrs = attributeParser.lexAndParse(this.getPreprocessedData(), true);
|
||||
} catch (e) {
|
||||
// the input is incorrect because the user messed up with it and now needs to fix it manually
|
||||
return null;
|
||||
}
|
||||
|
||||
let matchedAttr: Attribute | null = null;
|
||||
|
||||
for (const attr of parsedAttrs) {
|
||||
if (attr.startIndex && clickIndex > attr.startIndex && attr.endIndex && clickIndex <= attr.endIndex) {
|
||||
matchedAttr = attr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (matchedAttr) {
|
||||
this.$editor.tooltip("hide");
|
||||
|
||||
this.attributeDetailWidget.showAttributeDetail({
|
||||
allAttributes: parsedAttrs,
|
||||
attribute: matchedAttr,
|
||||
isOwned: true,
|
||||
x: e.pageX,
|
||||
y: e.pageY
|
||||
});
|
||||
} else {
|
||||
this.showHelpTooltip();
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
this.showHelpTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
showHelpTooltip() {
|
||||
this.attributeDetailWidget.hide();
|
||||
|
||||
this.$editor.tooltip({
|
||||
trigger: "focus",
|
||||
html: true,
|
||||
title: HELP_TEXT,
|
||||
placement: "bottom",
|
||||
offset: "0,30"
|
||||
});
|
||||
|
||||
this.$editor.tooltip("show");
|
||||
}
|
||||
|
||||
getClickIndex(pos: ModelPosition) {
|
||||
let clickIndex = pos.offset - (pos.textNode?.startOffset ?? 0);
|
||||
|
||||
let curNode: ModelNode | Text | ModelElement | null = pos.textNode;
|
||||
|
||||
while (curNode?.previousSibling) {
|
||||
curNode = curNode.previousSibling;
|
||||
|
||||
if ((curNode as ModelElement).name === "reference") {
|
||||
clickIndex += (curNode.getAttribute("href") as string).length + 1;
|
||||
} else if ("data" in curNode) {
|
||||
clickIndex += (curNode.data as string).length;
|
||||
}
|
||||
}
|
||||
|
||||
return clickIndex;
|
||||
}
|
||||
|
||||
async loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string) {
|
||||
const { noteId } = linkService.parseNavigationStateFromUrl(href);
|
||||
const note = noteId ? await froca.getNote(noteId, true) : null;
|
||||
const title = note ? note.title : "[missing]";
|
||||
|
||||
$el.text(title);
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
await this.renderOwnedAttributes(note.getOwnedAttributes(), true);
|
||||
}
|
||||
|
||||
async renderOwnedAttributes(ownedAttributes: FAttribute[], saved: boolean) {
|
||||
// attrs are not resorted if position changes after the initial load
|
||||
ownedAttributes.sort((a, b) => a.position - b.position);
|
||||
|
||||
let htmlAttrs = (await attributeRenderer.renderAttributes(ownedAttributes, true)).html();
|
||||
|
||||
if (htmlAttrs.length > 0) {
|
||||
htmlAttrs += " ";
|
||||
}
|
||||
|
||||
this.textEditor.setData(htmlAttrs);
|
||||
|
||||
if (saved) {
|
||||
this.lastSavedContent = this.textEditor.getData();
|
||||
|
||||
this.$saveAttributesButton.fadeOut(0);
|
||||
}
|
||||
}
|
||||
|
||||
async createNoteForReferenceLink(title: string) {
|
||||
let result;
|
||||
if (this.notePath) {
|
||||
result = await noteCreateService.createNoteWithTypePrompt(this.notePath, {
|
||||
activate: false,
|
||||
title: title
|
||||
});
|
||||
}
|
||||
|
||||
return result?.note?.getBestNotePathString();
|
||||
}
|
||||
|
||||
async updateAttributeList(attributes: FAttribute[]) {
|
||||
await this.renderOwnedAttributes(attributes, false);
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.$editor.trigger("focus");
|
||||
|
||||
this.textEditor.model.change((writer) => {
|
||||
const documentRoot = this.textEditor.editing.model.document.getRoot();
|
||||
if (!documentRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const positionAt = writer.createPositionAt(documentRoot, "end");
|
||||
writer.setSelection(positionAt);
|
||||
});
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import { isValidElement, VNode } from "preact";
|
||||
import Component, { TypedComponent } from "../components/component.js";
|
||||
import froca from "../services/froca.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import { renderReactWidget } from "./react/react_utils.jsx";
|
||||
import { EventNames, EventData } from "../components/app_context.js";
|
||||
import { Handler } from "leaflet";
|
||||
|
||||
export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedComponent<T> {
|
||||
protected attrs: Record<string, string>;
|
||||
@@ -22,11 +26,14 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
|
||||
this.childPositionCounter = 10;
|
||||
}
|
||||
|
||||
child(...components: T[]) {
|
||||
if (!components) {
|
||||
child(..._components: (T | VNode)[]) {
|
||||
if (!_components) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Convert any React components to legacy wrapped components.
|
||||
const components = wrapReactWidgets(_components);
|
||||
|
||||
super.child(...components);
|
||||
|
||||
for (const component of components) {
|
||||
@@ -48,7 +55,7 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
|
||||
* @param components the components to be added as children to this component provided the condition is truthy.
|
||||
* @returns self for chaining.
|
||||
*/
|
||||
optChild(condition: boolean, ...components: T[]) {
|
||||
optChild(condition: boolean, ...components: (T | VNode)[]) {
|
||||
if (condition) {
|
||||
return this.child(...components);
|
||||
} else {
|
||||
@@ -258,3 +265,30 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
|
||||
* For information on using widgets, see the tutorial {@tutorial widget_basics}.
|
||||
*/
|
||||
export default class BasicWidget extends TypedBasicWidget<Component> {}
|
||||
|
||||
export function wrapReactWidgets<T extends TypedComponent<any>>(components: (T | VNode)[]) {
|
||||
const wrappedResult: T[] = [];
|
||||
for (const component of components) {
|
||||
if (isValidElement(component)) {
|
||||
wrappedResult.push(new ReactWrappedWidget(component) as unknown as T);
|
||||
} else {
|
||||
wrappedResult.push(component);
|
||||
}
|
||||
}
|
||||
return wrappedResult;
|
||||
}
|
||||
|
||||
export class ReactWrappedWidget extends BasicWidget {
|
||||
|
||||
private el: VNode;
|
||||
|
||||
constructor(el: VNode) {
|
||||
super();
|
||||
this.el = el;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = renderReactWidget(this, this.el);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import SwitchWidget from "./switch.js";
|
||||
import server from "../services/server.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { EventData } from "../components/app_context.js";
|
||||
|
||||
// TODO: Deduplicate
|
||||
type Response = {
|
||||
success: true;
|
||||
} | {
|
||||
success: false;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export default class BookmarkSwitchWidget extends SwitchWidget {
|
||||
isEnabled() {
|
||||
return (
|
||||
super.isEnabled() &&
|
||||
// it's not possible to bookmark root because that would clone it under bookmarks and thus create a cycle
|
||||
!["root", "_hidden"].includes(this.noteId ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
doRender() {
|
||||
super.doRender();
|
||||
|
||||
this.switchOnName = t("bookmark_switch.bookmark");
|
||||
this.switchOnTooltip = t("bookmark_switch.bookmark_this_note");
|
||||
|
||||
this.switchOffName = t("bookmark_switch.bookmark");
|
||||
this.switchOffTooltip = t("bookmark_switch.remove_bookmark");
|
||||
}
|
||||
|
||||
async toggle(state: boolean | null | undefined) {
|
||||
const resp = await server.put<Response>(`notes/${this.noteId}/toggle-in-parent/_lbBookmarks/${!!state}`);
|
||||
|
||||
if (!resp.success && "message" in resp) {
|
||||
toastService.showError(resp.message);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
const isBookmarked = !!note.getParentBranches().find((b) => b.parentNoteId === "_lbBookmarks");
|
||||
|
||||
this.isToggled = isBookmarked;
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (loadResults.getBranchRows().find((b) => b.noteId === this.noteId)) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ComponentChildren } from "preact";
|
||||
import { memo } from "preact/compat";
|
||||
import AbstractBulkAction from "./abstract_bulk_action";
|
||||
import HelpRemoveButtons from "../react/HelpRemoveButtons";
|
||||
|
||||
interface BulkActionProps {
|
||||
label: string | ComponentChildren;
|
||||
@@ -24,19 +25,11 @@ const BulkAction = memo(({ label, children, helpText, bulkAction }: BulkActionPr
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
<td className="button-column">
|
||||
{helpText && <div className="dropdown help-dropdown">
|
||||
<span className="bx bx-help-circle icon-action" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
|
||||
<div className="dropdown-menu dropdown-menu-right p-4">
|
||||
{helpText}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<span
|
||||
className="bx bx-x icon-action action-conf-del"
|
||||
onClick={() => bulkAction?.deleteAction()}
|
||||
/>
|
||||
</td>
|
||||
<HelpRemoveButtons
|
||||
help={helpText}
|
||||
removeText="Delete"
|
||||
onRemove={() => bulkAction?.deleteAction()}
|
||||
/>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
import BulkAction, { BulkActionText } from "../BulkAction.jsx";
|
||||
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { useSpacedUpdate } from "../../react/hooks.jsx";
|
||||
|
||||
function MoveNoteBulkActionComponent({ bulkAction, actionDef }: { bulkAction: AbstractBulkAction, actionDef: ActionDefinition }) {
|
||||
function MoveNoteBulkActionComponent({ bulkAction }: { bulkAction: AbstractBulkAction }) {
|
||||
const [ targetParentNoteId, setTargetParentNoteId ] = useState<string>();
|
||||
const spacedUpdate = useSpacedUpdate(() => {
|
||||
return bulkAction.saveAction({ targetParentNoteId: targetParentNoteId })
|
||||
@@ -45,6 +45,6 @@ export default class MoveNoteBulkAction extends AbstractBulkAction {
|
||||
}
|
||||
|
||||
doRender() {
|
||||
return <MoveNoteBulkActionComponent bulkAction={this} actionDef={this.actionDef} />
|
||||
return <MoveNoteBulkActionComponent bulkAction={this} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action.js";
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import BulkAction from "../BulkAction.jsx";
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action.js";
|
||||
import noteAutocompleteService from "../../../services/note_autocomplete.js";
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import BulkAction, { BulkActionText } from "../BulkAction.jsx";
|
||||
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";
|
||||
|
||||
102
apps/client/src/widgets/buttons/global_menu.css
Normal file
102
apps/client/src/widgets/buttons/global_menu.css
Normal file
@@ -0,0 +1,102 @@
|
||||
.global-menu {
|
||||
width: 53px;
|
||||
height: 53px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.global-menu .dropdown-menu {
|
||||
min-width: 20em;
|
||||
}
|
||||
|
||||
.global-menu-button {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
position: relative;
|
||||
padding: 6px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.global-menu-button svg path {
|
||||
fill: var(--launcher-pane-text-color);
|
||||
}
|
||||
|
||||
.global-menu-button:hover { border: 0; }
|
||||
.global-menu-button:hover svg path {
|
||||
transition: 200ms ease-in-out fill;
|
||||
}
|
||||
.global-menu-button:hover svg path.st0 { fill:#95C980; }
|
||||
.global-menu-button:hover svg path.st1 { fill:#72B755; }
|
||||
.global-menu-button:hover svg path.st2 { fill:#4FA52B; }
|
||||
.global-menu-button:hover svg path.st3 { fill:#EE8C89; }
|
||||
.global-menu-button:hover svg path.st4 { fill:#E96562; }
|
||||
.global-menu-button:hover svg path.st5 { fill:#E33F3B; }
|
||||
.global-menu-button:hover svg path.st6 { fill:#EFB075; }
|
||||
.global-menu-button:hover svg path.st7 { fill:#E99547; }
|
||||
.global-menu-button:hover svg path.st8 { fill:#E47B19; }
|
||||
|
||||
.global-menu-button-update-available {
|
||||
position: absolute;
|
||||
right: -30px;
|
||||
bottom: -30px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.global-menu .zoom-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.global-menu .zoom-buttons {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
.global-menu .zoom-buttons a {
|
||||
display: inline-block;
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: var(--button-border-radius);
|
||||
color: var(--button-text-color);
|
||||
background-color: var(--button-background-color);
|
||||
padding: 3px;
|
||||
margin-left: 3px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.global-menu .zoom-buttons a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.global-menu .zoom-state {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.global-menu .dropdown-item .bx {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 120%;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* #region Update available */
|
||||
.global-menu-button-update-available-button {
|
||||
width: 21px !important;
|
||||
height: 21px !important;
|
||||
padding: 0 !important;
|
||||
|
||||
border-radius: var(--button-border-radius);
|
||||
transform: scale(0.9);
|
||||
border: none;
|
||||
opacity: 0.8;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.global-menu-button-wrapper:hover .global-menu-button-update-available-button {
|
||||
opacity: 1;
|
||||
}
|
||||
/* #endregion */
|
||||
@@ -1,436 +0,0 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import UpdateAvailableWidget from "./update_available.js";
|
||||
import options from "../../services/options.js";
|
||||
import { Tooltip, Dropdown } from "bootstrap";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="dropdown global-menu">
|
||||
<style>
|
||||
.global-menu {
|
||||
width: 53px;
|
||||
height: 53px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.global-menu .dropdown-menu {
|
||||
min-width: 20em;
|
||||
}
|
||||
|
||||
.global-menu-button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
padding: 6px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.global-menu-button > svg path {
|
||||
fill: var(--launcher-pane-text-color);
|
||||
}
|
||||
|
||||
.global-menu-button:hover { border: 0; }
|
||||
.global-menu-button:hover > svg path {
|
||||
transition: 200ms ease-in-out fill;
|
||||
}
|
||||
.global-menu-button:hover > svg path.st0 { fill:#95C980; }
|
||||
.global-menu-button:hover > svg path.st1 { fill:#72B755; }
|
||||
.global-menu-button:hover > svg path.st2 { fill:#4FA52B; }
|
||||
.global-menu-button:hover > svg path.st3 { fill:#EE8C89; }
|
||||
.global-menu-button:hover > svg path.st4 { fill:#E96562; }
|
||||
.global-menu-button:hover > svg path.st5 { fill:#E33F3B; }
|
||||
.global-menu-button:hover > svg path.st6 { fill:#EFB075; }
|
||||
.global-menu-button:hover > svg path.st7 { fill:#E99547; }
|
||||
.global-menu-button:hover > svg path.st8 { fill:#E47B19; }
|
||||
|
||||
.global-menu-button-update-available {
|
||||
position: absolute;
|
||||
right: -30px;
|
||||
bottom: -30px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.global-menu .zoom-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.global-menu .zoom-buttons a {
|
||||
display: inline-block;
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: var(--button-border-radius);
|
||||
color: var(--button-text-color);
|
||||
background-color: var(--button-background-color);
|
||||
padding: 3px;
|
||||
margin-left: 3px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.global-menu .zoom-buttons a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.global-menu .zoom-state {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.global-menu .dropdown-item .bx {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 120%;
|
||||
margin-right: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true"
|
||||
aria-expanded="false" class="icon-action global-menu-button">
|
||||
<div class="global-menu-button-update-available"></div>
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li class="dropdown-item" data-trigger-command="openNewWindow">
|
||||
<span class="bx bx-window-open"></span>
|
||||
${t("global_menu.open_new_window")}
|
||||
<kbd data-command="openNewWindow"></kbd>
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item" data-trigger-command="showShareSubtree">
|
||||
<span class="bx bx-share-alt"></span>
|
||||
${t("global_menu.show_shared_notes_subtree")}
|
||||
</li>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
<span class="zoom-container dropdown-item dropdown-item-container">
|
||||
<div>
|
||||
<span class="bx bx-empty"></span>
|
||||
${t("global_menu.zoom")}
|
||||
</div>
|
||||
|
||||
<div class="zoom-buttons">
|
||||
<a data-trigger-command="toggleFullscreen" title="${t("global_menu.toggle_fullscreen")}" class="bx bx-expand-alt"></a>
|
||||
|
||||
|
||||
|
||||
<a data-trigger-command="zoomOut" title="${t("global_menu.zoom_out")}" class="bx bx-minus"></a>
|
||||
|
||||
<span data-trigger-command="zoomReset" title="${t("global_menu.reset_zoom_level")}" class="zoom-state"></span>
|
||||
|
||||
<a data-trigger-command="zoomIn" title="${t("global_menu.zoom_in")}" class="bx bx-plus"></a>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<li class="dropdown-item toggle-pin">
|
||||
<span class="bx bx-pin"></span>
|
||||
${t("title_bar_buttons.window-on-top")}
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item" data-trigger-command="toggleZenMode">
|
||||
<span class="bx bxs-yin-yang"></span>
|
||||
${t("global_menu.toggle-zen-mode")}
|
||||
<kbd data-command="toggleZenMode"></kbd>
|
||||
</li>
|
||||
|
||||
<div class="dropdown-divider desktop-only"></div>
|
||||
|
||||
<li class="dropdown-item switch-to-mobile-version-button" data-trigger-command="switchToMobileVersion">
|
||||
<span class="bx bx-mobile"></span>
|
||||
${t("global_menu.switch_to_mobile_version")}
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item switch-to-desktop-version-button" data-trigger-command="switchToDesktopVersion">
|
||||
<span class="bx bx-desktop"></span>
|
||||
${t("global_menu.switch_to_desktop_version")}
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item" data-trigger-command="showLaunchBarSubtree">
|
||||
<span class="bx ${utils.isMobile() ? "bx-mobile" : "bx-sidebar"}"></span>
|
||||
${t("global_menu.configure_launchbar")}
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item dropdown-submenu">
|
||||
<span class="dropdown-toggle">
|
||||
<span class="bx bx-chip"></span>${t("global_menu.advanced")}
|
||||
</span>
|
||||
|
||||
<ul class="dropdown-menu">
|
||||
<li class="dropdown-item" data-trigger-command="showHiddenSubtree">
|
||||
<span class="bx bx-hide"></span>
|
||||
${t("global_menu.show_hidden_subtree")}
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item" data-trigger-command="showSearchHistory">
|
||||
<span class="bx bx-search-alt"></span>
|
||||
${t("global_menu.open_search_history")}
|
||||
</li>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
<li class="dropdown-item" data-trigger-command="showBackendLog">
|
||||
<span class="bx bx-detail"></span>
|
||||
${t("global_menu.show_backend_log")}
|
||||
<kbd data-command="showBackendLog"></kbd>
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item" data-trigger-command="showSQLConsole">
|
||||
<span class="bx bx-data"></span>
|
||||
${t("global_menu.open_sql_console")}
|
||||
<kbd data-command="showSQLConsole"></kbd>
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item" data-trigger-command="showSQLConsoleHistory">
|
||||
<span class="bx bx-data"></span>
|
||||
${t("global_menu.open_sql_console_history")}
|
||||
</li>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
<li class="dropdown-item open-dev-tools-button" data-trigger-command="openDevTools">
|
||||
<span class="bx bx-bug-alt"></span>
|
||||
${t("global_menu.open_dev_tools")}
|
||||
<kbd data-command="openDevTools"></kbd>
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item" data-trigger-command="reloadFrontendApp"
|
||||
title="${t("global_menu.reload_hint")}">
|
||||
<span class="bx bx-refresh"></span>
|
||||
${t("global_menu.reload_frontend")}
|
||||
<kbd data-command="reloadFrontendApp"></kbd>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item" data-trigger-command="showOptions">
|
||||
<span class="bx bx-cog"></span>
|
||||
${t("global_menu.options")}
|
||||
</li>
|
||||
|
||||
<div class="dropdown-divider desktop-only"></div>
|
||||
|
||||
<li class="dropdown-item show-help-button" data-trigger-command="showHelp">
|
||||
<span class="bx bx-help-circle"></span>
|
||||
${t("global_menu.show_help")}
|
||||
<kbd data-command="showHelp"></kbd>
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item show-help-button" data-trigger-command="showCheatsheet">
|
||||
<span class="bx bxs-keyboard"></span>
|
||||
${t("global_menu.show-cheatsheet")}
|
||||
<kbd data-command="showCheatsheet"></kbd>
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item show-about-dialog-button">
|
||||
<span class="bx bx-info-circle"></span>
|
||||
${t("global_menu.about")}
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item update-to-latest-version-button" style="display: none;" data-trigger-command="downloadLatestVersion">
|
||||
<span class="bx bx-sync"></span>
|
||||
|
||||
<span class="version-text"></span>
|
||||
</li>
|
||||
|
||||
<div class="dropdown-divider logout-button-separator"></div>
|
||||
|
||||
<li class="dropdown-item logout-button" data-trigger-command="logout">
|
||||
<span class="bx bx-log-out"></span>
|
||||
${t("global_menu.logout")}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class GlobalMenuWidget extends BasicWidget {
|
||||
private updateAvailableWidget: UpdateAvailableWidget;
|
||||
private isHorizontalLayout: boolean;
|
||||
private tooltip!: Tooltip;
|
||||
private dropdown!: Dropdown;
|
||||
|
||||
private $updateToLatestVersionButton!: JQuery<HTMLElement>;
|
||||
private $zoomState!: JQuery<HTMLElement>;
|
||||
private $toggleZenMode!: JQuery<HTMLElement>;
|
||||
|
||||
constructor(isHorizontalLayout: boolean) {
|
||||
super();
|
||||
|
||||
this.updateAvailableWidget = new UpdateAvailableWidget();
|
||||
this.isHorizontalLayout = isHorizontalLayout;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
if (!this.isHorizontalLayout) {
|
||||
this.$widget.addClass("dropend");
|
||||
}
|
||||
|
||||
const $globalMenuButton = this.$widget.find(".global-menu-button");
|
||||
if (!this.isHorizontalLayout) {
|
||||
$globalMenuButton.prepend(
|
||||
$(`\
|
||||
<svg viewBox="0 0 256 256" data-bs-toggle="tooltip" title="${t("global_menu.menu")}">
|
||||
<g>
|
||||
<path class="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
|
||||
<path class="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
|
||||
<path class="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
|
||||
|
||||
<path class="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
|
||||
<path class="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
|
||||
<path class="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
|
||||
|
||||
<path class="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
|
||||
<path class="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
|
||||
<path class="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
|
||||
</g>
|
||||
</svg>`)
|
||||
);
|
||||
|
||||
this.tooltip = new Tooltip(this.$widget.find("[data-bs-toggle='tooltip']")[0], { trigger: "hover" });
|
||||
} else {
|
||||
$globalMenuButton.toggleClass("bx bx-menu");
|
||||
}
|
||||
|
||||
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0], {
|
||||
popperConfig: {
|
||||
placement: "bottom"
|
||||
}
|
||||
});
|
||||
|
||||
this.$widget.find(".show-about-dialog-button").on("click", () => this.triggerCommand("openAboutDialog"));
|
||||
|
||||
const isElectron = utils.isElectron();
|
||||
|
||||
this.$widget.find(".toggle-pin").toggle(isElectron);
|
||||
if (isElectron) {
|
||||
this.$widget.on("click", ".toggle-pin", (e) => {
|
||||
const $el = $(e.target);
|
||||
const remote = utils.dynamicRequire("@electron/remote");
|
||||
const focusedWindow = remote.BrowserWindow.getFocusedWindow();
|
||||
const isAlwaysOnTop = focusedWindow.isAlwaysOnTop();
|
||||
if (isAlwaysOnTop) {
|
||||
focusedWindow.setAlwaysOnTop(false);
|
||||
$el.removeClass("active");
|
||||
} else {
|
||||
focusedWindow.setAlwaysOnTop(true);
|
||||
$el.addClass("active");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.$widget.find(".logout-button").toggle(!isElectron);
|
||||
this.$widget.find(".logout-button-separator").toggle(!isElectron);
|
||||
|
||||
this.$widget.find(".open-dev-tools-button").toggle(isElectron);
|
||||
this.$widget.find(".switch-to-mobile-version-button").toggle(!isElectron && utils.isDesktop());
|
||||
this.$widget.find(".switch-to-desktop-version-button").toggle(!isElectron && utils.isMobile());
|
||||
|
||||
this.$widget.on("click", ".dropdown-item", (e) => {
|
||||
if ($(e.target).parent(".zoom-buttons")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dropdown.toggle();
|
||||
});
|
||||
if (utils.isMobile()) {
|
||||
this.$widget.on("click", ".dropdown-submenu .dropdown-toggle", (e) => {
|
||||
const $submenu = $(e.target).closest(".dropdown-item");
|
||||
$submenu.toggleClass("submenu-open");
|
||||
$submenu.find("ul.dropdown-menu").toggleClass("show");
|
||||
e.stopPropagation();
|
||||
return;
|
||||
});
|
||||
}
|
||||
this.$widget.on("click", ".dropdown-submenu", (e) => {
|
||||
if ($(e.target).children(".dropdown-menu").length === 1 || $(e.target).hasClass("dropdown-toggle")) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
this.$widget.find(".global-menu-button-update-available").append(this.updateAvailableWidget.render());
|
||||
|
||||
this.$updateToLatestVersionButton = this.$widget.find(".update-to-latest-version-button");
|
||||
|
||||
if (!utils.isElectron()) {
|
||||
this.$widget.find(".zoom-container").hide();
|
||||
}
|
||||
|
||||
this.$zoomState = this.$widget.find(".zoom-state");
|
||||
this.$toggleZenMode = this.$widget.find('[data-trigger-command="toggleZenMode"');
|
||||
this.$widget.on("show.bs.dropdown", () => this.#onShown());
|
||||
if (this.tooltip) {
|
||||
this.$widget.on("hide.bs.dropdown", () => this.tooltip.enable());
|
||||
}
|
||||
|
||||
this.$widget.find(".zoom-buttons").on(
|
||||
"click",
|
||||
// delay to wait for the actual zoom change
|
||||
() => setTimeout(() => this.updateZoomState(), 300)
|
||||
);
|
||||
|
||||
this.updateVersionStatus();
|
||||
|
||||
setInterval(() => this.updateVersionStatus(), 8 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
#onShown() {
|
||||
this.$toggleZenMode.toggleClass("active", $("body").hasClass("zen"));
|
||||
this.updateZoomState();
|
||||
if (this.tooltip) {
|
||||
this.tooltip.hide();
|
||||
this.tooltip.disable();
|
||||
}
|
||||
}
|
||||
|
||||
updateZoomState() {
|
||||
if (!utils.isElectron()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zoomFactor = utils.dynamicRequire("electron").webFrame.getZoomFactor();
|
||||
const zoomPercent = Math.round(zoomFactor * 100);
|
||||
|
||||
this.$zoomState.text(`${zoomPercent}%`);
|
||||
}
|
||||
|
||||
async updateVersionStatus() {
|
||||
await options.initializedPromise;
|
||||
|
||||
if (options.get("checkForUpdates") !== "true") {
|
||||
return;
|
||||
}
|
||||
|
||||
const latestVersion = await this.fetchLatestVersion();
|
||||
this.updateAvailableWidget.updateVersionStatus(latestVersion);
|
||||
// Show "click to download" button in options menu if there's a new version available
|
||||
this.$updateToLatestVersionButton.toggle(utils.isUpdateAvailable(latestVersion, glob.triliumVersion));
|
||||
this.$updateToLatestVersionButton.find(".version-text").text(`Version ${latestVersion} is available, click to download.`);
|
||||
}
|
||||
|
||||
async fetchLatestVersion() {
|
||||
const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Notes/releases/latest";
|
||||
|
||||
const resp = await fetch(RELEASES_API_URL);
|
||||
const data = await resp.json();
|
||||
|
||||
return data?.tag_name?.substring(1);
|
||||
}
|
||||
|
||||
downloadLatestVersionCommand() {
|
||||
window.open("https://github.com/TriliumNext/Trilium/releases/latest");
|
||||
}
|
||||
|
||||
activeContextChangedEvent() {
|
||||
this.dropdown.hide();
|
||||
}
|
||||
|
||||
noteSwitchedEvent() {
|
||||
this.dropdown.hide();
|
||||
}
|
||||
}
|
||||
239
apps/client/src/widgets/buttons/global_menu.tsx
Normal file
239
apps/client/src/widgets/buttons/global_menu.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import "./global_menu.css";
|
||||
import { useStaticTooltip, useStaticTooltipWithKeyboardShortcut, useTriliumOption, useTriliumOptionBool } from "../react/hooks";
|
||||
import { useContext, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { t } from "../../services/i18n";
|
||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList";
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import KeyboardShortcut from "../react/KeyboardShortcut";
|
||||
import { KeyboardActionNames } from "@triliumnext/commons";
|
||||
import { ComponentChildren } from "preact";
|
||||
import Component from "../../components/component";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import utils, { dynamicRequire, isElectron, isMobile } from "../../services/utils";
|
||||
|
||||
interface MenuItemProps<T> {
|
||||
icon: string,
|
||||
text: ComponentChildren,
|
||||
title?: string,
|
||||
command: T,
|
||||
disabled?: boolean
|
||||
active?: boolean;
|
||||
outsideChildren?: ComponentChildren;
|
||||
}
|
||||
|
||||
export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: boolean }) {
|
||||
const isVerticalLayout = !isHorizontalLayout;
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const { isUpdateAvailable, latestVersion } = useTriliumUpdateStatus();
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
className="global-menu"
|
||||
buttonClassName={`global-menu-button ${isHorizontalLayout ? "bx bx-menu" : ""}`} noSelectButtonStyle iconAction hideToggleArrow
|
||||
text={<>
|
||||
{isVerticalLayout && <VerticalLayoutIcon />}
|
||||
{isUpdateAvailable && <div class="global-menu-button-update-available">
|
||||
<span className="bx bx-sync global-menu-button-update-available-button" title={t("update_available.update_available")}></span>
|
||||
</div>}
|
||||
</>}
|
||||
>
|
||||
|
||||
<MenuItem command="openNewWindow" icon="bx bx-window-open" text={t("global_menu.open_new_window")} />
|
||||
<MenuItem command="showShareSubtree" icon="bx bx-share-alt" text={t("global_menu.show_shared_notes_subtree")} />
|
||||
<FormDropdownDivider />
|
||||
|
||||
<ZoomControls parentComponent={parentComponent} />
|
||||
<ToggleWindowOnTop />
|
||||
<KeyboardActionMenuItem command="toggleZenMode" icon="bx bxs-yin-yang" text={t("global_menu.toggle-zen-mode")} />
|
||||
<FormDropdownDivider />
|
||||
|
||||
<SwitchToOptions />
|
||||
<MenuItem command="showLaunchBarSubtree" icon={`bx ${isMobile() ? "bx-mobile" : "bx-sidebar"}`} text={t("global_menu.configure_launchbar")} />
|
||||
<AdvancedMenu />
|
||||
<MenuItem command="showOptions" icon="bx bx-cog" text={t("global_menu.options")} />
|
||||
<FormDropdownDivider />
|
||||
|
||||
<KeyboardActionMenuItem command="showHelp" icon="bx bx-help-circle" text={t("global_menu.show_help")} />
|
||||
<KeyboardActionMenuItem command="showCheatsheet" icon="bx bxs-keyboard" text={t("global_menu.show-cheatsheet")} />
|
||||
<MenuItem command="openAboutDialog" icon="bx bx-info-circle" text={t("global_menu.about")} />
|
||||
{isUpdateAvailable && <MenuItem command={() => window.open("https://github.com/TriliumNext/Trilium/releases/latest")} icon="bx bx-sync" text={`Version ${latestVersion} is available, click to download.`} /> }
|
||||
{!isElectron() && <BrowserOnlyOptions />}
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
function AdvancedMenu() {
|
||||
return (
|
||||
<FormDropdownSubmenu icon="bx bx-chip" title={t("global_menu.advanced")}>
|
||||
<MenuItem command="showHiddenSubtree" icon="bx bx-hide" text={t("global_menu.show_hidden_subtree")} />
|
||||
<MenuItem command="showSearchHistory" icon="bx bx-search-alt" text={t("global_menu.open_search_history")} />
|
||||
<FormDropdownDivider />
|
||||
|
||||
<KeyboardActionMenuItem command="showBackendLog" icon="bx bx-detail" text={t("global_menu.show_backend_log")} />
|
||||
<KeyboardActionMenuItem command="showSQLConsole" icon="bx bx-data" text={t("global_menu.open_sql_console")} />
|
||||
<MenuItem command="showSQLConsoleHistory" icon="bx bx-data" text={t("global_menu.open_sql_console_history")} />
|
||||
<FormDropdownDivider />
|
||||
|
||||
{isElectron() && <MenuItem command="openDevTools" icon="bx bx-bug-alt" text={t("global_menu.open_dev_tools")} />}
|
||||
<KeyboardActionMenuItem command="reloadFrontendApp" icon="bx bx-refresh" text={t("global_menu.reload_frontend")} title={t("global_menu.reload_hint")} />
|
||||
</FormDropdownSubmenu>
|
||||
)
|
||||
}
|
||||
|
||||
function BrowserOnlyOptions() {
|
||||
return <>
|
||||
<FormDropdownDivider />
|
||||
<MenuItem command="logout" icon="bx bx-log-out" text={t("global_menu.logout")} />
|
||||
</>;
|
||||
}
|
||||
|
||||
function SwitchToOptions() {
|
||||
if (isElectron()) {
|
||||
return;
|
||||
} else if (!isMobile()) {
|
||||
return <MenuItem command="switchToMobileVersion" icon="bx bx-mobile" text={t("global_menu.switch_to_mobile_version")} />
|
||||
} else {
|
||||
return <MenuItem command="switchToDesktopVersion" icon="bx bx-desktop" text={t("global_menu.switch_to_desktop_version")} />
|
||||
}
|
||||
}
|
||||
|
||||
function MenuItem({ icon, text, title, command, disabled, active }: MenuItemProps<KeyboardActionNames | CommandNames | (() => void)>) {
|
||||
return <FormListItem
|
||||
icon={icon}
|
||||
title={title}
|
||||
triggerCommand={typeof command === "string" ? command : undefined}
|
||||
onClick={typeof command === "function" ? command : undefined}
|
||||
disabled={disabled}
|
||||
active={active}
|
||||
>{text}</FormListItem>
|
||||
}
|
||||
|
||||
function KeyboardActionMenuItem({ text, command, ...props }: MenuItemProps<KeyboardActionNames>) {
|
||||
return <MenuItem
|
||||
{...props}
|
||||
command={command}
|
||||
text={<>{text} <KeyboardShortcut actionName={command as KeyboardActionNames} /></>}
|
||||
/>
|
||||
}
|
||||
|
||||
function VerticalLayoutIcon() {
|
||||
const logoRef = useRef<SVGSVGElement>(null);
|
||||
useStaticTooltip(logoRef);
|
||||
|
||||
return (
|
||||
<svg ref={logoRef} viewBox="0 0 256 256" title={t("global_menu.menu")}>
|
||||
<g>
|
||||
<path className="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
|
||||
<path className="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
|
||||
<path className="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
|
||||
|
||||
<path className="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
|
||||
<path className="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
|
||||
<path className="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
|
||||
|
||||
<path className="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
|
||||
<path className="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
|
||||
<path className="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ZoomControls({ parentComponent }: { parentComponent?: Component | null }) {
|
||||
const [ zoomLevel, setZoomLevel ] = useState(100);
|
||||
|
||||
function updateZoomState() {
|
||||
if (!isElectron()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zoomFactor = dynamicRequire("electron").webFrame.getZoomFactor();
|
||||
setZoomLevel(Math.round(zoomFactor * 100));
|
||||
}
|
||||
|
||||
useEffect(updateZoomState, []);
|
||||
|
||||
function ZoomControlButton({ command, title, icon, children }: { command: KeyboardActionNames, title: string, icon?: string, children?: ComponentChildren }) {
|
||||
const linkRef = useRef<HTMLAnchorElement>(null);
|
||||
useStaticTooltipWithKeyboardShortcut(linkRef, title, command);
|
||||
return (
|
||||
<a
|
||||
ref={linkRef}
|
||||
onClick={(e) => {
|
||||
parentComponent?.triggerCommand(command);
|
||||
setTimeout(() => updateZoomState(), 300)
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className={icon}
|
||||
>{children}</a>
|
||||
)
|
||||
}
|
||||
|
||||
return isElectron() ? (
|
||||
<FormListItem
|
||||
icon="bx bx-empty"
|
||||
className="zoom-container"
|
||||
>
|
||||
{t("global_menu.zoom")}
|
||||
<>
|
||||
<div className="zoom-buttons">
|
||||
<ZoomControlButton command="toggleFullscreen" title={t("global_menu.toggle_fullscreen")} icon="bx bx-expand-alt" />
|
||||
|
||||
<ZoomControlButton command="zoomOut" title={t("global_menu.zoom_out")} icon="bx bx-minus" />
|
||||
<ZoomControlButton command="zoomReset" title={t("global_menu.reset_zoom_level")}>{zoomLevel}{t("units.percentage")}</ZoomControlButton>
|
||||
<ZoomControlButton command="zoomIn" title={t("global_menu.zoom_in")} icon="bx bx-plus" />
|
||||
</div>
|
||||
</>
|
||||
</FormListItem>
|
||||
) : (
|
||||
<MenuItem icon="bx bx-expand-alt" command="toggleFullscreen" text={t("global_menu.toggle_fullscreen")} />
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleWindowOnTop() {
|
||||
const focusedWindow = isElectron() ? dynamicRequire("@electron/remote").BrowserWindow.getFocusedWindow() : null;
|
||||
const [ isAlwaysOnTop, setIsAlwaysOnTop ] = useState(focusedWindow?.isAlwaysOnTop());
|
||||
|
||||
return (isElectron() &&
|
||||
<MenuItem
|
||||
icon="bx bx-pin"
|
||||
text={t("title_bar_buttons.window-on-top")}
|
||||
active={isAlwaysOnTop}
|
||||
command={() => {
|
||||
const newState = !isAlwaysOnTop;
|
||||
focusedWindow?.setAlwaysOnTop(newState);
|
||||
setIsAlwaysOnTop(newState);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function useTriliumUpdateStatus() {
|
||||
const [ latestVersion, setLatestVersion ] = useState<string>();
|
||||
const [ checkForUpdates ] = useTriliumOptionBool("checkForUpdates");
|
||||
const isUpdateAvailable = utils.isUpdateAvailable(latestVersion, glob.triliumVersion);
|
||||
|
||||
async function updateVersionStatus() {
|
||||
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();
|
||||
const latestVersion = data?.tag_name?.substring(1);
|
||||
setLatestVersion(latestVersion);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!checkForUpdates) {
|
||||
setLatestVersion(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
updateVersionStatus();
|
||||
|
||||
const interval = setInterval(() => updateVersionStatus(), 8 * 60 * 60 * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [ checkForUpdates ]);
|
||||
|
||||
return { isUpdateAvailable, latestVersion };
|
||||
}
|
||||
@@ -1,24 +1,11 @@
|
||||
import utils from "../../services/utils.js";
|
||||
import contextMenu from "../../menus/context_menu.js";
|
||||
import contextMenu, { MenuCommandItem } from "../../menus/context_menu.js";
|
||||
import treeService from "../../services/tree.js";
|
||||
import ButtonFromNoteWidget from "./button_from_note.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import type { CommandNames } from "../../components/app_context.js";
|
||||
|
||||
interface WebContents {
|
||||
history: string[];
|
||||
getActiveIndex(): number;
|
||||
clearHistory(): void;
|
||||
canGoBack(): boolean;
|
||||
canGoForward(): boolean;
|
||||
goToIndex(index: string): void;
|
||||
}
|
||||
|
||||
interface ContextMenuItem {
|
||||
title: string;
|
||||
idx: string;
|
||||
uiIcon: string;
|
||||
}
|
||||
import type { WebContents } from "electron";
|
||||
import link from "../../services/link.js";
|
||||
|
||||
export default class HistoryNavigationButton extends ButtonFromNoteWidget {
|
||||
private webContents?: WebContents;
|
||||
@@ -51,28 +38,24 @@ export default class HistoryNavigationButton extends ButtonFromNoteWidget {
|
||||
async showContextMenu(e: JQuery.ContextMenuEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.webContents || this.webContents.history.length < 2) {
|
||||
if (!this.webContents || this.webContents.navigationHistory.length() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
let items: ContextMenuItem[] = [];
|
||||
let items: MenuCommandItem<string>[] = [];
|
||||
|
||||
const activeIndex = this.webContents.getActiveIndex();
|
||||
const history = this.webContents.history;
|
||||
const history = this.webContents.navigationHistory.getAllEntries();
|
||||
const activeIndex = this.webContents.navigationHistory.getActiveIndex();
|
||||
|
||||
for (const idx in history) {
|
||||
const url = history[idx];
|
||||
const parts = url.split("#");
|
||||
if (parts.length < 2) continue;
|
||||
|
||||
const notePathWithTab = parts[1];
|
||||
const notePath = notePathWithTab.split("-")[0];
|
||||
const { notePath } = link.parseNavigationStateFromUrl(history[idx].url);
|
||||
if (!notePath) continue;
|
||||
|
||||
const title = await treeService.getNotePathTitle(notePath);
|
||||
|
||||
items.push({
|
||||
title,
|
||||
idx,
|
||||
command: idx,
|
||||
uiIcon:
|
||||
parseInt(idx) === activeIndex
|
||||
? "bx bx-radio-circle-marked" // compare with type coercion!
|
||||
@@ -92,9 +75,10 @@ export default class HistoryNavigationButton extends ButtonFromNoteWidget {
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items,
|
||||
selectMenuItemHandler: (item: any) => {
|
||||
if (item && item.idx && this.webContents) {
|
||||
this.webContents.goToIndex(item.idx);
|
||||
selectMenuItemHandler: (item: MenuCommandItem<string>) => {
|
||||
if (item && item.command && this.webContents) {
|
||||
const idx = parseInt(item.command, 10);
|
||||
this.webContents.navigationHistory.goToIndex(idx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import options from "../../services/options.js";
|
||||
import splitService from "../../services/resizer.js";
|
||||
import CommandButtonWidget from "./command_button.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
|
||||
export default class LeftPaneToggleWidget extends CommandButtonWidget {
|
||||
private currentLeftPaneVisible: boolean;
|
||||
|
||||
constructor(isHorizontalLayout: boolean) {
|
||||
super();
|
||||
|
||||
this.currentLeftPaneVisible = options.is("leftPaneVisible");
|
||||
|
||||
this.class(isHorizontalLayout ? "toggle-button" : "launcher-button");
|
||||
|
||||
this.settings.icon = () => {
|
||||
if (options.get("layoutOrientation") === "horizontal") {
|
||||
return "bx-sidebar";
|
||||
}
|
||||
|
||||
return this.currentLeftPaneVisible ? "bx-chevrons-left" : "bx-chevrons-right";
|
||||
};
|
||||
|
||||
this.settings.title = () => (this.currentLeftPaneVisible ? t("left_pane_toggle.hide_panel") : t("left_pane_toggle.show_panel"));
|
||||
|
||||
this.settings.command = () => (this.currentLeftPaneVisible ? "hideLeftPane" : "showLeftPane");
|
||||
|
||||
if (isHorizontalLayout) {
|
||||
this.settings.titlePlacement = "bottom";
|
||||
}
|
||||
}
|
||||
|
||||
refreshIcon() {
|
||||
super.refreshIcon();
|
||||
splitService.setupLeftPaneResizer(this.currentLeftPaneVisible);
|
||||
}
|
||||
|
||||
setLeftPaneVisibilityEvent({ leftPaneVisible }: EventData<"setLeftPaneVisibility">) {
|
||||
this.currentLeftPaneVisible = leftPaneVisible ?? !this.currentLeftPaneVisible;
|
||||
this.refreshIcon();
|
||||
}
|
||||
}
|
||||
29
apps/client/src/widgets/buttons/left_pane_toggle.tsx
Normal file
29
apps/client/src/widgets/buttons/left_pane_toggle.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import options from "../../services/options";
|
||||
import { t } from "../../services/i18n";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import resizer from "../../services/resizer";
|
||||
|
||||
export default function LeftPaneToggle({ isHorizontalLayout }: { isHorizontalLayout: boolean }) {
|
||||
const [ currentLeftPaneVisible, setCurrentLeftPaneVisible ] = useState(options.is("leftPaneVisible"));
|
||||
|
||||
useTriliumEvent("setLeftPaneVisibility", ({ leftPaneVisible }) => {
|
||||
setCurrentLeftPaneVisible(leftPaneVisible ?? !currentLeftPaneVisible);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
resizer.setupLeftPaneResizer(currentLeftPaneVisible);
|
||||
}, [ currentLeftPaneVisible ]);
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
className={`${isHorizontalLayout ? "toggle-button" : "launcher-button"}`}
|
||||
text={currentLeftPaneVisible ? t("left_pane_toggle.hide_panel") : t("left_pane_toggle.show_panel")}
|
||||
triggerCommand={currentLeftPaneVisible ? "hideLeftPane" : "showLeftPane"}
|
||||
icon={isHorizontalLayout
|
||||
? "bx bx-sidebar"
|
||||
: (currentLeftPaneVisible ? "bx bx-chevrons-left" : "bx bx-chevrons-right" )}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import branchService from "../../services/branches.js";
|
||||
import dialogService from "../../services/dialog.js";
|
||||
import server from "../../services/server.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import ws from "../../services/ws.js";
|
||||
import appContext, { type EventData } from "../../components/app_context.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import type { FAttachmentRow } from "../../entities/fattachment.js";
|
||||
|
||||
// TODO: Deduplicate with server
|
||||
interface ConvertToAttachmentResponse {
|
||||
attachment: FAttachmentRow;
|
||||
}
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="dropdown note-actions">
|
||||
<style>
|
||||
.note-actions {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.note-actions .dropdown-menu {
|
||||
min-width: 15em;
|
||||
}
|
||||
|
||||
.note-actions .dropdown-item .bx {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 120%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover {
|
||||
color: var(--muted-text-color) !important;
|
||||
background-color: transparent !important;
|
||||
pointer-events: none; /* makes it unclickable */
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
||||
class="icon-action bx bx-dots-vertical-rounded"></button>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<li data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">
|
||||
<span class="bx bx-paperclip"></span> ${t("note_actions.convert_into_attachment")}
|
||||
</li>
|
||||
|
||||
<li data-trigger-command="renderActiveNote" class="dropdown-item render-note-button">
|
||||
<span class="bx bx-extension"></span> ${t("note_actions.re_render_note")}<kbd data-command="renderActiveNote"></kbd>
|
||||
</li>
|
||||
|
||||
<li data-trigger-command="findInText" class="dropdown-item find-in-text-button">
|
||||
<span class='bx bx-search'></span> ${t("note_actions.search_in_note")}<kbd data-command="findInText"></kbd>
|
||||
</li>
|
||||
|
||||
<li data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button">
|
||||
<span class="bx bx-printer"></span> ${t("note_actions.print_note")}<kbd data-command="printActiveNote"></kbd>
|
||||
</li>
|
||||
|
||||
<li data-trigger-command="exportAsPdf" class="dropdown-item export-as-pdf-button">
|
||||
<span class="bx bxs-file-pdf"></span> ${t("note_actions.print_pdf")}<kbd data-command="exportAsPdf"></kbd>
|
||||
</li>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
|
||||
<li class="dropdown-item import-files-button"><span class="bx bx-import"></span> ${t("note_actions.import_files")}</li>
|
||||
|
||||
<li class="dropdown-item export-note-button"><span class="bx bx-export"></span> ${t("note_actions.export_note")}</li>
|
||||
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
|
||||
|
||||
<li data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button" title="${t("note_actions.open_note_externally_title")}">
|
||||
<span class="bx bx-file-find"></span> ${t("note_actions.open_note_externally")}<kbd data-command="openNoteExternally"></kbd>
|
||||
</li>
|
||||
|
||||
<li data-trigger-command="openNoteCustom" class="dropdown-item open-note-custom-button">
|
||||
<span class="bx bx-customize"></span> ${t("note_actions.open_note_custom")}<kbd data-command="openNoteCustom"></kbd>
|
||||
</li>
|
||||
|
||||
<li data-trigger-command="showNoteSource" class="dropdown-item show-source-button">
|
||||
<span class="bx bx-code"></span> ${t("note_actions.note_source")}<kbd data-command="showNoteSource"></kbd>
|
||||
</li>
|
||||
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
|
||||
<li data-trigger-command="forceSaveRevision" class="dropdown-item save-revision-button">
|
||||
<span class="bx bx-save"></span> ${t("note_actions.save_revision")}<kbd data-command="forceSaveRevision"></kbd>
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item delete-note-button"><span class="bx bx-trash destructive-action-icon"></span> ${t("note_actions.delete_note")}</li>
|
||||
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
|
||||
<li data-trigger-command="showAttachments" class="dropdown-item show-attachments-button">
|
||||
<span class="bx bx-paperclip"></span> ${t("note_actions.note_attachments")}<kbd data-command="showAttachments"></kbd>
|
||||
</li>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class NoteActionsWidget extends NoteContextAwareWidget {
|
||||
|
||||
private $convertNoteIntoAttachmentButton!: JQuery<HTMLElement>;
|
||||
private $findInTextButton!: JQuery<HTMLElement>;
|
||||
private $printActiveNoteButton!: JQuery<HTMLElement>;
|
||||
private $exportAsPdfButton!: JQuery<HTMLElement>;
|
||||
private $showSourceButton!: JQuery<HTMLElement>;
|
||||
private $showAttachmentsButton!: JQuery<HTMLElement>;
|
||||
private $renderNoteButton!: JQuery<HTMLElement>;
|
||||
private $saveRevisionButton!: JQuery<HTMLElement>;
|
||||
private $exportNoteButton!: JQuery<HTMLElement>;
|
||||
private $importNoteButton!: JQuery<HTMLElement>;
|
||||
private $openNoteExternallyButton!: JQuery<HTMLElement>;
|
||||
private $openNoteCustomButton!: JQuery<HTMLElement>;
|
||||
private $deleteNoteButton!: JQuery<HTMLElement>;
|
||||
|
||||
isEnabled() {
|
||||
return this.note?.type !== "launcher";
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$widget.on("show.bs.dropdown", () => {
|
||||
if (this.note) {
|
||||
this.refreshVisibility(this.note);
|
||||
}
|
||||
});
|
||||
|
||||
this.$convertNoteIntoAttachmentButton = this.$widget.find("[data-trigger-command='convertNoteIntoAttachment']");
|
||||
this.$findInTextButton = this.$widget.find(".find-in-text-button");
|
||||
this.$printActiveNoteButton = this.$widget.find(".print-active-note-button");
|
||||
this.$exportAsPdfButton = this.$widget.find(".export-as-pdf-button");
|
||||
this.$showSourceButton = this.$widget.find(".show-source-button");
|
||||
this.$showAttachmentsButton = this.$widget.find(".show-attachments-button");
|
||||
this.$renderNoteButton = this.$widget.find(".render-note-button");
|
||||
this.$saveRevisionButton = this.$widget.find(".save-revision-button");
|
||||
|
||||
this.$exportNoteButton = this.$widget.find(".export-note-button");
|
||||
this.$exportNoteButton.on("click", () => {
|
||||
if (this.$exportNoteButton.hasClass("disabled") || !this.noteContext?.notePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.triggerCommand("showExportDialog", {
|
||||
notePath: this.noteContext.notePath,
|
||||
defaultType: "single"
|
||||
});
|
||||
});
|
||||
|
||||
this.$importNoteButton = this.$widget.find(".import-files-button");
|
||||
this.$importNoteButton.on("click", () => {
|
||||
if (this.noteId) {
|
||||
this.triggerCommand("showImportDialog", { noteId: this.noteId });
|
||||
}
|
||||
});
|
||||
|
||||
this.$widget.on("click", ".dropdown-item", () => this.$widget.find("[data-bs-toggle='dropdown']").dropdown("toggle"));
|
||||
|
||||
this.$openNoteExternallyButton = this.$widget.find(".open-note-externally-button");
|
||||
this.$openNoteCustomButton = this.$widget.find(".open-note-custom-button");
|
||||
|
||||
this.$deleteNoteButton = this.$widget.find(".delete-note-button");
|
||||
this.$deleteNoteButton.on("click", () => {
|
||||
if (!this.note || this.note.noteId === "root") {
|
||||
return;
|
||||
}
|
||||
|
||||
branchService.deleteNotes([this.note.getParentBranches()[0].branchId], true);
|
||||
});
|
||||
}
|
||||
|
||||
async refreshVisibility(note: FNote) {
|
||||
const isInOptions = note.noteId.startsWith("_options");
|
||||
|
||||
this.$convertNoteIntoAttachmentButton.toggle(note.isEligibleForConversionToAttachment());
|
||||
|
||||
this.toggleDisabled(this.$findInTextButton, ["text", "code", "book", "mindMap", "doc"].includes(note.type));
|
||||
|
||||
this.toggleDisabled(this.$showAttachmentsButton, !isInOptions);
|
||||
this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type));
|
||||
|
||||
const canPrint = ["text", "code"].includes(note.type);
|
||||
this.toggleDisabled(this.$printActiveNoteButton, canPrint);
|
||||
this.toggleDisabled(this.$exportAsPdfButton, canPrint);
|
||||
this.$exportAsPdfButton.toggleClass("hidden-ext", !utils.isElectron());
|
||||
|
||||
this.$renderNoteButton.toggle(note.type === "render");
|
||||
|
||||
this.toggleDisabled(this.$openNoteExternallyButton, utils.isElectron() && !["search", "book"].includes(note.type));
|
||||
this.toggleDisabled(
|
||||
this.$openNoteCustomButton,
|
||||
utils.isElectron() &&
|
||||
!utils.isMac() && // no implementation for Mac yet
|
||||
!["search", "book"].includes(note.type)
|
||||
);
|
||||
|
||||
// I don't want to handle all special notes like this, but intuitively user might want to export content of backend log
|
||||
this.toggleDisabled(this.$exportNoteButton, !["_backendLog"].includes(note.noteId) && !isInOptions);
|
||||
|
||||
this.toggleDisabled(this.$importNoteButton, !["search"].includes(note.type) && !isInOptions);
|
||||
this.toggleDisabled(this.$deleteNoteButton, !isInOptions);
|
||||
this.toggleDisabled(this.$saveRevisionButton, !isInOptions);
|
||||
}
|
||||
|
||||
async convertNoteIntoAttachmentCommand() {
|
||||
if (!this.note || !(await dialogService.confirm(t("note_actions.convert_into_attachment_prompt", { title: this.note.title })))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { attachment: newAttachment } = await server.post<ConvertToAttachmentResponse>(`notes/${this.noteId}/convert-to-attachment`);
|
||||
|
||||
if (!newAttachment) {
|
||||
toastService.showMessage(t("note_actions.convert_into_attachment_failed", { title: this.note.title }));
|
||||
return;
|
||||
}
|
||||
|
||||
toastService.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title }));
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, {
|
||||
viewScope: {
|
||||
viewMode: "attachments",
|
||||
attachmentId: newAttachment.attachmentId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleDisabled($el: JQuery<HTMLElement>, enable: boolean) {
|
||||
if (enable) {
|
||||
$el.removeAttr("disabled");
|
||||
} else {
|
||||
$el.attr("disabled", "disabled");
|
||||
}
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (loadResults.isNoteReloaded(this.noteId)) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import CommandButtonWidget from "./command_button.js";
|
||||
|
||||
export default class RevisionsButton extends CommandButtonWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.icon("bx-history").title(t("revisions_button.note_revisions")).command("showRevisions").titlePlacement("bottom").class("icon-action");
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled() && !["launcher", "doc"].includes(this.note?.type ?? "");
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import OnClickButtonWidget from "./onclick_button.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import attributeService from "../../services/attributes.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import LoadResults from "../../services/load_results.js";
|
||||
import type { AttributeRow } from "../../services/load_results.js";
|
||||
|
||||
export default class ShowHighlightsListWidgetButton extends OnClickButtonWidget {
|
||||
isEnabled(): boolean {
|
||||
return Boolean(super.isEnabled() && this.note && this.note.type === "text" && this.noteContext?.viewScope?.viewMode === "default");
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.icon("bx-bookmarks")
|
||||
.title(t("show_highlights_list_widget_button.show_highlights_list"))
|
||||
.titlePlacement("bottom")
|
||||
.onClick(() => {
|
||||
if (this.noteContext?.viewScope && this.noteId) {
|
||||
this.noteContext.viewScope.highlightsListTemporarilyHidden = false;
|
||||
appContext.triggerEvent("showHighlightsListWidget", { noteId: this.noteId });
|
||||
}
|
||||
this.toggleInt(false);
|
||||
});
|
||||
}
|
||||
|
||||
async refreshWithNote(): Promise<void> {
|
||||
if (this.noteContext?.viewScope) {
|
||||
this.toggleInt(this.noteContext.viewScope.highlightsListTemporarilyHidden);
|
||||
}
|
||||
}
|
||||
|
||||
async reEvaluateHighlightsListWidgetVisibilityEvent({ noteId }: { noteId: string }): Promise<void> {
|
||||
if (noteId === this.noteId) {
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({ loadResults }: { loadResults: LoadResults }): Promise<void> {
|
||||
if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) {
|
||||
await this.refresh();
|
||||
} else if (
|
||||
loadResults
|
||||
.getAttributeRows()
|
||||
.find((attr: AttributeRow) =>
|
||||
attr.type === "label" &&
|
||||
(attr.name?.toLowerCase().includes("readonly") || attr.name === "hideHighlightWidget") &&
|
||||
this.note &&
|
||||
attributeService.isAffecting(attr, this.note)
|
||||
)
|
||||
) {
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async noteTypeMimeChangedEvent({ noteId }: { noteId: string }): Promise<void> {
|
||||
if (this.isNote(noteId)) {
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import OnClickButtonWidget from "./onclick_button.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import attributeService from "../../services/attributes.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import LoadResults from "../../services/load_results.js";
|
||||
import type { AttributeRow } from "../../services/load_results.js";
|
||||
|
||||
export default class ShowTocWidgetButton extends OnClickButtonWidget {
|
||||
isEnabled(): boolean {
|
||||
return Boolean(super.isEnabled() && this.note && this.note.type === "text" && this.noteContext?.viewScope?.viewMode === "default");
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.icon("bx-tn-toc")
|
||||
.title(t("show_toc_widget_button.show_toc"))
|
||||
.titlePlacement("bottom")
|
||||
.onClick(() => {
|
||||
if (this.noteContext?.viewScope && this.noteId) {
|
||||
this.noteContext.viewScope.tocTemporarilyHidden = false;
|
||||
appContext.triggerEvent("showTocWidget", { noteId: this.noteId });
|
||||
}
|
||||
this.toggleInt(false);
|
||||
});
|
||||
}
|
||||
|
||||
async refreshWithNote(): Promise<void> {
|
||||
if (this.noteContext?.viewScope) {
|
||||
this.toggleInt(this.noteContext.viewScope.tocTemporarilyHidden);
|
||||
}
|
||||
}
|
||||
|
||||
async reEvaluateTocWidgetVisibilityEvent({ noteId }: { noteId: string }): Promise<void> {
|
||||
if (noteId === this.noteId) {
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({ loadResults }: { loadResults: LoadResults }): Promise<void> {
|
||||
if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) {
|
||||
await this.refresh();
|
||||
} else if (
|
||||
loadResults
|
||||
.getAttributeRows()
|
||||
.find((attr: AttributeRow) =>
|
||||
attr.type === "label" &&
|
||||
(attr.name?.toLowerCase().includes("readonly") || attr.name === "toc") &&
|
||||
this.note &&
|
||||
attributeService.isAffecting(attr, this.note)
|
||||
)
|
||||
) {
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async noteTypeMimeChangedEvent({ noteId }: { noteId: string }): Promise<void> {
|
||||
if (this.isNote(noteId)) {
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import utils from "../../services/utils.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div style="display: none;">
|
||||
<style>
|
||||
.global-menu-button-update-available-button {
|
||||
width: 21px !important;
|
||||
height: 21px !important;
|
||||
padding: 0 !important;
|
||||
|
||||
border-radius: var(--button-border-radius);
|
||||
transform: scale(0.9);
|
||||
border: none;
|
||||
opacity: 0.8;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.global-menu-button-wrapper:hover .global-menu-button-update-available-button {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<span class="bx bx-sync global-menu-button-update-available-button" title="${t("update_available.update_available")}"></span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class UpdateAvailableWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
}
|
||||
|
||||
updateVersionStatus(latestVersion: string) {
|
||||
this.$widget.toggle(utils.isUpdateAvailable(latestVersion, glob.triliumVersion));
|
||||
}
|
||||
}
|
||||
26
apps/client/src/widgets/close_zen_button.css
Normal file
26
apps/client/src/widgets/close_zen_button.css
Normal file
@@ -0,0 +1,26 @@
|
||||
:root {
|
||||
--zen-button-size: 32px;
|
||||
}
|
||||
|
||||
.close-zen-container {
|
||||
width: var(--zen-button-size);
|
||||
height: var(--zen-button-size);
|
||||
}
|
||||
|
||||
body.zen .close-zen-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
z-index: 9999;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
body.zen.mobile .close-zen-container {
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
body.zen.electron:not(.platform-darwin):not(.native-titlebar) .close-zen-container {
|
||||
left: calc(env(titlebar-area-width) - var(--zen-button-size) - 2px);
|
||||
right: unset;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import BasicWidget from "./basic_widget.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const TPL = /*html*/`\
|
||||
<div class="close-zen-container">
|
||||
<button class="button-widget bx icon-action bxs-yin-yang"
|
||||
data-trigger-command="toggleZenMode"
|
||||
title="${t("zen_mode.button_exit")}"
|
||||
/>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--zen-button-size: 32px;
|
||||
}
|
||||
|
||||
.close-zen-container {
|
||||
display: none;
|
||||
width: var(--zen-button-size);
|
||||
height: var(--zen-button-size);
|
||||
}
|
||||
|
||||
body.zen .close-zen-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
z-index: 9999;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
body.zen.mobile .close-zen-container {
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
body.zen.electron:not(.platform-darwin):not(.native-titlebar) .close-zen-container {
|
||||
left: calc(env(titlebar-area-width) - var(--zen-button-size) - 2px);
|
||||
right: unset;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class CloseZenButton extends BasicWidget {
|
||||
|
||||
doRender(): void {
|
||||
this.$widget = $(TPL);
|
||||
}
|
||||
|
||||
zenChangedEvent() {
|
||||
this.toggleInt(true);
|
||||
}
|
||||
|
||||
}
|
||||
25
apps/client/src/widgets/close_zen_button.tsx
Normal file
25
apps/client/src/widgets/close_zen_button.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useState } from "preact/hooks";
|
||||
import { t } from "../services/i18n";
|
||||
import ActionButton from "./react/ActionButton";
|
||||
import { useTriliumEvent } from "./react/hooks";
|
||||
import "./close_zen_button.css";
|
||||
|
||||
export default function CloseZenModeButton() {
|
||||
const [ zenModeEnabled, setZenModeEnabled ] = useState(false);
|
||||
|
||||
useTriliumEvent("zenModeChanged", ({ isEnabled }) => {
|
||||
setZenModeEnabled(isEnabled);
|
||||
});
|
||||
|
||||
return (
|
||||
<div class={`close-zen-container ${!zenModeEnabled ? "hidden-ext" : ""}`}>
|
||||
{zenModeEnabled && (
|
||||
<ActionButton
|
||||
icon="bx bxs-yin-yang"
|
||||
triggerCommand="toggleZenMode"
|
||||
text={t("zen_mode.button_exit")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,388 +0,0 @@
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import keyboardActionsService from "../../services/keyboard_actions.js";
|
||||
import attributeService from "../../services/attributes.js";
|
||||
import type CommandButtonWidget from "../buttons/command_button.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import type { NoteType } from "../../entities/fnote.js";
|
||||
import type { EventData, EventNames } from "../../components/app_context.js";
|
||||
import type NoteActionsWidget from "../buttons/note_actions.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="ribbon-container">
|
||||
<style>
|
||||
.ribbon-container {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.ribbon-top-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ribbon-tab-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
margin-left: 10px;
|
||||
flex-grow: 1;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
.ribbon-tab-title {
|
||||
color: var(--muted-text-color);
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
min-width: 24px;
|
||||
flex-basis: 24px;
|
||||
max-width: max-content;
|
||||
flex-grow: 10;
|
||||
}
|
||||
|
||||
.ribbon-tab-title .bx {
|
||||
font-size: 150%;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.ribbon-tab-title.active {
|
||||
color: var(--main-text-color);
|
||||
border-bottom: 3px solid var(--main-text-color);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ribbon-tab-title:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ribbon-tab-title:hover {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.ribbon-tab-title:first-of-type {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.ribbon-tab-spacer {
|
||||
flex-basis: 0;
|
||||
min-width: 0;
|
||||
max-width: 35px;
|
||||
flex-grow: 1;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
.ribbon-tab-spacer:last-of-type {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
min-width: 0;
|
||||
max-width: 10000px;
|
||||
}
|
||||
|
||||
.ribbon-button-container {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.ribbon-button-container > * {
|
||||
position: relative;
|
||||
top: -3px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.ribbon-body {
|
||||
display: none;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
margin-left: 10px;
|
||||
margin-right: 5px; /* needs to have this value so that the bottom border is the same width as the top one */
|
||||
}
|
||||
|
||||
.ribbon-body.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ribbon-tab-title-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ribbon-tab-title.active .ribbon-tab-title-label {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="ribbon-top-row">
|
||||
<div class="ribbon-tab-container"></div>
|
||||
<div class="ribbon-button-container"></div>
|
||||
</div>
|
||||
|
||||
<div class="ribbon-body-container"></div>
|
||||
</div>`;
|
||||
|
||||
type ButtonWidget = (CommandButtonWidget | NoteActionsWidget);
|
||||
|
||||
export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
|
||||
private lastActiveComponentId?: string | null;
|
||||
private lastNoteType?: NoteType;
|
||||
|
||||
private ribbonWidgets: NoteContextAwareWidget[];
|
||||
private buttonWidgets: ButtonWidget[];
|
||||
private $tabContainer!: JQuery<HTMLElement>;
|
||||
private $buttonContainer!: JQuery<HTMLElement>;
|
||||
private $bodyContainer!: JQuery<HTMLElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.contentSized();
|
||||
this.ribbonWidgets = [];
|
||||
this.buttonWidgets = [];
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled() && this.noteContext?.viewScope?.viewMode === "default";
|
||||
}
|
||||
|
||||
ribbon(widget: NoteContextAwareWidget) {
|
||||
// TODO: Base class
|
||||
super.child(widget);
|
||||
|
||||
this.ribbonWidgets.push(widget);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
button(widget: ButtonWidget) {
|
||||
super.child(widget);
|
||||
|
||||
this.buttonWidgets.push(widget);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$tabContainer = this.$widget.find(".ribbon-tab-container");
|
||||
this.$buttonContainer = this.$widget.find(".ribbon-button-container");
|
||||
this.$bodyContainer = this.$widget.find(".ribbon-body-container");
|
||||
|
||||
for (const ribbonWidget of this.ribbonWidgets) {
|
||||
this.$bodyContainer.append($('<div class="ribbon-body">').attr("data-ribbon-component-id", ribbonWidget.componentId).append(ribbonWidget.render()));
|
||||
}
|
||||
|
||||
for (const buttonWidget of this.buttonWidgets) {
|
||||
this.$buttonContainer.append(buttonWidget.render());
|
||||
}
|
||||
|
||||
this.$tabContainer.on("click", ".ribbon-tab-title", (e) => {
|
||||
const $ribbonTitle = $(e.target).closest(".ribbon-tab-title");
|
||||
|
||||
this.toggleRibbonTab($ribbonTitle);
|
||||
});
|
||||
}
|
||||
|
||||
toggleRibbonTab($ribbonTitle: JQuery<HTMLElement>, refreshActiveTab = true) {
|
||||
const activate = !$ribbonTitle.hasClass("active");
|
||||
|
||||
this.$tabContainer.find(".ribbon-tab-title").removeClass("active");
|
||||
this.$bodyContainer.find(".ribbon-body").removeClass("active");
|
||||
|
||||
if (activate) {
|
||||
const ribbonComponendId = $ribbonTitle.attr("data-ribbon-component-id");
|
||||
|
||||
const wasAlreadyActive = this.lastActiveComponentId === ribbonComponendId;
|
||||
|
||||
this.lastActiveComponentId = ribbonComponendId;
|
||||
|
||||
this.$tabContainer.find(`.ribbon-tab-title[data-ribbon-component-id="${ribbonComponendId}"]`).addClass("active");
|
||||
this.$bodyContainer.find(`.ribbon-body[data-ribbon-component-id="${ribbonComponendId}"]`).addClass("active");
|
||||
|
||||
const activeChild = this.getActiveRibbonWidget();
|
||||
|
||||
if (activeChild && (refreshActiveTab || !wasAlreadyActive) && this.noteContext && this.notePath) {
|
||||
const handleEventPromise = activeChild.handleEvent("noteSwitched", { noteContext: this.noteContext, notePath: this.notePath });
|
||||
|
||||
if (refreshActiveTab) {
|
||||
if (handleEventPromise) {
|
||||
handleEventPromise.then(() => (activeChild as any).focus?.()); // TODO: Base class
|
||||
} else {
|
||||
// TODO: Base class
|
||||
(activeChild as any).focus?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.lastActiveComponentId = null;
|
||||
}
|
||||
}
|
||||
|
||||
async noteSwitched() {
|
||||
this.lastActiveComponentId = null;
|
||||
|
||||
await super.noteSwitched();
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote, noExplicitActivation = false) {
|
||||
this.lastNoteType = note.type;
|
||||
|
||||
let $ribbonTabToActivate, $lastActiveRibbon;
|
||||
|
||||
this.$tabContainer.empty();
|
||||
|
||||
for (const ribbonWidget of this.ribbonWidgets) {
|
||||
// TODO: Base class for ribbon widget
|
||||
const ret = await (ribbonWidget as any).getTitle(note);
|
||||
|
||||
if (!ret.show) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const $ribbonTitle = $('<div class="ribbon-tab-title">')
|
||||
.attr("data-ribbon-component-id", ribbonWidget.componentId)
|
||||
.attr("data-ribbon-component-name", (ribbonWidget as any).name as string) // TODO: base class for ribbon widgets
|
||||
.append(
|
||||
$('<span class="ribbon-tab-title-icon">')
|
||||
.addClass(ret.icon)
|
||||
.attr("title", ret.title)
|
||||
.attr("data-toggle-command", (ribbonWidget as any).toggleCommand)
|
||||
) // TODO: base class
|
||||
.append(" ")
|
||||
.append($('<span class="ribbon-tab-title-label">').text(ret.title));
|
||||
|
||||
this.$tabContainer.append($ribbonTitle);
|
||||
this.$tabContainer.append('<div class="ribbon-tab-spacer">');
|
||||
|
||||
if (ret.activate && !this.lastActiveComponentId && !$ribbonTabToActivate && !noExplicitActivation) {
|
||||
$ribbonTabToActivate = $ribbonTitle;
|
||||
}
|
||||
|
||||
if (this.lastActiveComponentId === ribbonWidget.componentId) {
|
||||
$lastActiveRibbon = $ribbonTitle;
|
||||
}
|
||||
}
|
||||
|
||||
keyboardActionsService.getActions().then((actions) => {
|
||||
this.$tabContainer.find(".ribbon-tab-title-icon").tooltip({
|
||||
title: () => {
|
||||
const toggleCommandName = $(this).attr("data-toggle-command");
|
||||
const action = actions.find((act) => act.actionName === toggleCommandName);
|
||||
const title = $(this).attr("data-title");
|
||||
|
||||
if (action?.effectiveShortcuts && action.effectiveShortcuts.length > 0) {
|
||||
return `${title} (${action.effectiveShortcuts.join(", ")})`;
|
||||
} else {
|
||||
return title ?? "";
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!$ribbonTabToActivate) {
|
||||
$ribbonTabToActivate = $lastActiveRibbon;
|
||||
}
|
||||
|
||||
if ($ribbonTabToActivate) {
|
||||
this.toggleRibbonTab($ribbonTabToActivate, false);
|
||||
} else {
|
||||
this.$bodyContainer.find(".ribbon-body").removeClass("active");
|
||||
}
|
||||
}
|
||||
|
||||
isRibbonTabActive(name: string) {
|
||||
const $ribbonComponent = this.$widget.find(`.ribbon-tab-title[data-ribbon-component-name='${name}']`);
|
||||
|
||||
return $ribbonComponent.hasClass("active");
|
||||
}
|
||||
|
||||
ensureOwnedAttributesAreOpen(ntxId: string | null | undefined) {
|
||||
if (ntxId && this.isNoteContext(ntxId) && !this.isRibbonTabActive("ownedAttributes")) {
|
||||
this.toggleRibbonTabWithName("ownedAttributes", ntxId);
|
||||
}
|
||||
}
|
||||
|
||||
addNewLabelEvent({ ntxId }: EventData<"addNewLabel">) {
|
||||
this.ensureOwnedAttributesAreOpen(ntxId);
|
||||
}
|
||||
|
||||
addNewRelationEvent({ ntxId }: EventData<"addNewRelation">) {
|
||||
this.ensureOwnedAttributesAreOpen(ntxId);
|
||||
}
|
||||
|
||||
toggleRibbonTabWithName(name: string, ntxId?: string) {
|
||||
if (!this.isNoteContext(ntxId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const $ribbonComponent = this.$widget.find(`.ribbon-tab-title[data-ribbon-component-name='${name}']`);
|
||||
|
||||
if ($ribbonComponent) {
|
||||
this.toggleRibbonTab($ribbonComponent);
|
||||
}
|
||||
}
|
||||
|
||||
handleEvent<T extends EventNames>(name: T, data: EventData<T>) {
|
||||
const PREFIX = "toggleRibbonTab";
|
||||
|
||||
if (name.startsWith(PREFIX)) {
|
||||
let componentName = name.substr(PREFIX.length);
|
||||
componentName = componentName[0].toLowerCase() + componentName.substr(1);
|
||||
|
||||
this.toggleRibbonTabWithName(componentName, (data as any).ntxId);
|
||||
} else {
|
||||
return super.handleEvent(name, data);
|
||||
}
|
||||
}
|
||||
|
||||
async handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>) {
|
||||
if (["activeContextChanged", "setNoteContext"].includes(name)) {
|
||||
// won't trigger .refresh();
|
||||
await super.handleEventInChildren("setNoteContext", data as EventData<"activeContextChanged" | "setNoteContext">);
|
||||
} else if (this.isEnabled() || name === "initialRenderComplete") {
|
||||
const activeRibbonWidget = this.getActiveRibbonWidget();
|
||||
|
||||
// forward events only to active ribbon tab, inactive ones don't need to be updated
|
||||
if (activeRibbonWidget) {
|
||||
await activeRibbonWidget.handleEvent(name, data);
|
||||
}
|
||||
|
||||
for (const buttonWidget of this.buttonWidgets) {
|
||||
await buttonWidget.handleEvent(name, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (!this.note) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.noteId && loadResults.isNoteReloaded(this.noteId) && this.lastNoteType !== this.note.type) {
|
||||
// note type influences the list of available ribbon tabs the most
|
||||
// check for the type is so that we don't update on each title rename
|
||||
this.lastNoteType = this.note.type;
|
||||
|
||||
this.refresh();
|
||||
} else if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) {
|
||||
this.refreshWithNote(this.note, true);
|
||||
}
|
||||
}
|
||||
|
||||
async noteTypeMimeChangedEvent() {
|
||||
// We are ignoring the event which triggers a refresh since it is usually already done by a different
|
||||
// event and causing a race condition in which the items appear twice.
|
||||
}
|
||||
|
||||
/**
|
||||
* Executed as soon as the user presses the "Edit" floating button in a read-only text note.
|
||||
*
|
||||
* <p>
|
||||
* We need to refresh the ribbon for cases such as the classic editor which relies on the read-only state.
|
||||
*/
|
||||
readOnlyTemporarilyDisabledEvent() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
getActiveRibbonWidget() {
|
||||
return this.ribbonWidgets.find((ch) => ch.componentId === this.lastActiveComponentId);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget.js";
|
||||
import Modal from "../react/Modal.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import { formatDateTime } from "../../utils/formatters.js";
|
||||
@@ -8,11 +7,11 @@ import openService from "../../services/open.js";
|
||||
import { useState } from "preact/hooks";
|
||||
import type { CSSProperties } from "preact/compat";
|
||||
import type { AppInfo } from "@triliumnext/commons";
|
||||
import useTriliumEvent from "../react/hooks.jsx";
|
||||
import { useTriliumEvent } from "../react/hooks.jsx";
|
||||
|
||||
function AboutDialogComponent() {
|
||||
let [appInfo, setAppInfo] = useState<AppInfo | null>(null);
|
||||
let [shown, setShown] = useState(false);
|
||||
export default function AboutDialog() {
|
||||
const [appInfo, setAppInfo] = useState<AppInfo | null>(null);
|
||||
const [shown, setShown] = useState(false);
|
||||
const forceWordBreak: CSSProperties = { wordBreak: "break-all" };
|
||||
|
||||
useTriliumEvent("openAboutDialog", () => setShown(true));
|
||||
@@ -77,16 +76,8 @@ function DirectoryLink({ directory, style }: { directory: string, style?: CSSPro
|
||||
openService.openDirectory(directory);
|
||||
};
|
||||
|
||||
return <a className="tn-link" href="#" onClick={onClick} style={style}></a>
|
||||
return <a className="tn-link" href="#" onClick={onClick} style={style}>{directory}</a>
|
||||
} else {
|
||||
return <span style={style}>{directory}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
export default class AboutDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <AboutDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { t } from "../../services/i18n";
|
||||
import Modal from "../react/Modal";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import Button from "../react/Button";
|
||||
import FormRadioGroup from "../react/FormRadioGroup";
|
||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||
@@ -11,11 +10,11 @@ import { default as TextTypeWidget } from "../type_widgets/editable_text.js";
|
||||
import { logError } from "../../services/ws";
|
||||
import FormGroup from "../react/FormGroup.js";
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
|
||||
type LinkType = "reference-link" | "external-link" | "hyper-link";
|
||||
|
||||
function AddLinkDialogComponent() {
|
||||
export default function AddLinkDialog() {
|
||||
const [ textTypeWidget, setTextTypeWidget ] = useState<TextTypeWidget>();
|
||||
const initialText = useRef<string>();
|
||||
const [ linkTitle, setLinkTitle ] = useState("");
|
||||
@@ -30,6 +29,14 @@ function AddLinkDialogComponent() {
|
||||
setShown(true);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (hasSelection) {
|
||||
setLinkType("hyper-link");
|
||||
} else {
|
||||
setLinkType("reference-link");
|
||||
}
|
||||
}, [ hasSelection ])
|
||||
|
||||
async function setDefaultLinkTitle(noteId: string) {
|
||||
const noteTitle = await tree.getNoteTitle(noteId);
|
||||
setLinkTitle(noteTitle);
|
||||
@@ -107,7 +114,7 @@ function AddLinkDialogComponent() {
|
||||
}}
|
||||
show={shown}
|
||||
>
|
||||
<FormGroup label={t("add_link.note")}>
|
||||
<FormGroup label={t("add_link.note")} name="note">
|
||||
<NoteAutocomplete
|
||||
inputRef={autocompleteRef}
|
||||
onChange={setSuggestion}
|
||||
@@ -152,11 +159,3 @@ function AddLinkDialogComponent() {
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default class AddLinkDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <AddLinkDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,15 +4,14 @@ import { t } from "../../services/i18n.js";
|
||||
import server from "../../services/server.js";
|
||||
import toast from "../../services/toast.js";
|
||||
import Modal from "../react/Modal.jsx";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import tree from "../../services/tree.js";
|
||||
import Button from "../react/Button.jsx";
|
||||
import FormGroup from "../react/FormGroup.js";
|
||||
import useTriliumEvent from "../react/hooks.jsx";
|
||||
import { useTriliumEvent } from "../react/hooks.jsx";
|
||||
import FBranch from "../../entities/fbranch.js";
|
||||
|
||||
function BranchPrefixDialogComponent() {
|
||||
export default function BranchPrefixDialog() {
|
||||
const [ shown, setShown ] = useState(false);
|
||||
const [ branch, setBranch ] = useState<FBranch>();
|
||||
const [ prefix, setPrefix ] = useState(branch?.prefix ?? "");
|
||||
@@ -64,7 +63,7 @@ function BranchPrefixDialogComponent() {
|
||||
footer={<Button text={t("branch_prefix.save")} />}
|
||||
show={shown}
|
||||
>
|
||||
<FormGroup label={t("branch_prefix.prefix")}>
|
||||
<FormGroup label={t("branch_prefix.prefix")} name="prefix">
|
||||
<div class="input-group">
|
||||
<input class="branch-prefix-input form-control" value={prefix} ref={branchInput}
|
||||
onChange={(e) => setPrefix((e.target as HTMLInputElement).value)} />
|
||||
@@ -75,14 +74,6 @@ function BranchPrefixDialogComponent() {
|
||||
);
|
||||
}
|
||||
|
||||
export default class BranchPrefixDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <BranchPrefixDialogComponent />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function savePrefix(branchId: string, prefix: string) {
|
||||
await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix });
|
||||
toast.showMessage(t("branch_prefix.branch_prefix_saved"));
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useState, useCallback } from "preact/hooks";
|
||||
import { t } from "../../services/i18n";
|
||||
import Modal from "../react/Modal";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import "./bulk_actions.css";
|
||||
import { BulkActionAffectedNotes } from "@triliumnext/commons";
|
||||
import server from "../../services/server";
|
||||
@@ -12,9 +11,9 @@ import toast from "../../services/toast";
|
||||
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
|
||||
import FNote from "../../entities/fnote";
|
||||
import froca from "../../services/froca";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
|
||||
function BulkActionComponent() {
|
||||
export default function BulkActionsDialog() {
|
||||
const [ selectedOrActiveNoteIds, setSelectedOrActiveNoteIds ] = useState<string[]>();
|
||||
const [ bulkActionNote, setBulkActionNote ] = useState<FNote | null>();
|
||||
const [ includeDescendants, setIncludeDescendants ] = useState(false);
|
||||
@@ -51,7 +50,7 @@ function BulkActionComponent() {
|
||||
row.type === "label" && row.name === "action" && row.noteId === "_bulkAction")) {
|
||||
refreshExistingActions();
|
||||
}
|
||||
}, shown);
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -94,7 +93,8 @@ function AvailableActionsList() {
|
||||
<td>{ actionGroup.title }:</td>
|
||||
{actionGroup.actions.map(({ actionName, actionTitle }) =>
|
||||
<Button
|
||||
small text={actionTitle}
|
||||
size="small"
|
||||
text={actionTitle}
|
||||
onClick={() => bulk_action.addAction("_bulkAction", actionName)}
|
||||
/>
|
||||
)}
|
||||
@@ -116,11 +116,3 @@ function ExistingActionsList({ existingActions }: { existingActions?: RenameNote
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
export default class BulkActionsDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <BulkActionComponent />
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
import { useState } from "preact/hooks";
|
||||
import { useMemo, useState } from "preact/hooks";
|
||||
import Button from "../react/Button";
|
||||
import Modal from "../react/Modal";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import { CallToAction, dismissCallToAction, getCallToActions } from "./call_to_action_definitions";
|
||||
import { dismissCallToAction, getCallToActions } from "./call_to_action_definitions";
|
||||
import { t } from "../../services/i18n";
|
||||
|
||||
function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActions: CallToAction[] }) {
|
||||
if (!activeCallToActions.length) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export default function CallToActionDialog() {
|
||||
const activeCallToActions = useMemo(() => getCallToActions(), []);
|
||||
const [ activeIndex, setActiveIndex ] = useState(0);
|
||||
const [ shown, setShown ] = useState(true);
|
||||
const activeItem = activeCallToActions[activeIndex];
|
||||
@@ -21,16 +18,16 @@ function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActi
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
return (activeCallToActions.length &&
|
||||
<Modal
|
||||
className="call-to-action"
|
||||
size="md"
|
||||
title="New features"
|
||||
title={activeItem.title}
|
||||
show={shown}
|
||||
onHidden={() => setShown(false)}
|
||||
footerAlignment="between"
|
||||
footer={<>
|
||||
<Button text="Dismiss" onClick={async () => {
|
||||
<Button text={t("call_to_action.dismiss")} onClick={async () => {
|
||||
await dismissCallToAction(activeItem.id);
|
||||
goToNext();
|
||||
}} />
|
||||
@@ -43,16 +40,7 @@ function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActi
|
||||
)}
|
||||
</>}
|
||||
>
|
||||
<h4>{activeItem.title}</h4>
|
||||
<p>{activeItem.message}</p>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export class CallToActionDialog extends ReactBasicWidget {
|
||||
|
||||
get component() {
|
||||
return <CallToActionDialogComponent activeCallToActions={getCallToActions()} />
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user