mirror of
https://github.com/zadam/trilium.git
synced 2026-07-10 02:52:53 +02:00
Compare commits
539 Commits
feat/rice-
...
copilot/sw
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11d396016f | ||
|
|
d2cf8434cf | ||
|
|
5fec097ff8 | ||
|
|
1812fdcfb6 | ||
|
|
49c0adb5d3 | ||
|
|
5c1c0efe0a | ||
|
|
a53dc1193b | ||
|
|
0d169b0641 | ||
|
|
c080bb7926 | ||
|
|
693a9363a4 | ||
|
|
7829957548 | ||
|
|
48ff643c7a | ||
|
|
71e9eae5e6 | ||
|
|
c7842c7f05 | ||
|
|
b812d7f918 | ||
|
|
3041cb088c | ||
|
|
638beb5b03 | ||
|
|
156aa55ee7 | ||
|
|
f197756bb4 | ||
|
|
ad1f83fc75 | ||
|
|
12d9391b1e | ||
|
|
60facceef1 | ||
|
|
8d0c61ea37 | ||
|
|
3865d96d44 | ||
|
|
647ba85211 | ||
|
|
ea09018acd | ||
|
|
81445901fa | ||
|
|
e53fd1b059 | ||
|
|
9e3bb585a2 | ||
|
|
aa9b05641f | ||
|
|
cffcfeb734 | ||
|
|
6e32aaac33 | ||
|
|
f0d91d45f4 | ||
|
|
29ebb316b6 | ||
|
|
e3172ebf1c | ||
|
|
9b2876a8ff | ||
|
|
3db2c910e0 | ||
|
|
2799e4392f | ||
|
|
61963fcfda | ||
|
|
b0a3d54276 | ||
|
|
2135412c84 | ||
|
|
7a534c3ea7 | ||
|
|
a6e596a5e3 | ||
|
|
8b3e3c2c3a | ||
|
|
ec8b0a3801 | ||
|
|
197b769838 | ||
|
|
1ba498c0e3 | ||
|
|
5550cb7b95 | ||
|
|
06a005acec | ||
|
|
2103be9d28 | ||
|
|
a02f3c4440 | ||
|
|
4b8d341e00 | ||
|
|
964633f426 | ||
|
|
d11fb38280 | ||
|
|
04f4530990 | ||
|
|
5a77318a9e | ||
|
|
9ed2894a0c | ||
|
|
e0766ad439 | ||
|
|
67acfaab62 | ||
|
|
10b5d29107 | ||
|
|
297dd41170 | ||
|
|
1364223599 | ||
|
|
a6b7761dfa | ||
|
|
2e6290c514 | ||
|
|
78f4928611 | ||
|
|
d044fce9c1 | ||
|
|
11fa815e13 | ||
|
|
8d917eb970 | ||
|
|
e411e5f2cf | ||
|
|
48773636ca | ||
|
|
5a3c7355c1 | ||
|
|
afa9fe3063 | ||
|
|
178ac088b4 | ||
|
|
ffd5ebbe79 | ||
|
|
6b78bfecb4 | ||
|
|
d3c0a44c00 | ||
|
|
33ea2de231 | ||
|
|
43ebbfc321 | ||
|
|
1e5b294eb3 | ||
|
|
29855112c8 | ||
|
|
8af7b3c81a | ||
|
|
b72b82ff1a | ||
|
|
1588c8103c | ||
|
|
15e569dcea | ||
|
|
4bdb407404 | ||
|
|
4568cedcd3 | ||
|
|
f290317acc | ||
|
|
645279c8fa | ||
|
|
09436f8d65 | ||
|
|
b616e0e5f9 | ||
|
|
f06a0852a1 | ||
|
|
9e688138be | ||
|
|
5d46970a38 | ||
|
|
ecc441c074 | ||
|
|
f2ce3678c4 | ||
|
|
c9ad390647 | ||
|
|
92acc7accd | ||
|
|
2ef4eb7eae | ||
|
|
7b230706cb | ||
|
|
5a2b04adba | ||
|
|
50dcd3ba44 | ||
|
|
740b02952f | ||
|
|
5d3d42ffdd | ||
|
|
866d3110da | ||
|
|
7a3e7fccec | ||
|
|
3107bc8840 | ||
|
|
2d34cdef5e | ||
|
|
bd1f0909a2 | ||
|
|
ef75de63fe | ||
|
|
a739d28563 | ||
|
|
66ff009b72 | ||
|
|
a68e82c1c8 | ||
|
|
46556c1c14 | ||
|
|
7be637798f | ||
|
|
9b3396349e | ||
|
|
ccff210b4c | ||
|
|
a2264847b6 | ||
|
|
7d103f8c50 | ||
|
|
f3dccc0aec | ||
|
|
311b1d8a64 | ||
|
|
f3094e3079 | ||
|
|
f3b37b16d5 | ||
|
|
5f16ecf02d | ||
|
|
8b75287827 | ||
|
|
bb38e806cd | ||
|
|
8cbc15f1b0 | ||
|
|
870524f9cf | ||
|
|
5da9963f31 | ||
|
|
34e885812f | ||
|
|
a9ac11452d | ||
|
|
04b91308b1 | ||
|
|
b09ef222f5 | ||
|
|
2d0ed06d50 | ||
|
|
8dd7cf6085 | ||
|
|
4999bd4f1e | ||
|
|
22f408addb | ||
|
|
9ca1dbe638 | ||
|
|
26662952e3 | ||
|
|
f0c9fa4ca3 | ||
|
|
b51aa1dd71 | ||
|
|
846253c9e3 | ||
|
|
6ab6ea97ac | ||
|
|
bf41f70b98 | ||
|
|
67ddbedd08 | ||
|
|
2573e219dc | ||
|
|
7e368678ab | ||
|
|
4a9fcf7ab6 | ||
|
|
65856c61c5 | ||
|
|
89ea0a2730 | ||
|
|
d847d966bc | ||
|
|
b5a97bffab | ||
|
|
e6d728715f | ||
|
|
54a52f0589 | ||
|
|
badfa23f86 | ||
|
|
30ccd3487a | ||
|
|
75e012f2c9 | ||
|
|
5ecb1d1e2d | ||
|
|
f8c24c838a | ||
|
|
4ad9cfcdf4 | ||
|
|
a57253dd35 | ||
|
|
222e65bd45 | ||
|
|
3192ea3383 | ||
|
|
3a27f873cd | ||
|
|
e8fb279036 | ||
|
|
21ec7078d2 | ||
|
|
94937e9fa4 | ||
|
|
36401a20b8 | ||
|
|
8d1c4e4661 | ||
|
|
14362060c8 | ||
|
|
de47e94f62 | ||
|
|
c78ed78bf6 | ||
|
|
7674c95124 | ||
|
|
ec522c20b2 | ||
|
|
81f9578526 | ||
|
|
20bca751d4 | ||
|
|
49b5c49776 | ||
|
|
5d514fae61 | ||
|
|
9e17e93dd7 | ||
|
|
7f70c641dc | ||
|
|
7e3af4b7bc | ||
|
|
59ff4c0aef | ||
|
|
875e6b8e53 | ||
|
|
fafc44de7c | ||
|
|
e0d7eb10d5 | ||
|
|
ae01eb28a3 | ||
|
|
637c66c04f | ||
|
|
1c061e4428 | ||
|
|
cbe0626572 | ||
|
|
86dc98174a | ||
|
|
f0ac8ea977 | ||
|
|
35d4c2cdfc | ||
|
|
394f7c0d09 | ||
|
|
b83eee9bdc | ||
|
|
e41b2e8d31 | ||
|
|
d93cec2bfd | ||
|
|
9eb87a39cd | ||
|
|
07818ec1df | ||
|
|
d4052dbe37 | ||
|
|
656f5e0a7f | ||
|
|
0cc5e4dac3 | ||
|
|
8bbfff3cb2 | ||
|
|
93059798b0 | ||
|
|
33fae88cad | ||
|
|
4feb23e9ca | ||
|
|
c2b6b7ba72 | ||
|
|
fb76aee258 | ||
|
|
320b1829cc | ||
|
|
601f0255a4 | ||
|
|
b6cc2b227a | ||
|
|
cc7da4b948 | ||
|
|
1ae3be2fda | ||
|
|
5b4d35ea86 | ||
|
|
00735e6c8e | ||
|
|
66a42a38c9 | ||
|
|
05af1fba80 | ||
|
|
3b2dd0f5e9 | ||
|
|
1493a66a36 | ||
|
|
b4bf103fd8 | ||
|
|
94286becfd | ||
|
|
a68aade58c | ||
|
|
014201edf4 | ||
|
|
8ae6297148 | ||
|
|
5e0300aa8e | ||
|
|
80a7e18413 | ||
|
|
43be0a1a3f | ||
|
|
5eb32744c3 | ||
|
|
89d39f5f2b | ||
|
|
1f4900dd1e | ||
|
|
1c561c1483 | ||
|
|
6baaf60b67 | ||
|
|
dde73f6c2b | ||
|
|
f445a49b34 | ||
|
|
29016d1cf5 | ||
|
|
c06435046b | ||
|
|
134422802f | ||
|
|
5e00d6a305 | ||
|
|
b5f0137d8e | ||
|
|
081d041cbc | ||
|
|
95e733a67c | ||
|
|
a664057312 | ||
|
|
5b1a2d93bf | ||
|
|
0323f95828 | ||
|
|
375838449f | ||
|
|
4562de8c2c | ||
|
|
68d21669e7 | ||
|
|
625e0cf159 | ||
|
|
551ef00c61 | ||
|
|
10518f6364 | ||
|
|
1eafda36a9 | ||
|
|
871ecf0158 | ||
|
|
429000bdcb | ||
|
|
607940ed60 | ||
|
|
46f61a4311 | ||
|
|
3721df0502 | ||
|
|
11add681ec | ||
|
|
6e36eea6c8 | ||
|
|
6a82e7a24c | ||
|
|
5f625fa9f3 | ||
|
|
a95527674f | ||
|
|
f6149c67dc | ||
|
|
00c2a07e33 | ||
|
|
bc49b22c32 | ||
|
|
fe1b1c8bc3 | ||
|
|
a2825a06d6 | ||
|
|
3c33b5b169 | ||
|
|
934a867c83 | ||
|
|
a36337fba8 | ||
|
|
b3bd53bdd0 | ||
|
|
55518d4a8e | ||
|
|
2949c330d7 | ||
|
|
7ec056dbe0 | ||
|
|
983b60a8b9 | ||
|
|
000c31b66c | ||
|
|
2cd3d4bfb7 | ||
|
|
9d9065c801 | ||
|
|
10e7fe56ab | ||
|
|
c832e62389 | ||
|
|
328e488322 | ||
|
|
fd5a43cdb4 | ||
|
|
fd7780abb0 | ||
|
|
11c23e0b5e | ||
|
|
1170cfd4a7 | ||
|
|
2eddaa954e | ||
|
|
2c472fd03f | ||
|
|
16019a787e | ||
|
|
717d0a75f4 | ||
|
|
cc9487bae8 | ||
|
|
5ed9ec8f46 | ||
|
|
6ca37ca7f4 | ||
|
|
1007b8b15d | ||
|
|
be3a95fd54 | ||
|
|
109cb6cc3f | ||
|
|
fe1509dcfc | ||
|
|
1797e33989 | ||
|
|
6c504eeb3e | ||
|
|
d4b16fcdd1 | ||
|
|
9a08c079b5 | ||
|
|
98e75a7d6c | ||
|
|
675fd13391 | ||
|
|
e4f042aba4 | ||
|
|
19527845d1 | ||
|
|
3aa981649c | ||
|
|
330d48f70d | ||
|
|
90e14aae99 | ||
|
|
77e2ed7e01 | ||
|
|
f720f921c3 | ||
|
|
328740909b | ||
|
|
c8a981e8d6 | ||
|
|
faac45784c | ||
|
|
25667e84b7 | ||
|
|
72e0d77be5 | ||
|
|
9ac4b9ed4f | ||
|
|
b3b89ba05c | ||
|
|
00dc04df25 | ||
|
|
21d47c3fef | ||
|
|
66de94f050 | ||
|
|
1917adb322 | ||
|
|
3360b29354 | ||
|
|
646d281759 | ||
|
|
e268d92d52 | ||
|
|
fa81db2f03 | ||
|
|
830199ba3a | ||
|
|
ea8dc506d3 | ||
|
|
c95483ed94 | ||
|
|
d9cb4480b2 | ||
|
|
c69afd6074 | ||
|
|
f541790ab4 | ||
|
|
707ac4ec36 | ||
|
|
cd55133e96 | ||
|
|
37a91f8529 | ||
|
|
0ed0fa37a1 | ||
|
|
26728b79b2 | ||
|
|
f7c8723539 | ||
|
|
c313916771 | ||
|
|
f0b30c5e91 | ||
|
|
73e3196124 | ||
|
|
8a7bcc316e | ||
|
|
a2921cb982 | ||
|
|
29ce004974 | ||
|
|
026ba5ddce | ||
|
|
ab0585609a | ||
|
|
14a8bdb0c0 | ||
|
|
397d04dd88 | ||
|
|
fbb0bb7491 | ||
|
|
ee987dae99 | ||
|
|
720281a8db | ||
|
|
ff0c89e5a3 | ||
|
|
442937f540 | ||
|
|
dc01b787c1 | ||
|
|
c709b5d34c | ||
|
|
f8b386e42d | ||
|
|
f6b454cb9a | ||
|
|
a82f8ce3ad | ||
|
|
529d45b762 | ||
|
|
d31135bf21 | ||
|
|
75a77acefe | ||
|
|
715f42b6c3 | ||
|
|
199bfc8a37 | ||
|
|
e82ae762f0 | ||
|
|
f90cc9aff7 | ||
|
|
ec915177ad | ||
|
|
5adee3e217 | ||
|
|
278d82645e | ||
|
|
e66f13b471 | ||
|
|
0e2955b57e | ||
|
|
2a4d5ec1ec | ||
|
|
07ce63de69 | ||
|
|
b539862eef | ||
|
|
ac4be3f8a8 | ||
|
|
0dc2d07b58 | ||
|
|
b097f9dc21 | ||
|
|
8aa4a97480 | ||
|
|
3e54d0ceae | ||
|
|
e1cec3404a | ||
|
|
9a2b7fbda1 | ||
|
|
e0b4ebed93 | ||
|
|
e00e3999c5 | ||
|
|
2cbe96d815 | ||
|
|
ac3b289c9e | ||
|
|
62534e0e93 | ||
|
|
b802c3174c | ||
|
|
aee1a6e1f0 | ||
|
|
46f1cd38e0 | ||
|
|
7f891ef523 | ||
|
|
06af5e15cd | ||
|
|
af89a0a883 | ||
|
|
fa72eb2edb | ||
|
|
c1ea94423b | ||
|
|
1e70d066bd | ||
|
|
ab89f16e7c | ||
|
|
8c848a4cb5 | ||
|
|
e021a54d2d | ||
|
|
70523574b0 | ||
|
|
66e0f1ab19 | ||
|
|
671a05470e | ||
|
|
99eec0c41e | ||
|
|
48d06dcb06 | ||
|
|
e38df0c731 | ||
|
|
0f3f49915e | ||
|
|
416144265b | ||
|
|
2e7ced8e60 | ||
|
|
563463782c | ||
|
|
fe02871e91 | ||
|
|
bb05aeeaf7 | ||
|
|
846358ccb0 | ||
|
|
dabc779727 | ||
|
|
08fd2ec64b | ||
|
|
c42c06d048 | ||
|
|
e951d60800 | ||
|
|
0d8453f6a7 | ||
|
|
52b41b1bb0 | ||
|
|
c7265017b3 | ||
|
|
634e0b6d30 | ||
|
|
1c260f5890 | ||
|
|
177aedeaae | ||
|
|
3f6f3d2565 | ||
|
|
38489dbfeb | ||
|
|
ff2a3d6a28 | ||
|
|
2c74697fb1 | ||
|
|
110e9200b9 | ||
|
|
6e792f9735 | ||
|
|
51d8b13a81 | ||
|
|
a05691fd07 | ||
|
|
d25e7915d9 | ||
|
|
1a025dfef3 | ||
|
|
84cc4194aa | ||
|
|
331a56277c | ||
|
|
bc2915adb9 | ||
|
|
703fe9a71b | ||
|
|
6c50664046 | ||
|
|
673cbc97e1 | ||
|
|
3e83766099 | ||
|
|
b453589077 | ||
|
|
654fa18ab1 | ||
|
|
7b0d91534c | ||
|
|
8d4801bb6f | ||
|
|
9e36f4f625 | ||
|
|
d83a824812 | ||
|
|
ca128f2fa9 | ||
|
|
c02642d0f9 | ||
|
|
7340709111 | ||
|
|
56d0383372 | ||
|
|
49d33ea19a | ||
|
|
979fa0359a | ||
|
|
c7381d058a | ||
|
|
5db298f031 | ||
|
|
171d948a00 | ||
|
|
facd56cdf4 | ||
|
|
ba17ec4be4 | ||
|
|
6c163b5479 | ||
|
|
79649805b8 | ||
|
|
220ca8a570 | ||
|
|
348c00f86d | ||
|
|
12b641b522 | ||
|
|
6855bc1de6 | ||
|
|
a9c5b99ae8 | ||
|
|
76f36e2fd3 | ||
|
|
afe710321c | ||
|
|
0d444daaca | ||
|
|
c8a0c9fd23 | ||
|
|
79f07ae923 | ||
|
|
c77e7a568b | ||
|
|
ff9ec2057b | ||
|
|
6a313b99e4 | ||
|
|
8a92370042 | ||
|
|
411a59ec54 | ||
|
|
2d4022044d | ||
|
|
bbc5ebd76b | ||
|
|
e9c90fcde8 | ||
|
|
911f78867f | ||
|
|
5507cc5abc | ||
|
|
0e5aa401ef | ||
|
|
d48473ab87 | ||
|
|
734efaf40c | ||
|
|
f89718d88a | ||
|
|
aa4942a0da | ||
|
|
90bb162a88 | ||
|
|
0382a4b30e | ||
|
|
490d940cd1 | ||
|
|
b090eb9359 | ||
|
|
cb9e67ce84 | ||
|
|
c4d131dd23 | ||
|
|
2667f266bf | ||
|
|
8258936d6c | ||
|
|
e4e7449078 | ||
|
|
11b020e859 | ||
|
|
72d6b83ec5 | ||
|
|
fbe5152cb3 | ||
|
|
6bef01f755 | ||
|
|
f0d4b4a6d9 | ||
|
|
a54fe62643 | ||
|
|
eb4bbd49fb | ||
|
|
ab4f1bd4f4 | ||
|
|
f8b414c354 | ||
|
|
82a21624c3 | ||
|
|
1b212ac720 | ||
|
|
c36ce3ea14 | ||
|
|
841fab77a8 | ||
|
|
fd6f910824 | ||
|
|
ce9ca1917d | ||
|
|
c702fb273c | ||
|
|
4c72d8691b | ||
|
|
35ac5fc514 | ||
|
|
92991cc03c | ||
|
|
76492475e3 | ||
|
|
e2363d860c | ||
|
|
c06a90913a | ||
|
|
e88c0f7326 | ||
|
|
2a4280b5bf | ||
|
|
d3c733c57f | ||
|
|
f91add3cd4 | ||
|
|
90e3f7508a | ||
|
|
5e981da4df | ||
|
|
2ddd5d75fc | ||
|
|
88f509cbb6 | ||
|
|
ca161bc881 | ||
|
|
676ca44cdf | ||
|
|
5d0c91202f | ||
|
|
a166f049d5 | ||
|
|
0dc4692dfc | ||
|
|
996607d096 | ||
|
|
bd907ea008 | ||
|
|
b1573b1f3b | ||
|
|
246849ce94 | ||
|
|
2c2c68261a | ||
|
|
e38a0361bf | ||
|
|
cbf879bd32 | ||
|
|
808625e564 | ||
|
|
c3b4c2f7d4 | ||
|
|
14f521fdd7 | ||
|
|
087831df5a | ||
|
|
6b0542a5bf | ||
|
|
ac57856f00 | ||
|
|
2ab1587df0 | ||
|
|
632aa6e003 | ||
|
|
280697f2f7 | ||
|
|
0650be664d | ||
|
|
60c61f553a | ||
|
|
022c967781 |
14
.github/workflows/main-docker.yml
vendored
14
.github/workflows/main-docker.yml
vendored
@@ -271,16 +271,13 @@ jobs:
|
||||
REF_NAME=$(echo "${GITHUB_REF}" | sed 's/refs\/heads\///' | sed 's/refs\/tags\///')
|
||||
|
||||
# Create and push the manifest list with both the branch/tag name and the commit SHA
|
||||
# Note: Images are only pushed to GHCR during build, so we always reference GHCR sources
|
||||
# and copy to DockerHub using imagetools create
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
-t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME} \
|
||||
$(printf '${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
# Copy from GHCR to DockerHub (source digests only exist on GHCR)
|
||||
docker buildx imagetools create \
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME} \
|
||||
$(printf '${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||
$(printf '${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
# If the ref is a tag, also tag the image as stable as this is part of a 'release'
|
||||
# and only go in the `if` if there is NOT a `-` in the tag's name, due to tagging of `-alpha`, `-beta`, etc...
|
||||
@@ -290,10 +287,9 @@ jobs:
|
||||
-t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:stable \
|
||||
$(printf '${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
# Copy stable tag from GHCR to DockerHub
|
||||
docker buildx imagetools create \
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:stable \
|
||||
$(printf '${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||
$(printf '${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
# Small delay to ensure stable tag is fully propagated
|
||||
sleep 5
|
||||
@@ -305,7 +301,7 @@ jobs:
|
||||
|
||||
docker buildx imagetools create \
|
||||
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
|
||||
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:stable
|
||||
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:stable
|
||||
|
||||
fi
|
||||
|
||||
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -42,5 +42,8 @@
|
||||
},
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "*", "severity": "warn" }
|
||||
],
|
||||
"cSpell.words": [
|
||||
"Trilium"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
"keywords": [],
|
||||
"author": "Elian Doran <contact@eliandoran.me>",
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.28.2",
|
||||
"packageManager": "pnpm@10.29.3",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.15.0",
|
||||
"@redocly/cli": "2.18.1",
|
||||
"archiver": "7.0.1",
|
||||
"fs-extra": "11.3.3",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"typedoc": "0.28.16",
|
||||
"typedoc": "0.28.17",
|
||||
"typedoc-plugin-missing-exports": "4.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,10 @@
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/multimonth": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@maplibre/maplibre-gl-leaflet": "0.1.3",
|
||||
"@mermaid-js/layout-elk": "0.2.0",
|
||||
"@mind-elixir/node-menu": "5.0.1",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@preact/signals": "2.6.2",
|
||||
"@preact/signals": "2.8.0",
|
||||
"@triliumnext/ckeditor5": "workspace:*",
|
||||
"@triliumnext/codemirror": "workspace:*",
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
@@ -42,26 +41,25 @@
|
||||
"color": "5.0.3",
|
||||
"debounce": "3.0.0",
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.51.0",
|
||||
"globals": "17.2.0",
|
||||
"i18next": "25.8.0",
|
||||
"force-graph": "1.51.1",
|
||||
"globals": "17.3.0",
|
||||
"i18next": "25.8.7",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "4.0.0",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
"jsplumb": "2.15.6",
|
||||
"katex": "0.16.28",
|
||||
"knockout": "3.5.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-gpx": "2.2.0",
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "17.0.1",
|
||||
"maplibre-gl": "5.18.0",
|
||||
"marked": "17.0.2",
|
||||
"mermaid": "11.12.2",
|
||||
"mind-elixir": "5.6.1",
|
||||
"mind-elixir": "5.8.0",
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.28.2",
|
||||
"preact": "10.28.3",
|
||||
"react-i18next": "16.5.4",
|
||||
"react-window": "2.2.5",
|
||||
"react-window": "2.2.7",
|
||||
"reveal.js": "5.2.1",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
"tabulator-tables": "6.3.1",
|
||||
@@ -72,13 +70,11 @@
|
||||
"@prefresh/vite": "2.4.11",
|
||||
"@types/bootstrap": "5.2.10",
|
||||
"@types/jquery": "3.5.33",
|
||||
"@types/leaflet": "1.9.21",
|
||||
"@types/leaflet-gpx": "1.3.8",
|
||||
"@types/mark.js": "8.11.12",
|
||||
"@types/reveal.js": "5.2.2",
|
||||
"@types/tabulator-tables": "6.3.1",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"happy-dom": "20.4.0",
|
||||
"happy-dom": "20.6.1",
|
||||
"lightningcss": "1.31.1",
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "3.2.0"
|
||||
|
||||
@@ -46,10 +46,6 @@ if (utils.isElectron()) {
|
||||
electronContextMenu.setupContextMenu();
|
||||
}
|
||||
|
||||
if (utils.isPWA()) {
|
||||
initPWATopbarColor();
|
||||
}
|
||||
|
||||
function initOnElectron() {
|
||||
const electron: typeof Electron = utils.dynamicRequire("electron");
|
||||
electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName));
|
||||
@@ -134,20 +130,3 @@ function initDarkOrLightMode(style: CSSStyleDeclaration) {
|
||||
const { nativeTheme } = utils.dynamicRequire("@electron/remote") as typeof ElectronRemote;
|
||||
nativeTheme.themeSource = themeSource;
|
||||
}
|
||||
|
||||
function initPWATopbarColor() {
|
||||
const tracker = $("#background-color-tracker");
|
||||
|
||||
if (tracker.length) {
|
||||
const applyThemeColor = () => {
|
||||
let meta = $("meta[name='theme-color']");
|
||||
if (!meta.length) {
|
||||
meta = $(`<meta name="theme-color">`).appendTo($("head"));
|
||||
}
|
||||
meta.attr("content", tracker.css("color"));
|
||||
};
|
||||
|
||||
tracker.on("transitionend", applyThemeColor);
|
||||
applyThemeColor();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -700,6 +700,15 @@ export default class FNote {
|
||||
return this.hasAttribute(LABEL, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the note has a label with the given name (same as {@link hasOwnedLabel}), or it has a label with the `disabled:` prefix (for example due to a safe import).
|
||||
* @param name the name of the label to look for.
|
||||
* @returns `true` if the label exists, or its version with the `disabled:` prefix.
|
||||
*/
|
||||
hasLabelOrDisabled(name: string) {
|
||||
return this.hasLabel(name) || this.hasLabel(`disabled:${name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - label name
|
||||
* @returns true if label exists (including inherited) and does not have "false" value.
|
||||
|
||||
76
apps/client/src/layouts/mobile_layout.css
Normal file
76
apps/client/src/layouts/mobile_layout.css
Normal file
@@ -0,0 +1,76 @@
|
||||
#background-color-tracker {
|
||||
color: var(--main-background-color) !important;
|
||||
}
|
||||
|
||||
span.keyboard-shortcut,
|
||||
kbd {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.25em;
|
||||
padding-inline-start: 0.5em;
|
||||
padding-inline-end: 0.5em;
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
.quick-search {
|
||||
margin: 0;
|
||||
}
|
||||
.quick-search .dropdown-menu {
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
/* #region Tree */
|
||||
.tree-wrapper {
|
||||
max-height: 100%;
|
||||
margin-top: 0px;
|
||||
overflow-y: auto;
|
||||
contain: content;
|
||||
padding-inline-start: 10px;
|
||||
}
|
||||
|
||||
.fancytree-title {
|
||||
margin-inline-start: 0.6em !important;
|
||||
}
|
||||
|
||||
.fancytree-node {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-expander {
|
||||
width: 24px !important;
|
||||
margin-inline-end: 5px;
|
||||
}
|
||||
|
||||
.fancytree-loading span.fancytree-expander {
|
||||
width: 24px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.fancytree-loading span.fancytree-expander:after {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: 4px;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.tree-wrapper .collapse-tree-button,
|
||||
.tree-wrapper .scroll-to-active-note-button,
|
||||
.tree-wrapper .tree-settings-button {
|
||||
position: fixed;
|
||||
margin-inline-end: 16px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tree-wrapper .unhoist-button {
|
||||
font-size: 200%;
|
||||
}
|
||||
/* #endregion */
|
||||
@@ -1,128 +1,40 @@
|
||||
import "./mobile_layout.css";
|
||||
|
||||
import type AppContext from "../components/app_context.js";
|
||||
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.js";
|
||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||
import ContentHeader from "../widgets/containers/content_header.js";
|
||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||
import FindWidget from "../widgets/find.js";
|
||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
|
||||
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
|
||||
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
|
||||
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
||||
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
|
||||
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
|
||||
import NoteIconWidget from "../widgets/note_icon.jsx";
|
||||
import NoteTitleWidget from "../widgets/note_title.js";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import { useNoteContext } from "../widgets/react/hooks.jsx";
|
||||
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
||||
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
||||
import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx";
|
||||
import ScrollPadding from "../widgets/scroll_padding";
|
||||
import SearchResult from "../widgets/search_result.jsx";
|
||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||
import TabRowWidget from "../widgets/tab_row.js";
|
||||
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
|
||||
import { applyModals } from "./layout_commons.js";
|
||||
|
||||
const MOBILE_CSS = `
|
||||
<style>
|
||||
span.keyboard-shortcut,
|
||||
kbd {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.25em;
|
||||
padding-inline-start: 0.5em;
|
||||
padding-inline-end: 0.5em;
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
.quick-search {
|
||||
margin: 0;
|
||||
}
|
||||
.quick-search .dropdown-menu {
|
||||
max-width: 350px;
|
||||
}
|
||||
</style>`;
|
||||
|
||||
const FANCYTREE_CSS = `
|
||||
<style>
|
||||
.tree-wrapper {
|
||||
max-height: 100%;
|
||||
margin-top: 0px;
|
||||
overflow-y: auto;
|
||||
contain: content;
|
||||
padding-inline-start: 10px;
|
||||
}
|
||||
|
||||
.fancytree-custom-icon {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.fancytree-title {
|
||||
font-size: 1.5em;
|
||||
margin-inline-start: 0.6em !important;
|
||||
}
|
||||
|
||||
.fancytree-node {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.fancytree-node .fancytree-expander:before {
|
||||
font-size: 2em !important;
|
||||
}
|
||||
|
||||
span.fancytree-expander {
|
||||
width: 24px !important;
|
||||
margin-inline-end: 5px;
|
||||
}
|
||||
|
||||
.fancytree-loading span.fancytree-expander {
|
||||
width: 24px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.fancytree-loading span.fancytree-expander:after {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: 4px;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.tree-wrapper .collapse-tree-button,
|
||||
.tree-wrapper .scroll-to-active-note-button,
|
||||
.tree-wrapper .tree-settings-button {
|
||||
position: fixed;
|
||||
margin-inline-end: 16px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tree-wrapper .unhoist-button {
|
||||
font-size: 200%;
|
||||
}
|
||||
</style>`;
|
||||
|
||||
export default class MobileLayout {
|
||||
getRootWidget(appContext: typeof AppContext) {
|
||||
const rootContainer = new RootContainer(true)
|
||||
.setParent(appContext)
|
||||
.class("horizontal-layout")
|
||||
.cssBlock(MOBILE_CSS)
|
||||
.child(new FlexContainer("column").id("mobile-sidebar-container"))
|
||||
.child(
|
||||
new FlexContainer("row")
|
||||
@@ -136,7 +48,7 @@ export default class MobileLayout {
|
||||
.css("padding-inline-start", "0")
|
||||
.css("padding-inline-end", "0")
|
||||
.css("contain", "content")
|
||||
.child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget().cssBlock(FANCYTREE_CSS)))
|
||||
.child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget()))
|
||||
)
|
||||
.child(
|
||||
new ScreenContainer("detail", "row")
|
||||
@@ -147,30 +59,28 @@ export default class MobileLayout {
|
||||
new NoteWrapperWidget()
|
||||
.child(
|
||||
new FlexContainer("row")
|
||||
.class("title-row note-split-title")
|
||||
.contentSized()
|
||||
.css("font-size", "larger")
|
||||
.css("align-items", "center")
|
||||
.child(<ToggleSidebarButton />)
|
||||
.child(<NoteIconWidget />)
|
||||
.child(<NoteTitleWidget />)
|
||||
.child(<NoteBadges />)
|
||||
.child(<MobileDetailMenu />)
|
||||
)
|
||||
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
|
||||
.child(<PromotedAttributes />)
|
||||
.child(
|
||||
new ScrollingContainer()
|
||||
.filling()
|
||||
.contentSized()
|
||||
.child(new ContentHeader()
|
||||
.child(<ReadOnlyNoteInfoBar />)
|
||||
.child(<SharedInfoWidget />)
|
||||
)
|
||||
.child(<InlineTitle />)
|
||||
.child(<NoteTitleActions />)
|
||||
.child(<NoteDetail />)
|
||||
.child(<NoteList media="screen" />)
|
||||
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
|
||||
.child(<SearchResult />)
|
||||
.child(<FilePropertiesWrapper />)
|
||||
.child(<ScrollPadding />)
|
||||
)
|
||||
.child(<MobileEditorToolbar />)
|
||||
.child(new FindWidget())
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -191,13 +101,3 @@ export default class MobileLayout {
|
||||
return rootContainer;
|
||||
}
|
||||
}
|
||||
|
||||
function FilePropertiesWrapper() {
|
||||
const { note, ntxId } = useNoteContext();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{note?.type === "file" && <FilePropertiesTab note={note} ntxId={ntxId} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ class ContextMenu {
|
||||
if ("uiIcon" in item || "checked" in item) {
|
||||
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
|
||||
if (icon) {
|
||||
$icon.addClass(icon);
|
||||
$icon.addClass([icon, "tn-icon"]);
|
||||
} else {
|
||||
$icon.append(" ");
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import treeService from "../services/tree.js";
|
||||
import froca from "../services/froca.js";
|
||||
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
|
||||
import dialogService from "../services/dialog.js";
|
||||
import server from "../services/server.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import type { ContextMenuCommandData,FilteredCommandNames } from "../components/app_context.js";
|
||||
import type { SelectMenuItemEventListener } from "../components/events.js";
|
||||
import dialogService from "../services/dialog.js";
|
||||
import froca from "../services/froca.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import server from "../services/server.js";
|
||||
import treeService from "../services/tree.js";
|
||||
import type NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import type { FilteredCommandNames, ContextMenuCommandData } from "../components/app_context.js";
|
||||
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
|
||||
|
||||
type LauncherCommandNames = FilteredCommandNames<ContextMenuCommandData>;
|
||||
|
||||
@@ -32,8 +32,8 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener<
|
||||
const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null;
|
||||
const parentNoteId = this.node.getParent().data.noteId;
|
||||
|
||||
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers";
|
||||
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers";
|
||||
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers" || note?.noteId === "_lbMobileVisibleLaunchers";
|
||||
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers" || note?.noteId === "_lbMobileAvailableLaunchers";
|
||||
const isVisibleItem = parentNoteId === "_lbVisibleLaunchers" || parentNoteId === "_lbMobileVisibleLaunchers";
|
||||
const isAvailableItem = parentNoteId === "_lbAvailableLaunchers" || parentNoteId === "_lbMobileAvailableLaunchers";
|
||||
const isItem = isVisibleItem || isAvailableItem;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LeafletMouseEvent } from "leaflet";
|
||||
import type { GeoMouseEvent } from "../widgets/collections/geomap/map.js";
|
||||
|
||||
import appContext, { type CommandNames } from "../components/app_context.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
@@ -16,7 +16,7 @@ function openContextMenu(notePath: string, e: ContextMenuEvent, viewScope: ViewS
|
||||
});
|
||||
}
|
||||
|
||||
function getItems(e: ContextMenuEvent | LeafletMouseEvent): MenuItem<CommandNames>[] {
|
||||
function getItems(e: ContextMenuEvent | GeoMouseEvent): MenuItem<CommandNames>[] {
|
||||
const ntxId = getNtxId(e);
|
||||
const isMobileSplitOpen = isMobile() && appContext.tabManager.getNoteContextById(ntxId).getMainContext().getSubContexts().length > 1;
|
||||
|
||||
@@ -28,7 +28,7 @@ function getItems(e: ContextMenuEvent | LeafletMouseEvent): MenuItem<CommandName
|
||||
];
|
||||
}
|
||||
|
||||
function handleLinkContextMenuItem(command: string | undefined, e: ContextMenuEvent | LeafletMouseEvent, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
|
||||
function handleLinkContextMenuItem(command: string | undefined, e: ContextMenuEvent | GeoMouseEvent, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
|
||||
if (!hoistedNoteId) {
|
||||
hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId ?? null;
|
||||
}
|
||||
@@ -52,7 +52,7 @@ function handleLinkContextMenuItem(command: string | undefined, e: ContextMenuEv
|
||||
return false;
|
||||
}
|
||||
|
||||
function getNtxId(e: ContextMenuEvent | LeafletMouseEvent) {
|
||||
function getNtxId(e: ContextMenuEvent | GeoMouseEvent) {
|
||||
if (utils.isDesktop()) {
|
||||
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
|
||||
if (!subContexts) return null;
|
||||
|
||||
@@ -18,6 +18,10 @@ export type PrintReport = {
|
||||
} | {
|
||||
type: "collection";
|
||||
ignoredNoteIds: string[];
|
||||
} | {
|
||||
type: "error";
|
||||
message: string;
|
||||
stack?: string;
|
||||
};
|
||||
|
||||
async function main() {
|
||||
|
||||
@@ -168,6 +168,49 @@ function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefin
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles whether a dangerous attribute is enabled or not. When an attribute is disabled, its name is prefixed with `disabled:`.
|
||||
*
|
||||
* Note that this work for non-dangerous attributes as well.
|
||||
*
|
||||
* If there are multiple attributes with the same name, all of them will be toggled at the same time.
|
||||
*
|
||||
* @param note the note whose attribute to change.
|
||||
* @param type the type of dangerous attribute (label or relation).
|
||||
* @param name the name of the dangerous attribute.
|
||||
* @param willEnable whether to enable or disable the attribute.
|
||||
* @returns a promise that will resolve when the request to the server completes.
|
||||
*/
|
||||
async function toggleDangerousAttribute(note: FNote, type: "label" | "relation", name: string, willEnable: boolean) {
|
||||
const attrs = [
|
||||
...note.getOwnedAttributes(type, name),
|
||||
...note.getOwnedAttributes(type, `disabled:${name}`)
|
||||
];
|
||||
|
||||
for (const attr of attrs) {
|
||||
const baseName = getNameWithoutDangerousPrefix(attr.name);
|
||||
const newName = willEnable ? baseName : `disabled:${baseName}`;
|
||||
if (newName === attr.name) continue;
|
||||
|
||||
// We are adding and removing afterwards to avoid a flicker (because for a moment there would be no active content attribute anymore) because the operations are done in sequence and not atomically.
|
||||
if (attr.type === "label") {
|
||||
await setLabel(note.noteId, newName, attr.value);
|
||||
} else {
|
||||
await setRelation(note.noteId, newName, attr.value);
|
||||
}
|
||||
await removeAttributeById(note.noteId, attr.attributeId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of an attribute without the `disabled:` prefix, or the same name if it's not disabled.
|
||||
* @param name the name of an attribute.
|
||||
* @returns the name without the `disabled:` prefix.
|
||||
*/
|
||||
function getNameWithoutDangerousPrefix(name: string) {
|
||||
return name.startsWith("disabled:") ? name.substring(9) : name;
|
||||
}
|
||||
|
||||
export default {
|
||||
addLabel,
|
||||
setLabel,
|
||||
@@ -177,5 +220,7 @@ export default {
|
||||
removeAttributeById,
|
||||
removeOwnedLabelByName,
|
||||
removeOwnedRelationByName,
|
||||
isAffecting
|
||||
isAffecting,
|
||||
toggleDangerousAttribute,
|
||||
getNameWithoutDangerousPrefix
|
||||
};
|
||||
|
||||
9
apps/client/src/services/content_renderer.css
Normal file
9
apps/client/src/services/content_renderer.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.rendered-content.no-preview > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
font-size: 500%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import "./content_renderer.css";
|
||||
|
||||
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||
import WheelZoom from 'vanilla-js-wheel-zoom';
|
||||
|
||||
@@ -71,18 +73,9 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
|
||||
|
||||
$renderedContent.append($("<div>").append("<div>This note is protected and to access it you need to enter password.</div>").append("<br/>").append($button));
|
||||
} else if (entity instanceof FNote) {
|
||||
$renderedContent
|
||||
.css("display", "flex")
|
||||
.css("flex-direction", "column");
|
||||
$renderedContent.addClass("no-preview");
|
||||
$renderedContent.append(
|
||||
$("<div>")
|
||||
.css("display", "flex")
|
||||
.css("justify-content", "space-around")
|
||||
.css("align-items", "center")
|
||||
.css("height", "100%")
|
||||
.css("font-size", "500%")
|
||||
.css("flex-grow", "1")
|
||||
.append($("<span>").addClass(entity.getIcon()))
|
||||
$("<div>").append($("<span>").addClass(entity.getIcon()))
|
||||
);
|
||||
|
||||
if (entity.type === "webView" && entity.hasLabel("webViewSrc")) {
|
||||
@@ -292,10 +285,11 @@ function getRenderingType(entity: FNote | FAttachment) {
|
||||
}
|
||||
|
||||
const mime = "mime" in entity && entity.mime;
|
||||
const isIconPack = entity instanceof FNote && entity.hasLabel("iconPack");
|
||||
|
||||
if (type === "file" && mime === "application/pdf") {
|
||||
type = "pdf";
|
||||
} else if ((type === "file" || type === "viewConfig") && mime && CODE_MIME_TYPES.has(mime)) {
|
||||
} else if ((type === "file" || type === "viewConfig") && mime && CODE_MIME_TYPES.has(mime) && !isIconPack) {
|
||||
type = "code";
|
||||
} else if (type === "file" && mime && mime.startsWith("audio/")) {
|
||||
type = "audio";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from "./i18n";
|
||||
import options from "./options";
|
||||
import { isMobile } from "./utils";
|
||||
|
||||
export interface ExperimentalFeature {
|
||||
id: string;
|
||||
@@ -21,7 +22,7 @@ let enabledFeatures: Set<ExperimentalFeatureId> | null = null;
|
||||
|
||||
export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId): boolean {
|
||||
if (featureId === "new-layout") {
|
||||
return options.is("newLayout");
|
||||
return (isMobile() || options.is("newLayout"));
|
||||
}
|
||||
|
||||
return getEnabledFeatures().has(featureId);
|
||||
@@ -29,7 +30,7 @@ export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId):
|
||||
|
||||
export function getEnabledExperimentalFeatureIds() {
|
||||
const values = [ ...getEnabledFeatures().values() ];
|
||||
if (options.is("newLayout")) {
|
||||
if (isMobile() || options.is("newLayout")) {
|
||||
values.push("new-layout");
|
||||
}
|
||||
return values;
|
||||
|
||||
@@ -6,7 +6,7 @@ import bundleService, { type Bundle } from "./bundle.js";
|
||||
import froca from "./froca.js";
|
||||
import server from "./server.js";
|
||||
|
||||
async function render(note: FNote, $el: JQuery<HTMLElement>) {
|
||||
async function render(note: FNote, $el: JQuery<HTMLElement>, onError?: (e: unknown) => void) {
|
||||
const relations = note.getRelations("renderNote");
|
||||
const renderNoteIds = relations.map((rel) => rel.value).filter((noteId) => noteId);
|
||||
|
||||
@@ -21,12 +21,14 @@ async function render(note: FNote, $el: JQuery<HTMLElement>) {
|
||||
$scriptContainer.append(bundle.html);
|
||||
|
||||
// async so that scripts cannot block trilium execution
|
||||
bundleService.executeBundle(bundle, note, $scriptContainer).then(result => {
|
||||
// Render JSX
|
||||
if (bundle.html === "") {
|
||||
renderIfJsx(bundle, result, $el);
|
||||
}
|
||||
});
|
||||
bundleService.executeBundle(bundle, note, $scriptContainer)
|
||||
.catch(onError)
|
||||
.then(result => {
|
||||
// Render JSX
|
||||
if (bundle.html === "") {
|
||||
renderIfJsx(bundle, result, $el).catch(onError);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return renderNoteIds.length > 0;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
--bs-body-color: var(--main-text-color) !important;
|
||||
--bs-body-bg: var(--main-background-color) !important;
|
||||
--ck-mention-list-max-height: 500px;
|
||||
--tn-modal-max-height: 90vh;
|
||||
--tn-modal-max-height: 90svh;
|
||||
|
||||
--tree-item-light-theme-max-color-lightness: 50;
|
||||
--tree-item-dark-theme-min-color-lightness: 75;
|
||||
@@ -111,6 +111,7 @@ body.mobile #root-widget.virtual-keyboard-opened #mobile-bottom-bar {
|
||||
}
|
||||
|
||||
#mobile-bottom-bar {
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
padding-bottom: var(--mobile-bottom-offset);
|
||||
}
|
||||
|
||||
@@ -152,6 +153,11 @@ textarea,
|
||||
background: var(--input-background-color);
|
||||
}
|
||||
|
||||
.form-control:disabled {
|
||||
background-color: var(--input-background-color);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
color: var(--input-text-color);
|
||||
background: var(--input-background-color);
|
||||
@@ -409,6 +415,7 @@ body.desktop .tabulator-popup-container,
|
||||
|
||||
.dropdown-menu.static {
|
||||
box-shadow: unset;
|
||||
backdrop-filter: unset !important;
|
||||
}
|
||||
|
||||
.dropend .dropdown-toggle::after {
|
||||
@@ -454,7 +461,7 @@ body.desktop .tabulator-popup-container,
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
body.desktop .dropdown-menu:not(#context-menu-container) .dropdown-item,
|
||||
.dropdown-menu:not(#context-menu-container) .dropdown-item,
|
||||
body.desktop .dropdown-menu .dropdown-toggle,
|
||||
body #context-menu-container .dropdown-item > span,
|
||||
body.mobile .dropdown .dropdown-submenu > span {
|
||||
@@ -462,6 +469,15 @@ body.mobile .dropdown .dropdown-submenu > span {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
body.mobile .dropdown .dropdown-submenu {
|
||||
flex-wrap: wrap;
|
||||
|
||||
& > span {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item span.keyboard-shortcut,
|
||||
.dropdown-item *:not(.keyboard-shortcut) > kbd {
|
||||
flex-grow: 1;
|
||||
@@ -931,6 +947,7 @@ table.promoted-attributes-in-tooltip th {
|
||||
color: var(--muted-text-color);
|
||||
opacity: 0.6;
|
||||
line-height: 1;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion p {
|
||||
@@ -1325,15 +1342,12 @@ body.desktop .dropdown-submenu > .dropdown-menu {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.dropdown-submenu.dropstart > .dropdown-menu {
|
||||
.dropdown-submenu.dropstart > .dropdown-menu,
|
||||
body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
inset-inline-start: auto;
|
||||
inset-inline-end: calc(100% - 2px);
|
||||
}
|
||||
|
||||
body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
inset-inline-start: calc(-100% + 10px);
|
||||
}
|
||||
|
||||
.right-dropdown-widget {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -1530,7 +1544,8 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
|
||||
@media (max-width: 991px) {
|
||||
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,
|
||||
body.mobile .dropdown-menu.mobile-bottom-menu.show {
|
||||
--dropdown-bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size));
|
||||
position: fixed !important;
|
||||
bottom: var(--dropdown-bottom) !important;
|
||||
@@ -1542,6 +1557,16 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
max-height: calc(var(--tn-modal-max-height) - var(--dropdown-bottom));
|
||||
}
|
||||
|
||||
body.mobile #launcher-container .dropdown > .dropdown-menu.show {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
body.mobile .dropdown-menu.mobile-bottom-menu.show {
|
||||
--dropdown-bottom: 0px;
|
||||
padding-bottom: calc(max(var(--menu-padding-size), env(safe-area-inset-bottom))) !important;
|
||||
}
|
||||
|
||||
#mobile-sidebar-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -1667,47 +1692,15 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
background: var(--main-background-color);
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
margin: var(--bs-modal-margin);
|
||||
max-width: 80%;
|
||||
}
|
||||
body.mobile {
|
||||
.modal-dialog {
|
||||
margin: var(--bs-modal-margin);
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
body.mobile.force-fixed-tree #mobile-sidebar-wrapper {
|
||||
padding-top: 0;
|
||||
position: static;
|
||||
height: 40vh;
|
||||
width: 100vw;
|
||||
transform: none !important;
|
||||
background-color: var(--left-pane-background-color) !important;
|
||||
border-bottom: 0.5px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
body.mobile.force-fixed-tree #mobile-sidebar-container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.mobile.force-fixed-tree #mobile-sidebar-wrapper .quick-search {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.mobile.force-fixed-tree .component > button.bx-sidebar {
|
||||
visibility: hidden;
|
||||
padding: 0;
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
body.mobile.force-fixed-tree #mobile-rest-container {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
body.mobile.force-fixed-tree #detail-container {
|
||||
flex-grow: 1;
|
||||
.modal-content {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2623,14 +2616,14 @@ iframe.print-iframe {
|
||||
}
|
||||
}
|
||||
|
||||
#root-widget.virtual-keyboard-opened .note-split:not(:focus-within) {
|
||||
#root-widget.virtual-keyboard-opened .note-split:not(.active) {
|
||||
max-height: 80px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.desktop .title-row {
|
||||
.title-row {
|
||||
height: 50px;
|
||||
min-height: 50px;
|
||||
align-items: center;
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
--left-pane-collapsed-border-color: #0009;
|
||||
--left-pane-background-color: #1f1f1f;
|
||||
--left-pane-text-color: #aaaaaa;
|
||||
--left-pane-icon-color: #c5c5c5;
|
||||
--left-pane-item-hover-background: #ffffff0d;
|
||||
--left-pane-item-selected-background: #ffffff25;
|
||||
--left-pane-item-selected-color: #dfdfdf;
|
||||
@@ -209,6 +210,7 @@
|
||||
--badge-share-background-color: #4d4d4d;
|
||||
--badge-clipped-note-background-color: #295773;
|
||||
--badge-execute-background-color: #604180;
|
||||
--badge-active-content-background-color: rgb(12, 68, 70);
|
||||
|
||||
--note-icon-background-color: #444444;
|
||||
--note-icon-color: #d4d4d4;
|
||||
@@ -237,9 +239,9 @@
|
||||
|
||||
--bottom-panel-background-color: #11111180;
|
||||
--bottom-panel-title-bar-background-color: #3F3F3F80;
|
||||
|
||||
|
||||
--status-bar-border-color: var(--main-border-color);
|
||||
|
||||
|
||||
--scrollbar-thumb-color: #fdfdfd5c;
|
||||
--scrollbar-thumb-hover-color: #ffffff7d;
|
||||
--scrollbar-background-color: transparent;
|
||||
@@ -336,6 +338,17 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
|
||||
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 13.2%, 20.8%);
|
||||
}
|
||||
|
||||
.modal.tab-bar-modal .tabs .tab-card.with-hue {
|
||||
background-color: hsl(var(--bg-hue), 8.8%, 11.2%);
|
||||
border-color: hsl(var(--bg-hue), 9.4%, 25.1%);
|
||||
}
|
||||
|
||||
.modal.tab-bar-modal .tabs .tab-card.active.with-hue {
|
||||
background-color: hsl(var(--bg-hue), 8.8%, 16.2%);
|
||||
border-color: hsl(var(--bg-hue), 9.4%, 25.1%);
|
||||
}
|
||||
|
||||
|
||||
.use-note-color {
|
||||
--custom-color: var(--dark-theme-custom-color);
|
||||
}
|
||||
@@ -350,4 +363,4 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
|
||||
.note-split.with-hue *::selection,
|
||||
.quick-edit-dialog-wrapper.with-hue *::selection {
|
||||
--selection-background-color: hsl(var(--custom-color-hue), 49.2%, 35%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
--left-pane-collapsed-border-color: #0000000d;
|
||||
--left-pane-background-color: #f2f2f2;
|
||||
--left-pane-text-color: #383838;
|
||||
--left-pane-icon-color: currentColor;
|
||||
--left-pane-item-hover-background: rgba(0, 0, 0, 0.032);
|
||||
--left-pane-item-selected-background: white;
|
||||
--left-pane-item-selected-color: black;
|
||||
@@ -201,6 +202,7 @@
|
||||
--badge-share-background-color: #6b6b6b;
|
||||
--badge-clipped-note-background-color: #2284c0;
|
||||
--badge-execute-background-color: #7b47af;
|
||||
--badge-active-content-background-color: rgb(27, 164, 168);
|
||||
|
||||
--note-icon-background-color: #4f4f4f;
|
||||
--note-icon-color: white;
|
||||
@@ -311,6 +313,16 @@
|
||||
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 40%, 88%);
|
||||
}
|
||||
|
||||
.modal.tab-bar-modal .tabs .tab-card.with-hue {
|
||||
background-color: hsl(var(--bg-hue), 56%, 96%);
|
||||
border-color: hsl(var(--bg-hue), 33%, 41%);
|
||||
}
|
||||
|
||||
.modal.tab-bar-modal .tabs .tab-card.active.with-hue {
|
||||
background-color: hsl(var(--bg-hue), 86%, 96%);
|
||||
border-color: hsl(var(--bg-hue), 33%, 41%);
|
||||
}
|
||||
|
||||
.note-split.with-hue,
|
||||
.quick-edit-dialog-wrapper.with-hue {
|
||||
--note-icon-custom-background-color: hsl(var(--custom-color-hue), 44.5%, 43.1%);
|
||||
@@ -321,4 +333,4 @@
|
||||
.note-split.with-hue *::selection,
|
||||
.quick-edit-dialog-wrapper.with-hue *::selection {
|
||||
--selection-background-color: hsl(var(--custom-color-hue), 60%, 90%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -800,3 +800,18 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
|
||||
background: var(--hover-item-background-color);
|
||||
color: var(--hover-item-text-color);
|
||||
}
|
||||
|
||||
/*
|
||||
* Alert bars
|
||||
*/
|
||||
|
||||
div.alert {
|
||||
margin-bottom: 8px;
|
||||
background: var(--alert-bar-background) !important;
|
||||
border-radius: 8px;
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
div.alert p + p {
|
||||
margin-block: 1em 0;
|
||||
}
|
||||
@@ -84,6 +84,22 @@ button.btn.btn-success kbd {
|
||||
letter-spacing: 0.5pt;
|
||||
}
|
||||
|
||||
/*
|
||||
* Low profile buttons
|
||||
*/
|
||||
|
||||
button.tn-low-profile {
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
button.tn-low-profile:hover {
|
||||
background-color: var(--icon-button-hover-background);
|
||||
}
|
||||
|
||||
/*
|
||||
* Icon buttons
|
||||
*/
|
||||
@@ -794,3 +810,35 @@ input[type="range"] {
|
||||
scrollbar-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Centered forms
|
||||
*/
|
||||
|
||||
.tn-centered-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 20vh;
|
||||
}
|
||||
|
||||
.tn-centered-form .form-group {
|
||||
text-align: center;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.tn-centered-form .form-icon {
|
||||
font-size: 140px;
|
||||
color: var(--main-border-color);
|
||||
}
|
||||
|
||||
.tn-centered-form .protected-session-password {
|
||||
margin-inline: auto;
|
||||
max-width: 350px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tn-centered-form .input-group,
|
||||
.tn-centered-form button {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
@@ -47,9 +47,14 @@
|
||||
}
|
||||
|
||||
/* The toolbar show / hide button for the current text block */
|
||||
.ck.ck-block-toolbar-button {
|
||||
:root .ck.ck-block-toolbar-button {
|
||||
--ck-color-block-toolbar-button: var(--muted-text-color);
|
||||
--ck-color-button-on-background: transparent;
|
||||
--ck-color-button-on-color: currentColor;
|
||||
--ck-color-button-on-color: var(--ck-editor-toolbar-button-on-color);
|
||||
translate: -40% 0;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
z-index: 1600;
|
||||
}
|
||||
|
||||
:root .ck.ck-toolbar .ck-button:not(.ck-disabled):active,
|
||||
@@ -517,6 +522,10 @@ button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel).ck
|
||||
* EDITOR'S CONTENT
|
||||
*/
|
||||
|
||||
.note-detail-editable-text-editor > .ck-placeholder {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
/*
|
||||
* Code Blocks
|
||||
*/
|
||||
|
||||
@@ -57,12 +57,12 @@
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* SEARCH PAGE
|
||||
*/
|
||||
|
||||
/* Button bar */
|
||||
.search-definition-widget .search-setting-table tbody:last-child div {
|
||||
.search-definition-widget .search-setting-table .search-actions-container {
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -143,7 +143,7 @@
|
||||
/*
|
||||
* OPTIONS PAGES
|
||||
*/
|
||||
|
||||
|
||||
:root {
|
||||
--options-card-min-width: 500px;
|
||||
--options-card-max-width: 900px;
|
||||
@@ -156,6 +156,10 @@
|
||||
--preferred-max-content-width: var(--options-card-max-width);
|
||||
}
|
||||
|
||||
.note-split.options .collection-properties {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Create a gap at the top of the option pages */
|
||||
.note-detail-content-widget-content.options>*:first-child {
|
||||
margin-top: var(--options-first-item-top-margin, 1em);
|
||||
@@ -261,13 +265,6 @@ body.desktop .options-section:not(.tn-no-card) {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.options-section .alert {
|
||||
margin-bottom: 8px;
|
||||
background: var(--alert-bar-background) !important;
|
||||
border-radius: 8px;
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
nav.options-section-tabs {
|
||||
min-width: var(--options-card-min-width);
|
||||
max-width: var(--options-card-max-width);
|
||||
@@ -331,4 +328,4 @@ nav.options-section-tabs + .options-section {
|
||||
|
||||
.etapi-options-section div {
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,18 +739,12 @@ body[dir=rtl] #left-pane span.fancytree-node.protected > span.fancytree-custom-i
|
||||
transform: translateX(-25%);
|
||||
}
|
||||
|
||||
body.mobile .fancytree-expander::before,
|
||||
body.mobile .fancytree-title,
|
||||
body.mobile .fancytree-node > span {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
body.mobile #mobile-sidebar-container {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
body.mobile:not(.force-fixed-tree) #mobile-sidebar-wrapper {
|
||||
body.mobile #mobile-sidebar-wrapper {
|
||||
border-top-right-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
border-inline-end: 1px solid var(--subtle-border-color);
|
||||
@@ -769,9 +763,9 @@ body.mobile .fancytree-node > span {
|
||||
|
||||
#left-pane .fancytree-custom-icon {
|
||||
margin-top: 0; /* Use this to align the icon with the tree view item's caption */
|
||||
color: var(--custom-color, var(--left-pane-icon-color));
|
||||
}
|
||||
|
||||
|
||||
#left-pane span.fancytree-active .fancytree-title {
|
||||
font-weight: normal;
|
||||
}
|
||||
@@ -1272,7 +1266,7 @@ body.layout-horizontal #rest-pane > .classic-toolbar-widget {
|
||||
#center-pane .note-split {
|
||||
padding-top: 2px;
|
||||
background-color: var(--note-split-background-color, var(--main-background-color));
|
||||
transition: border-color 250ms ease-in;
|
||||
transition: border-color 150ms ease-out;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
@@ -1322,7 +1316,7 @@ body.mobile .note-title {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
body.desktop .title-row {
|
||||
/* Aligns the "Create new split" button with the note menu button (the three dots button) */
|
||||
padding-inline-end: 3px;
|
||||
}
|
||||
|
||||
@@ -206,6 +206,7 @@ span.fancytree-selected .fancytree-title {
|
||||
}
|
||||
|
||||
span.fancytree-selected .fancytree-custom-icon::before {
|
||||
font-family: "boxicons";
|
||||
content: "\eb43";
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 3px;
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
"widget-render-error": {
|
||||
"title": "فشل عرض عنصر واجهة مستخدم React مخصص"
|
||||
},
|
||||
"widget-missing-parent": "لا تحتوي الأداة المخصصة على خاصية إلزامية '{{property}}'.\n\nإذا كان من المفترض تشغيل هذا البرنامج النصي بدون عنصر واجهة مستخدم، فاستخدم '#run=frontendStartup' بدلاً من ذلك."
|
||||
"widget-missing-parent": "لا تحتوي الأداة المخصصة على خاصية إلزامية '{{property}}'.\n\nإذا كان من المفترض تشغيل هذا البرنامج النصي بدون عنصر واجهة مستخدم، فاستخدم '#run=frontendStartup' بدلاً من ذلك.",
|
||||
"open-script-note": "فتح ملاحظة برمجية",
|
||||
"scripting-error": "خطأ في النص البرمجي المخصص: {{title}}"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "أضافة رابط",
|
||||
@@ -37,14 +39,19 @@
|
||||
"search_note": "البحث عن الملاحظة بالاسم",
|
||||
"link_title": "عنوان الرابط",
|
||||
"button_add_link": "اضافة رابط",
|
||||
"help_on_links": "مساعدة حول الارتباطات التشعبية"
|
||||
"help_on_links": "مساعدة حول الارتباطات التشعبية",
|
||||
"link_title_mirrors": "عنوان الرابط يعكس العنوان الحالي للملاحظة",
|
||||
"link_title_arbitrary": "يمكن تغيير عنوان الرابط حسب الرغبة"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"edit_branch_prefix": "تعديل بادئة الفرع",
|
||||
"prefix": "البادئة: ",
|
||||
"save": "حفظ",
|
||||
"help_on_tree_prefix": "مساعدة حول بادئة الشجرة",
|
||||
"branch_prefix_saved": "تم حفظ بادئة الفرع."
|
||||
"branch_prefix_saved": "تم حفظ بادئة الفرع.",
|
||||
"edit_branch_prefix_multiple": "تعديل البادئة لـ {{count}} من تفرعات الملاحظات",
|
||||
"branch_prefix_saved_multiple": "تم حفظ بادئة التفرع لـ {{count}} من التفرعات.",
|
||||
"affected_branches": "الفروع المتأثرة ({{count}}):"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "اجراءات جماعية",
|
||||
@@ -1173,9 +1180,6 @@
|
||||
"note_not_found": "الملاحظة {{noteId}} غير موجودة!",
|
||||
"cannot_match_transform": "تعذر مطابقة التحويل: {{transform}}"
|
||||
},
|
||||
"web_view": {
|
||||
"web_view": "عرض الويب"
|
||||
},
|
||||
"consistency_checks": {
|
||||
"title": "فحوصات التناسق"
|
||||
},
|
||||
|
||||
@@ -662,7 +662,8 @@
|
||||
"show-cheatsheet": "显示快捷帮助",
|
||||
"toggle-zen-mode": "禅模式",
|
||||
"new-version-available": "新更新可用",
|
||||
"download-update": "取得版本 {{latestVersion}}"
|
||||
"download-update": "取得版本 {{latestVersion}}",
|
||||
"search_notes": "搜索笔记"
|
||||
},
|
||||
"zen_mode": {
|
||||
"button_exit": "退出禅模式"
|
||||
@@ -745,7 +746,7 @@
|
||||
"button_title": "导出SVG格式图片"
|
||||
},
|
||||
"relation_map_buttons": {
|
||||
"create_child_note_title": "创建新的子笔记并添加到关系图",
|
||||
"create_child_note_title": "创建子笔记并添加到图",
|
||||
"reset_pan_zoom_title": "重置平移和缩放到初始坐标和放大倍率",
|
||||
"zoom_in_title": "放大",
|
||||
"zoom_out_title": "缩小"
|
||||
@@ -759,7 +760,9 @@
|
||||
"delete_this_note": "删除此笔记",
|
||||
"error_cannot_get_branch_id": "无法获取 notePath '{{notePath}}' 的 branchId",
|
||||
"error_unrecognized_command": "无法识别的命令 {{command}}",
|
||||
"note_revisions": "笔记历史版本"
|
||||
"note_revisions": "笔记历史版本",
|
||||
"backlinks": "反链",
|
||||
"content_language_switcher": "内容语言: {{language}}"
|
||||
},
|
||||
"note_icon": {
|
||||
"change_note_icon": "更改笔记图标",
|
||||
@@ -910,7 +913,8 @@
|
||||
"unknown_search_option": "未知的搜索选项 {{searchOptionName}}",
|
||||
"search_note_saved": "搜索笔记已保存到 {{- notePathTitle}}",
|
||||
"actions_executed": "操作已执行。",
|
||||
"view_options": "查看选项:"
|
||||
"view_options": "查看选项:",
|
||||
"option": "选项"
|
||||
},
|
||||
"similar_notes": {
|
||||
"title": "相似笔记",
|
||||
@@ -1064,11 +1068,6 @@
|
||||
"note_detail_render_help_1": "之所以显示此帮助说明,是因为这个类型为渲染 HTML 的笔记没有正常工作所需的关系。",
|
||||
"note_detail_render_help_2": "渲染 HTML 笔记类型用于<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">编写脚本</a>。简而言之,您有一份 HTML 代码笔记(可包含一些 JavaScript),然后这个笔记会把页面渲染出来。要使其正常工作,您需要定义一个名为 \"renderNote\" 的<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">关系</a>指向要渲染的 HTML 笔记。"
|
||||
},
|
||||
"web_view": {
|
||||
"web_view": "网页视图",
|
||||
"embed_websites": "网页视图类型的笔记允许您将网站嵌入到 Trilium 中。",
|
||||
"create_label": "首先,请创建一个带有您要嵌入的 URL 地址的标签,例如 #webViewSrc=\"https://www.bing.com\""
|
||||
},
|
||||
"backend_log": {
|
||||
"refresh": "刷新"
|
||||
},
|
||||
@@ -1417,7 +1416,8 @@
|
||||
"description": "描述",
|
||||
"reload_app": "重载应用以应用更改",
|
||||
"set_all_to_default": "将所有快捷键重置为默认值",
|
||||
"confirm_reset": "您确定要将所有键盘快捷键重置为默认值吗?"
|
||||
"confirm_reset": "您确定要将所有键盘快捷键重置为默认值吗?",
|
||||
"no_results": "未找到与“{{filter}}”匹配的快捷方式"
|
||||
},
|
||||
"spellcheck": {
|
||||
"title": "拼写检查",
|
||||
@@ -1618,7 +1618,9 @@
|
||||
"print_report_title": "打印报告",
|
||||
"print_report_collection_content_other": "集合中的 {{count}} 篇笔记无法打印,因为它们不受支持或受到保护。",
|
||||
"print_report_collection_details_button": "查看详情",
|
||||
"print_report_collection_details_ignored_notes": "忽略的笔记"
|
||||
"print_report_collection_details_ignored_notes": "忽略的笔记",
|
||||
"print_report_error_title": "打印失败",
|
||||
"print_report_stack_trace": "堆栈跟踪"
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "请输入笔记标题...",
|
||||
@@ -1782,8 +1784,8 @@
|
||||
"desktop-application": "桌面应用程序",
|
||||
"native-title-bar": "原生标题栏",
|
||||
"native-title-bar-description": "对于 Windows 和 macOS,关闭原生标题栏可使应用程序看起来更紧凑。在 Linux 上,保留原生标题栏可以更好地与系统集成。",
|
||||
"background-effects": "启用背景效果(仅适用于 Windows 11)",
|
||||
"background-effects-description": "Mica 效果为应用窗口添加模糊且时尚的背景,营造出深度感和现代外观。「原生标题栏」必須被禁用。",
|
||||
"background-effects": "启用背景效果",
|
||||
"background-effects-description": "为应用窗口添加模糊且时尚的背景,营造出深度感和现代外观。「原生标题栏」必須被禁用。",
|
||||
"restart-app-button": "重启应用程序以查看更改",
|
||||
"zoom-factor": "缩放系数"
|
||||
},
|
||||
@@ -1802,7 +1804,8 @@
|
||||
"geo-map": {
|
||||
"create-child-note-title": "创建一个新的子笔记并将其添加到地图中",
|
||||
"create-child-note-instruction": "单击地图以在该位置创建新笔记,或按 Escape 以取消。",
|
||||
"unable-to-load-map": "无法加载地图。"
|
||||
"unable-to-load-map": "无法加载地图。",
|
||||
"create-child-note-text": "添加标记"
|
||||
},
|
||||
"geo-map-context": {
|
||||
"open-location": "打开位置",
|
||||
@@ -2117,7 +2120,7 @@
|
||||
},
|
||||
"call_to_action": {
|
||||
"background_effects_title": "背景效果现已推出稳定版本",
|
||||
"background_effects_message": "在 Windows 装置上,背景效果现在已完全稳定。背景效果通过模糊背后的背景,为使用者界面增添一抹色彩。此技术也用于其他应用程序,例如 Windows 资源管理器。",
|
||||
"background_effects_message": "在 Windows 和 macOS 设备上,背景效果现在已稳定。背景效果通过模糊背后的背景,为使用者界面增添一抹色彩。",
|
||||
"background_effects_button": "启用背景效果",
|
||||
"next_theme_title": "试用新 Trilium 主题",
|
||||
"next_theme_message": "当前使用旧版主题,要试用新主题吗?",
|
||||
@@ -2253,5 +2256,22 @@
|
||||
"pages_alt": "第{{pageNumber}}页",
|
||||
"pages_loading": "加载中...",
|
||||
"layers_other": "{{count}} 层"
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "在 {{platform}} 上可用"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"title_other": "{{count}} 选项卡",
|
||||
"more_options": "更多选项"
|
||||
},
|
||||
"bookmark_buttons": {
|
||||
"bookmarks": "书签"
|
||||
},
|
||||
"web_view_setup": {
|
||||
"title": "直接在 Trilium 中创建网页的实时视图",
|
||||
"url_placeholder": "输入或粘贴网站地址,例如 https://triliumnotes.org",
|
||||
"create_button": "创建网页视图",
|
||||
"invalid_url_title": "无效的地址",
|
||||
"invalid_url_message": "请输入有效的网址,例如 https://triliumnotes.org。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"about": {
|
||||
"title": "Über Trilium Notizen",
|
||||
"title": "Über Trilium Notes",
|
||||
"homepage": "Startseite:",
|
||||
"app_version": "App-Version:",
|
||||
"db_version": "DB-Version:",
|
||||
@@ -662,7 +662,8 @@
|
||||
"show-cheatsheet": "Cheatsheet anzeigen",
|
||||
"toggle-zen-mode": "Zen Modus",
|
||||
"new-version-available": "Neues Update verfügbar",
|
||||
"download-update": "Version {{latestVersion}} herunterladen"
|
||||
"download-update": "Version {{latestVersion}} herunterladen",
|
||||
"search_notes": "Notizen durchsuchen"
|
||||
},
|
||||
"sync_status": {
|
||||
"unknown": "<p>Der Synchronisations-Status wird bekannt, sobald der nächste Synchronisierungsversuch gestartet wird.</p><p>Klicke, um eine Synchronisierung jetzt auszulösen.</p>",
|
||||
@@ -742,7 +743,7 @@
|
||||
"button_title": "Diagramm als SVG exportieren"
|
||||
},
|
||||
"relation_map_buttons": {
|
||||
"create_child_note_title": "Erstelle eine neue untergeordnete Notiz und füge sie dieser Beziehungskarte hinzu",
|
||||
"create_child_note_title": "Erstelle eine untergeordnete Notiz und füge sie dieser Karte hinzu",
|
||||
"reset_pan_zoom_title": "Schwenken und Zoomen auf die ursprünglichen Koordinaten und Vergrößerung zurücksetzen",
|
||||
"zoom_in_title": "Hineinzoom",
|
||||
"zoom_out_title": "Herauszoomen"
|
||||
@@ -757,7 +758,9 @@
|
||||
"delete_this_note": "Diese Notiz löschen",
|
||||
"error_cannot_get_branch_id": "BranchId für notePath „{{notePath}}“ kann nicht abgerufen werden",
|
||||
"error_unrecognized_command": "Unbekannter Befehl {{command}}",
|
||||
"note_revisions": "Notiz Revisionen"
|
||||
"note_revisions": "Notiz Revisionen",
|
||||
"backlinks": "Rücklinks",
|
||||
"content_language_switcher": "Inhaltssprache: {{language}}"
|
||||
},
|
||||
"note_icon": {
|
||||
"change_note_icon": "Notiz-Icon ändern",
|
||||
@@ -909,7 +912,8 @@
|
||||
"unknown_search_option": "Unbekannte Suchoption {{searchOptionName}}",
|
||||
"search_note_saved": "Suchnotiz wurde in {{-notePathTitle}} gespeichert",
|
||||
"actions_executed": "Aktionen wurden ausgeführt.",
|
||||
"view_options": "Optionen anzeigen:"
|
||||
"view_options": "Optionen anzeigen:",
|
||||
"option": "Option"
|
||||
},
|
||||
"similar_notes": {
|
||||
"title": "Ähnliche Notizen",
|
||||
@@ -1063,11 +1067,6 @@
|
||||
"note_detail_render_help_1": "Diese Hilfesnotiz wird angezeigt, da diese Notiz vom Typ „HTML rendern“ nicht über die erforderliche Beziehung verfügt, um ordnungsgemäß zu funktionieren.",
|
||||
"note_detail_render_help_2": "Render-HTML-Notiztyp wird benutzt für <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. Kurzgesagt, du hast ein HTML-Code-Notiz (optional mit JavaScript) und diese Notiz rendert es. Damit es funktioniert, musst du eine a <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">Beziehung</a> namens \"renderNote\" zeigend auf die HTML-Notiz zum rendern definieren."
|
||||
},
|
||||
"web_view": {
|
||||
"web_view": "Webansicht",
|
||||
"embed_websites": "Notiz vom Typ Web View ermöglicht das Einbetten von Websites in Trilium.",
|
||||
"create_label": "Um zu beginnen, erstelle bitte ein Label mit einer URL-Adresse, die eingebettet werden soll, z. B. #webViewSrc=\"https://www.google.com\""
|
||||
},
|
||||
"backend_log": {
|
||||
"refresh": "Aktualisieren"
|
||||
},
|
||||
@@ -1383,7 +1382,8 @@
|
||||
"description": "Beschreibung",
|
||||
"reload_app": "Lade die App neu, um die Änderungen zu übernehmen",
|
||||
"set_all_to_default": "Setze alle Verknüpfungen auf die Standardeinstellungen",
|
||||
"confirm_reset": "Möchtest du wirklich alle Tastaturkürzel auf die Standardeinstellungen zurücksetzen?"
|
||||
"confirm_reset": "Möchtest du wirklich alle Tastaturkürzel auf die Standardeinstellungen zurücksetzen?",
|
||||
"no_results": "Keine Tastenkürzel für '{{filter}}' gefunden"
|
||||
},
|
||||
"spellcheck": {
|
||||
"title": "Rechtschreibprüfung",
|
||||
@@ -1587,7 +1587,9 @@
|
||||
"print_report_collection_details_button": "Details anzeigen",
|
||||
"print_report_collection_details_ignored_notes": "Ignorierte Notizen",
|
||||
"print_report_collection_content_one": "{{count}} Notiz in der Sammlung konnte nicht gedruckt werden, weil sie nicht unterstützt oder geschützt ist.",
|
||||
"print_report_collection_content_other": "{{count}} Notizen in der Sammlung konnten nicht gedruckt werden, weil sie nicht unterstützt oder geschützt sind."
|
||||
"print_report_collection_content_other": "{{count}} Notizen in der Sammlung konnten nicht gedruckt werden, weil sie nicht unterstützt oder geschützt sind.",
|
||||
"print_report_error_title": "Druck fehlgeschlagen",
|
||||
"print_report_stack_trace": "Stapelzurückverfolgung"
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "Titel der Notiz hier eingeben…",
|
||||
@@ -1771,7 +1773,8 @@
|
||||
"geo-map": {
|
||||
"create-child-note-title": "Neue Unternotiz anlegen und zur Karte hinzufügen",
|
||||
"create-child-note-instruction": "Auf die Karte klicken, um eine neue Notiz an der Stelle zu erstellen oder Escape drücken um abzubrechen.",
|
||||
"unable-to-load-map": "Karte konnte nicht geladen werden."
|
||||
"unable-to-load-map": "Karte konnte nicht geladen werden.",
|
||||
"create-child-note-text": "Marker hinzufügen"
|
||||
},
|
||||
"geo-map-context": {
|
||||
"open-location": "Ort öffnen",
|
||||
@@ -2270,5 +2273,20 @@
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "Verfügbar auf {{platform}}"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"title_one": "{{count}} Tab",
|
||||
"title_other": "{{count}} Tabs",
|
||||
"more_options": "Weitere Optionen"
|
||||
},
|
||||
"bookmark_buttons": {
|
||||
"bookmarks": "Lesezeichen"
|
||||
},
|
||||
"web_view_setup": {
|
||||
"title": "Erstelle eine Live-Ansicht einer Webseite direkt in Trilium",
|
||||
"url_placeholder": "Gib oder füge die Adresse der Webseite ein, zum Beispiel https://triliumnotes.org",
|
||||
"create_button": "Erstelle Web Ansicht",
|
||||
"invalid_url_title": "Ungültige Adresse",
|
||||
"invalid_url_message": "Füge eine valide Webadresse ein, zum Beispiel https://triliumnotes.org."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,61 @@
|
||||
"critical-error": {
|
||||
"title": "Κρίσιμο σφάλμα",
|
||||
"message": "Συνέβη κάποιο κρίσιμο σφάλμα, το οποίο δεν επιτρέπει στην εφαρμογή χρήστη να ξεκινήσει:\n\n{{message}}\n\nΤο πιθανότερο είναι να προκλήθηκε από κάποιο script που απέτυχε απρόοπτα. Δοκιμάστε να ξεκινήσετε την εφαρμογή σε ασφαλή λειτουργία για να λύσετε το πρόβλημα."
|
||||
}
|
||||
},
|
||||
"widget-error": {
|
||||
"title": "Δεν ήταν δυνατή η αρχικοποίηση του widget",
|
||||
"message-custom": "Προσαρμοσμένο widget της σημείωσης με ID \"{{id}}\", με τίτλο \"{{title}}\", δεν ήταν δυνατό να αρχικοποιηθεί λόγω:\n\n{{message}}",
|
||||
"message-unknown": "Άγνωστο widget δεν ήταν δυνατό να αρχικοποιηθεί λόγω:\n\n{{message}}"
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Δεν ήταν δυνατή η φόρτωση προσαρμοσμένου script",
|
||||
"message": "Το script δεν ήταν δυνατό να εκτελεστεί λόγω:\n\n{{message}}"
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "Δεν ήταν δυνατή η λήψη της λίστας των widgets από τον server"
|
||||
},
|
||||
"widget-render-error": {
|
||||
"title": "Δεν ήταν δυνατή η απόδοση προσαρμοσμένου React widget"
|
||||
},
|
||||
"widget-missing-parent": "Το προσαρμοσμένο widget δεν έχει ορισμένη την υποχρεωτική ιδιότητα '{{property}}'.\n\nΕάν το script προορίζεται για εκτέλεση χωρίς UI element, χρησιμοποιήστε '#run=frontendStartup' αντί για αυτό.",
|
||||
"open-script-note": "Άνοιγμα σημείωσης script",
|
||||
"scripting-error": "Σφάλμα προσαρμοσμένου script: {{title}}"
|
||||
},
|
||||
"bookmark_buttons": {
|
||||
"bookmarks": "Σελιδοδείκτες"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Προσθήκη συνδέσμου",
|
||||
"help_on_links": "Βοήθεια για συνδέσμους",
|
||||
"note": "Σημείωση",
|
||||
"search_note": "Αναζήτηση σημείωσης με βάση το όνομά της",
|
||||
"link_title_mirrors": "Ο τίτλος του συνδέσμου αντικατοπτρίζει τον τρέχοντα τίτλο της σημείωσης",
|
||||
"link_title_arbitrary": "Ο τίτλος του συνδέσμου μπορεί να τροποποιηθεί ελεύθερα",
|
||||
"link_title": "Τίτλος συνδέσμου",
|
||||
"button_add_link": "Προσθήκη συνδέσμου"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"edit_branch_prefix": "Επεξεργασία προθέματος κλάδου",
|
||||
"edit_branch_prefix_multiple": "Επεξεργασία προθέματος κλάδου για {{count}} κλάδους",
|
||||
"help_on_tree_prefix": "Βοήθεια για πρόθεμα δέντρου",
|
||||
"prefix": "Πρόθεμα: ",
|
||||
"save": "Αποθήκευση",
|
||||
"branch_prefix_saved": "Το πρόθεμα κλάδου αποθηκεύτηκε.",
|
||||
"branch_prefix_saved_multiple": "Το πρόθεμα κλάδου αποθηκεύτηκε για {{count}} κλάδους.",
|
||||
"affected_branches": "Επηρεαζόμενοι κλάδοι ({{count}}):"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "Μαζικές ενέργειες",
|
||||
"affected_notes": "Επηρεαζόμενες σημειώσεις",
|
||||
"include_descendants": "Συμπερίληψη απογόνων των επιλεγμένων σημειώσεων",
|
||||
"available_actions": "Διαθέσιμες ενέργειες",
|
||||
"chosen_actions": "Επιλεγμένες ενέργειες",
|
||||
"execute_bulk_actions": "Εκτέλεση μαζικών ενεργειών",
|
||||
"bulk_actions_executed": "Οι μαζικές ενέργειες εκτελέστηκαν επιτυχώς.",
|
||||
"none_yet": "Καμία ακόμη… προσθέστε μια ενέργεια επιλέγοντας μία από τις διαθέσιμες παραπάνω.",
|
||||
"labels": "Ετικέτες",
|
||||
"relations": "Συσχετίσεις",
|
||||
"notes": "Σημειώσεις",
|
||||
"other": "Λοιπά"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,7 +662,8 @@
|
||||
"show-cheatsheet": "Show Cheatsheet",
|
||||
"toggle-zen-mode": "Zen Mode",
|
||||
"new-version-available": "New Update Available",
|
||||
"download-update": "Get Version {{latestVersion}}"
|
||||
"download-update": "Get Version {{latestVersion}}",
|
||||
"search_notes": "Search notes"
|
||||
},
|
||||
"zen_mode": {
|
||||
"button_exit": "Exit Zen Mode"
|
||||
@@ -745,7 +746,7 @@
|
||||
"button_title": "Export diagram as SVG"
|
||||
},
|
||||
"relation_map_buttons": {
|
||||
"create_child_note_title": "Create new child note and add it into this relation map",
|
||||
"create_child_note_title": "Create child note and add it to map",
|
||||
"reset_pan_zoom_title": "Reset pan & zoom to initial coordinates and magnification",
|
||||
"zoom_in_title": "Zoom In",
|
||||
"zoom_out_title": "Zoom Out"
|
||||
@@ -760,7 +761,9 @@
|
||||
"delete_this_note": "Delete this note",
|
||||
"note_revisions": "Note revisions",
|
||||
"error_cannot_get_branch_id": "Cannot get branchId for notePath '{{notePath}}'",
|
||||
"error_unrecognized_command": "Unrecognized command {{command}}"
|
||||
"error_unrecognized_command": "Unrecognized command {{command}}",
|
||||
"backlinks": "Backlinks",
|
||||
"content_language_switcher": "Content language: {{language}}"
|
||||
},
|
||||
"note_icon": {
|
||||
"change_note_icon": "Change note icon",
|
||||
@@ -905,6 +908,7 @@
|
||||
"debug": "debug",
|
||||
"debug_description": "Debug will print extra debugging information into the console to aid in debugging complex queries",
|
||||
"action": "action",
|
||||
"option": "option",
|
||||
"search_button": "Search",
|
||||
"search_execute": "Search & Execute actions",
|
||||
"save_to_note": "Save to note",
|
||||
@@ -1006,7 +1010,7 @@
|
||||
"no_attachments": "This note has no attachments."
|
||||
},
|
||||
"book": {
|
||||
"no_children_help": "This collection doesn't have any child notes so there's nothing to display. See <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> for details.",
|
||||
"no_children_help": "This collection doesn't have any child notes so there's nothing to display.",
|
||||
"drag_locked_title": "Locked for editing",
|
||||
"drag_locked_message": "Dragging not allowed since the collection is locked for editing."
|
||||
},
|
||||
@@ -1063,13 +1067,21 @@
|
||||
"click_on_canvas_to_place_new_note": "Click on canvas to place new note"
|
||||
},
|
||||
"render": {
|
||||
"note_detail_render_help_1": "This help note is shown because this note of type Render HTML doesn't have required relation to function properly.",
|
||||
"note_detail_render_help_2": "Render HTML note type is used for <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. In short, you have a HTML code note (optionally with some JavaScript) and this note will render it. To make it work, you need to define a <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relation</a> called \"renderNote\" pointing to the HTML note to render."
|
||||
"setup_title": "Display custom HTML or Preact JSX inside this note",
|
||||
"setup_create_sample_preact": "Create sample note with Preact",
|
||||
"setup_create_sample_html": "Create sample note with HTML",
|
||||
"setup_sample_created": "A sample note was created as a child note.",
|
||||
"disabled_description": "This render notes comes from an external source. To protect you from malicious content, it is not enabled by default. Make sure you trust the source before enabling it.",
|
||||
"disabled_button_enable": "Enable render note"
|
||||
},
|
||||
"web_view": {
|
||||
"web_view": "Web View",
|
||||
"embed_websites": "Note of type Web View allows you to embed websites into Trilium.",
|
||||
"create_label": "To start, please create a label with a URL address you want to embed, e.g. #webViewSrc=\"https://www.google.com\""
|
||||
"web_view_setup": {
|
||||
"title": "Create a live view of a webpage directly into Trilium",
|
||||
"url_placeholder": "Enter or paste the website address, for example https://triliumnotes.org",
|
||||
"create_button": "Create Web View",
|
||||
"invalid_url_title": "Invalid address",
|
||||
"invalid_url_message": "Insert a valid web address, for example https://triliumnotes.org.",
|
||||
"disabled_description": "This web view was imported from an external source. To help protect you from phishing or malicious content, it isn’t loading automatically. You can enable it if you trust the source.",
|
||||
"disabled_button_enable": "Enable web view"
|
||||
},
|
||||
"backend_log": {
|
||||
"refresh": "Refresh"
|
||||
@@ -1585,7 +1597,8 @@
|
||||
"description": "Description",
|
||||
"reload_app": "Reload app to apply changes",
|
||||
"set_all_to_default": "Set all shortcuts to the default",
|
||||
"confirm_reset": "Do you really want to reset all keyboard shortcuts to the default?"
|
||||
"confirm_reset": "Do you really want to reset all keyboard shortcuts to the default?",
|
||||
"no_results": "No shortcuts found matching '{{filter}}'"
|
||||
},
|
||||
"spellcheck": {
|
||||
"title": "Spell Check",
|
||||
@@ -1791,6 +1804,8 @@
|
||||
"printing": "Printing in progress...",
|
||||
"printing_pdf": "Exporting to PDF in progress...",
|
||||
"print_report_title": "Print report",
|
||||
"print_report_error_title": "Failed to print",
|
||||
"print_report_stack_trace": "Stack trace",
|
||||
"print_report_collection_content_one": "{{count}} note in the collection could not be printed because they are not supported or they are protected.",
|
||||
"print_report_collection_content_other": "{{count}} notes in the collection could not be printed because they are not supported or they are protected.",
|
||||
"print_report_collection_details_button": "See details",
|
||||
@@ -2095,7 +2110,8 @@
|
||||
"raster": "Raster",
|
||||
"vector_light": "Vector (Light)",
|
||||
"vector_dark": "Vector (Dark)",
|
||||
"show-scale": "Show scale"
|
||||
"show-scale": "Show scale",
|
||||
"show-labels": "Show marker names"
|
||||
},
|
||||
"table_context_menu": {
|
||||
"delete_row": "Delete row"
|
||||
@@ -2276,5 +2292,35 @@
|
||||
"title_one": "{{count}} tab",
|
||||
"title_other": "{{count}} tabs",
|
||||
"more_options": "More options"
|
||||
},
|
||||
"bookmark_buttons": {
|
||||
"bookmarks": "Bookmarks"
|
||||
},
|
||||
"active_content_badges": {
|
||||
"type_icon_pack": "Icon pack",
|
||||
"type_backend_script": "Backend script",
|
||||
"type_frontend_script": "Frontend script",
|
||||
"type_widget": "Widget",
|
||||
"type_app_css": "Custom CSS",
|
||||
"type_render_note": "Render note",
|
||||
"type_web_view": "Web view",
|
||||
"type_app_theme": "Custom theme",
|
||||
"toggle_tooltip_enable_tooltip": "Click to enable this {{type}}.",
|
||||
"toggle_tooltip_disable_tooltip": "Click to disable this {{type}}.",
|
||||
"menu_docs": "Open documentation",
|
||||
"menu_execute_now": "Execute script now",
|
||||
"menu_run": "Run automatically",
|
||||
"menu_run_disabled": "Manually",
|
||||
"menu_run_backend_startup": "When the backend starts up",
|
||||
"menu_run_hourly": "Hourly",
|
||||
"menu_run_daily": "Daily",
|
||||
"menu_run_frontend_startup": "When the desktop frontend starts up",
|
||||
"menu_run_mobile_startup": "When the mobile frontend starts up",
|
||||
"menu_change_to_widget": "Change to widget",
|
||||
"menu_change_to_frontend_script": "Change to frontend script",
|
||||
"menu_theme_base": "Theme base"
|
||||
},
|
||||
"setup_form": {
|
||||
"more_info": "Learn more"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,7 +662,8 @@
|
||||
"show-cheatsheet": "Mostrar hoja de trucos",
|
||||
"toggle-zen-mode": "Modo Zen",
|
||||
"new-version-available": "Nueva actualización disponible",
|
||||
"download-update": "Obtener versión {{latestVersion}}"
|
||||
"download-update": "Obtener versión {{latestVersion}}",
|
||||
"search_notes": "Buscar notas"
|
||||
},
|
||||
"zen_mode": {
|
||||
"button_exit": "Salir del modo Zen"
|
||||
@@ -745,7 +746,7 @@
|
||||
"button_title": "Exportar diagrama como SVG"
|
||||
},
|
||||
"relation_map_buttons": {
|
||||
"create_child_note_title": "Crear una nueva subnota y agregarla a este mapa de relaciones",
|
||||
"create_child_note_title": "Crear una subnota y agregarla al mapa",
|
||||
"reset_pan_zoom_title": "Restablecer la panorámica y el zoom a las coordenadas y ampliación iniciales",
|
||||
"zoom_in_title": "Acercar",
|
||||
"zoom_out_title": "Alejar"
|
||||
@@ -754,14 +755,16 @@
|
||||
"relation": "relación",
|
||||
"backlink_one": "{{count}} Vínculo de retroceso",
|
||||
"backlink_many": "{{count}} Vínculos de retroceso",
|
||||
"backlink_other": "{{count}} vínculos de retroceso"
|
||||
"backlink_other": "{{count}} Vínculos de retroceso"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"insert_child_note": "Insertar subnota",
|
||||
"delete_this_note": "Eliminar esta nota",
|
||||
"error_cannot_get_branch_id": "No se puede obtener el branchID del notePath '{{notePath}}'",
|
||||
"error_unrecognized_command": "Comando no reconocido {{command}}",
|
||||
"note_revisions": "Revisiones de notas"
|
||||
"note_revisions": "Revisiones de notas",
|
||||
"backlinks": "Vínculos de retroceso",
|
||||
"content_language_switcher": "Idioma de contenido: {{language}}"
|
||||
},
|
||||
"note_icon": {
|
||||
"change_note_icon": "Cambiar icono de nota",
|
||||
@@ -914,7 +917,8 @@
|
||||
"unknown_search_option": "Opción de búsqueda desconocida {{searchOptionName}}",
|
||||
"search_note_saved": "La nota de búsqueda se ha guardado en {{- notePathTitle}}",
|
||||
"actions_executed": "Las acciones han sido ejecutadas.",
|
||||
"view_options": "Ver opciones:"
|
||||
"view_options": "Ver opciones:",
|
||||
"option": "opción"
|
||||
},
|
||||
"similar_notes": {
|
||||
"title": "Notas similares",
|
||||
@@ -1068,11 +1072,6 @@
|
||||
"note_detail_render_help_1": "Esta nota de ayuda se muestra porque esta nota de tipo Renderizar HTML no tiene la relación requerida para funcionar correctamente.",
|
||||
"note_detail_render_help_2": "El tipo de nota Render HTML es usado para <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. De forma resumida, tiene una nota con código HTML (opcionalmente con algo de JavaScript) y esta nota la renderizará. Para que funcione, es necesario definir una <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relación</a> llamada \"renderNote\" apuntando a la nota HTML nota a renderizar."
|
||||
},
|
||||
"web_view": {
|
||||
"web_view": "Vista web",
|
||||
"embed_websites": "La nota de tipo Web View le permite insertar sitios web en Trilium.",
|
||||
"create_label": "Para comenzar, por favor cree una etiqueta con una dirección URL que desee empotrar, e.g. #webViewSrc=\"https://www.google.com\""
|
||||
},
|
||||
"backend_log": {
|
||||
"refresh": "Refrescar"
|
||||
},
|
||||
@@ -1573,7 +1572,8 @@
|
||||
"description": "Descripción",
|
||||
"reload_app": "Vuelva a cargar la aplicación para aplicar los cambios",
|
||||
"set_all_to_default": "Establecer todos los accesos directos al valor predeterminado",
|
||||
"confirm_reset": "¿Realmente desea restablecer todos los atajos de teclado a sus valores predeterminados?"
|
||||
"confirm_reset": "¿Realmente desea restablecer todos los atajos de teclado a sus valores predeterminados?",
|
||||
"no_results": "No se encontraron atajos que coincidan con '{{filter}} '"
|
||||
},
|
||||
"spellcheck": {
|
||||
"title": "Revisión ortográfica",
|
||||
@@ -1780,7 +1780,9 @@
|
||||
"print_report_collection_content_other": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas.",
|
||||
"print_report_title": "Imprimir informe",
|
||||
"print_report_collection_details_button": "Ver detalles",
|
||||
"print_report_collection_details_ignored_notes": "Notas ignoradas"
|
||||
"print_report_collection_details_ignored_notes": "Notas ignoradas",
|
||||
"print_report_stack_trace": "Rastreo de pila",
|
||||
"print_report_error_title": "Fallo al imprimir"
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "escriba el título de la nota aquí...",
|
||||
@@ -2285,5 +2287,21 @@
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "Disponible en {{platform}}"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"title_one": "{{count}} pestaña",
|
||||
"title_many": "{{count}} pestañas",
|
||||
"title_other": "{{count}} pestañas",
|
||||
"more_options": "Más opciones"
|
||||
},
|
||||
"bookmark_buttons": {
|
||||
"bookmarks": "Marcadores"
|
||||
},
|
||||
"web_view_setup": {
|
||||
"title": "Crear una vista en vivo de una página web directamente en Trilium",
|
||||
"url_placeholder": "Ingresar o pegar la dirección del sitio web, por ejemplo https://triliumnotes.org",
|
||||
"create_button": "Crear Vista Web",
|
||||
"invalid_url_title": "Dirección inválida",
|
||||
"invalid_url_message": "Ingrese una dirección web válida, por ejemplo https://triliumnotes.org."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1057,11 +1057,6 @@
|
||||
"note_detail_render_help_1": "Cette note d'aide s'affiche car cette note de type Rendu HTML n'a pas la relation requise pour fonctionner correctement.",
|
||||
"note_detail_render_help_2": "Le type de note Rendu HTML est utilisé pour les <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripts</a>. En résumé, vous disposez d'une note de code HTML (éventuellement contenant JavaScript) et cette note affichera le rendu. Pour que cela fonctionne, vous devez définir une <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relation</a> appelée \"renderNote\" pointant vers la note HTML à rendre."
|
||||
},
|
||||
"web_view": {
|
||||
"web_view": "Affichage Web",
|
||||
"embed_websites": "Les notes de type Affichage Web vous permet d'intégrer des sites Web dans Trilium.",
|
||||
"create_label": "Pour commencer, veuillez créer un label avec l'adresse URL que vous souhaitez intégrer, par ex. #webViewSrc=\"https://www.google.com\""
|
||||
},
|
||||
"backend_log": {
|
||||
"refresh": "Rafraîchir"
|
||||
},
|
||||
|
||||
2337
apps/client/src/translations/ga/translation.json
Normal file
2337
apps/client/src/translations/ga/translation.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -50,7 +50,8 @@
|
||||
"save": "Simpan",
|
||||
"branch_prefix_saved": "Prefiks cabang telah disimpan.",
|
||||
"branch_prefix_saved_multiple": "Prefix cabang telah disimpan pada {{count}} cabang.",
|
||||
"affected_branches": "Cabang terdampak ({{count}}):"
|
||||
"affected_branches": "Cabang terdampak ({{count}}):",
|
||||
"edit_branch_prefix": "Sunting awalan cabang"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "Aksi borongan",
|
||||
@@ -61,7 +62,10 @@
|
||||
"execute_bulk_actions": "Eksekusi aksi borongan",
|
||||
"bulk_actions_executed": "Aksi borongan telah di eksekusi dengan sukses.",
|
||||
"none_yet": "Belum ada... tambahkan aksi dengan memilih salah satu dari aksi di atas.",
|
||||
"labels": "Label-label"
|
||||
"labels": "Label-label",
|
||||
"relations": "Hubungan",
|
||||
"notes": "Catatan",
|
||||
"other": "Lainnya"
|
||||
},
|
||||
"confirm": {
|
||||
"cancel": "Batal",
|
||||
@@ -80,6 +84,8 @@
|
||||
"no_note_to_delete": "Tidak ada Catatan yang akan dihapus (hanya duplikat)."
|
||||
},
|
||||
"clone_to": {
|
||||
"clone_notes_to": "Duplikat catatan ke…"
|
||||
"clone_notes_to": "Duplikat catatan ke…",
|
||||
"help_on_links": "Bantuan pada tautan",
|
||||
"notes_to_clone": "Catatan untuk kloning"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +186,8 @@
|
||||
"geo-map": {
|
||||
"create-child-note-title": "Crea una nota figlia e aggiungila alla mappa",
|
||||
"create-child-note-instruction": "Clicca sulla mappa per creare una nuova nota qui o premi Escape per uscire.",
|
||||
"unable-to-load-map": "Impossibile caricare la mappa."
|
||||
"unable-to-load-map": "Impossibile caricare la mappa.",
|
||||
"create-child-note-text": "Aggiungi indicatore"
|
||||
},
|
||||
"geo-map-context": {
|
||||
"open-location": "Apri la posizione",
|
||||
@@ -422,7 +423,8 @@
|
||||
"unknown_search_option": "Opzione di ricerca sconosciuta {{searchOptionName}}",
|
||||
"search_note_saved": "La nota di ricerca è stata salvata in {{- notePathTitle}}",
|
||||
"actions_executed": "Le azioni sono state eseguite.",
|
||||
"view_options": "Opzioni di visualizzazione:"
|
||||
"view_options": "Opzioni di visualizzazione:",
|
||||
"option": "opzione"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Chiudi",
|
||||
@@ -1241,7 +1243,8 @@
|
||||
"show-cheatsheet": "Mostra il foglietto illustrativo",
|
||||
"toggle-zen-mode": "Modalità Zen",
|
||||
"new-version-available": "Nuovo aggiornamento disponibile",
|
||||
"download-update": "Ottieni la versione {{latestVersion}}"
|
||||
"download-update": "Ottieni la versione {{latestVersion}}",
|
||||
"search_notes": "Cerca note"
|
||||
},
|
||||
"zen_mode": {
|
||||
"button_exit": "Esci dalla modalità Zen"
|
||||
@@ -1340,7 +1343,9 @@
|
||||
"delete_this_note": "Elimina questa nota",
|
||||
"note_revisions": "Revisioni delle note",
|
||||
"error_cannot_get_branch_id": "Impossibile ottenere branchId per notePath '{{notePath}}'",
|
||||
"error_unrecognized_command": "Comando non riconosciuto {{command}}"
|
||||
"error_unrecognized_command": "Comando non riconosciuto {{command}}",
|
||||
"backlinks": "Backlinks",
|
||||
"content_language_switcher": "Lingua dei contenuti: {{language}}"
|
||||
},
|
||||
"note_icon": {
|
||||
"change_note_icon": "Cambia icona nota",
|
||||
@@ -1581,11 +1586,6 @@
|
||||
"note_detail_render_help_1": "Questa nota di aiuto viene visualizzata perché questa nota di tipo Render HTML non ha la relazione richiesta per funzionare correttamente.",
|
||||
"note_detail_render_help_2": "Il tipo di nota HTML Render viene utilizzato per lo <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. In breve, si ottiene una nota in codice HTML (opzionalmente con un po' di JavaScript) che verrà visualizzata. Per farla funzionare, è necessario definire una <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relazione</a> denominata \"renderNote\" che punti alla nota HTML da visualizzare."
|
||||
},
|
||||
"web_view": {
|
||||
"web_view": "Visualizzazione Web",
|
||||
"embed_websites": "La nota di tipo Web View consente di incorporare siti web in Trilium.",
|
||||
"create_label": "Per iniziare, crea un'etichetta con l'indirizzo URL che desideri incorporare, ad esempio #webViewSrc=\"https://www.google.com\""
|
||||
},
|
||||
"vacuum_database": {
|
||||
"title": "Pulizia del database",
|
||||
"description": "Questa operazione ricostruirà il database, generando in genere un file di dimensioni inferiori. In realtà, nessun dato verrà modificato.",
|
||||
@@ -2145,7 +2145,7 @@
|
||||
"background_effects_title": "Gli effetti di sfondo sono ora stabili",
|
||||
"background_effects_message": "Sui dispositivi Windows, gli effetti di sfondo sono ora completamente stabili. Gli effetti di sfondo aggiungono un tocco di colore all'interfaccia utente sfocando lo sfondo retrostante. Questa tecnica è utilizzata anche in altre applicazioni come Esplora risorse di Windows.",
|
||||
"background_effects_button": "Abilita gli effetti di sfondo",
|
||||
"dismiss": "Congedare",
|
||||
"dismiss": "Chiudi",
|
||||
"new_layout_title": "Nuovo layout",
|
||||
"new_layout_message": "Abbiamo introdotto un layout modernizzato per Trilium. La barra multifunzione è stata rimossa e integrata perfettamente nell'interfaccia principale, con una nuova barra di stato e sezioni espandibili (come gli attributi promossi) che assumono le funzioni chiave.\n\nIl nuovo layout è abilitato di default e può essere temporaneamente disabilitato tramite Opzioni → Aspetto.",
|
||||
"new_layout_button": "Maggiori informazioni"
|
||||
@@ -2281,5 +2281,24 @@
|
||||
"pages_other": "{{count}} pagine",
|
||||
"pages_alt": "Pagina {{pageNumber}}",
|
||||
"pages_loading": "Caricamento in corso..."
|
||||
},
|
||||
"web_view_setup": {
|
||||
"title": "Crea una visualizzazione live di una pagina web direttamente in Trilium",
|
||||
"url_placeholder": "Inserisci o incolla l'indirizzo del sito web, ad esempio https://triliumnotes.org",
|
||||
"create_button": "Crea vista Web",
|
||||
"invalid_url_title": "Indirizzo non valido",
|
||||
"invalid_url_message": "Inserisci un indirizzo web valido, ad esempio https://triliumnotes.org."
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "Disponibile su {{platform}}"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"title_one": "Scheda {{count}}",
|
||||
"title_many": "Schede {{count}}",
|
||||
"title_other": "Schede {{count}}",
|
||||
"more_options": "Altre opzioni"
|
||||
},
|
||||
"bookmark_buttons": {
|
||||
"bookmarks": "Segnalibri"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,8 @@
|
||||
"configure_launchbar": "ランチャーバーの設定",
|
||||
"show_shared_notes_subtree": "共有ノートのサブツリーを表示",
|
||||
"new-version-available": "新しいアップデートが利用可能",
|
||||
"download-update": "{{latestVersion}} をバージョンを入手"
|
||||
"download-update": "{{latestVersion}} をバージョンを入手",
|
||||
"search_notes": "検索ノート"
|
||||
},
|
||||
"left_pane_toggle": {
|
||||
"show_panel": "パネルを表示",
|
||||
@@ -234,7 +235,8 @@
|
||||
"search_note_saved": "検索ノートが {{- notePathTitle}} に保存されました",
|
||||
"actions_executed": "アクションが実行されました。",
|
||||
"ancestor": "祖先:",
|
||||
"view_options": "表示オプション:"
|
||||
"view_options": "表示オプション:",
|
||||
"option": "オプション"
|
||||
},
|
||||
"shortcuts": {
|
||||
"multiple_shortcuts": "同じアクションに対して複数のショートカットを設定する場合、カンマで区切ることができます。",
|
||||
@@ -247,7 +249,8 @@
|
||||
"reload_app": "リロードして変更を適用する",
|
||||
"set_all_to_default": "すべてのショートカットをデフォルトに戻す",
|
||||
"confirm_reset": "キーボードショートカットをすべてデフォルトにリセットしますか?",
|
||||
"keyboard_shortcuts": "キーボードショートカット"
|
||||
"keyboard_shortcuts": "キーボードショートカット",
|
||||
"no_results": "'{{filter}}' に一致するショートカットが見つかりません"
|
||||
},
|
||||
"confirm": {
|
||||
"confirmation": "確認",
|
||||
@@ -407,7 +410,7 @@
|
||||
"relation_map_buttons": {
|
||||
"zoom_out_title": "ズームアウト",
|
||||
"zoom_in_title": "ズームイン",
|
||||
"create_child_note_title": "新しい子ノートを作成し、関連マップに追加",
|
||||
"create_child_note_title": "子ノートを作成し、マップに追加",
|
||||
"reset_pan_zoom_title": "パンとズームを初期座標と倍率にリセット"
|
||||
},
|
||||
"tree-context-menu": {
|
||||
@@ -824,11 +827,6 @@
|
||||
"error_no_path": "移動するパスがありません。",
|
||||
"move_success_message": "選択したノートは以下に移動されました "
|
||||
},
|
||||
"web_view": {
|
||||
"web_view": "Web ビュー",
|
||||
"embed_websites": "Web ビュータイプでは、web サイトを Trilium に埋め込むことができます。",
|
||||
"create_label": "まず始めに、埋め込みたいURLアドレスのラベルを作成してください。例: #webViewSrc=\"https://www.google.com\""
|
||||
},
|
||||
"backend_log": {
|
||||
"refresh": "リフレッシュ"
|
||||
},
|
||||
@@ -1648,7 +1646,9 @@
|
||||
"error_unrecognized_command": "認識されないコマンド {{command}}",
|
||||
"insert_child_note": "子ノートを挿入",
|
||||
"error_cannot_get_branch_id": "ノートパス 「{{notePath}} のbranchIdを取得できません",
|
||||
"note_revisions": "ノートの変更履歴"
|
||||
"note_revisions": "ノートの変更履歴",
|
||||
"backlinks": "バックリンク",
|
||||
"content_language_switcher": "コンテンツの言語: {{language}}"
|
||||
},
|
||||
"inherited_attribute_list": {
|
||||
"title": "継承属性",
|
||||
@@ -1955,7 +1955,9 @@
|
||||
"print_report_title": "レポートを印刷",
|
||||
"print_report_collection_content_other": "コレクション内の {{count}} 件のノートは、サポートされていないか保護されているため、印刷できませんでした。",
|
||||
"print_report_collection_details_button": "詳細を見る",
|
||||
"print_report_collection_details_ignored_notes": "無視されたノート"
|
||||
"print_report_collection_details_ignored_notes": "無視されたノート",
|
||||
"print_report_error_title": "印刷に失敗しました",
|
||||
"print_report_stack_trace": "スタックトレース"
|
||||
},
|
||||
"watched_file_update_status": {
|
||||
"ignore_this_change": "この変更を無視する",
|
||||
@@ -2008,7 +2010,8 @@
|
||||
"geo-map": {
|
||||
"create-child-note-title": "新しい子ノートを作成し、マップに追加する",
|
||||
"create-child-note-instruction": "地図をクリックしてその場所に新しいノートを作成するか、Esc キーを押して閉じます。",
|
||||
"unable-to-load-map": "マップを読み込めません。"
|
||||
"unable-to-load-map": "マップを読み込めません。",
|
||||
"create-child-note-text": "マーカーを追加"
|
||||
},
|
||||
"geo-map-context": {
|
||||
"open-location": "現在位置を表示",
|
||||
@@ -2256,5 +2259,19 @@
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "{{platform}} で利用可能"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"title_other": "{{count}} タブ",
|
||||
"more_options": "その他のオプション"
|
||||
},
|
||||
"bookmark_buttons": {
|
||||
"bookmarks": "ブックマーク"
|
||||
},
|
||||
"web_view_setup": {
|
||||
"title": "Trilium に直接 Web ページのライブビューを作成",
|
||||
"url_placeholder": "Web サイトのアドレスを入力または貼り付けて下さい。 例: https://triliumnotes.org",
|
||||
"create_button": "Web ビューを作成",
|
||||
"invalid_url_title": "無効なアドレス",
|
||||
"invalid_url_message": "有効な Web アドレスを入力してください。 例: https://triliumnotes.org"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Nie udało się załadować niestandardowego skryptu",
|
||||
"message": "Skrypt z notatki o ID \"{{id}}\", zatytułowany \"{{title}}\", nie mógł zostać wykonany z powodu:\n\n{{message}}"
|
||||
"message": "Skrypt nie mógł zostać wykonany z powodu:\n\n{{message}}"
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "Nie udało się pobrać listy widżetów z serwera"
|
||||
@@ -29,8 +29,9 @@
|
||||
"widget-render-error": {
|
||||
"title": "Nie udało się wyrenderować niestandardowego widżetu React"
|
||||
},
|
||||
"widget-missing-parent": "Niestandardowy widżet nie ma zdefiniowanej obowiązkowej właściwości „{{property}}”.",
|
||||
"open-script-note": "Otwórz notatkę ze skryptem"
|
||||
"widget-missing-parent": "Niestandardowy widżet nie ma zdefiniowanej obowiązkowej właściwości „{{property}}”.\nJeśli skrypt ma działać bez interfejsu użytkownika (UI) wyłącz go: '#run=frontendStartup'.",
|
||||
"open-script-note": "Otwórz notatkę ze skryptem",
|
||||
"scripting-error": "Błąd skryptu użytkownika: {{title}}"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Dodaj link",
|
||||
@@ -191,7 +192,8 @@
|
||||
"expand_tooltip": "Rozwija bezpośrednie elementy podrzędne tej kolekcji (o jeden poziom). Aby uzyskać więcej opcji, naciśnij strzałkę po prawej.",
|
||||
"expand_first_level": "Rozwiń bezpośrednie elementy podrzędne",
|
||||
"expand_nth_level": "Rozwiń {{depth}} poziomów",
|
||||
"expand_all_levels": "Rozwiń wszystkie poziomy"
|
||||
"expand_all_levels": "Rozwiń wszystkie poziomy",
|
||||
"hide_child_notes": "Ukryj notatki podrzędne w derzwie"
|
||||
},
|
||||
"board_view": {
|
||||
"move-to": "Przenieś do",
|
||||
@@ -240,7 +242,7 @@
|
||||
"background_effects_title": "Efekty tła są teraz stabilne",
|
||||
"dismiss": "Odrzuć",
|
||||
"background_effects_button": "Włącz efekty tła",
|
||||
"background_effects_message": "Na urządzeniach z systemem Windows efekty tła są teraz w pełni stabilne. Efekty tła dodają odrobinę koloru do interfejsu użytkownika poprzez rozmycie tła za nim. Ta technika jest również stosowana w innych aplikacjach, takich jak Eksplorator Windows.",
|
||||
"background_effects_message": "Na urządzeniach z systemem Windows i macOS efekty tła są stabilne. Efekty tła dodają odrobinę koloru do interfejsu użytkownika poprzez rozmycie tła za nim.",
|
||||
"new_layout_title": "Nowy układ",
|
||||
"new_layout_message": "Wprowadziliśmy zmodernizowany układ interfejsu dla Trilium. Wstążka została usunięta i płynnie zintegrowana z głównym interfejsem, a jej kluczowe funkcje przejęły nowy pasek stanu i rozwijane sekcje (takie jak promowane atrybuty).\n\nNowy układ jest domyślnie włączony i można go tymczasowo wyłączyć w Ustawienia → Wygląd.",
|
||||
"new_layout_button": "Szczegóły"
|
||||
@@ -520,7 +522,8 @@
|
||||
"action": "akcja",
|
||||
"search_button": "Szukaj",
|
||||
"search_execute": "Szukaj i wykonaj akcje",
|
||||
"view_options": "Ustawienia widoku:"
|
||||
"view_options": "Ustawienia widoku:",
|
||||
"option": "opcja"
|
||||
},
|
||||
"similar_notes": {
|
||||
"title": "Podobne notatki",
|
||||
@@ -602,8 +605,8 @@
|
||||
"desktop-application": "Aplikacja desktopowa",
|
||||
"native-title-bar": "Natywny pasek tytułu",
|
||||
"native-title-bar-description": "Dla Windows i macOS, wyłączenie natywnego paska tytułu sprawia, że aplikacja wygląda bardziej kompaktowo. Na Linuxie, włączenie natywnego paska tytułu lepiej integruje się z resztą systemu.",
|
||||
"background-effects": "Włącz efekty tła (tylko Windows 11)",
|
||||
"background-effects-description": "Efekt Mica dodaje rozmyte, stylowe tło do okien aplikacji, tworząc głębię i nowoczesny wygląd. \"Natywny pasek tytułu\" musi być wyłączony.",
|
||||
"background-effects": "Włącz efekty tła",
|
||||
"background-effects-description": "Dodaje rozmyte, stylowe tło do okien aplikacji, tworząc głębię i nowoczesny wygląd. \"Natywny pasek tytułu\" musi być wyłączony.",
|
||||
"restart-app-button": "Zrestartuj aplikację, aby zobaczyć zmiany",
|
||||
"zoom-factor": "Współczynnik powiększenia"
|
||||
},
|
||||
@@ -1182,7 +1185,8 @@
|
||||
"show-cheatsheet": "Pokaż ściągawkę",
|
||||
"toggle-zen-mode": "Tryb Zen",
|
||||
"new-version-available": "Dostępna nowa aktualizacja",
|
||||
"download-update": "Pobierz wersję {{latestVersion}}"
|
||||
"download-update": "Pobierz wersję {{latestVersion}}",
|
||||
"search_notes": "Przeszukaj notatki"
|
||||
},
|
||||
"zen_mode": {
|
||||
"button_exit": "Wyjdź z trybu Zen"
|
||||
@@ -1265,7 +1269,7 @@
|
||||
"button_title": "Eksportuj diagram jako SVG"
|
||||
},
|
||||
"relation_map_buttons": {
|
||||
"create_child_note_title": "Utwórz nową notatkę podrzędną i dodaj ją do tej mapy relacji",
|
||||
"create_child_note_title": "Utwórz notatkę podrzędną i dodaj ją do mapy",
|
||||
"reset_pan_zoom_title": "Zresetuj przesunięcie i powiększenie do początkowych współrzędnych i powiększenia",
|
||||
"zoom_in_title": "Powiększ",
|
||||
"zoom_out_title": "Pomniejsz"
|
||||
@@ -1281,12 +1285,23 @@
|
||||
"delete_this_note": "Usuń tę notatkę",
|
||||
"note_revisions": "Wersje notatki",
|
||||
"error_cannot_get_branch_id": "Nie można pobrać branchId dla ścieżki notatki '{{notePath}}'",
|
||||
"error_unrecognized_command": "Nierozpoznane polecenie {{command}}"
|
||||
"error_unrecognized_command": "Nierozpoznane polecenie {{command}}",
|
||||
"backlinks": "Linki zwrotne",
|
||||
"content_language_switcher": "Język treści: {{language}}"
|
||||
},
|
||||
"note_icon": {
|
||||
"change_note_icon": "Zmień ikonę notatki",
|
||||
"search": "Szukaj:",
|
||||
"reset-default": "Przywróć domyślną ikonę"
|
||||
"reset-default": "Przywróć domyślną ikonę",
|
||||
"search_placeholder_one": "Znaleziono {{number}} ikonę w {{count}} pakietach",
|
||||
"search_placeholder_few": "Znaleziono {{number}} ikon w {{count}} pakietach",
|
||||
"search_placeholder_many": "Znaleziono {{number}} ikon w {{count}} pakietach",
|
||||
"search_placeholder_filtered": "Wyszukaj {{number}} ikon w {{name}}",
|
||||
"filter": "Filtr",
|
||||
"filter-none": "Wszystkie ikony",
|
||||
"filter-default": "Domyślne ikony",
|
||||
"icon_tooltip": "{{name}}\npakiet ikon: {{iconPack}}",
|
||||
"no_results": "Nie znaleziono ikon."
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "Typ notatki",
|
||||
@@ -1421,11 +1436,6 @@
|
||||
"note_detail_render_help_1": "Ta notatka pomocy jest wyświetlana, ponieważ ta notatka typu Render HTML nie ma wymaganej relacji do poprawnego działania.",
|
||||
"note_detail_render_help_2": "Typ notatki Render HTML jest używany do <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">skryptowania</a>. W skrócie, masz notatkę kodu HTML (opcjonalnie z JavaScript) i ta notatka ją wyrenderuje. Aby to zadziałało, musisz zdefiniować <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relację</a> o nazwie \"renderNote\" wskazującą na notatkę HTML do wyrenderowania."
|
||||
},
|
||||
"web_view": {
|
||||
"web_view": "Widok WWW",
|
||||
"embed_websites": "Notatka typu Widok WWW pozwala na osadzanie stron internetowych w Trilium.",
|
||||
"create_label": "Aby rozpocząć, utwórz etykietę z adresem URL, który chcesz osadzić, np. #webViewSrc=\"https://www.google.com\""
|
||||
},
|
||||
"backend_log": {
|
||||
"refresh": "Odśwież"
|
||||
},
|
||||
@@ -1826,7 +1836,7 @@
|
||||
"will_be_deleted_in": "Ten załącznik zostanie automatycznie usunięty za {{time}}",
|
||||
"will_be_deleted_soon": "Ten załącznik zostanie wkrótce automatycznie usunięty",
|
||||
"deletion_reason": ", ponieważ załącznik nie jest podlinkowany w treści notatki. Aby zapobiec usunięciu, dodaj link do załącznika z powrotem do treści lub przekonwertuj załącznik na notatkę.",
|
||||
"role_and_size": "Rola: {{role}}, Rozmiar: {{size}}",
|
||||
"role_and_size": "Rola: {{role}}, Rozmiar: {{size}}, MIME: {{- mimeType}}",
|
||||
"link_copied": "Link do załącznika skopiowany do schowka.",
|
||||
"unrecognized_role": "Nierozpoznana rola załącznika '{{role}}'."
|
||||
},
|
||||
@@ -1880,7 +1890,10 @@
|
||||
"apply-bulk-actions": "Zastosuj akcje masowe",
|
||||
"converted-to-attachments": "{{count}} notatek zostało przekonwertowanych na załączniki.",
|
||||
"convert-to-attachment-confirm": "Czy na pewno chcesz przekonwertować wybrane notatki na załączniki ich notatek nadrzędnych? Ta operacja dotyczy tylko notatek Obrazów, inne notatki zostaną pominięte.",
|
||||
"open-in-popup": "Szybka edycja"
|
||||
"open-in-popup": "Szybka edycja",
|
||||
"open-in-a-new-window": "Otwórz w nowym oknie",
|
||||
"hide-subtree": "Ukryj gałąź",
|
||||
"show-subtree": "Rozwiń gałąź"
|
||||
},
|
||||
"shared_info": {
|
||||
"shared_publicly": "Ta notatka jest udostępniona publicznie pod adresem {{- link}}.",
|
||||
@@ -1971,7 +1984,17 @@
|
||||
"create-child-note": "Utwórz notatkę podrzędną",
|
||||
"unhoist": "Cofnij zawężenie",
|
||||
"toggle-sidebar": "Przełącz pasek boczny",
|
||||
"dropping-not-allowed": "Upuszczanie notatek w tej lokalizacji jest niedozwolone."
|
||||
"dropping-not-allowed": "Upuszczanie notatek w tej lokalizacji jest niedozwolone.",
|
||||
"clone-indicator-tooltip": "Ta notatka ma {{- count}} notatek nadrzędnych: {{- parents}}",
|
||||
"clone-indicator-tooltip-single": "Ta notatka jest sklonowana (1 dodatkowa notatka nadrzędna: {{- parent}})",
|
||||
"shared-indicator-tooltip": "Ta notatka jest udostępniona publicznie",
|
||||
"shared-indicator-tooltip-with-url": "Ta notatka jest udostępniana publicznie jako: {{- url}}",
|
||||
"subtree-hidden-tooltip_one": "{{count}} notatka podrzędna ukryta w drzewie",
|
||||
"subtree-hidden-tooltip_few": "{{count}} notatek podrzędnych ukrytych w drzewie",
|
||||
"subtree-hidden-tooltip_many": "{{count}} notatek podrzędnych ukrytych w drzewie",
|
||||
"subtree-hidden-moved-title": "Dodano do {{title}}",
|
||||
"subtree-hidden-moved-description-collection": "Ta kolekcja ukrywa swoje notatki podrzędne w drzewie.",
|
||||
"subtree-hidden-moved-description-other": "Notatki podrzędne są ukryte w drzewie tej notatki."
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Utrzymuj okno na wierzchu"
|
||||
@@ -1979,7 +2002,13 @@
|
||||
"note_detail": {
|
||||
"could_not_find_typewidget": "Nie można znaleźć widżetu typu dla typu '{{type}}'",
|
||||
"printing": "Drukowanie w toku...",
|
||||
"printing_pdf": "Eksportowanie do PDF w toku..."
|
||||
"printing_pdf": "Eksportowanie do PDF w toku...",
|
||||
"print_report_title": "Wydrukuj raport",
|
||||
"print_report_collection_content_one": "Nie można wydrukować {{count}} notatki w kolekcji, ponieważ nie jest ona obsługiwana lub jest chroniona.",
|
||||
"print_report_collection_content_few": "Nie można wydrukować {{count}} notatek w kolekcji, ponieważ nie są one obsługiwane lub są chronione.",
|
||||
"print_report_collection_content_many": "Nie można wydrukować {{count}} notatek w kolekcji, ponieważ nie są one obsługiwane lub są chronione.",
|
||||
"print_report_collection_details_button": "Zobacz szczegóły",
|
||||
"print_report_collection_details_ignored_notes": "Zignorowane notatki"
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "wpisz tytuł notatki tutaj...",
|
||||
@@ -1989,7 +2018,8 @@
|
||||
"note_type_switcher_others": "Inny typ notatki",
|
||||
"note_type_switcher_templates": "Szablon",
|
||||
"note_type_switcher_collection": "Kolekcja",
|
||||
"edited_notes": "Edytowane notatki"
|
||||
"edited_notes": "Notatki edytowane dzisiaj",
|
||||
"promoted_attributes": "Sugerowane atrybuty"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "Nie znaleziono notatek dla podanych parametrów wyszukiwania.",
|
||||
@@ -1999,7 +2029,11 @@
|
||||
"configure_launchbar": "Konfiguruj pasek szybkiego dostępu"
|
||||
},
|
||||
"sql_result": {
|
||||
"no_rows": "Dla tego zapytania nie zwrócono żadnych wierszy"
|
||||
"no_rows": "Dla tego zapytania nie zwrócono żadnych wierszy",
|
||||
"not_executed": "Zapytanie nie zostało jeszcze wykonane.",
|
||||
"failed": "Wykonanie zapytania SQL nie powiodło się",
|
||||
"statement_result": "Wynik wyrażenia",
|
||||
"execute_now": "Wykonaj teraz"
|
||||
},
|
||||
"sql_table_schemas": {
|
||||
"tables": "Tabele"
|
||||
@@ -2116,7 +2150,8 @@
|
||||
"geo-map": {
|
||||
"create-child-note-title": "Utwórz nową notatkę podrzędną i dodaj ją do mapy",
|
||||
"create-child-note-instruction": "Kliknij na mapie, aby utworzyć nową notatkę w tej lokalizacji lub naciśnij Escape, aby anulować.",
|
||||
"unable-to-load-map": "Nie można załadować mapy."
|
||||
"unable-to-load-map": "Nie można załadować mapy.",
|
||||
"create-child-note-text": "Dodaj zaznaczenie"
|
||||
},
|
||||
"geo-map-context": {
|
||||
"open-location": "Otwórz lokalizację",
|
||||
@@ -2183,7 +2218,14 @@
|
||||
"execute_sql_description": "Ta notatka jest notatką SQL. Kliknij, aby wykonać zapytanie SQL.",
|
||||
"shared_copy_to_clipboard": "Kopiuj link do schowka",
|
||||
"shared_open_in_browser": "Otwórz link w przeglądarce",
|
||||
"shared_unshare": "Usuń udostępnienie"
|
||||
"shared_unshare": "Usuń udostępnienie",
|
||||
"save_status_saved": "Zapisane",
|
||||
"save_status_saving": "Zapisywanie...",
|
||||
"save_status_unsaved": "Niezapisane",
|
||||
"save_status_error": "Zapis nie powiódł się",
|
||||
"save_status_saving_tooltip": "Zmiany zostały zapisane.",
|
||||
"save_status_unsaved_tooltip": "Są niezapisane zmiany. Zostaną one zapisane automatycznie za chwilę.",
|
||||
"save_status_error_tooltip": "Wystąpił błąd podczas zapisywania notatki. Spróbuj skopiować treść notatki w inne miejsce i ponownie załadować aplikację."
|
||||
},
|
||||
"status_bar": {
|
||||
"language_title": "Zmień język treści",
|
||||
@@ -2226,5 +2268,30 @@
|
||||
"empty_button": "Ukryj panel",
|
||||
"toggle": "Pokaż/ukryj prawy panel",
|
||||
"custom_widget_go_to_source": "Przejdź do kodu źródłowego"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_one": "{{count}} załącznik",
|
||||
"attachments_few": "{{count}} załączniki",
|
||||
"attachments_many": "{{count}} załączników",
|
||||
"layers_one": "{{count}} warstwa",
|
||||
"layers_few": "{{count}} warstw",
|
||||
"layers_many": "{{count}} warstw",
|
||||
"pages_one": "{{count}} strona",
|
||||
"pages_few": "{{count}} stron",
|
||||
"pages_many": "{{count}} stron",
|
||||
"pages_alt": "Strona {{pageNumber}}",
|
||||
"pages_loading": "Wczytuję..."
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "Dostępne na {{platform}}"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"title_one": "{{count}} zakładka",
|
||||
"title_few": "{{count}} zakładki",
|
||||
"title_many": "{{count}} zakładek",
|
||||
"more_options": "Więcej opcji"
|
||||
},
|
||||
"bookmark_buttons": {
|
||||
"bookmarks": "Zakładki"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1068,11 +1068,6 @@
|
||||
"note_detail_render_help_1": "Esta nota de ajuda é mostrada porque esta nota do tipo Renderizar HTML não possui a relação necessária para funcionar corretamente.",
|
||||
"note_detail_render_help_2": "O tipo de nota Renderizar HTML é usado para <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">automação</a>. Em suma, tem uma nota de código HTML (opcionalmente com algum JavaScript) e esta nota irá renderizá-la. Para fazê-lo funcionar, deve definir uma <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relação</a> chamada \"renderNote\" que aponta para a nota HTML a ser renderizada."
|
||||
},
|
||||
"web_view": {
|
||||
"web_view": "Web View",
|
||||
"embed_websites": "Nota do tipo Visualização Web permite que incorpore sites no Trilium.",
|
||||
"create_label": "Para começar, crie uma etiqueta com um endereço URL que deseja incorporar, por exemplo, #webViewSrc=\"https://www.google.com\""
|
||||
},
|
||||
"backend_log": {
|
||||
"refresh": "Recarregar"
|
||||
},
|
||||
|
||||
@@ -1271,11 +1271,6 @@
|
||||
"start_dragging_relations": "Comece arrastando as relações daqui e solte-as em outra nota.",
|
||||
"cannot_match_transform": "Não foi possível combinar a transformação: {{transform}}"
|
||||
},
|
||||
"web_view": {
|
||||
"web_view": "Web View",
|
||||
"embed_websites": "Nota do tipo Visualização Web permite que você incorpore sites dentro do Trilium.",
|
||||
"create_label": "Para começar, crie uma etiqueta com um endereço URL que deseja incorporar, por exemplo, #webViewSrc=\"https://www.google.com\""
|
||||
},
|
||||
"backend_log": {
|
||||
"refresh": "Recarregar"
|
||||
},
|
||||
|
||||
@@ -1376,11 +1376,6 @@
|
||||
"enable_vim_keybindings": "Permite utilizarea combinațiilor de taste în stil Vim pentru notițele de tip cod (fără modul ex)",
|
||||
"use_vim_keybindings_in_code_notes": "Combinații de taste Vim"
|
||||
},
|
||||
"web_view": {
|
||||
"create_label": "Pentru a începe, creați o etichetă cu adresa URL de încorporat, e.g. #webViewSrc=\"https://www.google.com\"",
|
||||
"embed_websites": "Notițele de tip „Vizualizare web” permit încorporarea site-urilor web în Trilium.",
|
||||
"web_view": "Vizualizare web"
|
||||
},
|
||||
"wrap_lines": {
|
||||
"enable_line_wrap": "Activează trecerea automată pe rândul următor (poate necesita o reîncărcare a interfeței pentru a avea efect)",
|
||||
"wrap_lines_in_code_notes": "Trecerea automată pe rândul următor în notițe de cod"
|
||||
@@ -1761,8 +1756,8 @@
|
||||
"show-recent-notes": "Afișează notițele recente"
|
||||
},
|
||||
"electron_integration": {
|
||||
"background-effects": "Activează efectele de fundal (doar pentru Windows 11)",
|
||||
"background-effects-description": "Efectul Mica adaugă un fundal estompat și elegant ferestrelor aplicațiilor, creând profunzime și un aspect modern. Opțiunea „Bară de titlu nativă” trebuie să fie dezactivată.",
|
||||
"background-effects": "Activează efectele de fundal",
|
||||
"background-effects-description": "Adaugă un fundal estompat și elegant ferestrelor aplicațiilor, creând profunzime și un aspect modern. Opțiunea „Bară de titlu nativă” trebuie să fie dezactivată.",
|
||||
"desktop-application": "Aplicația desktop",
|
||||
"native-title-bar": "Bară de titlu nativă",
|
||||
"native-title-bar-description": "Pentru Windows și macOS, dezactivarea bării de titlu native face aplicația să pară mai compactă. Pe Linux, păstrarea bării integrează mai bine aplicația cu restul sistemului de operare.",
|
||||
@@ -1781,7 +1776,8 @@
|
||||
"geo-map": {
|
||||
"create-child-note-title": "Crează o notiță nouă și adaug-o pe hartă",
|
||||
"unable-to-load-map": "Nu s-a putut încărca harta.",
|
||||
"create-child-note-instruction": "Click pe hartă pentru a crea o nouă notiță la acea poziție sau apăsați Escape pentru a anula."
|
||||
"create-child-note-instruction": "Click pe hartă pentru a crea o nouă notiță la acea poziție sau apăsați Escape pentru a anula.",
|
||||
"create-child-note-text": "Adaugă marcaj"
|
||||
},
|
||||
"duration": {
|
||||
"days": "zile",
|
||||
@@ -2127,7 +2123,7 @@
|
||||
},
|
||||
"call_to_action": {
|
||||
"background_effects_title": "Efectele de fundal sunt acum stabile",
|
||||
"background_effects_message": "Pe dispozitive cu Windows, efectele de fundal sunt complet stabile. Acestea adaugă un strop de culoare interfeței grafice prin estomparea fundalului din spatele ferestrei. Această tehnică este folosită și în alte aplicații precum Windows Explorer.",
|
||||
"background_effects_message": "Pe dispozitive cu Windows și macOS, efectele de fundal sunt stabile. Acestea adaugă un strop de culoare interfeței grafice prin estomparea fundalului din spatele ferestrei.",
|
||||
"background_effects_button": "Activează efectele de fundal",
|
||||
"next_theme_title": "Încercați noua temă Trilium",
|
||||
"next_theme_message": "Utilizați tema clasică, doriți să încercați noua temă?",
|
||||
@@ -2281,5 +2277,14 @@
|
||||
"pages_other": "{{count}} de pagini",
|
||||
"pages_alt": "Pagina {{pageNumber}}",
|
||||
"pages_loading": "Încărcare..."
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "Disponibil pe {{platform}}"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"title_one": "{{count}} tab",
|
||||
"title_few": "{{count}} taburi",
|
||||
"title_other": "{{count}} de taburi",
|
||||
"more_options": "Mai multe opțiuni"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,7 +668,8 @@
|
||||
"geo-map": {
|
||||
"unable-to-load-map": "Не удалось загрузить карту.",
|
||||
"create-child-note-instruction": "Щелкните по карте, чтобы создать новую заметку в этом месте, или нажмите Escape, чтобы закрыть ее.",
|
||||
"create-child-note-title": "Создать новую дочернюю заметку и добавить ее на карту"
|
||||
"create-child-note-title": "Создать новую дочернюю заметку и добавить ее на карту",
|
||||
"create-child-note-text": "Добавить маркер"
|
||||
},
|
||||
"note_tooltip": {
|
||||
"quick-edit": "Быстрое редактирование",
|
||||
@@ -685,8 +686,8 @@
|
||||
"electron_integration": {
|
||||
"zoom-factor": "Коэффициент масштабирования",
|
||||
"restart-app-button": "Применить изменения и перезапустить приложение",
|
||||
"background-effects-description": "Эффект Mica добавляет размытый, стильный фон окнам приложений, создавая глубину и современный вид. Опция \"Системная строка заголовка\" должна быть отключена.",
|
||||
"background-effects": "Включить фоновые эффекты (только Windows 11)",
|
||||
"background-effects-description": "Добавляет размытый, стильный фон окнам приложений, создавая глубину и современный вид. Опция \"Системная строка заголовка\" должна быть отключена.",
|
||||
"background-effects": "Включить фоновые эффекты",
|
||||
"native-title-bar-description": "В Windows и macOS отключение системной строки заголовка делает приложение более компактным. В Linux включение системной строки заголовка улучшает интеграцию с остальной частью системы.",
|
||||
"native-title-bar": "Системная панель заголовка",
|
||||
"desktop-application": "Десктопное приложение"
|
||||
@@ -776,7 +777,11 @@
|
||||
"refresh-saved-search-results": "Обновить сохраненные результаты поиска",
|
||||
"automatically-collapse-notes-title": "Заметки будут свернуты после определенного периода бездействия, чтобы навести порядок в дереве.",
|
||||
"toggle-sidebar": "Переключить боковую панель",
|
||||
"dropping-not-allowed": "Перетаскивание заметок в эту область не разрешено."
|
||||
"dropping-not-allowed": "Перетаскивание заметок в эту область не разрешено.",
|
||||
"shared-indicator-tooltip": "Эта заметка опубликована",
|
||||
"shared-indicator-tooltip-with-url": "Эта заметка доступно публично по адресу: {{- url}}",
|
||||
"subtree-hidden-moved-description-other": "В дереве, к которому относится эта заметка, скрыты дочерние заметки.",
|
||||
"subtree-hidden-moved-description-collection": "Эта коллекция скрывает свои дочерние заметки в дереве."
|
||||
},
|
||||
"quick-search": {
|
||||
"no-results": "Результаты не найдены",
|
||||
@@ -856,7 +861,10 @@
|
||||
"convert-to-attachment-confirm": "Вы уверены, что хотите преобразовать выбранные заметки во вложения их родительских заметок? Эта операция применяется только к заметкам в виде изображений; другие заметки будут пропущены.",
|
||||
"converted-to-attachments": "{{count}} заметок были преобразованы во вложения.",
|
||||
"archive": "Архивировать",
|
||||
"unarchive": "Разархивировать"
|
||||
"unarchive": "Разархивировать",
|
||||
"open-in-a-new-window": "Открыть в новом окне",
|
||||
"hide-subtree": "Скрыть поддерево",
|
||||
"show-subtree": "Показать поддерево"
|
||||
},
|
||||
"info": {
|
||||
"closeButton": "Закрыть",
|
||||
@@ -1000,7 +1008,8 @@
|
||||
"switch_to_mobile_version": "Перейти на мобильную версию",
|
||||
"switch_to_desktop_version": "Переключиться на версию для ПК",
|
||||
"new-version-available": "Доступно обновление",
|
||||
"download-update": "Обновить до {{latestVersion}}"
|
||||
"download-update": "Обновить до {{latestVersion}}",
|
||||
"search_notes": "Поиск заметок"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"relation": "отношение",
|
||||
@@ -1047,7 +1056,8 @@
|
||||
"expand_all_levels": "Развернуть все вложенные уровни",
|
||||
"expand_nth_level": "Развернуть уровни: {{depth}} шт.",
|
||||
"expand_first_level": "Развернуть прямые дочерние уровни",
|
||||
"expand_tooltip": "Разщвернуть дочерние элементы этой коллекции (на один уровень вложенности). Для получения дополнительных параметров нажмите стрелку справа."
|
||||
"expand_tooltip": "Разщвернуть дочерние элементы этой коллекции (на один уровень вложенности). Для получения дополнительных параметров нажмите стрелку справа.",
|
||||
"hide_child_notes": "Скрыть дочерние заметки в дереве"
|
||||
},
|
||||
"edited_notes": {
|
||||
"deleted": "(удалено)",
|
||||
@@ -1692,7 +1702,7 @@
|
||||
"zoom_in_title": "Увеличить масштаб",
|
||||
"zoom_out_title": "Уменьшить масштаб",
|
||||
"reset_pan_zoom_title": "Сбросить панорамирование и масштабирование",
|
||||
"create_child_note_title": "Создать новую дочернюю заметку и добавить ее в эту карту связей"
|
||||
"create_child_note_title": "Создать дочернюю заметку и добавить ее в карту"
|
||||
},
|
||||
"code_auto_read_only_size": {
|
||||
"unit": "символов",
|
||||
@@ -1845,7 +1855,8 @@
|
||||
"error_cannot_get_branch_id": "Невозможно получить branchId для notePath '{{notePath}}'",
|
||||
"delete_this_note": "Удалить эту заметку",
|
||||
"insert_child_note": "Вставить дочернюю заметку",
|
||||
"note_revisions": "История изменений"
|
||||
"note_revisions": "История изменений",
|
||||
"content_language_switcher": "Язык содержимого: {{language}}"
|
||||
},
|
||||
"svg_export_button": {
|
||||
"button_title": "Экспортировать диаграмму как SVG"
|
||||
@@ -1900,7 +1911,7 @@
|
||||
"dismiss": "Отклонить",
|
||||
"background_effects_button": "Включить эффекты фона",
|
||||
"next_theme_button": "Попробовать новую тему",
|
||||
"background_effects_message": "На устройствах Windows фоновые эффекты теперь полностью стабильны. Они добавляют цвет в пользовательский интерфейс, размывая фон за ним. Этот приём также используется в других приложениях, например, в проводнике Windows.",
|
||||
"background_effects_message": "На устройствах с ОС Windows или macOS, фоновые эффекты теперь полностью стабильны. Они добавляют цвета в пользовательский интерфейс, размывая фон за ним.",
|
||||
"background_effects_title": "Фоновые эффекты теперь стабильны",
|
||||
"next_theme_title": "Попробуйте новую тему Trilium",
|
||||
"new_layout_button": "Подробнее",
|
||||
@@ -1988,11 +1999,6 @@
|
||||
"attachment_deleted": "Это вложение было удалено.",
|
||||
"you_can_also_open": ", вы также можете открыть "
|
||||
},
|
||||
"web_view": {
|
||||
"web_view": "Веб-страница",
|
||||
"create_label": "Для начала создайте метку с URL-адресом, который вы хотите встроить, например, #webViewSrc=\"https://www.google.com\"",
|
||||
"embed_websites": "Заметки типа \"Веб-страница\" позволяет встраивать веб-сайты в Trilium."
|
||||
},
|
||||
"ribbon": {
|
||||
"widgets": "Виджеты ленты",
|
||||
"promoted_attributes_message": "Вкладка \"Продвигаемые атрибуты\" будет автоматически открыта, если таковые атрибуты установлены у заметки",
|
||||
@@ -2094,7 +2100,11 @@
|
||||
"ui": "Пользовательский интерфейс"
|
||||
},
|
||||
"sql_result": {
|
||||
"no_rows": "По этому запросу не возвращено ни одной строки"
|
||||
"no_rows": "По этому запросу не возвращено ни одной строки",
|
||||
"not_executed": "Запрос еще не выполнен.",
|
||||
"failed": "Выполнение SQL-запроса завершилось с ошибкой",
|
||||
"statement_result": "Результат заявления",
|
||||
"execute_now": "Выполнить сейчас"
|
||||
},
|
||||
"editable_code": {
|
||||
"placeholder": "Введите содержимое для заметки с кодом..."
|
||||
@@ -2189,7 +2199,14 @@
|
||||
"read_only_auto_description": "Эта заметка была автоматически переведена в режим только для чтения по соображениям производительности. Это автоматическое ограничение можно изменить в настройках.\n\nНажмите, чтобы временно отредактировать её.",
|
||||
"read_only_auto": "Автоматический режим \"только для чтения\"",
|
||||
"read_only_explicit_description": "Эта заметка была вручную установлена в режим «только для чтения».\nНажмите, чтобы временно отредактировать её.",
|
||||
"read_only_explicit": "Только для чтения"
|
||||
"read_only_explicit": "Только для чтения",
|
||||
"save_status_saving": "Сохранение...",
|
||||
"save_status_saved": "Сохранение",
|
||||
"save_status_unsaved": "Не сохранено",
|
||||
"save_status_error": "Ошибка сохранения",
|
||||
"save_status_saving_tooltip": "Изменения сохраняются.",
|
||||
"save_status_unsaved_tooltip": "Есть несохраненные изменения. Они будут сохранены автоматически через некоторое время.",
|
||||
"save_status_error_tooltip": "Произошла ошибка при сохранении заметки. Если возможно, попробуйте скопировать содержимое заметки в другое место и перезагрузить приложение."
|
||||
},
|
||||
"breadcrumb": {
|
||||
"hoisted_badge_title": "Снять фокус",
|
||||
@@ -2243,5 +2260,30 @@
|
||||
},
|
||||
"attributes_panel": {
|
||||
"title": "Атрибуты заметки"
|
||||
},
|
||||
"bookmark_buttons": {
|
||||
"bookmarks": "Закладки"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"more_options": "Показать больше",
|
||||
"title_one": "{{count}} вкладка",
|
||||
"title_few": "{{count}} вкладки",
|
||||
"title_many": "{{count}} вкладок"
|
||||
},
|
||||
"pdf": {
|
||||
"pages_loading": "Загрузка...",
|
||||
"pages_alt": "Страница {{pageNumber}}",
|
||||
"pages_one": "{{count}} страница",
|
||||
"pages_few": "{{count}} страницы",
|
||||
"pages_many": "{{count}} страниц",
|
||||
"layers_one": "{{count}} слой",
|
||||
"layers_few": "{{count}} слоя",
|
||||
"layers_many": "{{count}} слоев",
|
||||
"attachments_one": "{{count}} вложение",
|
||||
"attachments_few": "{{count}} вложения",
|
||||
"attachments_many": "{{count}} вложений"
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "Доступно для {{platform}}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,7 +662,8 @@
|
||||
"show-cheatsheet": "顯示快捷鍵說明",
|
||||
"toggle-zen-mode": "禪模式",
|
||||
"new-version-available": "發現新更新",
|
||||
"download-update": "取得版本 {{latestVersion}}"
|
||||
"download-update": "取得版本 {{latestVersion}}",
|
||||
"search_notes": "搜尋筆記"
|
||||
},
|
||||
"sync_status": {
|
||||
"unknown": "<p>同步狀態將在下一次同步嘗試開始後顯示。</p><p>點擊以立即觸發同步。</p>",
|
||||
@@ -757,7 +758,9 @@
|
||||
"delete_this_note": "刪除此筆記",
|
||||
"error_cannot_get_branch_id": "無法獲取 notePath '{{notePath}}' 的 branchId",
|
||||
"error_unrecognized_command": "無法識別的命令 {{command}}",
|
||||
"note_revisions": "筆記歷史版本"
|
||||
"note_revisions": "筆記歷史版本",
|
||||
"backlinks": "反向連結",
|
||||
"content_language_switcher": "內文語言:{{language}}"
|
||||
},
|
||||
"note_icon": {
|
||||
"change_note_icon": "更改筆記圖標",
|
||||
@@ -909,7 +912,8 @@
|
||||
"unknown_search_option": "未知的搜尋選項 {{searchOptionName}}",
|
||||
"search_note_saved": "搜尋筆記已儲存至 {{- notePathTitle}}",
|
||||
"actions_executed": "已執行操作。",
|
||||
"view_options": "查看選項:"
|
||||
"view_options": "查看選項:",
|
||||
"option": "選項"
|
||||
},
|
||||
"similar_notes": {
|
||||
"title": "相似筆記",
|
||||
@@ -1063,11 +1067,6 @@
|
||||
"note_detail_render_help_1": "之所以顯示此說明筆記,是因為該類型的渲染 HTML 沒有設定好必須的關聯。",
|
||||
"note_detail_render_help_2": "渲染筆記類型用於編寫 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">腳本</a>。簡單說就是您可以寫HTML程式碼(或者加上一些JavaScript程式碼), 然後這個筆記會把頁面渲染出來。要使其正常工作,您需要定義一個名為 \"renderNote\" 的 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">關聯</a> 指向要呈現的 HTML 筆記。"
|
||||
},
|
||||
"web_view": {
|
||||
"web_view": "網頁顯示",
|
||||
"embed_websites": "網頁顯示類型的筆記允許您將網站嵌入至 Trilium 中。",
|
||||
"create_label": "首先,請新增一個帶有您要嵌入的 URL 地址的標籤,例如 #webViewSrc=\"https://www.bing.com\""
|
||||
},
|
||||
"backend_log": {
|
||||
"refresh": "重新整理"
|
||||
},
|
||||
@@ -1939,8 +1938,8 @@
|
||||
"desktop-application": "桌面版應用程式",
|
||||
"native-title-bar": "原生標題列",
|
||||
"native-title-bar-description": "對於 Windows 和 macOS,關閉原生標題列會讓應用程式看起來更緊湊。在 Linux 上,開啟原生標題列可以與系統的其他部分整合得更好。",
|
||||
"background-effects": "啟用背景效果(僅適用於 Windows 11)",
|
||||
"background-effects-description": "Mica 效果為程式視窗新增模糊且時尚的背景,營造出深度感和現代化外觀。「原生標題列」必須被禁用。",
|
||||
"background-effects": "啟用背景效果",
|
||||
"background-effects-description": "為程式視窗新增模糊且時尚的背景,營造立體感和現代化外觀。「原生標題列」必須被禁用。",
|
||||
"restart-app-button": "重新啟動應用程式以查看更改",
|
||||
"zoom-factor": "縮放係數"
|
||||
},
|
||||
@@ -1959,7 +1958,8 @@
|
||||
"geo-map": {
|
||||
"create-child-note-title": "建立一個新的子筆記並將其加至地圖中",
|
||||
"create-child-note-instruction": "點擊地圖以在該位置建立新筆記,或按 Escape 以取消。",
|
||||
"unable-to-load-map": "無法載入地圖。"
|
||||
"unable-to-load-map": "無法載入地圖。",
|
||||
"create-child-note-text": "新增地圖標記"
|
||||
},
|
||||
"geo-map-context": {
|
||||
"open-location": "打開位置",
|
||||
@@ -2122,7 +2122,7 @@
|
||||
},
|
||||
"call_to_action": {
|
||||
"background_effects_title": "背景特效已推出穩定版本",
|
||||
"background_effects_message": "在 Windows 裝置上,背景特效現在已完全穩定。背景特效透過模糊背後的背景,為使用者介面增添一抹色彩。此技術也用於其他應用程式,例如 Windows 檔案總管。",
|
||||
"background_effects_message": "在 Windows 和macOS裝置上,背景特效現在已穩定。背景特效透過模糊背後的背景,為使用者介面增添一抹色彩。",
|
||||
"background_effects_button": "啟用背景特效",
|
||||
"next_theme_title": "試用新 Trilium 主題",
|
||||
"next_theme_message": "您正在使用舊版主題,要試用新主題嗎?",
|
||||
@@ -2267,5 +2267,16 @@
|
||||
"pages_other": "",
|
||||
"pages_alt": "第 {{pageNumber}} 頁",
|
||||
"pages_loading": "正在載入…"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"more_options": "更多選項",
|
||||
"title_one": "{{count}} 個分頁",
|
||||
"title_other": ""
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "可於 {{platform}} 使用"
|
||||
},
|
||||
"bookmark_buttons": {
|
||||
"bookmarks": "書籤"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1134,11 +1134,6 @@
|
||||
"note_detail_render_help_1": "Ця довідка відображається, оскільки ця нотатка типу Render HTML не має необхідного зв'язку для належного функціонування.",
|
||||
"note_detail_render_help_2": "Тип нотатки Render HTML використовується для <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">скриптів</a>. Коротше кажучи, у вас є нотатка з HTML-кодом (за бажанням з деяким JavaScript), і ця нотатка її відобразить. Щоб це запрацювало, вам потрібно визначити <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">відношення</a> під назвою \"renderNote\", яке вказує на нотатку HTML для відображення."
|
||||
},
|
||||
"web_view": {
|
||||
"web_view": "Веб-перегляд",
|
||||
"embed_websites": "Нотатка типу Веб-перегляд дозволяє вбудовувати веб-сайти в Trilium.",
|
||||
"create_label": "Для початку створіть мітку з URL-адресою, яку ви хочете вбудувати, наприклад, #webViewSrc=\"https://www.google.com\""
|
||||
},
|
||||
"backend_log": {
|
||||
"refresh": "Оновити"
|
||||
},
|
||||
|
||||
22
apps/client/src/types-lib.d.ts
vendored
22
apps/client/src/types-lib.d.ts
vendored
@@ -1,3 +1,5 @@
|
||||
export {};
|
||||
|
||||
// TODO: Use real @types/ but that one generates a lot of errors.
|
||||
declare module "draggabilly" {
|
||||
type DraggabillyEventData = {};
|
||||
@@ -32,26 +34,6 @@ declare module "katex/contrib/auto-render" {
|
||||
export default renderMathInElement;
|
||||
}
|
||||
|
||||
import * as L from "leaflet";
|
||||
|
||||
declare module "leaflet" {
|
||||
interface GPXMarker {
|
||||
startIcon?: DivIcon | Icon | string | undefined;
|
||||
endIcon?: DivIcon | Icon | string | undefined;
|
||||
wptIcons?: {
|
||||
[key: string]: DivIcon | Icon | string;
|
||||
};
|
||||
wptTypeIcons?: {
|
||||
[key: string]: DivIcon | Icon | string;
|
||||
};
|
||||
pointMatchers?: Array<{ regex: RegExp; icon: DivIcon | Icon | string}>;
|
||||
}
|
||||
|
||||
interface GPXOptions {
|
||||
markers?: GPXMarker | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Navigator {
|
||||
/** Returns a boolean indicating whether the browser is running in standalone mode. Available on Apple's iOS Safari only. */
|
||||
|
||||
83
apps/client/src/widgets/Backlinks.css
Normal file
83
apps/client/src/widgets/Backlinks.css
Normal file
@@ -0,0 +1,83 @@
|
||||
.tn-backlinks-widget .backlinks-items {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: static;
|
||||
width: unset;
|
||||
|
||||
> li {
|
||||
--border-radius: 8px;
|
||||
|
||||
max-width: 600px;
|
||||
padding: 10px 20px;
|
||||
background: var(--card-background-color);
|
||||
|
||||
& + li {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
|
||||
/* Card header */
|
||||
& > span:first-child {
|
||||
display: block;
|
||||
|
||||
> span {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
/* Note path */
|
||||
> small {
|
||||
flex: 100%;
|
||||
order: -1;
|
||||
font-size: .65rem;
|
||||
|
||||
.note-path {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Note icon */
|
||||
> .tn-icon {
|
||||
color: var(--menu-item-icon-color);
|
||||
}
|
||||
|
||||
/* Note title */
|
||||
> a {
|
||||
margin-inline-start: 4px;
|
||||
color: currentColor;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Card content - excerpt */
|
||||
.backlink-excerpt {
|
||||
all: unset; /* TODO: Remove after disposing the old style from FloatingButtons.css */
|
||||
display: block;
|
||||
|
||||
margin: 8px 0;
|
||||
border-radius: 4px;
|
||||
background: var(--quick-search-result-content-background);
|
||||
padding: 8px;
|
||||
font-size: .75rem;
|
||||
|
||||
a {
|
||||
background: transparent;
|
||||
color: var(--quick-search-result-highlight-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,12 +88,6 @@
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region Geo map buttons */
|
||||
.leaflet-pane {
|
||||
z-index: 50;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region Close floating buttons */
|
||||
.close-floating-buttons {
|
||||
margin-inline-start: 5px !important;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import "./Backlinks.css";
|
||||
|
||||
import { BacklinkCountResponse, BacklinksResponse, SaveSqlConsoleResponse } from "@triliumnext/commons";
|
||||
import { VNode } from "preact";
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||
@@ -60,14 +62,6 @@ export const DESKTOP_FLOATING_BUTTONS: FloatingButtonsList = [
|
||||
Backlinks
|
||||
];
|
||||
|
||||
export const MOBILE_FLOATING_BUTTONS: FloatingButtonsList = [
|
||||
RefreshBackendLogButton,
|
||||
EditButton,
|
||||
RelationMapButtons,
|
||||
ExportImageButtons,
|
||||
Backlinks
|
||||
];
|
||||
|
||||
/**
|
||||
* Floating buttons that should be hidden in popup editor (Quick edit).
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
font-family: var(--detail-font-family);
|
||||
font-size: var(--detail-font-size);
|
||||
contain: none;
|
||||
|
||||
&.fixed-tree {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.fixed-note-tree-container {
|
||||
height: 60%;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
overflow: auto;
|
||||
|
||||
.tree-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tree {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.prefers-centered-content .note-detail {
|
||||
@@ -12,4 +36,4 @@ body.prefers-centered-content .note-detail {
|
||||
|
||||
.note-detail > * {
|
||||
contain: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import "./NoteDetail.css";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { note } from "mermaid/dist/rendering-util/rendering-elements/shapes/note.js";
|
||||
import { isValidElement, VNode } from "preact";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
@@ -12,8 +14,9 @@ import { t } from "../services/i18n";
|
||||
import protected_session_holder from "../services/protected_session_holder";
|
||||
import toast from "../services/toast.js";
|
||||
import { dynamicRequire, isElectron, isMobile } from "../services/utils";
|
||||
import NoteTreeWidget from "./note_tree";
|
||||
import { ExtendedNoteType, TYPE_MAPPINGS, TypeWidget } from "./note_types";
|
||||
import { useNoteContext, useTriliumEvent } from "./react/hooks";
|
||||
import { useLegacyWidget, useNoteContext, useTriliumEvent } from "./react/hooks";
|
||||
import { NoteListWithLinks } from "./react/NoteList";
|
||||
import { TypeWidgetProps } from "./type_widgets/type_widget";
|
||||
|
||||
@@ -36,6 +39,7 @@ export default function NoteDetail() {
|
||||
const [ noteTypesToRender, setNoteTypesToRender ] = useState<{ [ key in ExtendedNoteType ]?: (props: TypeWidgetProps) => VNode }>({});
|
||||
const [ activeNoteType, setActiveNoteType ] = useState<ExtendedNoteType>();
|
||||
const widgetRequestId = useRef(0);
|
||||
const hasFixedTree = note && noteContext?.hoistedNoteId === "_lbMobileRoot" && isMobile() && note.noteId.startsWith("_lbMobile");
|
||||
|
||||
const props: TypeWidgetProps = {
|
||||
note: note!,
|
||||
@@ -119,13 +123,6 @@ export default function NoteDetail() {
|
||||
}
|
||||
});
|
||||
|
||||
// Fixed tree for launch bar config on mobile.
|
||||
useEffect(() => {
|
||||
if (!isMobile) return;
|
||||
const hasFixedTree = noteContext?.hoistedNoteId === "_lbMobileRoot";
|
||||
document.body.classList.toggle("force-fixed-tree", hasFixedTree);
|
||||
}, [ note ]);
|
||||
|
||||
// Handle toast notifications.
|
||||
useEffect(() => {
|
||||
if (!isElectron()) return;
|
||||
@@ -215,8 +212,13 @@ export default function NoteDetail() {
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
class={`component note-detail ${isFullHeight ? "full-height" : ""}`}
|
||||
class={clsx("component note-detail", {
|
||||
"full-height": isFullHeight,
|
||||
"fixed-tree": hasFixedTree
|
||||
})}
|
||||
>
|
||||
{hasFixedTree && <FixedTree noteContext={noteContext} />}
|
||||
|
||||
{Object.entries(noteTypesToRender).map(([ itemType, Element ]) => {
|
||||
return <NoteDetailWrapper
|
||||
Element={Element}
|
||||
@@ -231,6 +233,11 @@ export default function NoteDetail() {
|
||||
);
|
||||
}
|
||||
|
||||
function FixedTree({ noteContext }: { noteContext: NoteContext }) {
|
||||
const [ treeEl ] = useLegacyWidget(() => new NoteTreeWidget(), { noteContext });
|
||||
return <div class="fixed-note-tree-container">{treeEl}</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a single note type widget, in order to keep it in the DOM even after the user has switched away to another note type. This allows faster loading of the same note type again. The properties are cached, so that they are updated only
|
||||
* while the widget is visible, to avoid rendering in the background. When not visible, the DOM element is simply hidden.
|
||||
@@ -349,6 +356,14 @@ export function checkFullHeight(noteContext: NoteContext | undefined, type: Exte
|
||||
// https://github.com/zadam/trilium/issues/2522
|
||||
const isBackendNote = noteContext?.noteId === "_backendLog";
|
||||
const isFullHeightNoteType = type && TYPE_MAPPINGS[type].isFullHeight;
|
||||
|
||||
// Allow vertical centering when there are no results.
|
||||
if (type === "book" &&
|
||||
[ "grid", "list" ].includes(noteContext.note?.getLabelValue("viewType") ?? "grid") &&
|
||||
!noteContext.note?.hasChildren()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (!noteContext?.hasNoteList() && isFullHeightNoteType)
|
||||
|| noteContext?.viewScope?.viewMode === "attachments"
|
||||
|| isBackendNote;
|
||||
@@ -364,7 +379,33 @@ function showToast(type: "printing" | "exporting_pdf", progress: number = 0) {
|
||||
}
|
||||
|
||||
function handlePrintReport(printReport?: PrintReport) {
|
||||
if (printReport?.type === "collection" && printReport.ignoredNoteIds.length > 0) {
|
||||
if (!printReport) return;
|
||||
|
||||
if (printReport.type === "error") {
|
||||
toast.showPersistent({
|
||||
id: "print-error",
|
||||
icon: "bx bx-error-circle",
|
||||
title: t("note_detail.print_report_error_title"),
|
||||
message: printReport.message,
|
||||
buttons: printReport.stack ? [
|
||||
{
|
||||
text: t("note_detail.print_report_collection_details_button"),
|
||||
onClick(api) {
|
||||
api.dismissToast();
|
||||
dialog.info(<>
|
||||
<p>{printReport.message}</p>
|
||||
<details>
|
||||
<summary>{t("note_detail.print_report_stack_trace")}</summary>
|
||||
<pre style="font-size: 0.85em; overflow-x: auto;">{printReport.stack}</pre>
|
||||
</details>
|
||||
</>, {
|
||||
title: t("note_detail.print_report_error_title")
|
||||
});
|
||||
}
|
||||
}
|
||||
] : undefined
|
||||
});
|
||||
} else if (printReport.type === "collection" && printReport.ignoredNoteIds.length > 0) {
|
||||
toast.showPersistent({
|
||||
id: "print-report",
|
||||
icon: "bx bx-collection",
|
||||
|
||||
@@ -16,6 +16,10 @@ body.mobile .promoted-attributes-widget {
|
||||
display: table;
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout .promoted-attributes-container {
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
.promoted-attribute-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -94,4 +98,4 @@ body.mobile .promoted-attributes-widget {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
transform: rotate(45deg);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import clsx from "clsx";
|
||||
import { ComponentChild, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact";
|
||||
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import NoteContext from "../components/note_context";
|
||||
import FAttribute from "../entities/fattribute";
|
||||
import FNote from "../entities/fnote";
|
||||
import { Attribute } from "../services/attribute_parser";
|
||||
@@ -40,8 +41,8 @@ type OnChangeEventData = TargetedEvent<HTMLInputElement, Event> | InputEvent | J
|
||||
type OnChangeListener = (e: OnChangeEventData) => Promise<void>;
|
||||
|
||||
export default function PromotedAttributes() {
|
||||
const { note, componentId } = useNoteContext();
|
||||
const [ cells, setCells ] = usePromotedAttributeData(note, componentId);
|
||||
const { note, componentId, noteContext } = useNoteContext();
|
||||
const [ cells, setCells ] = usePromotedAttributeData(note, componentId, noteContext);
|
||||
return <PromotedAttributesContent note={note} componentId={componentId} cells={cells} setCells={setCells} />;
|
||||
}
|
||||
|
||||
@@ -74,12 +75,12 @@ export function PromotedAttributesContent({ note, componentId, cells, setCells }
|
||||
*
|
||||
* The cells are returned as a state since they can also be altered internally if needed, for example to add a new empty cell.
|
||||
*/
|
||||
export function usePromotedAttributeData(note: FNote | null | undefined, componentId: string): [ Cell[] | undefined, Dispatch<StateUpdater<Cell[] | undefined>> ] {
|
||||
export function usePromotedAttributeData(note: FNote | null | undefined, componentId: string, noteContext: NoteContext | undefined): [ Cell[] | undefined, Dispatch<StateUpdater<Cell[] | undefined>> ] {
|
||||
const [ viewType ] = useNoteLabel(note, "viewType");
|
||||
const [ cells, setCells ] = useState<Cell[]>();
|
||||
|
||||
function refresh() {
|
||||
if (!note || viewType === "table") {
|
||||
if (!note || viewType === "table" || noteContext?.viewScope?.viewMode !== "default") {
|
||||
setCells([]);
|
||||
return;
|
||||
}
|
||||
@@ -124,7 +125,7 @@ export function usePromotedAttributeData(note: FNote | null | undefined, compone
|
||||
setCells(cells);
|
||||
}
|
||||
|
||||
useEffect(refresh, [ note, viewType ]);
|
||||
useEffect(refresh, [ note, viewType, noteContext ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getAttributeRows(componentId).find((attr) => attributes.isAffecting(attr, note))) {
|
||||
refresh();
|
||||
|
||||
@@ -29,7 +29,6 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
|
||||
const isVerticalLayout = !isHorizontalLayout;
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const { isUpdateAvailable, latestVersion } = useTriliumUpdateStatus();
|
||||
const isMobileLocal = isMobile();
|
||||
const logoRef = useRef<SVGSVGElement>(null);
|
||||
useStaticTooltip(logoRef);
|
||||
|
||||
@@ -44,9 +43,12 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
|
||||
</div>}
|
||||
</>}
|
||||
noDropdownListStyle
|
||||
onShown={isMobileLocal ? () => document.getElementById("context-menu-cover")?.classList.add("show", "global-menu-cover") : undefined}
|
||||
onHidden={isMobileLocal ? () => document.getElementById("context-menu-cover")?.classList.remove("show", "global-menu-cover") : undefined}
|
||||
mobileBackdrop
|
||||
>
|
||||
{isMobile() && <>
|
||||
<MenuItem command="searchNotes" icon="bx bx-search" text={t("global_menu.search_notes")} />
|
||||
<FormDropdownDivider />
|
||||
</>}
|
||||
|
||||
<MenuItem command="openNewWindow" icon="bx bx-window-open" text={t("global_menu.open_new_window")} />
|
||||
<MenuItem command="showShareSubtree" icon="bx bx-share-alt" text={t("global_menu.show_shared_notes_subtree")} />
|
||||
@@ -107,8 +109,7 @@ function BrowserOnlyOptions() {
|
||||
|
||||
function DevelopmentOptions({ dropStart }: { dropStart: boolean }) {
|
||||
return <>
|
||||
<FormDropdownDivider />
|
||||
<FormListItem disabled>Development Options</FormListItem>
|
||||
<FormListHeader text="Development Options" />
|
||||
<FormDropdownSubmenu icon="bx bx-test-tube" title="Experimental features" dropStart={dropStart}>
|
||||
{experimentalFeatures.map((feature) => (
|
||||
<ExperimentalFeatureToggle key={feature.id} experimentalFeature={feature as ExperimentalFeature} />
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
min-height: 0;
|
||||
max-width: var(--max-content-width); /* Inherited from .note-split */
|
||||
|
||||
overflow: auto;
|
||||
overflow: visible;
|
||||
contain: none !important;
|
||||
|
||||
&.full-height {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
body.prefers-centered-content .note-list-widget:not(.full-height) {
|
||||
|
||||
@@ -10,12 +10,6 @@
|
||||
--card-padding: 0.6em;
|
||||
}
|
||||
|
||||
body.mobile .board-view {
|
||||
scroll-snap-type: x mandatory;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.board-view-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@@ -26,6 +20,12 @@ body.mobile .board-view {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
body.mobile .board-view-container {
|
||||
scroll-snap-type: x mandatory;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.board-view-container .board-column {
|
||||
width: 250px;
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -79,6 +79,7 @@ export const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, (() => Promise<{ de
|
||||
es: () => import("@fullcalendar/core/locales/es"),
|
||||
fr: () => import("@fullcalendar/core/locales/fr"),
|
||||
it: () => import("@fullcalendar/core/locales/it"),
|
||||
ga: null,
|
||||
cn: () => import("@fullcalendar/core/locales/zh-cn"),
|
||||
tw: () => import("@fullcalendar/core/locales/zh-tw"),
|
||||
ro: () => import("@fullcalendar/core/locales/ro"),
|
||||
|
||||
@@ -1,14 +1,51 @@
|
||||
import type { LatLng, LeafletMouseEvent } from "leaflet";
|
||||
import appContext, { type CommandMappings } from "../../../components/app_context.js";
|
||||
import contextMenu, { type MenuItem } from "../../../menus/context_menu.js";
|
||||
import linkContextMenu from "../../../menus/link_context_menu.js";
|
||||
import NoteColorPicker from "../../../menus/custom-items/NoteColorPicker.jsx";
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import { createNewNote } from "./api.js";
|
||||
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
|
||||
import link from "../../../services/link.js";
|
||||
import { MapMouseEvent } from "maplibre-gl";
|
||||
import { useCallback, useContext, useEffect } from "preact/hooks";
|
||||
|
||||
export default function openContextMenu(noteId: string, e: LeafletMouseEvent, isEditable: boolean) {
|
||||
import appContext, { type CommandMappings } from "../../../components/app_context.js";
|
||||
import FNote from "../../../entities/fnote.js";
|
||||
import contextMenu, { type MenuItem } from "../../../menus/context_menu.js";
|
||||
import NoteColorPicker from "../../../menus/custom-items/NoteColorPicker.jsx";
|
||||
import linkContextMenu from "../../../menus/link_context_menu.js";
|
||||
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import link from "../../../services/link.js";
|
||||
import { createNewNote } from "./api.js";
|
||||
import { type GeoMouseEvent,ParentMap, toMapLibreEvent } from "./map.js";
|
||||
import { MARKER_LAYER } from "./Markers.js";
|
||||
|
||||
export default function ContextMenus({ note, isReadOnly }: { note: FNote, isReadOnly }) {
|
||||
const map = useContext(ParentMap);
|
||||
|
||||
const onContextMenu = useCallback((e: GeoMouseEvent) => {
|
||||
if (!map) return;
|
||||
const features = map.queryRenderedFeatures(e.point, {
|
||||
layers: [ MARKER_LAYER ]
|
||||
});
|
||||
|
||||
if (features.length > 0) {
|
||||
// Marker context menu.
|
||||
openContextMenu(features[0].properties.id, e, !isReadOnly);
|
||||
} else {
|
||||
// Empty area context menu.
|
||||
openMapContextMenu(note.noteId, e, !isReadOnly);
|
||||
}
|
||||
}, [ map, note.noteId, isReadOnly ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!onContextMenu || !map) return;
|
||||
|
||||
const handler = (e: MapMouseEvent) => {
|
||||
e.preventDefault();
|
||||
onContextMenu(toMapLibreEvent(e));
|
||||
};
|
||||
map.on("contextmenu", handler);
|
||||
return () => { map.off("contextmenu", handler); };
|
||||
}, [ map, onContextMenu ]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function openContextMenu(noteId: string, e: GeoMouseEvent, isEditable: boolean) {
|
||||
let items: MenuItem<keyof CommandMappings>[] = [
|
||||
...buildGeoLocationItem(e),
|
||||
{ kind: "separator" },
|
||||
@@ -44,7 +81,7 @@ export default function openContextMenu(noteId: string, e: LeafletMouseEvent, is
|
||||
});
|
||||
}
|
||||
|
||||
export function openMapContextMenu(noteId: string, e: LeafletMouseEvent, isEditable: boolean) {
|
||||
export function openMapContextMenu(noteId: string, e: GeoMouseEvent, isEditable: boolean) {
|
||||
let items: MenuItem<keyof CommandMappings>[] = [
|
||||
...buildGeoLocationItem(e)
|
||||
];
|
||||
@@ -58,7 +95,7 @@ export function openMapContextMenu(noteId: string, e: LeafletMouseEvent, isEdita
|
||||
handler: () => createNewNote(noteId, e),
|
||||
uiIcon: "bx bx-plus"
|
||||
}
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
contextMenu.show({
|
||||
@@ -71,8 +108,8 @@ export function openMapContextMenu(noteId: string, e: LeafletMouseEvent, isEdita
|
||||
});
|
||||
}
|
||||
|
||||
function buildGeoLocationItem(e: LeafletMouseEvent) {
|
||||
function formatGeoLocation(latlng: LatLng, precision: number = 6) {
|
||||
function buildGeoLocationItem(e: GeoMouseEvent) {
|
||||
function formatGeoLocation(latlng: { lat: number; lng: number }, precision: number = 6) {
|
||||
return `${latlng.lat.toFixed(precision)}, ${latlng.lng.toFixed(precision)}`;
|
||||
}
|
||||
|
||||
250
apps/client/src/widgets/collections/geomap/Markers.tsx
Normal file
250
apps/client/src/widgets/collections/geomap/Markers.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import { AddLayerObject, type GeoJSONSource } from "maplibre-gl";
|
||||
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import FNote from "../../../entities/fnote";
|
||||
import { useChildNotes } from "../../react/hooks";
|
||||
import { ParentMap } from "./map";
|
||||
|
||||
export const LOCATION_ATTRIBUTE = "geolocation";
|
||||
export const MARKER_LAYER = "points-layer";
|
||||
const MARKER_SOURCE = "points";
|
||||
const DEFAULT_MARKER_COLOR = "#2A81CB";
|
||||
|
||||
// SVG marker pin shape (replaces the Leaflet marker PNG).
|
||||
export const MARKER_SVG = "foo"; // TODO: Fix
|
||||
const iconSvgCache = new Map<string, string>();
|
||||
const iconClassToBitmapCache = new Map<string, string | undefined>();
|
||||
|
||||
const LABEL_LAYOUT: Extract<AddLayerObject, { type: "symbol"}>["layout"] = {
|
||||
"text-field": ["get", "name"],
|
||||
"text-font": ["Open Sans Regular"],
|
||||
"text-size": 12,
|
||||
"text-anchor": "top",
|
||||
"text-allow-overlap": true,
|
||||
};
|
||||
|
||||
const MARKER_WIDTH = 25;
|
||||
const MARKER_ICON_SIZE = 20;
|
||||
const MARKER_ICON_X = (MARKER_WIDTH / 2) - (MARKER_ICON_SIZE / 2);
|
||||
|
||||
export default function Markers({ note, hideLabels }: { note: FNote, hideLabels: boolean }) {
|
||||
const map = useContext(ParentMap);
|
||||
const childNotes = useChildNotes(note?.noteId);
|
||||
|
||||
// Add the source and layer.
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
// Wait for styles to be loaded.
|
||||
map.on("style.load", () => {
|
||||
map.addSource(MARKER_SOURCE, {
|
||||
type: "geojson",
|
||||
data: {
|
||||
type: "FeatureCollection",
|
||||
features: []
|
||||
}
|
||||
});
|
||||
map.addLayer({
|
||||
id: MARKER_LAYER,
|
||||
type: "symbol",
|
||||
source: MARKER_SOURCE,
|
||||
layout: {
|
||||
"icon-image": [ "get", "icon" ],
|
||||
"icon-size": 1,
|
||||
"icon-anchor": "bottom",
|
||||
"icon-allow-overlap": true,
|
||||
...hideLabels ? [] : LABEL_LAYOUT
|
||||
},
|
||||
paint: {
|
||||
"text-color": "#333",
|
||||
"text-halo-color": "#fff",
|
||||
"text-halo-width": 1,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (map.getLayer(MARKER_LAYER)) {
|
||||
map.removeLayer(MARKER_LAYER);
|
||||
}
|
||||
if (map.getSource(MARKER_SOURCE)) {
|
||||
map.removeSource(MARKER_SOURCE);
|
||||
}
|
||||
};
|
||||
}, [ map ]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!map) return;
|
||||
|
||||
async function ensureIcon(color: string, iconClass: string) {
|
||||
const key = `marker-${color}-${iconClass}`;
|
||||
|
||||
if (!iconSvgCache.has(key)) {
|
||||
const svg = await buildMarkerIcon(color, iconClass);
|
||||
iconSvgCache.set(key, svg);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
const features: GeoJSON.Feature[] = [];
|
||||
for (const childNote of childNotes) {
|
||||
const location = childNote.getLabelValue(LOCATION_ATTRIBUTE);
|
||||
const latLng = location?.split(",", 2).map((el) => parseFloat(el)) as [ number, number ] | undefined;
|
||||
if (!latLng) continue;
|
||||
latLng.reverse();
|
||||
|
||||
const color = childNote.getLabelValue("color") ?? DEFAULT_MARKER_COLOR;
|
||||
features.push({
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: latLng
|
||||
},
|
||||
properties: {
|
||||
id: childNote.noteId,
|
||||
name: childNote.title,
|
||||
icon: await ensureIcon(color, childNote.getIcon())
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Build all the icons.
|
||||
await Promise.all(iconSvgCache.entries().map(async ([ key, svg ]) => {
|
||||
const image = await svgToImage(svg);
|
||||
map.addImage(key, image, {
|
||||
pixelRatio: window.devicePixelRatio
|
||||
});
|
||||
}));
|
||||
|
||||
// Update the source
|
||||
const source = map.getSource<GeoJSONSource>(MARKER_SOURCE);
|
||||
source?.setData({
|
||||
type: "FeatureCollection",
|
||||
features
|
||||
});
|
||||
}, [ childNotes, map ]);
|
||||
|
||||
// Refresh the data.
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [ refresh ]);
|
||||
return null;
|
||||
}
|
||||
|
||||
//#region Renderer
|
||||
async function buildMarkerIcon(color: string, iconClass: string, scale = window.devicePixelRatio || 1) {
|
||||
const iconUrl = iconClassToBitmapCache.get(iconClass) ?? await snapshotIcon(iconClass, MARKER_ICON_SIZE * scale);
|
||||
return `\
|
||||
<svg width="${MARKER_WIDTH * scale}" height="${41 * scale}" viewBox="0 0 ${MARKER_WIDTH} 41" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="black" flood-opacity="0.4" />
|
||||
</filter>
|
||||
</defs>
|
||||
<path d="M12.5 0C5.6 0 0 5.6 0 12.5C0 21.9 12.5 41 12.5 41S25 21.9 25 12.5C25 5.6 19.4 0 12.5 0Z" filter="url(#shadow)" fill="${color}" />
|
||||
<image href="${iconUrl}" x="${MARKER_ICON_X}" y="4" width="${MARKER_ICON_SIZE}" height="${MARKER_ICON_SIZE}" preserveAspectRatio="xMidYMid meet" />
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
async function snapshotIcon(iconClass: string, size: number) {
|
||||
const cachedIcon = iconClassToBitmapCache.get(iconClass);
|
||||
if (cachedIcon) return cachedIcon;
|
||||
|
||||
await document.fonts.ready;
|
||||
const glyph = getGlyphFromClass(iconClass);
|
||||
const rendered = renderMarkerCanvas({
|
||||
color: "black",
|
||||
glyph,
|
||||
size
|
||||
});
|
||||
const dataUrl = rendered?.toDataURL();
|
||||
iconClassToBitmapCache.set(iconClass, dataUrl);
|
||||
return dataUrl;
|
||||
}
|
||||
|
||||
function renderMarkerCanvas({
|
||||
color,
|
||||
glyph, // e.g. "\uf123"
|
||||
size = 32,
|
||||
scale = window.devicePixelRatio || 1
|
||||
}) {
|
||||
const canvas = document.createElement("canvas");
|
||||
|
||||
// High-DPI canvas
|
||||
canvas.width = size * scale;
|
||||
canvas.height = size * scale;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return null;
|
||||
|
||||
// Scale for retina
|
||||
ctx.scale(scale, scale);
|
||||
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
|
||||
// Set font
|
||||
ctx.font = `${size}px ${glyph.fontFamily}`;
|
||||
ctx.fillStyle = color;
|
||||
|
||||
// Measure glyph
|
||||
const metrics = ctx.measureText(glyph.content);
|
||||
|
||||
const glyphWidth =
|
||||
metrics.actualBoundingBoxLeft +
|
||||
metrics.actualBoundingBoxRight;
|
||||
|
||||
const glyphHeight =
|
||||
metrics.actualBoundingBoxAscent +
|
||||
metrics.actualBoundingBoxDescent;
|
||||
|
||||
// Center position
|
||||
const x = (size - glyphWidth) / 2 + metrics.actualBoundingBoxLeft;
|
||||
const y = (size - glyphHeight) / 2 + metrics.actualBoundingBoxAscent;
|
||||
|
||||
// Draw
|
||||
ctx.fillText(glyph.content, x, y);
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function getGlyphFromClass(iconClass: string) {
|
||||
const el = document.createElement("span");
|
||||
el.className = iconClass;
|
||||
|
||||
document.body.appendChild(el);
|
||||
|
||||
const style = window.getComputedStyle(el, "::before");
|
||||
const content = style.getPropertyValue("content");
|
||||
const fontFamily = style.getPropertyValue("font-family");
|
||||
|
||||
document.body.removeChild(el);
|
||||
|
||||
if (!content || content === "none") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// content is usually quoted like: '"\f123"'
|
||||
return {
|
||||
fontFamily,
|
||||
content: content.replace(/^["']|["']$/g, "")
|
||||
};
|
||||
}
|
||||
|
||||
function svgToImage(svgString: string){
|
||||
return new Promise<HTMLImageElement>(resolve => {
|
||||
const svgBlob = new Blob([svgString], { type: "image/svg+xml" });
|
||||
const url = URL.createObjectURL(svgBlob);
|
||||
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
resolve(img);
|
||||
};
|
||||
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
44
apps/client/src/widgets/collections/geomap/Tooltips.tsx
Normal file
44
apps/client/src/widgets/collections/geomap/Tooltips.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { type MapGeoJSONFeature, MapMouseEvent, Popup } from "maplibre-gl";
|
||||
import { useContext, useEffect } from "preact/hooks";
|
||||
|
||||
import { ParentMap } from "./map";
|
||||
import { MARKER_LAYER } from "./Markers";
|
||||
|
||||
export default function Tooltips() {
|
||||
const map = useContext(ParentMap);
|
||||
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
const tooltip = new Popup({
|
||||
closeButton: false,
|
||||
closeOnClick: false,
|
||||
offset: 12,
|
||||
className: "marker-tooltip"
|
||||
});
|
||||
|
||||
function onMouseEnter(e: MapMouseEvent & { features?: MapGeoJSONFeature[]; }) {
|
||||
const feature = e.features?.[0];
|
||||
if (!feature || !map || feature.geometry.type !== "Point") return;
|
||||
|
||||
tooltip
|
||||
.setLngLat(feature.geometry.coordinates as [ number, number ])
|
||||
.setHTML(`<strong>${feature.properties.name}</strong>`)
|
||||
.addTo(map);
|
||||
}
|
||||
|
||||
function onMouseLeave() {
|
||||
tooltip.remove();
|
||||
}
|
||||
|
||||
map.on("mouseenter", MARKER_LAYER, onMouseEnter);
|
||||
map.on("mouseleave", MARKER_LAYER, onMouseLeave);
|
||||
|
||||
return () => {
|
||||
map.off("mouseenter", MARKER_LAYER, onMouseEnter);
|
||||
map.off("mouseleave", MARKER_LAYER, onMouseLeave);
|
||||
};
|
||||
}, [ map ]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
import type { LatLng, LeafletMouseEvent } from "leaflet";
|
||||
import { LOCATION_ATTRIBUTE } from ".";
|
||||
import { CreateChildrenResponse } from "@triliumnext/commons";
|
||||
|
||||
import attributes from "../../../services/attributes";
|
||||
import { prompt } from "../../../services/dialog";
|
||||
import server from "../../../services/server";
|
||||
import { t } from "../../../services/i18n";
|
||||
import { CreateChildrenResponse } from "@triliumnext/commons";
|
||||
import server from "../../../services/server";
|
||||
import type { GeoMouseEvent } from "./map";
|
||||
import { LOCATION_ATTRIBUTE } from "./Markers";
|
||||
|
||||
const CHILD_NOTE_ICON = "bx bx-pin";
|
||||
|
||||
export async function moveMarker(noteId: string, latLng: LatLng | null) {
|
||||
export async function moveMarker(noteId: string, latLng: { lat: number; lng: number } | null) {
|
||||
const value = latLng ? [latLng.lat, latLng.lng].join(",") : "";
|
||||
await attributes.setLabel(noteId, LOCATION_ATTRIBUTE, value);
|
||||
}
|
||||
|
||||
export async function createNewNote(noteId: string, e: LeafletMouseEvent) {
|
||||
export async function createNewNote(noteId: string, e: GeoMouseEvent) {
|
||||
const title = await prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") });
|
||||
|
||||
if (title?.trim()) {
|
||||
|
||||
@@ -7,50 +7,41 @@
|
||||
|
||||
> .collection-properties {
|
||||
position: relative;
|
||||
z-index: 2000;
|
||||
}
|
||||
}
|
||||
|
||||
.geo-map-container {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.leaflet-pane {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
z-index: 997;
|
||||
.maplibregl-canvas-container {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.geo-view.placing-note .geo-map-container {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.geo-map-container .marker-pin {
|
||||
.geo-map-container .geo-marker {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.geo-map-container .leaflet-div-icon {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.geo-map-container .leaflet-div-icon .icon-shadow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
z-index: -1;
|
||||
.geo-map-container .geo-marker .marker-pin {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.geo-map-container .leaflet-div-icon .tn-icon {
|
||||
.geo-map-container .geo-marker .marker-pin svg {
|
||||
display: block;
|
||||
filter: drop-shadow(2px 2px 2px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
|
||||
.geo-map-container .geo-marker .tn-icon {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
inset-inline-start: 2px;
|
||||
inset-inline-start: 4px;
|
||||
background-color: white;
|
||||
color: var(--light-theme-custom-color, black);
|
||||
padding: 2px;
|
||||
@@ -58,7 +49,7 @@
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.geo-map-container .leaflet-div-icon .title-label {
|
||||
.geo-map-container .geo-marker .title-label {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
@@ -71,19 +62,19 @@
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 1px 0 white;
|
||||
white-space: no-wrap;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body[dir=rtl] .geo-map-container .leaflet-div-icon .title-label {
|
||||
body[dir=rtl] .geo-map-container .geo-marker .title-label {
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
.geo-map-container .leaflet-div-icon .archived {
|
||||
.geo-map-container .geo-marker .archived {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.geo-map-container.dark .leaflet-div-icon .title-label {
|
||||
.geo-map-container.dark .geo-marker .title-label {
|
||||
color: white;
|
||||
text-shadow: -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black, 1px 1px 0 black;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import "./index.css";
|
||||
|
||||
import { divIcon, GPXOptions, LatLng, LeafletMouseEvent } from "leaflet";
|
||||
import markerIcon from "leaflet/dist/images/marker-icon.png";
|
||||
import markerIconShadow from "leaflet/dist/images/marker-shadow.png";
|
||||
import type { Map as MapLibreGL } from "maplibre-gl";
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext from "../../../components/app_context";
|
||||
@@ -15,23 +13,24 @@ import toast from "../../../services/toast";
|
||||
import CollectionProperties from "../../note_bars/CollectionProperties";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import { ButtonOrActionButton } from "../../react/Button";
|
||||
import { useNoteBlob, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useNoteTreeDrag, useSpacedUpdate, useTriliumEvent } from "../../react/hooks";
|
||||
import { useNoteBlob, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useNoteTreeDrag, useSpacedUpdate, useTooltip, useTriliumEvent } from "../../react/hooks";
|
||||
import { ParentComponent } from "../../react/react_utils";
|
||||
import TouchBar, { TouchBarButton, TouchBarSlider } from "../../react/TouchBar";
|
||||
import { ViewModeProps } from "../interface";
|
||||
import { createNewNote, moveMarker } from "./api";
|
||||
import openContextMenu, { openMapContextMenu } from "./context_menu";
|
||||
import Map from "./map";
|
||||
import { DEFAULT_MAP_LAYER_NAME } from "./map_layer";
|
||||
import ContextMenus from "./ContextMenus";
|
||||
import Map, { GeoMouseEvent } from "./map";
|
||||
import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS, MapLayer } from "./map_layer";
|
||||
import Marker, { GpxTrack } from "./marker";
|
||||
import Markers, { LOCATION_ATTRIBUTE } from "./Markers";
|
||||
import Tooltips from "./Tooltips";
|
||||
|
||||
const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659];
|
||||
const DEFAULT_ZOOM = 2;
|
||||
export const LOCATION_ATTRIBUTE = "geolocation";
|
||||
|
||||
interface MapData {
|
||||
view?: {
|
||||
center?: LatLng | [number, number];
|
||||
center?: { lat: number; lng: number } | [number, number];
|
||||
zoom?: number;
|
||||
};
|
||||
}
|
||||
@@ -45,10 +44,11 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
|
||||
const [ state, setState ] = useState(State.Normal);
|
||||
const [ coordinates, setCoordinates ] = useState(viewConfig?.view?.center);
|
||||
const [ zoom, setZoom ] = useState(viewConfig?.view?.zoom);
|
||||
const [ layerName ] = useNoteLabel(note, "map:style");
|
||||
const [ hasScale ] = useNoteLabelBoolean(note, "map:scale");
|
||||
const [ hideLabels ] = useNoteLabelBoolean(note, "map:hideLabels");
|
||||
const [ isReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const [ notes, setNotes ] = useState<FNote[]>([]);
|
||||
const layerData = useLayerData(note);
|
||||
const spacedUpdate = useSpacedUpdate(() => {
|
||||
if (viewConfig) {
|
||||
saveConfig(viewConfig);
|
||||
@@ -89,7 +89,7 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
|
||||
moveMarker(noteId, null);
|
||||
});
|
||||
|
||||
const onClick = useCallback(async (e: LeafletMouseEvent) => {
|
||||
const onClick = useCallback(async (e: GeoMouseEvent) => {
|
||||
if (state === State.NewNote) {
|
||||
toast.closePersistent("geo-new-note");
|
||||
await createNewNote(note.noteId, e);
|
||||
@@ -97,13 +97,9 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
|
||||
}
|
||||
}, [ state ]);
|
||||
|
||||
const onContextMenu = useCallback((e: LeafletMouseEvent) => {
|
||||
openMapContextMenu(note.noteId, e, !isReadOnly);
|
||||
}, [ note.noteId, isReadOnly ]);
|
||||
|
||||
// Dragging
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const apiRef = useRef<L.Map>(null);
|
||||
const apiRef = useRef<MapLibreGL | null>(null);
|
||||
useNoteTreeDrag(containerRef, {
|
||||
dragEnabled: !isReadOnly,
|
||||
dragNotEnabledMessage: {
|
||||
@@ -120,15 +116,15 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
|
||||
const offset = containerRef.current?.getBoundingClientRect();
|
||||
const x = e.clientX - (offset?.left ?? 0);
|
||||
const y = e.clientY - (offset?.top ?? 0);
|
||||
const latlng = api.containerPointToLatLng([ x, y ]);
|
||||
const lngLat = api.unproject([x, y]);
|
||||
|
||||
const targetNote = await froca.getNote(noteId, true);
|
||||
const parents = targetNote?.getParentNoteIds();
|
||||
if (parents?.includes(note.noteId)) {
|
||||
await moveMarker(noteId, latlng);
|
||||
await moveMarker(noteId, { lat: lngLat.lat, lng: lngLat.lng });
|
||||
} else {
|
||||
await branches.cloneNoteToParentNote(noteId, noteId);
|
||||
await moveMarker(noteId, latlng);
|
||||
await moveMarker(noteId, { lat: lngLat.lat, lng: lngLat.lng });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -152,23 +148,46 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
|
||||
apiRef={apiRef} containerRef={containerRef}
|
||||
coordinates={coordinates}
|
||||
zoom={zoom}
|
||||
layerName={layerName ?? DEFAULT_MAP_LAYER_NAME}
|
||||
layerData={layerData}
|
||||
viewportChanged={(coordinates, zoom) => {
|
||||
if (!viewConfig) viewConfig = {};
|
||||
viewConfig.view = { center: coordinates, zoom };
|
||||
spacedUpdate.scheduleUpdate();
|
||||
}}
|
||||
onClick={onClick}
|
||||
onContextMenu={onContextMenu}
|
||||
scale={hasScale}
|
||||
>
|
||||
{notes.map(note => <NoteWrapper note={note} isReadOnly={isReadOnly} />)}
|
||||
<Tooltips />
|
||||
<ContextMenus note={note} isReadOnly={isReadOnly} />
|
||||
<Markers note={note} hideLabels={hideLabels} />
|
||||
</Map>}
|
||||
<GeoMapTouchBar state={state} map={apiRef.current} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useLayerData(note: FNote) {
|
||||
const [ layerName ] = useNoteLabel(note, "map:style");
|
||||
// Memo is needed because it would generate unnecessary reloads due to layer change.
|
||||
const layerData = useMemo(() => {
|
||||
// Custom layers.
|
||||
if (layerName?.startsWith("http")) {
|
||||
return {
|
||||
name: "Custom",
|
||||
type: "raster",
|
||||
url: layerName,
|
||||
attribution: ""
|
||||
} satisfies MapLayer;
|
||||
}
|
||||
|
||||
// Built-in layers.
|
||||
const layerData = MAP_LAYERS[layerName ?? ""] ?? MAP_LAYERS[DEFAULT_MAP_LAYER_NAME];
|
||||
return layerData;
|
||||
}, [ layerName ]);
|
||||
|
||||
return layerData;
|
||||
}
|
||||
|
||||
function ToggleReadOnlyButton({ note }: { note: FNote }) {
|
||||
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
|
||||
@@ -179,31 +198,36 @@ function ToggleReadOnlyButton({ note }: { note: FNote }) {
|
||||
/>;
|
||||
}
|
||||
|
||||
function NoteWrapper({ note, isReadOnly }: { note: FNote, isReadOnly: boolean }) {
|
||||
function NoteWrapper({ note, isReadOnly, hideLabels }: {
|
||||
note: FNote,
|
||||
isReadOnly: boolean,
|
||||
hideLabels: boolean
|
||||
}) {
|
||||
const mime = useNoteProperty(note, "mime");
|
||||
const [ location ] = useNoteLabel(note, LOCATION_ATTRIBUTE);
|
||||
|
||||
if (mime === "application/gpx+xml") {
|
||||
return <NoteGpxTrack note={note} />;
|
||||
return <NoteGpxTrack note={note} hideLabels={hideLabels} />;
|
||||
}
|
||||
|
||||
if (location) {
|
||||
const latLng = location?.split(",", 2).map((el) => parseFloat(el)) as [ number, number ] | undefined;
|
||||
if (!latLng) return;
|
||||
return <NoteMarker note={note} editable={!isReadOnly} latLng={latLng} />;
|
||||
return <NoteMarker note={note} editable={!isReadOnly} latLng={latLng} hideLabels={hideLabels} />;
|
||||
}
|
||||
}
|
||||
|
||||
function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean, latLng: [number, number] }) {
|
||||
function NoteMarker({ note, editable, latLng, hideLabels }: { note: FNote, editable: boolean, latLng: [number, number], hideLabels: boolean }) {
|
||||
// React to changes
|
||||
const [ color ] = useNoteLabel(note, "color");
|
||||
const [ iconClass ] = useNoteLabel(note, "iconClass");
|
||||
const [ archived ] = useNoteLabelBoolean(note, "archived");
|
||||
|
||||
const title = useNoteProperty(note, "title");
|
||||
const icon = useMemo(() => {
|
||||
return buildIcon(note.getIcon(), note.getColorClass() ?? undefined, title, note.noteId, archived);
|
||||
}, [ iconClass, color, title, note.noteId, archived]);
|
||||
const iconHtml = useMemo(() => {
|
||||
const titleOrNone = hideLabels ? undefined : title;
|
||||
return buildIconHtml(note.getIcon(), note.getColorClass() ?? undefined, titleOrNone, note.noteId, archived);
|
||||
}, [ note, iconClass, color, title, archived, hideLabels ]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId });
|
||||
@@ -218,15 +242,17 @@ function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean
|
||||
}
|
||||
}, [ note.noteId ]);
|
||||
|
||||
const onDragged = useCallback((newCoordinates: LatLng) => {
|
||||
const onDragged = useCallback((newCoordinates: { lat: number; lng: number }) => {
|
||||
moveMarker(note.noteId, newCoordinates);
|
||||
}, [ note.noteId ]);
|
||||
|
||||
const onContextMenu = useCallback((e: LeafletMouseEvent) => openContextMenu(note.noteId, e, editable), [ note.noteId, editable ]);
|
||||
const onContextMenu = useCallback((e: GeoMouseEvent) => openContextMenu(note.noteId, e, editable), [ note.noteId, editable ]);
|
||||
|
||||
return latLng && <Marker
|
||||
coordinates={latLng}
|
||||
icon={icon}
|
||||
iconHtml={iconHtml}
|
||||
iconSize={[25, 41]}
|
||||
iconAnchor={[12, 41]}
|
||||
draggable={editable}
|
||||
onMouseDown={onMouseDown}
|
||||
onDragged={editable ? onDragged : undefined}
|
||||
@@ -235,7 +261,7 @@ function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean
|
||||
/>;
|
||||
}
|
||||
|
||||
function NoteGpxTrack({ note }: { note: FNote }) {
|
||||
function NoteGpxTrack({ note, hideLabels }: { note: FNote, hideLabels?: boolean }) {
|
||||
const [ xmlString, setXmlString ] = useState<string>();
|
||||
const blob = useNoteBlob(note);
|
||||
|
||||
@@ -254,40 +280,34 @@ function NoteGpxTrack({ note }: { note: FNote }) {
|
||||
const color = useNoteLabel(note, "color");
|
||||
const iconClass = useNoteLabel(note, "iconClass");
|
||||
|
||||
const options = useMemo<GPXOptions>(() => ({
|
||||
markers: {
|
||||
startIcon: buildIcon(note.getIcon(), note.getColorClass(), note.title),
|
||||
endIcon: buildIcon("bxs-flag-checkered"),
|
||||
wptIcons: {
|
||||
"": buildIcon("bx bx-pin")
|
||||
}
|
||||
},
|
||||
polyline_options: {
|
||||
color: note.getLabelValue("color") ?? "blue"
|
||||
}
|
||||
}), [ color, iconClass ]);
|
||||
return xmlString && <GpxTrack gpxXmlString={xmlString} options={options} />;
|
||||
const trackColor = useMemo(() => note.getLabelValue("color") ?? "blue", [ color ]);
|
||||
const startIconHtml = useMemo(() => buildIconHtml(note.getIcon(), note.getColorClass() ?? undefined, note.title), [ iconClass, color ]);
|
||||
const endIconHtml = useMemo(() => buildIconHtml("bxs-flag-checkered"), [ ]);
|
||||
const waypointIconHtml = useMemo(() => buildIconHtml("bx bx-pin"), [ ]);
|
||||
|
||||
return xmlString && <GpxTrack
|
||||
gpxXmlString={xmlString}
|
||||
trackColor={trackColor}
|
||||
startIconHtml={startIconHtml}
|
||||
endIconHtml={endIconHtml}
|
||||
waypointIconHtml={waypointIconHtml}
|
||||
/>;
|
||||
}
|
||||
|
||||
function buildIcon(bxIconClass: string, colorClass?: string, title?: string, noteIdLink?: string, archived?: boolean) {
|
||||
function buildIconHtml(bxIconClass: string, colorClass?: string, title?: string, noteIdLink?: string, archived?: boolean) {
|
||||
let html = /*html*/`\
|
||||
<img class="icon" src="${markerIcon}" />
|
||||
<img class="icon-shadow" src="${markerIconShadow}" />
|
||||
<span class="bx ${bxIconClass} ${colorClass ?? ""}"></span>
|
||||
<div class="marker-pin">${MARKER_SVG}</div>
|
||||
<span class="bx ${bxIconClass} tn-icon ${colorClass ?? ""}"></span>
|
||||
<span class="title-label">${title ?? ""}</span>`;
|
||||
|
||||
if (noteIdLink) {
|
||||
html = `<div data-href="#root/${noteIdLink}" class="${archived ? "archived" : ""}">${html}</div>`;
|
||||
}
|
||||
|
||||
return divIcon({
|
||||
html,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41]
|
||||
});
|
||||
return html;
|
||||
}
|
||||
|
||||
function GeoMapTouchBar({ state, map }: { state: State, map: L.Map | null | undefined }) {
|
||||
function GeoMapTouchBar({ state, map }: { state: State, map: MapLibreGL | null | undefined }) {
|
||||
const [ currentZoom, setCurrentZoom ] = useState<number>();
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
|
||||
@@ -299,7 +319,7 @@ function GeoMapTouchBar({ state, map }: { state: State, map: L.Map | null | unde
|
||||
}
|
||||
|
||||
map.on("zoom", onZoomChanged);
|
||||
return () => map.off("zoom", onZoomChanged);
|
||||
return () => { map.off("zoom", onZoomChanged); };
|
||||
}, [ map ]);
|
||||
|
||||
return map && currentZoom && (
|
||||
|
||||
@@ -1,147 +1,208 @@
|
||||
import { useEffect, useImperativeHandle, useRef, useState } from "preact/hooks";
|
||||
import L, { control, LatLng, Layer, LeafletMouseEvent } from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { MAP_LAYERS } from "./map_layer";
|
||||
import { ComponentChildren, createContext, RefObject } from "preact";
|
||||
import { useElementSize, useSyncedRef } from "../../react/hooks";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
|
||||
export const ParentMap = createContext<L.Map | null>(null);
|
||||
import { Map as MapLibreGLMap, MapMouseEvent, NavigationControl, type Point, ScaleControl, StyleSpecification } from "maplibre-gl";
|
||||
import { ComponentChildren, createContext, RefObject } from "preact";
|
||||
import { useEffect, useImperativeHandle, useState } from "preact/hooks";
|
||||
|
||||
import { useElementSize, useSyncedRef } from "../../react/hooks";
|
||||
import { MapLayer } from "./map_layer";
|
||||
|
||||
export interface GeoMouseEvent {
|
||||
latlng: { lat: number; lng: number };
|
||||
originalEvent: MouseEvent;
|
||||
point: Point;
|
||||
}
|
||||
|
||||
export const ParentMap = createContext<MapLibreGLMap | null>(null);
|
||||
|
||||
interface MapProps {
|
||||
apiRef?: RefObject<L.Map | null>;
|
||||
apiRef?: RefObject<MapLibreGLMap | null>;
|
||||
containerRef?: RefObject<HTMLDivElement>;
|
||||
coordinates: LatLng | [number, number];
|
||||
coordinates: { lat: number; lng: number } | [number, number];
|
||||
zoom: number;
|
||||
layerName: string;
|
||||
viewportChanged: (coordinates: LatLng, zoom: number) => void;
|
||||
layerData: MapLayer;
|
||||
viewportChanged: (coordinates: { lat: number; lng: number }, zoom: number) => void;
|
||||
children: ComponentChildren;
|
||||
onClick?: (e: LeafletMouseEvent) => void;
|
||||
onContextMenu?: (e: LeafletMouseEvent) => void;
|
||||
onClick?: (e: GeoMouseEvent) => void;
|
||||
onContextMenu?: (e: GeoMouseEvent) => void;
|
||||
onZoom?: () => void;
|
||||
scale: boolean;
|
||||
}
|
||||
|
||||
export default function Map({ coordinates, zoom, layerName, viewportChanged, children, onClick, onContextMenu, scale, apiRef, containerRef: _containerRef, onZoom }: MapProps) {
|
||||
const mapRef = useRef<L.Map>(null);
|
||||
export function toMapLibreEvent(e: MapMouseEvent): GeoMouseEvent {
|
||||
return {
|
||||
latlng: { lat: e.lngLat.lat, lng: e.lngLat.lng },
|
||||
originalEvent: e.originalEvent,
|
||||
point: e.point
|
||||
};
|
||||
}
|
||||
|
||||
export default function Map({ coordinates, zoom, layerData, viewportChanged, children, onClick, onContextMenu, scale, apiRef, containerRef: _containerRef, onZoom }: MapProps) {
|
||||
const [ map, setMap ] = useState<MapLibreGLMap | null>(null);
|
||||
const containerRef = useSyncedRef<HTMLDivElement>(_containerRef);
|
||||
|
||||
useImperativeHandle(apiRef ?? null, () => mapRef.current);
|
||||
useImperativeHandle(apiRef ?? null, () => map);
|
||||
|
||||
// Initialize the map.
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
const mapInstance = L.map(containerRef.current, {
|
||||
worldCopyJump: false,
|
||||
maxBounds: [
|
||||
[-90, -180],
|
||||
[90, 180]
|
||||
],
|
||||
minZoom: 2
|
||||
|
||||
let style: StyleSpecification | string;
|
||||
|
||||
if (layerData.type === "vector") {
|
||||
style = typeof layerData.style === "string"
|
||||
? layerData.style
|
||||
: layerData.styleFallback;
|
||||
} else {
|
||||
style = {
|
||||
version: 8,
|
||||
sources: {
|
||||
"raster-tiles": {
|
||||
type: "raster",
|
||||
tiles: [layerData.url],
|
||||
tileSize: 256,
|
||||
attribution: layerData.attribution
|
||||
}
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
id: "raster-layer",
|
||||
type: "raster",
|
||||
source: "raster-tiles"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
const center = Array.isArray(coordinates)
|
||||
? [coordinates[1], coordinates[0]] as [number, number]
|
||||
: [coordinates.lng, coordinates.lat] as [number, number];
|
||||
|
||||
const mapInstance = new MapLibreGLMap({
|
||||
container: containerRef.current,
|
||||
style,
|
||||
center,
|
||||
zoom,
|
||||
minZoom: 1,
|
||||
renderWorldCopies: false
|
||||
});
|
||||
|
||||
mapRef.current = mapInstance;
|
||||
// Add navigation buttons.
|
||||
mapInstance.addControl(new NavigationControl({
|
||||
showCompass: false,
|
||||
showZoom: true
|
||||
}), "top-left");
|
||||
|
||||
setMap(mapInstance);
|
||||
|
||||
// Load async vector style if needed.
|
||||
if (layerData.type === "vector" && typeof layerData.style !== "string") {
|
||||
layerData.style().then(asyncStyle => {
|
||||
mapInstance.setStyle(asyncStyle as StyleSpecification);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
mapInstance.off();
|
||||
mapInstance.remove();
|
||||
setMap(null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Load the layer asynchronously.
|
||||
const [ layer, setLayer ] = useState<Layer>();
|
||||
// React to layer changes.
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const layerData = MAP_LAYERS[layerName];
|
||||
if (!map) return;
|
||||
|
||||
if (layerData.type === "vector") {
|
||||
const style = (typeof layerData.style === "string" ? layerData.style : await layerData.style());
|
||||
await import("@maplibre/maplibre-gl-leaflet");
|
||||
|
||||
setLayer(L.maplibreGL({
|
||||
style: style as any
|
||||
}));
|
||||
if (layerData.type === "vector") {
|
||||
if (typeof layerData.style === "string") {
|
||||
map.setStyle(layerData.style);
|
||||
} else {
|
||||
setLayer(L.tileLayer(layerData.url, {
|
||||
attribution: layerData.attribution,
|
||||
detectRetina: true,
|
||||
noWrap: true
|
||||
}));
|
||||
layerData.style().then(asyncStyle => {
|
||||
map.setStyle(asyncStyle as StyleSpecification);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
map.setStyle({
|
||||
version: 8,
|
||||
sources: {
|
||||
"raster-tiles": {
|
||||
type: "raster",
|
||||
tiles: [layerData.url],
|
||||
tileSize: 256,
|
||||
attribution: layerData.attribution
|
||||
}
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
id: "raster-layer",
|
||||
type: "raster",
|
||||
source: "raster-tiles"
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
load();
|
||||
}, [ layerName ]);
|
||||
|
||||
// Attach layer to the map.
|
||||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
const layerToAdd = layer;
|
||||
if (!map || !layerToAdd) return;
|
||||
layerToAdd.addTo(map);
|
||||
return () => layerToAdd.removeFrom(map);
|
||||
}, [ mapRef, layer ]);
|
||||
}, [ map, layerData ]);
|
||||
|
||||
// React to coordinate changes.
|
||||
useEffect(() => {
|
||||
if (!mapRef.current) return;
|
||||
mapRef.current.setView(coordinates, zoom);
|
||||
}, [ mapRef, coordinates, zoom ]);
|
||||
if (!map) return;
|
||||
const center = Array.isArray(coordinates)
|
||||
? [coordinates[1], coordinates[0]] as [number, number]
|
||||
: [coordinates.lng, coordinates.lat] as [number, number];
|
||||
|
||||
map.setCenter(center);
|
||||
map.setZoom(zoom);
|
||||
}, [ map, coordinates, zoom ]);
|
||||
|
||||
// Viewport callback.
|
||||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
|
||||
const updateFn = () => viewportChanged(map.getBounds().getCenter(), map.getZoom());
|
||||
const updateFn = () => {
|
||||
const center = map.getCenter();
|
||||
viewportChanged({ lat: center.lat, lng: center.lng }, map.getZoom());
|
||||
};
|
||||
map.on("moveend", updateFn);
|
||||
map.on("zoomend", updateFn);
|
||||
|
||||
return () => {
|
||||
map.off("moveend", updateFn);
|
||||
map.off("zoomend", updateFn);
|
||||
};
|
||||
}, [ mapRef, viewportChanged ]);
|
||||
}, [ map, viewportChanged ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onClick && mapRef.current) {
|
||||
mapRef.current.on("click", onClick);
|
||||
return () => mapRef.current?.off("click", onClick);
|
||||
}
|
||||
}, [ mapRef, onClick ]);
|
||||
if (!onClick || !map) return;
|
||||
|
||||
const handler = (e: MapMouseEvent) => onClick(toMapLibreEvent(e));
|
||||
map.on("click", handler);
|
||||
return () => { map.off("click", handler); };
|
||||
}, [ map, onClick ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onContextMenu && mapRef.current) {
|
||||
mapRef.current.on("contextmenu", onContextMenu);
|
||||
return () => mapRef.current?.off("contextmenu", onContextMenu);
|
||||
}
|
||||
}, [ mapRef, onContextMenu ]);
|
||||
if (!onZoom || !map) return;
|
||||
|
||||
useEffect(() => {
|
||||
if (onZoom && mapRef.current) {
|
||||
mapRef.current.on("zoom", onZoom);
|
||||
return () => mapRef.current?.off("zoom", onZoom);
|
||||
}
|
||||
}, [ mapRef, onZoom ]);
|
||||
map.on("zoom", onZoom);
|
||||
return () => { map.off("zoom", onZoom); };
|
||||
}, [ map, onZoom ]);
|
||||
|
||||
// Scale
|
||||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
if (!scale || !map) return;
|
||||
const scaleControl = control.scale();
|
||||
scaleControl.addTo(map);
|
||||
return () => scaleControl.remove();
|
||||
}, [ mapRef, scale ]);
|
||||
const scaleControl = new ScaleControl();
|
||||
map.addControl(scaleControl);
|
||||
return () => { map.removeControl(scaleControl); };
|
||||
}, [ map, scale ]);
|
||||
|
||||
// Adapt to container size changes.
|
||||
const size = useElementSize(containerRef);
|
||||
useEffect(() => {
|
||||
mapRef.current?.invalidateSize();
|
||||
}, [ size?.width, size?.height ]);
|
||||
map?.resize();
|
||||
}, [ map, size?.width, size?.height ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`geo-map-container ${MAP_LAYERS[layerName].isDarkTheme ? "dark" : ""}`}
|
||||
className={`geo-map-container ${layerData.isDarkTheme ? "dark" : ""}`}
|
||||
>
|
||||
<ParentMap.Provider value={mapRef.current}>
|
||||
<ParentMap.Provider value={map}>
|
||||
{children}
|
||||
</ParentMap.Provider>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
export interface MapLayer {
|
||||
name: string;
|
||||
isDarkTheme?: boolean;
|
||||
}
|
||||
import { type StyleSpecification } from "maplibre-gl";
|
||||
|
||||
interface VectorLayer extends MapLayer {
|
||||
export type MapLayer = ({
|
||||
type: "vector";
|
||||
style: string | (() => Promise<{}>)
|
||||
}
|
||||
|
||||
interface RasterLayer extends MapLayer {
|
||||
style: string | (() => Promise<StyleSpecification>);
|
||||
styleFallback: StyleSpecification;
|
||||
} | {
|
||||
type: "raster";
|
||||
url: string;
|
||||
attribution: string;
|
||||
}
|
||||
}) & {
|
||||
// Common properties
|
||||
name: string;
|
||||
isDarkTheme?: boolean;
|
||||
};
|
||||
|
||||
export const MAP_LAYERS: Record<string, VectorLayer | RasterLayer> = {
|
||||
// Minimal empty style used as a placeholder while the real style loads asynchronously.
|
||||
const EMPTY_STYLE: StyleSpecification = { version: 8, sources: {}, layers: [] };
|
||||
|
||||
export const MAP_LAYERS: Record<string, MapLayer> = {
|
||||
"openstreetmap": {
|
||||
name: "OpenStreetMap",
|
||||
type: "raster",
|
||||
@@ -24,28 +27,33 @@ export const MAP_LAYERS: Record<string, VectorLayer | RasterLayer> = {
|
||||
"versatiles-colorful": {
|
||||
name: "VersaTiles Colorful",
|
||||
type: "vector",
|
||||
style: async () => (await import("./styles/colorful/en.json")).default
|
||||
style: async () => (await import("./styles/colorful/en.json")).default as unknown as StyleSpecification,
|
||||
styleFallback: EMPTY_STYLE
|
||||
},
|
||||
"versatiles-eclipse": {
|
||||
name: "VersaTiles Eclipse",
|
||||
type: "vector",
|
||||
style: async () => (await import("./styles/eclipse/en.json")).default,
|
||||
style: async () => (await import("./styles/eclipse/en.json")).default as unknown as StyleSpecification,
|
||||
styleFallback: EMPTY_STYLE,
|
||||
isDarkTheme: true
|
||||
},
|
||||
"versatiles-graybeard": {
|
||||
name: "VersaTiles Graybeard",
|
||||
type: "vector",
|
||||
style: async () => (await import("./styles/graybeard/en.json")).default
|
||||
style: async () => (await import("./styles/graybeard/en.json")).default as unknown as StyleSpecification,
|
||||
styleFallback: EMPTY_STYLE,
|
||||
},
|
||||
"versatiles-neutrino": {
|
||||
name: "VersaTiles Neutrino",
|
||||
type: "vector",
|
||||
style: async () => (await import("./styles/neutrino/en.json")).default
|
||||
style: async () => (await import("./styles/neutrino/en.json")).default as unknown as StyleSpecification,
|
||||
styleFallback: EMPTY_STYLE,
|
||||
},
|
||||
"versatiles-shadow": {
|
||||
name: "VersaTiles Shadow",
|
||||
type: "vector",
|
||||
style: async () => (await import("./styles/shadow/en.json")).default,
|
||||
style: async () => (await import("./styles/shadow/en.json")).default as unknown as StyleSpecification,
|
||||
styleFallback: EMPTY_STYLE,
|
||||
isDarkTheme: true
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,71 +1,208 @@
|
||||
import { useContext, useEffect } from "preact/hooks";
|
||||
import { ParentMap } from "./map";
|
||||
import { DivIcon, GPX, GPXOptions, Icon, LatLng, Marker as LeafletMarker, LeafletMouseEvent, marker, MarkerOptions } from "leaflet";
|
||||
import "leaflet-gpx";
|
||||
import { Marker as MapLibreMarker } from "maplibre-gl";
|
||||
import { useContext, useEffect, useRef } from "preact/hooks";
|
||||
|
||||
import { GeoMouseEvent,ParentMap } from "./map";
|
||||
|
||||
export interface MarkerProps {
|
||||
coordinates: [ number, number ];
|
||||
icon?: Icon | DivIcon;
|
||||
iconHtml?: string;
|
||||
iconSize?: [number, number];
|
||||
iconAnchor?: [number, number];
|
||||
onClick?: () => void;
|
||||
onMouseDown?: (e: MouseEvent) => void;
|
||||
onDragged?: ((newCoordinates: LatLng) => void);
|
||||
onContextMenu: (e: LeafletMouseEvent) => void;
|
||||
onDragged?: ((newCoordinates: { lat: number; lng: number }) => void);
|
||||
onContextMenu: (e: GeoMouseEvent) => void;
|
||||
draggable?: boolean;
|
||||
}
|
||||
|
||||
export default function Marker({ coordinates, icon, draggable, onClick, onDragged, onMouseDown, onContextMenu }: MarkerProps) {
|
||||
export default function Marker({ coordinates, iconHtml, iconSize, iconAnchor, draggable, onClick, onDragged, onMouseDown, onContextMenu }: MarkerProps) {
|
||||
const parentMap = useContext(ParentMap);
|
||||
const markerRef = useRef<MapLibreMarker>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!parentMap) return;
|
||||
|
||||
const options: MarkerOptions = { icon };
|
||||
if (draggable) {
|
||||
options.draggable = true;
|
||||
options.autoPan = true;
|
||||
options.autoPanSpeed = 5;
|
||||
const el = document.createElement("div");
|
||||
el.className = "geo-marker";
|
||||
if (iconHtml) {
|
||||
el.innerHTML = iconHtml;
|
||||
}
|
||||
if (iconSize) {
|
||||
el.style.width = `${iconSize[0]}px`;
|
||||
el.style.height = `${iconSize[1]}px`;
|
||||
}
|
||||
|
||||
const newMarker = marker(coordinates, options);
|
||||
const newMarker = new MapLibreMarker({
|
||||
element: el,
|
||||
draggable: !!draggable,
|
||||
anchor: "bottom"
|
||||
})
|
||||
.setLngLat([coordinates[1], coordinates[0]])
|
||||
.addTo(parentMap);
|
||||
|
||||
markerRef.current = newMarker;
|
||||
|
||||
if (onClick) {
|
||||
newMarker.on("click", () => onClick());
|
||||
el.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
});
|
||||
}
|
||||
|
||||
if (onMouseDown) {
|
||||
newMarker.on("mousedown", e => onMouseDown(e.originalEvent));
|
||||
el.addEventListener("mousedown", (e) => {
|
||||
if (e.button === 1) {
|
||||
e.stopPropagation();
|
||||
onMouseDown(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (onDragged) {
|
||||
newMarker.on("moveend", e => {
|
||||
const coordinates = (e.target as LeafletMarker).getLatLng();
|
||||
onDragged(coordinates);
|
||||
newMarker.on("dragend", () => {
|
||||
const lngLat = newMarker.getLngLat();
|
||||
onDragged({ lat: lngLat.lat, lng: lngLat.lng });
|
||||
});
|
||||
}
|
||||
|
||||
if (onContextMenu) {
|
||||
newMarker.on("contextmenu", e => onContextMenu(e))
|
||||
el.addEventListener("contextmenu", (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const lngLat = newMarker.getLngLat();
|
||||
onContextMenu({
|
||||
latlng: { lat: lngLat.lat, lng: lngLat.lng },
|
||||
originalEvent: e
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
newMarker.addTo(parentMap);
|
||||
return () => {
|
||||
newMarker.remove();
|
||||
markerRef.current = null;
|
||||
};
|
||||
}, [ parentMap, coordinates, onMouseDown, onDragged, iconHtml ]);
|
||||
|
||||
return () => newMarker.removeFrom(parentMap);
|
||||
}, [ parentMap, coordinates, onMouseDown, onDragged, icon ]);
|
||||
|
||||
return (<div />)
|
||||
return (<div />);
|
||||
}
|
||||
|
||||
export function GpxTrack({ gpxXmlString, options }: { gpxXmlString: string, options: GPXOptions }) {
|
||||
export interface GpxTrackProps {
|
||||
gpxXmlString: string;
|
||||
trackColor?: string;
|
||||
startIconHtml?: string;
|
||||
endIconHtml?: string;
|
||||
waypointIconHtml?: string;
|
||||
}
|
||||
|
||||
export function GpxTrack({ gpxXmlString, trackColor, startIconHtml, endIconHtml, waypointIconHtml }: GpxTrackProps) {
|
||||
const parentMap = useContext(ParentMap);
|
||||
|
||||
useEffect(() => {
|
||||
if (!parentMap) return;
|
||||
|
||||
const track = new GPX(gpxXmlString, options);
|
||||
track.addTo(parentMap);
|
||||
const markers: MapLibreMarker[] = [];
|
||||
const sourceId = `gpx-source-${Math.random().toString(36).slice(2)}`;
|
||||
const layerId = `gpx-layer-${sourceId}`;
|
||||
|
||||
return () => track.removeFrom(parentMap);
|
||||
}, [ parentMap, gpxXmlString, options ]);
|
||||
function addGpxToMap() {
|
||||
const parser = new DOMParser();
|
||||
const gpxDoc = parser.parseFromString(gpxXmlString, "application/xml");
|
||||
|
||||
// Parse tracks.
|
||||
const coordinates: [number, number][] = [];
|
||||
const trackPoints = gpxDoc.querySelectorAll("trkpt, rtept");
|
||||
for (const pt of trackPoints) {
|
||||
const lat = parseFloat(pt.getAttribute("lat") ?? "0");
|
||||
const lon = parseFloat(pt.getAttribute("lon") ?? "0");
|
||||
coordinates.push([lon, lat]);
|
||||
}
|
||||
|
||||
// Add GeoJSON line for the track.
|
||||
if (coordinates.length > 0) {
|
||||
parentMap.addSource(sourceId, {
|
||||
type: "geojson",
|
||||
data: {
|
||||
type: "Feature",
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
parentMap.addLayer({
|
||||
id: layerId,
|
||||
type: "line",
|
||||
source: sourceId,
|
||||
paint: {
|
||||
"line-color": trackColor ?? "blue",
|
||||
"line-width": 3
|
||||
}
|
||||
});
|
||||
|
||||
// Start marker
|
||||
if (startIconHtml) {
|
||||
const startEl = document.createElement("div");
|
||||
startEl.className = "geo-marker";
|
||||
startEl.innerHTML = startIconHtml;
|
||||
const startMarker = new MapLibreMarker({ element: startEl, anchor: "bottom" })
|
||||
.setLngLat(coordinates[0])
|
||||
.addTo(parentMap);
|
||||
markers.push(startMarker);
|
||||
}
|
||||
|
||||
// End marker
|
||||
if (endIconHtml && coordinates.length > 1) {
|
||||
const endEl = document.createElement("div");
|
||||
endEl.className = "geo-marker";
|
||||
endEl.innerHTML = endIconHtml;
|
||||
const endMarker = new MapLibreMarker({ element: endEl, anchor: "bottom" })
|
||||
.setLngLat(coordinates[coordinates.length - 1])
|
||||
.addTo(parentMap);
|
||||
markers.push(endMarker);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse waypoints.
|
||||
const waypoints = gpxDoc.querySelectorAll("wpt");
|
||||
for (const wpt of waypoints) {
|
||||
const lat = parseFloat(wpt.getAttribute("lat") ?? "0");
|
||||
const lon = parseFloat(wpt.getAttribute("lon") ?? "0");
|
||||
if (waypointIconHtml) {
|
||||
const wptEl = document.createElement("div");
|
||||
wptEl.className = "geo-marker";
|
||||
wptEl.innerHTML = waypointIconHtml;
|
||||
const wptMarker = new MapLibreMarker({ element: wptEl, anchor: "bottom" })
|
||||
.setLngLat([lon, lat])
|
||||
.addTo(parentMap);
|
||||
markers.push(wptMarker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parentMap.isStyleLoaded()) {
|
||||
addGpxToMap();
|
||||
} else {
|
||||
parentMap.once("style.load", addGpxToMap);
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const m of markers) {
|
||||
m.remove();
|
||||
}
|
||||
try {
|
||||
if (parentMap.getLayer(layerId)) {
|
||||
parentMap.removeLayer(layerId);
|
||||
}
|
||||
if (parentMap.getSource(sourceId)) {
|
||||
parentMap.removeSource(sourceId);
|
||||
}
|
||||
} catch {
|
||||
// Map may be already removed.
|
||||
}
|
||||
};
|
||||
}, [ parentMap, gpxXmlString, trackColor, startIconHtml, endIconHtml, waypointIconHtml ]);
|
||||
|
||||
return <div />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.note-list {
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -128,6 +128,10 @@
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
body.mobile .note-list.grid-view .note-book-card {
|
||||
flex-basis: 150px;
|
||||
}
|
||||
|
||||
.note-list.grid-view .note-book-card {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { EventData } from "../../components/app_context.js";
|
||||
import { LOCALES } from "@triliumnext/commons";
|
||||
import { readCssVar } from "../../utils/css-var.js";
|
||||
import FlexContainer from "./flex_container.js";
|
||||
import options from "../../services/options.js";
|
||||
import type BasicWidget from "../basic_widget.js";
|
||||
import utils from "../../services/utils.js";
|
||||
|
||||
import { EventData } from "../../components/app_context.js";
|
||||
import { getEnabledExperimentalFeatureIds } from "../../services/experimental_features.js";
|
||||
import options from "../../services/options.js";
|
||||
import utils, { isMobile } from "../../services/utils.js";
|
||||
import { readCssVar } from "../../utils/css-var.js";
|
||||
import type BasicWidget from "../basic_widget.js";
|
||||
import FlexContainer from "./flex_container.js";
|
||||
|
||||
/**
|
||||
* The root container is the top-most widget/container, from which the entire layout derives.
|
||||
@@ -17,14 +18,12 @@ import { getEnabledExperimentalFeatureIds } from "../../services/experimental_fe
|
||||
* - `#root-container.vertical-layout`, if the current layout is horizontal.
|
||||
*/
|
||||
export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
private originalViewportHeight: number;
|
||||
|
||||
constructor(isHorizontalLayout: boolean) {
|
||||
super(isHorizontalLayout ? "column" : "row");
|
||||
|
||||
this.id("root-widget");
|
||||
this.css("height", "100dvh");
|
||||
this.originalViewportHeight = getViewportHeight();
|
||||
}
|
||||
|
||||
render(): JQuery<HTMLElement> {
|
||||
@@ -39,6 +38,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
this.#setThemeCapabilities();
|
||||
this.#setLocaleAndDirection(options.get("locale"));
|
||||
this.#setExperimentalFeatures();
|
||||
this.#initPWATopbarColor();
|
||||
|
||||
return super.render();
|
||||
}
|
||||
@@ -64,8 +64,12 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
}
|
||||
|
||||
#onMobileResize() {
|
||||
const currentViewportHeight = getViewportHeight();
|
||||
const isKeyboardOpened = (currentViewportHeight < this.originalViewportHeight);
|
||||
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
// If viewport is significantly smaller, keyboard is likely open
|
||||
const isKeyboardOpened = windowHeight - viewportHeight > 150;
|
||||
|
||||
this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened);
|
||||
}
|
||||
|
||||
@@ -88,7 +92,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
}
|
||||
|
||||
#setBackdropEffects() {
|
||||
const enabled = options.is("backdropEffectsEnabled");
|
||||
const enabled = options.is("backdropEffectsEnabled") && !isMobile();
|
||||
document.body.classList.toggle("backdrop-effects-disabled", !enabled);
|
||||
}
|
||||
|
||||
@@ -96,7 +100,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
// Supports background effects
|
||||
|
||||
const useBgfx = readCssVar(document.documentElement, "allow-background-effects")
|
||||
.asBoolean(false);
|
||||
.asBoolean(false);
|
||||
|
||||
document.body.classList.toggle("theme-supports-background-effects", useBgfx);
|
||||
}
|
||||
@@ -112,8 +116,23 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
document.body.lang = locale;
|
||||
document.body.dir = correspondingLocale?.rtl ? "rtl" : "ltr";
|
||||
}
|
||||
|
||||
#initPWATopbarColor() {
|
||||
if (!utils.isPWA()) return;
|
||||
const tracker = $("#background-color-tracker");
|
||||
|
||||
if (tracker.length) {
|
||||
const applyThemeColor = () => {
|
||||
let meta = $("meta[name='theme-color']");
|
||||
if (!meta.length) {
|
||||
meta = $(`<meta name="theme-color">`).appendTo($("head"));
|
||||
}
|
||||
meta.attr("content", tracker.css("color"));
|
||||
};
|
||||
|
||||
tracker.on("transitionend", applyThemeColor);
|
||||
applyThemeColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getViewportHeight() {
|
||||
return window.visualViewport?.height ?? window.innerHeight;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
.scrolling-container {
|
||||
--content-margin-inline: 24px;
|
||||
|
||||
overflow: auto;
|
||||
scroll-behavior: smooth;
|
||||
position: relative;
|
||||
|
||||
> .inline-title,
|
||||
> .note-detail > .note-detail-editable-text,
|
||||
> .note-detail > .note-detail-editable-text > .note-detail-editable-text-editor,
|
||||
> .note-list-widget:not(.full-height) .note-list-wrapper {
|
||||
padding-inline: 24px;
|
||||
margin-inline: var(--content-margin-inline);
|
||||
}
|
||||
|
||||
> .inline-title {
|
||||
padding-inline: var(--content-margin-inline);
|
||||
}
|
||||
|
||||
> .note-detail > .note-detail-editable-text > .note-detail-editable-text-editor {
|
||||
overflow: unset;
|
||||
}
|
||||
}
|
||||
|
||||
body.mobile .scrolling-container {
|
||||
--content-margin-inline: 8px;
|
||||
}
|
||||
|
||||
.note-split.type-code:not(.mime-text-x-sqlite) {
|
||||
|
||||
@@ -32,6 +32,12 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-block: 0;
|
||||
|
||||
.note-icon-widget {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .modal-header .note-title-widget {
|
||||
@@ -91,8 +97,9 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-detail-editable-text {
|
||||
padding: 0 1em;
|
||||
.modal.popup-editor-dialog .note-detail-editable-text-editor {
|
||||
margin: 0 28px;
|
||||
overflow: visible; /* Allow selection rectangle to go outside of the editor area */
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-detail-file {
|
||||
|
||||
@@ -5,13 +5,15 @@ import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||
import froca from "../../services/froca";
|
||||
import { t } from "../../services/i18n";
|
||||
import tree from "../../services/tree";
|
||||
import utils from "../../services/utils";
|
||||
import NoteList from "../collections/NoteList";
|
||||
import FloatingButtons from "../FloatingButtons";
|
||||
import { DESKTOP_FLOATING_BUTTONS, MOBILE_FLOATING_BUTTONS, POPUP_HIDDEN_FLOATING_BUTTONS } from "../FloatingButtonsDefinitions";
|
||||
import { DESKTOP_FLOATING_BUTTONS, POPUP_HIDDEN_FLOATING_BUTTONS } from "../FloatingButtonsDefinitions";
|
||||
import NoteBadges from "../layout/NoteBadges";
|
||||
import NoteIcon from "../note_icon";
|
||||
import NoteTitleWidget from "../note_title";
|
||||
import NoteDetail from "../NoteDetail";
|
||||
@@ -23,8 +25,6 @@ import ReadOnlyNoteInfoBar from "../ReadOnlyNoteInfoBar";
|
||||
import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter";
|
||||
import FormattingToolbar from "../ribbon/FormattingToolbar";
|
||||
import MobileEditorToolbar from "../type_widgets/text/mobile_editor_toolbar";
|
||||
import NoteBadges from "../layout/NoteBadges";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function PopupEditor() {
|
||||
const [ noteContext, setNoteContext ] = useState(new NoteContext("_popup-editor"));
|
||||
const isMobile = utils.isMobile();
|
||||
const items = useMemo(() => {
|
||||
const baseItems = isMobile ? MOBILE_FLOATING_BUTTONS : DESKTOP_FLOATING_BUTTONS;
|
||||
const baseItems = isMobile ? [] : DESKTOP_FLOATING_BUTTONS;
|
||||
return baseItems.filter(item => !POPUP_HIDDEN_FLOATING_BUTTONS.includes(item));
|
||||
}, [ isMobile ]);
|
||||
|
||||
@@ -67,10 +67,7 @@ export default function PopupEditor() {
|
||||
<NoteContextContext.Provider value={noteContext}>
|
||||
<DialogWrapper>
|
||||
<Modal
|
||||
title={<>
|
||||
<TitleRow />
|
||||
{isNewLayout && <NoteBadges />}
|
||||
</>}
|
||||
title={<TitleRow />}
|
||||
customTitleBarButtons={[{
|
||||
iconClassName: "bx-expand-alt",
|
||||
title: t("popup-editor.maximize"),
|
||||
@@ -123,6 +120,7 @@ export function TitleRow() {
|
||||
<div className="title-row">
|
||||
<NoteIcon />
|
||||
<NoteTitleWidget />
|
||||
{isNewLayout && <NoteBadges />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { useContext, useMemo } from "preact/hooks";
|
||||
import { LaunchBarContext, LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
import { CSSProperties } from "preact";
|
||||
import type FNote from "../../entities/fnote";
|
||||
import { useChildNotes, useNoteLabelBoolean } from "../react/hooks";
|
||||
import "./BookmarkButtons.css";
|
||||
|
||||
import { CSSProperties } from "preact";
|
||||
import { useContext, useMemo } from "preact/hooks";
|
||||
|
||||
import type FNote from "../../entities/fnote";
|
||||
import { t } from "../../services/i18n";
|
||||
import { FormDropdownSubmenu, FormListItem } from "../react/FormList";
|
||||
import { useChildNotes, useNote, useNoteIcon, useNoteLabelBoolean } from "../react/hooks";
|
||||
import NoteLink from "../react/NoteLink";
|
||||
import { CustomNoteLauncher } from "./GenericButtons";
|
||||
import ResponsiveContainer from "../react/ResponsiveContainer";
|
||||
import { CustomNoteLauncher, launchCustomNoteLauncher } from "./GenericButtons";
|
||||
import { LaunchBarContext, LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
|
||||
const PARENT_NOTE_ID = "_lbBookmarks";
|
||||
|
||||
@@ -19,17 +24,64 @@ export default function BookmarkButtons() {
|
||||
const childNotes = useChildNotes(PARENT_NOTE_ID);
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
|
||||
</div>
|
||||
)
|
||||
<ResponsiveContainer
|
||||
desktop={
|
||||
<div style={style}>
|
||||
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
|
||||
</div>
|
||||
}
|
||||
mobile={
|
||||
<LaunchBarDropdownButton
|
||||
icon="bx bx-bookmark"
|
||||
title={t("bookmark_buttons.bookmarks")}
|
||||
>
|
||||
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
|
||||
</LaunchBarDropdownButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SingleBookmark({ note }: { note: FNote }) {
|
||||
const [ bookmarkFolder ] = useNoteLabelBoolean(note, "bookmarkFolder");
|
||||
return bookmarkFolder
|
||||
? <BookmarkFolder note={note} />
|
||||
: <CustomNoteLauncher launcherNote={note} getTargetNoteId={() => note.noteId} />
|
||||
return <ResponsiveContainer
|
||||
desktop={
|
||||
bookmarkFolder
|
||||
? <BookmarkFolder note={note} />
|
||||
: <CustomNoteLauncher launcherNote={note} getTargetNoteId={() => note.noteId} />
|
||||
}
|
||||
mobile={<MobileBookmarkItem noteId={note.noteId} bookmarkFolder={bookmarkFolder} />}
|
||||
/>;
|
||||
}
|
||||
|
||||
function MobileBookmarkItem({ noteId, bookmarkFolder }: { noteId: string, bookmarkFolder: boolean }) {
|
||||
const note = useNote(noteId);
|
||||
const noteIcon = useNoteIcon(note);
|
||||
if (!note) return null;
|
||||
|
||||
return (
|
||||
!bookmarkFolder
|
||||
? <FormListItem icon={noteIcon} onClick={(e) => launchCustomNoteLauncher(e, { launcherNote: note, getTargetNoteId: () => note.noteId })}>{note.title}</FormListItem>
|
||||
: <MobileBookmarkFolder note={note} />
|
||||
);
|
||||
}
|
||||
|
||||
function MobileBookmarkFolder({ note }: { note: FNote }) {
|
||||
const childNotes = useChildNotes(note.noteId);
|
||||
|
||||
return (
|
||||
<FormDropdownSubmenu icon="bx bx-folder" title={note.title}>
|
||||
{childNotes.map(childNote => (
|
||||
<FormListItem
|
||||
key={childNote.noteId}
|
||||
icon={childNote.getIcon()}
|
||||
onClick={(e) => launchCustomNoteLauncher(e, { launcherNote: childNote, getTargetNoteId: () => childNote.noteId })}
|
||||
>
|
||||
{childNote.title}
|
||||
</FormListItem>
|
||||
))}
|
||||
</FormDropdownSubmenu>
|
||||
);
|
||||
}
|
||||
|
||||
function BookmarkFolder({ note }: { note: FNote }) {
|
||||
@@ -55,5 +107,5 @@ function BookmarkFolder({ note }: { note: FNote }) {
|
||||
</ul>
|
||||
</div>
|
||||
</LaunchBarDropdownButton>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,32 +7,18 @@ import { isCtrlKey } from "../../services/utils";
|
||||
import { useGlobalShortcut, useNoteLabel } from "../react/hooks";
|
||||
import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
|
||||
export function CustomNoteLauncher({ launcherNote, getTargetNoteId, getHoistedNoteId }: {
|
||||
export function CustomNoteLauncher(props: {
|
||||
launcherNote: FNote;
|
||||
getTargetNoteId: (launcherNote: FNote) => string | null | Promise<string | null>;
|
||||
getHoistedNoteId?: (launcherNote: FNote) => string | null;
|
||||
keyboardShortcut?: string;
|
||||
}) {
|
||||
const { launcherNote, getTargetNoteId } = props;
|
||||
const { icon, title } = useLauncherIconAndTitle(launcherNote);
|
||||
|
||||
const launch = useCallback(async (evt: MouseEvent | KeyboardEvent) => {
|
||||
if (evt.which === 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetNoteId = await getTargetNoteId(launcherNote);
|
||||
if (!targetNoteId) return;
|
||||
|
||||
const hoistedNoteIdWithDefault = getHoistedNoteId?.(launcherNote) || appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
||||
const ctrlKey = isCtrlKey(evt);
|
||||
|
||||
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
|
||||
const activate = !!evt.shiftKey;
|
||||
await appContext.tabManager.openInNewTab(targetNoteId, hoistedNoteIdWithDefault, activate);
|
||||
} else {
|
||||
await appContext.tabManager.openInSameTab(targetNoteId, hoistedNoteIdWithDefault);
|
||||
}
|
||||
}, [ launcherNote, getTargetNoteId, getHoistedNoteId ]);
|
||||
await launchCustomNoteLauncher(evt, props);
|
||||
}, [ props ]);
|
||||
|
||||
// Keyboard shortcut.
|
||||
const [ shortcut ] = useNoteLabel(launcherNote, "keyboardShortcut");
|
||||
@@ -54,3 +40,24 @@ export function CustomNoteLauncher({ launcherNote, getTargetNoteId, getHoistedNo
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export async function launchCustomNoteLauncher(evt: MouseEvent | KeyboardEvent, { launcherNote, getTargetNoteId, getHoistedNoteId }: {
|
||||
launcherNote: FNote;
|
||||
getTargetNoteId: (launcherNote: FNote) => string | null | Promise<string | null>;
|
||||
getHoistedNoteId?: (launcherNote: FNote) => string | null;
|
||||
}) {
|
||||
if (evt.which === 3) return;
|
||||
|
||||
const targetNoteId = await getTargetNoteId(launcherNote);
|
||||
if (!targetNoteId) return;
|
||||
|
||||
const hoistedNoteIdWithDefault = getHoistedNoteId?.(launcherNote) || appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
||||
const ctrlKey = isCtrlKey(evt);
|
||||
|
||||
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
|
||||
const activate = !!evt.shiftKey;
|
||||
await appContext.tabManager.openInNewTab(targetNoteId, hoistedNoteIdWithDefault, activate);
|
||||
} else {
|
||||
await appContext.tabManager.openInSameTab(targetNoteId, hoistedNoteIdWithDefault);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ export function LaunchBarDropdownButton({ children, icon, dropdownOptions, ...pr
|
||||
placement: isHorizontalLayout ? "bottom" : "right"
|
||||
}
|
||||
}}
|
||||
mobileBackdrop
|
||||
{...props}
|
||||
>{children}</Dropdown>
|
||||
);
|
||||
|
||||
282
apps/client/src/widgets/layout/ActiveContentBadges.tsx
Normal file
282
apps/client/src/widgets/layout/ActiveContentBadges.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
import { BUILTIN_ATTRIBUTES } from "@triliumnext/commons";
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import attributes from "../../services/attributes";
|
||||
import { t } from "../../services/i18n";
|
||||
import { openInAppHelpFromUrl } from "../../services/utils";
|
||||
import { BadgeWithDropdown } from "../react/Badge";
|
||||
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||
import FormToggle from "../react/FormToggle";
|
||||
import { useNoteContext, useTriliumEvent } from "../react/hooks";
|
||||
import { BookProperty, ViewProperty } from "../react/NotePropertyMenu";
|
||||
|
||||
const NON_DANGEROUS_ACTIVE_CONTENT = [ "appCss", "appTheme" ];
|
||||
const DANGEROUS_ATTRIBUTES = BUILTIN_ATTRIBUTES.filter(a => a.isDangerous || NON_DANGEROUS_ACTIVE_CONTENT.includes(a.name));
|
||||
const activeContentLabels = [ "iconPack", "widget", "appCss", "appTheme" ] as const;
|
||||
|
||||
interface ActiveContentInfo {
|
||||
type: "iconPack" | "backendScript" | "frontendScript" | "widget" | "appCss" | "renderNote" | "webView" | "appTheme";
|
||||
isEnabled: boolean;
|
||||
canToggleEnabled: boolean;
|
||||
}
|
||||
|
||||
const executeOption: BookProperty = {
|
||||
type: "button",
|
||||
icon: "bx bx-play",
|
||||
label: t("active_content_badges.menu_execute_now"),
|
||||
onClick: context => context.triggerCommand("runActiveNote")
|
||||
};
|
||||
|
||||
const typeMappings: Record<ActiveContentInfo["type"], {
|
||||
title: string;
|
||||
icon: string;
|
||||
helpPage: string;
|
||||
apiDocsPage?: string;
|
||||
isExecutable?: boolean;
|
||||
additionalOptions?: BookProperty[];
|
||||
}> = {
|
||||
iconPack: {
|
||||
title: t("active_content_badges.type_icon_pack"),
|
||||
icon: "bx bx-package",
|
||||
helpPage: "g1mlRoU8CsqC",
|
||||
},
|
||||
backendScript: {
|
||||
title: t("active_content_badges.type_backend_script"),
|
||||
icon: "bx bx-server",
|
||||
helpPage: "SPirpZypehBG",
|
||||
apiDocsPage: "MEtfsqa5VwNi",
|
||||
isExecutable: true,
|
||||
additionalOptions: [
|
||||
executeOption,
|
||||
{
|
||||
type: "combobox",
|
||||
bindToLabel: "run",
|
||||
label: t("active_content_badges.menu_run"),
|
||||
icon: "bx bx-rss",
|
||||
dropStart: true,
|
||||
options: [
|
||||
{ value: null, label: t("active_content_badges.menu_run_disabled") },
|
||||
{ value: "backendStartup", label: t("active_content_badges.menu_run_backend_startup") },
|
||||
{ value: "daily", label: t("active_content_badges.menu_run_daily") },
|
||||
{ value: "hourly", label: t("active_content_badges.menu_run_hourly") }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
frontendScript: {
|
||||
title: t("active_content_badges.type_frontend_script"),
|
||||
icon: "bx bx-window",
|
||||
helpPage: "yIhgI5H7A2Sm",
|
||||
apiDocsPage: "Q2z6av6JZVWm",
|
||||
isExecutable: true,
|
||||
additionalOptions: [
|
||||
executeOption,
|
||||
{
|
||||
type: "combobox",
|
||||
bindToLabel: "run",
|
||||
label: t("active_content_badges.menu_run"),
|
||||
icon: "bx bx-rss",
|
||||
dropStart: true,
|
||||
options: [
|
||||
{ value: null, label: t("active_content_badges.menu_run_disabled") },
|
||||
{ value: "frontendStartup", label: t("active_content_badges.menu_run_frontend_startup") },
|
||||
{ value: "mobileStartup", label: t("active_content_badges.menu_run_mobile_startup") },
|
||||
]
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
type: "button",
|
||||
label: t("active_content_badges.menu_change_to_widget"),
|
||||
icon: "bx bxs-widget",
|
||||
onClick: ({ note }) => attributes.setLabel(note.noteId, "widget")
|
||||
}
|
||||
]
|
||||
},
|
||||
widget: {
|
||||
title: t("active_content_badges.type_widget"),
|
||||
icon: "bx bxs-widget",
|
||||
helpPage: "MgibgPcfeuGz",
|
||||
additionalOptions: [
|
||||
{
|
||||
type: "button",
|
||||
label: t("active_content_badges.menu_change_to_frontend_script"),
|
||||
icon: "bx bx-window",
|
||||
onClick: ({ note }) => {
|
||||
attributes.removeOwnedLabelByName(note, "widget");
|
||||
attributes.removeOwnedLabelByName(note, "disabled:widget");
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
appCss: {
|
||||
title: t("active_content_badges.type_app_css"),
|
||||
icon: "bx bxs-file-css",
|
||||
helpPage: "AlhDUqhENtH7"
|
||||
},
|
||||
renderNote: {
|
||||
title: t("active_content_badges.type_render_note"),
|
||||
icon: "bx bx-extension",
|
||||
helpPage: "HcABDtFCkbFN"
|
||||
},
|
||||
webView: {
|
||||
title: t("active_content_badges.type_web_view"),
|
||||
icon: "bx bx-globe",
|
||||
helpPage: "1vHRoWCEjj0L"
|
||||
},
|
||||
appTheme: {
|
||||
title :t("active_content_badges.type_app_theme"),
|
||||
icon: "bx bx-palette",
|
||||
helpPage: "7NfNr5pZpVKV",
|
||||
additionalOptions: [
|
||||
{
|
||||
type: "combobox",
|
||||
bindToLabel: "appThemeBase",
|
||||
label: t("active_content_badges.menu_theme_base"),
|
||||
icon: "bx bx-layer",
|
||||
dropStart: true,
|
||||
options: [
|
||||
{ label: t("theme.auto_theme"), value: null },
|
||||
{ type: "separator" },
|
||||
{ label: t("theme.triliumnext"), value: "next" },
|
||||
{ label: t("theme.triliumnext-light"), value: "next-light" },
|
||||
{ label: t("theme.triliumnext-dark"), value: "next-dark" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
export function ActiveContentBadges() {
|
||||
const { note } = useNoteContext();
|
||||
const info = useActiveContentInfo(note);
|
||||
|
||||
return (note && info &&
|
||||
<>
|
||||
{info.canToggleEnabled && <ActiveContentToggle info={info} note={note} />}
|
||||
<ActiveContentBadge info={info} note={note} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ActiveContentBadge({ info, note }: { note: FNote, info: ActiveContentInfo }) {
|
||||
const { title, icon, helpPage, apiDocsPage, additionalOptions } = typeMappings[info.type];
|
||||
return (
|
||||
<BadgeWithDropdown
|
||||
className={clsx("active-content-badge", info.canToggleEnabled && !info.isEnabled && "disabled")}
|
||||
icon={icon}
|
||||
text={title}
|
||||
dropdownOptions={{
|
||||
dropdownContainerClassName: "mobile-bottom-menu",
|
||||
mobileBackdrop: true
|
||||
}}
|
||||
>
|
||||
{additionalOptions?.length && (
|
||||
<>
|
||||
{additionalOptions?.map((property, i) => (
|
||||
<ViewProperty key={i} note={note} property={property} />
|
||||
))}
|
||||
<FormDropdownDivider />
|
||||
</>
|
||||
)}
|
||||
|
||||
<FormListItem
|
||||
icon="bx bx-help-circle"
|
||||
onClick={() => openInAppHelpFromUrl(helpPage)}
|
||||
>{t("active_content_badges.menu_docs")}</FormListItem>
|
||||
|
||||
{apiDocsPage && <FormListItem
|
||||
icon="bx bx-book-content"
|
||||
onClick={() => openInAppHelpFromUrl(apiDocsPage)}
|
||||
>{t("code_buttons.trilium_api_docs_button_title")}</FormListItem>}
|
||||
</BadgeWithDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function ActiveContentToggle({ note, info }: { note: FNote, info: ActiveContentInfo }) {
|
||||
const { title } = typeMappings[info.type];
|
||||
|
||||
return info && <FormToggle
|
||||
switchOnName="" switchOffName=""
|
||||
currentValue={info.isEnabled}
|
||||
switchOffTooltip={t("active_content_badges.toggle_tooltip_disable_tooltip", { type: title })}
|
||||
switchOnTooltip={t("active_content_badges.toggle_tooltip_enable_tooltip", { type: title })}
|
||||
onChange={async (willEnable) => {
|
||||
await Promise.all(note.getOwnedAttributes()
|
||||
.map(attr => ({ name: attributes.getNameWithoutDangerousPrefix(attr.name), type: attr.type }))
|
||||
.filter(({ name, type }) => DANGEROUS_ATTRIBUTES.some(item => item.name === name && item.type === type))
|
||||
.map(({ name, type }) => attributes.toggleDangerousAttribute(note, type, name, willEnable)));
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
function useActiveContentInfo(note: FNote | null | undefined) {
|
||||
const [ info, setInfo ] = useState<ActiveContentInfo | null>(null);
|
||||
|
||||
function refresh() {
|
||||
let type: ActiveContentInfo["type"] | null = null;
|
||||
let isEnabled = false;
|
||||
let canToggleEnabled = false;
|
||||
|
||||
if (!note) {
|
||||
setInfo(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (note.type === "render") {
|
||||
type = "renderNote";
|
||||
isEnabled = note.hasRelation("renderNote");
|
||||
} else if (note.type === "webView") {
|
||||
type = "webView";
|
||||
isEnabled = note.hasLabel("webViewSrc");
|
||||
} else if (note.type === "code" && note.mime === "application/javascript;env=backend") {
|
||||
type = "backendScript";
|
||||
for (const backendLabel of [ "run", "customRequestHandler", "customResourceProvider" ]) {
|
||||
isEnabled ||= note.hasLabel(backendLabel);
|
||||
|
||||
if (!canToggleEnabled && note.hasLabelOrDisabled(backendLabel)) {
|
||||
canToggleEnabled = true;
|
||||
}
|
||||
}
|
||||
} else if (note.type === "code" && note.mime === "application/javascript;env=frontend") {
|
||||
type = "frontendScript";
|
||||
isEnabled = note.hasLabel("widget") || note.hasLabel("run");
|
||||
canToggleEnabled = note.hasLabelOrDisabled("widget") || note.hasLabelOrDisabled("run");
|
||||
} else if (note.type === "code" && note.hasLabelOrDisabled("appTheme")) {
|
||||
isEnabled = note.hasLabel("appTheme");
|
||||
canToggleEnabled = true;
|
||||
}
|
||||
|
||||
for (const labelToCheck of activeContentLabels) {
|
||||
if (note.hasLabel(labelToCheck)) {
|
||||
type = labelToCheck;
|
||||
isEnabled = true;
|
||||
canToggleEnabled = true;
|
||||
break;
|
||||
} else if (note.hasLabel(`disabled:${labelToCheck}`)) {
|
||||
type = labelToCheck;
|
||||
isEnabled = false;
|
||||
canToggleEnabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (type) {
|
||||
setInfo({ type, isEnabled, canToggleEnabled });
|
||||
} else {
|
||||
setInfo(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh on note change.
|
||||
useEffect(refresh, [ note ]);
|
||||
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
return info;
|
||||
}
|
||||
@@ -37,6 +37,10 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
&.active-content-badge { --color: var(--badge-active-content-background-color); }
|
||||
&.active-content-badge.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
min-width: 0;
|
||||
|
||||
@@ -45,6 +49,11 @@
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.switch-button {
|
||||
--switch-track-height: 8px;
|
||||
--switch-track-width: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-badge {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||
import { useGetContextData, useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
|
||||
import { useShareState } from "../ribbon/BasicPropertiesTab";
|
||||
import { useShareInfo } from "../shared_info";
|
||||
import { ActiveContentBadges } from "./ActiveContentBadges";
|
||||
|
||||
export default function NoteBadges() {
|
||||
return (
|
||||
@@ -19,6 +20,7 @@ export default function NoteBadges() {
|
||||
<ShareBadge />
|
||||
<ClippedNoteBadge />
|
||||
<ExecuteBadge />
|
||||
<ActiveContentBadges />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ body.experimental-feature-new-layout {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
body.desktop .title-actions {
|
||||
> .collapsible,
|
||||
> .note-type-switcher {
|
||||
padding-inline-start: calc(24px - var(--title-actions-padding-start));
|
||||
@@ -57,3 +60,4 @@ body.experimental-feature-new-layout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
import "./NoteTitleActions.css";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import NoteContext from "../../components/note_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import { t } from "../../services/i18n";
|
||||
import CollectionProperties from "../note_bars/CollectionProperties";
|
||||
import { checkFullHeight, getExtendedWidgetType } from "../NoteDetail";
|
||||
import { PromotedAttributesContent, usePromotedAttributeData } from "../PromotedAttributes";
|
||||
import SimpleBadge from "../react/Badge";
|
||||
import Collapsible, { ExternallyControlledCollapsible } from "../react/Collapsible";
|
||||
import { useNoteContext, useNoteLabel, useNoteProperty, useTriliumEvent, useTriliumOptionBool } from "../react/hooks";
|
||||
import NoteLink, { NewNoteLink } from "../react/NoteLink";
|
||||
import { NewNoteLink } from "../react/NoteLink";
|
||||
import { useEditedNotes } from "../ribbon/EditedNotesTab";
|
||||
import SearchDefinitionTab from "../ribbon/SearchDefinitionTab";
|
||||
import NoteTypeSwitcher from "./NoteTypeSwitcher";
|
||||
|
||||
export default function NoteTitleActions() {
|
||||
const { note, ntxId, componentId, noteContext } = useNoteContext();
|
||||
const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_");
|
||||
const { note, ntxId, componentId, noteContext, viewScope } = useNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
|
||||
return (
|
||||
@@ -27,7 +23,7 @@ export default function NoteTitleActions() {
|
||||
<PromotedAttributes note={note} componentId={componentId} noteContext={noteContext} />
|
||||
{noteType === "search" && <SearchProperties note={note} ntxId={ntxId} />}
|
||||
<EditedNotes />
|
||||
<NoteTypeSwitcher />
|
||||
{(!viewScope?.viewMode || viewScope.viewMode === "default") && <NoteTypeSwitcher />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -48,7 +44,7 @@ function PromotedAttributes({ note, componentId, noteContext }: {
|
||||
componentId: string,
|
||||
noteContext: NoteContext | undefined
|
||||
}) {
|
||||
const [ cells, setCells ] = usePromotedAttributeData(note, componentId);
|
||||
const [ cells, setCells ] = usePromotedAttributeData(note, componentId, noteContext);
|
||||
const [ expanded, setExpanded ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -37,6 +37,10 @@
|
||||
&:hover {
|
||||
background: var(--input-background-color);
|
||||
}
|
||||
|
||||
.text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.status-bar-dropdown-button {
|
||||
@@ -58,101 +62,12 @@
|
||||
|
||||
.dropdown-note-info {
|
||||
padding: 1em !important;
|
||||
|
||||
ul {
|
||||
--row-block-margin: .2em;
|
||||
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-top: calc(0px - var(--row-block-margin));
|
||||
margin-bottom: 12px;
|
||||
display: table;
|
||||
|
||||
li {
|
||||
display: table-row;
|
||||
|
||||
> strong {
|
||||
display: table-cell;
|
||||
padding: var(--row-block-margin) 0;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: table-cell;
|
||||
user-select: text;
|
||||
padding-left: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-note-paths {
|
||||
.note-paths-widget {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.note-path-intro {
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.note-path-list {
|
||||
margin: 12px 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
/* Note path card */
|
||||
li {
|
||||
--border-radius: 6px;
|
||||
|
||||
position: relative;
|
||||
background: var(--card-background-color);
|
||||
padding: 8px 20px 8px 25px;
|
||||
|
||||
&:first-child {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
|
||||
& + li {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Current path arrow */
|
||||
&.path-current::before {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
content: "\ee8f";
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 20px;
|
||||
bottom: 0;
|
||||
font-family: "boxicons";
|
||||
font-size: .75em;
|
||||
color: var(--menu-item-icon-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Note path segment */
|
||||
a {
|
||||
margin-inline: 2px;
|
||||
padding-inline: 2px;
|
||||
color: currentColor;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
|
||||
/* The last segment of the note path */
|
||||
&.basename {
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.backlinks-widget > .dropdown-menu {
|
||||
@@ -160,84 +75,6 @@
|
||||
|
||||
max-height: 60vh;
|
||||
overflow-y: scroll;
|
||||
|
||||
/* Backlink card */
|
||||
li {
|
||||
--border-radius: 8px;
|
||||
|
||||
max-width: 600px;
|
||||
padding: 10px 20px;
|
||||
background: var(--card-background-color);
|
||||
|
||||
& + li {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
|
||||
/* Card header */
|
||||
& > span:first-child {
|
||||
display: block;
|
||||
|
||||
> span {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
/* Note path */
|
||||
> small {
|
||||
flex: 100%;
|
||||
order: -1;
|
||||
font-size: .65rem;
|
||||
|
||||
.note-path {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Note icon */
|
||||
> .tn-icon {
|
||||
color: var(--menu-item-icon-color);
|
||||
}
|
||||
|
||||
/* Note title */
|
||||
> a {
|
||||
margin-inline-start: 4px;
|
||||
color: currentColor;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Card content - excerpt */
|
||||
& > span:nth-child(2) > div {
|
||||
all: unset; /* TODO: Remove after disposing the old style from FloatingButtons.css */
|
||||
display: block;
|
||||
|
||||
margin: 8px 0;
|
||||
border-radius: 4px;
|
||||
background: var(--quick-search-result-content-background);
|
||||
padding: 8px;
|
||||
font-size: .75rem;
|
||||
|
||||
a {
|
||||
background: transparent;
|
||||
color: var(--quick-search-result-highlight-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,16 +121,50 @@
|
||||
|
||||
}
|
||||
|
||||
div.similar-notes-widget div.similar-notes-wrapper {
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
button.select-button:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout .note-info-content {
|
||||
ul {
|
||||
--row-block-margin: .2em;
|
||||
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-top: calc(0px - var(--row-block-margin));
|
||||
margin-bottom: 12px;
|
||||
display: table;
|
||||
|
||||
li {
|
||||
display: table-row;
|
||||
|
||||
> strong {
|
||||
display: table-cell;
|
||||
padding: var(--row-block-margin) 0;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: table-cell;
|
||||
user-select: text;
|
||||
padding-left: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout div.similar-notes-widget div.similar-notes-wrapper {
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout.mobile div.similar-notes-widget div.similar-notes-wrapper {
|
||||
max-height: unset;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bottom-panel {
|
||||
margin: 0 !important;
|
||||
padding: 0;
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function StatusBar() {
|
||||
similarNotesShown: activePane === "similar-notes",
|
||||
setSimilarNotesShown: (shown) => setActivePane(shown && "similar-notes")
|
||||
};
|
||||
const isHiddenNote = note?.isInHiddenSubtree();
|
||||
const isHiddenNote = note?.isHiddenCompletely();
|
||||
|
||||
return (
|
||||
<div className="status-bar">
|
||||
@@ -212,8 +212,8 @@ export function getLocaleName(locale: Locale | null | undefined) {
|
||||
|
||||
//#region Note info & Similar
|
||||
interface NoteInfoContext extends StatusBarContext {
|
||||
similarNotesShown: boolean;
|
||||
setSimilarNotesShown: (value: boolean) => void;
|
||||
similarNotesShown?: boolean;
|
||||
setSimilarNotesShown?: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function NoteInfoBadge(context: NoteInfoContext) {
|
||||
@@ -225,7 +225,7 @@ export function NoteInfoBadge(context: NoteInfoContext) {
|
||||
|
||||
// Keyboard shortcut.
|
||||
useTriliumEvent("toggleRibbonTabNoteInfo", () => enabled && dropdownRef.current?.show());
|
||||
useTriliumEvent("toggleRibbonTabSimilarNotes", () => setSimilarNotesShown(!similarNotesShown));
|
||||
useTriliumEvent("toggleRibbonTabSimilarNotes", () => setSimilarNotesShown && setSimilarNotesShown(!similarNotesShown));
|
||||
|
||||
return (enabled &&
|
||||
<StatusBarDropdown
|
||||
@@ -242,8 +242,8 @@ export function NoteInfoBadge(context: NoteInfoContext) {
|
||||
);
|
||||
}
|
||||
|
||||
function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }: NoteInfoContext & {
|
||||
dropdownRef: RefObject<BootstrapDropdown>;
|
||||
export function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }: Pick<NoteInfoContext, "note" | "setSimilarNotesShown"> & {
|
||||
dropdownRef?: RefObject<BootstrapDropdown>;
|
||||
noteType: NoteType;
|
||||
}) {
|
||||
const { metadata, ...sizeProps } = useNoteMetadata(note);
|
||||
@@ -251,7 +251,7 @@ function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }:
|
||||
const noteTypeMapping = useMemo(() => NOTE_TYPES.find(t => t.type === noteType), [ noteType ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="note-info-content">
|
||||
<ul>
|
||||
{originalFileName && <NoteInfoValue text={t("file_properties.original_file_name")} value={originalFileName} />}
|
||||
<NoteInfoValue text={t("note_info_widget.created")} value={formatDateTime(metadata?.dateCreated)} />
|
||||
@@ -262,14 +262,14 @@ function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }:
|
||||
<NoteInfoValue text={t("note_info_widget.note_size")} title={t("note_info_widget.note_size_info")} value={<NoteSizeWidget {...sizeProps} />} />
|
||||
</ul>
|
||||
|
||||
<LinkButton
|
||||
{setSimilarNotesShown && <LinkButton
|
||||
text={t("note_info_widget.show_similar_notes")}
|
||||
onClick={() => {
|
||||
dropdownRef.current?.hide();
|
||||
dropdownRef?.current?.hide();
|
||||
setSimilarNotesShown(true);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ function BacklinksBadge({ note, viewScope }: StatusBarContext) {
|
||||
const count = useBacklinkCount(note, viewScope?.viewMode === "default");
|
||||
return (note && count > 0 &&
|
||||
<StatusBarDropdown
|
||||
className="backlinks-badge backlinks-widget"
|
||||
className="backlinks-badge backlinks-widget tn-backlinks-widget"
|
||||
icon="bx bx-link"
|
||||
text={t("status_bar.backlinks", { count })}
|
||||
title={t("status_bar.backlinks_title", { count })}
|
||||
|
||||
@@ -35,11 +35,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.with-hue {
|
||||
background-color: hsl(var(--bg-hue), 8.8%, 11.2%);
|
||||
border-color: hsl(var(--bg-hue), 9.4%, 25.1%);
|
||||
}
|
||||
|
||||
&.active {
|
||||
outline: 4px solid var(--more-accented-background-color);
|
||||
background: var(--card-background-hover-color);
|
||||
@@ -89,12 +84,25 @@
|
||||
|
||||
&.type-text {
|
||||
padding: 10px;
|
||||
--ck-content-todo-list-checkmark-size: 8px;
|
||||
|
||||
p { margin-bottom: 0.2em;}
|
||||
hr { margin-block: 0.1em; height: 1px; }
|
||||
h2 { font-size: 1.20em; }
|
||||
h3 { font-size: 1.15em; }
|
||||
h4 { font-size: 1.10em; }
|
||||
h5 { font-size: 1.05em}
|
||||
h6 { font-size: 1em; }
|
||||
ul, ol { margin: 0 }
|
||||
}
|
||||
|
||||
&.type-book,
|
||||
&.type-contentWidget,
|
||||
&.type-search,
|
||||
&.type-empty {
|
||||
&.type-empty,
|
||||
&.type-relationMap,
|
||||
&.type-launcher,
|
||||
&.tab-preview-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -105,13 +113,6 @@
|
||||
.preview-placeholder {
|
||||
font-size: 500%;
|
||||
}
|
||||
|
||||
p { margin-bottom: 0.2em;}
|
||||
h2 { font-size: 1.20em; }
|
||||
h3 { font-size: 1.15em; }
|
||||
h4 { font-size: 1.10em; }
|
||||
h5 { font-size: 1.05em}
|
||||
h6 { font-size: 1em; }
|
||||
}
|
||||
|
||||
&.with-split {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "./TabSwitcher.css";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { ComponentChild } from "preact";
|
||||
import { createPortal, Fragment } from "preact/compat";
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
@@ -11,6 +12,7 @@ import contextMenu from "../../menus/context_menu";
|
||||
import { getHue, parseColor } from "../../services/css_class_manager";
|
||||
import froca from "../../services/froca";
|
||||
import { t } from "../../services/i18n";
|
||||
import type { ViewMode, ViewScope } from "../../services/link";
|
||||
import { NoteContent } from "../collections/legacy/ListOrGridView";
|
||||
import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets";
|
||||
import { ICON_MAPPINGS } from "../note_bars/CollectionProperties";
|
||||
@@ -20,6 +22,13 @@ import Icon from "../react/Icon";
|
||||
import LinkButton from "../react/LinkButton";
|
||||
import Modal from "../react/Modal";
|
||||
|
||||
const VIEW_MODE_ICON_MAPPINGS: Record<Exclude<ViewMode, "default">, string> = {
|
||||
source: "bx bx-code",
|
||||
"contextual-help": "bx bx-help-circle",
|
||||
"note-map": "bx bxs-network-chart",
|
||||
attachments: "bx bx-paperclip",
|
||||
};
|
||||
|
||||
export default function TabSwitcher() {
|
||||
const [ shown, setShown ] = useState(false);
|
||||
const mainNoteContexts = useMainNoteContexts();
|
||||
@@ -138,7 +147,6 @@ function Tab({ noteContext, containerRef, selectTab, activeNtxId }: {
|
||||
activeNtxId: string | null | undefined;
|
||||
}) {
|
||||
const { note } = noteContext;
|
||||
const iconClass = useNoteIcon(note);
|
||||
const colorClass = note?.getColorClass() || '';
|
||||
const workspaceTabBackgroundColorHue = getWorkspaceTabBackgroundColorHue(noteContext);
|
||||
const subContexts = noteContext.getSubContexts();
|
||||
@@ -158,46 +166,65 @@ function Tab({ noteContext, containerRef, selectTab, activeNtxId }: {
|
||||
>
|
||||
{subContexts.map(subContext => (
|
||||
<Fragment key={subContext.ntxId}>
|
||||
<header className={colorClass}>
|
||||
{subContext.note && <Icon icon={iconClass} />}
|
||||
<span className="title">{subContext.note?.title ?? t("tab_row.new_tab")}</span>
|
||||
{subContext.isMainContext() && <ActionButton
|
||||
icon="bx bx-x"
|
||||
text={t("tab_row.close_tab")}
|
||||
onClick={(e) => {
|
||||
// We are closing a tab, so we need to prevent propagation for click (activate tab).
|
||||
e.stopPropagation();
|
||||
appContext.tabManager.removeNoteContext(subContext.ntxId);
|
||||
}}
|
||||
/>}
|
||||
</header>
|
||||
<div className={clsx("tab-preview", `type-${subContext.note?.type ?? "empty"}`)}>
|
||||
<TabPreviewContent note={subContext.note} />
|
||||
</div>
|
||||
<TabHeader noteContext={subContext} colorClass={colorClass} />
|
||||
<TabPreviewContent note={subContext.note} viewScope={subContext.viewScope} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TabPreviewContent({ note }: {
|
||||
note: FNote | null
|
||||
}) {
|
||||
if (!note) {
|
||||
return <PreviewPlaceholder icon="bx bx-plus" />;
|
||||
}
|
||||
function TabHeader({ noteContext, colorClass }: { noteContext: NoteContext, colorClass: string }) {
|
||||
const iconClass = useNoteIcon(noteContext.note);
|
||||
const [ navigationTitle, setNavigationTitle ] = useState<string | null>(null);
|
||||
|
||||
if (note.type === "book") {
|
||||
return <PreviewPlaceholder icon={ICON_MAPPINGS[note.getLabelValue("viewType") ?? ""] ?? "bx bx-book"} />;
|
||||
}
|
||||
// Manage the title for read-only notes
|
||||
useEffect(() => {
|
||||
noteContext?.getNavigationTitle().then(setNavigationTitle);
|
||||
}, [noteContext]);
|
||||
|
||||
return (
|
||||
<NoteContent
|
||||
<header className={colorClass}>
|
||||
{noteContext.note && <Icon icon={iconClass} />}
|
||||
<span className="title">{navigationTitle ?? noteContext.note?.title ?? t("tab_row.new_tab")}</span>
|
||||
{noteContext.isMainContext() && <ActionButton
|
||||
icon="bx bx-x"
|
||||
text={t("tab_row.close_tab")}
|
||||
onClick={(e) => {
|
||||
// We are closing a tab, so we need to prevent propagation for click (activate tab).
|
||||
e.stopPropagation();
|
||||
appContext.tabManager.removeNoteContext(noteContext.ntxId);
|
||||
}}
|
||||
/>}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function TabPreviewContent({ note, viewScope }: {
|
||||
note: FNote | null,
|
||||
viewScope: ViewScope | undefined
|
||||
}) {
|
||||
let el: ComponentChild;
|
||||
let isPlaceholder = true;
|
||||
|
||||
if (!note) {
|
||||
el = <PreviewPlaceholder icon="bx bx-plus" />;
|
||||
} else if (note.type === "book") {
|
||||
el = <PreviewPlaceholder icon={ICON_MAPPINGS[note.getLabelValue("viewType") ?? ""] ?? "bx bx-book"} />;
|
||||
} else if (viewScope?.viewMode && viewScope.viewMode !== "default") {
|
||||
el = <PreviewPlaceholder icon={VIEW_MODE_ICON_MAPPINGS[viewScope?.viewMode ?? ""] ?? "bx bx-empty"} />;
|
||||
} else {
|
||||
el = <NoteContent
|
||||
note={note}
|
||||
highlightedTokens={undefined}
|
||||
trim
|
||||
includeArchivedNotes={false}
|
||||
/>
|
||||
/>;
|
||||
isPlaceholder = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx("tab-preview", `type-${note?.type ?? "empty"}`, { "tab-preview-placeholder": isPlaceholder })}>{el}</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.code-note-switcher-modal .dropdown-menu {
|
||||
background: none !important;
|
||||
}
|
||||
@@ -1,84 +1,240 @@
|
||||
import { useContext } from "preact/hooks";
|
||||
import "./mobile_detail_menu.css";
|
||||
|
||||
import appContext, { CommandMappings } from "../../components/app_context";
|
||||
import contextMenu, { MenuItem } from "../../menus/context_menu";
|
||||
import branches from "../../services/branches";
|
||||
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||
import { createPortal, useRef, useState } from "preact/compat";
|
||||
|
||||
import FNote, { NotePathRecord } from "../../entities/fnote";
|
||||
import { t } from "../../services/i18n";
|
||||
import { getHelpUrlForNote } from "../../services/in_app_help";
|
||||
import note_create from "../../services/note_create";
|
||||
import tree from "../../services/tree";
|
||||
import { openInAppHelpFromUrl } from "../../services/utils";
|
||||
import BasicWidget from "../basic_widget";
|
||||
import server from "../../services/server";
|
||||
import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions";
|
||||
import { getLocaleName, NoteInfoContent } from "../layout/StatusBar";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList";
|
||||
import { useNoteContext, useNoteProperty } from "../react/hooks";
|
||||
import Modal from "../react/Modal";
|
||||
import { NoteTypeCodeNoteList, useLanguageSwitcher, useMimeTypes } from "../ribbon/BasicPropertiesTab";
|
||||
import { NoteContextMenu } from "../ribbon/NoteActions";
|
||||
import NoteActionsCustom from "../ribbon/NoteActionsCustom";
|
||||
import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab";
|
||||
import SimilarNotesTab from "../ribbon/SimilarNotesTab";
|
||||
import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector";
|
||||
|
||||
export default function MobileDetailMenu() {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const dropdownRef = useRef<BootstrapDropdown | null>(null);
|
||||
const { note, noteContext, parentComponent, ntxId, viewScope, hoistedNoteId } = useNoteContext();
|
||||
const subContexts = noteContext?.getMainContext().getSubContexts() ?? [];
|
||||
const isMainContext = noteContext?.isMainContext();
|
||||
const [ backlinksModalShown, setBacklinksModalShown ] = useState(false);
|
||||
const [ notePathsModalShown, setNotePathsModalShown ] = useState(false);
|
||||
const [ noteInfoModalShown, setNoteInfoModalShown ] = useState(false);
|
||||
const [ similarNotesModalShown, setSimilarNotesModalShown ] = useState(false);
|
||||
const [ codeNoteSwitcherModalShown, setCodeNoteSwitcherModalShown ] = useState(false);
|
||||
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
|
||||
const backlinksCount = useBacklinkCount(note, viewScope?.viewMode === "default");
|
||||
|
||||
function closePane() {
|
||||
// Wait first for the context menu to be dismissed, otherwise the backdrop stays on.
|
||||
requestAnimationFrame(() => {
|
||||
parentComponent.triggerCommand("closeThisNoteSplit", { ntxId });
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
icon="bx bx-dots-vertical-rounded"
|
||||
text=""
|
||||
onClick={(e) => {
|
||||
const ntxId = (parentComponent as BasicWidget | null)?.getClosestNtxId();
|
||||
if (!ntxId) return;
|
||||
<div style={{ contain: "none" }}>
|
||||
{note ? (
|
||||
<NoteContextMenu
|
||||
dropdownRef={dropdownRef}
|
||||
note={note} noteContext={noteContext}
|
||||
itemsAtStart={<>
|
||||
<div className="form-list-row">
|
||||
<div className="form-list-col">
|
||||
<FormListItem
|
||||
icon="bx bx-link"
|
||||
onClick={() => setBacklinksModalShown(true)}
|
||||
disabled={backlinksCount === 0}
|
||||
>{t("status_bar.backlinks", { count: backlinksCount })}</FormListItem>
|
||||
</div>
|
||||
<div className="form-list-col">
|
||||
<FormListItem
|
||||
icon="bx bx-directions"
|
||||
onClick={() => setNotePathsModalShown(true)}
|
||||
disabled={(sortedNotePaths?.length ?? 0) <= 1}
|
||||
>{t("status_bar.note_paths", { count: sortedNotePaths?.length })}</FormListItem>
|
||||
</div>
|
||||
</div>
|
||||
<FormDropdownDivider />
|
||||
|
||||
const noteContext = appContext.tabManager.getNoteContextById(ntxId);
|
||||
const subContexts = noteContext.getMainContext().getSubContexts();
|
||||
const isMainContext = noteContext?.isMainContext();
|
||||
const note = noteContext.note;
|
||||
const helpUrl = getHelpUrlForNote(note);
|
||||
{noteContext && ntxId && <NoteActionsCustom note={note} noteContext={noteContext} ntxId={ntxId} />}
|
||||
<FormListItem
|
||||
onClick={() => noteContext?.notePath && note_create.createNote(noteContext.notePath)}
|
||||
icon="bx bx-plus"
|
||||
>{t("mobile_detail_menu.insert_child_note")}</FormListItem>
|
||||
{subContexts.length < 2 && <>
|
||||
<FormDropdownDivider />
|
||||
<FormListItem
|
||||
onClick={(e) => {
|
||||
// We have to manually manage the hide because otherwise the old note context gets activated.
|
||||
e.stopPropagation();
|
||||
dropdownRef.current?.hide();
|
||||
parentComponent.triggerCommand("openNewNoteSplit", { ntxId });
|
||||
}}
|
||||
icon="bx bx-dock-right"
|
||||
>{t("create_pane_button.create_new_split")}</FormListItem>
|
||||
</>}
|
||||
{!isMainContext && <>
|
||||
<FormDropdownDivider />
|
||||
<FormListItem
|
||||
icon="bx bx-x"
|
||||
onClick={closePane}
|
||||
>{t("close_pane_button.close_this_pane")}</FormListItem>
|
||||
</>}
|
||||
<FormDropdownDivider />
|
||||
</>}
|
||||
itemsNearNoteSettings={<>
|
||||
{note.type === "text" && <ContentLanguageSelector note={note} />}
|
||||
{note.type === "code" && <FormListItem icon={"bx bx-code"} onClick={() => setCodeNoteSwitcherModalShown(true)}>{t("status_bar.code_note_switcher")}</FormListItem>}
|
||||
<FormListItem icon="bx bx-info-circle" onClick={() => setNoteInfoModalShown(true)}>{t("note_info_widget.title")}</FormListItem>
|
||||
<FormListItem icon="bx bx-bar-chart" onClick={() => setSimilarNotesModalShown(true)}>{t("similar_notes.title")}</FormListItem>
|
||||
<FormDropdownDivider />
|
||||
</>}
|
||||
/>
|
||||
) : (
|
||||
<ActionButton
|
||||
icon="bx bx-x"
|
||||
onClick={closePane}
|
||||
text={t("close_pane_button.close_this_pane")}
|
||||
/>
|
||||
)}
|
||||
|
||||
const items: (MenuItem<keyof CommandMappings>)[] = [
|
||||
{ title: t("mobile_detail_menu.insert_child_note"), command: "insertChildNote", uiIcon: "bx bx-plus", enabled: note?.type !== "search" },
|
||||
{ title: t("mobile_detail_menu.delete_this_note"), command: "delete", uiIcon: "bx bx-trash", enabled: note?.noteId !== "root" },
|
||||
{ kind: "separator" },
|
||||
{ title: t("mobile_detail_menu.note_revisions"), command: "showRevisions", uiIcon: "bx bx-history" },
|
||||
{ kind: "separator" },
|
||||
helpUrl && {
|
||||
title: t("help-button.title"),
|
||||
uiIcon: "bx bx-help-circle",
|
||||
handler: () => openInAppHelpFromUrl(helpUrl)
|
||||
},
|
||||
{ kind: "separator" },
|
||||
subContexts.length < 2 && { title: t("create_pane_button.create_new_split"), command: "openNewNoteSplit", uiIcon: "bx bx-dock-right" },
|
||||
!isMainContext && { title: t("close_pane_button.close_this_pane"), command: "closeThisNoteSplit", uiIcon: "bx bx-x" }
|
||||
].filter(i => !!i) as MenuItem<keyof CommandMappings>[];
|
||||
|
||||
const lastItem = items.at(-1);
|
||||
if (lastItem && "kind" in lastItem && lastItem.kind === "separator") {
|
||||
items.pop();
|
||||
}
|
||||
|
||||
contextMenu.show<keyof CommandMappings>({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items,
|
||||
selectMenuItemHandler: async ({ command }) => {
|
||||
if (command === "insertChildNote") {
|
||||
note_create.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined);
|
||||
} else if (command === "delete") {
|
||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||
if (!notePath) {
|
||||
throw new Error("Cannot get note path to delete.");
|
||||
}
|
||||
|
||||
const branchId = await tree.getBranchIdFromUrl(notePath);
|
||||
|
||||
if (!branchId) {
|
||||
throw new Error(t("mobile_detail_menu.error_cannot_get_branch_id", { notePath }));
|
||||
}
|
||||
|
||||
if (await branches.deleteNotes([branchId]) && parentComponent) {
|
||||
parentComponent.triggerCommand("setActiveScreen", { screen: "tree" });
|
||||
}
|
||||
} else if (command && parentComponent) {
|
||||
parentComponent.triggerCommand(command, { ntxId });
|
||||
}
|
||||
},
|
||||
forcePositionOnMobile: true
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{createPortal((
|
||||
<>
|
||||
<BacklinksModal note={note} modalShown={backlinksModalShown} setModalShown={setBacklinksModalShown} />
|
||||
<NotePathsModal note={note} modalShown={notePathsModalShown} notePath={noteContext?.notePath} sortedNotePaths={sortedNotePaths} setModalShown={setNotePathsModalShown} />
|
||||
<NoteInfoModal note={note} modalShown={noteInfoModalShown} setModalShown={setNoteInfoModalShown} />
|
||||
<SimilarNotesModal note={note} modalShown={similarNotesModalShown} setModalShown={setSimilarNotesModalShown} />
|
||||
<CodeNoteSwitcherModal note={note} modalShown={codeNoteSwitcherModalShown} setModalShown={setCodeNoteSwitcherModalShown} />
|
||||
</>
|
||||
), document.body)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ContentLanguageSelector({ note }: { note: FNote | null | undefined }) {
|
||||
const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note);
|
||||
const { activeLocale, processedLocales } = useProcessedLocales(locales, DEFAULT_LOCALE, currentNoteLanguage ?? DEFAULT_LOCALE.id);
|
||||
|
||||
return (
|
||||
<FormDropdownSubmenu
|
||||
icon="bx bx-globe"
|
||||
title={t("mobile_detail_menu.content_language_switcher", { language: getLocaleName(activeLocale ?? DEFAULT_LOCALE) })}
|
||||
>
|
||||
{processedLocales.map((locale, index) =>
|
||||
(typeof locale === "object") ? (
|
||||
<FormListItem
|
||||
key={locale.id}
|
||||
rtl={locale.rtl}
|
||||
checked={locale.id === currentNoteLanguage}
|
||||
onClick={() => setCurrentNoteLanguage(locale.id)}
|
||||
>{locale.name}</FormListItem>
|
||||
) : (
|
||||
<FormDropdownDivider key={`divider-${index}`} />
|
||||
)
|
||||
)}
|
||||
</FormDropdownSubmenu>
|
||||
);
|
||||
}
|
||||
|
||||
interface WithModal {
|
||||
modalShown: boolean;
|
||||
setModalShown: (shown: boolean) => void;
|
||||
}
|
||||
|
||||
function BacklinksModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
|
||||
return (
|
||||
<Modal
|
||||
className="backlinks-modal tn-backlinks-widget"
|
||||
size="md"
|
||||
title={t("mobile_detail_menu.backlinks")}
|
||||
show={modalShown}
|
||||
onHidden={() => setModalShown(false)}
|
||||
>
|
||||
<ul className="backlinks-items">
|
||||
{note && <BacklinksList note={note} />}
|
||||
</ul>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function NotePathsModal({ note, modalShown, notePath, sortedNotePaths, setModalShown }: { note: FNote | null | undefined, sortedNotePaths: NotePathRecord[] | undefined, notePath: string | null | undefined } & WithModal) {
|
||||
return (
|
||||
<Modal
|
||||
className="note-paths-modal"
|
||||
size="md"
|
||||
title={t("note_paths.title")}
|
||||
show={modalShown}
|
||||
onHidden={() => setModalShown(false)}
|
||||
>
|
||||
{note && (
|
||||
<NotePathsWidget
|
||||
sortedNotePaths={sortedNotePaths}
|
||||
currentNotePath={notePath}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function NoteInfoModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
|
||||
return (
|
||||
<Modal
|
||||
className="note-info-modal"
|
||||
size="md"
|
||||
title={t("note_info_widget.title")}
|
||||
show={modalShown}
|
||||
onHidden={() => setModalShown(false)}
|
||||
>
|
||||
{note && <NoteInfoContent note={note} noteType={note.type} />}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function SimilarNotesModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
|
||||
return (
|
||||
<Modal
|
||||
className="similar-notes-modal"
|
||||
size="md"
|
||||
title={t("similar_notes.title")}
|
||||
show={modalShown}
|
||||
onHidden={() => setModalShown(false)}
|
||||
>
|
||||
<SimilarNotesTab note={note} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function CodeNoteSwitcherModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
|
||||
const currentNoteMime = useNoteProperty(note, "mime");
|
||||
const mimeTypes = useMimeTypes();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="code-note-switcher-modal"
|
||||
size="md"
|
||||
title={t("status_bar.code_note_switcher")}
|
||||
show={modalShown}
|
||||
onHidden={() => setModalShown(false)}
|
||||
>
|
||||
<div className="dropdown-menu static show">
|
||||
{note && <NoteTypeCodeNoteList
|
||||
currentMimeType={currentNoteMime}
|
||||
mimeTypes={mimeTypes}
|
||||
changeNoteType={(type, mime) => {
|
||||
server.put(`notes/${note.noteId}/type`, { type, mime });
|
||||
setModalShown(false);
|
||||
}}
|
||||
/>}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
font-size: 0.8em;
|
||||
font-size: 0.8rem;
|
||||
|
||||
.dropdown-menu {
|
||||
input.form-control {
|
||||
|
||||
@@ -2,18 +2,16 @@ import "./CollectionProperties.css";
|
||||
|
||||
import { t } from "i18next";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { useContext, useRef } from "preact/hooks";
|
||||
import { Fragment } from "preact/jsx-runtime";
|
||||
import { useRef } from "preact/hooks";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import { ViewTypeOptions } from "../collections/interface";
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
|
||||
import FormTextBox from "../react/FormTextBox";
|
||||
import { useNoteLabel, useNoteLabelBoolean, useNoteLabelWithDefault, useNoteProperty, useTriliumEvent } from "../react/hooks";
|
||||
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||
import { useNoteProperty, useTriliumEvent } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config";
|
||||
import { CheckBoxProperty, ViewProperty } from "../react/NotePropertyMenu";
|
||||
import { bookPropertiesConfig } from "../ribbon/collection-properties-config";
|
||||
import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab";
|
||||
|
||||
export const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
|
||||
@@ -85,9 +83,11 @@ function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOption
|
||||
<Dropdown
|
||||
buttonClassName="bx bx-cog icon-action"
|
||||
hideToggleArrow
|
||||
dropdownContainerClassName="mobile-bottom-menu"
|
||||
mobileBackdrop
|
||||
>
|
||||
{properties.map(property => (
|
||||
<ViewProperty key={property.label} note={note} property={property} />
|
||||
{properties.map((property, index) => (
|
||||
<ViewProperty key={index} note={note} property={property} />
|
||||
))}
|
||||
{properties.length > 0 && <FormDropdownDivider />}
|
||||
|
||||
@@ -107,127 +107,3 @@ function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOption
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function ViewProperty({ note, property }: { note: FNote, property: BookProperty }) {
|
||||
switch (property.type) {
|
||||
case "button":
|
||||
return <ButtonPropertyView note={note} property={property} />;
|
||||
case "split-button":
|
||||
return <SplitButtonPropertyView note={note} property={property} />;
|
||||
case "checkbox":
|
||||
return <CheckBoxPropertyView note={note} property={property} />;
|
||||
case "number":
|
||||
return <NumberPropertyView note={note} property={property} />;
|
||||
case "combobox":
|
||||
return <ComboBoxPropertyView note={note} property={property} />;
|
||||
}
|
||||
}
|
||||
|
||||
function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
|
||||
return (
|
||||
<FormListItem
|
||||
icon={property.icon}
|
||||
title={property.title}
|
||||
onClick={() => {
|
||||
if (!parentComponent) return;
|
||||
property.onClick({
|
||||
note,
|
||||
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
|
||||
});
|
||||
}}
|
||||
>{property.label}</FormListItem>
|
||||
);
|
||||
}
|
||||
|
||||
function SplitButtonPropertyView({ note, property }: { note: FNote, property: SplitButtonProperty }) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const ItemsComponent = property.items;
|
||||
const clickContext = parentComponent && {
|
||||
note,
|
||||
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
|
||||
};
|
||||
|
||||
return (parentComponent &&
|
||||
<FormDropdownSubmenu
|
||||
icon={property.icon ?? "bx bx-empty"}
|
||||
title={property.label}
|
||||
onDropdownToggleClicked={() => clickContext && property.onClick(clickContext)}
|
||||
>
|
||||
<ItemsComponent note={note} parentComponent={parentComponent} />
|
||||
</FormDropdownSubmenu>
|
||||
);
|
||||
}
|
||||
|
||||
function NumberPropertyView({ note, property }: { note: FNote, property: NumberProperty }) {
|
||||
//@ts-expect-error Interop with text box which takes in string values even for numbers.
|
||||
const [ value, setValue ] = useNoteLabel(note, property.bindToLabel);
|
||||
const disabled = property.disabled?.(note);
|
||||
|
||||
return (
|
||||
<FormListItem
|
||||
icon={property.icon}
|
||||
disabled={disabled}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{property.label}
|
||||
<FormTextBox
|
||||
type="number"
|
||||
currentValue={value ?? ""} onChange={setValue}
|
||||
style={{ width: (property.width ?? 100) }}
|
||||
min={property.min ?? 0}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</FormListItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ComboBoxPropertyView({ note, property }: { note: FNote, property: ComboBoxProperty }) {
|
||||
const [ value, setValue ] = useNoteLabelWithDefault(note, property.bindToLabel, property.defaultValue ?? "");
|
||||
|
||||
function renderItem(option: ComboBoxItem) {
|
||||
return (
|
||||
<FormListItem
|
||||
key={option.value}
|
||||
checked={value === option.value}
|
||||
onClick={() => setValue(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</FormListItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormDropdownSubmenu
|
||||
title={property.label}
|
||||
icon={property.icon ?? "bx bx-empty"}
|
||||
>
|
||||
{(property.options).map((option, index) => {
|
||||
if ("items" in option) {
|
||||
return (
|
||||
<Fragment key={option.title}>
|
||||
<FormListItem key={option.title} disabled>{option.title}</FormListItem>
|
||||
{option.items.map(renderItem)}
|
||||
{index < property.options.length - 1 && <FormDropdownDivider />}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
return renderItem(option);
|
||||
|
||||
})}
|
||||
</FormDropdownSubmenu>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckBoxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) {
|
||||
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
|
||||
return (
|
||||
<FormListToggleableItem
|
||||
icon={property.icon}
|
||||
title={property.label}
|
||||
currentValue={value}
|
||||
onChange={setValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,3 +117,35 @@ body.experimental-feature-new-layout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.mobile .modal.icon-switcher {
|
||||
.modal-dialog {
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: unset;
|
||||
transform: unset;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .filter-row {
|
||||
padding: 0.25em var(--bs-modal-padding) 0.5em var(--bs-modal-padding);
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.icon-list {
|
||||
margin: auto;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
|
||||
span {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { IconRegistry } from "@triliumnext/commons";
|
||||
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||
import clsx from "clsx";
|
||||
import { t } from "i18next";
|
||||
import { CSSProperties, RefObject } from "preact";
|
||||
import { CSSProperties } from "preact";
|
||||
import { createPortal } from "preact/compat";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import type React from "react";
|
||||
import { CellComponentProps, Grid } from "react-window";
|
||||
@@ -12,11 +13,15 @@ import { CellComponentProps, Grid } from "react-window";
|
||||
import FNote from "../entities/fnote";
|
||||
import attributes from "../services/attributes";
|
||||
import server from "../services/server";
|
||||
import { isDesktop, isMobile } from "../services/utils";
|
||||
import ActionButton from "./react/ActionButton";
|
||||
import Dropdown from "./react/Dropdown";
|
||||
import { FormDropdownDivider, FormListItem } from "./react/FormList";
|
||||
import FormTextBox from "./react/FormTextBox";
|
||||
import { useNoteContext, useNoteLabel, useStaticTooltip } from "./react/hooks";
|
||||
import { useNoteContext, useNoteLabel, useStaticTooltip, useWindowSize } from "./react/hooks";
|
||||
import Modal from "./react/Modal";
|
||||
|
||||
const ICON_SIZE = isMobile() ? 56 : 48;
|
||||
|
||||
interface IconToCountCache {
|
||||
iconClassToCountMap: Record<string, number>;
|
||||
@@ -37,6 +42,10 @@ export default function NoteIcon() {
|
||||
setIcon(note?.getIcon());
|
||||
}, [ note, iconClass, workspaceIconClass ]);
|
||||
|
||||
if (isMobile()) {
|
||||
return <MobileNoteIconSwitcher note={note} icon={icon} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
className="note-icon-widget"
|
||||
@@ -48,16 +57,47 @@ export default function NoteIcon() {
|
||||
hideToggleArrow
|
||||
disabled={viewScope?.viewMode !== "default"}
|
||||
>
|
||||
{ note && <NoteIconList note={note} dropdownRef={dropdownRef} /> }
|
||||
{ note && <NoteIconList note={note} onHide={() => dropdownRef?.current?.hide()} columnCount={12} /> }
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function NoteIconList({ note, dropdownRef }: {
|
||||
note: FNote,
|
||||
dropdownRef: RefObject<BootstrapDropdown>;
|
||||
function MobileNoteIconSwitcher({ note, icon }: {
|
||||
note: FNote | null | undefined;
|
||||
icon: string | null | undefined;
|
||||
}) {
|
||||
const [ modalShown, setModalShown ] = useState(false);
|
||||
const { windowWidth } = useWindowSize();
|
||||
|
||||
return (note &&
|
||||
<div className="note-icon-widget">
|
||||
<ActionButton
|
||||
className="note-icon"
|
||||
icon={icon ?? "bx bx-empty"}
|
||||
text={t("note_icon.change_note_icon")}
|
||||
onClick={() => setModalShown(true)}
|
||||
/>
|
||||
|
||||
{createPortal((
|
||||
<Modal
|
||||
title={t("note_icon.change_note_icon")}
|
||||
size="xl"
|
||||
show={modalShown} onHidden={() => setModalShown(false)}
|
||||
className="icon-switcher note-icon-widget"
|
||||
scrollable
|
||||
>
|
||||
<NoteIconList note={note} onHide={() => setModalShown(false)} columnCount={Math.max(1, Math.floor(windowWidth / ICON_SIZE))} />
|
||||
</Modal>
|
||||
), document.body)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NoteIconList({ note, onHide, columnCount }: {
|
||||
note: FNote;
|
||||
onHide: () => void;
|
||||
columnCount: number;
|
||||
}) {
|
||||
const searchBoxRef = useRef<HTMLInputElement>(null);
|
||||
const iconListRef = useRef<HTMLDivElement>(null);
|
||||
const [ search, setSearch ] = useState<string>();
|
||||
const [ filterByPrefix, setFilterByPrefix ] = useState<string | null>(null);
|
||||
@@ -73,53 +113,22 @@ function NoteIconList({ note, dropdownRef }: {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="filter-row">
|
||||
<span>{t("note_icon.search")}</span>
|
||||
<FormTextBox
|
||||
inputRef={searchBoxRef}
|
||||
type="text"
|
||||
name="icon-search"
|
||||
placeholder={ filterByPrefix
|
||||
? t("note_icon.search_placeholder_filtered", {
|
||||
number: filteredIcons.length ?? 0,
|
||||
name: glob.iconRegistry.sources.find(s => s.prefix === filterByPrefix)?.name ?? ""
|
||||
})
|
||||
: t("note_icon.search_placeholder", { number: filteredIcons.length ?? 0, count: glob.iconRegistry.sources.length })}
|
||||
currentValue={search} onChange={setSearch}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
{getIconLabels(note).length > 0 && (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<ActionButton
|
||||
icon="bx bx-reset"
|
||||
text={t("note_icon.reset-default")}
|
||||
onClick={() => {
|
||||
if (!note) return;
|
||||
for (const label of getIconLabels(note)) {
|
||||
attributes.removeAttributeById(note.noteId, label.attributeId);
|
||||
}
|
||||
dropdownRef?.current?.hide();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{glob.iconRegistry.sources.length > 0 && <Dropdown
|
||||
buttonClassName="bx bx-filter-alt"
|
||||
hideToggleArrow
|
||||
noSelectButtonStyle
|
||||
noDropdownListStyle
|
||||
iconAction
|
||||
title={t("note_icon.filter")}
|
||||
>
|
||||
<IconFilterContent filterByPrefix={filterByPrefix} setFilterByPrefix={setFilterByPrefix} />
|
||||
</Dropdown>}
|
||||
</div>
|
||||
<FilterRow
|
||||
note={note}
|
||||
filterByPrefix={filterByPrefix}
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
setFilterByPrefix={setFilterByPrefix}
|
||||
filteredIcons={filteredIcons}
|
||||
onHide={onHide}
|
||||
/>
|
||||
|
||||
<div
|
||||
class="icon-list"
|
||||
ref={iconListRef}
|
||||
style={{
|
||||
width: (columnCount * ICON_SIZE + 10),
|
||||
}}
|
||||
onClick={(e) => {
|
||||
// Make sure we are not clicking on something else than a button.
|
||||
const clickedTarget = e.target as HTMLElement;
|
||||
@@ -130,18 +139,19 @@ function NoteIconList({ note, dropdownRef }: {
|
||||
const attributeToSet = note.hasOwnedLabel("workspace") ? "workspaceIconClass" : "iconClass";
|
||||
attributes.setLabel(note.noteId, attributeToSet, iconClass);
|
||||
}
|
||||
dropdownRef?.current?.hide();
|
||||
onHide();
|
||||
}}
|
||||
>
|
||||
{filteredIcons.length ? (
|
||||
<Grid
|
||||
columnCount={12}
|
||||
columnWidth={48}
|
||||
rowCount={Math.ceil(filteredIcons.length / 12)}
|
||||
rowHeight={48}
|
||||
columnCount={columnCount}
|
||||
columnWidth={ICON_SIZE}
|
||||
rowCount={Math.ceil(filteredIcons.length / columnCount)}
|
||||
rowHeight={ICON_SIZE}
|
||||
cellComponent={IconItemCell}
|
||||
cellProps={{
|
||||
filteredIcons
|
||||
filteredIcons,
|
||||
columnCount
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@@ -152,10 +162,95 @@ function NoteIconList({ note, dropdownRef }: {
|
||||
);
|
||||
}
|
||||
|
||||
function IconItemCell({ rowIndex, columnIndex, style, filteredIcons }: CellComponentProps<{
|
||||
function FilterRow({ note, filterByPrefix, search, setSearch, setFilterByPrefix, filteredIcons, onHide }: {
|
||||
note: FNote;
|
||||
filterByPrefix: string | null;
|
||||
search: string | undefined;
|
||||
setSearch: (value: string | undefined) => void;
|
||||
setFilterByPrefix: (value: string | null) => void;
|
||||
filteredIcons: IconWithName[];
|
||||
onHide: () => void;
|
||||
}) {
|
||||
const searchBoxRef = useRef<HTMLInputElement>(null);
|
||||
const hasCustomIcon = getIconLabels(note).length > 0;
|
||||
|
||||
function resetToDefaultIcon() {
|
||||
if (!note) return;
|
||||
for (const label of getIconLabels(note)) {
|
||||
attributes.removeAttributeById(note.noteId, label.attributeId);
|
||||
}
|
||||
onHide();
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="filter-row">
|
||||
<span>{t("note_icon.search")}</span>
|
||||
<FormTextBox
|
||||
inputRef={searchBoxRef}
|
||||
type="text"
|
||||
name="icon-search"
|
||||
placeholder={ filterByPrefix
|
||||
? t("note_icon.search_placeholder_filtered", {
|
||||
number: filteredIcons.length ?? 0,
|
||||
name: glob.iconRegistry.sources.find(s => s.prefix === filterByPrefix)?.name ?? ""
|
||||
})
|
||||
: t("note_icon.search_placeholder", { number: filteredIcons.length ?? 0, count: glob.iconRegistry.sources.length })}
|
||||
currentValue={search} onChange={setSearch}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
{isDesktop()
|
||||
? <>
|
||||
{hasCustomIcon && (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<ActionButton
|
||||
icon="bx bx-reset"
|
||||
text={t("note_icon.reset-default")}
|
||||
onClick={resetToDefaultIcon}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{<Dropdown
|
||||
buttonClassName="bx bx-filter-alt"
|
||||
hideToggleArrow
|
||||
noSelectButtonStyle
|
||||
noDropdownListStyle
|
||||
iconAction
|
||||
title={t("note_icon.filter")}
|
||||
>
|
||||
<IconFilterContent filterByPrefix={filterByPrefix} setFilterByPrefix={setFilterByPrefix} />
|
||||
</Dropdown>}
|
||||
</> : (
|
||||
<Dropdown
|
||||
buttonClassName="bx bx-dots-vertical-rounded"
|
||||
hideToggleArrow
|
||||
noSelectButtonStyle
|
||||
noDropdownListStyle
|
||||
iconAction
|
||||
dropdownContainerClassName="mobile-bottom-menu"
|
||||
>
|
||||
{hasCustomIcon && <>
|
||||
<FormListItem
|
||||
icon="bx bx-reset"
|
||||
onClick={resetToDefaultIcon}
|
||||
disabled={!hasCustomIcon}
|
||||
>{t("note_icon.reset-default")}</FormListItem>
|
||||
<FormDropdownDivider />
|
||||
</>}
|
||||
|
||||
<IconFilterContent filterByPrefix={filterByPrefix} setFilterByPrefix={setFilterByPrefix} />
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IconItemCell({ rowIndex, columnIndex, style, filteredIcons, columnCount }: CellComponentProps<{
|
||||
filteredIcons: IconWithName[];
|
||||
columnCount: number;
|
||||
}>) {
|
||||
const iconIndex = rowIndex * 12 + columnIndex;
|
||||
const iconIndex = rowIndex * columnCount + columnIndex;
|
||||
const iconData = filteredIcons[iconIndex] as IconWithName | undefined;
|
||||
if (!iconData) return <></> as React.ReactElement;
|
||||
|
||||
@@ -184,7 +279,7 @@ function IconFilterContent({ filterByPrefix, setFilterByPrefix }: {
|
||||
checked={filterByPrefix === "bx"}
|
||||
onClick={() => setFilterByPrefix("bx")}
|
||||
>{t("note_icon.filter-default")}</FormListItem>
|
||||
<FormDropdownDivider />
|
||||
{glob.iconRegistry.sources.length > 1 && <FormDropdownDivider />}
|
||||
|
||||
{glob.iconRegistry.sources.map(({ prefix, name, icon }) => (
|
||||
prefix !== "bx" && <FormListItem
|
||||
|
||||
@@ -109,4 +109,29 @@ body.experimental-feature-new-layout {
|
||||
--input-focus-color: initial;
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile .title-row {
|
||||
.icon-action:not(.note-icon) {
|
||||
--icon-button-size: 45px;
|
||||
--icon-button-icon-ratio: 0.5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.note-actions {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.note-badges {
|
||||
margin-inline: 0.5em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.note-icon-widget {
|
||||
margin-inline: 0.5em;
|
||||
|
||||
.note-icon {
|
||||
--icon-button-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#left-pane .tree-wrapper {
|
||||
.tree-wrapper {
|
||||
.note-indicator-icon.subtree-hidden-badge {
|
||||
font-family: inherit !important;
|
||||
margin-inline: 0.5em;
|
||||
@@ -9,7 +9,6 @@
|
||||
border-radius: 0.5em;
|
||||
font-size: 0.7rem;
|
||||
font-weight: normal;
|
||||
float: right;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
@@ -1992,7 +1992,7 @@ function buildEnhanceTitle() {
|
||||
if (isSubtreeHidden && count > 0) {
|
||||
const $badge = $(`<span class="note-indicator-icon subtree-hidden-badge">${count}</span>`);
|
||||
$badge.attr("title", t("note_tree.subtree-hidden-tooltip", { count }));
|
||||
$span.find(".fancytree-title").append($badge);
|
||||
$span.append($badge);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ export const TYPE_MAPPINGS: Record<ExtendedNoteType, NoteTypeMapping> = {
|
||||
},
|
||||
protectedSession: {
|
||||
view: () => import("./type_widgets/ProtectedSession"),
|
||||
className: "protected-session-password-component"
|
||||
className: "protected-session-password-component",
|
||||
isFullHeight: true
|
||||
},
|
||||
book: {
|
||||
view: () => import("./type_widgets/Book"),
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
line-height: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
padding-inline-end: 12px;
|
||||
|
||||
.arrow {
|
||||
font-size: 1.3em;
|
||||
@@ -17,21 +14,34 @@
|
||||
.collapsible-body {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
&.fully-expanded {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.collapsible-inner-body {
|
||||
padding-top: 0.5em;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
.collapsible-title .arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.collapsible-inner-body {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.with-transition {
|
||||
.collapsible-body {
|
||||
transition: height 250ms ease-in;
|
||||
}
|
||||
|
||||
.collapsible-inner-body {
|
||||
transition: opacity 250ms ease-in;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export function ExternallyControlledCollapsible({ title, children, className, ex
|
||||
const { height } = useElementSize(innerRef) ?? {};
|
||||
const contentId = useUniqueName();
|
||||
const [ transitionEnabled, setTransitionEnabled ] = useState(false);
|
||||
const [ fullyExpanded, setFullyExpanded ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
@@ -35,13 +36,28 @@ export function ExternallyControlledCollapsible({ title, children, className, ex
|
||||
return () => clearTimeout(timeout);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (expanded) {
|
||||
if (transitionEnabled) {
|
||||
const timeout = setTimeout(() => {
|
||||
setFullyExpanded(true);
|
||||
}, 250);
|
||||
return () => clearTimeout(timeout);
|
||||
} else {
|
||||
setFullyExpanded(true);
|
||||
}
|
||||
} else {
|
||||
setFullyExpanded(false);
|
||||
}
|
||||
}, [expanded, transitionEnabled])
|
||||
|
||||
return (
|
||||
<div className={clsx("collapsible", className, {
|
||||
expanded,
|
||||
"with-transition": transitionEnabled
|
||||
})}>
|
||||
<button
|
||||
className="collapsible-title"
|
||||
className="collapsible-title tn-low-profile"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
aria-expanded={expanded}
|
||||
aria-controls={contentId}
|
||||
@@ -53,7 +69,7 @@ export function ExternallyControlledCollapsible({ title, children, className, ex
|
||||
<div
|
||||
id={contentId}
|
||||
ref={bodyRef}
|
||||
className="collapsible-body"
|
||||
className={clsx("collapsible-body", {"fully-expanded": fullyExpanded})}
|
||||
style={{ height: expanded ? height : "0" }}
|
||||
aria-hidden={!expanded}
|
||||
>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ComponentChildren, HTMLAttributes } from "preact";
|
||||
import { CSSProperties, HTMLProps } from "preact/compat";
|
||||
import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { isMobile } from "../../services/utils";
|
||||
import { useTooltip, useUniqueName } from "./hooks";
|
||||
|
||||
type DataAttributes = {
|
||||
@@ -32,9 +33,10 @@ export interface DropdownProps extends Pick<HTMLProps<HTMLDivElement>, "id" | "c
|
||||
dropdownRef?: MutableRef<BootstrapDropdown | null>;
|
||||
titlePosition?: "top" | "right" | "bottom" | "left";
|
||||
titleOptions?: Partial<Tooltip.Options>;
|
||||
mobileBackdrop?: boolean;
|
||||
}
|
||||
|
||||
export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, dropdownContainerRef: externalContainerRef, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps, dropdownRef, titlePosition, titleOptions }: DropdownProps) {
|
||||
export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, dropdownContainerRef: externalContainerRef, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps, dropdownRef, titlePosition, titleOptions, mobileBackdrop }: DropdownProps) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||
const dropdownContainerRef = useRef<HTMLUListElement | null>(null);
|
||||
@@ -74,12 +76,18 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi
|
||||
setShown(true);
|
||||
externalOnShown?.();
|
||||
hideTooltip();
|
||||
}, [ hideTooltip ]);
|
||||
if (mobileBackdrop && isMobile()) {
|
||||
document.getElementById("context-menu-cover")?.classList.add("show", "global-menu-cover");
|
||||
}
|
||||
}, [ hideTooltip, mobileBackdrop ]);
|
||||
|
||||
const onHidden = useCallback(() => {
|
||||
setShown(false);
|
||||
externalOnHidden?.();
|
||||
}, []);
|
||||
if (mobileBackdrop && isMobile()) {
|
||||
document.getElementById("context-menu-cover")?.classList.remove("show", "global-menu-cover");
|
||||
}
|
||||
}, [ mobileBackdrop ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
@@ -3,8 +3,9 @@ import { useEffect, useRef } from "preact/hooks";
|
||||
|
||||
import ActionButton, { ActionButtonProps } from "./ActionButton";
|
||||
import Button, { ButtonProps } from "./Button";
|
||||
import { FormListItem, FormListItemOpts } from "./FormList";
|
||||
|
||||
interface FormFileUploadProps {
|
||||
export interface FormFileUploadProps {
|
||||
name?: string;
|
||||
onChange: (files: FileList | null) => void;
|
||||
multiple?: boolean;
|
||||
@@ -75,3 +76,25 @@ export function FormFileUploadActionButton({ onChange, ...buttonProps }: Omit<Ac
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to {@link FormFileUploadButton}, but uses an {@link FormListItem} instead of a normal {@link Button}.
|
||||
* @param param the change listener for the file upload and the properties for the button.
|
||||
*/
|
||||
export function FormFileUploadFormListItem({ onChange, children, ...buttonProps }: Omit<FormListItemOpts, "onClick"> & Pick<FormFileUploadProps, "onChange">) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormListItem
|
||||
{...buttonProps}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>{children}</FormListItem>
|
||||
<FormFileUpload
|
||||
inputRef={inputRef}
|
||||
hidden
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,3 +27,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-list-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1em;
|
||||
|
||||
.form-list-col {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export interface FormListBadge {
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface FormListItemOpts {
|
||||
export interface FormListItemOpts {
|
||||
children: ComponentChildren;
|
||||
icon?: string;
|
||||
value?: string;
|
||||
|
||||
212
apps/client/src/widgets/react/NotePropertyMenu.tsx
Normal file
212
apps/client/src/widgets/react/NotePropertyMenu.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import { FilterLabelsByType } from "@triliumnext/commons";
|
||||
import { Fragment, VNode } from "preact";
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import Component from "../../components/component";
|
||||
import FNote from "../../entities/fnote";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget";
|
||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "./FormList";
|
||||
import FormTextBox from "./FormTextBox";
|
||||
import { useNoteLabel, useNoteLabelBoolean } from "./hooks";
|
||||
import { ParentComponent } from "./react_utils";
|
||||
|
||||
export interface ClickContext {
|
||||
note: FNote;
|
||||
triggerCommand: NoteContextAwareWidget["triggerCommand"];
|
||||
}
|
||||
|
||||
export interface CheckBoxProperty {
|
||||
type: "checkbox",
|
||||
label: string;
|
||||
bindToLabel: FilterLabelsByType<boolean>;
|
||||
icon?: string;
|
||||
/** When true, the checkbox will be checked when the label value is false. Useful when the label represents a "hide" action, without exposing double negatives to the user. */
|
||||
reverseValue?: boolean;
|
||||
}
|
||||
|
||||
export interface ButtonProperty {
|
||||
type: "button",
|
||||
label: string;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
onClick(context: ClickContext): void;
|
||||
}
|
||||
|
||||
export interface SplitButtonProperty extends Omit<ButtonProperty, "type"> {
|
||||
type: "split-button";
|
||||
items({ note, parentComponent }: { note: FNote, parentComponent: Component }): VNode;
|
||||
}
|
||||
|
||||
export interface NumberProperty {
|
||||
type: "number",
|
||||
label: string;
|
||||
bindToLabel: FilterLabelsByType<number>;
|
||||
width?: number;
|
||||
min?: number;
|
||||
icon?: string;
|
||||
disabled?: (note: FNote) => boolean;
|
||||
}
|
||||
|
||||
export interface ComboBoxItem {
|
||||
/**
|
||||
* The value to set to the bound label, `null` has a special meaning which removes the label entirely.
|
||||
*/
|
||||
value: string | null;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface ComboBoxGroup {
|
||||
title: string;
|
||||
items: ComboBoxItem[];
|
||||
}
|
||||
|
||||
interface Separator {
|
||||
type: "separator"
|
||||
}
|
||||
|
||||
export interface ComboBoxProperty {
|
||||
type: "combobox",
|
||||
label: string;
|
||||
icon?: string;
|
||||
bindToLabel: FilterLabelsByType<string>;
|
||||
/**
|
||||
* The default value is used when the label is not set.
|
||||
*/
|
||||
defaultValue?: string;
|
||||
options: (ComboBoxItem | Separator | ComboBoxGroup)[];
|
||||
dropStart?: boolean;
|
||||
}
|
||||
|
||||
export type BookProperty = CheckBoxProperty | ButtonProperty | NumberProperty | ComboBoxProperty | SplitButtonProperty | Separator;
|
||||
|
||||
export function ViewProperty({ note, property }: { note: FNote, property: BookProperty }) {
|
||||
switch (property.type) {
|
||||
case "button":
|
||||
return <ButtonPropertyView note={note} property={property} />;
|
||||
case "split-button":
|
||||
return <SplitButtonPropertyView note={note} property={property} />;
|
||||
case "checkbox":
|
||||
return <CheckBoxPropertyView note={note} property={property} />;
|
||||
case "number":
|
||||
return <NumberPropertyView note={note} property={property} />;
|
||||
case "combobox":
|
||||
return <ComboBoxPropertyView note={note} property={property} />;
|
||||
case "separator":
|
||||
return <FormDropdownDivider />;
|
||||
}
|
||||
}
|
||||
|
||||
function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
|
||||
return (
|
||||
<FormListItem
|
||||
icon={property.icon}
|
||||
title={property.title}
|
||||
onClick={() => {
|
||||
if (!parentComponent) return;
|
||||
property.onClick({
|
||||
note,
|
||||
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
|
||||
});
|
||||
}}
|
||||
>{property.label}</FormListItem>
|
||||
);
|
||||
}
|
||||
|
||||
function SplitButtonPropertyView({ note, property }: { note: FNote, property: SplitButtonProperty }) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const ItemsComponent = property.items;
|
||||
const clickContext = parentComponent && {
|
||||
note,
|
||||
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
|
||||
};
|
||||
|
||||
return (parentComponent &&
|
||||
<FormDropdownSubmenu
|
||||
icon={property.icon ?? "bx bx-empty"}
|
||||
title={property.label}
|
||||
onDropdownToggleClicked={() => clickContext && property.onClick(clickContext)}
|
||||
>
|
||||
<ItemsComponent note={note} parentComponent={parentComponent} />
|
||||
</FormDropdownSubmenu>
|
||||
);
|
||||
}
|
||||
|
||||
function NumberPropertyView({ note, property }: { note: FNote, property: NumberProperty }) {
|
||||
//@ts-expect-error Interop with text box which takes in string values even for numbers.
|
||||
const [ value, setValue ] = useNoteLabel(note, property.bindToLabel);
|
||||
const disabled = property.disabled?.(note);
|
||||
|
||||
return (
|
||||
<FormListItem
|
||||
icon={property.icon}
|
||||
disabled={disabled}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{property.label}
|
||||
<FormTextBox
|
||||
type="number"
|
||||
currentValue={value ?? ""} onChange={setValue}
|
||||
style={{ width: (property.width ?? 100) }}
|
||||
min={property.min ?? 0}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</FormListItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ComboBoxPropertyView({ note, property }: { note: FNote, property: ComboBoxProperty }) {
|
||||
const [ value, setValue ] = useNoteLabel(note, property.bindToLabel);
|
||||
const valueWithDefault = value ?? property.defaultValue ?? null;
|
||||
|
||||
function renderItem(option: ComboBoxItem) {
|
||||
return (
|
||||
<FormListItem
|
||||
key={option.value}
|
||||
checked={valueWithDefault === option.value}
|
||||
onClick={() => setValue(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</FormListItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormDropdownSubmenu
|
||||
title={property.label}
|
||||
icon={property.icon ?? "bx bx-empty"}
|
||||
dropStart={property.dropStart}
|
||||
>
|
||||
{(property.options).map((option, index) => {
|
||||
if ("items" in option) {
|
||||
return (
|
||||
<Fragment key={option.title}>
|
||||
<FormListItem key={option.title} disabled>{option.title}</FormListItem>
|
||||
{option.items.map(renderItem)}
|
||||
{index < property.options.length - 1 && <FormDropdownDivider />}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
if ("type" in option) {
|
||||
return <FormDropdownDivider key={index} />;
|
||||
}
|
||||
|
||||
return renderItem(option);
|
||||
|
||||
})}
|
||||
</FormDropdownSubmenu>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckBoxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) {
|
||||
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
|
||||
return (
|
||||
<FormListToggleableItem
|
||||
icon={property.icon}
|
||||
title={property.label}
|
||||
currentValue={ property.reverseValue ? !value : value }
|
||||
onChange={newValue => setValue(property.reverseValue ? !newValue : newValue)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user