mirror of
https://github.com/zadam/trilium.git
synced 2025-12-16 21:29:56 +01:00
Compare commits
1485 Commits
feat/rice-
...
renovate/v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3445b594e8 | ||
|
|
16a6344687 | ||
|
|
7bac0b25ce | ||
|
|
6094f738f2 | ||
|
|
c3a6d1bba8 | ||
|
|
1feeb350ce | ||
|
|
f21ba207fe | ||
|
|
07c8ff4571 | ||
|
|
71d8588091 | ||
|
|
3c41b7e5a9 | ||
|
|
41b7a295b9 | ||
|
|
ca8e889e1e | ||
|
|
6d4e52c928 | ||
|
|
e2fac8ab05 | ||
|
|
af3883fdac | ||
|
|
83777d7ea0 | ||
|
|
c6854c84b9 | ||
|
|
eb99352fff | ||
|
|
7dac5d424b | ||
|
|
441958028d | ||
|
|
80b61a35a9 | ||
|
|
61df0f3d31 | ||
|
|
23ece9fc86 | ||
|
|
220e3d7195 | ||
|
|
392c0311e5 | ||
|
|
3abdcfa7a5 | ||
|
|
4896042fc4 | ||
|
|
7edfd5d7b4 | ||
|
|
ad8e52f744 | ||
|
|
455dc5dc11 | ||
|
|
158f5ac310 | ||
|
|
fb70029091 | ||
|
|
b370512893 | ||
|
|
764607314c | ||
|
|
dbcf9b01c5 | ||
|
|
93ce77438f | ||
|
|
14c30661e6 | ||
|
|
37efc44f43 | ||
|
|
6aaade846a | ||
|
|
bd2402396b | ||
|
|
a9b3479216 | ||
|
|
05e98877b0 | ||
|
|
862ddf3a71 | ||
|
|
c1df2c45de | ||
|
|
065e97c940 | ||
|
|
adae7fa03b | ||
|
|
b725dbea7e | ||
|
|
4b80eec000 | ||
|
|
d7722a1e05 | ||
|
|
35cfcc59f6 | ||
|
|
192190d685 | ||
|
|
d6cc4bfa9c | ||
|
|
ed284fbc5f | ||
|
|
cb0efe25f5 | ||
|
|
906fe4f8da | ||
|
|
04a641199b | ||
|
|
50cbad22d0 | ||
|
|
4cfe59271f | ||
|
|
ba7969dad4 | ||
|
|
43b6440bf9 | ||
|
|
3e19a163c2 | ||
|
|
ca39282269 | ||
|
|
016389df68 | ||
|
|
17db2a6b38 | ||
|
|
bc8f17ee5c | ||
|
|
0c8944ab8e | ||
|
|
0960b585bd | ||
|
|
728fab1dda | ||
|
|
d5ec80d85d | ||
|
|
15ef93d7e6 | ||
|
|
9711b22ea9 | ||
|
|
b9c7d2b01d | ||
|
|
9834846a23 | ||
|
|
65f425df2c | ||
|
|
a551dfe4d6 | ||
|
|
e9bfacdb7c | ||
|
|
3ba7b7d439 | ||
|
|
381943818d | ||
|
|
d1ae2db587 | ||
|
|
8fa6e38382 | ||
|
|
749074ea94 | ||
|
|
f1bb786a49 | ||
|
|
42bde3873b | ||
|
|
4877238015 | ||
|
|
16374aaf1d | ||
|
|
19709f749a | ||
|
|
09c7affc16 | ||
|
|
01e197fd46 | ||
|
|
0fe129ac16 | ||
|
|
3c52ceb4e6 | ||
|
|
8ba2357d91 | ||
|
|
20f4990d48 | ||
|
|
8c793bf0fe | ||
|
|
12a0eebafe | ||
|
|
092c7dff6b | ||
|
|
7a1ff42d67 | ||
|
|
5a09a80902 | ||
|
|
a7ca839afb | ||
|
|
6b9b9a96c3 | ||
|
|
272888acab | ||
|
|
283e3c9de1 | ||
|
|
859087b850 | ||
|
|
f7b911dc0b | ||
|
|
9d7e2855d3 | ||
|
|
8e6ea87754 | ||
|
|
34bc444b18 | ||
|
|
860a903336 | ||
|
|
be923ad2b7 | ||
|
|
d2da1ed1e7 | ||
|
|
1c05f5e5c3 | ||
|
|
e043f30cc6 | ||
|
|
e1611d83a3 | ||
|
|
58e2111a8f | ||
|
|
c5e4c484dc | ||
|
|
75a6dece7a | ||
|
|
5c0e7736d6 | ||
|
|
2562ecd055 | ||
|
|
aaaa47b575 | ||
|
|
21d82ec1d7 | ||
|
|
5af8444cac | ||
|
|
cd82c34b93 | ||
|
|
d182659d62 | ||
|
|
171f428b9d | ||
|
|
da4ca9c804 | ||
|
|
c019341503 | ||
|
|
7234f04b56 | ||
|
|
1998cbc005 | ||
|
|
5914073c3f | ||
|
|
d5aadf2604 | ||
|
|
1fe22f940b | ||
|
|
0cdaf70efe | ||
|
|
8174c65243 | ||
|
|
2645801277 | ||
|
|
fb8c31cb9c | ||
|
|
7287dbd64f | ||
|
|
6569d64931 | ||
|
|
e9f3216926 | ||
|
|
ca0af9646d | ||
|
|
92dfafd1ff | ||
|
|
d04dde3b97 | ||
|
|
4c520c6df3 | ||
|
|
65d6ed1cdc | ||
|
|
3352a92445 | ||
|
|
bc8c55b8fb | ||
|
|
7660914eb8 | ||
|
|
869aec778c | ||
|
|
255726dcc4 | ||
|
|
9969000807 | ||
|
|
3b909fd739 | ||
|
|
ad08fb8132 | ||
|
|
8d536a6040 | ||
|
|
2b1bc8e2b9 | ||
|
|
563194ff6c | ||
|
|
0c9ff4dae4 | ||
|
|
b10e7f1811 | ||
|
|
f93ad499e2 | ||
|
|
87a51251ca | ||
|
|
b56e5b2483 | ||
|
|
476c162016 | ||
|
|
4182f6043a | ||
|
|
aa528c65b7 | ||
|
|
4998560e31 | ||
|
|
86f36922c4 | ||
|
|
4f617b86d3 | ||
|
|
b28527e10d | ||
|
|
fbb8924ebf | ||
|
|
f68c9b751f | ||
|
|
8091f02b16 | ||
|
|
f4c68d115b | ||
|
|
6c70d6b9ae | ||
|
|
1ea12567a3 | ||
|
|
2d16ab7a70 | ||
|
|
a228ba5273 | ||
|
|
d0477e9ebf | ||
|
|
c99907972d | ||
|
|
b9ebc7d7ea | ||
|
|
4f9e2c5eca | ||
|
|
ab1f8ee5ae | ||
|
|
89276ad51a | ||
|
|
eca533a517 | ||
|
|
0be578c517 | ||
|
|
198b315602 | ||
|
|
6474abc983 | ||
|
|
2137dbe849 | ||
|
|
b7b46703d9 | ||
|
|
d2d96a1421 | ||
|
|
cfcc309e5a | ||
|
|
7d87ec942e | ||
|
|
4def13272f | ||
|
|
c4f914bb7b | ||
|
|
6bf213a0b0 | ||
|
|
694cd2bc7c | ||
|
|
3851a94400 | ||
|
|
e296416a54 | ||
|
|
0bd89a659c | ||
|
|
0ada6523a8 | ||
|
|
56570d7ba1 | ||
|
|
0ffdedcfa6 | ||
|
|
f391bb8eec | ||
|
|
7000076961 | ||
|
|
e0f6ba808c | ||
|
|
4c2fe8a846 | ||
|
|
2ea23368bc | ||
|
|
87666005a6 | ||
|
|
7666f44b7a | ||
|
|
470f6e5334 | ||
|
|
a2b007874b | ||
|
|
9946d8c6b9 | ||
|
|
02fab16475 | ||
|
|
5145ce2d23 | ||
|
|
e06abe6e5b | ||
|
|
50a847777e | ||
|
|
4473f80d73 | ||
|
|
70c918c9c6 | ||
|
|
0939975631 | ||
|
|
0ef90c6165 | ||
|
|
cef14a3b19 | ||
|
|
61d3141bce | ||
|
|
f040a0b6d1 | ||
|
|
e9dfec88c9 | ||
|
|
6fa97c845a | ||
|
|
f686d9ecd0 | ||
|
|
621ebe4396 | ||
|
|
ac2a566685 | ||
|
|
ac3d57d5da | ||
|
|
9ab5eef984 | ||
|
|
912f90accf | ||
|
|
6463b0dcaa | ||
|
|
0b45fb6764 | ||
|
|
330d71847b | ||
|
|
60c8f0c78b | ||
|
|
fcbd1ab0b1 | ||
|
|
3549bfb328 | ||
|
|
c97038fffd | ||
|
|
15b5885982 | ||
|
|
6aa8d9fbf9 | ||
|
|
eccf4620ac | ||
|
|
f08fbe9bb2 | ||
|
|
bfa87af489 | ||
|
|
a7899b7505 | ||
|
|
e80b5cddcd | ||
|
|
db12f9b8dc | ||
|
|
f4c95195c9 | ||
|
|
e2cbff7b3a | ||
|
|
98a3c8150c | ||
|
|
447e09fec1 | ||
|
|
7d2a1bb2e5 | ||
|
|
40fcf79778 | ||
|
|
88a779bbdb | ||
|
|
db04514769 | ||
|
|
23062470f5 | ||
|
|
5bad043ed5 | ||
|
|
4ab8af0995 | ||
|
|
1a65c5e13e | ||
|
|
fc08946038 | ||
|
|
4d6dba06ad | ||
|
|
d7887fe25f | ||
|
|
81dd50e752 | ||
|
|
fe13065ef8 | ||
|
|
eb02330fdf | ||
|
|
738fa6fd0e | ||
|
|
0c1c7e4f8e | ||
|
|
9eb9b66398 | ||
|
|
9db046b401 | ||
|
|
914272eee0 | ||
|
|
2b7e203bcc | ||
|
|
a61ddedc0b | ||
|
|
60fc34ffac | ||
|
|
685109556c | ||
|
|
45927053f3 | ||
|
|
5d438a877b | ||
|
|
870499bc3a | ||
|
|
c6d97e3d4b | ||
|
|
efff38b116 | ||
|
|
1b725175c6 | ||
|
|
6eff62f73f | ||
|
|
95d2160c76 | ||
|
|
2b195155ed | ||
|
|
28e9abc8bb | ||
|
|
0162b9d441 | ||
|
|
0545b929e1 | ||
|
|
d2b32ff5af | ||
|
|
2d3776cd5f | ||
|
|
2638963171 | ||
|
|
24ed97f65d | ||
|
|
c099634e39 | ||
|
|
12be14e6cf | ||
|
|
4dc773c1a3 | ||
|
|
31c5323fd9 | ||
|
|
74b6e7bf63 | ||
|
|
34025fa646 | ||
|
|
df9554194a | ||
|
|
4e1188484d | ||
|
|
2f44b9dc59 | ||
|
|
9ee3c48485 | ||
|
|
78b9c94829 | ||
|
|
4c8225ed73 | ||
|
|
88aad6d351 | ||
|
|
d99d701095 | ||
|
|
61fe27abbe | ||
|
|
24cd5006d5 | ||
|
|
726d6aad65 | ||
|
|
bd9fe14a6c | ||
|
|
792a10ace5 | ||
|
|
e9ac69b8e5 | ||
|
|
c76ff2d371 | ||
|
|
8ab9e30404 | ||
|
|
53b7d93efb | ||
|
|
00df3c3d1f | ||
|
|
e766b82418 | ||
|
|
9f4757af5b | ||
|
|
1a9fb34a6e | ||
|
|
a1513a3567 | ||
|
|
0de67b6a69 | ||
|
|
fec5ee9335 | ||
|
|
b540111fa4 | ||
|
|
0eed72b888 | ||
|
|
0856d3dbdf | ||
|
|
a9b453c27a | ||
|
|
fa8287269f | ||
|
|
1eee471018 | ||
|
|
c3829f82ab | ||
|
|
a51820f5df | ||
|
|
68591fb511 | ||
|
|
3795ce2143 | ||
|
|
3561a4f14d | ||
|
|
84cda001aa | ||
|
|
481127a560 | ||
|
|
c708e7cd61 | ||
|
|
fee0268792 | ||
|
|
953593c9d4 | ||
|
|
5ff60e53cb | ||
|
|
b38ee36fae | ||
|
|
38a415faf0 | ||
|
|
1e26864842 | ||
|
|
4b74ad5577 | ||
|
|
e5696713de | ||
|
|
2e44397c88 | ||
|
|
5d19881981 | ||
|
|
1711384eaa | ||
|
|
9897efe4af | ||
|
|
884578ea95 | ||
|
|
e404e76299 | ||
|
|
1db54cba3e | ||
|
|
77e3cc4021 | ||
|
|
242c63dfb4 | ||
|
|
f5440576b5 | ||
|
|
b020365af4 | ||
|
|
25e5bf0b86 | ||
|
|
19b32dd3a6 | ||
|
|
1ab89d0db0 | ||
|
|
6e8e10323f | ||
|
|
58bc5dc66a | ||
|
|
db42bb603b | ||
|
|
cb382c9537 | ||
|
|
a4b79a2dc9 | ||
|
|
0f867e02c4 | ||
|
|
ab1b4b37f4 | ||
|
|
5a1d138f29 | ||
|
|
06a5298efa | ||
|
|
db720acc18 | ||
|
|
8d8ff25bae | ||
|
|
6f85b7cc09 | ||
|
|
77f5770bff | ||
|
|
14cda5b921 | ||
|
|
36b1182565 | ||
|
|
483327c808 | ||
|
|
efb2f9a048 | ||
|
|
01978dabf0 | ||
|
|
cfbd2bf53a | ||
|
|
9262f94190 | ||
|
|
b36a0bd10b | ||
|
|
2dc8948f33 | ||
|
|
9f2ed2f9d4 | ||
|
|
e0f7d65f77 | ||
|
|
f18ac3a923 | ||
|
|
b39a6bcc97 | ||
|
|
8fa9c25f2a | ||
|
|
84bde62e05 | ||
|
|
5bb4621097 | ||
|
|
f1edf84f4d | ||
|
|
f7955a9040 | ||
|
|
7c5df21685 | ||
|
|
2060bb8cdd | ||
|
|
a9b4e7b1e2 | ||
|
|
82528c4478 | ||
|
|
4dcfc3e0bc | ||
|
|
999315d3c6 | ||
|
|
aef0b03c34 | ||
|
|
49f008c46f | ||
|
|
bd81db4117 | ||
|
|
9f274883e3 | ||
|
|
07b76b80f4 | ||
|
|
0014f0a88d | ||
|
|
63f7a78d31 | ||
|
|
e556c090ff | ||
|
|
c4f483c250 | ||
|
|
4031332b98 | ||
|
|
10cb7c8d6a | ||
|
|
be190bfe33 | ||
|
|
4d7d642952 | ||
|
|
737711e5eb | ||
|
|
42fc128f97 | ||
|
|
b03e6c3b19 | ||
|
|
66008489c4 | ||
|
|
3262e3490a | ||
|
|
16a73b0848 | ||
|
|
52bb4d7a0e | ||
|
|
40b5e4d549 | ||
|
|
b014ea8950 | ||
|
|
61592716f9 | ||
|
|
efe7fc0ee7 | ||
|
|
a810db3641 | ||
|
|
f8b292dfa3 | ||
|
|
fc2ab91280 | ||
|
|
668ee219c6 | ||
|
|
ee6512a1a6 | ||
|
|
fe1f590286 | ||
|
|
876e8f843a | ||
|
|
a45c1a1dc8 | ||
|
|
f8377169e6 | ||
|
|
a197a33d35 | ||
|
|
3060207d04 | ||
|
|
28c1d0b3f5 | ||
|
|
644d051477 | ||
|
|
f42031c8de | ||
|
|
6b50d9b087 | ||
|
|
a0f0da64b4 | ||
|
|
1e72ebd104 | ||
|
|
1184a95697 | ||
|
|
cd0e4a5678 | ||
|
|
394f6c3110 | ||
|
|
e2b6d0c256 | ||
|
|
fe7ca210dd | ||
|
|
e58d6bf2a3 | ||
|
|
460d20d6b2 | ||
|
|
ae154212fe | ||
|
|
28bb4edbac | ||
|
|
1ceed1b47b | ||
|
|
9445e64c2e | ||
|
|
e6fba03ba7 | ||
|
|
b027ca5c09 | ||
|
|
e98df30500 | ||
|
|
111c44dadf | ||
|
|
cb31c25e6c | ||
|
|
5d59c953c2 | ||
|
|
a2cff42981 | ||
|
|
cae892a971 | ||
|
|
f8447d923e | ||
|
|
3b8dabc9d2 | ||
|
|
cda39e967c | ||
|
|
7da9367dc9 | ||
|
|
82d97ef26f | ||
|
|
9e094f1d96 | ||
|
|
da7e15c268 | ||
|
|
24806a810c | ||
|
|
a2ace4510a | ||
|
|
5c8132088f | ||
|
|
7ee060b228 | ||
|
|
4b2a4b8f7b | ||
|
|
5a668ede01 | ||
|
|
9e099444b6 | ||
|
|
e3f5b3535a | ||
|
|
346ad1e8a3 | ||
|
|
2a9558e9c5 | ||
|
|
c324f66aef | ||
|
|
e688f2cdb6 | ||
|
|
2ff8762a22 | ||
|
|
4d75221938 | ||
|
|
658b699b71 | ||
|
|
72b0d03546 | ||
|
|
19980807f2 | ||
|
|
3514e3d057 | ||
|
|
fb6c82740c | ||
|
|
8df5a010c9 | ||
|
|
895e9b8bf0 | ||
|
|
bfcf85e0d2 | ||
|
|
5770222304 | ||
|
|
d5d2815bdf | ||
|
|
7fc3d413e5 | ||
|
|
474228b630 | ||
|
|
0805e077a1 | ||
|
|
6b059a9a75 | ||
|
|
7377e4e34d | ||
|
|
6fac947d9c | ||
|
|
5973e5ca26 | ||
|
|
6570a55e7e | ||
|
|
608ab53933 | ||
|
|
05679f7a8d | ||
|
|
f4838bb3b5 | ||
|
|
edb2a65196 | ||
|
|
fd721cac51 | ||
|
|
a1ff3cc8f7 | ||
|
|
3fa6b264e5 | ||
|
|
fcf51ec6da | ||
|
|
d15b5f8cbc | ||
|
|
ef3cbcac6d | ||
|
|
b16893c4d2 | ||
|
|
a365814aaa | ||
|
|
eca2116adc | ||
|
|
4cfa403657 | ||
|
|
70ded4c2cd | ||
|
|
3fe45db6ef | ||
|
|
11467775b6 | ||
|
|
1e5fcf635e | ||
|
|
223ba4643f | ||
|
|
200fd76929 | ||
|
|
c5c4ecd6e6 | ||
|
|
bedca9f82c | ||
|
|
adc356eff3 | ||
|
|
c4285772b3 | ||
|
|
a02235f2bd | ||
|
|
5f215b14c2 | ||
|
|
6e29fe8d58 | ||
|
|
43ceb1982d | ||
|
|
d02ec47d77 | ||
|
|
9942950710 | ||
|
|
a6682be251 | ||
|
|
baee2cd6b2 | ||
|
|
3ee8eac635 | ||
|
|
ce5a775160 | ||
|
|
2cf66d1c53 | ||
|
|
eaee67d742 | ||
|
|
c681496b1b | ||
|
|
829d3e046d | ||
|
|
5c8df540db | ||
|
|
8920e6e448 | ||
|
|
3cb860232e | ||
|
|
d588518ba1 | ||
|
|
d6f727d17a | ||
|
|
82c40302bd | ||
|
|
c40c62a247 | ||
|
|
8048b5ebca | ||
|
|
03ba43df5d | ||
|
|
a4b95b45ec | ||
|
|
59513962fe | ||
|
|
a201b43cde | ||
|
|
f98c77bd16 | ||
|
|
2596359b25 | ||
|
|
e0a0263607 | ||
|
|
2d3feedb07 | ||
|
|
af392fad3d | ||
|
|
ef74490c44 | ||
|
|
b6856e18a8 | ||
|
|
de1d4424d9 | ||
|
|
5ec45bb575 | ||
|
|
cb3aced2ed | ||
|
|
2f13a1ad21 | ||
|
|
045127adee | ||
|
|
db1a0c0362 | ||
|
|
dcaf91a878 | ||
|
|
a9209f5103 | ||
|
|
ea613986c2 | ||
|
|
1ed46bd47c | ||
|
|
f1ee79e75a | ||
|
|
cd27160905 | ||
|
|
9ddf4a1308 | ||
|
|
a1c5ed9eb5 | ||
|
|
7a4f19eada | ||
|
|
397fb785d6 | ||
|
|
75a1fcc933 | ||
|
|
292cbf1383 | ||
|
|
37a14fefb3 | ||
|
|
f424633d8c | ||
|
|
048258d2d1 | ||
|
|
f779108b6c | ||
|
|
522f3ae0a1 | ||
|
|
3fc7067c59 | ||
|
|
c600e8ef89 | ||
|
|
5ad267fe1b | ||
|
|
d8b3e438f8 | ||
|
|
2834af66e9 | ||
|
|
1bbf86fbeb | ||
|
|
f662b95dc9 | ||
|
|
9ad4b725ac | ||
|
|
0182c61aec | ||
|
|
78362535c7 | ||
|
|
ae7b31f343 | ||
|
|
f16441bba4 | ||
|
|
d3f9bb6def | ||
|
|
e02440aa59 | ||
|
|
f1d87c29d3 | ||
|
|
21335b1b00 | ||
|
|
7463570e76 | ||
|
|
da17a63ef5 | ||
|
|
eb8f2021cb | ||
|
|
888ff33be1 | ||
|
|
b46850e86e | ||
|
|
f053587f09 | ||
|
|
7a3092a23b | ||
|
|
d95450ae07 | ||
|
|
230def10fe | ||
|
|
036f8e49a4 | ||
|
|
4eca8a5640 | ||
|
|
08f96a91f3 | ||
|
|
2e915eccd6 | ||
|
|
c05c58c82b | ||
|
|
572feed918 | ||
|
|
d30d207ab5 | ||
|
|
2a6b91dd04 | ||
|
|
3dee1725b3 | ||
|
|
f7ac465e67 | ||
|
|
912f14549c | ||
|
|
21079335e7 | ||
|
|
85741240f1 | ||
|
|
3df9a87b29 | ||
|
|
acd60007ac | ||
|
|
f66c9630e3 | ||
|
|
50a5892a31 | ||
|
|
c5f58437b1 | ||
|
|
a915424d9a | ||
|
|
be7699c600 | ||
|
|
a7c946ddae | ||
|
|
4b741a9434 | ||
|
|
cd963272d4 | ||
|
|
9de17ead91 | ||
|
|
d98b133d63 | ||
|
|
361e8e0066 | ||
|
|
523c44b796 | ||
|
|
4bd60ed6a1 | ||
|
|
7d57f08baf | ||
|
|
5e7f54dbc3 | ||
|
|
dfb9ce990d | ||
|
|
e87a368e87 | ||
|
|
44506057fd | ||
|
|
ff08eadb23 | ||
|
|
ebb1a3feb2 | ||
|
|
81bc85b8e4 | ||
|
|
4a749f52e9 | ||
|
|
25e37ddd78 | ||
|
|
46a9cfcc67 | ||
|
|
66ff944660 | ||
|
|
988db59197 | ||
|
|
34e321407a | ||
|
|
44c0028a51 | ||
|
|
264b75cd68 | ||
|
|
cf9ccdcab6 | ||
|
|
48e18e533c | ||
|
|
12ef778bf4 | ||
|
|
6c31a1788a | ||
|
|
aa7c6da8ef | ||
|
|
1ff40cace0 | ||
|
|
0a1dadbea1 | ||
|
|
24edbdba5e | ||
|
|
db504eff88 | ||
|
|
5e09925659 | ||
|
|
d1b2b351c8 | ||
|
|
50b0dc178e | ||
|
|
b34118e395 | ||
|
|
e6436f9021 | ||
|
|
831d1b4f3a | ||
|
|
0456d1ca29 | ||
|
|
fcd151022e | ||
|
|
0c1998002e | ||
|
|
b8c33ce7fa | ||
|
|
454cd633e8 | ||
|
|
53cb9a6e10 | ||
|
|
8d6ff763d6 | ||
|
|
ecf15af3a0 | ||
|
|
716823789d | ||
|
|
3c192badce | ||
|
|
d94b611d10 | ||
|
|
e2ce329b6c | ||
|
|
ef902fc706 | ||
|
|
a7fc94c303 | ||
|
|
c8c6d1bb1e | ||
|
|
a205108681 | ||
|
|
31561879b3 | ||
|
|
fdb6677153 | ||
|
|
17241be4bc | ||
|
|
3bf6de9c76 | ||
|
|
a53322e7cb | ||
|
|
a107c126e4 | ||
|
|
3a8dcae53a | ||
|
|
b99d4532df | ||
|
|
6e8f8ea357 | ||
|
|
83838bbe76 | ||
|
|
66620aabe2 | ||
|
|
74fcf8270d | ||
|
|
91b4e32a38 | ||
|
|
3f0c114f24 | ||
|
|
30fe6b93c4 | ||
|
|
ec99242314 | ||
|
|
9149fb7a85 | ||
|
|
3c919d9a8a | ||
|
|
22f9ce1e2e | ||
|
|
72b01cec70 | ||
|
|
982fb212e4 | ||
|
|
9fcee9cc53 | ||
|
|
57b8bc2645 | ||
|
|
3ad4ca3943 | ||
|
|
237ffeff52 | ||
|
|
fb491d9790 | ||
|
|
facd03b6ad | ||
|
|
f5f38ca670 | ||
|
|
83e599f0e9 | ||
|
|
48cd06f37e | ||
|
|
aac9d2d1c4 | ||
|
|
7e2e1c12b9 | ||
|
|
3410dd4eba | ||
|
|
d511085db3 | ||
|
|
caaa3583a7 | ||
|
|
185e5691a4 | ||
|
|
20f44cc64f | ||
|
|
07ef94afd9 | ||
|
|
2d33b8a958 | ||
|
|
d283f5dbb4 | ||
|
|
1af76c4d06 | ||
|
|
07498c6bef | ||
|
|
18f9ebbc4f | ||
|
|
85b4f652f4 | ||
|
|
eec6f7336c | ||
|
|
f976dd8d30 | ||
|
|
2d3aa3a96e | ||
|
|
1195cbd772 | ||
|
|
cebfa674ef | ||
|
|
3ed596496d | ||
|
|
d99ef78348 | ||
|
|
2666c1e196 | ||
|
|
4b8c8888ee | ||
|
|
ce1fd64aa9 | ||
|
|
bd1479b14a | ||
|
|
baee9520d1 | ||
|
|
adb30a526e | ||
|
|
60c40457fc | ||
|
|
a6df457c9c | ||
|
|
3fd8fb0308 | ||
|
|
7d4a7d4ab6 | ||
|
|
4571b95683 | ||
|
|
a65d2a1bba | ||
|
|
5c9503732d | ||
|
|
2dbbf7f350 | ||
|
|
e1cce220b3 | ||
|
|
e0aed26f63 | ||
|
|
62fd07258e | ||
|
|
0d8127140f | ||
|
|
bab5326d7c | ||
|
|
5c8445f3fe | ||
|
|
604488b166 | ||
|
|
b7d7fc8b67 | ||
|
|
1f4872f72b | ||
|
|
f89c40cde6 | ||
|
|
670edbc22a | ||
|
|
54f70c8158 | ||
|
|
0d6bcba023 | ||
|
|
06a9f95979 | ||
|
|
03cdfc259e | ||
|
|
1963b5732a | ||
|
|
1e05dc937c | ||
|
|
c3b22ff737 | ||
|
|
991f07e148 | ||
|
|
8efb849391 | ||
|
|
5b310f3e46 | ||
|
|
5f54e42a43 | ||
|
|
a83f20e454 | ||
|
|
cdab86bd83 | ||
|
|
48cbb80e79 | ||
|
|
1af6200655 | ||
|
|
b8585594cd | ||
|
|
d173cc982c | ||
|
|
471c57b3ed | ||
|
|
093d7d783b | ||
|
|
7cc20600e7 | ||
|
|
559c654fbb | ||
|
|
01a03e3e97 | ||
|
|
dd3233a556 | ||
|
|
c4a426566f | ||
|
|
c081a596df | ||
|
|
6b07908cf7 | ||
|
|
2985bd0a1c | ||
|
|
975e8487fc | ||
|
|
54408d3ec8 | ||
|
|
8b3afc1f49 | ||
|
|
e15bc5a232 | ||
|
|
8fdda59440 | ||
|
|
d5cbf362f8 | ||
|
|
6f85d3370c | ||
|
|
8c324cd185 | ||
|
|
f7f7fda040 | ||
|
|
74c11f4d4e | ||
|
|
0525cfab79 | ||
|
|
2d73627908 | ||
|
|
94d015789d | ||
|
|
af2f6246e8 | ||
|
|
ebbdf0294a | ||
|
|
5df539f0a4 | ||
|
|
4f6dfeb773 | ||
|
|
52bb83e878 | ||
|
|
286a8626d1 | ||
|
|
aa62dc3f32 | ||
|
|
045e7977d5 | ||
|
|
e0dc25ad23 | ||
|
|
9d0499a306 | ||
|
|
b971e002ce | ||
|
|
5fd488e210 | ||
|
|
eb84da4c51 | ||
|
|
49243148a2 | ||
|
|
276241cdff | ||
|
|
6772453b3a | ||
|
|
18e2f1f90c | ||
|
|
67d2175ce9 | ||
|
|
8a3283f1ea | ||
|
|
2fb47fc186 | ||
|
|
4530c9a40c | ||
|
|
a867a25d5f | ||
|
|
10910ac2ed | ||
|
|
514f5a0c81 | ||
|
|
6b18ed6477 | ||
|
|
e07b6cc409 | ||
|
|
5ae67fda7f | ||
|
|
1a03c0ca9f | ||
|
|
dc211b4d00 | ||
|
|
2e767ffde1 | ||
|
|
9f74a54c0d | ||
|
|
a7870495ac | ||
|
|
a8ec323ea8 | ||
|
|
4b7d243406 | ||
|
|
1855588270 | ||
|
|
e545e0c3a5 | ||
|
|
b24a0f1595 | ||
|
|
3f2bfc3050 | ||
|
|
66f100d534 | ||
|
|
e641d5ba0f | ||
|
|
6f8cebf954 | ||
|
|
2d2f2457cb | ||
|
|
68fa243af4 | ||
|
|
9cb133c8a6 | ||
|
|
33f6c75917 | ||
|
|
49acd5e8c2 | ||
|
|
85266adf20 | ||
|
|
27edea8380 | ||
|
|
dccddb8d43 | ||
|
|
5f9d7a223a | ||
|
|
29b71262d6 | ||
|
|
0cfcbcf5df | ||
|
|
e1258384d8 | ||
|
|
8837eddd40 | ||
|
|
f919d46e9a | ||
|
|
98094fac63 | ||
|
|
f94ccee252 | ||
|
|
7c4b338539 | ||
|
|
d88431f9e3 | ||
|
|
5f86068489 | ||
|
|
36bc226674 | ||
|
|
7947745218 | ||
|
|
19697aabfb | ||
|
|
87af23598d | ||
|
|
4ed7966a5a | ||
|
|
8d86b5c7e6 | ||
|
|
cdf9458962 | ||
|
|
83df37c2d1 | ||
|
|
bc7b3165c8 | ||
|
|
c332e9764a | ||
|
|
004d48d36c | ||
|
|
28b263a445 | ||
|
|
e0e9310907 | ||
|
|
68b9159b2b | ||
|
|
f5940cbf70 | ||
|
|
bfb143bb51 | ||
|
|
64662d5215 | ||
|
|
31cedad976 | ||
|
|
9682df6240 | ||
|
|
a718908385 | ||
|
|
12ac5147d3 | ||
|
|
17291ff61d | ||
|
|
f3e334470e | ||
|
|
9407051f1e | ||
|
|
08a6d36153 | ||
|
|
f906fb9b4c | ||
|
|
b4a6356724 | ||
|
|
8eca14069a | ||
|
|
1af0477ac0 | ||
|
|
43920f12ae | ||
|
|
5a0beec6cb | ||
|
|
98241fb54b | ||
|
|
3051664228 | ||
|
|
1ed774365c | ||
|
|
f2e33dfd58 | ||
|
|
90b5282b39 | ||
|
|
d520fc46b9 | ||
|
|
e69b5988ec | ||
|
|
3cdc1ba794 | ||
|
|
25e1008c5c | ||
|
|
a093862311 | ||
|
|
53057ea9fc | ||
|
|
94db96de3e | ||
|
|
60e4fbbf75 | ||
|
|
d35dd67632 | ||
|
|
8813985c68 | ||
|
|
538c98b587 | ||
|
|
389c7029cf | ||
|
|
d47f9e1131 | ||
|
|
c0a8d29756 | ||
|
|
668fd34af6 | ||
|
|
8aa08cf8fe | ||
|
|
16c04f5ae4 | ||
|
|
32c16021c4 | ||
|
|
7713c1173a | ||
|
|
8018f400c3 | ||
|
|
79c8293881 | ||
|
|
db5652623b | ||
|
|
0f7a48b323 | ||
|
|
309fbab2e6 | ||
|
|
99da145d65 | ||
|
|
415d2826c6 | ||
|
|
7787e7085e | ||
|
|
4ab8417168 | ||
|
|
a77e76d5c6 | ||
|
|
ca6660e2ff | ||
|
|
af94410c55 | ||
|
|
b47bc50147 | ||
|
|
5ff77c16ab | ||
|
|
11618260cf | ||
|
|
63f9006d17 | ||
|
|
7779acc7bc | ||
|
|
aacd92eee3 | ||
|
|
1bf8be2874 | ||
|
|
66f2d0c7dc | ||
|
|
597d952254 | ||
|
|
288595ce5d | ||
|
|
c89e8c78d3 | ||
|
|
81a37e3fc4 | ||
|
|
368c590976 | ||
|
|
6e982e646d | ||
|
|
030582b2d5 | ||
|
|
7dd4b10a96 | ||
|
|
b055e79b4c | ||
|
|
ba980aa93f | ||
|
|
15baf04ce9 | ||
|
|
4dc2587817 | ||
|
|
d5e046c289 | ||
|
|
ef8073ac58 | ||
|
|
38c9d25214 | ||
|
|
e2f0e4089f | ||
|
|
80ce2c04ed | ||
|
|
4ebd82beeb | ||
|
|
8cc43cd9a6 | ||
|
|
15190abb69 | ||
|
|
0b28159e8e | ||
|
|
d9e8f8e69b | ||
|
|
fa224e46bc | ||
|
|
06320953e8 | ||
|
|
2163334c4f | ||
|
|
f5d180af6b | ||
|
|
d676084cb3 | ||
|
|
0cb5941be0 | ||
|
|
732494dfc5 | ||
|
|
b8748b856a | ||
|
|
cc71f15700 | ||
|
|
124ef640b1 | ||
|
|
f5e3df0cd2 | ||
|
|
c8431181c8 | ||
|
|
07fb5ab017 | ||
|
|
6735b257b4 | ||
|
|
cef242a9ce | ||
|
|
2923d917e5 | ||
|
|
1d1639e5e1 | ||
|
|
6ab05fdb76 | ||
|
|
91ae8c0aaf | ||
|
|
fcb69c0190 | ||
|
|
2cf6fe4352 | ||
|
|
e3a2623a53 | ||
|
|
e0c0a423c1 | ||
|
|
fd99246c49 | ||
|
|
d247edd870 | ||
|
|
9a76a9069c | ||
|
|
8e1d796870 | ||
|
|
8b0d4e5c3b | ||
|
|
b9e257a39d | ||
|
|
e7eaa5fd58 | ||
|
|
c9aa992e73 | ||
|
|
f325930f68 | ||
|
|
1346ffb77e | ||
|
|
3378746530 | ||
|
|
ce2d94f04e | ||
|
|
b3c2a1e6c5 | ||
|
|
dbf63787da | ||
|
|
88a7ebef69 | ||
|
|
a716151dd9 | ||
|
|
7462f1b7a5 | ||
|
|
ec76b9dc5c | ||
|
|
79cd96ade9 | ||
|
|
a5b84406be | ||
|
|
8c1a04c4b2 | ||
|
|
ee81037173 | ||
|
|
453349be26 | ||
|
|
81a9e06b23 | ||
|
|
7d8af0f252 | ||
|
|
a68cd7526b | ||
|
|
470ca3b6dc | ||
|
|
e8bae61afc | ||
|
|
c1f663a200 | ||
|
|
22b2e21df0 | ||
|
|
5f19710791 | ||
|
|
d3f3ff4eab | ||
|
|
5af7425cae | ||
|
|
fe10c9f8c8 | ||
|
|
cd2a085d00 | ||
|
|
3c61626370 | ||
|
|
351fe5848f | ||
|
|
ca7bbefbdc | ||
|
|
7094f71e32 | ||
|
|
88b5e9db87 | ||
|
|
b4ab07bd78 | ||
|
|
fd6ad6dce3 | ||
|
|
ab97068a1d | ||
|
|
70fe3b9773 | ||
|
|
1fe8079fd5 | ||
|
|
80627997d1 | ||
|
|
12abdcaf6c | ||
|
|
a6ed4d92c9 | ||
|
|
0471640f54 | ||
|
|
4cf3e82fb5 | ||
|
|
fbbe999806 | ||
|
|
76af488d35 | ||
|
|
a54d2a5f22 | ||
|
|
a1df075194 | ||
|
|
4de2182b40 | ||
|
|
6fa88123f1 | ||
|
|
f81dbde15e | ||
|
|
484fbc6b9d | ||
|
|
8ba30135a1 | ||
|
|
ba5a72fdad | ||
|
|
70b39ddadf | ||
|
|
8200c0b0ab | ||
|
|
59ebfa6cc7 | ||
|
|
57b694162d | ||
|
|
2e6bdc225f | ||
|
|
8ced689432 | ||
|
|
53a8f6b4c0 | ||
|
|
9ae1a55896 | ||
|
|
5ecafe214f | ||
|
|
8baf0ad6af | ||
|
|
3cc64b5764 | ||
|
|
19cf07564f | ||
|
|
5847ce5c14 | ||
|
|
a7ad45635e | ||
|
|
781215394e | ||
|
|
0aafdca999 | ||
|
|
18d3cb6f0c | ||
|
|
263a96e8b7 | ||
|
|
27d5009486 | ||
|
|
4d1a91baa6 | ||
|
|
1898efa282 | ||
|
|
648ab4d736 | ||
|
|
407cac588a | ||
|
|
210dcfb989 | ||
|
|
2e431b1135 | ||
|
|
e2ec27250c | ||
|
|
1228eda5ea | ||
|
|
435794df73 | ||
|
|
7e3d0639f7 | ||
|
|
86b0005821 | ||
|
|
2fb78275f7 | ||
|
|
98f421c697 | ||
|
|
a5572b7d45 | ||
|
|
fdecbaaa6a | ||
|
|
c6afd7fa24 | ||
|
|
5cad522a60 | ||
|
|
82f64677cb | ||
|
|
3ee086a063 | ||
|
|
13da444a69 | ||
|
|
b51ceaaadc | ||
|
|
2024c72209 | ||
|
|
b5959c55e1 | ||
|
|
16f0ac97f4 | ||
|
|
073c02ee0c | ||
|
|
786f0db4bb | ||
|
|
6958e4b74f | ||
|
|
22c4fba665 | ||
|
|
f7c0e56cec | ||
|
|
5f423cd22e | ||
|
|
282b3a109c | ||
|
|
dddb051d8b | ||
|
|
63d3706003 | ||
|
|
22ca2494f5 | ||
|
|
a0f02b6877 | ||
|
|
487fcff61f | ||
|
|
bfb8897188 | ||
|
|
ce1ccf378a | ||
|
|
96d5ee3d46 | ||
|
|
6f3771e7cd | ||
|
|
e6b00b05a2 | ||
|
|
148d0afe81 | ||
|
|
c5d63dbdb9 | ||
|
|
1af072b059 | ||
|
|
a6a8fdd2f8 | ||
|
|
8836021ff9 | ||
|
|
915803c5be | ||
|
|
cd95d43457 | ||
|
|
35af5fd13c | ||
|
|
35e602f75f | ||
|
|
b4f0a1acc0 | ||
|
|
7b7e9f6868 | ||
|
|
de3892950c | ||
|
|
5c53826da3 | ||
|
|
d358073081 | ||
|
|
d2d7fd7c4c | ||
|
|
706da768e2 | ||
|
|
408073ee19 | ||
|
|
2695b7fc38 | ||
|
|
c32b6393af | ||
|
|
abbb4e793f | ||
|
|
e4fae2d660 | ||
|
|
dc572c3815 | ||
|
|
c65b03db41 | ||
|
|
45d2e1f5e2 | ||
|
|
b658f5bd0e | ||
|
|
f199d85d5b | ||
|
|
445dfaaeb4 | ||
|
|
cccd4122c5 | ||
|
|
1fcbe828bd | ||
|
|
e94b78a37b | ||
|
|
af7cecb667 | ||
|
|
4068e9c6b8 | ||
|
|
185c1fcdac | ||
|
|
29c5d35c08 | ||
|
|
0117f54ef8 | ||
|
|
56d41916c4 | ||
|
|
fddf73b1bb | ||
|
|
d670c2ae5e | ||
|
|
6f5c9eb600 | ||
|
|
49c416c90d | ||
|
|
8fef468eb9 | ||
|
|
976b1e1e0f | ||
|
|
d6de7cca96 | ||
|
|
fb02751bdd | ||
|
|
3fb5c52af1 | ||
|
|
18d17570f6 | ||
|
|
fa5eb16054 | ||
|
|
b1c77b508a | ||
|
|
b982ca2c5b | ||
|
|
219753039d | ||
|
|
c16f9af6a9 | ||
|
|
a544b0dc19 | ||
|
|
a398f07f9f | ||
|
|
7ecdee7a89 | ||
|
|
24361ccd97 | ||
|
|
3afe6df9f2 | ||
|
|
df7f79004d | ||
|
|
405bce9f82 | ||
|
|
b9ef7af791 | ||
|
|
761891abe9 | ||
|
|
6a52637695 | ||
|
|
b4b957d2c3 | ||
|
|
dda4a0a887 | ||
|
|
b069a936ab | ||
|
|
15c088ec21 | ||
|
|
015d70afb6 | ||
|
|
2158f69c09 | ||
|
|
c49c5fd58c | ||
|
|
26be131b4b | ||
|
|
f8533eb2c6 | ||
|
|
4cc545659b | ||
|
|
70a0b5c22f | ||
|
|
a2976d9ad5 | ||
|
|
01b05f186e | ||
|
|
79a4da9db8 | ||
|
|
84890fd5ad | ||
|
|
29f9d0c1cd | ||
|
|
95169bbc84 | ||
|
|
9a2979e577 | ||
|
|
9f381a7b30 | ||
|
|
c07ad348bd | ||
|
|
d7ae2e4307 | ||
|
|
71b86b3cbc | ||
|
|
621e8078d9 | ||
|
|
7155ab8bdc | ||
|
|
0a3e788d21 | ||
|
|
f83c46d1c7 | ||
|
|
11932353f7 | ||
|
|
ed373107c2 | ||
|
|
1cca15ca5d | ||
|
|
11e59a970e | ||
|
|
5abb69d022 | ||
|
|
dc1d497ff3 | ||
|
|
7f909fa098 | ||
|
|
320f064775 | ||
|
|
c9b1691998 | ||
|
|
08ade8371c | ||
|
|
081ac2d109 | ||
|
|
3fde546b83 | ||
|
|
a12b3cb51a | ||
|
|
6160945b9e | ||
|
|
6a126009a8 | ||
|
|
891e71aec6 | ||
|
|
cd164049b7 | ||
|
|
709a47bc6b | ||
|
|
9c6cd80867 | ||
|
|
832d9a2ab8 | ||
|
|
8d4e30a2e4 | ||
|
|
baf41eb104 | ||
|
|
7f0fe1681b | ||
|
|
49189bc63e | ||
|
|
87f30ed3d5 | ||
|
|
aca390ee19 | ||
|
|
598bb6d742 | ||
|
|
33b19e40e0 | ||
|
|
5b9401fafe | ||
|
|
9bed6b7e22 | ||
|
|
aca003c554 | ||
|
|
3c806558eb | ||
|
|
d195d46f8f | ||
|
|
755abc6487 | ||
|
|
718282bae3 | ||
|
|
8cc4d08eae | ||
|
|
0bcc02dfab | ||
|
|
33c3fb7de0 | ||
|
|
d99b8f5864 | ||
|
|
98bf63e94b | ||
|
|
2d8b1299b3 | ||
|
|
5edc4abfb4 | ||
|
|
26f7264f3c | ||
|
|
56c82d7f0f | ||
|
|
6946da3571 | ||
|
|
2985c762e6 | ||
|
|
8001d940eb | ||
|
|
2f440eba37 | ||
|
|
bb9cb2fb75 | ||
|
|
5531c15126 | ||
|
|
29f049c411 | ||
|
|
31180afbd1 | ||
|
|
435b856b72 | ||
|
|
af62526b92 | ||
|
|
beb7d09aee | ||
|
|
b4b1b7a3fa | ||
|
|
d0342598c4 | ||
|
|
4a71b00b71 | ||
|
|
268bbf3b9e | ||
|
|
24cdeb06e8 | ||
|
|
6b64d85db0 | ||
|
|
08e542dfb3 | ||
|
|
8b997cffdd | ||
|
|
33aece6f96 | ||
|
|
6040eea3bd | ||
|
|
9b3ca65492 | ||
|
|
c76f368fa0 | ||
|
|
664d28f105 | ||
|
|
7daee36d3e | ||
|
|
5fc10fe041 | ||
|
|
6692de33b1 | ||
|
|
494bd425af | ||
|
|
135ce2285d | ||
|
|
be2e82788f | ||
|
|
e84fb63343 | ||
|
|
97422c1d7a | ||
|
|
66cc739bb2 | ||
|
|
790454f194 | ||
|
|
d751966078 | ||
|
|
a8e9f7b445 | ||
|
|
144afc346d | ||
|
|
63854156eb | ||
|
|
7800e76b26 | ||
|
|
1e5b95f64a | ||
|
|
097e36677a | ||
|
|
7e6e10e3ef | ||
|
|
f72ce697a7 | ||
|
|
e6843bc3e0 | ||
|
|
5c0cf09c42 | ||
|
|
ab14bdbb18 | ||
|
|
8dc43dab59 | ||
|
|
0c5a6a7548 | ||
|
|
779e2f4633 | ||
|
|
644e3e200d | ||
|
|
670cc474a4 | ||
|
|
c43820776f | ||
|
|
749740242e | ||
|
|
f864746b54 | ||
|
|
42dcd4750a | ||
|
|
cb0c6a344f | ||
|
|
d1d652495d | ||
|
|
4b574cecf7 | ||
|
|
e7dbaf78b5 | ||
|
|
6f83b932b0 | ||
|
|
4552b2b158 | ||
|
|
5b708e77aa | ||
|
|
a3f1e46107 | ||
|
|
097808752d | ||
|
|
39be268969 | ||
|
|
6023d53506 | ||
|
|
1a6e653600 | ||
|
|
586c707e51 | ||
|
|
6ca941e8e9 | ||
|
|
b83675cdd0 | ||
|
|
1a583913a7 | ||
|
|
d98a644b75 | ||
|
|
11c0c93fe2 | ||
|
|
6d78867d69 | ||
|
|
54813b8b93 | ||
|
|
6ab31c67fc | ||
|
|
69af2ff7e8 | ||
|
|
b505a2a94d | ||
|
|
6a5fd0d6f0 | ||
|
|
e21557645b | ||
|
|
28cab146f3 | ||
|
|
43df4ae0e7 | ||
|
|
5037027030 | ||
|
|
5e11684665 | ||
|
|
c7793beb0f | ||
|
|
33b9e6d0c1 | ||
|
|
d87e8b729f | ||
|
|
0db08f4c62 | ||
|
|
e9796c9a35 | ||
|
|
36350bd71a | ||
|
|
e53a2255a9 | ||
|
|
d42f911df9 | ||
|
|
1b2d922c3f | ||
|
|
a5c5486474 | ||
|
|
926f0f85f3 | ||
|
|
63d430c3d9 | ||
|
|
26a009b397 | ||
|
|
be115c74c3 | ||
|
|
049721bbfe | ||
|
|
8b4e76832f | ||
|
|
25a51a71a0 | ||
|
|
f4d6e98d61 | ||
|
|
eee496a050 | ||
|
|
89a83a625b | ||
|
|
c17df24a19 | ||
|
|
bbcc2f4be4 | ||
|
|
5e63d9015f | ||
|
|
4958b89636 | ||
|
|
f4b6e9c25a | ||
|
|
a59d407f12 | ||
|
|
c95cb79672 | ||
|
|
73e7fa0f85 | ||
|
|
7f81b83955 | ||
|
|
a42ae62042 | ||
|
|
165357f444 | ||
|
|
e4c928ae87 | ||
|
|
d51e3de674 | ||
|
|
b0476c7017 | ||
|
|
1de9f715fa | ||
|
|
f15e048763 | ||
|
|
422b324f7c | ||
|
|
fb163367d4 | ||
|
|
c91eec8b3e | ||
|
|
c16eee79d4 | ||
|
|
1e86d85035 | ||
|
|
abfc2fea3e | ||
|
|
991d61600d | ||
|
|
0f4713bddc | ||
|
|
51205fffa0 | ||
|
|
9f4fd92452 | ||
|
|
354fccab8b | ||
|
|
a6ba87c2f8 | ||
|
|
ae8aa0374f | ||
|
|
dc5e073715 | ||
|
|
8c68ff5419 | ||
|
|
bc10408729 | ||
|
|
96b059f657 | ||
|
|
4db2ae53cb | ||
|
|
f7308f53d3 | ||
|
|
17fbd78b97 | ||
|
|
3ad03ebc1d | ||
|
|
32e4e69930 | ||
|
|
b20f7aca53 | ||
|
|
e72ee606fd | ||
|
|
1386d1ae32 | ||
|
|
903d678f30 | ||
|
|
828a786414 | ||
|
|
3ee8e7b755 | ||
|
|
0d0448d86b | ||
|
|
5da4762f40 | ||
|
|
36bcb2ce92 | ||
|
|
151d7c1ba5 | ||
|
|
19d444e387 | ||
|
|
e41c718bb3 | ||
|
|
2e5d91a5bc | ||
|
|
ae184ab894 | ||
|
|
1ceedf2372 | ||
|
|
416c05ed3b | ||
|
|
623da7eade | ||
|
|
84b3d6db76 | ||
|
|
25a27c9b11 | ||
|
|
456d59f582 | ||
|
|
1d641cdf82 | ||
|
|
4420d7962e | ||
|
|
21e6e3df3f | ||
|
|
7e04098971 | ||
|
|
24df4dd0c1 | ||
|
|
8ed87e77be | ||
|
|
10c0ce26dc | ||
|
|
3a38b59dad | ||
|
|
24820b914c | ||
|
|
97339fd45d | ||
|
|
a1314d645b | ||
|
|
571a4dca7a | ||
|
|
36f5e62537 | ||
|
|
a21b716d0a | ||
|
|
c25859cee9 | ||
|
|
79dc5e4344 | ||
|
|
c81aef6d05 | ||
|
|
c744e36f3b | ||
|
|
9d96b3d1d0 | ||
|
|
b246c638fd | ||
|
|
15dd8dfe8c | ||
|
|
50859fd661 | ||
|
|
eada994a82 | ||
|
|
3da2046fa0 | ||
|
|
989ca08c94 | ||
|
|
e9ccd7120d | ||
|
|
c15ae293aa | ||
|
|
50501aef56 | ||
|
|
45747183e7 | ||
|
|
c9424d6f8d | ||
|
|
5ecd8b41e5 | ||
|
|
01d6dee9fc | ||
|
|
12865ac7cc | ||
|
|
362f0b2fe5 | ||
|
|
9f800df5ad | ||
|
|
e937f1b601 | ||
|
|
5b387a0b11 | ||
|
|
9226b165bd | ||
|
|
a6586c9d1c | ||
|
|
0a36d759e4 | ||
|
|
328bcd0532 | ||
|
|
ec76e9cf2a | ||
|
|
88ae996694 | ||
|
|
d1d1d05ce7 | ||
|
|
c62a03c97b | ||
|
|
65cc57ec03 | ||
|
|
806c9a57fc | ||
|
|
3a18ce0cf9 | ||
|
|
4fde6e1293 | ||
|
|
31b43301a4 | ||
|
|
6a77e412fa | ||
|
|
53a6424348 | ||
|
|
c8d933469a | ||
|
|
e456373671 | ||
|
|
afdb806083 | ||
|
|
02c63dd22f | ||
|
|
529f9a2fb9 | ||
|
|
6b0bcf93d3 | ||
|
|
13afe33244 | ||
|
|
d441bccf8b | ||
|
|
e6847355e7 | ||
|
|
72051c8660 | ||
|
|
87afc64f16 | ||
|
|
870fef3ea6 | ||
|
|
e5ac8a0a67 | ||
|
|
87fcc0afe6 | ||
|
|
79830870dd | ||
|
|
69ad40c27f | ||
|
|
1ac7ce00fb | ||
|
|
e239bca0f2 | ||
|
|
8729fe48c3 | ||
|
|
441c55eb31 | ||
|
|
5291a6856e | ||
|
|
e011f99161 | ||
|
|
64a756cc04 | ||
|
|
f57e90b35c | ||
|
|
5a5d242ea0 | ||
|
|
4afea27fa5 | ||
|
|
fc8042aa25 | ||
|
|
2b6220beb8 | ||
|
|
78426a6c7b | ||
|
|
620e53c255 | ||
|
|
753fc6c769 | ||
|
|
3d6e1dfc0a | ||
|
|
d92431ad65 | ||
|
|
be19d1f5b5 | ||
|
|
ca08a52998 | ||
|
|
e54822f3b0 | ||
|
|
3863e657ef | ||
|
|
341ef79b49 | ||
|
|
335f34b824 | ||
|
|
c864863be4 | ||
|
|
a22687e2d8 | ||
|
|
44475853df | ||
|
|
309d7e704c | ||
|
|
ecf9ce586c | ||
|
|
d0de9e5e21 | ||
|
|
2154a5e1db | ||
|
|
dbe51ccaf3 | ||
|
|
993d53ed97 | ||
|
|
730e2da932 | ||
|
|
18a198496b | ||
|
|
5eb791fd65 | ||
|
|
27cc022fb8 | ||
|
|
35f244cf50 | ||
|
|
fa30bfc04b | ||
|
|
a072466f75 | ||
|
|
ae5c898537 | ||
|
|
921a37f4a2 |
@@ -1,6 +1,6 @@
|
|||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*.{js,ts,tsx}]
|
[*.{js,ts,tsx,css}]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|||||||
4
.github/actions/build-electron/action.yml
vendored
4
.github/actions/build-electron/action.yml
vendored
@@ -21,7 +21,7 @@ runs:
|
|||||||
# Certificate setup
|
# Certificate setup
|
||||||
- name: Import Apple certificates
|
- name: Import Apple certificates
|
||||||
if: inputs.os == 'macos'
|
if: inputs.os == 'macos'
|
||||||
uses: apple-actions/import-codesign-certs@v5
|
uses: apple-actions/import-codesign-certs@v6
|
||||||
with:
|
with:
|
||||||
p12-file-base64: ${{ env.APPLE_APP_CERTIFICATE_BASE64 }}
|
p12-file-base64: ${{ env.APPLE_APP_CERTIFICATE_BASE64 }}
|
||||||
p12-password: ${{ env.APPLE_APP_CERTIFICATE_PASSWORD }}
|
p12-password: ${{ env.APPLE_APP_CERTIFICATE_PASSWORD }}
|
||||||
@@ -30,7 +30,7 @@ runs:
|
|||||||
|
|
||||||
- name: Install Installer certificate
|
- name: Install Installer certificate
|
||||||
if: inputs.os == 'macos'
|
if: inputs.os == 'macos'
|
||||||
uses: apple-actions/import-codesign-certs@v5
|
uses: apple-actions/import-codesign-certs@v6
|
||||||
with:
|
with:
|
||||||
p12-file-base64: ${{ env.APPLE_INSTALLER_CERTIFICATE_BASE64 }}
|
p12-file-base64: ${{ env.APPLE_INSTALLER_CERTIFICATE_BASE64 }}
|
||||||
p12-password: ${{ env.APPLE_INSTALLER_CERTIFICATE_PASSWORD }}
|
p12-password: ${{ env.APPLE_INSTALLER_CERTIFICATE_PASSWORD }}
|
||||||
|
|||||||
2
.github/actions/report-size/action.yml
vendored
2
.github/actions/report-size/action.yml
vendored
@@ -44,7 +44,7 @@ runs:
|
|||||||
steps:
|
steps:
|
||||||
# Checkout branch to compare to [required]
|
# Checkout branch to compare to [required]
|
||||||
- name: Checkout base branch
|
- name: Checkout base branch
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.branch }}
|
ref: ${{ inputs.branch }}
|
||||||
path: br-base
|
path: br-base
|
||||||
|
|||||||
334
.github/copilot-instructions.md
vendored
Normal file
334
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
# Trilium Notes - AI Coding Agent Instructions
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. Built as a TypeScript monorepo using pnpm, it implements a three-layer caching architecture (Becca/Froca/Shaca) with a widget-based UI system and supports extensive user scripting capabilities.
|
||||||
|
|
||||||
|
## Essential Architecture Patterns
|
||||||
|
|
||||||
|
### Three-Layer Cache System (Critical to Understand)
|
||||||
|
- **Becca** (`apps/server/src/becca/`): Server-side entity cache, primary data source
|
||||||
|
- **Froca** (`apps/client/src/services/froca.ts`): Client-side mirror synchronized via WebSocket
|
||||||
|
- **Shaca** (`apps/server/src/share/`): Optimized cache for public/shared notes
|
||||||
|
|
||||||
|
**Key insight**: Never bypass these caches with direct DB queries. Always use `becca.notes[noteId]`, `froca.getNote()`, or equivalent cache methods.
|
||||||
|
|
||||||
|
### Entity Relationship Model
|
||||||
|
Notes use a **multi-parent tree** via branches:
|
||||||
|
- `BNote` - The note content and metadata
|
||||||
|
- `BBranch` - Tree relationships (one note can have multiple parents via cloning)
|
||||||
|
- `BAttribute` - Key-value metadata attached to notes (labels and relations)
|
||||||
|
|
||||||
|
### Entity Change System & Sync
|
||||||
|
Every entity modification (notes, branches, attributes) creates an `EntityChange` record that drives synchronization:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Entity changes are automatically tracked
|
||||||
|
note.title = "New Title";
|
||||||
|
note.save(); // Creates EntityChange record with changeId
|
||||||
|
|
||||||
|
// Sync protocol via WebSocket
|
||||||
|
ws.sendMessage({ type: 'sync-pull-in-progress', ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical**: This is why you must use Becca/Froca methods instead of direct DB writes - they create the change tracking records needed for sync.
|
||||||
|
|
||||||
|
### Entity Lifecycle & Events
|
||||||
|
The event system (`apps/server/src/services/events.ts`) broadcasts entity lifecycle events:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Subscribe to events in widgets or services
|
||||||
|
eventService.subscribe('noteChanged', ({ noteId }) => {
|
||||||
|
// React to note changes
|
||||||
|
});
|
||||||
|
|
||||||
|
// Common events: noteChanged, branchChanged, attributeChanged, noteDeleted
|
||||||
|
// Widget method: entitiesReloadedEvent({loadResults}) for handling reloads
|
||||||
|
```
|
||||||
|
|
||||||
|
**Becca loader priorities**: Events are emitted in order (notes → branches → attributes) during initial load to ensure referential integrity.
|
||||||
|
|
||||||
|
### TaskContext for Long Operations
|
||||||
|
Use `TaskContext` for operations with progress reporting (imports, exports, bulk operations):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const taskContext = new TaskContext("task-id", "import", "Import Notes");
|
||||||
|
taskContext.increaseProgressCount();
|
||||||
|
|
||||||
|
// WebSocket messages: { type: 'taskProgressCount', taskId, taskType, data, progressCount }
|
||||||
|
|
||||||
|
**Pattern**: All long-running operations (delete note trees, export, import) use TaskContext to send WebSocket updates to the frontend.
|
||||||
|
|
||||||
|
### Protected Session Handling
|
||||||
|
Protected notes require an active encryption session:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Always check before accessing protected content
|
||||||
|
if (note.isContentAvailable()) {
|
||||||
|
const content = note.getContent(); // Safe
|
||||||
|
} else {
|
||||||
|
const title = note.getTitleOrProtected(); // Returns "[protected]"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protected session management
|
||||||
|
protectedSessionService.isProtectedSessionAvailable() // Check session
|
||||||
|
protectedSessionService.startProtectedSession() // After password entry
|
||||||
|
```
|
||||||
|
|
||||||
|
**Session timeout**: Protected sessions expire after inactivity. The encryption key is kept in memory only.
|
||||||
|
|
||||||
|
### Attribute Inheritance Patterns
|
||||||
|
Attributes can be inherited through three mechanisms:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. Standard inheritance (#hidePromotedAttributes ~hidePromotedAttributes)
|
||||||
|
note.getInheritableAttributes() // Walks up parent tree
|
||||||
|
|
||||||
|
// 2. Child prefix inheritance (child:label copies to children)
|
||||||
|
parentNote.setLabel("child:icon", "book") // All children inherit this
|
||||||
|
|
||||||
|
// 3. Template relation inheritance (#template=templateNoteId)
|
||||||
|
note.setRelation("template", templateNoteId)
|
||||||
|
note.getInheritedAttributes() // Includes template's inheritable attributes
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cycle prevention**: Inheritance tracking prevents infinite loops when notes reference each other.
|
||||||
|
|
||||||
|
### Widget-Based UI Architecture
|
||||||
|
All UI components extend from widget base classes (`apps/client/src/widgets/`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Right panel widget (sidebar)
|
||||||
|
class MyWidget extends RightPanelWidget {
|
||||||
|
get position() { return 100; } // Order in panel
|
||||||
|
get parentWidget() { return 'right-pane'; }
|
||||||
|
isEnabled() { return this.note && this.note.hasLabel('myLabel'); }
|
||||||
|
async refreshWithNote(note) { /* Update UI */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note-aware widget (responds to note changes)
|
||||||
|
class MyNoteWidget extends NoteContextAwareWidget {
|
||||||
|
async refreshWithNote(note) { /* Refresh when note changes */ }
|
||||||
|
async entitiesReloadedEvent({loadResults}) { /* Handle entity updates */ }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important**: Widgets use jQuery (`this.$widget`) for DOM manipulation. Don't mix React patterns here.
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Running & Testing
|
||||||
|
```bash
|
||||||
|
# From root directory
|
||||||
|
pnpm install # Install dependencies
|
||||||
|
corepack enable # Enable pnpm if not available
|
||||||
|
pnpm server:start # Dev server (http://localhost:8080)
|
||||||
|
pnpm server:start-prod # Production mode server
|
||||||
|
pnpm desktop:start # Desktop app development
|
||||||
|
pnpm server:test spec/etapi/search.spec.ts # Run specific test
|
||||||
|
pnpm test:parallel # Client tests (can run parallel)
|
||||||
|
pnpm test:sequential # Server tests (sequential due to shared DB)
|
||||||
|
pnpm test:all # All tests (parallel + sequential)
|
||||||
|
pnpm coverage # Generate coverage reports
|
||||||
|
pnpm typecheck # Type check all projects
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building
|
||||||
|
```bash
|
||||||
|
pnpm client:build # Build client application
|
||||||
|
pnpm server:build # Build server application
|
||||||
|
pnpm desktop:build # Build desktop application
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Organization
|
||||||
|
- **Server tests** (`apps/server/spec/`): Must run sequentially (shared database state)
|
||||||
|
- **Client tests** (`apps/client/src/`): Can run in parallel
|
||||||
|
- **E2E tests** (`apps/server-e2e/`): Use Playwright for integration testing
|
||||||
|
- **ETAPI tests** (`apps/server/spec/etapi/`): External API contract tests
|
||||||
|
|
||||||
|
**Pattern**: When adding new API endpoints, add tests in `spec/etapi/` following existing patterns (see `search.spec.ts`).
|
||||||
|
|
||||||
|
### Monorepo Navigation
|
||||||
|
```
|
||||||
|
apps/
|
||||||
|
client/ # Frontend (shared by server & desktop)
|
||||||
|
server/ # Node.js backend with REST API
|
||||||
|
desktop/ # Electron wrapper
|
||||||
|
web-clipper/ # Browser extension for saving web content
|
||||||
|
db-compare/ # Database comparison tool
|
||||||
|
dump-db/ # Database export utility
|
||||||
|
edit-docs/ # Documentation editing tools
|
||||||
|
packages/
|
||||||
|
commons/ # Shared types and utilities
|
||||||
|
ckeditor5/ # Custom rich text editor with Trilium-specific plugins
|
||||||
|
codemirror/ # Code editor integration
|
||||||
|
highlightjs/ # Syntax highlighting
|
||||||
|
share-theme/ # Theme for shared/published notes
|
||||||
|
ckeditor5-admonition/ # Admonition blocks plugin
|
||||||
|
ckeditor5-footnotes/ # Footnotes plugin
|
||||||
|
ckeditor5-math/ # Math equations plugin
|
||||||
|
ckeditor5-mermaid/ # Mermaid diagrams plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
**Filter commands**: Use `pnpm --filter server test` to run commands in specific packages.
|
||||||
|
|
||||||
|
## Critical Code Patterns
|
||||||
|
|
||||||
|
### ETAPI Backwards Compatibility
|
||||||
|
When adding query parameters to ETAPI endpoints (`apps/server/src/etapi/`), maintain backwards compatibility by checking if new params exist before changing response format.
|
||||||
|
|
||||||
|
**Pattern**: ETAPI consumers expect specific response shapes. Always check for breaking changes.
|
||||||
|
|
||||||
|
### Frontend-Backend Communication
|
||||||
|
- **REST API**: `apps/server/src/routes/api/` - Internal endpoints (no auth required when `noAuthentication=true`)
|
||||||
|
- **ETAPI**: `apps/server/src/etapi/` - External API with authentication
|
||||||
|
- **WebSocket**: Real-time sync via `apps/server/src/services/ws.ts`
|
||||||
|
|
||||||
|
**Auth note**: ETAPI uses basic auth with tokens. Internal API endpoints trust the frontend.
|
||||||
|
|
||||||
|
### Database Migrations
|
||||||
|
- Add scripts in `apps/server/src/migrations/YYMMDD_HHMM__description.sql`
|
||||||
|
- Update schema in `apps/server/src/assets/db/schema.sql`
|
||||||
|
- Never bypass Becca cache after migrations
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
1. **Never bypass the cache layers** - Always use `becca.notes[noteId]`, `froca.getNote()`, or equivalent cache methods. Direct database queries will cause sync issues between Becca/Froca/Shaca and won't create EntityChange records needed for synchronization.
|
||||||
|
|
||||||
|
2. **Protected notes require session check** - Before accessing `note.title` or `note.getContent()` on protected notes, check `note.isContentAvailable()` or use `note.getTitleOrProtected()` which handles this automatically.
|
||||||
|
|
||||||
|
3. **Widget lifecycle matters** - Override `refreshWithNote()` for note changes, `doRenderBody()` for initial render, `entitiesReloadedEvent()` for entity updates. Widgets use jQuery (`this.$widget`) - don't mix React patterns.
|
||||||
|
|
||||||
|
4. **Tests run differently** - Server tests must run sequentially (shared database state), client tests can run in parallel. Use `pnpm test:sequential` for backend, `pnpm test:parallel` for frontend.
|
||||||
|
|
||||||
|
5. **ETAPI requires authentication** - ETAPI endpoints use basic auth with tokens. Internal API endpoints (`apps/server/src/routes/api/`) trust the frontend when `noAuthentication=true`.
|
||||||
|
|
||||||
|
6. **Search expressions are evaluated in memory** - The search service loads all matching notes, scores them in JavaScript, then sorts. You cannot add SQL-level LIMIT/OFFSET without losing scoring functionality.
|
||||||
|
|
||||||
|
7. **Documentation edits have rules** - `docs/Script API/` is auto-generated (never edit directly). `docs/User Guide/` should be edited via `pnpm edit-docs:edit-docs`, not manually. Only `docs/Developer Guide/` and `docs/Release Notes/` are safe for direct Markdown editing.
|
||||||
|
|
||||||
|
8. **pnpm workspace filtering** - Use `pnpm --filter server <command>` or shorthand `pnpm server:test` defined in root `package.json`. Note the `--filter` syntax, not `-F` or other shortcuts.
|
||||||
|
|
||||||
|
9. **Event subscription cleanup** - When subscribing to events in widgets, unsubscribe in `cleanup()` or `doDestroy()` to prevent memory leaks.
|
||||||
|
|
||||||
|
10. **Attribute inheritance can be complex** - When checking for labels/relations, use `note.getOwnedAttribute()` for direct attributes or `note.getAttribute()` for inherited ones. Don't assume attributes are directly on the note.
|
||||||
|
|
||||||
|
## TypeScript Configuration
|
||||||
|
|
||||||
|
- **Project references**: Monorepo uses TypeScript project references (`tsconfig.json`)
|
||||||
|
- **Path mapping**: Use relative imports, not path aliases
|
||||||
|
- **Build order**: `pnpm typecheck` builds all projects in dependency order
|
||||||
|
- **Build system**: Uses Vite for fast development, ESBuild for production optimization
|
||||||
|
- **Patches**: Custom patches in `patches/` directory for CKEditor and other dependencies
|
||||||
|
|
||||||
|
## Key Files for Context
|
||||||
|
|
||||||
|
- `apps/server/src/becca/entities/bnote.ts` - Note entity methods
|
||||||
|
- `apps/client/src/services/froca.ts` - Frontend cache API
|
||||||
|
- `apps/server/src/services/search/services/search.ts` - Search implementation
|
||||||
|
- `apps/server/src/routes/routes.ts` - API route registration
|
||||||
|
- `apps/client/src/widgets/basic_widget.ts` - Widget base class
|
||||||
|
- `apps/server/src/main.ts` - Server startup entry point
|
||||||
|
- `apps/client/src/desktop.ts` - Client initialization
|
||||||
|
- `apps/server/src/services/backend_script_api.ts` - Scripting API
|
||||||
|
- `apps/server/src/assets/db/schema.sql` - Database schema
|
||||||
|
|
||||||
|
## Note Types and Features
|
||||||
|
|
||||||
|
Trilium supports multiple note types with specialized widgets in `apps/client/src/widgets/type_widgets/`:
|
||||||
|
- **Text**: Rich text with CKEditor5 (markdown import/export)
|
||||||
|
- **Code**: Syntax-highlighted code editing with CodeMirror
|
||||||
|
- **File**: Binary file attachments
|
||||||
|
- **Image**: Image display with editing capabilities
|
||||||
|
- **Canvas**: Drawing/diagramming with Excalidraw
|
||||||
|
- **Mermaid**: Diagram generation
|
||||||
|
- **Relation Map**: Visual note relationship mapping
|
||||||
|
- **Web View**: Embedded web pages
|
||||||
|
- **Doc/Book**: Hierarchical documentation structure
|
||||||
|
|
||||||
|
### Collections
|
||||||
|
Notes can be marked with the `#collection` label to enable collection view modes. Collections support multiple view types:
|
||||||
|
- **List**: Standard list view
|
||||||
|
- **Grid**: Card/grid layout
|
||||||
|
- **Calendar**: Calendar-based view
|
||||||
|
- **Table**: Tabular data view
|
||||||
|
- **GeoMap**: Geographic map view
|
||||||
|
- **Board**: Kanban-style board
|
||||||
|
- **Presentation**: Slideshow presentation mode
|
||||||
|
|
||||||
|
View types are configured via `#viewType` label (e.g., `#viewType=table`). Each view mode stores its configuration in a separate attachment (e.g., `table.json`). Collections are organized separately from regular note type templates in the note creation menu.
|
||||||
|
|
||||||
|
## Common Development Tasks
|
||||||
|
|
||||||
|
### Adding New Note Types
|
||||||
|
1. Create widget in `apps/client/src/widgets/type_widgets/`
|
||||||
|
2. Register in `apps/client/src/services/note_types.ts`
|
||||||
|
3. Add backend handling in `apps/server/src/services/notes.ts`
|
||||||
|
|
||||||
|
### Extending Search
|
||||||
|
- Search expressions handled in `apps/server/src/services/search/`
|
||||||
|
- Add new search operators in search context files
|
||||||
|
- Remember: scoring happens in-memory, not at database level
|
||||||
|
|
||||||
|
### Custom CKEditor Plugins
|
||||||
|
- Create new package in `packages/` following existing plugin structure
|
||||||
|
- Register in `packages/ckeditor5/src/plugins.ts`
|
||||||
|
- See `ckeditor5-admonition`, `ckeditor5-footnotes`, `ckeditor5-math`, `ckeditor5-mermaid` for examples
|
||||||
|
|
||||||
|
### Database Migrations
|
||||||
|
- Add migration scripts in `apps/server/src/migrations/YYMMDD_HHMM__description.sql`
|
||||||
|
- Update schema in `apps/server/src/assets/db/schema.sql`
|
||||||
|
- Never bypass Becca cache after migrations
|
||||||
|
|
||||||
|
## Security & Features
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
- Per-note encryption with granular protected sessions
|
||||||
|
- CSRF protection for API endpoints
|
||||||
|
- OpenID and TOTP authentication support
|
||||||
|
- Sanitization of user-generated content
|
||||||
|
|
||||||
|
### Scripting System
|
||||||
|
Trilium provides powerful user scripting capabilities:
|
||||||
|
- **Frontend scripts**: Run in browser context with UI access
|
||||||
|
- **Backend scripts**: Run in Node.js context with full API access
|
||||||
|
- Script API documentation in `docs/Script API/`
|
||||||
|
- Backend API available via `api` object in script context
|
||||||
|
|
||||||
|
### Internationalization
|
||||||
|
- Translation files in `apps/client/src/translations/`
|
||||||
|
- Use translation system via `t()` function
|
||||||
|
- Automatic pluralization: Add `_other` suffix to translation keys (e.g., `item` and `item_other` for singular/plural)
|
||||||
|
|
||||||
|
## Testing Conventions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ETAPI test pattern
|
||||||
|
describe("etapi/feature", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
config.General.noAuthentication = false;
|
||||||
|
app = await buildApp();
|
||||||
|
token = await login(app);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should test feature", async () => {
|
||||||
|
const response = await supertest(app)
|
||||||
|
.get("/etapi/notes?search=test")
|
||||||
|
.auth(USER, token, { type: "basic" })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.results).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Questions to Verify Understanding
|
||||||
|
|
||||||
|
Before implementing significant changes, confirm:
|
||||||
|
- Is this touching the cache layer? (Becca/Froca/Shaca must stay in sync via EntityChange records)
|
||||||
|
- Does this change API response shape? (Check backwards compatibility for ETAPI)
|
||||||
|
- Are you adding search features? (Understand expression-based architecture and in-memory scoring first)
|
||||||
|
- Is this a new widget? (Know which base class and lifecycle methods to use)
|
||||||
|
- Does this involve protected notes? (Check `isContentAvailable()` before accessing content)
|
||||||
|
- Is this a long-running operation? (Use TaskContext for progress reporting)
|
||||||
|
- Are you working with attributes? (Understand inheritance patterns: direct, child-prefix, template)
|
||||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
|||||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
# Add any setup steps before running the `github/codeql-action/init` action.
|
# Add any setup steps before running the `github/codeql-action/init` action.
|
||||||
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
||||||
|
|||||||
2
.github/workflows/deploy-docs.yml
vendored
2
.github/workflows/deploy-docs.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
|
|||||||
6
.github/workflows/dev.yml
vendored
6
.github/workflows/dev.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- test_dev
|
- test_dev
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -80,7 +80,7 @@ jobs:
|
|||||||
- dockerfile: Dockerfile
|
- dockerfile: Dockerfile
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
12
.github/workflows/main-docker.yml
vendored
12
.github/workflows/main-docker.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
- dockerfile: Dockerfile
|
- dockerfile: Dockerfile
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set IMAGE_NAME to lowercase
|
- name: Set IMAGE_NAME to lowercase
|
||||||
run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV
|
run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV
|
||||||
@@ -86,12 +86,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Playwright trace
|
- name: Upload Playwright trace
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: Playwright trace (${{ matrix.dockerfile }})
|
name: Playwright trace (${{ matrix.dockerfile }})
|
||||||
path: test-output/playwright/output
|
path: test-output/playwright/output
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v5
|
- uses: actions/upload-artifact@v6
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
with:
|
with:
|
||||||
name: Playwright report (${{ matrix.dockerfile }})
|
name: Playwright report (${{ matrix.dockerfile }})
|
||||||
@@ -141,7 +141,7 @@ jobs:
|
|||||||
run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
|
run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
@@ -213,7 +213,7 @@ jobs:
|
|||||||
touch "/tmp/digests/${digest#sha256:}"
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: digests-${{ env.PLATFORM_PAIR }}-${{ matrix.dockerfile }}
|
name: digests-${{ env.PLATFORM_PAIR }}-${{ matrix.dockerfile }}
|
||||||
path: /tmp/digests/*
|
path: /tmp/digests/*
|
||||||
@@ -227,7 +227,7 @@ jobs:
|
|||||||
- build
|
- build
|
||||||
steps:
|
steps:
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
pattern: digests-*
|
pattern: digests-*
|
||||||
|
|||||||
23
.github/workflows/nightly.yml
vendored
23
.github/workflows/nightly.yml
vendored
@@ -45,9 +45,22 @@ jobs:
|
|||||||
image: win-signing
|
image: win-signing
|
||||||
shell: cmd
|
shell: cmd
|
||||||
forge_platform: win32
|
forge_platform: win32
|
||||||
|
# Exclude ARM64 Linux from default matrix to use native runner
|
||||||
|
exclude:
|
||||||
|
- arch: arm64
|
||||||
|
os:
|
||||||
|
name: linux
|
||||||
|
# Add ARM64 Linux with native ubuntu-24.04-arm runner for better-sqlite3 compatibility
|
||||||
|
include:
|
||||||
|
- arch: arm64
|
||||||
|
os:
|
||||||
|
name: linux
|
||||||
|
image: ubuntu-24.04-arm
|
||||||
|
shell: bash
|
||||||
|
forge_platform: linux
|
||||||
runs-on: ${{ matrix.os.image }}
|
runs-on: ${{ matrix.os.image }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
@@ -77,7 +90,7 @@ jobs:
|
|||||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
||||||
|
|
||||||
- name: Publish release
|
- name: Publish release
|
||||||
uses: softprops/action-gh-release@v2.4.2
|
uses: softprops/action-gh-release@v2.5.0
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
make_latest: false
|
make_latest: false
|
||||||
@@ -89,7 +102,7 @@ jobs:
|
|||||||
name: Nightly Build
|
name: Nightly Build
|
||||||
|
|
||||||
- name: Publish artifacts
|
- name: Publish artifacts
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
name: TriliumNotes ${{ matrix.os.name }} ${{ matrix.arch }}
|
name: TriliumNotes ${{ matrix.os.name }} ${{ matrix.arch }}
|
||||||
@@ -109,7 +122,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Run the build
|
- name: Run the build
|
||||||
uses: ./.github/actions/build-server
|
uses: ./.github/actions/build-server
|
||||||
@@ -118,7 +131,7 @@ jobs:
|
|||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
|
|
||||||
- name: Publish release
|
- name: Publish release
|
||||||
uses: softprops/action-gh-release@v2.4.2
|
uses: softprops/action-gh-release@v2.5.0
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
make_latest: false
|
make_latest: false
|
||||||
|
|||||||
6
.github/workflows/playwright.yml
vendored
6
.github/workflows/playwright.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
|||||||
TRILIUM_DATA_DIR: "${{ github.workspace }}/apps/server/spec/db"
|
TRILIUM_DATA_DIR: "${{ github.workspace }}/apps/server/spec/db"
|
||||||
TRILIUM_INTEGRATION_TEST: memory
|
TRILIUM_INTEGRATION_TEST: memory
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
filter: tree:0
|
filter: tree:0
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -77,9 +77,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload test report
|
- name: Upload test report
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: e2e report
|
name: e2e report ${{ matrix.arch }}
|
||||||
path: apps/server-e2e/test-output
|
path: apps/server-e2e/test-output
|
||||||
|
|
||||||
- name: Kill the server
|
- name: Kill the server
|
||||||
|
|||||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
|||||||
forge_platform: linux
|
forge_platform: linux
|
||||||
runs-on: ${{ matrix.os.image }}
|
runs-on: ${{ matrix.os.image }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
@@ -73,7 +73,7 @@ jobs:
|
|||||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
||||||
|
|
||||||
- name: Upload the artifact
|
- name: Upload the artifact
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: release-desktop-${{ matrix.os.name }}-${{ matrix.arch }}
|
name: release-desktop-${{ matrix.os.name }}-${{ matrix.arch }}
|
||||||
path: apps/desktop/upload/*.*
|
path: apps/desktop/upload/*.*
|
||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Run the build
|
- name: Run the build
|
||||||
uses: ./.github/actions/build-server
|
uses: ./.github/actions/build-server
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
|
|
||||||
- name: Upload the artifact
|
- name: Upload the artifact
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: release-server-linux-${{ matrix.arch }}
|
name: release-server-linux-${{ matrix.arch }}
|
||||||
path: upload/*.*
|
path: upload/*.*
|
||||||
@@ -114,20 +114,20 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- run: mkdir upload
|
- run: mkdir upload
|
||||||
|
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
sparse-checkout: |
|
sparse-checkout: |
|
||||||
docs/Release Notes
|
docs/Release Notes
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
pattern: release-*
|
pattern: release-*
|
||||||
path: upload
|
path: upload
|
||||||
|
|
||||||
- name: Publish stable release
|
- name: Publish stable release
|
||||||
uses: softprops/action-gh-release@v2.4.2
|
uses: softprops/action-gh-release@v2.5.0
|
||||||
with:
|
with:
|
||||||
draft: false
|
draft: false
|
||||||
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
|
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
|
||||||
|
|||||||
2
.github/workflows/website.yml
vendored
2
.github/workflows/website.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
pull-requests: write # For PR preview comments
|
pull-requests: write # For PR preview comments
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -44,8 +44,10 @@ upload
|
|||||||
.rollup.cache
|
.rollup.cache
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
/.direnv
|
||||||
/result
|
/result
|
||||||
.svelte-kit
|
.svelte-kit
|
||||||
|
|
||||||
# docs
|
# docs
|
||||||
site/
|
site/
|
||||||
|
apps/*/coverage
|
||||||
|
|||||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -9,7 +9,6 @@
|
|||||||
"tobermory.es6-string-html",
|
"tobermory.es6-string-html",
|
||||||
"vitest.explorer",
|
"vitest.explorer",
|
||||||
"yzhang.markdown-all-in-one",
|
"yzhang.markdown-all-in-one",
|
||||||
"svelte.svelte-vscode",
|
"usernamehw.errorlens"
|
||||||
"bradlc.vscode-tailwindcss"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -36,5 +36,11 @@
|
|||||||
"docs/**/*.png": true,
|
"docs/**/*.png": true,
|
||||||
"apps/server/src/assets/doc_notes/**": true,
|
"apps/server/src/assets/doc_notes/**": true,
|
||||||
"apps/edit-docs/demo/**": true
|
"apps/edit-docs/demo/**": true
|
||||||
}
|
},
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit"
|
||||||
|
},
|
||||||
|
"eslint.rules.customizations": [
|
||||||
|
{ "rule": "*", "severity": "warn" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
63
README.md
63
README.md
@@ -16,13 +16,14 @@
|
|||||||

|

|
||||||
[](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) [](https://hosted.weblate.org/engage/trilium/)
|
[](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) [](https://hosted.weblate.org/engage/trilium/)
|
||||||
|
|
||||||
[English](./README.md) | [Chinese (Simplified)](./docs/README-ZH_CN.md) | [Chinese (Traditional)](./docs/README-ZH_TW.md) | [Russian](./docs/README-ru.md) | [Japanese](./docs/README-ja.md) | [Italian](./docs/README-it.md) | [Spanish](./docs/README-es.md)
|
<!-- translate:off -->
|
||||||
|
<!-- LANGUAGE SWITCHER -->
|
||||||
|
[Chinese (Simplified Han script)](./docs/README-ZH_CN.md) | [Chinese (Traditional Han script)](./docs/README-ZH_TW.md) | [English](./docs/README.md) | [French](./docs/README-fr.md) | [German](./docs/README-de.md) | [Greek](./docs/README-el.md) | [Italian](./docs/README-it.md) | [Japanese](./docs/README-ja.md) | [Romanian](./docs/README-ro.md) | [Spanish](./docs/README-es.md)
|
||||||
|
<!-- translate:on -->
|
||||||
|
|
||||||
Trilium Notes is a free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases.
|
Trilium Notes is a free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases.
|
||||||
|
|
||||||
See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for quick overview:
|
<img src="./docs/app.png" alt="Trilium Screenshot" width="1000">
|
||||||
|
|
||||||
<a href="https://triliumnext.github.io/Docs/Wiki/screenshot-tour"><img src="./docs/app.png" alt="Trilium Screenshot" width="1000"></a>
|
|
||||||
|
|
||||||
## ⏬ Download
|
## ⏬ Download
|
||||||
- [Latest release](https://github.com/TriliumNext/Trilium/releases/latest) – stable version, recommended for most users.
|
- [Latest release](https://github.com/TriliumNext/Trilium/releases/latest) – stable version, recommended for most users.
|
||||||
@@ -39,39 +40,39 @@ Our documentation is available in multiple formats:
|
|||||||
|
|
||||||
### Quick Links
|
### Quick Links
|
||||||
- [Getting Started Guide](https://docs.triliumnotes.org/)
|
- [Getting Started Guide](https://docs.triliumnotes.org/)
|
||||||
- [Installation Instructions](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation.md)
|
- [Installation Instructions](https://docs.triliumnotes.org/user-guide/setup)
|
||||||
- [Docker Setup](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.md)
|
- [Docker Setup](https://docs.triliumnotes.org/user-guide/setup/server/installation/docker)
|
||||||
- [Upgrading TriliumNext](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Upgrading%20TriliumNext.md)
|
- [Upgrading TriliumNext](https://docs.triliumnotes.org/user-guide/setup/upgrading)
|
||||||
- [Basic Concepts and Features](./docs/User%20Guide/User%20Guide/Basic%20Concepts%20and%20Features/Notes.md)
|
- [Basic Concepts and Features](https://docs.triliumnotes.org/user-guide/concepts/notes)
|
||||||
- [Patterns of Personal Knowledge Base](https://triliumnext.github.io/Docs/Wiki/patterns-of-personal-knowledge)
|
- [Patterns of Personal Knowledge Base](https://docs.triliumnotes.org/user-guide/misc/patterns-of-personal-knowledge)
|
||||||
|
|
||||||
## 🎁 Features
|
## 🎁 Features
|
||||||
|
|
||||||
* Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://triliumnext.github.io/Docs/Wiki/cloning-notes))
|
* Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://docs.triliumnotes.org/user-guide/concepts/notes/cloning))
|
||||||
* Rich WYSIWYG note editor including e.g. tables, images and [math](https://triliumnext.github.io/Docs/Wiki/text-notes) with markdown [autoformat](https://triliumnext.github.io/Docs/Wiki/text-notes#autoformat)
|
* Rich WYSIWYG note editor including e.g. tables, images and [math](https://docs.triliumnotes.org/user-guide/note-types/text) with markdown [autoformat](https://docs.triliumnotes.org/user-guide/note-types/text/markdown-formatting)
|
||||||
* Support for editing [notes with source code](https://triliumnext.github.io/Docs/Wiki/code-notes), including syntax highlighting
|
* Support for editing [notes with source code](https://docs.triliumnotes.org/user-guide/note-types/code), including syntax highlighting
|
||||||
* Fast and easy [navigation between notes](https://triliumnext.github.io/Docs/Wiki/note-navigation), full text search and [note hoisting](https://triliumnext.github.io/Docs/Wiki/note-hoisting)
|
* Fast and easy [navigation between notes](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-navigation), full text search and [note hoisting](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-hoisting)
|
||||||
* Seamless [note versioning](https://triliumnext.github.io/Docs/Wiki/note-revisions)
|
* Seamless [note versioning](https://docs.triliumnotes.org/user-guide/concepts/notes/note-revisions)
|
||||||
* Note [attributes](https://triliumnext.github.io/Docs/Wiki/attributes) can be used for note organization, querying and advanced [scripting](https://triliumnext.github.io/Docs/Wiki/scripts)
|
* Note [attributes](https://docs.triliumnotes.org/user-guide/advanced-usage/attributes) can be used for note organization, querying and advanced [scripting](https://docs.triliumnotes.org/user-guide/scripts)
|
||||||
* UI available in English, German, Spanish, French, Romanian, and Chinese (simplified and traditional)
|
* UI available in English, German, Spanish, French, Romanian, and Chinese (simplified and traditional)
|
||||||
* Direct [OpenID and TOTP integration](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation/Multi-Factor%20Authentication.md) for more secure login
|
* Direct [OpenID and TOTP integration](https://docs.triliumnotes.org/user-guide/setup/server/mfa) for more secure login
|
||||||
* [Synchronization](https://triliumnext.github.io/Docs/Wiki/synchronization) with self-hosted sync server
|
* [Synchronization](https://docs.triliumnotes.org/user-guide/setup/synchronization) with self-hosted sync server
|
||||||
* there's a [3rd party service for hosting synchronisation server](https://trilium.cc/paid-hosting)
|
* there are [3rd party services for hosting synchronisation server](https://docs.triliumnotes.org/user-guide/setup/server/cloud-hosting)
|
||||||
* [Sharing](https://triliumnext.github.io/Docs/Wiki/sharing) (publishing) notes to public internet
|
* [Sharing](https://docs.triliumnotes.org/user-guide/advanced-usage/sharing) (publishing) notes to public internet
|
||||||
* Strong [note encryption](https://triliumnext.github.io/Docs/Wiki/protected-notes) with per-note granularity
|
* Strong [note encryption](https://docs.triliumnotes.org/user-guide/concepts/notes/protected-notes) with per-note granularity
|
||||||
* Sketching diagrams, based on [Excalidraw](https://excalidraw.com/) (note type "canvas")
|
* Sketching diagrams, based on [Excalidraw](https://excalidraw.com/) (note type "canvas")
|
||||||
* [Relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map) and [link maps](https://triliumnext.github.io/Docs/Wiki/link-map) for visualizing notes and their relations
|
* [Relation maps](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and [note/link maps](https://docs.triliumnotes.org/user-guide/note-types/note-map) for visualizing notes and their relations
|
||||||
* Mind maps, based on [Mind Elixir](https://docs.mind-elixir.com/)
|
* Mind maps, based on [Mind Elixir](https://docs.mind-elixir.com/)
|
||||||
* [Geo maps](./docs/User%20Guide/User%20Guide/Note%20Types/Geo%20Map.md) with location pins and GPX tracks
|
* [Geo maps](https://docs.triliumnotes.org/user-guide/collections/geomap) with location pins and GPX tracks
|
||||||
* [Scripting](https://triliumnext.github.io/Docs/Wiki/scripts) - see [Advanced showcases](https://triliumnext.github.io/Docs/Wiki/advanced-showcases)
|
* [Scripting](https://docs.triliumnotes.org/user-guide/scripts) - see [Advanced showcases](https://docs.triliumnotes.org/user-guide/advanced-usage/advanced-showcases)
|
||||||
* [REST API](https://triliumnext.github.io/Docs/Wiki/etapi) for automation
|
* [REST API](https://docs.triliumnotes.org/user-guide/advanced-usage/etapi) for automation
|
||||||
* Scales well in both usability and performance upwards of 100 000 notes
|
* Scales well in both usability and performance upwards of 100 000 notes
|
||||||
* Touch optimized [mobile frontend](https://triliumnext.github.io/Docs/Wiki/mobile-frontend) for smartphones and tablets
|
* Touch optimized [mobile frontend](https://docs.triliumnotes.org/user-guide/setup/mobile-frontend) for smartphones and tablets
|
||||||
* Built-in [dark theme](https://triliumnext.github.io/Docs/Wiki/themes), support for user themes
|
* Built-in [dark theme](https://docs.triliumnotes.org/user-guide/concepts/themes), support for user themes
|
||||||
* [Evernote](https://triliumnext.github.io/Docs/Wiki/evernote-import) and [Markdown import & export](https://triliumnext.github.io/Docs/Wiki/markdown)
|
* [Evernote](https://docs.triliumnotes.org/user-guide/concepts/import-export/evernote) and [Markdown import & export](https://docs.triliumnotes.org/user-guide/concepts/import-export/markdown)
|
||||||
* [Web Clipper](https://triliumnext.github.io/Docs/Wiki/web-clipper) for easy saving of web content
|
* [Web Clipper](https://docs.triliumnotes.org/user-guide/setup/web-clipper) for easy saving of web content
|
||||||
* Customizable UI (sidebar buttons, user-defined widgets, ...)
|
* Customizable UI (sidebar buttons, user-defined widgets, ...)
|
||||||
* [Metrics](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics.md), along with a [Grafana Dashboard](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics/grafana-dashboard.json)
|
* [Metrics](https://docs.triliumnotes.org/user-guide/advanced-usage/metrics), along with a Grafana Dashboard.
|
||||||
|
|
||||||
✨ Check out the following third-party resources/communities for more TriliumNext related goodies:
|
✨ Check out the following third-party resources/communities for more TriliumNext related goodies:
|
||||||
|
|
||||||
@@ -131,7 +132,7 @@ Note: It is best to disable automatic updates on your server installation (see b
|
|||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
|
||||||
To install TriliumNext on your own server (including via Docker from [Dockerhub](https://hub.docker.com/r/triliumnext/trilium)) follow [the server installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation).
|
To install TriliumNext on your own server (including via Docker from [Dockerhub](https://hub.docker.com/r/triliumnext/trilium)) follow [the server installation docs](https://docs.triliumnotes.org/user-guide/setup/server).
|
||||||
|
|
||||||
|
|
||||||
## 💻 Contribute
|
## 💻 Contribute
|
||||||
@@ -198,7 +199,7 @@ Trilium would not be possible without the technologies behind it:
|
|||||||
* [Leaflet](https://github.com/Leaflet/Leaflet) - for rendering geographical maps.
|
* [Leaflet](https://github.com/Leaflet/Leaflet) - for rendering geographical maps.
|
||||||
* [Tabulator](https://github.com/olifolkerd/tabulator) - for the interactive table used in collections.
|
* [Tabulator](https://github.com/olifolkerd/tabulator) - for the interactive table used in collections.
|
||||||
* [FancyTree](https://github.com/mar10/fancytree) - feature-rich tree library without real competition.
|
* [FancyTree](https://github.com/mar10/fancytree) - feature-rich tree library without real competition.
|
||||||
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library. Used in [relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map.html) and [link maps](https://triliumnext.github.io/Docs/Wiki/note-map.html#link-map)
|
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library. Used in [relation maps](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and [link maps](https://docs.triliumnotes.org/user-guide/advanced-usage/note-map#link-map)
|
||||||
|
|
||||||
## 🤝 Support
|
## 🤝 Support
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import anonymizationService from "../src/services/anonymization.js";
|
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
fs.writeFileSync(path.resolve(__dirname, "tpl", "anonymize-database.sql"), anonymizationService.getFullAnonymizationScript());
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
SCHEMA_FILE_PATH=db/schema.sql
|
|
||||||
|
|
||||||
sqlite3 ./data/document.db .schema | grep -v "sqlite_sequence" > "$SCHEMA_FILE_PATH"
|
|
||||||
|
|
||||||
echo "DB schema exported to $SCHEMA_FILE_PATH"
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
if [[ $# -eq 0 ]] ; then
|
|
||||||
echo "Missing argument of new version"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
VERSION=$1
|
|
||||||
SERIES=${VERSION:0:4}-latest
|
|
||||||
|
|
||||||
docker push zadam/trilium:$VERSION
|
|
||||||
docker push zadam/trilium:$SERIES
|
|
||||||
|
|
||||||
if [[ $1 != *"beta"* ]]; then
|
|
||||||
docker push zadam/trilium:latest
|
|
||||||
fi
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
if [[ $# -eq 0 ]] ; then
|
|
||||||
echo "Missing argument of new version"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
VERSION=$1
|
|
||||||
|
|
||||||
if ! [[ ${VERSION} =~ ^[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}(-.+)?$ ]] ;
|
|
||||||
then
|
|
||||||
echo "Version ${VERSION} isn't in format X.Y.Z"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
VERSION_DATE=$(git log -1 --format=%aI "v${VERSION}" | cut -c -10)
|
|
||||||
VERSION_COMMIT=$(git rev-list -n 1 "v${VERSION}")
|
|
||||||
|
|
||||||
# expecting the directory at a specific path
|
|
||||||
cd ~/trilium-flathub || exit
|
|
||||||
|
|
||||||
if ! git diff-index --quiet HEAD --; then
|
|
||||||
echo "There are uncommitted changes"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
BASE_BRANCH=main
|
|
||||||
|
|
||||||
if [[ "$VERSION" == *"beta"* ]]; then
|
|
||||||
BASE_BRANCH=beta
|
|
||||||
fi
|
|
||||||
|
|
||||||
git switch "${BASE_BRANCH}"
|
|
||||||
git pull
|
|
||||||
|
|
||||||
BRANCH=b${VERSION}
|
|
||||||
|
|
||||||
git branch "${BRANCH}"
|
|
||||||
git switch "${BRANCH}"
|
|
||||||
|
|
||||||
echo "Updating files with version ${VERSION}, date ${VERSION_DATE} and commit ${VERSION_COMMIT}"
|
|
||||||
|
|
||||||
flatpak-node-generator npm ../trilium/package-lock.json
|
|
||||||
|
|
||||||
xmlstarlet ed --inplace --update "/component/releases/release/@version" --value "${VERSION}" --update "/component/releases/release/@date" --value "${VERSION_DATE}" ./com.github.zadam.trilium.metainfo.xml
|
|
||||||
|
|
||||||
yq --inplace "(.modules[0].sources[0].tag = \"v${VERSION}\") | (.modules[0].sources[0].commit = \"${VERSION_COMMIT}\")" ./com.github.zadam.trilium.yml
|
|
||||||
|
|
||||||
git add ./generated-sources.json
|
|
||||||
git add ./com.github.zadam.trilium.metainfo.xml
|
|
||||||
git add ./com.github.zadam.trilium.yml
|
|
||||||
|
|
||||||
git commit -m "release $VERSION"
|
|
||||||
git push --set-upstream origin "${BRANCH}"
|
|
||||||
|
|
||||||
gh pr create --fill -B "${BASE_BRANCH}"
|
|
||||||
gh pr merge --auto --merge --delete-branch
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [[ $# -eq 0 ]] ; then
|
|
||||||
echo "Missing argument of new version"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v jq &> /dev/null; then
|
|
||||||
echo "Missing command: jq"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
VERSION=$1
|
|
||||||
|
|
||||||
if ! [[ ${VERSION} =~ ^[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}(-.+)?$ ]] ;
|
|
||||||
then
|
|
||||||
echo "Version ${VERSION} isn't in format X.Y.Z"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! git diff-index --quiet HEAD --; then
|
|
||||||
echo "There are uncommitted changes"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Releasing Trilium $VERSION"
|
|
||||||
|
|
||||||
jq '.version = "'$VERSION'"' package.json > package.json.tmp
|
|
||||||
mv package.json.tmp package.json
|
|
||||||
|
|
||||||
git add package.json
|
|
||||||
|
|
||||||
npm run chore:update-build-info
|
|
||||||
|
|
||||||
git add src/services/build.ts
|
|
||||||
|
|
||||||
TAG=v$VERSION
|
|
||||||
|
|
||||||
echo "Committing package.json version change"
|
|
||||||
|
|
||||||
git commit -m "chore(release): $VERSION"
|
|
||||||
git push
|
|
||||||
|
|
||||||
echo "Tagging commit with $TAG"
|
|
||||||
|
|
||||||
git tag $TAG
|
|
||||||
git push origin $TAG
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>com.apple.security.cs.allow-jit</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.files.user-selected.read-write</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import eslint from "@eslint/js";
|
|
||||||
import tseslint from "typescript-eslint";
|
|
||||||
import simpleImportSort from "eslint-plugin-simple-import-sort";
|
|
||||||
|
|
||||||
export default tseslint.config(
|
|
||||||
eslint.configs.recommended,
|
|
||||||
tseslint.configs.recommended,
|
|
||||||
// consider using rules below, once we have a full TS codebase and can be more strict
|
|
||||||
// tseslint.configs.strictTypeChecked,
|
|
||||||
// tseslint.configs.stylisticTypeChecked,
|
|
||||||
// tseslint.configs.recommendedTypeChecked,
|
|
||||||
{
|
|
||||||
languageOptions: {
|
|
||||||
parserOptions: {
|
|
||||||
projectService: true,
|
|
||||||
tsconfigRootDir: import.meta.dirname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
plugins: {
|
|
||||||
"simple-import-sort": simpleImportSort
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rules: {
|
|
||||||
// add rule overrides here
|
|
||||||
"no-undef": "off",
|
|
||||||
"no-unused-vars": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
argsIgnorePattern: "^_",
|
|
||||||
varsIgnorePattern: "^_"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"simple-import-sort/imports": "error",
|
|
||||||
"simple-import-sort/exports": "error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ignores: [
|
|
||||||
"build/*",
|
|
||||||
"dist/*",
|
|
||||||
"docs/*",
|
|
||||||
"demo/*",
|
|
||||||
"src/public/app-dist/*",
|
|
||||||
"src/public/app/doc_notes/*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import stylistic from "@stylistic/eslint-plugin";
|
|
||||||
import tsParser from "@typescript-eslint/parser";
|
|
||||||
|
|
||||||
// eslint config just for formatting rules
|
|
||||||
// potentially to be merged with the linting rules into one single config,
|
|
||||||
// once we have fixed the majority of lint errors
|
|
||||||
|
|
||||||
// Go to https://eslint.style/rules/default/${rule_without_prefix} to check the rule details
|
|
||||||
export const stylisticRules = {
|
|
||||||
"@stylistic/indent": [ "error", 4 ],
|
|
||||||
"@stylistic/quotes": [ "error", "double", { avoidEscape: true, allowTemplateLiterals: "always" } ],
|
|
||||||
"@stylistic/semi": [ "error", "always" ],
|
|
||||||
"@stylistic/quote-props": [ "error", "consistent-as-needed" ],
|
|
||||||
"@stylistic/max-len": [ "error", { code: 100 } ],
|
|
||||||
"@stylistic/comma-dangle": [ "error", "never" ],
|
|
||||||
"@stylistic/linebreak-style": [ "error", "unix" ],
|
|
||||||
"@stylistic/array-bracket-spacing": [ "error", "always" ],
|
|
||||||
"@stylistic/object-curly-spacing": [ "error", "always" ],
|
|
||||||
"@stylistic/padded-blocks": [ "error", { classes: "always" } ]
|
|
||||||
};
|
|
||||||
|
|
||||||
export default [
|
|
||||||
{
|
|
||||||
files: [ "**/*.{js,ts,mjs,cjs}" ],
|
|
||||||
languageOptions: {
|
|
||||||
parser: tsParser
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
"@stylistic": stylistic
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...stylisticRules
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ignores: [
|
|
||||||
"build/*",
|
|
||||||
"dist/*",
|
|
||||||
"docs/*",
|
|
||||||
"demo/*",
|
|
||||||
// TriliumNextTODO: check if we want to format packages here as well - for now skipping it
|
|
||||||
"packages/*",
|
|
||||||
"src/public/app-dist/*",
|
|
||||||
"src/public/app/doc_notes/*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { test as setup, expect } from "@playwright/test";
|
|
||||||
|
|
||||||
const authFile = "playwright/.auth/user.json";
|
|
||||||
|
|
||||||
const ROOT_URL = "http://localhost:8082";
|
|
||||||
const LOGIN_PASSWORD = "demo1234";
|
|
||||||
|
|
||||||
// Reference: https://playwright.dev/docs/auth#basic-shared-account-in-all-tests
|
|
||||||
|
|
||||||
setup("authenticate", async ({ page }) => {
|
|
||||||
await page.goto(ROOT_URL);
|
|
||||||
await expect(page).toHaveURL(`${ROOT_URL}/login`);
|
|
||||||
|
|
||||||
await page.getByRole("textbox", { name: "Password" }).fill(LOGIN_PASSWORD);
|
|
||||||
await page.getByRole("button", { name: "Login" }).click();
|
|
||||||
await page.context().storageState({ path: authFile });
|
|
||||||
});
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { test, expect } from "@playwright/test";
|
|
||||||
|
|
||||||
test("Can duplicate note with broken links", async ({ page }) => {
|
|
||||||
await page.goto(`http://localhost:8082/#2VammGGdG6Ie`);
|
|
||||||
await page.locator(".tree-wrapper .fancytree-active").getByText("Note map").click({ button: "right" });
|
|
||||||
await page.getByText("Duplicate subtree").click();
|
|
||||||
await expect(page.locator(".toast-body")).toBeHidden();
|
|
||||||
await expect(page.locator(".tree-wrapper").getByText("Note map (dup)")).toBeVisible();
|
|
||||||
});
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { test, expect } from "@playwright/test";
|
|
||||||
|
|
||||||
test("has title", async ({ page }) => {
|
|
||||||
await page.goto("https://playwright.dev/");
|
|
||||||
|
|
||||||
// Expect a title "to contain" a substring.
|
|
||||||
await expect(page).toHaveTitle(/Playwright/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("get started link", async ({ page }) => {
|
|
||||||
await page.goto("https://playwright.dev/");
|
|
||||||
|
|
||||||
// Click the get started link.
|
|
||||||
await page.getByRole("link", { name: "Get started" }).click();
|
|
||||||
|
|
||||||
// Expects page to have a heading with the name of Installation.
|
|
||||||
await expect(page.getByRole("heading", { name: "Installation" })).toBeVisible();
|
|
||||||
});
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import test, { expect } from "@playwright/test";
|
|
||||||
|
|
||||||
test("Native Title Bar not displayed on web", async ({ page }) => {
|
|
||||||
await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsAppearance");
|
|
||||||
await expect(page.getByRole("heading", { name: "Theme" })).toBeVisible();
|
|
||||||
await expect(page.getByRole("heading", { name: "Native Title Bar (requires" })).toBeHidden();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Tray settings not displayed on web", async ({ page }) => {
|
|
||||||
await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsOther");
|
|
||||||
await expect(page.getByRole("heading", { name: "Note Erasure Timeout" })).toBeVisible();
|
|
||||||
await expect(page.getByRole("heading", { name: "Tray" })).toBeHidden();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Spellcheck settings not displayed on web", async ({ page }) => {
|
|
||||||
await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsSpellcheck");
|
|
||||||
await expect(page.getByRole("heading", { name: "Spell Check" })).toBeVisible();
|
|
||||||
await expect(page.getByRole("heading", { name: "Tray" })).toBeHidden();
|
|
||||||
await expect(page.getByText("These options apply only for desktop builds")).toBeVisible();
|
|
||||||
await expect(page.getByText("Enable spellcheck")).toBeHidden();
|
|
||||||
});
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import test, { expect } from "@playwright/test";
|
|
||||||
|
|
||||||
test("Renders on desktop", async ({ page, context }) => {
|
|
||||||
await page.goto("http://localhost:8082");
|
|
||||||
await expect(page.locator(".tree")).toContainText("Trilium Integration Test");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Renders on mobile", async ({ page, context }) => {
|
|
||||||
await context.addCookies([
|
|
||||||
{
|
|
||||||
url: "http://localhost:8082",
|
|
||||||
name: "trilium-device",
|
|
||||||
value: "mobile"
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
await page.goto("http://localhost:8082");
|
|
||||||
await expect(page.locator(".tree")).toContainText("Trilium Integration Test");
|
|
||||||
});
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { test, expect } from "@playwright/test";
|
|
||||||
|
|
||||||
const expectedVersion = "0.90.3";
|
|
||||||
|
|
||||||
test("Displays update badge when there is a version available", async ({ page }) => {
|
|
||||||
await page.goto("http://localhost:8080");
|
|
||||||
await page.getByRole("button", { name: "" }).click();
|
|
||||||
await page.getByText(`Version ${expectedVersion} is available,`).click();
|
|
||||||
|
|
||||||
const page1 = await page.waitForEvent("popup");
|
|
||||||
expect(page1.url()).toBe(`https://github.com/TriliumNext/Trilium/releases/tag/v${expectedVersion}`);
|
|
||||||
});
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
{
|
|
||||||
"main": "./electron-main.js",
|
|
||||||
"bin": {
|
|
||||||
"trilium": "src/main.js"
|
|
||||||
},
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"server:start-safe": "cross-env TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev nodemon src/main.ts",
|
|
||||||
"server:start-no-dir": "cross-env TRILIUM_ENV=dev nodemon src/main.ts",
|
|
||||||
"server:start-test": "npm run server:switch && rimraf ./data-test && cross-env TRILIUM_DATA_DIR=./data-test TRILIUM_ENV=dev TRILIUM_PORT=9999 nodemon src/main.ts",
|
|
||||||
"server:qstart": "npm run server:switch && npm run server:start",
|
|
||||||
"server:switch": "rimraf ./node_modules/better-sqlite3 && npm install",
|
|
||||||
"electron:start-no-dir": "cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_ENV=dev TRILIUM_PORT=37742 electron --inspect=5858 .",
|
|
||||||
"electron:start-nix": "electron-rebuild --version 33.3.1 && cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./electron-main.ts --inspect=5858 .\"",
|
|
||||||
"electron:start-nix-no-dir": "electron-rebuild --version 33.3.1 && cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_ENV=dev TRILIUM_PORT=37742 nix-shell -p electron_33 --run \"electron ./electron-main.ts --inspect=5858 .\"",
|
|
||||||
"electron:start-prod-no-dir": "npm run build:prepare-dist && cross-env TRILIUM_ENV=prod electron --inspect=5858 .",
|
|
||||||
"electron:start-prod-nix": "electron-rebuild --version 33.3.1 && npm run build:prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
|
|
||||||
"electron:start-prod-nix-no-dir": "electron-rebuild --version 33.3.1 && npm run build:prepare-dist && cross-env TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
|
|
||||||
"electron:qstart": "npm run electron:switch && npm run electron:start",
|
|
||||||
"electron:switch": "electron-rebuild",
|
|
||||||
"docs:build": "typedoc",
|
|
||||||
"test": "npm run client:test && npm run server:test",
|
|
||||||
"client:test": "cross-env TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db TRILIUM_INTEGRATION_TEST=memory vitest --root src/public/app",
|
|
||||||
"client:coverage": "cross-env TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db TRILIUM_INTEGRATION_TEST=memory vitest --root src/public/app --coverage",
|
|
||||||
"test:playwright": "playwright test --workers 1",
|
|
||||||
"test:integration-edit-db": "cross-env TRILIUM_INTEGRATION_TEST=edit TRILIUM_PORT=8081 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
|
|
||||||
"test:integration-mem-db": "cross-env nodemon src/main.ts",
|
|
||||||
"test:integration-mem-db-dev": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
|
|
||||||
"dev:watch-dist": "tsx ./bin/watch-dist.ts",
|
|
||||||
"dev:format-check": "eslint -c eslint.format.config.js .",
|
|
||||||
"dev:format-fix": "eslint -c eslint.format.config.js . --fix",
|
|
||||||
"dev:linter-check": "eslint .",
|
|
||||||
"dev:linter-fix": "eslint . --fix",
|
|
||||||
"chore:generate-document": "cross-env nodemon ./bin/generate_document.ts 1000",
|
|
||||||
"chore:generate-openapi": "tsx bin/generate-openapi.js"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@playwright/test": "1.56.1",
|
|
||||||
"@stylistic/eslint-plugin": "5.5.0",
|
|
||||||
"@types/express": "5.0.5",
|
|
||||||
"@types/node": "24.10.1",
|
|
||||||
"@types/yargs": "17.0.35",
|
|
||||||
"@vitest/coverage-v8": "3.2.4",
|
|
||||||
"eslint": "9.39.1",
|
|
||||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
|
||||||
"esm": "3.2.25",
|
|
||||||
"jsdoc": "4.0.5",
|
|
||||||
"lorem-ipsum": "2.0.8",
|
|
||||||
"rcedit": "5.0.1",
|
|
||||||
"rimraf": "6.1.0",
|
|
||||||
"tslib": "2.8.1"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"appdmg": "0.6.6"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import etapi from "../support/etapi.js";
|
|
||||||
/* TriliumNextTODO: port to Vitest
|
|
||||||
etapi.describeEtapi("app_info", () => {
|
|
||||||
it("get", async () => {
|
|
||||||
const appInfo = await etapi.getEtapi("app-info");
|
|
||||||
expect(appInfo.clipperProtocolVersion).toEqual("1.0");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import etapi from "../support/etapi.js";
|
|
||||||
|
|
||||||
/* TriliumNextTODO: port to Vitest
|
|
||||||
etapi.describeEtapi("backup", () => {
|
|
||||||
it("create", async () => {
|
|
||||||
const response = await etapi.putEtapiContent("backup/etapi_test");
|
|
||||||
expect(response.status).toEqual(204);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import etapi from "../support/etapi.js";
|
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
/* TriliumNextTODO: port to Vitest
|
|
||||||
etapi.describeEtapi("import", () => {
|
|
||||||
// temporarily skip this test since test-export.zip is missing
|
|
||||||
xit("import", async () => {
|
|
||||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
||||||
|
|
||||||
const zipFileBuffer = fs.readFileSync(path.resolve(scriptDir, "test-export.zip"));
|
|
||||||
|
|
||||||
const response = await etapi.postEtapiContent("notes/root/import", zipFileBuffer);
|
|
||||||
expect(response.status).toEqual(201);
|
|
||||||
|
|
||||||
const { note, branch } = await response.json();
|
|
||||||
|
|
||||||
expect(note.title).toEqual("test-export");
|
|
||||||
expect(branch.parentNoteId).toEqual("root");
|
|
||||||
|
|
||||||
const content = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).text();
|
|
||||||
expect(content).toContain("test export content");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import crypto from "crypto";
|
|
||||||
import etapi from "../support/etapi.js";
|
|
||||||
|
|
||||||
/* TriliumNextTODO: port to Vitest
|
|
||||||
etapi.describeEtapi("notes", () => {
|
|
||||||
it("create", async () => {
|
|
||||||
const { note, branch } = await etapi.postEtapi("create-note", {
|
|
||||||
parentNoteId: "root",
|
|
||||||
type: "text",
|
|
||||||
title: "Hello World!",
|
|
||||||
content: "Content",
|
|
||||||
prefix: "Custom prefix"
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(note.title).toEqual("Hello World!");
|
|
||||||
expect(branch.parentNoteId).toEqual("root");
|
|
||||||
expect(branch.prefix).toEqual("Custom prefix");
|
|
||||||
|
|
||||||
const rNote = await etapi.getEtapi(`notes/${note.noteId}`);
|
|
||||||
expect(rNote.title).toEqual("Hello World!");
|
|
||||||
|
|
||||||
const rContent = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).text();
|
|
||||||
expect(rContent).toEqual("Content");
|
|
||||||
|
|
||||||
const rBranch = await etapi.getEtapi(`branches/${branch.branchId}`);
|
|
||||||
expect(rBranch.parentNoteId).toEqual("root");
|
|
||||||
expect(rBranch.prefix).toEqual("Custom prefix");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("patch", async () => {
|
|
||||||
const { note } = await etapi.postEtapi("create-note", {
|
|
||||||
parentNoteId: "root",
|
|
||||||
type: "text",
|
|
||||||
title: "Hello World!",
|
|
||||||
content: "Content"
|
|
||||||
});
|
|
||||||
|
|
||||||
await etapi.patchEtapi(`notes/${note.noteId}`, {
|
|
||||||
title: "new title",
|
|
||||||
type: "code",
|
|
||||||
mime: "text/apl",
|
|
||||||
dateCreated: "2000-01-01 12:34:56.999+0200",
|
|
||||||
utcDateCreated: "2000-01-01 10:34:56.999Z"
|
|
||||||
});
|
|
||||||
|
|
||||||
const rNote = await etapi.getEtapi(`notes/${note.noteId}`);
|
|
||||||
expect(rNote.title).toEqual("new title");
|
|
||||||
expect(rNote.type).toEqual("code");
|
|
||||||
expect(rNote.mime).toEqual("text/apl");
|
|
||||||
expect(rNote.dateCreated).toEqual("2000-01-01 12:34:56.999+0200");
|
|
||||||
expect(rNote.utcDateCreated).toEqual("2000-01-01 10:34:56.999Z");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("update content", async () => {
|
|
||||||
const { note } = await etapi.postEtapi("create-note", {
|
|
||||||
parentNoteId: "root",
|
|
||||||
type: "text",
|
|
||||||
title: "Hello World!",
|
|
||||||
content: "Content"
|
|
||||||
});
|
|
||||||
|
|
||||||
await etapi.putEtapiContent(`notes/${note.noteId}/content`, "new content");
|
|
||||||
|
|
||||||
const rContent = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).text();
|
|
||||||
expect(rContent).toEqual("new content");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("create / update binary content", async () => {
|
|
||||||
const { note } = await etapi.postEtapi("create-note", {
|
|
||||||
parentNoteId: "root",
|
|
||||||
type: "file",
|
|
||||||
title: "Hello World!",
|
|
||||||
content: "ZZZ"
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedContent = crypto.randomBytes(16);
|
|
||||||
|
|
||||||
await etapi.putEtapiContent(`notes/${note.noteId}/content`, updatedContent);
|
|
||||||
|
|
||||||
const rContent = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).arrayBuffer();
|
|
||||||
expect(Buffer.from(new Uint8Array(rContent))).toEqual(updatedContent);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("delete note", async () => {
|
|
||||||
const { note } = await etapi.postEtapi("create-note", {
|
|
||||||
parentNoteId: "root",
|
|
||||||
type: "text",
|
|
||||||
title: "Hello World!",
|
|
||||||
content: "Content"
|
|
||||||
});
|
|
||||||
|
|
||||||
await etapi.deleteEtapi(`notes/${note.noteId}`);
|
|
||||||
|
|
||||||
const resp = await etapi.getEtapiResponse(`notes/${note.noteId}`);
|
|
||||||
expect(resp.status).toEqual(404);
|
|
||||||
|
|
||||||
const error = await resp.json();
|
|
||||||
expect(error.status).toEqual(404);
|
|
||||||
expect(error.code).toEqual("NOTE_NOT_FOUND");
|
|
||||||
expect(error.message).toEqual(`Note '${note.noteId}' not found.`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
import { describe, beforeAll, afterAll } from "vitest";
|
|
||||||
|
|
||||||
let etapiAuthToken: string | undefined;
|
|
||||||
|
|
||||||
const getEtapiAuthorizationHeader = (): string => "Basic " + Buffer.from(`etapi:${etapiAuthToken}`).toString("base64");
|
|
||||||
|
|
||||||
const PORT: string = "9999";
|
|
||||||
const HOST: string = "http://localhost:" + PORT;
|
|
||||||
|
|
||||||
type SpecDefinitionsFunc = () => void;
|
|
||||||
|
|
||||||
function describeEtapi(description: string, specDefinitions: SpecDefinitionsFunc): void {
|
|
||||||
describe(description, () => {
|
|
||||||
beforeAll(async () => {});
|
|
||||||
|
|
||||||
afterAll(() => {});
|
|
||||||
|
|
||||||
specDefinitions();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getEtapiResponse(url: string): Promise<Response> {
|
|
||||||
return await fetch(`${HOST}/etapi/${url}`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Authorization: getEtapiAuthorizationHeader()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getEtapi(url: string): Promise<any> {
|
|
||||||
const response = await getEtapiResponse(url);
|
|
||||||
return await processEtapiResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getEtapiContent(url: string): Promise<Response> {
|
|
||||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Authorization: getEtapiAuthorizationHeader()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
checkStatus(response);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function postEtapi(url: string, data: Record<string, unknown> = {}): Promise<any> {
|
|
||||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: getEtapiAuthorizationHeader()
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
return await processEtapiResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function postEtapiContent(url: string, data: BodyInit): Promise<Response> {
|
|
||||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/octet-stream",
|
|
||||||
Authorization: getEtapiAuthorizationHeader()
|
|
||||||
},
|
|
||||||
body: data
|
|
||||||
});
|
|
||||||
|
|
||||||
checkStatus(response);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function putEtapi(url: string, data: Record<string, unknown> = {}): Promise<any> {
|
|
||||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: getEtapiAuthorizationHeader()
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
return await processEtapiResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function putEtapiContent(url: string, data?: BodyInit): Promise<Response> {
|
|
||||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/octet-stream",
|
|
||||||
Authorization: getEtapiAuthorizationHeader()
|
|
||||||
},
|
|
||||||
body: data
|
|
||||||
});
|
|
||||||
|
|
||||||
checkStatus(response);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function patchEtapi(url: string, data: Record<string, unknown> = {}): Promise<any> {
|
|
||||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: getEtapiAuthorizationHeader()
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
return await processEtapiResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteEtapi(url: string): Promise<any> {
|
|
||||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
Authorization: getEtapiAuthorizationHeader()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return await processEtapiResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processEtapiResponse(response: Response): Promise<any> {
|
|
||||||
const text = await response.text();
|
|
||||||
|
|
||||||
if (response.status < 200 || response.status >= 300) {
|
|
||||||
throw new Error(`ETAPI error ${response.status}: ${text}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return text?.trim() ? JSON.parse(text) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkStatus(response: Response): void {
|
|
||||||
if (response.status < 200 || response.status >= 300) {
|
|
||||||
throw new Error(`ETAPI error ${response.status}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
describeEtapi,
|
|
||||||
getEtapi,
|
|
||||||
getEtapiResponse,
|
|
||||||
getEtapiContent,
|
|
||||||
postEtapi,
|
|
||||||
postEtapiContent,
|
|
||||||
putEtapi,
|
|
||||||
putEtapiContent,
|
|
||||||
patchEtapi,
|
|
||||||
deleteEtapi
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"module": "NodeNext",
|
|
||||||
"declaration": false,
|
|
||||||
"sourceMap": true,
|
|
||||||
"outDir": "./build",
|
|
||||||
"strict": true,
|
|
||||||
"noImplicitAny": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"lib": ["ES2023"],
|
|
||||||
"downlevelIteration": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowJs": true
|
|
||||||
},
|
|
||||||
"include": ["./src/public/app/**/*"],
|
|
||||||
"files": [
|
|
||||||
"./src/public/app/types.d.ts",
|
|
||||||
"./src/public/app/types-lib.d.ts",
|
|
||||||
"./src/types.d.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -9,14 +9,14 @@
|
|||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Elian Doran <contact@eliandoran.me>",
|
"author": "Elian Doran <contact@eliandoran.me>",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"packageManager": "pnpm@10.22.0",
|
"packageManager": "pnpm@10.25.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@redocly/cli": "2.11.1",
|
"@redocly/cli": "2.12.6",
|
||||||
"archiver": "7.0.1",
|
"archiver": "7.0.1",
|
||||||
"fs-extra": "11.3.2",
|
"fs-extra": "11.3.2",
|
||||||
"react": "19.2.0",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.3",
|
||||||
"typedoc": "0.28.14",
|
"typedoc": "0.28.15",
|
||||||
"typedoc-plugin-missing-exports": "4.1.2"
|
"typedoc-plugin-missing-exports": "4.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import baseConfig from "../../eslint.config.mjs";
|
|
||||||
|
|
||||||
export default [
|
|
||||||
...baseConfig
|
|
||||||
];
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@triliumnext/client",
|
"name": "@triliumnext/client",
|
||||||
"version": "0.99.5",
|
"version": "0.100.0",
|
||||||
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
@@ -12,10 +12,10 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build",
|
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
|
"coverage": "vitest --coverage",
|
||||||
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
|
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/js": "9.39.1",
|
|
||||||
"@excalidraw/excalidraw": "0.18.0",
|
"@excalidraw/excalidraw": "0.18.0",
|
||||||
"@fullcalendar/core": "6.1.19",
|
"@fullcalendar/core": "6.1.19",
|
||||||
"@fullcalendar/daygrid": "6.1.19",
|
"@fullcalendar/daygrid": "6.1.19",
|
||||||
@@ -27,39 +27,40 @@
|
|||||||
"@mermaid-js/layout-elk": "0.2.0",
|
"@mermaid-js/layout-elk": "0.2.0",
|
||||||
"@mind-elixir/node-menu": "5.0.1",
|
"@mind-elixir/node-menu": "5.0.1",
|
||||||
"@popperjs/core": "2.11.8",
|
"@popperjs/core": "2.11.8",
|
||||||
|
"@preact/signals": "2.5.1",
|
||||||
"@triliumnext/ckeditor5": "workspace:*",
|
"@triliumnext/ckeditor5": "workspace:*",
|
||||||
"@triliumnext/codemirror": "workspace:*",
|
"@triliumnext/codemirror": "workspace:*",
|
||||||
"@triliumnext/commons": "workspace:*",
|
"@triliumnext/commons": "workspace:*",
|
||||||
"@triliumnext/highlightjs": "workspace:*",
|
"@triliumnext/highlightjs": "workspace:*",
|
||||||
"@triliumnext/share-theme": "workspace:*",
|
"@triliumnext/share-theme": "workspace:*",
|
||||||
"@triliumnext/split.js": "workspace:*",
|
"@triliumnext/split.js": "workspace:*",
|
||||||
|
"@zumer/snapdom": "2.0.1",
|
||||||
"autocomplete.js": "0.38.1",
|
"autocomplete.js": "0.38.1",
|
||||||
"bootstrap": "5.3.8",
|
"bootstrap": "5.3.8",
|
||||||
"boxicons": "2.1.4",
|
"boxicons": "2.1.4",
|
||||||
|
"clsx": "2.1.1",
|
||||||
"color": "5.0.3",
|
"color": "5.0.3",
|
||||||
"dayjs": "1.11.19",
|
|
||||||
"dayjs-plugin-utc": "0.1.2",
|
|
||||||
"debounce": "3.0.0",
|
"debounce": "3.0.0",
|
||||||
"draggabilly": "3.0.0",
|
"draggabilly": "3.0.0",
|
||||||
"force-graph": "1.51.0",
|
"force-graph": "1.51.0",
|
||||||
"globals": "16.5.0",
|
"globals": "16.5.0",
|
||||||
"i18next": "25.6.2",
|
"i18next": "25.7.2",
|
||||||
"i18next-http-backend": "3.0.2",
|
"i18next-http-backend": "3.0.2",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"jquery.fancytree": "2.38.5",
|
"jquery.fancytree": "2.38.5",
|
||||||
"jsplumb": "2.15.6",
|
"jsplumb": "2.15.6",
|
||||||
"katex": "0.16.25",
|
"katex": "0.16.27",
|
||||||
"knockout": "3.5.1",
|
"knockout": "3.5.1",
|
||||||
"leaflet": "1.9.4",
|
"leaflet": "1.9.4",
|
||||||
"leaflet-gpx": "2.2.0",
|
"leaflet-gpx": "2.2.0",
|
||||||
"mark.js": "8.11.1",
|
"mark.js": "8.11.1",
|
||||||
"marked": "16.4.2",
|
"marked": "17.0.1",
|
||||||
"mermaid": "11.12.1",
|
"mermaid": "11.12.2",
|
||||||
"mind-elixir": "5.3.5",
|
"mind-elixir": "5.3.7",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"panzoom": "9.4.3",
|
"panzoom": "9.4.3",
|
||||||
"preact": "10.27.2",
|
"preact": "10.28.0",
|
||||||
"react-i18next": "16.3.3",
|
"react-i18next": "16.5.0",
|
||||||
"reveal.js": "5.2.1",
|
"reveal.js": "5.2.1",
|
||||||
"svg-pan-zoom": "3.6.2",
|
"svg-pan-zoom": "3.6.2",
|
||||||
"tabulator-tables": "6.3.1",
|
"tabulator-tables": "6.3.1",
|
||||||
@@ -73,10 +74,10 @@
|
|||||||
"@types/leaflet": "1.9.21",
|
"@types/leaflet": "1.9.21",
|
||||||
"@types/leaflet-gpx": "1.3.8",
|
"@types/leaflet-gpx": "1.3.8",
|
||||||
"@types/mark.js": "8.11.12",
|
"@types/mark.js": "8.11.12",
|
||||||
"@types/reveal.js": "5.2.1",
|
"@types/reveal.js": "5.2.2",
|
||||||
"@types/tabulator-tables": "6.3.0",
|
"@types/tabulator-tables": "6.3.0",
|
||||||
"copy-webpack-plugin": "13.0.1",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"happy-dom": "20.0.10",
|
"happy-dom": "20.0.11",
|
||||||
"script-loader": "0.7.2",
|
"script-loader": "0.7.2",
|
||||||
"vite-plugin-static-copy": "3.1.4"
|
"vite-plugin-static-copy": "3.1.4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { AddLinkOpts } from "../widgets/dialogs/add_link.jsx";
|
|||||||
import { IncludeNoteOpts } from "../widgets/dialogs/include_note.jsx";
|
import { IncludeNoteOpts } from "../widgets/dialogs/include_note.jsx";
|
||||||
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
||||||
import type { MarkdownImportOpts } from "../widgets/dialogs/markdown_import.jsx";
|
import type { MarkdownImportOpts } from "../widgets/dialogs/markdown_import.jsx";
|
||||||
|
import type { InfoProps } from "../widgets/dialogs/info.jsx";
|
||||||
|
|
||||||
interface Layout {
|
interface Layout {
|
||||||
getRootWidget: (appContext: AppContext) => RootContainer;
|
getRootWidget: (appContext: AppContext) => RootContainer;
|
||||||
@@ -124,7 +125,7 @@ export type CommandMappings = {
|
|||||||
isNewNote?: boolean;
|
isNewNote?: boolean;
|
||||||
};
|
};
|
||||||
showPromptDialog: PromptDialogOptions;
|
showPromptDialog: PromptDialogOptions;
|
||||||
showInfoDialog: ConfirmWithMessageOptions;
|
showInfoDialog: InfoProps;
|
||||||
showConfirmDialog: ConfirmWithMessageOptions;
|
showConfirmDialog: ConfirmWithMessageOptions;
|
||||||
showRecentChanges: CommandData & { ancestorNoteId: string };
|
showRecentChanges: CommandData & { ancestorNoteId: string };
|
||||||
showImportDialog: CommandData & { noteId: string };
|
showImportDialog: CommandData & { noteId: string };
|
||||||
@@ -264,7 +265,7 @@ export type CommandMappings = {
|
|||||||
|
|
||||||
reEvaluateRightPaneVisibility: CommandData;
|
reEvaluateRightPaneVisibility: CommandData;
|
||||||
runActiveNote: CommandData;
|
runActiveNote: CommandData;
|
||||||
scrollContainerToCommand: CommandData & {
|
scrollContainerTo: CommandData & {
|
||||||
position: number;
|
position: number;
|
||||||
};
|
};
|
||||||
scrollToEnd: CommandData;
|
scrollToEnd: CommandData;
|
||||||
@@ -445,6 +446,7 @@ type EventMappings = {
|
|||||||
error: string;
|
error: string;
|
||||||
};
|
};
|
||||||
searchRefreshed: { ntxId?: string | null };
|
searchRefreshed: { ntxId?: string | null };
|
||||||
|
textEditorRefreshed: { ntxId?: string | null, editor: CKTextEditor };
|
||||||
hoistedNoteChanged: {
|
hoistedNoteChanged: {
|
||||||
noteId: string;
|
noteId: string;
|
||||||
ntxId: string | null;
|
ntxId: string | null;
|
||||||
@@ -486,7 +488,7 @@ type EventMappings = {
|
|||||||
relationMapResetPanZoom: { ntxId: string | null | undefined };
|
relationMapResetPanZoom: { ntxId: string | null | undefined };
|
||||||
relationMapResetZoomIn: { ntxId: string | null | undefined };
|
relationMapResetZoomIn: { ntxId: string | null | undefined };
|
||||||
relationMapResetZoomOut: { ntxId: string | null | undefined };
|
relationMapResetZoomOut: { ntxId: string | null | undefined };
|
||||||
activeNoteChanged: {};
|
activeNoteChanged: {ntxId: string | null | undefined};
|
||||||
showAddLinkDialog: AddLinkOpts;
|
showAddLinkDialog: AddLinkOpts;
|
||||||
showIncludeDialog: IncludeNoteOpts;
|
showIncludeDialog: IncludeNoteOpts;
|
||||||
openBulkActionsDialog: {
|
openBulkActionsDialog: {
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
|
||||||
import server from "../services/server.js";
|
|
||||||
import utils from "../services/utils.js";
|
|
||||||
import appContext, { type EventData, type EventListener } from "./app_context.js";
|
|
||||||
import treeService from "../services/tree.js";
|
|
||||||
import Component from "./component.js";
|
|
||||||
import froca from "../services/froca.js";
|
|
||||||
import hoistedNoteService from "../services/hoisted_note.js";
|
|
||||||
import options from "../services/options.js";
|
|
||||||
import type { ViewScope } from "../services/link.js";
|
|
||||||
import type FNote from "../entities/fnote.js";
|
|
||||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||||
import type CodeMirror from "@triliumnext/codemirror";
|
import type CodeMirror from "@triliumnext/codemirror";
|
||||||
|
|
||||||
|
import type FNote from "../entities/fnote.js";
|
||||||
import { closeActiveDialog } from "../services/dialog.js";
|
import { closeActiveDialog } from "../services/dialog.js";
|
||||||
|
import froca from "../services/froca.js";
|
||||||
|
import hoistedNoteService from "../services/hoisted_note.js";
|
||||||
|
import type { ViewScope } from "../services/link.js";
|
||||||
|
import options from "../services/options.js";
|
||||||
|
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||||
|
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 { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
||||||
|
import appContext, { type EventData, type EventListener } from "./app_context.js";
|
||||||
|
import Component from "./component.js";
|
||||||
|
|
||||||
export interface SetNoteOpts {
|
export interface SetNoteOpts {
|
||||||
triggerSwitchEvent?: unknown;
|
triggerSwitchEvent?: unknown;
|
||||||
@@ -321,6 +322,10 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (note.type === "search") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "")) {
|
if (!["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import openService from "../services/open.js";
|
|||||||
import protectedSessionService from "../services/protected_session.js";
|
import protectedSessionService from "../services/protected_session.js";
|
||||||
import options from "../services/options.js";
|
import options from "../services/options.js";
|
||||||
import froca from "../services/froca.js";
|
import froca from "../services/froca.js";
|
||||||
import utils from "../services/utils.js";
|
import utils, { openInReusableSplit } from "../services/utils.js";
|
||||||
import toastService from "../services/toast.js";
|
import toastService from "../services/toast.js";
|
||||||
import noteCreateService from "../services/note_create.js";
|
import noteCreateService from "../services/note_create.js";
|
||||||
|
|
||||||
@@ -193,6 +193,16 @@ export default class RootCommandExecutor extends Component {
|
|||||||
appContext.triggerEvent("zenModeChanged", { isEnabled });
|
appContext.triggerEvent("zenModeChanged", { isEnabled });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async toggleRibbonTabNoteMapCommand() {
|
||||||
|
const { isExperimentalFeatureEnabled } = await import("../services/experimental_features.js");
|
||||||
|
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||||
|
if (!isNewLayout) return;
|
||||||
|
|
||||||
|
const activeContext = appContext.tabManager.getActiveContext();
|
||||||
|
if (!activeContext?.notePath) return;
|
||||||
|
openInReusableSplit(activeContext.notePath, "note-map");
|
||||||
|
}
|
||||||
|
|
||||||
firstTabCommand() {
|
firstTabCommand() {
|
||||||
this.#goToTab(1);
|
this.#goToTab(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ export default class TabManager extends Component {
|
|||||||
const activeNoteContext = this.getActiveContext();
|
const activeNoteContext = this.getActiveContext();
|
||||||
this.updateDocumentTitle(activeNoteContext);
|
this.updateDocumentTitle(activeNoteContext);
|
||||||
|
|
||||||
this.triggerEvent("activeNoteChanged", {}); // trigger this even in on popstate event
|
this.triggerEvent("activeNoteChanged", {ntxId:activeNoteContext?.ntxId}); // trigger this even in on popstate event
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateHash(): string {
|
calculateHash(): string {
|
||||||
@@ -647,7 +647,32 @@ export default class TabManager extends Component {
|
|||||||
...this.noteContexts.slice(-noteContexts.length),
|
...this.noteContexts.slice(-noteContexts.length),
|
||||||
...this.noteContexts.slice(lastClosedTab.position, -noteContexts.length)
|
...this.noteContexts.slice(lastClosedTab.position, -noteContexts.length)
|
||||||
];
|
];
|
||||||
this.noteContextReorderEvent({ ntxIdsInOrder: ntxsInOrder.map((nc) => nc.ntxId).filter((id) => id !== null) });
|
|
||||||
|
// Update mainNtxId if the restored pane is the main pane in the split pane
|
||||||
|
const { oldMainNtxId, newMainNtxId } = (() => {
|
||||||
|
if (noteContexts.length !== 1) {
|
||||||
|
return { oldMainNtxId: undefined, newMainNtxId: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainNtxId = noteContexts[0]?.mainNtxId;
|
||||||
|
const index = this.noteContexts.findIndex(c => c.ntxId === mainNtxId);
|
||||||
|
|
||||||
|
// No need to update if the restored position is after mainNtxId
|
||||||
|
if (index === -1 || lastClosedTab.position > index) {
|
||||||
|
return { oldMainNtxId: undefined, newMainNtxId: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
oldMainNtxId: this.noteContexts[index].ntxId ?? undefined,
|
||||||
|
newMainNtxId: noteContexts[0]?.ntxId ?? undefined
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
this.triggerCommand("noteContextReorder", {
|
||||||
|
ntxIdsInOrder: ntxsInOrder.map((nc) => nc.ntxId).filter((id) => id !== null),
|
||||||
|
oldMainNtxId,
|
||||||
|
newMainNtxId
|
||||||
|
});
|
||||||
|
|
||||||
let mainNtx = noteContexts.find((nc) => nc.isMainContext());
|
let mainNtx = noteContexts.find((nc) => nc.isMainContext());
|
||||||
if (mainNtx) {
|
if (mainNtx) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ bundleService.getWidgetBundlesByParent().then(async (widgetBundles) => {
|
|||||||
appContext.setLayout(new DesktopLayout(widgetBundles));
|
appContext.setLayout(new DesktopLayout(widgetBundles));
|
||||||
appContext.start().catch((e) => {
|
appContext.start().catch((e) => {
|
||||||
toastService.showPersistent({
|
toastService.showPersistent({
|
||||||
|
id: "critical-error",
|
||||||
title: t("toast.critical-error.title"),
|
title: t("toast.critical-error.title"),
|
||||||
icon: "alert",
|
icon: "alert",
|
||||||
message: t("toast.critical-error.message", { message: e.message })
|
message: t("toast.critical-error.message", { message: e.message })
|
||||||
@@ -58,10 +59,14 @@ function initOnElectron() {
|
|||||||
|
|
||||||
initDarkOrLightMode(style);
|
initDarkOrLightMode(style);
|
||||||
initTransparencyEffects(style, currentWindow);
|
initTransparencyEffects(style, currentWindow);
|
||||||
|
initFullScreenDetection(currentWindow);
|
||||||
|
|
||||||
if (options.get("nativeTitleBarVisible") !== "true") {
|
if (options.get("nativeTitleBarVisible") !== "true") {
|
||||||
initTitleBarButtons(style, currentWindow);
|
initTitleBarButtons(style, currentWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear navigation history on frontend refresh.
|
||||||
|
currentWindow.webContents.navigationHistory.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
|
function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
|
||||||
@@ -87,6 +92,11 @@ function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initFullScreenDetection(currentWindow: Electron.BrowserWindow) {
|
||||||
|
currentWindow.on("enter-full-screen", () => document.body.classList.add("full-screen"));
|
||||||
|
currentWindow.on("leave-full-screen", () => document.body.classList.remove("full-screen"));
|
||||||
|
}
|
||||||
|
|
||||||
function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
|
function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
|
||||||
if (window.glob.platform === "win32") {
|
if (window.glob.platform === "win32") {
|
||||||
const material = style.getPropertyValue("--background-material");
|
const material = style.getPropertyValue("--background-material");
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ export default class FNote {
|
|||||||
|
|
||||||
const aNote = this.froca.getNoteFromCache(aNoteId);
|
const aNote = this.froca.getNoteFromCache(aNoteId);
|
||||||
|
|
||||||
if (aNote.isArchived || aNote.isHiddenCompletely()) {
|
if (!aNote || aNote.isArchived || aNote.isHiddenCompletely()) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +1,57 @@
|
|||||||
import { applyModals } from "./layout_commons.js";
|
import type { AppContext } from "../components/app_context.js";
|
||||||
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
import type { WidgetsByParent } from "../services/bundle.js";
|
||||||
|
import { isExperimentalFeatureEnabled } from "../services/experimental_features.js";
|
||||||
|
import options from "../services/options.js";
|
||||||
|
import utils from "../services/utils.js";
|
||||||
import ApiLog from "../widgets/api_log.jsx";
|
import ApiLog from "../widgets/api_log.jsx";
|
||||||
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
||||||
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
|
||||||
import ContentHeader from "../widgets/containers/content_header.js";
|
|
||||||
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
||||||
import FindWidget from "../widgets/find.js";
|
|
||||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
|
||||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
|
||||||
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
|
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
|
||||||
import HighlightsListWidget from "../widgets/highlights_list.js";
|
|
||||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
|
||||||
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
|
||||||
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
|
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
|
||||||
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
||||||
import NoteIconWidget from "../widgets/note_icon.jsx";
|
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
||||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||||
import NoteTitleWidget from "../widgets/note_title.jsx";
|
import ContentHeader from "../widgets/containers/content_header.js";
|
||||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
||||||
import options from "../services/options.js";
|
|
||||||
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
|
||||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
|
||||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
|
||||||
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
|
||||||
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
|
||||||
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
||||||
import RootContainer from "../widgets/containers/root_container.js";
|
import RootContainer from "../widgets/containers/root_container.js";
|
||||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||||
|
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||||
|
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
||||||
|
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
||||||
|
import FindWidget from "../widgets/find.js";
|
||||||
|
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||||
|
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||||
|
import HighlightsListWidget from "../widgets/highlights_list.js";
|
||||||
|
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||||
|
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
|
||||||
|
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
|
||||||
|
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
|
||||||
|
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
|
||||||
|
import StatusBar from "../widgets/layout/StatusBar.jsx";
|
||||||
|
import NoteIconWidget from "../widgets/note_icon.jsx";
|
||||||
|
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||||
|
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 ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
||||||
|
import { FixedFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.jsx";
|
||||||
|
import NoteActions from "../widgets/ribbon/NoteActions.jsx";
|
||||||
|
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
||||||
import ScrollPadding from "../widgets/scroll_padding.js";
|
import ScrollPadding from "../widgets/scroll_padding.js";
|
||||||
import SearchResult from "../widgets/search_result.jsx";
|
import SearchResult from "../widgets/search_result.jsx";
|
||||||
import SharedInfo from "../widgets/shared_info.jsx";
|
import SharedInfo from "../widgets/shared_info.jsx";
|
||||||
import SpacerWidget from "../widgets/spacer.js";
|
|
||||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
|
||||||
import SqlResults from "../widgets/sql_result.js";
|
import SqlResults from "../widgets/sql_result.js";
|
||||||
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
|
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
|
||||||
import TabRowWidget from "../widgets/tab_row.js";
|
import TabRowWidget from "../widgets/tab_row.js";
|
||||||
|
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
|
||||||
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
|
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
|
||||||
import TocWidget from "../widgets/toc.js";
|
import TocWidget from "../widgets/toc.js";
|
||||||
import type { AppContext } from "../components/app_context.js";
|
|
||||||
import type { WidgetsByParent } from "../services/bundle.js";
|
|
||||||
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
|
||||||
import utils from "../services/utils.js";
|
|
||||||
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
||||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
import { applyModals } from "./layout_commons.js";
|
||||||
|
|
||||||
export default class DesktopLayout {
|
export default class DesktopLayout {
|
||||||
|
|
||||||
@@ -69,16 +77,18 @@ export default class DesktopLayout {
|
|||||||
*/
|
*/
|
||||||
const fullWidthTabBar = launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac);
|
const fullWidthTabBar = launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac);
|
||||||
const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows;
|
const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows;
|
||||||
|
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||||
|
|
||||||
const rootContainer = new RootContainer(true)
|
const rootContainer = new RootContainer(true)
|
||||||
.setParent(appContext)
|
.setParent(appContext)
|
||||||
.class((launcherPaneIsHorizontal ? "horizontal" : "vertical") + "-layout")
|
.class(`${launcherPaneIsHorizontal ? "horizontal" : "vertical" }-layout`)
|
||||||
.optChild(
|
.optChild(
|
||||||
fullWidthTabBar,
|
fullWidthTabBar,
|
||||||
new FlexContainer("row")
|
new FlexContainer("row")
|
||||||
.class("tab-row-container")
|
.class("tab-row-container")
|
||||||
.child(new FlexContainer("row").id("tab-row-left-spacer"))
|
.child(new FlexContainer("row").id("tab-row-left-spacer"))
|
||||||
.optChild(launcherPaneIsHorizontal, <LeftPaneToggle isHorizontalLayout={true} />)
|
.optChild(launcherPaneIsHorizontal, <LeftPaneToggle isHorizontalLayout={true} />)
|
||||||
|
.child(<TabHistoryNavigationButtons />)
|
||||||
.child(new TabRowWidget().class("full-width"))
|
.child(new TabRowWidget().class("full-width"))
|
||||||
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
||||||
.css("height", "40px")
|
.css("height", "40px")
|
||||||
@@ -101,7 +111,13 @@ export default class DesktopLayout {
|
|||||||
new FlexContainer("column")
|
new FlexContainer("column")
|
||||||
.id("rest-pane")
|
.id("rest-pane")
|
||||||
.css("flex-grow", "1")
|
.css("flex-grow", "1")
|
||||||
.optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, <TitleBarButtons />).css("height", "40px"))
|
.optChild(!fullWidthTabBar,
|
||||||
|
new FlexContainer("row")
|
||||||
|
.child(<TabHistoryNavigationButtons />)
|
||||||
|
.child(new TabRowWidget())
|
||||||
|
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
||||||
|
.css("height", "40px"))
|
||||||
|
.optChild(isNewLayout, <FixedFormattingToolbar />)
|
||||||
.child(
|
.child(
|
||||||
new FlexContainer("row")
|
new FlexContainer("row")
|
||||||
.filling()
|
.filling()
|
||||||
@@ -115,32 +131,31 @@ export default class DesktopLayout {
|
|||||||
.child(
|
.child(
|
||||||
new SplitNoteContainer(() =>
|
new SplitNoteContainer(() =>
|
||||||
new NoteWrapperWidget()
|
new NoteWrapperWidget()
|
||||||
.child(
|
.child(new FlexContainer("row")
|
||||||
new FlexContainer("row")
|
.class("title-row")
|
||||||
.class("title-row")
|
.cssBlock(".title-row > * { margin: 5px; }")
|
||||||
.css("height", "50px")
|
.child(<NoteIconWidget />)
|
||||||
.css("min-height", "50px")
|
.child(<NoteTitleWidget />)
|
||||||
.css("align-items", "center")
|
.optChild(isNewLayout, <NoteBadges />)
|
||||||
.cssBlock(".title-row > * { margin: 5px; }")
|
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
|
||||||
.child(<NoteIconWidget />)
|
.optChild(!isNewLayout, <MovePaneButton direction="left" />)
|
||||||
.child(<NoteTitleWidget />)
|
.optChild(!isNewLayout, <MovePaneButton direction="right" />)
|
||||||
.child(new SpacerWidget(0, 1))
|
.optChild(!isNewLayout, <ClosePaneButton />)
|
||||||
.child(<MovePaneButton direction="left" />)
|
.optChild(!isNewLayout, <CreatePaneButton />)
|
||||||
.child(<MovePaneButton direction="right" />)
|
.optChild(isNewLayout, <NoteActions />))
|
||||||
.child(<ClosePaneButton />)
|
.optChild(!isNewLayout, <Ribbon />)
|
||||||
.child(<CreatePaneButton />)
|
|
||||||
)
|
|
||||||
.child(<Ribbon />)
|
|
||||||
.child(new WatchedFileUpdateStatusWidget())
|
.child(new WatchedFileUpdateStatusWidget())
|
||||||
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
||||||
.child(
|
.child(
|
||||||
new ScrollingContainer()
|
new ScrollingContainer()
|
||||||
.filling()
|
.filling()
|
||||||
.child(new ContentHeader()
|
.optChild(isNewLayout, <InlineTitle />)
|
||||||
|
.optChild(isNewLayout, <NoteTitleActions />)
|
||||||
|
.optChild(!isNewLayout, new ContentHeader()
|
||||||
.child(<ReadOnlyNoteInfoBar />)
|
.child(<ReadOnlyNoteInfoBar />)
|
||||||
.child(<SharedInfo />)
|
.child(<SharedInfo />)
|
||||||
)
|
)
|
||||||
.child(new PromotedAttributesWidget())
|
.optChild(!isNewLayout, <PromotedAttributes />)
|
||||||
.child(<SqlTableSchemas />)
|
.child(<SqlTableSchemas />)
|
||||||
.child(<NoteDetail />)
|
.child(<NoteDetail />)
|
||||||
.child(<NoteList media="screen" />)
|
.child(<NoteList media="screen" />)
|
||||||
@@ -157,6 +172,7 @@ export default class DesktopLayout {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.child(...this.customWidgets.get("center-pane"))
|
.child(...this.customWidgets.get("center-pane"))
|
||||||
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
new RightPaneContainer()
|
new RightPaneContainer()
|
||||||
@@ -165,8 +181,10 @@ export default class DesktopLayout {
|
|||||||
.child(...this.customWidgets.get("right-pane"))
|
.child(...this.customWidgets.get("right-pane"))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.optChild(!launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.optChild(launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
|
||||||
.child(<CloseZenModeButton />)
|
.child(<CloseZenModeButton />)
|
||||||
|
|
||||||
// Desktop-specific dialogs.
|
// Desktop-specific dialogs.
|
||||||
@@ -184,14 +202,14 @@ export default class DesktopLayout {
|
|||||||
launcherPane = new FlexContainer("row")
|
launcherPane = new FlexContainer("row")
|
||||||
.css("height", "53px")
|
.css("height", "53px")
|
||||||
.class("horizontal")
|
.class("horizontal")
|
||||||
.child(new LauncherContainer(true))
|
.child(<LauncherContainer isHorizontalLayout={true} />)
|
||||||
.child(<GlobalMenu isHorizontalLayout={true} />);
|
.child(<GlobalMenu isHorizontalLayout={true} />);
|
||||||
} else {
|
} else {
|
||||||
launcherPane = new FlexContainer("column")
|
launcherPane = new FlexContainer("column")
|
||||||
.css("width", "53px")
|
.css("width", "53px")
|
||||||
.class("vertical")
|
.class("vertical")
|
||||||
.child(<GlobalMenu isHorizontalLayout={false} />)
|
.child(<GlobalMenu isHorizontalLayout={false} />)
|
||||||
.child(new LauncherContainer(false))
|
.child(<LauncherContainer isHorizontalLayout={false} />)
|
||||||
.child(<LeftPaneToggle isHorizontalLayout={false} />);
|
.child(<LeftPaneToggle isHorizontalLayout={false} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,16 +22,9 @@ import RevisionsDialog from "../widgets/dialogs/revisions.js";
|
|||||||
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
|
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
|
||||||
import InfoDialog from "../widgets/dialogs/info.js";
|
import InfoDialog from "../widgets/dialogs/info.js";
|
||||||
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
|
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
|
||||||
import PopupEditorDialog from "../widgets/dialogs/popup_editor.js";
|
|
||||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
|
||||||
import NoteIconWidget from "../widgets/note_icon";
|
|
||||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
|
||||||
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
|
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
|
||||||
import NoteTitleWidget from "../widgets/note_title.jsx";
|
import PopupEditorDialog from "../widgets/dialogs/PopupEditor.jsx";
|
||||||
import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.js";
|
import ToastContainer from "../widgets/Toast.jsx";
|
||||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
|
||||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
|
||||||
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
|
||||||
|
|
||||||
export function applyModals(rootContainer: RootContainer) {
|
export function applyModals(rootContainer: RootContainer) {
|
||||||
rootContainer
|
rootContainer
|
||||||
@@ -57,16 +50,7 @@ export function applyModals(rootContainer: RootContainer) {
|
|||||||
.child(<ConfirmDialog />)
|
.child(<ConfirmDialog />)
|
||||||
.child(<PromptDialog />)
|
.child(<PromptDialog />)
|
||||||
.child(<IncorrectCpuArchDialog />)
|
.child(<IncorrectCpuArchDialog />)
|
||||||
.child(new PopupEditorDialog()
|
.child(<PopupEditorDialog />)
|
||||||
.child(new FlexContainer("row")
|
.child(<CallToActionDialog />)
|
||||||
.class("title-row")
|
.child(<ToastContainer />);
|
||||||
.css("align-items", "center")
|
|
||||||
.cssBlock(".title-row > * { margin: 5px; }")
|
|
||||||
.child(<NoteIconWidget />)
|
|
||||||
.child(<NoteTitleWidget />))
|
|
||||||
.child(<StandaloneRibbonAdapter component={FormattingToolbar} />)
|
|
||||||
.child(new PromotedAttributesWidget())
|
|
||||||
.child(<NoteDetail />)
|
|
||||||
.child(<NoteList media="screen" displayOnlyCollections />))
|
|
||||||
.child(<CallToActionDialog />);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,12 @@ import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
|||||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||||
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
||||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
|
||||||
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||||
import NoteTitleWidget from "../widgets/note_title.js";
|
import NoteTitleWidget from "../widgets/note_title.js";
|
||||||
import ContentHeader from "../widgets/containers/content_header.js";
|
import ContentHeader from "../widgets/containers/content_header.js";
|
||||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
|
||||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||||
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
||||||
import RootContainer from "../widgets/containers/root_container.js";
|
import RootContainer from "../widgets/containers/root_container.js";
|
||||||
@@ -29,9 +27,13 @@ import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button
|
|||||||
import type AppContext from "../components/app_context.js";
|
import type AppContext from "../components/app_context.js";
|
||||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||||
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.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";
|
||||||
|
|
||||||
const MOBILE_CSS = `
|
const MOBILE_CSS = `
|
||||||
<style>
|
<style>
|
||||||
|
span.keyboard-shortcut,
|
||||||
kbd {
|
kbd {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -141,33 +143,35 @@ export default class MobileLayout {
|
|||||||
.id("detail-container")
|
.id("detail-container")
|
||||||
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9")
|
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9")
|
||||||
.child(
|
.child(
|
||||||
new NoteWrapperWidget()
|
new SplitNoteContainer(() =>
|
||||||
.child(
|
new NoteWrapperWidget()
|
||||||
new FlexContainer("row")
|
.child(
|
||||||
.contentSized()
|
new FlexContainer("row")
|
||||||
.css("font-size", "larger")
|
.contentSized()
|
||||||
.css("align-items", "center")
|
.css("font-size", "larger")
|
||||||
.child(<ToggleSidebarButton />)
|
.css("align-items", "center")
|
||||||
.child(<NoteTitleWidget />)
|
.child(<ToggleSidebarButton />)
|
||||||
.child(<MobileDetailMenu />)
|
.child(<NoteTitleWidget />)
|
||||||
)
|
.child(<MobileDetailMenu />)
|
||||||
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
|
)
|
||||||
.child(new PromotedAttributesWidget())
|
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
|
||||||
.child(
|
.child(<PromotedAttributes />)
|
||||||
new ScrollingContainer()
|
.child(
|
||||||
.filling()
|
new ScrollingContainer()
|
||||||
.contentSized()
|
.filling()
|
||||||
.child(new ContentHeader()
|
.contentSized()
|
||||||
.child(<ReadOnlyNoteInfoBar />)
|
.child(new ContentHeader()
|
||||||
.child(<SharedInfoWidget />)
|
.child(<ReadOnlyNoteInfoBar />)
|
||||||
)
|
.child(<SharedInfoWidget />)
|
||||||
.child(<NoteDetail />)
|
)
|
||||||
.child(<NoteList media="screen" />)
|
.child(<NoteDetail />)
|
||||||
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
|
.child(<NoteList media="screen" />)
|
||||||
.child(<SearchResult />)
|
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
|
||||||
.child(<FilePropertiesWrapper />)
|
.child(<SearchResult />)
|
||||||
)
|
.child(<FilePropertiesWrapper />)
|
||||||
.child(<MobileEditorToolbar />)
|
)
|
||||||
|
.child(<MobileEditorToolbar />)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -179,7 +183,7 @@ export default class MobileLayout {
|
|||||||
.child(new FlexContainer("row")
|
.child(new FlexContainer("row")
|
||||||
.class("horizontal")
|
.class("horizontal")
|
||||||
.css("height", "53px")
|
.css("height", "53px")
|
||||||
.child(new LauncherContainer(true))
|
.child(<LauncherContainer isHorizontalLayout />)
|
||||||
.child(<GlobalMenuWidget isHorizontalLayout />)
|
.child(<GlobalMenuWidget isHorizontalLayout />)
|
||||||
.id("launcher-pane"))
|
.id("launcher-pane"))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { KeyboardActionNames } from "@triliumnext/commons";
|
|||||||
import keyboardActionService, { getActionSync } from "../services/keyboard_actions.js";
|
import keyboardActionService, { getActionSync } from "../services/keyboard_actions.js";
|
||||||
import note_tooltip from "../services/note_tooltip.js";
|
import note_tooltip from "../services/note_tooltip.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
import { should } from "vitest";
|
import { h, JSX, render } from "preact";
|
||||||
|
|
||||||
export interface ContextMenuOptions<T> {
|
export interface ContextMenuOptions<T> {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -15,6 +15,11 @@ export interface ContextMenuOptions<T> {
|
|||||||
onHide?: () => void;
|
onHide?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CustomMenuItem {
|
||||||
|
kind: "custom",
|
||||||
|
componentFn: () => JSX.Element | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MenuSeparatorItem {
|
export interface MenuSeparatorItem {
|
||||||
kind: "separator";
|
kind: "separator";
|
||||||
}
|
}
|
||||||
@@ -51,7 +56,7 @@ export interface MenuCommandItem<T> {
|
|||||||
columns?: number;
|
columns?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem | MenuHeader;
|
export type MenuItem<T> = MenuCommandItem<T> | CustomMenuItem | MenuSeparatorItem | MenuHeader;
|
||||||
export type MenuHandler<T> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
export type MenuHandler<T> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
||||||
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
|
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
|
||||||
|
|
||||||
@@ -160,16 +165,19 @@ class ContextMenu {
|
|||||||
let $group = $parent; // The current group or parent element to which items are being appended
|
let $group = $parent; // The current group or parent element to which items are being appended
|
||||||
let shouldStartNewGroup = false; // If true, the next item will start a new group
|
let shouldStartNewGroup = false; // If true, the next item will start a new group
|
||||||
let shouldResetGroup = false; // If true, the next item will be the last one from the group
|
let shouldResetGroup = false; // If true, the next item will be the last one from the group
|
||||||
|
let prevItemKind: string = "";
|
||||||
|
|
||||||
for (let index = 0; index < items.length; index++) {
|
for (let index = 0; index < items.length; index++) {
|
||||||
const item = items[index];
|
const item = items[index];
|
||||||
|
const itemKind = ("kind" in item) ? item.kind : "";
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the current item is a header, start a new group. This group will contain the
|
// If the current item is a header, start a new group. This group will contain the
|
||||||
// header and the next item that follows the header.
|
// header and the next item that follows the header.
|
||||||
if ("kind" in item && item.kind === "header") {
|
if (itemKind === "header") {
|
||||||
if (multicolumn && !shouldResetGroup) {
|
if (multicolumn && !shouldResetGroup) {
|
||||||
shouldStartNewGroup = true;
|
shouldStartNewGroup = true;
|
||||||
}
|
}
|
||||||
@@ -195,125 +203,25 @@ class ContextMenu {
|
|||||||
shouldStartNewGroup = false;
|
shouldStartNewGroup = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("kind" in item && item.kind === "separator") {
|
if (itemKind === "separator") {
|
||||||
|
if (prevItemKind === "separator") {
|
||||||
|
// Skip consecutive separators
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$group.append($("<div>").addClass("dropdown-divider"));
|
$group.append($("<div>").addClass("dropdown-divider"));
|
||||||
shouldResetGroup = true; // End the group after the next item
|
shouldResetGroup = true; // End the group after the next item
|
||||||
} else if ("kind" in item && item.kind === "header") {
|
} else if (itemKind === "header") {
|
||||||
$group.append($("<h6>").addClass("dropdown-header").text(item.title));
|
$group.append($("<h6>").addClass("dropdown-header").text((item as MenuHeader).title));
|
||||||
shouldResetGroup = true;
|
shouldResetGroup = true;
|
||||||
} else {
|
} else {
|
||||||
const $icon = $("<span>");
|
if (itemKind === "custom") {
|
||||||
|
// Custom menu item
|
||||||
if ("uiIcon" in item || "checked" in item) {
|
$group.append(this.createCustomMenuItem(item as CustomMenuItem));
|
||||||
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
|
} else {
|
||||||
if (icon) {
|
// Standard menu item
|
||||||
$icon.addClass(icon);
|
$group.append(this.createMenuItem(item as MenuCommandItem<any>));
|
||||||
} else {
|
|
||||||
$icon.append(" ");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const $link = $("<span>")
|
|
||||||
.append($icon)
|
|
||||||
.append(" ") // some space between icon and text
|
|
||||||
.append(item.title);
|
|
||||||
|
|
||||||
if ("badges" in item && item.badges) {
|
|
||||||
for (let badge of item.badges) {
|
|
||||||
const badgeElement = $(`<span class="badge">`).text(badge.title);
|
|
||||||
|
|
||||||
if (badge.className) {
|
|
||||||
badgeElement.addClass(badge.className);
|
|
||||||
}
|
|
||||||
|
|
||||||
$link.append(badgeElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("keyboardShortcut" in item && item.keyboardShortcut) {
|
|
||||||
const shortcuts = getActionSync(item.keyboardShortcut).effectiveShortcuts;
|
|
||||||
if (shortcuts) {
|
|
||||||
const allShortcuts: string[] = [];
|
|
||||||
for (const effectiveShortcut of shortcuts) {
|
|
||||||
allShortcuts.push(effectiveShortcut.split("+")
|
|
||||||
.map(key => `<kbd>${key}</kbd>`)
|
|
||||||
.join("+"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allShortcuts.length) {
|
|
||||||
const container = $("<span>").addClass("keyboard-shortcut");
|
|
||||||
container.append($(allShortcuts.join(",")));
|
|
||||||
$link.append(container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if ("shortcut" in item && item.shortcut) {
|
|
||||||
$link.append($("<kbd>").text(item.shortcut));
|
|
||||||
}
|
|
||||||
|
|
||||||
const $item = $("<li>")
|
|
||||||
.addClass("dropdown-item")
|
|
||||||
.append($link)
|
|
||||||
.on("contextmenu", (e) => false)
|
|
||||||
// important to use mousedown instead of click since the former does not change focus
|
|
||||||
// (especially important for focused text for spell check)
|
|
||||||
.on("mousedown", (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (e.which !== 1) {
|
|
||||||
// only left click triggers menu items
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isMobile && "items" in item && item.items) {
|
|
||||||
const $item = $(e.target).closest(".dropdown-item");
|
|
||||||
|
|
||||||
$item.toggleClass("submenu-open");
|
|
||||||
$item.find("ul.dropdown-menu").toggleClass("show");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("handler" in item && item.handler) {
|
|
||||||
item.handler(item, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.options?.selectMenuItemHandler(item, e);
|
|
||||||
|
|
||||||
// it's important to stop the propagation especially for sub-menus, otherwise the event
|
|
||||||
// might be handled again by top-level menu
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
$item.on("mouseup", (e) => {
|
|
||||||
// Prevent submenu from failing to expand on mobile
|
|
||||||
if (!this.isMobile || !("items" in item && item.items)) {
|
|
||||||
e.stopPropagation();
|
|
||||||
// Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below.
|
|
||||||
this.hide();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if ("enabled" in item && item.enabled !== undefined && !item.enabled) {
|
|
||||||
$item.addClass("disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("items" in item && item.items) {
|
|
||||||
$item.addClass("dropdown-submenu");
|
|
||||||
$link.addClass("dropdown-toggle");
|
|
||||||
|
|
||||||
const $subMenu = $("<ul>").addClass("dropdown-menu");
|
|
||||||
const hasColumns = !!item.columns && item.columns > 1;
|
|
||||||
if (!this.isMobile && hasColumns) {
|
|
||||||
$subMenu.css("column-count", item.columns!);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addItems($subMenu, item.items, hasColumns);
|
|
||||||
|
|
||||||
$item.append($subMenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
$group.append($item);
|
|
||||||
|
|
||||||
// After adding a menu item, if the previous item was a separator or header,
|
// After adding a menu item, if the previous item was a separator or header,
|
||||||
// reset the group so that the next item will be appended directly to the parent.
|
// reset the group so that the next item will be appended directly to the parent.
|
||||||
if (shouldResetGroup) {
|
if (shouldResetGroup) {
|
||||||
@@ -321,9 +229,126 @@ class ContextMenu {
|
|||||||
shouldResetGroup = false;
|
shouldResetGroup = false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prevItemKind = itemKind;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createCustomMenuItem(item: CustomMenuItem) {
|
||||||
|
const element = document.createElement("li");
|
||||||
|
element.classList.add("dropdown-custom-item");
|
||||||
|
element.onclick = () => this.hide();
|
||||||
|
render(h(item.componentFn, {}), element);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createMenuItem(item: MenuCommandItem<any>) {
|
||||||
|
const $icon = $("<span>");
|
||||||
|
|
||||||
|
if ("uiIcon" in item || "checked" in item) {
|
||||||
|
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
|
||||||
|
if (icon) {
|
||||||
|
$icon.addClass(icon);
|
||||||
|
} else {
|
||||||
|
$icon.append(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const $link = $("<span>")
|
||||||
|
.append($icon)
|
||||||
|
.append(" ") // some space between icon and text
|
||||||
|
.append(item.title);
|
||||||
|
|
||||||
|
if ("badges" in item && item.badges) {
|
||||||
|
for (let badge of item.badges) {
|
||||||
|
const badgeElement = $(`<span class="badge">`).text(badge.title);
|
||||||
|
|
||||||
|
if (badge.className) {
|
||||||
|
badgeElement.addClass(badge.className);
|
||||||
|
}
|
||||||
|
|
||||||
|
$link.append(badgeElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("keyboardShortcut" in item && item.keyboardShortcut) {
|
||||||
|
const shortcuts = getActionSync(item.keyboardShortcut).effectiveShortcuts;
|
||||||
|
if (shortcuts) {
|
||||||
|
const allShortcuts: string[] = [];
|
||||||
|
for (const effectiveShortcut of shortcuts) {
|
||||||
|
allShortcuts.push(effectiveShortcut.split("+")
|
||||||
|
.map(key => `<kbd>${key}</kbd>`)
|
||||||
|
.join("+"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allShortcuts.length) {
|
||||||
|
const container = $("<span>").addClass("keyboard-shortcut");
|
||||||
|
container.append($(allShortcuts.join(",")));
|
||||||
|
$link.append(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ("shortcut" in item && item.shortcut) {
|
||||||
|
$link.append($("<kbd>").text(item.shortcut));
|
||||||
|
}
|
||||||
|
|
||||||
|
const $item = $("<li>")
|
||||||
|
.addClass("dropdown-item")
|
||||||
|
.append($link)
|
||||||
|
.on("contextmenu", (e) => false)
|
||||||
|
// important to use mousedown instead of click since the former does not change focus
|
||||||
|
// (especially important for focused text for spell check)
|
||||||
|
.on("mousedown", (e) => {
|
||||||
|
if (e.which !== 1) {
|
||||||
|
// only left click triggers menu items
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isMobile && "items" in item && item.items) {
|
||||||
|
const $item = $(e.target).closest(".dropdown-item");
|
||||||
|
|
||||||
|
$item.toggleClass("submenu-open");
|
||||||
|
$item.find("ul.dropdown-menu").toggleClass("show");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent submenu from failing to expand on mobile
|
||||||
|
if (!("items" in item && item.items)) {
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("handler" in item && item.handler) {
|
||||||
|
item.handler(item, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.options?.selectMenuItemHandler(item, e);
|
||||||
|
|
||||||
|
// it's important to stop the propagation especially for sub-menus, otherwise the event
|
||||||
|
// might be handled again by top-level menu
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if ("enabled" in item && item.enabled !== undefined && !item.enabled) {
|
||||||
|
$item.addClass("disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("items" in item && item.items) {
|
||||||
|
$item.addClass("dropdown-submenu");
|
||||||
|
$link.addClass("dropdown-toggle");
|
||||||
|
|
||||||
|
const $subMenu = $("<ul>").addClass("dropdown-menu");
|
||||||
|
const hasColumns = !!item.columns && item.columns > 1;
|
||||||
|
if (!this.isMobile && hasColumns) {
|
||||||
|
$subMenu.css("column-count", item.columns!);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addItems($subMenu, item.items, hasColumns);
|
||||||
|
|
||||||
|
$item.append($subMenu);
|
||||||
|
}
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
|
|
||||||
async hide() {
|
async hide() {
|
||||||
this.options?.onHide?.();
|
this.options?.onHide?.();
|
||||||
this.$widget.removeClass("show");
|
this.$widget.removeClass("show");
|
||||||
|
|||||||
21
apps/client/src/menus/context_menu_utils.ts
Normal file
21
apps/client/src/menus/context_menu_utils.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { t } from "../services/i18n"
|
||||||
|
import attributes from "../services/attributes"
|
||||||
|
import FNote from "../entities/fnote"
|
||||||
|
|
||||||
|
export function getArchiveMenuItem(note: FNote) {
|
||||||
|
if (!note.isArchived) {
|
||||||
|
return {
|
||||||
|
title: t("board_view.archive-note"),
|
||||||
|
uiIcon: "bx bx-archive",
|
||||||
|
handler: () => attributes.addLabel(note.noteId, "archived")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
title: t("board_view.unarchive-note"),
|
||||||
|
uiIcon: "bx bx-archive-out",
|
||||||
|
handler: async () => {
|
||||||
|
attributes.removeOwnedLabelByName(note, "archived")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
86
apps/client/src/menus/custom-items/NoteColorPicker.css
Normal file
86
apps/client/src/menus/custom-items/NoteColorPicker.css
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
:root {
|
||||||
|
--note-color-picker-clear-color-cell-background: var(--primary-button-background-color);
|
||||||
|
--note-color-picker-clear-color-cell-color: var(--main-background-color);
|
||||||
|
--note-color-picker-clear-color-cell-selection-outline-color: var(--primary-button-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-color-picker {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-color-picker .color-cell {
|
||||||
|
--color-picker-cell-size: 14px;
|
||||||
|
|
||||||
|
width: var(--color-picker-cell-size);
|
||||||
|
height: var(--color-picker-cell-size);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-color-picker .color-cell:not(.selected):hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-color-picker .color-cell.disabled-color-cell {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-color-picker .color-cell.selected {
|
||||||
|
outline: 2px solid var(--outline-color, var(--color));
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* RESET COLOR CELL
|
||||||
|
*/
|
||||||
|
|
||||||
|
.note-color-picker .color-cell-reset {
|
||||||
|
--color: var(--note-color-picker-clear-color-cell-background);
|
||||||
|
--outline-color: var(--note-color-picker-clear-color-cell-selection-outline-color);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-color-picker .color-cell-reset svg {
|
||||||
|
width: var(--color-picker-cell-size);
|
||||||
|
height: var(--color-picker-cell-size);
|
||||||
|
fill: var(--note-color-picker-clear-color-cell-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* CUSTOM COLOR CELL
|
||||||
|
*/
|
||||||
|
|
||||||
|
.note-color-picker .custom-color-cell::before {
|
||||||
|
position: absolute;
|
||||||
|
content: "\ed35";
|
||||||
|
display: flex;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
font-size: calc(var(--color-picker-cell-size) * 1.3);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-family: boxicons;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-color-picker .custom-color-cell {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-color-picker .custom-color-cell.custom-color-cell-empty {
|
||||||
|
background-image: url(./custom-color.png);
|
||||||
|
background-size: cover;
|
||||||
|
--foreground: transparent;
|
||||||
|
}
|
||||||
204
apps/client/src/menus/custom-items/NoteColorPicker.tsx
Normal file
204
apps/client/src/menus/custom-items/NoteColorPicker.tsx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import "./NoteColorPicker.css";
|
||||||
|
import { t } from "../../services/i18n";
|
||||||
|
import { useCallback, useEffect, useRef, useState} from "preact/hooks";
|
||||||
|
import {ComponentChildren} from "preact";
|
||||||
|
import attributes from "../../services/attributes";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import Color, { ColorInstance } from "color";
|
||||||
|
import Debouncer from "../../utils/debouncer";
|
||||||
|
import FNote from "../../entities/fnote";
|
||||||
|
import froca from "../../services/froca";
|
||||||
|
import { isMobile } from "../../services/utils";
|
||||||
|
|
||||||
|
const COLOR_PALETTE = [
|
||||||
|
"#e64d4d", "#e6994d", "#e5e64d", "#99e64d", "#4de64d", "#4de699",
|
||||||
|
"#4de5e6", "#4d99e6", "#4d4de6", "#994de6", "#e64db3"
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface NoteColorPickerProps {
|
||||||
|
/** The target Note instance or its ID string. */
|
||||||
|
note: FNote | string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NoteColorPicker(props: NoteColorPickerProps) {
|
||||||
|
if (!props.note) return null;
|
||||||
|
|
||||||
|
const [note, setNote] = useState<FNote | null>(null);
|
||||||
|
const [currentColor, setCurrentColor] = useState<string | null>(null);
|
||||||
|
const [isCustomColor, setIsCustomColor] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const retrieveNote = async (noteId: string) => {
|
||||||
|
const noteInstance = await froca.getNote(noteId, true);
|
||||||
|
if (noteInstance) {
|
||||||
|
setNote(noteInstance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof props.note === "string") {
|
||||||
|
retrieveNote(props.note); // Get the note from the given ID string
|
||||||
|
} else {
|
||||||
|
setNote(props.note);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const colorLabel = note?.getLabel("color")?.value ?? null;
|
||||||
|
if (colorLabel) {
|
||||||
|
let color = tryParseColor(colorLabel);
|
||||||
|
if (color) {
|
||||||
|
setCurrentColor(color.hex().toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [note]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsCustomColor(currentColor !== null && COLOR_PALETTE.indexOf(currentColor) === -1);
|
||||||
|
}, [currentColor])
|
||||||
|
|
||||||
|
const onColorCellClicked = useCallback((color: string | null) => {
|
||||||
|
if (note) {
|
||||||
|
if (color !== null) {
|
||||||
|
attributes.setLabel(note.noteId, "color", color);
|
||||||
|
} else {
|
||||||
|
attributes.removeOwnedLabelByName(note, "color");
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentColor(color);
|
||||||
|
}
|
||||||
|
}, [note, currentColor]);
|
||||||
|
|
||||||
|
return <div className="note-color-picker">
|
||||||
|
|
||||||
|
<ColorCell className="color-cell-reset"
|
||||||
|
tooltip={t("note-color.clear-color")}
|
||||||
|
color={null}
|
||||||
|
isSelected={(currentColor === null)}
|
||||||
|
isDisabled={(note === null)}
|
||||||
|
onSelect={onColorCellClicked}>
|
||||||
|
|
||||||
|
{/* https://pictogrammers.com/library/mdi/icon/close/ */}
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
|
||||||
|
</svg>
|
||||||
|
</ColorCell>
|
||||||
|
|
||||||
|
|
||||||
|
{COLOR_PALETTE.map((color) => (
|
||||||
|
<ColorCell key={color}
|
||||||
|
tooltip={t("note-color.set-color")}
|
||||||
|
color={color}
|
||||||
|
isSelected={(color === currentColor)}
|
||||||
|
isDisabled={(note === null)}
|
||||||
|
onSelect={onColorCellClicked} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
<CustomColorCell tooltip={t("note-color.set-custom-color")}
|
||||||
|
color={currentColor}
|
||||||
|
isSelected={isCustomColor}
|
||||||
|
isDisabled={(note === null)}
|
||||||
|
onSelect={onColorCellClicked} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ColorCellProps {
|
||||||
|
children?: ComponentChildren,
|
||||||
|
className?: string,
|
||||||
|
tooltip?: string,
|
||||||
|
color: string | null,
|
||||||
|
isSelected: boolean,
|
||||||
|
isDisabled?: boolean,
|
||||||
|
onSelect?: (color: string | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function ColorCell(props: ColorCellProps) {
|
||||||
|
return <div className={clsx(props.className, {
|
||||||
|
"color-cell": true,
|
||||||
|
"selected": props.isSelected,
|
||||||
|
"disabled-color-cell": props.isDisabled
|
||||||
|
})}
|
||||||
|
style={`${(props.color !== null) ? `--color: ${props.color}` : ""}`}
|
||||||
|
title={props.tooltip}
|
||||||
|
onClick={() => props.onSelect?.(props.color)}>
|
||||||
|
{props.children}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomColorCell(props: ColorCellProps) {
|
||||||
|
const [pickedColor, setPickedColor] = useState<string | null>(null);
|
||||||
|
const colorInput = useRef<HTMLInputElement>(null);
|
||||||
|
const colorInputDebouncer = useRef<Debouncer<string | null> | null>(null);
|
||||||
|
const callbackRef = useRef(props.onSelect);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
colorInputDebouncer.current = new Debouncer(250, (color) => {
|
||||||
|
callbackRef.current?.(color);
|
||||||
|
setPickedColor(color);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
colorInputDebouncer.current?.destroy();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.isSelected && pickedColor === null) {
|
||||||
|
setPickedColor(props.color);
|
||||||
|
}
|
||||||
|
}, [props.isSelected])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
callbackRef.current = props.onSelect;
|
||||||
|
}, [props.onSelect]);
|
||||||
|
|
||||||
|
const onSelect = useCallback(() => {
|
||||||
|
if (pickedColor !== null) {
|
||||||
|
callbackRef.current?.(pickedColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
colorInput.current?.click();
|
||||||
|
}, [pickedColor]);
|
||||||
|
|
||||||
|
return <div style={`--foreground: ${getForegroundColor(props.color)};`}
|
||||||
|
onClick={isMobile() ? (e) => {
|
||||||
|
// The color picker dropdown will close on some browser if the parent context menu is
|
||||||
|
// dismissed, so stop the click propagation to prevent dismissing the menu.
|
||||||
|
e.stopPropagation();
|
||||||
|
} : undefined}>
|
||||||
|
<ColorCell {...props}
|
||||||
|
color={pickedColor}
|
||||||
|
className={clsx("custom-color-cell", {
|
||||||
|
"custom-color-cell-empty": (pickedColor === null)
|
||||||
|
})}
|
||||||
|
onSelect={onSelect}>
|
||||||
|
|
||||||
|
<input ref={colorInput}
|
||||||
|
type="color"
|
||||||
|
value={pickedColor ?? props.color ?? "#40bfbf"}
|
||||||
|
onChange={() => {colorInputDebouncer.current?.updateValue(colorInput.current?.value ?? null)}}
|
||||||
|
style="width: 0; height: 0; opacity: 0" />
|
||||||
|
</ColorCell>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
function getForegroundColor(backgroundColor: string | null) {
|
||||||
|
if (backgroundColor === null) return "inherit";
|
||||||
|
|
||||||
|
const colorHsl = tryParseColor(backgroundColor)?.hsl();
|
||||||
|
if (colorHsl) {
|
||||||
|
let l = colorHsl.lightness();
|
||||||
|
return colorHsl.saturationl(0).lightness(l >= 50 ? 0 : 100).hex();
|
||||||
|
} else {
|
||||||
|
return "inherit";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryParseColor(colorStr: string): ColorInstance | null {
|
||||||
|
try {
|
||||||
|
return new Color(colorStr);
|
||||||
|
} catch(ex) {
|
||||||
|
console.error(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
BIN
apps/client/src/menus/custom-items/custom-color.png
Normal file
BIN
apps/client/src/menus/custom-items/custom-color.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -4,7 +4,7 @@ import zoomService from "../components/zoom.js";
|
|||||||
import contextMenu, { type MenuItem } from "./context_menu.js";
|
import contextMenu, { type MenuItem } from "./context_menu.js";
|
||||||
import { t } from "../services/i18n.js";
|
import { t } from "../services/i18n.js";
|
||||||
import type { BrowserWindow } from "electron";
|
import type { BrowserWindow } from "electron";
|
||||||
import type { CommandNames } from "../components/app_context.js";
|
import type { CommandNames, AppContext } from "../components/app_context.js";
|
||||||
|
|
||||||
function setupContextMenu() {
|
function setupContextMenu() {
|
||||||
const electron = utils.dynamicRequire("electron");
|
const electron = utils.dynamicRequire("electron");
|
||||||
@@ -13,6 +13,8 @@ function setupContextMenu() {
|
|||||||
// FIXME: Remove typecast once Electron is properly integrated.
|
// FIXME: Remove typecast once Electron is properly integrated.
|
||||||
const { webContents } = remote.getCurrentWindow() as BrowserWindow;
|
const { webContents } = remote.getCurrentWindow() as BrowserWindow;
|
||||||
|
|
||||||
|
let appContext: AppContext;
|
||||||
|
|
||||||
webContents.on("context-menu", (event, params) => {
|
webContents.on("context-menu", (event, params) => {
|
||||||
const { editFlags } = params;
|
const { editFlags } = params;
|
||||||
const hasText = params.selectionText.trim().length > 0;
|
const hasText = params.selectionText.trim().length > 0;
|
||||||
@@ -119,6 +121,20 @@ function setupContextMenu() {
|
|||||||
uiIcon: "bx bx-search-alt",
|
uiIcon: "bx bx-search-alt",
|
||||||
handler: () => electron.shell.openExternal(searchUrl)
|
handler: () => electron.shell.openExternal(searchUrl)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
title: t("electron_context_menu.search_in_trilium", { term: shortenedSelection }),
|
||||||
|
uiIcon: "bx bx-search",
|
||||||
|
handler: async () => {
|
||||||
|
if (!appContext) {
|
||||||
|
appContext = (await import("../components/app_context.js")).default;
|
||||||
|
}
|
||||||
|
|
||||||
|
await appContext.triggerCommand("searchNotes", {
|
||||||
|
searchString: params.selectionText
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
|
|||||||
@@ -2,26 +2,32 @@ import { t } from "../services/i18n.js";
|
|||||||
import contextMenu, { type ContextMenuEvent, type MenuItem } from "./context_menu.js";
|
import contextMenu, { type ContextMenuEvent, type MenuItem } from "./context_menu.js";
|
||||||
import appContext, { type CommandNames } from "../components/app_context.js";
|
import appContext, { type CommandNames } from "../components/app_context.js";
|
||||||
import type { ViewScope } from "../services/link.js";
|
import type { ViewScope } from "../services/link.js";
|
||||||
|
import utils, { isMobile } from "../services/utils.js";
|
||||||
|
import { getClosestNtxId } from "../widgets/widget_utils.js";
|
||||||
|
import type { LeafletMouseEvent } from "leaflet";
|
||||||
|
|
||||||
function openContextMenu(notePath: string, e: ContextMenuEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) {
|
function openContextMenu(notePath: string, e: ContextMenuEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) {
|
||||||
contextMenu.show({
|
contextMenu.show({
|
||||||
x: e.pageX,
|
x: e.pageX,
|
||||||
y: e.pageY,
|
y: e.pageY,
|
||||||
items: getItems(),
|
items: getItems(e),
|
||||||
selectMenuItemHandler: ({ command }) => handleLinkContextMenuItem(command, notePath, viewScope, hoistedNoteId)
|
selectMenuItemHandler: ({ command }) => handleLinkContextMenuItem(command, e, notePath, viewScope, hoistedNoteId)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getItems(): MenuItem<CommandNames>[] {
|
function getItems(e: ContextMenuEvent | LeafletMouseEvent): MenuItem<CommandNames>[] {
|
||||||
|
const ntxId = getNtxId(e);
|
||||||
|
const isMobileSplitOpen = isMobile() && appContext.tabManager.getNoteContextById(ntxId).getMainContext().getSubContexts().length > 1;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" },
|
{ title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" },
|
||||||
{ title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" },
|
{ title: !isMobileSplitOpen ? t("link_context_menu.open_note_in_new_split") : t("link_context_menu.open_note_in_other_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" },
|
||||||
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" },
|
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" },
|
||||||
{ title: t("link_context_menu.open_note_in_popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit" }
|
{ title: t("link_context_menu.open_note_in_popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit" }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLinkContextMenuItem(command: string | undefined, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
|
function handleLinkContextMenuItem(command: string | undefined, e: ContextMenuEvent | LeafletMouseEvent, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
|
||||||
if (!hoistedNoteId) {
|
if (!hoistedNoteId) {
|
||||||
hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId ?? null;
|
hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId ?? null;
|
||||||
}
|
}
|
||||||
@@ -29,15 +35,8 @@ function handleLinkContextMenuItem(command: string | undefined, notePath: string
|
|||||||
if (command === "openNoteInNewTab") {
|
if (command === "openNoteInNewTab") {
|
||||||
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
|
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
|
||||||
} else if (command === "openNoteInNewSplit") {
|
} else if (command === "openNoteInNewSplit") {
|
||||||
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
|
const ntxId = getNtxId(e);
|
||||||
|
if (!ntxId) return;
|
||||||
if (!subContexts) {
|
|
||||||
logError("subContexts is null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { ntxId } = subContexts[subContexts.length - 1];
|
|
||||||
|
|
||||||
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
|
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
|
||||||
} else if (command === "openNoteInNewWindow") {
|
} else if (command === "openNoteInNewWindow") {
|
||||||
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
|
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
|
||||||
@@ -46,6 +45,18 @@ function handleLinkContextMenuItem(command: string | undefined, notePath: string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNtxId(e: ContextMenuEvent | LeafletMouseEvent) {
|
||||||
|
if (utils.isDesktop()) {
|
||||||
|
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
|
||||||
|
if (!subContexts) return null;
|
||||||
|
return subContexts[subContexts.length - 1].ntxId;
|
||||||
|
} else if (e.target instanceof HTMLElement) {
|
||||||
|
return getClosestNtxId(e.target);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getItems,
|
getItems,
|
||||||
handleLinkContextMenuItem,
|
handleLinkContextMenuItem,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import NoteColorPicker from "./custom-items/NoteColorPicker.jsx";
|
||||||
import treeService from "../services/tree.js";
|
import treeService from "../services/tree.js";
|
||||||
import froca from "../services/froca.js";
|
import froca from "../services/froca.js";
|
||||||
import clipboard from "../services/clipboard.js";
|
import clipboard from "../services/clipboard.js";
|
||||||
@@ -139,7 +140,13 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
uiIcon: "bx bx-rename",
|
uiIcon: "bx bx-rename",
|
||||||
enabled: isNotRoot && parentNotSearch && notOptionsOrHelp
|
enabled: isNotRoot && parentNotSearch && notOptionsOrHelp
|
||||||
},
|
},
|
||||||
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },
|
{
|
||||||
|
title:
|
||||||
|
t("tree-context-menu.convert-to-attachment"),
|
||||||
|
command: "convertNoteToAttachment",
|
||||||
|
uiIcon: "bx bx-paperclip",
|
||||||
|
enabled: isNotRoot && !isHoisted && notOptionsOrHelp && selectedNotes.some(note => note.isEligibleForConversionToAttachment())
|
||||||
|
},
|
||||||
|
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
|
|
||||||
@@ -241,6 +248,15 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
enabled: isNotRoot && !isHoisted && parentNotSearch && notOptionsOrHelp
|
enabled: isNotRoot && !isHoisted && parentNotSearch && notOptionsOrHelp
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{ kind: "separator"},
|
||||||
|
|
||||||
|
(notOptionsOrHelp && selectedNotes.length === 1) ? {
|
||||||
|
kind: "custom",
|
||||||
|
componentFn: () => {
|
||||||
|
return NoteColorPicker({note});
|
||||||
|
}
|
||||||
|
} : null,
|
||||||
|
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
|
|
||||||
{ title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import", enabled: notSearch && noSelectedNotes && notOptionsOrHelp },
|
{ title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import", enabled: notSearch && noSelectedNotes && notOptionsOrHelp },
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import FNote from "./entities/fnote";
|
import FNote from "./entities/fnote";
|
||||||
import { render } from "preact";
|
import { render } from "preact";
|
||||||
import { CustomNoteList } from "./widgets/collections/NoteList";
|
import { CustomNoteList, useNoteViewType } from "./widgets/collections/NoteList";
|
||||||
import { useCallback, useLayoutEffect, useRef } from "preact/hooks";
|
import { useCallback, useLayoutEffect, useRef } from "preact/hooks";
|
||||||
import content_renderer from "./services/content_renderer";
|
import content_renderer from "./services/content_renderer";
|
||||||
|
import { dynamicRequire, isElectron } from "./services/utils";
|
||||||
|
import { applyInlineMermaid } from "./services/content_renderer_text";
|
||||||
|
|
||||||
interface RendererProps {
|
interface RendererProps {
|
||||||
note: FNote;
|
note: FNote;
|
||||||
onReady: () => void;
|
onReady: () => void;
|
||||||
|
onProgressChanged?: (progress: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@@ -23,13 +26,21 @@ async function main() {
|
|||||||
|
|
||||||
function App({ note, noteId }: { note: FNote | null | undefined, noteId: string }) {
|
function App({ note, noteId }: { note: FNote | null | undefined, noteId: string }) {
|
||||||
const sentReadyEvent = useRef(false);
|
const sentReadyEvent = useRef(false);
|
||||||
|
const onProgressChanged = useCallback((progress: number) => {
|
||||||
|
if (isElectron()) {
|
||||||
|
const { ipcRenderer } = dynamicRequire('electron');
|
||||||
|
ipcRenderer.send("print-progress", progress);
|
||||||
|
} else {
|
||||||
|
window.dispatchEvent(new CustomEvent("note-load-progress", { detail: { progress } }));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
const onReady = useCallback(() => {
|
const onReady = useCallback(() => {
|
||||||
if (sentReadyEvent.current) return;
|
if (sentReadyEvent.current) return;
|
||||||
window.dispatchEvent(new Event("note-ready"));
|
window.dispatchEvent(new Event("note-ready"));
|
||||||
window._noteReady = true;
|
window._noteReady = true;
|
||||||
sentReadyEvent.current = true;
|
sentReadyEvent.current = true;
|
||||||
}, []);
|
}, []);
|
||||||
const props: RendererProps | undefined | null = note && { note, onReady };
|
const props: RendererProps | undefined | null = note && { note, onReady, onProgressChanged };
|
||||||
|
|
||||||
if (!note || !props) return <Error404 noteId={noteId} />
|
if (!note || !props) return <Error404 noteId={noteId} />
|
||||||
|
|
||||||
@@ -71,6 +82,11 @@ function SingleNoteRenderer({ note, onReady }: RendererProps) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Initialize mermaid.
|
||||||
|
if (note.type === "text") {
|
||||||
|
await applyInlineMermaid(container);
|
||||||
|
}
|
||||||
|
|
||||||
// Check custom CSS.
|
// Check custom CSS.
|
||||||
await loadCustomCss(note);
|
await loadCustomCss(note);
|
||||||
}
|
}
|
||||||
@@ -84,8 +100,10 @@ function SingleNoteRenderer({ note, onReady }: RendererProps) {
|
|||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CollectionRenderer({ note, onReady }: RendererProps) {
|
function CollectionRenderer({ note, onReady, onProgressChanged }: RendererProps) {
|
||||||
|
const viewType = useNoteViewType(note);
|
||||||
return <CustomNoteList
|
return <CustomNoteList
|
||||||
|
viewType={viewType}
|
||||||
isEnabled
|
isEnabled
|
||||||
note={note}
|
note={note}
|
||||||
notePath={note.getBestNotePath().join("/")}
|
notePath={note.getBestNotePath().join("/")}
|
||||||
@@ -96,6 +114,7 @@ function CollectionRenderer({ note, onReady }: RendererProps) {
|
|||||||
await loadCustomCss(note);
|
await loadCustomCss(note);
|
||||||
onReady();
|
onReady();
|
||||||
}}
|
}}
|
||||||
|
onProgressChanged={onProgressChanged}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,9 +126,7 @@ function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This doesn't seem right.
|
if (attrRow.isInheritable) {
|
||||||
//@ts-ignore
|
|
||||||
if (this.isInheritable) {
|
|
||||||
for (const owningNote of owningNotes) {
|
for (const owningNote of owningNotes) {
|
||||||
if (owningNote.hasAncestor(attrNote.noteId, true)) {
|
if (owningNote.hasAncestor(attrNote.noteId, true)) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
import server from "./server.js";
|
import server from "./server.js";
|
||||||
import toastService, { type ToastOptions } from "./toast.js";
|
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
|
||||||
import froca from "./froca.js";
|
import froca from "./froca.js";
|
||||||
import hoistedNoteService from "./hoisted_note.js";
|
import hoistedNoteService from "./hoisted_note.js";
|
||||||
import ws from "./ws.js";
|
import ws from "./ws.js";
|
||||||
@@ -176,11 +176,6 @@ async function moveNodeUpInHierarchy(node: Fancytree.FancytreeNode) {
|
|||||||
toastService.showError(resp.message);
|
toastService.showError(resp.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hoistedNoteService.isTopLevelNode(node) && node.getParent().getChildren().length <= 1) {
|
|
||||||
node.getParent().folder = false;
|
|
||||||
node.getParent().renderTitle();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterSearchBranches(branchIds: string[]) {
|
function filterSearchBranches(branchIds: string[]) {
|
||||||
@@ -200,11 +195,11 @@ function filterRootNote(branchIds: string[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeToast(id: string, message: string): ToastOptions {
|
function makeToast(id: string, message: string): ToastOptionsWithRequiredId {
|
||||||
return {
|
return {
|
||||||
id: id,
|
id,
|
||||||
title: t("branches.delete-status"),
|
title: t("branches.delete-status"),
|
||||||
message: message,
|
message,
|
||||||
icon: "trash"
|
icon: "trash"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -221,7 +216,7 @@ ws.subscribeToMessages(async (message) => {
|
|||||||
toastService.showPersistent(makeToast(message.taskId, t("branches.delete-notes-in-progress", { count: message.progressCount })));
|
toastService.showPersistent(makeToast(message.taskId, t("branches.delete-notes-in-progress", { count: message.progressCount })));
|
||||||
} else if (message.type === "taskSucceeded") {
|
} else if (message.type === "taskSucceeded") {
|
||||||
const toast = makeToast(message.taskId, t("branches.delete-finished-successfully"));
|
const toast = makeToast(message.taskId, t("branches.delete-finished-successfully"));
|
||||||
toast.closeAfter = 5000;
|
toast.timeout = 5000;
|
||||||
|
|
||||||
toastService.showPersistent(toast);
|
toastService.showPersistent(toast);
|
||||||
}
|
}
|
||||||
@@ -239,7 +234,7 @@ ws.subscribeToMessages(async (message) => {
|
|||||||
toastService.showPersistent(makeToast(message.taskId, t("branches.undeleting-notes-in-progress", { count: message.progressCount })));
|
toastService.showPersistent(makeToast(message.taskId, t("branches.undeleting-notes-in-progress", { count: message.progressCount })));
|
||||||
} else if (message.type === "taskSucceeded") {
|
} else if (message.type === "taskSucceeded") {
|
||||||
const toast = makeToast(message.taskId, t("branches.undeleting-notes-finished-successfully"));
|
const toast = makeToast(message.taskId, t("branches.undeleting-notes-finished-successfully"));
|
||||||
toast.closeAfter = 5000;
|
toast.timeout = 5000;
|
||||||
|
|
||||||
toastService.showPersistent(toast);
|
toastService.showPersistent(toast);
|
||||||
}
|
}
|
||||||
@@ -247,7 +242,7 @@ ws.subscribeToMessages(async (message) => {
|
|||||||
|
|
||||||
async function cloneNoteToBranch(childNoteId: string, parentBranchId: string, prefix?: string) {
|
async function cloneNoteToBranch(childNoteId: string, parentBranchId: string, prefix?: string) {
|
||||||
const resp = await server.put<Response>(`notes/${childNoteId}/clone-to-branch/${parentBranchId}`, {
|
const resp = await server.put<Response>(`notes/${childNoteId}/clone-to-branch/${parentBranchId}`, {
|
||||||
prefix: prefix
|
prefix
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
@@ -257,7 +252,7 @@ async function cloneNoteToBranch(childNoteId: string, parentBranchId: string, pr
|
|||||||
|
|
||||||
async function cloneNoteToParentNote(childNoteId: string, parentNoteId: string, prefix?: string) {
|
async function cloneNoteToParentNote(childNoteId: string, parentNoteId: string, prefix?: string) {
|
||||||
const resp = await server.put<Response>(`notes/${childNoteId}/clone-to-note/${parentNoteId}`, {
|
const resp = await server.put<Response>(`notes/${childNoteId}/clone-to-note/${parentNoteId}`, {
|
||||||
prefix: prefix
|
prefix
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
|
|||||||
44
apps/client/src/services/bundle.spec.ts
Normal file
44
apps/client/src/services/bundle.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { Bundle, executeBundle } from "./bundle";
|
||||||
|
import { buildNote } from "../test/easy-froca";
|
||||||
|
|
||||||
|
describe("Script bundle", () => {
|
||||||
|
it("dayjs is available", async () => {
|
||||||
|
const script = /* js */`return api.dayjs().format("YYYY-MM-DD");`;
|
||||||
|
const bundle = getBundle(script);
|
||||||
|
const result = await executeBundle(bundle, null, $());
|
||||||
|
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dayjs is-same-or-before plugin exists", async () => {
|
||||||
|
const script = /* js */`return api.dayjs("2023-10-01").isSameOrBefore(api.dayjs("2023-10-02"));`;
|
||||||
|
const bundle = getBundle(script);
|
||||||
|
const result = await executeBundle(bundle, null, $());
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function getBundle(script: string) {
|
||||||
|
const id = buildNote({
|
||||||
|
title: "Script note"
|
||||||
|
}).noteId;
|
||||||
|
const bundle: Bundle = {
|
||||||
|
script: [
|
||||||
|
'',
|
||||||
|
`apiContext.modules['${id}'] = { exports: {} };`,
|
||||||
|
`return await ((async function(exports, module, require, api) {`,
|
||||||
|
`try {`,
|
||||||
|
`${script}`,
|
||||||
|
`;`,
|
||||||
|
`} catch (e) { throw new Error(\"Load of script note \\\"Client\\\" (${id}) failed with: \" + e.message); }`,
|
||||||
|
`for (const exportKey in exports) module.exports[exportKey] = exports[exportKey];`,
|
||||||
|
`return module.exports;`,
|
||||||
|
`}).call({}, {}, apiContext.modules['${id}'], apiContext.require([]), apiContext.apis['${id}']));`,
|
||||||
|
''
|
||||||
|
].join('\n'),
|
||||||
|
html: "",
|
||||||
|
noteId: id,
|
||||||
|
allNoteIds: [ id ]
|
||||||
|
};
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ async function getAndExecuteBundle(noteId: string, originEntity = null, script =
|
|||||||
return await executeBundle(bundle, originEntity);
|
return await executeBundle(bundle, originEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery<HTMLElement>) {
|
export async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery<HTMLElement>) {
|
||||||
const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity, $container);
|
const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity, $container);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -36,10 +36,17 @@ async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $cont
|
|||||||
}.call(apiContext);
|
}.call(apiContext);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const note = await froca.getNote(bundle.noteId);
|
const note = await froca.getNote(bundle.noteId);
|
||||||
|
toastService.showPersistent({
|
||||||
const message = `Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`;
|
id: `custom-script-failure-${note?.noteId}`,
|
||||||
showError(message);
|
title: t("toast.bundle-error.title"),
|
||||||
logError(message);
|
icon: "bx bx-error-circle",
|
||||||
|
message: t("toast.bundle-error.message", {
|
||||||
|
id: note?.noteId,
|
||||||
|
title: note?.title,
|
||||||
|
message: e.message
|
||||||
|
})
|
||||||
|
});
|
||||||
|
logError("Widget initialization failed: ", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,8 +109,9 @@ async function getWidgetBundlesByParent() {
|
|||||||
const noteId = bundle.noteId;
|
const noteId = bundle.noteId;
|
||||||
const note = await froca.getNote(noteId);
|
const note = await froca.getNote(noteId);
|
||||||
toastService.showPersistent({
|
toastService.showPersistent({
|
||||||
|
id: `custom-script-failure-${noteId}`,
|
||||||
title: t("toast.bundle-error.title"),
|
title: t("toast.bundle-error.title"),
|
||||||
icon: "alert",
|
icon: "bx bx-error-circle",
|
||||||
message: t("toast.bundle-error.message", {
|
message: t("toast.bundle-error.message", {
|
||||||
id: noteId,
|
id: noteId,
|
||||||
title: note?.title,
|
title: note?.title,
|
||||||
|
|||||||
@@ -2,24 +2,21 @@ import renderService from "./render.js";
|
|||||||
import protectedSessionService from "./protected_session.js";
|
import protectedSessionService from "./protected_session.js";
|
||||||
import protectedSessionHolder from "./protected_session_holder.js";
|
import protectedSessionHolder from "./protected_session_holder.js";
|
||||||
import openService from "./open.js";
|
import openService from "./open.js";
|
||||||
import froca from "./froca.js";
|
|
||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
import linkService from "./link.js";
|
|
||||||
import treeService from "./tree.js";
|
|
||||||
import FNote from "../entities/fnote.js";
|
import FNote from "../entities/fnote.js";
|
||||||
import FAttachment from "../entities/fattachment.js";
|
import FAttachment from "../entities/fattachment.js";
|
||||||
import imageContextMenuService from "../menus/image_context_menu.js";
|
import imageContextMenuService from "../menus/image_context_menu.js";
|
||||||
import { applySingleBlockSyntaxHighlight, formatCodeBlocks } from "./syntax_highlight.js";
|
import { applySingleBlockSyntaxHighlight } from "./syntax_highlight.js";
|
||||||
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
|
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
|
||||||
import renderDoc from "./doc_renderer.js";
|
import renderDoc from "./doc_renderer.js";
|
||||||
import { t } from "../services/i18n.js";
|
import { t } from "../services/i18n.js";
|
||||||
import WheelZoom from 'vanilla-js-wheel-zoom';
|
import WheelZoom from 'vanilla-js-wheel-zoom';
|
||||||
import { renderMathInElement } from "./math.js";
|
|
||||||
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||||
|
import renderText from "./content_renderer_text.js";
|
||||||
|
|
||||||
let idCounter = 1;
|
let idCounter = 1;
|
||||||
|
|
||||||
interface Options {
|
export interface RenderOptions {
|
||||||
tooltip?: boolean;
|
tooltip?: boolean;
|
||||||
trim?: boolean;
|
trim?: boolean;
|
||||||
imageHasZoom?: boolean;
|
imageHasZoom?: boolean;
|
||||||
@@ -29,7 +26,7 @@ interface Options {
|
|||||||
|
|
||||||
const CODE_MIME_TYPES = new Set(["application/json"]);
|
const CODE_MIME_TYPES = new Set(["application/json"]);
|
||||||
|
|
||||||
export async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: Options = {}) {
|
export async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: RenderOptions = {}) {
|
||||||
|
|
||||||
options = Object.assign(
|
options = Object.assign(
|
||||||
{
|
{
|
||||||
@@ -116,32 +113,6 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: Options = {}) {
|
|
||||||
// entity must be FNote
|
|
||||||
const blob = await note.getBlob();
|
|
||||||
|
|
||||||
if (blob && !utils.isHtmlEmpty(blob.content)) {
|
|
||||||
$renderedContent.append($('<div class="ck-content">').html(blob.content));
|
|
||||||
|
|
||||||
if ($renderedContent.find("span.math-tex").length > 0) {
|
|
||||||
renderMathInElement($renderedContent[0], { trust: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const getNoteIdFromLink = (el: HTMLElement) => treeService.getNoteIdFromUrl($(el).attr("href") || "");
|
|
||||||
const referenceLinks = $renderedContent.find("a.reference-link");
|
|
||||||
const noteIdsToPrefetch = referenceLinks.map((i, el) => getNoteIdFromLink(el));
|
|
||||||
await froca.getNotes(noteIdsToPrefetch);
|
|
||||||
|
|
||||||
for (const el of referenceLinks) {
|
|
||||||
await linkService.loadReferenceLinkTitle($(el));
|
|
||||||
}
|
|
||||||
|
|
||||||
await formatCodeBlocks($renderedContent);
|
|
||||||
} else if (note instanceof FNote && !options.noChildrenList) {
|
|
||||||
await renderChildrenList($renderedContent, note);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a code note, by displaying its content and applying syntax highlighting based on the selected MIME type.
|
* Renders a code note, by displaying its content and applying syntax highlighting based on the selected MIME type.
|
||||||
*/
|
*/
|
||||||
@@ -163,7 +134,7 @@ async function renderCode(note: FNote | FAttachment, $renderedContent: JQuery<HT
|
|||||||
await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
|
await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: Options = {}) {
|
function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: RenderOptions = {}) {
|
||||||
const encodedTitle = encodeURIComponent(entity.title);
|
const encodedTitle = encodeURIComponent(entity.title);
|
||||||
|
|
||||||
let url;
|
let url;
|
||||||
@@ -305,40 +276,6 @@ async function renderMermaid(note: FNote | FAttachment, $renderedContent: JQuery
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {jQuery} $renderedContent
|
|
||||||
* @param {FNote} note
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote) {
|
|
||||||
let childNoteIds = note.getChildNoteIds();
|
|
||||||
|
|
||||||
if (!childNoteIds.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$renderedContent.css("padding", "10px");
|
|
||||||
$renderedContent.addClass("text-with-ellipsis");
|
|
||||||
|
|
||||||
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) {
|
|
||||||
$renderedContent.append(
|
|
||||||
await linkService.createLink(`${note.noteId}/${childNote.noteId}`, {
|
|
||||||
showTooltip: false,
|
|
||||||
showNoteIcon: true
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
$renderedContent.append("<br>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRenderingType(entity: FNote | FAttachment) {
|
function getRenderingType(entity: FNote | FAttachment) {
|
||||||
let type: string = "";
|
let type: string = "";
|
||||||
if ("type" in entity) {
|
if ("type" in entity) {
|
||||||
|
|||||||
126
apps/client/src/services/content_renderer_text.ts
Normal file
126
apps/client/src/services/content_renderer_text.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { formatCodeBlocks } from "./syntax_highlight.js";
|
||||||
|
import { getMermaidConfig } from "./mermaid.js";
|
||||||
|
import { renderMathInElement } from "./math.js";
|
||||||
|
import FNote from "../entities/fnote.js";
|
||||||
|
import FAttachment from "../entities/fattachment.js";
|
||||||
|
import tree from "./tree.js";
|
||||||
|
import froca from "./froca.js";
|
||||||
|
import link from "./link.js";
|
||||||
|
import { isHtmlEmpty } from "./utils.js";
|
||||||
|
import { default as content_renderer, type RenderOptions } from "./content_renderer.js";
|
||||||
|
|
||||||
|
export default async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: RenderOptions = {}) {
|
||||||
|
// entity must be FNote
|
||||||
|
const blob = await note.getBlob();
|
||||||
|
|
||||||
|
if (blob && !isHtmlEmpty(blob.content)) {
|
||||||
|
$renderedContent.append($('<div class="ck-content">').html(blob.content));
|
||||||
|
await renderIncludedNotes($renderedContent[0]);
|
||||||
|
|
||||||
|
if ($renderedContent.find("span.math-tex").length > 0) {
|
||||||
|
renderMathInElement($renderedContent[0], { trust: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNoteIdFromLink = (el: HTMLElement) => tree.getNoteIdFromUrl($(el).attr("href") || "");
|
||||||
|
const referenceLinks = $renderedContent.find("a.reference-link");
|
||||||
|
const noteIdsToPrefetch = referenceLinks.map((i, el) => getNoteIdFromLink(el));
|
||||||
|
await froca.getNotes(noteIdsToPrefetch);
|
||||||
|
|
||||||
|
for (const el of referenceLinks) {
|
||||||
|
await link.loadReferenceLinkTitle($(el));
|
||||||
|
}
|
||||||
|
|
||||||
|
await rewriteMermaidDiagramsInContainer($renderedContent[0] as HTMLDivElement);
|
||||||
|
await formatCodeBlocks($renderedContent);
|
||||||
|
} else if (note instanceof FNote && !options.noChildrenList) {
|
||||||
|
await renderChildrenList($renderedContent, note);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderIncludedNotes(contentEl: HTMLElement) {
|
||||||
|
// TODO: Consider duplicating with server's share/content_renderer.ts.
|
||||||
|
const includeNoteEls = contentEl.querySelectorAll("section.include-note");
|
||||||
|
|
||||||
|
// Gather the list of items to load.
|
||||||
|
const noteIds: string[] = [];
|
||||||
|
for (const includeNoteEl of includeNoteEls) {
|
||||||
|
const noteId = includeNoteEl.getAttribute("data-note-id");
|
||||||
|
if (noteId) {
|
||||||
|
noteIds.push(noteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the required notes.
|
||||||
|
await froca.getNotes(noteIds);
|
||||||
|
|
||||||
|
// Render and integrate the notes.
|
||||||
|
for (const includeNoteEl of includeNoteEls) {
|
||||||
|
const noteId = includeNoteEl.getAttribute("data-note-id");
|
||||||
|
if (!noteId) continue;
|
||||||
|
|
||||||
|
const note = froca.getNoteFromCache(noteId);
|
||||||
|
if (!note) {
|
||||||
|
console.warn(`Unable to include ${noteId} because it could not be found.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedContent = (await content_renderer.getRenderedContent(note)).$renderedContent;
|
||||||
|
includeNoteEl.replaceChildren(...renderedContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rewrite the code block from <pre><code> to <div> in order not to apply a codeblock style to it. */
|
||||||
|
export async function rewriteMermaidDiagramsInContainer(container: HTMLDivElement) {
|
||||||
|
const mermaidBlocks = container.querySelectorAll('pre:has(code[class="language-mermaid"])');
|
||||||
|
if (!mermaidBlocks.length) return;
|
||||||
|
const nodes: HTMLElement[] = [];
|
||||||
|
|
||||||
|
for (const mermaidBlock of mermaidBlocks) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.classList.add("mermaid-diagram");
|
||||||
|
div.innerHTML = mermaidBlock.querySelector("code")?.innerHTML ?? "";
|
||||||
|
mermaidBlock.replaceWith(div);
|
||||||
|
nodes.push(div);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyInlineMermaid(container: HTMLDivElement) {
|
||||||
|
// Initialize mermaid
|
||||||
|
const mermaid = (await import("mermaid")).default;
|
||||||
|
mermaid.initialize(getMermaidConfig());
|
||||||
|
const nodes = Array.from(container.querySelectorAll<HTMLElement>("div.mermaid-diagram"));
|
||||||
|
try {
|
||||||
|
await mermaid.run({ nodes });
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote) {
|
||||||
|
let childNoteIds = note.getChildNoteIds();
|
||||||
|
|
||||||
|
if (!childNoteIds.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$renderedContent.css("padding", "10px");
|
||||||
|
$renderedContent.addClass("text-with-ellipsis");
|
||||||
|
|
||||||
|
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) {
|
||||||
|
$renderedContent.append(
|
||||||
|
await link.createLink(`${note.noteId}/${childNote.noteId}`, {
|
||||||
|
showTooltip: false,
|
||||||
|
showNoteIcon: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
$renderedContent.append("<br>");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
import {readCssVar} from "../utils/css-var";
|
import {readCssVar} from "../utils/css-var";
|
||||||
import Color, { ColorInstance } from "color";
|
import Color, { ColorInstance } from "color";
|
||||||
|
|
||||||
const registeredClasses = new Set<string>();
|
const registeredClasses = new Set<string>();
|
||||||
|
const colorsWithHue = new Set<string>();
|
||||||
|
|
||||||
// Read the color lightness limits defined in the theme as CSS variables
|
// Read the color lightness limits defined in the theme as CSS variables
|
||||||
|
|
||||||
@@ -26,19 +28,24 @@ function createClassForColor(colorString: string | null) {
|
|||||||
if (!registeredClasses.has(className)) {
|
if (!registeredClasses.has(className)) {
|
||||||
const adjustedColor = adjustColorLightness(color, lightThemeColorMaxLightness!,
|
const adjustedColor = adjustColorLightness(color, lightThemeColorMaxLightness!,
|
||||||
darkThemeColorMinLightness!);
|
darkThemeColorMinLightness!);
|
||||||
|
const hue = getHue(color);
|
||||||
|
|
||||||
$("head").append(`<style>
|
$("head").append(`<style>
|
||||||
.${className}, span.fancytree-active.${className} {
|
.${className}, span.fancytree-active.${className} {
|
||||||
|
--original-custom-color: ${color.hex()};
|
||||||
--light-theme-custom-color: ${adjustedColor.lightThemeColor};
|
--light-theme-custom-color: ${adjustedColor.lightThemeColor};
|
||||||
--dark-theme-custom-color: ${adjustedColor.darkThemeColor};
|
--dark-theme-custom-color: ${adjustedColor.darkThemeColor};
|
||||||
--custom-color-hue: ${getHue(color) ?? 'unset'};
|
--custom-color-hue: ${hue ?? 'unset'};
|
||||||
}
|
}
|
||||||
</style>`);
|
</style>`);
|
||||||
|
|
||||||
registeredClasses.add(className);
|
registeredClasses.add(className);
|
||||||
|
if (hue !== undefined) {
|
||||||
|
colorsWithHue.add(className);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return className;
|
return clsx("use-note-color", className, colorsWithHue.has(className) && "with-hue");
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseColor(color: string) {
|
function parseColor(color: string) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import dayjs from "dayjs";
|
import { dayjs } from "@triliumnext/commons";
|
||||||
import type { FNoteRow } from "../entities/fnote.js";
|
import type { FNoteRow } from "../entities/fnote.js";
|
||||||
import froca from "./froca.js";
|
import froca from "./froca.js";
|
||||||
import server from "./server.js";
|
import server from "./server.js";
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* @param whether to execute at the beginning (`false`)
|
* @param whether to execute at the beginning (`false`)
|
||||||
* @api public
|
* @api public
|
||||||
*/
|
*/
|
||||||
function debounce<T>(func: (...args: unknown[]) => T, waitMs: number, immediate: boolean = false) {
|
function debounce<T>(func: (...args: any[]) => T, waitMs: number, immediate: boolean = false) {
|
||||||
let timeout: any; // TODO: fix once we split client and server.
|
let timeout: any; // TODO: fix once we split client and server.
|
||||||
let args: unknown[] | null;
|
let args: unknown[] | null;
|
||||||
let context: unknown;
|
let context: unknown;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Modal } from "bootstrap";
|
import { Modal } from "bootstrap";
|
||||||
import appContext from "../components/app_context.js";
|
import appContext from "../components/app_context.js";
|
||||||
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
|
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions, MessageType } from "../widgets/dialogs/confirm.js";
|
||||||
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||||
import { focusSavedElement, saveFocusedElement } from "./focus.js";
|
import { focusSavedElement, saveFocusedElement } from "./focus.js";
|
||||||
|
import { InfoExtraProps } from "../widgets/dialogs/info.jsx";
|
||||||
|
|
||||||
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true, config?: Partial<Modal.Options>) {
|
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true, config?: Partial<Modal.Options>) {
|
||||||
if (closeActDialog) {
|
if (closeActDialog) {
|
||||||
@@ -37,8 +38,8 @@ export function closeActiveDialog() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function info(message: string) {
|
async function info(message: MessageType, extraProps?: InfoExtraProps) {
|
||||||
return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res }));
|
return new Promise((res) => appContext.triggerCommand("showInfoDialog", { ...extraProps, message, callback: res }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
51
apps/client/src/services/experimental_features.ts
Normal file
51
apps/client/src/services/experimental_features.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { t } from "./i18n";
|
||||||
|
import options from "./options";
|
||||||
|
|
||||||
|
export interface ExperimentalFeature {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const experimentalFeatures = [
|
||||||
|
{
|
||||||
|
id: "new-layout",
|
||||||
|
name: t("experimental_features.new_layout_name"),
|
||||||
|
description: t("experimental_features.new_layout_description"),
|
||||||
|
}
|
||||||
|
] as const satisfies ExperimentalFeature[];
|
||||||
|
|
||||||
|
export type ExperimentalFeatureId = typeof experimentalFeatures[number]["id"];
|
||||||
|
|
||||||
|
let enabledFeatures: Set<ExperimentalFeatureId> | null = null;
|
||||||
|
|
||||||
|
export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId): boolean {
|
||||||
|
return getEnabledFeatures().has(featureId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEnabledExperimentalFeatureIds() {
|
||||||
|
return getEnabledFeatures().values();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function toggleExperimentalFeature(featureId: ExperimentalFeatureId, enable: boolean) {
|
||||||
|
const features = new Set(getEnabledFeatures());
|
||||||
|
if (enable) {
|
||||||
|
features.add(featureId);
|
||||||
|
} else {
|
||||||
|
features.delete(featureId);
|
||||||
|
}
|
||||||
|
await options.save("experimentalFeatures", JSON.stringify(Array.from(features)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnabledFeatures() {
|
||||||
|
if (!enabledFeatures) {
|
||||||
|
let features: ExperimentalFeatureId[] = [];
|
||||||
|
try {
|
||||||
|
features = JSON.parse(options.get("experimentalFeatures")) as ExperimentalFeatureId[];
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to parse experimental features from options:", e);
|
||||||
|
}
|
||||||
|
enabledFeatures = new Set(features);
|
||||||
|
}
|
||||||
|
return enabledFeatures;
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ export interface Froca {
|
|||||||
|
|
||||||
getBlob(entityType: string, entityId: string): Promise<FBlob | null>;
|
getBlob(entityType: string, entityId: string): Promise<FBlob | null>;
|
||||||
getNote(noteId: string, silentNotFoundError?: boolean): Promise<FNote | null>;
|
getNote(noteId: string, silentNotFoundError?: boolean): Promise<FNote | null>;
|
||||||
getNoteFromCache(noteId: string): FNote;
|
getNoteFromCache(noteId: string): FNote | undefined;
|
||||||
getNotesFromCache(noteIds: string[], silentNotFoundError?: boolean): FNote[];
|
getNotesFromCache(noteIds: string[], silentNotFoundError?: boolean): FNote[];
|
||||||
getNotes(noteIds: string[], silentNotFoundError?: boolean): Promise<FNote[]>;
|
getNotes(noteIds: string[], silentNotFoundError?: boolean): Promise<FNote[]>;
|
||||||
|
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ class FrocaImpl implements Froca {
|
|||||||
return (await this.getNotes([noteId], silentNotFoundError))[0];
|
return (await this.getNotes([noteId], silentNotFoundError))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
getNoteFromCache(noteId: string) {
|
getNoteFromCache(noteId: string): FNote | undefined {
|
||||||
if (!noteId) {
|
if (!noteId) {
|
||||||
throw new Error("Empty noteId");
|
throw new Error("Empty noteId");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import shortcutService from "./shortcuts.js";
|
|||||||
import dialogService from "./dialog.js";
|
import dialogService from "./dialog.js";
|
||||||
import type FNote from "../entities/fnote.js";
|
import type FNote from "../entities/fnote.js";
|
||||||
import { t } from "./i18n.js";
|
import { t } from "./i18n.js";
|
||||||
import dayjs from "dayjs";
|
import { dayjs } from "@triliumnext/commons";
|
||||||
import type NoteContext from "../components/note_context.js";
|
import type NoteContext from "../components/note_context.js";
|
||||||
import type Component from "../components/component.js";
|
import type Component from "../components/component.js";
|
||||||
import { formatLogMessage } from "@triliumnext/commons";
|
import { formatLogMessage } from "@triliumnext/commons";
|
||||||
@@ -77,6 +77,10 @@ export interface Api {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity whose event triggered this execution.
|
* Entity whose event triggered this execution.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* For front-end scripts, generally there's no origin entity specified since the scripts are run by the user or automatically by the UI (widgets).
|
||||||
|
* If there is an origin entity specified, then it's going to be a note entity.
|
||||||
*/
|
*/
|
||||||
originEntity: unknown | null;
|
originEntity: unknown | null;
|
||||||
|
|
||||||
@@ -278,12 +282,16 @@ export interface Api {
|
|||||||
getActiveContextNote(): FNote;
|
getActiveContextNote(): FNote;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns returns active context (split)
|
* Obtains the currently active/focused split in the current tab.
|
||||||
|
*
|
||||||
|
* Note that this method does not return the note context of the "Quick edit" panel, it will return the note context behind it.
|
||||||
*/
|
*/
|
||||||
getActiveContext(): NoteContext;
|
getActiveContext(): NoteContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns returns active main context (first split in a tab, represents the tab as a whole)
|
* Obtains the main context of the current tab. This is the left-most split.
|
||||||
|
*
|
||||||
|
* Note that this method does not return the note context of the "Quick edit" panel, it will return the note context behind it.
|
||||||
*/
|
*/
|
||||||
getActiveMainContext(): NoteContext;
|
getActiveMainContext(): NoteContext;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import options from "./options.js";
|
|||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import i18nextHttpBackend from "i18next-http-backend";
|
import i18nextHttpBackend from "i18next-http-backend";
|
||||||
import server from "./server.js";
|
import server from "./server.js";
|
||||||
import type { Locale } from "@triliumnext/commons";
|
import { LOCALE_IDS, setDayjsLocale, type Locale } from "@triliumnext/commons";
|
||||||
import { initReactI18next } from "react-i18next";
|
import { initReactI18next } from "react-i18next";
|
||||||
|
|
||||||
let locales: Locale[] | null;
|
let locales: Locale[] | null;
|
||||||
@@ -13,7 +13,7 @@ let locales: Locale[] | null;
|
|||||||
export let translationsInitializedPromise = $.Deferred();
|
export let translationsInitializedPromise = $.Deferred();
|
||||||
|
|
||||||
export async function initLocale() {
|
export async function initLocale() {
|
||||||
const locale = (options.get("locale") as string) || "en";
|
const locale = ((options.get("locale") as string) || "en") as LOCALE_IDS;
|
||||||
|
|
||||||
locales = await server.get<Locale[]>("options/locales");
|
locales = await server.get<Locale[]>("options/locales");
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ export async function initLocale() {
|
|||||||
returnEmptyString: false
|
returnEmptyString: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await setDayjsLocale(locale);
|
||||||
translationsInitializedPromise.resolve();
|
translationsInitializedPromise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import toastService, { type ToastOptions } from "./toast.js";
|
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
|
||||||
import server from "./server.js";
|
import server from "./server.js";
|
||||||
import ws from "./ws.js";
|
import ws from "./ws.js";
|
||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
@@ -57,11 +57,11 @@ export async function uploadFiles(entityType: string, parentNoteId: string, file
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeToast(id: string, message: string): ToastOptions {
|
function makeToast(id: string, message: string): ToastOptionsWithRequiredId {
|
||||||
return {
|
return {
|
||||||
id: id,
|
id,
|
||||||
title: t("import.import-status"),
|
title: t("import.import-status"),
|
||||||
message: message,
|
message,
|
||||||
icon: "plus"
|
icon: "plus"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ ws.subscribeToMessages(async (message) => {
|
|||||||
toastService.showPersistent(makeToast(message.taskId, t("import.in-progress", { progress: message.progressCount })));
|
toastService.showPersistent(makeToast(message.taskId, t("import.in-progress", { progress: message.progressCount })));
|
||||||
} else if (message.type === "taskSucceeded") {
|
} else if (message.type === "taskSucceeded") {
|
||||||
const toast = makeToast(message.taskId, t("import.successful"));
|
const toast = makeToast(message.taskId, t("import.successful"));
|
||||||
toast.closeAfter = 5000;
|
toast.timeout = 5000;
|
||||||
|
|
||||||
toastService.showPersistent(toast);
|
toastService.showPersistent(toast);
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ ws.subscribeToMessages(async (message: WebSocketMessage) => {
|
|||||||
toastService.showPersistent(makeToast(message.taskId, t("import.in-progress", { progress: message.progressCount })));
|
toastService.showPersistent(makeToast(message.taskId, t("import.in-progress", { progress: message.progressCount })));
|
||||||
} else if (message.type === "taskSucceeded") {
|
} else if (message.type === "taskSucceeded") {
|
||||||
const toast = makeToast(message.taskId, t("import.successful"));
|
const toast = makeToast(message.taskId, t("import.successful"));
|
||||||
toast.closeAfter = 5000;
|
toast.timeout = 5000;
|
||||||
|
|
||||||
toastService.showPersistent(toast);
|
toastService.showPersistent(toast);
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ async function getActionsForScope(scope: string) {
|
|||||||
return actions.filter((action) => action.scope === scope);
|
return actions.filter((action) => action.scope === scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, component: Component) {
|
async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, component: Component, ntxId: string | null | undefined) {
|
||||||
if (!$el[0]) return [];
|
if (!$el[0]) return [];
|
||||||
|
|
||||||
const actions = await getActionsForScope(scope);
|
const actions = await getActionsForScope(scope);
|
||||||
@@ -36,7 +36,9 @@ async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, c
|
|||||||
|
|
||||||
for (const action of actions) {
|
for (const action of actions) {
|
||||||
for (const shortcut of action.effectiveShortcuts ?? []) {
|
for (const shortcut of action.effectiveShortcuts ?? []) {
|
||||||
const binding = shortcutService.bindElShortcut($el, shortcut, () => component.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId }));
|
const binding = shortcutService.bindElShortcut($el, shortcut, () => {
|
||||||
|
component.triggerCommand(action.actionName, { ntxId });
|
||||||
|
});
|
||||||
if (binding) {
|
if (binding) {
|
||||||
bindings.push(binding);
|
bindings.push(binding);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ async function getLinkIcon(noteId: string, viewMode: ViewMode | undefined) {
|
|||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ViewMode = "default" | "source" | "attachments" | "contextual-help";
|
export type ViewMode = "default" | "source" | "attachments" | "contextual-help" | "note-map";
|
||||||
|
|
||||||
export interface ViewScope {
|
export interface ViewScope {
|
||||||
/**
|
/**
|
||||||
@@ -99,7 +99,7 @@ async function createLink(notePath: string | undefined, options: CreateLinkOptio
|
|||||||
const viewMode = viewScope.viewMode || "default";
|
const viewMode = viewScope.viewMode || "default";
|
||||||
let linkTitle = options.title;
|
let linkTitle = options.title;
|
||||||
|
|
||||||
if (!linkTitle) {
|
if (linkTitle === undefined) {
|
||||||
if (viewMode === "attachments" && viewScope.attachmentId) {
|
if (viewMode === "attachments" && viewScope.attachmentId) {
|
||||||
const attachment = await froca.getAttachment(viewScope.attachmentId);
|
const attachment = await froca.getAttachment(viewScope.attachmentId);
|
||||||
|
|
||||||
@@ -467,28 +467,30 @@ function getReferenceLinkTitleSync(href: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Check why the event is not supported.
|
if (glob.device !== "print") {
|
||||||
//@ts-ignore
|
// TODO: Check why the event is not supported.
|
||||||
$(document).on("click", "a", goToLink);
|
//@ts-ignore
|
||||||
// TODO: Check why the event is not supported.
|
$(document).on("click", "a", goToLink);
|
||||||
//@ts-ignore
|
// TODO: Check why the event is not supported.
|
||||||
$(document).on("auxclick", "a", goToLink); // to handle the middle button
|
//@ts-ignore
|
||||||
// TODO: Check why the event is not supported.
|
$(document).on("auxclick", "a", goToLink); // to handle the middle button
|
||||||
//@ts-ignore
|
// TODO: Check why the event is not supported.
|
||||||
$(document).on("contextmenu", "a", linkContextMenu);
|
//@ts-ignore
|
||||||
// TODO: Check why the event is not supported.
|
$(document).on("contextmenu", "a", linkContextMenu);
|
||||||
//@ts-ignore
|
// TODO: Check why the event is not supported.
|
||||||
$(document).on("dblclick", "a", goToLink);
|
//@ts-ignore
|
||||||
|
$(document).on("dblclick", "a", goToLink);
|
||||||
|
|
||||||
$(document).on("mousedown", "a", (e) => {
|
$(document).on("mousedown", "a", (e) => {
|
||||||
if (e.which === 2) {
|
if (e.which === 2) {
|
||||||
// prevent paste on middle click
|
// prevent paste on middle click
|
||||||
// https://github.com/zadam/trilium/issues/2995
|
// https://github.com/zadam/trilium/issues/2995
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#preventing_default_actions
|
// https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#preventing_default_actions
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getNotePathFromUrl,
|
getNotePathFromUrl,
|
||||||
|
|||||||
@@ -41,6 +41,17 @@ function parse(value: string) {
|
|||||||
return defObj;
|
return defObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For an attribute definition name (e.g. `label:TEST:TEST1`), extracts its type (label) and name (TEST:TEST1).
|
||||||
|
* @param definitionAttrName the attribute definition name, without the leading `#` (e.g. `label:TEST:TEST1`)
|
||||||
|
* @return a tuple of [type, name].
|
||||||
|
*/
|
||||||
|
export function extractAttributeDefinitionTypeAndName(definitionAttrName: string): [ "label" | "relation", string ] {
|
||||||
|
const valueType = definitionAttrName.startsWith("label:") ? "label" : "relation";
|
||||||
|
const valueName = definitionAttrName.substring(valueType.length + 1);
|
||||||
|
return [ valueType, valueName ];
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse
|
parse
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import server from "./server.js";
|
import server from "./server.js";
|
||||||
import protectedSessionHolder from "./protected_session_holder.js";
|
import protectedSessionHolder from "./protected_session_holder.js";
|
||||||
import toastService from "./toast.js";
|
import toastService from "./toast.js";
|
||||||
import type { ToastOptions } from "./toast.js";
|
import type { ToastOptionsWithRequiredId } from "./toast.js";
|
||||||
import ws from "./ws.js";
|
import ws from "./ws.js";
|
||||||
import appContext from "../components/app_context.js";
|
import appContext from "../components/app_context.js";
|
||||||
import froca from "./froca.js";
|
import froca from "./froca.js";
|
||||||
@@ -97,7 +97,7 @@ async function protectNote(noteId: string, protect: boolean, includingSubtree: b
|
|||||||
await server.put(`notes/${noteId}/protect/${protect ? 1 : 0}?subtree=${includingSubtree ? 1 : 0}`);
|
await server.put(`notes/${noteId}/protect/${protect ? 1 : 0}?subtree=${includingSubtree ? 1 : 0}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeToast(message: Message, title: string, text: string): ToastOptions {
|
function makeToast(message: Message, title: string, text: string): ToastOptionsWithRequiredId {
|
||||||
return {
|
return {
|
||||||
id: message.taskId,
|
id: message.taskId,
|
||||||
title,
|
title,
|
||||||
@@ -124,7 +124,7 @@ ws.subscribeToMessages(async (message) => {
|
|||||||
} else if (message.type === "taskSucceeded") {
|
} else if (message.type === "taskSucceeded") {
|
||||||
const text = isProtecting ? t("protected_session.protecting-finished-successfully") : t("protected_session.unprotecting-finished-successfully");
|
const text = isProtecting ? t("protected_session.protecting-finished-successfully") : t("protected_session.unprotecting-finished-successfully");
|
||||||
const toast = makeToast(message, title, text);
|
const toast = makeToast(message, title, text);
|
||||||
toast.closeAfter = 3000;
|
toast.timeout = 3000;
|
||||||
|
|
||||||
toastService.showPersistent(toast);
|
toastService.showPersistent(toast);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ async function reportError(method: string, url: string, statusCode: number, resp
|
|||||||
|
|
||||||
const toastService = (await import("./toast.js")).default;
|
const toastService = (await import("./toast.js")).default;
|
||||||
|
|
||||||
const messageStr = typeof message === "string" ? message : JSON.stringify(message);
|
const messageStr = (typeof message === "string" ? message : JSON.stringify(message)) || "-";
|
||||||
|
|
||||||
if ([400, 404].includes(statusCode) && response && typeof response === "object") {
|
if ([400, 404].includes(statusCode) && response && typeof response === "object") {
|
||||||
toastService.showError(messageStr);
|
toastService.showError(messageStr);
|
||||||
@@ -274,10 +274,22 @@ async function reportError(method: string, url: string, statusCode: number, resp
|
|||||||
...response
|
...response
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const title = `${statusCode} ${method} ${url}`;
|
const { t } = await import("./i18n.js");
|
||||||
toastService.showErrorTitleAndMessage(title, messageStr);
|
if (statusCode === 400 && (url.includes("%23") || url.includes("%2F"))) {
|
||||||
|
toastService.showPersistent({
|
||||||
|
id: "trafik-blocked",
|
||||||
|
icon: "bx bx-unlink",
|
||||||
|
title: t("server.unknown_http_error_title"),
|
||||||
|
message: t("server.traefik_blocks_requests")
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toastService.showErrorTitleAndMessage(
|
||||||
|
t("server.unknown_http_error_title"),
|
||||||
|
t("server.unknown_http_error_content", { statusCode, method, url, message: messageStr }),
|
||||||
|
15_000);
|
||||||
|
}
|
||||||
const { throwError } = await import("./ws.js");
|
const { throwError } = await import("./ws.js");
|
||||||
throwError(`${title} - ${message}`);
|
throwError(`${statusCode} ${method} ${url} - ${message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
|
|
||||||
type ElementType = HTMLElement | Document;
|
type ElementType = HTMLElement | Document;
|
||||||
type Handler = (e: KeyboardEvent) => void;
|
export type Handler = (e: KeyboardEvent) => void;
|
||||||
|
|
||||||
export interface ShortcutBinding {
|
export interface ShortcutBinding {
|
||||||
element: HTMLElement | Document;
|
element: HTMLElement | Document;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { signal } from "@preact/signals";
|
||||||
|
|
||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
|
|
||||||
export interface ToastOptions {
|
export interface ToastOptions {
|
||||||
@@ -5,112 +7,84 @@ export interface ToastOptions {
|
|||||||
icon: string;
|
icon: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
message: string;
|
message: string;
|
||||||
delay?: number;
|
timeout?: number;
|
||||||
autohide?: boolean;
|
progress?: number;
|
||||||
closeAfter?: number;
|
buttons?: {
|
||||||
|
text: string;
|
||||||
|
onClick: (api: { dismissToast: () => void }) => void;
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function toast(options: ToastOptions) {
|
export type ToastOptionsWithRequiredId = Omit<ToastOptions, "id"> & Required<Pick<ToastOptions, "id">>;
|
||||||
const $toast = $(options.title
|
|
||||||
? `\
|
|
||||||
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
|
||||||
<div class="toast-header">
|
|
||||||
<strong class="me-auto">
|
|
||||||
<span class="bx bx-${options.icon}"></span>
|
|
||||||
<span class="toast-title"></span>
|
|
||||||
</strong>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="toast-body"></div>
|
|
||||||
</div>`
|
|
||||||
: `
|
|
||||||
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
|
||||||
<div class="toast-icon">
|
|
||||||
<span class="bx bx-${options.icon}"></span>
|
|
||||||
</div>
|
|
||||||
<div class="toast-body"></div>
|
|
||||||
<div class="toast-header">
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
</div>`
|
|
||||||
);
|
|
||||||
|
|
||||||
$toast.toggleClass("no-title", !options.title);
|
function showPersistent(options: ToastOptionsWithRequiredId) {
|
||||||
$toast.find(".toast-title").text(options.title ?? "");
|
const existingToast = toasts.value.find(toast => toast.id === options.id);
|
||||||
$toast.find(".toast-body").html(options.message);
|
if (existingToast) {
|
||||||
|
updateToast(options.id, options);
|
||||||
if (options.id) {
|
|
||||||
$toast.attr("id", `toast-${options.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#toast-container").append($toast);
|
|
||||||
|
|
||||||
$toast.toast({
|
|
||||||
delay: options.delay || 3000,
|
|
||||||
autohide: !!options.autohide
|
|
||||||
});
|
|
||||||
|
|
||||||
$toast.on("hidden.bs.toast", (e) => e.target.remove());
|
|
||||||
|
|
||||||
$toast.toast("show");
|
|
||||||
|
|
||||||
return $toast;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showPersistent(options: ToastOptions) {
|
|
||||||
let $toast = $(`#toast-${options.id}`);
|
|
||||||
|
|
||||||
if ($toast.length > 0) {
|
|
||||||
$toast.find(".toast-body").html(options.message);
|
|
||||||
} else {
|
} else {
|
||||||
options.autohide = false;
|
addToast(options);
|
||||||
|
|
||||||
$toast = toast(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.closeAfter) {
|
|
||||||
setTimeout(() => $toast.remove(), options.closeAfter);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closePersistent(id: string) {
|
function closePersistent(id: string) {
|
||||||
$(`#toast-${id}`).remove();
|
removeToastFromStore(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMessage(message: string, delay = 2000, icon = "check") {
|
function showMessage(message: string, timeout = 2000, icon = "bx bx-check") {
|
||||||
console.debug(utils.now(), "message:", message);
|
console.debug(utils.now(), "message:", message);
|
||||||
|
|
||||||
toast({
|
addToast({
|
||||||
icon,
|
icon,
|
||||||
message: message,
|
message,
|
||||||
autohide: true,
|
timeout
|
||||||
delay
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showError(message: string, delay = 10000) {
|
export function showError(message: string, timeout = 10000) {
|
||||||
console.log(utils.now(), "error: ", message);
|
console.log(utils.now(), "error: ", message);
|
||||||
|
|
||||||
toast({
|
addToast({
|
||||||
icon: "alert",
|
icon: "bx bx-error-circle",
|
||||||
message: message,
|
message,
|
||||||
autohide: true,
|
timeout
|
||||||
delay
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showErrorTitleAndMessage(title: string, message: string, delay = 10000) {
|
function showErrorTitleAndMessage(title: string, message: string, timeout = 10000) {
|
||||||
console.log(utils.now(), "error: ", message);
|
console.log(utils.now(), "error: ", message);
|
||||||
|
|
||||||
toast({
|
addToast({
|
||||||
title: title,
|
title,
|
||||||
icon: "alert",
|
icon: "bx bx-error-circle",
|
||||||
message: message,
|
message,
|
||||||
autohide: true,
|
timeout
|
||||||
delay
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#region Toast store
|
||||||
|
export const toasts = signal<ToastOptionsWithRequiredId[]>([]);
|
||||||
|
|
||||||
|
function addToast(opts: ToastOptions) {
|
||||||
|
const id = opts.id ?? crypto.randomUUID();
|
||||||
|
const toast = { ...opts, id };
|
||||||
|
toasts.value = [ ...toasts.value, toast ];
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateToast(id: string, partial: Partial<ToastOptions>) {
|
||||||
|
toasts.value = toasts.value.map(toast => {
|
||||||
|
if (toast.id === id) {
|
||||||
|
return { ...toast, ...partial }
|
||||||
|
}
|
||||||
|
return toast;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeToastFromStore(id: string) {
|
||||||
|
toasts.value = toasts.value.filter(toast => toast.id !== id);
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
showMessage,
|
showMessage,
|
||||||
showError,
|
showError,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import froca from "./froca.js";
|
|||||||
import hoistedNoteService from "./hoisted_note.js";
|
import hoistedNoteService from "./hoisted_note.js";
|
||||||
import appContext from "../components/app_context.js";
|
import appContext from "../components/app_context.js";
|
||||||
|
|
||||||
|
export const NOTE_PATH_TITLE_SEPARATOR = " › ";
|
||||||
|
|
||||||
async function resolveNotePath(notePath: string, hoistedNoteId = "root") {
|
async function resolveNotePath(notePath: string, hoistedNoteId = "root") {
|
||||||
const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId);
|
const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId);
|
||||||
|
|
||||||
@@ -89,7 +91,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
|
|||||||
|
|
||||||
effectivePathSegments.reverse();
|
effectivePathSegments.reverse();
|
||||||
|
|
||||||
if (effectivePathSegments.includes(hoistedNoteId)) {
|
if (effectivePathSegments.includes(hoistedNoteId) && effectivePathSegments.includes('root')) {
|
||||||
return effectivePathSegments;
|
return effectivePathSegments;
|
||||||
} else {
|
} else {
|
||||||
const noteId = getNoteIdFromUrl(notePath);
|
const noteId = getNoteIdFromUrl(notePath);
|
||||||
@@ -254,7 +256,7 @@ async function getNotePathTitle(notePath: string) {
|
|||||||
|
|
||||||
const titlePath = await getNotePathTitleComponents(notePath);
|
const titlePath = await getNotePathTitleComponents(notePath);
|
||||||
|
|
||||||
return titlePath.join(" / ");
|
return titlePath.join(NOTE_PATH_TITLE_SEPARATOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getNoteTitleWithPathAsSuffix(notePath: string) {
|
async function getNoteTitleWithPathAsSuffix(notePath: string) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import dayjs from "dayjs";
|
import { dayjs } from "@triliumnext/commons";
|
||||||
import type { ViewScope } from "./link.js";
|
import type { ViewMode, ViewScope } from "./link.js";
|
||||||
import FNote from "../entities/fnote";
|
import FNote from "../entities/fnote";
|
||||||
|
import { snapdom } from "@zumer/snapdom";
|
||||||
|
|
||||||
const SVG_MIME = "image/svg+xml";
|
const SVG_MIME = "image/svg+xml";
|
||||||
|
|
||||||
@@ -150,7 +151,7 @@ export function isMac() {
|
|||||||
|
|
||||||
export const hasTouchBar = (isMac() && isElectron());
|
export const hasTouchBar = (isMac() && isElectron());
|
||||||
|
|
||||||
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement> | JQueryEventObject) {
|
export function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement> | JQueryEventObject) {
|
||||||
return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey);
|
return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +208,7 @@ function toObject<T, R>(array: T[], fn: (arg0: T) => [key: string, value: R]) {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
function randomString(len: number) {
|
export function randomString(len: number) {
|
||||||
let text = "";
|
let text = "";
|
||||||
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
|
||||||
@@ -236,7 +237,7 @@ export function isIOS() {
|
|||||||
return /iPad|iPhone|iPod/.test(navigator.userAgent);
|
return /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDesktop() {
|
export function isDesktop() {
|
||||||
return (
|
return (
|
||||||
window.glob?.device === "desktop" ||
|
window.glob?.device === "desktop" ||
|
||||||
// window.glob.device is not available in setup
|
// window.glob.device is not available in setup
|
||||||
@@ -274,7 +275,7 @@ function getMimeTypeClass(mime: string) {
|
|||||||
return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`;
|
return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isHtmlEmpty(html: string) {
|
export function isHtmlEmpty(html: string) {
|
||||||
if (!html) {
|
if (!html) {
|
||||||
return true;
|
return true;
|
||||||
} else if (typeof html !== "string") {
|
} else if (typeof html !== "string") {
|
||||||
@@ -438,7 +439,20 @@ async function openInAppHelp($button: JQuery<HTMLElement>) {
|
|||||||
* @param inAppHelpPage the ID of the help note (excluding the `_help_` prefix).
|
* @param inAppHelpPage the ID of the help note (excluding the `_help_` prefix).
|
||||||
* @returns a promise that resolves once the help has been opened.
|
* @returns a promise that resolves once the help has been opened.
|
||||||
*/
|
*/
|
||||||
export async function openInAppHelpFromUrl(inAppHelpPage: string) {
|
export function openInAppHelpFromUrl(inAppHelpPage: string) {
|
||||||
|
return openInReusableSplit(`_help_${inAppHelpPage}`, "contextual-help");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to opening a new note in a split, but re-uses an existing split if there is already one open with the same view mode.
|
||||||
|
*
|
||||||
|
* @param targetNoteId the note ID to open in the split.
|
||||||
|
* @param targetViewMode the view mode of the split to open the note in.
|
||||||
|
* @param openOpts additional options for opening the note.
|
||||||
|
*/
|
||||||
|
export async function openInReusableSplit(targetNoteId: string, targetViewMode: ViewMode, openOpts: {
|
||||||
|
hoistedNoteId?: string;
|
||||||
|
} = {}) {
|
||||||
// Dynamic import to avoid import issues in tests.
|
// Dynamic import to avoid import issues in tests.
|
||||||
const appContext = (await import("../components/app_context.js")).default;
|
const appContext = (await import("../components/app_context.js")).default;
|
||||||
const activeContext = appContext.tabManager.getActiveContext();
|
const activeContext = appContext.tabManager.getActiveContext();
|
||||||
@@ -446,23 +460,20 @@ export async function openInAppHelpFromUrl(inAppHelpPage: string) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const subContexts = activeContext.getSubContexts();
|
const subContexts = activeContext.getSubContexts();
|
||||||
const targetNote = `_help_${inAppHelpPage}`;
|
const existingSubcontext = subContexts.find((s) => s.viewScope?.viewMode === targetViewMode);
|
||||||
const helpSubcontext = subContexts.find((s) => s.viewScope?.viewMode === "contextual-help");
|
const viewScope: ViewScope = { viewMode: targetViewMode };
|
||||||
const viewScope: ViewScope = {
|
if (!existingSubcontext) {
|
||||||
viewMode: "contextual-help",
|
// The target split is not already open, open a new split with it.
|
||||||
};
|
|
||||||
if (!helpSubcontext) {
|
|
||||||
// The help is not already open, open a new split with it.
|
|
||||||
const { ntxId } = subContexts[subContexts.length - 1];
|
const { ntxId } = subContexts[subContexts.length - 1];
|
||||||
appContext.triggerCommand("openNewNoteSplit", {
|
appContext.triggerCommand("openNewNoteSplit", {
|
||||||
ntxId,
|
ntxId,
|
||||||
notePath: targetNote,
|
notePath: targetNoteId,
|
||||||
hoistedNoteId: "_help",
|
hoistedNoteId: openOpts.hoistedNoteId,
|
||||||
viewScope
|
viewScope
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
// There is already a help window open, make sure it opens on the right note.
|
// There is already a target split open, make sure it opens on the right note.
|
||||||
helpSubcontext.setNote(targetNote, { viewScope });
|
existingSubcontext.setNote(targetNoteId, { viewScope });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,16 +639,69 @@ export function createImageSrcUrl(note: FNote) {
|
|||||||
return `api/images/${note.noteId}/${encodeURIComponent(note.title)}?timestamp=${Date.now()}`;
|
return `api/images/${note.noteId}/${encodeURIComponent(note.title)}?timestamp=${Date.now()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a string representation of an SVG, triggers a download of the file on the client device.
|
* Helper function to prepare an element for snapdom rendering.
|
||||||
|
* Handles string parsing and temporary DOM attachment for style computation.
|
||||||
|
*
|
||||||
|
* @param source - Either an SVG/HTML string to be parsed, or an existing SVG/HTML element.
|
||||||
|
* @returns An object containing the prepared element and a cleanup function.
|
||||||
|
* The cleanup function removes temporarily attached elements from the DOM,
|
||||||
|
* or is a no-op if the element was already in the DOM.
|
||||||
|
*/
|
||||||
|
function prepareElementForSnapdom(source: string | SVGElement | HTMLElement): {
|
||||||
|
element: SVGElement | HTMLElement;
|
||||||
|
cleanup: () => void;
|
||||||
|
} {
|
||||||
|
if (typeof source === 'string') {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
|
||||||
|
// Detect if content is SVG or HTML
|
||||||
|
const isSvg = source.trim().startsWith('<svg');
|
||||||
|
const mimeType = isSvg ? SVG_MIME : 'text/html';
|
||||||
|
|
||||||
|
const doc = parser.parseFromString(source, mimeType);
|
||||||
|
const element = doc.documentElement;
|
||||||
|
|
||||||
|
// Temporarily attach to DOM for proper style computation
|
||||||
|
element.style.position = 'absolute';
|
||||||
|
element.style.left = '-9999px';
|
||||||
|
element.style.top = '-9999px';
|
||||||
|
document.body.appendChild(element);
|
||||||
|
|
||||||
|
return {
|
||||||
|
element,
|
||||||
|
cleanup: () => document.body.removeChild(element)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
element: source,
|
||||||
|
cleanup: () => {} // No-op for existing elements
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads an SVG using snapdom for proper rendering. Can accept either an SVG string, an SVG element, or an HTML element.
|
||||||
*
|
*
|
||||||
* @param nameWithoutExtension the name of the file. The .svg suffix is automatically added to it.
|
* @param nameWithoutExtension the name of the file. The .svg suffix is automatically added to it.
|
||||||
* @param svgContent the content of the SVG file download.
|
* @param svgSource either an SVG string, an SVGElement, or an HTMLElement to be downloaded.
|
||||||
*/
|
*/
|
||||||
function downloadSvg(nameWithoutExtension: string, svgContent: string) {
|
async function downloadAsSvg(nameWithoutExtension: string, svgSource: string | SVGElement | HTMLElement) {
|
||||||
const filename = `${nameWithoutExtension}.svg`;
|
const { element, cleanup } = prepareElementForSnapdom(svgSource);
|
||||||
const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`;
|
|
||||||
triggerDownload(filename, dataUrl);
|
try {
|
||||||
|
const result = await snapdom(element, {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
scale: 2
|
||||||
|
});
|
||||||
|
triggerDownload(`${nameWithoutExtension}.svg`, result.url);
|
||||||
|
} finally {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -658,62 +722,26 @@ function triggerDownload(fileName: string, dataUrl: string) {
|
|||||||
|
|
||||||
document.body.removeChild(element);
|
document.body.removeChild(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a string representation of an SVG, renders the SVG to PNG and triggers a download of the file on the client device.
|
* Downloads an SVG as PNG using snapdom. Can accept either an SVG string, an SVG element, or an HTML element.
|
||||||
*
|
|
||||||
* Note that the SVG must specify its width and height as attributes in order for it to be rendered.
|
|
||||||
*
|
*
|
||||||
* @param nameWithoutExtension the name of the file. The .png suffix is automatically added to it.
|
* @param nameWithoutExtension the name of the file. The .png suffix is automatically added to it.
|
||||||
* @param svgContent the content of the SVG file download.
|
* @param svgSource either an SVG string, an SVGElement, or an HTMLElement to be converted to PNG.
|
||||||
* @returns a promise which resolves if the operation was successful, or rejects if it failed (permissions issue or some other issue).
|
|
||||||
*/
|
*/
|
||||||
function downloadSvgAsPng(nameWithoutExtension: string, svgContent: string) {
|
async function downloadAsPng(nameWithoutExtension: string, svgSource: string | SVGElement | HTMLElement) {
|
||||||
return new Promise<void>((resolve, reject) => {
|
const { element, cleanup } = prepareElementForSnapdom(svgSource);
|
||||||
// First, we need to determine the width and the height from the input SVG.
|
|
||||||
const result = getSizeFromSvg(svgContent);
|
|
||||||
if (!result) {
|
|
||||||
reject();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the image to a blob.
|
try {
|
||||||
const { width, height } = result;
|
const result = await snapdom(element, {
|
||||||
|
backgroundColor: "transparent",
|
||||||
// Create an image element and load the SVG.
|
scale: 2
|
||||||
const imageEl = new Image();
|
});
|
||||||
imageEl.width = width;
|
const pngImg = await result.toPng();
|
||||||
imageEl.height = height;
|
await triggerDownload(`${nameWithoutExtension}.png`, pngImg.src);
|
||||||
imageEl.crossOrigin = "anonymous";
|
} finally {
|
||||||
imageEl.onload = () => {
|
cleanup();
|
||||||
try {
|
}
|
||||||
// Draw the image with a canvas.
|
|
||||||
const canvasEl = document.createElement("canvas");
|
|
||||||
canvasEl.width = imageEl.width;
|
|
||||||
canvasEl.height = imageEl.height;
|
|
||||||
document.body.appendChild(canvasEl);
|
|
||||||
|
|
||||||
const ctx = canvasEl.getContext("2d");
|
|
||||||
if (!ctx) {
|
|
||||||
reject();
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx?.drawImage(imageEl, 0, 0);
|
|
||||||
|
|
||||||
const imgUri = canvasEl.toDataURL("image/png")
|
|
||||||
triggerDownload(`${nameWithoutExtension}.png`, imgUri);
|
|
||||||
document.body.removeChild(canvasEl);
|
|
||||||
resolve();
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e);
|
|
||||||
reject();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
imageEl.onerror = (e) => reject(e);
|
|
||||||
imageEl.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSizeFromSvg(svgContent: string) {
|
export function getSizeFromSvg(svgContent: string) {
|
||||||
const svgDocument = (new DOMParser()).parseFromString(svgContent, SVG_MIME);
|
const svgDocument = (new DOMParser()).parseFromString(svgContent, SVG_MIME);
|
||||||
|
|
||||||
@@ -925,8 +953,8 @@ export default {
|
|||||||
areObjectsEqual,
|
areObjectsEqual,
|
||||||
copyHtmlToClipboard,
|
copyHtmlToClipboard,
|
||||||
createImageSrcUrl,
|
createImageSrcUrl,
|
||||||
downloadSvg,
|
downloadAsSvg,
|
||||||
downloadSvgAsPng,
|
downloadAsPng,
|
||||||
compareVersions,
|
compareVersions,
|
||||||
isUpdateAvailable,
|
isUpdateAvailable,
|
||||||
isLaunchBarConfig
|
isLaunchBarConfig
|
||||||
|
|||||||
@@ -24,8 +24,12 @@
|
|||||||
--bs-body-font-family: var(--main-font-family) !important;
|
--bs-body-font-family: var(--main-font-family) !important;
|
||||||
--bs-body-font-weight: var(--main-font-weight) !important;
|
--bs-body-font-weight: var(--main-font-weight) !important;
|
||||||
--bs-body-color: var(--main-text-color) !important;
|
--bs-body-color: var(--main-text-color) !important;
|
||||||
--bs-body-bg: var(--main-background-color) !important;
|
--bs-body-bg: var(--main-background-color) !important;
|
||||||
--ck-mention-list-max-height: 500px;
|
--ck-mention-list-max-height: 500px;
|
||||||
|
--tn-modal-max-height: 90vh;
|
||||||
|
|
||||||
|
--tree-item-light-theme-max-color-lightness: 50;
|
||||||
|
--tree-item-dark-theme-min-color-lightness: 75;
|
||||||
}
|
}
|
||||||
|
|
||||||
body#trilium-app.motion-disabled *,
|
body#trilium-app.motion-disabled *,
|
||||||
@@ -212,6 +216,16 @@ input::placeholder,
|
|||||||
background-color: var(--modal-backdrop-color) !important;
|
background-color: var(--modal-backdrop-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.mobile .modal .modal-dialog {
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile .modal .modal-content {
|
||||||
|
border-radius: var(--bs-modal-border-radius) var(--bs-modal-border-radius) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
.component {
|
.component {
|
||||||
contain: size;
|
contain: size;
|
||||||
}
|
}
|
||||||
@@ -243,6 +257,11 @@ button.close:hover {
|
|||||||
color: var(--hover-item-text-color);
|
color: var(--hover-item-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.custom-title-bar-button {
|
||||||
|
background: transparent;
|
||||||
|
border: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background-color: var(--modal-background-color) !important;
|
background-color: var(--modal-background-color) !important;
|
||||||
}
|
}
|
||||||
@@ -404,16 +423,16 @@ body.desktop .tabulator-popup-container,
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu .disabled .disabled-tooltip {
|
.dropdown-menu .disabled .contextual-help {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
margin-inline-start: 8px;
|
margin-inline-start: 8px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--disabled-tooltip-icon-color);
|
color: var(--contextual-help-icon-color);
|
||||||
cursor: help;
|
cursor: help;
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu .disabled .disabled-tooltip:hover {
|
.dropdown-menu .disabled .contextual-help:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,7 +458,8 @@ body.desktop .tabulator-popup-container,
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.desktop .dropdown-menu:not(#context-menu-container) .dropdown-item,
|
body.desktop .dropdown-menu:not(#context-menu-container) .dropdown-item,
|
||||||
body #context-menu-container .dropdown-item > span {
|
body #context-menu-container .dropdown-item > span,
|
||||||
|
body.mobile .dropdown .dropdown-submenu > span {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -451,7 +471,7 @@ body #context-menu-container .dropdown-item > span {
|
|||||||
padding-inline-start: 12px;
|
padding-inline-start: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu kbd {
|
.dropdown-menu kbd {
|
||||||
color: var(--muted-text-color);
|
color: var(--muted-text-color);
|
||||||
border: none;
|
border: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -467,7 +487,7 @@ body #context-menu-container .dropdown-item > span {
|
|||||||
border: 1px solid transparent !important;
|
border: 1px solid transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* This is a workaround for Firefox not supporting break-before / break-after: avoid on columns.
|
/* This is a workaround for Firefox not supporting break-before / break-after: avoid on columns.
|
||||||
* It usually wraps a menu item followed by a separator / header and another menu item. */
|
* It usually wraps a menu item followed by a separator / header and another menu item. */
|
||||||
.dropdown-no-break {
|
.dropdown-no-break {
|
||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
@@ -494,12 +514,14 @@ body #context-menu-container .dropdown-item > span {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-menu .note-color-picker {
|
||||||
|
padding: 4px 12px 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.cm-editor {
|
.cm-editor {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 4px;
|
|
||||||
font-size: var(--monospace-font-size);
|
font-size: var(--monospace-font-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,11 +598,6 @@ button.btn-sm {
|
|||||||
color: var(--left-pane-text-color);
|
color: var(--left-pane-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.active:not(.btn-primary) {
|
|
||||||
background-color: var(--button-disabled-background-color) !important;
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck.ck-block-toolbar-button {
|
.ck.ck-block-toolbar-button {
|
||||||
transform: translateX(7px);
|
transform: translateX(7px);
|
||||||
color: var(--muted-text-color);
|
color: var(--muted-text-color);
|
||||||
@@ -610,6 +627,11 @@ pre:not(.hljs) {
|
|||||||
padding: var(--padding-size);
|
padding: var(--padding-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre:has(> .cm-editor) {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
pre > button.copy-button {
|
pre > button.copy-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: var(--copy-button-margin-size);
|
top: var(--copy-button-margin-size);
|
||||||
@@ -701,11 +723,6 @@ table.promoted-attributes-in-tooltip th {
|
|||||||
z-index: 32767 !important;
|
z-index: 32767 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-trigger {
|
|
||||||
background: transparent;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-tooltip-bottom .tooltip-arrow::before {
|
.bs-tooltip-bottom .tooltip-arrow::before {
|
||||||
border-bottom-color: var(--main-border-color) !important;
|
border-bottom-color: var(--main-border-color) !important;
|
||||||
}
|
}
|
||||||
@@ -1001,7 +1018,7 @@ div[data-notify="container"] {
|
|||||||
font-family: var(--monospace-font-family);
|
font-family: var(--monospace-font-family);
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.ck-icon .note-icon {
|
svg.ck-icon.note-icon {
|
||||||
color: var(--main-text-color);
|
color: var(--main-text-color);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
@@ -1112,10 +1129,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-detail-empty {
|
|
||||||
margin: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
padding: 0.5rem 1rem 0.5rem 1rem !important; /* make modal header padding slightly smaller */
|
padding: 0.5rem 1rem 0.5rem 1rem !important; /* make modal header padding slightly smaller */
|
||||||
}
|
}
|
||||||
@@ -1125,50 +1138,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
|||||||
margin: 0 12px;
|
margin: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#toast-container {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
top: 20px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast {
|
|
||||||
--bs-toast-bg: var(--accented-background-color);
|
|
||||||
--bs-toast-color: var(--main-text-color);
|
|
||||||
z-index: 9999999999 !important;
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-header {
|
|
||||||
background-color: var(--more-accented-background-color) !important;
|
|
||||||
color: var(--main-text-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-body {
|
|
||||||
white-space: preserve-breaks;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast.no-title {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast.no-title .toast-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: var(--bs-toast-padding-y) var(--bs-toast-padding-x);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast.no-title .toast-body {
|
|
||||||
padding-inline-start: 0;
|
|
||||||
padding-inline-end: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast.no-title .toast-header {
|
|
||||||
background-color: unset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck-mentions .ck-button {
|
.ck-mentions .ck-button {
|
||||||
font-size: var(--detail-font-size) !important;
|
font-size: var(--detail-font-size) !important;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
@@ -1300,11 +1269,11 @@ body.mobile #context-menu-container.mobile-bottom-menu {
|
|||||||
inset-inline-end: 0 !important;
|
inset-inline-end: 0 !important;
|
||||||
bottom: 0 !important;
|
bottom: 0 !important;
|
||||||
top: unset !important;
|
top: unset !important;
|
||||||
max-height: 70vh;
|
max-height: var(--tn-modal-max-height);
|
||||||
overflow: auto !important;
|
overflow: auto !important;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
padding-bottom: env(safe-area-inset-bottom) !important;
|
padding-bottom: max(env(safe-area-inset-bottom), var(--padding, var(--menu-padding-size))) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.mobile .dropdown-menu {
|
body.mobile .dropdown-menu {
|
||||||
@@ -1349,12 +1318,21 @@ body.desktop li.dropdown-submenu:hover > ul.dropdown-menu {
|
|||||||
top: 0;
|
top: 0;
|
||||||
inset-inline-start: calc(100% - 2px); /* -2px, otherwise there's a small gap between menu and submenu where the hover can disappear */
|
inset-inline-start: calc(100% - 2px); /* -2px, otherwise there's a small gap between menu and submenu where the hover can disappear */
|
||||||
margin-top: -10px;
|
margin-top: -10px;
|
||||||
min-width: 15rem;
|
|
||||||
/* to make submenu scrollable https://github.com/zadam/trilium/issues/3136 */
|
/* to make submenu scrollable https://github.com/zadam/trilium/issues/3136 */
|
||||||
max-height: 600px;
|
max-height: 600px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.desktop .dropdown-submenu > .dropdown-menu {
|
||||||
|
min-width: max-content;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-submenu.dropstart > .dropdown-menu {
|
||||||
|
inset-inline-start: auto;
|
||||||
|
inset-inline-end: calc(100% - 2px);
|
||||||
|
}
|
||||||
|
|
||||||
body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||||
inset-inline-start: calc(-100% + 10px);
|
inset-inline-start: calc(-100% + 10px);
|
||||||
}
|
}
|
||||||
@@ -1363,6 +1341,20 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.right-dropdown-widget .right-dropdown-button {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-trigger {
|
||||||
|
background: transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#launcher-pane.horizontal .right-dropdown-widget {
|
#launcher-pane.horizontal .right-dropdown-widget {
|
||||||
width: 53px;
|
width: 53px;
|
||||||
}
|
}
|
||||||
@@ -1387,6 +1379,10 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
|||||||
background-color: var(--scrollbar-background-color);
|
background-color: var(--scrollbar-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-corner {
|
::-webkit-scrollbar-corner {
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
}
|
}
|
||||||
@@ -1538,12 +1534,15 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
|||||||
@media (max-width: 991px) {
|
@media (max-width: 991px) {
|
||||||
body.mobile #launcher-pane .dropdown.global-menu > .dropdown-menu.show,
|
body.mobile #launcher-pane .dropdown.global-menu > .dropdown-menu.show,
|
||||||
body.mobile #launcher-container .dropdown > .dropdown-menu.show {
|
body.mobile #launcher-container .dropdown > .dropdown-menu.show {
|
||||||
|
--dropdown-bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size));
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size)) !important;
|
bottom: var(--dropdown-bottom) !important;
|
||||||
top: unset !important;
|
top: unset !important;
|
||||||
inset-inline-start: 0 !important;
|
inset-inline-start: 0 !important;
|
||||||
inset-inline-end: 0 !important;
|
inset-inline-end: 0 !important;
|
||||||
transform: unset !important;
|
transform: unset !important;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: calc(var(--tn-modal-max-height) - var(--dropdown-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
#mobile-sidebar-container {
|
#mobile-sidebar-container {
|
||||||
@@ -1578,6 +1577,14 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-split.empty-note {
|
||||||
|
--max-content-width: var(--preferred-max-content-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-empty {
|
||||||
|
margin: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
#mobile-sidebar-container.show #mobile-sidebar-wrapper {
|
#mobile-sidebar-container.show #mobile-sidebar-wrapper {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
@@ -1600,7 +1607,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
|||||||
inset-inline-start: 0;
|
inset-inline-start: 0;
|
||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
max-height: 85vh;
|
max-height: 85vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1640,46 +1647,6 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
|||||||
body.mobile .modal-dialog.modal-dialog-scrollable {
|
body.mobile .modal-dialog.modal-dialog-scrollable {
|
||||||
height: unset;
|
height: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.mobile .revisions-dialog .modal-dialog {
|
|
||||||
height: 95vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.mobile .revisions-dialog .modal-body {
|
|
||||||
height: 100% !important;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.mobile .revisions-dialog .revision-list {
|
|
||||||
height: unset;
|
|
||||||
max-height: 20vh;
|
|
||||||
border-bottom: 1px solid var(--main-border-color) !important;
|
|
||||||
padding: 0 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.mobile .revisions-dialog .modal-body > .revision-content-wrapper {
|
|
||||||
flex-grow: 1;
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.mobile .revisions-dialog .modal-body > .revision-content-wrapper > div:first-of-type {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.mobile .revisions-dialog .revision-title {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.mobile .revisions-dialog .revision-title-buttons {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.mobile .revisions-dialog .revision-content {
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile, tablet mode */
|
/* Mobile, tablet mode */
|
||||||
@@ -1985,7 +1952,7 @@ body.electron.platform-darwin:not(.native-titlebar) .tab-row-container {
|
|||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.electron.platform-darwin:not(.native-titlebar) #tab-row-left-spacer {
|
body.electron.platform-darwin:not(.native-titlebar):not(.full-screen) #tab-row-left-spacer {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2142,65 +2109,113 @@ body.zen .note-split.type-text .scrolling-container {
|
|||||||
|
|
||||||
body.zen:not(.backdrop-effects-disabled) .note-split.type-text .scrolling-container {
|
body.zen:not(.backdrop-effects-disabled) .note-split.type-text .scrolling-container {
|
||||||
--padding-top: 50px; /* Should be enough to cover the title row */
|
--padding-top: 50px; /* Should be enough to cover the title row */
|
||||||
|
|
||||||
padding-top: var(--padding-top);
|
padding-top: var(--padding-top);
|
||||||
scroll-padding-top: var(--padding-top);
|
scroll-padding-top: var(--padding-top);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fixed formatting toolbar */
|
/* Fixed formatting toolbar */
|
||||||
|
|
||||||
body.zen .note-split .ribbon-container {
|
body.zen:not(.experimental-feature-new-layout) {
|
||||||
position: fixed;
|
.note-split .ribbon-container {
|
||||||
left: 0;
|
position: fixed;
|
||||||
bottom: 20px;
|
left: 0;
|
||||||
width: 100%;
|
bottom: 20px;
|
||||||
z-index: 1000;
|
|
||||||
opacity: 0; /* Hidden unless the current note split is focused */
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity 100ms linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.zen .note-split:focus-within .ribbon-container {
|
|
||||||
opacity: 1; /* Show when the note split is focused */
|
|
||||||
}
|
|
||||||
|
|
||||||
body.zen .note-split .ribbon-container .ribbon-body {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.zen .note-split .ribbon-container .classic-toolbar-widget {
|
|
||||||
margin: auto;
|
|
||||||
width: fit-content;
|
|
||||||
box-shadow: 0px 10px 20px rgba(0, 0, 0, .1);
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--main-border-color);
|
|
||||||
padding: 4px;
|
|
||||||
background: var(--menu-background-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.zen .note-split .ribbon-container .classic-toolbar-widget:not(:has(> .ck-toolbar)) {
|
|
||||||
/* Hide the toolbar wrapper if the toolbar is missing */
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.zen .note-split:focus-within .ribbon-container .classic-toolbar-widget {
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1300px) {
|
|
||||||
body.zen .note-split .ribbon-container .classic-toolbar-widget {
|
|
||||||
/* Set the toolbar to full with */
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
opacity: 0; /* Hidden unless the current note split is focused */
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 100ms linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_se,
|
.note-split:focus-within .ribbon-container {
|
||||||
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sw,
|
opacity: 1; /* Show when the note split is focused */
|
||||||
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_smw,
|
}
|
||||||
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sme,
|
|
||||||
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_s {
|
.note-split .ribbon-container .ribbon-body {
|
||||||
/* Force toolbar items overflow dropdowns open upwards */
|
border: 0;
|
||||||
top: auto;
|
}
|
||||||
bottom: 100%;
|
|
||||||
|
.note-split .ribbon-container .classic-toolbar-widget {
|
||||||
|
margin: auto;
|
||||||
|
width: fit-content;
|
||||||
|
box-shadow: 0px 10px 20px rgba(0, 0, 0, .1);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--main-border-color);
|
||||||
|
padding: 4px;
|
||||||
|
background: var(--menu-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-split .ribbon-container .classic-toolbar-widget:not(:has(> .ck-toolbar)) {
|
||||||
|
/* Hide the toolbar wrapper if the toolbar is missing */
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-split:focus-within .ribbon-container .classic-toolbar-widget {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1300px) {
|
||||||
|
.note-split .ribbon-container .classic-toolbar-widget {
|
||||||
|
/* Set the toolbar to full with */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_se,
|
||||||
|
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sw,
|
||||||
|
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_smw,
|
||||||
|
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sme,
|
||||||
|
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_s {
|
||||||
|
/* Force toolbar items overflow dropdowns open upwards */
|
||||||
|
top: auto;
|
||||||
|
bottom: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen.experimental-feature-new-layout {
|
||||||
|
.status-bar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classic-toolbar-widget {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
opacity: 0; /* Hidden unless the current note split is focused */
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 100ms linear;
|
||||||
|
width: fit-content;
|
||||||
|
box-shadow: 0px 10px 20px rgba(0, 0, 0, .1);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--main-border-color);
|
||||||
|
padding: 4px;
|
||||||
|
background: var(--menu-background-color);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#root-widget:has(.note-split.type-text:focus-within) .classic-toolbar-widget,
|
||||||
|
.classic-toolbar-widget:focus-within {
|
||||||
|
opacity: 1; /* Show when the note split is focused */
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1300px) {
|
||||||
|
.classic-toolbar-widget {
|
||||||
|
/* Set the toolbar to full with */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_se,
|
||||||
|
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sw,
|
||||||
|
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_smw,
|
||||||
|
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sme,
|
||||||
|
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_s {
|
||||||
|
/* Force toolbar items overflow dropdowns open upwards */
|
||||||
|
top: auto;
|
||||||
|
bottom: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2414,7 +2429,7 @@ footer.webview-footer button {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admonition::before {
|
.admonition::before {
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
font-family: boxicons !important;
|
font-family: boxicons !important;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -2434,6 +2449,15 @@ footer.webview-footer button {
|
|||||||
.admonition.caution::before { content: "\eac7"; }
|
.admonition.caution::before { content: "\eac7"; }
|
||||||
.admonition.warning::before { content: "\eac5"; }
|
.admonition.warning::before { content: "\eac5"; }
|
||||||
|
|
||||||
|
.ck-content ul.todo-list li span.todo-list__label__description {
|
||||||
|
transition: opacity 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-content ul.todo-list li:has(> span.todo-list__label input[type="checkbox"]:checked) > span.todo-list__label span.todo-list__label__description {
|
||||||
|
text-decoration: line-through;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-options-container {
|
.chat-options-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
@@ -2498,6 +2522,11 @@ footer.webview-footer button {
|
|||||||
inset-inline-start: 10px;
|
inset-inline-start: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-floating-buttons.top-right {
|
||||||
|
top: 10px;
|
||||||
|
inset-inline-end: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.content-floating-buttons.bottom-left {
|
.content-floating-buttons.bottom-left {
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
inset-inline-start: 10px;
|
inset-inline-start: 10px;
|
||||||
@@ -2559,9 +2588,45 @@ iframe.print-iframe {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrolling-container > .note-detail.full-height,
|
.note-detail.full-height,
|
||||||
.scrolling-container > .note-list-widget.full-height {
|
.scrolling-container > .note-list-widget.full-height {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar collection */
|
||||||
|
|
||||||
|
.calendar-view a.fc-timegrid-event,
|
||||||
|
.calendar-view a.fc-daygrid-event {
|
||||||
|
/* Workaround: set font weight only if the theme-next is not active */
|
||||||
|
font-weight: var(--root-background, 800);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
body.mobile {
|
||||||
|
.split-note-container-widget {
|
||||||
|
flex-direction: column !important;
|
||||||
|
|
||||||
|
.note-split {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-split.visible + .note-split.visible {
|
||||||
|
border-top: 1px solid var(--main-border-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#root-widget.virtual-keyboard-opened .note-split:not(:focus-within) {
|
||||||
|
max-height: 80px;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.desktop .title-row {
|
||||||
|
height: 50px;
|
||||||
|
min-height: 50px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
--dropdown-border-color: #555;
|
--dropdown-border-color: #555;
|
||||||
--dropdown-shadow-opacity: 0.4;
|
--dropdown-shadow-opacity: 0.4;
|
||||||
--dropdown-item-icon-destructive-color: #de6e5b;
|
--dropdown-item-icon-destructive-color: #de6e5b;
|
||||||
--disabled-tooltip-icon-color: #7fd2ef;
|
--contextual-help-icon-color: #7fd2ef;
|
||||||
|
|
||||||
--accented-background-color: #555;
|
--accented-background-color: #555;
|
||||||
--more-accented-background-color: #777;
|
--more-accented-background-color: #777;
|
||||||
@@ -76,6 +76,9 @@
|
|||||||
|
|
||||||
--mermaid-theme: dark;
|
--mermaid-theme: dark;
|
||||||
--native-titlebar-background: #00000000;
|
--native-titlebar-background: #00000000;
|
||||||
|
|
||||||
|
--calendar-coll-event-background-saturation: 30%;
|
||||||
|
--calendar-coll-event-background-lightness: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body ::-webkit-calendar-picker-indicator {
|
body ::-webkit-calendar-picker-indicator {
|
||||||
@@ -109,3 +112,6 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
|
|||||||
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6) !important;
|
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.use-note-color {
|
||||||
|
--custom-color: var(--dark-theme-custom-color);
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ html {
|
|||||||
--dropdown-border-color: #ccc;
|
--dropdown-border-color: #ccc;
|
||||||
--dropdown-shadow-opacity: 0.2;
|
--dropdown-shadow-opacity: 0.2;
|
||||||
--dropdown-item-icon-destructive-color: #ec5138;
|
--dropdown-item-icon-destructive-color: #ec5138;
|
||||||
--disabled-tooltip-icon-color: #004382;
|
--contextual-help-icon-color: #004382;
|
||||||
|
|
||||||
--accented-background-color: #f5f5f5;
|
--accented-background-color: #f5f5f5;
|
||||||
--more-accented-background-color: #ddd;
|
--more-accented-background-color: #ddd;
|
||||||
@@ -80,6 +80,9 @@ html {
|
|||||||
|
|
||||||
--mermaid-theme: default;
|
--mermaid-theme: default;
|
||||||
--native-titlebar-background: #ffffff00;
|
--native-titlebar-background: #ffffff00;
|
||||||
|
|
||||||
|
--calendar-coll-event-background-lightness: 95%;
|
||||||
|
--calendar-coll-event-background-saturation: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#left-pane .fancytree-node.tinted {
|
#left-pane .fancytree-node.tinted {
|
||||||
@@ -91,4 +94,8 @@ html {
|
|||||||
.ck-content a.reference-link > span,
|
.ck-content a.reference-link > span,
|
||||||
.board-note {
|
.board-note {
|
||||||
color: var(--light-theme-custom-color, inherit);
|
color: var(--light-theme-custom-color, inherit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.use-note-color {
|
||||||
|
--custom-color: var(--light-theme-custom-color);
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
:root {
|
:root {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ⚠️ NOTICE: This theme is currently in the beta stage of development.
|
* ⚠️ NOTICE: This theme is currently in the beta stage of development.
|
||||||
* The names and purposes of these CSS variables are subject to frequent changes.
|
* The names and purposes of these CSS variables are subject to frequent changes.
|
||||||
*/
|
*/
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
--dropdown-border-color: #404040;
|
--dropdown-border-color: #404040;
|
||||||
--dropdown-shadow-opacity: 0.6;
|
--dropdown-shadow-opacity: 0.6;
|
||||||
--dropdown-item-icon-destructive-color: #de6e5b;
|
--dropdown-item-icon-destructive-color: #de6e5b;
|
||||||
--disabled-tooltip-icon-color: #7fd2ef;
|
--contextual-help-icon-color: #7fd2ef;
|
||||||
|
|
||||||
--accented-background-color: #555;
|
--accented-background-color: #555;
|
||||||
|
|
||||||
@@ -41,6 +41,9 @@
|
|||||||
--cmd-button-keyboard-shortcut-color: white;
|
--cmd-button-keyboard-shortcut-color: white;
|
||||||
--cmd-button-disabled-opacity: 0.5;
|
--cmd-button-disabled-opacity: 0.5;
|
||||||
|
|
||||||
|
--button-group-active-button-background: #ffffff4e;
|
||||||
|
--button-group-active-button-text-color: white;
|
||||||
|
|
||||||
--icon-button-color: currentColor;
|
--icon-button-color: currentColor;
|
||||||
--icon-button-hover-background: var(--hover-item-background-color);
|
--icon-button-hover-background: var(--hover-item-background-color);
|
||||||
--icon-button-hover-color: var(--hover-item-text-color);
|
--icon-button-hover-color: var(--hover-item-text-color);
|
||||||
@@ -98,6 +101,7 @@
|
|||||||
--menu-item-delimiter-color: #ffffff1c;
|
--menu-item-delimiter-color: #ffffff1c;
|
||||||
--menu-item-group-header-color: #ffffff91;
|
--menu-item-group-header-color: #ffffff91;
|
||||||
--menu-section-background-color: #fefefe08;
|
--menu-section-background-color: #fefefe08;
|
||||||
|
--menu-submenu-mobile-background-color: rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
--modal-backdrop-color: #000;
|
--modal-backdrop-color: #000;
|
||||||
--modal-shadow-color: rgba(0, 0, 0, .5);
|
--modal-shadow-color: rgba(0, 0, 0, .5);
|
||||||
@@ -178,7 +182,7 @@
|
|||||||
|
|
||||||
--tab-close-button-hover-background: #a45353;
|
--tab-close-button-hover-background: #a45353;
|
||||||
--tab-close-button-hover-color: white;
|
--tab-close-button-hover-color: white;
|
||||||
|
|
||||||
--active-tab-background-color: #ffffff1c;
|
--active-tab-background-color: #ffffff1c;
|
||||||
--active-tab-hover-background-color: var(--active-tab-background-color);
|
--active-tab-hover-background-color: var(--active-tab-background-color);
|
||||||
--active-tab-icon-color: #a9a9a9;
|
--active-tab-icon-color: #a9a9a9;
|
||||||
@@ -197,7 +201,7 @@
|
|||||||
|
|
||||||
--promoted-attribute-card-background-color: #ffffff21;
|
--promoted-attribute-card-background-color: #ffffff21;
|
||||||
--promoted-attribute-card-shadow: none;
|
--promoted-attribute-card-shadow: none;
|
||||||
|
|
||||||
--floating-button-shadow-color: #00000080;
|
--floating-button-shadow-color: #00000080;
|
||||||
--floating-button-background-color: #494949d2;
|
--floating-button-background-color: #494949d2;
|
||||||
--floating-button-color: var(--button-text-color);
|
--floating-button-color: var(--button-text-color);
|
||||||
@@ -222,7 +226,7 @@
|
|||||||
--scrollbar-border-color: unset; /* Deprecated */
|
--scrollbar-border-color: unset; /* Deprecated */
|
||||||
|
|
||||||
--selection-background-color: #3399FF70;
|
--selection-background-color: #3399FF70;
|
||||||
|
|
||||||
--link-color: lightskyblue;
|
--link-color: lightskyblue;
|
||||||
|
|
||||||
--mermaid-theme: dark;
|
--mermaid-theme: dark;
|
||||||
@@ -266,6 +270,14 @@
|
|||||||
--ck-editor-toolbar-button-on-color: white;
|
--ck-editor-toolbar-button-on-color: white;
|
||||||
--ck-editor-toolbar-button-on-shadow: 1px 1px 2px rgba(0, 0, 0, .75);
|
--ck-editor-toolbar-button-on-shadow: 1px 1px 2px rgba(0, 0, 0, .75);
|
||||||
--ck-editor-toolbar-dropdown-button-open-background: #ffffff14;
|
--ck-editor-toolbar-dropdown-button-open-background: #ffffff14;
|
||||||
|
|
||||||
|
--calendar-coll-event-background-saturation: 25%;
|
||||||
|
--calendar-coll-event-background-lightness: 20%;
|
||||||
|
--calendar-coll-event-background-color: #3c3c3c;
|
||||||
|
--calendar-coll-event-text-color: white;
|
||||||
|
--calendar-coll-event-hover-filter: brightness(1.25);
|
||||||
|
--callendar-coll-event-archived-sripe-color: #00000026;
|
||||||
|
--calendar-coll-today-background-color: #ffffff08;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -300,8 +312,12 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
|
|||||||
border-color: var(--muted-text-color) !important;
|
border-color: var(--muted-text-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tinted-quick-edit-dialog {
|
.quick-edit-dialog-wrapper.with-hue {
|
||||||
--modal-background-color: hsl(var(--custom-color-hue), 8.8%, 11.2%);
|
--modal-background-color: hsl(var(--custom-color-hue), 8.8%, 11.2%);
|
||||||
--modal-border-color: hsl(var(--custom-color-hue), 9.4%, 25.1%);
|
--modal-border-color: hsl(var(--custom-color-hue), 9.4%, 25.1%);
|
||||||
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 13.2%, 20.8%);
|
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 13.2%, 20.8%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.use-note-color {
|
||||||
|
--custom-color: var(--dark-theme-custom-color);
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
:root {
|
:root {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ⚠️ NOTICE: This theme is currently in the beta stage of development.
|
* ⚠️ NOTICE: This theme is currently in the beta stage of development.
|
||||||
* The names and purposes of these CSS variables are subject to frequent changes.
|
* The names and purposes of these CSS variables are subject to frequent changes.
|
||||||
*/
|
*/
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
--dropdown-border-color: #ccc;
|
--dropdown-border-color: #ccc;
|
||||||
--dropdown-shadow-opacity: 0.2;
|
--dropdown-shadow-opacity: 0.2;
|
||||||
--dropdown-item-icon-destructive-color: #ec5138;
|
--dropdown-item-icon-destructive-color: #ec5138;
|
||||||
--disabled-tooltip-icon-color: #004382;
|
--contextual-help-icon-color: #004382;
|
||||||
|
|
||||||
--accented-background-color: #f5f5f5;
|
--accented-background-color: #f5f5f5;
|
||||||
|
|
||||||
@@ -41,6 +41,9 @@
|
|||||||
--cmd-button-keyboard-shortcut-color: black;
|
--cmd-button-keyboard-shortcut-color: black;
|
||||||
--cmd-button-disabled-opacity: 0.5;
|
--cmd-button-disabled-opacity: 0.5;
|
||||||
|
|
||||||
|
--button-group-active-button-background: #00000026;
|
||||||
|
--button-group-active-button-text-color: black;
|
||||||
|
|
||||||
--icon-button-color: currentColor;
|
--icon-button-color: currentColor;
|
||||||
--icon-button-hover-background: var(--hover-item-background-color);
|
--icon-button-hover-background: var(--hover-item-background-color);
|
||||||
--icon-button-hover-color: var(--hover-item-text-color);
|
--icon-button-hover-color: var(--hover-item-text-color);
|
||||||
@@ -135,7 +138,7 @@
|
|||||||
/* Deprecated: now local variables in #launcher, with the values dependent on the current layout. */
|
/* Deprecated: now local variables in #launcher, with the values dependent on the current layout. */
|
||||||
--launcher-pane-background-color: unset;
|
--launcher-pane-background-color: unset;
|
||||||
--launcher-pane-text-color: unset;
|
--launcher-pane-text-color: unset;
|
||||||
|
|
||||||
--launcher-pane-vert-background-color: #e8e8e8;
|
--launcher-pane-vert-background-color: #e8e8e8;
|
||||||
--launcher-pane-vert-text-color: #000000bd;
|
--launcher-pane-vert-text-color: #000000bd;
|
||||||
--launcher-pane-vert-button-hover-color: black;
|
--launcher-pane-vert-button-hover-color: black;
|
||||||
@@ -171,7 +174,7 @@
|
|||||||
|
|
||||||
--tab-close-button-hover-background: #c95a5a;
|
--tab-close-button-hover-background: #c95a5a;
|
||||||
--tab-close-button-hover-color: white;
|
--tab-close-button-hover-color: white;
|
||||||
|
|
||||||
--active-tab-background-color: white;
|
--active-tab-background-color: white;
|
||||||
--active-tab-hover-background-color: var(--active-tab-background-color);
|
--active-tab-hover-background-color: var(--active-tab-background-color);
|
||||||
--active-tab-icon-color: gray;
|
--active-tab-icon-color: gray;
|
||||||
@@ -265,6 +268,14 @@
|
|||||||
--ck-editor-toolbar-button-on-color: black;
|
--ck-editor-toolbar-button-on-color: black;
|
||||||
--ck-editor-toolbar-button-on-shadow: none;
|
--ck-editor-toolbar-button-on-shadow: none;
|
||||||
--ck-editor-toolbar-dropdown-button-open-background: #0000000f;
|
--ck-editor-toolbar-dropdown-button-open-background: #0000000f;
|
||||||
|
|
||||||
|
--calendar-coll-event-background-lightness: 95%;
|
||||||
|
--calendar-coll-event-background-saturation: 80%;
|
||||||
|
--calendar-coll-event-background-color: #eaeaea;
|
||||||
|
--calendar-coll-event-text-color: black;
|
||||||
|
--calendar-coll-event-hover-filter: brightness(.95) saturate(1.25);
|
||||||
|
--callendar-coll-event-archived-sripe-color: #0000000a;
|
||||||
|
--calendar-coll-today-background-color: #00000006;
|
||||||
}
|
}
|
||||||
|
|
||||||
#left-pane .fancytree-node.tinted {
|
#left-pane .fancytree-node.tinted {
|
||||||
@@ -276,8 +287,8 @@
|
|||||||
--custom-bg-color: hsl(var(--custom-color-hue), 37%, 89%, 1);
|
--custom-bg-color: hsl(var(--custom-color-hue), 37%, 89%, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tinted-quick-edit-dialog {
|
.quick-edit-dialog-wrapper.with-hue {
|
||||||
--modal-background-color: hsl(var(--custom-color-hue), 56%, 96%);
|
--modal-background-color: hsl(var(--custom-color-hue), 56%, 96%);
|
||||||
--modal-border-color: hsl(var(--custom-color-hue), 33%, 41%);
|
--modal-border-color: hsl(var(--custom-color-hue), 33%, 41%);
|
||||||
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 40%, 88%);
|
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 40%, 88%);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
|
|
||||||
--menu-padding-size: 8px;
|
--menu-padding-size: 8px;
|
||||||
--menu-item-icon-vert-offset: -2px;
|
--menu-item-icon-vert-offset: -2px;
|
||||||
|
--menu-submenu-mobile-background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
|
||||||
--more-accented-background-color: var(--card-background-hover-color);
|
--more-accented-background-color: var(--card-background-hover-color);
|
||||||
|
|
||||||
@@ -88,17 +89,25 @@
|
|||||||
* the color is adjusted based on the current color scheme (light or dark). The lightness
|
* the color is adjusted based on the current color scheme (light or dark). The lightness
|
||||||
* component of the color represented in the CIELAB color space, will be
|
* component of the color represented in the CIELAB color space, will be
|
||||||
* constrained to a certain percentage defined below.
|
* constrained to a certain percentage defined below.
|
||||||
*
|
*
|
||||||
* Note: the tree background may vary when background effects are enabled, so it is recommended
|
* Note: the tree background may vary when background effects are enabled, so it is recommended
|
||||||
* to maintain a higher contrast margin than on the usual note tree solid background. */
|
* to maintain a higher contrast margin than on the usual note tree solid background. */
|
||||||
|
|
||||||
/* The maximum perceptual lightness for the custom color in the light theme (%): */
|
/* The maximum perceptual lightness for the custom color in the light theme (%): */
|
||||||
--tree-item-light-theme-max-color-lightness: 60;
|
--tree-item-light-theme-max-color-lightness: 60;
|
||||||
|
|
||||||
/* The minimum perceptual lightness for the custom color in the dark theme (%): */
|
/* The minimum perceptual lightness for the custom color in the dark theme (%): */
|
||||||
--tree-item-dark-theme-min-color-lightness: 65;
|
--tree-item-dark-theme-min-color-lightness: 65;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectable-text {
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
body.backdrop-effects-disabled {
|
body.backdrop-effects-disabled {
|
||||||
/* Backdrop effects are disabled, replace the menu background color with the
|
/* Backdrop effects are disabled, replace the menu background color with the
|
||||||
* no-backdrop fallback color */
|
* no-backdrop fallback color */
|
||||||
@@ -119,17 +128,6 @@ body.backdrop-effects-disabled {
|
|||||||
font-size: 0.9rem !important;
|
font-size: 0.9rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.mobile .dropdown-menu {
|
|
||||||
backdrop-filter: var(--dropdown-backdrop-filter);
|
|
||||||
border-radius: var(--dropdown-border-radius);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.mobile .dropdown-menu .dropdown-menu {
|
|
||||||
backdrop-filter: unset !important;
|
|
||||||
border-radius: unset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.desktop .dropdown-menu::before,
|
body.desktop .dropdown-menu::before,
|
||||||
:root .ck.ck-dropdown__panel::before,
|
:root .ck.ck-dropdown__panel::before,
|
||||||
:root .excalidraw .popover::before,
|
:root .excalidraw .popover::before,
|
||||||
@@ -157,32 +155,45 @@ body.desktop .dropdown-submenu .dropdown-menu::before {
|
|||||||
content: unset;
|
content: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.mobile .dropdown-submenu .dropdown-menu {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.desktop .dropdown-submenu .dropdown-menu {
|
body.desktop .dropdown-submenu .dropdown-menu {
|
||||||
backdrop-filter: var(--dropdown-backdrop-filter);
|
backdrop-filter: var(--dropdown-backdrop-filter);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-item,
|
.dropdown-item,
|
||||||
body.mobile .dropdown-submenu .dropdown-toggle,
|
|
||||||
.excalidraw .context-menu .context-menu-item {
|
.excalidraw .context-menu .context-menu-item {
|
||||||
--menu-item-start-padding: 8px;
|
--menu-item-start-padding: 8px;
|
||||||
--menu-item-end-padding: 22px;
|
--menu-item-end-padding: 22px;
|
||||||
--menu-item-vertical-padding: 2px;
|
--menu-item-vertical-padding: 2px;
|
||||||
|
|
||||||
padding-top: var(--menu-item-vertical-padding) !important;
|
|
||||||
padding-bottom: var(--menu-item-vertical-padding) !important;
|
|
||||||
padding-inline-start: var(--menu-item-start-padding) !important;
|
|
||||||
padding-inline-end: var(--menu-item-end-padding) !important;
|
|
||||||
|
|
||||||
/* Note: the right padding should also accommodate the submenu arrow. */
|
/* Note: the right padding should also accommodate the submenu arrow. */
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
cursor: default !important;
|
cursor: default !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-item:not(.dropdown-submenu),
|
||||||
|
body.desktop .dropdown-item.dropdown-submenu .dropdown-toggle,
|
||||||
|
.excalidraw .context-menu .context-menu-item {
|
||||||
|
padding-top: var(--menu-item-vertical-padding) !important;
|
||||||
|
padding-bottom: var(--menu-item-vertical-padding) !important;
|
||||||
|
padding-inline-start: var(--menu-item-start-padding) !important;
|
||||||
|
padding-inline-end: var(--menu-item-end-padding) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item.dropdown-submenu {
|
||||||
|
padding: 0 !important;
|
||||||
|
|
||||||
|
.dropdown-toggle {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.desktop .dropdown-menu:has(> .dropdown-submenu.dropstart) > .dropdown-item:not(.dropdown-submenu),
|
||||||
|
body.desktop .dropdown-menu:has(> .dropdown-submenu.dropstart) > .dropdown-item.dropdown-submenu .dropdown-toggle {
|
||||||
|
padding-inline-end: var(--menu-item-start-padding) !important;
|
||||||
|
padding-inline-start: var(--menu-item-end-padding) !important;
|
||||||
|
}
|
||||||
|
|
||||||
:root .dropdown-item:focus-visible {
|
:root .dropdown-item:focus-visible {
|
||||||
outline: 2px solid var(--input-focus-outline-color) !important;
|
outline: 2px solid var(--input-focus-outline-color) !important;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -201,10 +212,6 @@ body.mobile .dropdown-item:not(:last-of-type) {
|
|||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.mobile .dropdown-submenu:hover {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
html body .dropdown-item.disabled,
|
html body .dropdown-item.disabled,
|
||||||
html body .dropdown-item[disabled] {
|
html body .dropdown-item[disabled] {
|
||||||
color: var(--menu-text-color) !important;
|
color: var(--menu-text-color) !important;
|
||||||
@@ -260,7 +267,8 @@ html body .dropdown-item[disabled] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Menu item arrow */
|
/* Menu item arrow */
|
||||||
.dropdown-menu .dropdown-toggle::after {
|
body.mobile .dropdown-submenu .dropdown-toggle::after,
|
||||||
|
body.desktop .dropdown-submenu:not(.dropstart) .dropdown-toggle::after {
|
||||||
content: "\ed3b" !important;
|
content: "\ed3b" !important;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
@@ -276,6 +284,26 @@ html body .dropdown-item[disabled] {
|
|||||||
color: var(--menu-item-arrow-color) !important;
|
color: var(--menu-item-arrow-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.mobile .dropdown-submenu.dropstart .dropdown-toggle::before {
|
||||||
|
content: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.desktop .dropdown-submenu.dropstart .dropdown-toggle::before {
|
||||||
|
content: "\ea4d" !important;
|
||||||
|
position: absolute;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
top: 0;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
margin: unset !important;
|
||||||
|
border: unset !important;
|
||||||
|
padding: 0 4px;
|
||||||
|
font-family: boxicons;
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: var(--menu-item-arrow-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
body[dir=rtl] .dropdown-menu:not([data-popper-placement="bottom-start"]) .dropdown-toggle::after {
|
body[dir=rtl] .dropdown-menu:not([data-popper-placement="bottom-start"]) .dropdown-toggle::after {
|
||||||
content: "\ea4d" !important;
|
content: "\ea4d" !important;
|
||||||
}
|
}
|
||||||
@@ -321,17 +349,126 @@ body.desktop .dropdown-menu.static .dropdown-item.active {
|
|||||||
--active-item-text-color: var(--menu-text-color);
|
--active-item-text-color: var(--menu-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* #region Mobile tweaks for dropdown menus */
|
||||||
|
body.mobile #context-menu-cover {
|
||||||
|
transition: background-color 150ms ease-in;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.global-menu-cover {
|
||||||
|
bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size));
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile .dropdown-menu.mobile-bottom-menu,
|
||||||
|
body.mobile .dropdown.global-menu .dropdown-menu {
|
||||||
|
border-radius: var(--dropdown-border-radius) var(--dropdown-border-radius) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile .dropdown-menu {
|
||||||
|
--dropdown-menu-padding-vertical: 0.7em;
|
||||||
|
--dropdown-menu-padding-horizontal: 1em;
|
||||||
|
--hover-item-background-color: var(--card-background-color);
|
||||||
|
font-size: 1em !important;
|
||||||
|
backdrop-filter: var(--dropdown-backdrop-filter);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.dropdown-toggle::after {
|
||||||
|
top: 0.5em;
|
||||||
|
right: var(--dropdown-menu-padding-horizontal);
|
||||||
|
transform: translateX(50%) rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item.submenu-open .dropdown-toggle::after {
|
||||||
|
transform: rotate(270deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item,
|
||||||
|
.dropdown-custom-item {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: var(--dropdown-menu-padding-vertical) var(--dropdown-menu-padding-horizontal) !important;
|
||||||
|
background: var(--card-background-color);
|
||||||
|
border-bottom: 1px solid var(--menu-item-delimiter-color) !important;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:first-of-type,
|
||||||
|
.dropdown-divider + .dropdown-item,
|
||||||
|
.dropdown-custom-item:first-of-type,
|
||||||
|
.dropdown-divider + .dropdown-custom-item {
|
||||||
|
border-top-left-radius: 6px;
|
||||||
|
border-top-right-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:last-of-type,
|
||||||
|
.dropdown-item:has(+ .dropdown-divider),
|
||||||
|
.dropdown-custom-item:last-of-type,
|
||||||
|
.dropdown-custom-item:has(+ .dropdown-divider) {
|
||||||
|
border-bottom-left-radius: 6px;
|
||||||
|
border-bottom-right-radius: 6px;
|
||||||
|
border-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-divider {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-submenu {
|
||||||
|
padding: 0 !important;
|
||||||
|
backdrop-filter: unset !important;
|
||||||
|
|
||||||
|
.dropdown-toggle {
|
||||||
|
padding: var(--dropdown-menu-padding-vertical) var(--dropdown-menu-padding-horizontal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
--menu-background-color: --menu-submenu-mobile-background-color;
|
||||||
|
--bs-dropdown-divider-margin-y: 0.25rem;
|
||||||
|
border-radius: 0;
|
||||||
|
max-height: 0;
|
||||||
|
transition: max-height 100ms ease-in;
|
||||||
|
display: block !important;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
max-height: 1000px;
|
||||||
|
padding: 0.5rem 0.75rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.submenu-open {
|
||||||
|
.dropdown-toggle {
|
||||||
|
padding-bottom: var(--dropdown-menu-padding-vertical);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-custom-item:has(.note-color-picker) {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-color-picker {
|
||||||
|
padding: 0;
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
.color-cell {
|
||||||
|
--color-picker-cell-size: 26px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
body.desktop .dropdown-menu .dropdown-toggle::after {
|
body.desktop .dropdown-menu .dropdown-toggle::after {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.mobile .dropdown-menu .dropdown-toggle::after {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after {
|
|
||||||
transform: rotate(270deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dropdown item button (used in zoom buttons in global menu) */
|
/* Dropdown item button (used in zoom buttons in global menu) */
|
||||||
|
|
||||||
@@ -347,6 +484,12 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
|
|||||||
outline: 2px solid var(--input-focus-outline-color) !important;
|
outline: 2px solid var(--input-focus-outline-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root .dropdown-menu .note-color-picker {
|
||||||
|
padding: 4px 10px;
|
||||||
|
--note-color-picker-clear-color-cell-background: var(--main-text-color);
|
||||||
|
--note-color-picker-clear-color-cell-selection-outline-color: var(--main-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* TOASTS
|
* TOASTS
|
||||||
*/
|
*/
|
||||||
@@ -639,4 +782,4 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
|
|||||||
.note-detail-empty .aa-suggestions div.aa-cursor {
|
.note-detail-empty .aa-suggestions div.aa-cursor {
|
||||||
background: var(--hover-item-background-color);
|
background: var(--hover-item-background-color);
|
||||||
color: var(--hover-item-text-color);
|
color: var(--hover-item-text-color);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
.modal .modal-header .btn-close,
|
.modal .modal-header .btn-close,
|
||||||
.modal .modal-header .help-button,
|
.modal .modal-header .help-button,
|
||||||
|
.modal .modal-header .custom-title-bar-button,
|
||||||
#toast-container .toast .toast-header .btn-close {
|
#toast-container .toast .toast-header .btn-close {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -55,15 +56,17 @@
|
|||||||
font-family: boxicons;
|
font-family: boxicons;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal .modal-header .help-button {
|
.modal .modal-header .help-button,
|
||||||
|
.modal .modal-header .custom-title-bar-button {
|
||||||
margin-inline-end: 0;
|
margin-inline-end: 0;
|
||||||
font-size: calc(var(--modal-control-button-size) * .75);
|
font-size: calc(var(--modal-control-button-size) * .70);
|
||||||
font-family: unset;
|
font-family: unset;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal .modal-header .btn-close:hover,
|
.modal .modal-header .btn-close:hover,
|
||||||
.modal .modal-header .help-button:hover,
|
.modal .modal-header .help-button:hover,
|
||||||
|
.modal .modal-header .custom-title-bar-button:hover,
|
||||||
#toast-container .toast .toast-header .btn-close:hover {
|
#toast-container .toast .toast-header .btn-close:hover {
|
||||||
background: var(--modal-control-button-hover-background);
|
background: var(--modal-control-button-hover-background);
|
||||||
color: var(--modal-control-button-hover-color);
|
color: var(--modal-control-button-hover-color);
|
||||||
@@ -71,6 +74,7 @@
|
|||||||
|
|
||||||
.modal .modal-header .btn-close:active,
|
.modal .modal-header .btn-close:active,
|
||||||
.modal .modal-header .help-button:active,
|
.modal .modal-header .help-button:active,
|
||||||
|
.modal .modal-header .custom-title-bar-button:active,
|
||||||
#toast-container .toast .toast-header .btn-close:active {
|
#toast-container .toast .toast-header .btn-close:active {
|
||||||
transform: scale(.85);
|
transform: scale(.85);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel, .c
|
|||||||
padding: 4px 16px;
|
padding: 4px 16px;
|
||||||
background: var(--cmd-button-background-color);
|
background: var(--cmd-button-background-color);
|
||||||
color: var(--cmd-button-text-color);
|
color: var(--cmd-button-text-color);
|
||||||
|
|
||||||
|
&.dropdown-toggle-split {
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button.btn.btn-primary:hover,
|
button.btn.btn-primary:hover,
|
||||||
@@ -142,7 +146,15 @@ button.btn.btn-success kbd {
|
|||||||
outline: 2px solid var(--input-focus-outline-color);
|
outline: 2px solid var(--input-focus-outline-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/* Button groups */
|
||||||
|
|
||||||
|
/* Active button */
|
||||||
|
:root .btn-group button.btn.active {
|
||||||
|
background-color: var(--button-group-active-button-background);
|
||||||
|
color: var(--button-group-active-button-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
* Input boxes
|
* Input boxes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -387,7 +399,8 @@ button.select-button.dropdown-toggle.btn:active {
|
|||||||
select:focus,
|
select:focus,
|
||||||
select.form-select:focus,
|
select.form-select:focus,
|
||||||
select.form-control:focus,
|
select.form-control:focus,
|
||||||
.select-button.dropdown-toggle.btn:focus {
|
.select-button.dropdown-toggle.btn:focus,
|
||||||
|
.select-button.focus-outline:focus {
|
||||||
box-shadow: unset;
|
box-shadow: unset;
|
||||||
outline: 3px solid var(--input-focus-outline-color);
|
outline: 3px solid var(--input-focus-outline-color);
|
||||||
outline-offset: 0;
|
outline-offset: 0;
|
||||||
@@ -410,7 +423,7 @@ optgroup {
|
|||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* File input
|
* File input
|
||||||
*
|
*
|
||||||
* <label class="tn-file-input tn-input-field">
|
* <label class="tn-file-input tn-input-field">
|
||||||
@@ -772,4 +785,4 @@ input[type="range"] {
|
|||||||
scrollbar-color: unset;
|
scrollbar-color: unset;
|
||||||
scrollbar-width: unset;
|
scrollbar-width: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -526,11 +526,14 @@ button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel).ck
|
|||||||
.ck-mermaid__editing-view {
|
.ck-mermaid__editing-view {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: var(--code-block-box-shadow);
|
box-shadow: var(--code-block-box-shadow);
|
||||||
padding: 0;
|
|
||||||
margin-top: 2px !important;
|
margin-top: 2px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root .ck-content pre:has(> code) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
:root .ck-content pre {
|
:root .ck-content pre {
|
||||||
--icon-button-size: 1.8em;
|
--icon-button-size: 1.8em;
|
||||||
--copy-button-width: var(--icon-button-size);
|
--copy-button-width: var(--icon-button-size);
|
||||||
@@ -643,7 +646,7 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-detail-printable:not(.word-wrap) pre code {
|
.ck-content:not(.word-wrap) pre code {
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,12 +124,8 @@
|
|||||||
|
|
||||||
/* The container */
|
/* The container */
|
||||||
|
|
||||||
.note-split.empty-note {
|
|
||||||
--max-content-width: 70%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-split.empty-note div.note-detail {
|
.note-split.empty-note div.note-detail {
|
||||||
margin: 50px auto;
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The search results list */
|
/* The search results list */
|
||||||
|
|||||||
@@ -164,17 +164,11 @@ ul.editability-dropdown li.dropdown-item > div {
|
|||||||
background: var(--cmd-button-hover-background-color);
|
background: var(--cmd-button-hover-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Note info
|
* Note info
|
||||||
*/
|
*/
|
||||||
|
|
||||||
:root .note-info-widget-table button.calculate-button {
|
|
||||||
min-width: 0;
|
|
||||||
padding: 4px 10px !important;
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Narrow width layout */
|
/* Narrow width layout */
|
||||||
.note-info-widget {
|
.note-info-widget {
|
||||||
container: info-section / inline-size;
|
container: info-section / inline-size;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,7 +212,8 @@ body[dir=ltr] #launcher-container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#launcher-pane .launcher-button,
|
#launcher-pane .launcher-button,
|
||||||
#launcher-pane .dropdown {
|
#launcher-pane .right-dropdown-widget,
|
||||||
|
#launcher-pane .global-menu {
|
||||||
width: calc(var(--launcher-pane-size) - (var(--launcher-pane-button-margin) * 2)) !important;
|
width: calc(var(--launcher-pane-size) - (var(--launcher-pane-button-margin) * 2)) !important;
|
||||||
height: calc(var(--launcher-pane-size) - (var(--launcher-pane-button-margin) * 2)) !important;
|
height: calc(var(--launcher-pane-size) - (var(--launcher-pane-button-margin) * 2)) !important;
|
||||||
margin: var(--launcher-pane-button-gap) var(--launcher-pane-button-margin);
|
margin: var(--launcher-pane-button-gap) var(--launcher-pane-button-margin);
|
||||||
@@ -345,7 +346,7 @@ body[dir=ltr] #launcher-container {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.calendar-dropdown-widget {
|
.calendar-dropdown-widget {
|
||||||
padding: 12px;
|
padding: 18px;
|
||||||
color: var(--calendar-color);
|
color: var(--calendar-color);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
@@ -501,7 +502,7 @@ div.bookmark-folder-widget .note-link .bx {
|
|||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* QUICK SEARCH BOX
|
* QUICK SEARCH BOX
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -612,7 +613,7 @@ div.quick-search .dropdown-menu {
|
|||||||
* As a temporary workaround, the backdrop and transparency are disabled for the
|
* As a temporary workaround, the backdrop and transparency are disabled for the
|
||||||
* vertical layout.
|
* vertical layout.
|
||||||
*/
|
*/
|
||||||
body.layout-vertical.background-effects div.quick-search .dropdown-menu {
|
body.layout-vertical.background-effects div.quick-search .dropdown-menu {
|
||||||
--menu-background-color: var(--menu-background-color-no-backdrop) !important;
|
--menu-background-color: var(--menu-background-color-no-backdrop) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -944,12 +945,26 @@ body.electron.background-effects.layout-horizontal .tab-row-container .toggle-bu
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
inset-inline-start: -10px;
|
inset-inline-start: -10px;
|
||||||
inset-inline-end: -10px;
|
inset-inline-end: -6px;
|
||||||
top: 32px;
|
top: 32px;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
|
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.electron.background-effects.layout-horizontal .tab-row-container .tab-history-navigation-buttons {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
inset-inline-end: -7px;
|
||||||
|
height: 1px;
|
||||||
|
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-left,
|
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-left,
|
||||||
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-right {
|
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-right {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -1428,9 +1443,7 @@ div.promoted-attribute-cell .tn-checkbox {
|
|||||||
height: 1cap;
|
height: 1cap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Relocate the checkbox before the label */
|
|
||||||
div.promoted-attribute-cell.promoted-attribute-label-boolean > div:first-of-type {
|
div.promoted-attribute-cell.promoted-attribute-label-boolean > div:first-of-type {
|
||||||
order: -1;
|
|
||||||
margin-inline-end: 1.5em;
|
margin-inline-end: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1450,12 +1463,20 @@ div.promoted-attribute-cell .multiplicity:has(span) span {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.promoted-attribute-cell.promoted-attribute-label-color {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.promoted-attribute-cell.promoted-attribute-label-color .input-group {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Floating buttons
|
* Floating buttons
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Floating buttons container */
|
/* Floating buttons container */
|
||||||
div#center-pane .floating-buttons-children {
|
.floating-buttons-children {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
min-height: var(--floating-button-height);
|
min-height: var(--floating-button-height);
|
||||||
transform-origin: right;
|
transform-origin: right;
|
||||||
@@ -1467,12 +1488,12 @@ div#center-pane .floating-buttons-children {
|
|||||||
opacity 250ms ease-out;
|
opacity 250ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[dir=rtl] div#center-pane .floating-buttons-children {
|
body[dir=rtl] .floating-buttons-children {
|
||||||
transform-origin: left;
|
transform-origin: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Floating buttons container (collapsed) */
|
/* Floating buttons container (collapsed) */
|
||||||
div#center-pane .floating-buttons-children.temporarily-hidden {
|
.floating-buttons-children.temporarily-hidden {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scaleX(0);
|
transform: scaleX(0);
|
||||||
@@ -1562,7 +1583,7 @@ div.floating-buttons .show-floating-buttons-button {
|
|||||||
div.floating-buttons .show-floating-buttons-button::before {
|
div.floating-buttons .show-floating-buttons-button::before {
|
||||||
animation: floating-buttons-show-hide-button-animation 400ms ease-out;
|
animation: floating-buttons-show-hide-button-animation 400ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.floating-buttons .show-floating-buttons-button:hover,
|
div.floating-buttons .show-floating-buttons-button:hover,
|
||||||
div.floating-buttons .show-floating-buttons-button:active {
|
div.floating-buttons .show-floating-buttons-button:active {
|
||||||
box-shadow: var(--floating-button-show-button-hover-shadow);
|
box-shadow: var(--floating-button-show-button-hover-shadow);
|
||||||
@@ -1824,7 +1845,7 @@ div.find-replace-widget div.find-widget-found-wrapper > span {
|
|||||||
|
|
||||||
.excalidraw .dropdown-menu {
|
.excalidraw .dropdown-menu {
|
||||||
border: unset !important;
|
border: unset !important;
|
||||||
box-shadow: unset !important;
|
box-shadow: unset !important;
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
--island-bg-color: var(--menu-background-color);
|
--island-bg-color: var(--menu-background-color);
|
||||||
--shadow-island: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
|
--shadow-island: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
|
||||||
@@ -1843,4 +1864,4 @@ div.find-replace-widget div.find-widget-found-wrapper > span {
|
|||||||
|
|
||||||
.excalidraw .dropdown-menu:before {
|
.excalidraw .dropdown-menu:before {
|
||||||
content: unset !important;
|
content: unset !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,6 +160,11 @@ span.fancytree-node.multiple-parents .fancytree-title::after {
|
|||||||
content: " \eb3d"; /* lookup code for "link-alt" in boxicons.css */
|
content: " \eb3d"; /* lookup code for "link-alt" in boxicons.css */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.experimental-feature-new-layout span.fancytree-node.multiple-parents .fancytree-title::after {
|
||||||
|
content: " \ed82";
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
span.fancytree-node.shared .fancytree-title::after {
|
span.fancytree-node.shared .fancytree-title::after {
|
||||||
font-family: "boxicons" !important;
|
font-family: "boxicons" !important;
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import utils from "../services/utils.js";
|
import { NoteType } from "@triliumnext/commons";
|
||||||
|
|
||||||
|
import FAttribute from "../entities/fattribute.js";
|
||||||
|
import FBlob from "../entities/fblob.js";
|
||||||
|
import FBranch from "../entities/fbranch.js";
|
||||||
import FNote from "../entities/fnote.js";
|
import FNote from "../entities/fnote.js";
|
||||||
import froca from "../services/froca.js";
|
import froca from "../services/froca.js";
|
||||||
import FAttribute from "../entities/fattribute.js";
|
|
||||||
import noteAttributeCache from "../services/note_attribute_cache.js";
|
import noteAttributeCache from "../services/note_attribute_cache.js";
|
||||||
import FBranch from "../entities/fbranch.js";
|
import utils from "../services/utils.js";
|
||||||
import FBlob from "../entities/fblob.js";
|
|
||||||
|
|
||||||
type AttributeDefinitions = { [key in `#${string}`]: string; };
|
type AttributeDefinitions = { [key in `#${string}`]: string; };
|
||||||
type RelationDefinitions = { [key in `~${string}`]: string; };
|
type RelationDefinitions = { [key in `~${string}`]: string; };
|
||||||
@@ -12,6 +14,7 @@ type RelationDefinitions = { [key in `~${string}`]: string; };
|
|||||||
interface NoteDefinition extends AttributeDefinitions, RelationDefinitions {
|
interface NoteDefinition extends AttributeDefinitions, RelationDefinitions {
|
||||||
id?: string | undefined;
|
id?: string | undefined;
|
||||||
title: string;
|
title: string;
|
||||||
|
type?: NoteType;
|
||||||
children?: NoteDefinition[];
|
children?: NoteDefinition[];
|
||||||
content?: string;
|
content?: string;
|
||||||
}
|
}
|
||||||
@@ -45,7 +48,7 @@ export function buildNote(noteDef: NoteDefinition) {
|
|||||||
const note = new FNote(froca, {
|
const note = new FNote(froca, {
|
||||||
noteId: noteDef.id ?? utils.randomString(12),
|
noteId: noteDef.id ?? utils.randomString(12),
|
||||||
title: noteDef.title,
|
title: noteDef.title,
|
||||||
type: "text",
|
type: noteDef.type ?? "text",
|
||||||
mime: "text/html",
|
mime: "text/html",
|
||||||
isProtected: false,
|
isProtected: false,
|
||||||
blobId: ""
|
blobId: ""
|
||||||
@@ -87,7 +90,11 @@ export function buildNote(noteDef: NoteDefinition) {
|
|||||||
let position = 0;
|
let position = 0;
|
||||||
for (const [ key, value ] of Object.entries(noteDef)) {
|
for (const [ key, value ] of Object.entries(noteDef)) {
|
||||||
const attributeId = utils.randomString(12);
|
const attributeId = utils.randomString(12);
|
||||||
const name = key.substring(1);
|
let name = key.substring(1);
|
||||||
|
const isInheritable = key.endsWith("(inheritable)");
|
||||||
|
if (isInheritable) {
|
||||||
|
name = name.substring(0, name.length - "(inheritable)".length);
|
||||||
|
}
|
||||||
|
|
||||||
let attribute: FAttribute | null = null;
|
let attribute: FAttribute | null = null;
|
||||||
if (key.startsWith("#")) {
|
if (key.startsWith("#")) {
|
||||||
@@ -98,7 +105,7 @@ export function buildNote(noteDef: NoteDefinition) {
|
|||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
position,
|
position,
|
||||||
isInheritable: false
|
isInheritable
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +117,7 @@ export function buildNote(noteDef: NoteDefinition) {
|
|||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
position,
|
position,
|
||||||
isInheritable: false
|
isInheritable
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ function mockServer() {
|
|||||||
attributes: []
|
attributes: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.warn(`Unsupported GET to mocked server: ${url}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
async post(url: string, data: object) {
|
async post(url: string, data: object) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user