mirror of
https://github.com/zadam/trilium.git
synced 2026-01-13 02:42:13 +01:00
Compare commits
546 Commits
static-cor
...
renovate/d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
028334407c | ||
|
|
fcfa64ae52 | ||
|
|
62f5b800b6 | ||
|
|
5b910cce56 | ||
|
|
2c8edb413e | ||
|
|
71d3eb4fde | ||
|
|
7c2340d60e | ||
|
|
24013ef020 | ||
|
|
b572ea0954 | ||
|
|
060257fa06 | ||
|
|
1c6bb0a20e | ||
|
|
1479109582 | ||
|
|
13f4e38f48 | ||
|
|
5cbde8d32a | ||
|
|
f3e3ef2f7d | ||
|
|
0a58f8108a | ||
|
|
768213438a | ||
|
|
00e0eb6f8a | ||
|
|
3abea13d79 | ||
|
|
67ab7f0c1e | ||
|
|
b38e8e27b2 | ||
|
|
a70c103b93 | ||
|
|
b83c3090f7 | ||
|
|
59ee38e7a6 | ||
|
|
890fe5929b | ||
|
|
56cc312565 | ||
|
|
9dfd015a27 | ||
|
|
04618dcdab | ||
|
|
f408e15c32 | ||
|
|
00e60c147c | ||
|
|
ad6fd64226 | ||
|
|
a268507b80 | ||
|
|
9e847f67f2 | ||
|
|
df4992122b | ||
|
|
a325ba7b8f | ||
|
|
26e64ae7d0 | ||
|
|
d793774f51 | ||
|
|
c5f2b5c177 | ||
|
|
b2f782f2a3 | ||
|
|
b65b31ca4d | ||
|
|
64827dcdcf | ||
|
|
96d5d07087 | ||
|
|
ffd0f4727a | ||
|
|
e33cd86d30 | ||
|
|
fc85c23a67 | ||
|
|
a55c8fb210 | ||
|
|
19b865a5b4 | ||
|
|
61d90dda36 | ||
|
|
a4b1f06475 | ||
|
|
49704ea928 | ||
|
|
a73df362d5 | ||
|
|
f42010e22a | ||
|
|
0b85e0fe2d | ||
|
|
c7f0720237 | ||
|
|
cc3031eaad | ||
|
|
2850c7808c | ||
|
|
30bbcd866f | ||
|
|
55b6f322ac | ||
|
|
4518c9bb99 | ||
|
|
c551c863f4 | ||
|
|
3c25f8b4f3 | ||
|
|
d3c37556c3 | ||
|
|
cf60fcd6c1 | ||
|
|
97075ff91b | ||
|
|
ce57a43d90 | ||
|
|
33485369c3 | ||
|
|
7fcd93a61b | ||
|
|
3d4b84c7c4 | ||
|
|
61eb4017dd | ||
|
|
b4df4aaf0d | ||
|
|
244294f699 | ||
|
|
d609ee028e | ||
|
|
8dc8b046fb | ||
|
|
349203e300 | ||
|
|
83a8f07998 | ||
|
|
94d4a307cf | ||
|
|
2e35e0a830 | ||
|
|
ecdb819067 | ||
|
|
e2cf0c6e3e | ||
|
|
5dd600a291 | ||
|
|
df1beb1ffb | ||
|
|
7773059ac0 | ||
|
|
a238fc16b2 | ||
|
|
e298f5ea6f | ||
|
|
a5512267c1 | ||
|
|
f503c4ca6c | ||
|
|
c834c01c8e | ||
|
|
1f72ab9593 | ||
|
|
c7d446f4aa | ||
|
|
fb1530423d | ||
|
|
545464efee | ||
|
|
29d0223fd1 | ||
|
|
00852277f2 | ||
|
|
edfe23d88c | ||
|
|
2b8a7a28d9 | ||
|
|
7f29480237 | ||
|
|
61ac482946 | ||
|
|
de6a6cbb07 | ||
|
|
2dcb003909 | ||
|
|
d5c934a518 | ||
|
|
779909837c | ||
|
|
a4cb375a0f | ||
|
|
e2da8c28ca | ||
|
|
7808848f05 | ||
|
|
7c05109645 | ||
|
|
8f9e89b73b | ||
|
|
861a61a4d8 | ||
|
|
52d4083814 | ||
|
|
0272189b22 | ||
|
|
ddba0e823c | ||
|
|
be81acb9e7 | ||
|
|
3bb97385c9 | ||
|
|
a72cec0494 | ||
|
|
cb02198c6f | ||
|
|
298d438230 | ||
|
|
cb2f7932dd | ||
|
|
3354bd669f | ||
|
|
8ad779be66 | ||
|
|
f462034868 | ||
|
|
26c25cd4cd | ||
|
|
6398830c2d | ||
|
|
0b065063f2 | ||
|
|
a3a9de6fdd | ||
|
|
d77d30f29e | ||
|
|
5cabc6379d | ||
|
|
af537e6a48 | ||
|
|
faf3797663 | ||
|
|
db57f3ff62 | ||
|
|
0f77caad69 | ||
|
|
751b91e1b8 | ||
|
|
968a17fbfb | ||
|
|
712e87b39f | ||
|
|
0b40b42315 | ||
|
|
62996b1162 | ||
|
|
b67ccc6091 | ||
|
|
211d2dcf99 | ||
|
|
ee52e16a75 | ||
|
|
0c27bd25fa | ||
|
|
b6a6e78d01 | ||
|
|
92e6a29e70 | ||
|
|
acc8cee7cd | ||
|
|
afefbe154b | ||
|
|
83fa55b7d9 | ||
|
|
4f6c10d995 | ||
|
|
ed972d2601 | ||
|
|
6b57ee5654 | ||
|
|
e469af1ca5 | ||
|
|
6d41f076c2 | ||
|
|
8cff591746 | ||
|
|
b3ccf89094 | ||
|
|
d31c6b1627 | ||
|
|
1481356d1f | ||
|
|
a54661fd0a | ||
|
|
ae4a3f10ae | ||
|
|
fe3160e7a1 | ||
|
|
66659d4786 | ||
|
|
0b25b09040 | ||
|
|
0d41cc2660 | ||
|
|
f5e8822718 | ||
|
|
bdc220ec12 | ||
|
|
3eb68e5271 | ||
|
|
521952ebcc | ||
|
|
034091a696 | ||
|
|
ae881101d8 | ||
|
|
b11a30c49c | ||
|
|
4625efda7f | ||
|
|
3c168d750d | ||
|
|
5cc7b259ce | ||
|
|
f7ae046b20 | ||
|
|
02f43d6239 | ||
|
|
53e1fa1047 | ||
|
|
b1dc0e234f | ||
|
|
9d380dd828 | ||
|
|
1f77540dbb | ||
|
|
455edbfb5d | ||
|
|
7288b66d27 | ||
|
|
3d72ec80bb | ||
|
|
f2a74df511 | ||
|
|
68c6052d10 | ||
|
|
c4edb56bd4 | ||
|
|
b6a3fe7cfb | ||
|
|
7a088c5b7d | ||
|
|
2e845a9faa | ||
|
|
ac3ae0dbbe | ||
|
|
a3fc13de3a | ||
|
|
ee6cbc710c | ||
|
|
18d701525e | ||
|
|
e47c848ec8 | ||
|
|
cd64548299 | ||
|
|
8645d053de | ||
|
|
91f2dabed7 | ||
|
|
716612680d | ||
|
|
3800fb85eb | ||
|
|
d807984be4 | ||
|
|
2c92ae8898 | ||
|
|
3d8cbc81c4 | ||
|
|
d747c94450 | ||
|
|
a627d1f96e | ||
|
|
869db5e478 | ||
|
|
73e94d385e | ||
|
|
8f4ebeb335 | ||
|
|
263ee864be | ||
|
|
f078732624 | ||
|
|
fac1f6b16c | ||
|
|
a5841c1423 | ||
|
|
aaca18003d | ||
|
|
5ec521b024 | ||
|
|
b3c0be7559 | ||
|
|
d52b735b99 | ||
|
|
639b1f2863 | ||
|
|
aff4f7e010 | ||
|
|
dec4dafba6 | ||
|
|
d0cdcfc32c | ||
|
|
0867b81c7a | ||
|
|
bde6068f2d | ||
|
|
47fd2affa4 | ||
|
|
7f2cc885fe | ||
|
|
19a365a370 | ||
|
|
9a50da328e | ||
|
|
181e36a7c1 | ||
|
|
178508d245 | ||
|
|
d132d084cf | ||
|
|
494b55d685 | ||
|
|
51513d3779 | ||
|
|
5b95b9875b | ||
|
|
688d197472 | ||
|
|
b745fb476e | ||
|
|
047b5a85d2 | ||
|
|
370a0c6a05 | ||
|
|
0d4558fee1 | ||
|
|
76526e0a96 | ||
|
|
70093e0a7d | ||
|
|
458398f2ca | ||
|
|
7a6cc4f51e | ||
|
|
f4ccce7de5 | ||
|
|
f8b5417d6c | ||
|
|
13ce8cf498 | ||
|
|
6c2afc086c | ||
|
|
93d50712a9 | ||
|
|
ed91a44928 | ||
|
|
cd10e66fbb | ||
|
|
d6aa126fcc | ||
|
|
3308c7bdf4 | ||
|
|
56341a1a73 | ||
|
|
0857e1a536 | ||
|
|
5d6b25a29e | ||
|
|
5bc15a5448 | ||
|
|
51a19d0544 | ||
|
|
fb4e912ed0 | ||
|
|
20c2652013 | ||
|
|
971d6ad9e3 | ||
|
|
757fc7a7fe | ||
|
|
e4d0a4554a | ||
|
|
dfab7dbc4b | ||
|
|
0039f4c155 | ||
|
|
23f7dc63b8 | ||
|
|
e485b75a44 | ||
|
|
dbef57d329 | ||
|
|
c650441655 | ||
|
|
e573a8af77 | ||
|
|
b23252d046 | ||
|
|
2f7448dbd4 | ||
|
|
9bf4aa2968 | ||
|
|
d78a7bad3b | ||
|
|
b812177e78 | ||
|
|
4710a6af41 | ||
|
|
a613980ea4 | ||
|
|
20ae1f844b | ||
|
|
69511134e5 | ||
|
|
75952563e4 | ||
|
|
21cf5e1df7 | ||
|
|
9df5505989 | ||
|
|
1809d59193 | ||
|
|
feaa54d660 | ||
|
|
c94bd41162 | ||
|
|
f1f3e66537 | ||
|
|
80363cdc73 | ||
|
|
02e08fdf12 | ||
|
|
42283b2469 | ||
|
|
d3b598a5b2 | ||
|
|
0dd3a03c6b | ||
|
|
2144888447 | ||
|
|
b2549066dc | ||
|
|
cd1f3aa9a7 | ||
|
|
1674401342 | ||
|
|
7ba8dbbf6e | ||
|
|
ad27d9ed0e | ||
|
|
482d2f9624 | ||
|
|
824ef704d4 | ||
|
|
58b73cfc7d | ||
|
|
0465fea2db | ||
|
|
39b75e3561 | ||
|
|
2933db9b16 | ||
|
|
d94914046b | ||
|
|
9cf384b14b | ||
|
|
614a2f0ccb | ||
|
|
5cecc72384 | ||
|
|
3ad37fb602 | ||
|
|
42b048c2bf | ||
|
|
a01bf3dfa1 | ||
|
|
ad60988553 | ||
|
|
c9ae4e4cc6 | ||
|
|
d2639851d5 | ||
|
|
8dc5f9cfa4 | ||
|
|
d99a408e04 | ||
|
|
5f14861682 | ||
|
|
8f8493f3ec | ||
|
|
62af66b5ae | ||
|
|
e8d1fa7447 | ||
|
|
ee03871405 | ||
|
|
345378d97f | ||
|
|
07a463ee52 | ||
|
|
3157047160 | ||
|
|
a1dda3b578 | ||
|
|
e161ffce57 | ||
|
|
0c1859dc43 | ||
|
|
e4dcc0f768 | ||
|
|
74ab591214 | ||
|
|
7bd7996893 | ||
|
|
505ae4eeb5 | ||
|
|
951d6d3ce3 | ||
|
|
5ff7764699 | ||
|
|
0d74998625 | ||
|
|
29b70a12bd | ||
|
|
d84150e97b | ||
|
|
2b2ef4251f | ||
|
|
2840ea0f38 | ||
|
|
542d485267 | ||
|
|
cdd4fbc81d | ||
|
|
bfdddab0a0 | ||
|
|
44d1d01105 | ||
|
|
120bb09171 | ||
|
|
b7af99c671 | ||
|
|
869e0b3973 | ||
|
|
b68613dee4 | ||
|
|
ce0f32e7d5 | ||
|
|
78bc9b59c2 | ||
|
|
23cf3d2923 | ||
|
|
335136f3a3 | ||
|
|
11dd7aef09 | ||
|
|
2d1769e2f9 | ||
|
|
21e26147b0 | ||
|
|
ba301f8c12 | ||
|
|
3420374649 | ||
|
|
644d3a181f | ||
|
|
4be3011a8a | ||
|
|
5aa0a956dd | ||
|
|
7fdb1bdce8 | ||
|
|
57c6cef2bd | ||
|
|
e5599adca1 | ||
|
|
ab392ffb7f | ||
|
|
7585d4b258 | ||
|
|
ff82d9c38c | ||
|
|
920fde69bb | ||
|
|
053812e5f0 | ||
|
|
c2f59c4b6c | ||
|
|
06980fe9b5 | ||
|
|
3f5616f1fc | ||
|
|
b6af3b70b0 | ||
|
|
d8e4547988 | ||
|
|
34f649155e | ||
|
|
11779fe3e3 | ||
|
|
032cde67b0 | ||
|
|
229636a796 | ||
|
|
da9c9ac346 | ||
|
|
3fecc4c648 | ||
|
|
98cefcf77b | ||
|
|
413ee81ffa | ||
|
|
578ca8785e | ||
|
|
da4112c078 | ||
|
|
704c7c881d | ||
|
|
63b6abdb9d | ||
|
|
2e936a3d5c | ||
|
|
606574e18e | ||
|
|
1021879167 | ||
|
|
dc4aa9c607 | ||
|
|
b2c3d78773 | ||
|
|
8d3a0b5295 | ||
|
|
9879d07bec | ||
|
|
7bfce851e7 | ||
|
|
34e81881ec | ||
|
|
0143d6c60d | ||
|
|
267c2bc907 | ||
|
|
316f27d88c | ||
|
|
452b56f470 | ||
|
|
43aeaa4455 | ||
|
|
08b7a6985e | ||
|
|
4bbd8e28c1 | ||
|
|
fcf4c09389 | ||
|
|
2c323cbe80 | ||
|
|
7ec7b6bd7b | ||
|
|
b2378f2a53 | ||
|
|
8bf8d85bb7 | ||
|
|
676173e895 | ||
|
|
d8649c87e0 | ||
|
|
b9456ca466 | ||
|
|
cfccbb8927 | ||
|
|
a18578362a | ||
|
|
2f9f94dee0 | ||
|
|
c84e45ddee | ||
|
|
ea558d8c9d | ||
|
|
b936a35b63 | ||
|
|
b4ef4c2143 | ||
|
|
0ff4756ef4 | ||
|
|
94204b4739 | ||
|
|
bf3a2b768e | ||
|
|
5fb7badfb4 | ||
|
|
239d56f9a3 | ||
|
|
9163fc23f4 | ||
|
|
d225c28fde | ||
|
|
8a3f02e845 | ||
|
|
d0dc92c891 | ||
|
|
8d660f5a2f | ||
|
|
b41b4e77b2 | ||
|
|
267a37d3bd | ||
|
|
0cf23c7d7c | ||
|
|
a632486229 | ||
|
|
64a518a00b | ||
|
|
2f3a914027 | ||
|
|
7182d32d9c | ||
|
|
18381c5d32 | ||
|
|
79327073b4 | ||
|
|
018f2fd789 | ||
|
|
3889392aed | ||
|
|
bd976a25f1 | ||
|
|
01f05ac6fd | ||
|
|
52292cb5a5 | ||
|
|
c6dd1ba0ca | ||
|
|
84f069087c | ||
|
|
76cfced60f | ||
|
|
5c2aea0a6b | ||
|
|
7a883c62df | ||
|
|
ff97461ff8 | ||
|
|
51b0eb74a5 | ||
|
|
2304407986 | ||
|
|
a1ebdc3004 | ||
|
|
fef30f4bea | ||
|
|
eee8d9ab7c | ||
|
|
4f2678d321 | ||
|
|
c473fba628 | ||
|
|
9a9cd8e6a5 | ||
|
|
f5a89aa81a | ||
|
|
3c1beab725 | ||
|
|
79f03ad3ac | ||
|
|
574138a1fb | ||
|
|
6513e2cfca | ||
|
|
43a749b6a7 | ||
|
|
c1d6b3121a | ||
|
|
0d9c8ae4df | ||
|
|
62d8c089ed | ||
|
|
971a76ce11 | ||
|
|
cb33404122 | ||
|
|
bcf72f4624 | ||
|
|
77ad6950e8 | ||
|
|
e2d29aadca | ||
|
|
64ca04ad07 | ||
|
|
b6506a9331 | ||
|
|
fd7222242a | ||
|
|
e36049cd43 | ||
|
|
257f6c5994 | ||
|
|
9098bfb63a | ||
|
|
118d22c4ec | ||
|
|
758df0d85a | ||
|
|
59bbd902fc | ||
|
|
fffab73061 | ||
|
|
0a9ce84cf2 | ||
|
|
07a1734d4b | ||
|
|
6e41d3591d | ||
|
|
4134e5054a | ||
|
|
bb374a5ce2 | ||
|
|
359f398afa | ||
|
|
84425e86e9 | ||
|
|
ebf725c949 | ||
|
|
fc0ea36cf3 | ||
|
|
7836de3f08 | ||
|
|
406232c478 | ||
|
|
9e0c29496f | ||
|
|
480954ee87 | ||
|
|
94039bd9b1 | ||
|
|
667eaca9f2 | ||
|
|
446822a7ae | ||
|
|
f09a3e06f4 | ||
|
|
7c4a56f5f2 | ||
|
|
08f6a32c34 | ||
|
|
3e255fa647 | ||
|
|
c0a90402ef | ||
|
|
5e42627bce | ||
|
|
41bcf9524a | ||
|
|
914cf10911 | ||
|
|
855d4d139d | ||
|
|
abb7b0f8c8 | ||
|
|
d78ad52662 | ||
|
|
25b4bcd311 | ||
|
|
1d3e971ed7 | ||
|
|
3d1f6c4f91 | ||
|
|
8368969932 | ||
|
|
cb016c4307 | ||
|
|
7c7797d35a | ||
|
|
87ab41c80c | ||
|
|
d2391f94c0 | ||
|
|
050ddb8c55 | ||
|
|
bc23e0984a | ||
|
|
07de353207 | ||
|
|
c02491d2e6 | ||
|
|
a6ede8f905 | ||
|
|
22941a9ce0 | ||
|
|
633a09d414 | ||
|
|
29f0881c5a | ||
|
|
60debca37b | ||
|
|
30ea81d0fb | ||
|
|
b1d92c4fe6 | ||
|
|
70f46de2d8 | ||
|
|
f1b2d0b870 | ||
|
|
8a385972fc | ||
|
|
28dd85c1d1 | ||
|
|
827c8e0e72 | ||
|
|
162c076a14 | ||
|
|
9386465de7 | ||
|
|
acca22f3a1 | ||
|
|
f8d84814e0 | ||
|
|
c46cf41842 | ||
|
|
64ab1c4116 | ||
|
|
a6de1041c7 | ||
|
|
c8d34e65ea | ||
|
|
51db729546 | ||
|
|
d2052ad236 | ||
|
|
9c4301467f | ||
|
|
e7355dc0e4 | ||
|
|
4110fec94f | ||
|
|
d5e601eae9 | ||
|
|
4f044c4a57 | ||
|
|
5821c350e1 | ||
|
|
edba8188fe | ||
|
|
1471a72633 | ||
|
|
56834cb88a | ||
|
|
a0f16f9184 | ||
|
|
de80eb4806 | ||
|
|
48a4b81fbe | ||
|
|
e225794f72 | ||
|
|
4eef30f8b5 | ||
|
|
569b09609d | ||
|
|
39838c25c2 | ||
|
|
49e90c08a9 | ||
|
|
e777b06fb8 | ||
|
|
497ec2ac74 | ||
|
|
c5d282d203 |
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -11,6 +11,14 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
sanity-check:
|
||||
name: Sanity Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Check version consistency
|
||||
run: pnpm tsx ${{ github.workspace }}/scripts/check-version-consistency.ts ${{ github.ref_name }}
|
||||
make-electron:
|
||||
name: Make Electron
|
||||
strategy:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -51,3 +51,4 @@ upload
|
||||
# docs
|
||||
site/
|
||||
apps/*/coverage
|
||||
scripts/translation/.language*.json
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
"keywords": [],
|
||||
"author": "Elian Doran <contact@eliandoran.me>",
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.26.2",
|
||||
"packageManager": "pnpm@10.28.0",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.14.1",
|
||||
"@redocly/cli": "2.14.4",
|
||||
"archiver": "7.0.1",
|
||||
"fs-extra": "11.3.3",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"typedoc": "0.28.15",
|
||||
"typedoc": "0.28.16",
|
||||
"typedoc-plugin-missing-exports": "4.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@triliumnext/client",
|
||||
"version": "0.101.1",
|
||||
"version": "0.101.3",
|
||||
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
@@ -43,7 +43,7 @@
|
||||
"debounce": "3.0.0",
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.51.0",
|
||||
"globals": "16.5.0",
|
||||
"globals": "17.0.0",
|
||||
"i18next": "25.7.3",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "3.7.1",
|
||||
@@ -56,12 +56,12 @@
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "17.0.1",
|
||||
"mermaid": "11.12.2",
|
||||
"mind-elixir": "5.3.8",
|
||||
"mind-elixir": "5.5.0",
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.28.1",
|
||||
"react-i18next": "16.5.0",
|
||||
"react-window": "2.2.3",
|
||||
"preact": "10.28.2",
|
||||
"react-i18next": "16.5.1",
|
||||
"react-window": "2.2.5",
|
||||
"reveal.js": "5.2.1",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
"tabulator-tables": "6.3.1",
|
||||
@@ -69,7 +69,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-inspector": "5.0.0",
|
||||
"@preact/preset-vite": "2.10.2",
|
||||
"@prefresh/vite": "2.4.11",
|
||||
"@types/bootstrap": "5.2.10",
|
||||
"@types/jquery": "3.5.33",
|
||||
"@types/leaflet": "1.9.21",
|
||||
@@ -79,6 +79,7 @@
|
||||
"@types/tabulator-tables": "6.3.1",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"happy-dom": "20.0.11",
|
||||
"lightningcss": "1.30.2",
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "3.1.4"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ColumnComponent } from "tabulator-tables";
|
||||
|
||||
import type { Attribute } from "../services/attribute_parser.js";
|
||||
import froca from "../services/froca.js";
|
||||
import { initLocale,t } from "../services/i18n.js";
|
||||
import { initLocale, t } from "../services/i18n.js";
|
||||
import keyboardActionsService from "../services/keyboard_actions.js";
|
||||
import linkService, { type ViewScope } from "../services/link.js";
|
||||
import type LoadResults from "../services/load_results.js";
|
||||
@@ -154,6 +154,7 @@ export type CommandMappings = {
|
||||
};
|
||||
openInTab: ContextMenuCommandData;
|
||||
openNoteInSplit: ContextMenuCommandData;
|
||||
openNoteInWindow: ContextMenuCommandData;
|
||||
openNoteInPopup: ContextMenuCommandData;
|
||||
toggleNoteHoisting: ContextMenuCommandData;
|
||||
insertNoteAfter: ContextMenuCommandData;
|
||||
@@ -382,7 +383,8 @@ export type CommandMappings = {
|
||||
reloadTextEditor: CommandData;
|
||||
chooseNoteType: CommandData & {
|
||||
callback: ChooseNoteTypeCallback
|
||||
}
|
||||
};
|
||||
customDownload: CommandData;
|
||||
};
|
||||
|
||||
type EventMappings = {
|
||||
@@ -473,6 +475,11 @@ type EventMappings = {
|
||||
noteContextRemoved: {
|
||||
ntxIds: string[];
|
||||
};
|
||||
contextDataChanged: {
|
||||
noteContext: NoteContext;
|
||||
key: string;
|
||||
value: unknown;
|
||||
};
|
||||
exportSvg: { ntxId: string | null | undefined; };
|
||||
exportPng: { ntxId: string | null | undefined; };
|
||||
geoMapCreateChildNote: {
|
||||
|
||||
@@ -57,6 +57,18 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a child component from this component's children array.
|
||||
* This is used for cleanup when a widget is unmounted to prevent event listener accumulation.
|
||||
*/
|
||||
removeChild(component: ChildT) {
|
||||
const index = this.children.indexOf(component);
|
||||
if (index !== -1) {
|
||||
this.children.splice(index, 1);
|
||||
component.parent = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null | undefined {
|
||||
try {
|
||||
const callMethodPromise = this.initialized ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) : this.callMethod((this as any)[`${name}Event`], data);
|
||||
|
||||
@@ -12,6 +12,7 @@ import server from "../services/server.js";
|
||||
import treeService from "../services/tree.js";
|
||||
import utils from "../services/utils.js";
|
||||
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
||||
import type { HeadingContext } from "../widgets/sidebar/TableOfContents.js";
|
||||
import appContext, { type EventData, type EventListener } from "./app_context.js";
|
||||
import Component from "./component.js";
|
||||
|
||||
@@ -22,6 +23,31 @@ export interface SetNoteOpts {
|
||||
|
||||
export type GetTextEditorCallback = (editor: CKTextEditor) => void;
|
||||
|
||||
export type SaveState = "saved" | "saving" | "unsaved" | "error";
|
||||
|
||||
export interface NoteContextDataMap {
|
||||
toc: HeadingContext;
|
||||
pdfPages: {
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
scrollToPage(page: number): void;
|
||||
requestThumbnail(page: number): void;
|
||||
};
|
||||
pdfAttachments: {
|
||||
attachments: PdfAttachment[];
|
||||
downloadAttachment(filename: string): void;
|
||||
};
|
||||
pdfLayers: {
|
||||
layers: PdfLayer[];
|
||||
toggleLayer(layerId: string, visible: boolean): void;
|
||||
};
|
||||
saveState: {
|
||||
state: SaveState;
|
||||
};
|
||||
}
|
||||
|
||||
type ContextDataKey = keyof NoteContextDataMap;
|
||||
|
||||
class NoteContext extends Component implements EventListener<"entitiesReloaded"> {
|
||||
ntxId: string | null;
|
||||
hoistedNoteId: string;
|
||||
@@ -32,6 +58,13 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
parentNoteId?: string | null;
|
||||
viewScope?: ViewScope;
|
||||
|
||||
/**
|
||||
* Metadata storage for UI components (e.g., table of contents, PDF page list, code outline).
|
||||
* This allows type widgets to publish data that sidebar/toolbar components can consume.
|
||||
* Data is automatically cleared when navigating to a different note.
|
||||
*/
|
||||
private contextData: Map<string, unknown> = new Map();
|
||||
|
||||
constructor(ntxId: string | null = null, hoistedNoteId: string = "root", mainNtxId: string | null = null) {
|
||||
super();
|
||||
|
||||
@@ -91,6 +124,22 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
this.viewScope = opts.viewScope;
|
||||
({ noteId: this.noteId, parentNoteId: this.parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(resolvedNotePath));
|
||||
|
||||
// Clear context data when switching notes and notify subscribers
|
||||
const oldKeys = Array.from(this.contextData.keys());
|
||||
this.contextData.clear();
|
||||
if (oldKeys.length > 0) {
|
||||
// Notify subscribers asynchronously to avoid blocking navigation
|
||||
window.setTimeout(() => {
|
||||
for (const key of oldKeys) {
|
||||
this.triggerEvent("contextDataChanged", {
|
||||
noteContext: this,
|
||||
key,
|
||||
value: undefined
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
this.saveToRecentNotes(resolvedNotePath);
|
||||
|
||||
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
|
||||
@@ -443,6 +492,52 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set metadata for this note context (e.g., table of contents, PDF pages, code outline).
|
||||
* This data can be consumed by sidebar/toolbar components.
|
||||
*
|
||||
* @param key - Unique identifier for the data type (e.g., "toc", "pdfPages", "codeOutline")
|
||||
* @param value - The data to store (will be cleared when switching notes)
|
||||
*/
|
||||
setContextData<K extends ContextDataKey>(key: K, value: NoteContextDataMap[K]): void {
|
||||
this.contextData.set(key, value);
|
||||
// Trigger event so subscribers can react
|
||||
this.triggerEvent("contextDataChanged", {
|
||||
noteContext: this,
|
||||
key,
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata for this note context.
|
||||
*
|
||||
* @param key - The data key to retrieve
|
||||
* @returns The stored data, or undefined if not found
|
||||
*/
|
||||
getContextData<K extends ContextDataKey>(key: K): NoteContextDataMap[K] | undefined {
|
||||
return this.contextData.get(key) as NoteContextDataMap[K] | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if context data exists for a given key.
|
||||
*/
|
||||
hasContextData(key: ContextDataKey): boolean {
|
||||
return this.contextData.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear specific context data.
|
||||
*/
|
||||
clearContextData(key: ContextDataKey): void {
|
||||
this.contextData.delete(key);
|
||||
this.triggerEvent("contextDataChanged", {
|
||||
noteContext: this,
|
||||
key,
|
||||
value: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function openInCurrentNoteContext(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, notePath: string, viewScope?: ViewScope) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import search from "../services/search.js";
|
||||
import server from "../services/server.js";
|
||||
import utils from "../services/utils.js";
|
||||
import type FAttachment from "./fattachment.js";
|
||||
import type { AttributeType,default as FAttribute } from "./fattribute.js";
|
||||
import type { AttributeType, default as FAttribute } from "./fattribute.js";
|
||||
|
||||
const LABEL = "label";
|
||||
const RELATION = "relation";
|
||||
@@ -616,7 +616,9 @@ export default class FNote {
|
||||
}
|
||||
|
||||
isFolder() {
|
||||
return this.type === "search" || this.getFilteredChildBranches().length > 0;
|
||||
if (this.isLabelTruthy("subtreeHidden")) return false;
|
||||
if (this.type === "search") return true;
|
||||
return this.getFilteredChildBranches().length > 0;
|
||||
}
|
||||
|
||||
getFilteredChildBranches() {
|
||||
|
||||
29
apps/client/src/index.html
Normal file
29
apps/client/src/index.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
|
||||
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
|
||||
<title>Trilium Notes</title>
|
||||
</head>
|
||||
|
||||
<body id="trilium-app">
|
||||
<noscript>Trilium requires JavaScript to be enabled.</noscript>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container" style="display: none"></div>
|
||||
|
||||
<!-- Required to match the PWA's top bar color with the theme -->
|
||||
<!-- This works even when the user directly changes --root-background in CSS -->
|
||||
<div id="background-color-tracker" style="position: absolute; visibility: hidden; color: var(--root-background); transition: color 1ms;"></div>
|
||||
|
||||
<script src="./index.ts" type="module"></script>
|
||||
|
||||
<!-- Required for correct loading of scripts in Electron -->
|
||||
<script>
|
||||
if (typeof module === 'object') {window.module = module; module = undefined;}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,110 @@
|
||||
async function bootstrap() {
|
||||
showSplash();
|
||||
await setupGlob();
|
||||
await Promise.all([
|
||||
initJQuery(),
|
||||
loadBootstrapCss()
|
||||
]);
|
||||
loadStylesheets();
|
||||
loadIcons();
|
||||
setBodyAttributes();
|
||||
await loadScripts();
|
||||
hideSplash();
|
||||
}
|
||||
|
||||
async function initJQuery() {
|
||||
const $ = (await import("jquery")).default;
|
||||
window.$ = $;
|
||||
window.jQuery = $;
|
||||
}
|
||||
|
||||
async function setupGlob() {
|
||||
const response = await fetch(`/bootstrap${window.location.search}`);
|
||||
const json = await response.json();
|
||||
|
||||
window.global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
|
||||
window.glob = {
|
||||
...json,
|
||||
activeDialog: null
|
||||
};
|
||||
}
|
||||
|
||||
async function loadBootstrapCss() {
|
||||
// We have to selectively import Bootstrap CSS based on text direction.
|
||||
if (glob.isRtl) {
|
||||
await import("bootstrap/dist/css/bootstrap.rtl.min.css");
|
||||
} else {
|
||||
await import("bootstrap/dist/css/bootstrap.min.css");
|
||||
}
|
||||
}
|
||||
|
||||
function loadStylesheets() {
|
||||
const { assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
|
||||
const cssToLoad: string[] = [];
|
||||
cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`);
|
||||
cssToLoad.push(`api/fonts`);
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`);
|
||||
if (themeCssUrl) {
|
||||
cssToLoad.push(themeCssUrl);
|
||||
}
|
||||
if (themeUseNextAsBase === "next") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`);
|
||||
} else if (themeUseNextAsBase === "next-dark") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`);
|
||||
} else if (themeUseNextAsBase === "next-light") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`);
|
||||
}
|
||||
cssToLoad.push(`${assetPath}/stylesheets/style.css`);
|
||||
|
||||
for (const href of cssToLoad) {
|
||||
const linkEl = document.createElement("link");
|
||||
linkEl.href = href;
|
||||
linkEl.rel = "stylesheet";
|
||||
document.head.appendChild(linkEl);
|
||||
}
|
||||
}
|
||||
|
||||
function loadIcons() {
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.innerText = window.glob.iconPackCss;
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
|
||||
function setBodyAttributes() {
|
||||
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale } = window.glob;
|
||||
const classesToSet = [
|
||||
device,
|
||||
`heading-style-${headingStyle}`,
|
||||
`layout-${layoutOrientation}`,
|
||||
`platform-${platform}`,
|
||||
isElectron && "electron",
|
||||
hasNativeTitleBar && "native-titlebar",
|
||||
hasBackgroundEffects && "background-effects"
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
for (const classToSet of classesToSet) {
|
||||
document.body.classList.add(classToSet);
|
||||
}
|
||||
|
||||
document.body.lang = currentLocale.id;
|
||||
document.body.dir = currentLocale.rtl ? "rtl" : "ltr";
|
||||
}
|
||||
|
||||
async function loadScripts() {
|
||||
if (glob.device === "mobile") {
|
||||
await import("./mobile.js");
|
||||
} else {
|
||||
await import("./desktop.js");
|
||||
}
|
||||
}
|
||||
|
||||
function showSplash() {
|
||||
// hide body to reduce flickering on the startup. This is done through JS and not CSS to not hide <noscript>
|
||||
document.body.style.display = "none";
|
||||
}
|
||||
|
||||
function hideSplash() {
|
||||
document.body.style.display = "block";
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
import { applyModals } from "./layout_commons.js";
|
||||
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||
import { useNoteContext } from "../widgets/react/hooks.jsx";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.js";
|
||||
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||
import type AppContext from "../components/app_context.js";
|
||||
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
||||
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.js";
|
||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||
import NoteTitleWidget from "../widgets/note_title.js";
|
||||
import ContentHeader from "../widgets/containers/content_header.js";
|
||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
||||
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
|
||||
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
|
||||
import NoteTitleWidget from "../widgets/note_title.js";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import { useNoteContext } from "../widgets/react/hooks.jsx";
|
||||
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
||||
import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx";
|
||||
import SearchResult from "../widgets/search_result.jsx";
|
||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
|
||||
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||
import TabRowWidget from "../widgets/tab_row.js";
|
||||
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
|
||||
import type AppContext from "../components/app_context.js";
|
||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
|
||||
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||
import { applyModals } from "./layout_commons.js";
|
||||
|
||||
const MOBILE_CSS = `
|
||||
<style>
|
||||
@@ -194,11 +194,11 @@ export default class MobileLayout {
|
||||
}
|
||||
|
||||
function FilePropertiesWrapper() {
|
||||
const { note } = useNoteContext();
|
||||
const { note, ntxId } = useNoteContext();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{note?.type === "file" && <FilePropertiesTab note={note} />}
|
||||
{note?.type === "file" && <FilePropertiesTab note={note} ntxId={ntxId} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import NoteColorPicker from "./custom-items/NoteColorPicker.jsx";
|
||||
import treeService from "../services/tree.js";
|
||||
import froca from "../services/froca.js";
|
||||
import clipboard from "../services/clipboard.js";
|
||||
import noteCreateService from "../services/note_create.js";
|
||||
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
|
||||
import appContext, { type ContextMenuCommandData, type FilteredCommandNames } from "../components/app_context.js";
|
||||
import type { SelectMenuItemEventListener } from "../components/events.js";
|
||||
import type FAttachment from "../entities/fattachment.js";
|
||||
import attributes from "../services/attributes.js";
|
||||
import { executeBulkActions } from "../services/bulk_action.js";
|
||||
import clipboard from "../services/clipboard.js";
|
||||
import dialogService from "../services/dialog.js";
|
||||
import froca from "../services/froca.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import noteCreateService from "../services/note_create.js";
|
||||
import noteTypesService from "../services/note_types.js";
|
||||
import server from "../services/server.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import dialogService from "../services/dialog.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import type NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import type FAttachment from "../entities/fattachment.js";
|
||||
import type { SelectMenuItemEventListener } from "../components/events.js";
|
||||
import treeService from "../services/tree.js";
|
||||
import utils from "../services/utils.js";
|
||||
import attributes from "../services/attributes.js";
|
||||
import { executeBulkActions } from "../services/bulk_action.js";
|
||||
import type NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
|
||||
import NoteColorPicker from "./custom-items/NoteColorPicker.jsx";
|
||||
|
||||
// TODO: Deduplicate once client/server is well split.
|
||||
interface ConvertToAttachmentResponse {
|
||||
@@ -72,6 +72,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
const noSelectedNotes = selNodes.length === 0 || (selNodes.length === 1 && selNodes[0] === this.node);
|
||||
|
||||
const notSearch = note?.type !== "search";
|
||||
const hasSubtreeHidden = note?.isLabelTruthy("subtreeHidden") ?? false;
|
||||
const isSpotlighted = this.node.extraClasses.includes("spotlighted-node");
|
||||
const notOptionsOrHelp = !note?.noteId.startsWith("_options") && !note?.noteId.startsWith("_help");
|
||||
const parentNotSearch = !parentNote || parentNote.type !== "search";
|
||||
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
|
||||
@@ -79,17 +81,18 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
const items: (MenuItem<TreeCommandNames> | null)[] = [
|
||||
{ title: t("tree-context-menu.open-in-a-new-tab"), command: "openInTab", shortcut: "Ctrl+Click", uiIcon: "bx bx-link-external", enabled: noSelectedNotes },
|
||||
{ title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes },
|
||||
{ title: t("tree-context-menu.open-in-a-new-window"), command: "openNoteInWindow", uiIcon: "bx bx-window-open", enabled: noSelectedNotes },
|
||||
{ title: t("tree-context-menu.open-in-popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit", enabled: noSelectedNotes },
|
||||
|
||||
isHoisted
|
||||
? null
|
||||
: {
|
||||
title: `${t("tree-context-menu.hoist-note")}`,
|
||||
command: "toggleNoteHoisting",
|
||||
keyboardShortcut: "toggleNoteHoisting",
|
||||
uiIcon: "bx bxs-chevrons-up",
|
||||
enabled: noSelectedNotes && notSearch
|
||||
},
|
||||
title: `${t("tree-context-menu.hoist-note")}`,
|
||||
command: "toggleNoteHoisting",
|
||||
keyboardShortcut: "toggleNoteHoisting",
|
||||
uiIcon: "bx bxs-chevrons-up",
|
||||
enabled: noSelectedNotes && notSearch
|
||||
},
|
||||
!isHoisted || !isNotRoot
|
||||
? null
|
||||
: { title: t("tree-context-menu.unhoist-note"), command: "toggleNoteHoisting", keyboardShortcut: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
|
||||
@@ -112,7 +115,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
keyboardShortcut: "createNoteInto",
|
||||
uiIcon: "bx bx-plus",
|
||||
items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
|
||||
enabled: notSearch && noSelectedNotes && notOptionsOrHelp,
|
||||
enabled: notSearch && noSelectedNotes && notOptionsOrHelp && !hasSubtreeHidden && !isSpotlighted,
|
||||
columns: 2
|
||||
},
|
||||
|
||||
@@ -150,8 +153,17 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
|
||||
{ kind: "separator" },
|
||||
|
||||
{ title: t("tree-context-menu.expand-subtree"), command: "expandSubtree", keyboardShortcut: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
|
||||
{ title: t("tree-context-menu.collapse-subtree"), command: "collapseSubtree", keyboardShortcut: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
|
||||
!hasSubtreeHidden && { title: t("tree-context-menu.expand-subtree"), command: "expandSubtree", keyboardShortcut: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
|
||||
!hasSubtreeHidden && { title: t("tree-context-menu.collapse-subtree"), command: "collapseSubtree", keyboardShortcut: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
|
||||
{
|
||||
title: hasSubtreeHidden ? t("tree-context-menu.show-subtree") : t("tree-context-menu.hide-subtree"),
|
||||
uiIcon: "bx bx-show",
|
||||
handler: async () => {
|
||||
const note = await froca.getNote(this.node.data.noteId);
|
||||
if (!note) return;
|
||||
attributes.setBooleanWithInheritance(note, "subtreeHidden", !hasSubtreeHidden);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("tree-context-menu.sort-by"),
|
||||
command: "sortChildNotes",
|
||||
@@ -164,7 +176,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
|
||||
{ title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-directions", enabled: true },
|
||||
{ title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptionsOrHelp }
|
||||
]
|
||||
].filter(Boolean) as MenuItem<TreeCommandNames>[]
|
||||
},
|
||||
|
||||
{ kind: "separator" },
|
||||
@@ -292,25 +304,30 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
noteCreateService.createNote(parentNotePath, {
|
||||
target: "after",
|
||||
targetBranchId: this.node.data.branchId,
|
||||
type: type,
|
||||
isProtected: isProtected,
|
||||
templateNoteId: templateNoteId
|
||||
type,
|
||||
isProtected,
|
||||
templateNoteId
|
||||
});
|
||||
} else if (command === "insertChildNote") {
|
||||
const parentNotePath = treeService.getNotePath(this.node);
|
||||
|
||||
noteCreateService.createNote(parentNotePath, {
|
||||
type: type,
|
||||
type,
|
||||
isProtected: this.node.data.isProtected,
|
||||
templateNoteId: templateNoteId
|
||||
templateNoteId
|
||||
});
|
||||
} else if (command === "openNoteInSplit") {
|
||||
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
|
||||
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
|
||||
|
||||
this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath });
|
||||
} else if (command === "openNoteInWindow") {
|
||||
appContext.triggerCommand("openInWindow", {
|
||||
notePath,
|
||||
hoistedNoteId: appContext.tabManager.getActiveContext()?.hoistedNoteId
|
||||
});
|
||||
} else if (command === "openNoteInPopup") {
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath })
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
|
||||
} else if (command === "convertNoteToAttachment") {
|
||||
if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) {
|
||||
return;
|
||||
@@ -332,11 +349,11 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
|
||||
toastService.showMessage(t("tree-context-menu.converted-to-attachments", { count: converted }));
|
||||
} else if (command === "copyNotePathToClipboard") {
|
||||
navigator.clipboard.writeText("#" + notePath);
|
||||
navigator.clipboard.writeText(`#${ notePath}`);
|
||||
} else if (command) {
|
||||
this.treeWidget.triggerCommand<TreeCommandNames>(command, {
|
||||
node: this.node,
|
||||
notePath: notePath,
|
||||
notePath,
|
||||
noteId: this.node.data.noteId,
|
||||
selectedOrActiveBranchIds: this.treeWidget.getSelectedOrActiveBranchIds(this.node),
|
||||
selectedOrActiveNoteIds: this.treeWidget.getSelectedOrActiveNoteIds(this.node)
|
||||
|
||||
139
apps/client/src/services/attributes.spec.ts
Normal file
139
apps/client/src/services/attributes.spec.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { buildNote } from "../test/easy-froca";
|
||||
import { setBooleanWithInheritance } from "./attributes";
|
||||
import froca from "./froca";
|
||||
import server from "./server.js";
|
||||
|
||||
// Spy on server methods to track calls
|
||||
// @ts-expect-error the generic typing is causing issues here
|
||||
server.put = vi.fn(async <T> (url: string, data?: T) => ({} as T));
|
||||
// @ts-expect-error the generic typing is causing issues here
|
||||
server.remove = vi.fn(async <T> (url: string) => ({} as T));
|
||||
|
||||
describe("Set boolean with inheritance", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("doesn't call server if value matches directly", async () => {
|
||||
const noteWithLabel = buildNote({
|
||||
title: "New note",
|
||||
"#foo": ""
|
||||
});
|
||||
const noteWithoutLabel = buildNote({
|
||||
title: "New note"
|
||||
});
|
||||
|
||||
await setBooleanWithInheritance(noteWithLabel, "foo", true);
|
||||
await setBooleanWithInheritance(noteWithoutLabel, "foo", false);
|
||||
expect(server.put).not.toHaveBeenCalled();
|
||||
expect(server.remove).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets boolean normally without inheritance", async () => {
|
||||
const standaloneNote = buildNote({
|
||||
title: "New note"
|
||||
});
|
||||
|
||||
await setBooleanWithInheritance(standaloneNote, "foo", true);
|
||||
expect(server.put).toHaveBeenCalledWith(`notes/${standaloneNote.noteId}/set-attribute`, {
|
||||
type: "label",
|
||||
name: "foo",
|
||||
value: "",
|
||||
isInheritable: false
|
||||
});
|
||||
});
|
||||
|
||||
it("removes boolean normally without inheritance", async () => {
|
||||
const standaloneNote = buildNote({
|
||||
title: "New note",
|
||||
"#foo": ""
|
||||
});
|
||||
|
||||
const attributeId = standaloneNote.getLabel("foo")!.attributeId;
|
||||
await setBooleanWithInheritance(standaloneNote, "foo", false);
|
||||
expect(server.remove).toHaveBeenCalledWith(`notes/${standaloneNote.noteId}/attributes/${attributeId}`);
|
||||
});
|
||||
|
||||
it("doesn't call server if value matches inherited", async () => {
|
||||
const parentNote = buildNote({
|
||||
title: "Parent note",
|
||||
"#foo(inheritable)": "",
|
||||
"children": [
|
||||
{
|
||||
title: "Child note"
|
||||
}
|
||||
]
|
||||
});
|
||||
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
|
||||
expect(childNote.isLabelTruthy("foo")).toBe(true);
|
||||
await setBooleanWithInheritance(childNote, "foo", true);
|
||||
expect(server.put).not.toHaveBeenCalled();
|
||||
expect(server.remove).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("overrides boolean with inheritance", async () => {
|
||||
const parentNote = buildNote({
|
||||
title: "Parent note",
|
||||
"#foo(inheritable)": "",
|
||||
"children": [
|
||||
{
|
||||
title: "Child note"
|
||||
}
|
||||
]
|
||||
});
|
||||
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
|
||||
expect(childNote.isLabelTruthy("foo")).toBe(true);
|
||||
await setBooleanWithInheritance(childNote, "foo", false);
|
||||
expect(server.put).toHaveBeenCalledWith(`notes/${childNote.noteId}/set-attribute`, {
|
||||
type: "label",
|
||||
name: "foo",
|
||||
value: "false",
|
||||
isInheritable: false
|
||||
});
|
||||
});
|
||||
|
||||
it("overrides boolean with inherited false", async () => {
|
||||
const parentNote = buildNote({
|
||||
title: "Parent note",
|
||||
"#foo(inheritable)": "false",
|
||||
"children": [
|
||||
{
|
||||
title: "Child note"
|
||||
}
|
||||
]
|
||||
});
|
||||
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
|
||||
expect(childNote.isLabelTruthy("foo")).toBe(false);
|
||||
await setBooleanWithInheritance(childNote, "foo", true);
|
||||
expect(server.put).toHaveBeenCalledWith(`notes/${childNote.noteId}/set-attribute`, {
|
||||
type: "label",
|
||||
name: "foo",
|
||||
value: "",
|
||||
isInheritable: false
|
||||
});
|
||||
});
|
||||
|
||||
it("deletes override boolean with inherited false with already existing value", async () => {
|
||||
const parentNote = buildNote({
|
||||
title: "Parent note",
|
||||
"#foo(inheritable)": "false",
|
||||
"children": [
|
||||
{
|
||||
title: "Child note",
|
||||
"#foo": "false",
|
||||
}
|
||||
]
|
||||
});
|
||||
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
|
||||
expect(childNote.isLabelTruthy("foo")).toBe(false);
|
||||
await setBooleanWithInheritance(childNote, "foo", true);
|
||||
expect(server.put).toBeCalledWith(`notes/${childNote.noteId}/set-attribute`, {
|
||||
type: "label",
|
||||
name: "foo",
|
||||
value: "",
|
||||
isInheritable: false
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,15 @@
|
||||
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";
|
||||
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import froca from "./froca.js";
|
||||
import type { AttributeRow } from "./load_results.js";
|
||||
import server from "./server.js";
|
||||
|
||||
async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||
await server.put(`notes/${noteId}/attribute`, {
|
||||
type: "label",
|
||||
name: name,
|
||||
value: value,
|
||||
name,
|
||||
value,
|
||||
isInheritable
|
||||
});
|
||||
}
|
||||
@@ -16,8 +17,8 @@ async function addLabel(noteId: string, name: string, value: string = "", isInhe
|
||||
export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||
await server.put(`notes/${noteId}/set-attribute`, {
|
||||
type: "label",
|
||||
name: name,
|
||||
value: value,
|
||||
name,
|
||||
value,
|
||||
isInheritable
|
||||
});
|
||||
}
|
||||
@@ -25,12 +26,42 @@ export async function setLabel(noteId: string, name: string, value: string = "",
|
||||
export async function setRelation(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||
await server.put(`notes/${noteId}/set-attribute`, {
|
||||
type: "relation",
|
||||
name: name,
|
||||
value: value,
|
||||
name,
|
||||
value,
|
||||
isInheritable
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a boolean label on the given note, taking inheritance into account. If the desired value matches the inherited
|
||||
* value, any owned label will be removed to allow the inherited value to take effect. If the desired value differs
|
||||
* from the inherited value, an owned label will be created or updated to reflect the desired value.
|
||||
*
|
||||
* When checking if the boolean value is set, don't use `note.hasLabel`; instead use `note.isLabelTruthy`.
|
||||
*
|
||||
* @param note the note on which to set the boolean label.
|
||||
* @param labelName the name of the label to set.
|
||||
* @param value the boolean value to set for the label.
|
||||
*/
|
||||
export async function setBooleanWithInheritance(note: FNote, labelName: string, value: boolean) {
|
||||
const actualValue = note.isLabelTruthy(labelName);
|
||||
if (actualValue === value) return;
|
||||
const hasInheritedValue = !note.hasOwnedLabel(labelName) && note.hasLabel(labelName);
|
||||
|
||||
if (hasInheritedValue) {
|
||||
if (value) {
|
||||
setLabel(note.noteId, labelName, "");
|
||||
} else {
|
||||
// Label is inherited - override to false.
|
||||
setLabel(note.noteId, labelName, "false");
|
||||
}
|
||||
} else if (value) {
|
||||
setLabel(note.noteId, labelName, "");
|
||||
} else {
|
||||
removeOwnedLabelByName(note, labelName);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeAttributeById(noteId: string, attributeId: string) {
|
||||
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
|
||||
}
|
||||
@@ -142,6 +173,7 @@ export default {
|
||||
setLabel,
|
||||
setRelation,
|
||||
setAttribute,
|
||||
setBooleanWithInheritance,
|
||||
removeAttributeById,
|
||||
removeOwnedLabelByName,
|
||||
removeOwnedRelationByName,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import utils from "./utils.js";
|
||||
import server from "./server.js";
|
||||
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
|
||||
import froca from "./froca.js";
|
||||
import hoistedNoteService from "./hoisted_note.js";
|
||||
import ws from "./ws.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import { t } from "./i18n.js";
|
||||
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
|
||||
import server from "./server.js";
|
||||
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
|
||||
import utils from "./utils.js";
|
||||
import ws from "./ws.js";
|
||||
|
||||
// TODO: Deduplicate type with server
|
||||
interface Response {
|
||||
@@ -66,7 +66,7 @@ async function moveAfterBranch(branchIdsToMove: string[], afterBranchId: string)
|
||||
}
|
||||
}
|
||||
|
||||
async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: string) {
|
||||
async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: string, componentId?: string) {
|
||||
const newParentBranch = froca.getBranch(newParentBranchId);
|
||||
if (!newParentBranch) {
|
||||
return;
|
||||
@@ -86,7 +86,7 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st
|
||||
continue;
|
||||
}
|
||||
|
||||
const resp = await server.put<Response>(`branches/${branchIdToMove}/move-to/${newParentBranchId}`);
|
||||
const resp = await server.put<Response>(`branches/${branchIdToMove}/move-to/${newParentBranchId}`, undefined, componentId);
|
||||
|
||||
if (!resp.success) {
|
||||
toastService.showError(resp.message);
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import renderService from "./render.js";
|
||||
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||
import WheelZoom from 'vanilla-js-wheel-zoom';
|
||||
|
||||
import FAttachment from "../entities/fattachment.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
import imageContextMenuService from "../menus/image_context_menu.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import renderText from "./content_renderer_text.js";
|
||||
import renderDoc from "./doc_renderer.js";
|
||||
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
|
||||
import openService from "./open.js";
|
||||
import protectedSessionService from "./protected_session.js";
|
||||
import protectedSessionHolder from "./protected_session_holder.js";
|
||||
import openService from "./open.js";
|
||||
import utils from "./utils.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
import FAttachment from "../entities/fattachment.js";
|
||||
import imageContextMenuService from "../menus/image_context_menu.js";
|
||||
import renderService from "./render.js";
|
||||
import { applySingleBlockSyntaxHighlight } from "./syntax_highlight.js";
|
||||
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
|
||||
import renderDoc from "./doc_renderer.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import WheelZoom from 'vanilla-js-wheel-zoom';
|
||||
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||
import renderText from "./content_renderer_text.js";
|
||||
import utils from "./utils.js";
|
||||
|
||||
let idCounter = 1;
|
||||
|
||||
@@ -22,6 +23,12 @@ export interface RenderOptions {
|
||||
imageHasZoom?: boolean;
|
||||
/** If enabled, it will prevent the default behavior in which an empty note would display a list of children. */
|
||||
noChildrenList?: boolean;
|
||||
/** If enabled, it will prevent rendering of included notes. */
|
||||
noIncludedNotes?: boolean;
|
||||
/** If enabled, it will include archived notes when rendering children list. */
|
||||
includeArchivedNotes?: boolean;
|
||||
/** Set of note IDs that have already been seen during rendering to prevent infinite recursion. */
|
||||
seenNoteIds?: Set<string>;
|
||||
}
|
||||
|
||||
const CODE_MIME_TYPES = new Set(["application/json"]);
|
||||
@@ -152,7 +159,7 @@ function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLE
|
||||
|
||||
const $img = $("<img>")
|
||||
.attr("src", url || "")
|
||||
.attr("id", "attachment-image-" + idCounter++)
|
||||
.attr("id", `attachment-image-${idCounter++}`)
|
||||
.css("max-width", "100%");
|
||||
|
||||
$renderedContent.append($img);
|
||||
@@ -193,7 +200,7 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
|
||||
|
||||
if (type === "pdf") {
|
||||
const $pdfPreview = $('<iframe class="pdf-preview" style="width: 100%; flex-grow: 100;"></iframe>');
|
||||
$pdfPreview.attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open`));
|
||||
$pdfPreview.attr("src", openService.getUrlForDownload(`pdfjs/web/viewer.html?file=../../api/${entityType}/${entityId}/open`));
|
||||
|
||||
$content.append($pdfPreview);
|
||||
} else if (type === "audio") {
|
||||
@@ -217,28 +224,28 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
|
||||
// in attachment list
|
||||
const $downloadButton = $(`
|
||||
<button class="file-download btn btn-primary" type="button">
|
||||
<span class="bx bx-download"></span>
|
||||
<span class="tn-icon bx bx-download"></span>
|
||||
${t("file_properties.download")}
|
||||
</button>
|
||||
`);
|
||||
|
||||
const $openButton = $(`
|
||||
<button class="file-open btn btn-primary" type="button">
|
||||
<span class="bx bx-link-external"></span>
|
||||
<span class="tn-icon bx bx-link-external"></span>
|
||||
${t("file_properties.open")}
|
||||
</button>
|
||||
`);
|
||||
|
||||
$downloadButton.on("click", (e) => {
|
||||
e.stopPropagation();
|
||||
openService.downloadFileNote(entity.noteId)
|
||||
openService.downloadFileNote(entity, null, null);
|
||||
});
|
||||
$openButton.on("click", async (e) => {
|
||||
const iconEl = $openButton.find("> .bx");
|
||||
iconEl.removeClass("bx bx-link-external");
|
||||
iconEl.addClass("bx bx-loader spin");
|
||||
e.stopPropagation();
|
||||
await openService.openNoteExternally(entity.noteId, entity.mime)
|
||||
await openService.openNoteExternally(entity.noteId, entity.mime);
|
||||
iconEl.removeClass("bx bx-loader spin");
|
||||
iconEl.addClass("bx bx-link-external");
|
||||
});
|
||||
@@ -266,7 +273,7 @@ async function renderMermaid(note: FNote | FAttachment, $renderedContent: JQuery
|
||||
|
||||
try {
|
||||
await loadElkIfNeeded(mermaid, content);
|
||||
const { svg } = await mermaid.mermaidAPI.render("in-mermaid-graph-" + idCounter++, content);
|
||||
const { svg } = await mermaid.mermaidAPI.render(`in-mermaid-graph-${idCounter++}`, content);
|
||||
|
||||
$renderedContent.append($(postprocessMermaidSvg(svg)));
|
||||
} catch (e) {
|
||||
|
||||
132
apps/client/src/services/content_renderer_text.spec.ts
Normal file
132
apps/client/src/services/content_renderer_text.spec.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { trimIndentation } from "@triliumnext/commons";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildNote } from "../test/easy-froca";
|
||||
import renderText from "./content_renderer_text";
|
||||
|
||||
describe("Text content renderer", () => {
|
||||
it("renders included note", async () => {
|
||||
const contentEl = document.createElement("div");
|
||||
const includedNote = buildNote({
|
||||
title: "Included note",
|
||||
content: "<p>This is the included note.</p>"
|
||||
});
|
||||
const note = buildNote({
|
||||
title: "New note",
|
||||
content: trimIndentation`
|
||||
<p>
|
||||
Hi there
|
||||
</p>
|
||||
<section class="include-note" data-note-id="${includedNote.noteId}" data-box-size="medium">
|
||||
|
||||
</section>
|
||||
`
|
||||
});
|
||||
await renderText(note, $(contentEl));
|
||||
expect(contentEl.querySelectorAll("section.include-note").length).toBe(1);
|
||||
expect(contentEl.querySelectorAll("section.include-note p").length).toBe(1);
|
||||
});
|
||||
|
||||
it("skips rendering included note", async () => {
|
||||
const contentEl = document.createElement("div");
|
||||
const includedNote = buildNote({
|
||||
title: "Included note",
|
||||
content: "<p>This is the included note.</p>"
|
||||
});
|
||||
const note = buildNote({
|
||||
title: "New note",
|
||||
content: trimIndentation`
|
||||
<p>
|
||||
Hi there
|
||||
</p>
|
||||
<section class="include-note" data-note-id="${includedNote.noteId}" data-box-size="medium">
|
||||
|
||||
</section>
|
||||
`
|
||||
});
|
||||
await renderText(note, $(contentEl), { noIncludedNotes: true });
|
||||
expect(contentEl.querySelectorAll("section.include-note").length).toBe(0);
|
||||
});
|
||||
|
||||
it("doesn't enter infinite loop on direct recursion", async () => {
|
||||
const contentEl = document.createElement("div");
|
||||
const note = buildNote({
|
||||
title: "New note",
|
||||
id: "Y7mBwmRjQyb4",
|
||||
content: trimIndentation`
|
||||
<p>
|
||||
Hi there
|
||||
</p>
|
||||
<section class="include-note" data-note-id="Y7mBwmRjQyb4" data-box-size="medium">
|
||||
|
||||
</section>
|
||||
<section class="include-note" data-note-id="Y7mBwmRjQyb4" data-box-size="medium">
|
||||
|
||||
</section>
|
||||
`
|
||||
});
|
||||
await renderText(note, $(contentEl));
|
||||
expect(contentEl.querySelectorAll("section.include-note").length).toBe(0);
|
||||
});
|
||||
|
||||
it("doesn't enter infinite loop on indirect recursion", async () => {
|
||||
const contentEl = document.createElement("div");
|
||||
buildNote({
|
||||
id: "first",
|
||||
title: "Included note",
|
||||
content: trimIndentation`\
|
||||
<p>This is the included note.</p>
|
||||
<section class="include-note" data-note-id="second" data-box-size="medium">
|
||||
|
||||
</section>
|
||||
`
|
||||
});
|
||||
const note = buildNote({
|
||||
id: "second",
|
||||
title: "New note",
|
||||
content: trimIndentation`
|
||||
<p>
|
||||
Hi there
|
||||
</p>
|
||||
<section class="include-note" data-note-id="first" data-box-size="medium">
|
||||
|
||||
</section>
|
||||
`
|
||||
});
|
||||
await renderText(note, $(contentEl));
|
||||
expect(contentEl.querySelectorAll("section.include-note").length).toBe(1);
|
||||
});
|
||||
|
||||
it("renders children list when note is empty", async () => {
|
||||
const contentEl = document.createElement("div");
|
||||
const parentNote = buildNote({
|
||||
title: "Parent note",
|
||||
children: [
|
||||
{ title: "Child note 1" },
|
||||
{ title: "Child note 2" }
|
||||
]
|
||||
});
|
||||
await renderText(parentNote, $(contentEl));
|
||||
const items = contentEl.querySelectorAll("a");
|
||||
expect(items.length).toBe(2);
|
||||
expect(items[0].textContent).toBe("Child note 1");
|
||||
expect(items[1].textContent).toBe("Child note 2");
|
||||
});
|
||||
|
||||
it("skips archived notes in children list", async () => {
|
||||
const contentEl = document.createElement("div");
|
||||
const parentNote = buildNote({
|
||||
title: "Parent note",
|
||||
children: [
|
||||
{ title: "Child note 1" },
|
||||
{ title: "Child note 2", "#archived": "" },
|
||||
{ title: "Child note 3" }
|
||||
]
|
||||
});
|
||||
await renderText(parentNote, $(contentEl));
|
||||
const items = contentEl.querySelectorAll("a");
|
||||
expect(items.length).toBe(2);
|
||||
expect(items[0].textContent).toBe("Child note 1");
|
||||
expect(items[1].textContent).toBe("Child note 3");
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,14 @@ export default async function renderText(note: FNote | FAttachment, $renderedCon
|
||||
|
||||
if (blob && !isHtmlEmpty(blob.content)) {
|
||||
$renderedContent.append($('<div class="ck-content">').html(blob.content));
|
||||
await renderIncludedNotes($renderedContent[0]);
|
||||
|
||||
const seenNoteIds = options.seenNoteIds ?? new Set<string>();
|
||||
seenNoteIds.add("noteId" in note ? note.noteId : note.attachmentId);
|
||||
if (!options.noIncludedNotes) {
|
||||
await renderIncludedNotes($renderedContent[0], seenNoteIds);
|
||||
} else {
|
||||
$renderedContent.find("section.include-note").remove();
|
||||
}
|
||||
|
||||
if ($renderedContent.find("span.math-tex").length > 0) {
|
||||
renderMathInElement($renderedContent[0], { trust: true });
|
||||
@@ -35,11 +42,11 @@ export default async function renderText(note: FNote | FAttachment, $renderedCon
|
||||
await rewriteMermaidDiagramsInContainer($renderedContent[0] as HTMLDivElement);
|
||||
await formatCodeBlocks($renderedContent);
|
||||
} else if (note instanceof FNote && !options.noChildrenList) {
|
||||
await renderChildrenList($renderedContent, note);
|
||||
await renderChildrenList($renderedContent, note, options.includeArchivedNotes ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
async function renderIncludedNotes(contentEl: HTMLElement) {
|
||||
async function renderIncludedNotes(contentEl: HTMLElement, seenNoteIds: Set<string>) {
|
||||
// TODO: Consider duplicating with server's share/content_renderer.ts.
|
||||
const includeNoteEls = contentEl.querySelectorAll("section.include-note");
|
||||
|
||||
@@ -66,7 +73,15 @@ async function renderIncludedNotes(contentEl: HTMLElement) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const renderedContent = (await content_renderer.getRenderedContent(note)).$renderedContent;
|
||||
if (seenNoteIds.has(noteId)) {
|
||||
console.warn(`Skipping inclusion of ${noteId} to avoid circular reference.`);
|
||||
includeNoteEl.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
const renderedContent = (await content_renderer.getRenderedContent(note, {
|
||||
seenNoteIds
|
||||
})).$renderedContent;
|
||||
includeNoteEl.replaceChildren(...renderedContent);
|
||||
}
|
||||
}
|
||||
@@ -98,7 +113,7 @@ export async function applyInlineMermaid(container: HTMLDivElement) {
|
||||
}
|
||||
}
|
||||
|
||||
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote) {
|
||||
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote, includeArchivedNotes: boolean) {
|
||||
let childNoteIds = note.getChildNoteIds();
|
||||
|
||||
if (!childNoteIds.length) {
|
||||
@@ -108,14 +123,16 @@ async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: F
|
||||
$renderedContent.css("padding", "10px");
|
||||
$renderedContent.addClass("text-with-ellipsis");
|
||||
|
||||
// just load the first 10 child notes
|
||||
if (childNoteIds.length > 10) {
|
||||
childNoteIds = childNoteIds.slice(0, 10);
|
||||
}
|
||||
|
||||
// just load the first 10 child notes
|
||||
const childNotes = await froca.getNotes(childNoteIds);
|
||||
|
||||
for (const childNote of childNotes) {
|
||||
if (childNote.isArchived && !includeArchivedNotes) continue;
|
||||
|
||||
$renderedContent.append(
|
||||
await link.createLink(`${note.noteId}/${childNote.noteId}`, {
|
||||
showTooltip: false,
|
||||
|
||||
14
apps/client/src/services/css_class_manager.spec.ts
Normal file
14
apps/client/src/services/css_class_manager.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getReadableTextColor } from "./css_class_manager";
|
||||
|
||||
describe("getReadableTextColor", () => {
|
||||
it("doesn't crash for invalid color", () => {
|
||||
expect(getReadableTextColor("RandomColor")).toBe("#000");
|
||||
});
|
||||
|
||||
it("tolerates different casing", () => {
|
||||
expect(getReadableTextColor("Blue"))
|
||||
.toBe(getReadableTextColor("blue"));
|
||||
});
|
||||
});
|
||||
@@ -1,21 +1,22 @@
|
||||
import clsx from "clsx";
|
||||
import {readCssVar} from "../utils/css-var";
|
||||
import Color, { ColorInstance } from "color";
|
||||
|
||||
import {readCssVar} from "../utils/css-var";
|
||||
|
||||
const registeredClasses = new Set<string>();
|
||||
const colorsWithHue = new Set<string>();
|
||||
|
||||
// Read the color lightness limits defined in the theme as CSS variables
|
||||
|
||||
const lightThemeColorMaxLightness = readCssVar(
|
||||
document.documentElement,
|
||||
"tree-item-light-theme-max-color-lightness"
|
||||
).asNumber(70);
|
||||
document.documentElement,
|
||||
"tree-item-light-theme-max-color-lightness"
|
||||
).asNumber(70);
|
||||
|
||||
const darkThemeColorMinLightness = readCssVar(
|
||||
document.documentElement,
|
||||
"tree-item-dark-theme-min-color-lightness"
|
||||
).asNumber(50);
|
||||
document.documentElement,
|
||||
"tree-item-dark-theme-min-color-lightness"
|
||||
).asNumber(50);
|
||||
|
||||
function createClassForColor(colorString: string | null) {
|
||||
if (!colorString?.trim()) return "";
|
||||
@@ -27,7 +28,7 @@ function createClassForColor(colorString: string | null) {
|
||||
|
||||
if (!registeredClasses.has(className)) {
|
||||
const adjustedColor = adjustColorLightness(color, lightThemeColorMaxLightness!,
|
||||
darkThemeColorMinLightness!);
|
||||
darkThemeColorMinLightness!);
|
||||
const hue = getHue(color);
|
||||
|
||||
$("head").append(`<style>
|
||||
@@ -50,7 +51,7 @@ function createClassForColor(colorString: string | null) {
|
||||
|
||||
function parseColor(color: string) {
|
||||
try {
|
||||
return Color(color);
|
||||
return Color(color.toLowerCase());
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
@@ -84,8 +85,8 @@ function getHue(color: ColorInstance) {
|
||||
}
|
||||
|
||||
export function getReadableTextColor(bgColor: string) {
|
||||
const colorInstance = Color(bgColor);
|
||||
return colorInstance.isLight() ? "#000" : "#fff";
|
||||
const colorInstance = parseColor(bgColor);
|
||||
return !colorInstance || colorInstance?.isLight() ? "#000" : "#fff";
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AttachmentRow, EtapiTokenRow, NoteType, OptionNames } from "@triliumnext/commons";
|
||||
|
||||
import type { AttributeType } from "../entities/fattribute.js";
|
||||
import type { EntityChange } from "../server_types.js";
|
||||
|
||||
@@ -135,7 +136,14 @@ export default class LoadResults {
|
||||
}
|
||||
|
||||
getBranchRows() {
|
||||
return this.branchRows.map((row) => this.getEntityRow("branches", row.branchId)).filter((branch) => !!branch);
|
||||
return this.branchRows.map((row) => {
|
||||
const branch = this.getEntityRow("branches", row.branchId);
|
||||
if (branch) {
|
||||
// Merge the componentId from the tracked row with the entity data
|
||||
return { ...branch, componentId: row.componentId };
|
||||
}
|
||||
return null;
|
||||
}).filter((branch) => !!branch) as BranchRow[];
|
||||
}
|
||||
|
||||
addNoteReordering(parentNoteId: string, componentId: string) {
|
||||
@@ -153,7 +161,14 @@ export default class LoadResults {
|
||||
getAttributeRows(componentId = "none"): AttributeRow[] {
|
||||
return this.attributeRows
|
||||
.filter((row) => row.componentId !== componentId)
|
||||
.map((row) => this.getEntityRow("attributes", row.attributeId))
|
||||
.map((row) => {
|
||||
const attr = this.getEntityRow("attributes", row.attributeId);
|
||||
if (attr) {
|
||||
// Merge the componentId from the tracked row with the entity data
|
||||
return { ...attr, componentId: row.componentId };
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((attr) => !!attr) as AttributeRow[];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import utils from "./utils.js";
|
||||
import Component from "../components/component.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
import options from "./options.js";
|
||||
import server from "./server.js";
|
||||
import utils from "./utils.js";
|
||||
|
||||
type ExecFunction = (command: string, cb: (err: string, stdout: string, stderror: string) => void) => void;
|
||||
|
||||
@@ -36,9 +38,14 @@ function download(url: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function downloadFileNote(noteId: string) {
|
||||
const url = `${getFileUrl("notes", noteId)}?${Date.now()}`; // don't use cache
|
||||
export function downloadFileNote(note: FNote, parentComponent: Component | null, ntxId: string | null | undefined) {
|
||||
if (note.type === "file" && note.mime === "application/pdf" && parentComponent) {
|
||||
// Special handling, manages its own downloading process.
|
||||
parentComponent.triggerEvent("customDownload", { ntxId });
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${getFileUrl("notes", note.noteId)}?${Date.now()}`; // don't use cache
|
||||
download(url);
|
||||
}
|
||||
|
||||
@@ -97,7 +104,7 @@ async function openCustom(type: string, entityId: string, mime: string) {
|
||||
// Note that the path separator must be \ instead of /
|
||||
filePath = filePath.replace(/\//g, "\\");
|
||||
}
|
||||
const command = `rundll32.exe shell32.dll,OpenAs_RunDLL ` + filePath;
|
||||
const command = `rundll32.exe shell32.dll,OpenAs_RunDLL ${filePath}`;
|
||||
exec(command, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.error("Open Note custom: ", err);
|
||||
@@ -131,10 +138,10 @@ export function getUrlForDownload(url: string) {
|
||||
if (utils.isElectron()) {
|
||||
// electron needs absolute URL, so we extract current host, port, protocol
|
||||
return `${getHost()}/${url}`;
|
||||
} else {
|
||||
// web server can be deployed on subdomain, so we need to use a relative path
|
||||
return url;
|
||||
}
|
||||
// web server can be deployed on subdomain, so we need to use a relative path
|
||||
return url;
|
||||
|
||||
}
|
||||
|
||||
function canOpenInBrowser(mime: string) {
|
||||
|
||||
@@ -85,13 +85,15 @@ async function remove<T>(url: string, componentId?: string) {
|
||||
return await call<T>("DELETE", url, componentId);
|
||||
}
|
||||
|
||||
async function upload(url: string, fileToUpload: File) {
|
||||
async function upload(url: string, fileToUpload: File, componentId?: string) {
|
||||
const formData = new FormData();
|
||||
formData.append("upload", fileToUpload);
|
||||
|
||||
return await $.ajax({
|
||||
url: window.glob.baseApiUrl + url,
|
||||
headers: await getHeaders(),
|
||||
headers: await getHeaders(componentId ? {
|
||||
"trilium-component-id": componentId
|
||||
} : undefined),
|
||||
data: formData,
|
||||
type: "PUT",
|
||||
timeout: 60 * 60 * 1000,
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
import type { SaveState } from "../components/note_context";
|
||||
import { getErrorMessage } from "./utils";
|
||||
|
||||
type Callback = () => Promise<void> | void;
|
||||
|
||||
export type StateCallback = (state: SaveState) => void;
|
||||
|
||||
export default class SpacedUpdate {
|
||||
private updater: Callback;
|
||||
private lastUpdated: number;
|
||||
private changed: boolean;
|
||||
private updateInterval: number;
|
||||
private changeForbidden?: boolean;
|
||||
private stateCallback?: StateCallback;
|
||||
|
||||
constructor(updater: Callback, updateInterval = 1000) {
|
||||
constructor(updater: Callback, updateInterval = 1000, stateCallback?: StateCallback) {
|
||||
this.updater = updater;
|
||||
this.lastUpdated = Date.now();
|
||||
this.changed = false;
|
||||
this.updateInterval = updateInterval;
|
||||
this.stateCallback = stateCallback;
|
||||
}
|
||||
|
||||
scheduleUpdate() {
|
||||
if (!this.changeForbidden) {
|
||||
this.changed = true;
|
||||
this.stateCallback?.("unsaved");
|
||||
setTimeout(() => this.triggerUpdate());
|
||||
}
|
||||
}
|
||||
@@ -26,10 +34,13 @@ export default class SpacedUpdate {
|
||||
this.changed = false; // optimistic...
|
||||
|
||||
try {
|
||||
this.stateCallback?.("saving");
|
||||
await this.updater();
|
||||
this.stateCallback?.("saved");
|
||||
} catch (e) {
|
||||
this.changed = true;
|
||||
|
||||
this.stateCallback?.("error");
|
||||
logError(getErrorMessage(e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -59,15 +70,22 @@ export default class SpacedUpdate {
|
||||
this.updateInterval = interval;
|
||||
}
|
||||
|
||||
triggerUpdate() {
|
||||
async triggerUpdate() {
|
||||
if (!this.changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Date.now() - this.lastUpdated > this.updateInterval) {
|
||||
this.updater();
|
||||
this.stateCallback?.("saving");
|
||||
try {
|
||||
await this.updater();
|
||||
this.stateCallback?.("saved");
|
||||
this.changed = false;
|
||||
} catch (e) {
|
||||
this.stateCallback?.("error");
|
||||
logError(getErrorMessage(e));
|
||||
}
|
||||
this.lastUpdated = Date.now();
|
||||
this.changed = false;
|
||||
} else {
|
||||
// update isn't triggered but changes are still pending, so we need to schedule another check
|
||||
this.scheduleUpdate();
|
||||
|
||||
@@ -187,13 +187,15 @@ export function formatSize(size: number | null | undefined) {
|
||||
return "";
|
||||
}
|
||||
|
||||
size = Math.max(Math.round(size / 1024), 1);
|
||||
|
||||
if (size < 1024) {
|
||||
return `${size} KiB`;
|
||||
if (size === 0) {
|
||||
return "0 B";
|
||||
}
|
||||
return `${Math.round(size / 102.4) / 10} MiB`;
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KiB", "MiB", "GiB"];
|
||||
const i = Math.floor(Math.log(size) / Math.log(k));
|
||||
|
||||
return `${Math.round((size / Math.pow(k, i)) * 100) / 100} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
function toObject<T, R>(array: T[], fn: (arg0: T) => [key: string, value: R]) {
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
|
||||
:root {
|
||||
color-scheme: var(--theme-style);
|
||||
|
||||
--main-font-family: "Inter", sans-serif;
|
||||
|
||||
--main-font-size: normal;
|
||||
|
||||
@@ -767,7 +767,7 @@ body.mobile .fancytree-node > span {
|
||||
background: var(--left-pane-item-hover-background);
|
||||
}
|
||||
|
||||
#left-pane span.fancytree-node.shared .fancytree-title::after {
|
||||
#left-pane .note-indicator-icon.shared-indicator {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -1259,8 +1259,16 @@ body.layout-horizontal #rest-pane > .classic-toolbar-widget {
|
||||
#center-pane .note-split {
|
||||
padding-top: 2px;
|
||||
background-color: var(--note-split-background-color, var(--main-background-color));
|
||||
transition: border-color 250ms ease-in;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
/* The active split in a multi-split view */
|
||||
#center-pane > .split-note-container-widget:has(> .note-split.visible ~ .note-split.visible) > .note-split.active {
|
||||
border-color: var(--link-selection-outline-color);
|
||||
}
|
||||
|
||||
|
||||
body:not(.background-effects) #center-pane .note-split {
|
||||
animation: note-entrance 100ms linear;
|
||||
}
|
||||
|
||||
@@ -148,29 +148,28 @@ span.fancytree-node.protected > span.fancytree-custom-icon {
|
||||
filter: drop-shadow(2px 2px 2px var(--main-text-color));
|
||||
}
|
||||
|
||||
span.fancytree-node.multiple-parents.shared .fancytree-title::after {
|
||||
/* Note indicator icons (clone, shared) - real DOM elements for tooltip support */
|
||||
.note-indicator-icon {
|
||||
font-family: "boxicons" !important;
|
||||
font-size: smaller;
|
||||
content: " \eb3d \ec03";
|
||||
margin-inline-start: 4px;
|
||||
opacity: 0.8;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
span.fancytree-node.multiple-parents .fancytree-title::after {
|
||||
font-family: "boxicons" !important;
|
||||
font-size: smaller;
|
||||
content: " \eb3d"; /* lookup code for "link-alt" in boxicons.css */
|
||||
.note-indicator-icon.clone-indicator::before {
|
||||
content: "\eb3d"; /* bx-link-alt */
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout span.fancytree-node.multiple-parents .fancytree-title::after {
|
||||
content: " \ed82";
|
||||
.note-indicator-icon.shared-indicator::before {
|
||||
content: "\ec03"; /* bx-share-alt */
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout .note-indicator-icon.clone-indicator::before {
|
||||
content: "\ed82";
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
span.fancytree-node.shared .fancytree-title::after {
|
||||
font-family: "boxicons" !important;
|
||||
font-size: smaller;
|
||||
content: " \ec03"; /* lookup code for "share-alt" in boxicons.css */
|
||||
}
|
||||
|
||||
span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -69,24 +69,6 @@ export function buildNote(noteDef: NoteDefinition) {
|
||||
});
|
||||
note.getBlob = async () => blob;
|
||||
|
||||
// Manage children.
|
||||
if (noteDef.children) {
|
||||
for (const childDef of noteDef.children) {
|
||||
const childNote = buildNote(childDef);
|
||||
const branchId = `${note.noteId}_${childNote.noteId}`;
|
||||
const branch = new FBranch(froca, {
|
||||
branchId,
|
||||
noteId: childNote.noteId,
|
||||
parentNoteId: note.noteId,
|
||||
notePosition: childNotePosition,
|
||||
fromSearchNote: false
|
||||
});
|
||||
froca.branches[branchId] = branch;
|
||||
note.addChild(childNote.noteId, branchId, false);
|
||||
childNotePosition += 10;
|
||||
}
|
||||
}
|
||||
|
||||
let position = 0;
|
||||
for (const [ key, value ] of Object.entries(noteDef)) {
|
||||
const attributeId = utils.randomString(12);
|
||||
@@ -136,5 +118,25 @@ export function buildNote(noteDef: NoteDefinition) {
|
||||
}
|
||||
noteAttributeCache.attributes[note.noteId].push(attribute);
|
||||
}
|
||||
|
||||
// Manage children.
|
||||
if (noteDef.children) {
|
||||
for (const childDef of noteDef.children) {
|
||||
const childNote = buildNote(childDef);
|
||||
const branchId = `${note.noteId}_${childNote.noteId}`;
|
||||
const branch = new FBranch(froca, {
|
||||
branchId,
|
||||
noteId: childNote.noteId,
|
||||
parentNoteId: note.noteId,
|
||||
notePosition: childNotePosition,
|
||||
fromSearchNote: false
|
||||
});
|
||||
froca.branches[branchId] = branch;
|
||||
note.addChild(childNote.noteId, branchId, false);
|
||||
childNote.addParent(note.noteId, branchId, false);
|
||||
childNotePosition += 10;
|
||||
}
|
||||
}
|
||||
|
||||
return note;
|
||||
}
|
||||
|
||||
@@ -484,7 +484,6 @@
|
||||
"delete_button": "حذف",
|
||||
"download_button": "تنزيل",
|
||||
"restore_button": "أستعادة",
|
||||
"preview": "معاينة:",
|
||||
"note_revisions": "مراجعات الملاحظة",
|
||||
"diff_on": "عرض الفروقات",
|
||||
"diff_off": "عرض المحتوى",
|
||||
|
||||
@@ -64,8 +64,7 @@
|
||||
"restore_button": "Restaura",
|
||||
"delete_button": "Suprimeix",
|
||||
"download_button": "Descarrega",
|
||||
"mime": "MIME: ",
|
||||
"preview": "Vista prèvia:"
|
||||
"mime": "MIME: "
|
||||
},
|
||||
"sort_child_notes": {
|
||||
"title": "títol",
|
||||
|
||||
@@ -290,7 +290,6 @@
|
||||
"download_button": "下载",
|
||||
"mime": "MIME 类型: ",
|
||||
"file_size": "文件大小:",
|
||||
"preview": "预览:",
|
||||
"preview_not_available": "无法预览此类型的笔记。",
|
||||
"diff_on": "显示差异",
|
||||
"diff_off": "显示内容",
|
||||
@@ -765,7 +764,14 @@
|
||||
"note_icon": {
|
||||
"change_note_icon": "更改笔记图标",
|
||||
"search": "搜索:",
|
||||
"reset-default": "重置为默认图标"
|
||||
"reset-default": "重置为默认图标",
|
||||
"search_placeholder_other": "在 {{count}} 个图标包中搜索 {{number}} 个图标",
|
||||
"search_placeholder_filtered": "在 {{name}} 中搜索 {{number}} 个图标",
|
||||
"filter": "筛选",
|
||||
"filter-none": "所有图标",
|
||||
"filter-default": "默认图标",
|
||||
"icon_tooltip": "{{name}}\n图标包:{{iconPack}}",
|
||||
"no_results": "没有找到图标。"
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "笔记类型",
|
||||
@@ -792,7 +798,8 @@
|
||||
"expand_tooltip": "展开此集合的直接子代(单层深度)。点击右方箭头以查看更多选项。",
|
||||
"expand_first_level": "展开直接子代",
|
||||
"expand_nth_level": "展开 {{depth}} 层",
|
||||
"expand_all_levels": "展开所有层级"
|
||||
"expand_all_levels": "展开所有层级",
|
||||
"hide_child_notes": "隐藏树中的子笔记"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "今天还没有编辑过的笔记...",
|
||||
@@ -1445,7 +1452,7 @@
|
||||
"will_be_deleted_in": "此附件将在 {{time}} 后自动删除",
|
||||
"will_be_deleted_soon": "该附件在不久后将被自动删除",
|
||||
"deletion_reason": ",因为该附件未链接在笔记的内容中。为防止被删除,请将附件链接重新添加到内容中或将附件转换为笔记。",
|
||||
"role_and_size": "角色:{{role}},大小:{{size}}",
|
||||
"role_and_size": "角色:{{role}},大小:{{size}},文件类型:{{- mimeType}}",
|
||||
"link_copied": "附件链接已复制到剪贴板。",
|
||||
"unrecognized_role": "无法识别的附件角色 '{{role}}'。"
|
||||
},
|
||||
@@ -1499,7 +1506,10 @@
|
||||
"duplicate": "复制",
|
||||
"open-in-popup": "快速编辑",
|
||||
"archive": "归档",
|
||||
"unarchive": "解压"
|
||||
"unarchive": "解压",
|
||||
"open-in-a-new-window": "在新窗口中打开",
|
||||
"hide-subtree": "隐藏子树",
|
||||
"show-subtree": "显示子树"
|
||||
},
|
||||
"shared_info": {
|
||||
"help_link": "访问 <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a> 获取帮助。",
|
||||
@@ -1588,7 +1598,13 @@
|
||||
"create-child-note": "创建子笔记",
|
||||
"unhoist": "取消聚焦",
|
||||
"toggle-sidebar": "切换侧边栏",
|
||||
"dropping-not-allowed": "不允许移动笔记到此处。"
|
||||
"dropping-not-allowed": "不允许移动笔记到此处。",
|
||||
"shared-indicator-tooltip": "此笔记已公开分享",
|
||||
"shared-indicator-tooltip-with-url": "此笔记已公开分享至:{{- url}}",
|
||||
"clone-indicator-tooltip": "此笔记有 {{- count}} 个父级: {{- parents}}",
|
||||
"clone-indicator-tooltip-single": "此笔记已克隆(1 个额外的父级:{{- parent}})",
|
||||
"subtree-hidden-tooltip_other": "从树中隐藏的 {{count}} 篇子笔记",
|
||||
"subtree-hidden-moved-title": "已添加到 {{title}}"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "保持此窗口置顶"
|
||||
@@ -2186,7 +2202,14 @@
|
||||
"execute_sql_description": "这是一篇 SQL 笔记。点击即可执行 SQL 查询。",
|
||||
"shared_copy_to_clipboard": "复制链接到剪贴板",
|
||||
"shared_open_in_browser": "在浏览器中打开链接",
|
||||
"shared_unshare": "取消共享"
|
||||
"shared_unshare": "取消共享",
|
||||
"save_status_saved": "已保存",
|
||||
"save_status_saving": "保存中...",
|
||||
"save_status_unsaved": "未保存",
|
||||
"save_status_error": "保存失败",
|
||||
"save_status_unsaved_tooltip": "还有一些更改尚未保存。它们将稍后自动保存。",
|
||||
"save_status_error_tooltip": "保存笔记时出错。如果可以,请尝试将笔记内容复制到其他位置并重新加载应用程序。",
|
||||
"save_status_saving_tooltip": "更改正在保存。"
|
||||
},
|
||||
"status_bar": {
|
||||
"language_title": "更改内容语言",
|
||||
@@ -2217,5 +2240,12 @@
|
||||
},
|
||||
"attributes_panel": {
|
||||
"title": "笔记属性"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_other": "{{count}} 个附件",
|
||||
"pages_other": "共{{count}}页",
|
||||
"pages_alt": "第{{pageNumber}}页",
|
||||
"pages_loading": "加载中...",
|
||||
"layers_other": "{{count}} 层"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "Abruf der Liste von Widgets vom Server ist fehlgeschlagen"
|
||||
}
|
||||
},
|
||||
"open-script-note": "Script-Notiz öffnen"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Link hinzufügen",
|
||||
@@ -208,7 +209,8 @@
|
||||
"info": {
|
||||
"modalTitle": "Infonachricht",
|
||||
"closeButton": "Schließen",
|
||||
"okButton": "OK"
|
||||
"okButton": "OK",
|
||||
"copy_to_clipboard": "In die Zwischenablage kopieren"
|
||||
},
|
||||
"jump_to_note": {
|
||||
"search_button": "Suche im Volltext",
|
||||
@@ -281,7 +283,6 @@
|
||||
"download_button": "Herunterladen",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "Dateigröße:",
|
||||
"preview": "Vorschau:",
|
||||
"preview_not_available": "Für diesen Notiztyp ist keine Vorschau verfügbar.",
|
||||
"restore_button": "Wiederherstellen",
|
||||
"delete_button": "Löschen",
|
||||
@@ -692,7 +693,13 @@
|
||||
"convert_into_attachment_successful": "Notiz '{{title}}' wurde als Anhang konvertiert.",
|
||||
"convert_into_attachment_prompt": "Bist du dir sicher, dass du die Notiz '{{title}}' in ein Anhang der übergeordneten Notiz konvertieren möchtest?",
|
||||
"print_pdf": "Export als PDF...",
|
||||
"open_note_on_server": "Öffne Notiz auf dem Server"
|
||||
"open_note_on_server": "Öffne Notiz auf dem Server",
|
||||
"export_as_image": "Als Bild exportieren",
|
||||
"export_as_image_png": "PNG (Raster)",
|
||||
"export_as_image_svg": "SVG (Vektor)",
|
||||
"note_map": "Notizen Karte",
|
||||
"view_revisions": "Notizrevisionen",
|
||||
"advanced": "Erweitert"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "Das Schaltflächen-Widget „{{componentId}}“ hat keinen definierten Klick-Handler"
|
||||
@@ -750,7 +757,15 @@
|
||||
"note_icon": {
|
||||
"change_note_icon": "Notiz-Icon ändern",
|
||||
"search": "Suche:",
|
||||
"reset-default": "Standard wiederherstellen"
|
||||
"reset-default": "Standard wiederherstellen",
|
||||
"search_placeholder_one": "Suche {{number}} Icons über {{count}} Pakete",
|
||||
"search_placeholder_other": "Suche {{number}} Icons über {{count}} Pakete",
|
||||
"search_placeholder_filtered": "Suche {{number}} Icons in {{name}}",
|
||||
"filter": "Filter",
|
||||
"filter-none": "Alle Icons",
|
||||
"filter-default": "Standard Icons",
|
||||
"icon_tooltip": "{{name}}\nIcon Paket: {{iconPack}}",
|
||||
"no_results": "Keine Icons gefunden."
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "Notiztyp",
|
||||
@@ -810,7 +825,8 @@
|
||||
},
|
||||
"inherited_attribute_list": {
|
||||
"title": "Geerbte Attribute",
|
||||
"no_inherited_attributes": "Keine geerbten Attribute."
|
||||
"no_inherited_attributes": "Keine geerbten Attribute.",
|
||||
"none": "Keine"
|
||||
},
|
||||
"note_info_widget": {
|
||||
"note_id": "Notiz-ID",
|
||||
@@ -821,7 +837,9 @@
|
||||
"note_size_info": "Die Notizgröße bietet eine grobe Schätzung des Speicherbedarfs für diese Notiz. Es berücksichtigt den Inhalt der Notiz und den Inhalt ihrer Notizrevisionen.",
|
||||
"calculate": "berechnen",
|
||||
"subtree_size": "(Teilbaumgröße: {{size}} in {{count}} Notizen)",
|
||||
"title": "Notizinfo"
|
||||
"title": "Notizinfo",
|
||||
"mime": "MIME Typ",
|
||||
"show_similar_notes": "Zeige ähnliche Notizen"
|
||||
},
|
||||
"note_map": {
|
||||
"open_full": "Vollständig erweitern",
|
||||
@@ -884,7 +902,8 @@
|
||||
"search_parameters": "Suchparameter",
|
||||
"unknown_search_option": "Unbekannte Suchoption {{searchOptionName}}",
|
||||
"search_note_saved": "Suchnotiz wurde in {{-notePathTitle}} gespeichert",
|
||||
"actions_executed": "Aktionen wurden ausgeführt."
|
||||
"actions_executed": "Aktionen wurden ausgeführt.",
|
||||
"view_options": "Anzeigeoptionen:"
|
||||
},
|
||||
"similar_notes": {
|
||||
"title": "Ähnliche Notizen",
|
||||
@@ -988,7 +1007,12 @@
|
||||
"editable_text": {
|
||||
"placeholder": "Gebe hier den Inhalt deiner Notiz ein...",
|
||||
"auto-detect-language": "Automatisch erkannt",
|
||||
"keeps-crashing": "Die Bearbeitungskomponente stürzt immer wieder ab. Bitte starten Sie Trilium neu. Wenn das Problem weiterhin besteht, erstellen Sie einen Fehlerbericht."
|
||||
"keeps-crashing": "Die Bearbeitungskomponente stürzt immer wieder ab. Bitte starten Sie Trilium neu. Wenn das Problem weiterhin besteht, erstellen Sie einen Fehlerbericht.",
|
||||
"editor_crashed_title": "Der Text Editor ist abgestürzt",
|
||||
"editor_crashed_content": "Ihr Inhalt wurde erfolgreich wiederhergestellt, aber einzelne Ihrer letzten Änderungen waren möglicherweise noch nicht gespeichert.",
|
||||
"editor_crashed_details_button": "Zeige mehr Details…",
|
||||
"editor_crashed_details_intro": "Falls Sie diesen Fehler mehrmals sehen, melden Sie dies auf GitHub mit den folgenden Informationen.",
|
||||
"editor_crashed_details_title": "Technische Informationen"
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "Öffne eine Notiz, indem du den Titel der Notiz in die Eingabe unten eingibst oder eine Notiz in der Baumstruktur auswählst.",
|
||||
@@ -1503,7 +1527,12 @@
|
||||
},
|
||||
"highlights_list_2": {
|
||||
"title": "Hervorhebungs-Liste",
|
||||
"options": "Optionen"
|
||||
"options": "Optionen",
|
||||
"title_with_count_one": "{{count}} Highlight",
|
||||
"title_with_count_other": "{{count}} Highlights",
|
||||
"modal_title": "Highlight Liste konfigurieren",
|
||||
"menu_configure": "Highlight Liste konfigurieren…",
|
||||
"no_highlights": "Keine Highlights gefunden."
|
||||
},
|
||||
"quick-search": {
|
||||
"placeholder": "Schnellsuche",
|
||||
@@ -1535,10 +1564,21 @@
|
||||
"note_detail": {
|
||||
"could_not_find_typewidget": "Konnte typeWidget für Typ ‚{{type}}‘ nicht finden",
|
||||
"printing": "Druckvorgang läuft…",
|
||||
"printing_pdf": "PDF-Export läuft…"
|
||||
"printing_pdf": "PDF-Export läuft…",
|
||||
"print_report_title": "Druckreport",
|
||||
"print_report_collection_details_button": "Details anzeigen",
|
||||
"print_report_collection_details_ignored_notes": "Ignorierte Notizen"
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "Titel der Notiz hier eingeben…"
|
||||
"placeholder": "Titel der Notiz hier eingeben…",
|
||||
"created_on": "Erstellt am <Value />",
|
||||
"last_modified": "Bearbeitet am <Value />",
|
||||
"note_type_switcher_label": "Ändere von {{type}} zu:",
|
||||
"note_type_switcher_others": "Andere Notizart",
|
||||
"note_type_switcher_templates": "Template",
|
||||
"note_type_switcher_collection": "Sammlung",
|
||||
"edited_notes": "Notizen, bearbeitet an diesem Tag",
|
||||
"promoted_attributes": "Hervorgehobene Attribute"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "Es wurden keine Notizen mit den angegebenen Suchparametern gefunden.",
|
||||
@@ -1567,7 +1607,8 @@
|
||||
},
|
||||
"toc": {
|
||||
"table_of_contents": "Inhaltsverzeichnis",
|
||||
"options": "Optionen"
|
||||
"options": "Optionen",
|
||||
"no_headings": "Keine Überschriften."
|
||||
},
|
||||
"watched_file_update_status": {
|
||||
"file_last_modified": "Datei <code class=\"file-path\"></code> wurde zuletzt geändert am <span class=\"file-last-modified\"></span>.",
|
||||
@@ -2106,5 +2147,10 @@
|
||||
},
|
||||
"popup-editor": {
|
||||
"maximize": "Wechsele zum vollständigen Editor"
|
||||
},
|
||||
"experimental_features": {
|
||||
"title": "Experimentelle Optionen",
|
||||
"disclaimer": "Diese Optionen sind experimentell und können Instabilitäten verursachen. Achtsam zu verwenden.",
|
||||
"new_layout_name": "Neues Layout"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +295,6 @@
|
||||
"download_button": "Download",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "File size:",
|
||||
"preview": "Preview:",
|
||||
"preview_not_available": "Preview isn't available for this note type."
|
||||
},
|
||||
"sort_child_notes": {
|
||||
@@ -801,7 +800,8 @@
|
||||
"geo-map": "Geo Map",
|
||||
"board": "Board",
|
||||
"presentation": "Presentation",
|
||||
"include_archived_notes": "Show archived notes"
|
||||
"include_archived_notes": "Show archived notes",
|
||||
"hide_child_notes": "Hide child notes in tree"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "No edited notes on this day yet...",
|
||||
@@ -1644,6 +1644,7 @@
|
||||
"tree-context-menu": {
|
||||
"open-in-a-new-tab": "Open in a new tab",
|
||||
"open-in-a-new-split": "Open in a new split",
|
||||
"open-in-a-new-window": "Open in a new window",
|
||||
"insert-note-after": "Insert note after",
|
||||
"insert-child-note": "Insert child note",
|
||||
"archive": "Archive",
|
||||
@@ -1656,6 +1657,8 @@
|
||||
"advanced": "Advanced",
|
||||
"expand-subtree": "Expand subtree",
|
||||
"collapse-subtree": "Collapse subtree",
|
||||
"hide-subtree": "Hide subtree",
|
||||
"show-subtree": "Show subtree",
|
||||
"sort-by": "Sort by...",
|
||||
"recent-changes-in-subtree": "Recent changes in subtree",
|
||||
"convert-to-attachment": "Convert to attachment",
|
||||
@@ -1769,7 +1772,16 @@
|
||||
"create-child-note": "Create child note",
|
||||
"unhoist": "Unhoist",
|
||||
"toggle-sidebar": "Toggle sidebar",
|
||||
"dropping-not-allowed": "Dropping notes into this location is not allowed."
|
||||
"dropping-not-allowed": "Dropping notes into this location is not allowed.",
|
||||
"clone-indicator-tooltip": "This note has {{- count}} parents: {{- parents}}",
|
||||
"clone-indicator-tooltip-single": "This note is cloned (1 additional parent: {{- parent}})",
|
||||
"shared-indicator-tooltip": "This note is shared publicly",
|
||||
"shared-indicator-tooltip-with-url": "This note is shared publicly at: {{- url}}",
|
||||
"subtree-hidden-tooltip_one": "{{count}} child note that is hidden from the tree",
|
||||
"subtree-hidden-tooltip_other": "{{count}} child notes that are hidden from the tree",
|
||||
"subtree-hidden-moved-title": "Added to {{title}}",
|
||||
"subtree-hidden-moved-description-collection": "This collection hides its child notes in the tree.",
|
||||
"subtree-hidden-moved-description-other": "Child notes are hidden in the tree for this note."
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Keep Window on Top"
|
||||
@@ -2205,7 +2217,14 @@
|
||||
"execute_script": "Run script",
|
||||
"execute_script_description": "This note is a script note. Click to execute the script.",
|
||||
"execute_sql": "Run SQL",
|
||||
"execute_sql_description": "This note is a SQL note. Click to execute the SQL query."
|
||||
"execute_sql_description": "This note is a SQL note. Click to execute the SQL query.",
|
||||
"save_status_saved": "Saved",
|
||||
"save_status_saving": "Saving...",
|
||||
"save_status_unsaved": "Unsaved",
|
||||
"save_status_error": "Save failed",
|
||||
"save_status_saving_tooltip": "Changes are being saved.",
|
||||
"save_status_unsaved_tooltip": "There are unsaved changes. They will be saved automatically in a moment.",
|
||||
"save_status_error_tooltip": "An error occurred while saving the note. If possible, try copying the note content elsewhere and reloading the application."
|
||||
},
|
||||
"status_bar": {
|
||||
"language_title": "Change content language",
|
||||
@@ -2234,5 +2253,15 @@
|
||||
"empty_button": "Hide the panel",
|
||||
"toggle": "Toggle right panel",
|
||||
"custom_widget_go_to_source": "Go to source code"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_one": "{{count}} attachment",
|
||||
"attachments_other": "{{count}} attachments",
|
||||
"layers_one": "{{count}} layer",
|
||||
"layers_other": "{{count}} layers",
|
||||
"pages_one": "{{count}} page",
|
||||
"pages_other": "{{count}} pages",
|
||||
"pages_alt": "Page {{pageNumber}}",
|
||||
"pages_loading": "Loading..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,13 @@
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Hubo un fallo al cargar un script personalizado",
|
||||
"message": "El script de la nota con ID \"{{id}}\", titulado \"{{title}}\" no pudo ser ejecutado debido a:\n\n{{message}}"
|
||||
"message": "El script no pudo ser ejecutado debido a:\n\n{{message}}"
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "Hubo un fallo al obtener la lista de widgets del servidor"
|
||||
},
|
||||
"widget-render-error": {
|
||||
"title": "Hubo un fallo al renderizar un widget personalizado de React"
|
||||
}
|
||||
},
|
||||
"add_link": {
|
||||
@@ -162,7 +168,8 @@
|
||||
"other": "Otro",
|
||||
"quickSearch": "centrarse en la entrada de búsqueda rápida",
|
||||
"inPageSearch": "búsqueda en la página",
|
||||
"title": "Hoja de ayuda"
|
||||
"title": "Hoja de ayuda",
|
||||
"editShortcuts": "Editar atajos de teclado"
|
||||
},
|
||||
"import": {
|
||||
"importIntoNote": "Importar a nota",
|
||||
@@ -279,7 +286,6 @@
|
||||
"download_button": "Descargar",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "Tamaño del archivo:",
|
||||
"preview": "Vista previa:",
|
||||
"preview_not_available": "La vista previa no está disponible para este tipo de notas.",
|
||||
"diff_off": "Mostrar contenido",
|
||||
"diff_on": "Mostrar diferencia",
|
||||
@@ -691,7 +697,7 @@
|
||||
"convert_into_attachment_successful": "La nota '{{title}}' ha sido convertida a un archivo adjunto.",
|
||||
"convert_into_attachment_prompt": "¿Está seguro que desea convertir la nota '{{title}}' en un archivo adjunto de la nota padre?",
|
||||
"print_pdf": "Exportar como PDF...",
|
||||
"open_note_on_server": "Abrir nota en el servidor"
|
||||
"open_note_on_server": "Abrir nota en servidor"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "El widget de botón '{{componentId}}' no tiene un controlador de clics definido"
|
||||
@@ -737,7 +743,7 @@
|
||||
"zpetne_odkazy": {
|
||||
"relation": "relación",
|
||||
"backlink_one": "{{count}} Vínculo de retroceso",
|
||||
"backlink_many": "",
|
||||
"backlink_many": "{{count}} Vínculos de retroceso",
|
||||
"backlink_other": "{{count}} vínculos de retroceso"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
@@ -750,7 +756,10 @@
|
||||
"note_icon": {
|
||||
"change_note_icon": "Cambiar icono de nota",
|
||||
"search": "Búsqueda:",
|
||||
"reset-default": "Restablecer a icono por defecto"
|
||||
"reset-default": "Restablecer a icono por defecto",
|
||||
"search_placeholder_one": "Buscar {{number}} icono a través de {{count}} paquetes",
|
||||
"search_placeholder_many": "Buscar {{number}} iconos a través de {{count}} paquetes",
|
||||
"search_placeholder_other": "Buscar {{number}} iconos a través de {{count}} paquetes"
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "Tipo de nota",
|
||||
@@ -790,7 +799,7 @@
|
||||
"file_type": "Tipo de archivo",
|
||||
"file_size": "Tamaño del archivo",
|
||||
"download": "Descargar",
|
||||
"open": "Abrir",
|
||||
"open": "Abrir externamente",
|
||||
"upload_new_revision": "Subir nueva revisión",
|
||||
"upload_success": "Se ha subido una nueva revisión de archivo.",
|
||||
"upload_failed": "Error al cargar una nueva revisión de archivo.",
|
||||
@@ -1303,11 +1312,11 @@
|
||||
"code_mime_types": {
|
||||
"title": "Tipos MIME disponibles en el menú desplegable",
|
||||
"tooltip_syntax_highlighting": "Resaltado de sintaxis",
|
||||
"tooltip_code_block_syntax": "Bloques de código en notas de texto",
|
||||
"tooltip_code_note_syntax": "Notas de código"
|
||||
"tooltip_code_block_syntax": "Bloques de Código en notas de Texto",
|
||||
"tooltip_code_note_syntax": "Notas de Código"
|
||||
},
|
||||
"vim_key_bindings": {
|
||||
"use_vim_keybindings_in_code_notes": "Atajos de teclas de Vim",
|
||||
"use_vim_keybindings_in_code_notes": "Combinaciones de teclas Vim",
|
||||
"enable_vim_keybindings": "Habilitar los atajos de teclas de Vim en la notas de código (no es modo ex)"
|
||||
},
|
||||
"wrap_lines": {
|
||||
@@ -1572,7 +1581,7 @@
|
||||
"will_be_deleted_in": "Este archivo adjunto se eliminará automáticamente en {{time}}",
|
||||
"will_be_deleted_soon": "Este archivo adjunto se eliminará automáticamente pronto",
|
||||
"deletion_reason": ", porque el archivo adjunto no está vinculado en el contenido de la nota. Para evitar la eliminación, vuelva a agregar el enlace del archivo adjunto al contenido o convierta el archivo adjunto en una nota.",
|
||||
"role_and_size": "Rol: {{role}}, Tamaño: {{size}}",
|
||||
"role_and_size": "Rol: {{role}}, tamaño: {{size}}, MIME: {{- mimeType}}",
|
||||
"link_copied": "Enlace del archivo adjunto copiado al portapapeles.",
|
||||
"unrecognized_role": "Rol de archivo adjunto no reconocido '{{role}}'."
|
||||
},
|
||||
@@ -1623,7 +1632,7 @@
|
||||
"import-into-note": "Importar a nota",
|
||||
"apply-bulk-actions": "Aplicar acciones en lote",
|
||||
"converted-to-attachments": "{{count}} notas han sido convertidas en archivos adjuntos.",
|
||||
"convert-to-attachment-confirm": "¿Está seguro que desea convertir las notas seleccionadas en archivos adjuntos de sus notas padres?",
|
||||
"convert-to-attachment-confirm": "¿Está seguro que desea convertir las notas seleccionadas en archivos adjuntos de sus notas padres? Esta operación solo aplica a notas de Imagen, otras notas serán omitidas.",
|
||||
"open-in-popup": "Edición rápida",
|
||||
"archive": "Archivar",
|
||||
"unarchive": "Desarchivar"
|
||||
@@ -1718,7 +1727,10 @@
|
||||
"note_detail": {
|
||||
"could_not_find_typewidget": "No se pudo encontrar typeWidget para el tipo '{{type}}'",
|
||||
"printing": "Impresión en curso...",
|
||||
"printing_pdf": "Exportando a PDF en curso.."
|
||||
"printing_pdf": "Exportando a PDF en curso..",
|
||||
"print_report_collection_content_one": "{{count}} nota en la colección no se puede imprimir porque no son compatibles o está protegida.",
|
||||
"print_report_collection_content_many": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas.",
|
||||
"print_report_collection_content_other": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas."
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "escriba el título de la nota aquí..."
|
||||
@@ -1930,7 +1942,7 @@
|
||||
"unknown_widget": "Widget desconocido para \"{{id}}\"."
|
||||
},
|
||||
"note_language": {
|
||||
"not_set": "No establecido",
|
||||
"not_set": "Idioma no establecido",
|
||||
"configure-languages": "Configurar idiomas..."
|
||||
},
|
||||
"content_language": {
|
||||
@@ -1969,7 +1981,7 @@
|
||||
"hide-weekends": "Ocultar fines de semana",
|
||||
"show-scale": "Mostrar escala",
|
||||
"display-week-numbers": "Mostrar números de semana",
|
||||
"map-style": "Estilo de mapa:",
|
||||
"map-style": "Estilo de mapa",
|
||||
"max-nesting-depth": "Máxima profundidad de anidamiento:",
|
||||
"vector_light": "Vector (claro)",
|
||||
"vector_dark": "Vector (oscuro)",
|
||||
@@ -2098,5 +2110,36 @@
|
||||
"clear-color": "Borrar color de nota",
|
||||
"set-color": "Asignar color de nota",
|
||||
"set-custom-color": "Asignar color de nota personalizado"
|
||||
},
|
||||
"status_bar": {
|
||||
"backlinks_one": "{{count}} vínculo de retroceso",
|
||||
"backlinks_many": "{{count}} vínculos de retroceso",
|
||||
"backlinks_other": "{{count}} vínculos de retroceso",
|
||||
"backlinks_title_one": "Ver vínculo de retroceso",
|
||||
"backlinks_title_many": "Ver vínculos de retroceso",
|
||||
"backlinks_title_other": "Ver vínculos de retroceso",
|
||||
"attachments_one": "{{count}} adjunto",
|
||||
"attachments_many": "{{count}} adjuntos",
|
||||
"attachments_other": "{{count}} adjuntos",
|
||||
"attachments_title_one": "Ver adjunto en una nueva pestaña",
|
||||
"attachments_title_many": "Ver adjuntos en una nueva pestaña",
|
||||
"attachments_title_other": "Ver adjuntos en una nueva pestaña",
|
||||
"attributes_one": "{{count}} atributo",
|
||||
"attributes_many": "{{count}} atributos",
|
||||
"attributes_other": "{{count}} atributos",
|
||||
"note_paths_one": "{{count}} ruta",
|
||||
"note_paths_many": "{{count}} rutas",
|
||||
"note_paths_other": "{{count}} rutas"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_one": "{{count}} adjunto",
|
||||
"attachments_many": "{{count}} adjuntos",
|
||||
"attachments_other": "{{count}} adjuntos",
|
||||
"layers_one": "{{count}} capa",
|
||||
"layers_many": "{{count}} capas",
|
||||
"layers_other": "{{count}} capas",
|
||||
"pages_one": "{{count}} página",
|
||||
"pages_many": "{{count}} páginas",
|
||||
"pages_other": "{{count}} páginas"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,13 @@
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Echec du chargement d'un script personnalisé",
|
||||
"message": "Le script de la note avec l'ID \"{{id}}\", intitulé \"{{title}}\" n'a pas pu être exécuté à cause de\n\n{{message}}"
|
||||
"message": "Le script n'a pas pu être exécuté à cause de\n\n{{message}}"
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "Impossible d'obtenir la liste des widgets depuis le serveur"
|
||||
},
|
||||
"widget-render-error": {
|
||||
"title": "Rendu impossible d'un widget React custom"
|
||||
}
|
||||
},
|
||||
"add_link": {
|
||||
@@ -279,7 +285,6 @@
|
||||
"download_button": "Télécharger",
|
||||
"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.",
|
||||
"restore_button": "Restaurer",
|
||||
"delete_button": "Supprimer",
|
||||
@@ -757,7 +762,11 @@
|
||||
"note_icon": {
|
||||
"change_note_icon": "Changer l'icône de note",
|
||||
"search": "Recherche :",
|
||||
"reset-default": "Réinitialiser l'icône par défaut"
|
||||
"reset-default": "Réinitialiser l'icône par défaut",
|
||||
"filter": "Filtre",
|
||||
"filter-none": "Toutes les icônes",
|
||||
"filter-default": "Icônes par défaut",
|
||||
"icon_tooltip": "{{name}}\nPack d'icônes : {{iconPack}}"
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "Type de note",
|
||||
@@ -1541,7 +1550,8 @@
|
||||
"refresh-saved-search-results": "Rafraîchir les résultats de recherche enregistrée",
|
||||
"create-child-note": "Créer une note enfant",
|
||||
"unhoist": "Désactiver le focus",
|
||||
"toggle-sidebar": "Basculer la barre latérale"
|
||||
"toggle-sidebar": "Basculer la barre latérale",
|
||||
"dropping-not-allowed": "Lâcher des notes à cet endroit n'est pas autorisé"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Épingler cette fenêtre au premier plan"
|
||||
@@ -1549,10 +1559,19 @@
|
||||
"note_detail": {
|
||||
"could_not_find_typewidget": "Impossible de trouver typeWidget pour le type '{{type}}'",
|
||||
"printing": "Impression en cours...",
|
||||
"printing_pdf": "Export au format PDF en cours..."
|
||||
"printing_pdf": "Export au format PDF en cours...",
|
||||
"print_report_title": "Imprimer le rapport",
|
||||
"print_report_collection_details_button": "Consulter les détails",
|
||||
"print_report_collection_details_ignored_notes": "Notes ignorées"
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "saisir le titre de la note ici..."
|
||||
"placeholder": "saisir le titre de la note ici...",
|
||||
"created_on": "Créé le <Value />",
|
||||
"last_modified": "Modifié le <Value />",
|
||||
"note_type_switcher_label": "Basculer de {{type}} à :",
|
||||
"note_type_switcher_others": "Autre type de note",
|
||||
"note_type_switcher_templates": "Modèle",
|
||||
"note_type_switcher_collection": "Collection"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "Aucune note n'a été trouvée pour les paramètres de recherche donnés.",
|
||||
@@ -1581,7 +1600,8 @@
|
||||
},
|
||||
"toc": {
|
||||
"table_of_contents": "Table des matières",
|
||||
"options": "Options"
|
||||
"options": "Options",
|
||||
"no_headings": "Pas d'en-tête."
|
||||
},
|
||||
"watched_file_update_status": {
|
||||
"file_last_modified": "Le fichier <code class=\"file-path\"></code> a été modifié pour la dernière fois le <span class=\"file-last-modified\"></span>.",
|
||||
@@ -1682,7 +1702,8 @@
|
||||
"copy-link": "Copier le lien",
|
||||
"paste": "Coller",
|
||||
"paste-as-plain-text": "Coller comme texte brut",
|
||||
"search_online": "Rechercher «{{term}}» avec {{searchEngine}}"
|
||||
"search_online": "Rechercher «{{term}}» avec {{searchEngine}}",
|
||||
"search_in_trilium": "Rechercher \"{{term}}\" dans Trilium"
|
||||
},
|
||||
"image_context_menu": {
|
||||
"copy_reference_to_clipboard": "Copier la référence dans le presse-papiers",
|
||||
@@ -1991,7 +2012,8 @@
|
||||
"add-column": "Ajouter une colonne",
|
||||
"add-column-placeholder": "Entrez le nom de la colonne...",
|
||||
"edit-note-title": "Cliquez pour modifier le titre de la note",
|
||||
"edit-column-title": "Cliquez pour modifier le titre de la colonne"
|
||||
"edit-column-title": "Cliquez pour modifier le titre de la colonne",
|
||||
"column-already-exists": "Cette colonne existe déjà dans le tableau."
|
||||
},
|
||||
"presentation_view": {
|
||||
"edit-slide": "Modifier cette diapositive",
|
||||
@@ -2075,7 +2097,8 @@
|
||||
"button_title": "Exporter le diagramme au format PNG"
|
||||
},
|
||||
"svg": {
|
||||
"export_to_png": "Le diagramme n'a pas pu être exporté au format PNG."
|
||||
"export_to_png": "Le diagramme n'a pas pu être exporté au format PNG.",
|
||||
"export_to_svg": "Le diagramme n'a pas pu être exporté en SVG."
|
||||
},
|
||||
"code_theme": {
|
||||
"title": "Apparence",
|
||||
@@ -2108,6 +2131,10 @@
|
||||
},
|
||||
"read-only-info": {
|
||||
"read-only-note": "Vous consultez actuellement une note en lecture seule.",
|
||||
"auto-read-only-note": "Cette note s'affiche en mode lecture seule pour un chargement plus rapide."
|
||||
"auto-read-only-note": "Cette note s'affiche en mode lecture seule pour un chargement plus rapide.",
|
||||
"edit-note": "Editer la note"
|
||||
},
|
||||
"calendar_view": {
|
||||
"delete_note": "Effacer la note..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,50 @@
|
||||
{
|
||||
"about": {
|
||||
"title": "ट्रिलियम नोट्स के बारें में"
|
||||
"title": "ट्रिलियम नोट्स के बारें में",
|
||||
"build_date": "निर्माण की तारीख:",
|
||||
"app_version": "ऐप वर्ज़न:",
|
||||
"db_version": "DB वर्ज़न:",
|
||||
"build_revision": "बिल्ड रिविज़न:"
|
||||
},
|
||||
"toast": {
|
||||
"widget-error": {
|
||||
"title": "एक विजेट को इनिशियलाइज़ करने में विफल रहा"
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "एक कस्टम स्क्रिप्ट लोड करने में विफल रहा"
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "सर्वर से विजेट्स की सूची प्राप्त करने में विफल"
|
||||
},
|
||||
"open-script-note": "स्क्रिप्ट नोट खोलें"
|
||||
},
|
||||
"update_available": {
|
||||
"update_available": "उपलब्ध अद्यतन"
|
||||
},
|
||||
"code_buttons": {
|
||||
"execute_button_title": "स्क्रिप्ट एक्सीक्यूट करें",
|
||||
"trilium_api_docs_button_title": "ट्रिलियम एपीआई डॉक्स खोलें",
|
||||
"save_to_note_button_title": "नोट में सेव करें"
|
||||
},
|
||||
"hide_floating_buttons_button": {
|
||||
"button_title": "बटन छुपाएं"
|
||||
},
|
||||
"show_floating_buttons_button": {
|
||||
"button_title": "बटन दिखाएं"
|
||||
},
|
||||
"add_link": {
|
||||
"note": "नोट"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"other": "अन्य"
|
||||
},
|
||||
"clone_to": {
|
||||
"search_for_note_by_its_name": "नोट क नाम से नोट खोजें"
|
||||
},
|
||||
"confirm": {
|
||||
"also_delete_note": "नोट भी डिलीट करें"
|
||||
},
|
||||
"delete_notes": {
|
||||
"delete_notes_preview": "नोट्स प्रिव्यू डिलीट करें"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,13 @@
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Nem sikerült betölteni az egyéni szkriptet",
|
||||
"message": "A(z) \"{{id}}\" azonosítójú, \"{{title}}\" című jegyzetből származó szkript nem hajtható végre a következő ok miatt:\n\n{{message}}"
|
||||
"message": "A skript nem hajtható végre a következő ok miatt:\n\n{{message}}"
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "A Widget-ek letöltése sikertelen volt"
|
||||
},
|
||||
"widget-render-error": {
|
||||
"title": "Nem sikerült renderelni a React widget-et"
|
||||
}
|
||||
},
|
||||
"add_link": {
|
||||
|
||||
@@ -16,13 +16,22 @@
|
||||
},
|
||||
"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}}"
|
||||
"message": "Impossibile eseguire lo script a causa di:\n\n{{message}}"
|
||||
},
|
||||
"widget-error": {
|
||||
"title": "Impossibile inizializzare un widget",
|
||||
"message-custom": "Il widget personalizzato dalla nota con ID “{{id}}”, intitolato “{{title}}”, non è stato possibile inizializzare a causa di:\n\n{{message}}",
|
||||
"message-unknown": "Un widget sconosciuto non è stato inizializzato a causa di:\n\n{{message}}"
|
||||
}
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "Impossibile ottenere l'elenco dei widget dal server"
|
||||
},
|
||||
"widget-render-error": {
|
||||
"title": "Impossibile eseguire il rendering di un widget React personalizzato"
|
||||
},
|
||||
"widget-missing-parent": "Il widget personalizzato non ha la proprietà obbligatoria '{{property}}' definita.\n\nSe questo script deve essere eseguito senza un elemento dell'interfaccia utente, utilizzare invece '#run=frontendStartup'.",
|
||||
"open-script-note": "Apri script note",
|
||||
"scripting-error": "Errore script personalizzato: {{title}}"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Aggiungi un collegamento",
|
||||
@@ -893,7 +902,6 @@
|
||||
"download_button": "Scarica",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "Dimensione del file:",
|
||||
"preview": "Anteprima:",
|
||||
"preview_not_available": "L'anteprima non è disponibile per questo tipo di nota."
|
||||
},
|
||||
"sort_child_notes": {
|
||||
@@ -1334,7 +1342,16 @@
|
||||
"note_icon": {
|
||||
"change_note_icon": "Cambia icona nota",
|
||||
"search": "Ricerca:",
|
||||
"reset-default": "Ripristina l'icona predefinita"
|
||||
"reset-default": "Ripristina l'icona predefinita",
|
||||
"search_placeholder_one": "Cerca {{number}} icona in {{count}} pacchetto",
|
||||
"search_placeholder_many": "Cerca {{number}} icone in {{count}} pacchetti",
|
||||
"search_placeholder_other": "Cerca {{number}} icone in {{count}} pacchetti",
|
||||
"search_placeholder_filtered": "Cerca {{number}} icone in {{name}}",
|
||||
"filter": "Filtro",
|
||||
"filter-none": "Tutte le icone",
|
||||
"filter-default": "Icone predefinite",
|
||||
"icon_tooltip": "{{name}}\nPacchetto icone: {{iconPack}}",
|
||||
"no_results": "Nessuna icona trovata."
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "Tipo di nota",
|
||||
@@ -1792,7 +1809,7 @@
|
||||
"will_be_deleted_in": "Questo allegato verrà eliminato automaticamente tra {{time}}",
|
||||
"will_be_deleted_soon": "Questo allegato verrà eliminato automaticamente a breve",
|
||||
"deletion_reason": ", perché l'allegato non è collegato al contenuto della nota. Per impedirne l'eliminazione, aggiungi nuovamente il collegamento all'allegato nel contenuto o converti l'allegato in nota.",
|
||||
"role_and_size": "Ruolo: {{role}}, Dimensione: {{size}}",
|
||||
"role_and_size": "Ruolo: {{role}}, dimensione: {{size}}, MIME: {{- mimeType}}",
|
||||
"link_copied": "Link all'allegato copiato negli appunti.",
|
||||
"unrecognized_role": "Ruolo di allegato non riconosciuto '{{role}}'."
|
||||
},
|
||||
@@ -1878,7 +1895,11 @@
|
||||
"create-child-note": "Crea nota figlio",
|
||||
"unhoist": "Sganciare",
|
||||
"toggle-sidebar": "Attiva/disattiva la barra laterale",
|
||||
"dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione."
|
||||
"dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione.",
|
||||
"clone-indicator-tooltip": "Questa nota ha {{- count}} genitori: {{- parents}}",
|
||||
"clone-indicator-tooltip-single": "Questa nota è stata clonata (1 genitore aggiuntivo: {{- parent}})",
|
||||
"shared-indicator-tooltip": "Questa nota è condivisa pubblicamente",
|
||||
"shared-indicator-tooltip-with-url": "Questa nota è condivisa pubblicamente all'indirizzo: {{- url}}"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Mantieni la finestra in primo piano"
|
||||
@@ -1886,7 +1907,13 @@
|
||||
"note_detail": {
|
||||
"could_not_find_typewidget": "Impossibile trovare typeWidget per il tipo '{{type}}'",
|
||||
"printing": "Stampa in corso...",
|
||||
"printing_pdf": "Esportazione in PDF in corso..."
|
||||
"printing_pdf": "Esportazione in PDF in corso...",
|
||||
"print_report_title": "Stampa rapporto",
|
||||
"print_report_collection_content_one": "{{count}} la note nella raccolta non può essere stampata perché non è supportata o è protetta.",
|
||||
"print_report_collection_content_many": "{{count}} le note nella raccolta non possono essere stampate perché non sono supportate o sono protette.",
|
||||
"print_report_collection_content_other": "{{count}} le note nella raccolta non possono essere stampate perché non sono supportate o sono protette.",
|
||||
"print_report_collection_details_button": "Vedi dettagli",
|
||||
"print_report_collection_details_ignored_notes": "Note ignorate"
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "scrivi qui il titolo della nota...",
|
||||
@@ -1896,7 +1923,8 @@
|
||||
"note_type_switcher_others": "Altro tipo di nota",
|
||||
"note_type_switcher_templates": "Modello",
|
||||
"note_type_switcher_collection": "Collezione",
|
||||
"edited_notes": "Note modificate"
|
||||
"edited_notes": "Note modificate in questo giorno",
|
||||
"promoted_attributes": "Attributi promossi"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "Non sono state trovate note per i parametri di ricerca specificati.",
|
||||
@@ -2176,7 +2204,14 @@
|
||||
"execute_sql_description": "Questa nota è una nota SQL. Clicca per eseguire la query SQL.",
|
||||
"shared_copy_to_clipboard": "Copia link negli appunti",
|
||||
"shared_open_in_browser": "Apri il link nel browser",
|
||||
"shared_unshare": "Rimuovi condivisione"
|
||||
"shared_unshare": "Rimuovi condivisione",
|
||||
"save_status_saved": "Salvato",
|
||||
"save_status_saving": "Salvataggio in corso...",
|
||||
"save_status_unsaved": "Non salvato",
|
||||
"save_status_error": "Salvataggio non riuscito",
|
||||
"save_status_saving_tooltip": "Le modifiche sono state salvate.",
|
||||
"save_status_unsaved_tooltip": "Ci sono modifiche non salvate. Verranno salvate automaticamente tra un attimo.",
|
||||
"save_status_error_tooltip": "Si è verificato un errore durante il salvataggio della nota. Se possibile, prova a copiare il contenuto della nota altrove e a ricaricare l'applicazione."
|
||||
},
|
||||
"breadcrumb": {
|
||||
"workspace_badge": "Area di lavoro",
|
||||
@@ -2219,5 +2254,18 @@
|
||||
"empty_button": "Nascondi il pannello",
|
||||
"toggle": "Attiva/disattiva pannello destro",
|
||||
"custom_widget_go_to_source": "Vai al codice sorgente"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_one": "{{count}} allegato",
|
||||
"attachments_many": "{{count}} allegati",
|
||||
"attachments_other": "{{count}} allegati",
|
||||
"layers_one": "{{count}} livello",
|
||||
"layers_many": "{{count}} livelli",
|
||||
"layers_other": "{{count}} livelli",
|
||||
"pages_one": "{{count}} pagina",
|
||||
"pages_many": "{{count}} pagine",
|
||||
"pages_other": "{{count}} pagine",
|
||||
"pages_alt": "Pagina {{pageNumber}}",
|
||||
"pages_loading": "Caricamento in corso..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,14 @@
|
||||
"note_icon": {
|
||||
"change_note_icon": "ノートアイコンの変更",
|
||||
"search": "検索:",
|
||||
"reset-default": "アイコンをデフォルトに戻す"
|
||||
"reset-default": "アイコンをデフォルトに戻す",
|
||||
"search_placeholder_other": "{{count}} 個のパックから {{number}} 個のアイコンを検索",
|
||||
"search_placeholder_filtered": "{{name}} で {{number}} 個のアイコンを検索",
|
||||
"filter": "フィルター",
|
||||
"filter-none": "すべてのアイコン",
|
||||
"filter-default": "デフォルトアイコン",
|
||||
"icon_tooltip": "{{name}}\nアイコンパック: {{iconPack}}",
|
||||
"no_results": "アイコンが見つかりません。"
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "ノートタイプ",
|
||||
@@ -436,7 +443,10 @@
|
||||
"unhoist-note": "ノートのホイストを解除",
|
||||
"edit-branch-prefix": "ブランチの接頭辞を編集",
|
||||
"archive": "アーカイブ",
|
||||
"unarchive": "アーカイブ解除"
|
||||
"unarchive": "アーカイブ解除",
|
||||
"open-in-a-new-window": "新しいウィンドウで開く",
|
||||
"hide-subtree": "サブツリーを非表示",
|
||||
"show-subtree": "サブツリーを表示"
|
||||
},
|
||||
"zen_mode": {
|
||||
"button_exit": "禅モードを退出"
|
||||
@@ -561,7 +571,8 @@
|
||||
"expand_tooltip": "このコレクションの直下の子(1階層下)を展開します。その他のオプションについては、右側の矢印を押してください。",
|
||||
"expand_first_level": "直下の子を展開",
|
||||
"expand_nth_level": "{{depth}} 階層下まで展開",
|
||||
"expand_all_levels": "すべての階層を展開"
|
||||
"expand_all_levels": "すべての階層を展開",
|
||||
"hide_child_notes": "ツリー内の子ノートを非表示"
|
||||
},
|
||||
"note_types": {
|
||||
"geo-map": "ジオマップ",
|
||||
@@ -647,7 +658,6 @@
|
||||
"revision_deleted": "ノートの変更履歴は削除されました。",
|
||||
"settings": "ノートの変更履歴の設定",
|
||||
"file_size": "ファイルサイズ:",
|
||||
"preview": "プレビュー:",
|
||||
"preview_not_available": "このノートタイプではプレビューは利用できません。",
|
||||
"diff_on": "差分を表示",
|
||||
"diff_off": "内容を表示",
|
||||
@@ -1238,7 +1248,15 @@
|
||||
"saved-search-note-refreshed": "保存した検索ノートが更新されました。",
|
||||
"refresh-saved-search-results": "保存した検索結果を更新",
|
||||
"toggle-sidebar": "サイドバーを切り替え",
|
||||
"dropping-not-allowed": "この場所にノートをドロップすることはできません。"
|
||||
"dropping-not-allowed": "この場所にノートをドロップすることはできません。",
|
||||
"clone-indicator-tooltip": "このノートには {{- count}} 個の親があります: {{- parents}}",
|
||||
"clone-indicator-tooltip-single": "このノートは複製されています (親が 1 件追加: {{- parent}})",
|
||||
"shared-indicator-tooltip": "このノートは公開されています",
|
||||
"shared-indicator-tooltip-with-url": "このノートは以下で公開されています: {{- url}}",
|
||||
"subtree-hidden-tooltip_other": "{{count}} 個の子ノートがツリーで非表示になっています",
|
||||
"subtree-hidden-moved-title": "{{title}} に追加されました",
|
||||
"subtree-hidden-moved-description-collection": "このコレクションはツリー内の子ノートを非表示にします。",
|
||||
"subtree-hidden-moved-description-other": "このノートのツリーでは子ノートは非表示になっています。"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "一括操作",
|
||||
@@ -2129,7 +2147,7 @@
|
||||
"will_be_deleted_in": "この添付ファイルは {{time}} 後に自動的に削除されます",
|
||||
"will_be_deleted_soon": "この添付ファイルはすぐに自動的に削除されます",
|
||||
"deletion_reason": "、添付ファイルがノートのコンテンツにリンクされていないためです。削除されないようにするには、添付ファイルのリンクをコンテンツに再度追加するか、添付ファイルをノートに変換してください。",
|
||||
"role_and_size": "ロール: {{role}},サイズ: {{size}}",
|
||||
"role_and_size": "ロール: {{role}},サイズ: {{size}}, MIME: {{- mimeType}}",
|
||||
"link_copied": "添付ファイルのリンクをクリップボードにコピーしました。",
|
||||
"unrecognized_role": "添付ファイルのロール「{{role}}」は認識されません。"
|
||||
},
|
||||
@@ -2186,7 +2204,14 @@
|
||||
"execute_sql_description": "このノートは SQL ノートです。クリックすると SQL クエリが実行されます。",
|
||||
"shared_copy_to_clipboard": "リンクをクリップボードにコピー",
|
||||
"shared_open_in_browser": "ブラウザでリンクを開く",
|
||||
"shared_unshare": "共有を削除"
|
||||
"shared_unshare": "共有を削除",
|
||||
"save_status_saved": "保存されました",
|
||||
"save_status_saving": "保存中...",
|
||||
"save_status_unsaved": "未保存",
|
||||
"save_status_error": "保存に失敗しました",
|
||||
"save_status_saving_tooltip": "変更を保存しています。",
|
||||
"save_status_unsaved_tooltip": "未保存の変更があります。すぐに自動的に保存されます。",
|
||||
"save_status_error_tooltip": "ノートの保存中にエラーが発生しました。可能であれば、ノートの内容を別の場所にコピーして、アプリケーションを再読み込みしてください。"
|
||||
},
|
||||
"status_bar": {
|
||||
"language_title": "コンテンツの言語を変更",
|
||||
@@ -2217,5 +2242,12 @@
|
||||
},
|
||||
"attributes_panel": {
|
||||
"title": "ノート属性"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_other": "{{count}} 添付ファイル",
|
||||
"layers_other": "{{count}} 層",
|
||||
"pages_other": "{{count}} ページ",
|
||||
"pages_alt": "ページ {{pageNumber}}",
|
||||
"pages_loading": "読み込み中..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,64 @@
|
||||
{}
|
||||
{
|
||||
"about": {
|
||||
"title": "Om Trilium Notes",
|
||||
"app_version": "App versjon:",
|
||||
"db_version": "DB versjon:",
|
||||
"sync_version": "Synk versjon:",
|
||||
"build_date": "Byggdato:",
|
||||
"build_revision": "Bygg versjon:",
|
||||
"data_directory": "Datamappe:",
|
||||
"homepage": "Hjemmeside:"
|
||||
},
|
||||
"experimental_features": {
|
||||
"new_layout_description": "Prøv det nye grensesnittet for et mer moderne utseende og forbedret brukervenlighet. Det må påregnes betydelige endringer i kommende versjoner."
|
||||
},
|
||||
"cpu_arch_warning": {
|
||||
"recommendation": "For den beste brukeropplevelsen, vennligst last ned den tilpassede ARM64-versjonen av TriliumNext fra siden for utgivelser."
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink_one": "{{count}} Tilbakelenke",
|
||||
"backlink_other": "{{count}} Tilbakelenker"
|
||||
},
|
||||
"add_link": {
|
||||
"note": "Notat"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"prefix": "Prefiks : ",
|
||||
"save": "Lagre"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"labels": "Etiketter",
|
||||
"relations": "Relasjoner",
|
||||
"notes": "Notater",
|
||||
"other": "Andre"
|
||||
},
|
||||
"confirm": {
|
||||
"confirmation": "Bekreftelse",
|
||||
"cancel": "Avbryt",
|
||||
"ok": "OK"
|
||||
},
|
||||
"delete_notes": {
|
||||
"close": "Lukk",
|
||||
"cancel": "Avbryt",
|
||||
"ok": "OK"
|
||||
},
|
||||
"export": {
|
||||
"close": "Lukk",
|
||||
"export": "Eksporter"
|
||||
},
|
||||
"note_type_chooser": {
|
||||
"templates": "Maler"
|
||||
},
|
||||
"help": {
|
||||
"title": "Hurtigveiledning",
|
||||
"troubleshooting": "Feilsøking",
|
||||
"other": "Andre"
|
||||
},
|
||||
"import": {
|
||||
"options": "Alternativer",
|
||||
"import": "Importer"
|
||||
},
|
||||
"include_note": {
|
||||
"label_note": "Notat"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"toast": {
|
||||
"critical-error": {
|
||||
"title": "Kritische Error",
|
||||
"message": "Een kritieke fout heeft plaatsgevonden waardoor de cliënt zich aanmeldt vanaf het begin:\n\n84X\n\nDit is waarschijnlijk veroorzaakt door een script dat op een onverwachte manier faalt. Probeer de sollicitatie in veilige modus te starten en de kwestie aan te spreken."
|
||||
"message": "Een kritieke fout heeft plaatsgevonden waardoor de applicatie niet kon opstarten:\n\n{{message}}\n\nDit is waarschijnlijk veroorzaakt door een onverwachte fout in een script. Probeer de applicatie op te starten in veilige modus en het probleem op te lossen."
|
||||
},
|
||||
"widget-error": {
|
||||
"title": "Starten widget mislukt",
|
||||
@@ -22,7 +22,16 @@
|
||||
"bundle-error": {
|
||||
"title": "Custom script laden mislukt",
|
||||
"message": "Script van notitie met ID \"{{id}}\", getiteld \"{{title}}\" kon niet worden uitgevoerd vanwege:\n\n{{message}}"
|
||||
}
|
||||
},
|
||||
"scripting-error": "Error met script: {{title}}",
|
||||
"widget-list-error": {
|
||||
"title": "Kon geen lijst met widgets ophalen van de server"
|
||||
},
|
||||
"widget-render-error": {
|
||||
"title": "React-widget kon niet geladen worden"
|
||||
},
|
||||
"widget-missing-parent": "Widget heeft niet het verplichte '{{property}}'-veld gedefinieerd.\n\nAls dit script is bedoeld om zonder interface te draaien, gebruik dan in plaats daarvan '#run=frontendStartup'.",
|
||||
"open-script-note": "Open scriptnotitie"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Voeg link toe",
|
||||
@@ -41,7 +50,8 @@
|
||||
"help_on_tree_prefix": "Help bij boomvoorvoegsel",
|
||||
"prefix": "Voorvoegsel: ",
|
||||
"edit_branch_prefix_multiple": "Bewerk zijtakvoorvoegsel voor {{count}} zijtakken",
|
||||
"branch_prefix_saved_multiple": "Vertakkingsvoorvoegsel opgeslagen voor {{count}} vertakkingen."
|
||||
"branch_prefix_saved_multiple": "Vertakkingsvoorvoegsel opgeslagen voor {{count}} vertakkingen.",
|
||||
"affected_branches": "Aangetaste takken ({{count}}):"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "Bulk acties",
|
||||
@@ -54,7 +64,8 @@
|
||||
"labels": "Labels",
|
||||
"relations": "Relaties",
|
||||
"notes": "Notities",
|
||||
"other": "Andere"
|
||||
"other": "Andere",
|
||||
"include_descendants": "Tel afstammelingen van de geselecteerde notities mee"
|
||||
},
|
||||
"calendar": {
|
||||
"april": "April",
|
||||
@@ -78,5 +89,35 @@
|
||||
},
|
||||
"show_toc_widget_button": {
|
||||
"show_toc": "Laat Inhoudsopgave zien"
|
||||
},
|
||||
"status_bar": {
|
||||
"note_paths_one": "{{count}} pad",
|
||||
"note_paths_other": "{{count}} paden",
|
||||
"note_paths_title": "Notitiepaden",
|
||||
"code_note_switcher": "Verander de taalmodus"
|
||||
},
|
||||
"attributes_panel": {
|
||||
"title": "Notitie-attributen"
|
||||
},
|
||||
"right_pane": {
|
||||
"empty_message": "Geen informatie voor deze notitie",
|
||||
"empty_button": "Verberg dit paneel",
|
||||
"toggle": "Schakel rechterpaneel in/uit",
|
||||
"custom_widget_go_to_source": "Go naar de broncode"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_one": "{{count}} bijlage",
|
||||
"attachments_other": "{{count}} bijlagen",
|
||||
"layers_one": "{{count}} laag",
|
||||
"layers_other": "{{count}} lagen",
|
||||
"pages_one": "{{count}} pagina",
|
||||
"pages_other": "{{count}} pagina's",
|
||||
"pages_alt": "Pagina {{pageNumber}}",
|
||||
"pages_loading": "Laden..."
|
||||
},
|
||||
"clone_to": {
|
||||
"clone_notes_to": "Kloon de notities naar...",
|
||||
"help_on_links": "Hulp op links",
|
||||
"notes_to_clone": "Notities om te klonen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -959,7 +959,6 @@
|
||||
"download_button": "Pobierz",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "Rozmiar pliku:",
|
||||
"preview": "Podgląd:",
|
||||
"preview_not_available": "Podgląd nie jest dostępny dla tego typu notatki."
|
||||
},
|
||||
"sort_child_notes": {
|
||||
|
||||
@@ -22,7 +22,16 @@
|
||||
"bundle-error": {
|
||||
"title": "Falha para carregar o script customizado",
|
||||
"message": "O script da nota com ID \"{{id}}\", intitulada \"{{title}}\", não pôde ser executado devido a:\n\n{{message}}"
|
||||
}
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "Falha ao obter a lista de widgets do servidor"
|
||||
},
|
||||
"scripting-error": "Erro do script específicado: {{title}}",
|
||||
"open-script-note": "Abrir script da nota",
|
||||
"widget-render-error": {
|
||||
"title": "Falha do renderizar um widget React personalizado"
|
||||
},
|
||||
"widget-missing-parent": "Widget adaptado não tem a propriedade '{{property}}' mandatória definida.\n\nSe este script é para ser executado sem um element de UI, usar '#run=frontendStartup'."
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Adicionar ligação",
|
||||
@@ -39,7 +48,10 @@
|
||||
"help_on_tree_prefix": "Ajuda sobre o prefixo da árvore de notas",
|
||||
"prefix": "Prefixo: ",
|
||||
"save": "Gravar",
|
||||
"branch_prefix_saved": "O prefixo de ramificação foi gravado."
|
||||
"branch_prefix_saved": "O prefixo de ramificação foi gravado.",
|
||||
"edit_branch_prefix_multiple": "Editar prefixo para {{count}} branches",
|
||||
"branch_prefix_saved_multiple": "Prefixo dos branches foi editado para {{count}} branches.",
|
||||
"affected_branches": "Alterados ({{count}}) branches:"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "Ações em massa",
|
||||
@@ -104,7 +116,8 @@
|
||||
"export_status": "Estado da exportação",
|
||||
"export_in_progress": "Exportação em andamento: {{progressCount}}",
|
||||
"export_finished_successfully": "Exportação concluída com sucesso.",
|
||||
"format_pdf": "PDF – para impressão ou compartilhamento."
|
||||
"format_pdf": "PDF – para impressão ou compartilhamento.",
|
||||
"share-format": "HTML para publicação web - usa o mesmo tema que é usado para notas partilhadas, mas pode ser publicado como um site estatico."
|
||||
},
|
||||
"help": {
|
||||
"title": "Folha de Dicas",
|
||||
@@ -158,7 +171,8 @@
|
||||
"showSQLConsole": "mostrar console SQL",
|
||||
"other": "Outros",
|
||||
"quickSearch": "focar no campo de pesquisa rápida",
|
||||
"inPageSearch": "pesquisa na página"
|
||||
"inPageSearch": "pesquisa na página",
|
||||
"editShortcuts": "Editar atalhos do teclado"
|
||||
},
|
||||
"import": {
|
||||
"importIntoNote": "Importar para a nota",
|
||||
@@ -184,7 +198,8 @@
|
||||
},
|
||||
"import-status": "Estado da importação",
|
||||
"in-progress": "Importação em andamento: {{progress}}",
|
||||
"successful": "Importação concluída com sucesso."
|
||||
"successful": "Importação concluída com sucesso.",
|
||||
"importZipRecommendation": "Quando a importar ficheiro ZIP, a hierarquia de notas vai reflectir a estrutura da sub directoria dentro do ficheiro."
|
||||
},
|
||||
"include_note": {
|
||||
"dialog_title": "Incluir nota",
|
||||
@@ -199,7 +214,8 @@
|
||||
"info": {
|
||||
"modalTitle": "Mensagem informativa",
|
||||
"closeButton": "Fechar",
|
||||
"okButton": "OK"
|
||||
"okButton": "OK",
|
||||
"copy_to_clipboard": "Copiar para a área de transferência"
|
||||
},
|
||||
"jump_to_note": {
|
||||
"search_placeholder": "Pesquise uma nota pelo nome ou digite > para comandos...",
|
||||
@@ -274,8 +290,12 @@
|
||||
"download_button": "Descarregar",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "Tamanho do ficheiro:",
|
||||
"preview": "Visualizar:",
|
||||
"preview_not_available": "A visualização não está disponível para este tipo de nota."
|
||||
"preview_not_available": "A visualização não está disponível para este tipo de nota.",
|
||||
"diff_on": "Mostrar diferenças",
|
||||
"diff_off": "Mostrar conteúdos",
|
||||
"diff_on_hint": "Carregar para mostrar diferenças da fonte da nota",
|
||||
"diff_off_hint": "Carregar para mostrar conteúdos da nota",
|
||||
"diff_not_available": "Diferenças não disponível."
|
||||
},
|
||||
"sort_child_notes": {
|
||||
"sort_children_by": "Ordenar notas filhas por...",
|
||||
@@ -586,7 +606,18 @@
|
||||
"september": "Setembro",
|
||||
"october": "Outubro",
|
||||
"november": "Novembro",
|
||||
"december": "Dezembro"
|
||||
"december": "Dezembro",
|
||||
"week": "Semana",
|
||||
"week_previous": "Semana anterior",
|
||||
"week_next": "Próxima semana",
|
||||
"month": "Mês",
|
||||
"month_previous": "Mês anterior",
|
||||
"month_next": "Próximo mês",
|
||||
"year": "Ano",
|
||||
"year_previous": "Ano anterior",
|
||||
"year_next": "Próximo ano",
|
||||
"list": "Lista",
|
||||
"today": "Hoje"
|
||||
},
|
||||
"close_pane_button": {
|
||||
"close_this_pane": "Fechar este painel"
|
||||
@@ -629,7 +660,9 @@
|
||||
"about": "Sobre o Trilium Notes",
|
||||
"logout": "Sair",
|
||||
"show-cheatsheet": "Exibir Cheatsheet",
|
||||
"toggle-zen-mode": "Modo Zen"
|
||||
"toggle-zen-mode": "Modo Zen",
|
||||
"new-version-available": "Nova actualização disponível",
|
||||
"download-update": "Obter versão {{latestVersion}}"
|
||||
},
|
||||
"zen_mode": {
|
||||
"button_exit": "Sair do Modo Zen"
|
||||
@@ -667,7 +700,14 @@
|
||||
"convert_into_attachment_failed": "A conversão da nota '{{title}}' falhou.",
|
||||
"convert_into_attachment_successful": "A nota '{{title}}' foi convertida para anexo.",
|
||||
"convert_into_attachment_prompt": "Tem certeza que quer converter a nota '{{title}}' num anexo da nota pai?",
|
||||
"print_pdf": "Exportar como PDF…"
|
||||
"print_pdf": "Exportar como PDF…",
|
||||
"open_note_on_server": "Abrir nota no servidor",
|
||||
"export_as_image": "Exportar como imagem",
|
||||
"note_map": "Mapa de notas",
|
||||
"advanced": "Avançadas",
|
||||
"view_revisions": "Revisões da nota...",
|
||||
"export_as_image_svg": "SVG (vectorial)",
|
||||
"export_as_image_png": "PNG (matricial)"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "Componente de botão '{{componentId}}' não possui manipulador de clique definido"
|
||||
@@ -713,19 +753,26 @@
|
||||
"zpetne_odkazy": {
|
||||
"relation": "relação",
|
||||
"backlink_one": "{{count}} Ligação Reversa",
|
||||
"backlink_many": "",
|
||||
"backlink_many": "{{count}} Ligações Reversas",
|
||||
"backlink_other": "{{count}} Ligações Reversas"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"insert_child_note": "Inserir nota filha",
|
||||
"delete_this_note": "Apagar esta nota",
|
||||
"error_cannot_get_branch_id": "Não foi possível obter o branchId para o notePath '{{notePath}} '",
|
||||
"error_unrecognized_command": "Comando não reconhecido {{command}}"
|
||||
"error_unrecognized_command": "Comando não reconhecido {{command}}",
|
||||
"note_revisions": "Revisões da nota"
|
||||
},
|
||||
"note_icon": {
|
||||
"change_note_icon": "Alterar ícone da nota",
|
||||
"search": "Pesquisa:",
|
||||
"reset-default": "Redefinir para o ícone padrão"
|
||||
"reset-default": "Redefinir para o ícone padrão",
|
||||
"filter": "Filtrar",
|
||||
"filter-none": "Todos os icons",
|
||||
"filter-default": "Icons default",
|
||||
"no_results": "Não foram encontrados icons.",
|
||||
"search_placeholder_filtered": "Procurar {{number}} icons no {{name}}",
|
||||
"icon_tooltip": "{{name}}\nPacote de icons: {{iconPack}}"
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "Tipo da nota",
|
||||
@@ -746,7 +793,13 @@
|
||||
"calendar": "Calendário",
|
||||
"table": "Tabela",
|
||||
"geo-map": "Mapa geográfico",
|
||||
"board": "Quadro"
|
||||
"board": "Quadro",
|
||||
"expand_first_level": "Expandir descendentes directos",
|
||||
"presentation": "Apresentação",
|
||||
"expand_nth_level": "Expandir {{depth}} níveis",
|
||||
"expand_all_levels": "Expandir todos os níveis",
|
||||
"include_archived_notes": "Mostrar notas arquivadas",
|
||||
"expand_tooltip": "Expande a direcção dos descendentes desta colecção (um nível). Para mais opções, carregar na seta à direita."
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "Ainda não há nenhuma nota editada neste dia…",
|
||||
@@ -779,7 +832,8 @@
|
||||
},
|
||||
"inherited_attribute_list": {
|
||||
"title": "Atributos Herdados",
|
||||
"no_inherited_attributes": "Nenhum atributo herdado."
|
||||
"no_inherited_attributes": "Nenhum atributo herdado.",
|
||||
"none": "Nenhum"
|
||||
},
|
||||
"note_info_widget": {
|
||||
"note_id": "ID da Nota",
|
||||
@@ -790,7 +844,9 @@
|
||||
"note_size_info": "O tamanho da nota fornece uma estimativa aproximada dos requisitos de armazenamento para esta nota. Leva em conta o conteúdo e o conteúdo das suas revisões de nota.",
|
||||
"calculate": "calcular",
|
||||
"subtree_size": "(tamanho da subárvore: {{size}} em {{count}} notas)",
|
||||
"title": "Informações da nota"
|
||||
"title": "Informações da nota",
|
||||
"mime": "Tipo MIME",
|
||||
"show_similar_notes": "Mostrar notas semelhantes"
|
||||
},
|
||||
"note_map": {
|
||||
"open_full": "Expandir completamente",
|
||||
@@ -853,7 +909,8 @@
|
||||
"search_parameters": "Parâmetros de Pesquisa",
|
||||
"unknown_search_option": "Opção de pesquisa desconhecida {{searchOptionName}}",
|
||||
"search_note_saved": "Nota de pesquisa foi gravada em {{- notePathTitle}}",
|
||||
"actions_executed": "As ações foram executadas."
|
||||
"actions_executed": "As ações foram executadas.",
|
||||
"view_options": "Ver opções:"
|
||||
},
|
||||
"similar_notes": {
|
||||
"title": "Notas Similares",
|
||||
@@ -947,14 +1004,20 @@
|
||||
"no_attachments": "Esta nota não possuí anexos."
|
||||
},
|
||||
"book": {
|
||||
"no_children_help": "Esta coleção não possui nenhum nota filha, então não há nada para exibir. Veja <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> para pormenores."
|
||||
"no_children_help": "Esta coleção não possui nenhum nota filha, então não há nada para exibir. Veja <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> para pormenores.",
|
||||
"drag_locked_title": "Bloqueado para edição",
|
||||
"drag_locked_message": "Arrastar não permitida pois a coleção está bloqueada para edição."
|
||||
},
|
||||
"editable_code": {
|
||||
"placeholder": "Digite o conteúdo da sua nota de código aqui…"
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "Digite o conteúdo da sua nota aqui…",
|
||||
"auto-detect-language": "Detetado automaticamente"
|
||||
"auto-detect-language": "Detetado automaticamente",
|
||||
"editor_crashed_title": "O editor de texto quebrou",
|
||||
"editor_crashed_details_button": "Ver mais detalhes...",
|
||||
"editor_crashed_details_title": "Informação técnica",
|
||||
"editor_crashed_details_intro": "Se teve este erro várias vezes, considerer reportar no GitHub disponibilizando a informação abaixo."
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "Abra uma nota a digitar o título da nota no campo abaixo ou escolha uma nota na árvore.",
|
||||
|
||||
@@ -439,7 +439,6 @@
|
||||
"download_button": "Download",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "Tamanho do arquivo:",
|
||||
"preview": "Visualizar:",
|
||||
"preview_not_available": "A visualização não está disponível para este tipo de nota.",
|
||||
"diff_on": "Exibir diferença",
|
||||
"diff_off": "Exibir conteúdo",
|
||||
|
||||
@@ -1103,7 +1103,6 @@
|
||||
"mime": "MIME: ",
|
||||
"no_revisions": "Nu există încă nicio revizie pentru această notiță...",
|
||||
"note_revisions": "Revizii ale notiței",
|
||||
"preview": "Previzualizare:",
|
||||
"preview_not_available": "Nu este disponibilă o previzualizare pentru acest tip de notiță.",
|
||||
"restore_button": "Restaurează",
|
||||
"revision_deleted": "Revizia notiței a fost ștearsă.",
|
||||
|
||||
@@ -387,7 +387,6 @@
|
||||
"revision_deleted": "Версия заметки была удалена.",
|
||||
"download_button": "Скачать",
|
||||
"file_size": "Размер файла:",
|
||||
"preview": "Предпросмотр:",
|
||||
"preview_not_available": "Предпосмотр недоступен для заметки этого типа.",
|
||||
"mime": "MIME: ",
|
||||
"settings": "Настройка версионирования заметок",
|
||||
@@ -1012,7 +1011,16 @@
|
||||
"note_icon": {
|
||||
"search": "Поиск:",
|
||||
"change_note_icon": "Изменить иконку заметки",
|
||||
"reset-default": "Сбросить к значку по умолчанию"
|
||||
"reset-default": "Сбросить к значку по умолчанию",
|
||||
"no_results": "Иконки не найдены.",
|
||||
"icon_tooltip": "{{name}}\nНабор иконок: {{iconPack}}",
|
||||
"filter-default": "Иконки по-умолчанию",
|
||||
"filter-none": "Все иконки",
|
||||
"filter": "Фильтр",
|
||||
"search_placeholder_filtered": "Поиск {{number}} иконок в {{name}}",
|
||||
"search_placeholder_one": "Поиск {{number}} иконки среди {{count}} наборов",
|
||||
"search_placeholder_few": "Поиск {{number}} иконок среди {{count}} наборов",
|
||||
"search_placeholder_many": "Поиск {{number}} иконок среди {{count}} наборов"
|
||||
},
|
||||
"basic_properties": {
|
||||
"editable": "Изменяемое",
|
||||
@@ -2025,7 +2033,7 @@
|
||||
"lost-websocket-connection-message": "Проверьте конфигурацию обратного прокси (например, nginx или Apache), чтобы убедиться, что соединения WebSocket должным образом разрешены и не заблокированы."
|
||||
},
|
||||
"attachment_detail_2": {
|
||||
"role_and_size": "Роль: {{role}}, Размер: {{size}}",
|
||||
"role_and_size": "Роль: {{role}}, размер: {{size}}, MIME: {{- mimeType}}",
|
||||
"unrecognized_role": "Нераспознанная роль вложения '{{role}}'.",
|
||||
"link_copied": "Ссылка на вложение скопирована в буфер обмена.",
|
||||
"will_be_deleted_soon": "Это вложение скоро будет автоматически удалено",
|
||||
|
||||
@@ -271,7 +271,6 @@
|
||||
"download_button": "Preuzmi",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "Veličina datoteke:",
|
||||
"preview": "Pregled:",
|
||||
"preview_not_available": "Pregled nije dostupan za ovaj tip beleške."
|
||||
},
|
||||
"sort_child_notes": {
|
||||
|
||||
@@ -29,8 +29,9 @@
|
||||
"widget-render-error": {
|
||||
"title": "無法渲染自訂 React 元件"
|
||||
},
|
||||
"widget-missing-parent": "自訂元件未定義強制性的 \"{{property}}\" 屬性。",
|
||||
"open-script-note": "打開腳本筆記"
|
||||
"widget-missing-parent": "自訂元件未定義強制性的 \"{{property}}\" 屬性。\n\n若此腳本需在無 UI 的情況下執行,請改用 \"#run=frontendStartup\"。",
|
||||
"open-script-note": "打開腳本筆記",
|
||||
"scripting-error": "自訂腳本錯誤:{{title}}"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "新增連結",
|
||||
@@ -287,7 +288,6 @@
|
||||
"download_button": "下載",
|
||||
"mime": "MIME類型: ",
|
||||
"file_size": "檔案大小:",
|
||||
"preview": "預覽:",
|
||||
"preview_not_available": "無法預覽此類型的筆記。",
|
||||
"restore_button": "還原",
|
||||
"delete_button": "刪除",
|
||||
@@ -762,7 +762,15 @@
|
||||
"note_icon": {
|
||||
"change_note_icon": "更改筆記圖標",
|
||||
"search": "搜尋:",
|
||||
"reset-default": "重置為預設圖標"
|
||||
"reset-default": "重置為預設圖標",
|
||||
"search_placeholder_one": "在 {{count}} 個圖示包中搜尋 {{number}} 個圖示",
|
||||
"search_placeholder_other": "",
|
||||
"search_placeholder_filtered": "在 {{name}} 中搜尋 {{number}} 個圖示",
|
||||
"filter": "篩選",
|
||||
"filter-none": "所有圖示",
|
||||
"filter-default": "預設圖示",
|
||||
"icon_tooltip": "{{name}}\n圖示包:{{iconPack}}",
|
||||
"no_results": "找不到圖示。"
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "筆記類型",
|
||||
@@ -789,7 +797,8 @@
|
||||
"expand_tooltip": "展開此集合的直接子級(單層深度)。按下右側箭頭以查看更多選項。",
|
||||
"expand_first_level": "展開直接子級",
|
||||
"expand_nth_level": "展開 {{depth}} 層",
|
||||
"expand_all_levels": "展開所有層級"
|
||||
"expand_all_levels": "展開所有層級",
|
||||
"hide_child_notes": "隱藏樹中的子筆記"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "今天還沒有編輯過的筆記...",
|
||||
@@ -1404,7 +1413,7 @@
|
||||
"will_be_deleted_in": "此附件將在 {{time}} 後自動刪除",
|
||||
"will_be_deleted_soon": "該附件即將被自動刪除",
|
||||
"deletion_reason": ",因為該附件未連結在筆記的內容中。為防止被刪除,請將附件連結重新新增至內容中或將附件轉換為筆記。",
|
||||
"role_and_size": "角色:{{role}},大小:{{size}}",
|
||||
"role_and_size": "角色:{{role}},大小:{{size}},MIME:{{- mimeType}}",
|
||||
"link_copied": "已複製附件連結到剪貼簿。",
|
||||
"unrecognized_role": "無法識別的附件角色 '{{role}}'。"
|
||||
},
|
||||
@@ -1458,7 +1467,10 @@
|
||||
"duplicate": "複製副本",
|
||||
"open-in-popup": "快速編輯",
|
||||
"archive": "封存",
|
||||
"unarchive": "解除封存"
|
||||
"unarchive": "解除封存",
|
||||
"open-in-a-new-window": "在新視窗打開",
|
||||
"hide-subtree": "隱藏子階層",
|
||||
"show-subtree": "顯示子階層"
|
||||
},
|
||||
"shared_info": {
|
||||
"help_link": "如需幫助,請訪問 <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a>。",
|
||||
@@ -1548,7 +1560,15 @@
|
||||
"create-child-note": "建立子筆記",
|
||||
"unhoist": "取消聚焦",
|
||||
"toggle-sidebar": "切換側邊欄",
|
||||
"dropping-not-allowed": "不允許移動筆記至此處。"
|
||||
"dropping-not-allowed": "不允許移動筆記至此處。",
|
||||
"clone-indicator-tooltip": "此筆記有 {{- count}} 個父級:{{- parents}}",
|
||||
"clone-indicator-tooltip-single": "此筆記已克隆(新增 1 個父級:{{- parent}})",
|
||||
"shared-indicator-tooltip": "此筆記已公開分享",
|
||||
"shared-indicator-tooltip-with-url": "此筆記已公開分享至:{{- url}}",
|
||||
"subtree-hidden-tooltip_one": "從樹中隱藏的 {{count}} 篇子筆記",
|
||||
"subtree-hidden-moved-title": "已新增至 {{title}}",
|
||||
"subtree-hidden-moved-description-collection": "此集合隱藏其樹中的子筆記。",
|
||||
"subtree-hidden-moved-description-other": "子筆記隱藏於此筆記的樹中。"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "保持此視窗置頂"
|
||||
@@ -1556,7 +1576,12 @@
|
||||
"note_detail": {
|
||||
"could_not_find_typewidget": "找不到類型為 '{{type}}' 的 typeWidget",
|
||||
"printing": "正在列印…",
|
||||
"printing_pdf": "正在匯出為 PDF…"
|
||||
"printing_pdf": "正在匯出為 PDF…",
|
||||
"print_report_title": "列印報告",
|
||||
"print_report_collection_content_one": "集合中的 {{count}} 篇筆記無法列印,因為它們不被支援或受到保護。",
|
||||
"print_report_collection_content_other": "",
|
||||
"print_report_collection_details_button": "查看詳情",
|
||||
"print_report_collection_details_ignored_notes": "忽略的筆記"
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "請輸入筆記標題...",
|
||||
@@ -1566,7 +1591,8 @@
|
||||
"note_type_switcher_others": "其他筆記類型",
|
||||
"note_type_switcher_templates": "模板",
|
||||
"note_type_switcher_collection": "集合",
|
||||
"edited_notes": "編輯過的筆記"
|
||||
"edited_notes": "今天編輯過的筆記",
|
||||
"promoted_attributes": "升級屬性"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "沒有找到符合搜尋條件的筆記。",
|
||||
@@ -2182,7 +2208,14 @@
|
||||
"read_only_temporarily_disabled_description": "此筆記目前可編輯,但通常為唯讀狀態。當您切換至其他筆記時,本筆記將立即恢復為唯讀模式。\n\n點擊此處重新啟用唯讀模式。",
|
||||
"clipped_note_description": "本筆記原始來源為 {{url}}。\n\n點擊此處前往原網頁。",
|
||||
"execute_script_description": "此筆記為腳本筆記。點擊以執行腳本。",
|
||||
"execute_sql_description": "此筆記為 SQL 筆記。點擊以執行 SQL 查詢。"
|
||||
"execute_sql_description": "此筆記為 SQL 筆記。點擊以執行 SQL 查詢。",
|
||||
"save_status_saved": "已儲存",
|
||||
"save_status_saving": "正在儲存…",
|
||||
"save_status_unsaved": "未儲存",
|
||||
"save_status_error": "儲存失敗",
|
||||
"save_status_saving_tooltip": "正在儲存更動。",
|
||||
"save_status_unsaved_tooltip": "仍有更動尚未儲存。它們將在稍後自動儲存。",
|
||||
"save_status_error_tooltip": "在儲存筆記時發生錯誤。如果可以,請嘗試將筆記內容複製至他處並重新載入應用程式。"
|
||||
},
|
||||
"breadcrumb": {
|
||||
"hoisted_badge": "聚焦",
|
||||
@@ -2219,5 +2252,15 @@
|
||||
},
|
||||
"attributes_panel": {
|
||||
"title": "筆記屬性"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_one": "{{count}} 個附件",
|
||||
"attachments_other": "",
|
||||
"layers_one": "{{count}} 層",
|
||||
"layers_other": "",
|
||||
"pages_one": "共 {{count}} 頁",
|
||||
"pages_other": "",
|
||||
"pages_alt": "第 {{pageNumber}} 頁",
|
||||
"pages_loading": "正在載入…"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,7 +321,6 @@
|
||||
"download_button": "Завантажити",
|
||||
"mime": "МІМЕ: ",
|
||||
"file_size": "Розмір файлу:",
|
||||
"preview": "Попередній перегляд:",
|
||||
"preview_not_available": "Попередній перегляд недоступний для цього типу нотатки.",
|
||||
"diff_on": "Показати різницю",
|
||||
"diff_off": "Показати вміст",
|
||||
|
||||
21
apps/client/src/types-fancytree.d.ts
vendored
21
apps/client/src/types-fancytree.d.ts
vendored
@@ -69,7 +69,7 @@ declare namespace Fancytree {
|
||||
debug(msg: any): void;
|
||||
|
||||
/** Expand (or collapse) all parent nodes. */
|
||||
expandAll(flag?: boolean, options?: Object): void;
|
||||
expandAll(flag?: boolean, options?: object): void;
|
||||
|
||||
/** [ext-filter] Dimm or hide whole branches.
|
||||
* @returns {integer} count
|
||||
@@ -221,6 +221,7 @@ declare namespace Fancytree {
|
||||
branchId: string;
|
||||
isProtected: boolean;
|
||||
noteType: NoteType;
|
||||
subtreeHidden: boolean;
|
||||
}
|
||||
|
||||
interface FancytreeNewNode extends FancytreeNodeData {
|
||||
@@ -369,7 +370,7 @@ declare namespace Fancytree {
|
||||
* @param mode 'before', 'after', or 'child' (default='child')
|
||||
* @param init NodeData (or simple title string)
|
||||
*/
|
||||
editCreateNode(mode?: string, init?: Object): void;
|
||||
editCreateNode(mode?: string, init?: object): void;
|
||||
|
||||
/** [ext-edit] Stop inline editing.
|
||||
*
|
||||
@@ -526,7 +527,7 @@ declare namespace Fancytree {
|
||||
*
|
||||
* @param opts passed to `setExpanded()`. Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true}
|
||||
*/
|
||||
makeVisible(opts?: Object): JQueryPromise<any>;
|
||||
makeVisible(opts?: object): JQueryPromise<any>;
|
||||
|
||||
/** Move this node to targetNode.
|
||||
*
|
||||
@@ -589,25 +590,25 @@ declare namespace Fancytree {
|
||||
* @param effects animation options.
|
||||
* @param options {topNode: null, effects: ..., parent: ...} this node will remain visible in any case, even if `this` is outside the scroll pane.
|
||||
*/
|
||||
scrollIntoView(effects?: boolean, options?: Object): JQueryPromise<any>;
|
||||
scrollIntoView(effects?: boolean, options?: object): JQueryPromise<any>;
|
||||
|
||||
/**
|
||||
* @param effects animation options.
|
||||
* @param options {topNode: null, effects: ..., parent: ...} this node will remain visible in any case, even if `this` is outside the scroll pane.
|
||||
*/
|
||||
scrollIntoView(effects?: Object, options?: Object): JQueryPromise<any>;
|
||||
scrollIntoView(effects?: object, options?: object): JQueryPromise<any>;
|
||||
|
||||
/**
|
||||
* @param flag pass false to deactivate
|
||||
* @param opts additional options. Defaults to {noEvents: false}
|
||||
*/
|
||||
setActive(flag?: boolean, opts?: Object): JQueryPromise<any>;
|
||||
setActive(flag?: boolean, opts?: object): JQueryPromise<any>;
|
||||
|
||||
/**
|
||||
* @param flag pass false to collapse.
|
||||
* @param opts additional options. Defaults to {noAnimation:false, noEvents:false}
|
||||
*/
|
||||
setExpanded(flag?: boolean, opts?: Object): JQueryPromise<any>;
|
||||
setExpanded(flag?: boolean, opts?: object): JQueryPromise<any>;
|
||||
|
||||
/**
|
||||
* Set keyboard focus to this node.
|
||||
@@ -1109,7 +1110,7 @@ declare namespace Fancytree {
|
||||
/** class names added to the node markup (separate with space) */
|
||||
extraClasses?: string | undefined;
|
||||
/** all properties from will be copied to `node.data` */
|
||||
data?: Object | undefined;
|
||||
data?: object | undefined;
|
||||
|
||||
/** Will be added as title attribute of the node's icon span,thus enabling a tooltip. */
|
||||
iconTooltip?: string | undefined;
|
||||
@@ -1160,7 +1161,7 @@ declare namespace Fancytree {
|
||||
|
||||
escapeHtml(s: string): string;
|
||||
|
||||
getEventTarget(event: Event): Object;
|
||||
getEventTarget(event: Event): object;
|
||||
|
||||
getEventTargetType(event: Event): string;
|
||||
|
||||
@@ -1179,7 +1180,7 @@ declare namespace Fancytree {
|
||||
parseHtml($ul: JQuery): NodeData[];
|
||||
|
||||
/** Add Fancytree extension definition to the list of globally available extensions. */
|
||||
registerExtension(definition: Object): void;
|
||||
registerExtension(definition: object): void;
|
||||
|
||||
unescapeHtml(s: string): string;
|
||||
|
||||
|
||||
121
apps/client/src/types-pdfjs.d.ts
vendored
Normal file
121
apps/client/src/types-pdfjs.d.ts
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
type HistoryData = {
|
||||
files: {
|
||||
fingerprint: string;
|
||||
page: number;
|
||||
zoom: string;
|
||||
scrollLeft: number;
|
||||
scrollTop: number;
|
||||
rotation: number;
|
||||
sidebarView: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
interface Window {
|
||||
/**
|
||||
* By default, pdf.js will try to store information about the opened PDFs such as zoom and scroll position in local storage.
|
||||
* The Trilium alternative is to use attachments stored at note level.
|
||||
* This variable represents the direct content used by the pdf.js viewer in its local storage key, but in plain JS object format.
|
||||
* The variable must be set early at startup, before pdf.js fully initializes.
|
||||
*/
|
||||
TRILIUM_VIEW_HISTORY_STORE?: HistoryData;
|
||||
|
||||
/**
|
||||
* If set to true, hides the pdf.js viewer default sidebar containing the outline, page navigation, etc.
|
||||
* This needs to be set early in the main method.
|
||||
*/
|
||||
TRILIUM_HIDE_SIDEBAR?: boolean;
|
||||
|
||||
TRILIUM_NOTE_ID: string;
|
||||
|
||||
TRILIUM_NTX_ID: string | null | undefined;
|
||||
}
|
||||
|
||||
interface PdfOutlineItem {
|
||||
title: string;
|
||||
level: number;
|
||||
dest: unknown;
|
||||
id: string;
|
||||
items: PdfOutlineItem[];
|
||||
}
|
||||
|
||||
interface WithContext {
|
||||
ntxId: string;
|
||||
noteId: string | null | undefined;
|
||||
}
|
||||
|
||||
interface PdfDocumentModifiedMessage extends WithContext {
|
||||
type: "pdfjs-viewer-document-modified";
|
||||
}
|
||||
|
||||
interface PdfDocumentBlobResultMessage extends WithContext {
|
||||
type: "pdfjs-viewer-blob";
|
||||
data: Uint8Array<ArrayBufferLike>;
|
||||
}
|
||||
|
||||
interface PdfSaveViewHistoryMessage extends WithContext {
|
||||
type: "pdfjs-viewer-save-view-history";
|
||||
data: string;
|
||||
}
|
||||
|
||||
interface PdfViewerTocMessage {
|
||||
type: "pdfjs-viewer-toc";
|
||||
data: PdfOutlineItem[];
|
||||
}
|
||||
|
||||
interface PdfViewerActiveHeadingMessage {
|
||||
type: "pdfjs-viewer-active-heading";
|
||||
headingId: string;
|
||||
}
|
||||
|
||||
interface PdfViewerPageInfoMessage {
|
||||
type: "pdfjs-viewer-page-info";
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
}
|
||||
|
||||
interface PdfViewerCurrentPageMessage {
|
||||
type: "pdfjs-viewer-current-page";
|
||||
currentPage: number;
|
||||
}
|
||||
|
||||
interface PdfViewerThumbnailMessage {
|
||||
type: "pdfjs-viewer-thumbnail";
|
||||
pageNumber: number;
|
||||
dataUrl: string;
|
||||
}
|
||||
|
||||
interface PdfAttachment {
|
||||
filename: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface PdfViewerAttachmentsMessage {
|
||||
type: "pdfjs-viewer-attachments";
|
||||
attachments: PdfAttachment[];
|
||||
downloadAttachment?: (fileName: string) => void;
|
||||
}
|
||||
|
||||
interface PdfLayer {
|
||||
id: string;
|
||||
name: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
interface PdfViewerLayersMessage {
|
||||
type: "pdfjs-viewer-layers";
|
||||
layers: PdfLayer[];
|
||||
toggleLayer?: (layerId: string, visible: boolean) => void;
|
||||
}
|
||||
|
||||
type PdfMessageEvent = MessageEvent<
|
||||
PdfDocumentModifiedMessage
|
||||
| PdfSaveViewHistoryMessage
|
||||
| PdfViewerTocMessage
|
||||
| PdfViewerActiveHeadingMessage
|
||||
| PdfViewerPageInfoMessage
|
||||
| PdfViewerCurrentPageMessage
|
||||
| PdfViewerThumbnailMessage
|
||||
| PdfViewerAttachmentsMessage
|
||||
| PdfViewerLayersMessage
|
||||
| PdfDocumentBlobResultMessage
|
||||
>;
|
||||
13
apps/client/src/types.d.ts
vendored
13
apps/client/src/types.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import { IconRegistry } from "@triliumnext/commons";
|
||||
import { IconRegistry, Locale } from "@triliumnext/commons";
|
||||
|
||||
import appContext, { AppContext } from "./components/app_context";
|
||||
import type FNote from "./entities/fnote";
|
||||
@@ -47,14 +47,25 @@ interface CustomGlobals {
|
||||
platform?: typeof process.platform;
|
||||
linter: typeof lint;
|
||||
hasNativeTitleBar: boolean;
|
||||
hasBackgroundEffects: boolean;
|
||||
isElectron: boolean;
|
||||
isRtl: boolean;
|
||||
iconRegistry: IconRegistry;
|
||||
themeCssUrl: string;
|
||||
themeUseNextAsBase?: "next" | "next-light" | "next-dark";
|
||||
iconPackCss: string;
|
||||
headingStyle: "plain" | "underline" | "markdown";
|
||||
layoutOrientation: "vertical" | "horizontal";
|
||||
currentLocale: Locale;
|
||||
}
|
||||
|
||||
type RequireMethod = (moduleName: string) => any;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
$: JQueryStatic;
|
||||
jQuery: JQueryStatic;
|
||||
|
||||
logError(message: string);
|
||||
logInfo(message: string);
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ export default function NoteDetail() {
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
class={`note-detail ${isFullHeight ? "full-height" : ""}`}
|
||||
class={`component note-detail ${isFullHeight ? "full-height" : ""}`}
|
||||
>
|
||||
{Object.entries(noteTypesToRender).map(([ itemType, Element ]) => {
|
||||
return <NoteDetailWrapper
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import clsx from "clsx";
|
||||
|
||||
import { t } from "../../services/i18n";
|
||||
import options from "../../services/options";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { useTriliumOptionBool } from "../react/hooks";
|
||||
import { useState, useCallback } from "preact/hooks";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
|
||||
export default function RightPaneToggle() {
|
||||
const [ rightPaneVisible, setRightPaneVisible ] = useTriliumOptionBool("rightPaneVisible");
|
||||
const [ rightPaneVisible, setRightPaneVisible ] = useState(options.is("rightPaneVisible"));
|
||||
|
||||
useTriliumEvent("toggleRightPane", useCallback(() => {
|
||||
setRightPaneVisible(current => !current);
|
||||
}, []));
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
@@ -15,7 +21,7 @@ export default function RightPaneToggle() {
|
||||
)}
|
||||
text={t("right_pane.toggle")}
|
||||
icon="bx bx-sidebar"
|
||||
onClick={() => setRightPaneVisible(!rightPaneVisible)}
|
||||
triggerCommand="toggleRightPane"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import froca from "../../services/froca";
|
||||
import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } from "../../services/ws";
|
||||
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent } from "../react/hooks";
|
||||
import { allViewTypes, ViewModeMedia, ViewModeProps, ViewTypeOptions } from "./interface";
|
||||
import ViewModeStorage from "./view_mode_storage";
|
||||
import ViewModeStorage, { type ViewModeStorageType } from "./view_mode_storage";
|
||||
|
||||
interface NoteListProps {
|
||||
note: FNote | null | undefined;
|
||||
notePath: string | null | undefined;
|
||||
@@ -215,7 +216,7 @@ export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOpt
|
||||
return noteIds;
|
||||
}
|
||||
|
||||
export function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined) {
|
||||
export function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewModeStorageType | undefined) {
|
||||
const [ viewConfig, setViewConfig ] = useState<{
|
||||
config: T | undefined;
|
||||
storeFn: (data: T) => void;
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
import { DateSelectArg, EventChangeArg, EventMountArg, EventSourceFuncArg, LocaleInput, PluginDef } from "@fullcalendar/core/index.js";
|
||||
import { ViewModeProps } from "../interface";
|
||||
import Calendar from "./calendar";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import "./index.css";
|
||||
import { useNoteLabel, useNoteLabelBoolean, useResizeObserver, useSpacedUpdate, useTriliumEvent, useTriliumOption, useTriliumOptionInt } from "../../react/hooks";
|
||||
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
|
||||
|
||||
import { Calendar as FullCalendar } from "@fullcalendar/core";
|
||||
import { parseStartEndDateFromEvent, parseStartEndTimeFromEvent } from "./utils";
|
||||
import dialog from "../../../services/dialog";
|
||||
import { t } from "../../../services/i18n";
|
||||
import { buildEvents, buildEventsForCalendar } from "./event_builder";
|
||||
import { changeEvent, newEvent } from "./api";
|
||||
import froca from "../../../services/froca";
|
||||
import date_notes from "../../../services/date_notes";
|
||||
import appContext from "../../../components/app_context";
|
||||
import { DateSelectArg, EventChangeArg, EventMountArg, EventSourceFuncArg, LocaleInput, PluginDef } from "@fullcalendar/core/index.js";
|
||||
import { DateClickArg } from "@fullcalendar/interaction";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import Button, { ButtonGroup } from "../../react/Button";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
|
||||
import { RefObject } from "preact";
|
||||
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
|
||||
import { openCalendarContextMenu } from "./context_menu";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext from "../../../components/app_context";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import date_notes from "../../../services/date_notes";
|
||||
import dialog from "../../../services/dialog";
|
||||
import froca from "../../../services/froca";
|
||||
import { t } from "../../../services/i18n";
|
||||
import { isMobile } from "../../../services/utils";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import Button, { ButtonGroup } from "../../react/Button";
|
||||
import { useNoteLabel, useNoteLabelBoolean, useResizeObserver, useSpacedUpdate, useTriliumEvent, useTriliumOption, useTriliumOptionInt } from "../../react/hooks";
|
||||
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
|
||||
import { ViewModeProps } from "../interface";
|
||||
import { changeEvent, newEvent } from "./api";
|
||||
import Calendar from "./calendar";
|
||||
import { openCalendarContextMenu } from "./context_menu";
|
||||
import { buildEvents, buildEventsForCalendar } from "./event_builder";
|
||||
import { parseStartEndDateFromEvent, parseStartEndTimeFromEvent } from "./utils";
|
||||
|
||||
interface CalendarViewData {
|
||||
|
||||
@@ -59,7 +61,7 @@ const CALENDAR_VIEWS = [
|
||||
previousText: t("calendar.month_previous"),
|
||||
nextText: t("calendar.month_next")
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
const SUPPORTED_CALENDAR_VIEW_TYPE = CALENDAR_VIEWS.map(v => v.type);
|
||||
|
||||
@@ -75,6 +77,7 @@ export const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, (() => Promise<{ de
|
||||
ru: () => import("@fullcalendar/core/locales/ru"),
|
||||
ja: () => import("@fullcalendar/core/locales/ja"),
|
||||
pt: () => import("@fullcalendar/core/locales/pt"),
|
||||
pl: () => import("@fullcalendar/core/locales/pl"),
|
||||
"pt_br": () => import("@fullcalendar/core/locales/pt-br"),
|
||||
uk: () => import("@fullcalendar/core/locales/uk"),
|
||||
en: null,
|
||||
@@ -102,9 +105,9 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
|
||||
const eventBuilder = useMemo(() => {
|
||||
if (!isCalendarRoot) {
|
||||
return async () => await buildEvents(noteIds);
|
||||
} else {
|
||||
return async (e: EventSourceFuncArg) => await buildEventsForCalendar(note, e);
|
||||
}
|
||||
}
|
||||
return async (e: EventSourceFuncArg) => await buildEventsForCalendar(note, e);
|
||||
|
||||
}, [isCalendarRoot, noteIds]);
|
||||
|
||||
const plugins = usePlugins(isEditable, isCalendarRoot);
|
||||
@@ -178,7 +181,7 @@ function CalendarHeader({ calendarRef }: { calendarRef: RefObject<FullCalendar>
|
||||
<ActionButton icon="bx bx-chevron-right" text={currentViewData?.nextText ?? ""} frame onClick={() => calendarRef.current?.next()} />
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function usePlugins(isEditable: boolean, isCalendarRoot: boolean) {
|
||||
@@ -293,7 +296,7 @@ function useEventDisplayCustomization(parentNote: FNote) {
|
||||
if (promotedAttributes) {
|
||||
let promotedAttributesHtml = "";
|
||||
for (const [name, value] of promotedAttributes) {
|
||||
promotedAttributesHtml = promotedAttributesHtml + /*html*/`\
|
||||
promotedAttributesHtml = `${promotedAttributesHtml /*html*/}\
|
||||
<div class="promoted-attribute">
|
||||
<span class="promoted-attribute-name">${name}</span>: <span class="promoted-attribute-value">${value}</span>
|
||||
</div>`;
|
||||
|
||||
@@ -142,4 +142,10 @@
|
||||
border: 1px solid var(--main-border-color);
|
||||
background: var(--more-accented-background-color);
|
||||
}
|
||||
|
||||
.note-list.grid-view .note-path {
|
||||
margin-left: 0.5em;
|
||||
vertical-align: middle;
|
||||
opacity: 0.5;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
@@ -7,7 +7,6 @@ import attribute_renderer from "../../../services/attribute_renderer";
|
||||
import content_renderer from "../../../services/content_renderer";
|
||||
import { t } from "../../../services/i18n";
|
||||
import link from "../../../services/link";
|
||||
import tree from "../../../services/tree";
|
||||
import { useImperativeSearchHighlighlighting, useNoteLabel, useNoteLabelBoolean } from "../../react/hooks";
|
||||
import Icon from "../../react/Icon";
|
||||
import NoteLink from "../../react/NoteLink";
|
||||
@@ -45,6 +44,7 @@ export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
|
||||
export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
|
||||
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
|
||||
const { pageNotes, ...pagination } = usePagination(note, noteIds);
|
||||
const [ includeArchived ] = useNoteLabelBoolean(note, "includeArchived");
|
||||
|
||||
return (
|
||||
<div class="note-list grid-view">
|
||||
@@ -53,7 +53,7 @@ export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
|
||||
|
||||
<div class="note-list-container use-tn-links">
|
||||
{pageNotes?.map(childNote => (
|
||||
<GridNoteCard note={childNote} parentNote={note} highlightedTokens={highlightedTokens} />
|
||||
<GridNoteCard note={childNote} parentNote={note} highlightedTokens={highlightedTokens} includeArchived={includeArchived} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -95,24 +95,17 @@ function ListNoteCard({ note, parentNote, highlightedTokens, currentLevel, expan
|
||||
</h5>
|
||||
|
||||
{isExpanded && <>
|
||||
<NoteContent note={note} highlightedTokens={highlightedTokens} noChildrenList />
|
||||
<NoteContent note={note} highlightedTokens={highlightedTokens} noChildrenList includeArchivedNotes={includeArchived} />
|
||||
<NoteChildren note={note} parentNote={parentNote} highlightedTokens={highlightedTokens} currentLevel={currentLevel} expandDepth={expandDepth} includeArchived={includeArchived} />
|
||||
</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined }) {
|
||||
function GridNoteCard({ note, parentNote, highlightedTokens, includeArchived }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined, includeArchived: boolean }) {
|
||||
const titleRef = useRef<HTMLSpanElement>(null);
|
||||
const [ noteTitle, setNoteTitle ] = useState<string>();
|
||||
const notePath = getNotePath(parentNote, note);
|
||||
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
|
||||
|
||||
useEffect(() => {
|
||||
tree.getNoteTitle(note.noteId, parentNote.noteId).then(setNoteTitle);
|
||||
}, [ note ]);
|
||||
|
||||
useEffect(() => highlightSearch(titleRef.current), [ noteTitle, highlightedTokens ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -123,13 +116,14 @@ function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, pa
|
||||
>
|
||||
<h5 className="note-book-header">
|
||||
<Icon className="note-icon" icon={note.getIcon()} />
|
||||
<span ref={titleRef} className="note-book-title">{noteTitle}</span>
|
||||
<NoteLink className="note-book-title" notePath={notePath} noPreview showNotePath={parentNote.type === "search"} highlightedTokens={highlightedTokens} />
|
||||
<NoteAttributes note={note} />
|
||||
</h5>
|
||||
<NoteContent
|
||||
note={note}
|
||||
trim
|
||||
highlightedTokens={highlightedTokens}
|
||||
includeArchivedNotes={includeArchived}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -146,14 +140,22 @@ function NoteAttributes({ note }: { note: FNote }) {
|
||||
return <span className="note-list-attributes" ref={ref} />;
|
||||
}
|
||||
|
||||
function NoteContent({ note, trim, noChildrenList, highlightedTokens }: { note: FNote, trim?: boolean, noChildrenList?: boolean, highlightedTokens: string[] | null | undefined }) {
|
||||
function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes }: {
|
||||
note: FNote;
|
||||
trim?: boolean;
|
||||
noChildrenList?: boolean;
|
||||
highlightedTokens: string[] | null | undefined;
|
||||
includeArchivedNotes: boolean;
|
||||
}) {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
|
||||
|
||||
useEffect(() => {
|
||||
content_renderer.getRenderedContent(note, {
|
||||
trim,
|
||||
noChildrenList
|
||||
noChildrenList,
|
||||
noIncludedNotes: true,
|
||||
includeArchivedNotes
|
||||
})
|
||||
.then(({ $renderedContent, type }) => {
|
||||
if (!contentRef.current) return;
|
||||
|
||||
@@ -4,14 +4,16 @@ import { ViewTypeOptions } from "../collections/interface";
|
||||
|
||||
const ATTACHMENT_ROLE = "viewConfig";
|
||||
|
||||
export type ViewModeStorageType = ViewTypeOptions | "pdfHistory";
|
||||
|
||||
export default class ViewModeStorage<T extends object> {
|
||||
|
||||
private note: FNote;
|
||||
private attachmentName: string;
|
||||
|
||||
constructor(note: FNote, viewType: ViewTypeOptions) {
|
||||
constructor(note: FNote, viewType: ViewModeStorageType) {
|
||||
this.note = note;
|
||||
this.attachmentName = viewType + ".json";
|
||||
this.attachmentName = `${viewType}.json`;
|
||||
}
|
||||
|
||||
async store(data: T) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
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 Component from "../../components/component.js";
|
||||
import NoteContext from "../../components/note_context.js";
|
||||
import splitService from "../../services/resizer.js";
|
||||
import { isMobile } from "../../services/utils.js";
|
||||
import NoteContext from "../../components/note_context.js";
|
||||
import type BasicWidget from "../basic_widget.js";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import FlexContainer from "./flex_container.js";
|
||||
|
||||
interface SplitNoteWidget extends BasicWidget {
|
||||
hasBeenAlreadyShown?: boolean;
|
||||
@@ -74,7 +75,7 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
|
||||
|
||||
|
||||
const subContexts = activeContext.getSubContexts();
|
||||
let noteContext: NoteContext | undefined = undefined;
|
||||
let noteContext: NoteContext | undefined;
|
||||
if (isMobile() && subContexts.length > 1) {
|
||||
noteContext = subContexts.find(s => s.ntxId !== ntxId);
|
||||
}
|
||||
@@ -201,6 +202,11 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
|
||||
|
||||
async refresh() {
|
||||
this.toggleExt(true);
|
||||
|
||||
// Mark the active note context.
|
||||
for (const child of this.children as NoteContextAwareWidget[]) {
|
||||
child.$widget.toggleClass("active", !!child.noteContext?.isActive());
|
||||
}
|
||||
}
|
||||
|
||||
toggleInt(show: boolean) {} // not needed
|
||||
@@ -239,16 +245,16 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
|
||||
widget.hasBeenAlreadyShown = true;
|
||||
|
||||
return [widget.handleEvent("noteSwitched", noteSwitchedContext), this.refreshNotShown(noteSwitchedContext)];
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.resolve();
|
||||
|
||||
}
|
||||
|
||||
if (name === "activeContextChanged") {
|
||||
return this.refreshNotShown(data as EventData<"activeContextChanged">);
|
||||
} else {
|
||||
return super.handleEventInChildren(name, data);
|
||||
}
|
||||
return super.handleEventInChildren(name, data);
|
||||
|
||||
}
|
||||
|
||||
refreshNotShown(data: NoteSwitchedContext | EventData<"activeContextChanged">) {
|
||||
|
||||
@@ -88,6 +88,7 @@ export default function PopupEditor() {
|
||||
onHidden={() => setShown(false)}
|
||||
keepInDom // needed for faster loading
|
||||
noFocus // automatic focus breaks block popup
|
||||
stackable
|
||||
>
|
||||
{!isNewLayout && <ReadOnlyNoteInfoBar />}
|
||||
<PromotedAttributes />
|
||||
|
||||
@@ -14,7 +14,7 @@ body.mobile .revisions-dialog {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.modal-body {
|
||||
height: fit-content !important;
|
||||
flex-direction: column;
|
||||
@@ -24,7 +24,7 @@ body.mobile .revisions-dialog {
|
||||
.modal-footer {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
|
||||
.revision-list {
|
||||
height: fit-content !important;
|
||||
max-height: 20vh;
|
||||
@@ -32,7 +32,7 @@ body.mobile .revisions-dialog {
|
||||
padding: 0 1em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
.modal-body > .revision-content-wrapper {
|
||||
flex-grow: 1;
|
||||
max-width: unset !important;
|
||||
@@ -40,24 +40,68 @@ body.mobile .revisions-dialog {
|
||||
margin: 0;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
|
||||
.modal-body > .revision-content-wrapper > div:first-of-type {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.revision-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.revision-title-buttons {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
|
||||
.revision-content {
|
||||
padding: 0.5em;
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.revisions-dialog {
|
||||
.revision-title-buttons {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.revision-list {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.revision-content.type-file {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex-grow: 1;
|
||||
|
||||
.file-preview-table {
|
||||
th,
|
||||
td {
|
||||
padding: 0.25em 0;
|
||||
}
|
||||
}
|
||||
|
||||
.revision-file-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.revision-file-preview-content {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> * {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
import type { RevisionPojo, RevisionItem } from "@triliumnext/commons";
|
||||
import "./revisions.css";
|
||||
|
||||
import type { RevisionItem, RevisionPojo } from "@triliumnext/commons";
|
||||
import clsx from "clsx";
|
||||
import { diffWords } from "diff";
|
||||
import type { CSSProperties } from "preact/compat";
|
||||
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import dialog from "../../services/dialog";
|
||||
import froca from "../../services/froca";
|
||||
import { t } from "../../services/i18n";
|
||||
import { renderMathInElement } from "../../services/math";
|
||||
import open from "../../services/open";
|
||||
import options from "../../services/options";
|
||||
import protected_session_holder from "../../services/protected_session_holder";
|
||||
import server from "../../services/server";
|
||||
import toast from "../../services/toast";
|
||||
import Button from "../react/Button";
|
||||
import FormToggle from "../react/FormToggle";
|
||||
import Modal from "../react/Modal";
|
||||
import FormList, { FormListItem } from "../react/FormList";
|
||||
import utils from "../../services/utils";
|
||||
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
|
||||
import protected_session_holder from "../../services/protected_session_holder";
|
||||
import { renderMathInElement } from "../../services/math";
|
||||
import type { CSSProperties } from "preact/compat";
|
||||
import open from "../../services/open";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import options from "../../services/options";
|
||||
import Button from "../react/Button";
|
||||
import FormList, { FormListItem } from "../react/FormList";
|
||||
import FormToggle from "../react/FormToggle";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import { diffWords } from "diff";
|
||||
import "./revisions.css";
|
||||
import Modal from "../react/Modal";
|
||||
import { RawHtmlBlock } from "../react/RawHtml";
|
||||
import PdfViewer from "../type_widgets/file/PdfViewer";
|
||||
|
||||
export default function RevisionsDialog() {
|
||||
const [ note, setNote ] = useState<FNote>();
|
||||
@@ -47,7 +52,7 @@ export default function RevisionsDialog() {
|
||||
setRevisions(undefined);
|
||||
setNoteContent(undefined);
|
||||
}
|
||||
}, [ note?.noteId, refreshCounter ]);
|
||||
}, [ note, refreshCounter ]);
|
||||
|
||||
if (revisions?.length && !currentRevision) {
|
||||
setCurrentRevision(revisions[0]);
|
||||
@@ -102,38 +107,38 @@ export default function RevisionsDialog() {
|
||||
setRevisions(undefined);
|
||||
}}
|
||||
show={shown}
|
||||
>
|
||||
<RevisionsList
|
||||
revisions={revisions ?? []}
|
||||
onSelect={(revisionId) => {
|
||||
const correspondingRevision = (revisions ?? []).find((r) => r.revisionId === revisionId);
|
||||
if (correspondingRevision) {
|
||||
setCurrentRevision(correspondingRevision);
|
||||
}
|
||||
}}
|
||||
currentRevision={currentRevision}
|
||||
/>
|
||||
>
|
||||
<RevisionsList
|
||||
revisions={revisions ?? []}
|
||||
onSelect={(revisionId) => {
|
||||
const correspondingRevision = (revisions ?? []).find((r) => r.revisionId === revisionId);
|
||||
if (correspondingRevision) {
|
||||
setCurrentRevision(correspondingRevision);
|
||||
}
|
||||
}}
|
||||
currentRevision={currentRevision}
|
||||
/>
|
||||
|
||||
<div className="revision-content-wrapper" style={{
|
||||
flexGrow: "1",
|
||||
marginInlineStart: "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
maxWidth: "calc(100% - 150px)",
|
||||
minWidth: 0
|
||||
}}>
|
||||
<RevisionPreview
|
||||
noteContent={noteContent}
|
||||
revisionItem={currentRevision}
|
||||
showDiff={showDiff}
|
||||
setShown={setShown}
|
||||
onRevisionDeleted={() => {
|
||||
setRefreshCounter(c => c + 1);
|
||||
setCurrentRevision(undefined);
|
||||
}} />
|
||||
</div>
|
||||
<div className="revision-content-wrapper" style={{
|
||||
flexGrow: "1",
|
||||
marginInlineStart: "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
maxWidth: "calc(100% - 150px)",
|
||||
minWidth: 0
|
||||
}}>
|
||||
<RevisionPreview
|
||||
noteContent={noteContent}
|
||||
revisionItem={currentRevision}
|
||||
showDiff={showDiff}
|
||||
setShown={setShown}
|
||||
onRevisionDeleted={() => {
|
||||
setRefreshCounter(c => c + 1);
|
||||
setCurrentRevision(undefined);
|
||||
}} />
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: RevisionItem[], onSelect: (val: string) => void, currentRevision?: RevisionItem }) {
|
||||
@@ -141,6 +146,7 @@ function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: Re
|
||||
<FormList onSelect={onSelect} fullHeight wrapperClassName="revision-list">
|
||||
{revisions.map((item) =>
|
||||
<FormListItem
|
||||
key={item.revisionId}
|
||||
value={item.revisionId}
|
||||
active={currentRevision && item.revisionId === currentRevision.revisionId}
|
||||
>
|
||||
@@ -202,14 +208,17 @@ function RevisionPreview({noteContent, revisionItem, showDiff, setShown, onRevis
|
||||
text={t("revisions.download_button")}
|
||||
onClick={() => {
|
||||
if (revisionItem.revisionId) {
|
||||
open.downloadRevision(revisionItem.noteId, revisionItem.revisionId)}
|
||||
}
|
||||
open.downloadRevision(revisionItem.noteId, revisionItem.revisionId);}
|
||||
}
|
||||
}/>
|
||||
</>
|
||||
}
|
||||
</div>)}
|
||||
</div>
|
||||
<div className="revision-content use-tn-links selectable-text" style={{ overflow: "auto", wordBreak: "break-word" }}>
|
||||
<div
|
||||
className={clsx("revision-content use-tn-links selectable-text", `type-${revisionItem?.type}`)}
|
||||
style={{ overflow: "auto", wordBreak: "break-word" }}
|
||||
>
|
||||
<RevisionContent noteContent={noteContent} revisionItem={revisionItem} fullRevision={fullRevision} showDiff={showDiff}/>
|
||||
</div>
|
||||
</>
|
||||
@@ -230,16 +239,16 @@ const CODE_STYLE: CSSProperties = {
|
||||
|
||||
function RevisionContent({ noteContent, revisionItem, fullRevision, showDiff }: { noteContent?:string, revisionItem?: RevisionItem, fullRevision?: RevisionPojo, showDiff: boolean}) {
|
||||
const content = fullRevision?.content;
|
||||
if (!revisionItem || !content) {
|
||||
if (!revisionItem || !fullRevision) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (showDiff) {
|
||||
return <RevisionContentDiff noteContent={noteContent} itemContent={content} itemType={revisionItem.type}/>
|
||||
return <RevisionContentDiff noteContent={noteContent} itemContent={content} itemType={revisionItem.type}/>;
|
||||
}
|
||||
switch (revisionItem.type) {
|
||||
case "text":
|
||||
return <RevisionContentText content={content} />
|
||||
return <RevisionContentText content={content} />;
|
||||
case "code":
|
||||
return <pre style={CODE_STYLE}>{content}</pre>;
|
||||
case "image":
|
||||
@@ -256,28 +265,11 @@ function RevisionContent({ noteContent, revisionItem, fullRevision, showDiff }:
|
||||
// as a URL to be used in a note. Instead, if they copy and paste it into a note, it will be uploaded as a new note
|
||||
return <img
|
||||
src={`data:${fullRevision.mime};base64,${fullRevision.content}`}
|
||||
style={IMAGE_STYLE} />
|
||||
style={IMAGE_STYLE} />;
|
||||
}
|
||||
}
|
||||
case "file":
|
||||
return <table cellPadding="10">
|
||||
<tr>
|
||||
<th>{t("revisions.mime")}</th>
|
||||
<td>{revisionItem.mime}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t("revisions.file_size")}</th>
|
||||
<td>{revisionItem.contentLength && utils.formatSize(revisionItem.contentLength)}</td>
|
||||
</tr>
|
||||
{fullRevision.content &&
|
||||
<tr>
|
||||
<td colspan={2}>
|
||||
<strong>{t("revisions.preview")}</strong>
|
||||
<pre className="file-preview-content" style={CODE_STYLE}>{fullRevision.content}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>;
|
||||
return <FilePreview fullRevision={fullRevision} revisionItem={revisionItem} />;
|
||||
case "canvas":
|
||||
case "mindMap":
|
||||
case "mermaid": {
|
||||
@@ -287,7 +279,7 @@ function RevisionContent({ noteContent, revisionItem, fullRevision, showDiff }:
|
||||
style={IMAGE_STYLE} />;
|
||||
}
|
||||
default:
|
||||
return <>{t("revisions.preview_not_available")}</>
|
||||
return <>{t("revisions.preview_not_available")}</>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +290,7 @@ function RevisionContentText({ content }: { content: string | Buffer<ArrayBuffer
|
||||
renderMathInElement(contentRef.current, { trust: true });
|
||||
}
|
||||
}, [content]);
|
||||
return <div ref={contentRef} className="ck-content" dangerouslySetInnerHTML={{ __html: content as string }}></div>
|
||||
return <RawHtmlBlock containerRef={contentRef} className="ck-content" html={content as string} />;
|
||||
}
|
||||
|
||||
function RevisionContentDiff({ noteContent, itemContent, itemType }: {
|
||||
@@ -330,9 +322,9 @@ function RevisionContentDiff({ noteContent, itemContent, itemType }: {
|
||||
return `<span class="revision-diff-added">${utils.escapeHtml(part.value)}</span>`;
|
||||
} else if (part.removed) {
|
||||
return `<span class="revision-diff-removed">${utils.escapeHtml(part.value)}</span>`;
|
||||
} else {
|
||||
return utils.escapeHtml(part.value);
|
||||
}
|
||||
return utils.escapeHtml(part.value);
|
||||
|
||||
}).join("");
|
||||
|
||||
if (contentRef.current) {
|
||||
@@ -340,7 +332,7 @@ function RevisionContentDiff({ noteContent, itemContent, itemType }: {
|
||||
}
|
||||
}, [noteContent, itemContent, itemType]);
|
||||
|
||||
return <div ref={contentRef} className="ck-content" style={{ whiteSpace: "pre-wrap" }}></div>;
|
||||
return <div ref={contentRef} className="ck-content" style={{ whiteSpace: "pre-wrap" }} />;
|
||||
}
|
||||
|
||||
function RevisionFooter({ note }: { note?: FNote }) {
|
||||
@@ -348,7 +340,7 @@ function RevisionFooter({ note }: { note?: FNote }) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
let revisionsNumberLimit: number | string = parseInt(note?.getLabelValue("versioningLimit") ?? "");
|
||||
let revisionsNumberLimit: number | string = parseInt(note?.getLabelValue("versioningLimit") ?? "", 10);
|
||||
if (!Number.isInteger(revisionsNumberLimit)) {
|
||||
revisionsNumberLimit = options.getInt("revisionSnapshotNumberLimit") ?? 0;
|
||||
}
|
||||
@@ -370,10 +362,67 @@ function RevisionFooter({ note }: { note?: FNote }) {
|
||||
</>;
|
||||
}
|
||||
|
||||
function FilePreview({ revisionItem, fullRevision }: { revisionItem: RevisionItem, fullRevision: RevisionPojo }) {
|
||||
return (
|
||||
<div className="revision-file-preview">
|
||||
<table className="file-preview-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{t("revisions.mime")}</th>
|
||||
<td>{revisionItem.mime}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t("revisions.file_size")}</th>
|
||||
<td>{revisionItem.contentLength && utils.formatSize(revisionItem.contentLength)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="revision-file-preview-content">
|
||||
<FilePreviewInner revisionItem={revisionItem} fullRevision={fullRevision} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FilePreviewInner({ revisionItem, fullRevision }: { revisionItem: RevisionItem, fullRevision: RevisionPojo }) {
|
||||
if (revisionItem.mime.startsWith("audio/")) {
|
||||
return (
|
||||
<audio
|
||||
src={`api/revisions/${revisionItem.revisionId}/download`}
|
||||
controls
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (revisionItem.mime.startsWith("video/")) {
|
||||
return (
|
||||
<video
|
||||
src={`api/revisions/${revisionItem.revisionId}/download`}
|
||||
controls
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (revisionItem.mime === "application/pdf") {
|
||||
return (
|
||||
<PdfViewer
|
||||
pdfUrl={`../../api/revisions/${revisionItem.revisionId}/download`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (fullRevision.content) {
|
||||
return <pre className="file-preview-content" style={CODE_STYLE}>{fullRevision.content}</pre>;
|
||||
}
|
||||
|
||||
return t("revisions.preview_not_available");
|
||||
}
|
||||
|
||||
async function getNote(noteId?: string | null) {
|
||||
if (noteId) {
|
||||
return await froca.getNote(noteId);
|
||||
} else {
|
||||
return appContext.tabManager.getActiveContextNote();
|
||||
}
|
||||
return appContext.tabManager.getActiveContextNote();
|
||||
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { useCallback, useLayoutEffect, useState } from "preact/hooks";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import froca from "../../services/froca";
|
||||
import { isDesktop, isMobile } from "../../services/utils";
|
||||
import CalendarWidget from "./CalendarWidget";
|
||||
import SpacerWidget from "./SpacerWidget";
|
||||
import BookmarkButtons from "./BookmarkButtons";
|
||||
import ProtectedSessionStatusWidget from "./ProtectedSessionStatusWidget";
|
||||
import SyncStatus from "./SyncStatus";
|
||||
import HistoryNavigationButton from "./HistoryNavigation";
|
||||
import { AiChatButton, CommandButton, CustomWidget, NoteLauncher, QuickSearchLauncherWidget, ScriptLauncher, TodayLauncher } from "./LauncherDefinitions";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import { onWheelHorizontalScroll } from "../widget_utils";
|
||||
import BookmarkButtons from "./BookmarkButtons";
|
||||
import CalendarWidget from "./CalendarWidget";
|
||||
import HistoryNavigationButton from "./HistoryNavigation";
|
||||
import { LaunchBarContext } from "./launch_bar_widgets";
|
||||
import { AiChatButton, CommandButton, CustomWidget, NoteLauncher, QuickSearchLauncherWidget, ScriptLauncher, TodayLauncher } from "./LauncherDefinitions";
|
||||
import ProtectedSessionStatusWidget from "./ProtectedSessionStatusWidget";
|
||||
import SpacerWidget from "./SpacerWidget";
|
||||
import SyncStatus from "./SyncStatus";
|
||||
|
||||
export default function LauncherContainer({ isHorizontalLayout }: { isHorizontalLayout: boolean }) {
|
||||
const childNotes = useLauncherChildNotes();
|
||||
@@ -34,18 +35,19 @@ export default function LauncherContainer({ isHorizontalLayout }: { isHorizontal
|
||||
}}>
|
||||
{childNotes?.map(childNote => {
|
||||
if (childNote.type !== "launcher") {
|
||||
throw new Error(`Note '${childNote.noteId}' '${childNote.title}' is not a launcher even though it's in the launcher subtree`);
|
||||
console.warn(`Note '${childNote.noteId}' '${childNote.title}' is not a launcher even though it's in the launcher subtree`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isDesktop() && childNote.isLabelTruthy("desktopOnly")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return <Launcher key={childNote.noteId} note={childNote} isHorizontalLayout={isHorizontalLayout} />
|
||||
return <Launcher key={childNote.noteId} note={childNote} isHorizontalLayout={isHorizontalLayout} />;
|
||||
})}
|
||||
</LaunchBarContext.Provider>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function Launcher({ note, isHorizontalLayout }: { note: FNote, isHorizontalLayout: boolean }) {
|
||||
@@ -72,7 +74,7 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) {
|
||||
const builtinWidget = note.getLabelValue("builtinWidget");
|
||||
switch (builtinWidget) {
|
||||
case "calendar":
|
||||
return <CalendarWidget launcherNote={note} />
|
||||
return <CalendarWidget launcherNote={note} />;
|
||||
case "spacer":
|
||||
// || has to be inside since 0 is a valid value
|
||||
const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
|
||||
@@ -86,15 +88,15 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) {
|
||||
case "syncStatus":
|
||||
return <SyncStatus />;
|
||||
case "backInHistoryButton":
|
||||
return <HistoryNavigationButton launcherNote={note} command="backInNoteHistory" />
|
||||
return <HistoryNavigationButton launcherNote={note} command="backInNoteHistory" />;
|
||||
case "forwardInHistoryButton":
|
||||
return <HistoryNavigationButton launcherNote={note} command="forwardInNoteHistory" />
|
||||
return <HistoryNavigationButton launcherNote={note} command="forwardInNoteHistory" />;
|
||||
case "todayInJournal":
|
||||
return <TodayLauncher launcherNote={note} />
|
||||
return <TodayLauncher launcherNote={note} />;
|
||||
case "quickSearch":
|
||||
return <QuickSearchLauncherWidget />
|
||||
return <QuickSearchLauncherWidget />;
|
||||
case "aiChatLauncher":
|
||||
return <AiChatButton launcherNote={note} />
|
||||
return <AiChatButton launcherNote={note} />;
|
||||
default:
|
||||
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
contain: none;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from { opacity: var(--default-opacity); }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
.note-badges {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
@@ -16,6 +21,23 @@
|
||||
&.share-badge {--color: var(--badge-share-background-color);}
|
||||
&.clipped-note-badge {--color: var(--badge-clipped-note-background-color);}
|
||||
&.execute-badge {--color: var(--badge-execute-background-color);}
|
||||
&.save-status-badge {
|
||||
--default-opacity: 0.4;
|
||||
opacity: var(--default-opacity);
|
||||
transition: opacity 250ms ease-in;
|
||||
color: var(--main-text-color);
|
||||
|
||||
&.error {
|
||||
color: var(--dropdown-item-icon-destructive-color);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.saved {
|
||||
animation: fadeOut 250ms ease-in 5s forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
min-width: 0;
|
||||
|
||||
.text {
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import "./NoteBadges.css";
|
||||
|
||||
import { clsx } from "clsx";
|
||||
|
||||
import { copyTextWithToast } from "../../services/clipboard_ext";
|
||||
import { t } from "../../services/i18n";
|
||||
import { goToLinkExt } from "../../services/link";
|
||||
import { Badge, BadgeWithDropdown } from "../react/Badge";
|
||||
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
|
||||
import { useGetContextData, useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
|
||||
import { useShareState } from "../ribbon/BasicPropertiesTab";
|
||||
import { useShareInfo } from "../shared_info";
|
||||
|
||||
export default function NoteBadges() {
|
||||
return (
|
||||
<div className="note-badges">
|
||||
<SaveStatusBadge />
|
||||
<ReadOnlyBadge />
|
||||
<ShareBadge />
|
||||
<ClippedNoteBadge />
|
||||
@@ -105,3 +108,42 @@ function ExecuteBadge() {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SaveStatusBadge() {
|
||||
const saveState = useGetContextData("saveState");
|
||||
if (!saveState) return;
|
||||
|
||||
const stateConfig = {
|
||||
saved: {
|
||||
icon: "bx bx-check",
|
||||
title: t("breadcrumb_badges.save_status_saved"),
|
||||
tooltip: undefined
|
||||
},
|
||||
saving: {
|
||||
icon: "bx bx-loader bx-spin",
|
||||
title: t("breadcrumb_badges.save_status_saving"),
|
||||
tooltip: t("breadcrumb_badges.save_status_saving_tooltip")
|
||||
},
|
||||
unsaved: {
|
||||
icon: "bx bx-pencil",
|
||||
title: t("breadcrumb_badges.save_status_unsaved"),
|
||||
tooltip: t("breadcrumb_badges.save_status_unsaved_tooltip")
|
||||
},
|
||||
error: {
|
||||
icon: "bx bxs-error",
|
||||
title: t("breadcrumb_badges.save_status_error"),
|
||||
tooltip: t("breadcrumb_badges.save_status_error_tooltip")
|
||||
}
|
||||
};
|
||||
|
||||
const { icon, title, tooltip } = stateConfig[saveState.state];
|
||||
|
||||
return (
|
||||
<Badge
|
||||
className={clsx("save-status-badge", saveState.state)}
|
||||
icon={icon}
|
||||
text={title}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||
import clsx from "clsx";
|
||||
import { type ComponentChildren, RefObject } from "preact";
|
||||
import { createPortal } from "preact/compat";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import NoteContext from "../../components/note_context";
|
||||
@@ -338,15 +338,19 @@ interface AttributesProps extends StatusBarContext {
|
||||
function AttributesButton({ note, attributesShown, setAttributesShown }: AttributesProps) {
|
||||
const [ count, setCount ] = useState(note.attributes.length);
|
||||
|
||||
const getAttributeCount = useCallback((note: FNote) => {
|
||||
return note.getAttributes().filter(a => !a.isAutoLink).length;
|
||||
}, []);
|
||||
|
||||
// React to note changes.
|
||||
useEffect(() => {
|
||||
setCount(note.getAttributes().filter(a => !a.isAutoLink).length);
|
||||
}, [ note ]);
|
||||
setCount(getAttributeCount(note));
|
||||
}, [ note, getAttributeCount ]);
|
||||
|
||||
// React to changes in count.
|
||||
useTriliumEvent("entitiesReloaded", (({loadResults}) => {
|
||||
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
|
||||
setCount(note.attributes.length);
|
||||
setCount(getAttributeCount(note));
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -82,6 +82,13 @@ function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOption
|
||||
))}
|
||||
{properties.length > 0 && <FormDropdownDivider />}
|
||||
|
||||
<ViewProperty note={note} property={{
|
||||
type: "checkbox",
|
||||
icon: "bx bx-hide",
|
||||
label: t("book_properties.hide_child_notes"),
|
||||
bindToLabel: "subtreeHidden"
|
||||
} as CheckBoxProperty} />
|
||||
|
||||
<ViewProperty note={note} property={{
|
||||
type: "checkbox",
|
||||
icon: "bx bx-archive",
|
||||
|
||||
20
apps/client/src/widgets/note_tree.css
Normal file
20
apps/client/src/widgets/note_tree.css
Normal file
@@ -0,0 +1,20 @@
|
||||
#left-pane .tree-wrapper {
|
||||
.note-indicator-icon.subtree-hidden-badge {
|
||||
font-family: inherit !important;
|
||||
margin-inline: 0.5em;
|
||||
margin-top: 0.3em;
|
||||
background: var(--left-pane-item-action-button-background);
|
||||
color: var(--left-pane-item-action-button-color);
|
||||
padding: 0.1em 0.6em;
|
||||
border-radius: 0.5em;
|
||||
font-size: 0.7rem;
|
||||
font-weight: normal;
|
||||
float: right;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.spotlighted-node {
|
||||
opacity: 0.8;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@ import "jquery.fancytree/dist/modules/jquery.fancytree.dnd5.js";
|
||||
import "jquery.fancytree/dist/modules/jquery.fancytree.clones.js";
|
||||
import "jquery.fancytree/dist/modules/jquery.fancytree.filter.js";
|
||||
import "../stylesheets/tree.css";
|
||||
import "./note_tree.css";
|
||||
|
||||
import appContext, { type CommandListenerData, type EventData } from "../components/app_context.js";
|
||||
import type { SetNoteOpts } from "../components/note_context.js";
|
||||
import type { TouchBarItem } from "../components/touch_bar.js";
|
||||
import type FBranch from "../entities/fbranch.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { NoteType } from "../entities/fnote.js";
|
||||
import contextMenu from "../menus/context_menu.js";
|
||||
import type { TreeCommandNames } from "../menus/tree_context_menu.js";
|
||||
import branchService from "../services/branches.js";
|
||||
@@ -153,7 +153,7 @@ const TPL = /*html*/`
|
||||
const MAX_SEARCH_RESULTS_IN_TREE = 100;
|
||||
|
||||
// this has to be hanged on the actual elements to effectively intercept and stop click event
|
||||
const cancelClickPropagation: (e: JQuery.ClickEvent) => void = (e) => e.stopPropagation();
|
||||
const cancelClickPropagation: (e: Event) => void = (e) => e.stopPropagation();
|
||||
|
||||
// TODO: Fix once we remove Node.js API from public
|
||||
type Timeout = NodeJS.Timeout | string | number | undefined;
|
||||
@@ -190,6 +190,9 @@ export interface DragData {
|
||||
|
||||
export const TREE_CLIPBOARD_TYPE = "application/x-fancytree-node";
|
||||
|
||||
/** Entity changes below the given threshold will be processed without batching to avoid performance degradation. */
|
||||
const BATCH_UPDATE_THRESHOLD = 10;
|
||||
|
||||
export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
private $tree!: JQuery<HTMLElement>;
|
||||
private $treeActions!: JQuery<HTMLElement>;
|
||||
@@ -201,6 +204,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
private treeName: "main";
|
||||
private autoCollapseTimeoutId?: Timeout;
|
||||
private lastFilteredHoistedNotePath?: string | null;
|
||||
private spotlightedNotePath?: string | null;
|
||||
private spotlightedNode: Fancytree.FancytreeNode | null = null;
|
||||
private tree!: Fancytree.Fancytree;
|
||||
|
||||
constructor() {
|
||||
@@ -353,6 +358,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
this.$tree.fancytree({
|
||||
titlesTabbable: true,
|
||||
keyboard: true,
|
||||
toggleEffect: options.is("motionEnabled") ? undefined : false,
|
||||
extensions: ["dnd5", "clones", "filter"],
|
||||
source: treeData,
|
||||
scrollOfs: {
|
||||
@@ -552,7 +558,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
} else if (data.hitMode === "after") {
|
||||
branchService.moveAfterBranch(selectedBranchIds, node.data.branchId);
|
||||
} else if (data.hitMode === "over") {
|
||||
branchService.moveToParentNote(selectedBranchIds, node.data.branchId);
|
||||
branchService.moveToParentNote(selectedBranchIds, node.data.branchId, this.componentId);
|
||||
} else {
|
||||
throw new Error(`Unknown hitMode '${data.hitMode}'`);
|
||||
}
|
||||
@@ -598,73 +604,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
clones: {
|
||||
highlightActiveClones: true
|
||||
},
|
||||
async enhanceTitle (
|
||||
event: Event,
|
||||
data: {
|
||||
node: Fancytree.FancytreeNode;
|
||||
noteId: string;
|
||||
}
|
||||
) {
|
||||
const node = data.node;
|
||||
|
||||
if (!node.data.noteId) {
|
||||
// if there's "non-note" node, then don't enhance
|
||||
// this can happen for e.g. "Load error!" node
|
||||
return;
|
||||
}
|
||||
|
||||
const note = await froca.getNote(node.data.noteId, true);
|
||||
|
||||
if (!note) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeNoteContext = appContext.tabManager.getActiveContext();
|
||||
|
||||
const $span = $(node.span);
|
||||
|
||||
$span.find(".tree-item-button").remove();
|
||||
|
||||
const isHoistedNote = activeNoteContext && activeNoteContext.hoistedNoteId === note.noteId && note.noteId !== "root";
|
||||
|
||||
if (note.hasLabel("workspace") && !isHoistedNote) {
|
||||
const $enterWorkspaceButton = $(`<span class="tree-item-button tn-icon enter-workspace-button bx bx-door-open" title="${t("note_tree.hoist-this-note-workspace")}"></span>`).on(
|
||||
"click",
|
||||
cancelClickPropagation
|
||||
);
|
||||
|
||||
$span.append($enterWorkspaceButton);
|
||||
}
|
||||
|
||||
if (note.type === "search") {
|
||||
const $refreshSearchButton = $(`<span class="tree-item-button tn-icon refresh-search-button bx bx-refresh" title="${t("note_tree.refresh-saved-search-results")}"></span>`).on(
|
||||
"click",
|
||||
cancelClickPropagation
|
||||
);
|
||||
|
||||
$span.append($refreshSearchButton);
|
||||
}
|
||||
|
||||
// TODO: Deduplicate with server's notes.ts#getAndValidateParent
|
||||
if (!["search", "launcher"].includes(note.type)
|
||||
&& !note.isOptions()
|
||||
&& !note.isLaunchBarConfig()
|
||||
&& !note.noteId.startsWith("_help")
|
||||
) {
|
||||
const $createChildNoteButton = $(`<span class="tree-item-button tn-icon add-note-button bx bx-plus" title="${t("note_tree.create-child-note")}"></span>`).on(
|
||||
"click",
|
||||
cancelClickPropagation
|
||||
);
|
||||
|
||||
$span.append($createChildNoteButton);
|
||||
}
|
||||
|
||||
if (isHoistedNote) {
|
||||
const $unhoistButton = $(`<span class="tree-item-button tn-icon unhoist-button bx bx-door-open" title="${t("note_tree.unhoist")}"></span>`).on("click", cancelClickPropagation);
|
||||
|
||||
$span.append($unhoistButton);
|
||||
}
|
||||
},
|
||||
enhanceTitle: buildEnhanceTitle(),
|
||||
// this is done to automatically lazy load all expanded notes after tree load
|
||||
loadChildren: (event, data) => {
|
||||
data.node.visit((subNode) => {
|
||||
@@ -774,6 +714,23 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
childBranches = childBranches.slice(0, MAX_SEARCH_RESULTS_IN_TREE);
|
||||
}
|
||||
|
||||
if (parentNote.isLabelTruthy("subtreeHidden")) {
|
||||
// If we have a spotlighted note path, show only the child that leads to it
|
||||
if (this.spotlightedNotePath) {
|
||||
const spotlightPathSegments = this.spotlightedNotePath.split('/');
|
||||
const parentIndex = spotlightPathSegments.indexOf(parentNote.noteId);
|
||||
|
||||
if (parentIndex >= 0 && parentIndex < spotlightPathSegments.length - 1) {
|
||||
const nextNoteIdInPath = spotlightPathSegments[parentIndex + 1];
|
||||
childBranches = childBranches.filter(branch => branch.noteId === nextNoteIdInPath);
|
||||
} else {
|
||||
childBranches = [];
|
||||
}
|
||||
} else {
|
||||
childBranches = [];
|
||||
}
|
||||
}
|
||||
|
||||
for (const branch of childBranches) {
|
||||
if (hideArchivedNotes) {
|
||||
const note = branch.getNoteFromCache();
|
||||
@@ -845,6 +802,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
lazy: true,
|
||||
folder: isFolder,
|
||||
expanded: !!branch.isExpanded && note.type !== "search",
|
||||
subtreeHidden: note.isLabelTruthy("subtreeHidden"),
|
||||
key: utils.randomString(12) // this should prevent some "duplicate key" errors
|
||||
};
|
||||
|
||||
@@ -903,6 +861,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
extraClasses.push(...["tinted", colorClass]);
|
||||
}
|
||||
|
||||
if (this.spotlightedNotePath && this.spotlightedNotePath.endsWith(`/${note.noteId}`)) {
|
||||
extraClasses.push("spotlighted-node");
|
||||
}
|
||||
|
||||
return extraClasses.join(" ");
|
||||
}
|
||||
|
||||
@@ -1053,18 +1015,43 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
foundChildNode = this.findChildNode(parentNode, childNoteId);
|
||||
|
||||
if (!foundChildNode) {
|
||||
if (logErrors) {
|
||||
// besides real errors, this can be also caused by hiding of e.g. included images
|
||||
// these are real notes with real notePath, user can display them in a detail,
|
||||
// but they don't have a node in the tree
|
||||
const childNote = await froca.getNote(childNoteId);
|
||||
|
||||
const childNote = await froca.getNote(childNoteId);
|
||||
if (childNote?.type === "image") return;
|
||||
|
||||
if (!childNote || childNote.type !== "image") {
|
||||
ws.logError(
|
||||
`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`
|
||||
);
|
||||
// The child note can be part of a note with #subtreeHidden, case in which we need to "spotlight" it.
|
||||
const parentNote = froca.getNoteFromCache(parentNode.data.noteId);
|
||||
if (parentNote?.isLabelTruthy("subtreeHidden")) {
|
||||
// Enable spotlight mode and reload the parent to show only the path to this note
|
||||
this.spotlightedNotePath = notePath;
|
||||
await parentNode.load(true);
|
||||
|
||||
// Try to find the child again after reload
|
||||
foundChildNode = this.findChildNode(parentNode, childNoteId);
|
||||
this.spotlightedNode = foundChildNode ?? null;
|
||||
|
||||
if (!foundChildNode) {
|
||||
if (logErrors || !childNote) {
|
||||
ws.logError(
|
||||
`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
parentNode = foundChildNode;
|
||||
continue;
|
||||
}
|
||||
|
||||
// besides real errors, this can be also caused by hiding of e.g. included images
|
||||
// these are real notes with real notePath, user can display them in a detail,
|
||||
// but they don't have a node in the tree
|
||||
if (logErrors || !childNote) {
|
||||
ws.logError(
|
||||
`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -1079,7 +1066,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
findChildNode(parentNode: Fancytree.FancytreeNode, childNoteId: string) {
|
||||
return parentNode.getChildren().find((childNode) => childNode.data.noteId === childNoteId);
|
||||
return parentNode.getChildren()?.find((childNode) => childNode.data.noteId === childNoteId);
|
||||
}
|
||||
|
||||
async expandToNote(notePath: string, logErrors = true) {
|
||||
@@ -1120,12 +1107,20 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
(!treeService.isNotePathInHiddenSubtree(this.noteContext.notePath) || (await hoistedNoteService.isHoistedInHiddenSubtree())) &&
|
||||
(await this.getNodeFromPath(this.noteContext.notePath));
|
||||
|
||||
if (this.spotlightedNode && newActiveNode !== this.spotlightedNode) {
|
||||
// Can get removed when switching to another note in a spotlighted subtree.
|
||||
if (this.spotlightedNode.parent) {
|
||||
this.spotlightedNode.remove();
|
||||
}
|
||||
this.spotlightedNode = null;
|
||||
this.spotlightedNotePath = null;
|
||||
}
|
||||
|
||||
if (newActiveNode !== oldActiveNode) {
|
||||
let oldActiveNodeFocused = false;
|
||||
|
||||
if (oldActiveNode) {
|
||||
oldActiveNodeFocused = oldActiveNode.hasFocus();
|
||||
|
||||
oldActiveNode.setActive(false);
|
||||
oldActiveNode.setFocus(false);
|
||||
}
|
||||
@@ -1228,10 +1223,18 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
const { movedActiveNode, parentsOfAddedNodes } = await this.#processBranchRows(branchRows, refreshCtx);
|
||||
|
||||
for (const noteId of loadResults.getNoteIds()) {
|
||||
const contentReloaded = loadResults.isNoteContentReloaded(noteId);
|
||||
if (contentReloaded && !loadResults.isNoteReloaded(noteId, contentReloaded.componentId)) {
|
||||
// Only the note content was reloaded, not the note itself. This would cause a redundant update on every few seconds while editing a note.
|
||||
continue;
|
||||
}
|
||||
|
||||
refreshCtx.noteIdsToUpdate.add(noteId);
|
||||
}
|
||||
|
||||
await this.#executeTreeUpdates(refreshCtx, loadResults);
|
||||
if (refreshCtx.noteIdsToUpdate.size + refreshCtx.noteIdsToReload.size > 0) {
|
||||
await this.#executeTreeUpdates(refreshCtx, loadResults);
|
||||
}
|
||||
|
||||
await this.#setActiveNode(activeNotePath, activeNodeFocused, movedActiveNode, parentsOfAddedNodes);
|
||||
|
||||
@@ -1251,7 +1254,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
} else {
|
||||
refreshCtx.noteIdsToUpdate.add(attrRow.noteId);
|
||||
}
|
||||
} else if (attrRow.type === "label" && attrRow.name === "archived" && attrRow.noteId) {
|
||||
} else if (attrRow.type === "label" && (attrRow.name === "archived" || attrRow.name === "subtreeHidden") && attrRow.noteId) {
|
||||
const note = froca.getNoteFromCache(attrRow.noteId);
|
||||
|
||||
if (note) {
|
||||
@@ -1336,18 +1339,34 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
} else if (frocaBranch) {
|
||||
// make sure it's loaded
|
||||
// we're forcing lazy since it's not clear if the whole required subtree is in froca
|
||||
const newNode = this.prepareNode(frocaBranch, true);
|
||||
if (newNode) {
|
||||
parentNode.addChildren([newNode]);
|
||||
}
|
||||
if (!parentNode.data.subtreeHidden) {
|
||||
const newNode = this.prepareNode(frocaBranch, true);
|
||||
if (newNode) {
|
||||
parentNode.addChildren([newNode]);
|
||||
}
|
||||
|
||||
if (frocaBranch?.isExpanded && note && note.hasChildren()) {
|
||||
refreshCtx.noteIdsToReload.add(frocaBranch.noteId);
|
||||
}
|
||||
if (frocaBranch?.isExpanded && note && note.hasChildren()) {
|
||||
refreshCtx.noteIdsToReload.add(frocaBranch.noteId);
|
||||
}
|
||||
|
||||
this.sortChildren(parentNode);
|
||||
this.sortChildren(parentNode);
|
||||
} else if (branchRow.componentId === this.componentId) {
|
||||
// Display the toast and focus to parent note only if we know for sure that the operation comes from the tree.
|
||||
const parentNote = froca.getNoteFromCache(parentNode.data.noteId || "");
|
||||
toastService.showPersistent({
|
||||
id: `subtree-hidden-moved`,
|
||||
title: t("note_tree.subtree-hidden-moved-title", { title: parentNote?.title }),
|
||||
message: parentNote?.type === "book"
|
||||
? t("note_tree.subtree-hidden-moved-description-collection")
|
||||
: t("note_tree.subtree-hidden-moved-description-other"),
|
||||
icon: "bx bx-hide",
|
||||
timeout: 5_000,
|
||||
});
|
||||
parentNode.setActive(true);
|
||||
}
|
||||
|
||||
// this might be a first child which would force an icon change
|
||||
// also update the count if the subtree is hidden.
|
||||
if (branchRow.parentNoteId) {
|
||||
refreshCtx.noteIdsToUpdate.add(branchRow.parentNoteId);
|
||||
}
|
||||
@@ -1363,7 +1382,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
async #executeTreeUpdates(refreshCtx: RefreshContext, loadResults: LoadResults) {
|
||||
await this.batchUpdate(async () => {
|
||||
const performUpdates = async () => {
|
||||
for (const noteId of refreshCtx.noteIdsToReload) {
|
||||
for (const node of this.getNodesByNoteId(noteId)) {
|
||||
await node.load(true);
|
||||
@@ -1379,7 +1398,19 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (refreshCtx.noteIdsToReload.size + refreshCtx.noteIdsToUpdate.size >= BATCH_UPDATE_THRESHOLD) {
|
||||
/**
|
||||
* Batch updates are used for large number of updates to prevent multiple re-renders, however in the context of small updates (such as changing a note title)
|
||||
* it can cause up to 400ms of delay for ~8k notes which is not acceptable. Therefore we use batching only for larger number of updates.
|
||||
* Without batching, the updates would take a couple of milliseconds.
|
||||
* We still keep the batching for potential cases where there are many updates, for example in a sync.
|
||||
*/
|
||||
await this.batchUpdate(performUpdates);
|
||||
} else {
|
||||
await performUpdates();
|
||||
}
|
||||
|
||||
// for some reason, node update cannot be in the batchUpdate() block (node is not re-rendered)
|
||||
for (const noteId of refreshCtx.noteIdsToUpdate) {
|
||||
@@ -1643,7 +1674,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
const toNode = node.getPrevSibling();
|
||||
|
||||
if (toNode !== null) {
|
||||
branchService.moveToParentNote([node.data.branchId], toNode.data.branchId);
|
||||
branchService.moveToParentNote([node.data.branchId], toNode.data.branchId, this.componentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1780,12 +1811,12 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
#moveLaunchers(selectedOrActiveBranchIds: string[], desktopParent: string, mobileParent: string) {
|
||||
const desktopLaunchersToMove = selectedOrActiveBranchIds.filter((branchId) => !branchId.startsWith("_lbMobile"));
|
||||
if (desktopLaunchersToMove) {
|
||||
branchService.moveToParentNote(desktopLaunchersToMove, `_lbRoot_${ desktopParent}`);
|
||||
branchService.moveToParentNote(desktopLaunchersToMove, `_lbRoot_${ desktopParent}`, this.componentId);
|
||||
}
|
||||
|
||||
const mobileLaunchersToMove = selectedOrActiveBranchIds.filter((branchId) => branchId.startsWith("_lbMobile"));
|
||||
if (mobileLaunchersToMove) {
|
||||
branchService.moveToParentNote(mobileLaunchersToMove, `_lbMobileRoot_${ mobileParent}`);
|
||||
branchService.moveToParentNote(mobileLaunchersToMove, `_lbMobileRoot_${mobileParent}`, this.componentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1853,3 +1884,112 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
function buildEnhanceTitle() {
|
||||
const createChildTemplate = document.createElement("span");
|
||||
createChildTemplate.className = "tree-item-button tn-icon add-note-button bx bx-plus";
|
||||
createChildTemplate.title = t("note_tree.create-child-note");
|
||||
|
||||
return async function enhanceTitle(event: Event,
|
||||
data: {
|
||||
node: Fancytree.FancytreeNode;
|
||||
noteId: string;
|
||||
}) {
|
||||
const node = data.node;
|
||||
|
||||
if (!node.data.noteId) {
|
||||
// if there's "non-note" node, then don't enhance
|
||||
// this can happen for e.g. "Load error!" node
|
||||
return;
|
||||
}
|
||||
|
||||
const note = froca.getNoteFromCache(node.data.noteId);
|
||||
if (!note) return;
|
||||
|
||||
const activeNoteContext = appContext.tabManager.getActiveContext();
|
||||
|
||||
const $span = $(node.span);
|
||||
|
||||
$span.find(".tree-item-button").remove();
|
||||
$span.find(".note-indicator-icon").remove();
|
||||
|
||||
const isHoistedNote = activeNoteContext && activeNoteContext.hoistedNoteId === note.noteId && note.noteId !== "root";
|
||||
|
||||
if (note.hasLabel("workspace") && !isHoistedNote) {
|
||||
const $enterWorkspaceButton = $(`<span class="tree-item-button tn-icon enter-workspace-button bx bx-door-open" title="${t("note_tree.hoist-this-note-workspace")}"></span>`).on(
|
||||
"click",
|
||||
cancelClickPropagation
|
||||
);
|
||||
|
||||
$span.append($enterWorkspaceButton);
|
||||
}
|
||||
|
||||
if (note.type === "search") {
|
||||
const $refreshSearchButton = $(`<span class="tree-item-button tn-icon refresh-search-button bx bx-refresh" title="${t("note_tree.refresh-saved-search-results")}"></span>`).on(
|
||||
"click",
|
||||
cancelClickPropagation
|
||||
);
|
||||
|
||||
$span.append($refreshSearchButton);
|
||||
}
|
||||
|
||||
// TODO: Deduplicate with server's notes.ts#getAndValidateParent
|
||||
const isSubtreeHidden = note.isLabelTruthy("subtreeHidden");
|
||||
if (!["search", "launcher"].includes(note.type)
|
||||
&& !note.isOptions()
|
||||
&& !note.isLaunchBarConfig()
|
||||
&& !note.noteId.startsWith("_help")
|
||||
&& !isSubtreeHidden
|
||||
&& !node.extraClasses.includes("spotlighted-node")
|
||||
) {
|
||||
const createChildItem = createChildTemplate.cloneNode();
|
||||
createChildItem.addEventListener("click", cancelClickPropagation);
|
||||
node.span.append(createChildItem);
|
||||
}
|
||||
|
||||
if (isHoistedNote) {
|
||||
const $unhoistButton = $(`<span class="tree-item-button tn-icon unhoist-button bx bx-door-open" title="${t("note_tree.unhoist")}"></span>`).on("click", cancelClickPropagation);
|
||||
|
||||
$span.append($unhoistButton);
|
||||
}
|
||||
|
||||
// Add clone indicator with tooltip if note has multiple parents
|
||||
const parentNotes = note.getParentNotes();
|
||||
const realParents: FNote[] = [];
|
||||
for (const parent of parentNotes) {
|
||||
if (parent.noteId !== "_share" && parent.noteId !== "_lbBookmarks" && parent.type !== "search") {
|
||||
realParents.push(parent);
|
||||
}
|
||||
}
|
||||
|
||||
if (realParents.length > 1) {
|
||||
const parentTitles = realParents.map((p) => p.title).join(", ");
|
||||
const tooltipText = realParents.length === 2
|
||||
? t("note_tree.clone-indicator-tooltip-single", { parent: realParents[1].title })
|
||||
: t("note_tree.clone-indicator-tooltip", { count: realParents.length, parents: parentTitles });
|
||||
|
||||
const $cloneIndicator = $(`<span class="note-indicator-icon clone-indicator"></span>`);
|
||||
$cloneIndicator.attr("title", tooltipText);
|
||||
$span.find(".fancytree-title").append($cloneIndicator);
|
||||
}
|
||||
|
||||
// Add shared indicator with tooltip if note is shared
|
||||
if (note.isShared()) {
|
||||
const shareId = note.getOwnedLabelValue("shareAlias") || note.noteId;
|
||||
const shareUrl = `${location.origin}${location.pathname}share/${shareId}`;
|
||||
const tooltipText = t("note_tree.shared-indicator-tooltip-with-url", { url: shareUrl });
|
||||
|
||||
const $sharedIndicator = $(`<span class="note-indicator-icon shared-indicator"></span>`);
|
||||
$sharedIndicator.attr("title", tooltipText);
|
||||
$span.find(".fancytree-title").append($sharedIndicator);
|
||||
}
|
||||
|
||||
// Add a badge with the number of items if it hides children.
|
||||
const count = note.getChildNoteIds().length;
|
||||
if (isSubtreeHidden && count > 0) {
|
||||
const $badge = $(`<span class="note-indicator-icon subtree-hidden-badge">${count}</span>`);
|
||||
$badge.attr("title", t("note_tree.subtree-hidden-tooltip", { count }));
|
||||
$span.find(".fancytree-title").append($badge);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
*/
|
||||
|
||||
import { NoteType } from "@triliumnext/commons";
|
||||
import { VNode, type JSX } from "preact";
|
||||
import { type JSX, VNode } from "preact";
|
||||
|
||||
import { TypeWidgetProps } from "./type_widgets/type_widget";
|
||||
|
||||
/**
|
||||
@@ -13,7 +14,7 @@ import { TypeWidgetProps } from "./type_widgets/type_widget";
|
||||
*/
|
||||
export type ExtendedNoteType = Exclude<NoteType, "launcher" | "text" | "code"> | "empty" | "readOnlyCode" | "readOnlyText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession" | "aiChat";
|
||||
|
||||
export type TypeWidget = ((props: TypeWidgetProps) => VNode | JSX.Element);
|
||||
export type TypeWidget = ((props: TypeWidgetProps) => VNode | JSX.Element | undefined);
|
||||
type NoteTypeView = () => (Promise<{ default: TypeWidget } | TypeWidget> | TypeWidget);
|
||||
|
||||
interface NoteTypeMapping {
|
||||
|
||||
@@ -64,7 +64,8 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
|
||||
this.$widget.addClass(utils.getMimeTypeClass(note.mime));
|
||||
this.$widget.addClass(`view-mode-${this.noteContext?.viewScope?.viewMode ?? "default"}`);
|
||||
this.$widget.addClass(note.getColorClass());
|
||||
this.$widget.toggleClass(["bgfx", "options"], note.isOptions());
|
||||
this.$widget.toggleClass("options", note.isOptions());
|
||||
this.$widget.toggleClass("bgfx", this.#hasBackgroundEffects(note));
|
||||
this.$widget.toggleClass("protected", note.isProtected);
|
||||
|
||||
const noteLanguage = note?.getLabelValue("language");
|
||||
@@ -88,6 +89,22 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
|
||||
return !!note?.isLabelTruthy("fullContentWidth");
|
||||
}
|
||||
|
||||
#hasBackgroundEffects(note: FNote): boolean {
|
||||
const MIME_TYPES_WITH_BACKGROUND_EFFECTS = [
|
||||
"application/pdf"
|
||||
]
|
||||
|
||||
if (note.isOptions()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (note.type === "file" && MIME_TYPES_WITH_BACKGROUND_EFFECTS.includes(note.mime)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
// listening on changes of note.type and CSS class
|
||||
const LABELS_CAUSING_REFRESH = ["cssClass", "language", "viewType", "color"];
|
||||
|
||||
@@ -8,7 +8,7 @@ import { MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayou
|
||||
|
||||
import appContext, { EventData, EventNames } from "../../components/app_context";
|
||||
import Component from "../../components/component";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import NoteContext, { NoteContextDataMap } from "../../components/note_context";
|
||||
import FBlob from "../../entities/fblob";
|
||||
import FNote from "../../entities/fnote";
|
||||
import attributes from "../../services/attributes";
|
||||
@@ -19,7 +19,7 @@ import options, { type OptionValue } from "../../services/options";
|
||||
import protected_session_holder from "../../services/protected_session_holder";
|
||||
import server from "../../services/server";
|
||||
import shortcuts, { Handler, removeIndividualBinding } from "../../services/shortcuts";
|
||||
import SpacedUpdate from "../../services/spaced_update";
|
||||
import SpacedUpdate, { type StateCallback } from "../../services/spaced_update";
|
||||
import toast, { ToastOptions } from "../../services/toast";
|
||||
import tree from "../../services/tree";
|
||||
import utils, { escapeRegExp, getErrorMessage, randomString, reloadFrontendApp } from "../../services/utils";
|
||||
@@ -63,22 +63,29 @@ export function useTriliumEvents<T extends EventNames>(eventNames: T[], handler:
|
||||
useDebugValue(() => eventNames.join(", "));
|
||||
}
|
||||
|
||||
export function useSpacedUpdate(callback: () => void | Promise<void>, interval = 1000) {
|
||||
export function useSpacedUpdate(callback: () => void | Promise<void>, interval = 1000, stateCallback?: StateCallback) {
|
||||
const callbackRef = useRef(callback);
|
||||
const stateCallbackRef = useRef(stateCallback);
|
||||
const spacedUpdateRef = useRef<SpacedUpdate>(new SpacedUpdate(
|
||||
() => callbackRef.current(),
|
||||
interval
|
||||
interval,
|
||||
(state) => stateCallbackRef.current?.(state)
|
||||
));
|
||||
|
||||
// Update callback ref when it changes
|
||||
useEffect(() => {
|
||||
callbackRef.current = callback;
|
||||
}, [callback]);
|
||||
}, [ callback ]);
|
||||
|
||||
// Update state callback when it changes.
|
||||
useEffect(() => {
|
||||
stateCallbackRef.current = stateCallback;
|
||||
}, [ stateCallback ]);
|
||||
|
||||
// Update interval if it changes
|
||||
useEffect(() => {
|
||||
spacedUpdateRef.current?.setUpdateInterval(interval);
|
||||
}, [interval]);
|
||||
}, [ interval ]);
|
||||
|
||||
return spacedUpdateRef.current;
|
||||
}
|
||||
@@ -121,7 +128,12 @@ export function useEditorSpacedUpdate({ note, noteType, noteContext, getData, on
|
||||
dataSaved?.(data);
|
||||
};
|
||||
}, [ note, getData, dataSaved, noteType, parentComponent ]);
|
||||
const spacedUpdate = useSpacedUpdate(callback);
|
||||
const stateCallback = useCallback<StateCallback>((state) => {
|
||||
noteContext?.setContextData("saveState", {
|
||||
state
|
||||
});
|
||||
}, [ noteContext ]);
|
||||
const spacedUpdate = useSpacedUpdate(callback, updateInterval, stateCallback);
|
||||
|
||||
// React to note/blob changes.
|
||||
useEffect(() => {
|
||||
@@ -158,6 +170,73 @@ export function useEditorSpacedUpdate({ note, noteType, noteContext, getData, on
|
||||
return spacedUpdate;
|
||||
}
|
||||
|
||||
export function useBlobEditorSpacedUpdate({ note, noteType, noteContext, getData, onContentChange, dataSaved, updateInterval, replaceWithoutRevision }: {
|
||||
noteType: NoteType;
|
||||
note: FNote,
|
||||
noteContext: NoteContext | null | undefined,
|
||||
getData: () => Promise<Blob | undefined> | Blob | undefined,
|
||||
onContentChange: (newBlob: FBlob) => void,
|
||||
dataSaved?: (savedData: Blob) => void,
|
||||
updateInterval?: number;
|
||||
/** If set to true, then the blob is replaced directly without saving a revision before. */
|
||||
replaceWithoutRevision?: boolean;
|
||||
}) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const blob = useNoteBlob(note, parentComponent?.componentId);
|
||||
|
||||
const callback = useMemo(() => {
|
||||
return async () => {
|
||||
const data = await getData();
|
||||
|
||||
// for read only notes
|
||||
if (data === undefined || note.type !== noteType) return;
|
||||
|
||||
protected_session_holder.touchProtectedSessionIfNecessary(note);
|
||||
await server.upload(`notes/${note.noteId}/file?replace=${replaceWithoutRevision ? "1" : "0"}`, new File([ data ], note.title, { type: note.mime }), parentComponent?.componentId);
|
||||
dataSaved?.(data);
|
||||
};
|
||||
}, [ note, getData, dataSaved, noteType, parentComponent, replaceWithoutRevision ]);
|
||||
const stateCallback = useCallback<StateCallback>((state) => {
|
||||
noteContext?.setContextData("saveState", {
|
||||
state
|
||||
});
|
||||
}, [ noteContext ]);
|
||||
const spacedUpdate = useSpacedUpdate(callback, updateInterval, stateCallback);
|
||||
|
||||
// React to note/blob changes.
|
||||
useEffect(() => {
|
||||
if (!blob) return;
|
||||
spacedUpdate.allowUpdateWithoutChange(() => onContentChange(blob));
|
||||
}, [ blob ]);
|
||||
|
||||
// React to update interval changes.
|
||||
useEffect(() => {
|
||||
if (!updateInterval) return;
|
||||
spacedUpdate.setUpdateInterval(updateInterval);
|
||||
}, [ updateInterval ]);
|
||||
|
||||
// Save if needed upon switching tabs.
|
||||
useTriliumEvent("beforeNoteSwitch", async ({ noteContext: eventNoteContext }) => {
|
||||
if (eventNoteContext.ntxId !== noteContext?.ntxId) return;
|
||||
await spacedUpdate.updateNowIfNecessary();
|
||||
});
|
||||
|
||||
// Save if needed upon tab closing.
|
||||
useTriliumEvent("beforeNoteContextRemove", async ({ ntxIds }) => {
|
||||
if (!noteContext?.ntxId || !ntxIds.includes(noteContext.ntxId)) return;
|
||||
await spacedUpdate.updateNowIfNecessary();
|
||||
});
|
||||
|
||||
// Save if needed upon window/browser closing.
|
||||
useEffect(() => {
|
||||
const listener = () => spacedUpdate.isAllSavedAndTriggerUpdate();
|
||||
appContext.addBeforeUnloadListener(listener);
|
||||
return () => appContext.removeBeforeUnloadListener(listener);
|
||||
}, []);
|
||||
|
||||
return spacedUpdate;
|
||||
}
|
||||
|
||||
export function useNoteSavedData(noteId: string | undefined) {
|
||||
return useSyncExternalStore(
|
||||
(cb) => noteId ? noteSavedDataStore.subscribe(noteId, cb) : () => {},
|
||||
@@ -567,17 +646,13 @@ export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: F
|
||||
|
||||
const setter = useCallback((value: boolean) => {
|
||||
if (note) {
|
||||
if (value) {
|
||||
attributes.setLabel(note.noteId, labelName, "");
|
||||
} else {
|
||||
attributes.removeOwnedLabelByName(note, labelName);
|
||||
}
|
||||
attributes.setBooleanWithInheritance(note, labelName, value);
|
||||
}
|
||||
}, [note]);
|
||||
}, [note, labelName]);
|
||||
|
||||
useDebugValue(labelName);
|
||||
|
||||
const labelValue = !!note?.hasLabel(labelName);
|
||||
const labelValue = !!note?.isLabelTruthy(labelName);
|
||||
return [ labelValue, setter ] as const;
|
||||
}
|
||||
|
||||
@@ -634,7 +709,8 @@ export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
|
||||
// Render the widget once.
|
||||
// Render the widget once - note that noteContext is intentionally NOT a dependency
|
||||
// to prevent creating new widget instances on every note switch.
|
||||
const [ widget, renderedWidget ] = useMemo(() => {
|
||||
const widget = widgetFactory();
|
||||
|
||||
@@ -642,14 +718,21 @@ export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, {
|
||||
parentComponent.child(widget);
|
||||
}
|
||||
|
||||
if (noteContext && widget instanceof NoteContextAwareWidget) {
|
||||
widget.setNoteContextEvent({ noteContext });
|
||||
}
|
||||
|
||||
const renderedWidget = widget.render();
|
||||
return [ widget, renderedWidget ];
|
||||
}, [ noteContext, parentComponent ]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
// widgetFactory() is intentionally left out
|
||||
}, [ parentComponent ]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
// widgetFactory() and noteContext are intentionally left out - widget should be created once
|
||||
// and updated via activeContextChangedEvent when noteContext changes.
|
||||
|
||||
// Cleanup: remove widget from parent's children when unmounted
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (parentComponent) {
|
||||
parentComponent.removeChild(widget);
|
||||
}
|
||||
widget.cleanup();
|
||||
};
|
||||
}, [ parentComponent, widget ]);
|
||||
|
||||
// Attach the widget to the parent.
|
||||
useEffect(() => {
|
||||
@@ -660,10 +743,17 @@ export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, {
|
||||
}
|
||||
}, [ renderedWidget ]);
|
||||
|
||||
// Inject the note context.
|
||||
// Inject the note context - this updates the existing widget without recreating it.
|
||||
// We check if the context actually changed to avoid double refresh when the event system
|
||||
// also delivers activeContextChanged to the widget through component tree propagation.
|
||||
useEffect(() => {
|
||||
if (noteContext && widget instanceof NoteContextAwareWidget) {
|
||||
widget.activeContextChangedEvent({ noteContext });
|
||||
// Only trigger refresh if the context actually changed.
|
||||
// The event system may have already updated the widget, in which case
|
||||
// widget.noteContext will already equal noteContext.
|
||||
if (widget.noteContext !== noteContext) {
|
||||
widget.activeContextChangedEvent({ noteContext });
|
||||
}
|
||||
}
|
||||
}, [ noteContext, widget ]);
|
||||
|
||||
@@ -1192,3 +1282,92 @@ export function useContentElement(noteContext: NoteContext | null | undefined) {
|
||||
|
||||
return contentElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set context data on the current note context.
|
||||
* This allows type widgets to publish data (e.g., table of contents, PDF pages)
|
||||
* that can be consumed by sidebar/toolbar components.
|
||||
*
|
||||
* Data is automatically cleared when navigating to a different note.
|
||||
*
|
||||
* @param key - Unique identifier for the data type (e.g., "toc", "pdfPages")
|
||||
* @param value - The data to publish
|
||||
*
|
||||
* @example
|
||||
* // In a PDF viewer widget:
|
||||
* const { noteContext } = useActiveNoteContext();
|
||||
* useSetContextData(noteContext, "pdfPages", pages);
|
||||
*/
|
||||
export function useSetContextData<K extends keyof NoteContextDataMap>(
|
||||
noteContext: NoteContext | null | undefined,
|
||||
key: K,
|
||||
value: NoteContextDataMap[K] | undefined
|
||||
) {
|
||||
useEffect(() => {
|
||||
if (!noteContext) return;
|
||||
|
||||
if (value !== undefined) {
|
||||
noteContext.setContextData(key, value);
|
||||
} else {
|
||||
noteContext.clearContextData(key);
|
||||
}
|
||||
|
||||
return () => {
|
||||
noteContext.clearContextData(key);
|
||||
};
|
||||
}, [noteContext, key, value]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context data from the active note context.
|
||||
* This is typically used in sidebar/toolbar components that need to display
|
||||
* data published by type widgets.
|
||||
*
|
||||
* The component will automatically re-render when the data changes.
|
||||
*
|
||||
* @param key - The data key to retrieve (e.g., "toc", "pdfPages")
|
||||
* @returns The current data, or undefined if not available
|
||||
*
|
||||
* @example
|
||||
* // In a Table of Contents sidebar widget:
|
||||
* function TableOfContents() {
|
||||
* const headings = useGetContextData<Heading[]>("toc");
|
||||
* if (!headings) return <div>No headings available</div>;
|
||||
* return <ul>{headings.map(h => <li>{h.text}</li>)}</ul>;
|
||||
* }
|
||||
*/
|
||||
export function useGetContextData<K extends keyof NoteContextDataMap>(key: K): NoteContextDataMap[K] | undefined {
|
||||
const { noteContext } = useActiveNoteContext();
|
||||
return useGetContextDataFrom(noteContext, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context data from a specific note context (not necessarily the active one).
|
||||
*
|
||||
* @param noteContext - The specific note context to get data from
|
||||
* @param key - The data key to retrieve
|
||||
* @returns The current data, or undefined if not available
|
||||
*/
|
||||
export function useGetContextDataFrom<K extends keyof NoteContextDataMap>(
|
||||
noteContext: NoteContext | null | undefined,
|
||||
key: K
|
||||
): NoteContextDataMap[K] | undefined {
|
||||
const [data, setData] = useState<NoteContextDataMap[K] | undefined>(() =>
|
||||
noteContext?.getContextData(key)
|
||||
);
|
||||
|
||||
// Update initial value when noteContext changes
|
||||
useEffect(() => {
|
||||
setData(noteContext?.getContextData(key));
|
||||
}, [noteContext, key]);
|
||||
|
||||
// Subscribe to changes via Trilium event system
|
||||
useTriliumEvent("contextDataChanged", ({ noteContext: eventNoteContext, key: changedKey, value }) => {
|
||||
if (eventNoteContext === noteContext && changedKey === key) {
|
||||
setData(value as NoteContextDataMap[K]);
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import { t } from "../../services/i18n";
|
||||
import { downloadFileNote, openNoteExternally } from "../../services/open";
|
||||
@@ -8,11 +10,14 @@ import { formatSize } from "../../services/utils";
|
||||
import Button from "../react/Button";
|
||||
import { FormFileUploadButton } from "../react/FormFileUpload";
|
||||
import { useNoteBlob, useNoteLabel } from "../react/hooks";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
|
||||
export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
|
||||
export default function FilePropertiesTab({ note, ntxId }: Pick<TabContext, "note" | "ntxId">) {
|
||||
const [ originalFileName ] = useNoteLabel(note, "originalFileName");
|
||||
const canAccessProtectedNote = !note?.isProtected || protected_session_holder.isProtectedSessionAvailable();
|
||||
const blob = useNoteBlob(note);
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
|
||||
return (
|
||||
<div className="file-properties-widget">
|
||||
@@ -40,7 +45,7 @@ export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
|
||||
text={t("file_properties.download")}
|
||||
primary
|
||||
disabled={!canAccessProtectedNote}
|
||||
onClick={() => downloadFileNote(note.noteId)}
|
||||
onClick={() => downloadFileNote(note, parentComponent, ntxId)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function ImagePropertiesTab({ note, ntxId }: TabContext) {
|
||||
text={t("image_properties.download")}
|
||||
icon="bx bx-download"
|
||||
primary
|
||||
onClick={() => downloadFileNote(note.noteId)}
|
||||
onClick={() => downloadFileNote(note, parentComponent, ntxId)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -135,13 +135,13 @@ function OpenExternallyButton({ note, noteMime }: NoteActionsCustomInnerProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function DownloadFileButton({ note }: NoteActionsCustomInnerProps) {
|
||||
function DownloadFileButton({ note, parentComponent, ntxId }: NoteActionsCustomInnerProps) {
|
||||
return (
|
||||
<ActionButton
|
||||
icon="bx bx-download"
|
||||
text={t("file_properties.download")}
|
||||
disabled={!note.isContentAvailable()}
|
||||
onClick={() => downloadFileNote(note.noteId)}
|
||||
onClick={() => downloadFileNote(note, parentComponent, ntxId)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import "./RightPanelContainer.css";
|
||||
|
||||
import Split from "@triliumnext/split.js";
|
||||
import { VNode } from "preact";
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
import { useState, useEffect, useRef, useCallback } from "preact/hooks";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import { WidgetsByParent } from "../../services/bundle";
|
||||
@@ -11,10 +11,13 @@ import { t } from "../../services/i18n";
|
||||
import options from "../../services/options";
|
||||
import { DEFAULT_GUTTER_SIZE } from "../../services/resizer";
|
||||
import Button from "../react/Button";
|
||||
import { useActiveNoteContext, useLegacyWidget, useNoteProperty, useTriliumEvent, useTriliumOptionBool, useTriliumOptionJson } from "../react/hooks";
|
||||
import { useActiveNoteContext, useLegacyWidget, useNoteProperty, useTriliumEvent, useTriliumOptionJson } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import LegacyRightPanelWidget from "../right_panel_widget";
|
||||
import HighlightsList from "./HighlightsList";
|
||||
import PdfAttachments from "./pdf/PdfAttachments";
|
||||
import PdfLayers from "./pdf/PdfLayers";
|
||||
import PdfPages from "./pdf/PdfPages";
|
||||
import RightPanelWidget from "./RightPanelWidget";
|
||||
import TableOfContents from "./TableOfContents";
|
||||
|
||||
@@ -27,12 +30,16 @@ interface RightPanelWidgetDefinition {
|
||||
}
|
||||
|
||||
export default function RightPanelContainer({ widgetsByParent }: { widgetsByParent: WidgetsByParent }) {
|
||||
const [ rightPaneVisible, setRightPaneVisible ] = useTriliumOptionBool("rightPaneVisible");
|
||||
const [ rightPaneVisible, setRightPaneVisible ] = useState(options.is("rightPaneVisible"));
|
||||
const items = useItems(rightPaneVisible, widgetsByParent);
|
||||
useSplit(rightPaneVisible);
|
||||
useTriliumEvent("toggleRightPane", () => {
|
||||
setRightPaneVisible(!rightPaneVisible);
|
||||
});
|
||||
useTriliumEvent("toggleRightPane", useCallback(() => {
|
||||
setRightPaneVisible(current => {
|
||||
const newValue = !current;
|
||||
options.save("rightPaneVisible", newValue.toString());
|
||||
return newValue;
|
||||
});
|
||||
}, []));
|
||||
|
||||
return (
|
||||
<div id="right-pane">
|
||||
@@ -45,7 +52,7 @@ export default function RightPanelContainer({ widgetsByParent }: { widgetsByPare
|
||||
{t("right_pane.empty_message")}
|
||||
<Button
|
||||
text={t("right_pane.empty_button")}
|
||||
onClick={() => setRightPaneVisible(!rightPaneVisible)}
|
||||
triggerCommand="toggleRightPane"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -57,13 +64,27 @@ export default function RightPanelContainer({ widgetsByParent }: { widgetsByPare
|
||||
function useItems(rightPaneVisible: boolean, widgetsByParent: WidgetsByParent) {
|
||||
const { note } = useActiveNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const noteMime = useNoteProperty(note, "mime");
|
||||
const [ highlightsList ] = useTriliumOptionJson<string[]>("highlightsList");
|
||||
const isPdf = noteType === "file" && noteMime === "application/pdf";
|
||||
|
||||
if (!rightPaneVisible) return [];
|
||||
const definitions: RightPanelWidgetDefinition[] = [
|
||||
{
|
||||
el: <TableOfContents />,
|
||||
enabled: (noteType === "text" || noteType === "doc"),
|
||||
enabled: (noteType === "text" || noteType === "doc" || isPdf),
|
||||
},
|
||||
{
|
||||
el: <PdfPages />,
|
||||
enabled: isPdf,
|
||||
},
|
||||
{
|
||||
el: <PdfAttachments />,
|
||||
enabled: isPdf,
|
||||
},
|
||||
{
|
||||
el: <PdfLayers />,
|
||||
enabled: isPdf,
|
||||
},
|
||||
{
|
||||
el: <HighlightsList />,
|
||||
|
||||
@@ -29,6 +29,11 @@
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.toc li.active > .item-content {
|
||||
font-weight: bold;
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.toc > ol {
|
||||
--toc-depth-level: 1;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ import "./TableOfContents.css";
|
||||
|
||||
import { CKTextEditor, ModelElement } from "@triliumnext/ckeditor5";
|
||||
import clsx from "clsx";
|
||||
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { t } from "../../services/i18n";
|
||||
import math from "../../services/math";
|
||||
import { randomString } from "../../services/utils";
|
||||
import { useActiveNoteContext, useContentElement, useIsNoteReadOnly, useNoteProperty, useTextEditor } from "../react/hooks";
|
||||
import { useActiveNoteContext, useContentElement, useGetContextData, useIsNoteReadOnly, useNoteProperty, useTextEditor } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import RawHtml from "../react/RawHtml";
|
||||
import RightPanelWidget from "./RightPanelWidget";
|
||||
|
||||
//#region Generic impl.
|
||||
@@ -21,29 +23,50 @@ interface HeadingsWithNesting extends RawHeading {
|
||||
children: HeadingsWithNesting[];
|
||||
}
|
||||
|
||||
export interface HeadingContext {
|
||||
scrollToHeading(heading: RawHeading): void;
|
||||
headings: RawHeading[];
|
||||
activeHeadingId?: string | null;
|
||||
}
|
||||
|
||||
export default function TableOfContents() {
|
||||
const { note, noteContext } = useActiveNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const noteMime = useNoteProperty(note, "mime");
|
||||
const { isReadOnly } = useIsNoteReadOnly(note, noteContext);
|
||||
|
||||
return (
|
||||
<RightPanelWidget id="toc" title={t("toc.table_of_contents")} grow>
|
||||
{((noteType === "text" && isReadOnly) || (noteType === "doc")) && <ReadOnlyTextTableOfContents />}
|
||||
{noteType === "text" && !isReadOnly && <EditableTextTableOfContents />}
|
||||
{noteType === "file" && noteMime === "application/pdf" && <PdfTableOfContents />}
|
||||
</RightPanelWidget>
|
||||
);
|
||||
}
|
||||
|
||||
function AbstractTableOfContents<T extends RawHeading>({ headings, scrollToHeading }: {
|
||||
function PdfTableOfContents() {
|
||||
const data = useGetContextData("toc");
|
||||
|
||||
return (
|
||||
<AbstractTableOfContents
|
||||
headings={data?.headings || []}
|
||||
scrollToHeading={data?.scrollToHeading || (() => {})}
|
||||
activeHeadingId={data?.activeHeadingId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AbstractTableOfContents<T extends RawHeading>({ headings, scrollToHeading, activeHeadingId }: {
|
||||
headings: T[];
|
||||
scrollToHeading(heading: T): void;
|
||||
activeHeadingId?: string | null;
|
||||
}) {
|
||||
const nestedHeadings = buildHeadingTree(headings);
|
||||
return (
|
||||
<span className="toc">
|
||||
{nestedHeadings.length > 0 ? (
|
||||
<ol>
|
||||
{nestedHeadings.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} />)}
|
||||
{nestedHeadings.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} activeHeadingId={activeHeadingId} />)}
|
||||
</ol>
|
||||
) : (
|
||||
<div className="no-headings">{t("toc.no_headings")}</div>
|
||||
@@ -52,14 +75,32 @@ function AbstractTableOfContents<T extends RawHeading>({ headings, scrollToHeadi
|
||||
);
|
||||
}
|
||||
|
||||
function TableOfContentsHeading({ heading, scrollToHeading }: {
|
||||
function TableOfContentsHeading({ heading, scrollToHeading, activeHeadingId }: {
|
||||
heading: HeadingsWithNesting;
|
||||
scrollToHeading(heading: RawHeading): void;
|
||||
activeHeadingId?: string | null;
|
||||
}) {
|
||||
const [ collapsed, setCollapsed ] = useState(false);
|
||||
const isActive = heading.id === activeHeadingId;
|
||||
const contentRef = useRef<HTMLElement>(null);
|
||||
|
||||
// Render math equations after component mounts/updates
|
||||
useEffect(() => {
|
||||
if (!contentRef.current) return;
|
||||
const mathElements = contentRef.current.querySelectorAll(".ck-math-tex");
|
||||
|
||||
for (const mathEl of mathElements ?? []) {
|
||||
try {
|
||||
math.render(mathEl.textContent || "", mathEl as HTMLElement);
|
||||
} catch (e) {
|
||||
console.warn("Failed to render math in TOC:", e);
|
||||
}
|
||||
}
|
||||
}, [heading.text]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<li className={clsx(collapsed && "collapsed")}>
|
||||
<li className={clsx(collapsed && "collapsed", isActive && "active")}>
|
||||
{heading.children.length > 0 && (
|
||||
<Icon
|
||||
className="collapse-button"
|
||||
@@ -67,14 +108,16 @@ function TableOfContentsHeading({ heading, scrollToHeading }: {
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
<RawHtml
|
||||
containerRef={contentRef}
|
||||
className="item-content"
|
||||
onClick={() => scrollToHeading(heading)}
|
||||
>{heading.text}</span>
|
||||
html={heading.text}
|
||||
/>
|
||||
</li>
|
||||
{heading.children && (
|
||||
{heading.children.length > 0 && (
|
||||
<ol>
|
||||
{heading.children.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} />)}
|
||||
{heading.children.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} activeHeadingId={activeHeadingId} />)}
|
||||
</ol>
|
||||
)}
|
||||
</>
|
||||
@@ -166,9 +209,23 @@ function extractTocFromTextEditor(editor: CKTextEditor) {
|
||||
if (type !== "elementStart" || !item.is('element') || !item.name.startsWith('heading')) continue;
|
||||
|
||||
const level = Number(item.name.replace( 'heading', '' ));
|
||||
const text = Array.from( item.getChildren() )
|
||||
.map( c => c.is( '$text' ) ? c.data : '' )
|
||||
.join( '' );
|
||||
|
||||
// Convert model element to view, then to DOM to get HTML
|
||||
const viewEl = editor.editing.mapper.toViewElement(item);
|
||||
let text = '';
|
||||
if (viewEl) {
|
||||
const domEl = editor.editing.view.domConverter.mapViewToDom(viewEl);
|
||||
if (domEl instanceof HTMLElement) {
|
||||
text = domEl.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to plain text if conversion fails
|
||||
if (!text) {
|
||||
text = Array.from( item.getChildren() )
|
||||
.map( c => c.is( '$text' ) ? c.data : '' )
|
||||
.join( '' );
|
||||
}
|
||||
|
||||
// Assign a unique ID
|
||||
let tocId = item.getAttribute(TOC_ID) as string | undefined;
|
||||
|
||||
57
apps/client/src/widgets/sidebar/pdf/PdfAttachments.css
Normal file
57
apps/client/src/widgets/sidebar/pdf/PdfAttachments.css
Normal file
@@ -0,0 +1,57 @@
|
||||
.pdf-attachments-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pdf-attachment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.pdf-attachment-item:hover {
|
||||
background-color: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
.pdf-attachment-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.pdf-attachment-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pdf-attachment-filename {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--main-text-color);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdf-attachment-size {
|
||||
font-size: 11px;
|
||||
color: var(--muted-text-color);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.no-attachments {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.pdf-attachment-item .bx {
|
||||
flex-shrink: 0;
|
||||
font-size: 18px;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.pdf-attachment-item:hover .bx {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
62
apps/client/src/widgets/sidebar/pdf/PdfAttachments.tsx
Normal file
62
apps/client/src/widgets/sidebar/pdf/PdfAttachments.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import "./PdfAttachments.css";
|
||||
|
||||
import { t } from "../../../services/i18n";
|
||||
import { formatSize } from "../../../services/utils";
|
||||
import { useActiveNoteContext, useGetContextData, useNoteProperty } from "../../react/hooks";
|
||||
import Icon from "../../react/Icon";
|
||||
import RightPanelWidget from "../RightPanelWidget";
|
||||
|
||||
interface AttachmentInfo {
|
||||
filename: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export default function PdfAttachments() {
|
||||
const { note } = useActiveNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const noteMime = useNoteProperty(note, "mime");
|
||||
const attachmentsData = useGetContextData("pdfAttachments");
|
||||
|
||||
if (noteType !== "file" || noteMime !== "application/pdf") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!attachmentsData || attachmentsData.attachments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RightPanelWidget id="pdf-attachments" title={t("pdf.attachments", { count: attachmentsData.attachments.length })}>
|
||||
<div className="pdf-attachments-list">
|
||||
{attachmentsData.attachments.map((attachment) => (
|
||||
<PdfAttachmentItem
|
||||
key={attachment.filename}
|
||||
attachment={attachment}
|
||||
onDownload={attachmentsData.downloadAttachment}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</RightPanelWidget>
|
||||
);
|
||||
}
|
||||
|
||||
function PdfAttachmentItem({
|
||||
attachment,
|
||||
onDownload
|
||||
}: {
|
||||
attachment: AttachmentInfo;
|
||||
onDownload: (filename: string) => void;
|
||||
}) {
|
||||
const sizeText = formatSize(attachment.size);
|
||||
|
||||
return (
|
||||
<div className="pdf-attachment-item" onClick={() => onDownload(attachment.filename)}>
|
||||
<Icon icon="bx bx-paperclip" />
|
||||
<div className="pdf-attachment-info">
|
||||
<div className="pdf-attachment-filename">{attachment.filename}</div>
|
||||
<div className="pdf-attachment-size">{sizeText}</div>
|
||||
</div>
|
||||
<Icon icon="bx bx-download" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
54
apps/client/src/widgets/sidebar/pdf/PdfLayers.css
Normal file
54
apps/client/src/widgets/sidebar/pdf/PdfLayers.css
Normal file
@@ -0,0 +1,54 @@
|
||||
.pdf-layers-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pdf-layer-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.pdf-layer-item:hover {
|
||||
background-color: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
.pdf-layer-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.pdf-layer-item.hidden {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.pdf-layer-name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: var(--main-text-color);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.no-layers {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.pdf-layer-item .bx {
|
||||
flex-shrink: 0;
|
||||
font-size: 18px;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.pdf-layer-item:hover .bx {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.pdf-layer-item.visible .bx {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
55
apps/client/src/widgets/sidebar/pdf/PdfLayers.tsx
Normal file
55
apps/client/src/widgets/sidebar/pdf/PdfLayers.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import "./PdfLayers.css";
|
||||
|
||||
import { t } from "../../../services/i18n";
|
||||
import { useActiveNoteContext, useGetContextData, useNoteProperty } from "../../react/hooks";
|
||||
import Icon from "../../react/Icon";
|
||||
import RightPanelWidget from "../RightPanelWidget";
|
||||
|
||||
interface LayerInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export default function PdfLayers() {
|
||||
const { note } = useActiveNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const noteMime = useNoteProperty(note, "mime");
|
||||
const layersData = useGetContextData("pdfLayers");
|
||||
|
||||
if (noteType !== "file" || noteMime !== "application/pdf") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (layersData?.layers && layersData.layers.length > 0 &&
|
||||
<RightPanelWidget id="pdf-layers" title={t("pdf.layers", { count: layersData.layers.length })}>
|
||||
<div className="pdf-layers-list">
|
||||
{layersData.layers.map((layer) => (
|
||||
<PdfLayerItem
|
||||
key={layer.id}
|
||||
layer={layer}
|
||||
onToggle={layersData.toggleLayer}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</RightPanelWidget>
|
||||
);
|
||||
}
|
||||
|
||||
function PdfLayerItem({
|
||||
layer,
|
||||
onToggle
|
||||
}: {
|
||||
layer: LayerInfo;
|
||||
onToggle: (layerId: string, visible: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`pdf-layer-item ${layer.visible ? 'visible' : 'hidden'}`}
|
||||
onClick={() => onToggle(layer.id, !layer.visible)}
|
||||
>
|
||||
<Icon icon={layer.visible ? "bx bx-show" : "bx bx-hide"} />
|
||||
<div className="pdf-layer-name">{layer.name}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
67
apps/client/src/widgets/sidebar/pdf/PdfPages.css
Normal file
67
apps/client/src/widgets/sidebar/pdf/PdfPages.css
Normal file
@@ -0,0 +1,67 @@
|
||||
.pdf-pages-list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
.pdf-page-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.2s;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
|
||||
.pdf-page-number {
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
color: var(--main-text-color);
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: var(--accented-background-color);
|
||||
padding: 0.2em 0.5em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.pdf-page-item:hover {
|
||||
background-color: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
.pdf-page-item.active {
|
||||
border-color: var(--main-border-color);
|
||||
background-color: var(--active-item-background-color);
|
||||
}
|
||||
|
||||
.pdf-page-thumbnail {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pdf-page-thumbnail img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.pdf-page-loading {
|
||||
color: var(--muted-text-color);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.no-pages {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
111
apps/client/src/widgets/sidebar/pdf/PdfPages.tsx
Normal file
111
apps/client/src/widgets/sidebar/pdf/PdfPages.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import "./PdfPages.css";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { NoteContextDataMap } from "../../../components/note_context";
|
||||
import { t } from "../../../services/i18n";
|
||||
import { useActiveNoteContext, useGetContextData, useNoteProperty } from "../../react/hooks";
|
||||
import RightPanelWidget from "../RightPanelWidget";
|
||||
|
||||
export default function PdfPages() {
|
||||
const { note } = useActiveNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const noteMime = useNoteProperty(note, "mime");
|
||||
const pagesData = useGetContextData("pdfPages");
|
||||
|
||||
if (noteType !== "file" || noteMime !== "application/pdf") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (pagesData &&
|
||||
<RightPanelWidget id="pdf-pages" title={t("pdf.pages", { count: pagesData?.totalPages || 0 })} grow>
|
||||
<PdfPagesList key={note?.noteId} pagesData={pagesData} />
|
||||
</RightPanelWidget>
|
||||
);
|
||||
}
|
||||
|
||||
function PdfPagesList({ pagesData }: { pagesData: NoteContextDataMap["pdfPages"] }) {
|
||||
const [thumbnails, setThumbnails] = useState<Map<number, string>>(new Map());
|
||||
const requestedThumbnails = useRef<Set<number>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for thumbnail responses via custom event
|
||||
function handleThumbnail(event: CustomEvent) {
|
||||
const { pageNumber, dataUrl } = event.detail;
|
||||
setThumbnails(prev => new Map(prev).set(pageNumber, dataUrl));
|
||||
}
|
||||
|
||||
window.addEventListener("pdf-thumbnail", handleThumbnail as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener("pdf-thumbnail", handleThumbnail as EventListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const requestThumbnail = useCallback((pageNumber: number) => {
|
||||
// Only request if we haven't already requested it and don't have it
|
||||
if (!requestedThumbnails.current.has(pageNumber) && !thumbnails.has(pageNumber) && pagesData) {
|
||||
requestedThumbnails.current.add(pageNumber);
|
||||
pagesData.requestThumbnail(pageNumber);
|
||||
}
|
||||
}, [pagesData, thumbnails]);
|
||||
|
||||
if (!pagesData || pagesData.totalPages === 0) {
|
||||
return <div className="no-pages">No pages available</div>;
|
||||
}
|
||||
|
||||
const pages = Array.from({ length: pagesData.totalPages }, (_, i) => i + 1);
|
||||
|
||||
return (
|
||||
<div className="pdf-pages-list">
|
||||
{pages.map(pageNumber => (
|
||||
<PdfPageItem
|
||||
key={pageNumber}
|
||||
pageNumber={pageNumber}
|
||||
isActive={pageNumber === pagesData.currentPage}
|
||||
thumbnail={thumbnails.get(pageNumber)}
|
||||
onRequestThumbnail={requestThumbnail}
|
||||
onPageClick={() => pagesData.scrollToPage(pageNumber)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PdfPageItem({
|
||||
pageNumber,
|
||||
isActive,
|
||||
thumbnail,
|
||||
onRequestThumbnail,
|
||||
onPageClick
|
||||
}: {
|
||||
pageNumber: number;
|
||||
isActive: boolean;
|
||||
thumbnail?: string;
|
||||
onRequestThumbnail(page: number): void;
|
||||
onPageClick(): void;
|
||||
}) {
|
||||
const hasRequested = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!thumbnail && !hasRequested.current) {
|
||||
hasRequested.current = true;
|
||||
onRequestThumbnail(pageNumber);
|
||||
}
|
||||
}, [pageNumber, thumbnail, onRequestThumbnail]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`pdf-page-item ${isActive ? 'active' : ''}`}
|
||||
onClick={onPageClick}
|
||||
>
|
||||
<div className="pdf-page-number">{pageNumber}</div>
|
||||
<div className="pdf-page-thumbnail">
|
||||
{thumbnail ? (
|
||||
<img src={thumbnail} alt={t("pdf.pages_alt", { pageNumber })} />
|
||||
) : (
|
||||
<div className="pdf-page-loading">{t("pdf.pages_loading")}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export default function SqlResults() {
|
||||
{t("sql_result.no_rows")}
|
||||
</Alert>
|
||||
) : (
|
||||
<div class="sql-console-result-container">
|
||||
<div className="sql-console-result-container selectable-text">
|
||||
{results?.map(rows => {
|
||||
// inserts, updates
|
||||
if (typeof rows === "object" && !Array.isArray(rows)) {
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
import { useNoteBlob } from "../react/hooks";
|
||||
import "./File.css";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import { t } from "../../services/i18n";
|
||||
import { getUrlForDownload } from "../../services/open";
|
||||
import Alert from "../react/Alert";
|
||||
import { t } from "../../services/i18n";
|
||||
import { useNoteBlob } from "../react/hooks";
|
||||
import PdfPreview from "./file/Pdf";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
|
||||
const TEXT_MAX_NUM_CHARS = 5000;
|
||||
|
||||
export default function File({ note }: TypeWidgetProps) {
|
||||
const blob = useNoteBlob(note);
|
||||
export default function FileTypeWidget({ note, parentComponent, noteContext }: TypeWidgetProps) {
|
||||
const blob = useNoteBlob(note, parentComponent?.componentId);
|
||||
|
||||
if (blob?.content) {
|
||||
return <TextPreview content={blob.content} />
|
||||
return <TextPreview content={blob.content} />;
|
||||
} else if (note.mime === "application/pdf") {
|
||||
return <PdfPreview note={note} />
|
||||
return noteContext && <PdfPreview blob={blob} note={note} componentId={parentComponent?.componentId} noteContext={noteContext} />;
|
||||
} else if (note.mime.startsWith("video/")) {
|
||||
return <VideoPreview note={note} />
|
||||
return <VideoPreview note={note} />;
|
||||
} else if (note.mime.startsWith("audio/")) {
|
||||
return <AudioPreview note={note} />
|
||||
} else {
|
||||
return <NoPreview />
|
||||
return <AudioPreview note={note} />;
|
||||
}
|
||||
return <NoPreview />;
|
||||
|
||||
}
|
||||
|
||||
function TextPreview({ content }: { content: string }) {
|
||||
@@ -37,14 +39,6 @@ function TextPreview({ content }: { content: string }) {
|
||||
)}
|
||||
<pre class="file-preview-content">{trimmedContent}</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function PdfPreview({ note }: { note: FNote }) {
|
||||
return (
|
||||
<iframe
|
||||
class="pdf-preview"
|
||||
src={getUrlForDownload(`api/notes/${note.noteId}/open`)} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,7 +50,7 @@ function VideoPreview({ note }: { note: FNote }) {
|
||||
datatype={note?.mime}
|
||||
controls
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AudioPreview({ note }: { note: FNote }) {
|
||||
@@ -66,7 +60,7 @@ function AudioPreview({ note }: { note: FNote }) {
|
||||
src={getUrlForDownload(`api/notes/${note.noteId}/open-partial`)}
|
||||
controls
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function NoPreview() {
|
||||
|
||||
@@ -29,6 +29,7 @@ export default function Mermaid(props: TypeWidgetProps) {
|
||||
<SvgSplitEditor
|
||||
attachmentName="mermaid-export"
|
||||
renderSvg={renderSvg}
|
||||
noteType="mermaid"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -37,6 +37,7 @@ const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Options["locale"] | null>
|
||||
it: "it",
|
||||
ja: "ja",
|
||||
pt: "pt",
|
||||
pl: null,
|
||||
pt_br: "pt",
|
||||
ro: "ro",
|
||||
ru: "ru",
|
||||
|
||||
@@ -13,6 +13,7 @@ export const LANGUAGE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Language["code"]
|
||||
it: "it-IT",
|
||||
ja: "ja-JP",
|
||||
pt: "pt-PT",
|
||||
pl: "pl-PL",
|
||||
pt_br: "pt-BR",
|
||||
ro: "ro-RO",
|
||||
ru: "ru-RU",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "./code.css";
|
||||
|
||||
import { default as VanillaCodeMirror, getThemeById } from "@triliumnext/codemirror";
|
||||
import { NoteType } from "@triliumnext/commons";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext, { CommandListenerData } from "../../../components/app_context";
|
||||
@@ -24,6 +25,7 @@ export interface EditableCodeProps extends TypeWidgetProps, Omit<CodeEditorProps
|
||||
debounceUpdate?: boolean;
|
||||
lineWrapping?: boolean;
|
||||
updateInterval?: number;
|
||||
noteType?: NoteType;
|
||||
/** Invoked when the content of the note is changed, such as a different revision or a note switch. */
|
||||
onContentChanged?: (content: string) => void;
|
||||
/** Invoked after the content of the note has been uploaded to the server, using a spaced update. */
|
||||
@@ -72,14 +74,14 @@ function formatViewSource(note: FNote, content: string) {
|
||||
return content;
|
||||
}
|
||||
|
||||
export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentComponent, updateInterval, onContentChanged, dataSaved, ...editorProps }: EditableCodeProps) {
|
||||
export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentComponent, updateInterval, noteType = "code", onContentChanged, dataSaved, ...editorProps }: EditableCodeProps) {
|
||||
const editorRef = useRef<VanillaCodeMirror>(null);
|
||||
const containerRef = useRef<HTMLPreElement>(null);
|
||||
const [ vimKeymapEnabled ] = useTriliumOptionBool("vimKeymapEnabled");
|
||||
const mime = useNoteProperty(note, "mime");
|
||||
const spacedUpdate = useEditorSpacedUpdate({
|
||||
note,
|
||||
noteType: "code",
|
||||
noteType,
|
||||
noteContext,
|
||||
getData: () => ({ content: editorRef.current?.getText() ?? "" }),
|
||||
onContentChange: (content) => {
|
||||
|
||||
234
apps/client/src/widgets/type_widgets/file/Pdf.tsx
Normal file
234
apps/client/src/widgets/type_widgets/file/Pdf.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
|
||||
import appContext from "../../../components/app_context";
|
||||
import type NoteContext from "../../../components/note_context";
|
||||
import FBlob from "../../../entities/fblob";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import { useViewModeConfig } from "../../collections/NoteList";
|
||||
import { useBlobEditorSpacedUpdate, useTriliumEvent } from "../../react/hooks";
|
||||
import PdfViewer from "./PdfViewer";
|
||||
|
||||
export default function PdfPreview({ note, blob, componentId, noteContext }: {
|
||||
note: FNote;
|
||||
noteContext: NoteContext;
|
||||
blob: FBlob | null | undefined;
|
||||
componentId: string | undefined;
|
||||
}) {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const historyConfig = useViewModeConfig<HistoryData>(note, "pdfHistory");
|
||||
|
||||
const spacedUpdate = useBlobEditorSpacedUpdate({
|
||||
note,
|
||||
noteType: "file",
|
||||
noteContext,
|
||||
getData() {
|
||||
if (!iframeRef.current?.contentWindow) return undefined;
|
||||
|
||||
return new Promise<Blob>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("Timeout while waiting for blob response"));
|
||||
}, 10_000);
|
||||
|
||||
const onMessageReceived = (event: PdfMessageEvent) => {
|
||||
if (event.data.type !== "pdfjs-viewer-blob") return;
|
||||
if (event.data.noteId !== note.noteId || event.data.ntxId !== noteContext.ntxId) return;
|
||||
const blob = new Blob([event.data.data as Uint8Array<ArrayBuffer>], { type: note.mime });
|
||||
|
||||
clearTimeout(timeout);
|
||||
window.removeEventListener("message", onMessageReceived);
|
||||
resolve(blob);
|
||||
};
|
||||
|
||||
window.addEventListener("message", onMessageReceived);
|
||||
iframeRef.current?.contentWindow?.postMessage({
|
||||
type: "trilium-request-blob",
|
||||
}, window.location.origin);
|
||||
});
|
||||
},
|
||||
onContentChange() {
|
||||
if (iframeRef.current?.contentWindow) {
|
||||
iframeRef.current.contentWindow.location.reload();
|
||||
}
|
||||
},
|
||||
replaceWithoutRevision: true
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function handleMessage(event: PdfMessageEvent) {
|
||||
if (event.data?.type === "pdfjs-viewer-document-modified") {
|
||||
if (event.data.noteId === note.noteId && event.data.ntxId === noteContext.ntxId) {
|
||||
spacedUpdate.resetUpdateTimer();
|
||||
spacedUpdate.scheduleUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
if (event.data.type === "pdfjs-viewer-save-view-history" && event.data?.data) {
|
||||
if (event.data.noteId === note.noteId && event.data.ntxId === noteContext.ntxId) {
|
||||
historyConfig?.storeFn(JSON.parse(event.data.data));
|
||||
}
|
||||
}
|
||||
|
||||
if (event.data.type === "pdfjs-viewer-toc") {
|
||||
if (event.data.data) {
|
||||
// Convert PDF outline to HeadingContext format
|
||||
const headings = convertPdfOutlineToHeadings(event.data.data);
|
||||
noteContext.setContextData("toc", {
|
||||
headings,
|
||||
activeHeadingId: null,
|
||||
scrollToHeading: (heading) => {
|
||||
iframeRef.current?.contentWindow?.postMessage({
|
||||
type: "trilium-scroll-to-heading",
|
||||
headingId: heading.id
|
||||
}, window.location.origin);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// No ToC available, use empty headings
|
||||
noteContext.setContextData("toc", {
|
||||
headings: [],
|
||||
activeHeadingId: null,
|
||||
scrollToHeading: () => {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (event.data.type === "pdfjs-viewer-active-heading") {
|
||||
const currentToc = noteContext.getContextData("toc");
|
||||
if (currentToc) {
|
||||
noteContext.setContextData("toc", {
|
||||
...currentToc,
|
||||
activeHeadingId: event.data.headingId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (event.data.type === "pdfjs-viewer-page-info") {
|
||||
noteContext.setContextData("pdfPages", {
|
||||
totalPages: event.data.totalPages,
|
||||
currentPage: event.data.currentPage,
|
||||
scrollToPage: (page: number) => {
|
||||
iframeRef.current?.contentWindow?.postMessage({
|
||||
type: "trilium-scroll-to-page",
|
||||
pageNumber: page
|
||||
}, window.location.origin);
|
||||
},
|
||||
requestThumbnail: (page: number) => {
|
||||
iframeRef.current?.contentWindow?.postMessage({
|
||||
type: "trilium-request-thumbnail",
|
||||
pageNumber: page
|
||||
}, window.location.origin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.type === "pdfjs-viewer-current-page") {
|
||||
const currentPages = noteContext.getContextData("pdfPages");
|
||||
if (currentPages) {
|
||||
noteContext.setContextData("pdfPages", {
|
||||
...currentPages,
|
||||
currentPage: event.data.currentPage
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (event.data.type === "pdfjs-viewer-thumbnail") {
|
||||
// Forward thumbnail to any listeners
|
||||
window.dispatchEvent(new CustomEvent("pdf-thumbnail", {
|
||||
detail: {
|
||||
pageNumber: event.data.pageNumber,
|
||||
dataUrl: event.data.dataUrl
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (event.data.type === "pdfjs-viewer-attachments") {
|
||||
noteContext.setContextData("pdfAttachments", {
|
||||
attachments: event.data.attachments,
|
||||
downloadAttachment: (filename: string) => {
|
||||
iframeRef.current?.contentWindow?.postMessage({
|
||||
type: "trilium-download-attachment",
|
||||
filename
|
||||
}, window.location.origin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.type === "pdfjs-viewer-layers") {
|
||||
noteContext.setContextData("pdfLayers", {
|
||||
layers: event.data.layers,
|
||||
toggleLayer: (layerId: string, visible: boolean) => {
|
||||
iframeRef.current?.contentWindow?.postMessage({
|
||||
type: "trilium-toggle-layer",
|
||||
layerId,
|
||||
visible
|
||||
}, window.location.origin);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage);
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage);
|
||||
};
|
||||
}, [ note, historyConfig, componentId, blob, noteContext ]);
|
||||
|
||||
useTriliumEvent("customDownload", ({ ntxId }) => {
|
||||
if (ntxId !== noteContext.ntxId) return;
|
||||
iframeRef.current?.contentWindow?.postMessage({
|
||||
type: "trilium-request-download"
|
||||
});
|
||||
});
|
||||
|
||||
return (historyConfig &&
|
||||
<PdfViewer
|
||||
iframeRef={iframeRef}
|
||||
tabIndex={300}
|
||||
pdfUrl={`../../api/notes/${note.noteId}/open`}
|
||||
onLoad={() => {
|
||||
const win = iframeRef.current?.contentWindow;
|
||||
if (win) {
|
||||
win.TRILIUM_VIEW_HISTORY_STORE = historyConfig.config;
|
||||
win.TRILIUM_NOTE_ID = note.noteId;
|
||||
win.TRILIUM_NTX_ID = noteContext.ntxId;
|
||||
}
|
||||
|
||||
if (iframeRef.current?.contentWindow) {
|
||||
iframeRef.current.contentWindow.addEventListener('click', () => {
|
||||
appContext.tabManager.activateNoteContext(noteContext.ntxId);
|
||||
});
|
||||
}
|
||||
}}
|
||||
editable
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface PdfHeading {
|
||||
level: number;
|
||||
text: string;
|
||||
id: string;
|
||||
element: null;
|
||||
}
|
||||
|
||||
function convertPdfOutlineToHeadings(outline: PdfOutlineItem[]): PdfHeading[] {
|
||||
const headings: PdfHeading[] = [];
|
||||
|
||||
function flatten(items: PdfOutlineItem[]) {
|
||||
for (const item of items) {
|
||||
headings.push({
|
||||
level: item.level + 1,
|
||||
text: item.title,
|
||||
id: item.id,
|
||||
element: null // PDFs don't have DOM elements
|
||||
});
|
||||
|
||||
if (item.items && item.items.length > 0) {
|
||||
flatten(item.items);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flatten(outline);
|
||||
return headings;
|
||||
}
|
||||
94
apps/client/src/widgets/type_widgets/file/PdfViewer.tsx
Normal file
94
apps/client/src/widgets/type_widgets/file/PdfViewer.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { HTMLAttributes, RefObject } from "preact";
|
||||
import { useCallback, useEffect, useRef } from "preact/hooks";
|
||||
|
||||
import { useSyncedRef, useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
||||
|
||||
const VARIABLE_WHITELIST = new Set([
|
||||
"root-background",
|
||||
"main-background-color",
|
||||
"main-border-color",
|
||||
"main-text-color"
|
||||
]);
|
||||
|
||||
interface PdfViewerProps extends Pick<HTMLAttributes<HTMLIFrameElement>, "tabIndex"> {
|
||||
iframeRef?: RefObject<HTMLIFrameElement>;
|
||||
/** Note: URLs are relative to /pdfjs/web. */
|
||||
pdfUrl: string;
|
||||
onLoad?(): void;
|
||||
/**
|
||||
* If set, enables editable mode which includes persistence of user settings, annotations as well as specific features such as sending table of contents data for the sidebar.
|
||||
*/
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable component displaying a PDF. The PDF needs to be provided via a URL.
|
||||
*/
|
||||
export default function PdfViewer({ iframeRef: externalIframeRef, pdfUrl, onLoad, editable }: PdfViewerProps) {
|
||||
const iframeRef = useSyncedRef(externalIframeRef, null);
|
||||
const [ locale ] = useTriliumOption("locale");
|
||||
const [ newLayout ] = useTriliumOptionBool("newLayout");
|
||||
const injectStyles = useStyleInjection(iframeRef);
|
||||
|
||||
return (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
class="pdf-preview"
|
||||
src={`pdfjs/web/viewer.html?file=${pdfUrl}&lang=${locale}&sidebar=${newLayout ? "0" : "1"}&editable=${editable ? "1" : "0"}`}
|
||||
onLoad={() => {
|
||||
injectStyles();
|
||||
onLoad?.();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function useStyleInjection(iframeRef: RefObject<HTMLIFrameElement>) {
|
||||
const styleRef = useRef<HTMLStyleElement | null>(null);
|
||||
|
||||
// First load.
|
||||
const onLoad = useCallback(() => {
|
||||
const doc = iframeRef.current?.contentDocument;
|
||||
if (!doc) return;
|
||||
|
||||
const style = doc.createElement('style');
|
||||
style.id = 'client-root-vars';
|
||||
style.textContent = cssVarsToString(getRootCssVariables());
|
||||
styleRef.current = style;
|
||||
|
||||
doc.head.appendChild(style);
|
||||
}, [ iframeRef ]);
|
||||
|
||||
// React to changes.
|
||||
useEffect(() => {
|
||||
const listener = () => {
|
||||
styleRef.current!.textContent = cssVarsToString(getRootCssVariables());
|
||||
};
|
||||
|
||||
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
media.addEventListener("change", listener);
|
||||
return () => media.removeEventListener("change", listener);
|
||||
}, [ iframeRef ]);
|
||||
|
||||
return onLoad;
|
||||
}
|
||||
|
||||
function getRootCssVariables() {
|
||||
const styles = getComputedStyle(document.documentElement);
|
||||
const vars: Record<string, string> = {};
|
||||
|
||||
for (let i = 0; i < styles.length; i++) {
|
||||
const prop = styles[i];
|
||||
if (prop.startsWith('--') && VARIABLE_WHITELIST.has(prop.substring(2))) {
|
||||
vars[`--tn-${prop.substring(2)}`] = styles.getPropertyValue(prop).trim();
|
||||
}
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
function cssVarsToString(vars: Record<string, string>) {
|
||||
return `:root {\n${Object.entries(vars)
|
||||
.map(([k, v]) => ` ${k}: ${v};`)
|
||||
.join('\n')}\n}`;
|
||||
}
|
||||
@@ -15,6 +15,8 @@
|
||||
.note-detail-split .note-detail-split-editor {
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.note-detail-split .note-detail-split-editor .note-detail-code {
|
||||
@@ -30,6 +32,7 @@
|
||||
margin: 5px;
|
||||
white-space: pre-wrap;
|
||||
font-size: 0.85em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.note-detail-split .note-detail-split-preview {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { t } from "../../../services/i18n";
|
||||
import SplitEditor, { PreviewButton, SplitEditorProps } from "./SplitEditor";
|
||||
import { RawHtmlBlock } from "../../react/RawHtml";
|
||||
@@ -55,7 +55,9 @@ export default function SvgSplitEditor({ ntxId, note, attachmentName, renderSvg,
|
||||
}
|
||||
|
||||
// Save as attachment.
|
||||
function onSave() {
|
||||
const onSave = useCallback(() => {
|
||||
if (!svg) return; // Don't save if SVG hasn't been rendered yet
|
||||
|
||||
const payload = {
|
||||
role: "image",
|
||||
title: `${attachmentName}.svg`,
|
||||
@@ -65,16 +67,18 @@ export default function SvgSplitEditor({ ntxId, note, attachmentName, renderSvg,
|
||||
};
|
||||
|
||||
server.post(`notes/${note.noteId}/attachments?matchBy=title`, payload);
|
||||
}
|
||||
}, [ svg, attachmentName, note.noteId ]);
|
||||
|
||||
// Save the SVG when entering a note only when it does not have an attachment.
|
||||
useEffect(() => {
|
||||
if (!svg) return; // Wait until SVG is rendered
|
||||
|
||||
note?.getAttachments().then((attachments) => {
|
||||
if (!attachments.find((a) => a.title === `${attachmentName}.svg`)) {
|
||||
onSave();
|
||||
}
|
||||
});
|
||||
}, [ note ]);
|
||||
}).catch(e => console.error("Failed to get attachments for SVGSplitEditor", e));
|
||||
}, [ note, svg, attachmentName, onSave ]);
|
||||
|
||||
// Import/export
|
||||
useTriliumEvent("exportSvg", async({ ntxId: eventNtxId }) => {
|
||||
|
||||
@@ -286,7 +286,7 @@ function useWatchdogCrashHandling() {
|
||||
const currentState = watchdog.state;
|
||||
logInfo(`CKEditor state changed to ${currentState}`);
|
||||
|
||||
if (currentState === "ready") {
|
||||
if (currentState === "ready" && hasCrashed.current) {
|
||||
hasCrashed.current = false;
|
||||
watchdog.editor?.focus();
|
||||
}
|
||||
|
||||
@@ -35,5 +35,5 @@ describe("CK config", () => {
|
||||
expect(config.translations, locale.id).toHaveLength(2);
|
||||
}
|
||||
}
|
||||
}, 10_000);
|
||||
}, 20_000);
|
||||
});
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { ALLOWED_PROTOCOLS, DISPLAYABLE_LOCALE_IDS, MIME_TYPE_AUTO } from "@triliumnext/commons";
|
||||
import { buildExtraCommands, type EditorConfig, getCkLocale, PREMIUM_PLUGINS, TemplateDefinition } from "@triliumnext/ckeditor5";
|
||||
import { getHighlightJsNameForMime } from "../../../services/mime_types.js";
|
||||
import options from "../../../services/options.js";
|
||||
import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
|
||||
import { buildExtraCommands, type EditorConfig, getCkLocale, loadPremiumPlugins, TemplateDefinition } from "@triliumnext/ckeditor5";
|
||||
import emojiDefinitionsUrl from "@triliumnext/ckeditor5/src/emoji_definitions/en.json?url";
|
||||
import { ALLOWED_PROTOCOLS, DISPLAYABLE_LOCALE_IDS, MIME_TYPE_AUTO, normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||
|
||||
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import { getMermaidConfig } from "../../../services/mermaid.js";
|
||||
import { default as mimeTypesService, getHighlightJsNameForMime } from "../../../services/mime_types.js";
|
||||
import noteAutocompleteService, { type Suggestion } from "../../../services/note_autocomplete.js";
|
||||
import mimeTypesService from "../../../services/mime_types.js";
|
||||
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||
import options from "../../../services/options.js";
|
||||
import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
|
||||
import { buildToolbarConfig } from "./toolbar.js";
|
||||
|
||||
export const OPEN_SOURCE_LICENSE_KEY = "GPL";
|
||||
@@ -36,7 +35,7 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
|
||||
engine: "katex",
|
||||
outputType: "span", // or script
|
||||
lazyLoad: async () => {
|
||||
(window as any).katex = (await import("../../../services/math.js")).default
|
||||
(window as any).katex = (await import("../../../services/math.js")).default;
|
||||
},
|
||||
forceOutputType: false, // forces output to use outputType
|
||||
enablePreview: true // Enable preview view
|
||||
@@ -172,7 +171,7 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
|
||||
config.language = {
|
||||
ui: (typeof config.language === "string" ? config.language : "en"),
|
||||
content: contentLanguage
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Mention customisation.
|
||||
@@ -195,11 +194,9 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
|
||||
};
|
||||
}
|
||||
|
||||
// Enable premium plugins.
|
||||
// Enable premium plugins dynamically to avoid eager loading.
|
||||
if (hasPremiumLicense) {
|
||||
config.extraPlugins = [
|
||||
...PREMIUM_PLUGINS
|
||||
];
|
||||
config.extraPlugins = await loadPremiumPlugins();
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -237,7 +234,7 @@ function getLicenseKey() {
|
||||
}
|
||||
|
||||
function getDisabledPlugins() {
|
||||
let disabledPlugins: string[] = [];
|
||||
const disabledPlugins: string[] = [];
|
||||
|
||||
if (options.get("textNoteEmojiCompletionEnabled") !== "true") {
|
||||
disabledPlugins.push("EmojiMention");
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/vitest",
|
||||
"types": [
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user