Compare commits
798 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
494d159107 | ||
|
|
0532186cb2 | ||
|
|
f6a28dab67 | ||
|
|
85cd981d8d | ||
|
|
f172b8508f | ||
|
|
4a71e3d795 | ||
|
|
101758a52b | ||
|
|
a493120095 | ||
|
|
8248de14d9 | ||
|
|
961914c4af | ||
|
|
71e3c79078 | ||
|
|
432ecc6072 | ||
|
|
71c2a309b7 | ||
|
|
3bdc358643 | ||
|
|
960fa91b52 | ||
|
|
644c5ce099 | ||
|
|
937e5939bc | ||
|
|
4215b22559 | ||
|
|
c6c334bd16 | ||
|
|
32ce9f55ed | ||
|
|
b41cc7641d | ||
|
|
ec73ad869f | ||
|
|
681320e2f2 | ||
|
|
a4951dbd86 | ||
|
|
3a744e06b8 | ||
|
|
aae916aafa | ||
|
|
1e99850592 | ||
|
|
b296a3d7e6 | ||
|
|
9c1b4aeba2 | ||
|
|
3fce80d6fb | ||
|
|
2683ee1a1a | ||
|
|
86bd49d873 | ||
|
|
89f5db7bf0 | ||
|
|
5ed694654c | ||
|
|
1ace668c8f | ||
|
|
3daea7efd3 | ||
|
|
347c409619 | ||
|
|
8fbf999df4 | ||
|
|
8344490906 | ||
|
|
7c41751c34 | ||
|
|
7ad48ee08f | ||
|
|
6a83bab773 | ||
|
|
5908ff0b79 | ||
|
|
a044e6c2eb | ||
|
|
88cb9541f0 | ||
|
|
47c6490ba9 | ||
|
|
4d30ea696e | ||
|
|
ef59e9998e | ||
|
|
f4dbd3e93c | ||
|
|
1d7a065b8a | ||
|
|
fdade5d96b | ||
|
|
e0613b7d27 | ||
|
|
ce4eab2d64 | ||
|
|
abfab4fc86 | ||
|
|
997ffb583b | ||
|
|
5eeb86fa0b | ||
|
|
08b8056df6 | ||
|
|
203d9f4119 | ||
|
|
03b6de9db7 | ||
|
|
e826fe6740 | ||
|
|
765c048fbd | ||
|
|
5638c3c40d | ||
|
|
8e2c3376a6 | ||
|
|
b24b25ef34 | ||
|
|
f670410cf2 | ||
|
|
3e23140bfb | ||
|
|
68d9b0b8d5 | ||
|
|
e88ccfeb06 | ||
|
|
7c31c7cec5 | ||
|
|
113cf5f40c | ||
|
|
8adf6186ac | ||
|
|
d59f9dc631 | ||
|
|
d98958c7bd | ||
|
|
b0955915fb | ||
|
|
c5cd9abc79 | ||
|
|
2504d69c31 | ||
|
|
86288080c8 | ||
|
|
e87aec28c1 | ||
|
|
247c252ef8 | ||
|
|
eedbd8fca9 | ||
|
|
d58492f8a4 | ||
|
|
726476a42c | ||
|
|
a66847a38e | ||
|
|
7f59b13788 | ||
|
|
c87a8b67d9 | ||
|
|
2dad83ee7f | ||
|
|
2fdda5c199 | ||
|
|
f73515245c | ||
|
|
4f018a9158 | ||
|
|
b165b46aa7 | ||
|
|
ccc3b24da7 | ||
|
|
ffaa74faaa | ||
|
|
cc2b15d8db | ||
|
|
e46191d137 | ||
|
|
9a1e5773a0 | ||
|
|
ba442d20ce | ||
|
|
f024b44f21 | ||
|
|
ecc8dc266a | ||
|
|
ce9fcb72c1 | ||
|
|
0b6ecc730d | ||
|
|
4c3251fcea | ||
|
|
26ea44a0b3 | ||
|
|
c039b942e0 | ||
|
|
f3f835844d | ||
|
|
ae1f67c84b | ||
|
|
243aa671f1 | ||
|
|
a2aca093ed | ||
|
|
7d61dcf049 | ||
|
|
b64119268b | ||
|
|
6e9a7c7e17 | ||
|
|
b441347d1f | ||
|
|
4c986d7543 | ||
|
|
ef0dbbd66b | ||
|
|
0adebd0042 | ||
|
|
50bedc6ef4 | ||
|
|
41187bf1fe | ||
|
|
97c9f27988 | ||
|
|
1f1be4a563 | ||
|
|
5f2f2b0830 | ||
|
|
30a2a5f09e | ||
|
|
13afbd66df | ||
|
|
f8d79fae43 | ||
|
|
b6df0cf1c7 | ||
|
|
9e5e297ddf | ||
|
|
86dad65c83 | ||
|
|
b581b30abc | ||
|
|
f910338a1b | ||
|
|
2f41144e53 | ||
|
|
51a32d350e | ||
|
|
4d6820dc53 | ||
|
|
c8e54037c6 | ||
|
|
2c8c54ee75 | ||
|
|
d050217f5b | ||
|
|
6c7c732d5b | ||
|
|
d12ffadf4a | ||
|
|
b2a490576d | ||
|
|
758b87affc | ||
|
|
393fcb9048 | ||
|
|
1141ed8b48 | ||
|
|
05fdaf5162 | ||
|
|
6d60f5b217 | ||
|
|
ac33e80ce9 | ||
|
|
2ac869b50b | ||
|
|
84bf4e0d99 | ||
|
|
2bc19a09ab | ||
|
|
54f2e33b77 | ||
|
|
9d01829ff2 | ||
|
|
bd866bc206 | ||
|
|
3f6be04dc8 | ||
|
|
7003d10671 | ||
|
|
a82126ca55 | ||
|
|
77d74f7993 | ||
|
|
d1ee82fe1d | ||
|
|
3011bbad82 | ||
|
|
caa3688c8c | ||
|
|
fbe65f8f5d | ||
|
|
b6102e3124 | ||
|
|
75c7fadb8b | ||
|
|
1f5f383921 | ||
|
|
d39214f59f | ||
|
|
c5c001cffc | ||
|
|
1c437fb6e6 | ||
|
|
7c12335581 | ||
|
|
f98472457c | ||
|
|
256b254c0a | ||
|
|
a59fc25ff9 | ||
|
|
d1e2aa1c64 | ||
|
|
877aa85fff | ||
|
|
17251a4826 | ||
|
|
fd44ed7873 | ||
|
|
415ccdd57f | ||
|
|
33c77530a9 | ||
|
|
b429ec2041 | ||
|
|
18e1f4dc45 | ||
|
|
b56055fca4 | ||
|
|
f13f1be14a | ||
|
|
1d55c4117c | ||
|
|
6e54dfa907 | ||
|
|
f5b7df6a58 | ||
|
|
c0bdf0a6c2 | ||
|
|
a1c4f2d3c6 | ||
|
|
e7653658fc | ||
|
|
86d1abab49 | ||
|
|
b613d40679 | ||
|
|
53bc204cf4 | ||
|
|
1d77169f54 | ||
|
|
2a1d71c106 | ||
|
|
3d897a1135 | ||
|
|
0a528039b0 | ||
|
|
6f75d505cb | ||
|
|
decb48db85 | ||
|
|
a640c25f84 | ||
|
|
4770d466be | ||
|
|
4871580e10 | ||
|
|
8533e30ce5 | ||
|
|
153aeebb13 | ||
|
|
1f04660242 | ||
|
|
d6cf91b767 | ||
|
|
d34084a563 | ||
|
|
095e275ef8 | ||
|
|
84d8c608fd | ||
|
|
25ae1596f4 | ||
|
|
9833a8ac06 | ||
|
|
27ab512611 | ||
|
|
94ce26c3e7 | ||
|
|
fe64c66b60 | ||
|
|
f53e2b6648 | ||
|
|
e7adc6f7ca | ||
|
|
7b9b6fabfa | ||
|
|
04e611fb97 | ||
|
|
a050c2673c | ||
|
|
30dce8bda0 | ||
|
|
2241a33d41 | ||
|
|
2692a22f93 | ||
|
|
fc80e33c43 | ||
|
|
8cc82e2967 | ||
|
|
7d71087231 | ||
|
|
219647482c | ||
|
|
0739e10a8e | ||
|
|
3d5185fe23 | ||
|
|
65813918d0 | ||
|
|
cffb667940 | ||
|
|
fed9fad7d7 | ||
|
|
78a28b09b5 | ||
|
|
d0aea34ea9 | ||
|
|
3924d53360 | ||
|
|
8c57f7e10f | ||
|
|
d7d6562cc4 | ||
|
|
07dfd5f504 | ||
|
|
7bd36394e2 | ||
|
|
94d3a92181 | ||
|
|
70f1f5559f | ||
|
|
308dff16be | ||
|
|
a933f1c3a6 | ||
|
|
0bc9ebf201 | ||
|
|
fad9053b08 | ||
|
|
59fcc3b256 | ||
|
|
729c3d2828 | ||
|
|
8ca22f1b93 | ||
|
|
bc48b43b34 | ||
|
|
73d7addee6 | ||
|
|
6349262332 | ||
|
|
7130e7c9a6 | ||
|
|
31e357efcb | ||
|
|
3437541940 | ||
|
|
0264b32886 | ||
|
|
4e5a805889 | ||
|
|
fedddcaa84 | ||
|
|
06f0138bca | ||
|
|
805fc8ac77 | ||
|
|
8986c5b524 | ||
|
|
8f131c83a8 | ||
|
|
9c8760ef7d | ||
|
|
d4f7050351 | ||
|
|
a3a306c9cf | ||
|
|
1fc237c21b | ||
|
|
029bf36dbf | ||
|
|
d5179f5780 | ||
|
|
7903068cc6 | ||
|
|
086c791a82 | ||
|
|
0fa791aefb | ||
|
|
3860de6050 | ||
|
|
efe4db4fdd | ||
|
|
15b9d64b7b | ||
|
|
c836ce9a45 | ||
|
|
d83e9ef60e | ||
|
|
283aef2fe0 | ||
|
|
6f6c208360 | ||
|
|
889688c3a4 | ||
|
|
e09f190985 | ||
|
|
7aad817dfb | ||
|
|
be8e17f135 | ||
|
|
09c09ee31a | ||
|
|
abf6336514 | ||
|
|
e3e51062fa | ||
|
|
69235ef3ed | ||
|
|
dad0042aa6 | ||
|
|
4c03e553f7 | ||
|
|
790fc07f42 | ||
|
|
a9da31b6ed | ||
|
|
d59340176c | ||
|
|
5d84788cde | ||
|
|
c5454e0de5 | ||
|
|
578e549cb8 | ||
|
|
3acfba425e | ||
|
|
2733830531 | ||
|
|
17ae3f6461 | ||
|
|
faa86db7d6 | ||
|
|
416bf25a91 | ||
|
|
d9ac3d199c | ||
|
|
8e4e522fee | ||
|
|
e7804b7e3d | ||
|
|
34615acb46 | ||
|
|
451de100be | ||
|
|
7d1b03ec17 | ||
|
|
a54808da93 | ||
|
|
b0d74adfe6 | ||
|
|
c1d9fbca7f | ||
|
|
3370060c80 | ||
|
|
962b5a5b32 | ||
|
|
64a417bc04 | ||
|
|
5486b04f25 | ||
|
|
d197fb9fdb | ||
|
|
85bebc97e8 | ||
|
|
2338b32d96 | ||
|
|
50461f5e34 | ||
|
|
5188d45f5d | ||
|
|
0fe07eed04 | ||
|
|
fd67bfae84 | ||
|
|
36925bfc39 | ||
|
|
8b7a2fad6a | ||
|
|
54285cb8b7 | ||
|
|
df07661d3b | ||
|
|
ddb64526af | ||
|
|
5b75e0988b | ||
|
|
0936bc9bb6 | ||
|
|
cdb7d77dcf | ||
|
|
e29833cdf2 | ||
|
|
b79942f1b9 | ||
|
|
541ff4f9be | ||
|
|
ffe72b2094 | ||
|
|
a335939483 | ||
|
|
c867127b16 | ||
|
|
18dec14866 | ||
|
|
e2ab165d52 | ||
|
|
dd6a59131b | ||
|
|
8c8804dc24 | ||
|
|
45d4b3ebb9 | ||
|
|
04272814d5 | ||
|
|
50f7808dad | ||
|
|
8d68bbd0b4 | ||
|
|
2f7d6a76f7 | ||
|
|
df7f95252b | ||
|
|
85cc1a3a23 | ||
|
|
01e4fc7f66 | ||
|
|
7f26da6ba7 | ||
|
|
8612246987 | ||
|
|
07ad2b927b | ||
|
|
a95db26128 | ||
|
|
2286c489ed | ||
|
|
cecd3e719f | ||
|
|
fd0819b235 | ||
|
|
fd967fc2fc | ||
|
|
f268793990 | ||
|
|
7a40e41224 | ||
|
|
7b086542d8 | ||
|
|
c98fe4ed4c | ||
|
|
cdb4fa11f7 | ||
|
|
fffb10e802 | ||
|
|
25c52658e1 | ||
|
|
1bdad7d4f0 | ||
|
|
abc72d3216 | ||
|
|
61220433e1 | ||
|
|
8ef02fbdb8 | ||
|
|
f67031a7fc | ||
|
|
182839c8de | ||
|
|
341dde9c74 | ||
|
|
b059914c6f | ||
|
|
2e2d343aed | ||
|
|
62c4b5e149 | ||
|
|
4c3e186d0b | ||
|
|
b1e03cd36b | ||
|
|
81d7236a1e | ||
|
|
85d82ed732 | ||
|
|
23a702d9a3 | ||
|
|
b65d624e3b | ||
|
|
5c4c20d170 | ||
|
|
5106b6f471 | ||
|
|
b3e0c0c1b3 | ||
|
|
7829afef3e | ||
|
|
6119cef5c8 | ||
|
|
f605ec0cff | ||
|
|
c1605ae673 | ||
|
|
1333c28b8d | ||
|
|
2bf6c26fb2 | ||
|
|
69a1318442 | ||
|
|
ba4069fe7d | ||
|
|
4673d9db29 | ||
|
|
cc830920dd | ||
|
|
76ee4f2bac | ||
|
|
3406e6e489 | ||
|
|
c1e7567a6e | ||
|
|
8627c2db54 | ||
|
|
6743340aba | ||
|
|
75443255b0 | ||
|
|
1e4ed4b102 | ||
|
|
4b2995ca47 | ||
|
|
1804c4b11a | ||
|
|
230060d592 | ||
|
|
6c1346d513 | ||
|
|
0f5cd8692e | ||
|
|
e4f6419687 | ||
|
|
168ac4fe5b | ||
|
|
cfb4bff22a | ||
|
|
1c794c18ea | ||
|
|
423071e492 | ||
|
|
401be90177 | ||
|
|
511a516f20 | ||
|
|
0d9a4ad83b | ||
|
|
17fa0a7e45 | ||
|
|
988025d9c5 | ||
|
|
c3e2437eae | ||
|
|
1a0220709d | ||
|
|
0b33f170ad | ||
|
|
6fb731275f | ||
|
|
62037c60e2 | ||
|
|
f4408c3acd | ||
|
|
446185c51d | ||
|
|
62d05b90c5 | ||
|
|
4962ac5236 | ||
|
|
98158cbef9 | ||
|
|
db2b0c096a | ||
|
|
07b9b812e2 | ||
|
|
8baef8b7f7 | ||
|
|
bd78c12329 | ||
|
|
5e903abab5 | ||
|
|
6d1c1d0cd4 | ||
|
|
e25ded832d | ||
|
|
cde7cf09ec | ||
|
|
84bd8c55c7 | ||
|
|
52b0905d0d | ||
|
|
5d3a77d711 | ||
|
|
78b9838fde | ||
|
|
ca622350e4 | ||
|
|
5173962ee8 | ||
|
|
6b5514d7f2 | ||
|
|
51f2340d8c | ||
|
|
385a410917 | ||
|
|
435a2587bc | ||
|
|
25b3eeaa2e | ||
|
|
ad84aa66f1 | ||
|
|
e9c0e0a266 | ||
|
|
942d779a82 | ||
|
|
eebc0d1d58 | ||
|
|
76ebb1aa27 | ||
|
|
181cedbcf2 | ||
|
|
2e24920a1e | ||
|
|
8f444cb2a3 | ||
|
|
f5a832ae68 | ||
|
|
1b611136d6 | ||
|
|
04f6e95f93 | ||
|
|
f2e907d741 | ||
|
|
6e86172195 | ||
|
|
7e26d0e246 | ||
|
|
53be512154 | ||
|
|
1310db6be7 | ||
|
|
b69e5d66f9 | ||
|
|
1242a9f7cd | ||
|
|
4376dd0670 | ||
|
|
6e3cf90a1a | ||
|
|
7bbfeeef65 | ||
|
|
6fca9ac1e7 | ||
|
|
490d5e190b | ||
|
|
901174ef25 | ||
|
|
3d10f1eb41 | ||
|
|
a26422d0f2 | ||
|
|
e7a100e9fb | ||
|
|
f8ac0362ec | ||
|
|
b705810d3d | ||
|
|
046773f901 | ||
|
|
f1b181e772 | ||
|
|
df3b30a422 | ||
|
|
12c03793ed | ||
|
|
9513713edf | ||
|
|
ce4374e99f | ||
|
|
fe562944fa | ||
|
|
b957b2ad64 | ||
|
|
370c219670 | ||
|
|
161372754a | ||
|
|
e7c2ce8262 | ||
|
|
8043570761 | ||
|
|
879856308b | ||
|
|
63547e2f10 | ||
|
|
d7f713c918 | ||
|
|
20bf0daf97 | ||
|
|
c762deecd5 | ||
|
|
c92e401b37 | ||
|
|
9aa01aebbf | ||
|
|
4fc6980cd5 | ||
|
|
217a622334 | ||
|
|
345df3e6fe | ||
|
|
e3cf795ab3 | ||
|
|
c3babf96bd | ||
|
|
c0c3b4bf52 | ||
|
|
e10eb00788 | ||
|
|
e64f88c742 | ||
|
|
59dd52f0e6 | ||
|
|
39efafcbb8 | ||
|
|
45e21ea0db | ||
|
|
3fb7b37aaf | ||
|
|
c7c7ecfb2e | ||
|
|
0bc4cea2b1 | ||
|
|
c1da176baa | ||
|
|
914c111fa9 | ||
|
|
b2544f4750 | ||
|
|
3c77bee0f5 | ||
|
|
3673e6c0e2 | ||
|
|
a975b85469 | ||
|
|
504a484c8b | ||
|
|
e8e80f6ddb | ||
|
|
c72697b81b | ||
|
|
65503affd0 | ||
|
|
d4e3da3088 | ||
|
|
1de99078f7 | ||
|
|
f8a77854ee | ||
|
|
7b52e04d38 | ||
|
|
64ce683a5c | ||
|
|
0978c220af | ||
|
|
f48598d55f | ||
|
|
cd38702394 | ||
|
|
8cd11dd959 | ||
|
|
945c25f97e | ||
|
|
a16f4635e7 | ||
|
|
45df35d913 | ||
|
|
dc2e53c589 | ||
|
|
486350e447 | ||
|
|
065000d28e | ||
|
|
e59f3343e2 | ||
|
|
42d598f62c | ||
|
|
58c6ed7885 | ||
|
|
5b5990fd32 | ||
|
|
582df27c70 | ||
|
|
271cc47fb1 | ||
|
|
6a2297e739 | ||
|
|
215836483b | ||
|
|
55113c7998 | ||
|
|
29b0a2d922 | ||
|
|
63ee47ba7f | ||
|
|
c991c1220e | ||
|
|
8b1804caf2 | ||
|
|
068a2babc7 | ||
|
|
18593d252e | ||
|
|
404eeac1e8 | ||
|
|
cfbee6a814 | ||
|
|
518bf64c29 | ||
|
|
ca194f7172 | ||
|
|
f37a9d7a83 | ||
|
|
0162af9aff | ||
|
|
48ffcdbe9f | ||
|
|
b2fe971c28 | ||
|
|
dd583a3079 | ||
|
|
f6744f75f8 | ||
|
|
e403d35b7b | ||
|
|
15f4d2844c | ||
|
|
5fafc8f3d7 | ||
|
|
f044351727 | ||
|
|
841b4e211b | ||
|
|
77700797a2 | ||
|
|
dec8a6e237 | ||
|
|
4371aa56d3 | ||
|
|
35e53aa5f5 | ||
|
|
402e28828d | ||
|
|
63fd0d7085 | ||
|
|
b8817a5692 | ||
|
|
45a43c8748 | ||
|
|
21e733f263 | ||
|
|
4fce7adbf0 | ||
|
|
7c455e695c | ||
|
|
41968bb9b8 | ||
|
|
23f8a05101 | ||
|
|
671b8416bc | ||
|
|
0bb2dd5a91 | ||
|
|
160ba24d02 | ||
|
|
2637e33954 | ||
|
|
dd24f3ffa3 | ||
|
|
42dc67e2fc | ||
|
|
2b2f2adde7 | ||
|
|
1dcb40c65e | ||
|
|
cb29a307f0 | ||
|
|
ee1b2c699d | ||
|
|
bcec6dfca8 | ||
|
|
9ed663948e | ||
|
|
63d96c0da3 | ||
|
|
b959f0bd0c | ||
|
|
7dd5678621 | ||
|
|
a6069c1b12 | ||
|
|
a67f075686 | ||
|
|
8590752582 | ||
|
|
01aad39f3a | ||
|
|
019a9eed9d | ||
|
|
e6f7c435fe | ||
|
|
b9e1c3bad9 | ||
|
|
5c5a9129d7 | ||
|
|
2fe7ef0b57 | ||
|
|
69ac23abe7 | ||
|
|
d34e181da8 | ||
|
|
2917ff57e0 | ||
|
|
1fb8ead51e | ||
|
|
89cf7d694f | ||
|
|
5f252cc4d6 | ||
|
|
4ddd56cef8 | ||
|
|
c4d9af53f1 | ||
|
|
e2ee295e6c | ||
|
|
015602e29c | ||
|
|
e2efbb3110 | ||
|
|
dd5f829585 | ||
|
|
d0e47e8d1c | ||
|
|
f7a28db487 | ||
|
|
b80a7acfba | ||
|
|
130a4076c5 | ||
|
|
346b451f01 | ||
|
|
4151efe9f7 | ||
|
|
562e1fde5b | ||
|
|
3d7b9177c2 | ||
|
|
ef4906427b | ||
|
|
b337729c31 | ||
|
|
358c498f99 | ||
|
|
fa380c0544 | ||
|
|
270a19e462 | ||
|
|
628e9392da | ||
|
|
50e515e4dd | ||
|
|
ebbc2e56dc | ||
|
|
666fb97133 | ||
|
|
40d3a34e2b | ||
|
|
6afadde899 | ||
|
|
64763d0bdf | ||
|
|
a329191a61 | ||
|
|
dabe261541 | ||
|
|
be4df08e7e | ||
|
|
19f8a91569 | ||
|
|
46ec0b14e2 | ||
|
|
d41710ac50 | ||
|
|
947ddf635d | ||
|
|
974cedabd2 | ||
|
|
77cece4427 | ||
|
|
3463adcda5 | ||
|
|
88b3125a54 | ||
|
|
c89fa0ea6a | ||
|
|
6f042a5f54 | ||
|
|
c4617e8ea7 | ||
|
|
7183145273 | ||
|
|
f8be003704 | ||
|
|
3f24150e1e | ||
|
|
ea7b72521e | ||
|
|
e32f957a5d | ||
|
|
01a75d8bbc | ||
|
|
8e537c0f0a | ||
|
|
0c5715b4d7 | ||
|
|
47200625cc | ||
|
|
5d94e27be6 | ||
|
|
6680918c50 | ||
|
|
9259bb7f42 | ||
|
|
c8c88dd1ce | ||
|
|
f174cf416e | ||
|
|
8bc0090d5d | ||
|
|
d233e007c4 | ||
|
|
8df5ed544d | ||
|
|
2c5096f638 | ||
|
|
32b098771f | ||
|
|
7bf6cd7b13 | ||
|
|
f549de1ce2 | ||
|
|
17182dc316 | ||
|
|
3d570f73ce | ||
|
|
ce21b2ebc0 | ||
|
|
00046b6d9f | ||
|
|
a11d301525 | ||
|
|
bc6ab7bddf | ||
|
|
f235200551 | ||
|
|
23a7e39cb2 | ||
|
|
50ab633e6f | ||
|
|
c430d9ea46 | ||
|
|
baebfe9630 | ||
|
|
3aeb8f6854 | ||
|
|
d57d5e983d | ||
|
|
0f42669043 | ||
|
|
d632b4e975 | ||
|
|
7f64339ffc | ||
|
|
c994f5256a | ||
|
|
190cf013ce | ||
|
|
5f6b20ca8b | ||
|
|
f5b545cefd | ||
|
|
515d9e879c | ||
|
|
d4500f092b | ||
|
|
2fbe4384aa | ||
|
|
3691f26372 | ||
|
|
de610bbbb0 | ||
|
|
1105f198d6 | ||
|
|
11cd03be69 | ||
|
|
0b0544dbdb | ||
|
|
14e10d8a42 | ||
|
|
7dd715894c | ||
|
|
8a5ea01d0d | ||
|
|
19d5339b02 | ||
|
|
f233e4e5dd | ||
|
|
afda85027e | ||
|
|
4dbcc9fc7e | ||
|
|
e040d554ec | ||
|
|
0b33958f3c | ||
|
|
008c4715b4 | ||
|
|
8a685f9f16 | ||
|
|
2d34383cd0 | ||
|
|
5e066e8f17 | ||
|
|
97409d1dfe | ||
|
|
f301478535 | ||
|
|
5e3424ae3e | ||
|
|
4b3a5a09a2 | ||
|
|
507e4f77ac | ||
|
|
80193b9b58 | ||
|
|
b796389d37 | ||
|
|
8f91a53fed | ||
|
|
e04f41d25f | ||
|
|
8c84faecd3 | ||
|
|
b40b81bf98 | ||
|
|
dab7312ec8 | ||
|
|
1d51e12de6 | ||
|
|
9e8f1fe4e9 | ||
|
|
d7423a148b | ||
|
|
b9f5c63011 | ||
|
|
36a74e65a6 | ||
|
|
103145c45a | ||
|
|
cd943b3df4 | ||
|
|
3eefeef7fd | ||
|
|
20ff71ed11 | ||
|
|
6423d35b00 | ||
|
|
f2ac573821 | ||
|
|
1369cda622 | ||
|
|
4053b5cf0a | ||
|
|
4cddc2a04f | ||
|
|
5e582e66b6 | ||
|
|
d414b8bb41 | ||
|
|
b2976d922f | ||
|
|
8e7ac4a589 | ||
|
|
46c529610c | ||
|
|
45c2a788e6 | ||
|
|
747679eb52 | ||
|
|
b3d8f6e01e | ||
|
|
61db5be4b1 | ||
|
|
7207ce6e66 | ||
|
|
e97d80a30f | ||
|
|
d70de78357 | ||
|
|
2791dd736d | ||
|
|
c8022055e6 | ||
|
|
203c067d8d | ||
|
|
64571285bc | ||
|
|
794cc1499b | ||
|
|
fd245c128b | ||
|
|
c59f59b793 | ||
|
|
d27712b4b4 | ||
|
|
446f56904c | ||
|
|
7edd9fbf6d | ||
|
|
888122c9d8 | ||
|
|
7d803748fe | ||
|
|
ed0421bcd5 | ||
|
|
581b273407 | ||
|
|
3e2a60a191 | ||
|
|
d5c80d3a20 | ||
|
|
fe627f9944 | ||
|
|
8f55b618f3 | ||
|
|
493ce0de27 | ||
|
|
7fc4ff1fad | ||
|
|
d2c1baf5a0 | ||
|
|
0ef6ece62c | ||
|
|
6291cceb9d | ||
|
|
069be643db | ||
|
|
990a3d5305 | ||
|
|
6b13e52d72 | ||
|
|
9f181ccafb | ||
|
|
72dc9dacab | ||
|
|
da68103ac1 | ||
|
|
c2367ccbec | ||
|
|
24ee7f72a1 | ||
|
|
fe2421f836 | ||
|
|
fd091606f2 | ||
|
|
0ba38cc092 | ||
|
|
e1ebd6ac91 | ||
|
|
229f3e44f2 | ||
|
|
39a9f6dffe | ||
|
|
a05e7b03ae | ||
|
|
91606f76c7 | ||
|
|
00cf009439 | ||
|
|
c0315208fd | ||
|
|
a2f3dfb373 | ||
|
|
41b0c5a2fe | ||
|
|
9d23948900 | ||
|
|
9a2057f15e | ||
|
|
651d74ac9c | ||
|
|
13df3a4acb | ||
|
|
2d965c462b | ||
|
|
00ae688bdb | ||
|
|
05de0d8c64 | ||
|
|
2a19e7afe0 | ||
|
|
ce9210067a | ||
|
|
5e4606f8c4 | ||
|
|
00ef243084 | ||
|
|
790188a41a | ||
|
|
f903987606 | ||
|
|
6d9907858b | ||
|
|
fa79cc0340 | ||
|
|
2d51467728 | ||
|
|
477b4e0654 | ||
|
|
d7c5e5866c | ||
|
|
36f3b275b8 | ||
|
|
38a5fdfe89 | ||
|
|
08c1218a40 | ||
|
|
0eeff1bc0e | ||
|
|
5b8edf1e76 | ||
|
|
1db67b1ef6 | ||
|
|
e1e2013eaf |
|
|
@ -1,2 +0,0 @@
|
|||
npm node_modules
|
||||
build
|
||||
140
.eslintrc
|
|
@ -1,140 +0,0 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"args": "none"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"brace-style": [
|
||||
"error",
|
||||
"1tbs",
|
||||
{
|
||||
"allowSingleLine": true
|
||||
}
|
||||
],
|
||||
"consistent-return": "off",
|
||||
"quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"no-mixed-spaces-and-tabs": "error",
|
||||
"indent": [
|
||||
"error",
|
||||
"tab",
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"arrow-parens": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"eol-last": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"func-call-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"always-multiline"
|
||||
],
|
||||
"no-multi-spaces": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-whitespace-before-property": "off",
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"semi-style": [
|
||||
"error",
|
||||
"last"
|
||||
],
|
||||
"space-in-parens": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"block-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"eqeqeq": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"null": "ignore"
|
||||
}
|
||||
],
|
||||
"spaced-comment": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"markers": [
|
||||
"!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"yoda": "error",
|
||||
"prefer-destructuring": [
|
||||
"error",
|
||||
{
|
||||
"object": false,
|
||||
"array": false
|
||||
}
|
||||
],
|
||||
"operator-assignment": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"no-useless-computed-key": "error",
|
||||
"no-unneeded-ternary": [
|
||||
"error",
|
||||
{
|
||||
"defaultAssignment": false
|
||||
}
|
||||
],
|
||||
"no-invalid-regexp": "error",
|
||||
"no-constant-condition": [
|
||||
"error",
|
||||
{
|
||||
"checkLoops": false
|
||||
}
|
||||
],
|
||||
"no-duplicate-imports": "error",
|
||||
"no-extra-semi": "error",
|
||||
"dot-notation": "error",
|
||||
"no-useless-escape": [
|
||||
"error"
|
||||
]
|
||||
}
|
||||
}
|
||||
1
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
ko_fi: paul305844
|
||||
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Report issues with plugin
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Steps to Reproduce**
|
||||
(It would be great if you could do it in the sandbox vault (https://help.obsidian.md/sandbox) or at least disable all other plugins except folder notes)
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Obsidian version**
|
||||
- Installer version: [e.g. v1.8.7]
|
||||
- Current version: [e.g. v1.8.10]
|
||||
|
||||
**Plugin information**
|
||||
- Version: [e.g. v1.7.30]
|
||||
- [ ] Beta version
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
7
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
contact_links:
|
||||
- name: Request a new feature
|
||||
url: https://github.com/LostPaul/obsidian-folder-notes/discussions/
|
||||
about: Instead of creating a new issue for feature requests, use the discussions feature instead
|
||||
- name: Anything regarding the folder overview feature
|
||||
url: https://github.com/LostPaul/obsidian-folder-overview
|
||||
about: The folder overview feature is a separate plugin, but it is bundled with this plugin. If you have any issues or feature requests regarding the folder overview feature, please create an issue in the folder overview repository.
|
||||
10
.github/ISSUE_TEMPLATE/docs-issue.md
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
name: Docs issue
|
||||
about: Report issues related to the docs of the plugin
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
81
.github/workflows/beta-release.yaml
vendored
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# https://github.com/SilentVoid13/Templater/blob/master/.github/workflows/release.yml
|
||||
|
||||
name: Plugin-beta release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+.[0-9]+-[0-9]+-beta"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout current branch
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
# This checks out the branch that triggered the workflow
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Initialize and update submodules
|
||||
run: |
|
||||
git submodule init
|
||||
git submodule update --remote
|
||||
|
||||
- name: Fetch all branches
|
||||
run: git fetch --all
|
||||
|
||||
- name: Extract version from tag
|
||||
id: extract_version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Strip -beta from version
|
||||
run: |
|
||||
VERSION=${{ env.VERSION }}
|
||||
RELEASE_VERSION=${VERSION%-beta}
|
||||
echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV
|
||||
|
||||
# Update manifest-beta.json in main branch
|
||||
- name: Checkout main branch to update manifest-beta.json
|
||||
run: |
|
||||
git checkout main
|
||||
git pull origin main
|
||||
|
||||
- name: Update manifest-beta.json in main branch
|
||||
run: |
|
||||
VERSION=${{ env.VERSION }}
|
||||
jq --arg version "$VERSION" '.version = $version' manifest-beta.json > manifest-beta-temp.json
|
||||
mv manifest-beta-temp.json manifest-beta.json
|
||||
|
||||
- name: Commit and push manifest-beta.json changes in main
|
||||
run: |
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git add manifest-beta.json
|
||||
git commit -m "Update manifest-beta.json to version $VERSION in main branch"
|
||||
git push origin main
|
||||
|
||||
# Switch back to the original branch that triggered the workflow
|
||||
- name: Switch back to branch that triggered the workflow
|
||||
run: |
|
||||
git checkout ${{ github.ref_name }}
|
||||
git pull origin ${{ github.ref_name }}
|
||||
|
||||
- name: npm build
|
||||
run: |
|
||||
npm install
|
||||
npm run fn-build --if-present
|
||||
|
||||
- name: Set Release Name
|
||||
run: echo "RELEASE_NAME=${{ env.RELEASE_VERSION }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Plugin beta release
|
||||
uses: ncipollo/release-action@v1.12.0
|
||||
with:
|
||||
name: Beta release ${{ env.RELEASE_NAME }}
|
||||
prerelease: true
|
||||
artifacts: "main.js,manifest.json,styles.css"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ env.VERSION }}
|
||||
66
.github/workflows/build-and-push.yml
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
name: Build and Push to Folder Overview
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Tag to build"
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout folder-notes
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Update submodules manually
|
||||
run: |
|
||||
git submodule sync
|
||||
git submodule update --init --recursive --remote
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build folder-overview
|
||||
run: npm run fv-build
|
||||
|
||||
- name: Verify build output
|
||||
run: |
|
||||
ls -la src/obsidian-folder-overview/
|
||||
|
||||
- name: Checkout folder-overview
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: LostPaul/obsidian-folder-overview
|
||||
token: ${{ secrets.PAT }}
|
||||
ref: main
|
||||
path: folder-overview
|
||||
|
||||
- name: Copy built files to folder-overview
|
||||
run: |
|
||||
cp src/obsidian-folder-overview/main.js \
|
||||
src/obsidian-folder-overview/styles.css folder-overview/
|
||||
|
||||
- name: Commit and push built files
|
||||
run: |
|
||||
cd folder-overview
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git add -f main.js styles.css
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit."
|
||||
else
|
||||
git commit -m "Add built files for release"
|
||||
git push origin main
|
||||
fi
|
||||
|
||||
- name: Trigger release on folder-overview
|
||||
run: |
|
||||
curl -X POST -H "Authorization: token ${{ secrets.PAT }}" \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
https://api.github.com/repos/LostPaul/obsidian-folder-overview/actions/workflows/release.yml/dispatches \
|
||||
-d '{"ref": "main", "inputs": {"tag": "${{ github.event.inputs.tag }}"}}'
|
||||
29
.github/workflows/deploy-mkdocs.yml
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: .cache
|
||||
key: mkdocs-${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
mkdocs-${{ runner.os }}-
|
||||
|
||||
- run: pip install mkdocs-material mkdocs-video pillow cairosvg
|
||||
|
||||
- working-directory: ./docs
|
||||
run: mkdocs gh-deploy --force
|
||||
|
||||
30
.github/workflows/label-beta.yml
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
name: Label Beta Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
jobs:
|
||||
label-beta:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check for Beta Version checkbox
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const betaChecked = context.payload.issue.body.includes("- [x] Beta version");
|
||||
|
||||
if (betaChecked) {
|
||||
const issue_number = context.payload.issue.number;
|
||||
const labels = context.payload.issue.labels.map(label => label.name);
|
||||
|
||||
if (!labels.includes('beta')) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
labels: ['beta']
|
||||
});
|
||||
}
|
||||
}
|
||||
80
.github/workflows/release.yaml
vendored
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# https://github.com/SilentVoid13/Templater/blob/master/.github/workflows/release.yml
|
||||
name: Plugin release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
attestations: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch all branches
|
||||
run: git fetch --all
|
||||
|
||||
- name: Extract branch name
|
||||
run: |
|
||||
BRANCH_NAME=$(git name-rev --name-only $GITHUB_SHA | sed 's|^remotes/origin/||' | sed 's|^origin/||')
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
|
||||
|
||||
# Checkout the main branch to update manifest.json
|
||||
- name: Checkout main branch to update manifest
|
||||
run: |
|
||||
git checkout main
|
||||
git pull origin main
|
||||
|
||||
- name: Update manifest.json in main branch
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
jq --arg version "$VERSION" '.version = $version' manifest.json > manifest-temp.json
|
||||
mv manifest-temp.json manifest.json
|
||||
|
||||
- name: Commit and push manifest.json changes in main
|
||||
run: |
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git add manifest.json
|
||||
git commit -m "Update manifest.json to version $VERSION" || echo "No changes to commit"
|
||||
git push origin main
|
||||
|
||||
|
||||
- name: Ensure HEAD is at updated main commit
|
||||
run: |
|
||||
git fetch origin
|
||||
git checkout main
|
||||
git reset --hard origin/main
|
||||
|
||||
- name: npm build
|
||||
run: |
|
||||
npm install
|
||||
npm run fn-build --if-present
|
||||
|
||||
- name: Generate artifact attestation for main.js
|
||||
uses: actions/attest@v4
|
||||
with:
|
||||
subject-path: 'main.js'
|
||||
|
||||
- name: Generate artifact attestation for styles.css
|
||||
uses: actions/attest@v4
|
||||
with:
|
||||
subject-path: 'styles.css'
|
||||
|
||||
- name: Create Plugin release
|
||||
uses: ncipollo/release-action@v1.12.0
|
||||
with:
|
||||
artifacts: "main.js,manifest.json,styles.css"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref_name }}
|
||||
21
.github/workflows/release.yml
vendored
|
|
@ -1,21 +0,0 @@
|
|||
# https://github.com/SilentVoid13/Templater/blob/master/.github/workflows/release.yml
|
||||
name: Plugin release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: npm build
|
||||
run: |
|
||||
npm install
|
||||
npm run build --if-present
|
||||
- name: Plugin release
|
||||
uses: ncipollo/release-action@v1.12.0
|
||||
with:
|
||||
artifacts: "main.js,manifest.json,styles.css"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
3
.gitignore
vendored
|
|
@ -17,6 +17,9 @@ main.js
|
|||
|
||||
# obsidian
|
||||
data.json
|
||||
.obsidian
|
||||
|
||||
# Exclude macOS Finder (System Explorer) View States
|
||||
.DS_Store
|
||||
|
||||
venv
|
||||
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "src/obsidian-folder-overview"]
|
||||
path = src/obsidian-folder-overview
|
||||
url = https://github.com/LostPaul/obsidian-folder-overview
|
||||
54
README.md
|
|
@ -1,51 +1,19 @@
|
|||
# Obsidian folder notes
|
||||
Create notes within folders that can be accessed without collapsing the folder, similar to the functionality offered in Notion.
|
||||
|
||||
## Table of contents
|
||||
Folder notes is a plugin for the note taking app [Obsidian](https://obsidian.md/) that lets you attach notes to folders so that you can click on the name of a folder to open the note like in the app [Notion](https://www.notion.so/).
|
||||
This plugin has some unique features that separate it from similar "Folder note" plugins like opening folder notes through the path, creating folder notes for every existing folder, templater/template support and more.
|
||||
|
||||
- [Features](#features)
|
||||
- [Credits](#credits)
|
||||
- [Showcase](#showcase)
|
||||
- [How to install the plugin](#how-to-install-the-plugin)
|
||||
Support the development of the plugin
|
||||
|
||||
## Features
|
||||
<a href='https://ko-fi.com/D1D1GHGSI' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi2.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
|
||||
## Documentation & download link
|
||||
The plugin can be downloaded by clicking on https://obsidian.md/plugins?id=folder-notes and then on install. If you need help with the plugin or want to know what the features are that the plugin has then you can find the documentation at https://lostpaul.github.io/obsidian-folder-notes/.
|
||||
|
||||
- Notes in folders
|
||||
- Click on folder name to open folder note
|
||||
- Folders don't collapse (can be changed in the settings)
|
||||
- Templater and templates support
|
||||
- The name of folders that contain folder notes are underlined (can get toggled in the settings)
|
||||
- Automatically create folder notes in folder on folder creation (can be turned on in the settings)
|
||||
- Exclude folders
|
||||
- Hide folder notes from file explorer
|
||||
#### Soon
|
||||
- Create folder note for every existing folder
|
||||
## How to install the beta version
|
||||
|
||||
## Showcase
|
||||
The easiest option is to install the [BRAT plugin](https://obsidian.md/plugins?id=obsidian42-brat) and then to follow the following guide: https://tfthacker.com/brat-quick-guide & use this link https://github.com/LostPaul/obsidian-folder-notes to install the beta version.
|
||||
|
||||
#### Open folder notes
|
||||

|
||||
Join the Discord server to chat about the beta and to also get the beta user role.
|
||||
|
||||
#### Automatically create folder notes
|
||||

|
||||
|
||||
#### Exlude folders
|
||||

|
||||
|
||||
|
||||
## How to install the plugin
|
||||
|
||||
1. Download main.js, manifest.json, styles.css from the latest release (see [releases](https://github.com/LostPaul/obsidian-folder-notes/releases/))
|
||||
2. Create a new folder in `Vault-name/.obsidian/plugins` and put the downloaded files in there
|
||||
3. Reload your plugins and enable the plugin
|
||||
<img width="585" alt="Obsidian_QaY1e3nCzL" src="https://user-images.githubusercontent.com/70213368/227159411-5a2b0a99-f612-42f2-bc57-4f0a7f97d65e.png">
|
||||
<img width="582" alt="Obsidian_w4hTprcOjW" src="https://user-images.githubusercontent.com/70213368/227159064-e116ea0c-c249-4750-9b0f-b010e4a6afc5.png">
|
||||
|
||||
|
||||
## Credits
|
||||
Thank you to everyone who has created a plugin that inspired me and I took code from.
|
||||
- Template suggester, created by [SilentVoid13](https://github.com/SilentVoid13) and [Liamcain](https://github.com/liamcain) (their plugins: [Templater](https://github.com/SilentVoid13/Templater/), [Periodic notes](https://github.com/liamcain/obsidian-periodic-notes))
|
||||
- Apply template to note, first seen in https://github.com/mgmeyers/obsidian-kanban/ from [Mgmeyers](https://github.com/mgmeyers)
|
||||
- Folder underline, inspired from https://github.com/aidenlx/alx-folder-note
|
||||
- Stop folders from collapsing, the basic idea from https://github.com/alangrainger/obsidian-folder-notes and some code lines
|
||||
- The basic idea is from https://github.com/xpgo/obsidian-folder-note-plugin
|
||||
## Discord server
|
||||
[For regular updates on Folder Notes and my other plugins, join the Discord server to get notified and participate in discussions.](https://discord.gg/4UQEDfQmuH)
|
||||
|
|
|
|||
18
docs/docs/Features/Command palette commands.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Command palette commands
|
||||
|
||||
## Use this file as the folder note for its parent folder
|
||||
The note that's currently in focus will be the new folder note for the parent folder of the file.
|
||||
## Make a folder with this file as its folder note
|
||||
This command creates a new folder and uses the current file that is in focus as the folder note. The new folder will be in the same folder as the folder the file was in.
|
||||
## Create {type} folder note for this folder
|
||||
Create a new folder note for the folder of the current file that is in focus. The plugin will not use the file in focus but instead a completely new file.
|
||||
## Delete this folder's linked note
|
||||
Delete the folder note (only the file itself) of the parent folder of the currently focused file.
|
||||
## Open this folder's linked note
|
||||
Open the folder note of the parent folder of the currently focused file.
|
||||
## Insert folder overview
|
||||
Insert a folder overview in a note. [Click here to know more about a folder overview](./Folder%20overview.md).
|
||||
## Edit folder overview
|
||||
Edit the default settings or an already created folder overview in the right view. [Click here to know more about a folder overview](./Folder%20overview.md).
|
||||
## Create folder note from selection
|
||||
Create a new file, a new folder and the file as the folder note. The text that has been selected will be the name of the folder note and the folder.
|
||||
38
docs/docs/Features/Exclude folders.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Exclude folders
|
||||
The exclude folder feature can be found in the plugin settings under the tab "Exclude folders". Depending on what you exclude a folder from you for example can't open a folder note, the file/folder names don't sync, it doesn't show up in the folder overview and more.
|
||||
## Exclude only one folder
|
||||
To only exclude the folder itself and not the sub folders also you have to click on the edit icon and disable include subfolders in the settings.
|
||||
|
||||
## How to exclude a folder?
|
||||
### Directly in the settings
|
||||
Click on the plus icon, then click on the text input, search a folder name and then select the folder you want to exclude from the list.
|
||||

|
||||
### Through the file explorer
|
||||
Click on a folder and then use the context menu and click on "exclude folder from folder notes". The plugin will then apply the [default exclude folder settings](#Default settings) and add it to the excluded folders list.
|
||||

|
||||
## Exclude a folder and their subfolders
|
||||
To exclude a folder and its subfolders you have to do the same steps as in how to exclude only one folder but enable include subfolders in the settings of an excluded folder.
|
||||
## Exclude multiple folders (with a pattern)
|
||||
The pattern only looks at the names of the folders and nothing else. You have two options to exclude folders with a pattern and the first option is to use * before and after the string. The second option is to use a [regex](https://en.wikipedia.org/wiki/Regular_expression).
|
||||
|
||||
To use a regex you have to add "{regex}" at the beginning of the string and everything after it is going to be a regex that matches the folder names.
|
||||
|
||||
For example this regex matches every folder in the file explorer.
|
||||

|
||||
|
||||
When we use * to match a folder it only works when it's at the start or at the end of a string or both.
|
||||
For example when we use the folder name "Test" and the following patterns we get this:
|
||||
|
||||
\*est => matches the name
|
||||
Test* => matches the name
|
||||
\*es\* => matches the name
|
||||
\*Test\* => matches the name
|
||||
\*Test 2\* => doesn't match the name
|
||||
## How to change the settings
|
||||
Open the exclude folders settings tab and click on a edit button if you've already excluded folders or added patterns to exclude folders.
|
||||

|
||||
## Default settings
|
||||
To edit the default settings for new excluded folders/patterns you just have to click on one of the "Manage" buttons in the settings that are next to "Exclude folder/pattern default settings".
|
||||
|
||||
## Whitelisted folders
|
||||
The whitelisted folders overwrite the already excluded folders with options that are the opposite from the excluded folders. For example when you add a pattern to exclude all folders, select to disable opening of folders and then add one specific folder to the whitelist and select "open folder ..." you can only open the specific folder and not all the other folder notes.
|
||||
10
docs/docs/Features/File context menu commands.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# File context menu commands
|
||||
|
||||
## Create folder note
|
||||
This command creates a new folder and uses the file you clicked on as the folder note. The new folder will be in the same folder as the folder the file was in.
|
||||
|
||||

|
||||
## Turn into folder note for "{folderName}"
|
||||
This command will attach the file to the folder it's in. For example file "test" that is in the folder "test2" will be the new folder note for the folder "test2".
|
||||
|
||||

|
||||
14
docs/docs/Features/Folder context menu commands.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Folder context menu commands
|
||||
|
||||
|
||||
## Exclude folder from folder notes
|
||||
This will add the folder to the excluded folder list. [Click here to find more about excluded folders out.](Exclude%20folders.md)
|
||||
## Create {type} folder note
|
||||
This command will create a folder note of the file type you selected and depending on your settings the folder note will be in the folder itself or in the parent folder of the folder.
|
||||
## Delete folder note
|
||||
The folder note from the folder you clicked on gets deleted. (Only the file and note the folder itself)
|
||||
## Open folder note
|
||||
Opens folder notes in the currently active tab or if you have the setting "open folder note in a new tab by default" enabled the note opens in a new tab.
|
||||
|
||||
## Copy Obsidian URL
|
||||
Copies the Obsidian URL of the file which is linked to the folder and it can be used in other programs to directly open the file.
|
||||
32
docs/docs/Features/Settings/File explorer.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# File explorer settings
|
||||
### Hide folder note
|
||||
Hide files in the file explorer which are linked to folders
|
||||
### Disable click-to-open folder note on mobile
|
||||
Disable the option to open folder notes by tapping on the folder name of a folder with a folder note in the file explorer. With that option enabled it's only possible to open folder notes with the command palette oder the context menu.
|
||||
|
||||
### Open folder notes by only clicking directly on the folder name
|
||||
With this option enabled you can only open a folder note by clicking when doing so exactly on the folder name and not on the whitespace around it. When you also have "Disable folder collapsing" enabled folders also won't collapse when you click on a folder name.
|
||||
|
||||
### Disable folder collapsing
|
||||
Stop folders with folder notes from collapsing when click directly on the folder name or on the whitespace around it. Except when you click on the collapse icon you can still collapse folders.
|
||||
|
||||
### Use submenus
|
||||
When this option is enabled and you use the context menu for folder/files at first you can only see "Folder Note Commands" in the menu and you have to hover over it to see all options.
|
||||
|
||||
## Style settings
|
||||
|
||||
### Highlight folder in the file explorer
|
||||
Normally only the file gets highlighted in the explorer but when you enable this option the folder of a folder note gets also highlighted.
|
||||
|
||||
### Hide collapse icon
|
||||
Hide the collapse icon next to a folder name for a folder with a folder note or also for every folder with no files.
|
||||
|
||||
### Underline the name of folder note
|
||||
Underline the names of folders with folder notes in the file explorer.
|
||||
|
||||
### Bold the name of folder notes
|
||||
Bold folders in the file explorer that have a folder note.
|
||||
|
||||
### Cursive the name of folder notes
|
||||
Make folder with folder notes in the file explorer cursive.
|
||||
|
||||
29
docs/docs/Features/Settings/General.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# General settings
|
||||
### Folder note name template
|
||||
The entered text is going to be the name of all folder notes and when file name matches this text it becomes a folder note. {{folder_name}} is a placeholder for the name of the folder linked to the file. If you change the text every existing folder note which matched the old text won't be a folder note anymore and you have to use the button "Rename existing folder notes" to rename all folder notes.
|
||||
|
||||
### Supported file types
|
||||
Only the file types you select will be selected will be recognized as folder notes. This means you that a file that matches the name of a folder and normally would be a folder note but has an extension that isn't included in the supported file types list won't be a folder note and you can't open it by click on the folder name.
|
||||
|
||||
### Template path
|
||||
When you don't have templater installed & enabled you can choose any file in your vault as a template. But if you have templater enabled you first have to choose the folder where your templates are located, in the settings of the templater plugin. You can also use the core templates plugin for applying templates to new folder notes.
|
||||
|
||||
### Storage location
|
||||
Choose if the folder notes should be stored in the folder they're linked to or in the parent folder of the folder they're linked to. If you switch the storage method all existing folder won't be recognized as folder notes anymore until you click on the switch button and the location of old folder note files gets changed.
|
||||
|
||||
## Folder note behavior
|
||||
### Confirm folder note deletion
|
||||
When enabled a modal pops up when you try to delete folder note using the context menu.
|
||||
### Open folder note in a new tab by default
|
||||
When enabled every folder note opens in a new tab unless it is already open in the currently focused file in the editor and if the subsetting "focus existing tab instead of creating a new one" is also enabled every folder note only has one tab open. If you have different tab open than of a folder note you want to open and the folder note already has a tab open it'll switch to it.
|
||||
|
||||
## Integration & Compatibility
|
||||
### Enable [front matter title plugin](https://github.com/snezhig/obsidian-front-matter-title) integration
|
||||
Applies the changes made to the files names in the frontmatter also to the folder names in the file explorer & the path for folders with folder notes. This is also requires to enable the auto update setting in the file explorer & path settings of the folder notes plugin.
|
||||
|
||||
## Session & Persistence
|
||||
|
||||
### Persist tab after restart
|
||||
Open the same tab you had open in the plugin settings before restarting Obsidian.
|
||||
### Persist tab during session only
|
||||
Open the same tab after closing the plugin settings and opening it again but if "Persist tab after restart" is disabled it won't open the same tab again after restarting Obsidian.
|
||||
22
docs/docs/Features/Settings/Path.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Path settings
|
||||
The "path" is the file path above a note in the editor.
|
||||
|
||||

|
||||
### Open folder note through path
|
||||
Open a folder note by click on a folder name in the path when it has a folder note linked to it.
|
||||
|
||||
### Open sidebar when opening a folder note through path
|
||||
Disable/enable if the left sidebar should also open when you open a folder note through the path.
|
||||
|
||||
### Auto update folder name in the path ([front matter title plugin only](https://github.com/snezhig/obsidian-front-matter-title))
|
||||
Automatically update the folder name in the path when the front matter title plugin is enabled and the title for a folder note is changed in the front matter. This will not change the file name, only the displayed name in the path.
|
||||
|
||||
## Style settings
|
||||
|
||||
### Underline folder in the path
|
||||
Underline every folder with a folder note in the path.
|
||||
|
||||
### Bold folders in the path
|
||||
Bold every folder with a folder note in the path.
|
||||
### Cursive folders in the path
|
||||
Make every folder with a folder note in the path cursive.
|
||||
80
docs/docs/Folder overview.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Folder overview
|
||||
[The folder overview feature can be installed as a standalone plugin and works without the folder notes plugin by clicking on this.](https://obsidian.md/plugins?id=folder-overview)
|
||||
**Don't install "folder notes" & the "folder overview" plugin instead only install the folder notes plugin otherwise you can't enable both and there can be other bugs.**
|
||||
|
||||
This feature/plugin creates a [code block](https://help.obsidian.md/Editing+and+formatting/Basic+formatting+syntax#Code+blocks) that provides an overview of your entire vault or specific folders. Instructions for creating or editing a folder overview, along with additional guidance, can be found further down the page.
|
||||
Currently there are two styles available: **file explorer** & **list style**, with more coming soon. The **overview updates** itself **automatically** when you create, delete or modify a file. You can also integrate it into **templates**, as explained further down below this page.
|
||||
|
||||
Additionally, you can **customize** the overview by selecting **specific file types**, adjusting **file depth**, and changing the **file order** (e.g., alphabetical: A-Z).
|
||||
## Create overview
|
||||
There are two options to create a folder overview in a note
|
||||
|
||||
- Use the command "Insert folder overview" from the [command palette](https://help.obsidian.md/Plugins/Command+palette)
|
||||
- On desktop right click and click on the option "Insert folder overview"
|
||||
## Edit overview
|
||||
If the overview shows no files or folders yet you can just click on the button that says "Edit overview". But if there are already files/folders you can use the command "Edit folder overview" from the [command palette](https://help.obsidian.md/Plugins/Command+palette) or click on the button as showed in the video below.
|
||||
|
||||

|
||||
|
||||
## Default settings
|
||||
To edit the default values for new folder overviews you can either use the command "Edit folder overview" or open the plugins settings tab and then the folder overview tab to the edit the default settings. This won't change the settings of already existing folder overviews.
|
||||
|
||||
|
||||

|
||||
(On the left is the plugin settings tab and the right the view you see when you use the "Edit folder overview" command)
|
||||
|
||||
## FAQ
|
||||
### How can I use this in a template?
|
||||
|
||||
Just create the folder overview as normal in a template and then change the settings to be what you want to use as a default for the files you apply the templates to. It also doesn't matter that the id of the overview in the template will be the same as the id of the overview that gets created when the template has been used.
|
||||
|
||||
### What's the id in the code block for?
|
||||
They're there to differentiate the overviews from each other so that you can use "Edit folder overview" command to edit the overviews from one file (there can be multiple overviews in one file). It's especially useful on mobile because the button to edit the code block on mobile is quite small.
|
||||
|
||||
### How can I make the links appear in the graph view?
|
||||
[Edit the folder overview](#edit-overview) and then enable "Use actual links" to let the links appear in the graph view. The file will be then edited under the code block and there will be another list added which is hidden by default. You can choose to either hide the code block or the list under it in the folder overview settings. The list of links under the code block will also be visible in other apps that support markdown.
|
||||
|
||||

|
||||

|
||||
|
||||
## Settings
|
||||
|
||||
### Auto sync
|
||||
When this is enabled and you rename/create/delete a file/folder which is a children of the selected folder the overview will automatically update.
|
||||
### Title
|
||||
The title is above the overview when you enable "Show the title". You can use variables like this: {{variableName}} which will be replaced with something and the list of variables you can use is below this text.
|
||||
|
||||
| Variable | Explanation | Example |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
||||
| folderName | The name of the folder depending on what folder you choose to display | Folder1 |
|
||||
| folderPath | The path of the choosen folder | Folder1/Folder2 |
|
||||
| fileName | The name of the file where the overview is located in | File1 |
|
||||
| filePath | The path of the file where the overview is located in | Folder1/File1 |
|
||||
| fmtpFileName | The changed file name using the [front matter title plugin](https://github.com/snezhig/obsidian-front-matter-title) | Real file name: 1234, changed name: File1 |
|
||||
| properties.<name\> | Choose any property from a file | properties.name => File1<br>properties.date => e.g. 01.01.2001 |
|
||||
|
||||
### Folder path
|
||||
The overview will show the children of the selected folder.
|
||||
|
||||
When you leave the path empty it will use the parent folder of where the overview is located in. The same is true for "File's parent folder path". If you're using the folder notes plugin and have the storage location set to parent folder you'll need to use "Path of folder linked to the file" to show the children of the folder which is linked to the file. Otherwise if you would for example have the overview located in "File1" which is linked to "Folder1" it would choose all the files/folders of the vault.
|
||||
|
||||
### Use actual links
|
||||
This allows the links of the overview to show up in the grap view, see [How can I make the links appear in the graph view?](#how-can-i-make-the-links-appear-in-the-graph-view?).
|
||||
|
||||
#### Hide link list/folder overview
|
||||
Either hide the code block or the list under it. When you hide the link list every list item will have a span item added to it. It looks like on the image at [How can I make the links appear in the graph view?](#how-can-i-make-the-links-appear-in-the-graph-view?).
|
||||
|
||||
### Include types
|
||||
Only the file types which are selected here will show up in the overview.
|
||||
|
||||
### Show folder notes
|
||||
Show the file itself which is linked to one folder.
|
||||
|
||||
### File depth
|
||||
Limit the depth of files/folders which are shown in the overview. When it is at depth one only the first level files/folders will be shown. When you can't see the childrens of folder the name won't be shown except when you enable ["Show folder names of folders that appear empty in the folder overview"](#show-folder-names-of-folders-that-appear-empty-in-the-folder-overview) or the folder has a file linked to it.
|
||||
|
||||
### Sort files by
|
||||
Choose the order of files and folders.
|
||||
|
||||
### Show folder names of folders that appear empty in the folder overview
|
||||
When folders don't have any childrens they don't show up until you enable this setting but they also don't show up until you set the "File depth" setting until the level of the children of a folder. For example you can see "Folder1" at level 2 and their children at level 3 and the folder name gets shown at level 3 or when you enable this setting.
|
||||
46
docs/docs/Getting started.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Getting started
|
||||
|
||||
## How to install the plugin
|
||||
The plugin can be downloaded by clicking on [https://obsidian.md/plugins?id=folder-notes](https://obsidian.md/plugins?id=folder-notes) and then on install.
|
||||
## Basic features
|
||||
|
||||
### Create folder note
|
||||
|
||||
There are three options: the context menu, ctrl/strg + click and commands
|
||||
|
||||
Right click or on mobile hold longer on a folder name to get to the context menu and then click on create folder note.
|
||||
|
||||

|
||||
|
||||
On PC either use ctrl or strg (depending on your settings) and then click on a folder name to create a folder note.
|
||||
|
||||
Open the [command palette](https://help.obsidian.md/Plugins/Command+palette) and then type "folder notes" and select the command you need.
|
||||
|
||||

|
||||
|
||||
### Open folder note
|
||||
|
||||
If you haven't changed any settings the simplest option is just to click on the name of a folder to open its folder note.
|
||||
|
||||
Use the context menu and instead of create click of open folder note. It only works with folders that also have a folder note and [if you don't know how to open the context menu click here.](#Create folder note)
|
||||
|
||||

|
||||
|
||||
The second option is to open one file of the files from the folder from which you want to open the folder note and then use the command "Open folder note of current folder of active note" from the [command palette](https://help.obsidian.md/Plugins/Command+palette)
|
||||
|
||||
The last option and the option that is only available to desktop users is to use alt or ctrl and click on the folder name. When you don't hold ctrl/alt and click on a folder name the folder gets collapsed as normal. To use this option you first have to enable it in the plugin settings under the tab general.
|
||||
|
||||

|
||||
|
||||
### Create folder overview
|
||||
Find more out about the folder overview feature on the [folder overview page](./Folder%20overview.md)
|
||||
|
||||
To create a folder overview in a note either use the command "Insert folder overview" or on pc right click and select "create folder overview"
|
||||
|
||||
{: style="max-width: 40%;max-height: 40%"}
|
||||
|
||||
The folder overview with the default settings looks like this with one note
|
||||
|
||||

|
||||
To find out how to edit the folder overview have a look at [folder overview page](./Folder%20overview.md)
|
||||
|
||||
7
docs/docs/Troubleshooting.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Troubleshooting
|
||||
## Folder with folder note still collapses
|
||||
Issue: You've "Disable folder collapsing" enabled, click on a folder name that has folder note linked to it and it still collapses.
|
||||
|
||||
Possible cause & solution: You've "auto-reveal current file" enabled and need to disable it in order to make this feature work.
|
||||
|
||||

|
||||
BIN
docs/docs/assets/2023_Obsidian_logo.svg.png
Normal file
|
After Width: | Height: | Size: 271 KiB |
BIN
docs/docs/assets/2KHZfsNVuJL8dIz6cTF5.png
Normal file
|
After Width: | Height: | Size: 380 KiB |
BIN
docs/docs/assets/2wzCXFTpD2.gif
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
docs/docs/assets/FKhiQZLm4Juu4VdFTxPC.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
docs/docs/assets/Ny4aCJtZStlFmlsN9zp2.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
docs/docs/assets/TOtiFIYzUI8rwxjCLhyN.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
docs/docs/assets/Untitled.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
docs/docs/assets/VyBTGhA5eJAVVFusZXIz.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
docs/docs/assets/XjlSBfx6ouPrRpb2JmcL.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
docs/docs/assets/mbZW1SXv8o3fKU4g8j3V.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
docs/docs/assets/n5AGi3VCxF5JcNx2Wm5O.mp4
Normal file
BIN
docs/docs/assets/screenshots/Obsidian_3VEQ3kOT4S.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
docs/docs/assets/screenshots/Obsidian_7B8zdp5xDh.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
docs/docs/assets/screenshots/Obsidian_K3ph5zw0mq.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
docs/docs/assets/screenshots/Obsidian_LaoyRX8jr2.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/docs/assets/screenshots/Obsidian_nAqAIrlZFW.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
docs/docs/assets/screenshots/P7yvNZmF5e.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
docs/docs/assets/screenshots/b4QOtkzJs0.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
docs/docs/assets/u6ccTTzVbwzBivySFacZ.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
docs/docs/assets/videos/Obsidian_Nc9OYAHHT1.mp4
Normal file
BIN
docs/docs/assets/videos/Obsidian_bUlTDSG9a9.mp4
Normal file
BIN
docs/docs/assets/xUo6aJuIiABXaPl7eosX.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
docs/docs/assets/xdS3VV7wjIRFNHADE0lx.mp4
Normal file
34
docs/docs/index.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Overview
|
||||
Folder Notes is a plugin for the note-taking app [Obsidian](https://obsidian.md/) that allows you to attach notes directly to folders. This lets you open a note by clicking on a folder name—similar to the functionality found in [Notion](https://www.notion.so/).
|
||||
|
||||
Unlike other “folder note” plugins, Folder Notes offers several unique features, including support for custom file types, templating, and advanced folder handling options.
|
||||
|
||||
## Get started
|
||||
|
||||
Read the [Getting started page](Getting%20started.md) for further details and the pages under the "Features" category explain every feature in detail.
|
||||
## Features
|
||||
- Open folder notes by clicking folder names in the file explorer or path
|
||||
- Optional modifier key (Alt or Ctrl + click) to open folder notes
|
||||
- Full support for any file type as folder notes (e.g., Excalidraw, Canvas, etc.)
|
||||
- Automatically sync folder note names with their folder names
|
||||
- Auto-create a folder note when a new folder is created
|
||||
- Convert all existing folders into folder notes
|
||||
- Use a folder-overview code block with configurable default settings
|
||||
- Option to hide folder notes from the file explorer
|
||||
- Style folder names (underline, bold, italic) in the file explorer or path
|
||||
- Prevent automatic collapsing of folders that have folder notes
|
||||
- Choose to only open the folder note when clicking the folder name; other clicks collapse the folder
|
||||
- Front matter title plugin support
|
||||
- Exclude specific folders using regex, path, or wildcard patterns (`*`)
|
||||
|
||||
## Credits
|
||||
Thank you to everyone who has created a plugin that inspired me and I took code from.
|
||||
- Template suggester, created by [SilentVoid13](https://github.com/SilentVoid13) and [Liamcain](https://github.com/liamcain) (their plugins: [Templater](https://github.com/SilentVoid13/Templater/), [Periodic notes](https://github.com/liamcain/obsidian-periodic-notes))
|
||||
- Apply template to note, first seen in [https://github.com/mgmeyers/obsidian-kanban/](https://github.com/mgmeyers/obsidian-kanban/) from [Mgmeyers](https://github.com/mgmeyers)
|
||||
- Folder underline, inspired from [https://github.com/aidenlx/alx-folder-note](https://github.com/aidenlx/alx-folder-note)
|
||||
- Stop folders from collapsing, the basic idea from [https://github.com/alangrainger/obsidian-folder-notes](https://github.com/alangrainger/obsidian-folder-notes)
|
||||
- The basic idea is from [https://github.com/xpgo/obsidian-folder-note-plugin](https://github.com/xpgo/obsidian-folder-note-plugin)
|
||||
|
||||
## Support the development of the plugin
|
||||
|
||||
<a href='https://ko-fi.com/D1D1GHGSI' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi2.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
|
||||
25
docs/mkdocs.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
site_name: Obsidian folder notes
|
||||
theme:
|
||||
name: material
|
||||
logo: assets/2023_Obsidian_logo.svg.png
|
||||
features:
|
||||
- navigation.expand
|
||||
palette:
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
toggle:
|
||||
icon: material/brightness-7
|
||||
name: Switch to dark mode
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: slate
|
||||
toggle:
|
||||
icon: material/brightness-4
|
||||
name: Switch to light mode
|
||||
|
||||
markdown_extensions:
|
||||
- attr_list
|
||||
- md_in_html
|
||||
- def_list
|
||||
|
||||
plugins:
|
||||
- mkdocs-video
|
||||
|
|
@ -3,7 +3,7 @@ import process from "process";
|
|||
import builtins from 'builtin-modules'
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
|
|
@ -39,4 +39,5 @@ esbuild.build({
|
|||
sourcemap: prod ? false : 'inline',
|
||||
treeShaking: true,
|
||||
outfile: 'main.js',
|
||||
conditions: ['types'],
|
||||
}).catch(() => process.exit(1));
|
||||
|
|
|
|||
177
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default [
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
sourceType: "module"
|
||||
}
|
||||
},
|
||||
ignores: [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
],
|
||||
plugins: {
|
||||
"@typescript-eslint": tseslint.plugin,
|
||||
obsidianmd
|
||||
},
|
||||
rules: {
|
||||
"obsidianmd/no-tfile-tfolder-cast": "error",
|
||||
"obsidianmd/rule-custom-message": [
|
||||
"error",
|
||||
{
|
||||
"no-console": {
|
||||
messages: {
|
||||
"Unexpected console statement. Only these console methods are allowed: warn, error, debug.": "Avoid unnecessary logging to console. See https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines#Avoid+unnecessary+logging+to+console",
|
||||
},
|
||||
options: [{ allow: ["warn", "error", "debug"] }],
|
||||
}
|
||||
}
|
||||
],
|
||||
"no-unused-vars": "off",
|
||||
"quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"no-mixed-spaces-and-tabs": "error",
|
||||
"indent": [
|
||||
"error",
|
||||
"tab",
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"arrow-parens": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"eol-last": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"func-call-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"always-multiline"
|
||||
],
|
||||
"no-multi-spaces": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-whitespace-before-property": "off",
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"semi-style": [
|
||||
"error",
|
||||
"last"
|
||||
],
|
||||
"space-in-parens": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"block-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"eqeqeq": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"null": "ignore"
|
||||
}
|
||||
],
|
||||
"spaced-comment": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"markers": [
|
||||
"!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"yoda": "error",
|
||||
"prefer-destructuring": [
|
||||
"error",
|
||||
{
|
||||
"object": true,
|
||||
"array": false
|
||||
}
|
||||
],
|
||||
"operator-assignment": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"no-useless-computed-key": "error",
|
||||
"no-unneeded-ternary": [
|
||||
"error",
|
||||
{
|
||||
"defaultAssignment": false
|
||||
}
|
||||
],
|
||||
"no-invalid-regexp": "error",
|
||||
"no-constant-condition": [
|
||||
"error",
|
||||
{
|
||||
"checkLoops": false
|
||||
}
|
||||
],
|
||||
"no-duplicate-imports": "error",
|
||||
"no-extra-semi": "error",
|
||||
"dot-notation": "error",
|
||||
"no-useless-escape": "error",
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
'@typescript-eslint/consistent-type-definitions': [
|
||||
'error',
|
||||
'interface'],
|
||||
'@typescript-eslint/explicit-function-return-type': 'warn',
|
||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||
'array-bracket-spacing': [
|
||||
'error', 'never'],
|
||||
'linebreak-style': [
|
||||
'error',
|
||||
'unix'
|
||||
],
|
||||
'no-nested-ternary': 'error',
|
||||
'no-shadow': 'error',
|
||||
'no-return-await': 'error',
|
||||
'no-else-return': 'error',
|
||||
'no-empty-function': 'warn',
|
||||
'complexity': [
|
||||
'warn',
|
||||
15
|
||||
],
|
||||
'max-len': [
|
||||
'warn', {
|
||||
code: 110,
|
||||
ignoreComments: true,
|
||||
}
|
||||
],
|
||||
'no-inline-comments': 'warn',
|
||||
'no-magic-numbers': [
|
||||
'warn', {
|
||||
ignore: [0, 1],
|
||||
enforceConst: true,
|
||||
ignoreTypeIndexes: true,
|
||||
ignoreEnums: true,
|
||||
ignoreNumericLiteralTypes: true,
|
||||
ignoreClassFieldInitialValues: true,
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
];
|
||||
12
manifest-beta.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"id": "folder-notes",
|
||||
"name": "Folder notes beta",
|
||||
"version": "1.8.20-1-beta",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Create notes within folders that can be accessed without collapsing the folder, similar to the functionality offered in Notion.",
|
||||
"author": "Lost Paul",
|
||||
"authorUrl": "https://github.com/LostPaul",
|
||||
"fundingUrl": "https://ko-fi.com/paul305844",
|
||||
"helpUrl": "https://lostpaul.github.io/obsidian-folder-notes/",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
{
|
||||
"id": "folder-notes",
|
||||
"name": "Folder notes",
|
||||
"version": "1.1.2",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Create notes within folders that can be accessed without collapsing the folder, similar to the functionality offered in Notion.",
|
||||
"author": "Lost Paul",
|
||||
"authorUrl": "https://github.com/LostPaul",
|
||||
"isDesktopOnly": false
|
||||
"id": "folder-notes",
|
||||
"name": "Folder notes",
|
||||
"version": "1.8.26",
|
||||
"minAppVersion": "1.4.10",
|
||||
"description": "Create notes within folders that can be accessed without collapsing the folder, similar to the functionality offered in Notion.",
|
||||
"author": "Lost Paul",
|
||||
"authorUrl": "https://github.com/LostPaul",
|
||||
"fundingUrl": "https://ko-fi.com/paul305844",
|
||||
"helpUrl": "https://lostpaul.github.io/obsidian-folder-notes/",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
|
|
|||
12550
package-lock.json
generated
26
package.json
|
|
@ -4,22 +4,34 @@
|
|||
"description": "Adds Folder Notes to the default file tree.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
"fn-dev": "node esbuild.config.mjs",
|
||||
"dev": "npm run fn-dev",
|
||||
"fn-build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"fv-build": "tsc -noEmit -skipLibCheck && node ./src/obsidian-folder-overview/esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||
"folder-overview": "node ./src/obsidian-folder-overview/esbuild.config.mjs",
|
||||
"fv-dev": "npm run folder-overview"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Alan Grainger",
|
||||
"author": "Lost Paul",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^8.57.1",
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.60.0",
|
||||
"@typescript-eslint/parser": "^8.60.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.14.47",
|
||||
"eslint": "^10.4.1",
|
||||
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||
"front-matter-plugin-api-provider": "^0.1.4-alpha",
|
||||
"globals": "^16.3.0",
|
||||
"jiti": "^2.5.1",
|
||||
"obsidian": "latest",
|
||||
"obsidian-typings": "^2.2.0",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.38.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.6"
|
||||
|
|
|
|||
582
src/Commands.ts
Normal file
|
|
@ -0,0 +1,582 @@
|
|||
import {
|
||||
TFolder,
|
||||
Notice,
|
||||
TFile,
|
||||
Platform,
|
||||
type App,
|
||||
type Menu,
|
||||
type TAbstractFile,
|
||||
type Editor,
|
||||
type MarkdownView,
|
||||
type MarkdownFileInfo,
|
||||
} from 'obsidian';
|
||||
import type FolderNotesPlugin from './main';
|
||||
import {
|
||||
getFolderNote,
|
||||
createFolderNote,
|
||||
deleteFolderNote,
|
||||
turnIntoFolderNote,
|
||||
openFolderNote,
|
||||
extractFolderName,
|
||||
detachFolderNote,
|
||||
} from './functions/folderNoteFunctions';
|
||||
import { ExcludedFolder } from './ExcludeFolders/ExcludeFolder';
|
||||
import { getFolderPathFromString, getFileExplorerActiveFolder } from './functions/utils';
|
||||
import {
|
||||
deleteExcludedFolder,
|
||||
getDetachedFolder,
|
||||
getExcludedFolder,
|
||||
} from './ExcludeFolders/functions/folderFunctions';
|
||||
import {
|
||||
hideFolderNoteInFileExplorer,
|
||||
showFolderNoteInFileExplorer,
|
||||
} from './functions/styleFunctions';
|
||||
|
||||
type MarkdownEditorContext = MarkdownView | MarkdownFileInfo;
|
||||
|
||||
|
||||
export class Commands {
|
||||
plugin: FolderNotesPlugin;
|
||||
app: App;
|
||||
constructor(app: App, plugin: FolderNotesPlugin) {
|
||||
this.plugin = plugin;
|
||||
this.app = app;
|
||||
}
|
||||
registerCommands(): void {
|
||||
this.editorCommands();
|
||||
this.fileCommands();
|
||||
this.regularCommands();
|
||||
}
|
||||
|
||||
regularCommands(): void {
|
||||
this.plugin.addCommand({
|
||||
id: 'turn-into-folder-note',
|
||||
name: 'Use this file as the folder note for its parent folder',
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!(file instanceof TFile)) return false;
|
||||
const folder = file.parent;
|
||||
if (!folder || !(folder instanceof TFolder)) return false;
|
||||
// Only show if file is NOT in the root folder
|
||||
if (folder.path === '' || folder.path === '/') return false;
|
||||
const folderNote = getFolderNote(this.plugin, folder.path);
|
||||
if (folderNote instanceof TFile && folderNote === file) return false;
|
||||
if (checking) return true;
|
||||
void turnIntoFolderNote(this.plugin, file, folder, folderNote);
|
||||
},
|
||||
});
|
||||
|
||||
this.plugin.addCommand({
|
||||
id: 'create-folder-note',
|
||||
name: 'Make a folder with this file as its folder note',
|
||||
callback: async () => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!(file instanceof TFile)) return;
|
||||
let newPath = file.parent?.path + '/' + file.basename;
|
||||
if (file.parent?.path === '' || file.parent?.path === '/') {
|
||||
newPath = file.basename;
|
||||
}
|
||||
if (this.plugin.app.vault.getAbstractFileByPath(newPath)) {
|
||||
return new Notice('Folder already exists');
|
||||
}
|
||||
const automaticallyCreateFolderNote =
|
||||
this.plugin.settings.autoCreate;
|
||||
this.plugin.settings.autoCreate = false;
|
||||
void this.plugin.saveSettings();
|
||||
await this.plugin.app.vault.createFolder(newPath);
|
||||
const folder = this.plugin.app.vault.getAbstractFileByPath(newPath);
|
||||
if (!(folder instanceof TFolder)) return;
|
||||
await createFolderNote(this.plugin, folder.path, true, '.' + file.extension, false, file);
|
||||
this.plugin.settings.autoCreate = automaticallyCreateFolderNote;
|
||||
void this.plugin.saveSettings();
|
||||
},
|
||||
});
|
||||
|
||||
this.plugin.addCommand({
|
||||
id: 'create-folder-note-for-current-folder',
|
||||
name: 'Create Markdown folder note for this folder',
|
||||
checkCallback: (checking) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!(file instanceof TFile)) return false;
|
||||
const folder = file.parent;
|
||||
if (!(folder instanceof TFolder)) return false;
|
||||
if (folder.path === '' || folder.path === '/') return false;
|
||||
if (checking) return true;
|
||||
void createFolderNote(this.plugin, folder.path, true, '.md', false);
|
||||
},
|
||||
});
|
||||
|
||||
this.plugin.settings.supportedFileTypes.forEach((fileType) => {
|
||||
if (fileType === 'md') return;
|
||||
this.plugin.addCommand({
|
||||
id: `create-${fileType}-folder-note-for-current-folder`,
|
||||
name: `Create ${fileType} folder note for this folder`,
|
||||
checkCallback: (checking) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!(file instanceof TFile)) return false;
|
||||
const folder = file.parent;
|
||||
if (!(folder instanceof TFolder)) return false;
|
||||
if (folder.path === '' || folder.path === '/') return false;
|
||||
if (checking) return true;
|
||||
void createFolderNote(this.plugin, folder.path, true, '.' + fileType, false);
|
||||
},
|
||||
});
|
||||
});
|
||||
this.plugin.settings.supportedFileTypes.forEach((fileType) => {
|
||||
const type = fileType === 'md' ? 'markdown' : fileType;
|
||||
this.plugin.addCommand({
|
||||
id: `create-${type}-folder-note-for-active-file-explorer-folder`,
|
||||
name: `Create ${type} folder note for current active folder in file explorer`,
|
||||
checkCallback: (checking: boolean) => {
|
||||
const folder = getFileExplorerActiveFolder(this.plugin);
|
||||
if (!folder) return false;
|
||||
// Is there already a folder note for the active folder?
|
||||
const folderNote = getFolderNote(this.plugin, folder.path);
|
||||
if (folderNote instanceof TFile) return false;
|
||||
if (checking) return true;
|
||||
|
||||
// Everything is fine and not checking, let's create the folder note.
|
||||
const ext = '.' + fileType;
|
||||
const { path } = folder;
|
||||
void createFolderNote(this.plugin, path, true, ext, false);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
this.plugin.addCommand({
|
||||
id: 'delete-folder-note-for-current-folder',
|
||||
name: 'Delete this folder\'s linked note',
|
||||
checkCallback: (checking) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!(file instanceof TFile)) return false;
|
||||
const folder = file.parent;
|
||||
if (!(folder instanceof TFolder)) return false;
|
||||
const folderNote = getFolderNote(this.plugin, folder.path);
|
||||
if (!(folderNote instanceof TFile)) return false;
|
||||
if (checking) return true;
|
||||
void deleteFolderNote(this.plugin, folderNote, true);
|
||||
},
|
||||
});
|
||||
|
||||
this.plugin.addCommand({
|
||||
id: 'delete-folder-note-of-active-file-explorer-folder',
|
||||
name: 'Delete folder note of current active folder in file explorer',
|
||||
checkCallback: (checking: boolean) => {
|
||||
const folder = getFileExplorerActiveFolder(this.plugin);
|
||||
if (!folder) return false;
|
||||
// Is there any folder note for the active folder?
|
||||
const folderNote = getFolderNote(this.plugin, folder.path);
|
||||
if (!(folderNote instanceof TFile)) return false;
|
||||
if (checking) return true;
|
||||
|
||||
// Everything is fine and not checking, let's delete the folder note.
|
||||
void deleteFolderNote(this.plugin, folderNote, true);
|
||||
},
|
||||
});
|
||||
this.plugin.addCommand({
|
||||
id: 'open-folder-note-for-current-folder',
|
||||
name: 'Open this folder\'s linked note',
|
||||
checkCallback: (checking) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!(file instanceof TFile)) return false;
|
||||
const folder = file.parent;
|
||||
if (!(folder instanceof TFolder)) return false;
|
||||
const folderNote = getFolderNote(this.plugin, folder.path);
|
||||
if (!(folderNote instanceof TFile)) return false;
|
||||
if (checking) return true;
|
||||
void openFolderNote(this.plugin, folderNote);
|
||||
},
|
||||
});
|
||||
this.plugin.addCommand({
|
||||
id: 'open-folder-note-of-active-file-explorer-folder',
|
||||
name: 'Open folder note of current active folder in file explorer',
|
||||
checkCallback: (checking: boolean) => {
|
||||
const folder = getFileExplorerActiveFolder(this.plugin);
|
||||
if (!folder) return false;
|
||||
// Is there any folder note for the active folder?
|
||||
const folderNote = getFolderNote(this.plugin, folder.path);
|
||||
if (!(folderNote instanceof TFile)) return false;
|
||||
if (checking) return true;
|
||||
|
||||
// Everything is fine and not checking, let's open the folder note.
|
||||
void openFolderNote(this.plugin, folderNote);
|
||||
},
|
||||
});
|
||||
|
||||
this.plugin.addCommand({
|
||||
id: 'create-folder-note-from-selected-text',
|
||||
name: 'Create folder note from selection',
|
||||
editorCheckCallback: (checking: boolean, editor: Editor, view: MarkdownEditorContext) => {
|
||||
const text = editor.getSelection().trim();
|
||||
const { file } = view;
|
||||
if (!(file instanceof TFile)) return false;
|
||||
if (text && text.trim() !== '') {
|
||||
if (checking) { return true; }
|
||||
const blacklist = ['*', '\\', '"', '/', '<', '>', '?', '|', ':'];
|
||||
for (const char of blacklist) {
|
||||
if (text.includes(char)) {
|
||||
// eslint-disable-next-line max-len
|
||||
new Notice('File name cannot contain any of the following characters: * " \\ / < > : | ?');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (text.endsWith('.')) {
|
||||
new Notice('File name cannot end with a dot');
|
||||
return;
|
||||
}
|
||||
let folder: TAbstractFile | null;
|
||||
const folderPath = getFolderPathFromString(file.path);
|
||||
if (folderPath === '') {
|
||||
folder = this.plugin.app.vault.getAbstractFileByPath(text);
|
||||
if (folder instanceof TFolder) {
|
||||
new Notice('Folder note already exists');
|
||||
return false;
|
||||
}
|
||||
void this.plugin.app.vault.createFolder(text);
|
||||
void createFolderNote(this.plugin, text, false);
|
||||
|
||||
} else {
|
||||
const folderFullPath = folderPath + '/' + text;
|
||||
folder = this.plugin.app.vault.getAbstractFileByPath(folderFullPath);
|
||||
if (folder instanceof TFolder) {
|
||||
new Notice('Folder note already exists');
|
||||
return false;
|
||||
}
|
||||
if (this.plugin.settings.storageLocation === 'parentFolder') {
|
||||
if (
|
||||
this.app.vault.getAbstractFileByPath(
|
||||
folderPath + '/' + text + this.plugin.settings.folderNoteType,
|
||||
)
|
||||
) {
|
||||
new Notice('File already exists');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
void this.plugin.app.vault.createFolder(folderPath + '/' + text);
|
||||
void createFolderNote(this.plugin, folderPath + '/' + text, false);
|
||||
}
|
||||
|
||||
const { folderNoteName } = this.plugin.settings;
|
||||
const fileName = folderNoteName.replace('{{folder_name}}', text);
|
||||
if (fileName !== text) {
|
||||
editor.replaceSelection(`[[${fileName}]]`);
|
||||
} else {
|
||||
editor.replaceSelection(`[[${fileName}|${text}]]`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fileCommands(): void {
|
||||
this.plugin.registerEvent(
|
||||
// eslint-disable-next-line complexity
|
||||
this.app.workspace.on('file-menu', (menu: Menu, file: TAbstractFile) => {
|
||||
let folder: TAbstractFile | TFolder | null = file.parent;
|
||||
if (file instanceof TFile) {
|
||||
if (this.plugin.settings.storageLocation === 'insideFolder') {
|
||||
folder = file.parent;
|
||||
} else {
|
||||
const { folderNoteName } = this.plugin.settings;
|
||||
const fileName = extractFolderName(folderNoteName, file.basename);
|
||||
if (fileName) {
|
||||
if (file.parent?.path === '' || file.parent?.path === '/') {
|
||||
folder = this.plugin.app.vault.getAbstractFileByPath(fileName);
|
||||
} else {
|
||||
folder = this.plugin.app.vault.getAbstractFileByPath(
|
||||
file.parent?.path + '/' + fileName,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (folder instanceof TFolder) {
|
||||
const folderNote = getFolderNote(this.plugin, folder.path);
|
||||
const excludedFolder = getExcludedFolder(this.plugin, folder.path, true);
|
||||
if (folderNote?.path === file.path && !excludedFolder?.detached) { return; }
|
||||
} else if (file.parent instanceof TFolder) {
|
||||
folder = file.parent;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
const addFolderNoteActions = (folderMenu: Menu): void => {
|
||||
if (file instanceof TFile) {
|
||||
folderMenu.addItem((item) => {
|
||||
item.setTitle('Create folder note');
|
||||
item.setIcon('edit');
|
||||
item.onClick(async () => {
|
||||
if (!folder) return;
|
||||
let newPath = folder.path + '/' + file.basename;
|
||||
if (folder.path === '' || folder.path === '/') {
|
||||
newPath = file.basename;
|
||||
}
|
||||
if (this.plugin.app.vault.getAbstractFileByPath(newPath)) {
|
||||
return new Notice('Folder already exists');
|
||||
}
|
||||
const automaticallyCreateFolderNote =
|
||||
this.plugin.settings.autoCreate;
|
||||
this.plugin.settings.autoCreate = false;
|
||||
void this.plugin.saveSettings();
|
||||
await this.plugin.app.vault.createFolder(newPath);
|
||||
const newFolder = this.plugin.app.vault
|
||||
.getAbstractFileByPath(newPath);
|
||||
if (!(newFolder instanceof TFolder)) return;
|
||||
await createFolderNote(
|
||||
this.plugin,
|
||||
newFolder.path,
|
||||
true,
|
||||
'.' + file.extension,
|
||||
false,
|
||||
file,
|
||||
);
|
||||
this.plugin.settings.autoCreate = automaticallyCreateFolderNote;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
if (getFolderPathFromString(file.path) === '') return;
|
||||
|
||||
if (!(folder instanceof TFolder)) return;
|
||||
|
||||
if (folder.path === '' || folder.path === '/') return;
|
||||
|
||||
folderMenu.addItem((item) => {
|
||||
item.setTitle(`Turn into folder note for ${folder?.name}`);
|
||||
item.setIcon('edit');
|
||||
item.onClick(() => {
|
||||
if (!folder || !(folder instanceof TFolder)) return;
|
||||
const folderNote = getFolderNote(this.plugin, folder.path);
|
||||
void turnIntoFolderNote(this.plugin, file, folder, folderNote);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!(file instanceof TFolder)) return;
|
||||
|
||||
const excludedFolder = getExcludedFolder(this.plugin, file.path, false);
|
||||
const detachedExcludedFolder = getDetachedFolder(this.plugin, file.path);
|
||||
|
||||
if (excludedFolder && !excludedFolder.hideInSettings) {
|
||||
// I'm not sure if I'm ever going to add this because of the possibility that a folder got more than one excluded
|
||||
// menu.addItem((item) => {
|
||||
// item.setTitle('Manage excluded folder');
|
||||
// item.setIcon('settings-2');
|
||||
// item.onClick(() => {
|
||||
// if (excludedFolder instanceof ExcludedFolder) {
|
||||
// new ExcludedFolderSettings(this.plugin.app, this.plugin, excludedFolder).open();
|
||||
// } else if (excludedFolder instanceof ExcludePattern) {
|
||||
// new PatternSettings(this.plugin.app, this.plugin, excludedFolder).open();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
folderMenu.addItem((item) => {
|
||||
item.setTitle('Remove folder from excluded folders');
|
||||
item.setIcon('trash');
|
||||
item.onClick(() => {
|
||||
this.plugin.settings.excludeFolders =
|
||||
this.plugin.settings.excludeFolders.filter(
|
||||
(excluded) =>
|
||||
(excluded.path !== file.path) || excluded.detached,
|
||||
);
|
||||
void this.plugin.saveSettings(true);
|
||||
new Notice('Successfully removed folder from excluded folders');
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (detachedExcludedFolder) {
|
||||
folderMenu.addItem((item) => {
|
||||
item.setTitle('Remove folder from detached folders');
|
||||
item.setIcon('trash');
|
||||
item.onClick(() => {
|
||||
void deleteExcludedFolder(this.plugin, detachedExcludedFolder);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (detachedExcludedFolder) { return; }
|
||||
|
||||
folderMenu.addItem((item) => {
|
||||
item.setTitle('Exclude folder from folder notes');
|
||||
item.setIcon('x-circle');
|
||||
item.onClick(() => {
|
||||
const newExcludedFolder = new ExcludedFolder(
|
||||
file.path,
|
||||
this.plugin.settings.excludeFolders.length,
|
||||
undefined,
|
||||
this.plugin,
|
||||
);
|
||||
this.plugin.settings.excludeFolders.push(newExcludedFolder);
|
||||
void this.plugin.saveSettings(true);
|
||||
new Notice('Successfully excluded folder from folder notes');
|
||||
});
|
||||
});
|
||||
|
||||
if (!(file instanceof TFolder)) return;
|
||||
|
||||
const folderNote = getFolderNote(this.plugin, file.path);
|
||||
|
||||
if (folderNote instanceof TFile && !detachedExcludedFolder) {
|
||||
folderMenu.addItem((item) => {
|
||||
item.setTitle('Delete folder note');
|
||||
item.setIcon('trash');
|
||||
item.onClick(() => {
|
||||
void deleteFolderNote(this.plugin, folderNote, true);
|
||||
});
|
||||
});
|
||||
|
||||
folderMenu.addItem((item) => {
|
||||
item.setTitle('Open folder note');
|
||||
item.setIcon('chevron-right-square');
|
||||
item.onClick(() => {
|
||||
void openFolderNote(this.plugin, folderNote);
|
||||
});
|
||||
});
|
||||
|
||||
folderMenu.addItem((item) => {
|
||||
item.setTitle('Detach folder note');
|
||||
item.setIcon('unlink');
|
||||
item.onClick(() => {
|
||||
detachFolderNote(this.plugin, folderNote);
|
||||
});
|
||||
});
|
||||
|
||||
folderMenu.addItem((item) => {
|
||||
item.setTitle('Copy Obsidian URL');
|
||||
item.setIcon('link');
|
||||
item.onClick(() => {
|
||||
this.app.copyObsidianUrl(folderNote);
|
||||
});
|
||||
});
|
||||
|
||||
if (this.plugin.settings.hideFolderNote) {
|
||||
if (excludedFolder?.showFolderNote) {
|
||||
folderMenu.addItem((item) => {
|
||||
item.setTitle('Hide folder note in explorer');
|
||||
item.setIcon('eye-off');
|
||||
item.onClick(() => {
|
||||
hideFolderNoteInFileExplorer(file.path, this.plugin);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
folderMenu.addItem((item) => {
|
||||
item.setTitle('Show folder note in explorer');
|
||||
item.setIcon('eye');
|
||||
item.onClick(() => {
|
||||
showFolderNoteInFileExplorer(file.path, this.plugin);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
folderMenu.addItem((item) => {
|
||||
item.setTitle('Create Markdown folder note');
|
||||
item.setIcon('edit');
|
||||
item.onClick(() => {
|
||||
void createFolderNote(this.plugin, file.path, true, '.md');
|
||||
});
|
||||
});
|
||||
|
||||
this.plugin.settings.supportedFileTypes.forEach((fileType) => {
|
||||
if (fileType === 'md') return;
|
||||
folderMenu.addItem((item) => {
|
||||
item.setTitle(`Create ${fileType} folder note`);
|
||||
item.setIcon('edit');
|
||||
item.onClick(() => {
|
||||
void createFolderNote(this.plugin, file.path, true, '.' + fileType);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
Platform.isDesktop &&
|
||||
!Platform.isTablet &&
|
||||
this.plugin.settings.useSubmenus
|
||||
) {
|
||||
menu.addItem(async (item) => {
|
||||
item.setTitle('Folder note commands').setIcon('folder-edit');
|
||||
let subMenu: Menu = item.setSubmenu();
|
||||
addFolderNoteActions(subMenu);
|
||||
});
|
||||
} else {
|
||||
addFolderNoteActions(menu);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
editorCommands(): void {
|
||||
// eslint-disable-next-line max-len
|
||||
this.plugin.registerEvent(this.plugin.app.workspace.on('editor-menu', (menu: Menu, editor: Editor, view: MarkdownEditorContext) => {
|
||||
const text = editor.getSelection().trim();
|
||||
if (!text || text.trim() === '') return;
|
||||
menu.addItem((item) => {
|
||||
item.setTitle('Create folder note')
|
||||
.setIcon('edit')
|
||||
.onClick(() => {
|
||||
const { file } = view;
|
||||
if (!(file instanceof TFile)) return;
|
||||
const blacklist = ['*', '\\', '"', '/', '<', '>', '?', '|', ':'];
|
||||
for (const char of blacklist) {
|
||||
if (text.includes(char)) {
|
||||
// eslint-disable-next-line max-len
|
||||
new Notice('File name cannot contain any of the following characters: * " \\ / < > : | ?');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (text.endsWith('.')) {
|
||||
new Notice('File name cannot end with a dot');
|
||||
return;
|
||||
}
|
||||
|
||||
let folder: TAbstractFile | null;
|
||||
const folderPath = getFolderPathFromString(file.path);
|
||||
const { folderNoteName } = this.plugin.settings;
|
||||
const fileName = folderNoteName.replace('{{folder_name}}', text);
|
||||
if (folderPath === '') {
|
||||
folder = this.plugin.app.vault.getAbstractFileByPath(text);
|
||||
if (folder instanceof TFolder) {
|
||||
return new Notice('Folder note already exists');
|
||||
}
|
||||
void this.plugin.app.vault.createFolder(text);
|
||||
void createFolderNote(this.plugin, text, false);
|
||||
|
||||
} else {
|
||||
folder = this.plugin.app.vault.getAbstractFileByPath(
|
||||
folderPath + '/' + text,
|
||||
);
|
||||
if (folder instanceof TFolder) {
|
||||
return new Notice('Folder note already exists');
|
||||
}
|
||||
if (this.plugin.settings.storageLocation === 'parentFolder') {
|
||||
if (
|
||||
this.app.vault.getAbstractFileByPath(
|
||||
folderPath +
|
||||
'/' +
|
||||
fileName +
|
||||
this.plugin.settings.folderNoteType,
|
||||
)
|
||||
) {
|
||||
return new Notice('File already exists');
|
||||
}
|
||||
}
|
||||
void this.plugin.app.vault.createFolder(folderPath + '/' + text);
|
||||
void createFolderNote(this.plugin, folderPath + '/' + text, false);
|
||||
}
|
||||
if (fileName !== text) {
|
||||
editor.replaceSelection(`[[${fileName}]]`);
|
||||
} else {
|
||||
editor.replaceSelection(`[[${fileName}|${text}]]`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
34
src/ExcludeFolders/ExcludeFolder.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type FolderNotesPlugin from '../main';
|
||||
export class ExcludedFolder {
|
||||
type: string;
|
||||
id: string;
|
||||
path: string;
|
||||
string: string;
|
||||
subFolders: boolean;
|
||||
disableSync: boolean;
|
||||
disableAutoCreate: boolean;
|
||||
disableFolderNote: boolean;
|
||||
enableCollapsing: boolean;
|
||||
position: number;
|
||||
excludeFromFolderOverview: boolean;
|
||||
hideInSettings: boolean;
|
||||
detached: boolean = false;
|
||||
detachedFilePath?: string;
|
||||
showFolderNote: boolean;
|
||||
constructor(path: string, position: number, id: string | undefined, plugin: FolderNotesPlugin) {
|
||||
this.type = 'folder';
|
||||
this.id = id || crypto.randomUUID();
|
||||
this.path = path;
|
||||
this.subFolders = plugin.settings.excludeFolderDefaultSettings.subFolders;
|
||||
this.disableSync = plugin.settings.excludeFolderDefaultSettings.disableSync;
|
||||
this.disableAutoCreate = plugin.settings.excludeFolderDefaultSettings.disableAutoCreate;
|
||||
this.disableFolderNote = plugin.settings.excludeFolderDefaultSettings.disableFolderNote;
|
||||
this.enableCollapsing = plugin.settings.excludeFolderDefaultSettings.enableCollapsing;
|
||||
this.position = position;
|
||||
// eslint-disable-next-line max-len
|
||||
this.excludeFromFolderOverview = plugin.settings.excludeFolderDefaultSettings.excludeFromFolderOverview;
|
||||
this.string = '';
|
||||
this.hideInSettings = false;
|
||||
this.showFolderNote = plugin.settings.excludeFolderDefaultSettings.showFolderNote;
|
||||
}
|
||||
}
|
||||
39
src/ExcludeFolders/ExcludePattern.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import type FolderNotesPlugin from '../main';
|
||||
export class ExcludePattern {
|
||||
type: string;
|
||||
id: string;
|
||||
string: string;
|
||||
path: string;
|
||||
position: number;
|
||||
subFolders: boolean;
|
||||
disableSync: boolean;
|
||||
disableAutoCreate: boolean;
|
||||
disableFolderNote: boolean;
|
||||
enableCollapsing: boolean;
|
||||
excludeFromFolderOverview: boolean;
|
||||
hideInSettings: boolean;
|
||||
detached: boolean = false;
|
||||
detachedFilePath?: string;
|
||||
showFolderNote: boolean;
|
||||
constructor(
|
||||
pattern: string,
|
||||
position: number,
|
||||
id: string | undefined,
|
||||
plugin: FolderNotesPlugin,
|
||||
) {
|
||||
this.type = 'pattern';
|
||||
this.id = id || crypto.randomUUID();
|
||||
this.string = pattern;
|
||||
this.position = position;
|
||||
this.subFolders = plugin.settings.excludePatternDefaultSettings.subFolders;
|
||||
this.disableSync = plugin.settings.excludePatternDefaultSettings.disableSync;
|
||||
this.disableAutoCreate = plugin.settings.excludePatternDefaultSettings.disableAutoCreate;
|
||||
this.disableFolderNote = plugin.settings.excludePatternDefaultSettings.disableFolderNote;
|
||||
this.enableCollapsing = plugin.settings.excludePatternDefaultSettings.enableCollapsing;
|
||||
// eslint-disable-next-line max-len
|
||||
this.excludeFromFolderOverview = plugin.settings.excludePatternDefaultSettings.excludeFromFolderOverview;
|
||||
this.path = '';
|
||||
this.hideInSettings = false;
|
||||
this.showFolderNote = plugin.settings.excludePatternDefaultSettings.showFolderNote;
|
||||
}
|
||||
}
|
||||
24
src/ExcludeFolders/WhitelistFolder.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import type FolderNotesPlugin from '../main';
|
||||
export class WhitelistedFolder {
|
||||
type: string;
|
||||
id: string;
|
||||
path: string;
|
||||
string: string;
|
||||
subFolders: boolean;
|
||||
enableSync: boolean = false;
|
||||
enableAutoCreate: boolean = false;
|
||||
enableFolderNote: boolean = false;
|
||||
disableCollapsing: boolean = false;
|
||||
showInFolderOverview: boolean = false;
|
||||
hideInFileExplorer: boolean = false;
|
||||
position: number;
|
||||
hideInSettings: boolean = false;
|
||||
constructor(path: string, position: number, id: string | undefined, plugin: FolderNotesPlugin) {
|
||||
this.type = 'folder';
|
||||
this.id = id || crypto.randomUUID();
|
||||
this.path = path;
|
||||
this.subFolders = plugin.settings.excludeFolderDefaultSettings.subFolders;
|
||||
this.position = position;
|
||||
this.string = '';
|
||||
}
|
||||
}
|
||||
29
src/ExcludeFolders/WhitelistPattern.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import type FolderNotesPlugin from '../main';
|
||||
export class WhitelistedPattern {
|
||||
type: string;
|
||||
id: string;
|
||||
string: string;
|
||||
path: string;
|
||||
position: number;
|
||||
subFolders: boolean;
|
||||
enableSync: boolean = false;
|
||||
enableAutoCreate: boolean = false;
|
||||
enableFolderNote: boolean = false;
|
||||
disableCollapsing: boolean = false;
|
||||
showInFolderOverview: boolean = false;
|
||||
hideInFileExplorer: boolean = false;
|
||||
hideInSettings: boolean = false;
|
||||
constructor(
|
||||
pattern: string,
|
||||
position: number,
|
||||
id: string | undefined,
|
||||
plugin: FolderNotesPlugin,
|
||||
) {
|
||||
this.type = 'pattern';
|
||||
this.id = id || crypto.randomUUID();
|
||||
this.subFolders = plugin.settings.excludePatternDefaultSettings.subFolders;
|
||||
this.position = position;
|
||||
this.string = pattern;
|
||||
this.path = '';
|
||||
}
|
||||
}
|
||||
328
src/ExcludeFolders/functions/folderFunctions.ts
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
import type FolderNotesPlugin from '../../main';
|
||||
import { getFolderNameFromPathString, getFolderPathFromString } from '../../functions/utils';
|
||||
import type { ExcludedFolder } from '../ExcludeFolder';
|
||||
import { ExcludePattern } from '../ExcludePattern';
|
||||
import { Platform, Setting } from 'obsidian';
|
||||
import { FolderSuggest } from '../../suggesters/FolderSuggester';
|
||||
import type { SettingsTab } from '../../settings/SettingsTab';
|
||||
import ExcludedFolderSettings from '../modals/ExcludeFolderSettings';
|
||||
import {
|
||||
updatePattern,
|
||||
getExcludedFoldersByPattern,
|
||||
addExcludePatternListItem,
|
||||
} from './patternFunctions';
|
||||
import { getWhitelistedFolder } from './whitelistFolderFunctions';
|
||||
import type { WhitelistedFolder } from '../WhitelistFolder';
|
||||
import type { WhitelistedPattern } from '../WhitelistPattern';
|
||||
|
||||
function combineExcluded(
|
||||
plugin: FolderNotesPlugin,
|
||||
path: string,
|
||||
includeDetached: boolean,
|
||||
pathOnly?: boolean,
|
||||
): Array<ExcludedFolder | ExcludePattern> {
|
||||
const folderName = getFolderNameFromPathString(path);
|
||||
const matchedPatterns = pathOnly ? [] : getExcludedFoldersByPattern(plugin, folderName);
|
||||
const excludedByPath = getExcludedFoldersByPath(plugin, path);
|
||||
let combined = [...matchedPatterns, ...excludedByPath];
|
||||
if (!includeDetached) combined = combined.filter((f) => !f.detached);
|
||||
return combined;
|
||||
}
|
||||
|
||||
function aggregateFlags(
|
||||
combinedExcludedFolders: Array<ExcludedFolder | ExcludePattern>,
|
||||
): Partial<ExcludedFolder> | undefined {
|
||||
if (combinedExcludedFolders.length === 0) return undefined;
|
||||
const result: Partial<ExcludedFolder> = {};
|
||||
const propertiesToCopy: (keyof ExcludedFolder)[] = [
|
||||
'disableAutoCreate',
|
||||
'disableFolderNote',
|
||||
'disableSync',
|
||||
'enableCollapsing',
|
||||
'excludeFromFolderOverview',
|
||||
'detached',
|
||||
'hideInSettings',
|
||||
'id',
|
||||
'showFolderNote',
|
||||
];
|
||||
for (const matchedFolder of combinedExcludedFolders) {
|
||||
for (const property of propertiesToCopy) {
|
||||
const value = (matchedFolder as Partial<ExcludedFolder>)[property];
|
||||
if (value === true) {
|
||||
(result)[property] = true as never;
|
||||
} else if (!value) {
|
||||
(result)[property] = false as never;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function applyWhitelistOverrides(
|
||||
excluded: Partial<ExcludedFolder>,
|
||||
whitelisted: WhitelistedFolder | WhitelistedPattern,
|
||||
): Partial<ExcludedFolder> {
|
||||
const out: Partial<ExcludedFolder> = { ...excluded };
|
||||
if (out.disableAutoCreate !== undefined) {
|
||||
out.disableAutoCreate = !whitelisted.enableAutoCreate;
|
||||
}
|
||||
if (out.disableFolderNote !== undefined) {
|
||||
out.disableFolderNote = !whitelisted.enableFolderNote;
|
||||
}
|
||||
if (out.disableSync !== undefined) {
|
||||
out.disableSync = !whitelisted.enableSync;
|
||||
}
|
||||
out.enableCollapsing = !whitelisted.disableCollapsing;
|
||||
if (out.excludeFromFolderOverview !== undefined) {
|
||||
out.excludeFromFolderOverview = !whitelisted.showInFolderOverview;
|
||||
}
|
||||
out.showFolderNote = !whitelisted.hideInFileExplorer;
|
||||
return out;
|
||||
}
|
||||
|
||||
function defaultExcludedIfEmpty(
|
||||
value: Partial<ExcludedFolder> | undefined,
|
||||
): ExcludedFolder | undefined {
|
||||
if (value && Object.keys(value).length === 0) {
|
||||
return {
|
||||
type: 'folder',
|
||||
id: '',
|
||||
path: '',
|
||||
string: '',
|
||||
subFolders: false,
|
||||
disableSync: false,
|
||||
disableAutoCreate: false,
|
||||
disableFolderNote: false,
|
||||
enableCollapsing: false,
|
||||
position: 0,
|
||||
excludeFromFolderOverview: false,
|
||||
hideInSettings: false,
|
||||
detached: false,
|
||||
showFolderNote: false,
|
||||
};
|
||||
}
|
||||
return value as ExcludedFolder | undefined;
|
||||
}
|
||||
|
||||
export function getExcludedFolder(
|
||||
plugin: FolderNotesPlugin,
|
||||
path: string,
|
||||
includeDetached: boolean,
|
||||
pathOnly?: boolean,
|
||||
ignoreWhitelist?: boolean,
|
||||
): ExcludedFolder | ExcludePattern | undefined {
|
||||
const combined = combineExcluded(plugin, path, includeDetached, pathOnly);
|
||||
let excluded = aggregateFlags(combined);
|
||||
|
||||
const whitelist = getWhitelistedFolder(
|
||||
plugin,
|
||||
path,
|
||||
);
|
||||
|
||||
let skipWhitelist = ignoreWhitelist ?? false;
|
||||
if (excluded?.detached) skipWhitelist = true;
|
||||
|
||||
if (whitelist && excluded && !skipWhitelist) {
|
||||
excluded = applyWhitelistOverrides(excluded, whitelist);
|
||||
}
|
||||
|
||||
return defaultExcludedIfEmpty(excluded) as ExcludedFolder | ExcludePattern | undefined;
|
||||
}
|
||||
|
||||
export function getDetachedFolder(
|
||||
plugin: FolderNotesPlugin,
|
||||
path: string,
|
||||
): ExcludedFolder | undefined {
|
||||
return plugin.settings.excludeFolders.find((f) => f.path === path && f.detached);
|
||||
}
|
||||
|
||||
export function getExcludedFolderByPath(
|
||||
plugin: FolderNotesPlugin,
|
||||
path: string,
|
||||
): ExcludedFolder | undefined {
|
||||
return plugin.settings.excludeFolders.find((excludedFolder) => {
|
||||
if (path.trim() === '' || !excludedFolder.path) { return false; }
|
||||
if (excludedFolder.path === path) { return true; }
|
||||
if (!excludedFolder.subFolders) { return false; }
|
||||
const excludedFolderPath = excludedFolder.path.includes('/')
|
||||
? excludedFolder.path
|
||||
: `${excludedFolder.path}/`;
|
||||
let folderPath = getFolderPathFromString(path);
|
||||
folderPath = folderPath.includes('/') ? folderPath : `${folderPath}/`;
|
||||
|
||||
if (folderPath.includes('/') || folderPath.includes('\\')) {
|
||||
return folderPath.startsWith(excludedFolderPath) || folderPath === excludedFolderPath;
|
||||
}
|
||||
return folderPath === excludedFolderPath;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
export function getExcludedFoldersByPath(
|
||||
plugin: FolderNotesPlugin,
|
||||
path: string,
|
||||
): ExcludedFolder[] {
|
||||
return plugin.settings.excludeFolders.filter((excludedFolder) => {
|
||||
if (path.trim() === '' || !excludedFolder.path) { return false; }
|
||||
if (excludedFolder.path === path) { return true; }
|
||||
if (!excludedFolder.subFolders) { return false; }
|
||||
const excludedFolderPath = excludedFolder.path.includes('/')
|
||||
? excludedFolder.path
|
||||
: `${excludedFolder.path}/`;
|
||||
let folderPath = getFolderPathFromString(path);
|
||||
folderPath = folderPath.includes('/') ? folderPath : `${folderPath}/`;
|
||||
|
||||
if (folderPath.includes('/') || folderPath.includes('\\')) {
|
||||
return folderPath.startsWith(excludedFolderPath) || folderPath === excludedFolderPath;
|
||||
}
|
||||
return folderPath === excludedFolderPath;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
export function addExcludedFolder(
|
||||
plugin: FolderNotesPlugin,
|
||||
excludedFolder: ExcludedFolder,
|
||||
reloadStyles = true,
|
||||
): void {
|
||||
plugin.settings.excludeFolders.push(excludedFolder);
|
||||
void plugin.saveSettings(reloadStyles);
|
||||
}
|
||||
|
||||
export async function deleteExcludedFolder(
|
||||
plugin: FolderNotesPlugin,
|
||||
excludedFolder: ExcludedFolder,
|
||||
): Promise<void> {
|
||||
plugin.settings.excludeFolders = plugin.settings.excludeFolders.filter(
|
||||
(folder) => folder.id !== excludedFolder.id || folder.type === 'pattern',
|
||||
);
|
||||
await plugin.saveSettings(true);
|
||||
resyncArray(plugin);
|
||||
}
|
||||
|
||||
export function updateExcludedFolder(
|
||||
plugin: FolderNotesPlugin,
|
||||
excludedFolder: ExcludePattern,
|
||||
newExcludeFolder: ExcludePattern,
|
||||
): void {
|
||||
plugin.settings.excludeFolders = plugin.settings.excludeFolders.filter(
|
||||
(folder) => folder.id !== excludedFolder.id,
|
||||
);
|
||||
addExcludedFolder(plugin, newExcludeFolder);
|
||||
}
|
||||
|
||||
export function resyncArray(plugin: FolderNotesPlugin): void {
|
||||
plugin.settings.excludeFolders = plugin.settings.excludeFolders.sort(
|
||||
(a, b) => a.position - b.position,
|
||||
);
|
||||
plugin.settings.excludeFolders.forEach((folder, index) => {
|
||||
folder.position = index;
|
||||
});
|
||||
void plugin.saveSettings();
|
||||
}
|
||||
|
||||
export function addExcludeFolderListItem(
|
||||
settings: SettingsTab,
|
||||
containerEl: HTMLElement,
|
||||
excludedFolder: ExcludedFolder,
|
||||
): void {
|
||||
const { plugin } = settings;
|
||||
const setting = new Setting(containerEl);
|
||||
setting.setClass('fn-exclude-folder-list');
|
||||
setting.addSearch((cb) => {
|
||||
new FolderSuggest(
|
||||
cb.inputEl,
|
||||
plugin,
|
||||
false,
|
||||
);
|
||||
cb.containerEl.addClass('fn-exclude-folder-path');
|
||||
cb.setPlaceholder('Folder path');
|
||||
cb.setValue(excludedFolder.path || '');
|
||||
cb.onChange((value) => {
|
||||
if (value.startsWith('{regex}') || value.includes('*')) {
|
||||
deleteExcludedFolder(plugin, excludedFolder);
|
||||
const pattern = new ExcludePattern(
|
||||
value,
|
||||
plugin.settings.excludeFolders.length,
|
||||
undefined,
|
||||
plugin,
|
||||
);
|
||||
addExcludedFolder(plugin, pattern);
|
||||
addExcludePatternListItem(settings, containerEl, pattern);
|
||||
setting.clear();
|
||||
setting.settingEl.remove();
|
||||
}
|
||||
if (!plugin.app.vault.getAbstractFileByPath(value)) return;
|
||||
excludedFolder.path = value;
|
||||
updateExcludedFolder(plugin, excludedFolder, excludedFolder);
|
||||
});
|
||||
});
|
||||
|
||||
setting.addButton((cb) => {
|
||||
cb.setIcon('edit');
|
||||
cb.setTooltip('Edit folder note');
|
||||
cb.onClick(() => {
|
||||
new ExcludedFolderSettings(plugin.app, plugin, excludedFolder).open();
|
||||
});
|
||||
});
|
||||
|
||||
if (Platform.isDesktop || Platform.isTablet) {
|
||||
setting.addButton((cb) => {
|
||||
cb.setIcon('up-chevron-glyph');
|
||||
cb.setTooltip('Move up');
|
||||
cb.onClick(() => {
|
||||
if (excludedFolder.position === 0) { return; }
|
||||
excludedFolder.position -= 1;
|
||||
updateExcludedFolder(plugin, excludedFolder, excludedFolder);
|
||||
const oldExcludedFolder = plugin.settings.excludeFolders.find(
|
||||
(folder) => folder.position === excludedFolder.position,
|
||||
);
|
||||
if (oldExcludedFolder) {
|
||||
oldExcludedFolder.position += 1;
|
||||
if (oldExcludedFolder.type === 'pattern') {
|
||||
updatePattern(plugin, oldExcludedFolder, oldExcludedFolder);
|
||||
} else {
|
||||
updateExcludedFolder(plugin, oldExcludedFolder, oldExcludedFolder);
|
||||
}
|
||||
}
|
||||
settings.display();
|
||||
});
|
||||
});
|
||||
|
||||
setting.addButton((cb) => {
|
||||
cb.setIcon('down-chevron-glyph');
|
||||
cb.setTooltip('Move down');
|
||||
cb.onClick(() => {
|
||||
if (excludedFolder.position === plugin.settings.excludeFolders.length - 1) {
|
||||
return;
|
||||
}
|
||||
excludedFolder.position += 1;
|
||||
|
||||
updateExcludedFolder(plugin, excludedFolder, excludedFolder);
|
||||
const oldExcludedFolder = plugin.settings.excludeFolders.find(
|
||||
(folder) => folder.position === excludedFolder.position,
|
||||
);
|
||||
if (oldExcludedFolder) {
|
||||
oldExcludedFolder.position -= 1;
|
||||
if (oldExcludedFolder.type === 'pattern') {
|
||||
updatePattern(plugin, oldExcludedFolder, oldExcludedFolder);
|
||||
} else {
|
||||
updateExcludedFolder(plugin, oldExcludedFolder, oldExcludedFolder);
|
||||
}
|
||||
}
|
||||
|
||||
settings.display();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setting.addButton((cb) => {
|
||||
cb.setIcon('trash-2');
|
||||
cb.setTooltip('Delete excluded folder');
|
||||
cb.onClick(() => {
|
||||
deleteExcludedFolder(plugin, excludedFolder);
|
||||
setting.clear();
|
||||
setting.settingEl.remove();
|
||||
});
|
||||
});
|
||||
}
|
||||
182
src/ExcludeFolders/functions/patternFunctions.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import type FolderNotesPlugin from '../../main';
|
||||
import type { ExcludePattern } from '../ExcludePattern';
|
||||
import { Setting, Platform } from 'obsidian';
|
||||
import type { SettingsTab } from '../../settings/SettingsTab';
|
||||
import { addExcludedFolder, resyncArray, updateExcludedFolder } from './folderFunctions';
|
||||
import PatternSettings from '../modals/PatternSettings';
|
||||
|
||||
const REGEX_PREFIX = '{regex}';
|
||||
const STAR = '*';
|
||||
const INDEX_START = 0;
|
||||
const SLICE_START_ONE = 1;
|
||||
const SLICE_EXCLUDE_LAST = -1;
|
||||
|
||||
function matchesPatternSpec(raw: string | undefined, folderName: string): boolean {
|
||||
if (!raw) return false;
|
||||
const string = raw.trim();
|
||||
const isRegex = string.startsWith(REGEX_PREFIX);
|
||||
const hasStartStar = string.startsWith(STAR);
|
||||
const hasEndStar = string.endsWith(STAR);
|
||||
if (!isRegex && !(hasStartStar || hasEndStar)) return false;
|
||||
|
||||
if (isRegex) {
|
||||
const body = string.replace(REGEX_PREFIX, '').trim();
|
||||
if (body === '') return false;
|
||||
try {
|
||||
return new RegExp(body).test(folderName);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasStartStar && hasEndStar) {
|
||||
const inner = string.slice(SLICE_START_ONE, SLICE_EXCLUDE_LAST);
|
||||
return folderName.includes(inner);
|
||||
}
|
||||
if (hasStartStar) {
|
||||
const suffix = string.slice(SLICE_START_ONE);
|
||||
return folderName.endsWith(suffix);
|
||||
}
|
||||
if (hasEndStar) {
|
||||
const prefix = string.slice(INDEX_START, SLICE_EXCLUDE_LAST);
|
||||
return folderName.startsWith(prefix);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function updatePattern(
|
||||
plugin: FolderNotesPlugin,
|
||||
pattern: ExcludePattern,
|
||||
newPattern: ExcludePattern,
|
||||
): void {
|
||||
plugin.settings.excludeFolders = plugin.settings.excludeFolders.filter(
|
||||
(folder) => folder.id !== pattern.id,
|
||||
);
|
||||
addExcludedFolder(plugin, newPattern);
|
||||
}
|
||||
|
||||
export async function deletePattern(
|
||||
plugin: FolderNotesPlugin,
|
||||
pattern: ExcludePattern,
|
||||
): Promise<void> {
|
||||
plugin.settings.excludeFolders = plugin.settings.excludeFolders.filter(
|
||||
(folder) => folder.id !== pattern.id || folder.type === 'folder',
|
||||
);
|
||||
await plugin.saveSettings(true);
|
||||
resyncArray(plugin);
|
||||
}
|
||||
|
||||
export function getExcludedFoldersByPattern(
|
||||
plugin: FolderNotesPlugin,
|
||||
folderName: string,
|
||||
): ExcludePattern[] {
|
||||
return plugin.settings.excludeFolders
|
||||
.filter((s) => s.type === 'pattern')
|
||||
.filter((pattern) => matchesPatternSpec(pattern.string, folderName)) as ExcludePattern[];
|
||||
}
|
||||
|
||||
export function getExcludedFolderByPattern(
|
||||
plugin: FolderNotesPlugin,
|
||||
folderName: string,
|
||||
): ExcludePattern | undefined {
|
||||
return (
|
||||
plugin.settings.excludeFolders
|
||||
.filter((s) => s.type === 'pattern')
|
||||
.find((pattern) => matchesPatternSpec(pattern.string, folderName))
|
||||
) as ExcludePattern | undefined;
|
||||
}
|
||||
|
||||
export function addExcludePatternListItem(
|
||||
settings: SettingsTab,
|
||||
containerEl: HTMLElement,
|
||||
pattern: ExcludePattern,
|
||||
): void {
|
||||
const { plugin } = settings;
|
||||
const setting = new Setting(containerEl);
|
||||
setting.setClass('fn-exclude-folder-list');
|
||||
setting.addSearch((cb) => {
|
||||
cb.containerEl.addClass('fn-exclude-folder-path');
|
||||
cb.setPlaceholder('Pattern');
|
||||
cb.setValue(pattern.string);
|
||||
cb.onChange((value) => {
|
||||
pattern.string = value;
|
||||
updatePattern(plugin, pattern, pattern);
|
||||
});
|
||||
});
|
||||
setting.addButton((cb) => {
|
||||
cb.setIcon('edit');
|
||||
cb.setTooltip('Edit pattern');
|
||||
cb.onClick(() => {
|
||||
new PatternSettings(plugin.app, plugin, pattern).open();
|
||||
});
|
||||
});
|
||||
|
||||
if (Platform.isDesktop || Platform.isTablet) {
|
||||
setting.addButton((cb) => {
|
||||
cb.setIcon('up-chevron-glyph');
|
||||
cb.setTooltip('Move up');
|
||||
cb.onClick(() => {
|
||||
if (pattern.position === 0) { return; }
|
||||
pattern.position -= 1;
|
||||
updatePattern(plugin, pattern, pattern);
|
||||
const oldPattern = plugin.settings.excludeFolders.find(
|
||||
(folder) => folder.position === pattern.position,
|
||||
);
|
||||
if (oldPattern) {
|
||||
oldPattern.position += 1;
|
||||
if (oldPattern.type === 'pattern') {
|
||||
const pat = oldPattern as ExcludePattern;
|
||||
updatePattern(
|
||||
plugin,
|
||||
pat,
|
||||
pat,
|
||||
);
|
||||
} else {
|
||||
updateExcludedFolder(plugin, oldPattern, oldPattern);
|
||||
}
|
||||
}
|
||||
settings.display();
|
||||
});
|
||||
});
|
||||
|
||||
setting.addButton((cb) => {
|
||||
cb.setIcon('down-chevron-glyph');
|
||||
cb.setTooltip('Move down');
|
||||
cb.onClick(() => {
|
||||
if (pattern.position === plugin.settings.excludeFolders.length - 1) {
|
||||
return;
|
||||
}
|
||||
pattern.position += 1;
|
||||
|
||||
updatePattern(plugin, pattern, pattern);
|
||||
const oldPattern = plugin.settings.excludeFolders.find(
|
||||
(folder) => folder.position === pattern.position,
|
||||
);
|
||||
if (oldPattern) {
|
||||
oldPattern.position -= 1;
|
||||
if (oldPattern.type === 'pattern') {
|
||||
const pat = oldPattern as ExcludePattern;
|
||||
updatePattern(
|
||||
plugin,
|
||||
pat,
|
||||
pat,
|
||||
);
|
||||
} else {
|
||||
updateExcludedFolder(plugin, oldPattern, oldPattern);
|
||||
}
|
||||
}
|
||||
settings.display();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setting.addButton((cb) => {
|
||||
cb.setIcon('trash-2');
|
||||
cb.setTooltip('Delete pattern');
|
||||
cb.onClick(() => {
|
||||
void deletePattern(plugin, pattern);
|
||||
setting.clear();
|
||||
setting.settingEl.remove();
|
||||
});
|
||||
});
|
||||
}
|
||||
220
src/ExcludeFolders/functions/whitelistFolderFunctions.ts
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
import type FolderNotesPlugin from '../../main';
|
||||
import { getFolderNameFromPathString, getFolderPathFromString } from '../../functions/utils';
|
||||
import type { WhitelistedFolder } from '../WhitelistFolder';
|
||||
import { WhitelistedPattern } from '../WhitelistPattern';
|
||||
import { Setting, ButtonComponent } from 'obsidian';
|
||||
import { FolderSuggest } from '../../suggesters/FolderSuggester';
|
||||
import type { SettingsTab } from '../../settings/SettingsTab';
|
||||
import WhitelistFolderSettings from '../modals/WhitelistFolderSettings';
|
||||
import {
|
||||
updateWhitelistedPattern,
|
||||
getWhitelistedFoldersByPattern,
|
||||
addWhitelistedPatternListItem,
|
||||
} from './whitelistPatternFunctions';
|
||||
|
||||
export function getWhitelistedFolder(
|
||||
plugin: FolderNotesPlugin,
|
||||
path: string,
|
||||
): WhitelistedFolder | WhitelistedPattern | undefined {
|
||||
let whitelistedFolder: Partial<WhitelistedFolder> | undefined = {};
|
||||
const folderName = getFolderNameFromPathString(path);
|
||||
const matchedPatterns = getWhitelistedFoldersByPattern(plugin, folderName);
|
||||
const whitelistedFolders = getWhitelistedFoldersByPath(plugin, path);
|
||||
const combinedWhitelistedFolders = [...matchedPatterns, ...whitelistedFolders];
|
||||
const propertiesToCopy: (keyof WhitelistedFolder)[] = [
|
||||
'enableAutoCreate',
|
||||
'enableFolderNote',
|
||||
'enableSync',
|
||||
'showInFolderOverview',
|
||||
];
|
||||
|
||||
if (combinedWhitelistedFolders.length > 0) {
|
||||
for (const matchedFolder of combinedWhitelistedFolders) {
|
||||
propertiesToCopy.forEach((property) => {
|
||||
const value = (matchedFolder as Partial<WhitelistedFolder>)[property];
|
||||
if (value === true) {
|
||||
(whitelistedFolder as Partial<WhitelistedFolder>)[property] = true as never;
|
||||
} else if (!value) {
|
||||
(whitelistedFolder as Partial<WhitelistedFolder>)[property] = false as never;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
whitelistedFolder
|
||||
&& Object.keys(whitelistedFolder).length === 0
|
||||
) {
|
||||
whitelistedFolder = undefined;
|
||||
}
|
||||
|
||||
return whitelistedFolder as WhitelistedFolder | WhitelistedPattern | undefined;
|
||||
}
|
||||
|
||||
export function getWhitelistedFolderByPath(
|
||||
plugin: FolderNotesPlugin,
|
||||
path: string,
|
||||
): WhitelistedFolder | WhitelistedPattern | undefined {
|
||||
return plugin.settings.whitelistFolders.find((whitelistedFolder) => {
|
||||
if (whitelistedFolder.path === path) { return true; }
|
||||
if (!whitelistedFolder.subFolders) { return false; }
|
||||
return getFolderPathFromString(path).startsWith(whitelistedFolder.path);
|
||||
});
|
||||
}
|
||||
|
||||
export function getWhitelistedFoldersByPath(
|
||||
plugin: FolderNotesPlugin,
|
||||
path: string,
|
||||
): Array<WhitelistedFolder | WhitelistedPattern> {
|
||||
return plugin.settings.whitelistFolders.filter((whitelistedFolder) => {
|
||||
if (whitelistedFolder.path === path) { return true; }
|
||||
if (!whitelistedFolder.subFolders) { return false; }
|
||||
return getFolderPathFromString(path).startsWith(whitelistedFolder.path);
|
||||
});
|
||||
}
|
||||
|
||||
export function addWhitelistedFolder(
|
||||
plugin: FolderNotesPlugin,
|
||||
whitelistedFolder: WhitelistedFolder | WhitelistedPattern,
|
||||
): void {
|
||||
plugin.settings.whitelistFolders.push(whitelistedFolder);
|
||||
void plugin.saveSettings(true);
|
||||
}
|
||||
|
||||
export async function deleteWhitelistedFolder(
|
||||
plugin: FolderNotesPlugin,
|
||||
whitelistedFolder: WhitelistedFolder | WhitelistedPattern,
|
||||
): Promise<void> {
|
||||
plugin.settings.whitelistFolders = plugin.settings.whitelistFolders.filter(
|
||||
(folder) => folder.id !== whitelistedFolder.id || folder.type === 'pattern',
|
||||
);
|
||||
await plugin.saveSettings(true);
|
||||
resyncArray(plugin);
|
||||
}
|
||||
|
||||
export function updateWhitelistedFolder(
|
||||
plugin: FolderNotesPlugin,
|
||||
whitelistedFolder: WhitelistedFolder,
|
||||
newWhitelistFolder: WhitelistedFolder,
|
||||
): void {
|
||||
plugin.settings.whitelistFolders = plugin.settings.whitelistFolders.filter(
|
||||
(folder) => folder.id !== whitelistedFolder.id,
|
||||
);
|
||||
addWhitelistedFolder(plugin, newWhitelistFolder);
|
||||
}
|
||||
|
||||
export function resyncArray(plugin: FolderNotesPlugin): void {
|
||||
plugin.settings.whitelistFolders = plugin.settings.whitelistFolders.sort(
|
||||
(a, b) => a.position - b.position,
|
||||
);
|
||||
plugin.settings.whitelistFolders.forEach((folder, index) => {
|
||||
folder.position = index;
|
||||
});
|
||||
void plugin.saveSettings();
|
||||
}
|
||||
|
||||
export function addWhitelistFolderListItem(
|
||||
settings: SettingsTab,
|
||||
containerEl: HTMLElement,
|
||||
whitelistedFolder: WhitelistedFolder,
|
||||
): void {
|
||||
const { plugin } = settings;
|
||||
const setting = new Setting(containerEl);
|
||||
setting.setClass('fn-exclude-folder-list');
|
||||
|
||||
const inputContainer = setting.settingEl.createDiv({
|
||||
cls: 'fn-whitelist-folder-input-container',
|
||||
});
|
||||
const SearchComponent = new Setting(inputContainer);
|
||||
SearchComponent.addSearch((cb) => {
|
||||
new FolderSuggest(
|
||||
cb.inputEl,
|
||||
plugin,
|
||||
true,
|
||||
);
|
||||
cb.containerEl.addClass('fn-exclude-folder-path');
|
||||
cb.setPlaceholder('Folder path');
|
||||
cb.setValue(whitelistedFolder.path);
|
||||
cb.onChange((value) => {
|
||||
if (value.startsWith('{regex}') || value.includes('*')) {
|
||||
void deleteWhitelistedFolder(plugin, whitelistedFolder);
|
||||
const pattern = new WhitelistedPattern(
|
||||
value,
|
||||
plugin.settings.whitelistFolders.length,
|
||||
undefined,
|
||||
plugin,
|
||||
);
|
||||
addWhitelistedFolder(plugin, pattern);
|
||||
addWhitelistedPatternListItem(settings, containerEl, pattern);
|
||||
setting.clear();
|
||||
setting.settingEl.remove();
|
||||
}
|
||||
if (!plugin.app.vault.getAbstractFileByPath(value)) return;
|
||||
whitelistedFolder.path = value;
|
||||
updateWhitelistedFolder(plugin, whitelistedFolder, whitelistedFolder);
|
||||
});
|
||||
});
|
||||
const buttonContainer = setting.settingEl.createDiv({ cls: 'fn-whitelist-folder-buttons' });
|
||||
|
||||
new ButtonComponent(buttonContainer)
|
||||
.setIcon('edit')
|
||||
.setTooltip('Edit folder note')
|
||||
.onClick(() => {
|
||||
new WhitelistFolderSettings(plugin.app, plugin, whitelistedFolder).open();
|
||||
});
|
||||
|
||||
new ButtonComponent(buttonContainer)
|
||||
.setIcon('up-chevron-glyph')
|
||||
.setTooltip('Move up')
|
||||
.onClick(() => {
|
||||
if (whitelistedFolder.position === 0) { return; }
|
||||
whitelistedFolder.position -= 1;
|
||||
updateWhitelistedFolder(plugin, whitelistedFolder, whitelistedFolder);
|
||||
const oldWhitelistedFolder = plugin.settings.whitelistFolders.find(
|
||||
(folder) => folder.position === whitelistedFolder.position,
|
||||
);
|
||||
if (oldWhitelistedFolder) {
|
||||
oldWhitelistedFolder.position += 1;
|
||||
if (oldWhitelistedFolder.type === 'pattern') {
|
||||
updateWhitelistedPattern(plugin, oldWhitelistedFolder, oldWhitelistedFolder);
|
||||
} else {
|
||||
updateWhitelistedFolder(plugin, oldWhitelistedFolder, oldWhitelistedFolder);
|
||||
}
|
||||
}
|
||||
settings.display();
|
||||
});
|
||||
|
||||
new ButtonComponent(buttonContainer)
|
||||
.setIcon('down-chevron-glyph')
|
||||
.setTooltip('Move down')
|
||||
.onClick(() => {
|
||||
if (whitelistedFolder.position === plugin.settings.whitelistFolders.length - 1) {
|
||||
return;
|
||||
}
|
||||
whitelistedFolder.position += 1;
|
||||
|
||||
updateWhitelistedFolder(plugin, whitelistedFolder, whitelistedFolder);
|
||||
const oldWhitelistedFolder = plugin.settings.whitelistFolders.find(
|
||||
(folder) => folder.position === whitelistedFolder.position,
|
||||
);
|
||||
if (oldWhitelistedFolder) {
|
||||
oldWhitelistedFolder.position -= 1;
|
||||
if (oldWhitelistedFolder.type === 'pattern') {
|
||||
updateWhitelistedPattern(plugin, oldWhitelistedFolder, oldWhitelistedFolder);
|
||||
} else {
|
||||
updateWhitelistedFolder(plugin, oldWhitelistedFolder, oldWhitelistedFolder);
|
||||
}
|
||||
}
|
||||
|
||||
settings.display();
|
||||
});
|
||||
|
||||
new ButtonComponent(buttonContainer)
|
||||
.setIcon('trash-2')
|
||||
.setTooltip('Delete excluded folder')
|
||||
.onClick(() => {
|
||||
void deleteWhitelistedFolder(plugin, whitelistedFolder);
|
||||
setting.clear();
|
||||
setting.settingEl.remove();
|
||||
});
|
||||
}
|
||||
184
src/ExcludeFolders/functions/whitelistPatternFunctions.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
import type FolderNotesPlugin from '../../main';
|
||||
import { Setting } from 'obsidian';
|
||||
import type { SettingsTab } from '../../settings/SettingsTab';
|
||||
import { resyncArray } from './folderFunctions';
|
||||
import WhitelistPatternSettings from '../modals/WhitelistPatternSettings';
|
||||
import type { WhitelistedPattern } from '../WhitelistPattern';
|
||||
import { addWhitelistedFolder, updateWhitelistedFolder } from './whitelistFolderFunctions';
|
||||
|
||||
const REGEX_PREFIX = '{regex}';
|
||||
const STAR = '*';
|
||||
const SLICE_START_ONE = 1;
|
||||
const SLICE_EXCLUDE_LAST = -1;
|
||||
|
||||
function matchesPatternSpec(raw: string | undefined, folderName: string): boolean {
|
||||
if (!raw) return false;
|
||||
const string = raw.trim();
|
||||
const isRegex = string.startsWith(REGEX_PREFIX);
|
||||
const hasStartStar = string.startsWith(STAR);
|
||||
const hasEndStar = string.endsWith(STAR);
|
||||
if (!isRegex && !(hasStartStar || hasEndStar)) return false;
|
||||
|
||||
if (isRegex) {
|
||||
const body = string.replace(REGEX_PREFIX, '').trim();
|
||||
if (body === '') return false;
|
||||
try {
|
||||
return new RegExp(body).test(folderName);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasStartStar && hasEndStar) {
|
||||
const inner = string.slice(SLICE_START_ONE, SLICE_EXCLUDE_LAST);
|
||||
return folderName.includes(inner);
|
||||
}
|
||||
if (hasStartStar) {
|
||||
const suffix = string.slice(SLICE_START_ONE);
|
||||
return folderName.endsWith(suffix);
|
||||
}
|
||||
if (hasEndStar) {
|
||||
const prefix = string.slice(0, SLICE_EXCLUDE_LAST);
|
||||
return folderName.startsWith(prefix);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function updateWhitelistedPattern(
|
||||
plugin: FolderNotesPlugin,
|
||||
pattern: WhitelistedPattern,
|
||||
newPattern: WhitelistedPattern,
|
||||
): void {
|
||||
plugin.settings.whitelistFolders = plugin.settings.whitelistFolders.filter(
|
||||
(folder) => folder.id !== pattern.id,
|
||||
);
|
||||
addWhitelistedFolder(plugin, newPattern);
|
||||
}
|
||||
|
||||
export async function deletePattern(
|
||||
plugin: FolderNotesPlugin,
|
||||
pattern: WhitelistedPattern,
|
||||
): Promise<void> {
|
||||
plugin.settings.whitelistFolders = plugin.settings.whitelistFolders.filter(
|
||||
(folder) => folder.id !== pattern.id || folder.type === 'folder',
|
||||
);
|
||||
await plugin.saveSettings(true);
|
||||
resyncArray(plugin);
|
||||
}
|
||||
|
||||
export function getWhitelistedFolderByPattern(
|
||||
plugin: FolderNotesPlugin,
|
||||
folderName: string,
|
||||
): WhitelistedPattern | undefined {
|
||||
return (
|
||||
plugin.settings.whitelistFolders
|
||||
.filter((s) => s.type === 'pattern')
|
||||
.find((pattern) => matchesPatternSpec(pattern.string, folderName))
|
||||
) as WhitelistedPattern | undefined;
|
||||
}
|
||||
|
||||
export function getWhitelistedFoldersByPattern(
|
||||
plugin: FolderNotesPlugin,
|
||||
folderName: string,
|
||||
): WhitelistedPattern[] {
|
||||
return (
|
||||
plugin.settings.whitelistFolders
|
||||
.filter((s) => s.type === 'pattern')
|
||||
.filter((pattern) => matchesPatternSpec(pattern.string, folderName))
|
||||
) as WhitelistedPattern[];
|
||||
}
|
||||
|
||||
export function addWhitelistedPatternListItem(
|
||||
settings: SettingsTab,
|
||||
containerEl: HTMLElement,
|
||||
pattern: WhitelistedPattern,
|
||||
): void {
|
||||
const { plugin } = settings;
|
||||
const setting = new Setting(containerEl);
|
||||
setting.setClass('fn-exclude-folder-list');
|
||||
setting.addSearch((cb) => {
|
||||
cb.containerEl.addClass('fn-exclude-folder-path');
|
||||
cb.setPlaceholder('Pattern');
|
||||
cb.setValue(pattern.string);
|
||||
cb.onChange((value) => {
|
||||
const exists = plugin.settings.whitelistFolders.some(
|
||||
(folder) => folder.string === value,
|
||||
);
|
||||
if (exists) { return; }
|
||||
pattern.string = value;
|
||||
updateWhitelistedPattern(plugin, pattern, pattern);
|
||||
});
|
||||
});
|
||||
setting.addButton((cb) => {
|
||||
cb.setIcon('edit');
|
||||
cb.setTooltip('Edit pattern');
|
||||
cb.onClick(() => {
|
||||
new WhitelistPatternSettings(plugin.app, plugin, pattern).open();
|
||||
});
|
||||
});
|
||||
|
||||
setting.addButton((cb) => {
|
||||
cb.setIcon('up-chevron-glyph');
|
||||
cb.setTooltip('Move up');
|
||||
cb.onClick(() => {
|
||||
if (pattern.position === 0) { return; }
|
||||
pattern.position -= 1;
|
||||
updateWhitelistedPattern(plugin, pattern, pattern);
|
||||
const oldPattern = plugin.settings.whitelistFolders.find(
|
||||
(folder) => folder.position === pattern.position,
|
||||
);
|
||||
if (oldPattern) {
|
||||
oldPattern.position += 1;
|
||||
if (oldPattern.type === 'pattern') {
|
||||
updateWhitelistedPattern(
|
||||
plugin,
|
||||
oldPattern as WhitelistedPattern,
|
||||
oldPattern as WhitelistedPattern,
|
||||
);
|
||||
} else {
|
||||
updateWhitelistedFolder(plugin, oldPattern, oldPattern);
|
||||
}
|
||||
}
|
||||
settings.display();
|
||||
});
|
||||
});
|
||||
|
||||
setting.addButton((cb) => {
|
||||
cb.setIcon('down-chevron-glyph');
|
||||
cb.setTooltip('Move down');
|
||||
cb.onClick(() => {
|
||||
if (pattern.position === plugin.settings.whitelistFolders.length - 1) {
|
||||
return;
|
||||
}
|
||||
pattern.position += 1;
|
||||
|
||||
updateWhitelistedPattern(plugin, pattern, pattern);
|
||||
const oldPattern = plugin.settings.whitelistFolders.find(
|
||||
(folder) => folder.position === pattern.position,
|
||||
);
|
||||
if (oldPattern) {
|
||||
oldPattern.position -= 1;
|
||||
if (oldPattern.type === 'pattern') {
|
||||
updateWhitelistedPattern(
|
||||
plugin,
|
||||
oldPattern as WhitelistedPattern,
|
||||
oldPattern as WhitelistedPattern,
|
||||
);
|
||||
} else {
|
||||
updateWhitelistedFolder(plugin, oldPattern, oldPattern);
|
||||
}
|
||||
}
|
||||
settings.display();
|
||||
});
|
||||
});
|
||||
|
||||
setting.addButton((cb) => {
|
||||
cb.setIcon('trash-2');
|
||||
cb.setTooltip('Delete pattern');
|
||||
cb.onClick(() => {
|
||||
void deletePattern(plugin, pattern);
|
||||
setting.clear();
|
||||
setting.settingEl.remove();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { App, Modal, Setting } from 'obsidian';
|
||||
import FolderNotesPlugin from '../main';
|
||||
import { ExcludedFolder } from '../settings';
|
||||
import { Modal, Setting, type App } from 'obsidian';
|
||||
import type FolderNotesPlugin from '../../main';
|
||||
import type { ExcludedFolder } from 'src/ExcludeFolders/ExcludeFolder';
|
||||
import { updateCSSClassesForFolder } from 'src/functions/styleFunctions';
|
||||
export default class ExcludedFolderSettings extends Modal {
|
||||
plugin: FolderNotesPlugin;
|
||||
app: App;
|
||||
|
|
@ -11,13 +12,13 @@ export default class ExcludedFolderSettings extends Modal {
|
|||
this.app = app;
|
||||
this.excludedFolder = excludedFolder;
|
||||
}
|
||||
onOpen() {
|
||||
onOpen(): void {
|
||||
this.display();
|
||||
}
|
||||
display() {
|
||||
display(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl('h2', { text: 'Exluded folder settings' });
|
||||
contentEl.createEl('h2', { text: 'Excluded folder settings' });
|
||||
new Setting(contentEl)
|
||||
.setName('Include subfolders')
|
||||
.setDesc('Choose if the subfolders of the folder should also be excluded')
|
||||
|
|
@ -26,8 +27,8 @@ export default class ExcludedFolderSettings extends Modal {
|
|||
.setValue(this.excludedFolder.subFolders)
|
||||
.onChange(async (value) => {
|
||||
this.excludedFolder.subFolders = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
await this.plugin.saveSettings(true);
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
|
|
@ -39,7 +40,33 @@ export default class ExcludedFolderSettings extends Modal {
|
|||
.onChange(async (value) => {
|
||||
this.excludedFolder.disableSync = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Don\'t show folder in folder overview')
|
||||
.setDesc('Choose if the folder should be shown in the folder overview')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.excludedFolder.excludeFromFolderOverview)
|
||||
.onChange(async (value) => {
|
||||
this.excludedFolder.excludeFromFolderOverview = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Show folder note in the file explorer')
|
||||
.setDesc('Choose if the folder note should be shown in the file explorer')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.excludedFolder.showFolderNote)
|
||||
.onChange(async (value) => {
|
||||
this.excludedFolder.showFolderNote = value;
|
||||
updateCSSClassesForFolder(this.excludedFolder.path, this.plugin);
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
|
|
@ -51,10 +78,9 @@ export default class ExcludedFolderSettings extends Modal {
|
|||
.onChange(async (value) => {
|
||||
this.excludedFolder.disableAutoCreate = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Disable open folder note')
|
||||
.setDesc('Choose if the folder note should be opened when the folder is opened')
|
||||
|
|
@ -63,9 +89,9 @@ export default class ExcludedFolderSettings extends Modal {
|
|||
.setValue(this.excludedFolder.disableFolderNote)
|
||||
.onChange(async (value) => {
|
||||
this.excludedFolder.disableFolderNote = value;
|
||||
await this.plugin.saveSettings();
|
||||
await this.plugin.saveSettings(true);
|
||||
this.display();
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
if (!this.excludedFolder.disableFolderNote) {
|
||||
|
|
@ -78,12 +104,12 @@ export default class ExcludedFolderSettings extends Modal {
|
|||
.onChange(async (value) => {
|
||||
this.excludedFolder.enableCollapsing = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
onClose() {
|
||||
|
||||
onClose(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
110
src/ExcludeFolders/modals/PatternSettings.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { Modal, Setting, type App } from 'obsidian';
|
||||
import type FolderNotesPlugin from '../../main';
|
||||
import type { ExcludePattern } from 'src/ExcludeFolders/ExcludePattern';
|
||||
import { refreshAllFolderStyles } from 'src/functions/styleFunctions';
|
||||
|
||||
export default class PatternSettings extends Modal {
|
||||
plugin: FolderNotesPlugin;
|
||||
app: App;
|
||||
pattern: ExcludePattern;
|
||||
constructor(app: App, plugin: FolderNotesPlugin, pattern: ExcludePattern) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.app = app;
|
||||
this.pattern = pattern;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.display();
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl('h2', { text: 'Pattern settings' });
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Disable folder name sync')
|
||||
|
||||
.setDesc('Choose if the folder name should be renamed when the file name has been changed')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.pattern.disableSync)
|
||||
.onChange(async (value) => {
|
||||
this.pattern.disableSync = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Disable auto creation of folder notes in this folder')
|
||||
// eslint-disable-next-line max-len
|
||||
.setDesc('Choose if a folder note should be created when a new folder is created that matches this pattern')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.pattern.disableAutoCreate)
|
||||
.onChange(async (value) => {
|
||||
this.pattern.disableAutoCreate = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Don\'t show folder in folder overview')
|
||||
.setDesc('Choose if the folder should be shown in the folder overview')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.pattern.excludeFromFolderOverview)
|
||||
.onChange(async (value) => {
|
||||
this.pattern.excludeFromFolderOverview = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Show folder note in the file explorer')
|
||||
.setDesc('Choose if the folder note should be shown in the file explorer')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.pattern.showFolderNote)
|
||||
.onChange(async (value) => {
|
||||
this.pattern.showFolderNote = value;
|
||||
await this.plugin.saveSettings();
|
||||
refreshAllFolderStyles(true, this.plugin);
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Disable open folder note')
|
||||
.setDesc('Choose if the folder note should be opened when the folder is opened')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.pattern.disableFolderNote)
|
||||
.onChange(async (value) => {
|
||||
this.pattern.disableFolderNote = value;
|
||||
await this.plugin.saveSettings(true);
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
|
||||
if (!this.pattern.disableFolderNote) {
|
||||
new Setting(contentEl)
|
||||
.setName('Collapse folder when opening folder note')
|
||||
.setDesc('Choose if the folder should be collapsed when the folder note is opened')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.pattern.enableCollapsing)
|
||||
.onChange(async (value) => {
|
||||
this.pattern.enableCollapsing = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
116
src/ExcludeFolders/modals/WhitelistFolderSettings.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { Modal, Setting, type App } from 'obsidian';
|
||||
import type FolderNotesPlugin from '../../main';
|
||||
import type { WhitelistedFolder } from '../WhitelistFolder';
|
||||
export default class WhitelistFolderSettings extends Modal {
|
||||
plugin: FolderNotesPlugin;
|
||||
app: App;
|
||||
whitelistedFolder: WhitelistedFolder;
|
||||
constructor(app: App, plugin: FolderNotesPlugin, whitelistedFolder: WhitelistedFolder) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.app = app;
|
||||
this.whitelistedFolder = whitelistedFolder;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.display();
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl('h2', { text: 'Whitelisted folder settings' });
|
||||
new Setting(contentEl)
|
||||
.setName('Include subfolders')
|
||||
.setDesc('Choose if the subfolders of the folder should also be whitelisted')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.whitelistedFolder.subFolders)
|
||||
.onChange(async (value) => {
|
||||
this.whitelistedFolder.subFolders = value;
|
||||
await this.plugin.saveSettings(true);
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Enable folder name sync')
|
||||
|
||||
.setDesc('Choose if the name of a folder note should be renamed when the folder name is changed')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.whitelistedFolder.enableSync)
|
||||
.onChange(async (value) => {
|
||||
this.whitelistedFolder.enableSync = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Show folder in folder overview')
|
||||
.setDesc('Choose if the folder should be shown in the folder overview')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.whitelistedFolder.showInFolderOverview)
|
||||
.onChange(async (value) => {
|
||||
this.whitelistedFolder.showInFolderOverview = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Hide folder note in file explorer')
|
||||
.setDesc('Choose if the folder note should be hidden in the file explorer')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.whitelistedFolder.hideInFileExplorer)
|
||||
.onChange(async (value) => {
|
||||
this.whitelistedFolder.hideInFileExplorer = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Allow auto creation of folder notes in this folder')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.whitelistedFolder.enableAutoCreate)
|
||||
.onChange(async (value) => {
|
||||
this.whitelistedFolder.enableAutoCreate = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Open folder note when clicking on the folder')
|
||||
.setDesc('Choose if the folder note should be opened when the folder is opened')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.whitelistedFolder.enableFolderNote)
|
||||
.onChange(async (value) => {
|
||||
this.whitelistedFolder.enableFolderNote = value;
|
||||
await this.plugin.saveSettings(true);
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
|
||||
if (this.whitelistedFolder.enableFolderNote) {
|
||||
new Setting(contentEl)
|
||||
.setName('Don\'t collapse folder when opening folder note')
|
||||
.setDesc('Choose if the folder should be collapsed when the folder note is opened')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.whitelistedFolder.disableCollapsing)
|
||||
.onChange(async (value) => {
|
||||
this.whitelistedFolder.disableCollapsing = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
onClose(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
93
src/ExcludeFolders/modals/WhitelistPatternSettings.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { Modal, Setting, type App } from 'obsidian';
|
||||
import type FolderNotesPlugin from '../../main';
|
||||
import type { WhitelistedPattern } from '../WhitelistPattern';
|
||||
|
||||
export default class WhitelistPatternSettings extends Modal {
|
||||
plugin: FolderNotesPlugin;
|
||||
app: App;
|
||||
pattern: WhitelistedPattern;
|
||||
constructor(app: App, plugin: FolderNotesPlugin, pattern: WhitelistedPattern) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.app = app;
|
||||
this.pattern = pattern;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.display();
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl('h2', { text: 'Whitelisted pattern settings' });
|
||||
new Setting(contentEl)
|
||||
.setName('Enable folder name sync')
|
||||
|
||||
.setDesc('Choose if the name of a folder note should be renamed when the folder name is changed')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.pattern.enableSync)
|
||||
.onChange(async (value) => {
|
||||
this.pattern.enableSync = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Allow auto creation of folder notes in this folder')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.pattern.enableAutoCreate)
|
||||
.onChange(async (value) => {
|
||||
this.pattern.enableAutoCreate = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Show folder in folder overview')
|
||||
.setDesc('Choose if the folder should be shown in the folder overview')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.pattern.showInFolderOverview)
|
||||
.onChange(async (value) => {
|
||||
this.pattern.showInFolderOverview = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Open folder note when clicking on the folder')
|
||||
.setDesc('Choose if the folder note should be opened when you click on the folder')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.pattern.enableFolderNote)
|
||||
.onChange(async (value) => {
|
||||
this.pattern.enableFolderNote = value;
|
||||
await this.plugin.saveSettings(true);
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
|
||||
if (this.pattern.enableFolderNote) {
|
||||
new Setting(contentEl)
|
||||
.setName('Don\'t collapse folder when opening folder note')
|
||||
.setDesc('Choose if the folder should be collapsed when the folder note is opened')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.pattern.disableCollapsing)
|
||||
.onChange(async (value) => {
|
||||
this.pattern.disableCollapsing = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
63
src/ExcludeFolders/modals/WhitelistedFoldersSettings.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { Modal, Setting, type App } from 'obsidian';
|
||||
import type { SettingsTab } from 'src/settings/SettingsTab';
|
||||
import type FolderNotesPlugin from '../../main';
|
||||
import { WhitelistedFolder } from '../WhitelistFolder';
|
||||
import {
|
||||
addWhitelistFolderListItem,
|
||||
addWhitelistedFolder,
|
||||
} from '../functions/whitelistFolderFunctions';
|
||||
import { addWhitelistedPatternListItem } from '../functions/whitelistPatternFunctions';
|
||||
|
||||
export default class WhitelistedFoldersSettings extends Modal {
|
||||
plugin: FolderNotesPlugin;
|
||||
app: App;
|
||||
settingsTab: SettingsTab;
|
||||
constructor(settingsTab: SettingsTab) {
|
||||
super(settingsTab.app);
|
||||
this.plugin = settingsTab.plugin;
|
||||
this.settingsTab = settingsTab;
|
||||
this.app = settingsTab.app;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
|
||||
const { contentEl } = this;
|
||||
contentEl.createEl('h2', { text: 'Manage whitelisted folders' });
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Add whitelisted folder')
|
||||
.setClass('add-exclude-folder-item')
|
||||
.addButton((cb) => {
|
||||
cb.setIcon('plus');
|
||||
cb.setClass('add-exclude-folder');
|
||||
cb.setTooltip('Add whitelisted folder');
|
||||
cb.onClick(() => {
|
||||
const whitelistedFolder = new WhitelistedFolder(
|
||||
'', this.plugin.settings.whitelistFolders.length,
|
||||
undefined, this.plugin,
|
||||
);
|
||||
addWhitelistFolderListItem(
|
||||
this.plugin.settingsTab, contentEl, whitelistedFolder,
|
||||
);
|
||||
addWhitelistedFolder(this.plugin, whitelistedFolder);
|
||||
this.settingsTab.renderSettingsPage(this.settingsTab.plugin.settings.settingsTab);
|
||||
});
|
||||
});
|
||||
|
||||
this.plugin.settings.whitelistFolders
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.forEach((whitelistedFolder) => {
|
||||
if (whitelistedFolder.string?.trim() !== '' &&
|
||||
whitelistedFolder.path?.trim() === '') {
|
||||
addWhitelistedPatternListItem(this.settingsTab, contentEl, whitelistedFolder);
|
||||
} else {
|
||||
addWhitelistFolderListItem(this.settingsTab, contentEl, whitelistedFolder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import { App, TFolder, Menu, TAbstractFile, Notice, TFile } from 'obsidian';
|
||||
import FolderNotesPlugin from './main';
|
||||
import { ExcludedFolder } from './settings';
|
||||
export class Commands {
|
||||
plugin: FolderNotesPlugin;
|
||||
app: App;
|
||||
constructor(app: App, plugin: FolderNotesPlugin) {
|
||||
this.plugin = plugin;
|
||||
this.app = app;
|
||||
}
|
||||
registerCommands() {
|
||||
this.plugin.registerEvent(this.app.workspace.on('file-menu', (menu: Menu, file: TAbstractFile) => {
|
||||
if (!(file instanceof TFolder)) return;
|
||||
if (this.plugin.settings.excludeFolders.find((folder) => folder.path === file.path)) {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle('Remove folder from excluded folders')
|
||||
.setIcon('trash')
|
||||
.onClick(() => {
|
||||
this.plugin.settings.excludeFolders = this.plugin.settings.excludeFolders.filter(
|
||||
(folder) => folder.path !== file.path);
|
||||
this.plugin.saveSettings();
|
||||
new Notice('Successfully removed folder from excluded folders');
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
menu.addItem((item) => {
|
||||
item.setTitle('Exclude folder from folder notes')
|
||||
.setIcon('x-circle')
|
||||
.onClick(() => {
|
||||
const excludedFolder = new ExcludedFolder(file.path, this.plugin.settings.excludeFolders.length);
|
||||
this.plugin.settings.excludeFolders.push(excludedFolder);
|
||||
this.plugin.saveSettings();
|
||||
new Notice('Successfully excluded folder from folder notes');
|
||||
});
|
||||
});
|
||||
if (this.plugin.app.vault.getAbstractFileByPath(file.path + '/' + file.name + '.md')) {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle('Delete folder note')
|
||||
.setIcon('trash')
|
||||
.onClick(() => {
|
||||
file = this.plugin.app.vault.getAbstractFileByPath(file?.path + '/' + file?.name + '.md') as TFile;
|
||||
if (!(file instanceof TFile)) return;
|
||||
this.plugin.deleteFolderNote(file);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle('Create folder note')
|
||||
.setIcon('edit')
|
||||
.onClick(() => {
|
||||
this.plugin.createFolderNote(file.path + '/' + file.name + '.md' , true);
|
||||
});
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
22
src/events/EventEmitter.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export class CustomEventEmitter {
|
||||
private events: { [key: string]: Array<(data?: unknown) => void> } = {};
|
||||
|
||||
on(event: string, listener: (data?: unknown) => void): void {
|
||||
if (!this.events[event]) {
|
||||
this.events[event] = [];
|
||||
}
|
||||
this.events[event].push(listener);
|
||||
}
|
||||
|
||||
off(event: string, listener: (data?: unknown) => void): void {
|
||||
if (!this.events[event]) return;
|
||||
|
||||
this.events[event] = this.events[event].filter((l) => l !== listener);
|
||||
}
|
||||
|
||||
emit(event: string, data?: unknown): void {
|
||||
if (!this.events[event]) return;
|
||||
|
||||
this.events[event].forEach((listener) => listener(data));
|
||||
}
|
||||
}
|
||||
163
src/events/FrontMatterTitle.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import type FolderNotesPlugin from 'src/main';
|
||||
import {
|
||||
type Listener,
|
||||
type Events,
|
||||
type ApiInterface,
|
||||
type DeferInterface,
|
||||
type ListenerRef,
|
||||
type EventDispatcherInterface,
|
||||
getDefer,
|
||||
} from 'front-matter-plugin-api-provider';
|
||||
import { type App, TFile, TFolder } from 'obsidian';
|
||||
import { getFolder, getFolderNote } from 'src/functions/folderNoteFunctions';
|
||||
|
||||
interface UpdateData {
|
||||
id: string;
|
||||
result: boolean;
|
||||
path: string;
|
||||
pathOnly: boolean;
|
||||
breadcrumb?: HTMLElement;
|
||||
}
|
||||
|
||||
interface WrappedUpdateData {
|
||||
data: UpdateData;
|
||||
}
|
||||
|
||||
export class FrontMatterTitlePluginHandler {
|
||||
plugin: FolderNotesPlugin;
|
||||
app!: App;
|
||||
api: ApiInterface | null = null;
|
||||
deffer: DeferInterface | null = null;
|
||||
modifiedFolders: Map<string, TFolder> = new Map();
|
||||
eventRef: ListenerRef<'manager:update'> | null = null;
|
||||
dispatcher: EventDispatcherInterface<Events> | null = null;
|
||||
constructor(plugin: FolderNotesPlugin) {
|
||||
this.plugin = plugin;
|
||||
this.app = plugin.app;
|
||||
|
||||
(async (): Promise<void> => {
|
||||
this.deffer = getDefer(this.app);
|
||||
if (this.deffer.isPluginReady()) {
|
||||
this.api = this.deffer.getApi();
|
||||
} else {
|
||||
await this.deffer.awaitPlugin();
|
||||
this.api = this.deffer.getApi();
|
||||
// if you want to wait features you can use the following chain
|
||||
if (!this.deffer.isFeaturesReady()) {
|
||||
await this.deffer.awaitFeatures();
|
||||
}
|
||||
}
|
||||
if (plugin.settings.frontMatterTitle.enabled) {
|
||||
const dispatcher = this.api?.getEventDispatcher();
|
||||
if (dispatcher) {
|
||||
this.dispatcher = dispatcher;
|
||||
}
|
||||
const event: Listener<Events, 'manager:update'> = {
|
||||
name: 'manager:update',
|
||||
cb: (data): void => {
|
||||
this.fmptUpdateFileName(data as unknown as UpdateData, true);
|
||||
},
|
||||
};
|
||||
// Keep ref to remove listener
|
||||
const ref = dispatcher?.addListener(event);
|
||||
if (ref) {
|
||||
this.eventRef = ref;
|
||||
}
|
||||
// this.plugin.app.vault.getFiles().forEach((file) => {
|
||||
// this.handleRename({ id: '', result: false, path: file.path }, false);
|
||||
// });
|
||||
plugin.updateAllBreadcrumbs();
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
deleteEvent(): void {
|
||||
if (this.eventRef && this.dispatcher) {
|
||||
this.dispatcher.removeListener(this.eventRef);
|
||||
}
|
||||
}
|
||||
async fmptUpdateFileName(data: UpdateData, isEvent: boolean): Promise<void> {
|
||||
const hasNestedData = 'data' in (data as unknown as Record<string, unknown>);
|
||||
const actualData: UpdateData = hasNestedData
|
||||
? (data as unknown as WrappedUpdateData).data
|
||||
: data;
|
||||
const file = this.app.vault.getAbstractFileByPath(actualData.path);
|
||||
if (!(file instanceof TFile)) { return; }
|
||||
|
||||
const resolver = this.api?.getResolverFactory()?.createResolver('#feature-id#');
|
||||
const newName = resolver?.resolve(file?.path ?? '');
|
||||
const folder = getFolder(this.plugin, file);
|
||||
if (!(folder instanceof TFolder)) { return; }
|
||||
|
||||
const folderNote = getFolderNote(this.plugin, folder.path);
|
||||
if (!folderNote) { return; }
|
||||
if (folderNote !== file) { return; }
|
||||
if (!actualData.pathOnly) {
|
||||
this.plugin.changeFolderNameInExplorer(folder, newName);
|
||||
}
|
||||
|
||||
const { breadcrumb } = actualData;
|
||||
if (breadcrumb) {
|
||||
this.plugin.changeFolderNameInPath(folder, newName, breadcrumb);
|
||||
}
|
||||
|
||||
if (isEvent) {
|
||||
this.plugin.updateAllBreadcrumbs();
|
||||
}
|
||||
|
||||
if (newName) {
|
||||
folder.newName = newName;
|
||||
this.modifiedFolders.set(folder.path, folder);
|
||||
} else {
|
||||
folder.newName = null;
|
||||
this.modifiedFolders.delete(folder.path);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async fmptUpdateFolderName(data: UpdateData, _replacePath: boolean): Promise<void> {
|
||||
const hasNestedData = 'data' in (data as unknown as Record<string, unknown>);
|
||||
const actualData: UpdateData = hasNestedData
|
||||
? (data as unknown as WrappedUpdateData).data
|
||||
: data;
|
||||
const folder = this.app.vault.getAbstractFileByPath(actualData.path);
|
||||
if (!(folder instanceof TFolder)) { return; }
|
||||
const folderNote = getFolderNote(this.plugin, folder.path);
|
||||
if (!folderNote) { return; }
|
||||
|
||||
const resolver = this.api?.getResolverFactory()?.createResolver('#feature-id#');
|
||||
const newName = resolver?.resolve(folderNote?.path ?? '');
|
||||
if (!newName) return;
|
||||
|
||||
if (!actualData.pathOnly) {
|
||||
this.plugin.changeFolderNameInExplorer(folder, newName);
|
||||
}
|
||||
|
||||
const { breadcrumb } = actualData;
|
||||
if (breadcrumb) {
|
||||
this.plugin.changeFolderNameInPath(folder, newName, breadcrumb);
|
||||
}
|
||||
|
||||
folder.newName = newName;
|
||||
this.modifiedFolders.set(folder.path, folder);
|
||||
}
|
||||
|
||||
async getNewFolderName(folder: TFolder): Promise<string | null> {
|
||||
if (this.modifiedFolders.has(folder.path)) {
|
||||
const modifiedFolder = this.modifiedFolders.get(folder.path);
|
||||
if (modifiedFolder) {
|
||||
return modifiedFolder.newName;
|
||||
}
|
||||
}
|
||||
const folderNote = getFolderNote(this.plugin, folder.path);
|
||||
if (!folderNote) return null;
|
||||
const resolver = this.api?.getResolverFactory()?.createResolver('#feature-id#');
|
||||
return resolver?.resolve(folderNote?.path ?? '') ?? null;
|
||||
}
|
||||
|
||||
async getNewFileName(file: TFile): Promise<string | null> {
|
||||
const resolver = this.api?.getResolverFactory()?.createResolver('#feature-id#');
|
||||
const changedName = resolver?.resolve(file?.path ?? '');
|
||||
return changedName ?? null;
|
||||
}
|
||||
}
|
||||
240
src/events/MutationObserver.ts
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import { Keymap, Platform } from 'obsidian';
|
||||
import type FolderNotesPlugin from 'src/main';
|
||||
import { getFolderNote, getFolderNoteFolder } from 'src/functions/folderNoteFunctions';
|
||||
import { handleViewHeaderClick } from './handleClick';
|
||||
import { getExcludedFolder } from 'src/ExcludeFolders/functions/folderFunctions';
|
||||
import { updateCSSClassesForFolder } from 'src/functions/styleFunctions';
|
||||
|
||||
let fileExplorerMutationObserver: MutationObserver | null = null;
|
||||
|
||||
export function registerFileExplorerObserver(plugin: FolderNotesPlugin): void {
|
||||
// Run once on initial layout
|
||||
plugin.app.workspace.onLayoutReady(() => {
|
||||
initializeFolderNoteFeatures(plugin);
|
||||
initializeBreadcrumbs(plugin);
|
||||
});
|
||||
|
||||
// Re-run when layout changes (e.g. File Explorer is reopened)
|
||||
plugin.registerEvent(
|
||||
plugin.app.workspace.on('layout-change', () => {
|
||||
initializeFolderNoteFeatures(plugin);
|
||||
|
||||
const activeLeaf = plugin.app.workspace.getActiveFileView()?.containerEl;
|
||||
if (!activeLeaf) return;
|
||||
|
||||
const titleContainer = activeLeaf.querySelector('.view-header-title-container');
|
||||
if (!(titleContainer instanceof HTMLElement)) return;
|
||||
|
||||
updateFolderNamesInPath(plugin, titleContainer);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function unregisterFileExplorerObserver(): void {
|
||||
if (fileExplorerMutationObserver) {
|
||||
fileExplorerMutationObserver.disconnect();
|
||||
fileExplorerMutationObserver = null;
|
||||
}
|
||||
}
|
||||
|
||||
function initializeFolderNoteFeatures(plugin: FolderNotesPlugin): void {
|
||||
initializeAllFolderTitles(plugin);
|
||||
observeFolderTitleMutations(plugin);
|
||||
}
|
||||
|
||||
function initializeBreadcrumbs(plugin: FolderNotesPlugin): void {
|
||||
const titleContainers = activeDocument.querySelectorAll('.view-header-title-container');
|
||||
if (!titleContainers.length) return;
|
||||
titleContainers.forEach((container) => {
|
||||
if (!(container.instanceOf(HTMLElement))) return;
|
||||
scheduleIdle(() => updateFolderNamesInPath(plugin, container), { timeout: 1000 });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes the File Explorer for newly added folder elements and applies plugin logic (e.g., styles, event listeners)
|
||||
* automatically when folders are created, expanded, or when the File Explorer view is reopened.
|
||||
*/
|
||||
function observeFolderTitleMutations(plugin: FolderNotesPlugin): void {
|
||||
if (fileExplorerMutationObserver) {
|
||||
fileExplorerMutationObserver.disconnect();
|
||||
}
|
||||
fileExplorerMutationObserver = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
for (const node of Array.from(mutation.addedNodes)) {
|
||||
if (!(node.instanceOf(HTMLElement))) continue;
|
||||
processAddedFolders(node, plugin);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileExplorerMutationObserver.observe(document, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
function initializeAllFolderTitles(plugin: FolderNotesPlugin): void {
|
||||
const allTitles = activeDocument.querySelectorAll('.nav-folder-title-content');
|
||||
for (const title of Array.from(allTitles)) {
|
||||
const folderTitle = title as HTMLElement;
|
||||
const folderEl = folderTitle.closest('.nav-folder-title');
|
||||
if (!folderEl) continue;
|
||||
|
||||
const folderPath = folderEl.getAttribute('data-path') || '';
|
||||
setupFolderTitle(folderTitle, plugin, folderPath);
|
||||
}
|
||||
}
|
||||
|
||||
function processAddedFolders(node: HTMLElement, plugin: FolderNotesPlugin): void {
|
||||
const titles: HTMLElement[] = [];
|
||||
if (node.matches('.nav-folder-title-content')) {
|
||||
titles.push(node);
|
||||
}
|
||||
node.querySelectorAll('.nav-folder-title-content').forEach((el) => {
|
||||
titles.push(el as HTMLElement);
|
||||
});
|
||||
|
||||
titles.forEach((folderTitle) => {
|
||||
const folderEl = folderTitle.closest('.nav-folder-title');
|
||||
const folderPath = folderEl?.getAttribute('data-path') || '';
|
||||
const RETRY_TIMEOUT = 50;
|
||||
if (!folderEl || !folderPath) {
|
||||
window.setTimeout(() => {
|
||||
const retryFolderEl = folderTitle.closest('.nav-folder-title');
|
||||
const retryFolderPath = retryFolderEl?.getAttribute('data-path') || '';
|
||||
if (retryFolderEl && retryFolderPath) {
|
||||
setupFolderTitle(folderTitle, plugin, retryFolderPath);
|
||||
}
|
||||
}, RETRY_TIMEOUT);
|
||||
return;
|
||||
}
|
||||
setupFolderTitle(folderTitle, plugin, folderPath);
|
||||
});
|
||||
}
|
||||
|
||||
async function setupFolderTitle(
|
||||
folderTitle: HTMLElement,
|
||||
plugin: FolderNotesPlugin,
|
||||
folderPath: string,
|
||||
): Promise<void> {
|
||||
if (folderTitle.dataset.initialized === 'true') return;
|
||||
if (!folderPath) return;
|
||||
|
||||
folderTitle.dataset.initialized = 'true';
|
||||
await updateCSSClassesForFolder(folderPath, plugin);
|
||||
|
||||
if (plugin.settings.frontMatterTitle.enabled) {
|
||||
plugin.fmtpHandler?.fmptUpdateFolderName(
|
||||
{ id: '', result: false, path: folderPath, pathOnly: false },
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
if (Platform.isMobile && plugin.settings.disableOpenFolderNoteOnClick) return;
|
||||
|
||||
plugin.registerDomEvent(folderTitle, 'pointerover', (event: MouseEvent) => {
|
||||
plugin.hoveredElement = folderTitle;
|
||||
plugin.mouseEvent = event;
|
||||
|
||||
if (!Keymap.isModEvent(event)) return;
|
||||
if (!(event.target instanceof HTMLElement)) return;
|
||||
|
||||
const folderNote = getFolderNote(plugin, folderPath);
|
||||
if (!folderNote) return;
|
||||
|
||||
plugin.app.workspace.trigger('hover-link', {
|
||||
event,
|
||||
source: 'preview',
|
||||
hoverParent: { file: folderNote },
|
||||
targetEl: event.target,
|
||||
linktext: folderNote.basename,
|
||||
sourcePath: folderNote.path,
|
||||
});
|
||||
plugin.hoverLinkTriggered = true;
|
||||
});
|
||||
|
||||
plugin.registerDomEvent(folderTitle, 'pointerout', () => {
|
||||
plugin.hoveredElement = null;
|
||||
plugin.mouseEvent = null;
|
||||
plugin.hoverLinkTriggered = false;
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
async function updateFolderNamesInPath(
|
||||
plugin: FolderNotesPlugin,
|
||||
titleContainer: HTMLElement,
|
||||
): Promise<void> {
|
||||
const titleParent = titleContainer.querySelector('.view-header-title-parent');
|
||||
let path = '';
|
||||
const TRAILING_SLASH_LENGTH = 1;
|
||||
|
||||
if (titleParent?.childNodes.length === 0) {
|
||||
titleContainer.classList.remove('hide-folder-note-title-in-path');
|
||||
}
|
||||
|
||||
for (const breadcrumb of Array.from(titleParent?.childNodes ?? [])) {
|
||||
if (!(breadcrumb instanceof HTMLElement)) continue;
|
||||
if (breadcrumb.classList.contains('view-header-breadcrumb-separator')) {
|
||||
if (breadcrumb.nextSibling === null) {
|
||||
breadcrumb.classList.add('is-last-separator');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
path += breadcrumb.getAttribute('old-name') ?? (breadcrumb).innerText.trim();
|
||||
path += '/';
|
||||
const folderPath = path.slice(0, -TRAILING_SLASH_LENGTH);
|
||||
|
||||
const excludedFolder = getExcludedFolder(plugin, folderPath, true);
|
||||
if (excludedFolder?.disableFolderNote) return;
|
||||
const folderNote = getFolderNote(plugin, folderPath);
|
||||
const viewHeaderTitle = titleContainer.querySelector('.view-header-title');
|
||||
|
||||
if (viewHeaderTitle && folderNote) {
|
||||
const filePath = path + (viewHeaderTitle as HTMLElement).innerText.trim() + '.md';
|
||||
const file = plugin.app.vault.getAbstractFileByPath(
|
||||
filePath);
|
||||
const folder = getFolderNoteFolder(plugin, folderNote, file?.name ?? '');
|
||||
if (folder && file && file.path === folderNote?.path && file.parent?.path !== '/') {
|
||||
viewHeaderTitle.parentElement?.classList.add('hide-folder-note-title-in-path');
|
||||
viewHeaderTitle.classList.add('path-is-folder-note');
|
||||
} else {
|
||||
viewHeaderTitle.parentElement?.classList.remove('hide-folder-note-title-in-path');
|
||||
viewHeaderTitle.classList.remove('path-is-folder-note');
|
||||
}
|
||||
}
|
||||
if (!folderNote) {
|
||||
breadcrumb.classList.remove('has-folder-note');
|
||||
breadcrumb.removeAttribute('data-path');
|
||||
continue;
|
||||
}
|
||||
breadcrumb.classList.add('has-folder-note');
|
||||
breadcrumb?.setAttribute('data-path', path.slice(0, -TRAILING_SLASH_LENGTH));
|
||||
if (!breadcrumb.onclick) {
|
||||
breadcrumb.addEventListener('click', (e) => {
|
||||
handleViewHeaderClick(e, plugin);
|
||||
}, { capture: true });
|
||||
}
|
||||
|
||||
if (plugin.settings.frontMatterTitle.enabled) {
|
||||
plugin.fmtpHandler?.fmptUpdateFolderName(
|
||||
{ id: '', result: false, path: folderPath, pathOnly: true, breadcrumb: breadcrumb },
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Schedules a callback to run when the browser is idle, or after a timeout as a fallback.
|
||||
// - callback: The function to execute when idle or after the timeout.
|
||||
// - options: Optional object with a 'timeout' property (in milliseconds).
|
||||
function scheduleIdle(callback: () => void, options?: { timeout: number }): void {
|
||||
const DEFAULT_IDLE_TIMEOUT = 200;
|
||||
if ('requestIdleCallback' in window) {
|
||||
const windowWithIdle = window as Window & {
|
||||
requestIdleCallback: (callback: () => void, options?: { timeout: number }) => void
|
||||
};
|
||||
windowWithIdle.requestIdleCallback(callback, options);
|
||||
} else {
|
||||
globalThis.setTimeout(callback, options?.timeout || DEFAULT_IDLE_TIMEOUT);
|
||||
}
|
||||
}
|
||||
58
src/events/TabManager.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import type FolderNotesPlugin from 'src/main';
|
||||
import { EditableFileView, TFolder, type App } from 'obsidian';
|
||||
import { getFolder, getFolderNote } from 'src/functions/folderNoteFunctions';
|
||||
export class TabManager {
|
||||
plugin: FolderNotesPlugin;
|
||||
app: App;
|
||||
constructor(plugin: FolderNotesPlugin) {
|
||||
this.plugin = plugin;
|
||||
this.app = plugin.app;
|
||||
}
|
||||
|
||||
resetTabs(): void {
|
||||
if (!this.isEnabled()) return;
|
||||
|
||||
this.app.workspace.iterateAllLeaves((leaf) => {
|
||||
if (!(leaf.view instanceof EditableFileView)) return;
|
||||
const file = leaf.view?.file;
|
||||
if (!file) return;
|
||||
leaf.tabHeaderInnerTitleEl.setText(file.basename);
|
||||
});
|
||||
}
|
||||
|
||||
updateTabs(): void {
|
||||
if (!this.isEnabled()) return;
|
||||
this.app.workspace.iterateAllLeaves((leaf) => {
|
||||
if (!(leaf.view instanceof EditableFileView)) return;
|
||||
const file = leaf.view?.file;
|
||||
if (!file) return;
|
||||
const folder = getFolder(this.plugin, file);
|
||||
if (!folder) return;
|
||||
leaf.tabHeaderInnerTitleEl.setText(folder.name);
|
||||
});
|
||||
}
|
||||
|
||||
updateTab(folderPath: string): void {
|
||||
if (!this.isEnabled()) return;
|
||||
|
||||
const folder = this.app.vault.getAbstractFileByPath(folderPath);
|
||||
if (!(folder instanceof TFolder)) return;
|
||||
|
||||
const folderNote = getFolderNote(this.plugin, folder.path);
|
||||
if (!folderNote) return;
|
||||
|
||||
this.app.workspace.iterateAllLeaves((leaf) => {
|
||||
if (!(leaf.view instanceof EditableFileView)) return;
|
||||
const file = leaf.view?.file;
|
||||
if (!file) return;
|
||||
if (file.path === folderNote.path) {
|
||||
leaf.tabHeaderInnerTitleEl.setText(folder.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
if (this.plugin.settings.folderNoteName === '{{folder_name}}') return false;
|
||||
return this.plugin.settings.tabManagerEnabled;
|
||||
}
|
||||
}
|
||||
81
src/events/handleClick.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { Keymap, Platform, type TFile } from 'obsidian';
|
||||
import type FolderNotesPlugin from 'src/main';
|
||||
import { openFolderNote, createFolderNote, getFolderNote } from 'src/functions/folderNoteFunctions';
|
||||
import { getExcludedFolder } from 'src/ExcludeFolders/functions/folderFunctions';
|
||||
import {
|
||||
addCSSClassToFileExplorerEl,
|
||||
removeCSSClassFromFileExplorerEL,
|
||||
} from 'src/functions/styleFunctions';
|
||||
|
||||
|
||||
|
||||
export async function handleViewHeaderClick(
|
||||
event: MouseEvent,
|
||||
plugin: FolderNotesPlugin,
|
||||
): Promise<void> {
|
||||
if (!plugin.settings.openFolderNoteOnClickInPath) return;
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!(event.target instanceof HTMLElement)) return;
|
||||
|
||||
const folderPath = event.target.getAttribute('data-path');
|
||||
if (!folderPath) { return; }
|
||||
|
||||
if (await isExcludedFolder(event, plugin, folderPath)) return;
|
||||
|
||||
const folderNote = getFolderNote(plugin, folderPath);
|
||||
if (folderNote) {
|
||||
await openFolderNote(plugin, folderNote, event).then(() =>
|
||||
handleFolderNoteReveal(plugin, folderNote),
|
||||
);
|
||||
return;
|
||||
} else if (event.altKey || Keymap.isModEvent(event) === 'tab') {
|
||||
if (await handleFolderNoteCreation(event, plugin, folderPath)) return;
|
||||
}
|
||||
(event.target).onclick = null;
|
||||
(event.target).click();
|
||||
}
|
||||
|
||||
async function isExcludedFolder(
|
||||
event: MouseEvent,
|
||||
plugin: FolderNotesPlugin,
|
||||
folderPath: string,
|
||||
): Promise<boolean> {
|
||||
const excludedFolder = getExcludedFolder(plugin, folderPath, true);
|
||||
if (excludedFolder?.disableFolderNote) {
|
||||
(event.target as HTMLElement).onclick = null;
|
||||
(event.target as HTMLElement).click();
|
||||
return true;
|
||||
} else if (excludedFolder?.enableCollapsing || plugin.settings.enableCollapsing) {
|
||||
(event.target as HTMLElement).onclick = null;
|
||||
(event.target as HTMLElement).click();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function handleFolderNoteReveal(plugin: FolderNotesPlugin, folderNote: TFile): Promise<void> {
|
||||
const fileExplorerPlugin = plugin.app.internalPlugins.getEnabledPluginById('file-explorer');
|
||||
if (fileExplorerPlugin && Platform.isMobile && plugin.settings.openSidebar.mobile) {
|
||||
const OPEN_SIDEBAR_DELAY = 200;
|
||||
window.setTimeout(() => { fileExplorerPlugin.revealInFolder(folderNote); }, OPEN_SIDEBAR_DELAY);
|
||||
} else if (fileExplorerPlugin && Platform.isDesktop && plugin.settings.openSidebar.desktop) {
|
||||
fileExplorerPlugin.revealInFolder(folderNote);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFolderNoteCreation(
|
||||
event: MouseEvent,
|
||||
plugin: FolderNotesPlugin,
|
||||
folderPath: string,
|
||||
): Promise<boolean> {
|
||||
const usedCtrl = Platform.isMacOS ? event.metaKey : event.ctrlKey;
|
||||
if ((plugin.settings.altKey && event.altKey) ||
|
||||
(usedCtrl && Keymap.isModEvent(event) === 'tab')) {
|
||||
await createFolderNote(plugin, folderPath, true, undefined, true);
|
||||
addCSSClassToFileExplorerEl(folderPath, 'has-folder-note', false, plugin);
|
||||
removeCSSClassFromFileExplorerEL(folderPath, 'has-not-folder-note', false, plugin);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
85
src/events/handleCreate.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { TFolder, TFile, type TAbstractFile } from 'obsidian';
|
||||
import type FolderNotesPlugin from 'src/main';
|
||||
import {
|
||||
createFolderNote,
|
||||
getFolder,
|
||||
getFolderNote,
|
||||
turnIntoFolderNote,
|
||||
} from 'src/functions/folderNoteFunctions';
|
||||
import { getExcludedFolder } from 'src/ExcludeFolders/functions/folderFunctions';
|
||||
import {
|
||||
removeCSSClassFromFileExplorerEL,
|
||||
addCSSClassToFileExplorerEl,
|
||||
} from 'src/functions/styleFunctions';
|
||||
import { isFileInAttachmentFolder } from '@app/functions/utils';
|
||||
|
||||
export async function handleCreate(file: TAbstractFile, plugin: FolderNotesPlugin): Promise<void> {
|
||||
if (!plugin.app.workspace.layoutReady) return;
|
||||
|
||||
const folder = file.parent;
|
||||
if (folder instanceof TFolder) {
|
||||
if (plugin.isEmptyFolderNoteFolder(folder) && getFolderNote(plugin, folder.path)) {
|
||||
addCSSClassToFileExplorerEl(folder.path, 'only-has-folder-note', true, plugin);
|
||||
} else {
|
||||
removeCSSClassFromFileExplorerEL(folder.path, 'only-has-folder-note', true, plugin);
|
||||
}
|
||||
}
|
||||
|
||||
if (file instanceof TFile) {
|
||||
handleFileCreation(file, plugin);
|
||||
} else if (file instanceof TFolder && plugin.settings.autoCreate) {
|
||||
handleFolderCreation(file, plugin);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileCreation(file: TFile, plugin: FolderNotesPlugin): Promise<void> {
|
||||
const folder = getFolder(plugin, file);
|
||||
|
||||
if (!(folder instanceof TFolder) && plugin.settings.autoCreateForFiles) {
|
||||
if (!file.parent) { return; }
|
||||
const newFolder = await plugin.app.fileManager.createNewFolder(file.parent);
|
||||
turnIntoFolderNote(plugin, file, newFolder);
|
||||
} else if (folder instanceof TFolder) {
|
||||
if (folder.children.length >= 1) {
|
||||
removeCSSClassFromFileExplorerEL(folder.path, 'fn-empty-folder', false, plugin);
|
||||
}
|
||||
|
||||
const detachedFolder = getExcludedFolder(plugin, folder.path, true);
|
||||
if (detachedFolder) { return; }
|
||||
const folderNote = getFolderNote(plugin, folder.path);
|
||||
|
||||
if (folderNote && folderNote.path === file.path) {
|
||||
addCSSClassToFileExplorerEl(folder.path, 'has-folder-note', false, plugin);
|
||||
addCSSClassToFileExplorerEl(file.path, 'is-folder-note', false, plugin);
|
||||
} else if (plugin.settings.autoCreateForFiles && !isFileInAttachmentFolder(plugin, file)) {
|
||||
if (!plugin.settings.supportedFileTypes.includes(file.extension)) { return; }
|
||||
if (!file.parent) { return; }
|
||||
const newFolder = await plugin.app.fileManager.createNewFolder(file.parent);
|
||||
turnIntoFolderNote(plugin, file, newFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFolderCreation(folder: TFolder, plugin: FolderNotesPlugin): Promise<void> {
|
||||
let openFile = plugin.settings.autoCreateFocusFiles;
|
||||
|
||||
const attachmentFolderPath = plugin.app.vault.getConfig('attachmentFolderPath') as string;
|
||||
const cleanAttachmentFolderPath = attachmentFolderPath?.replace('./', '') || '';
|
||||
const attachmentsAreInRootFolder = attachmentFolderPath === './' || attachmentFolderPath === '';
|
||||
addCSSClassToFileExplorerEl(folder.path, 'fn-empty-folder', false, plugin);
|
||||
|
||||
if (!plugin.settings.autoCreateForAttachmentFolder) {
|
||||
if (!attachmentsAreInRootFolder && cleanAttachmentFolderPath === folder.name) return;
|
||||
} else if (!attachmentsAreInRootFolder && cleanAttachmentFolderPath === folder.name) {
|
||||
openFile = false;
|
||||
}
|
||||
|
||||
const excludedFolder = getExcludedFolder(plugin, folder.path, true);
|
||||
if (excludedFolder?.disableAutoCreate) return;
|
||||
|
||||
const folderNote = getFolderNote(plugin, folder.path);
|
||||
if (folderNote) return;
|
||||
|
||||
void createFolderNote(plugin, folder.path, openFile, undefined, true);
|
||||
addCSSClassToFileExplorerEl(folder.path, 'has-folder-note', false, plugin);
|
||||
}
|
||||
39
src/events/handleDelete.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { TFolder, TFile, type TAbstractFile } from 'obsidian';
|
||||
import type FolderNotesPlugin from 'src/main';
|
||||
import { getFolderNote, getFolder, deleteFolderNote } from 'src/functions/folderNoteFunctions';
|
||||
import {
|
||||
removeCSSClassFromFileExplorerEL,
|
||||
addCSSClassToFileExplorerEl,
|
||||
hideFolderNoteInFileExplorer,
|
||||
} from 'src/functions/styleFunctions';
|
||||
import { getFolderPathFromString } from 'src/functions/utils';
|
||||
|
||||
export function handleDelete(file: TAbstractFile, plugin: FolderNotesPlugin): void {
|
||||
const folder = plugin.app.vault.getAbstractFileByPath(getFolderPathFromString(file.path));
|
||||
if (folder instanceof TFolder) {
|
||||
if (plugin.isEmptyFolderNoteFolder(folder) && getFolderNote(plugin, folder.path)) {
|
||||
addCSSClassToFileExplorerEl(folder.path, 'only-has-folder-note', true, plugin);
|
||||
} else {
|
||||
removeCSSClassFromFileExplorerEL(folder.path, 'only-has-folder-note', true, plugin);
|
||||
}
|
||||
}
|
||||
|
||||
if (file instanceof TFile) {
|
||||
const folderNoteFolder = getFolder(plugin, file);
|
||||
if (!folderNoteFolder) { return; }
|
||||
const folderNote = getFolderNote(plugin, folderNoteFolder.path);
|
||||
if (folderNote) { return; }
|
||||
removeCSSClassFromFileExplorerEL(folderNoteFolder.path, 'has-folder-note', false, plugin);
|
||||
removeCSSClassFromFileExplorerEL(
|
||||
folderNoteFolder.path, 'only-has-folder-note', true, plugin,
|
||||
);
|
||||
hideFolderNoteInFileExplorer(folderNoteFolder.path, plugin);
|
||||
}
|
||||
|
||||
if (!(file instanceof TFolder)) { return; }
|
||||
const folderNote = getFolderNote(plugin, file.path);
|
||||
if (!folderNote) { return; }
|
||||
removeCSSClassFromFileExplorerEL(folderNote.path, 'is-folder-note', false, plugin);
|
||||
if (!plugin.settings.syncDelete) { return; }
|
||||
deleteFolderNote(plugin, folderNote, false);
|
||||
}
|
||||
433
src/events/handleRename.ts
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
import { TFile, TFolder, Notice, type TAbstractFile } from 'obsidian';
|
||||
import type FolderNotesPlugin from 'src/main';
|
||||
import {
|
||||
extractFolderName, getFolderNote, getFolderNoteFolder,
|
||||
} from '../functions/folderNoteFunctions';
|
||||
import {
|
||||
getExcludedFolder, addExcludedFolder,
|
||||
updateExcludedFolder, deleteExcludedFolder, getDetachedFolder,
|
||||
} from '../ExcludeFolders/functions/folderFunctions';
|
||||
import { ExcludedFolder } from 'src/ExcludeFolders/ExcludeFolder';
|
||||
import {
|
||||
removeCSSClassFromFileExplorerEL, addCSSClassToFileExplorerEl,
|
||||
markFileAsFolderNote, unmarkFileAsFolderNote,
|
||||
unmarkFolderAsFolderNote, markFolderWithFolderNoteClasses,
|
||||
hideFolderNoteInFileExplorer, removeActiveFolder, setActiveFolder,
|
||||
} from 'src/functions/styleFunctions';
|
||||
import {
|
||||
getFolderPathFromString, removeExtension, getFileNameFromPathString,
|
||||
} from 'src/functions/utils';
|
||||
|
||||
export function handleRename(
|
||||
file: TAbstractFile,
|
||||
oldPath: string,
|
||||
plugin: FolderNotesPlugin,
|
||||
): void {
|
||||
let folder = file.parent;
|
||||
const oldFolder = plugin.app.vault.getAbstractFileByPath(getFolderPathFromString(oldPath));
|
||||
|
||||
if (folder instanceof TFolder) {
|
||||
if (plugin.isEmptyFolderNoteFolder(folder) && getFolderNote(plugin, folder.path)) {
|
||||
addCSSClassToFileExplorerEl(folder.path, 'only-has-folder-note', true, plugin);
|
||||
} else {
|
||||
removeCSSClassFromFileExplorerEL(folder.path, 'only-has-folder-note', true, plugin);
|
||||
}
|
||||
}
|
||||
|
||||
if (oldFolder instanceof TFolder) {
|
||||
if (plugin.isEmptyFolderNoteFolder(oldFolder) && getFolderNote(plugin, oldFolder.path)) {
|
||||
addCSSClassToFileExplorerEl(oldFolder.path, 'only-has-folder-note', true, plugin);
|
||||
} else {
|
||||
removeCSSClassFromFileExplorerEL(oldFolder.path, 'only-has-folder-note', true, plugin);
|
||||
}
|
||||
}
|
||||
|
||||
if (file instanceof TFolder) {
|
||||
folder = file;
|
||||
plugin.tabManager.updateTab(folder.path);
|
||||
updateExcludedFolderPath(folder, oldPath, plugin);
|
||||
if (isFolderRename(folder, oldPath)) {
|
||||
handleFolderRename(folder, oldPath, plugin);
|
||||
return;
|
||||
}
|
||||
return handleFolderMove(folder, oldPath, plugin);
|
||||
|
||||
} else if (file instanceof TFile) {
|
||||
if (isFileRename(file, oldPath)) {
|
||||
handleFileRename(file, oldPath, plugin);
|
||||
return;
|
||||
}
|
||||
handleFileMove(file, oldPath, plugin);
|
||||
return;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function isFileRename(file: TFile, oldPath: string): boolean {
|
||||
const oldFolderPath = getFolderPathFromString(oldPath);
|
||||
const newFolderPath = file.parent?.path;
|
||||
const oldName = getFileNameFromPathString(oldPath);
|
||||
const newName = file.name;
|
||||
|
||||
return oldFolderPath === newFolderPath && oldName !== newName;
|
||||
}
|
||||
|
||||
|
||||
function isFolderRename(folder: TFolder, oldPath: string): boolean {
|
||||
const oldName = getFileNameFromPathString(oldPath);
|
||||
const newName = folder.name;
|
||||
const oldParent = getFolderPathFromString(oldPath);
|
||||
const newParent = folder.parent?.path;
|
||||
|
||||
return oldParent === newParent && oldName !== newName;
|
||||
}
|
||||
|
||||
export function handleFolderMove(file: TFolder, oldPath: string, plugin: FolderNotesPlugin): void {
|
||||
if (plugin.settings.storageLocation === 'insideFolder') { return; }
|
||||
if (!plugin.settings.syncMove) { return; }
|
||||
const folderNote = getFolderNote(plugin, oldPath, plugin.settings.storageLocation);
|
||||
if (!(file instanceof TFolder) || !folderNote) return;
|
||||
const newFolder = plugin.app.vault.getAbstractFileByPath(file.path);
|
||||
if (!(newFolder instanceof TFolder)) return;
|
||||
let newPath = folderNote.path;
|
||||
|
||||
if (newFolder.path === '/') {
|
||||
newPath = folderNote.name;
|
||||
} else {
|
||||
newPath = `${newFolder.parent?.path}/${folderNote.name}`;
|
||||
}
|
||||
|
||||
plugin.app.fileManager.renameFile(folderNote, newPath);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
export async function handleFileMove(
|
||||
file: TFile,
|
||||
oldPath: string,
|
||||
plugin: FolderNotesPlugin,
|
||||
): Promise<void> {
|
||||
const { folderName, oldFileName, newFolder, excludedFolder, oldFolder, folderNote } = getArgs(
|
||||
plugin, file, oldPath,
|
||||
);
|
||||
|
||||
const isFolderNoteInNewFolder = folderName === newFolder?.name;
|
||||
const fileMovedFromOldFolderNote = oldFolder && oldFolder.name === oldFileName
|
||||
&& newFolder?.path !== oldFolder.path;
|
||||
|
||||
// this is for turning files into folder notes for folders that already have a folder note
|
||||
// e.g. Turn into folder note for "Folder name"
|
||||
const isFileWithExistingNote = folderName === newFolder?.name && folderNote;
|
||||
|
||||
if (isFileWithExistingNote) {
|
||||
renameExistingFolderNote(
|
||||
file, oldPath, plugin, excludedFolder, oldFolder,
|
||||
);
|
||||
} else if (isFolderNoteInNewFolder) {
|
||||
if (excludedFolder?.disableFolderNote) { return; }
|
||||
markFileAsFolderNote(file, plugin);
|
||||
if (newFolder instanceof TFolder) {
|
||||
markFolderWithFolderNoteClasses(newFolder, plugin);
|
||||
if (plugin.app.workspace.getActiveFile()?.path === file.path) {
|
||||
removeActiveFolder(plugin);
|
||||
setActiveFolder(newFolder.path, plugin);
|
||||
}
|
||||
}
|
||||
if (oldFolder instanceof TFolder) {
|
||||
hideFolderNoteInFileExplorer(oldFolder.path, plugin);
|
||||
unmarkFolderAsFolderNote(oldFolder, plugin);
|
||||
}
|
||||
} else if (fileMovedFromOldFolderNote) {
|
||||
unmarkFileAsFolderNote(file, plugin);
|
||||
if (oldFolder instanceof TFolder) {
|
||||
removeActiveFolder(plugin);
|
||||
hideFolderNoteInFileExplorer(oldFolder.path, plugin);
|
||||
unmarkFolderAsFolderNote(oldFolder, plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getArgs(plugin: FolderNotesPlugin, file: TFile, oldPath: string): {
|
||||
folderName: string;
|
||||
oldFileName: string;
|
||||
newFolder: TAbstractFile | null;
|
||||
excludedFolder: ExcludedFolder | undefined;
|
||||
oldFolder: TAbstractFile | null;
|
||||
folderNote: TFile | null | undefined;
|
||||
} {
|
||||
const folderName = extractFolderName(plugin.settings.folderNoteName, file.basename)
|
||||
|| file.basename;
|
||||
const oldFileName = removeExtension(getFileNameFromPathString(oldPath));
|
||||
const newFolder = getFolderNoteFolder(plugin, file, file.basename);
|
||||
let excludedFolder = getExcludedFolder(plugin, newFolder?.path || '', true);
|
||||
const oldFolder = getFolderNoteFolder(plugin, oldPath, oldFileName);
|
||||
const folderNote = getFolderNote(plugin, oldPath, plugin.settings.storageLocation, file);
|
||||
|
||||
return {
|
||||
folderName,
|
||||
oldFileName,
|
||||
newFolder,
|
||||
excludedFolder,
|
||||
oldFolder,
|
||||
folderNote,
|
||||
};
|
||||
}
|
||||
|
||||
function renameExistingFolderNote(
|
||||
file: TFile, oldPath: string, plugin: FolderNotesPlugin,
|
||||
excludedFolder: ExcludedFolder | undefined,
|
||||
oldFolder: TAbstractFile | null,
|
||||
): void {
|
||||
let excludedFolderExisted = true;
|
||||
let disabledSync = false;
|
||||
|
||||
if (!excludedFolder) {
|
||||
excludedFolderExisted = false;
|
||||
excludedFolder = new ExcludedFolder(
|
||||
oldFolder?.path || '',
|
||||
plugin.settings.excludeFolders.length,
|
||||
undefined,
|
||||
plugin,
|
||||
);
|
||||
addExcludedFolder(plugin, excludedFolder);
|
||||
} else if (!excludedFolder.disableSync) {
|
||||
disabledSync = false;
|
||||
excludedFolder.disableSync = true;
|
||||
updateExcludedFolder(plugin, excludedFolder, excludedFolder);
|
||||
}
|
||||
plugin.app.fileManager.renameFile(file, oldPath).then(() => {
|
||||
if (!excludedFolder) { return; }
|
||||
if (!excludedFolderExisted) {
|
||||
deleteExcludedFolder(plugin, excludedFolder);
|
||||
} else if (!disabledSync) {
|
||||
excludedFolder.disableSync = false;
|
||||
updateExcludedFolder(plugin, excludedFolder, excludedFolder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function handleFolderRename(
|
||||
file: TFolder, oldPath: string, plugin: FolderNotesPlugin,
|
||||
): Promise<void> {
|
||||
const fileName = plugin.settings.folderNoteName.replace('{{folder_name}}', file.name);
|
||||
const oldFileName = plugin.settings.folderNoteName
|
||||
.replace('{{folder_name}}', getFileNameFromPathString(oldPath));
|
||||
|
||||
if (fileName === oldFileName) { return; }
|
||||
|
||||
const folderNote = getFolderNote(plugin, oldPath);
|
||||
if (!(folderNote instanceof TFile)) return;
|
||||
|
||||
|
||||
const excludedFolder = getExcludedFolder(plugin, file.path, true);
|
||||
if (excludedFolder?.disableSync && !folderNote) {
|
||||
return removeCSSClassFromFileExplorerEL(file.path, 'has-folder-note', false, plugin);
|
||||
}
|
||||
|
||||
if (!plugin.settings.syncFolderName) { return; }
|
||||
|
||||
let newPath = '';
|
||||
if (plugin.settings.storageLocation === 'parentFolder') {
|
||||
const parentFolderPath = getFolderPathFromString(file.path);
|
||||
const oldParentFolderPath = getFolderPathFromString(oldPath);
|
||||
if (parentFolderPath !== oldParentFolderPath) {
|
||||
if (!plugin.settings.syncMove) { return; }
|
||||
newPath = `${parentFolderPath}/${fileName}.${folderNote.extension}`;
|
||||
} else if (parentFolderPath.trim() === '') {
|
||||
folderNote.path = `${folderNote.name}`;
|
||||
newPath = `${fileName}.${folderNote.extension}`;
|
||||
} else {
|
||||
folderNote.path = `${parentFolderPath}/${folderNote.name}`;
|
||||
newPath = `${parentFolderPath}/${fileName}.${folderNote.extension}`;
|
||||
}
|
||||
} else {
|
||||
folderNote.path = `${file.path}/${folderNote.name}`;
|
||||
newPath = `${file.path}/${fileName}.${folderNote.extension}`;
|
||||
}
|
||||
plugin.app.fileManager.renameFile(folderNote, newPath);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
export async function handleFileRename(
|
||||
file: TFile,
|
||||
oldPath: string,
|
||||
plugin: FolderNotesPlugin,
|
||||
): Promise<void> {
|
||||
const oldFileName = removeExtension(getFileNameFromPathString(oldPath));
|
||||
const newFileName = file.basename;
|
||||
if (oldFileName === newFileName) { return; }
|
||||
|
||||
const oldFolder = getFolderNoteFolder(plugin, oldPath, oldFileName);
|
||||
const folderName = extractFolderName(plugin.settings.folderNoteName, file.basename)
|
||||
|| file.basename;
|
||||
const oldFolderName = extractFolderName(plugin.settings.folderNoteName, oldFileName)
|
||||
|| oldFileName;
|
||||
const newFolder = getFolderNoteFolder(plugin, file, file.basename);
|
||||
const excludedFolder = getExcludedFolder(plugin, newFolder?.path || '', true);
|
||||
const detachedExcludedFolder = getDetachedFolder(plugin, newFolder?.path || '');
|
||||
const folderNote = getFolderNote(plugin, oldPath, plugin.settings.storageLocation, file);
|
||||
|
||||
// Handle folder note creation
|
||||
if (shouldCreateFolderNote(excludedFolder, folderName, newFolder, detachedExcludedFolder)) {
|
||||
if (newFolder) {
|
||||
handleFolderNoteCreation(file, newFolder, plugin);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle folder note removal
|
||||
if (shouldRemoveFolderNoteClasses(excludedFolder, folderName, newFolder)) {
|
||||
handleFolderNoteRemoval(file, newFolder, plugin);
|
||||
}
|
||||
|
||||
// Early return if sync is disabled
|
||||
if (excludedFolder?.disableSync || !plugin.settings.syncFolderName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle same folder rename
|
||||
if (folderName === newFolder?.name && newFolder) {
|
||||
handleSameFolderRename(file, newFolder, oldFolder, plugin);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle folder rename on file rename
|
||||
if (shouldRenameFolderOnFileRename(oldFolderName, oldFolder, newFolder, folderNote)) {
|
||||
return renameFolderOnFileRename(file, oldPath, oldFolder!, plugin);
|
||||
}
|
||||
}
|
||||
|
||||
async function renameFolderOnFileRename(
|
||||
file: TFile,
|
||||
oldPath: string,
|
||||
oldFolder: TAbstractFile,
|
||||
plugin: FolderNotesPlugin,
|
||||
): Promise<void> {
|
||||
const newFolderName = extractFolderName(plugin.settings.folderNoteName, file.basename);
|
||||
if (!newFolderName) {
|
||||
removeCSSClassFromFileExplorerEL(oldFolder.path, 'has-folder-note', false, plugin);
|
||||
removeCSSClassFromFileExplorerEL(file.path, 'is-folder-note', false, plugin);
|
||||
return;
|
||||
} else if (newFolderName === oldFolder.name) {
|
||||
addCSSClassToFileExplorerEl(oldFolder.path, 'has-folder-note', false, plugin);
|
||||
addCSSClassToFileExplorerEl(file.path, 'is-folder-note', false, plugin);
|
||||
return;
|
||||
}
|
||||
|
||||
let newFolderPath = '';
|
||||
if (plugin.settings.storageLocation === 'insideFolder') {
|
||||
if (oldFolder.parent?.path === '/') {
|
||||
newFolderPath = `${newFolderName}`;
|
||||
} else {
|
||||
newFolderPath = oldFolder.parent?.path + '/' + newFolderName;
|
||||
}
|
||||
} else {
|
||||
const parentFolderPath = getFolderPathFromString(file.path);
|
||||
if (parentFolderPath.trim() === '' || parentFolderPath.trim() === '/') {
|
||||
newFolderPath = `${newFolderName}`;
|
||||
} else {
|
||||
newFolderPath = `${parentFolderPath}/${newFolderName}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (plugin.app.vault.getAbstractFileByPath(newFolderPath)) {
|
||||
await plugin.app.fileManager.renameFile(file, oldPath);
|
||||
new Notice('A folder with the same name already exists');
|
||||
return;
|
||||
}
|
||||
plugin.app.fileManager.renameFile(oldFolder, newFolderPath);
|
||||
}
|
||||
|
||||
function updateExcludedFolderPath(
|
||||
folder: TFolder,
|
||||
oldPath: string,
|
||||
plugin: FolderNotesPlugin,
|
||||
): void {
|
||||
const excludedFolders = plugin.settings.excludeFolders.filter(
|
||||
(excludedFolder) => excludedFolder.path?.includes(oldPath),
|
||||
);
|
||||
|
||||
excludedFolders.forEach((excludedFolder) => {
|
||||
if (excludedFolder.path === oldPath) {
|
||||
excludedFolder.path = folder.path;
|
||||
return;
|
||||
}
|
||||
if (!excludedFolder.path) return;
|
||||
const folders = excludedFolder.path.split('/');
|
||||
if (folders.length < 1) {
|
||||
folders.push(excludedFolder.path);
|
||||
}
|
||||
|
||||
folders[folders.indexOf(folder.name)] = folder.name;
|
||||
excludedFolder.path = folders.join('/');
|
||||
});
|
||||
plugin.saveSettings();
|
||||
}
|
||||
|
||||
|
||||
function shouldCreateFolderNote(
|
||||
excludedFolder: ExcludedFolder | undefined,
|
||||
folderName: string,
|
||||
newFolder: TAbstractFile | null,
|
||||
detachedExcludedFolder: ExcludedFolder | undefined,
|
||||
): boolean {
|
||||
return !excludedFolder?.disableFolderNote
|
||||
&& folderName === (newFolder)?.name
|
||||
&& !detachedExcludedFolder;
|
||||
}
|
||||
|
||||
function shouldRemoveFolderNoteClasses(
|
||||
excludedFolder: ExcludedFolder | undefined,
|
||||
folderName: string,
|
||||
newFolder: TAbstractFile | null,
|
||||
): boolean {
|
||||
return excludedFolder?.disableFolderNote || (folderName !== (newFolder)?.name);
|
||||
}
|
||||
|
||||
function handleFolderNoteCreation(
|
||||
file: TFile,
|
||||
newFolder: TAbstractFile,
|
||||
plugin: FolderNotesPlugin,
|
||||
): void {
|
||||
addCSSClassToFileExplorerEl(file.path, 'is-folder-note', false, plugin);
|
||||
addCSSClassToFileExplorerEl(newFolder.path, 'has-folder-note', false, plugin);
|
||||
}
|
||||
|
||||
function handleFolderNoteRemoval(
|
||||
file: TFile,
|
||||
newFolder: TAbstractFile | null,
|
||||
plugin: FolderNotesPlugin,
|
||||
): void {
|
||||
removeCSSClassFromFileExplorerEL(file.path, 'is-folder-note', false, plugin);
|
||||
removeCSSClassFromFileExplorerEL(newFolder?.path || '', 'has-folder-note', false, plugin);
|
||||
}
|
||||
|
||||
function handleSameFolderRename(
|
||||
file: TFile,
|
||||
newFolder: TAbstractFile,
|
||||
oldFolder: TAbstractFile | null,
|
||||
plugin: FolderNotesPlugin,
|
||||
): void {
|
||||
addCSSClassToFileExplorerEl(file.path, 'is-folder-note', false, plugin);
|
||||
removeCSSClassFromFileExplorerEL(oldFolder?.path, 'has-folder-note', false, plugin);
|
||||
addCSSClassToFileExplorerEl(newFolder.path, 'has-folder-note', false, plugin);
|
||||
}
|
||||
|
||||
function shouldRenameFolderOnFileRename(
|
||||
oldFolderName: string,
|
||||
oldFolder: TAbstractFile | null,
|
||||
newFolder: TAbstractFile | null,
|
||||
folderNote: TFile | null | undefined,
|
||||
): boolean {
|
||||
if (!oldFolder) return false;
|
||||
|
||||
const oldFolderAsFolder = oldFolder;
|
||||
const newFolderAsFolder = newFolder;
|
||||
|
||||
return (oldFolderName === oldFolderAsFolder.name
|
||||
&& newFolderAsFolder?.path === oldFolderAsFolder.path)
|
||||
|| (folderNote !== null && oldFolderName === oldFolderAsFolder.name);
|
||||
}
|
||||
106
src/functions/ListComponent.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { CustomEventEmitter } from 'src/events/EventEmitter';
|
||||
|
||||
export class ListComponent {
|
||||
emitter: CustomEventEmitter;
|
||||
containerEl: HTMLElement;
|
||||
controlEl: HTMLElement;
|
||||
emptyStateEl!: HTMLElement;
|
||||
listEl: HTMLElement;
|
||||
values!: string[];
|
||||
defaultValues: string[];
|
||||
constructor(containerEl: HTMLElement, values: string[] = [], defaultValues: string[] = []) {
|
||||
this.emitter = new CustomEventEmitter();
|
||||
this.containerEl = containerEl;
|
||||
this.controlEl = containerEl.querySelector('.setting-item-control') || containerEl;
|
||||
this.listEl = this.controlEl.createDiv('setting-command-hotkeys');
|
||||
this.addResetButton();
|
||||
this.setValues(values);
|
||||
this.defaultValues = defaultValues;
|
||||
}
|
||||
|
||||
on(event: string, listener: (data?: unknown) => void): void {
|
||||
this.emitter.on(event, listener);
|
||||
}
|
||||
|
||||
off(event: string, listener: (data?: unknown) => void): void {
|
||||
this.emitter.off(event, listener);
|
||||
}
|
||||
|
||||
private emit(event: string, data?: unknown): void {
|
||||
this.emitter.emit(event, data);
|
||||
}
|
||||
|
||||
setValues(values: string[]): void {
|
||||
this.removeElements();
|
||||
this.values = values;
|
||||
if (values.length !== 0) {
|
||||
values.forEach((value) => {
|
||||
this.addElement(value);
|
||||
});
|
||||
}
|
||||
this.emit('update', this.values);
|
||||
}
|
||||
|
||||
removeElements(): void {
|
||||
this.listEl.empty();
|
||||
}
|
||||
|
||||
addElement(value: string): void {
|
||||
this.listEl.createSpan('setting-hotkey', (span) => {
|
||||
if (value.toLocaleLowerCase() === 'md') {
|
||||
const markdown = 'markdown';
|
||||
span.innerText = markdown;
|
||||
} else {
|
||||
span.innerText = value;
|
||||
}
|
||||
span.setAttribute('extension', value);
|
||||
|
||||
const removeSpan = span.createEl('span', { cls: 'ofn-list-item-remove setting-hotkey-icon' });
|
||||
// eslint-disable-next-line max-len
|
||||
const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
|
||||
const svgElement = removeSpan.createEl('span', { cls: 'ofn-list-item-remove-icon' });
|
||||
const parser = new DOMParser();
|
||||
const svgDoc = parser.parseFromString(svg, 'image/svg+xml');
|
||||
const svgNode = svgDoc.documentElement;
|
||||
if (svgNode) {
|
||||
svgElement.appendChild(activeDocument.importNode(svgNode, true));
|
||||
}
|
||||
removeSpan.onClickEvent(() => {
|
||||
this.removeValue(value);
|
||||
span.remove();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async addValue(value: string): Promise<void> {
|
||||
this.values.push(value);
|
||||
this.addElement(value);
|
||||
this.emit('add', value);
|
||||
this.emit('update', this.values);
|
||||
}
|
||||
|
||||
addResetButton(): this {
|
||||
// eslint-disable-next-line max-len
|
||||
const resetButton = this.controlEl.createEl('span', { cls: 'clickable-icon setting-restore-hotkey-button' });
|
||||
// eslint-disable-next-line max-len
|
||||
const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-rotate-ccw"><path d="M3 2v6h6"></path><path d="M3 13a9 9 0 1 0 3-7.7L3 8"></path></svg>';
|
||||
const svgElement = resetButton.createEl('span', { cls: 'ofn-list-item-remove-icon' });
|
||||
const parser = new DOMParser();
|
||||
const svgDoc = parser.parseFromString(svg, 'image/svg+xml');
|
||||
const svgNode = svgDoc.documentElement;
|
||||
if (svgNode) {
|
||||
svgElement.appendChild(activeDocument.importNode(svgNode, true));
|
||||
}
|
||||
resetButton.onClickEvent(() => {
|
||||
this.setValues(this.defaultValues);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
removeValue(value: string): void {
|
||||
this.values = this.values.filter((v) => v !== value);
|
||||
this.listEl.find(`[extension='${value}']`).remove();
|
||||
this.emit('remove', value);
|
||||
this.emit('update', this.values);
|
||||
}
|
||||
}
|
||||
46
src/functions/excalidraw.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import type { WorkspaceLeaf, App, TFile } from 'obsidian';
|
||||
|
||||
interface ExcalidrawPlugin {
|
||||
setExcalidrawView(leaf: WorkspaceLeaf): void;
|
||||
openDrawing(file: TFile): void;
|
||||
getBlankDrawing(): Promise<string>;
|
||||
}
|
||||
|
||||
export async function openExcalidrawView(
|
||||
app: App,
|
||||
file: TFile,
|
||||
): Promise<void> {
|
||||
const { excalidraw, excalidrawEnabled } = await getExcalidrawPlugin(app);
|
||||
if (excalidrawEnabled && excalidraw) {
|
||||
excalidraw.openDrawing(file);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDefaultTemplate(app: App): Promise<string> {
|
||||
const { excalidraw, excalidrawEnabled } = await getExcalidrawPlugin(app);
|
||||
if (excalidrawEnabled && excalidraw) {
|
||||
return excalidraw.getBlankDrawing();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export async function getExcalidrawPlugin(
|
||||
app: App,
|
||||
): Promise<{ excalidraw: ExcalidrawPlugin | null; excalidrawEnabled: boolean }> {
|
||||
const { plugins: pluginManager } = app as App & {
|
||||
plugins: {
|
||||
plugins: Record<string, unknown>;
|
||||
enabledPlugins: Set<string>;
|
||||
};
|
||||
};
|
||||
const excalidraw = (
|
||||
pluginManager.plugins[
|
||||
'obsidian-excalidraw-plugin'
|
||||
] as unknown as ExcalidrawPlugin | undefined
|
||||
);
|
||||
const excalidrawEnabled = pluginManager.enabledPlugins.has('obsidian-excalidraw-plugin');
|
||||
return {
|
||||
excalidraw: excalidraw ?? null,
|
||||
excalidrawEnabled,
|
||||
};
|
||||
}
|
||||
623
src/functions/folderNoteFunctions.ts
Normal file
|
|
@ -0,0 +1,623 @@
|
|||
/* eslint-disable complexity */
|
||||
/* eslint-disable max-len */
|
||||
import type FolderNotesPlugin from '../main';
|
||||
import ExistingFolderNoteModal from '../modals/ExistingNote';
|
||||
import { applyTemplate } from '../template';
|
||||
import {
|
||||
TFolder,
|
||||
TFile,
|
||||
Keymap,
|
||||
type TAbstractFile,
|
||||
type MarkdownView,
|
||||
type WorkspaceLeaf,
|
||||
} from 'obsidian';
|
||||
import DeleteConfirmationModal from '../modals/DeleteConfirmation';
|
||||
import {
|
||||
addExcludedFolder,
|
||||
deleteExcludedFolder,
|
||||
getDetachedFolder,
|
||||
getExcludedFolder,
|
||||
updateExcludedFolder,
|
||||
} from '../ExcludeFolders/functions/folderFunctions';
|
||||
import { ExcludedFolder } from '../ExcludeFolders/ExcludeFolder';
|
||||
import { getDefaultTemplate, openExcalidrawView } from './excalidraw';
|
||||
import { AskForExtensionModal } from 'src/modals/AskForExtension';
|
||||
import {
|
||||
addCSSClassToFileExplorerEl,
|
||||
removeCSSClassFromFileExplorerEL,
|
||||
removeActiveFolder,
|
||||
setActiveFolder,
|
||||
updateCSSClassesForFolderNote,
|
||||
} from 'src/functions/styleFunctions';
|
||||
import {
|
||||
getFolderNameFromPathString,
|
||||
getFolderPathFromString,
|
||||
removeExtension,
|
||||
} from 'src/functions/utils';
|
||||
|
||||
|
||||
|
||||
export async function createFolderNote(
|
||||
plugin: FolderNotesPlugin,
|
||||
folderPath: string,
|
||||
openFile: boolean,
|
||||
extension?: string,
|
||||
displayModal?: boolean,
|
||||
preexistingNote?: TFile,
|
||||
): Promise<void> {
|
||||
let {
|
||||
leaf,
|
||||
fileName,
|
||||
folderNote,
|
||||
folderNoteType,
|
||||
detachedFolder,
|
||||
path,
|
||||
} = getArgs(plugin, folderPath, extension, preexistingNote);
|
||||
|
||||
if (folderNoteType === '.excalidraw') {
|
||||
folderNoteType = '.md';
|
||||
extension = '.excalidraw';
|
||||
} else if (folderNoteType === '.ask') {
|
||||
if (plugin.askModalCurrentlyOpen) return;
|
||||
return new AskForExtensionModal(
|
||||
plugin,
|
||||
folderPath,
|
||||
openFile,
|
||||
folderNoteType,
|
||||
displayModal,
|
||||
preexistingNote,
|
||||
).open();
|
||||
}
|
||||
|
||||
if (plugin.settings.storageLocation === 'parentFolder') {
|
||||
const parentFolderPath = getFolderPathFromString(folderPath);
|
||||
if (parentFolderPath.trim() === '') {
|
||||
path = `${fileName}${folderNoteType}`;
|
||||
} else {
|
||||
path = `${parentFolderPath}/${fileName}${folderNoteType}`;
|
||||
}
|
||||
} else if (plugin.settings.storageLocation === 'vaultFolder') {
|
||||
path = `${fileName}${folderNoteType}`;
|
||||
} else {
|
||||
path = `${folderPath}/${fileName}${folderNoteType}`;
|
||||
}
|
||||
|
||||
if (detachedFolder && folderNote?.extension !== extension && folderNote) {
|
||||
await handleTurnNoteIntoFolderNote(plugin, folderNote, detachedFolder, folderPath, fileName);
|
||||
}
|
||||
|
||||
if (!extension) {
|
||||
extension = folderNoteType;
|
||||
}
|
||||
|
||||
if (!folderNote) {
|
||||
folderNote = await handleCreateFolderNote(plugin, folderNoteType, openFile, leaf, folderNote, path, extension);
|
||||
} else {
|
||||
await plugin.app.fileManager.renameFile(folderNote, path);
|
||||
}
|
||||
|
||||
if (openFile) {
|
||||
if (plugin.app.workspace.getActiveFile()?.path === path) {
|
||||
removeActiveFolder(plugin);
|
||||
|
||||
const folder = getFolder(plugin, folderNote);
|
||||
if (!folder) { return; }
|
||||
|
||||
setActiveFolder(folder.path, plugin);
|
||||
}
|
||||
await leaf.openFile(folderNote);
|
||||
if (plugin.settings.folderNoteType === '.excalidraw' || extension === '.excalidraw') {
|
||||
openExcalidrawView(plugin.app, folderNote);
|
||||
}
|
||||
}
|
||||
|
||||
const matchingExtension =
|
||||
extension?.split('.').pop() ===
|
||||
plugin.settings.templatePath.split('.').pop();
|
||||
if (folderNote && matchingExtension && plugin.settings.folderNoteType !== '.excalidraw') {
|
||||
applyTemplate(plugin, folderNote, leaf, plugin.settings.templatePath);
|
||||
}
|
||||
|
||||
const folder = plugin.app.vault.getAbstractFileByPath(folderPath);
|
||||
if (!(folder instanceof TFolder)) return;
|
||||
addCSSClassToFileExplorerEl(path, 'is-folder-note', false, plugin, true);
|
||||
addCSSClassToFileExplorerEl(folder.path, 'has-folder-note', false, plugin);
|
||||
}
|
||||
|
||||
function getArgs(
|
||||
plugin: FolderNotesPlugin,
|
||||
folderPath: string,
|
||||
extension?: string,
|
||||
preexistingNote?: TFile,
|
||||
): {
|
||||
leaf: WorkspaceLeaf;
|
||||
fileName: string;
|
||||
folderNote: TFile | null | undefined;
|
||||
folderNoteType: string;
|
||||
detachedFolder: ExcludedFolder | undefined;
|
||||
path: string | '';
|
||||
} {
|
||||
const leaf = plugin.app.workspace.getLeaf(false);
|
||||
const folderName = getFolderNameFromPathString(folderPath);
|
||||
const fileName = plugin.settings.folderNoteName.replace('{{folder_name}}', folderName);
|
||||
let folderNote = getFolderNote(plugin, folderPath);
|
||||
if (preexistingNote) {
|
||||
folderNote = preexistingNote;
|
||||
}
|
||||
let folderNoteType = extension ?? plugin.settings.folderNoteType;
|
||||
const detachedFolder = getDetachedFolder(plugin, folderPath);
|
||||
let path = '';
|
||||
|
||||
return {
|
||||
leaf,
|
||||
fileName,
|
||||
folderNote,
|
||||
folderNoteType,
|
||||
detachedFolder,
|
||||
path,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleCreateFolderNote(plugin: FolderNotesPlugin, folderNoteType: string, openFile: boolean, leaf: WorkspaceLeaf, folderNote: TFile | null | undefined, path: string, extension?: string): Promise<TFile> {
|
||||
let content = '';
|
||||
if (extension !== '.md' && extension) {
|
||||
if (
|
||||
plugin.settings.templatePath &&
|
||||
folderNoteType.split('.').pop() ===
|
||||
plugin.settings.templatePath.split('.').pop()
|
||||
) {
|
||||
const templateFile = plugin.app.vault.getAbstractFileByPath(
|
||||
plugin.settings.templatePath,
|
||||
);
|
||||
if (templateFile instanceof TFile) {
|
||||
if (['md', 'canvas', 'txt'].includes(templateFile.extension)) {
|
||||
content = await plugin.app.vault.read(templateFile);
|
||||
if (
|
||||
extension === '.excalidraw' &&
|
||||
!content.includes(
|
||||
'==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this activeDocument. ⚠==',
|
||||
)
|
||||
) {
|
||||
content = await getDefaultTemplate(plugin.app);
|
||||
}
|
||||
} else {
|
||||
plugin.app.vault.readBinary(templateFile).then(async (data) => {
|
||||
folderNote = await plugin.app.vault.createBinary(path, data);
|
||||
if (openFile) {
|
||||
await leaf.openFile(folderNote);
|
||||
}
|
||||
return folderNote;
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
plugin.settings.folderNoteType === '.excalidraw' ||
|
||||
extension === '.excalidraw'
|
||||
) {
|
||||
content = await getDefaultTemplate(plugin.app);
|
||||
} else if (plugin.settings.folderNoteType === '.canvas') {
|
||||
content = '{}';
|
||||
}
|
||||
}
|
||||
|
||||
folderNote = await plugin.app.vault.create(path, content);
|
||||
return folderNote;
|
||||
}
|
||||
|
||||
async function handleTurnNoteIntoFolderNote(
|
||||
plugin: FolderNotesPlugin,
|
||||
folderNote: TFile,
|
||||
detachedFolder: ExcludedFolder,
|
||||
folderPath: string,
|
||||
fileName: string,
|
||||
): Promise<void> {
|
||||
deleteExcludedFolder(plugin, detachedFolder);
|
||||
removeCSSClassFromFileExplorerEL(folderNote?.path, 'is-folder-note', false, plugin);
|
||||
const folder = plugin.app.vault.getAbstractFileByPath(folderPath);
|
||||
if (!(folder instanceof TFolder)) return;
|
||||
if (!folderNote || folderNote.basename !== fileName) return;
|
||||
let count = 1;
|
||||
const baseName = removeExtension(folderNote.path);
|
||||
const ext = folderNote.path.split('.').pop();
|
||||
let newName = `${baseName} (${count}).${ext}`;
|
||||
const MAX_FOLDER_NOTE_RENAME_ATTEMPTS = 100;
|
||||
|
||||
while (
|
||||
count < MAX_FOLDER_NOTE_RENAME_ATTEMPTS &&
|
||||
plugin.app.vault.getAbstractFileByPath(newName)
|
||||
) {
|
||||
count++;
|
||||
newName = `${baseName} (${count}).${ext}`;
|
||||
}
|
||||
const [
|
||||
excludedFolder,
|
||||
excludedFolderExisted,
|
||||
disabledSync,
|
||||
] = await tempDisableSync(plugin, folder);
|
||||
|
||||
await plugin.app.fileManager.renameFile(folderNote, newName).then(() => {
|
||||
if (!excludedFolder) return;
|
||||
if (!excludedFolderExisted) {
|
||||
deleteExcludedFolder(plugin, excludedFolder);
|
||||
} else if (!disabledSync) {
|
||||
excludedFolder.disableSync = false;
|
||||
updateExcludedFolder(plugin, excludedFolder, excludedFolder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function turnIntoFolderNote(
|
||||
plugin: FolderNotesPlugin,
|
||||
file: TFile,
|
||||
folder: TFolder,
|
||||
folderNote?: TFile | null | TAbstractFile,
|
||||
skipConfirmation?: boolean,
|
||||
): Promise<void> {
|
||||
const { extension } = file;
|
||||
const detachedExcludedFolder = getDetachedFolder(plugin, folder.path);
|
||||
|
||||
if (folderNote) {
|
||||
if (
|
||||
plugin.settings.showRenameConfirmation &&
|
||||
!skipConfirmation &&
|
||||
!detachedExcludedFolder
|
||||
) {
|
||||
return new ExistingFolderNoteModal(plugin.app, plugin, file, folder, folderNote).open();
|
||||
}
|
||||
removeCSSClassFromFileExplorerEL(folderNote.path, 'is-folder-note', false, plugin);
|
||||
|
||||
const [
|
||||
excludedFolder,
|
||||
excludedFolderExisted,
|
||||
disabledSync,
|
||||
] = await tempDisableSync(plugin, folder);
|
||||
|
||||
const CTIME_SLICE_START = 10;
|
||||
const RANDOM_SUFFIX_MAX = 1000;
|
||||
const randomSuffix = Math.floor(Math.random() * RANDOM_SUFFIX_MAX);
|
||||
const ctimeSuffix = file.stat.ctime.toString().slice(CTIME_SLICE_START);
|
||||
const newPath = `${folder.path}/${folder.name} (${ctimeSuffix}${randomSuffix}).${extension}`;
|
||||
|
||||
plugin.app.fileManager.renameFile(folderNote, newPath).then(() => {
|
||||
if (!excludedFolder) return;
|
||||
if (!excludedFolderExisted) {
|
||||
deleteExcludedFolder(plugin, excludedFolder);
|
||||
} else if (!disabledSync) {
|
||||
excludedFolder.disableSync = false;
|
||||
updateExcludedFolder(plugin, excludedFolder, excludedFolder);
|
||||
}
|
||||
});
|
||||
}
|
||||
const folderName = folder.name;
|
||||
const fileName = plugin.settings.folderNoteName.replace('{{folder_name}}', folderName);
|
||||
|
||||
let path = `${folder.path}/${fileName}.${extension}`;
|
||||
if (plugin.settings.storageLocation === 'parentFolder') {
|
||||
const parentFolderPath = folder.parent?.path;
|
||||
if (!parentFolderPath) return;
|
||||
if (parentFolderPath.trim() === '') {
|
||||
path = `${fileName}.${extension}`;
|
||||
} else {
|
||||
path = `${parentFolderPath}/${fileName}.${extension}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (detachedExcludedFolder) {
|
||||
void deleteExcludedFolder(plugin, detachedExcludedFolder);
|
||||
}
|
||||
|
||||
await plugin.app.fileManager.renameFile(file, path);
|
||||
void addCSSClassToFileExplorerEl(path, 'is-folder-note', false, plugin, true);
|
||||
void addCSSClassToFileExplorerEl(folder.path, 'has-folder-note', false, plugin);
|
||||
|
||||
removeActiveFolder(plugin);
|
||||
setActiveFolder(folder.path, plugin);
|
||||
}
|
||||
|
||||
export async function tempDisableSync(
|
||||
plugin: FolderNotesPlugin,
|
||||
folder: TFolder,
|
||||
): Promise<
|
||||
[
|
||||
excludedFolder: ExcludedFolder | undefined,
|
||||
excludedFolderExisted: boolean,
|
||||
disabledSync: boolean
|
||||
]
|
||||
> {
|
||||
let excludedFolder = getExcludedFolder(plugin, folder.path, false);
|
||||
let excludedFolderExisted = true;
|
||||
let disabledSync = false;
|
||||
|
||||
if (!excludedFolder) {
|
||||
excludedFolderExisted = false;
|
||||
excludedFolder = new ExcludedFolder(
|
||||
folder.path,
|
||||
plugin.settings.excludeFolders.length,
|
||||
undefined,
|
||||
plugin,
|
||||
);
|
||||
excludedFolder.disableSync = true;
|
||||
addExcludedFolder(plugin, excludedFolder);
|
||||
} else if (!excludedFolder.disableSync) {
|
||||
disabledSync = false;
|
||||
excludedFolder.disableSync = true;
|
||||
updateExcludedFolder(plugin, excludedFolder, excludedFolder);
|
||||
}
|
||||
|
||||
return [excludedFolder, excludedFolderExisted, disabledSync];
|
||||
}
|
||||
|
||||
export async function openFolderNote(
|
||||
plugin: FolderNotesPlugin,
|
||||
file: TAbstractFile,
|
||||
evt?: MouseEvent,
|
||||
): Promise<void> {
|
||||
const { path } = file;
|
||||
const focusExistingTab = plugin.settings.focusExistingTab && plugin.settings.openInNewTab;
|
||||
const activeFilePath = plugin.app.workspace.getActiveFile()?.path;
|
||||
|
||||
// If already active and not opening in new tab, do nothing
|
||||
if (activeFilePath === path && !(Keymap.isModEvent(evt) === 'tab')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to find an existing tab with this file open
|
||||
let foundLeaf = null;
|
||||
if (focusExistingTab && file instanceof TFile) {
|
||||
plugin.app.workspace.iterateAllLeaves((leaf) => {
|
||||
if (
|
||||
leaf.getViewState().type === 'markdown' &&
|
||||
(leaf.view as MarkdownView).file?.path === path
|
||||
) {
|
||||
foundLeaf = leaf;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (foundLeaf) {
|
||||
plugin.app.workspace.setActiveLeaf(foundLeaf, { focus: true });
|
||||
} else {
|
||||
const shouldOpenInNewTab = Keymap.isModEvent(evt) || plugin.settings.openInNewTab;
|
||||
const leaf = plugin.app.workspace.getLeaf(shouldOpenInNewTab);
|
||||
if (file instanceof TFile) {
|
||||
await leaf.openFile(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteFolderNote(
|
||||
plugin: FolderNotesPlugin,
|
||||
file: TFile,
|
||||
displayModal: boolean,
|
||||
): Promise<void> {
|
||||
if (plugin.settings.showDeleteConfirmation && displayModal) {
|
||||
return new DeleteConfirmationModal(plugin.app, plugin, file).open();
|
||||
}
|
||||
const folder = getFolder(plugin, file);
|
||||
if (!folder) return;
|
||||
|
||||
plugin.settings.excludeFolders = plugin.settings.excludeFolders.filter(
|
||||
(excludedFolder) => (excludedFolder.path !== folder.path) || !excludedFolder.showFolderNote);
|
||||
plugin.saveSettings(false);
|
||||
|
||||
removeCSSClassFromFileExplorerEL(folder.path, 'has-folder-note', false, plugin);
|
||||
switch (plugin.settings.deleteFilesAction) {
|
||||
case 'trash':
|
||||
await plugin.app.vault.trash(file, true);
|
||||
break;
|
||||
case 'obsidianTrash':
|
||||
await plugin.app.vault.trash(file, false);
|
||||
break;
|
||||
case 'delete':
|
||||
await plugin.app.vault.delete(file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractFolderName(template: string, changedFileName: string): string | null {
|
||||
const [prefix, suffix] = template.split('{{folder_name}}');
|
||||
if (prefix.trim() === '' && suffix.trim() === '') {
|
||||
return changedFileName;
|
||||
}
|
||||
if (!changedFileName.startsWith(prefix) || !changedFileName.endsWith(suffix)) {
|
||||
return null;
|
||||
}
|
||||
if (changedFileName.startsWith(prefix) && prefix.trim() !== '') {
|
||||
return changedFileName.slice(prefix.length).replace(suffix, '');
|
||||
} else if (changedFileName.endsWith(suffix) && suffix.trim() !== '') {
|
||||
return changedFileName.slice(0, -suffix.length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findFolderNoteFile(
|
||||
plugin: FolderNotesPlugin,
|
||||
path: string,
|
||||
primaryType: string,
|
||||
): TFile | null {
|
||||
let folderNote = plugin.app.vault.getAbstractFileByPath(path + primaryType);
|
||||
if (
|
||||
folderNote instanceof TFile &&
|
||||
plugin.settings.supportedFileTypes.includes(primaryType.replace('.', ''))
|
||||
) {
|
||||
return folderNote;
|
||||
}
|
||||
|
||||
const supportedFileTypes = plugin.settings.supportedFileTypes.filter(
|
||||
(type) => type !== primaryType.replace('.', ''),
|
||||
);
|
||||
|
||||
for (let type of supportedFileTypes) {
|
||||
if (type === 'excalidraw' || type === '.excalidraw') {
|
||||
type = '.md';
|
||||
}
|
||||
if (!type.startsWith('.')) {
|
||||
type = '.' + type;
|
||||
}
|
||||
folderNote = plugin.app.vault.getAbstractFileByPath(path + type);
|
||||
if (folderNote instanceof TFile) {
|
||||
return folderNote;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getFolderNote(
|
||||
plugin: FolderNotesPlugin,
|
||||
folderPath: string,
|
||||
storageLocation?: string,
|
||||
file?: TFile,
|
||||
oldFolderNoteName?: string,
|
||||
): TFile | null | undefined {
|
||||
const folder = getFolderInfo(folderPath);
|
||||
if (!folder) return null;
|
||||
|
||||
let fileName = resolveFileName(plugin, folder, file, oldFolderNoteName);
|
||||
if (!fileName) return null;
|
||||
|
||||
adjustFolderPathForStorage(folder, folderPath, plugin, storageLocation);
|
||||
|
||||
const path = buildFullPath(folder, fileName);
|
||||
const primaryType = normalizeFolderNoteType(plugin.settings.folderNoteType);
|
||||
|
||||
return findFolderNoteFile(plugin, path, primaryType);
|
||||
}
|
||||
|
||||
|
||||
export function detachFolderNote(plugin: FolderNotesPlugin, file: TFile): void {
|
||||
const folder = getFolder(plugin, file);
|
||||
if (!folder) return;
|
||||
const excludedFolder = new ExcludedFolder(
|
||||
folder.path,
|
||||
plugin.settings.excludeFolders.length,
|
||||
undefined,
|
||||
plugin,
|
||||
);
|
||||
excludedFolder.showFolderNote = true;
|
||||
excludedFolder.hideInSettings = true;
|
||||
excludedFolder.disableFolderNote = true;
|
||||
excludedFolder.disableSync = true;
|
||||
excludedFolder.subFolders = false;
|
||||
excludedFolder.excludeFromFolderOverview = false;
|
||||
excludedFolder.detached = true;
|
||||
excludedFolder.detachedFilePath = file.path;
|
||||
addExcludedFolder(plugin, excludedFolder);
|
||||
void updateCSSClassesForFolderNote(file.path, plugin);
|
||||
}
|
||||
|
||||
|
||||
export function getFolder(
|
||||
plugin: FolderNotesPlugin,
|
||||
file: TFile,
|
||||
storageLocation?: string,
|
||||
): TFolder | TAbstractFile | null {
|
||||
if (!file) return null;
|
||||
let folderName = extractFolderName(plugin.settings.folderNoteName, file.basename);
|
||||
if (
|
||||
plugin.settings.folderNoteName === file.basename &&
|
||||
plugin.settings.storageLocation === 'insideFolder'
|
||||
) {
|
||||
folderName = file.parent?.name ?? '';
|
||||
}
|
||||
if (!folderName) return null;
|
||||
let folderPath = getFolderPathFromString(file.path);
|
||||
let folder: TFolder | TAbstractFile | null = null;
|
||||
|
||||
if (
|
||||
(plugin.settings.storageLocation === 'parentFolder' ||
|
||||
storageLocation === 'parentFolder') &&
|
||||
storageLocation !== 'insideFolder'
|
||||
) {
|
||||
if (folderPath.trim() === '' || folderPath === '/') {
|
||||
folderPath = folderName;
|
||||
} else {
|
||||
folderPath = `${folderPath}/${folderName}`;
|
||||
}
|
||||
folder = plugin.app.vault.getAbstractFileByPath(folderPath);
|
||||
} else {
|
||||
folder = plugin.app.vault.getAbstractFileByPath(folderPath);
|
||||
}
|
||||
|
||||
if (!folder) { return null; }
|
||||
return folder;
|
||||
}
|
||||
|
||||
export function getFolderNoteFolder(
|
||||
plugin: FolderNotesPlugin,
|
||||
folderNote: TFile | string,
|
||||
fileName: string,
|
||||
): TFolder | TAbstractFile | null {
|
||||
if (!folderNote) return null;
|
||||
let filePath = '';
|
||||
if (typeof folderNote === 'string') {
|
||||
filePath = folderNote;
|
||||
} else {
|
||||
fileName = folderNote.basename;
|
||||
filePath = folderNote.path;
|
||||
}
|
||||
const folderName = extractFolderName(plugin.settings.folderNoteName, fileName);
|
||||
if (!plugin.settings.folderNoteName.includes('{{folder_name}}') && plugin.settings.storageLocation === 'insideFolder') {
|
||||
if (folderNote instanceof TFile) {
|
||||
return folderNote.parent;
|
||||
}
|
||||
const file = plugin.app.vault.getAbstractFileByPath(filePath);
|
||||
return file instanceof TFile ? file.parent : null;
|
||||
}
|
||||
if (!folderName) return null;
|
||||
let folderPath = getFolderPathFromString(filePath);
|
||||
if (plugin.settings.storageLocation === 'parentFolder') {
|
||||
if (folderPath.trim() === '') {
|
||||
folderPath = folderName;
|
||||
} else {
|
||||
folderPath = `${folderPath}/${folderName}`;
|
||||
}
|
||||
} else {
|
||||
folderPath = getFolderPathFromString(filePath);
|
||||
}
|
||||
const folder = plugin.app.vault.getAbstractFileByPath(folderPath);
|
||||
if (!folder) { return null; }
|
||||
return folder;
|
||||
}
|
||||
|
||||
function getFolderInfo(folderPath: string): { path: string; name: string } | null {
|
||||
if (!folderPath) return null;
|
||||
return {
|
||||
path: folderPath,
|
||||
name: getFolderNameFromPathString(folderPath),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveFileName(
|
||||
plugin: FolderNotesPlugin,
|
||||
folder: { path: string; name: string },
|
||||
file?: TFile,
|
||||
oldFolderNoteName?: string,
|
||||
): string | null {
|
||||
const templateName = oldFolderNoteName ?? plugin.settings.folderNoteName;
|
||||
if (!templateName) return null;
|
||||
const nameSource = file ? file.basename : folder.name;
|
||||
return templateName.replace('{{folder_name}}', nameSource);
|
||||
}
|
||||
|
||||
function adjustFolderPathForStorage(
|
||||
folder: { path: string; name: string },
|
||||
folderPath: string,
|
||||
plugin: FolderNotesPlugin,
|
||||
storageLocation?: string,
|
||||
): void {
|
||||
if (
|
||||
(plugin.settings.storageLocation === 'parentFolder' ||
|
||||
storageLocation === 'parentFolder') &&
|
||||
storageLocation !== 'insideFolder'
|
||||
) {
|
||||
folder.path = getFolderPathFromString(folderPath);
|
||||
}
|
||||
}
|
||||
|
||||
function buildFullPath(folder: { path: string }, fileName: string): string {
|
||||
return folder.path === '/' ? fileName : `${folder.path}/${fileName}`;
|
||||
}
|
||||
|
||||
function normalizeFolderNoteType(type: string): string {
|
||||
return type === '.excalidraw' ? '.md' : type;
|
||||
}
|
||||
253
src/functions/styleFunctions.ts
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
import { TFile, TFolder } from 'obsidian';
|
||||
import type FolderNotesPlugin from '../main';
|
||||
import {
|
||||
getDetachedFolder,
|
||||
getExcludedFolder,
|
||||
addExcludedFolder,
|
||||
} from 'src/ExcludeFolders/functions/folderFunctions';
|
||||
import { getFolder, getFolderNote } from 'src/functions/folderNoteFunctions';
|
||||
import { getFileExplorer } from './utils';
|
||||
import { ExcludedFolder } from 'src/ExcludeFolders/ExcludeFolder';
|
||||
import type FolderOverviewPlugin from 'src/obsidian-folder-overview/src/main';
|
||||
|
||||
/**
|
||||
* @description Refreshes the CSS classes for all folder notes in the file explorer.
|
||||
*/
|
||||
export function refreshAllFolderStyles(forceReload = false, plugin: FolderNotesPlugin): void {
|
||||
const fileExplorer = getFileExplorer(plugin);
|
||||
if (!fileExplorer) { return; }
|
||||
if (plugin.activeFileExplorer === fileExplorer && !forceReload) { return; }
|
||||
plugin.activeFileExplorer = fileExplorer;
|
||||
plugin.app.vault.getAllLoadedFiles().forEach(async (file) => {
|
||||
if (file instanceof TFolder) {
|
||||
await updateCSSClassesForFolder(file.path, plugin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Updates the CSS classes for a specific folder in the file explorer.
|
||||
*/
|
||||
// eslint-disable-next-line complexity
|
||||
export async function updateCSSClassesForFolder(
|
||||
folderPath: string,
|
||||
plugin: FolderNotesPlugin,
|
||||
): Promise<void> {
|
||||
const folder = plugin.app.vault.getAbstractFileByPath(folderPath);
|
||||
if (!folder || !(folder instanceof TFolder)) { return; }
|
||||
|
||||
const folderNote = getFolderNote(plugin, folder.path);
|
||||
const detachedFolderNote = getDetachedFolder(plugin, folder.path);
|
||||
|
||||
if (folder.children.length === 0) {
|
||||
addCSSClassToFileExplorerEl(folder.path, 'fn-empty-folder', false, plugin);
|
||||
}
|
||||
|
||||
if (!folderNote || detachedFolderNote) {
|
||||
removeCSSClassFromFileExplorerEL(folder?.path, 'has-folder-note', false, plugin);
|
||||
removeCSSClassFromFileExplorerEL(folder?.path, 'only-has-folder-note', true, plugin);
|
||||
removeCSSClassFromFileExplorerEL(folderNote?.path, 'is-folder-note', false, plugin);
|
||||
return;
|
||||
}
|
||||
|
||||
const excludedFolder = getExcludedFolder(plugin, folder.path, true);
|
||||
if (excludedFolder?.disableFolderNote) {
|
||||
removeCSSClassFromFileExplorerEL(folderNote.path, 'is-folder-note', false, plugin);
|
||||
removeCSSClassFromFileExplorerEL(folder.path, 'has-folder-note', false, plugin);
|
||||
removeCSSClassFromFileExplorerEL(folder?.path, 'only-has-folder-note', true, plugin);
|
||||
} else {
|
||||
markFolderWithFolderNoteClasses(folder, plugin);
|
||||
if (excludedFolder?.showFolderNote) {
|
||||
addCSSClassToFileExplorerEl(folder.path, 'show-folder-note-in-explorer', true, plugin);
|
||||
unmarkFileAsFolderNote(folderNote, plugin);
|
||||
return;
|
||||
}
|
||||
if (plugin.isEmptyFolderNoteFolder(folder) && getFolderNote(plugin, folder.path)) {
|
||||
addCSSClassToFileExplorerEl(folder.path, 'only-has-folder-note', true, plugin);
|
||||
} else {
|
||||
removeCSSClassFromFileExplorerEL(folder.path, 'only-has-folder-note', true, plugin);
|
||||
}
|
||||
}
|
||||
|
||||
markFolderAndNoteWithClasses(folderNote, folder, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Updates the CSS classes for a folder note file in the file explorer and then also updates the folder it belongs to.
|
||||
*/
|
||||
export async function updateCSSClassesForFolderNote(
|
||||
filePath: string,
|
||||
plugin: FolderNotesPlugin,
|
||||
): Promise<void> {
|
||||
const file = plugin.app.vault.getAbstractFileByPath(filePath);
|
||||
if (!file || !(file instanceof TFile)) { return; }
|
||||
|
||||
const folder = getFolder(plugin, file);
|
||||
if (!folder || !(folder instanceof TFolder)) { return; }
|
||||
|
||||
updateCSSClassesForFolder(folder.path, plugin);
|
||||
}
|
||||
|
||||
export function markFolderAndNoteWithClasses(
|
||||
file: TFile,
|
||||
folder: TFolder,
|
||||
plugin: FolderNotesPlugin,
|
||||
): void {
|
||||
markFileAsFolderNote(file, plugin);
|
||||
markFolderWithFolderNoteClasses(folder, plugin);
|
||||
}
|
||||
|
||||
export function clearFolderAndNoteClasses(
|
||||
folder: TFolder,
|
||||
file: TFile,
|
||||
plugin: FolderNotesPlugin,
|
||||
): void {
|
||||
unmarkFileAsFolderNote(file, plugin);
|
||||
clearFolderNoteClassesFromFolder(folder, plugin);
|
||||
}
|
||||
|
||||
export function markFolderWithFolderNoteClasses(folder: TFolder, plugin: FolderNotesPlugin): void {
|
||||
addCSSClassToFileExplorerEl(folder.path, 'has-folder-note', false, plugin);
|
||||
if (plugin.isEmptyFolderNoteFolder(folder) && getFolderNote(plugin, folder.path)) {
|
||||
addCSSClassToFileExplorerEl(folder.path, 'only-has-folder-note', true, plugin);
|
||||
} else {
|
||||
removeCSSClassFromFileExplorerEL(folder.path, 'only-has-folder-note', true, plugin);
|
||||
}
|
||||
}
|
||||
|
||||
export function markFileAsFolderNote(file: TFile, plugin: FolderNotesPlugin): void {
|
||||
addCSSClassToFileExplorerEl(file.path, 'is-folder-note', false, plugin);
|
||||
}
|
||||
|
||||
export function unmarkFileAsFolderNote(file: TFile, plugin: FolderNotesPlugin): void {
|
||||
removeCSSClassFromFileExplorerEL(file.path, 'is-folder-note', false, plugin);
|
||||
}
|
||||
|
||||
export function unmarkFolderAsFolderNote(folder: TFolder, plugin: FolderNotesPlugin): void {
|
||||
removeCSSClassFromFileExplorerEL(folder.path, 'has-folder-note', false, plugin);
|
||||
removeCSSClassFromFileExplorerEL(folder.path, 'only-has-folder-note', true, plugin);
|
||||
}
|
||||
|
||||
export function clearFolderNoteClassesFromFolder(folder: TFolder, plugin: FolderNotesPlugin): void {
|
||||
removeCSSClassFromFileExplorerEL(folder.path, 'has-folder-note', false, plugin);
|
||||
removeCSSClassFromFileExplorerEL(folder.path, 'only-has-folder-note', true, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Can be a folder or file path
|
||||
* @returns nothing
|
||||
*/
|
||||
export async function addCSSClassToFileExplorerEl(
|
||||
path: string,
|
||||
cssClass: string,
|
||||
parent = false,
|
||||
plugin: FolderNotesPlugin,
|
||||
waitForCreate = false,
|
||||
count = 0,
|
||||
): Promise<void> {
|
||||
const fileExplorerItem = getFileExplorerElement(path, plugin);
|
||||
const MAX_RETRIES = 5;
|
||||
const RETRY_DELAY = 500;
|
||||
|
||||
if (!fileExplorerItem) {
|
||||
if (waitForCreate && count < MAX_RETRIES) {
|
||||
await new Promise((r) => window.setTimeout(r, RETRY_DELAY));
|
||||
addCSSClassToFileExplorerEl(path, cssClass, parent, plugin, waitForCreate, count + 1);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (parent) {
|
||||
const parentElement = fileExplorerItem?.parentElement;
|
||||
if (parentElement) {
|
||||
parentElement.addClass(cssClass);
|
||||
}
|
||||
} else {
|
||||
fileExplorerItem.addClass(cssClass);
|
||||
activeDocument.querySelectorAll(`[data-path='${CSS.escape(path)}']`).forEach((item) => {
|
||||
item.addClass(cssClass);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Can be a folder or file path
|
||||
* @param cssClass The CSS class to remove from the file explorer element
|
||||
* @returns nothing
|
||||
*/
|
||||
export function removeCSSClassFromFileExplorerEL(
|
||||
path: string | undefined,
|
||||
cssClass: string,
|
||||
parent: boolean,
|
||||
plugin: FolderNotesPlugin,
|
||||
): void {
|
||||
if (!path) return;
|
||||
const fileExplorerItem = getFileExplorerElement(path, plugin);
|
||||
activeDocument.querySelectorAll(`[data-path='${CSS.escape(path)}']`).forEach((item) => {
|
||||
item.removeClass(cssClass);
|
||||
});
|
||||
if (!fileExplorerItem) { return; }
|
||||
if (parent) {
|
||||
const parentElement = fileExplorerItem?.parentElement;
|
||||
if (parentElement) {
|
||||
parentElement.removeClass(cssClass);
|
||||
}
|
||||
return;
|
||||
}
|
||||
fileExplorerItem.removeClass(cssClass);
|
||||
|
||||
}
|
||||
|
||||
export function getFileExplorerElement(
|
||||
path: string,
|
||||
plugin: FolderNotesPlugin | FolderOverviewPlugin,
|
||||
): HTMLElement | null {
|
||||
const fileExplorer = getFileExplorer(plugin);
|
||||
if (!fileExplorer?.view?.fileItems) { return null; }
|
||||
const fileExplorerItem = fileExplorer.view.fileItems?.[path];
|
||||
return fileExplorerItem?.selfEl ?? fileExplorerItem?.titleEl ?? null;
|
||||
}
|
||||
|
||||
export function showFolderNoteInFileExplorer(path: string, plugin: FolderNotesPlugin): void {
|
||||
const excludedFolder = new ExcludedFolder(
|
||||
path,
|
||||
plugin.settings.excludeFolders.length,
|
||||
undefined,
|
||||
plugin,
|
||||
);
|
||||
excludedFolder.subFolders = false;
|
||||
excludedFolder.disableSync = false;
|
||||
excludedFolder.disableAutoCreate = false;
|
||||
excludedFolder.disableFolderNote = false;
|
||||
excludedFolder.enableCollapsing = false;
|
||||
excludedFolder.excludeFromFolderOverview = false;
|
||||
excludedFolder.hideInSettings = true;
|
||||
excludedFolder.showFolderNote = true;
|
||||
addExcludedFolder(plugin, excludedFolder, false);
|
||||
addCSSClassToFileExplorerEl(path, 'show-folder-note-in-explorer', true, plugin);
|
||||
updateCSSClassesForFolder(path, plugin);
|
||||
}
|
||||
|
||||
export function hideFolderNoteInFileExplorer(folderPath: string, plugin: FolderNotesPlugin): void {
|
||||
plugin.settings.excludeFolders = plugin.settings.excludeFolders.filter(
|
||||
(folder) => (folder.path !== folderPath) || !folder.showFolderNote);
|
||||
plugin.saveSettings(false);
|
||||
removeCSSClassFromFileExplorerEL(folderPath, 'show-folder-note-in-explorer', true, plugin);
|
||||
updateCSSClassesForFolder(folderPath, plugin);
|
||||
}
|
||||
|
||||
export function setActiveFolder(folderPath: string, plugin: FolderNotesPlugin): void {
|
||||
const fileExplorerItem = getFileExplorerElement(folderPath, plugin);
|
||||
if (fileExplorerItem) {
|
||||
fileExplorerItem.addClass('fn-is-active');
|
||||
plugin.activeFolderDom = fileExplorerItem;
|
||||
}
|
||||
}
|
||||
|
||||
export function removeActiveFolder(plugin: FolderNotesPlugin): void {
|
||||
if (plugin.activeFolderDom) {
|
||||
plugin.activeFolderDom.removeClass('fn-is-active');
|
||||
plugin.activeFolderDom?.removeClass('has-focus');
|
||||
plugin.activeFolderDom = null;
|
||||
}
|
||||
}
|
||||
111
src/functions/utils.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { TFolder, TFile, View, type TAbstractFile } from 'obsidian';
|
||||
import type { FileExplorerWorkspaceLeaf, FileExplorerView } from 'src/globals';
|
||||
import { getFolderNote } from './folderNoteFunctions';
|
||||
import type FolderNotesPlugin from 'src/main';
|
||||
import type { FileTreeItem } from 'obsidian-typings';
|
||||
import type FolderOverviewPlugin from 'src/obsidian-folder-overview/src/main';
|
||||
|
||||
function isFileExplorerWorkspaceLeaf(leaf: unknown): leaf is FileExplorerWorkspaceLeaf {
|
||||
const tabHeaderEl = (leaf as { tabHeaderEl?: HTMLElement | null } | null)?.tabHeaderEl;
|
||||
return tabHeaderEl?.dataset?.type === 'file-explorer';
|
||||
}
|
||||
|
||||
export function getFileNameFromPathString(path: string): string {
|
||||
return path.substring(path.lastIndexOf('/') >= 0 ? path.lastIndexOf('/') + 1 : 0);
|
||||
}
|
||||
|
||||
export function getFolderNameFromPathString(path: string): string {
|
||||
const PARENT_FOLDER_INDEX = -2;
|
||||
const LAST_FOLDER_INDEX = -1;
|
||||
if (path.endsWith('.md') || path.endsWith('.canvas')) {
|
||||
return path.split('/').slice(PARENT_FOLDER_INDEX)[0];
|
||||
}
|
||||
return path.split('/').slice(LAST_FOLDER_INDEX)[0];
|
||||
}
|
||||
|
||||
export function removeExtension(name: string): string {
|
||||
return name.replace(/\.[^/.]+$/, '');
|
||||
}
|
||||
|
||||
export function getExtensionFromPathString(path: string): string {
|
||||
return path.slice(path.lastIndexOf('.') >= 0 ? path.lastIndexOf('.') : 0);
|
||||
}
|
||||
|
||||
export function getFolderPathFromString(path: string): string {
|
||||
const subString = path.lastIndexOf('/') >= 0 ? path.lastIndexOf('/') : 0;
|
||||
const folderPath = path.substring(0, subString);
|
||||
if (folderPath === '') {
|
||||
return '/';
|
||||
}
|
||||
return folderPath;
|
||||
|
||||
}
|
||||
|
||||
export function getParentFolderPath(path: string): string {
|
||||
return getFolderPathFromString(getFolderPathFromString(path));
|
||||
}
|
||||
|
||||
export function getFileExplorer(
|
||||
plugin: FolderNotesPlugin | FolderOverviewPlugin,
|
||||
): FileExplorerWorkspaceLeaf | undefined {
|
||||
let leaf = plugin.app.workspace.getLeavesOfType('file-explorer')[0];
|
||||
|
||||
if (!leaf) { return undefined; }
|
||||
|
||||
/* make.md plugin integration */
|
||||
if ((leaf.containerEl?.lastChild as HTMLElement)?.dataset?.type === 'mk-path-view') {
|
||||
plugin.app.workspace.iterateAllLeaves((x) => {
|
||||
if (isFileExplorerWorkspaceLeaf(x)) {
|
||||
leaf = x;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return isFileExplorerWorkspaceLeaf(leaf) ? leaf : undefined;
|
||||
}
|
||||
|
||||
export function getFileExplorerView(plugin: FolderNotesPlugin): FileExplorerView | undefined {
|
||||
return getFileExplorer(plugin)?.view;
|
||||
}
|
||||
|
||||
export function getFocusedItem(plugin: FolderNotesPlugin): FileTreeItem | null {
|
||||
const fileExplorer = getFileExplorer(plugin);
|
||||
if (!fileExplorer) { return null; }
|
||||
const { focusedItem } = fileExplorer.view.tree;
|
||||
return focusedItem;
|
||||
}
|
||||
|
||||
export function getFileExplorerActiveFolder(plugin: FolderNotesPlugin): TFolder | null {
|
||||
// Check if the active view is a file explorer.
|
||||
const view = plugin.app.workspace.getActiveViewOfType(View);
|
||||
if (view?.getViewType() !== 'file-explorer') return null;
|
||||
// Check if there is a focused or active item in the file explorer.
|
||||
const fe = view as FileExplorerView;
|
||||
const activeFileOrFolder =
|
||||
fe.tree.focusedItem?.file ?? fe.activeDom?.file;
|
||||
if (!(activeFileOrFolder instanceof TFolder)) return null;
|
||||
return activeFileOrFolder;
|
||||
}
|
||||
|
||||
export function getFileExplorerActiveFolderNote(plugin: FolderNotesPlugin): TFile | null {
|
||||
const folder = getFileExplorerActiveFolder(plugin);
|
||||
if (!folder) return null;
|
||||
// Is there any folder note for the active folder?
|
||||
const folderNote = getFolderNote(plugin, folder.path);
|
||||
if (!(folderNote instanceof TFile)) return null;
|
||||
return folderNote;
|
||||
}
|
||||
|
||||
|
||||
export function getAttachmentFolderPath(plugin: FolderNotesPlugin): string {
|
||||
const attachmentFolderPath = plugin.app.vault.getConfig('attachmentFolderPath') as string;
|
||||
const cleanAttachmentFolderPath = attachmentFolderPath?.replace('./', '') || '';
|
||||
return cleanAttachmentFolderPath;
|
||||
}
|
||||
|
||||
export function isFileInAttachmentFolder(plugin: FolderNotesPlugin, file: TAbstractFile): boolean {
|
||||
const attachmentFolderPath = getAttachmentFolderPath(plugin);
|
||||
if (!attachmentFolderPath) return false;
|
||||
return file.path.startsWith(attachmentFolderPath + '/')
|
||||
|| file.path.includes('/' + attachmentFolderPath + '/');
|
||||
}
|
||||
79
src/globals.d.ts
vendored
|
|
@ -1,22 +1,32 @@
|
|||
import { Plugin, TAbstractFile, View, WorkspaceLeaf } from 'obsidian';
|
||||
import type { TAbstractFile, TFile, TFolder, View, WorkspaceLeaf } from 'obsidian';
|
||||
|
||||
declare module 'obsidian' {
|
||||
interface App {
|
||||
internalPlugins: {
|
||||
plugins: {
|
||||
[pluginId: string]: Plugin & {
|
||||
[pluginImplementations: string]: unknown;
|
||||
};
|
||||
};
|
||||
enablePlugin: (id: string) => Promise<void>;
|
||||
disablePlugin: (id: string) => Promise<void>;
|
||||
};
|
||||
interface Setting {
|
||||
createList: (list: ListComponent | ((list: ListComponent) => void)) => ListComponent;
|
||||
}
|
||||
interface TFolder {
|
||||
newName: string | null;
|
||||
collapsed: boolean;
|
||||
}
|
||||
interface MenuItem {
|
||||
dom: HTMLElement;
|
||||
}
|
||||
interface FileManager {
|
||||
promptForFolderDeletion: (folder: TFolder) => void;
|
||||
}
|
||||
|
||||
class ListComponent {
|
||||
containerEl: HTMLElement;
|
||||
emptyStateEl: HTMLElement;
|
||||
listEl: HTMLElement;
|
||||
values: string[];
|
||||
constructor(containerEl: HTMLElement);
|
||||
}
|
||||
}
|
||||
|
||||
interface FileExplorerWorkspaceLeaf extends WorkspaceLeaf {
|
||||
containerEl: HTMLElement;
|
||||
view: FileExplorerView;
|
||||
containerEl: HTMLElement;
|
||||
view: FileExplorerView;
|
||||
}
|
||||
|
||||
interface FileExplorerViewFileItem extends TAbstractFile {
|
||||
|
|
@ -24,6 +34,45 @@ interface FileExplorerViewFileItem extends TAbstractFile {
|
|||
selfEl: HTMLElement
|
||||
}
|
||||
|
||||
interface FileExplorerView extends View {
|
||||
fileItems: { [path: string]: FileExplorerViewFileItem };
|
||||
type FileOrFolderItem = FolderItem | FileItem;
|
||||
|
||||
interface FileItem {
|
||||
el: HTMLDivElement;
|
||||
file: TFile;
|
||||
fileExplorer: FileExplorerView;
|
||||
selfEl: HTMLDivElement;
|
||||
innerEl: HTMLDivElement;
|
||||
}
|
||||
|
||||
interface FolderItem {
|
||||
el: HTMLDivElement;
|
||||
fileExplorer: FileExplorerView;
|
||||
selfEl: HTMLDivElement;
|
||||
innerEl: HTMLDivElement;
|
||||
file: TFolder;
|
||||
children: FileOrFolderItem[];
|
||||
childrenEl: HTMLDivElement;
|
||||
collapseIndicatorEl: HTMLDivElement;
|
||||
collapsed: boolean;
|
||||
setCollapsed: (collapsed: boolean) => void;
|
||||
pusherEl: HTMLDivElement;
|
||||
}
|
||||
|
||||
interface TreeItem {
|
||||
focusedItem: FileOrFolderItem;
|
||||
setFocusedItem: (item: FileOrFolderItem, moveViewport: boolean) => void;
|
||||
selectedDoms: Set<FileOrFolderItem>;
|
||||
}
|
||||
interface FileExplorerView extends View {
|
||||
fileItems: { [path: string]: FileExplorerViewFileItem };
|
||||
activeDom: FileOrFolderItem;
|
||||
tree: TreeItem;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
i18next: {
|
||||
t: (key: string, options?: { [key: string]: string }) => string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
902
src/main.ts
|
|
@ -1,340 +1,668 @@
|
|||
import { Plugin, TFile, TFolder, TAbstractFile, Notice, Keymap } from 'obsidian';
|
||||
import { DEFAULT_SETTINGS, ExcludedFolder, FolderNotesSettings, SettingsTab } from './settings';
|
||||
import FolderNameModal from './modals/folderName';
|
||||
import { applyTemplate } from './template';
|
||||
import { Commands } from './commands';
|
||||
import DeleteConfirmationModal from './modals/deleteConfirmation';
|
||||
import { FileExplorerWorkspaceLeaf } from './globals';
|
||||
import {
|
||||
type App, type TAbstractFile,
|
||||
type MarkdownPostProcessorContext,
|
||||
type WorkspaceLeaf,
|
||||
Plugin, TFile, TFolder,
|
||||
parseYaml, Notice, Keymap,
|
||||
requireApiVersion, Platform, debounce,
|
||||
} from 'obsidian';
|
||||
import {
|
||||
type FolderNotesSettings, DEFAULT_SETTINGS, SettingsTab,
|
||||
} from './settings/SettingsTab';
|
||||
import { FolderOverview, type defaultOverviewSettings } from './obsidian-folder-overview/src/FolderOverview';
|
||||
import { Commands } from './Commands';
|
||||
import type { FileExplorerWorkspaceLeaf } from './globals';
|
||||
import {
|
||||
registerFileExplorerObserver, unregisterFileExplorerObserver,
|
||||
} from './events/MutationObserver';
|
||||
import { handleRename } from './events/handleRename';
|
||||
import {
|
||||
getFolderNote, getFolder, openFolderNote, createFolderNote,
|
||||
} from './functions/folderNoteFunctions';
|
||||
import { handleCreate } from './events/handleCreate';
|
||||
import { FrontMatterTitlePluginHandler } from './events/FrontMatterTitle';
|
||||
import { FolderOverviewSettings } from './obsidian-folder-overview/src/modals/Settings';
|
||||
import { TabManager } from './events/TabManager';
|
||||
import './functions/ListComponent';
|
||||
import { handleDelete } from './events/handleDelete';
|
||||
import {
|
||||
addCSSClassToFileExplorerEl, getFileExplorerElement, removeCSSClassFromFileExplorerEL,
|
||||
refreshAllFolderStyles, setActiveFolder, removeActiveFolder,
|
||||
} from './functions/styleFunctions';
|
||||
import { getExcludedFolder } from './ExcludeFolders/functions/folderFunctions';
|
||||
import { getFileExplorer } from './functions/utils';
|
||||
import { FOLDER_OVERVIEW_VIEW, FolderOverviewView } from './obsidian-folder-overview/src/view';
|
||||
import { registerOverviewCommands } from './obsidian-folder-overview/src/Commands';
|
||||
import { updateOverviewView, updateViewDropdown } from './obsidian-folder-overview/src/main';
|
||||
import { FvIndexDB } from './obsidian-folder-overview/src/utils/IndexDB';
|
||||
import { updateAllOverviews } from './obsidian-folder-overview/src/utils/functions';
|
||||
|
||||
interface FileExplorerPluginLike extends Plugin {
|
||||
revealInFolder: (file: TAbstractFile) => void;
|
||||
}
|
||||
|
||||
interface DragManagerLike {
|
||||
draggable?: {
|
||||
file?: TAbstractFile;
|
||||
type?: string;
|
||||
} | null;
|
||||
setAction(action: string): void;
|
||||
}
|
||||
|
||||
interface ClipboardManagerLike {
|
||||
app: App & {
|
||||
dragManager?: DragManagerLike;
|
||||
};
|
||||
handleDragOver: (evt: DragEvent, ...args: unknown[]) => void;
|
||||
handleDrop: (evt: DragEvent, ...args: unknown[]) => void;
|
||||
}
|
||||
|
||||
interface EditModeLike {
|
||||
clipboardManager: ClipboardManagerLike;
|
||||
}
|
||||
|
||||
interface ViewWithEditModes {
|
||||
editMode?: EditModeLike;
|
||||
sourceMode?: EditModeLike;
|
||||
}
|
||||
|
||||
interface ActiveEditorLike {
|
||||
editMode?: EditModeLike;
|
||||
}
|
||||
|
||||
type LegacySettingsData = Partial<FolderNotesSettings> & {
|
||||
allowWhitespaceCollapsing?: boolean;
|
||||
defaultOverview?: defaultOverviewSettings;
|
||||
};
|
||||
|
||||
export default class FolderNotesPlugin extends Plugin {
|
||||
observer: MutationObserver;
|
||||
settings: FolderNotesSettings;
|
||||
settingsTab: SettingsTab;
|
||||
activeFileExplorer: FileExplorerWorkspaceLeaf;
|
||||
settings!: FolderNotesSettings;
|
||||
settingsTab!: SettingsTab;
|
||||
activeFolderDom!: HTMLElement | null;
|
||||
activeFileExplorer!: FileExplorerWorkspaceLeaf;
|
||||
fmtpHandler: FrontMatterTitlePluginHandler | null = null;
|
||||
hoveredElement: HTMLElement | null = null;
|
||||
mouseEvent: MouseEvent | null = null;
|
||||
hoverLinkTriggered = false;
|
||||
tabManager!: TabManager;
|
||||
settingsOpened = false;
|
||||
askModalCurrentlyOpen = false;
|
||||
fvIndexDB!: FvIndexDB;
|
||||
|
||||
async onload() {
|
||||
console.log('loading folder notes plugin');
|
||||
async onload(): Promise<void> {
|
||||
console.debug('loading folder notes plugin');
|
||||
await this.loadSettings();
|
||||
this.settingsTab = new SettingsTab(this.app, this);
|
||||
this.addSettingTab(this.settingsTab);
|
||||
await this.saveSettings();
|
||||
this.fvIndexDB = new FvIndexDB(this);
|
||||
|
||||
// Add CSS Classes
|
||||
document.body.classList.add('folder-notes-plugin');
|
||||
if (this.settings.hideFolderNote) { document.body.classList.add('hide-folder-note'); }
|
||||
if (this.settings.underlineFolder) { document.body.classList.add('folder-note-underline'); }
|
||||
if (!this.settings.allowWhitespaceCollapsing) { document.body.classList.add('fn-whitespace-stop-collapsing'); }
|
||||
this.addSettingCssClasses();
|
||||
|
||||
new Commands(this.app, this).registerCommands();
|
||||
registerOverviewCommands(this);
|
||||
|
||||
this.observer = new MutationObserver((mutations: MutationRecord[]) => {
|
||||
mutations.forEach((rec) => {
|
||||
if (rec.type === 'childList') {
|
||||
(<Element>rec.target).querySelectorAll('div.nav-folder-title-content')
|
||||
.forEach((element: HTMLElement) => {
|
||||
if (element.onclick) return;
|
||||
element.onclick = (event: MouseEvent) => this.handleFolderClick(event);
|
||||
});
|
||||
}
|
||||
this.app.workspace.onLayoutReady(() => this.onLayoutReady());
|
||||
|
||||
if (!this.settings.persistentSettingsTab.afterRestart) {
|
||||
this.settings.settingsTab = 'general';
|
||||
}
|
||||
|
||||
this.registerDomEvent(window, 'keydown', (event: KeyboardEvent) => {
|
||||
const { hoveredElement } = this;
|
||||
if (this.hoverLinkTriggered) return;
|
||||
if (!hoveredElement) return;
|
||||
if (!Keymap.isModEvent(event)) return;
|
||||
|
||||
const folderPath = hoveredElement?.parentElement?.getAttribute('data-path') || '';
|
||||
const folderNote = getFolderNote(this, folderPath);
|
||||
if (!folderNote) return;
|
||||
|
||||
this.app.workspace.trigger('hover-link', {
|
||||
event: this.mouseEvent,
|
||||
source: 'preview',
|
||||
hoverParent: {
|
||||
file: folderNote,
|
||||
},
|
||||
targetEl: hoveredElement,
|
||||
linktext: folderNote?.basename,
|
||||
sourcePath: folderNote?.path,
|
||||
});
|
||||
this.hoverLinkTriggered = true;
|
||||
});
|
||||
this.observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
this.registerEvent(this.app.workspace.on('layout-change', () => { this.loadFileClasses(); }));
|
||||
this.registerEvent(this.app.vault.on('delete', (file: TAbstractFile) => {
|
||||
if(!(file instanceof TFile)) { return; }
|
||||
// parent is null here even if the parent exists
|
||||
// not entirely sure why
|
||||
const parentPath = this.getPathFromString(file.path);
|
||||
const parentName = this.getNameFromPathString(parentPath);
|
||||
if(parentName !== file.basename) { return; }
|
||||
this.removeCSSClassFromEL(parentPath, 'has-folder-note');
|
||||
|
||||
this.registerEvent(this.app.workspace.on('file-open', (openFile: TFile | null) => {
|
||||
removeActiveFolder(this);
|
||||
|
||||
if (!openFile || !openFile.basename) { return; }
|
||||
|
||||
const folder = getFolder(this, openFile);
|
||||
if (!folder) { return; }
|
||||
const excludedFolder = getExcludedFolder(this, folder.path, true);
|
||||
if (excludedFolder?.disableFolderNote) return;
|
||||
const folderNote = getFolderNote(this, folder.path);
|
||||
if (!folderNote) { return; }
|
||||
if (folderNote.path !== openFile.path) { return; }
|
||||
setActiveFolder(folder.path, this);
|
||||
}));
|
||||
|
||||
this.registerEvent(this.app.vault.on('create', (file: TAbstractFile) => {
|
||||
if (!this.app.workspace.layoutReady) return;
|
||||
if(file instanceof TFile) { return this.handleFileCreate(file); }
|
||||
if (!this.settings.autoCreate) return;
|
||||
if (!(file instanceof TFolder)) return;
|
||||
|
||||
const excludedFolder = this.getExcludedFolderByPath(file.path);
|
||||
if (excludedFolder?.disableAutoCreate) return;
|
||||
|
||||
const path = file.path + '/' + file.name + '.md';
|
||||
const folderNote = this.app.vault.getAbstractFileByPath(path);
|
||||
if (folderNote) return;
|
||||
this.createFolderNote(path, true, true);
|
||||
this.addCSSClassToTitleEL(file.path, 'has-folder-note');
|
||||
|
||||
handleCreate(file, this).catch((err) => {
|
||||
console.error(err); new Notice('Error handling file creation');
|
||||
});
|
||||
this.handleVaultChange();
|
||||
}));
|
||||
|
||||
this.registerEvent(this.app.vault.on('rename', (file: TAbstractFile, oldPath: string) => {
|
||||
if (!this.settings.syncFolderName) {
|
||||
// cleanup after ourselves
|
||||
this.removeCSSClassFromEL(file.path, 'has-folder-note');
|
||||
this.removeCSSClassFromEL(file.path, 'is-folder-note');
|
||||
return;
|
||||
}
|
||||
if (file instanceof TFolder) {
|
||||
return this.handleFolderRename(file, oldPath);
|
||||
} else if (file instanceof TFile) {
|
||||
return this.handleFileRename(file, oldPath);
|
||||
}
|
||||
handleRename(file, oldPath, this);
|
||||
this.handleVaultChange();
|
||||
}));
|
||||
|
||||
if (!this.app.workspace.layoutReady) {
|
||||
this.app.workspace.onLayoutReady(async () => this.loadFileClasses());
|
||||
}
|
||||
}
|
||||
this.registerEvent(this.app.vault.on('delete', (file: TAbstractFile) => {
|
||||
handleDelete(file, this);
|
||||
this.handleVaultChange();
|
||||
}));
|
||||
|
||||
async handleFolderClick(event: MouseEvent) {
|
||||
if (!(event.target instanceof HTMLElement)) return;
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
const folder = event.target.parentElement?.getAttribute('data-path');
|
||||
if (!folder) { return; }
|
||||
const excludedFolder = this.getExcludedFolderByPath(folder);
|
||||
if (excludedFolder?.disableFolderNote) {
|
||||
event.target.onclick = null;
|
||||
event.target.click();
|
||||
return;
|
||||
} else if (excludedFolder?.enableCollapsing || this.settings.enableCollapsing) {
|
||||
event.target.onclick = null;
|
||||
event.target.click();
|
||||
}
|
||||
const path = folder + '/' + event.target.innerText + '.md';
|
||||
|
||||
if (this.app.vault.getAbstractFileByPath(path)) {
|
||||
return this.openFolderNote(path);
|
||||
} else if (event.altKey || Keymap.isModEvent(event) === 'tab') {
|
||||
if ((this.settings.altKey && event.altKey) || (this.settings.ctrlKey && Keymap.isModEvent(event) === 'tab')) {
|
||||
await this.createFolderNote(path, true, true);
|
||||
this.addCSSClassToTitleEL(folder, 'has-folder-note');
|
||||
this.removeCSSClassFromEL(folder, 'has-not-folder-note');
|
||||
return;
|
||||
}
|
||||
}
|
||||
event.target.onclick = null;
|
||||
event.target.click();
|
||||
}
|
||||
|
||||
handleFolderRename(file: TFolder, oldPath: string) {
|
||||
const oldFileName = this.getNameFromPathString(oldPath);
|
||||
const folder = this.app.vault.getAbstractFileByPath(file.path);
|
||||
if (!folder) return;
|
||||
const excludedFolders = this.settings.excludeFolders.filter(
|
||||
(excludedFolder) => excludedFolder.path.includes(oldPath)
|
||||
this.registerMarkdownCodeBlockProcessor(
|
||||
'folder-overview',
|
||||
(source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => {
|
||||
this.handleOverviewBlock(source, el, ctx);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
excludedFolders.forEach((excludedFolder) => {
|
||||
if (excludedFolder.path === oldPath) {
|
||||
excludedFolder.path = folder.path;
|
||||
return;
|
||||
}
|
||||
const folders = excludedFolder.path.split('/');
|
||||
if (folders.length < 1) {
|
||||
folders.push(excludedFolder.path);
|
||||
}
|
||||
addSettingCssClasses(): void {
|
||||
activeDocument.body.classList.add('folder-notes-plugin');
|
||||
if (this.settings.hideFolderNote) { activeDocument.body.classList.add('hide-folder-note'); }
|
||||
if (this.settings.hideCollapsingIconForEmptyFolders) {
|
||||
activeDocument.body.classList.add('fn-hide-empty-collapse-icon');
|
||||
}
|
||||
if (this.settings.hideFolderNoteNameInPath) {
|
||||
activeDocument.body.classList.add('folder-note-hide-name-path');
|
||||
}
|
||||
if (this.settings.underlineFolder) {
|
||||
activeDocument.body.classList.add('folder-note-underline');
|
||||
}
|
||||
if (this.settings.boldName) { activeDocument.body.classList.add('folder-note-bold'); }
|
||||
if (this.settings.cursiveName) { activeDocument.body.classList.add('folder-note-cursive'); }
|
||||
if (this.settings.boldNameInPath) {
|
||||
activeDocument.body.classList.add('folder-note-bold-path');
|
||||
}
|
||||
if (this.settings.cursiveNameInPath) {
|
||||
activeDocument.body.classList.add('folder-note-cursive-path');
|
||||
}
|
||||
if (this.settings.underlineFolderInPath) {
|
||||
activeDocument.body.classList.add('folder-note-underline-path');
|
||||
}
|
||||
if (this.settings.stopWhitespaceCollapsing) {
|
||||
activeDocument.body.classList.add('fn-whitespace-stop-collapsing');
|
||||
}
|
||||
if (this.settings.hideCollapsingIcon) {
|
||||
activeDocument.body.classList.add('fn-hide-collapse-icon');
|
||||
}
|
||||
if (this.settings.ignoreAttachmentFolder) {
|
||||
activeDocument.body.classList.add('fn-ignore-attachment-folder');
|
||||
}
|
||||
if (!this.settings.highlightFolder) {
|
||||
activeDocument.body.classList.add('disable-folder-highlight');
|
||||
}
|
||||
|
||||
folders[folders.indexOf(oldFileName)] = folder.name;
|
||||
excludedFolder.path = folders.join('/');
|
||||
if (requireApiVersion('1.7.2')) {
|
||||
activeDocument.body.classList.add('version-1-7-2');
|
||||
}
|
||||
}
|
||||
|
||||
onLayoutReady(): void {
|
||||
if (!this._loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
registerFileExplorerObserver(this);
|
||||
|
||||
const fileExplorer = getFileExplorer(this);
|
||||
// @ts-expect-error use internal API
|
||||
const infinityScroll = fileExplorer?.view?.tree?.infinityScroll;
|
||||
|
||||
if (infinityScroll) {
|
||||
// increase infinity scroll buffer to show hidden folder notes
|
||||
// Issue: https://github.com/LostPaul/obsidian-folder-notes/issues/274
|
||||
infinityScroll.rootMargin = 1.5;
|
||||
}
|
||||
|
||||
this.registerView(FOLDER_OVERVIEW_VIEW, (leaf: WorkspaceLeaf) => {
|
||||
return new FolderOverviewView(leaf, this);
|
||||
});
|
||||
this.saveSettings();
|
||||
const excludedFolder = this.getExcludedFolderByPath(oldPath);
|
||||
if (excludedFolder?.disableSync) return;
|
||||
|
||||
const newPath = folder.path + '/' + folder.name + '.md';
|
||||
if (!(folder instanceof TFolder)) return;
|
||||
const note = this.app.vault.getAbstractFileByPath(oldPath + '/' + oldFileName + '.md');
|
||||
if (!note) return;
|
||||
(note as TFile).path = folder.path + '/' + oldFileName + '.md';
|
||||
this.app.vault.rename(note, newPath);
|
||||
}
|
||||
|
||||
handleFileRename(file: TFile, oldPath: string) {
|
||||
const oldFileName = this.getNameFromPathString(oldPath);
|
||||
const oldFilePath = this.getPathFromString(oldPath);
|
||||
const oldFolder = this.app.vault.getAbstractFileByPath(oldFilePath);
|
||||
|
||||
const newFilePath = this.getPathFromString(file.path);
|
||||
const newFolder = this.app.vault.getAbstractFileByPath(newFilePath);
|
||||
|
||||
if(oldFolder) {
|
||||
const excludedFolder = this.getExcludedFolderByPath(oldFolder.path);
|
||||
if (excludedFolder?.disableSync) {
|
||||
this.removeCSSClassFromEL(oldFolder.path, 'has-folder-note');
|
||||
this.removeCSSClassFromEL(file.path, 'is-folder-note');
|
||||
this.removeCSSClassFromEL(oldPath, 'is-folder-note');
|
||||
}
|
||||
// nothing else for us to do as we have no new folder to action on
|
||||
if (!newFolder) { return; }
|
||||
|
||||
const newExcludedFolder = this.getExcludedFolderByPath(newFolder.path);
|
||||
// Nothing else for us to do
|
||||
if(newExcludedFolder?.disableSync || newExcludedFolder?.disableFolderNote) { return; }
|
||||
}
|
||||
|
||||
// file matched folder name before rename
|
||||
// file hasnt moved just renamed
|
||||
// Need to rename the folder
|
||||
if (oldFolder && oldFolder.name + '.md' === oldFileName && newFilePath === oldFilePath) {
|
||||
return this.renameFolderOnFileRename(file, oldPath, oldFolder);
|
||||
}
|
||||
|
||||
// the note has been moved somewhere and is no longer a folder note
|
||||
// cleanup css on the folder and note
|
||||
if (oldFolder && oldFolder.name + '.md' === oldFileName && newFilePath !== oldFilePath) {
|
||||
this.removeCSSClassFromEL(oldFolder.path, 'has-folder-note');
|
||||
this.removeCSSClassFromEL(file.path, 'is-folder-note');
|
||||
this.removeCSSClassFromEL(oldPath, 'is-folder-note');
|
||||
}
|
||||
|
||||
// file has been moved into position where it can be a folder note!
|
||||
if(newFolder && newFolder.name === file.basename) {
|
||||
this.addCSSClassToTitleEL(newFolder.path, 'has-folder-note');
|
||||
this.addCSSClassToTitleEL(file.path, 'is-folder-note', true);
|
||||
}
|
||||
}
|
||||
|
||||
async renameFolderOnFileRename(file: TFile, oldPath: string, oldFolder: TAbstractFile) {
|
||||
const newFolderPath = oldFolder.parent.path + '/' + file.basename;
|
||||
if (this.app.vault.getAbstractFileByPath(newFolderPath)) {
|
||||
await this.app.vault.rename(file, oldPath);
|
||||
return new Notice('A folder with the same name already exists');
|
||||
}
|
||||
await this.app.vault.rename(oldFolder, newFolderPath);
|
||||
}
|
||||
|
||||
handleFileCreate(file: TFile) {
|
||||
if(file.basename !== file.parent.name) { return; }
|
||||
this.addCSSClassToTitleEL(file.parent.path, 'has-folder-note');
|
||||
this.addCSSClassToTitleEL(file.path, 'is-folder-note', true);
|
||||
}
|
||||
|
||||
async createFolderNote(path: string, openFile: boolean, useModal?: boolean) {
|
||||
const leaf = this.app.workspace.getLeaf(false);
|
||||
const file = await this.app.vault.create(path, '');
|
||||
if (openFile) {
|
||||
await leaf.openFile(file);
|
||||
}
|
||||
if (file) {
|
||||
applyTemplate(this, file, this.settings.templatePath);
|
||||
}
|
||||
if (!this.settings.autoCreate) return;
|
||||
if (!useModal) return;
|
||||
const folder = this.app.vault.getAbstractFileByPath(this.getPathFromString(path));
|
||||
if (!(folder instanceof TFolder)) return;
|
||||
const modal = new FolderNameModal(this.app, this, folder);
|
||||
modal.open();
|
||||
this.addCSSClassToTitleEL(path, 'is-folder-note', true);
|
||||
}
|
||||
|
||||
async openFolderNote(path: string) {
|
||||
const leaf = this.app.workspace.getLeaf(false);
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
if (file instanceof TFile) {
|
||||
await leaf.openFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFolderNote(file: TFile) {
|
||||
if (this.settings.showDeleteConfirmation) {
|
||||
return new DeleteConfirmationModal(this.app, this, file).open();
|
||||
}
|
||||
await this.app.vault.delete(file);
|
||||
}
|
||||
|
||||
getNameFromPathString(path: string): string {
|
||||
return path.substring(path.lastIndexOf('/' || '\\') >= 0 ? path.lastIndexOf('/' || '\\') + 1 : 0);
|
||||
}
|
||||
|
||||
getPathFromString(path: string): string {
|
||||
const subString = path.lastIndexOf('/' || '\\') >= 0 ? path.lastIndexOf('/') : path.length;
|
||||
return path.substring(0, subString);
|
||||
}
|
||||
|
||||
getExcludedFolderByPath(path: string): ExcludedFolder | undefined {
|
||||
return this.settings.excludeFolders.find((excludedFolder) => {
|
||||
if (excludedFolder.path === path) { return true; }
|
||||
if (!excludedFolder.subFolders) { return false; }
|
||||
return this.getPathFromString(path).startsWith(excludedFolder.path);
|
||||
this.app.workspace.on('layout-change', () => {
|
||||
this.tabManager.updateTabs();
|
||||
});
|
||||
|
||||
if (this.app.plugins.getPlugin('obsidian-front-matter-title-plugin')) {
|
||||
this.fmtpHandler = new FrontMatterTitlePluginHandler(this);
|
||||
}
|
||||
this.tabManager = new TabManager(this);
|
||||
this.tabManager.updateTabs();
|
||||
|
||||
this.registerDomEvent(activeDocument, 'click', (evt: MouseEvent) => {
|
||||
this.handleFileExplorerClick(evt);
|
||||
}, true);
|
||||
|
||||
// Handle middle mouse button clicks
|
||||
this.registerDomEvent(activeDocument, 'auxclick', (evt: MouseEvent) => {
|
||||
const rightClick = 2;
|
||||
if (evt.button === rightClick) return;
|
||||
this.handleFileExplorerClick(evt);
|
||||
}, true);
|
||||
|
||||
const fileExplorerPlugin = this.app.internalPlugins.getEnabledPluginById('file-explorer');
|
||||
if (fileExplorerPlugin) {
|
||||
const fileExplorer = fileExplorerPlugin as unknown as FileExplorerPluginLike;
|
||||
const originalRevealInFolder =
|
||||
(fileExplorer.revealInFolder as unknown as FileExplorerPluginLike['revealInFolder'])
|
||||
.bind(fileExplorer);
|
||||
fileExplorer.revealInFolder = (file: TAbstractFile): void => {
|
||||
if (file instanceof TFile) {
|
||||
const folder = getFolder(this, file);
|
||||
if (folder instanceof TFolder) {
|
||||
const folderNote = getFolderNote(this, folder.path);
|
||||
if (!folderNote || folderNote.path !== file.path) {
|
||||
originalRevealInFolder(file);
|
||||
return;
|
||||
}
|
||||
activeDocument.body.classList.remove('hide-folder-note');
|
||||
originalRevealInFolder(folder);
|
||||
const FOLDER_REVEAL_DELAY = 100;
|
||||
window.setTimeout(() => {
|
||||
activeDocument.body.classList.add('hide-folder-note');
|
||||
}, FOLDER_REVEAL_DELAY);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (file instanceof TFolder || file instanceof TFile) {
|
||||
originalRevealInFolder(file);
|
||||
return;
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const leaf = this.app.workspace.getLeavesOfType('markdown').first();
|
||||
const view = leaf?.view;
|
||||
|
||||
if (!view) { return; }
|
||||
|
||||
const viewWithEditModes = view as ViewWithEditModes;
|
||||
const activeEditor = this.app.workspace.activeEditor as ActiveEditorLike | undefined;
|
||||
const editMode = viewWithEditModes.editMode ?? viewWithEditModes.sourceMode
|
||||
?? activeEditor?.editMode;
|
||||
|
||||
if (!editMode) { return; }
|
||||
|
||||
const { clipboardManager } = editMode;
|
||||
const clipboardProto = Object.getPrototypeOf(clipboardManager) as ClipboardManagerLike;
|
||||
|
||||
const originalHandleDragOver =
|
||||
clipboardProto.handleDragOver as unknown as ClipboardManagerLike['handleDragOver'];
|
||||
const originalHandleDrop =
|
||||
clipboardProto.handleDrop as unknown as ClipboardManagerLike['handleDrop'];
|
||||
const folderNotePlugin = this;
|
||||
clipboardProto.handleDragOver = function (evt: DragEvent, ...args: unknown[]): void {
|
||||
const { dragManager } = (this as ClipboardManagerLike).app;
|
||||
const draggable = dragManager?.draggable;
|
||||
|
||||
if (draggable?.file instanceof TFolder) {
|
||||
const folderNote = getFolderNote(folderNotePlugin, draggable.file.path);
|
||||
if (folderNote) {
|
||||
dragManager.setAction(
|
||||
window.i18next.t('interface.drag-and-drop.insert-link-here'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return originalHandleDragOver.call(this, evt, ...args);
|
||||
};
|
||||
|
||||
clipboardProto.handleDrop = function (evt: DragEvent, ...args: unknown[]): void {
|
||||
const { dragManager } = (this as ClipboardManagerLike).app;
|
||||
const draggable = dragManager?.draggable;
|
||||
|
||||
if (draggable?.file instanceof TFolder) {
|
||||
const folderNote = getFolderNote(folderNotePlugin, draggable.file.path);
|
||||
if (folderNote) {
|
||||
draggable.file = folderNote;
|
||||
draggable.type = 'file';
|
||||
}
|
||||
}
|
||||
|
||||
return originalHandleDrop.call(this, evt, ...args);
|
||||
};
|
||||
|
||||
if (this.settings.fvGlobalSettings.autoUpdateLinks) {
|
||||
this.fvIndexDB.init(false);
|
||||
}
|
||||
}
|
||||
|
||||
getFileExplorer() {
|
||||
return this.app.workspace.getLeavesOfType('file-explorer')[0] as FileExplorerWorkspaceLeaf;
|
||||
handleVaultChange(): void {
|
||||
if (!this.settings.fvGlobalSettings.autoUpdateLinks) return;
|
||||
const DEBOUNCE_DELAY = 2000;
|
||||
debounce(() => {
|
||||
void updateAllOverviews(this);
|
||||
}, DEBOUNCE_DELAY, true)();
|
||||
}
|
||||
|
||||
getFileExplorerView() {
|
||||
return this.getFileExplorer().view;
|
||||
handleFileExplorerClick(evt: MouseEvent): void {
|
||||
const target = evt.target as HTMLElement;
|
||||
if (evt.shiftKey) return;
|
||||
if (this.isMobileClickDisabled()) return;
|
||||
|
||||
const { folderTitleEl, onlyClickedOnFolderTitle } = this.getFolderTitleInfo(target);
|
||||
if (!folderTitleEl) return;
|
||||
if (this.shouldIgnoreClickByWhitespaceOrCollapse(target, onlyClickedOnFolderTitle)) return;
|
||||
|
||||
const folderPath = this.getValidFolderPath(folderTitleEl);
|
||||
if (!folderPath) return;
|
||||
|
||||
const usedCtrl = this.isCtrlUsed(evt);
|
||||
const folderNote = getFolderNote(this, folderPath);
|
||||
|
||||
if (!folderNote && this.shouldCreateNote(evt, usedCtrl)) {
|
||||
this.createNoteAndMark(folderPath);
|
||||
return;
|
||||
}
|
||||
if (!(folderNote instanceof TFile)) return;
|
||||
if (!this.shouldOpenNote(usedCtrl, evt)) return;
|
||||
|
||||
if (!this.settings.enableCollapsing || usedCtrl) {
|
||||
evt.preventDefault();
|
||||
evt.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
void openFolderNote(this, folderNote, evt);
|
||||
}
|
||||
|
||||
async addCSSClassToTitleEL(path: string, cssClass: string, waitForCreate = false, count = 0) {
|
||||
const fileExplorerItem = this.getEL(path);
|
||||
if(!fileExplorerItem) {
|
||||
if(waitForCreate && count < 5) {
|
||||
// sleep for a second for the file-explorer event to catch up
|
||||
// this is annoying as in most scanarios our plugin recieves the event before file explorer
|
||||
// If we could guarrantee load order it wouldn't be an issue but we can't
|
||||
// realise this is racey and needs to be fixed.
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
this.addCSSClassToTitleEL(path, cssClass, waitForCreate, count + 1);
|
||||
private isMobileClickDisabled(): boolean {
|
||||
return Platform.isMobile && this.settings.disableOpenFolderNoteOnClick;
|
||||
}
|
||||
|
||||
private getFolderTitleInfo(target: HTMLElement): {
|
||||
folderTitleEl: HTMLElement | null;
|
||||
onlyClickedOnFolderTitle: boolean;
|
||||
} {
|
||||
const folderTitleEl = target.closest('.nav-folder-title');
|
||||
const onlyClickedOnFolderTitle = !!target.closest('.nav-folder-title-content');
|
||||
return {
|
||||
folderTitleEl: folderTitleEl instanceof HTMLElement ? folderTitleEl : null,
|
||||
onlyClickedOnFolderTitle,
|
||||
};
|
||||
}
|
||||
|
||||
private shouldIgnoreClickByWhitespaceOrCollapse(
|
||||
target: HTMLElement,
|
||||
onlyClickedOnFolderTitle: boolean,
|
||||
): boolean {
|
||||
if (!this.settings.stopWhitespaceCollapsing && !onlyClickedOnFolderTitle) return true;
|
||||
if (target.closest('.collapse-icon')) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private getValidFolderPath(folderTitleEl: HTMLElement): string | null {
|
||||
const folderPath = folderTitleEl.getAttribute('data-path');
|
||||
if (!folderPath) return null;
|
||||
const excludedFolder = getExcludedFolder(this, folderPath, true);
|
||||
if (excludedFolder?.disableFolderNote) return null;
|
||||
return folderPath;
|
||||
}
|
||||
|
||||
private isCtrlUsed(evt: MouseEvent): boolean {
|
||||
return Platform.isMacOS ? evt.metaKey : evt.ctrlKey;
|
||||
}
|
||||
|
||||
private shouldCreateNote(evt: MouseEvent, usedCtrl: boolean): boolean {
|
||||
const isTabMod = Keymap.isModEvent(evt) === 'tab';
|
||||
if (!(evt.altKey || isTabMod)) return false;
|
||||
return (this.settings.altKey && evt.altKey) || (usedCtrl && this.settings.ctrlKey);
|
||||
}
|
||||
|
||||
private createNoteAndMark(folderPath: string): void {
|
||||
void createFolderNote(this, folderPath, true, undefined, true);
|
||||
void addCSSClassToFileExplorerEl(folderPath, 'has-folder-note', false, this);
|
||||
void removeCSSClassFromFileExplorerEL(folderPath, 'has-not-folder-note', false, this);
|
||||
}
|
||||
|
||||
private shouldOpenNote(usedCtrl: boolean, evt: MouseEvent): boolean {
|
||||
if (this.settings.openWithCtrl && !usedCtrl) return false;
|
||||
if (this.settings.openWithAlt && !evt.altKey) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
handleOverviewBlock(source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext): void {
|
||||
const observer = new MutationObserver(() => {
|
||||
const editButton = el.parentElement?.childNodes.item(1);
|
||||
if (editButton) {
|
||||
editButton.addEventListener('click', (e) => {
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
new FolderOverviewSettings(
|
||||
this.app,
|
||||
this,
|
||||
parseYaml(source) as defaultOverviewSettings,
|
||||
ctx,
|
||||
el,
|
||||
this.settings.defaultOverview,
|
||||
).open();
|
||||
}, { capture: true });
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(el, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
try {
|
||||
if (this.app.workspace.layoutReady) {
|
||||
const { defaultOverview } = this.settings;
|
||||
const folderOverview = new FolderOverview(
|
||||
this,
|
||||
ctx,
|
||||
source,
|
||||
el,
|
||||
defaultOverview,
|
||||
);
|
||||
void folderOverview.create(this, el, ctx);
|
||||
} else {
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
const folderOverview = new FolderOverview(
|
||||
this,
|
||||
ctx,
|
||||
source,
|
||||
el,
|
||||
this.settings.defaultOverview,
|
||||
);
|
||||
void folderOverview.create(this, el, ctx);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line max-len
|
||||
new Notice('Error creating folder overview (folder notes plugin) - check console for more details');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async activateOverviewView(): Promise<void> {
|
||||
const { workspace } = this.app;
|
||||
|
||||
let leaf: WorkspaceLeaf | null = null;
|
||||
const leaves = workspace.getLeavesOfType(FOLDER_OVERVIEW_VIEW);
|
||||
|
||||
if (leaves.length > 0) {
|
||||
leaf = leaves[0];
|
||||
} else {
|
||||
leaf = workspace.getRightLeaf(false);
|
||||
await leaf?.setViewState({ type: FOLDER_OVERVIEW_VIEW, active: true });
|
||||
}
|
||||
|
||||
if (!leaf) return;
|
||||
void workspace.revealLeaf(leaf);
|
||||
}
|
||||
|
||||
updateOverviewView: typeof updateOverviewView = updateOverviewView;
|
||||
updateViewDropdown: typeof updateViewDropdown = updateViewDropdown;
|
||||
|
||||
isEmptyFolderNoteFolder(folder: TFolder): boolean {
|
||||
let attachmentFolderPath = this.app.vault.getConfig('attachmentFolderPath') as string;
|
||||
const cleanAttachmentFolderPath = attachmentFolderPath?.replace('./', '') || '';
|
||||
const attachmentsAreInRootFolder = attachmentFolderPath === './'
|
||||
|| attachmentFolderPath === '';
|
||||
const threshold = this.settings.storageLocation === 'insideFolder' ? 1 : 0;
|
||||
if (folder.children.length === 0) {
|
||||
void addCSSClassToFileExplorerEl(folder.path, 'fn-empty-folder', false, this);
|
||||
}
|
||||
attachmentFolderPath = `${folder.path}/${cleanAttachmentFolderPath}`;
|
||||
|
||||
if (folder.children.length === threshold) {
|
||||
void addCSSClassToFileExplorerEl(folder.path, 'fn-empty-folder', false, this);
|
||||
return true;
|
||||
} else if (folder.children.length > threshold) {
|
||||
if (attachmentsAreInRootFolder) {
|
||||
return false;
|
||||
} else if (
|
||||
this.app.vault.getAbstractFileByPath(attachmentFolderPath) instanceof TFolder
|
||||
) {
|
||||
const attachmentFolder = this.app.vault.getAbstractFileByPath(attachmentFolderPath);
|
||||
if (
|
||||
attachmentFolder instanceof TFolder &&
|
||||
folder.children.length <= threshold + 1
|
||||
) {
|
||||
void addCSSClassToFileExplorerEl(folder.path, 'fn-empty-folder', false, this);
|
||||
void addCSSClassToFileExplorerEl(
|
||||
folder.path, 'fn-has-attachment-folder',
|
||||
false, this,
|
||||
);
|
||||
void addCSSClassToFileExplorerEl(
|
||||
folder.path, 'fn-has-attachment-folder',
|
||||
true, this,
|
||||
);
|
||||
}
|
||||
return folder.children.length <= threshold + 1;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async changeFolderNameInExplorer(
|
||||
folder: TFolder,
|
||||
newName: string | null | undefined,
|
||||
waitForCreate = false,
|
||||
count = 0,
|
||||
): Promise<void> {
|
||||
const MAX_RETRY_COUNT = 5;
|
||||
const RETRY_DELAY_MS = 500;
|
||||
if (!newName) newName = folder.name;
|
||||
let fileExplorerItem = getFileExplorerElement(folder.path, this);
|
||||
if (!fileExplorerItem) {
|
||||
if (waitForCreate && count < MAX_RETRY_COUNT) {
|
||||
await new Promise<void>((resolve) => window.setTimeout(resolve, RETRY_DELAY_MS));
|
||||
void this.changeFolderNameInExplorer(folder, newName, waitForCreate, count + 1);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
fileExplorerItem.addClass(cssClass);
|
||||
|
||||
fileExplorerItem = fileExplorerItem?.querySelector('div.nav-folder-title-content');
|
||||
if (!fileExplorerItem) { return; }
|
||||
if (this.settings.frontMatterTitle.explorer && this.settings.frontMatterTitle.enabled) {
|
||||
(fileExplorerItem).innerText = newName;
|
||||
(fileExplorerItem).setAttribute('old-name', folder.name);
|
||||
} else {
|
||||
(fileExplorerItem).innerText = folder.name;
|
||||
(fileExplorerItem).removeAttribute('old-name');
|
||||
}
|
||||
}
|
||||
|
||||
removeCSSClassFromEL(path: string, cssClass: string) {
|
||||
const fileExplorerItem = this.getEL(path);
|
||||
if(!fileExplorerItem) { return; }
|
||||
fileExplorerItem.removeClass(cssClass);
|
||||
async changeFolderNameInPath(
|
||||
folder: TFolder,
|
||||
newName: string | null | undefined,
|
||||
breadcrumb: HTMLElement,
|
||||
): Promise<void> {
|
||||
if (!newName) newName = folder.name;
|
||||
|
||||
breadcrumb.textContent = folder.newName || folder.name;
|
||||
breadcrumb.setAttribute('old-name', folder.name);
|
||||
breadcrumb.setAttribute('data-path', folder.path);
|
||||
}
|
||||
|
||||
getEL(path: string): HTMLElement | null {
|
||||
const fileExplorer = this.getFileExplorer();
|
||||
if(!fileExplorer) { return null; }
|
||||
const fileExplorerItem = fileExplorer.view.fileItems[path];
|
||||
if(!fileExplorerItem) { return null; }
|
||||
if(fileExplorerItem.selfEl) return fileExplorerItem.selfEl;
|
||||
return fileExplorerItem.titleEl;
|
||||
}
|
||||
|
||||
loadFileClasses(forceReload = false) {
|
||||
if(this.activeFileExplorer === this.getFileExplorer() && !forceReload) { return; }
|
||||
this.activeFileExplorer = this.getFileExplorer();
|
||||
this.app.vault.getMarkdownFiles().forEach((file) => {
|
||||
if(file.basename !== file.parent.name) { return; }
|
||||
const excludedFolder = this.getExcludedFolderByPath(file.parent.path);
|
||||
// cleanup after ourselves
|
||||
// Incase settings have changed
|
||||
if(excludedFolder?.disableFolderNote) {
|
||||
this.removeCSSClassFromEL(file.path, 'is-folder-note');
|
||||
this.removeCSSClassFromEL(file.parent.path, 'has-folder-note');
|
||||
return;
|
||||
/**
|
||||
* Updates all folder names in the path above the note editor
|
||||
*/
|
||||
updateAllBreadcrumbs(remove?: boolean): void {
|
||||
if (!this.settings.frontMatterTitle.path && !remove) { return; }
|
||||
const viewHeaderItems = activeDocument.querySelectorAll('span.view-header-breadcrumb');
|
||||
const files = this.app.vault.getAllLoadedFiles().filter((file) => file instanceof TFolder);
|
||||
viewHeaderItems.forEach((item) => {
|
||||
if (!item.hasAttribute('data-path')) { return; }
|
||||
const path = item.getAttribute('data-path');
|
||||
const folder = files.find((file) => file.path === path);
|
||||
if (!(folder instanceof TFolder)) { return; }
|
||||
if (remove) {
|
||||
item.textContent = folder.name;
|
||||
item.removeAttribute('old-name');
|
||||
} else {
|
||||
item.textContent = folder.newName || folder.name;
|
||||
item.setAttribute('old-name', folder.name);
|
||||
item.setAttribute('data-path', folder.path);
|
||||
}
|
||||
this.addCSSClassToTitleEL(file.parent.path, 'has-folder-note');
|
||||
this.addCSSClassToTitleEL(file.path, 'is-folder-note');
|
||||
});
|
||||
}
|
||||
|
||||
onunload() {
|
||||
console.log('unloading folder notes plugin');
|
||||
this.observer.disconnect();
|
||||
document.body.classList.remove('folder-notes-plugin');
|
||||
document.body.classList.remove('folder-note-underline');
|
||||
document.body.classList.remove('hide-folder-note');
|
||||
document.body.classList.remove('fn-whitespace-stop-collapsing');
|
||||
onunload(): void {
|
||||
unregisterFileExplorerObserver();
|
||||
activeDocument.body.classList.remove('folder-notes-plugin');
|
||||
activeDocument.body.classList.remove('folder-note-underline');
|
||||
activeDocument.body.classList.remove('hide-folder-note');
|
||||
activeDocument.body.classList.remove('fn-whitespace-stop-collapsing');
|
||||
removeActiveFolder(this);
|
||||
if (this.fmtpHandler) {
|
||||
this.fmtpHandler.deleteEvent();
|
||||
}
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
async loadSettings(): Promise<void> {
|
||||
const data = await this.loadData() as LegacySettingsData | null;
|
||||
if (data) {
|
||||
if (data.allowWhitespaceCollapsing === true) {
|
||||
data.stopWhitespaceCollapsing = false;
|
||||
delete data.allowWhitespaceCollapsing;
|
||||
} else if (data.allowWhitespaceCollapsing === false) {
|
||||
data.stopWhitespaceCollapsing = true;
|
||||
delete data.allowWhitespaceCollapsing;
|
||||
}
|
||||
}
|
||||
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
|
||||
if (!this.settings.oldFolderNoteName) {
|
||||
this.settings.oldFolderNoteName = this.settings.folderNoteName;
|
||||
}
|
||||
|
||||
if (!data) { return; }
|
||||
const overview = data.defaultOverview;
|
||||
if (!overview) { return; }
|
||||
this.settings.defaultOverview = Object.assign(
|
||||
{}, DEFAULT_SETTINGS.defaultOverview, overview,
|
||||
);
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
async saveSettings(reloadStyles?: boolean): Promise<void> {
|
||||
await this.saveData(this.settings);
|
||||
// cleanup any css if we need too
|
||||
this.loadFileClasses(true);
|
||||
if ((!this.settingsOpened || reloadStyles === true) && reloadStyles !== false) {
|
||||
refreshAllFolderStyles(true, this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
62
src/modals/AddSupportedFileType.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { Modal, Setting, Notice, type App, type SettingTab } from 'obsidian';
|
||||
import type FolderNotesPlugin from '../main';
|
||||
import type { ListComponent } from 'src/functions/ListComponent';
|
||||
|
||||
export default class AddSupportedFileModal extends Modal {
|
||||
plugin: FolderNotesPlugin;
|
||||
app: App;
|
||||
name: string;
|
||||
list: ListComponent;
|
||||
settingsTab: SettingTab;
|
||||
constructor(app: App, plugin: FolderNotesPlugin, settingsTab: SettingTab, list: ListComponent) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.app = app;
|
||||
this.name = '';
|
||||
this.list = list;
|
||||
this.settingsTab = settingsTab;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
// close when user presses enter
|
||||
contentEl.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
contentEl.createEl('h2', { text: 'Extension name' });
|
||||
new Setting(contentEl)
|
||||
.setName('Enter the name of the extension (only the short form, e.g. "md")')
|
||||
.addText((text) =>
|
||||
text
|
||||
.setValue('')
|
||||
.onChange(async (value) => {
|
||||
if (value.trim() !== '') {
|
||||
this.name = value.trim();
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
onClose(): void {
|
||||
if (this.name.toLocaleLowerCase() === 'markdown') {
|
||||
this.name = 'md';
|
||||
}
|
||||
const { contentEl } = this;
|
||||
if (this.name === '') {
|
||||
contentEl.empty();
|
||||
this.settingsTab.display();
|
||||
} else if (this.plugin.settings.supportedFileTypes.includes(this.name.toLowerCase())) {
|
||||
new Notice('This extension is already supported');
|
||||
return;
|
||||
} else {
|
||||
// Run async operations without returning a Promise from onClose
|
||||
void (async (): Promise<void> => {
|
||||
await this.list.addValue(this.name.toLowerCase());
|
||||
this.settingsTab.display();
|
||||
await this.plugin.saveSettings();
|
||||
contentEl.empty();
|
||||
})();
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/modals/AskForExtension.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { FuzzySuggestModal, type TFile } from 'obsidian';
|
||||
import type FolderNotesPlugin from 'src/main';
|
||||
import { createFolderNote } from 'src/functions/folderNoteFunctions';
|
||||
export class AskForExtensionModal extends FuzzySuggestModal<string> {
|
||||
plugin: FolderNotesPlugin;
|
||||
extension: string;
|
||||
folderPath: string;
|
||||
openFile: boolean;
|
||||
useModal: boolean | undefined;
|
||||
existingNote: TFile | undefined;
|
||||
constructor(
|
||||
plugin: FolderNotesPlugin,
|
||||
folderPath: string,
|
||||
openFile: boolean,
|
||||
extension: string,
|
||||
useModal?: boolean,
|
||||
existingNote?: TFile,
|
||||
) {
|
||||
super(plugin.app);
|
||||
this.plugin = plugin;
|
||||
this.folderPath = folderPath;
|
||||
this.extension = extension;
|
||||
this.openFile = openFile;
|
||||
this.useModal = useModal;
|
||||
this.existingNote = existingNote;
|
||||
plugin.askModalCurrentlyOpen = true;
|
||||
}
|
||||
|
||||
getItems(): string[] {
|
||||
return this.plugin.settings.supportedFileTypes.filter(
|
||||
(item) => item.toLowerCase() !== '.ask',
|
||||
);
|
||||
}
|
||||
|
||||
getItemText(item: string): string {
|
||||
return item;
|
||||
}
|
||||
|
||||
onChooseItem(item: string, _evt: MouseEvent | KeyboardEvent): void {
|
||||
this.plugin.askModalCurrentlyOpen = false;
|
||||
this.extension = '.' + item;
|
||||
void createFolderNote(
|
||||
this.plugin,
|
||||
this.folderPath,
|
||||
this.openFile,
|
||||
this.extension,
|
||||
this.useModal,
|
||||
this.existingNote,
|
||||
);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
1
src/modals/CommandSettings.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export {};
|
||||
87
src/modals/DeleteConfirmation.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { Modal, Platform, type App, type TFile } from 'obsidian';
|
||||
import type FolderNotesPlugin from '../main';
|
||||
import { deleteFolderNote } from 'src/functions/folderNoteFunctions';
|
||||
export default class DeleteConfirmationModal extends Modal {
|
||||
plugin: FolderNotesPlugin;
|
||||
app: App;
|
||||
file: TFile;
|
||||
constructor(app: App, plugin: FolderNotesPlugin, file: TFile) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.app = app;
|
||||
this.file = file;
|
||||
}
|
||||
onOpen(): void {
|
||||
const { contentEl, plugin } = this;
|
||||
const modalTitle = contentEl.createDiv({ cls: 'fn-modal-title' });
|
||||
const modalContent = contentEl.createDiv({ cls: 'fn-modal-content' });
|
||||
modalTitle.createEl('h2', { text: 'Delete folder note' });
|
||||
// eslint-disable-next-line max-len
|
||||
modalContent.createEl('p', { text: `Are you sure you want to delete the folder note '${this.file.name}' ?` });
|
||||
switch (plugin.settings.deleteFilesAction) {
|
||||
case 'trash':
|
||||
modalContent.createEl('p', { text: 'It will be moved to your system trash.' });
|
||||
break;
|
||||
case 'obsidianTrash':
|
||||
// eslint-disable-next-line max-len
|
||||
modalContent.createEl('p', { text: 'It will be moved to your Obsidian trash, which is located in the ".trash" hidden folder in your vault.' });
|
||||
break;
|
||||
case 'delete':
|
||||
modalContent
|
||||
.createEl('p', { text: 'It will be permanently deleted.' })
|
||||
.setCssStyles({ color: 'red' });
|
||||
break;
|
||||
}
|
||||
|
||||
const buttonContainer = contentEl.createEl('div', { cls: 'modal-button-container' });
|
||||
|
||||
if (!Platform.isMobile) {
|
||||
const checkbox = buttonContainer.createEl('label', { cls: 'mod-checkbox' });
|
||||
checkbox.tabIndex = -1;
|
||||
const input = checkbox.createEl('input', { type: 'checkbox' });
|
||||
checkbox.appendText('Don\'t ask again');
|
||||
input.addEventListener('change', (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.checked) {
|
||||
plugin.settings.showDeleteConfirmation = false;
|
||||
} else {
|
||||
plugin.settings.showDeleteConfirmation = true;
|
||||
}
|
||||
plugin.saveSettings();
|
||||
});
|
||||
} else {
|
||||
const confirmButton = buttonContainer.createEl('button', {
|
||||
text: 'Delete and don\'t ask again',
|
||||
cls: 'mod-destructive',
|
||||
});
|
||||
confirmButton.addEventListener('click', async () => {
|
||||
plugin.settings.showDeleteConfirmation = false;
|
||||
plugin.saveSettings();
|
||||
this.close();
|
||||
deleteFolderNote(plugin, this.file, false);
|
||||
});
|
||||
}
|
||||
|
||||
const deleteButton = buttonContainer.createEl('button', {
|
||||
text: 'Delete',
|
||||
cls: 'mod-warning',
|
||||
});
|
||||
deleteButton.addEventListener('click', async () => {
|
||||
this.close();
|
||||
deleteFolderNote(plugin, this.file, false);
|
||||
});
|
||||
deleteButton.focus();
|
||||
|
||||
const cancelButton = buttonContainer.createEl('button', {
|
||||
text: 'Cancel',
|
||||
cls: 'mod-cancel',
|
||||
});
|
||||
cancelButton.addEventListener('click', async () => {
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
onClose(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
88
src/modals/ExistingNote.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import {
|
||||
Modal,
|
||||
Setting,
|
||||
Platform,
|
||||
type App,
|
||||
type TFile,
|
||||
type TFolder,
|
||||
type TAbstractFile,
|
||||
} from 'obsidian';
|
||||
import type FolderNotesPlugin from '../main';
|
||||
import { turnIntoFolderNote } from 'src/functions/folderNoteFunctions';
|
||||
export default class ExistingFolderNoteModal extends Modal {
|
||||
plugin: FolderNotesPlugin;
|
||||
app: App;
|
||||
file: TFile;
|
||||
folder: TFolder;
|
||||
folderNote: TAbstractFile;
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: FolderNotesPlugin,
|
||||
file: TFile,
|
||||
folder: TFolder,
|
||||
folderNote: TAbstractFile,
|
||||
) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.app = app;
|
||||
this.file = file;
|
||||
this.folder = folder;
|
||||
this.folderNote = folderNote;
|
||||
}
|
||||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.createEl('h2', { text: 'A folder note for this folder already exists' });
|
||||
const setting = new Setting(contentEl);
|
||||
// eslint-disable-next-line max-len
|
||||
setting.infoEl.createEl('p', { text: 'Are you sure you want to turn the note into a folder note and rename the existing folder note?' });
|
||||
|
||||
setting.infoEl.parentElement?.classList.add('fn-delete-confirmation-modal');
|
||||
|
||||
// Create a container for the buttons and the checkbox
|
||||
// eslint-disable-next-line max-len
|
||||
const buttonContainer = setting.infoEl.createEl('div', { cls: 'fn-delete-confirmation-modal-buttons' });
|
||||
if (Platform.isMobileApp) {
|
||||
const confirmButton = buttonContainer.createEl('button', {
|
||||
text: 'Rename and don\'t ask again',
|
||||
});
|
||||
confirmButton.classList.add('mod-warning', 'fn-confirmation-modal-button');
|
||||
confirmButton.addEventListener('click', () => {
|
||||
this.plugin.settings.showRenameConfirmation = false;
|
||||
void this.plugin.saveSettings();
|
||||
this.close();
|
||||
turnIntoFolderNote(this.plugin, this.file, this.folder, this.folderNote, true);
|
||||
});
|
||||
} else {
|
||||
const checkbox = buttonContainer.createEl('input', { type: 'checkbox' });
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.checked) {
|
||||
this.plugin.settings.showRenameConfirmation = false;
|
||||
} else {
|
||||
this.plugin.settings.showRenameConfirmation = true;
|
||||
}
|
||||
});
|
||||
const checkBoxText = buttonContainer.createEl('span', { text: 'Don\'t ask again' });
|
||||
checkBoxText.addEventListener('click', () => {
|
||||
checkbox.click();
|
||||
});
|
||||
}
|
||||
const button = buttonContainer.createEl('button', { text: 'Rename' });
|
||||
button.classList.add('mod-warning', 'fn-confirmation-modal-button');
|
||||
button.addEventListener('click', () => {
|
||||
void this.plugin.saveSettings();
|
||||
this.close();
|
||||
turnIntoFolderNote(this.plugin, this.file, this.folder, this.folderNote, true);
|
||||
});
|
||||
button.focus();
|
||||
const cancelButton = buttonContainer.createEl('button', { text: 'Cancel' });
|
||||
cancelButton.addEventListener('click', () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
}
|
||||
onClose(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { App, Modal, Setting, TFolder } from 'obsidian';
|
||||
import FolderNotesPlugin from '../main';
|
||||
import { Modal, Setting, type App, type TFolder } from 'obsidian';
|
||||
import type FolderNotesPlugin from '../main';
|
||||
export default class FolderNameModal extends Modal {
|
||||
plugin: FolderNotesPlugin;
|
||||
app: App;
|
||||
|
|
@ -10,7 +10,8 @@ export default class FolderNameModal extends Modal {
|
|||
this.app = app;
|
||||
this.folder = folder;
|
||||
}
|
||||
onOpen() {
|
||||
|
||||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
// close when user presses enter
|
||||
contentEl.addEventListener('keydown', (e) => {
|
||||
|
|
@ -23,17 +24,28 @@ export default class FolderNameModal extends Modal {
|
|||
.setName('Enter the name of the folder')
|
||||
.addText((text) =>
|
||||
text
|
||||
.setValue(this.folder.name.replace('.md', ''))
|
||||
.setValue(this.folder.name.replace(this.plugin.settings.folderNoteType, ''))
|
||||
.onChange(async (value) => {
|
||||
if (value.trim() !== '') {
|
||||
if (!this.app.vault.getAbstractFileByPath(this.folder.path.slice(0, this.folder.path.lastIndexOf('/') + 1) + value.trim())) {
|
||||
this.app.vault.rename(this.folder, this.folder.path.slice(0, this.folder.path.lastIndexOf('/') + 1) + value.trim());
|
||||
const parentPath = this.folder.path.slice(
|
||||
0,
|
||||
this.folder.path.lastIndexOf('/') + 1,
|
||||
);
|
||||
const newFolderPath = parentPath + value.trim();
|
||||
if (
|
||||
!this.app.vault.getAbstractFileByPath(newFolderPath)
|
||||
) {
|
||||
this.plugin.app.fileManager.renameFile(
|
||||
this.folder,
|
||||
newFolderPath,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
onClose() {
|
||||
|
||||
onClose(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
83
src/modals/NewFolderName.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { Modal, type App, type TFolder } from 'obsidian';
|
||||
import type FolderNotesPlugin from '../main';
|
||||
export default class NewFolderNameModal extends Modal {
|
||||
plugin: FolderNotesPlugin;
|
||||
app: App;
|
||||
folder: TFolder;
|
||||
constructor(app: App, plugin: FolderNotesPlugin, folder: TFolder) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.app = app;
|
||||
this.folder = folder;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
|
||||
contentEl.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.saveFolderName();
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
this.modalEl.classList.add('mod-file-rename');
|
||||
const modalTitle = this.modalEl.querySelector('div.modal-title');
|
||||
if (modalTitle) {
|
||||
modalTitle.textContent = 'Folder title';
|
||||
}
|
||||
|
||||
const textarea = contentEl.createEl('textarea', {
|
||||
text: this.folder.name.replace(this.plugin.settings.folderNoteType, ''),
|
||||
attr: {
|
||||
placeholder: 'Enter the name of the folder',
|
||||
rows: '1',
|
||||
spellcheck: 'false',
|
||||
class: 'rename-textarea',
|
||||
},
|
||||
});
|
||||
|
||||
textarea.addEventListener('focus', function () {
|
||||
this.select();
|
||||
});
|
||||
|
||||
textarea.focus();
|
||||
|
||||
const buttonContainer = this.modalEl.createDiv({ cls: 'modal-button-container' });
|
||||
const saveButton = buttonContainer.createEl('button', { text: 'Save', cls: 'mod-cta' });
|
||||
saveButton.addEventListener('click', async () => {
|
||||
this.saveFolderName();
|
||||
this.close();
|
||||
});
|
||||
|
||||
const cancelButton = buttonContainer.createEl('button', {
|
||||
text: 'Cancel',
|
||||
cls: 'mod-cancel',
|
||||
});
|
||||
cancelButton.addEventListener('click', () => {
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
|
||||
saveFolderName(): void {
|
||||
const textarea = this.contentEl.querySelector('textarea');
|
||||
if (textarea) {
|
||||
const newName = textarea.value.trim();
|
||||
if (newName.trim() !== '') {
|
||||
const folderBasePath = this.folder.path.slice(
|
||||
0,
|
||||
this.folder.path.lastIndexOf('/') + 1,
|
||||
);
|
||||
const newFolderPath = folderBasePath + newName.trim();
|
||||
if (!this.app.vault.getAbstractFileByPath(newFolderPath)) {
|
||||
this.plugin.app.fileManager.renameFile(this.folder, newFolderPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import { App, Modal, Setting, TFolder } from 'obsidian';
|
||||
import FolderNotesPlugin from '../main';
|
||||
export default class ConfirmationModal extends Modal {
|
||||
plugin: FolderNotesPlugin;
|
||||
app: App;
|
||||
folder: TFolder;
|
||||
constructor(app: App, plugin: FolderNotesPlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.app = app;
|
||||
}
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.createEl('h2', { text: 'Create folder note for every folder' });
|
||||
const setting = new Setting(contentEl);
|
||||
setting.infoEl.createEl('p', { text: 'Make sure to backup your vault before using this feature.' }).style.color = '#fb464c';
|
||||
setting.infoEl.createEl('p', { text: 'This feature will create a folder note for every folder in your vault.' });
|
||||
setting.infoEl.createEl('p', { text: 'Every folder that already has a folder note will be ignored.' });
|
||||
setting.infoEl.createEl('p', { text: 'Every excluded folder will be ignored.' });
|
||||
setting.infoEl.parentElement?.classList.add('fn-confirmation-modal');
|
||||
const button = setting.infoEl.createEl('button', { text: 'Create' });
|
||||
button.classList.add('mod-warning', 'fn-confirmation-modal-button');
|
||||
button.addEventListener('click', async () => {
|
||||
this.close();
|
||||
this.app.vault.getAllLoadedFiles().forEach(async (folder) => {
|
||||
if (folder instanceof TFolder) {
|
||||
const excludedFolder = this.plugin.settings.excludeFolders.find(
|
||||
(excludedFolder) => (excludedFolder.path === folder.path) ||
|
||||
(excludedFolder.path === folder.path?.slice(0, folder?.path.lastIndexOf('/') >= 0 ? folder.path?.lastIndexOf('/') : folder.path.length)
|
||||
&& excludedFolder.subFolders));
|
||||
if (excludedFolder) return;
|
||||
if (this.app.vault.getAbstractFileByPath(folder.path + '/' + folder.name + '.md')) return;
|
||||
await this.plugin.createFolderNote(folder.path + '/' + folder.name + '.md', true);
|
||||
}
|
||||
});
|
||||
});
|
||||
button.focus();
|
||||
const cancelButton = setting.infoEl.createEl('button', { text: 'Cancel' });
|
||||
cancelButton.addEventListener('click', async () => {
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
import { App, Modal, Setting, TFile, Platform } from 'obsidian';
|
||||
import FolderNotesPlugin from '../main';
|
||||
export default class DeleteConfirmationModal extends Modal {
|
||||
plugin: FolderNotesPlugin;
|
||||
app: App;
|
||||
file: TFile;
|
||||
constructor(app: App, plugin: FolderNotesPlugin, file: TFile) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.app = app;
|
||||
this.file = file;
|
||||
}
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.createEl('h2', { text: 'Delete folder note' });
|
||||
const setting = new Setting(contentEl);
|
||||
setting.infoEl.createEl('p', { text: `Are you sure you want to delete the folder note "${this.file.name}" ?` });
|
||||
setting.infoEl.createEl('p', { text: 'It will be moved to your system trash.' });
|
||||
|
||||
setting.infoEl.parentElement?.classList.add('fn-delete-confirmation-modal');
|
||||
|
||||
// Create a container for the buttons and the checkbox
|
||||
const buttonContainer = setting.infoEl.createEl('div', { cls: 'fn-delete-confirmation-modal-buttons' });
|
||||
if (Platform.isMobileApp) {
|
||||
const confirmButton = buttonContainer.createEl('button', { text: 'Delete and don\'t ask again' });
|
||||
confirmButton.classList.add('mod-warning', 'fn-confirmation-modal-button');
|
||||
confirmButton.addEventListener('click', async () => {
|
||||
this.plugin.settings.showDeleteConfirmation = false;
|
||||
this.plugin.saveSettings();
|
||||
this.close();
|
||||
this.app.vault.delete(this.file);
|
||||
});
|
||||
} else {
|
||||
const checkbox = buttonContainer.createEl('input', { type: 'checkbox' });
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.checked) {
|
||||
this.plugin.settings.showDeleteConfirmation = false;
|
||||
} else {
|
||||
this.plugin.settings.showDeleteConfirmation = true;
|
||||
}
|
||||
this.plugin.saveSettings();
|
||||
});
|
||||
const checkBoxText = buttonContainer.createEl('span', { text: 'Don\'t ask again' });
|
||||
checkBoxText.addEventListener('click', () => {
|
||||
checkbox.click();
|
||||
});
|
||||
}
|
||||
const button = buttonContainer.createEl('button', { text: 'Delete' });
|
||||
button.classList.add('mod-warning', 'fn-confirmation-modal-button');
|
||||
button.addEventListener('click', async () => {
|
||||
this.close();
|
||||
this.app.vault.delete(this.file);
|
||||
});
|
||||
button.focus();
|
||||
const cancelButton = buttonContainer.createEl('button', { text: 'Cancel' });
|
||||
cancelButton.addEventListener('click', async () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
}
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
1
src/obsidian-folder-overview
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit f047859cc9a7f1c04ad35ecdc440a31a300bc6c2
|
||||
309
src/settings.ts
|
|
@ -1,309 +0,0 @@
|
|||
import { App, Platform, PluginSettingTab, Setting } from 'obsidian';
|
||||
import FolderNotesPlugin from './main';
|
||||
import { FolderSuggest } from './suggesters/FolderSuggester';
|
||||
import ExcludedFolderSettings from './modals/exludeFolderSettings';
|
||||
import { TemplateSuggest } from './suggesters/templateSuggester';
|
||||
// import ConfirmationModal from "./modals/confirmCreation";
|
||||
export interface FolderNotesSettings {
|
||||
syncFolderName: boolean;
|
||||
ctrlKey: boolean;
|
||||
altKey: boolean;
|
||||
hideFolderNote: boolean;
|
||||
templatePath: string;
|
||||
autoCreate: boolean;
|
||||
enableCollapsing: boolean;
|
||||
excludeFolders: ExcludedFolder[];
|
||||
showDeleteConfirmation: boolean;
|
||||
underlineFolder: boolean;
|
||||
allowWhitespaceCollapsing: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: FolderNotesSettings = {
|
||||
syncFolderName: true,
|
||||
ctrlKey: true,
|
||||
altKey: false,
|
||||
hideFolderNote: false,
|
||||
templatePath: '',
|
||||
autoCreate: false,
|
||||
enableCollapsing: false,
|
||||
excludeFolders: [],
|
||||
showDeleteConfirmation: true,
|
||||
underlineFolder: true,
|
||||
allowWhitespaceCollapsing: false,
|
||||
};
|
||||
export class SettingsTab extends PluginSettingTab {
|
||||
plugin: FolderNotesPlugin;
|
||||
app: App;
|
||||
excludeFolders: ExcludedFolder[];
|
||||
constructor(app: App, plugin: FolderNotesPlugin) {
|
||||
super(app, plugin);
|
||||
}
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl('h2', { text: 'Folder notes settings' });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Disable folder collapsing')
|
||||
.setDesc('Disable the ability to collapse folders by clicking on the folder name')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(!this.plugin.settings.enableCollapsing)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.enableCollapsing = !value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Don\'t collapse folder when clicking on the whitespace around the folder name')
|
||||
.setDesc('Disable the ability to collapse folders by clicking on the whitespace around the folder name.')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(!this.plugin.settings.allowWhitespaceCollapsing)
|
||||
.onChange(async (value) => {
|
||||
if (value) {
|
||||
document.body.classList.add('fn-whitespace-stop-collapsing');
|
||||
} else {
|
||||
document.body.classList.remove('fn-whitespace-stop-collapsing');
|
||||
}
|
||||
this.plugin.settings.allowWhitespaceCollapsing = !value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Hide folder note')
|
||||
.setDesc('Hide the folder note in the file explorer')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.hideFolderNote)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hideFolderNote = value;
|
||||
await this.plugin.saveSettings();
|
||||
if (value) {
|
||||
document.body.classList.add('hide-folder-note');
|
||||
} else {
|
||||
document.body.classList.remove('hide-folder-note');
|
||||
}
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName('Sync folder name')
|
||||
.setDesc('Automatically rename the folder note when the folder name is changed')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.syncFolderName)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.syncFolderName = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName('Auto create folder note')
|
||||
.setDesc('Automatically create a folder note when a new folder is created')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.autoCreate)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoCreate = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
if (Platform.isDesktopApp) {
|
||||
new Setting(containerEl)
|
||||
.setName('Key for creating folder note')
|
||||
.setDesc('The key combination to create a folder note')
|
||||
.addDropdown((dropdown) => {
|
||||
if(!Platform.isMacOS) {
|
||||
dropdown.addOption('ctrl', 'Ctrl + Click');
|
||||
} else {
|
||||
dropdown.addOption('ctrl', 'Cmd + Click');
|
||||
}
|
||||
dropdown.addOption('alt', 'Alt + Click');
|
||||
dropdown.setValue(this.plugin.settings.ctrlKey ? 'ctrl' : 'alt');
|
||||
dropdown.onChange(async (value) => {
|
||||
this.plugin.settings.ctrlKey = value === 'ctrl';
|
||||
this.plugin.settings.altKey = value === 'alt';
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Add underline to folders with folder notes')
|
||||
.setDesc('Add an underline to folders that have a folder note')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.underlineFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.underlineFolder = value;
|
||||
if (value) {
|
||||
document.body.classList.add('folder-note-underline');
|
||||
} else {
|
||||
document.body.classList.remove('folder-note-underline');
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Template path')
|
||||
.setDesc('The path to the template file')
|
||||
.addSearch((cb) => {
|
||||
new TemplateSuggest(cb.inputEl, this.plugin);
|
||||
cb.setPlaceholder('Template path');
|
||||
cb.setValue(this.plugin.app.vault.getAbstractFileByPath(this.plugin.settings.templatePath)?.name.replace('.md', '') || '');
|
||||
cb.onChange(async (value) => {
|
||||
if (value.trim() === '') {
|
||||
this.plugin.settings.templatePath = '';
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Due to issue with templater it'll be disabled for now
|
||||
// If you want to try it yourself make a pr
|
||||
// The issue was that it only used the first folder for all of the other folder notes
|
||||
/*
|
||||
new Setting(containerEl)
|
||||
.setName('Create folder note for every folder')
|
||||
.setDesc('Create a folder note for every folder in the vault')
|
||||
.addButton((cb) => {
|
||||
cb.setIcon('plus');
|
||||
cb.setTooltip('Create folder notes');
|
||||
cb.onClick(async () => {
|
||||
new ConfirmationModal(this.app, this.plugin).open();
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
|
||||
new Setting(containerEl)
|
||||
.setHeading()
|
||||
.setName('Manage excluded folders');
|
||||
new Setting(containerEl)
|
||||
.setName('Add excluded folder')
|
||||
.addButton((cb) => {
|
||||
cb.setIcon('plus');
|
||||
cb.setClass('add-exclude-folder');
|
||||
cb.setTooltip('Add excluded folder');
|
||||
cb.onClick(() => {
|
||||
const excludedFolder = new ExcludedFolder('', this.plugin.settings.excludeFolders.length);
|
||||
this.addExcludeFolderListItem(containerEl, excludedFolder);
|
||||
this.addExcludedFolder(excludedFolder);
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
this.plugin.settings.excludeFolders.sort((a, b) => a.position - b.position).forEach((excludedFolder) => {
|
||||
this.addExcludeFolderListItem(containerEl, excludedFolder);
|
||||
});
|
||||
}
|
||||
addExcludeFolderListItem(containerEl: HTMLElement, excludedFolder: ExcludedFolder) {
|
||||
const setting = new Setting(containerEl);
|
||||
setting.setClass('fn-exclude-folder-list-item');
|
||||
setting.addSearch((cb) => {
|
||||
new FolderSuggest(
|
||||
cb.inputEl,
|
||||
this.plugin
|
||||
);
|
||||
// @ts-ignore
|
||||
cb.containerEl.addClass('fn-exclude-folder-path');
|
||||
cb.setPlaceholder('Folder path');
|
||||
cb.setValue(excludedFolder.path);
|
||||
cb.onChange((value) => {
|
||||
if (!this.app.vault.getAbstractFileByPath(value)) return;
|
||||
excludedFolder.path = value;
|
||||
this.updateExcludedFolder(excludedFolder, excludedFolder);
|
||||
});
|
||||
});
|
||||
setting.addButton((cb) => {
|
||||
cb.setIcon('edit');
|
||||
cb.setTooltip('Edit folder note');
|
||||
cb.onClick(() => {
|
||||
new ExcludedFolderSettings(this.app, this.plugin, excludedFolder).open();
|
||||
});
|
||||
});
|
||||
|
||||
setting.addButton((cb) => {
|
||||
cb.setIcon('up-chevron-glyph');
|
||||
cb.setTooltip('Move up');
|
||||
cb.onClick(() => {
|
||||
if (excludedFolder.position === 0) return;
|
||||
excludedFolder.position -= 1;
|
||||
this.updateExcludedFolder(excludedFolder, excludedFolder);
|
||||
const oldExcludedFolder = this.plugin.settings.excludeFolders.find((folder) => folder.position === excludedFolder.position);
|
||||
if (oldExcludedFolder) {
|
||||
|
||||
oldExcludedFolder.position += 1;
|
||||
this.updateExcludedFolder(oldExcludedFolder, oldExcludedFolder);
|
||||
}
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
setting.addButton((cb) => {
|
||||
cb.setIcon('down-chevron-glyph');
|
||||
cb.setTooltip('Move down');
|
||||
cb.onClick(() => {
|
||||
if (excludedFolder.position === this.plugin.settings.excludeFolders.length - 1) return;
|
||||
excludedFolder.position += 1;
|
||||
this.updateExcludedFolder(excludedFolder, excludedFolder);
|
||||
const oldExcludedFolder = this.plugin.settings.excludeFolders.find((folder) => folder.position === excludedFolder.position);
|
||||
if (oldExcludedFolder) {
|
||||
oldExcludedFolder.position -= 1;
|
||||
this.updateExcludedFolder(oldExcludedFolder, oldExcludedFolder);
|
||||
}
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
setting.addButton((cb) => {
|
||||
cb.setIcon('trash-2');
|
||||
cb.setTooltip('Delete excluded folder');
|
||||
cb.onClick(() => {
|
||||
this.deleteExcludedFolder(excludedFolder);
|
||||
setting.clear();
|
||||
setting.settingEl.remove();
|
||||
});
|
||||
});
|
||||
}
|
||||
addExcludedFolder(excludedFolder: ExcludedFolder) {
|
||||
this.plugin.settings.excludeFolders.push(excludedFolder);
|
||||
this.plugin.saveSettings();
|
||||
}
|
||||
deleteExcludedFolder(excludedFolder: ExcludedFolder) {
|
||||
this.plugin.settings.excludeFolders = this.plugin.settings.excludeFolders.filter((folder) => folder.path !== excludedFolder.path);
|
||||
this.plugin.saveSettings();
|
||||
}
|
||||
updateExcludedFolder(excludedFolder: ExcludedFolder, newExcludeFolder: ExcludedFolder) {
|
||||
this.plugin.settings.excludeFolders = this.plugin.settings.excludeFolders.filter((folder) => folder.path !== excludedFolder.path);
|
||||
this.addExcludedFolder(newExcludeFolder);
|
||||
}
|
||||
|
||||
}
|
||||
export class ExcludedFolder {
|
||||
path: string;
|
||||
subFolders: boolean;
|
||||
disableSync: boolean;
|
||||
disableAutoCreate: boolean;
|
||||
disableFolderNote: boolean;
|
||||
enableCollapsing: boolean;
|
||||
position: number;
|
||||
constructor(path: string, position: number) {
|
||||
this.path = path;
|
||||
this.subFolders = true;
|
||||
this.disableSync = true;
|
||||
this.disableAutoCreate = true;
|
||||
this.disableFolderNote = false;
|
||||
this.enableCollapsing = false;
|
||||
this.position = position;
|
||||
}
|
||||
}
|
||||
113
src/settings/ExcludedFoldersSettings.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import {
|
||||
addExcludeFolderListItem,
|
||||
addExcludedFolder,
|
||||
} from 'src/ExcludeFolders/functions/folderFunctions';
|
||||
import { ExcludedFolder } from 'src/ExcludeFolders/ExcludeFolder';
|
||||
import { addExcludePatternListItem } from 'src/ExcludeFolders/functions/patternFunctions';
|
||||
import { Setting } from 'obsidian';
|
||||
import type { SettingsTab } from './SettingsTab';
|
||||
import ExcludedFolderSettings from 'src/ExcludeFolders/modals/ExcludeFolderSettings';
|
||||
import PatternSettings from 'src/ExcludeFolders/modals/PatternSettings';
|
||||
import WhitelistedFoldersSettings from 'src/ExcludeFolders/modals/WhitelistedFoldersSettings';
|
||||
// import ExcludedFoldersWhitelist from 'src/ExcludeFolders/modals/WhitelistModal';
|
||||
|
||||
export async function renderExcludeFolders(settingsTab: SettingsTab): Promise<void> {
|
||||
const containerEl = settingsTab.settingsPage;
|
||||
const manageExcluded = new Setting(containerEl)
|
||||
.setHeading()
|
||||
.setClass('fn-excluded-folder-heading')
|
||||
.setName('Manage excluded folders');
|
||||
const desc3 = document.createDocumentFragment();
|
||||
desc3.append(
|
||||
'Add {regex} at the beginning of the folder name to use a regex pattern.',
|
||||
desc3.createEl('br'),
|
||||
'Use * before and after to exclude folders that include the name between the *s.',
|
||||
desc3.createEl('br'),
|
||||
'Use * before the folder name to exclude folders that end with the folder name.',
|
||||
desc3.createEl('br'),
|
||||
'Use * after the folder name to exclude folders that start with the folder name.',
|
||||
);
|
||||
manageExcluded.setDesc(desc3);
|
||||
|
||||
manageExcluded.infoEl.appendText('The regexes and wildcards are only for the folder name, not the path.');
|
||||
manageExcluded.infoEl.createEl('br');
|
||||
|
||||
manageExcluded.infoEl.appendText('If you want to switch to a folder path delete the pattern first.');
|
||||
|
||||
manageExcluded.infoEl.style.color = settingsTab.app.vault.getConfig('accentColor') as string || '#7d5bed';
|
||||
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Whitelisted folders')
|
||||
.setDesc('Folders that override the excluded folders/patterns')
|
||||
.addButton((cb) => {
|
||||
cb.setButtonText('Manage');
|
||||
cb.setCta();
|
||||
cb.onClick(async () => {
|
||||
new WhitelistedFoldersSettings(settingsTab).open();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Exclude folder default settings')
|
||||
.addButton((cb) => {
|
||||
cb.setButtonText('Manage');
|
||||
cb.setCta();
|
||||
cb.onClick(async () => {
|
||||
new ExcludedFolderSettings(
|
||||
settingsTab.app,
|
||||
settingsTab.plugin,
|
||||
settingsTab.plugin.settings.excludeFolderDefaultSettings,
|
||||
).open();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Exclude pattern default settings')
|
||||
.addButton((cb) => {
|
||||
cb.setButtonText('Manage');
|
||||
cb.setCta();
|
||||
cb.onClick(async () => {
|
||||
new PatternSettings(
|
||||
settingsTab.app,
|
||||
settingsTab.plugin,
|
||||
settingsTab.plugin.settings.excludePatternDefaultSettings,
|
||||
).open();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Add excluded folder')
|
||||
.setClass('add-exclude-folder-item')
|
||||
.addButton((cb) => {
|
||||
cb.setIcon('plus');
|
||||
cb.setClass('add-exclude-folder');
|
||||
cb.setTooltip('Add excluded folder');
|
||||
cb.onClick(() => {
|
||||
const excludedFolder = new ExcludedFolder(
|
||||
'',
|
||||
settingsTab.plugin.settings.excludeFolders.length,
|
||||
undefined,
|
||||
settingsTab.plugin,
|
||||
);
|
||||
addExcludeFolderListItem(settingsTab, containerEl, excludedFolder);
|
||||
addExcludedFolder(settingsTab.plugin, excludedFolder);
|
||||
settingsTab.renderSettingsPage(settingsTab.plugin.settings.settingsTab);
|
||||
});
|
||||
});
|
||||
|
||||
settingsTab.plugin.settings.excludeFolders
|
||||
.filter((folder) => !folder.hideInSettings)
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.forEach((excludedFolder) => {
|
||||
if (
|
||||
excludedFolder.string?.trim() !== '' &&
|
||||
excludedFolder.path?.trim() === ''
|
||||
) {
|
||||
addExcludePatternListItem(settingsTab, containerEl, excludedFolder);
|
||||
} else {
|
||||
addExcludeFolderListItem(settingsTab, containerEl, excludedFolder);
|
||||
}
|
||||
});
|
||||
}
|
||||
235
src/settings/FileExplorerSettings.ts
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
/* eslint-disable max-len */
|
||||
import { Setting } from 'obsidian';
|
||||
import type { SettingsTab } from './SettingsTab';
|
||||
export async function renderFileExplorer(settingsTab: SettingsTab): Promise<void> {
|
||||
const containerEl = settingsTab.settingsPage;
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Hide folder note')
|
||||
.setDesc('Hide the folder note file from appearing in the file explorer')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settingsTab.plugin.settings.hideFolderNote)
|
||||
.onChange(async (value) => {
|
||||
settingsTab.plugin.settings.hideFolderNote = value;
|
||||
await settingsTab.plugin.saveSettings();
|
||||
if (value) {
|
||||
activeDocument.body.classList.add('hide-folder-note');
|
||||
} else {
|
||||
activeDocument.body.classList.remove('hide-folder-note');
|
||||
}
|
||||
settingsTab.display();
|
||||
}),
|
||||
);
|
||||
|
||||
const setting2 = new Setting(containerEl)
|
||||
.setName('Disable click-to-open folder note on mobile')
|
||||
.setDesc('Prevents folder notes from opening when tapping the folder name or surrounding area on mobile devices. They can now only be opened via the context menu or a command.')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settingsTab.plugin.settings.disableOpenFolderNoteOnClick)
|
||||
.onChange(async (value) => {
|
||||
settingsTab.plugin.settings.disableOpenFolderNoteOnClick = value;
|
||||
await settingsTab.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
setting2.infoEl.appendText('Requires a restart to take effect');
|
||||
const setting2AccentColor = settingsTab.app.vault.getConfig('accentColor') as string || '#7d5bed';
|
||||
setting2.infoEl.style.color = setting2AccentColor;
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Open folder notes by only clicking directly on the folder name')
|
||||
.setDesc('Only allow folder notes to open when clicking directly on the folder name in the file explorer')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(!settingsTab.plugin.settings.stopWhitespaceCollapsing)
|
||||
.onChange(async (value) => {
|
||||
if (!value) {
|
||||
activeDocument.body.classList.add('fn-whitespace-stop-collapsing');
|
||||
} else {
|
||||
activeDocument.body.classList.remove('fn-whitespace-stop-collapsing');
|
||||
}
|
||||
settingsTab.plugin.settings.stopWhitespaceCollapsing = !value;
|
||||
await settingsTab.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
const disableSetting = new Setting(containerEl);
|
||||
disableSetting.setName('Disable folder collapsing');
|
||||
disableSetting.setDesc('When enabled, folders in the file explorer will only collapse when clicking the collapse icon next to the folder name, not when clicking near a folder name when it has a folder note.');
|
||||
disableSetting.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(!settingsTab.plugin.settings.enableCollapsing)
|
||||
.onChange(async (value) => {
|
||||
settingsTab.plugin.settings.enableCollapsing = !value;
|
||||
await settingsTab.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
disableSetting.infoEl.appendText('Requires a restart to take effect');
|
||||
const accentColor = settingsTab.app.vault.getConfig('accentColor') as string || '#7d5bed';
|
||||
disableSetting.infoEl.style.color = accentColor;
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Use submenus')
|
||||
.setDesc('Use submenus for file/folder commands')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settingsTab.plugin.settings.useSubmenus)
|
||||
.onChange(async (value) => {
|
||||
settingsTab.plugin.settings.useSubmenus = value;
|
||||
await settingsTab.plugin.saveSettings();
|
||||
settingsTab.display();
|
||||
}),
|
||||
);
|
||||
|
||||
if (settingsTab.plugin.settings.frontMatterTitle.enabled) {
|
||||
new Setting(containerEl)
|
||||
.setName('Auto update folder name in the file explorer (front matter title plugin only)')
|
||||
.setDesc('Automatically update the folder name in the file explorer when the front matter title plugin is enabled and the title for a folder note is changed in the front matter. This will not change the file name, only the displayed name in the file explorer.')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settingsTab.plugin.settings.frontMatterTitle.explorer)
|
||||
.onChange(async (value) => {
|
||||
settingsTab.plugin.settings.frontMatterTitle.explorer = value;
|
||||
await settingsTab.plugin.saveSettings();
|
||||
settingsTab.plugin.app.vault.getFiles().forEach((file) => {
|
||||
settingsTab.plugin.fmtpHandler?.fmptUpdateFileName(
|
||||
{
|
||||
id: '',
|
||||
result: false,
|
||||
path: file.path,
|
||||
pathOnly: false,
|
||||
},
|
||||
false,
|
||||
);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
settingsTab.settingsPage.createEl('h3', { text: 'Style settings' });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Highlight folder in the file explorer')
|
||||
.setDesc('Highlight the folder in the file explorer when it has a folder note and the folder note is open in the editor')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settingsTab.plugin.settings.highlightFolder)
|
||||
.onChange(async (value) => {
|
||||
settingsTab.plugin.settings.highlightFolder = value;
|
||||
if (!value) {
|
||||
activeDocument.body.classList.add('disable-folder-highlight');
|
||||
} else {
|
||||
activeDocument.body.classList.remove('disable-folder-highlight');
|
||||
}
|
||||
await settingsTab.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Hide collapse icon')
|
||||
.setDesc('Hide the collapse icon in the file explorer next to the name of a folder when a folder only contains a folder note')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settingsTab.plugin.settings.hideCollapsingIcon)
|
||||
.onChange(async (value) => {
|
||||
settingsTab.plugin.settings.hideCollapsingIcon = value;
|
||||
if (value) {
|
||||
activeDocument.body.classList.add('fn-hide-collapse-icon');
|
||||
} else {
|
||||
activeDocument.body.classList.remove('fn-hide-collapse-icon');
|
||||
}
|
||||
await settingsTab.plugin.saveSettings();
|
||||
settingsTab.display();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Hide collapse icon for every empty folder')
|
||||
.setDesc('Hide the collapse icon in the file explorer next to the name of a folder when a folder is empty')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settingsTab.plugin.settings.hideCollapsingIconForEmptyFolders)
|
||||
.onChange(async (value) => {
|
||||
settingsTab.plugin.settings.hideCollapsingIconForEmptyFolders = value;
|
||||
await settingsTab.plugin.saveSettings();
|
||||
if (value) {
|
||||
activeDocument.body.classList.add('fn-hide-empty-collapse-icon');
|
||||
} else {
|
||||
activeDocument.body.classList.remove('fn-hide-empty-collapse-icon');
|
||||
}
|
||||
settingsTab.display();
|
||||
},
|
||||
));
|
||||
|
||||
if (settingsTab.plugin.settings.hideCollapsingIcon) {
|
||||
new Setting(containerEl)
|
||||
.setName('Hide collapse icon also when only the attachment folder is in the same folder')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settingsTab.plugin.settings.ignoreAttachmentFolder)
|
||||
.onChange(async (value) => {
|
||||
if (value) {
|
||||
activeDocument.body.classList.add('fn-ignore-attachment-folder');
|
||||
} else {
|
||||
activeDocument.body.classList.remove('fn-ignore-attachment-folder');
|
||||
}
|
||||
settingsTab.plugin.settings.ignoreAttachmentFolder = value;
|
||||
await settingsTab.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Underline the name of folder notes')
|
||||
.setDesc('Add an underline to folders that have a folder note in the file explorer')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settingsTab.plugin.settings.underlineFolder)
|
||||
.onChange(async (value) => {
|
||||
settingsTab.plugin.settings.underlineFolder = value;
|
||||
if (value) {
|
||||
activeDocument.body.classList.add('folder-note-underline');
|
||||
} else {
|
||||
activeDocument.body.classList.remove('folder-note-underline');
|
||||
}
|
||||
await settingsTab.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Bold the name of folder notes')
|
||||
.setDesc('Make the folder name bold in the file explorer when it has a folder note')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settingsTab.plugin.settings.boldName)
|
||||
.onChange(async (value) => {
|
||||
settingsTab.plugin.settings.boldName = value;
|
||||
if (value) {
|
||||
activeDocument.body.classList.add('folder-note-bold');
|
||||
} else {
|
||||
activeDocument.body.classList.remove('folder-note-bold');
|
||||
}
|
||||
await settingsTab.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Cursive the name of folder notes')
|
||||
.setDesc('Make the folder name cursive in the file explorer when it has a folder note')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settingsTab.plugin.settings.cursiveName)
|
||||
.onChange(async (value) => {
|
||||
settingsTab.plugin.settings.cursiveName = value;
|
||||
if (value) {
|
||||
activeDocument.body.classList.add('folder-note-cursive');
|
||||
} else {
|
||||
activeDocument.body.classList.remove('folder-note-cursive');
|
||||
}
|
||||
await settingsTab.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
}
|
||||
51
src/settings/FolderOverviewSettings.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { Setting } from 'obsidian';
|
||||
import type { SettingsTab } from './SettingsTab';
|
||||
import { createOverviewSettings } from 'src/obsidian-folder-overview/src/settings';
|
||||
|
||||
export async function renderFolderOverview(settingsTab: SettingsTab): Promise<void> {
|
||||
const { plugin } = settingsTab;
|
||||
const defaultOverviewSettings = plugin.settings.defaultOverview;
|
||||
const containerEl = settingsTab.settingsPage;
|
||||
|
||||
containerEl.createEl('h3', { text: 'Global settings' });
|
||||
new Setting(containerEl)
|
||||
.setName('Auto-update links without opening the overview')
|
||||
// eslint-disable-next-line max-len
|
||||
.setDesc('If enabled, the links that appear in the graph view will be updated even when you don\'t have the overview open somewhere.')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(plugin.settings.fvGlobalSettings.autoUpdateLinks)
|
||||
.onChange(async (value) => {
|
||||
plugin.settings.fvGlobalSettings.autoUpdateLinks = value;
|
||||
await plugin.saveSettings();
|
||||
if (value) {
|
||||
plugin.fvIndexDB.init(true);
|
||||
} else {
|
||||
plugin.fvIndexDB.active = false;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
containerEl.createEl('h3', { text: 'Overviews default settings' });
|
||||
const pEl = containerEl.createEl('p', {
|
||||
text: 'Edit the default settings for new folder overviews, ',
|
||||
cls: 'setting-item-description',
|
||||
});
|
||||
const span = createSpan({ text: "this won't apply to already existing overviews.", cls: '' });
|
||||
const accentColor = (settingsTab.app.vault.getConfig('accentColor') as string) || '#7d5bed';
|
||||
span.setAttr('style', `color: ${accentColor};`);
|
||||
pEl.appendChild(span);
|
||||
|
||||
void createOverviewSettings(
|
||||
containerEl,
|
||||
defaultOverviewSettings,
|
||||
plugin,
|
||||
plugin.settings.defaultOverview,
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
settingsTab.display,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
settingsTab,
|
||||
);
|
||||
}
|
||||