Compare commits
1351 Commits
Author | SHA1 | Date | |
---|---|---|---|
dae9700b0b | |||
9f8d9030ae | |||
e4d335919d | |||
42bb015384 | |||
8e9dcfac95 | |||
7ad446ade8 | |||
d651b5ceea | |||
7dde3b8b3c | |||
d5e0810915 | |||
7fe9bb2543 | |||
d459455f3d | |||
d8bc86c943 | |||
9b36ca26b2 | |||
c8613d0937 | |||
af95a0bf49 | |||
df63bef8bc | |||
43c3b1bbca | |||
daee8c70f5 | |||
2365b9043c | |||
a09e6abff7 | |||
72e5302c61 | |||
35f243b032 | |||
e25f2db188 | |||
ec738cd03f | |||
fffc0c16d1 | |||
058f63c8e9 | |||
6ceca21be3 | |||
f2128fbf10 | |||
75a864db8c | |||
493ab5aa63 | |||
71ed1ddb1f | |||
6dce77714e | |||
6e80cf77b5 | |||
bbc8c7a1e0 | |||
47cfece013 | |||
26cbdf7e2e | |||
1576680a66 | |||
73665e9743 | |||
5b22ca9ff9 | |||
5f958726d9 | |||
8ea6ef30ff | |||
f0d7d89653 | |||
222d544f88 | |||
71f9ecc32b | |||
607fcee4f8 | |||
3d7efbef54 | |||
9b03b1d9f7 | |||
736292e1b8 | |||
a4b1e6e5f2 | |||
23ea8c1c55 | |||
b910f82acf | |||
951b199b54 | |||
44c480062b | |||
ef09280f61 | |||
1f717b98ca | |||
f0e2570448 | |||
7fe993d356 | |||
f32cf56a67 | |||
d156d810ba | |||
ad2c2cf199 | |||
9a7a814ab8 | |||
3601680c24 | |||
6bfe1d8b07 | |||
beb5873788 | |||
d8b6aef698 | |||
b55e74a2c5 | |||
ba8c660442 | |||
6661c5559b | |||
ff1e5f7550 | |||
c4eacd201c | |||
4e1a28df3d | |||
d420b2de4e | |||
|
848cd86b90 | ||
42b2fedacf | |||
aae4c7e663 | |||
1a709f5a84 | |||
4df00ca870 | |||
6e9f0a420f | |||
f085362774 | |||
54594350bd | |||
ff002044aa | |||
26c9638013 | |||
653d7d9bc8 | |||
346b61eff4 | |||
77322fc8ad | |||
db0f3c5228 | |||
bbb60ae76f | |||
23f8e3b560 | |||
68b73246fb | |||
69eb2866d5 | |||
493db7aa2e | |||
c722c7f854 | |||
a2efca4ca6 | |||
|
5ceec8eabf | ||
|
f7fdb38c8c | ||
a88c45cc80 | |||
8ec6094caf | |||
aed0081990 | |||
a75a2d2608 | |||
319db04403 | |||
463607c6d2 | |||
608b8d42d4 | |||
9f998b5320 | |||
8745dc9313 | |||
681d5c13f6 | |||
0ad2b2ff54 | |||
09b24147c5 | |||
1d501dffa5 | |||
95483f4f25 | |||
45e0be7d62 | |||
bb5853e866 | |||
94460375bf | |||
8d679ae3c5 | |||
19e6d5451c | |||
4f07420c4a | |||
e762e68f7f | |||
9a1b35afd9 | |||
025bbca44c | |||
a88c74d1c3 | |||
a65fb79878 | |||
705ba509cb | |||
f61d13ca91 | |||
d3840c5922 | |||
bf07257ae8 | |||
8e809d4233 | |||
2ac684056a | |||
38b056429e | |||
5a91e58104 | |||
3e379f44e8 | |||
0fff732fd9 | |||
809b59ebb1 | |||
374efd3458 | |||
6519f501d3 | |||
a6e9504b6f | |||
ba98ed6412 | |||
3cd8ed7703 | |||
0daf0a38c2 | |||
5640b12139 | |||
f1332fffbd | |||
567e678a6f | |||
f999969c13 | |||
f61ac3d8d9 | |||
71a0345768 | |||
38f59bab31 | |||
f943697ba7 | |||
50a23f17c5 | |||
c3317bd9ec | |||
7b6a244d72 | |||
e3f34dfc21 | |||
c19262a4ce | |||
72332acc58 | |||
23834475c0 | |||
106d776d85 | |||
7bafbedc67 | |||
4456871f6d | |||
f57e5b7f9b | |||
530be53e70 | |||
a8eb03ee67 | |||
570a6f26ab | |||
e681d462d3 | |||
d1f83884aa | |||
c95d96cd8e | |||
765e7b738c | |||
c6d58f37ee | |||
d738801dc1 | |||
d406f99d7e | |||
8d12d63349 | |||
09268a538a | |||
8fe05b2888 | |||
f85b3c1359 | |||
b1e1405409 | |||
5db238024a | |||
881f69c47d | |||
ac5daf5b73 | |||
70cd128a46 | |||
bb7f044d3a | |||
8c935ffb5d | |||
523e1f0bb4 | |||
0fbb85b138 | |||
b442e40446 | |||
c37877c6da | |||
53362a4cee | |||
c1480c3840 | |||
2719751e2b | |||
ee69d44796 | |||
29c4fff909 | |||
5fb4405614 | |||
40b150722e | |||
4965b28145 | |||
c7b384bc89 | |||
a81a4dda6f | |||
d304f698a8 | |||
44702af188 | |||
73e3c7c3a9 | |||
c38c5006e0 | |||
f8d647a1e6 | |||
dc0916ddda | |||
b1087a6187 | |||
979616e032 | |||
ebf15dbfb9 | |||
a7861be39a | |||
3e7b6dd6f8 | |||
92cc026904 | |||
7a065b1e69 | |||
d704e08012 | |||
9c51542e50 | |||
5c227381ea | |||
980e1c33aa | |||
89025f9fb6 | |||
eaa4fb9d35 | |||
016ed600b0 | |||
fd907a2ceb | |||
da30befa5c | |||
22787bb44e | |||
b574411edb | |||
1d43acf79e | |||
31a2300913 | |||
|
c1ba18b93e | ||
|
fd112e7b1a | ||
d4a595a6ec | |||
|
3ca816fef5 | ||
|
0c5cc6321b | ||
57b47b82aa | |||
7e9cd1b581 | |||
e1ac1bfcd6 | |||
974f7ec63b | |||
|
2c89ced97e | ||
|
70abec5630 | ||
|
f20e79cf3b | ||
|
fbc9709b66 | ||
|
b921a2f86a | ||
|
58d3030bf5 | ||
|
d884f146b3 | ||
|
c806bfd5be | ||
|
5fd7daec9c | ||
4e2253d0ac | |||
|
532e51abdd | ||
|
f7a708d8da | ||
ec5af9735d | |||
|
854c15b61b | ||
|
98ef8fa926 | ||
ad248ece6b | |||
|
58b3b22274 | ||
|
db22ab5f0e | ||
7358873fba | |||
|
d4dd19e1db | ||
|
c607b1a705 | ||
7993f0f257 | |||
442d6d3427 | |||
|
300f876a28 | ||
|
6c1e6ffbce | ||
|
8d317c9381 | ||
|
a2a5649327 | ||
b93f730170 | |||
|
43bcc8101c | ||
|
48fa479001 | ||
|
6d598bf6e3 | ||
|
ed743d2f65 | ||
|
ce500c7f8e | ||
|
d8999da4a6 | ||
|
91986905c8 | ||
|
69ca30a105 | ||
|
195c0917ff | ||
|
6178c59951 | ||
|
eb7e840ad8 | ||
|
fe41861a5e | ||
|
8e9442336d | ||
0fe314420c | |||
|
6f4774391b | ||
4f222a3b73 | |||
5b8f37f226 | |||
2ec3d6db1c | |||
f417de519a | |||
|
832db26d2e | ||
|
3eb704cb5c | ||
74e21aa4be | |||
|
a4584c2a19 | ||
|
bef2ec8106 | ||
c8163ef2bd | |||
|
d66446285b | ||
|
57f2337d51 | ||
33aaf8b50d | |||
825020bdae | |||
6fc767b5a2 | |||
1acaac2484 | |||
207757644a | |||
|
2fd4b30319 | ||
|
ee6c1f5ee2 | ||
38b2bcc47b | |||
|
00702dffbd | ||
|
6dec43e649 | ||
|
68c8a71c76 | ||
01abef3e30 | |||
|
6282377416 | ||
0c337cae3d | |||
21d5660ec3 | |||
4f0204aee3 | |||
7e4febd730 | |||
|
94f1d9bf84 | ||
a000e73847 | |||
3f5ff3642d | |||
e84967ae6c | |||
|
eb8673c484 | ||
|
88acc12530 | ||
10a1ba4099 | |||
|
170a1e3eda | ||
|
0ff214bce5 | ||
0052fa4cdd | |||
|
8fa498ef95 | ||
|
6f2018bcd1 | ||
081e59bb78 | |||
|
d3ad47d152 | ||
|
d425c7b228 | ||
0f912ebef8 | |||
34189f6a41 | |||
|
7eb7f1be12 | ||
|
0a7a45baf3 | ||
2724f3d859 | |||
e900bdb681 | |||
50a77d05a1 | |||
|
5ea1e70813 | ||
|
a44ae9f4f1 | ||
caa0117318 | |||
|
068199dbaf | ||
|
df2719ed71 | ||
3753126c26 | |||
|
e5ac4e204f | ||
|
5e15e8cfa5 | ||
|
670ea3f178 | ||
179dbabd5a | |||
1a03c08b36 | |||
b1351069b9 | |||
b8647deede | |||
|
f0e8bff9ec | ||
2775fab5d4 | |||
|
33856dea20 | ||
dd463ee9d8 | |||
3af07ca21a | |||
|
2a8ab5c51e | ||
2428a42103 | |||
|
a925981b16 | ||
01dc6d4bb7 | |||
|
d0a0b90274 | ||
ab300633ec | |||
|
bc73baa86a | ||
af977b1bfa | |||
|
b2c46d7fbe | ||
fc4cd900cf | |||
57c4d63275 | |||
|
041980aca4 | ||
01ae8d67a2 | |||
e4943d9abb | |||
|
0169506c49 | ||
8cc057a5f8 | |||
|
7cd96cbc7b | ||
2ffc6e1e12 | |||
|
961b79ad12 | ||
4fcb467351 | |||
|
f6ff48f072 | ||
3e07f23128 | |||
9bf68ca679 | |||
|
a4c29b4056 | ||
e4187700b2 | |||
|
2feb7b9e0a | ||
cb7838350f | |||
|
13d9b484c9 | ||
d984c8bc36 | |||
|
7f5815bb92 | ||
2dcfef0b6b | |||
3e927e75b8 | |||
|
f0a5f83255 | ||
58b44d556e | |||
|
74b0ae9f65 | ||
9b47ede63e | |||
|
09a86e074c | ||
eaf9c001dd | |||
e1190fe3f8 | |||
|
d334b6490d | ||
e071940a0d | |||
|
53589378b2 | ||
21d4991f38 | |||
1f0a738610 | |||
|
9a574bee3a | ||
c32b41021b | |||
|
6c3e2b941a | ||
b770eb64f0 | |||
3b787c1aa4 | |||
|
e466ceff7a | ||
16f1510779 | |||
|
2a4083c511 | ||
24bc8bda66 | |||
|
75311ddc98 | ||
d5a38c80ea | |||
2732d00825 | |||
|
761ae8f394 | ||
7abbb779f5 | |||
|
1bd52d42db | ||
ec91fb6bf0 | |||
d3b4461b49 | |||
|
32b9732139 | ||
|
e79de360f1 | ||
2ec9808e34 | |||
|
8d8fe96c68 | ||
eca85f7a7d | |||
a9a6c33436 | |||
97ef140789 | |||
|
8f936c3d7d | ||
e761b309bf | |||
|
895334c640 | ||
da70a3ed4f | |||
|
6625c9a328 | ||
7f53445ab0 | |||
|
40c0b8acb3 | ||
39f66c5bb8 | |||
|
0eb13f3eac | ||
022613d66b | |||
507731cefa | |||
|
801d79542b | ||
8f8dafeb70 | |||
|
6d6ac532eb | ||
da85adcd33 | |||
|
fb00c09cbe | ||
461533fa07 | |||
|
7ecf07fd0c | ||
7bdae2faa6 | |||
|
0f0e234ae7 | ||
a90c5fe946 | |||
|
0429da10a4 | ||
8afc70da95 | |||
80a4b66128 | |||
|
19c2da54c3 | ||
6874b905eb | |||
|
a0c54a3ea3 | ||
b6b997ca23 | |||
|
390613f1e7 | ||
6ee8086b51 | |||
|
cc34222e6f | ||
6008623812 | |||
3f57f01f55 | |||
|
bd94a6bb93 | ||
5cab38d07c | |||
616686b3c8 | |||
4df4235ad2 | |||
|
1337c3619c | ||
a72441ee86 | |||
|
6be658b41f | ||
bf3948f9c5 | |||
d8cec4368d | |||
|
21f588a289 | ||
0aabc67774 | |||
|
0ab9cb127f | ||
ba6008efd7 | |||
|
aedad789c6 | ||
781686f78b | |||
|
099723d469 | ||
302408f265 | |||
496305597a | |||
|
83ae3a0e4c | ||
671266fff0 | |||
da2be9d7c9 | |||
affa314f08 | |||
8306472bbd | |||
|
599d15bbff | ||
b91ce6f56c | |||
|
b1b1a1b725 | ||
|
5a796ab68d | ||
e3b598dc00 | |||
f1705d97fb | |||
|
1a6ffa8b4e | ||
a4119cd3b9 | |||
|
a6663ceea8 | ||
cd0a4d1ba3 | |||
|
8d1fb6ec56 | ||
e06b320ae6 | |||
|
79bae0116c | ||
177c16bce6 | |||
|
d8806fa15e | ||
|
9c4f24d4fa | ||
|
7e5c1b1575 | ||
f5f81547bf | |||
|
c4c2ce7165 | ||
bdba993cf8 | |||
b8966c8459 | |||
|
b6efd3301e | ||
1ac78b64e8 | |||
|
521e9d1148 | ||
76df1e7676 | |||
|
782082ceba | ||
9b1a1ea509 | |||
|
9876e195f9 | ||
21c56590a5 | |||
|
0a64c4eb5c | ||
a678716120 | |||
|
8566d0b5ed | ||
805cbb836d | |||
|
d2127207f3 | ||
96988d67e0 | |||
|
1c203f56d7 | ||
a497618ea4 | |||
|
c9f63e27ab | ||
6a84fbec91 | |||
|
ae6f5b86dd | ||
4d8eec7399 | |||
|
2329bc5520 | ||
10a58ca18d | |||
|
8dd4872f05 | ||
047b4cce7d | |||
|
6996ab84d6 | ||
38484b7e42 | |||
e72115fd51 | |||
790ddcbac2 | |||
|
fc418bc838 | ||
1f47f1be34 | |||
dbb94bc29b | |||
|
9517395f44 | ||
7da5f65c85 | |||
|
70b302a912 | ||
db5ec9a934 | |||
|
82564420e4 | ||
b1ced4fac8 | |||
b294964b26 | |||
|
1ecfd102ba | ||
6d40349389 | |||
41e1258469 | |||
|
921f89ec4b | ||
690b677dc9 | |||
|
1146e9eaa9 | ||
8a1eb811a6 | |||
|
642f9accbb | ||
382451156c | |||
|
8e97cf1030 | ||
fe42600ab8 | |||
|
04a1e4938c | ||
a9427f4520 | |||
|
67cc36c35a | ||
d25d4f2f13 | |||
|
3e7319464e | ||
|
18ee0d9ea9 | ||
b8fdad91c3 | |||
|
0198e29f46 | ||
|
e09443a048 | ||
a824ba8856 | |||
|
2dfb3bb539 | ||
860a943437 | |||
|
63bf82507f | ||
110d2f3551 | |||
7048abaedd | |||
|
9bc748d67c | ||
1f9537b755 | |||
|
00090853b9 | ||
85dbb33fab | |||
|
0bc838113e | ||
d5c45a8771 | |||
|
9641a35a06 | ||
9b3cf23b08 | |||
|
9ca4e51fab | ||
69c3034a8b | |||
|
c77cf71163 | ||
f9b95ef848 | |||
54319a8bf2 | |||
|
e990c51c00 | ||
50f8683ec8 | |||
|
fd0a635ae7 | ||
a03168d980 | |||
|
883f7915c7 | ||
1fda0e9c86 | |||
|
6a5c6e2a73 | ||
1e5c985e76 | |||
|
d99983105f | ||
3b4030d12b | |||
|
84ae933d4a | ||
cc1418ee5d | |||
|
ecac94a8b3 | ||
ef05403caf | |||
9d0994e225 | |||
|
b9b8c5fa67 | ||
7f266f207b | |||
|
f372f0d361 | ||
1af8878748 | |||
|
f718410970 | ||
4fa312c19b | |||
|
445714df01 | ||
6aacd075fb | |||
|
3986fc5670 | ||
81c0e13523 | |||
|
4ac45ea2d2 | ||
5f52f520f4 | |||
|
83d4518d27 | ||
02c085e832 | |||
|
661f5ee341 | ||
824da94ffb | |||
|
1dbaf2967d | ||
9b4b21c297 | |||
|
de5e7e846e | ||
bb42117024 | |||
|
2376d33e19 | ||
809df67a14 | |||
|
9017edd1cd | ||
60a7b57ccc | |||
|
9da41805f6 | ||
579050e349 | |||
|
a962b0044e | ||
c43dec218b | |||
|
cd18658d55 | ||
a4d4e4e007 | |||
|
ad217c0b6b | ||
e15370bede | |||
|
07a408f477 | ||
e78d6c2965 | |||
|
4345ff172f | ||
c569d4190a | |||
|
d14b53acd5 | ||
daa8489175 | |||
a8ed96e09b | |||
|
c83003cfac | ||
c4e6b6b62a | |||
|
c9dd292f68 | ||
1f88c043dc | |||
86721aabba | |||
|
574e5100cd | ||
7aa3f04f8b | |||
|
dc3c78d134 | ||
7418e33d1e | |||
3a781bb94c | |||
|
9e0b089e07 | ||
17c51e7110 | |||
|
d3a9e968ba | ||
6f4ca93d14 | |||
|
f4bf94a0c8 | ||
2df027e888 | |||
|
801ce4ec91 | ||
|
8d369bbef2 | ||
c894758f1a | |||
f7a888e590 | |||
|
7b2761460e | ||
a5ecd02068 | |||
|
c8ed9d3769 | ||
b463b2b7e7 | |||
|
a394c25d3d | ||
f2928371b6 | |||
ea0992a9d8 | |||
|
e035f385ca | ||
ae400099eb | |||
|
79039d0661 | ||
fd2b729228 | |||
|
f49be7a01f | ||
9bb779149b | |||
|
5f169ddf01 | ||
022c08fe75 | |||
|
270cf0e82c | ||
a86b363ea2 | |||
|
00595ce5bf | ||
7a5a954aa7 | |||
|
c52a849049 | ||
f9147991e8 | |||
|
d98e1ccfe0 | ||
74ea0e4bbf | |||
|
b9bf75d541 | ||
|
dd85d86052 | ||
19fbe73949 | |||
539fcc6f2e | |||
cce4788aba | |||
|
ecb702ae2c | ||
c36dc27637 | |||
|
6a820424ea | ||
2902e33d11 | |||
e9e0c2b557 | |||
7dcafa12f6 | |||
3f81a32351 | |||
039baf6ea7 | |||
59489df994 | |||
c80ee2635f | |||
44f8693490 | |||
a432a2d612 | |||
3192050de8 | |||
bb8220c66c | |||
d4b344900f | |||
8159fc2256 | |||
cdf8537153 | |||
d1fc35c608 | |||
9dcbfc213f | |||
42cfc69357 | |||
6656b639d7 | |||
bc38f5d972 | |||
33e8907d4f | |||
a7549cd9db | |||
8d377e1ac2 | |||
ac12b164da | |||
abbfce9af1 | |||
95621cd029 | |||
ccbcdaf76a | |||
775d8412d0 | |||
ea53f672ee | |||
8c1b5c0835 | |||
ea30d01296 | |||
7734e3977e | |||
f7c597ebb3 | |||
a2b93cb513 | |||
b900c00641 | |||
5bb8b5f3fc | |||
f383e6b401 | |||
f98dcb174a | |||
6eff0289f4 | |||
bafc714b59 | |||
d857209b19 | |||
|
8393aa325b | ||
|
b248ffa3ba | ||
46879fe47a | |||
99c166a1ac | |||
6f3c1691c4 | |||
a55482d62d | |||
4e3f921ee3 | |||
f6d8f222a6 | |||
cb4aad5ba0 | |||
28bb0663d4 | |||
2671410fe9 | |||
af7e659c0e | |||
86723e28eb | |||
ecfb3c1e50 | |||
69b73f6140 | |||
62ba127793 | |||
16ab3a4969 | |||
1d9785f804 | |||
15b34ef638 | |||
8d0ce2e777 | |||
042ddfed13 | |||
0bc27b9a57 | |||
8454f86024 | |||
9a0ffdd9bc | |||
f3c384e25d | |||
d58d5b1850 | |||
93e764fb0a | |||
d2292137da | |||
a5c4d47a6d | |||
5fe31ac703 | |||
05760882b5 | |||
68ae14d967 | |||
f1dcc694de | |||
5b516c21c6 | |||
|
b417882eec | ||
|
df92f22b0b | ||
a362f804d9 | |||
5aa36fef41 | |||
95bf160ba9 | |||
35d9c915b1 | |||
311e435e2e | |||
|
21552ee041 | ||
|
b79a40777d | ||
|
c21fe3e2f1 | ||
|
e724fe7aef | ||
|
458089a953 | ||
|
1b773a580a | ||
|
7e87b6f5c7 | ||
|
ae12bb81a7 | ||
|
652e67d6c7 | ||
|
a6bea11d93 | ||
|
e12b3bac85 | ||
|
f97ab060ce | ||
|
2b18b2ef68 | ||
|
00014e402c | ||
|
2245f8ccdd | ||
|
6caffa14ae | ||
|
c4b4d31aa2 | ||
|
1478d44828 | ||
|
3f48fa42ab | ||
|
80d1694977 | ||
|
31c43fdded | ||
|
74641c8d6c | ||
|
4b7c26f5b3 | ||
|
7b91dbcd85 | ||
|
943f58630e | ||
|
4211bd6189 | ||
a17de1e768 | |||
81216b8a90 | |||
|
5305c0b9b0 | ||
|
5f69306429 | ||
|
b48efb2f39 | ||
|
64ba7c79a1 | ||
|
2d43b79735 | ||
|
bb9a0e197d | ||
|
07d0836e97 | ||
|
b0430b4541 | ||
|
6a8006823f | ||
|
134fd074c2 | ||
|
67b396e511 | ||
|
88bbff62fe | ||
|
927ba69695 | ||
|
6fe02a710c | ||
|
ca605ec049 | ||
|
e68f3c7b79 | ||
|
4eb63f751f | ||
|
24cb16c0e6 | ||
|
1c3b8e5fa4 | ||
|
38dded687e | ||
6f4534c9fc | |||
4dc14699e1 | |||
caa2360a3d | |||
cb55a79971 | |||
|
406e6fa5d9 | ||
|
b76c04428c | ||
|
0476cd316f | ||
|
4998b34cbe | ||
|
74d95498ac | ||
|
6501ad9b80 | ||
|
23c477232f | ||
|
22d4f216d4 | ||
|
ba8a044e4f | ||
|
168f76e62e | ||
ba0fcf5115 | |||
be40723e1c | |||
eadbec249b | |||
|
894f2578df | ||
|
e91a363b08 | ||
d0b0493b16 | |||
|
7d61ca130b | ||
|
b04e3dc5f7 | ||
790dac681c | |||
70dd43bad4 | |||
af2af96d46 | |||
|
b598aa451e | ||
|
c767449bbf | ||
f85f3b19e5 | |||
4807cfd339 | |||
f77f5040a4 | |||
7ef483a7f1 | |||
|
c410b14c19 | ||
|
26d3a702b5 | ||
|
23f823d6ef | ||
|
a96ab5f3b0 | ||
|
5f096034c1 | ||
|
e8f8576f63 | ||
|
416a343809 | ||
|
8267d8e143 | ||
|
03139a5e7c | ||
|
3b08e51dcb | ||
|
b5bdeb108b | ||
|
b7e80cf25b | ||
|
c352cc9438 | ||
|
039670e639 | ||
|
0beebe8052 | ||
|
c08f7cdb86 | ||
|
4a9d6477b7 | ||
|
44fe96a56f | ||
|
794cb17ead | ||
|
353a538585 | ||
|
b25f24f5fe | ||
|
9deb75361a | ||
|
0d37708ad8 | ||
|
c86e2f7548 | ||
|
f86e87a1ab | ||
|
aa46310b3a | ||
|
01e17041a8 | ||
|
cd9283dcd0 | ||
|
8bcd6eb79a | ||
|
2fbe7a3798 | ||
|
14e6712037 | ||
|
9b644b3f86 | ||
|
e2d088d312 | ||
|
3c5fd94c2e | ||
|
ca9788136c | ||
|
7d239e6c0f | ||
|
57bdccf7dc | ||
|
0064ae35dc | ||
7949d6e451 | |||
ecc74244c4 | |||
dbd2044600 | |||
|
66b97450d4 | ||
|
206c347838 | ||
|
a53f5ad652 | ||
|
5c151f133b | ||
|
4cd612f92b | ||
|
afd7879ec6 | ||
1568d31b0c | |||
06b56538cf | |||
621aaa3978 | |||
a94f4165d0 | |||
722d0fe00a | |||
0b29387c53 | |||
c067d93a74 | |||
0acb214b19 | |||
9597dcc4d5 | |||
c43b4a14f1 | |||
bc46ac39b0 | |||
81f539f65e | |||
c5c02bc7e7 | |||
d919493094 | |||
967f8a61f9 | |||
53d7472c5e | |||
8567f2fa90 | |||
fb88dc5a90 | |||
61b6b6deca | |||
2fc1e66cff | |||
00e5b3f40e | |||
|
e7ad0fb51e | ||
|
479e4a6e0a | ||
efb20ebd19 | |||
decf8862de | |||
|
2b41b0e82c | ||
|
f38d781133 | ||
|
3b997d3c73 | ||
|
06f4d7b519 | ||
|
285aae3227 | ||
|
29be14801c | ||
|
80ab7b7b92 | ||
|
50fcc2604b | ||
|
a306fcea23 | ||
|
3557987988 | ||
|
ce6c58ef76 | ||
|
f1dd72cf11 | ||
|
b671451261 | ||
|
faa4ed02ed | ||
|
74b3bd4b52 | ||
|
58297e35bf | ||
|
590d4d9595 | ||
1e1c536f7b | |||
b02ed3e36a | |||
7bfb1bb9f9 | |||
ad4f1650f4 | |||
4d5754779a | |||
eb3aa26220 | |||
9553a49bd0 | |||
2a20d67be2 | |||
c4a890ea91 | |||
27525428fc | |||
b985217611 | |||
64c1e8bc5c | |||
f4d269bcdc | |||
ba8163a456 | |||
e24608f734 | |||
6b161eb403 | |||
17ceb88ed7 | |||
650ac550fa | |||
f6ec94b46e | |||
8e2087d2fd | |||
293e5ce228 | |||
5ac7e521b6 | |||
dccef7815f | |||
667c727ac9 | |||
eeea1c6b53 | |||
9e1417ac3e | |||
6a326a12be | |||
eeb83a14e0 | |||
dc60b3d9ae | |||
29ef9a57b8 | |||
bf800a0889 | |||
64c88f5c42 | |||
9d3ab72eb7 | |||
1f802e5146 | |||
64d9394a27 | |||
5d439bbdc8 | |||
a21bcc9803 | |||
cbe505c374 | |||
f66c78a381 | |||
e79685a6e8 | |||
35bbb12524 | |||
230bc5e248 | |||
3acbaa1f12 | |||
96e13a03f0 | |||
b1a42f2bf5 | |||
d307a2bedf | |||
c76750c088 | |||
84e895dccc | |||
f3eda9a34b | |||
c2a0b8e857 | |||
c0f847949f | |||
7885b2c204 | |||
|
d74eae1ebf | ||
|
fb63601ebf | ||
394a891115 | |||
cdbce12b11 | |||
d94629d9ed | |||
275c5ed4cd | |||
ca30e70062 | |||
8f35a99fdd | |||
78667d16d6 | |||
84ef0d9440 | |||
b1714dce54 | |||
1618b5d966 | |||
b2491a0a09 | |||
a37fcb300e | |||
1d1503c826 | |||
4519409050 | |||
cf4d8e63ee | |||
|
1e9adc51df | ||
|
0ac87603db | ||
|
f25e9f7f98 | ||
c40e678974 | |||
91ab4a1547 | |||
d6a95a77df | |||
088c4f746b | |||
a264cbf832 | |||
b4b54fdca3 | |||
ad645227d5 | |||
995587afa0 | |||
02fbd0e2bc | |||
ed5ededcbe | |||
63a88e1d66 | |||
b10ec8f136 | |||
e3391f7017 | |||
796fac9466 | |||
82e5db751f | |||
b23b928289 | |||
|
db3f89e3c4 | ||
|
dcbe96f447 | ||
d917c3aefd | |||
0910e56d53 | |||
4b464ca87c | |||
e826b9ff73 | |||
6a9f9bf79a | |||
21b8c3ca15 | |||
|
a26914fdbe | ||
|
a580315593 | ||
dee0a31283 | |||
01bc88a783 | |||
ee88c63f38 | |||
9b49e6034b | |||
22b4153703 | |||
d82dda1ff8 | |||
cb4407279e | |||
d2cd8baa77 | |||
|
01fe0d6782 | ||
|
9da9c2aee4 | ||
|
d0f273381b | ||
|
add5bb8d9c | ||
b69fa97572 | |||
45bc46aae4 | |||
1f38797cc4 | |||
3493bfac24 | |||
4886bbbd3e | |||
d75368b020 | |||
3d64a5e8c8 | |||
61206db05a | |||
bb18f2a1e6 | |||
b8b4af4f89 | |||
09d13f3ed3 | |||
40b1dc0877 | |||
0472460161 | |||
09069e03e9 | |||
|
0448e8643e | ||
|
3c71e945a3 | ||
f0232fea9e | |||
5ae9aed260 | |||
51f7f564f6 | |||
ec0d5517bc | |||
51acd63748 | |||
39ecd12943 | |||
c0d7d5170b | |||
6e43097a3f | |||
50b2ff180d | |||
157d440254 | |||
9ef6317529 | |||
5bec4e31b6 | |||
7a6411ad23 | |||
85636647bd | |||
ecdae521b4 | |||
4f053dc23f | |||
45c2ebf340 | |||
bc646ab8ae | |||
dbf1240034 | |||
e82fb38ce8 | |||
9429426ded | |||
a2f23a850e | |||
4cc002c7c2 | |||
f0b62d8df3 | |||
80fca9d187 | |||
e55159e3fa | |||
c8d34ea042 | |||
398e727ddb | |||
74a03a176c | |||
|
9d342fa99e | ||
|
4c480c5b5d | ||
cacb6554c7 | |||
a3e8758280 | |||
904cae5375 | |||
8f1612e43c | |||
b16281f28e | |||
1b0d6d5cc3 | |||
40336e78c1 | |||
481500c563 | |||
03c12b137d | |||
677fd3cf10 | |||
287e3551b5 | |||
21b3802e08 | |||
5655043b36 | |||
a31a93e206 | |||
9638a5765d | |||
|
b09d89645c | ||
|
1b56426c94 | ||
f021bec086 | |||
db1b0790e4 | |||
f39e497f4c | |||
840f994081 | |||
0c18f19dac | |||
00451b1bde | |||
d3b444827a | |||
1011875530 | |||
cc4e2fb807 | |||
fe2a0115c3 | |||
bb3439429f | |||
1a66d769cc | |||
525ff40bb3 | |||
dfcac064e8 | |||
6299ce4b81 | |||
2303783319 | |||
76ff3bd101 | |||
211b24d2d7 | |||
9147eb06e4 | |||
6dbef0e292 | |||
8dd5c7b479 | |||
53a1638549 | |||
|
b48cecf54d | ||
|
f304f78177 | ||
|
d66ff14e2c | ||
|
a93ca8270c | ||
72551c5f67 | |||
9998857aa1 | |||
988b78085a | |||
f9e53ad0b3 | |||
666925c081 | |||
54531f0a6d | |||
c5f5ce30e5 | |||
67f9ffaddb | |||
63e4b574a9 | |||
ecdf9a70c0 | |||
6ff3a96d81 | |||
58ff18724b | |||
513b5f4cab | |||
e158cc25b3 | |||
0355c7d344 | |||
648c1f8b99 | |||
3a608f8aff | |||
fed816b8da | |||
850610b213 | |||
1e1808fd09 | |||
ae3760e193 | |||
6c7e1cec9d | |||
15963bae36 | |||
23d79c5abc | |||
752bd8a45b | |||
890c3cbc40 | |||
e420172f31 | |||
1b66802fc6 | |||
2cb25fec12 | |||
|
ff3a89f65c | ||
|
51ad989a94 | ||
|
591b39494a | ||
|
69cdc4aff5 | ||
44e1472c0b | |||
9d79f7462f | |||
9f66acccd8 | |||
|
45282e0abf | ||
|
33777fb2f7 | ||
44423552af | |||
219260ad68 | |||
e93d5a7657 | |||
9f5cacefd1 | |||
39ebd0e3fd | |||
30e82d321e | |||
ffa35c74ae | |||
017cb66564 | |||
5117836dc7 | |||
ab7771cba9 | |||
b019a1cb83 | |||
b94c8e7b77 | |||
21a6cc2be5 | |||
4b2d0e71dc | |||
438ad10522 | |||
2eee02cfbd | |||
0081e3b1a1 | |||
|
b2fc6cc8ed | ||
8ca164e723 | |||
|
1901832d0e | ||
|
dfe7dbbd6c | ||
f71990b87a | |||
143f690740 | |||
b9ce8e5e54 | |||
4279bbc00d | |||
bae4c4cc06 | |||
a8dd8cb145 | |||
21380453be | |||
6081bdb563 | |||
b502d77545 | |||
8c18bb5174 | |||
a2ce74fc86 | |||
8691ac735d | |||
48eba9b956 | |||
371b89a262 | |||
4ad8a9446b | |||
19bf587b8a | |||
05c7ca099f | |||
934b3093bb | |||
7af9b7836d | |||
4e09b4f135 | |||
5275d71fd6 | |||
f95b5eadd0 | |||
58bbb9e385 | |||
8ee12ea57e | |||
e787427214 | |||
0af0e5026f | |||
583e96ac1a | |||
bb660a6bbb | |||
ebc9adc616 | |||
7b12815953 | |||
94830b5da5 | |||
a57b7ca57a | |||
3c14f1e85d | |||
fac7a72654 | |||
cf0d516234 | |||
330e5a5f62 | |||
3aa5cdef18 | |||
60dcb0f13d | |||
a9a6fc5929 | |||
69d45d9d77 | |||
7f2270904b | |||
64358e5576 | |||
ff86afd8b8 | |||
616cb839fc | |||
44b0a0e378 | |||
b348bf871b | |||
554f93efac | |||
b818f39386 | |||
f7c71eb9c7 | |||
18f24804d2 | |||
db7992cfc3 | |||
7b06843fe1 | |||
72236295e3 | |||
a45601b2ad | |||
606f6e6325 | |||
78b9510e93 | |||
ff41cb78d5 | |||
eae51c6021 | |||
c5ef5b5b70 | |||
3c50f5cbbc | |||
0b123cc41b | |||
7f779e35e5 | |||
5ba3971dee | |||
0a6a540a41 | |||
170700dffe | |||
92501bd7e1 | |||
d40462aa1b | |||
6dacae46b5 | |||
4e953c198d | |||
158ce182df | |||
583e6f9adf | |||
d8351aa49e | |||
e0bc581b41 | |||
19593e1d01 | |||
933ee3d3b1 | |||
7d0560228b | |||
060b1ffbe9 | |||
f6b52e5c02 | |||
7d51ea9caa | |||
8dd7d0649c | |||
9f8c70c203 | |||
9f92ff325d | |||
aadcc50a11 | |||
06211688b6 | |||
160fad9f60 | |||
5c84fbebb6 | |||
2edfcd63fe | |||
771aa0b493 | |||
c246b24af2 | |||
2f74449189 | |||
5752a95c23 | |||
830492e181 | |||
fd22fa286c | |||
2d37b807ea | |||
a0edfd1c27 | |||
8a1709c559 | |||
2f90f65f1c | |||
9a012eb3a7 | |||
9d32ae16e0 | |||
35a411b977 | |||
d2c5a800cb | |||
a3e55c6121 | |||
21facae40c | |||
0621a5710c | |||
d423bdbfd0 | |||
48fa62d75d | |||
b13fe98d40 | |||
4faa7bb919 | |||
8440a3dcca | |||
75cd042bd5 | |||
8925234dc5 | |||
b7038cda28 | |||
0d2dfaca61 | |||
26a82f400f | |||
2237df4334 | |||
2cb52722dd | |||
382af7117c | |||
5c648be5eb | |||
c60299ed85 | |||
7cbb744e5f | |||
2131e4e19d | |||
78865542b7 | |||
574741de06 | |||
a8472b2da0 | |||
7f31c24e92 | |||
78ca0b719d | |||
b5043508d9 | |||
d1875ffc90 | |||
|
c32417ca54 | ||
03b598a17d | |||
faab6d013f | |||
0e168b5f84 | |||
422be7410a | |||
156b4061b0 | |||
e5820f5c18 | |||
633a4250d1 | |||
582a8cc642 | |||
31ccd13ced | |||
96571f2b31 | |||
a5afb72f57 | |||
89a2dd9c39 | |||
31c75c7d70 | |||
f459d10c39 | |||
6960c0915f | |||
|
d2ba4cd346 | ||
ded4323544 | |||
8dfb764199 | |||
6cedb7790a | |||
d3d3b4007f | |||
382843668a | |||
f20aaf1f4c | |||
babcae1bd5 | |||
3b908e1e27 | |||
c1d6041c6a | |||
6f6edb2cd8 | |||
ef7718e920 | |||
|
d2ff1a855a | ||
3cc431c088 | |||
e9a45f7051 | |||
aa5df4341b | |||
|
ba4342dd60 | ||
|
b83c4e958c | ||
3a617933aa | |||
0f066316cc | |||
4a01fe9761 | |||
a5db291250 | |||
7e593bfa6f | |||
5eae5ecb5b | |||
b9810c9d62 | |||
c3436d372a | |||
33fe42b024 | |||
d6eca764d6 | |||
b139e2ddf9 | |||
c30d83bee8 | |||
182303aeda | |||
1f3b7fc20f | |||
ad040d83eb | |||
|
98ff85dfa8 |
107
.gitignore
vendored
@ -1,104 +1,9 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
# builds
|
||||
dist/
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
# dependencies
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
# yarn
|
||||
.yarn/
|
||||
.pnp.*
|
390
CHANGELOG.md
@ -1,7 +1,384 @@
|
||||
# changelog
|
||||
|
||||
if something is ~~crossed out~~, then it is no longer a feature included by default,
|
||||
but can still easily be enabled by following instructions in the [docs](README.md).
|
||||
### v0.11.0 (dev)
|
||||
|
||||
a complete redesign & rewrite of the enhancer, with new features and a port to the browser as a chrome extension.
|
||||
|
||||
#### new
|
||||
|
||||
- cross-environment and properly documented api to replace helpers.
|
||||
- cross-environment mod loader structure.
|
||||
- "integrations", a category of mods that can access/use an unofficial notion api.
|
||||
- notifications sourced from an online endpoint for sending global user alerts.
|
||||
- simplify user installations by depending on the chrome web store and [notion-repackaged](https://github.com/notion-enhancer/notion-repackaged).
|
||||
- separate menu profiles for mod configurations.
|
||||
- a hotkey option type that allows typing in/pressing a hotkey to enter it, instead of typing.
|
||||
- a rainbow indentation lines style.
|
||||
- border & background style options for the code line numbers extension.
|
||||
- an icon sets option to encode images to data urls to prevent quality reduction.
|
||||
- customisation of integrated titlebar & always on top window buttons.
|
||||
- an open on startup option under the tray mod.
|
||||
- optional icon or title-only tab labels.
|
||||
- choice of tab layout styles: traditional tabbed, traditional, bubble and compact.
|
||||
- a hotkey for reopening closed tabs.
|
||||
- an option to remember last open tabs for a continue-where-you-left-off experience
|
||||
(recently active tabs are reopened after an app relaunch).
|
||||
|
||||
#### improved
|
||||
|
||||
- split the core mod into separate mods for specific features.
|
||||
- theming variables that are applied more specifically, less laggy, and less complicated.
|
||||
- merged bracketed-links into tweaks.
|
||||
- a redesigned menu with nicer ui, separate categories for mods and a sidebar for configuration.
|
||||
- simplified and smoothened the side panel + moved it to the core so any mod can hook into it.
|
||||
- font chooser option for heading fonts.
|
||||
- renamed "property-layout" to "collapsible properties", added per-page memory of collapse state.
|
||||
- chevron icon instead of arrow for scroll to top.
|
||||
- moved word counter to display in the side panel instead of within the page,
|
||||
implemented a more accurate word counter method.
|
||||
- the topbar icons extension defaults to the notion default topbar icons for comment/updates/favorite/more,
|
||||
but can revert them to text (it still adds a custom icon for the share button).
|
||||
- relative indenting in outliner.
|
||||
- rtl support for toggles, indentation lines, table of contents and databases + force inline math to ltr.
|
||||
- replaced the "truncated table titles" extension with a "truncated titles" extension
|
||||
with an option to truncate timeline item titles.
|
||||
- renamed "notion icons" to "icon sets" with new support for uploading/reusing custom icons
|
||||
directly within the icon picker.
|
||||
- moved the tray to its own configurable and enable/disable-able mod, with window management enhancements
|
||||
that follow more sensible defaults and work more reliably.
|
||||
- tabs will auto shrink/expand to take up available space instead of wrapping to a second line.
|
||||
- a visually revamped cli to more clearly and aesthetically communicate status and usage.
|
||||
- cli can now detect and apply to user-only installations on macOS.
|
||||
- a shortcut built into the cli to fix the "you do not have permission to open this app" error on macos.
|
||||
|
||||
#### removed
|
||||
|
||||
- integrated scrollbar tweak (notion now includes by default).
|
||||
- js insert. css insert moved to tweaks mod.
|
||||
- majority of layout and font size variables - better to leave former to notion and use `ctrl +`/`ctrl -` for latter.
|
||||
- the "panel sites" extension, due to it's limited/buggy functionality and incompatibility with reimplementation.
|
||||
|
||||
#### fixed
|
||||
|
||||
- bypass csp restrictions.
|
||||
- many. like many many. all the bugfixes. (mostly a side effect of completely rewriting everything,
|
||||
but reported extension-specific bugs were all intentionally fixed.)
|
||||
|
||||
#### themes
|
||||
|
||||
- "nord" = an arctic, north-bluish color palette.
|
||||
- "gruvbox light" = a sepia, 'retro groove' palette based on the vim theme of the same name.
|
||||
- "gruvbox dark" = a gray, 'retro groove' palette based on the vim theme of the same name.
|
||||
- "light+" = a simple white theme that brightens coloured text and blocks,
|
||||
with configurable accents (formerly littlepig light).
|
||||
- "playful purple" = a purple-shaded theme with bright highlights (formerly littlepig dark and gameish).
|
||||
- "pinky boom" = pinkify your life.
|
||||
|
||||
#### extensions
|
||||
|
||||
- "calendar scroll" = add a button to jump down to the current week in fullpage/infinite-scroll calendars.
|
||||
- "global block links" = easily copy the global link of a page or block.
|
||||
- "collapsible headers" = adds toggles to collapse header sections of pages.
|
||||
- "simpler databases" = adds a menu to inline databases to toggle ui elements.
|
||||
- "view scale" = zoom in/out of the notion window with the mousewheel or a visual slider (`ctrl/cmd +/-` are available in-app by default).
|
||||
|
||||
#### tweaks
|
||||
|
||||
- wrap tables to page width. - hide "Type '/' for commands".
|
||||
- quote block quotation marks.
|
||||
- responsive columns breakpoint (%).
|
||||
- accented links.
|
||||
- full width pages.
|
||||
- image alignment (center/left/right).
|
||||
|
||||
#### integrations
|
||||
|
||||
- "quick note" = adds a hotkey & a button in the bottom right corner to jump to a new page in a notes database (target database id must be set).
|
||||
|
||||
**below this point the enhancer was desktop-only. in v0.11.0 it was been ported to also**
|
||||
**run as a chrome extension. changes made to both are indicated above.**
|
||||
|
||||
### v0.10.2 (2020-12-05)
|
||||
|
||||
again, an emergency release for bugfixes.
|
||||
not properly documented and new features have not yet been fully reviewed/edited.
|
||||
|
||||
- new: side panel - adds an extra sidebar on the right for use by other mods,
|
||||
toggleable with `ctrl+shift+backslash`.
|
||||
- improved: notion icons uses spritesheets for faster loading of icons.
|
||||
- improved: icon sets can be hidden/toggled.
|
||||
- improved: toggles in the enhancer menu follow the same style as notion's toggles.
|
||||
- improved: separate quote font variable & option in the font chooser mod (`--theme_[dark|light]--font_quote`).
|
||||
- improved: option to hide the "page details" text for the word counter extension.
|
||||
- bugfix: notion icons tab is now visible in fullpage databases.
|
||||
- bugfix: code line numbers handles wrapped code blocks.
|
||||
- bugfix: file explorer no longer opens when enhancer menu is opened.
|
||||
- bugfix: enable the remote module in webviews (windows/tabs) for compatibility with the
|
||||
updated version of electron used by new notion builds (>= 2.0.10).
|
||||
- bugfix: add support for enhancing an `app` folder if there is no `app.asar` file present.
|
||||
- extension: "outliner" = table of contents in right sidebar.
|
||||
- extension: "panel sites" = embed sites on the site panel.
|
||||
- extension: "indentation lines" = adds vertical relationship lines to make list trees easier to follow.
|
||||
- extension: "truncated table titles" = see the full text of the truncated table titles on hover over.
|
||||
|
||||
> 📥 `npm i -g notion-enhancer@0.10.2`
|
||||
|
||||
### v0.10.1 (2020-11-18)
|
||||
|
||||
essentially a prerelease for v0.11.0: pushed out for urgent bugfixes during
|
||||
exam/study weeks when there's no time to code a full release.
|
||||
|
||||
note that this means new features have not yet been fully documented and
|
||||
may not be fully ready for ideal use yet. however, things overall will
|
||||
work more reliably than v0.10.0.
|
||||
|
||||
- new: different css entrypoints for different components (tabs, menu, app).
|
||||
- improved: use an svg for the scroll-to-top button.
|
||||
- improved: use a better-matching icon and add transitions to the property layout toggle.
|
||||
- improved: themes are directly applied to tabs and menu rather than sync-ed between (infinite loading).
|
||||
- improved: error message "is notion running?" --> clearer "make sure notion isn't running!"
|
||||
- improved: auto-shrink system for tabs (max of 15 open in a window).
|
||||
- bugfix: disable fadein of selected block halo with snappy transitions.
|
||||
- bugfix: increase contrast of `--theme_dark--interactive_hover` in dark+ and dracula.
|
||||
- bugfix: tabs are focused properly for input.
|
||||
- bugfix: keyboard shortcut listeners are stricter so they don't conflict.
|
||||
- bugfix: dots indicating draggability are no longer next to the tabs mod in the menu.
|
||||
- bugfix: prevent empty hotkeys from triggering every keypress.
|
||||
- bugfix: don't try loading an empty default page url (infinite loading).
|
||||
- bugfix: remove `* { z-index: 1}` rule so format dropdowns in table view can be opened.
|
||||
- extension: "topbar icons" = replaces the topbar buttons with icons.
|
||||
- extension: "code line numbers" = adds line numbers to code blocks.
|
||||
- extension: "notion icons" = use custom icon sets directly in notion.
|
||||
- tweak: vertical indentation/relationship lines for lists.
|
||||
- tweak: scroll database toolbars horizontally if partially hidden.
|
||||
- tweak: condense bullet points (decrease line spacing).
|
||||
|
||||
> 📥 `npm i -g notion-enhancer@0.10.1`
|
||||
|
||||
### v0.10.0 (2020-11-02)
|
||||
|
||||
a flexibility update.
|
||||
|
||||
- new: mods can be reordered in the menu to control what order styling/scripts are added/executed in.
|
||||
higher up on the list = higher priority of application = loaded last in order to override others.
|
||||
(excluding the core, which though pinned to the top of the list is always loaded first so theming
|
||||
variables can be modified.)
|
||||
- new: relaunch button in tray menu.
|
||||
- new: a core mod option for a default page id/url (all new windows will load it instead of the
|
||||
normal "most recent" page).
|
||||
- new: css variables for increasing line spacing/paragraph margins.
|
||||
- new: patch the notion:// url scheme/protocol to work on linux.
|
||||
- new: menu shows theme conflicts + a core mod option to auto-resolve theme conflicts.
|
||||
- new: a `-n` cli option.
|
||||
- improved: menu will now respect integrated titlebar setting.
|
||||
- improved: use keyup listeners instead of a globalShortcut for the enhancements menu toggle.
|
||||
- improved: overwrite `app.asar.bak` if already exists (e.g. for app updates).
|
||||
- improved: additional menu option descriptions on hover.
|
||||
- improved: listen to prefers-color-scheme to better change theme in night shift.
|
||||
- improved: platform-specific option overrides for features not required on macOS.
|
||||
- improved: made extra padding at the bottom with the "focus mode" extension toggleable.
|
||||
- bugfix: removed messenger emoji set as the provider no longer supports it.
|
||||
- bugfix: remove shadow around light mode board headers.
|
||||
- bugfix: properly detect/respond to `EACCES`/`EBUSY` errors.
|
||||
- bugfix: night shift checks every interaction,
|
||||
will respond to system changes without any manual changes.
|
||||
- bugfix: toc blocks can have text colours.
|
||||
- bugfix: bypass preview extension works with the back/forward keyboard shortcuts.
|
||||
- bugfix: (maybe) fix csp issues under proxy.
|
||||
- bugfix: remove focus mode footer from neutral theme + better contrast in calendar views.
|
||||
- bugfix: improvements to the colour theming, particularly to make real- and fake-light/dark
|
||||
modes (as applied by the night shift extension) look consistent.
|
||||
relevant variables (assuming all are prefixed by `--theme_[dark|light]--`):
|
||||
`box-shadow`, `box-shadow_strong`, `select_input`, and `ui-border`
|
||||
- bugfix: font sizing applied to overlays/previews.
|
||||
- bugfix: removed typo in variable name for brown text.
|
||||
- bugfix: primary-colour text (mainly in "add a \_" popups) is now properly themed.
|
||||
- bugfix: right-to-left extension applies to text in columns.
|
||||
- bugfix: block text colour applies to text with backgrounds.
|
||||
- bugfix: font applied to wrong mode with littlepig dark.
|
||||
- bugfix: keep "empty" top bar visible in the menu.
|
||||
- bugfix: set NSRequiresAquaSystemAppearance to false in /Applications/Notion.app/Contents/Info.plist
|
||||
so system dark/light mode can be properly detected.
|
||||
- bugfix: make ctrl+f popover shadow less extreme.
|
||||
- bugfix: "weekly" calendar view name made case insensitive.
|
||||
- bugfix: re-show hidden windows when clicking on the dock.
|
||||
- tweak: sticky table/list rows.
|
||||
- theme: "material ocean" = an oceanic colour palette.
|
||||
- theme: "cherry cola" = a delightfully plummy, cherry cola flavored theme.
|
||||
- theme: "dracula" = a theme based on the popular dracula color palette
|
||||
originally by zeno rocha and friends.
|
||||
- extension: "tabs" = have multiple notion pages open in a single window. tabs can be controlled
|
||||
with keyboard shortcuts and dragged/reordered within/between windows.
|
||||
- extension: "scroll to top" = add an arrow above the help button to scroll back to the top of a page.
|
||||
- extension: "tweaks" = common style/layout changes. includes:
|
||||
- new: make transitions snappy/0s.
|
||||
- new: in-page columns are disabled/wrapped and pages are wider when
|
||||
the window is narrower than 600px for improved responsiveness.
|
||||
- new: thicker bold text for better visibility.
|
||||
- new: more readable line spacing.
|
||||
- moved: smooth scrollbars.
|
||||
- moved: change dragarea height.
|
||||
- moved: hide help.
|
||||
|
||||
a fork of notion-deb-builder that does generate an app.asar has been created and is once again supported.
|
||||
|
||||
> 📥 `npm i -g notion-enhancer@0.10.0`
|
||||
|
||||
### v0.9.1 (2020-09-26)
|
||||
|
||||
- bugfix: font chooser will continue iterating through fonts after encountering a blank option.
|
||||
- bugfix: block indents are no longer overriden.
|
||||
- bugfix: neutral does not force full width pages.
|
||||
- bugfix: bypass preview extension works with the back/forward arrows.
|
||||
- bugfix: check all views on a page for a weekly calendar.
|
||||
- bugfix: emoji sets no longer modifies the user agent = doesn't break hotkeys.
|
||||
|
||||
> 📥 `npm i -g notion-enhancer@0.9.1`
|
||||
|
||||
### v0.9.0 (2020-09-20)
|
||||
|
||||
a feature and cleanup update.
|
||||
|
||||
- improved: halved the number of css rules used -> much better performance.
|
||||
- improved: font imports must be define in the `mod.js` so that they can also be used in
|
||||
the enhancements menu.
|
||||
- improved: tiling window-manager support (can hide titlebars entirely without dragarea/buttons).
|
||||
- improved: extensions menu search is now case insensitive and includes options, inputs and versions.
|
||||
the search box can also for focused with `CMD/CTRL+F`.
|
||||
- improved: extensions menu filters shown either a ✓ or × to help understand the current state.
|
||||
- improved: added individual text-colour rules for different background colours.
|
||||
- improved: added variables for callout colouring.
|
||||
- improved: replaced with `helpers.getNotion()` with the constant `helpers.__notion` to reduce
|
||||
repeated function calls.
|
||||
- improved: added variables for page width.
|
||||
- improved/bugfix: emoji sets extension should now work on macOS and will change user agent to use
|
||||
real emojis instead of downloading images when system default is selected.
|
||||
- bugfix: enhancer settings should no longer reset on update (though this will not have
|
||||
effect until the release after this one).
|
||||
- bugfix: blue select tags are no longer purple.
|
||||
- bugfix: page titles now respond to small-text mode.
|
||||
- bugfix: weekly calendar view height is now sized correctly according to its contents.
|
||||
- bugfix: made the open enhancements menu hotkey configurable and changed the default to `ALT+E`.
|
||||
to remove conflict with the inline code highlight shortcut.
|
||||
- bugfix: update property-layout to match notion changes again.
|
||||
- bugfix: updated some of the tweak styling to match notion changes.
|
||||
- bugfix: block-level text colours are now changed properly.
|
||||
- bugfix: do not require data folder during installation, to prevent `sudo` attempting to
|
||||
create it in `/var/root/`.
|
||||
- bugfix: bullet points/checkboxes will now align properly in the right-to-left extension.
|
||||
- themes: "littlepig" (light + dark) = monospaced themes using emojis and colourful text.
|
||||
- extension: "font chooser" = customize fonts. for each option, type in the name of the font you would like to use,
|
||||
or leave it blank to not change anything.
|
||||
- extension: "always on top" = add an arrow/button to show the notion window on top of other windows
|
||||
even if it's not focused.
|
||||
- extension: "calendar scroll" = add a button to scroll down to the current week in fullpage/infinite-scroll calendars.
|
||||
- extension: "hide help button" = hide the help button if you don't need it.
|
||||
- extension: "bypass preview" = go straight to the normal full view when opening a page.
|
||||
- extension: "word counter" = add page details: word/character/sentence/block count & speaking/reading times.
|
||||
|
||||
notion-deb-builder has been discovered to not generate an app.asar and so is no longer supported.
|
||||
|
||||
> 📥 `npm i -g notion-enhancer@0.9.0`
|
||||
|
||||
### v0.8.5 (2020-08-29)
|
||||
|
||||
- bugfix: separate text highlight and select tag variables.
|
||||
- bugfix: bypass CSP for the `enhancement://` protocol - was failing on some platforms?
|
||||
|
||||
> 📥 `npm i -g notion-enhancer@0.8.5`
|
||||
|
||||
### v0.8.4 (2020-08-29)
|
||||
|
||||
- bugfix: property-layout now works consistently with or without a banner.
|
||||
|
||||
> 📥 `npm i -g notion-enhancer@0.8.4`
|
||||
|
||||
### v0.8.3 (2020-08-29)
|
||||
|
||||
previous release was a mistake: it did as intended on linux, but broke windows.
|
||||
this should achieve the same thing in a more compatible way.
|
||||
|
||||
> 📥 `npm i -g notion-enhancer@0.8.3`
|
||||
|
||||
### v0.8.2 (2020-08-28)
|
||||
|
||||
some things you just can't test until production... fixed the auto-installer
|
||||
to use `./bin.js` instead of `notion-enhancer`
|
||||
|
||||
> 📥 `npm i -g notion-enhancer@0.8.2`
|
||||
|
||||
### v0.8.1 (2020-08-28)
|
||||
|
||||
a clarity and stability update.
|
||||
|
||||
- improved: more informative cli error messages (original ones can be accessed with the `-d/--dev` flag).
|
||||
- bugfix: gallery variable didn't apply on fullpage.
|
||||
- bugfix: date picker hid current date number.
|
||||
- bugfix: small-text pages should now work as expected.
|
||||
- bugfix: padding issues in page previews.
|
||||
- bugfix: property-layout extension had been broken by internal notion changes.
|
||||
- bugfix: linux installer path typo.
|
||||
- bugfix: caret-color was being mistaken for color and block-level text colouring was broken.
|
||||
- improved: auto-application on install.
|
||||
|
||||
> 📥 `npm i -g notion-enhancer@0.8.1`
|
||||
|
||||
### v0.8.0 (2020-08-27)
|
||||
|
||||
complete rewrite with node.js.
|
||||
|
||||
- new: simpler cli installation system (inc. commands: `apply`, `remove`, and `check`).
|
||||
- new: mod loading system (easier to create new mods, adds to notion rather than overwriting).
|
||||
- new: mod configuration menu.
|
||||
- improved: more theming variable coverage - inc. light theme and sizing/spacing.
|
||||
- bugfix: non-reproducable errors with python.
|
||||
- bugfix: better launcher patching on linux.
|
||||
- bugfix: fix frameless window issue introduced by notion desktop 2.0.9.
|
||||
- extension: "custom inserts" = link files for small client-side tweaks.
|
||||
- extension: "bracketed links" = render links surrounded with \[\[brackets]] instead of underlined.
|
||||
- extension: "focus mode" = hide the titlebar/menubar if the sidebar is closed (will be shown on hover).
|
||||
- theme: "dark+" = a vivid-colour near-black theme.
|
||||
- theme: "neutral" = smoother colours and fonts, designed to be more pleasing to the eye.
|
||||
- theme: "gameish" = a purple, "gamer-styled" theme with a blocky-font.
|
||||
- theme: "pastel dark" = a smooth-transition true dark theme with a hint of pastel.
|
||||
- extension: "emoji sets" = pick from a variety of emoji styles to use.
|
||||
- extension: "night shift" = sync dark/light theme with the system (overrides normal theme setting).
|
||||
- extension: "right-to-left" = enables auto rtl/ltr text direction detection. (ported from [github.com/obahareth/notion-rtl](https://github.com/obahareth/notion-rtl).)
|
||||
- extension: "weekly view" = calendar views named "weekly" will show only the 7 days of this week. (ported from [github.com/adihd/notionweeklyview](https://github.com/adihd/notionweeklyview).)]
|
||||
- extension: "property layout" = auto-collapse page properties that usually push down page content. (ported from [github.com/alexander-kazakov/notion-layout-extension](https://github.com/alexander-kazakov/notion-layout-extension).)
|
||||
|
||||
> 📥 `npm i -g notion-enhancer@0.8.0`
|
||||
|
||||
### v0.7.0 (2020-07-09)
|
||||
|
||||
- new: tray option to use system default emojis (instead of twitter's emojiset).
|
||||
- new: mac support (identical functionality to other platforms with the
|
||||
exception of the native minimise/maximise/close buttons being kept, as they integrate
|
||||
better with the OS while not being out-of-place in notion).
|
||||
- new: notion-deb-builder support for linux.
|
||||
- new: an alert will be shown if there is an update available for the enhancer.
|
||||
- improved: replaced button symbols with svgs for multi-platform support.
|
||||
- improved: window close button is now red on hover (thanks to [@torchatlas](https://github.com/torchatlas)).
|
||||
- bugfix: `cleaner.py` patched for linux.
|
||||
- bugfix: tray now operates as expected on linux.
|
||||
- bugfix: odd mix of `\\` and `/` being used for windows filepaths.
|
||||
- bugfix: app no longer crashes when sidebar is toggled.
|
||||
|
||||
> 📥 [notion-enhancer.v0.7.0.zip](https://github.com/notion-enhancer/desktop/archive/v0.7.0.zip)
|
||||
|
||||
### v0.6.0 (2020-06-30)
|
||||
|
||||
- style: custom fonts.
|
||||
- style: font resizing.
|
||||
- style: hide discussions (thanks to [u/Roosmaryn](https://www.reddit.com/user/Roosmaryn/)).
|
||||
- new: custom colour theming, demonstrated via the dark+ theme.
|
||||
- new: linux support (thanks to [@Blacksuan19](https://github.com/Blacksuan19)).
|
||||
- improved: if hotkey is pressed while notion is unfocused, it will bring it to the front rather than hiding it.
|
||||
- improved: stop window buttons breaking at smaller widths.
|
||||
- improved: more obviously visible drag area.
|
||||
- bugfix: specify UTF-8 encoding to prevent multibyte/gbk codec errors (thanks to [@etnperlong](https://github.com/etnperlong)).
|
||||
|
||||
> 📥 [notion-enhancer.v0.6.0.zip](https://github.com/notion-enhancer/desktop/archive/v0.6.0.zip)
|
||||
|
||||
### v0.5.0 (2020-05-23)
|
||||
|
||||
@ -9,11 +386,16 @@ but can still easily be enabled by following instructions in the [docs](README.m
|
||||
- new: reload window with f5.
|
||||
- improved: code has been refactored and cleaned up,
|
||||
inc. file renaming and a `customiser.py` that doesn't require
|
||||
a run of `cleaner.py` to build updates.
|
||||
a run of `cleaner.py` to build modifications.
|
||||
improved: scrollbar colours that fit better with notion's theming.
|
||||
- bugfix: un-break having multiple notion windows open.
|
||||
|
||||
_(forked by [@dragonwocky](https://github.com/dragonwocky).)_
|
||||
> 📥 [notion-enhancer.v0.5.0.zip](https://github.com/notion-enhancer/desktop/archive/v0.5.0.zip)
|
||||
|
||||
**development here taken over by [@dragonwocky](https://github.com/dragonwocky).**
|
||||
|
||||
**the ~~crossed out~~ features below are no longer features included by default,**
|
||||
**but can still easily be added as [custom tweaks](https://github.com/notion-enhancer/tweaks).**
|
||||
|
||||
### v0.4.1 (2020-02-13)
|
||||
|
||||
|
5
LICENSE
@ -1,7 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 TarasokUA
|
||||
Copyright (c) 2020 dragonwocky
|
||||
Copyright (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@ -19,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
|
242
README.md
@ -1,241 +1,5 @@
|
||||
# notion enhancer
|
||||
# notion-enhancer/desktop
|
||||
|
||||
an enhancer/customiser for the all-in-one productivity workspace [notion.so](https://www.notion.so/)
|
||||
Customise the all-in-one productivity workspace Notion.
|
||||
|
||||
## installation
|
||||
|
||||
currently, only win10 is supported. it is possible to run this script via the wsl to modify the win10 notion app.
|
||||
|
||||
(the [styles](#styling) should also work for the web version.
|
||||
these can be installed via an extension like [stylus](https://chrome.google.com/webstore/detail/stylus/clngdbkpkpeebahjckkjfobafhncgmne?hl=en)
|
||||
or a built-in feature like [userChrome.css](https://www.userchrome.org/).)
|
||||
|
||||
1. install [node.js](https://nodejs.org/en/) (if using the wsl, it is recommended to install via [nvm](https://github.com/nvm-sh/nvm#install--update-script)).
|
||||
2. install [python](https://www.python.org/) (if using the wsl, follow [this guide](https://docs.python-guide.org/starting/install3/linux/)).
|
||||
3. reboot.
|
||||
4. in cmd (on windows) or bash (with wsl), run `npm install -g asar` (check installation by running `asar`).
|
||||
5. [download this enhancer](https://github.com/dragonwocky/notion-enhancer/archive/master.zip) & extract to a location it can safely remain (this must be in the windows filesystem,
|
||||
even if you are running the script from the wsl).
|
||||
6. ensure notion is closed.
|
||||
7. optional: to remove previous versions of notion enhancer, run `cleaner.py`
|
||||
8. optional: modify the `resources/user.css` files to your liking.
|
||||
9. run `customiser.py` to build changes.
|
||||
|
||||
done: run notion and enjoy.
|
||||
|
||||
**oh no, now my app won't open!**
|
||||
|
||||
1. kill any notion tasks in the task manager (`ctrl+shift+esc`).
|
||||
2. run `cleaner.py`.
|
||||
3. reboot.
|
||||
4. follow instructions above (ensuring notion _isn't_ running! again, check task manager).
|
||||
|
||||
## this is a fork
|
||||
|
||||
credit where credit is due, this was originally made by Uzver (github: [@TarasokUA](https://github.com/TarasokUA),
|
||||
telegram: [UserFromUkraine](https://t.me/UserFromUkraine), discord: Uzver#8760).
|
||||
|
||||
he has approved my go-ahead with this fork, as he himself no longer wishes to continue development on the project.
|
||||
|
||||
## features
|
||||
|
||||
### titlebar
|
||||
|
||||
default windows titlebar/frame has been replaced by one more fitting to the theme of the app.
|
||||
|
||||
this includes the addition of an extra button, "always on top"
|
||||
symbolised with an arrow (4th from the right). when toggled to point up,
|
||||
notion will remain the top visible window even if not focused.
|
||||
|
||||
to customise which characters are used for these buttons, open in the `resources/preload.js` file,
|
||||
find the relevant button (read the comments) and replace its icon with your chosen unicode character (e.g.
|
||||
replacing `element.innerHTML = '▢';` with `element.innerHTML = '🙄';`).
|
||||
|
||||
### nicer scrollbars
|
||||
|
||||
i mean, yeah. get rid of those ugly default scrollbars and use nice inconspicuous
|
||||
ones that actually look as if they're part of notion.
|
||||
|
||||
to add these to the web version, copy lines 44 - 75 from `user.css` into your css customiser.
|
||||
|
||||
### hotkeys
|
||||
|
||||
- **reload window**: in addition to the built-in `CmdOrCtrl+R` reload,
|
||||
you can now reload a window with `F5`.
|
||||
- **toggle all notion windows to/from the tray**: `CmdOrCtrl+Shift+A` by default.
|
||||
|
||||
to set your own toggle hotkey, open `customiser.py` and change line 16 (`hotkey = 'CmdOrCtrl+Shift+A'`)
|
||||
to your preference. you will need to run or re-run `customiser.py` afterwards.
|
||||
|
||||
### tray
|
||||
|
||||
- single-click to toggle app visibility. right click to open menu.
|
||||
- settings will be saved in `%localappdata%/Programs/Notion/resources/app/user-preferences.json`
|
||||
- **run on startup**: run notion on boot/startup. (default: true)
|
||||
- **hide on open**: hide the launch of notion to the tray. (default: false)
|
||||
- **open maximised**: maximise the app on open. (default: false)
|
||||
- **close to tray**: close window to tray rather than closing outright
|
||||
on click of `⨉`. does not apply if multiple notion windows are open. (default: false)
|
||||
|
||||
### styling
|
||||
|
||||
due to `customiser.py` setting up a direct link to `resources/user.css`,
|
||||
changes will be applied instantly on notion reload
|
||||
(no need to re-run `customiser.py` every time you want to change some styles).
|
||||
|
||||
these should also work for the web version, if copied into your css customiser.
|
||||
|
||||
css below will work for every instance of the element, but if you wish to hide only a specific element
|
||||
(e.g. the '+ new' table row) it is recommended that you prepend each selector with `[data-block-id='ID']` ([video tutorial on fetching IDs](https://www.youtube.com/watch?v=6V7eqShm_4w)).
|
||||
|
||||
#### wider page view
|
||||
|
||||
```css
|
||||
.notion-peek-renderer > div:nth-child(2) {
|
||||
max-width: 85vw !important;
|
||||
}
|
||||
```
|
||||
|
||||
#### thinner cover image
|
||||
|
||||
```css
|
||||
[style^='position: relative; width: 100%; display: flex; flex-direction: column; align-items: center; height: 30vh;'] {
|
||||
height: 12vh !important;
|
||||
}
|
||||
[style^='position: relative; width: 100%; display: flex; flex-direction: column; align-items: center; height: 30vh;']
|
||||
img {
|
||||
height: 20vh !important;
|
||||
}
|
||||
```
|
||||
|
||||
#### table columns below 100px
|
||||
|
||||
**not recommended!** this is unreliable and will cause bugs.
|
||||
coincidentally, this is also what the youtube video linked above shows how to do.
|
||||
as it is a per-table-column style, unlike all others here, it must be prepended with the block ID.
|
||||
|
||||
```css
|
||||
[data-block-id^='ID']
|
||||
> [style^='display: flex; position: absolute; background: rgb(47, 52, 55); z-index: 82; height: 33px; color: rgba(255, 255, 255, 0.6);']
|
||||
> div:nth-child(1)
|
||||
> div:nth-child(10)
|
||||
> div:nth-child(1),
|
||||
[data-block-id^='ID']
|
||||
> [style^='position: relative; min-width: calc(100% - 192px);']
|
||||
> [data-block-id]
|
||||
> div:nth-child(10),
|
||||
[data-block-id^='ID'] > div:nth-child(5) > div:nth-child(10) {
|
||||
width: 45px !important;
|
||||
}
|
||||
[data-block-id^='ID']
|
||||
[style^='position: absolute; top: 0px; left: 0px; pointer-events: none;']:not(.notion-presence-container) {
|
||||
display: none;
|
||||
}
|
||||
```
|
||||
|
||||
#### hide '+ new' table row
|
||||
|
||||
```css
|
||||
.notion-table-view-add-row {
|
||||
display: none !important;
|
||||
}
|
||||
```
|
||||
|
||||
#### hide calculations table row
|
||||
|
||||
```css
|
||||
.notion-table-view-add-row + div {
|
||||
display: none !important;
|
||||
}
|
||||
```
|
||||
|
||||
#### hide '+ new' board row
|
||||
|
||||
```css
|
||||
.notion-board-group
|
||||
[style='user-select: none; transition: background 120ms ease-in 0s; cursor: pointer; display: inline-flex; align-items: center; flex-shrink: 0; white-space: nowrap; height: 32px; border-radius: 3px; font-size: 14px; line-height: 1.2; min-width: 0px; padding-left: 6px; padding-right: 8px; color: rgba(255, 255, 255, 0.4); width: 100%;'] {
|
||||
display: none !important;
|
||||
}
|
||||
```
|
||||
|
||||
#### hide board view hidden columns
|
||||
|
||||
```css
|
||||
.notion-board-view > [data-block-id] > div:nth-last-child(2),
|
||||
.notion-board-view > [data-block-id] > div:first-child > div:nth-last-child(2) {
|
||||
display: none !important;
|
||||
}
|
||||
```
|
||||
|
||||
#### hide board view 'add a group'
|
||||
|
||||
```css
|
||||
.notion-board-view > [data-block-id] > div:last-child,
|
||||
.notion-board-view > [data-block-id] > div:first-child > div:last-child {
|
||||
display: none !important;
|
||||
}
|
||||
```
|
||||
|
||||
#### centre-align table column headers
|
||||
|
||||
```css
|
||||
.notion-table-view-header-cell > div > div {
|
||||
margin: 0px auto;
|
||||
}
|
||||
```
|
||||
|
||||
#### smaller table column header icons
|
||||
|
||||
```css
|
||||
[style^='display: flex; position: absolute; background: rgb(47, 52, 55); z-index: 82; height: 33px; color: rgba(255, 255, 255, 0.6);']
|
||||
div:nth-child(1)
|
||||
svg {
|
||||
height: 10px !important;
|
||||
width: 10px !important;
|
||||
margin-right: -4px;
|
||||
}
|
||||
```
|
||||
|
||||
#### remove icons from table column headers
|
||||
|
||||
```css
|
||||
.notion-table-view-header-cell [style^='margin-right: 6px;'] {
|
||||
display: none !important;
|
||||
}
|
||||
```
|
||||
|
||||
#### removing/decreasing side padding for tables
|
||||
|
||||
```css
|
||||
[style^='flex-shrink: 0; flex-grow: 1; width: 100%; max-width: 100%; display: flex; align-items: center; flex-direction: column; font-size: 16px; color: rgba(255, 255, 255, 0.9); padding: 0px 96px 30vh;']
|
||||
.notion-table-view,
|
||||
[class='notion-scroller'] > .notion-table-view {
|
||||
padding-left: 35px !important;
|
||||
padding-right: 15px !important;
|
||||
min-width: 0% !important;
|
||||
}
|
||||
[style^='flex-shrink: 0; flex-grow: 1; width: 100%; max-width: 100%; display: flex; align-items: center; flex-direction: column; font-size: 16px; color: rgba(255, 255, 255, 0.9); padding: 0px 96px 30vh;']
|
||||
.notion-selectable
|
||||
.notion-scroller.horizontal::-webkit-scrollbar-track {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
```
|
||||
|
||||
#### removing/decreasing side padding for boards
|
||||
|
||||
```css
|
||||
.notion-board-view {
|
||||
padding-left: 10px !important;
|
||||
padding-right: 10px !important;
|
||||
}
|
||||
```
|
||||
|
||||
## other details
|
||||
|
||||
i have an unhealthy habit of avoiding capital letters. nothing enforces this, i just do it.
|
||||
|
||||
the notion logo belongs entirely to the notion team, and was sourced from their
|
||||
[media kit](https://www.notion.so/Media-Kit-205535b1d9c4440497a3d7a2ac096286).
|
||||
|
||||
if you have any questions, check [my website](https://dragonwocky.me/) for contact details.
|
||||
[read the docs online](https://notion-enhancer.github.io/)
|
||||
|
331
bin.mjs
Executable file
@ -0,0 +1,331 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import os from "node:os";
|
||||
import { createRequire } from "node:module";
|
||||
import chalk from "chalk-template";
|
||||
import arg from "arg";
|
||||
import {
|
||||
backupApp,
|
||||
enhanceApp,
|
||||
getInsertVersion,
|
||||
getResourcePath,
|
||||
restoreApp,
|
||||
setNotionPath,
|
||||
} from "./scripts/enhance-desktop-app.mjs";
|
||||
import { greaterThan } from "./src/core/updateCheck.mjs";
|
||||
import { existsSync } from "node:fs";
|
||||
|
||||
const nodeRequire = createRequire(import.meta.url),
|
||||
manifest = nodeRequire("./package.json");
|
||||
|
||||
let __quiet, __debug;
|
||||
const print = (...args) => __quiet || process.stdout.write(chalk(...args)),
|
||||
printObject = (value) => __quiet || console.dir(value, { depth: null }),
|
||||
clearLine = `\r\x1b[K`,
|
||||
showCursor = `\x1b[?25h`,
|
||||
hideCursor = `\x1b[?25l`,
|
||||
cursorUp = (n) => `\x1b[${n}A`,
|
||||
cursorForward = (n) => `\x1b[${n}C`;
|
||||
|
||||
let __confirmation;
|
||||
const readStdin = () => {
|
||||
return new Promise((res) => {
|
||||
process.stdin.resume();
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.once("data", (key) => {
|
||||
process.stdin.pause();
|
||||
res(key);
|
||||
});
|
||||
});
|
||||
},
|
||||
promptConfirmation = async (prompt) => {
|
||||
let input;
|
||||
const validInputs = ["Y", "y", "N", "n"],
|
||||
promptLength = ` > ${prompt} [Y/n]: `.length;
|
||||
// prevent line clear remove existing stdout
|
||||
print`\n`;
|
||||
do {
|
||||
// clear line and repeat prompt until valid input is received
|
||||
print`${cursorUp(1)}${clearLine} {inverse > ${prompt} [Y/n]:} `;
|
||||
// autofill prompt response if --yes, --no or --quiet flags passed
|
||||
if (validInputs.includes(__confirmation)) {
|
||||
input = __confirmation;
|
||||
print`${__confirmation}\n`;
|
||||
} else input = (await readStdin()).trim();
|
||||
if (!input) {
|
||||
// default to Y if enter is pressed w/out input
|
||||
input = "Y";
|
||||
print`${cursorUp(1)}${cursorForward(promptLength)}Y\n`;
|
||||
}
|
||||
} while (!validInputs.includes(input));
|
||||
// move cursor to immediately after input
|
||||
print`${cursorUp(1)}${cursorForward(promptLength + 1)}`;
|
||||
return input;
|
||||
};
|
||||
|
||||
let __spinner;
|
||||
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
||||
stopSpinner = () => {
|
||||
if (!__spinner) return;
|
||||
clearInterval(__spinner);
|
||||
// show cursor and overwrite spinner with arrow on completion
|
||||
print`\b{bold.yellow →}\n${showCursor}`;
|
||||
__spinner = undefined;
|
||||
},
|
||||
startSpinner = () => {
|
||||
// cleanup prev spinner if necessary
|
||||
stopSpinner();
|
||||
// hide cursor and print first frame
|
||||
print`${hideCursor}{bold.yellow ${spinnerFrames[0]}}`;
|
||||
let i = 0;
|
||||
__spinner = setInterval(() => {
|
||||
i++;
|
||||
// overwrite spinner with next frame
|
||||
print`\b{bold.yellow ${spinnerFrames[i % spinnerFrames.length]}}`;
|
||||
}, 80);
|
||||
};
|
||||
|
||||
const compileOptsToArgSpec = (options) => {
|
||||
const args = {};
|
||||
for (const [opt, [type]] of options) {
|
||||
const aliases = opt.split(", ").map((alias) => alias.split("=")[0]),
|
||||
param = aliases[1] ?? aliases[0];
|
||||
args[param] = type;
|
||||
for (let i = 0; i < aliases.length; i++) {
|
||||
if (aliases[i] === param) continue;
|
||||
args[aliases[i]] = param;
|
||||
}
|
||||
}
|
||||
return args;
|
||||
},
|
||||
compileOptsToJsonOutput = (options) => {
|
||||
// the structure used to define options above
|
||||
// is convenient and compact, but requires additional
|
||||
// parsing to understand. this function processes
|
||||
// options into a more explicitly defined structure
|
||||
return options.map(([opt, [type, description]]) => {
|
||||
const option = {
|
||||
aliases: opt.split(", ").map((alias) => alias.split("=")[0]),
|
||||
type,
|
||||
description,
|
||||
},
|
||||
example = opt
|
||||
.split(", ")
|
||||
.map((alias) => alias.split("=")[1])
|
||||
.find((value) => value);
|
||||
if (example) option.example = example;
|
||||
return option;
|
||||
});
|
||||
};
|
||||
|
||||
let __json;
|
||||
const printHelp = (commands, options) => {
|
||||
const { name, version, homepage } = manifest,
|
||||
usage = `${name} <command> [options]`;
|
||||
if (__json) {
|
||||
printObject({
|
||||
name,
|
||||
version,
|
||||
homepage,
|
||||
usage,
|
||||
commands: Object.fromEntries(commands),
|
||||
options: compileOptsToJsonOutput(options),
|
||||
});
|
||||
} else {
|
||||
const cmdPad = Math.max(...commands.map(([cmd]) => cmd.length)),
|
||||
optPad = Math.max(...options.map((opt) => opt[0].length)),
|
||||
parseCmd = (cmd) =>
|
||||
chalk` ${cmd[0].padEnd(cmdPad)} {grey :} ${cmd[1]}`,
|
||||
parseOpt = (opt) =>
|
||||
chalk` ${opt[0].padEnd(optPad)} {grey :} ${opt[1][1]}`;
|
||||
print`{bold.whiteBright.underline ${name} v${version}}\n{grey ${homepage}}
|
||||
\n{bold.whiteBright USAGE}\n${name} <command> [options]
|
||||
\n{bold.whiteBright COMMANDS}\n${commands.map(parseCmd).join("\n")}
|
||||
\n{bold.whiteBright OPTIONS}\n${options.map(parseOpt).join("\n")}\n`;
|
||||
}
|
||||
},
|
||||
printVersion = () => {
|
||||
if (__json) {
|
||||
printObject({
|
||||
[manifest.name]: manifest.version,
|
||||
node: process.version.slice(1),
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
os: os.release(),
|
||||
});
|
||||
} else {
|
||||
const nodeVersion = `node@${process.version}`,
|
||||
enhancerVersion = `${manifest.name}@v${manifest.version}`,
|
||||
osVersion = `${process.platform}-${process.arch}/${os.release()}`;
|
||||
print`${enhancerVersion} via ${nodeVersion} on ${osVersion}\n`;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const commands = [
|
||||
// ["command", "description"]
|
||||
["apply", "Inject the notion-enhancer into Notion desktop."],
|
||||
["remove", "Restore Notion desktop to its pre-enhanced state."],
|
||||
["check", "Report Notion desktop's enhancement state."],
|
||||
],
|
||||
// prettier-ignore
|
||||
options = [
|
||||
// ["alias, option=example", [type, "description"]]
|
||||
["--path=</path/to/notion/resources>", [String, "Manually provide a Notion installation location."]],
|
||||
["--no-backup", [Boolean, "Skip backup; enhancement will be irreversible."]],
|
||||
["--json", [Boolean, "Output JSON from the `check` and `--version` commands."]],
|
||||
["-y, --yes", [Boolean, 'Skip prompts; assume "yes" and run non-interactively.']],
|
||||
["-n, --no", [Boolean, 'Skip prompts; assume "no" and run non-interactively.']],
|
||||
["-q, --quiet", [Boolean, 'Skip prompts; assume "no" unless -y and hide all output.']],
|
||||
["-d, --debug", [Boolean, "Show detailed error messages and keep extracted files."]],
|
||||
["-h, --help", [Boolean, "Display usage information for this CLI."]],
|
||||
["-v, --version", [Boolean, "Display this CLI's version number."]],
|
||||
];
|
||||
|
||||
const args = arg(compileOptsToArgSpec(options));
|
||||
if (args["--debug"]) __debug = true;
|
||||
if (args["--quiet"]) __quiet = true;
|
||||
if (args["--json"]) __json = true;
|
||||
if (args["--no"] || args["--quiet"]) __confirmation = "n";
|
||||
if (args["--yes"]) __confirmation = "y";
|
||||
if (args["--help"]) printHelp(commands, options), process.exit();
|
||||
if (args["--version"]) printVersion(), process.exit();
|
||||
if (args["--path"]) setNotionPath(args["--path"]);
|
||||
|
||||
const appPath = getResourcePath("app.asar"),
|
||||
backupPath = getResourcePath("app.asar.bak"),
|
||||
insertVersion = await getInsertVersion(),
|
||||
updateAvailable = greaterThan(manifest.version, insertVersion);
|
||||
|
||||
const messages = {
|
||||
"notion-found": insertVersion
|
||||
? // prettier-ignore
|
||||
`Notion desktop found with ${manifest.name} v${insertVersion
|
||||
} applied${updateAvailable ? "" : " (up to date)"}.`
|
||||
: `Notion desktop found (no enhancements applied).`,
|
||||
"notion-not-found": `Notion desktop not found.`,
|
||||
|
||||
// prettier-ignore
|
||||
"update-available": chalk`v${manifest.version
|
||||
} is available! To apply, run {underline ${manifest.name} apply -y}.`,
|
||||
// prettier-ignore
|
||||
"update-confirm": `${updateAvailable ? "Upgrade" : "Downgrade"
|
||||
} to ${manifest.name}${manifest.name} v${manifest.version}?`,
|
||||
|
||||
"backup-found": `Restoring to pre-enhanced state from backup...`,
|
||||
"backup-not-found": chalk`No backup found: to restore Notion desktop to its pre-enhanced state,
|
||||
uninstall it and reinstall Notion from {underline https://www.notion.so/desktop}.`,
|
||||
|
||||
"backup-app": `Backing up app before enhancement...`,
|
||||
"enhance-app": `Enhancing and patching app sources...`,
|
||||
};
|
||||
const SUCCESS = chalk`{bold.whiteBright SUCCESS} {green ✔}`,
|
||||
FAILURE = chalk`{bold.whiteBright FAILURE} {red ✘}`,
|
||||
CANCELLED = chalk`{bold.whiteBright CANCELLED} {red ✘}`,
|
||||
INCOMPLETE = Symbol();
|
||||
|
||||
const interactiveRestore = async () => {
|
||||
if (!backupPath || !existsSync(backupPath)) {
|
||||
print` {red * ${messages["backup-not-found"]}}\n`;
|
||||
return FAILURE;
|
||||
}
|
||||
print` {grey * ${messages["backup-found"]}} `;
|
||||
startSpinner();
|
||||
await restoreApp();
|
||||
stopSpinner();
|
||||
return SUCCESS;
|
||||
};
|
||||
|
||||
const getNotion = () => {
|
||||
if (!appPath || !existsSync(appPath)) {
|
||||
print` {red * ${messages["notion-not-found"]}}\n`;
|
||||
return FAILURE;
|
||||
} else {
|
||||
print` {grey * ${messages["notion-found"]}}\n`;
|
||||
return INCOMPLETE;
|
||||
}
|
||||
},
|
||||
compareVersions = async () => {
|
||||
if (!insertVersion) return INCOMPLETE;
|
||||
// same version already applied
|
||||
if (insertVersion === manifest.version) return SUCCESS;
|
||||
// diff version already applied
|
||||
print` {grey * ${messages["notion-found"]}}\n`;
|
||||
const replace = await promptConfirmation(messages["update-confirm"]);
|
||||
print`\n`;
|
||||
return ["Y", "y"].includes(replace)
|
||||
? (await interactiveRestore()) === SUCCESS
|
||||
? INCOMPLETE
|
||||
: FAILURE
|
||||
: CANCELLED;
|
||||
},
|
||||
interactiveEnhance = async () => {
|
||||
if (!args["--no-backup"]) {
|
||||
print` {grey * ${messages["backup-app"]}} `;
|
||||
startSpinner();
|
||||
await backupApp();
|
||||
stopSpinner();
|
||||
}
|
||||
print` {grey * ${messages["enhance-app"]}} `;
|
||||
startSpinner();
|
||||
await enhanceApp(__debug);
|
||||
stopSpinner();
|
||||
return SUCCESS;
|
||||
};
|
||||
|
||||
switch (args["_"][0]) {
|
||||
case "apply": {
|
||||
print`{bold.whiteBright [${manifest.name.toUpperCase()}] APPLY}\n`;
|
||||
let res = getNotion();
|
||||
if (res === INCOMPLETE) res = await compareVersions();
|
||||
if (res === INCOMPLETE) res = await interactiveEnhance();
|
||||
print`${res}\n`;
|
||||
break;
|
||||
}
|
||||
case "remove": {
|
||||
print`{bold.whiteBright [${manifest.name.toUpperCase()}] REMOVE}\n`;
|
||||
let res = getNotion();
|
||||
if (res === INCOMPLETE) {
|
||||
res = insertVersion ? await interactiveRestore() : SUCCESS;
|
||||
}
|
||||
print`${res}\n`;
|
||||
break;
|
||||
}
|
||||
case "check": {
|
||||
if (__json) {
|
||||
const cliVersion = manifest.version,
|
||||
state = { appPath, backupPath, insertVersion, cliVersion };
|
||||
if (appPath && !existsSync(appPath)) state.appPath = null;
|
||||
if (backupPath && !existsSync(backupPath)) state.backupPath = null;
|
||||
printObject(state), process.exit();
|
||||
}
|
||||
print`{bold.whiteBright [${manifest.name.toUpperCase()}] CHECK}\n`;
|
||||
let res = getNotion();
|
||||
if (res === INCOMPLETE && updateAvailable) {
|
||||
print` {grey * ${messages["update-available"]}}\n`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
printHelp(commands, options);
|
||||
}
|
||||
} catch (err) {
|
||||
stopSpinner();
|
||||
const message = err.message.split("\n")[0];
|
||||
if (__debug) {
|
||||
print`{bold.red ${err.name}:} ${message}\n{grey ${err.stack
|
||||
.split("\n")
|
||||
.splice(1)
|
||||
.map((at) => at.replace(/\s{4}/g, " "))
|
||||
.join("\n")}}`;
|
||||
} else {
|
||||
print`{bold.red Error:} ${message} {grey (Run with -d for more information.)}\n`;
|
||||
}
|
||||
}
|
58
cleaner.py
@ -1,58 +0,0 @@
|
||||
|
||||
# Notion Enhancer
|
||||
# (c) 2020 dragonwocky <thedragonring.bod@gmail.com>
|
||||
# (c) 2020 TarasokUA
|
||||
# (https://dragonwocky.me/) under the MIT license
|
||||
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
import subprocess
|
||||
from shutil import rmtree
|
||||
from time import sleep
|
||||
|
||||
# f'{bold}=== title ==={normal}' = headers
|
||||
# '*' = information
|
||||
# '...' = actions
|
||||
# '###' = warnings
|
||||
# '>' = exit
|
||||
|
||||
bold = '\033[1m'
|
||||
normal = '\033[0m'
|
||||
|
||||
print(f'{bold}=== NOTION ENHANCER CLEANING LOG ==={normal}\n')
|
||||
try:
|
||||
filepath = ''
|
||||
if 'microsoft' in platform.uname()[3].lower() and sys.platform == 'linux':
|
||||
filepath = '/mnt/c/' + \
|
||||
subprocess.run(
|
||||
['cmd.exe', '/c', 'echo', '%localappdata%'], stdout=subprocess.PIPE).stdout \
|
||||
.rstrip().decode('utf-8')[3:].replace('\\', '/') + '/Programs/Notion/resources'
|
||||
elif sys.platform == 'win32':
|
||||
filepath = subprocess.run(['echo', '%localappdata%'], shell=True, capture_output=True).stdout \
|
||||
.rstrip().decode('utf-8').replace('\\', '/') + '/Programs/Notion/resources'
|
||||
else:
|
||||
print(' > script not compatible with your os!\n (report this to dragonwocky#8449 on discord)')
|
||||
exit()
|
||||
|
||||
if os.path.exists(filepath + '/app'):
|
||||
print(
|
||||
f' ...removing folder {filepath}/app/')
|
||||
rmtree(filepath + '/app')
|
||||
else:
|
||||
print(
|
||||
f' * {filepath}/app/ was not found: step skipped.')
|
||||
|
||||
if os.path.isfile(filepath + '/app.asar.bak'):
|
||||
print(' ...renaming asar.app.bak to asar.app')
|
||||
os.rename(filepath + '/app.asar.bak', filepath + '/app.asar')
|
||||
else:
|
||||
print(
|
||||
f' * {filepath}/app.asar.bak was not found: step skipped.')
|
||||
|
||||
print(f'\n{bold}>>> SUCCESSFULLY CLEANED <<<{normal}')
|
||||
|
||||
except Exception as e:
|
||||
print(f'\n{bold}### ERROR ###{normal}\n{str(e)}')
|
||||
|
||||
print(f'\n{bold}=== END OF LOG ==={normal}')
|
175
customiser.py
@ -1,175 +0,0 @@
|
||||
|
||||
# Notion Enhancer
|
||||
# (c) 2020 dragonwocky <thedragonring.bod@gmail.com>
|
||||
# (c) 2020 TarasokUA
|
||||
# (https://dragonwocky.me/) under the MIT license
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
import subprocess
|
||||
from shutil import copyfile
|
||||
from time import sleep
|
||||
|
||||
# for toggling notion visibility
|
||||
hotkey = 'CmdOrCtrl+Shift+A'
|
||||
|
||||
# f'{bold}=== title ==={normal}' = headers
|
||||
# '*' = information
|
||||
# '...' = actions
|
||||
# '##' = warnings
|
||||
# '>' = exit
|
||||
|
||||
bold = '\033[1m'
|
||||
normal = '\033[0m'
|
||||
|
||||
print(f'{bold}=== NOTION ENHANCER CUSTOMISATION LOG ==={normal}\n')
|
||||
|
||||
try:
|
||||
filepath = ''
|
||||
__folder__ = os.path.dirname(os.path.realpath(__file__)).replace('\\', '/')
|
||||
if 'microsoft' in platform.uname()[3].lower() and sys.platform == 'linux':
|
||||
filepath = '/mnt/c/' + \
|
||||
subprocess.run(
|
||||
['cmd.exe', '/c', 'echo', '%localappdata%'], stdout=subprocess.PIPE).stdout \
|
||||
.rstrip().decode('utf-8')[3:].replace('\\', '/') + '/Programs/Notion/resources'
|
||||
drive = __folder__[5].capitalize() if __folder__.startswith(
|
||||
'/mnt/') else 'C'
|
||||
__folder__ = drive + ':/' + __folder__[6:]
|
||||
elif sys.platform == 'win32':
|
||||
filepath = subprocess.run(['echo', '%localappdata%'], shell=True, capture_output=True).stdout \
|
||||
.rstrip().decode('utf-8').replace('\\', '/') + '/Programs/Notion/resources'
|
||||
else:
|
||||
print(' > script not compatible with your os!\n (report this to dragonwocky#8449 on discord)')
|
||||
exit()
|
||||
|
||||
if os.path.isfile(filepath + '/app.asar'):
|
||||
print(' ...unpacking app.asar')
|
||||
subprocess.run(['asar', 'extract', filepath +
|
||||
'/app.asar', filepath + '/app'], shell=(True if sys.platform == 'win32' else False))
|
||||
print(' ...renaming asar.app to asar.app.bak')
|
||||
os.rename(filepath + '/app.asar', filepath + '/app.asar.bak')
|
||||
else:
|
||||
print(f' ## file {filepath}/app.asar not found!')
|
||||
print(' * attempting to locate')
|
||||
if os.path.exists(filepath + '/app'):
|
||||
print(' * app.asar was already unpacked: step skipped.')
|
||||
else:
|
||||
print(' > nothing found: exiting.')
|
||||
exit()
|
||||
|
||||
if os.path.isfile(filepath + '/app/renderer/preload.js'):
|
||||
print(f' ...adding preload.js to {filepath}/app/renderer/preload.js')
|
||||
with open(filepath + '/app/renderer/preload.js') as content:
|
||||
if '/* === INJECTION MARKER === */' in content.read():
|
||||
print(' * preload.js already added. replacing it.')
|
||||
content.seek(0)
|
||||
original = []
|
||||
for num, line in enumerate(content):
|
||||
if '/* === INJECTION MARKER === */' in line:
|
||||
break
|
||||
original += line
|
||||
with open(filepath + '/app/renderer/preload.js', 'w') as write:
|
||||
write.writelines(original)
|
||||
else:
|
||||
with open(filepath + '/app/renderer/preload.js', 'a') as append:
|
||||
append.write('\n\n')
|
||||
with open(filepath + '/app/renderer/preload.js', 'a') as append:
|
||||
print(' ...linking to ./resources/user.css')
|
||||
with open('./resources/preload.js') as insert:
|
||||
append.write(insert.read().replace(
|
||||
'___user.css___', __folder__
|
||||
+ '/resources/user.css'))
|
||||
else:
|
||||
print(
|
||||
f' * {filepath}/app/renderer/preload.js was not found: step skipped.')
|
||||
|
||||
if os.path.isfile(filepath + '/app/main/createWindow.js'):
|
||||
with open(filepath + '/app/main/createWindow.js') as content:
|
||||
content = content.read()
|
||||
print(
|
||||
f' ...making window frameless @ {filepath}/app/main/createWindow.js')
|
||||
if '{ frame: false, show: false' not in content:
|
||||
content = content.replace(
|
||||
'{ show: false', '{ frame: false, show: false')
|
||||
print(
|
||||
f' ...adding "open hidden" capabilities to {filepath}/app/main/createWindow.js')
|
||||
content = re.sub('\\s*\\/\\* === INJECTION START === \\*\\/.*?\\/\\* === INJECTION END === \\*\\/\\s*',
|
||||
'window.show()', content, flags=re.DOTALL).replace('window.show()', """
|
||||
/* === INJECTION START === */
|
||||
const path = require('path'),
|
||||
store = new (require(path.join(__dirname, '..', 'store.js')))({
|
||||
config: 'user-preferences',
|
||||
defaults: {
|
||||
openhidden: false,
|
||||
maximised: false
|
||||
}
|
||||
});
|
||||
if (!store.get('openhidden') || electron_1.BrowserWindow.getAllWindows().some(win => win.isVisible()))
|
||||
{ window.show(); if (store.get('maximised')) window.maximize(); }
|
||||
/* === INJECTION END === */
|
||||
""")
|
||||
with open(filepath + '/app/main/createWindow.js', 'w') as write:
|
||||
write.write(content)
|
||||
else:
|
||||
print(
|
||||
f' * {filepath}/app/main/createWindow.js was not found: step skipped.')
|
||||
|
||||
if os.path.isfile(filepath + '/app/renderer/index.js'):
|
||||
with open(filepath + '/app/renderer/index.js') as content:
|
||||
print(
|
||||
f' ...adjusting drag area for frameless window in {filepath}/app/renderer/index.js')
|
||||
content = content.read()
|
||||
top = content.rfind('top')
|
||||
content = content[:top] + content[top:].replace(
|
||||
'right: 0', 'right: 420').replace(
|
||||
'top: 0', 'top: 1 ').replace(
|
||||
'height: 34', 'height: 16')
|
||||
with open(filepath + '/app/renderer/index.js', 'w') as write:
|
||||
write.write(content)
|
||||
else:
|
||||
print(
|
||||
f' * {filepath}/app/renderer/index.js was not found: step skipped.')
|
||||
|
||||
if os.path.isfile(filepath + '/app/main/main.js'):
|
||||
with open(filepath + '/app/main/main.js') as content:
|
||||
print(
|
||||
f' ...adding tray support (inc. context menu with settings) to {filepath}/app/main/main.js')
|
||||
print(
|
||||
f' ...adding window toggle hotkey to {filepath}/app/main/main.js')
|
||||
content = content.read()
|
||||
with open(filepath + '/app/main/main.js', 'w') as write:
|
||||
if '/* === INJECTION MARKER === */' in content:
|
||||
print(' * hotkey.js already added. replacing it.')
|
||||
original = []
|
||||
for line in content.splitlines():
|
||||
if '/* === INJECTION MARKER === */' in line:
|
||||
break
|
||||
original.append(line)
|
||||
write.write('\n'.join(original))
|
||||
else:
|
||||
write.write(content.replace(
|
||||
'electron_1.app.on("ready", handleReady);',
|
||||
'electron_1.app.on("ready", () => handleReady() && enhancements());') + '\n')
|
||||
with open(filepath + '/app/main/main.js', 'a') as append:
|
||||
with open('./resources/hotkey.js') as insert:
|
||||
append.write('\n' + insert.read().replace(
|
||||
'___hotkey___', hotkey))
|
||||
print(
|
||||
f' ...copying tray icon ./resources/notion.ico to {filepath}/app/main/')
|
||||
copyfile('./resources/notion.ico',
|
||||
filepath + '/app/main/notion.ico')
|
||||
print(
|
||||
f' ...copying datastore wrapper ./resources/store.js to {filepath}/app/')
|
||||
copyfile('./resources/store.js', filepath + '/app/store.js')
|
||||
else:
|
||||
print(
|
||||
f' * {filepath}/app/main/main.js was not found: step skipped.')
|
||||
|
||||
print(f'\n{bold}>>> SUCCESSFULLY CUSTOMISED <<<{normal}')
|
||||
|
||||
except Exception as e:
|
||||
print(f'\n{bold}### ERROR ###{normal}\n{str(e)}')
|
||||
|
||||
print(f'\n{bold}=== END OF LOG ==={normal}')
|
26
docs.json
@ -1,26 +0,0 @@
|
||||
{
|
||||
"title": "notion enhancer",
|
||||
"primary": "rgb(75, 133, 209)",
|
||||
"git": "https://github.com/dragonwocky/notion-enhancer/blob/master/",
|
||||
"footer": "[Edit on GitHub](__git__) // © 2020 dragonwocky & Uzver, under the [MIT license](https://choosealicense.com/licenses/mit/).",
|
||||
"card": {
|
||||
"description": "an enhancer/customiser for the all-in-one productivity workspace notion.so",
|
||||
"url": "https://dragonwocky.me/notion-enhancer/"
|
||||
},
|
||||
"icon": {
|
||||
"light": "web-logo.png"
|
||||
},
|
||||
"overwrite": true,
|
||||
"exclude": ["cleaner.py", "customiser.py", "resources/*", ".gitignore"],
|
||||
"nav": [
|
||||
["index.html", "README.md"],
|
||||
"resources",
|
||||
["changelog.html", "CHANGELOG.md"],
|
||||
[
|
||||
"license",
|
||||
"https://github.com/dragonwocky/notion-enhancer/blob/master/LICENSE"
|
||||
],
|
||||
["github", "https://github.com/dragonwocky/notion-enhancer/"],
|
||||
["me (dragonwocky)", "https://dragonwocky.me/"]
|
||||
]
|
||||
}
|
22
docs/LICENSE
@ -1,22 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 TarasokUA
|
||||
Copyright (c) 2020 dragonwocky
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -1,93 +0,0 @@
|
||||
<!DOCTYPE html><!-- Documentative--><!-- (c) 2020 dragonwocky <thedragonring.bod@gmail.com>--><!-- (https://dragonwocky.me/) under the MIT license--><html prefix="og: http://ogp.me/ns#"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>changelog | notion enhancer</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Code+Pro|Nunito+Sans"><link rel="stylesheet" href="docs.css"><script src="docs.js"></script><link rel="icon" href="web-logo.png" media="(prefers-color-scheme: dark)"><link rel="icon" href="web-logo.png"><meta name="title" content="changelog | notion enhancer"><meta name="description" content="an enhancer/customiser for the all-in-one productivity workspace notion.so"><meta name="theme-color" content="rgb(75, 133, 209)"><meta property="og:type" content="article"><meta property="og:url" content="https://dragonwocky.me/notion-enhancer/changelog.html"><meta property="og:title" content="changelog"><meta property="og:site_name" content="notion enhancer"><meta property="og:description" content="an enhancer/customiser for the all-in-one productivity workspace notion.so"><meta property="og:image" content="https://dragonwocky.me/notion-enhancer/web-logo.png"><meta property="twitter:card" content="summary"></head><body><aside class="menu"><div><div class="title"><h1>notion enhancer</h1><picture class="icon"><source srcset="web-logo.png" media="(prefers-color-scheme: dark)"><img src="web-logo.png"></picture></div></div><ul class="nav"><li class="entry"><a href="index.html">notion enhancer</a></li><li class="entry"><p>resources</p></li><li class="entry"><a href="#changelog">changelog</a><ul><li class="level-3"><a href="#v050-2020-05-23">v0.5.0 (2020-05-23)</a></li><li class="level-3"><a href="#v041-2020-02-13">v0.4.1 (2020-02-13)</a></li><li class="level-3"><a href="#v040">v0.4.0</a></li><li class="level-3"><a href="#v030">v0.3.0</a></li><li class="level-3"><a href="#v020">v0.2.0</a></li><li class="level-3"><a href="#v010">v0.1.0</a></li></ul></li><li class="entry"><a href="https://github.com/dragonwocky/notion-enhancer/blob/master/LICENSE">license</a></li><li class="entry"><a href="https://github.com/dragonwocky/notion-enhancer/">github</a></li><li class="entry"><a href="https://dragonwocky.me/">me (dragonwocky)</a></li></ul><p class="mark"><a href="https://dragonwocky.me/documentative">docs by documentative</a></p></aside><div class="wrapper"><div class="toggle"><button>☰</button><h1>notion enhancer</h1></div><article class="documentative"><div class="content">
|
||||
|
||||
<section class="block" id="changelog">
|
||||
<h1>
|
||||
<a href="#changelog">changelog</a>
|
||||
</h1>
|
||||
<p>if something is <del>crossed out</del>, then it is no longer a feature included by default,
|
||||
but can still easily be enabled by following instructions in the <a href="/index.html">docs</a>.</p>
|
||||
|
||||
</section>
|
||||
<section class="block" id="v050-2020-05-23">
|
||||
<h3>
|
||||
<a href="#v050-2020-05-23">v0.5.0 (2020-05-23)</a>
|
||||
</h3>
|
||||
<ul>
|
||||
<li>new: running from the wsl.</li>
|
||||
<li>new: reload window with f5.</li>
|
||||
<li>improved: code has been refactored and cleaned up,
|
||||
inc. file renaming and a <code>customiser.py</code> that doesn't require
|
||||
a run of <code>cleaner.py</code> to build updates.
|
||||
improved: scrollbar colours that fit better with notion's theming.</li>
|
||||
<li>bugfix: un-break having multiple notion windows open.</li>
|
||||
</ul>
|
||||
<p><em>(forked by <a href="https://github.com/dragonwocky">@dragonwocky</a>.)</em></p>
|
||||
|
||||
</section>
|
||||
<section class="block" id="v041-2020-02-13">
|
||||
<h3>
|
||||
<a href="#v041-2020-02-13">v0.4.1 (2020-02-13)</a>
|
||||
</h3>
|
||||
<ul>
|
||||
<li>bugfix: wider table & the "+" button not working in database pages.</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<p>📥 <a href="https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d239a3cf-d553-4ef3-ab04-8b47892d9f9a/Notion_Customization_v4.1.zip">notion-enhancer.v4.1.zip</a></p>
|
||||
</blockquote>
|
||||
|
||||
</section>
|
||||
<section class="block" id="v040">
|
||||
<h3>
|
||||
<a href="#v040">v0.4.0</a>
|
||||
</h3>
|
||||
<ul>
|
||||
<li>new: tray icon.</li>
|
||||
<li>new: app startup options (+ saving).</li>
|
||||
<li>new: <code>Reset.py</code></li>
|
||||
<li>improved: better output from <code>Customization Patcher.py</code>.</li>
|
||||
<li>bugfix: wider tables in "short page" mode.</li>
|
||||
<li>bugfix: unclickable buttons/draggable area (of titlebar).</li>
|
||||
</ul>
|
||||
|
||||
</section>
|
||||
<section class="block" id="v030">
|
||||
<h3>
|
||||
<a href="#v030">v0.3.0</a>
|
||||
</h3>
|
||||
<ul>
|
||||
<li>new: show/hide window hotkey.</li>
|
||||
<li>new: app startup options.</li>
|
||||
<li><del>style: smaller table icons.</del></li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<p>📥 <a href="https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b01aa446-5727-476a-a25e-395472bfb1be/NotionScriptsV3.zip">notion-enhancer.v3.zip</a></p>
|
||||
</blockquote>
|
||||
|
||||
</section>
|
||||
<section class="block" id="v020">
|
||||
<h3>
|
||||
<a href="#v020">v0.2.0</a>
|
||||
</h3>
|
||||
<ul>
|
||||
<li>new: light/dark theme support for window control buttons + scrollbars.</li>
|
||||
<li>new: custom styles directly linked to the enhancer resources + compatible with web version.</li>
|
||||
<li><del>improved: making table column width go below 100px.</del></li>
|
||||
</ul>
|
||||
|
||||
</section>
|
||||
<section class="block" id="v010">
|
||||
<h3>
|
||||
<a href="#v010">v0.1.0</a>
|
||||
</h3>
|
||||
<ul>
|
||||
<li>new: custom window control buttons.</li>
|
||||
<li>removed: default titlebar/menubar.</li>
|
||||
<li><del>removed: huge padding of board view.</del></li>
|
||||
<li><del>removed: huge padding of table view.</del></li>
|
||||
<li><del>optional: making table column width go below 100px.</del></li>
|
||||
<li><del>style: thinner cover image + higher content block.</del></li>
|
||||
<li>style: scrollbars.</li>
|
||||
</ul>
|
||||
|
||||
</section></div><footer class="footer"><hr><p><a href="https://github.com/dragonwocky/notion-enhancer/blob/master/CHANGELOG.md">Edit on GitHub</a> // © 2020 dragonwocky & Uzver, under the <a href="https://choosealicense.com/licenses/mit/">MIT license</a>.</p>
|
||||
</footer><nav><a class="prev" href="index.html">ᐊ</a></nav></article></div></body></html>
|
498
docs/docs.css
@ -1,498 +0,0 @@
|
||||
/*
|
||||
* Documentative Styling
|
||||
* (c) 2020 dragonwocky <thedragonring.bod@gmail.com>
|
||||
* (https://dragonwocky.me/) under the MIT license
|
||||
*/
|
||||
:root {
|
||||
--primary: #4b85d1;
|
||||
--absolute: #000;
|
||||
--contrast: #fff;
|
||||
--text: rgba(0, 0, 0, 0.84);
|
||||
--link: var(--primary);
|
||||
--grey: #6f6f6f;
|
||||
--bg: #fbfcfc;
|
||||
--box: #f2f3f4;
|
||||
--code: #f7f9f9;
|
||||
--button: #eee;
|
||||
--border: #e5e7e9;
|
||||
--shadow: #eee;
|
||||
--glow: transparent;
|
||||
--scroll: #e9e9e9;
|
||||
--hover: #dedede;
|
||||
--code-lang: #555;
|
||||
--hljs-html: #000080;
|
||||
--hljs-attr: #008080;
|
||||
--hljs-obj: #2c426b;
|
||||
--hljs-string: #d14;
|
||||
--hljs-builtin: #0086b3;
|
||||
--hljs-keyword: rgba(0, 0, 0, 0.84);
|
||||
--hljs-selector: #900;
|
||||
--hljs-type: #458;
|
||||
--hljs-regex: #009926;
|
||||
--hljs-symbol: #990073;
|
||||
--hljs-meta: #999;
|
||||
--hljs-comment: #707070;
|
||||
--hljs-deletion: #e8b9b8;
|
||||
--hljs-deletion-text: #4c232d;
|
||||
--hljs-addition: #b9e0d3;
|
||||
--hljs-addition-text: #1e4839;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--absolute: #fff;
|
||||
--contrast: #000;
|
||||
--text: #ddd;
|
||||
--link: #a6c3e8;
|
||||
--grey: #52555c;
|
||||
--bg: #0e0f0f;
|
||||
--box: #050505;
|
||||
--code: #000;
|
||||
--button: #2d2d2d;
|
||||
--border: #2d2e2f;
|
||||
--shadow: #070707;
|
||||
--glow: var(--primary);
|
||||
--scroll: #202225;
|
||||
--hover: #36393f;
|
||||
--code-lang: #ccc;
|
||||
--hljs-html: #46db8c;
|
||||
--hljs-attr: #dd1111;
|
||||
--hljs-obj: #c6cbda;
|
||||
--hljs-string: #abcdef;
|
||||
--hljs-builtin: #b8528d;
|
||||
/* bd1a79, 926956 */
|
||||
--hljs-keyword: #2d8b59;
|
||||
--hljs-comment: #a0a0a0;
|
||||
--hljs-deletion: #4c232d;
|
||||
--hljs-deletion-text: #e8b9b8;
|
||||
--hljs-addition: #1e4839;
|
||||
--hljs-addition-text: #b9e0d3;
|
||||
}
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
word-break: break-word;
|
||||
text-decoration: none;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
color: var(--text);
|
||||
background-color: var(--bg);
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
::-webkit-scrollbar-corner,
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--scroll);
|
||||
border-radius: 5px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--hover);
|
||||
}
|
||||
aside {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--box);
|
||||
overflow-x: auto;
|
||||
}
|
||||
aside .title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
aside .title h1 {
|
||||
font: 1.8em 'Source Code Pro', monospace;
|
||||
margin: 0 0 1em 1.5rem;
|
||||
padding: 1em 8px 2.5px 0;
|
||||
letter-spacing: -2px;
|
||||
border-bottom: 5px solid var(--primary);
|
||||
color: var(--absolute);
|
||||
}
|
||||
aside .title .icon {
|
||||
margin: auto 0.5em;
|
||||
}
|
||||
aside .title .icon img {
|
||||
width: 2.5em;
|
||||
margin: auto 0.5em;
|
||||
}
|
||||
aside > ul:first-child > li:first-child {
|
||||
padding-top: 1em;
|
||||
}
|
||||
aside ul {
|
||||
list-style-type: none;
|
||||
padding-inline-start: 0;
|
||||
margin: 0;
|
||||
}
|
||||
aside ul li p {
|
||||
font-weight: bold;
|
||||
letter-spacing: -0.5px;
|
||||
margin-bottom: 0;
|
||||
padding: 2px 1.3em;
|
||||
font-size: 1.1em;
|
||||
color: var(--hljs-comment);
|
||||
}
|
||||
aside ul li a {
|
||||
color: var(--text);
|
||||
padding-bottom: 0.1em 5em;
|
||||
display: block;
|
||||
padding: 2px 1.5em;
|
||||
}
|
||||
aside ul li a:hover,
|
||||
aside ul li a:active {
|
||||
background: var(--scroll);
|
||||
}
|
||||
aside ul li a.active {
|
||||
color: var(--link);
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 0.75em var(--glow);
|
||||
}
|
||||
aside ul li.entry > a {
|
||||
text-decoration: underline var(--border);
|
||||
}
|
||||
aside ul li.level-1 > a {
|
||||
padding-left: 1.75em;
|
||||
}
|
||||
aside ul li.level-2 > a {
|
||||
padding-left: calc(1.5em + calc(0.75em * 1));
|
||||
}
|
||||
aside ul li.level-3 > a {
|
||||
padding-left: calc(1.5em + calc(0.75em * 2));
|
||||
}
|
||||
aside ul li.level-4 > a {
|
||||
padding-left: calc(1.5em + calc(0.75em * 3));
|
||||
}
|
||||
aside ul li.level-5 > a {
|
||||
padding-left: calc(1.5em + calc(0.75em * 4));
|
||||
}
|
||||
aside ul li.level-6 > a {
|
||||
padding-left: calc(1.5em + calc(0.75em * 5));
|
||||
}
|
||||
aside .mark {
|
||||
text-align: right;
|
||||
margin-top: auto;
|
||||
padding: 1.5em 1.5em 2px 1.5em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
aside .mark a {
|
||||
color: var(--grey);
|
||||
}
|
||||
.wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.wrapper .documentative {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 0 1.5em;
|
||||
padding-bottom: 4em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.wrapper .documentative .block {
|
||||
margin: 1.5em;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.wrapper .documentative .block:first-child {
|
||||
margin: 0 1.5em 1.5em 1.5em;
|
||||
}
|
||||
.wrapper .documentative .example {
|
||||
margin-top: 1em;
|
||||
padding: 1em;
|
||||
background-color: var(--box);
|
||||
box-shadow: 0.4em 0.4em 1em var(--shadow);
|
||||
}
|
||||
.wrapper .documentative .example p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.wrapper .documentative .example p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.wrapper .documentative nav {
|
||||
width: 75%;
|
||||
position: fixed;
|
||||
bottom: 1em;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.wrapper .documentative nav .prev {
|
||||
float: left;
|
||||
padding-right: 0.13em;
|
||||
}
|
||||
.wrapper .documentative nav .next {
|
||||
float: right;
|
||||
padding-left: 0.13em;
|
||||
}
|
||||
.wrapper .documentative nav .prev,
|
||||
.wrapper .documentative nav .next {
|
||||
opacity: 1;
|
||||
transition: opacity 200ms ease;
|
||||
pointer-events: all;
|
||||
border-radius: 50%;
|
||||
width: 1.75em;
|
||||
height: 1.75em;
|
||||
margin: 0 1em;
|
||||
font: 1.5em 'Source Code Pro', monospace;
|
||||
line-height: 1.75em;
|
||||
text-align: center;
|
||||
color: var(--text);
|
||||
text-shadow: none !important;
|
||||
background-color: var(--button);
|
||||
}
|
||||
.wrapper .documentative .footer {
|
||||
text-align: right;
|
||||
color: var(--grey);
|
||||
margin: auto 1.5em 0;
|
||||
}
|
||||
.wrapper .documentative .footer hr {
|
||||
border-color: var(--grey);
|
||||
}
|
||||
.wrapper .documentative .footer a {
|
||||
color: var(--grey);
|
||||
font-weight: bold;
|
||||
text-shadow: none;
|
||||
text-decoration: dotted underline;
|
||||
}
|
||||
.wrapper .documentative h1,
|
||||
.wrapper .documentative h2,
|
||||
.wrapper .documentative h3,
|
||||
.wrapper .documentative h4,
|
||||
.wrapper .documentative h5,
|
||||
.wrapper .documentative h6 {
|
||||
margin: 0;
|
||||
padding-top: 1em;
|
||||
}
|
||||
.wrapper .documentative h1 a,
|
||||
.wrapper .documentative h2 a,
|
||||
.wrapper .documentative h3 a,
|
||||
.wrapper .documentative h4 a,
|
||||
.wrapper .documentative h5 a,
|
||||
.wrapper .documentative h6 a {
|
||||
color: var(--text);
|
||||
text-shadow: none;
|
||||
}
|
||||
.wrapper .documentative h1 {
|
||||
padding-top: 1.5em;
|
||||
}
|
||||
.wrapper .documentative a {
|
||||
color: var(--link);
|
||||
text-shadow: 0 0 0.75em var(--glow);
|
||||
}
|
||||
.wrapper .documentative blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 1em;
|
||||
border-left: 0.25em solid var(--border);
|
||||
}
|
||||
.wrapper .documentative h1 + table,
|
||||
.wrapper .documentative h2 + table,
|
||||
.wrapper .documentative h3 + table,
|
||||
.wrapper .documentative h4 + table,
|
||||
.wrapper .documentative h5 + table,
|
||||
.wrapper .documentative h6 + table {
|
||||
margin-top: 1em;
|
||||
}
|
||||
.wrapper .documentative table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.wrapper .documentative table,
|
||||
.wrapper .documentative th,
|
||||
.wrapper .documentative td {
|
||||
padding: 0.2em 0.7em;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.wrapper .documentative code {
|
||||
font-size: 0.8em;
|
||||
background-color: var(--code);
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
display: block;
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
}
|
||||
.wrapper .documentative *:not(pre) > code {
|
||||
padding: 0.275em 0.35em;
|
||||
border-radius: 2px;
|
||||
display: inline;
|
||||
}
|
||||
.wrapper .documentative pre {
|
||||
position: relative;
|
||||
}
|
||||
.wrapper .documentative pre code {
|
||||
padding: 1.8em;
|
||||
border-radius: 5px;
|
||||
position: static;
|
||||
}
|
||||
.wrapper .documentative pre code::before {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
color: var(--code-lang);
|
||||
font-size: 0.65em;
|
||||
padding: 0.5em 0.8em;
|
||||
}
|
||||
@media (min-width: 769px) {
|
||||
body {
|
||||
display: grid;
|
||||
grid-template-columns: 25% 75%;
|
||||
}
|
||||
aside::-webkit-scrollbar-corner,
|
||||
aside::-webkit-scrollbar-track {
|
||||
background-color: var(--bg);
|
||||
}
|
||||
.toggle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
aside {
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: calc(4.5em - 100%);
|
||||
width: calc(100% - 4.5em);
|
||||
transition: left 300ms ease;
|
||||
}
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transition: left 300ms ease;
|
||||
}
|
||||
.wrapper .documentative {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.wrapper .documentative nav {
|
||||
width: 100%;
|
||||
}
|
||||
.wrapper .toggle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
padding: 0.8em 0;
|
||||
background-color: var(--box);
|
||||
}
|
||||
.wrapper .toggle h1 {
|
||||
letter-spacing: -2px;
|
||||
font-size: 1.8em;
|
||||
padding-top: 1.5px;
|
||||
margin: auto 1.5rem auto 0;
|
||||
}
|
||||
.wrapper .toggle button {
|
||||
font-size: 1.8em;
|
||||
width: 2.5em;
|
||||
margin: auto 0.5em;
|
||||
color: var(--absolute);
|
||||
border: none;
|
||||
background: none;
|
||||
text-align: center;
|
||||
transition: transform 150ms ease;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
.wrapper .toggle button:hover,
|
||||
.wrapper .toggle button:focus {
|
||||
color: var(--text);
|
||||
}
|
||||
.wrapper .toggle button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.mobilemenu aside {
|
||||
left: 0;
|
||||
}
|
||||
.mobilemenu .wrapper {
|
||||
left: calc(100% - 4.75em);
|
||||
}
|
||||
.mobilemenu .wrapper .prev,
|
||||
.mobilemenu .wrapper .next {
|
||||
opacity: 0 !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
}
|
||||
.hljs-subst {
|
||||
color: var(--text);
|
||||
}
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: var(--hljs-comment);
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: var(--hljs-keyword);
|
||||
font-weight: bold;
|
||||
}
|
||||
.hljs-attr {
|
||||
color: var(--hljs-obj);
|
||||
}
|
||||
.hljs-number,
|
||||
.hljs-literal,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-tag .hljs-attr {
|
||||
color: var(--hljs-attr);
|
||||
}
|
||||
.hljs-string,
|
||||
.hljs-doctag {
|
||||
color: var(--hljs-string);
|
||||
}
|
||||
.hljs-name,
|
||||
.hljs-attribute {
|
||||
color: var(--hljs-html);
|
||||
}
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name {
|
||||
color: var(--hljs-builtin);
|
||||
}
|
||||
.hljs-title,
|
||||
.hljs-section,
|
||||
.hljs-selector-id {
|
||||
color: var(--hljs-selector);
|
||||
font-weight: bold;
|
||||
}
|
||||
.hljs-type,
|
||||
.hljs-class .hljs-title {
|
||||
color: var(--hljs-type);
|
||||
font-weight: bold;
|
||||
}
|
||||
.hljs-regexp,
|
||||
.hljs-link {
|
||||
color: var(--hljs-regex);
|
||||
}
|
||||
.hljs-symbol,
|
||||
.hljs-bullet {
|
||||
color: var(--hljs-symbol);
|
||||
}
|
||||
.hljs-meta {
|
||||
color: var(--hljs-meta);
|
||||
font-weight: bold;
|
||||
}
|
||||
.hljs-deletion {
|
||||
background: var(--hljs-deletion);
|
||||
color: var(--hljs-deletion-text);
|
||||
}
|
||||
.hljs-addition {
|
||||
background: var(--hljs-addition);
|
||||
color: var(--hljs-addition-text);
|
||||
}
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
.documentative pre .lang-css::before { content: 'CSS'; }
|
193
docs/docs.js
@ -1,193 +0,0 @@
|
||||
/*
|
||||
* Documentative
|
||||
* (c) 2020 dragonwocky <thedragonring.bod@gmail.com>
|
||||
* (https://dragonwocky.me/) under the MIT license
|
||||
*/
|
||||
|
||||
class Scrollnav {
|
||||
constructor(menu, content, options) {
|
||||
if (!(menu instanceof HTMLElement))
|
||||
throw Error('scrollnav: invalid <menu> element provided');
|
||||
if (!(content instanceof HTMLElement))
|
||||
throw Error('scrollnav: invalid <content> element provided');
|
||||
if (typeof options !== 'object') options = {};
|
||||
|
||||
if (Scrollnav.prototype.INITIATED)
|
||||
throw Error('scrollnav: only 1 instance per page allowed!');
|
||||
Scrollnav.prototype.INITIATED = true;
|
||||
|
||||
this.ID;
|
||||
this.ticking = [];
|
||||
this._menu = menu;
|
||||
this._content = content;
|
||||
this._links = [];
|
||||
this._sections = [...this._menu.querySelectorAll('ul li a')].reduce(
|
||||
(list, link) => {
|
||||
if (!link.getAttribute('href').startsWith('#')) return list;
|
||||
let section = this._content.querySelector(link.getAttribute('href'));
|
||||
if (!section) return list;
|
||||
|
||||
this._links.push(link);
|
||||
link.onclick = async ev => {
|
||||
ev.preventDefault();
|
||||
const ID = link.getAttribute('href');
|
||||
this.highlightHeading(ID);
|
||||
this.scrollContent(ID);
|
||||
this.setHash(ID);
|
||||
};
|
||||
|
||||
return [...list, section];
|
||||
},
|
||||
[]
|
||||
);
|
||||
this._topheading = `#${this._sections[0].id}`;
|
||||
|
||||
window.onhashchange = this.watchHash.bind(this);
|
||||
this._content.addEventListener('scroll', ev => {
|
||||
if (!this.ticking.length) {
|
||||
this.ticking.push(1);
|
||||
requestAnimationFrame(() => {
|
||||
this.watchScroll(ev);
|
||||
this.ticking.pop();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.set(null, false);
|
||||
return this;
|
||||
}
|
||||
|
||||
set(ID, smooth) {
|
||||
this.highlightHeading(ID);
|
||||
this.scrollMenu(ID, smooth);
|
||||
this.scrollContent(ID, smooth);
|
||||
this.setHash(ID);
|
||||
}
|
||||
|
||||
parseID(ID) {
|
||||
if (!ID || typeof ID !== 'string') ID = location.hash || this._topheading;
|
||||
if (!ID.startsWith('#')) ID = `#${ID}`;
|
||||
if (!this._links.find(el => el.getAttribute('href') === ID))
|
||||
ID = this._topheading;
|
||||
this.ID = ID;
|
||||
return ID;
|
||||
}
|
||||
highlightHeading(ID) {
|
||||
this.parseID(ID);
|
||||
this._links.forEach(el =>
|
||||
el.getAttribute('href') === this.ID
|
||||
? el.classList.add('active')
|
||||
: el.classList.remove('active')
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
watchHash(ev) {
|
||||
ev.preventDefault();
|
||||
if (ev.newURL !== ev.oldURL) {
|
||||
this.set();
|
||||
}
|
||||
}
|
||||
setHash(ID) {
|
||||
if (!history.replaceState) return false;
|
||||
this.parseID(ID);
|
||||
history.replaceState(null, null, ID === this._topheading ? '#' : this.ID);
|
||||
return true;
|
||||
}
|
||||
|
||||
scrollContent(ID, smooth = true) {
|
||||
this.ticking.push(1);
|
||||
this.parseID(ID);
|
||||
let offset = this._sections.find(el => `#${el.id}` === this.ID).offsetTop;
|
||||
if (offset < this._content.clientHeight / 2) offset = 0;
|
||||
this._content.scroll({
|
||||
top: offset,
|
||||
behavior: smooth ? 'smooth' : 'auto'
|
||||
});
|
||||
setTimeout(() => this.ticking.pop(), 1000);
|
||||
return true;
|
||||
}
|
||||
scrollMenu(ID, smooth = true) {
|
||||
this.parseID(ID);
|
||||
let offset = this._links.find(el => el.getAttribute('href') === this.ID)
|
||||
.offsetTop;
|
||||
if (offset < this._menu.clientHeight / 2) offset = 0;
|
||||
this._menu.scroll({
|
||||
top: offset,
|
||||
behavior: smooth ? 'smooth' : 'auto'
|
||||
});
|
||||
return true;
|
||||
}
|
||||
watchScroll(ev) {
|
||||
const viewport = this._content.clientHeight,
|
||||
ID = this._sections.reduce(
|
||||
(carry, el) => {
|
||||
const rect = el.getBoundingClientRect(),
|
||||
height = rect.bottom - rect.top,
|
||||
visible = {
|
||||
top: rect.top >= 0 && rect.top < viewport,
|
||||
bottom: rect.bottom > 0 && rect.top < viewport
|
||||
};
|
||||
let pixels = 0;
|
||||
if (visible.top && visible.bottom) {
|
||||
pixels = height; // whole el
|
||||
} else if (visible.top) {
|
||||
pixels = viewport - rect.top;
|
||||
} else if (visible.bottom) {
|
||||
pixels = rect.bottom;
|
||||
} else if (height > viewport && rect.top < 0) {
|
||||
const absolute = Math.abs(rect.top);
|
||||
if (absolute < height) pixels = height - absolute; // part of el
|
||||
}
|
||||
pixels = (pixels / height) * 100;
|
||||
return pixels > carry[0] ? [pixels, el] : carry;
|
||||
},
|
||||
[0, null]
|
||||
)[1].id;
|
||||
this.ID = ID;
|
||||
this.scrollMenu(this.ID);
|
||||
clearTimeout(this.afterScroll);
|
||||
this.afterScroll = setTimeout(
|
||||
() => void (this.highlightHeading(this.ID) && this.setHash(this.ID)),
|
||||
100
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let constructed = false;
|
||||
const construct = () => {
|
||||
if (document.readyState !== 'complete' || constructed) return false;
|
||||
constructed = true;
|
||||
|
||||
if (
|
||||
location.pathname.endsWith('index.html') &&
|
||||
window.location.protocol === 'https:'
|
||||
)
|
||||
location.replace('./' + location.hash);
|
||||
|
||||
new Scrollnav(
|
||||
document.querySelector('aside'),
|
||||
document.querySelector('.documentative')
|
||||
);
|
||||
|
||||
document.querySelector('.toggle button').onclick = () =>
|
||||
document.body.classList.toggle('mobilemenu');
|
||||
|
||||
if (window.matchMedia) {
|
||||
let prev;
|
||||
const links = [...document.head.querySelectorAll('link[rel*="icon"]')],
|
||||
pointer = document.createElement('link');
|
||||
pointer.setAttribute('rel', 'icon');
|
||||
document.head.appendChild(pointer);
|
||||
setInterval(() => {
|
||||
const match = links.find(link => window.matchMedia(link.media).matches);
|
||||
if (!match || match.media === prev) return;
|
||||
prev = match.media;
|
||||
pointer.setAttribute('href', match.getAttribute('href'));
|
||||
}, 500);
|
||||
links.forEach(link => document.head.removeChild(link));
|
||||
}
|
||||
};
|
||||
|
||||
construct();
|
||||
document.addEventListener('readystatechange', construct);
|
282
docs/index.html
@ -1,282 +0,0 @@
|
||||
<!DOCTYPE html><!-- Documentative--><!-- (c) 2020 dragonwocky <thedragonring.bod@gmail.com>--><!-- (https://dragonwocky.me/) under the MIT license--><html prefix="og: http://ogp.me/ns#"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>notion enhancer</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Code+Pro|Nunito+Sans"><link rel="stylesheet" href="docs.css"><script src="docs.js"></script><link rel="icon" href="web-logo.png" media="(prefers-color-scheme: dark)"><link rel="icon" href="web-logo.png"><meta name="title" content="notion enhancer"><meta name="description" content="an enhancer/customiser for the all-in-one productivity workspace notion.so"><meta name="theme-color" content="rgb(75, 133, 209)"><meta property="og:type" content="article"><meta property="og:url" content="https://dragonwocky.me/notion-enhancer/index.html"><meta property="og:title" content="notion enhancer"><meta property="og:site_name" content="notion enhancer"><meta property="og:description" content="an enhancer/customiser for the all-in-one productivity workspace notion.so"><meta property="og:image" content="https://dragonwocky.me/notion-enhancer/web-logo.png"><meta property="twitter:card" content="summary"></head><body><aside class="menu"><div><div class="title"><h1>notion enhancer</h1><picture class="icon"><source srcset="web-logo.png" media="(prefers-color-scheme: dark)"><img src="web-logo.png"></picture></div></div><ul class="nav"><li class="entry"><a href="#notion-enhancer">notion enhancer</a><ul><li class="level-2"><a href="#installation">installation</a></li><li class="level-2"><a href="#this-is-a-fork">this is a fork</a></li><li class="level-2"><a href="#features">features</a></li><li class="level-3"><a href="#titlebar">titlebar</a></li><li class="level-3"><a href="#nicer-scrollbars">nicer scrollbars</a></li><li class="level-3"><a href="#hotkeys">hotkeys</a></li><li class="level-3"><a href="#tray">tray</a></li><li class="level-3"><a href="#styling">styling</a></li><li class="level-4"><a href="#wider-page-view">wider page view</a></li><li class="level-4"><a href="#thinner-cover-image">thinner cover image</a></li><li class="level-4"><a href="#table-columns-below-100px">table columns below 100px</a></li><li class="level-4"><a href="#hide--new-table-row">hide '+ new' table row</a></li><li class="level-4"><a href="#hide-calculations-table-row">hide calculations table row</a></li><li class="level-4"><a href="#hide--new-board-row">hide '+ new' board row</a></li><li class="level-4"><a href="#hide-board-view-hidden-columns">hide board view hidden columns</a></li><li class="level-4"><a href="#hide-board-view-add-a-group">hide board view 'add a group'</a></li><li class="level-4"><a href="#centre-align-table-column-headers">centre-align table column headers</a></li><li class="level-4"><a href="#smaller-table-column-header-icons">smaller table column header icons</a></li><li class="level-4"><a href="#remove-icons-from-table-column-headers">remove icons from table column headers</a></li><li class="level-4"><a href="#removingdecreasing-side-padding-for-tables">removing/decreasing side padding for tables</a></li><li class="level-4"><a href="#removingdecreasing-side-padding-for-boards">removing/decreasing side padding for boards</a></li><li class="level-2"><a href="#other-details">other details</a></li></ul></li><li class="entry"><p>resources</p></li><li class="entry"><a href="changelog.html">changelog</a></li><li class="entry"><a href="https://github.com/dragonwocky/notion-enhancer/blob/master/LICENSE">license</a></li><li class="entry"><a href="https://github.com/dragonwocky/notion-enhancer/">github</a></li><li class="entry"><a href="https://dragonwocky.me/">me (dragonwocky)</a></li></ul><p class="mark"><a href="https://dragonwocky.me/documentative">docs by documentative</a></p></aside><div class="wrapper"><div class="toggle"><button>☰</button><h1>notion enhancer</h1></div><article class="documentative"><div class="content">
|
||||
|
||||
<section class="block" id="notion-enhancer">
|
||||
<h1>
|
||||
<a href="#notion-enhancer">notion enhancer</a>
|
||||
</h1>
|
||||
<p>an enhancer/customiser for the all-in-one productivity workspace <a href="https://www.notion.so/">notion.so</a></p>
|
||||
|
||||
</section>
|
||||
<section class="block" id="installation">
|
||||
<h2>
|
||||
<a href="#installation">installation</a>
|
||||
</h2>
|
||||
<p>currently, only win10 is supported. it is possible to run this script via the wsl to modify the win10 notion app.</p>
|
||||
<p>(the <a href="#styling">styles</a> should also work for the web version.
|
||||
these can be installed via an extension like <a href="https://chrome.google.com/webstore/detail/stylus/clngdbkpkpeebahjckkjfobafhncgmne?hl=en">stylus</a>
|
||||
or a built-in feature like <a href="https://www.userchrome.org/">userChrome.css</a>.)</p>
|
||||
<ol>
|
||||
<li>install <a href="https://nodejs.org/en/">node.js</a> (if using the wsl, it is recommended to install via <a href="https://github.com/nvm-sh/nvm#install--update-script">nvm</a>).</li>
|
||||
<li>install <a href="https://www.python.org/">python</a> (if using the wsl, follow <a href="https://docs.python-guide.org/starting/install3/linux/">this guide</a>).</li>
|
||||
<li>reboot.</li>
|
||||
<li>in cmd (on windows) or bash (with wsl), run <code>npm install -g asar</code> (check installation by running <code>asar</code>).</li>
|
||||
<li><a href="https://github.com/dragonwocky/notion-enhancer/archive/master.zip">download this enhancer</a> & extract to a location it can safely remain (this must be in the windows filesystem,
|
||||
even if you are running the script from the wsl).</li>
|
||||
<li>ensure notion is closed.</li>
|
||||
<li>optional: to remove previous versions of notion enhancer, run <code>cleaner.py</code></li>
|
||||
<li>optional: modify the <code>resources/user.css</code> files to your liking.</li>
|
||||
<li>run <code>customiser.py</code> to build changes.</li>
|
||||
</ol>
|
||||
<p>done: run notion and enjoy.</p>
|
||||
<p><strong>oh no, now my app won't open!</strong></p>
|
||||
<ol>
|
||||
<li>kill any notion tasks in the task manager (<code>ctrl+shift+esc</code>).</li>
|
||||
<li>run <code>cleaner.py</code>.</li>
|
||||
<li>reboot.</li>
|
||||
<li>follow instructions above (ensuring notion <em>isn't</em> running! again, check task manager).</li>
|
||||
</ol>
|
||||
|
||||
</section>
|
||||
<section class="block" id="this-is-a-fork">
|
||||
<h2>
|
||||
<a href="#this-is-a-fork">this is a fork</a>
|
||||
</h2>
|
||||
<p>credit where credit is due, this was originally made by Uzver (github: <a href="https://github.com/TarasokUA">@TarasokUA</a>,
|
||||
telegram: <a href="https://t.me/UserFromUkraine">UserFromUkraine</a>, discord: Uzver#8760).</p>
|
||||
<p>he has approved my go-ahead with this fork, as he himself no longer wishes to continue development on the project.</p>
|
||||
|
||||
</section>
|
||||
<section class="block" id="features">
|
||||
<h2>
|
||||
<a href="#features">features</a>
|
||||
</h2>
|
||||
|
||||
</section>
|
||||
<section class="block" id="titlebar">
|
||||
<h3>
|
||||
<a href="#titlebar">titlebar</a>
|
||||
</h3>
|
||||
<p>default windows titlebar/frame has been replaced by one more fitting to the theme of the app.</p>
|
||||
<p>this includes the addition of an extra button, "always on top"
|
||||
symbolised with an arrow (4th from the right). when toggled to point up,
|
||||
notion will remain the top visible window even if not focused.</p>
|
||||
<p>to customise which characters are used for these buttons, open in the <code>resources/preload.js</code> file,
|
||||
find the relevant button (read the comments) and replace its icon with your chosen unicode character (e.g.
|
||||
replacing <code>element.innerHTML = '▢';</code> with <code>element.innerHTML = '🙄';</code>).</p>
|
||||
|
||||
</section>
|
||||
<section class="block" id="nicer-scrollbars">
|
||||
<h3>
|
||||
<a href="#nicer-scrollbars">nicer scrollbars</a>
|
||||
</h3>
|
||||
<p>i mean, yeah. get rid of those ugly default scrollbars and use nice inconspicuous
|
||||
ones that actually look as if they're part of notion.</p>
|
||||
<p>to add these to the web version, copy lines 44 - 75 from <code>user.css</code> into your css customiser.</p>
|
||||
|
||||
</section>
|
||||
<section class="block" id="hotkeys">
|
||||
<h3>
|
||||
<a href="#hotkeys">hotkeys</a>
|
||||
</h3>
|
||||
<ul>
|
||||
<li><strong>reload window</strong>: in addition to the built-in <code>CmdOrCtrl+R</code> reload,
|
||||
you can now reload a window with <code>F5</code>.</li>
|
||||
<li><strong>toggle all notion windows to/from the tray</strong>: <code>CmdOrCtrl+Shift+A</code> by default.</li>
|
||||
</ul>
|
||||
<p>to set your own toggle hotkey, open <code>customiser.py</code> and change line 16 (<code>hotkey = 'CmdOrCtrl+Shift+A'</code>)
|
||||
to your preference. you will need to run or re-run <code>customiser.py</code> afterwards.</p>
|
||||
|
||||
</section>
|
||||
<section class="block" id="tray">
|
||||
<h3>
|
||||
<a href="#tray">tray</a>
|
||||
</h3>
|
||||
<ul>
|
||||
<li>single-click to toggle app visibility. right click to open menu.</li>
|
||||
<li>settings will be saved in <code>%localappdata%/Programs/Notion/resources/app/user-preferences.json</code></li>
|
||||
<li><strong>run on startup</strong>: run notion on boot/startup. (default: true)</li>
|
||||
<li><strong>hide on open</strong>: hide the launch of notion to the tray. (default: false)</li>
|
||||
<li><strong>open maximised</strong>: maximise the app on open. (default: false)</li>
|
||||
<li><strong>close to tray</strong>: close window to tray rather than closing outright
|
||||
on click of <code>⨉</code>. does not apply if multiple notion windows are open. (default: false)</li>
|
||||
</ul>
|
||||
|
||||
</section>
|
||||
<section class="block" id="styling">
|
||||
<h3>
|
||||
<a href="#styling">styling</a>
|
||||
</h3>
|
||||
<p>due to <code>customiser.py</code> setting up a direct link to <code>resources/user.css</code>,
|
||||
changes will be applied instantly on notion reload
|
||||
(no need to re-run <code>customiser.py</code> every time you want to change some styles).</p>
|
||||
<p>these should also work for the web version, if copied into your css customiser.</p>
|
||||
<p>css below will work for every instance of the element, but if you wish to hide only a specific element
|
||||
(e.g. the '+ new' table row) it is recommended that you prepend each selector with <code>[data-block-id='ID']</code> (<a href="https://www.youtube.com/watch?v=6V7eqShm_4w">video tutorial on fetching IDs</a>).</p>
|
||||
|
||||
</section>
|
||||
<section class="block" id="wider-page-view">
|
||||
<h4>
|
||||
<a href="#wider-page-view">wider page view</a>
|
||||
</h4>
|
||||
<pre><code class="lang-css"><span class="hljs-selector-class">.notion-peek-renderer</span> > <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-child(2)</span> {
|
||||
<span class="hljs-attribute">max-width</span>: <span class="hljs-number">85vw</span> <span class="hljs-meta">!important</span>;
|
||||
}</code></pre>
|
||||
|
||||
</section>
|
||||
<section class="block" id="thinner-cover-image">
|
||||
<h4>
|
||||
<a href="#thinner-cover-image">thinner cover image</a>
|
||||
</h4>
|
||||
<pre><code class="lang-css"><span class="hljs-selector-attr">[style^=<span class="hljs-string">'position: relative; width: 100%; display: flex; flex-direction: column; align-items: center; height: 30vh;'</span>]</span> {
|
||||
<span class="hljs-attribute">height</span>: <span class="hljs-number">12vh</span> <span class="hljs-meta">!important</span>;
|
||||
}
|
||||
<span class="hljs-selector-attr">[style^=<span class="hljs-string">'position: relative; width: 100%; display: flex; flex-direction: column; align-items: center; height: 30vh;'</span>]</span>
|
||||
<span class="hljs-selector-tag">img</span> {
|
||||
<span class="hljs-attribute">height</span>: <span class="hljs-number">20vh</span> <span class="hljs-meta">!important</span>;
|
||||
}</code></pre>
|
||||
|
||||
</section>
|
||||
<section class="block" id="table-columns-below-100px">
|
||||
<h4>
|
||||
<a href="#table-columns-below-100px">table columns below 100px</a>
|
||||
</h4>
|
||||
<p><strong>not recommended!</strong> this is unreliable and will cause bugs.
|
||||
coincidentally, this is also what the youtube video linked above shows how to do.
|
||||
as it is a per-table-column style, unlike all others here, it must be prepended with the block ID.</p>
|
||||
<pre><code class="lang-css"><span class="hljs-selector-attr">[data-block-id^=<span class="hljs-string">'ID'</span>]</span>
|
||||
> <span class="hljs-selector-attr">[style^=<span class="hljs-string">'display: flex; position: absolute; background: rgb(47, 52, 55); z-index: 82; height: 33px; color: rgba(255, 255, 255, 0.6);'</span>]</span>
|
||||
> <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-child(1)</span>
|
||||
> <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-child(10)</span>
|
||||
> <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-child(1)</span>,
|
||||
<span class="hljs-selector-attr">[data-block-id^=<span class="hljs-string">'ID'</span>]</span>
|
||||
> <span class="hljs-selector-attr">[style^=<span class="hljs-string">'position: relative; min-width: calc(100% - 192px);'</span>]</span>
|
||||
> <span class="hljs-selector-attr">[data-block-id]</span>
|
||||
> <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-child(10)</span>,
|
||||
<span class="hljs-selector-attr">[data-block-id^=<span class="hljs-string">'ID'</span>]</span> > <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-child(5)</span> > <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-child(10)</span> {
|
||||
<span class="hljs-attribute">width</span>: <span class="hljs-number">45px</span> <span class="hljs-meta">!important</span>;
|
||||
}
|
||||
<span class="hljs-selector-attr">[data-block-id^=<span class="hljs-string">'ID'</span>]</span>
|
||||
<span class="hljs-selector-attr">[style^=<span class="hljs-string">'position: absolute; top: 0px; left: 0px; pointer-events: none;'</span>]</span><span class="hljs-selector-pseudo">:not(.notion-presence-container)</span> {
|
||||
<span class="hljs-attribute">display</span>: none;
|
||||
}</code></pre>
|
||||
|
||||
</section>
|
||||
<section class="block" id="hide--new-table-row">
|
||||
<h4>
|
||||
<a href="#hide--new-table-row">hide '+ new' table row</a>
|
||||
</h4>
|
||||
<pre><code class="lang-css"><span class="hljs-selector-class">.notion-table-view-add-row</span> {
|
||||
<span class="hljs-attribute">display</span>: none <span class="hljs-meta">!important</span>;
|
||||
}</code></pre>
|
||||
|
||||
</section>
|
||||
<section class="block" id="hide-calculations-table-row">
|
||||
<h4>
|
||||
<a href="#hide-calculations-table-row">hide calculations table row</a>
|
||||
</h4>
|
||||
<pre><code class="lang-css"><span class="hljs-selector-class">.notion-table-view-add-row</span> + <span class="hljs-selector-tag">div</span> {
|
||||
<span class="hljs-attribute">display</span>: none <span class="hljs-meta">!important</span>;
|
||||
}</code></pre>
|
||||
|
||||
</section>
|
||||
<section class="block" id="hide--new-board-row">
|
||||
<h4>
|
||||
<a href="#hide--new-board-row">hide '+ new' board row</a>
|
||||
</h4>
|
||||
<pre><code class="lang-css"><span class="hljs-selector-class">.notion-board-group</span>
|
||||
<span class="hljs-selector-attr">[style=<span class="hljs-string">'user-select: none; transition: background 120ms ease-in 0s; cursor: pointer; display: inline-flex; align-items: center; flex-shrink: 0; white-space: nowrap; height: 32px; border-radius: 3px; font-size: 14px; line-height: 1.2; min-width: 0px; padding-left: 6px; padding-right: 8px; color: rgba(255, 255, 255, 0.4); width: 100%;'</span>]</span> {
|
||||
<span class="hljs-attribute">display</span>: none <span class="hljs-meta">!important</span>;
|
||||
}</code></pre>
|
||||
|
||||
</section>
|
||||
<section class="block" id="hide-board-view-hidden-columns">
|
||||
<h4>
|
||||
<a href="#hide-board-view-hidden-columns">hide board view hidden columns</a>
|
||||
</h4>
|
||||
<pre><code class="lang-css"><span class="hljs-selector-class">.notion-board-view</span> > <span class="hljs-selector-attr">[data-block-id]</span> > <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-last-child(2)</span>,
|
||||
<span class="hljs-selector-class">.notion-board-view</span> > <span class="hljs-selector-attr">[data-block-id]</span> > <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:first-child</span> > <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-last-child(2)</span> {
|
||||
<span class="hljs-attribute">display</span>: none <span class="hljs-meta">!important</span>;
|
||||
}</code></pre>
|
||||
|
||||
</section>
|
||||
<section class="block" id="hide-board-view-add-a-group">
|
||||
<h4>
|
||||
<a href="#hide-board-view-add-a-group">hide board view 'add a group'</a>
|
||||
</h4>
|
||||
<pre><code class="lang-css"><span class="hljs-selector-class">.notion-board-view</span> > <span class="hljs-selector-attr">[data-block-id]</span> > <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:last-child</span>,
|
||||
<span class="hljs-selector-class">.notion-board-view</span> > <span class="hljs-selector-attr">[data-block-id]</span> > <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:first-child</span> > <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:last-child</span> {
|
||||
<span class="hljs-attribute">display</span>: none <span class="hljs-meta">!important</span>;
|
||||
}</code></pre>
|
||||
|
||||
</section>
|
||||
<section class="block" id="centre-align-table-column-headers">
|
||||
<h4>
|
||||
<a href="#centre-align-table-column-headers">centre-align table column headers</a>
|
||||
</h4>
|
||||
<pre><code class="lang-css"><span class="hljs-selector-class">.notion-table-view-header-cell</span> > <span class="hljs-selector-tag">div</span> > <span class="hljs-selector-tag">div</span> {
|
||||
<span class="hljs-attribute">margin</span>: <span class="hljs-number">0px</span> auto;
|
||||
}</code></pre>
|
||||
|
||||
</section>
|
||||
<section class="block" id="smaller-table-column-header-icons">
|
||||
<h4>
|
||||
<a href="#smaller-table-column-header-icons">smaller table column header icons</a>
|
||||
</h4>
|
||||
<pre><code class="lang-css"><span class="hljs-selector-attr">[style^=<span class="hljs-string">'display: flex; position: absolute; background: rgb(47, 52, 55); z-index: 82; height: 33px; color: rgba(255, 255, 255, 0.6);'</span>]</span>
|
||||
<span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-child(1)</span>
|
||||
<span class="hljs-selector-tag">svg</span> {
|
||||
<span class="hljs-attribute">height</span>: <span class="hljs-number">10px</span> <span class="hljs-meta">!important</span>;
|
||||
<span class="hljs-attribute">width</span>: <span class="hljs-number">10px</span> <span class="hljs-meta">!important</span>;
|
||||
<span class="hljs-attribute">margin-right</span>: -<span class="hljs-number">4px</span>;
|
||||
}</code></pre>
|
||||
|
||||
</section>
|
||||
<section class="block" id="remove-icons-from-table-column-headers">
|
||||
<h4>
|
||||
<a href="#remove-icons-from-table-column-headers">remove icons from table column headers</a>
|
||||
</h4>
|
||||
<pre><code class="lang-css"><span class="hljs-selector-class">.notion-table-view-header-cell</span> <span class="hljs-selector-attr">[style^=<span class="hljs-string">'margin-right: 6px;'</span>]</span> {
|
||||
<span class="hljs-attribute">display</span>: none <span class="hljs-meta">!important</span>;
|
||||
}</code></pre>
|
||||
|
||||
</section>
|
||||
<section class="block" id="removingdecreasing-side-padding-for-tables">
|
||||
<h4>
|
||||
<a href="#removingdecreasing-side-padding-for-tables">removing/decreasing side padding for tables</a>
|
||||
</h4>
|
||||
<pre><code class="lang-css"><span class="hljs-selector-attr">[style^=<span class="hljs-string">'flex-shrink: 0; flex-grow: 1; width: 100%; max-width: 100%; display: flex; align-items: center; flex-direction: column; font-size: 16px; color: rgba(255, 255, 255, 0.9); padding: 0px 96px 30vh;'</span>]</span>
|
||||
<span class="hljs-selector-class">.notion-table-view</span>,
|
||||
<span class="hljs-selector-attr">[class=<span class="hljs-string">'notion-scroller'</span>]</span> > <span class="hljs-selector-class">.notion-table-view</span> {
|
||||
<span class="hljs-attribute">padding-left</span>: <span class="hljs-number">35px</span> <span class="hljs-meta">!important</span>;
|
||||
<span class="hljs-attribute">padding-right</span>: <span class="hljs-number">15px</span> <span class="hljs-meta">!important</span>;
|
||||
<span class="hljs-attribute">min-width</span>: <span class="hljs-number">0%</span> <span class="hljs-meta">!important</span>;
|
||||
}
|
||||
<span class="hljs-selector-attr">[style^=<span class="hljs-string">'flex-shrink: 0; flex-grow: 1; width: 100%; max-width: 100%; display: flex; align-items: center; flex-direction: column; font-size: 16px; color: rgba(255, 255, 255, 0.9); padding: 0px 96px 30vh;'</span>]</span>
|
||||
<span class="hljs-selector-class">.notion-selectable</span>
|
||||
<span class="hljs-selector-class">.notion-scroller</span><span class="hljs-selector-class">.horizontal</span><span class="hljs-selector-pseudo">::-webkit-scrollbar-track</span> {
|
||||
<span class="hljs-attribute">margin-left</span>: <span class="hljs-number">10px</span>;
|
||||
<span class="hljs-attribute">margin-right</span>: <span class="hljs-number">10px</span>;
|
||||
}</code></pre>
|
||||
|
||||
</section>
|
||||
<section class="block" id="removingdecreasing-side-padding-for-boards">
|
||||
<h4>
|
||||
<a href="#removingdecreasing-side-padding-for-boards">removing/decreasing side padding for boards</a>
|
||||
</h4>
|
||||
<pre><code class="lang-css"><span class="hljs-selector-class">.notion-board-view</span> {
|
||||
<span class="hljs-attribute">padding-left</span>: <span class="hljs-number">10px</span> <span class="hljs-meta">!important</span>;
|
||||
<span class="hljs-attribute">padding-right</span>: <span class="hljs-number">10px</span> <span class="hljs-meta">!important</span>;
|
||||
}</code></pre>
|
||||
|
||||
</section>
|
||||
<section class="block" id="other-details">
|
||||
<h2>
|
||||
<a href="#other-details">other details</a>
|
||||
</h2>
|
||||
<p>i have an unhealthy habit of avoiding capital letters. nothing enforces this, i just do it.</p>
|
||||
<p>the notion logo belongs entirely to the notion team, and was sourced from their
|
||||
<a href="https://www.notion.so/Media-Kit-205535b1d9c4440497a3d7a2ac096286">media kit</a>.</p>
|
||||
<p>if you have any questions, check <a href="https://dragonwocky.me/">my website</a> for contact details.</p>
|
||||
|
||||
</section></div><footer class="footer"><hr><p><a href="https://github.com/dragonwocky/notion-enhancer/blob/master/README.md">Edit on GitHub</a> // © 2020 dragonwocky & Uzver, under the <a href="https://choosealicense.com/licenses/mit/">MIT license</a>.</p>
|
||||
</footer><nav><a class="next" href="changelog.html">ᐅ</a></nav></article></div></body></html>
|
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 15 KiB |
155
package-lock.json
generated
Normal file
@ -0,0 +1,155 @@
|
||||
{
|
||||
"name": "notion-enhancer",
|
||||
"version": "0.11.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "notion-enhancer",
|
||||
"version": "0.11.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/asar": "^3.2.9",
|
||||
"arg": "^5.0.2",
|
||||
"chalk-template": "^1.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"notion-enhancer": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.x.x"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/dragonwocky"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/asar": {
|
||||
"version": "3.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.9.tgz",
|
||||
"integrity": "sha512-Vu2P3X2gcZ3MY9W7yH72X9+AMXwUQZEJBrsPIbX0JsdllLtoh62/Q8Wg370/DawIEVKOyfD6KtTLo645ezqxUA==",
|
||||
"dependencies": {
|
||||
"commander": "^5.0.0",
|
||||
"glob": "^7.1.6",
|
||||
"minimatch": "^3.0.4"
|
||||
},
|
||||
"bin": {
|
||||
"asar": "bin/asar.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "5.0.2",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.2.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk-template": {
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk-template?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "5.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
}
|
42
package.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "notion-enhancer",
|
||||
"version": "0.11.1",
|
||||
"author": "dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)",
|
||||
"description": "Customise the all-in-one productivity workspace Notion.",
|
||||
"homepage": "https://notion-enhancer.github.io",
|
||||
"repository": "github:notion-enhancer/desktop",
|
||||
"bugs": "https://github.com/notion-enhancer/desktop/issues",
|
||||
"funding": "https://github.com/sponsors/dragonwocky",
|
||||
"license": "MIT",
|
||||
"bin": "bin.mjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "./scripts/build-browser-extension.sh",
|
||||
"vendor": "node ./scripts/vendor-dependencies.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.x.x"
|
||||
},
|
||||
"keywords": [
|
||||
"windows",
|
||||
"macos",
|
||||
"linux",
|
||||
"productivity",
|
||||
"hack",
|
||||
"extensions",
|
||||
"themes",
|
||||
"integrations",
|
||||
"addons",
|
||||
"mod",
|
||||
"mods",
|
||||
"mod-loader",
|
||||
"enhancer",
|
||||
"notion",
|
||||
"notion-enhancer"
|
||||
],
|
||||
"dependencies": {
|
||||
"@electron/asar": "^3.2.9",
|
||||
"chalk-template": "^1.1.0",
|
||||
"arg": "^5.0.2"
|
||||
}
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
/* === INJECTION MARKER === */
|
||||
|
||||
/*
|
||||
* Notion Enhancer
|
||||
* (c) 2020 dragonwocky <thedragonring.bod@gmail.com>
|
||||
* (c) 2020 TarasokUA
|
||||
* (https://dragonwocky.me/) under the MIT license
|
||||
*/
|
||||
|
||||
// adds: tray support (inc. context menu with settings), window toggle hotkey
|
||||
|
||||
// DO NOT REMOVE THE INJECTION MARKER ABOVE.
|
||||
// DO NOT CHANGE THE NAME OF THE 'enhancements()' FUNCTION.
|
||||
|
||||
let tray;
|
||||
|
||||
function enhancements() {
|
||||
const { Tray, Menu } = require('electron'),
|
||||
path = require('path'),
|
||||
store = new (require(path.join(__dirname, '..', 'store.js')))({
|
||||
config: 'user-preferences',
|
||||
defaults: {
|
||||
openhidden: false,
|
||||
maximised: false,
|
||||
tray: false
|
||||
}
|
||||
}),
|
||||
states = {
|
||||
startup: electron_1.app.getLoginItemSettings().openAtLogin,
|
||||
openhidden: store.get('openhidden'),
|
||||
maximised: store.get('maximised'),
|
||||
tray: store.get('tray')
|
||||
};
|
||||
|
||||
tray = new Tray(path.join(__dirname, './notion.ico'));
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
id: 'startup',
|
||||
label: 'run on startup',
|
||||
type: 'checkbox',
|
||||
checked: states.startup,
|
||||
click: () =>
|
||||
contextMenu.getMenuItemById('startup').checked
|
||||
? electron_1.app.setLoginItemSettings({ openAtLogin: true })
|
||||
: electron_1.app.setLoginItemSettings({ openAtLogin: false })
|
||||
},
|
||||
{
|
||||
id: 'openhidden',
|
||||
label: 'hide on open',
|
||||
type: 'checkbox',
|
||||
checked: states.openhidden,
|
||||
click: () =>
|
||||
contextMenu.getMenuItemById('openhidden').checked
|
||||
? store.set('openhidden', true)
|
||||
: store.set('openhidden', false)
|
||||
},
|
||||
{
|
||||
id: 'maximised',
|
||||
label: 'open maximised',
|
||||
type: 'checkbox',
|
||||
checked: states.maximised,
|
||||
click: () =>
|
||||
contextMenu.getMenuItemById('maximised').checked
|
||||
? store.set('maximised', true)
|
||||
: store.set('maximised', false)
|
||||
},
|
||||
{
|
||||
id: 'tray',
|
||||
label: 'close to tray',
|
||||
type: 'checkbox',
|
||||
checked: states.tray,
|
||||
click: () =>
|
||||
contextMenu.getMenuItemById('tray').checked
|
||||
? store.set('tray', true)
|
||||
: store.set('tray', false)
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: '(x) quit',
|
||||
role: 'quit'
|
||||
}
|
||||
]);
|
||||
tray.setContextMenu(contextMenu);
|
||||
|
||||
tray.on('click', function () {
|
||||
const win = electron_1.BrowserWindow.getAllWindows()[0];
|
||||
if (win.isVisible()) {
|
||||
if (win.isMinimized()) {
|
||||
win.show();
|
||||
} else win.hide();
|
||||
} else {
|
||||
if (contextMenu.getMenuItemById('maximised').checked) {
|
||||
win.maximize();
|
||||
} else win.show();
|
||||
}
|
||||
});
|
||||
|
||||
const hotkey = '___hotkey___'; // will be set by python script
|
||||
electron_1.globalShortcut.register(hotkey, () => {
|
||||
const windows = electron_1.BrowserWindow.getAllWindows();
|
||||
if (windows.some(win => !win.isVisible())) {
|
||||
if (contextMenu.getMenuItemById('maximised').checked) {
|
||||
windows.forEach(win => win.maximize());
|
||||
} else windows.forEach(win => win.show());
|
||||
} else windows.forEach(win => win.hide());
|
||||
});
|
||||
}
|
Before Width: | Height: | Size: 110 KiB |
@ -1,94 +0,0 @@
|
||||
/* === INJECTION MARKER === */
|
||||
|
||||
/*
|
||||
* Notion Enhancer
|
||||
* (c) 2020 dragonwocky <thedragonring.bod@gmail.com>
|
||||
* (c) 2020 TarasokUA
|
||||
* (https://dragonwocky.me/) under the MIT license
|
||||
*/
|
||||
|
||||
// adds: custom styles, nicer window control buttons
|
||||
|
||||
// DO NOT REMOVE THE INJECTION MARKER ABOVE
|
||||
|
||||
require('electron').remote.getGlobal('setTimeout')(() => {
|
||||
/* style injection */
|
||||
const fs = require('fs'),
|
||||
css = fs.readFileSync('___user.css___'), // will be set by python script
|
||||
style = document.createElement('style'),
|
||||
head = document.getElementsByTagName('head')[0];
|
||||
if (!head) return;
|
||||
style.type = 'text/css';
|
||||
style.innerHTML = css;
|
||||
head.appendChild(style);
|
||||
|
||||
const intervalID = setInterval(injection, 100);
|
||||
function injection() {
|
||||
if (document.querySelector('div.notion-topbar > div') == undefined) return;
|
||||
|
||||
const appwindow = require('electron').remote.getCurrentWindow();
|
||||
|
||||
/* window control buttons */
|
||||
let node = document.querySelector('div.notion-topbar > div'),
|
||||
element = document.createElement('div');
|
||||
element.id = 'window-buttons-area';
|
||||
node.appendChild(element);
|
||||
node = document.querySelector('#window-buttons-area');
|
||||
|
||||
// always-on-top
|
||||
element = document.createElement('button');
|
||||
element.classList.add('window-buttons');
|
||||
element.innerHTML = '🠛';
|
||||
element.onclick = function () {
|
||||
const state = appwindow.isAlwaysOnTop();
|
||||
appwindow.setAlwaysOnTop(!state);
|
||||
this.innerHTML = state ? '🠛' : '🠙';
|
||||
};
|
||||
node.appendChild(element);
|
||||
|
||||
// minimise
|
||||
element = document.createElement('button');
|
||||
element.classList.add('window-buttons');
|
||||
element.innerHTML = '⚊';
|
||||
element.onclick = () => appwindow.minimize();
|
||||
node.appendChild(element);
|
||||
|
||||
// maximise
|
||||
element = document.createElement('button');
|
||||
element.classList.add('window-buttons');
|
||||
element.innerHTML = '▢';
|
||||
element.onclick = () =>
|
||||
appwindow.isMaximized() ? appwindow.unmaximize() : appwindow.maximize();
|
||||
node.appendChild(element);
|
||||
|
||||
// close
|
||||
const path = require('path');
|
||||
element = document.createElement('button');
|
||||
element.classList.add('window-buttons');
|
||||
element.innerHTML = '⨉';
|
||||
element.onclick = () => {
|
||||
const store = new (require(path.join(__dirname, '..', 'store.js')))({
|
||||
config: 'user-preferences',
|
||||
defaults: {
|
||||
tray: false
|
||||
}
|
||||
});
|
||||
if (
|
||||
store.get('tray') &&
|
||||
require('electron').remote.BrowserWindow.getAllWindows().length === 1
|
||||
) {
|
||||
appwindow.hide();
|
||||
} else appwindow.close();
|
||||
};
|
||||
node.appendChild(element);
|
||||
|
||||
clearInterval(intervalID);
|
||||
|
||||
/* reload window */
|
||||
document.defaultView.addEventListener(
|
||||
'keyup',
|
||||
ev => void (ev.code === 'F5' ? appwindow.reload() : 0),
|
||||
true
|
||||
);
|
||||
}
|
||||
}, 100);
|
@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Notion Enhancer
|
||||
* (c) 2020 dragonwocky <thedragonring.bod@gmail.com>
|
||||
* (c) 2020 TarasokUA
|
||||
* (https://dragonwocky.me/) under the MIT license
|
||||
*/
|
||||
|
||||
// a wrapper for accessing data stored in a JSON file
|
||||
|
||||
const path = require('path'),
|
||||
fs = require('fs');
|
||||
|
||||
class Store {
|
||||
constructor(opts) {
|
||||
this.path = path.join(__dirname, opts.config + '.json');
|
||||
this.data = parseDataFile(this.path, opts.defaults);
|
||||
}
|
||||
get(key) {
|
||||
return this.data[key];
|
||||
}
|
||||
set(key, val) {
|
||||
this.data[key] = val;
|
||||
fs.writeFileSync(this.path, JSON.stringify(this.data));
|
||||
}
|
||||
}
|
||||
|
||||
function parseDataFile(path, defaults) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(path));
|
||||
} catch (error) {
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Store;
|
@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Notion Enhancer
|
||||
* (c) 2020 dragonwocky <thedragonring.bod@gmail.com>
|
||||
* (c) 2020 TarasokUA
|
||||
* (https://dragonwocky.me/) under the MIT license
|
||||
*/
|
||||
|
||||
/* window control buttons: block */
|
||||
#window-buttons-area {
|
||||
padding-left: 14px;
|
||||
user-select: none;
|
||||
}
|
||||
/* window control buttons: light theme */
|
||||
.notion-light-theme .window-buttons {
|
||||
background: rgb(255, 255, 255);
|
||||
color: black;
|
||||
border: 0;
|
||||
margin: 0px 0px 0px 9px;
|
||||
width: 32px;
|
||||
line-height: 26px;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
transition-duration: 0.2s;
|
||||
font-weight: bold;
|
||||
}
|
||||
.notion-light-theme .window-buttons:hover {
|
||||
background: rgb(239, 239, 239);
|
||||
}
|
||||
/* window control buttons: dark theme */
|
||||
.notion-dark-theme .window-buttons {
|
||||
background: rgb(47, 52, 55);
|
||||
border: 0;
|
||||
margin: 0px 0px 0px 9px;
|
||||
width: 32px;
|
||||
line-height: 26px;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
.notion-dark-theme .window-buttons:hover {
|
||||
background: rgb(71, 76, 80);
|
||||
}
|
||||
|
||||
/* scrollbar: pointer */
|
||||
.notion-scroller {
|
||||
cursor: auto;
|
||||
}
|
||||
/* scrollbar: size */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px; /* for vertical */
|
||||
height: 8px; /* for horizontal */
|
||||
}
|
||||
/* scrollbar: light theme */
|
||||
.notion-light-theme ::-webkit-scrollbar-corner {
|
||||
background-color: transparent; /* for overlap */
|
||||
}
|
||||
.notion-light-theme ::-webkit-scrollbar-thumb {
|
||||
border-radius: 5px;
|
||||
background-color: #d9d8d6;
|
||||
border: 1px solid #cacac8;
|
||||
}
|
||||
.notion-light-theme ::-webkit-scrollbar-thumb:hover {
|
||||
background: #cacac8;
|
||||
}
|
||||
/* scrollbar: dark theme */
|
||||
.notion-dark-theme ::-webkit-scrollbar-corner {
|
||||
background-color: transparent; /* for overlap */
|
||||
}
|
||||
.notion-dark-theme ::-webkit-scrollbar-thumb {
|
||||
border-radius: 5px;
|
||||
background-color: #505457;
|
||||
}
|
||||
.notion-dark-theme ::-webkit-scrollbar-thumb:hover {
|
||||
background: #696d6f;
|
||||
}
|
8
scripts/build-browser-extension.sh
Executable file
@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
version=$(node -p "require('./package.json').version")
|
||||
|
||||
cd src
|
||||
mkdir -p ../dist
|
||||
rm -f "../dist/notion-enhancer-$version.zip"
|
||||
zip -r9 "../dist/notion-enhancer-$version.zip" .
|
160
scripts/enhance-desktop-app.mjs
Executable file
@ -0,0 +1,160 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import os from "node:os";
|
||||
import fsp from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { existsSync } from "node:fs";
|
||||
import { execSync } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import asar from "@electron/asar";
|
||||
import patch from "./patch-desktop-app.mjs";
|
||||
|
||||
const nodeRequire = createRequire(import.meta.url),
|
||||
platform =
|
||||
process.platform === "linux" &&
|
||||
os.release().toLowerCase().includes("microsoft")
|
||||
? "wsl"
|
||||
: process.platform,
|
||||
getEnv = (name) => {
|
||||
if (platform !== "wsl" || process.env[name]) return process.env[name];
|
||||
// read windows environment variables and convert
|
||||
// windows paths to paths mounted in the wsl fs
|
||||
const pipe = { encoding: "utf8", stdio: "pipe" },
|
||||
value = execSync(`cmd.exe /c echo %${name}%`, pipe).trim(),
|
||||
isAbsolutePath = /^[a-zA-Z]:[\\\/]/.test(value),
|
||||
isSystemPath = /^[\\\/]/.test(value);
|
||||
if (isAbsolutePath) {
|
||||
// e.g. C:\Program Files
|
||||
const drive = value[0].toLowerCase(),
|
||||
path = value.slice(2).replace(/\\/g, "/");
|
||||
process.env[name] = `/mnt/${drive}${path}`;
|
||||
} else if (isSystemPath) {
|
||||
// e.g. \Program Files
|
||||
const drive = getEnv("SYSTEMDRIVE")[0].toLowerCase(),
|
||||
path = value.replace(/\\/g, "/");
|
||||
process.env[name] = `/mnt/${drive}${path}`;
|
||||
} else process.env[name] = value;
|
||||
return process.env[name];
|
||||
};
|
||||
|
||||
let __notionResources;
|
||||
const setNotionPath = (path) => {
|
||||
// sets notion resource path to user provided value
|
||||
// e.g. with the --path cli option
|
||||
__notionResources = path;
|
||||
},
|
||||
getResourcePath = (...paths) => {
|
||||
if (__notionResources) return resolve(__notionResources, ...paths);
|
||||
// prettier-ignore
|
||||
for (const [platforms, notionResources] of [
|
||||
[['win32', 'wsl'], resolve(`${getEnv("LOCALAPPDATA")}/Programs/Notion/resources`)],
|
||||
[['win32', 'wsl'], resolve(`${getEnv("PROGRAMW6432")}/Notion/resources`)],
|
||||
[['darwin'], `/Users/${getEnv("USER")}/Applications/Notion.app/Contents/Resources`],
|
||||
[['darwin'], "/Applications/Notion.app/Contents/Resources"],
|
||||
[['linux'], "/opt/notion-app"],
|
||||
]) {
|
||||
if (!platforms.includes(platform)) continue;
|
||||
if (!existsSync(notionResources)) continue;
|
||||
__notionResources = notionResources;
|
||||
return resolve(__notionResources, ...paths);
|
||||
}
|
||||
},
|
||||
extractFile = (path) => {
|
||||
const archive = getResourcePath("app.asar");
|
||||
return asar.extractFile(archive, path);
|
||||
};
|
||||
|
||||
const getInsertPath = (...paths) => {
|
||||
return "node_modules/notion-enhancer/" + paths.join("/");
|
||||
},
|
||||
getInsertVersion = () => {
|
||||
try {
|
||||
const manifest = extractFile(getInsertPath("package.json")).toString();
|
||||
return JSON.parse(manifest).version;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const backupApp = async () => {
|
||||
const archive = getResourcePath("app.asar");
|
||||
if (!existsSync(archive)) return false;
|
||||
await fsp.cp(archive, archive + ".bak");
|
||||
return true;
|
||||
},
|
||||
restoreApp = async () => {
|
||||
const archive = getResourcePath("app.asar");
|
||||
if (!existsSync(archive + ".bak")) return false;
|
||||
await fsp.rename(archive + ".bak", archive);
|
||||
return true;
|
||||
},
|
||||
enhanceApp = async (debug = false, directoryMode = false) => {
|
||||
const app = getResourcePath("app"),
|
||||
archive = getResourcePath("app.asar");
|
||||
// directory mode acts on pre-extracted sources
|
||||
// as part of the notion-repackaged build process
|
||||
if (directoryMode) {
|
||||
if (!existsSync(app)) return false;
|
||||
for (let file of await fsp.readdir(app, { recursive: true })) {
|
||||
file = file.replace(/^\//g, "");
|
||||
const appPath = resolve(app, file),
|
||||
stat = await fsp.stat(appPath);
|
||||
if (stat.isFile()) {
|
||||
const content = await fsp.readFile(appPath);
|
||||
await fsp.writeFile(appPath, patch(file, content));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!existsSync(archive)) return false;
|
||||
if (existsSync(app)) await fsp.rm(app, { recursive: true, force: true });
|
||||
await fsp.mkdir(app);
|
||||
// extract archive to folder and apply patches
|
||||
for (let file of asar.listPackage(archive)) {
|
||||
file = file.replace(/^\//g, "");
|
||||
const stat = asar.statFile(archive, file),
|
||||
isFolder = !!stat.files,
|
||||
isSymlink = !!stat.link,
|
||||
isExecutable = stat.executable,
|
||||
appPath = resolve(app, file);
|
||||
if (isFolder) {
|
||||
await fsp.mkdir(appPath);
|
||||
} else if (isSymlink) {
|
||||
await fsp.symlink(appPath, resolve(app, link));
|
||||
} else {
|
||||
await fsp.writeFile(appPath, patch(file, extractFile(file)));
|
||||
if (isExecutable) await fsp.chmod(appPath, "755");
|
||||
}
|
||||
}
|
||||
}
|
||||
// insert the notion-enhancer/src folder into notion's node_modules
|
||||
const insertSrc = fileURLToPath(new URL("../src", import.meta.url)),
|
||||
insertDest = resolve(app, getInsertPath());
|
||||
await fsp.cp(insertSrc, insertDest, { recursive: true });
|
||||
// create package.json with cli-specific fields removed
|
||||
const insertManifest = resolve(insertDest, "package.json"),
|
||||
manifest = { ...nodeRequire("../package.json"), main: "init.js" },
|
||||
excludes = ["bin", "type", "scripts", "engines", "dependencies"];
|
||||
for (const key of excludes) delete manifest[key];
|
||||
await fsp.writeFile(insertManifest, JSON.stringify(manifest));
|
||||
if (!directoryMode) {
|
||||
// re-package enhanced sources into executable archive
|
||||
await asar.createPackage(app, archive);
|
||||
// cleanup extracted files unless in debug mode
|
||||
if (!debug) await fsp.rm(app, { recursive: true });
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export {
|
||||
backupApp,
|
||||
restoreApp,
|
||||
enhanceApp,
|
||||
getInsertVersion,
|
||||
getResourcePath,
|
||||
setNotionPath,
|
||||
};
|
893
scripts/generate-theme-css.mjs
Normal file
@ -0,0 +1,893 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
// paste this in the devtools console at to generate theme css
|
||||
// at https://www.notion.so/9390e51f458940a5a339dc4b8fdea2fb.
|
||||
// to detect fonts, open the ... menu before running.
|
||||
|
||||
// repeat for both light and dark modes, pass the css through
|
||||
// https://css-minifier.com/ and https://css.github.io/csso/csso.html
|
||||
// and then save it to core/variables.css and core/theme.css
|
||||
|
||||
// todo: svg page & property icons
|
||||
|
||||
const darkMode = document.body.classList.contains("dark"),
|
||||
modeSelector = darkMode ? ".dark" : ":not(.dark)",
|
||||
bodySelector = `.notion-body${modeSelector}`;
|
||||
let cssRoot = "",
|
||||
cssBody = "",
|
||||
cssRefs = {};
|
||||
|
||||
const getComputedPropertyValue = (el, prop) => {
|
||||
const styles = window.getComputedStyle(el),
|
||||
value = styles.getPropertyValue(prop);
|
||||
return value;
|
||||
},
|
||||
cssVariable = ({ name, value, alias, splitValues = false }) => {
|
||||
const values = splitValues ? value.split(", ") : [value],
|
||||
rgbPattern = /^rgba?\(\d{1,3},\d{1,3},\d{1,3}(?:,\d{1,3})?\)$/,
|
||||
isColor = rgbPattern.test(value.replace(/\s/g, ""));
|
||||
if (isColor) {
|
||||
values[0] = values[0].replace(/\s/g, "");
|
||||
const hasOpaqueAlpha =
|
||||
values[0].trim().startsWith("rgba(") &&
|
||||
values[0].trim().endsWith(",1)");
|
||||
if (hasOpaqueAlpha) values[0] = `rgb(${values[0].slice(5, -3)})`;
|
||||
}
|
||||
if (!cssRoot.includes(`--theme--${name}:`)) {
|
||||
cssRoot += `--theme--${name}:${
|
||||
alias ? `var(--theme--${alias})` : value
|
||||
};`;
|
||||
}
|
||||
return {
|
||||
name,
|
||||
value,
|
||||
ref: `var(--theme--${name},${values[0]})${
|
||||
values.length > 1 ? ", " : ""
|
||||
}${values.slice(1).join(", ")} !important`,
|
||||
};
|
||||
},
|
||||
overrideStyle = ({
|
||||
element,
|
||||
selector = "",
|
||||
property,
|
||||
variable,
|
||||
variableAliases = {},
|
||||
valueAliases = [],
|
||||
specificity = ["mode", "value"],
|
||||
cssProps = {},
|
||||
postProcessor = (selector, cssProps) => [selector, cssProps],
|
||||
}) => {
|
||||
if (selector) element ??= document.querySelector(selector);
|
||||
const style = element?.getAttribute("style") ?? "",
|
||||
pattern = String.raw`(?:^|(?:;\s*))${property}:\s*([^;]+);?`,
|
||||
match = style.match(new RegExp(pattern));
|
||||
if (typeof variable === "string") {
|
||||
let value = match?.[1];
|
||||
if (element) {
|
||||
value ??= getComputedPropertyValue(
|
||||
element,
|
||||
property === "background" ? "background-color" : property
|
||||
);
|
||||
}
|
||||
if (!value) throw new Error(`${property} not found for ${selector}`);
|
||||
variable = cssVariable({
|
||||
name: variable,
|
||||
value: value,
|
||||
alias: variableAliases[value],
|
||||
splitValues: property === "font-family",
|
||||
});
|
||||
}
|
||||
if (specificity.includes("value")) {
|
||||
if (/(?<!rgb\()[^\s\d,]+,/g.test(selector) && !selector.includes(":is")) {
|
||||
selector = `:is(${selector})`;
|
||||
}
|
||||
if (match?.[0]) selector += `[style*="${match[0].replace(/"/g, `\\"`)}"]`;
|
||||
else {
|
||||
const propSelector = [variable.value, ...valueAliases]
|
||||
.map((value) =>
|
||||
property === "color"
|
||||
? `[style^="color: ${value}"],
|
||||
[style^="color:${value}"],
|
||||
[style*=";color: ${value}"],
|
||||
[style*=";color:${value}"],
|
||||
[style*=" color: ${value}"],
|
||||
[style*=" color:${value}"],
|
||||
[style*="fill: ${value}"],
|
||||
[style*="fill:${value}"]`
|
||||
: property === "background"
|
||||
? `[style^="background: ${value}"],
|
||||
[style^="background:${value}"],
|
||||
[style*=";background: ${value}"],
|
||||
[style*=";background:${value}"],
|
||||
[style*=" background: ${value}"],
|
||||
[style*=" background:${value}"],
|
||||
[style*="background-color: ${value}"],
|
||||
[style*="background-color:${value}"]`
|
||||
: `[style*="${property}: ${value}"],
|
||||
[style*="${property}:${value}"]`
|
||||
)
|
||||
.join(",");
|
||||
selector += selector ? `:is(${propSelector})` : propSelector;
|
||||
}
|
||||
}
|
||||
if (specificity.includes("mode")) {
|
||||
selector =
|
||||
/(?<!rgb\()[^\s\d,]+,/g.test(selector) && !selector.includes(":is")
|
||||
? `${bodySelector} :is(${selector})`
|
||||
: `${bodySelector} ${selector}`;
|
||||
}
|
||||
cssProps[property] = variable;
|
||||
cssProps["fill"] ??= cssProps["color"];
|
||||
[selector, cssProps] = postProcessor(selector, cssProps);
|
||||
const body = Object.entries(cssProps)
|
||||
.filter(([prop, val]) => prop && val)
|
||||
.map(([prop, val]) => `${prop}:${val?.ref ?? val}`)
|
||||
.join(";");
|
||||
cssRefs[body] ??= [];
|
||||
cssRefs[body].push(selector);
|
||||
variableAliases[variable.value] ??= variable.name;
|
||||
};
|
||||
|
||||
const styleText = () => {
|
||||
const primary = cssVariable({
|
||||
name: "fg-primary",
|
||||
value: darkMode ? "rgba(255, 255, 255, 0.81)" : "rgb(55, 53, 47)",
|
||||
}),
|
||||
primaryAliases = darkMode
|
||||
? [
|
||||
"rgb(211, 211, 211)",
|
||||
"rgb(255, 255, 255)",
|
||||
"rgba(255, 255, 255, 0.8",
|
||||
"rgba(255, 255, 255, 0.9",
|
||||
"rgba(255, 255, 255, 1",
|
||||
]
|
||||
: [
|
||||
"rgba(255, 255, 255, 0.9)",
|
||||
"rgba(55, 53, 47, 0.8",
|
||||
"rgba(55, 53, 47, 0.9",
|
||||
"rgba(55, 53, 47, 1",
|
||||
];
|
||||
|
||||
const secondary = cssVariable({
|
||||
name: "fg-secondary",
|
||||
value: darkMode ? "rgb(155, 155, 155)" : "rgba(25, 23, 17, 0.6)",
|
||||
}),
|
||||
secondaryAliases = darkMode
|
||||
? [
|
||||
"rgb(127, 127, 127)",
|
||||
"rgba(255, 255, 255, 0.0",
|
||||
"rgba(255, 255, 255, 0.1",
|
||||
"rgba(255, 255, 255, 0.2",
|
||||
"rgba(255, 255, 255, 0.3",
|
||||
"rgba(255, 255, 255, 0.4",
|
||||
"rgba(255, 255, 255, 0.5",
|
||||
"rgba(255, 255, 255, 0.6",
|
||||
"rgba(255, 255, 255, 0.7",
|
||||
]
|
||||
: [
|
||||
"rgba(206, 205, 202, 0.6)",
|
||||
"rgba(55, 53, 47, 0.0",
|
||||
"rgba(55, 53, 47, 0.1",
|
||||
"rgba(55, 53, 47, 0.2",
|
||||
"rgba(55, 53, 47, 0.3",
|
||||
"rgba(55, 53, 47, 0.4",
|
||||
"rgba(55, 53, 47, 0.5",
|
||||
"rgba(55, 53, 47, 0.6",
|
||||
"rgba(55, 53, 47, 0.7",
|
||||
];
|
||||
|
||||
overrideStyle({
|
||||
property: "color",
|
||||
variable: primary,
|
||||
valueAliases: primaryAliases,
|
||||
cssProps: {
|
||||
"caret-color": primary,
|
||||
"text-decoration-color": "currentColor",
|
||||
fill: primary,
|
||||
},
|
||||
});
|
||||
overrideStyle({
|
||||
property: "color",
|
||||
variable: secondary,
|
||||
valueAliases: secondaryAliases,
|
||||
cssProps: {
|
||||
"caret-color": secondary,
|
||||
"text-decoration-color": "currentColor",
|
||||
fill: secondary,
|
||||
},
|
||||
postProcessor(selector, cssProps) {
|
||||
return [
|
||||
`${bodySelector} :is(.rdp-nav_icon, .rdp-head_cell,
|
||||
.rdp-day.rdp-day_outside, ::placeholder), ${selector}`,
|
||||
cssProps,
|
||||
];
|
||||
},
|
||||
});
|
||||
overrideStyle({
|
||||
property: "caret-color",
|
||||
variable: primary,
|
||||
valueAliases: primaryAliases,
|
||||
});
|
||||
overrideStyle({
|
||||
property: "caret-color",
|
||||
variable: secondary,
|
||||
valueAliases: secondaryAliases,
|
||||
});
|
||||
overrideStyle({
|
||||
selector: `[style*="-webkit-text-fill-color:"]`,
|
||||
property: "-webkit-text-fill-color",
|
||||
variable: secondary,
|
||||
specificity: ["mode"],
|
||||
});
|
||||
|
||||
// light mode tags have coloured text,
|
||||
// replace with primary text for inter-mode consistency
|
||||
for (const tagSelector of [
|
||||
`[style*="height: 20px; border-radius: 3px; padding-left: 6px;"][style*="background:"]`,
|
||||
`.notion-collection_view-block [style*="height: 14px; border-radius: 3px; padding-left: 6px;"]`,
|
||||
`.notion-timeline-item-properties [style*="height: 18px; border-radius: 3px; padding-left: 8px;"]`,
|
||||
]) {
|
||||
for (const el of document.querySelectorAll(tagSelector)) {
|
||||
if (darkMode) continue;
|
||||
overrideStyle({
|
||||
element: el,
|
||||
selector: tagSelector,
|
||||
property: "color",
|
||||
variable: "fg-primary",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const styleBorders = () => {
|
||||
const border = cssVariable({
|
||||
name: "fg-border",
|
||||
value: darkMode ? "rgb(47, 47, 47)" : "rgb(233, 233, 231)",
|
||||
}),
|
||||
borderColors = darkMode
|
||||
? [border.value.slice(4, -1), "37, 37, 37", "255, 255, 255"]
|
||||
: [border.value.slice(4, -1), "238, 238, 237", "55, 53, 47"],
|
||||
boxShadows = darkMode
|
||||
? [
|
||||
"; box-shadow: rgba(255, 255, 255, 0.094) 0px -1px 0px;",
|
||||
"; box-shadow: rgba(15, 15, 15, 0.2) 0px 0px 0px 1px inset;",
|
||||
"; box-shadow: rgb(25, 25, 25) -3px 0px 0px, rgb(47, 47, 47) 0px 1px 0px;",
|
||||
]
|
||||
: [
|
||||
"; box-shadow: rgba(55, 53, 47, 0.09) 0px -1px 0px;",
|
||||
"; box-shadow: rgba(15, 15, 15, 0.1) 0px 0px 0px 1px inset;",
|
||||
"; box-shadow: white -3px 0px 0px, rgb(233, 233, 231) 0px 1px 0px;",
|
||||
];
|
||||
for (const el of document.querySelectorAll(`[style*="box-shadow:"]`)) {
|
||||
const boxShadow = el
|
||||
.getAttribute("style")
|
||||
.match(/(?:^|(?:;\s*))box-shadow:\s*([^;]+);?/)?.[0];
|
||||
if (borderColors.some((color) => boxShadow.includes(color))) {
|
||||
boxShadows.push(boxShadow);
|
||||
}
|
||||
}
|
||||
|
||||
overrideStyle({
|
||||
selector: `[style*="height: 1px;"][style*="background"]`,
|
||||
property: "background",
|
||||
variable: border,
|
||||
specificity: ["mode"],
|
||||
});
|
||||
cssBody += `
|
||||
${bodySelector} :is(${[...new Set(borderColors)]
|
||||
.map(
|
||||
(color) =>
|
||||
`[style*="px solid rgb(${color}"], [style*="px solid rgba(${color}"]`
|
||||
)
|
||||
.join(", ")}):is([style*="border:"], [style*="border-top:"],
|
||||
[style*="border-left:"], [style*="border-bottom:"],
|
||||
[style*="border-right:"]) { border-color: ${border.ref}; }
|
||||
${[...new Set(boxShadows)]
|
||||
.map((shadow) => {
|
||||
if (shadow.startsWith(";")) shadow = shadow.slice(1);
|
||||
return `${bodySelector} [style*="${shadow}"] { ${shadow
|
||||
.replace(
|
||||
/rgba?\([^\)]+\)/g,
|
||||
shadow.includes("-3px 0px 0px, ")
|
||||
? "transparent"
|
||||
: `var(--theme--fg-border, ${border.value})`
|
||||
)
|
||||
.slice(0, -1)} !important; }`;
|
||||
})
|
||||
.join("")}
|
||||
`;
|
||||
};
|
||||
|
||||
const styleColoredText = () => {
|
||||
// inline text
|
||||
for (const el of document.querySelectorAll(
|
||||
'.notion-selectable .notion-enable-hover[style*="color:"][style*="fill:"]:not([style*="mono"])'
|
||||
)) {
|
||||
if (!el.innerText || /\s/.test(el.innerText)) continue;
|
||||
overrideStyle({
|
||||
element: el,
|
||||
selector: `
|
||||
.notion-selectable .notion-enable-hover,
|
||||
.notion-code-block span.token
|
||||
`,
|
||||
property: "color",
|
||||
variable: `fg-${el.innerText}`,
|
||||
});
|
||||
}
|
||||
|
||||
// block text
|
||||
for (const el of document.querySelectorAll(
|
||||
'.notion-text-block > [style*="color:"][style*="fill:"]'
|
||||
)) {
|
||||
if (!el.innerText || /\s/.test(el.innerText)) continue;
|
||||
overrideStyle({
|
||||
element: el,
|
||||
selector: `.notion-text-block > [style*="color:"][style*="fill:"]`,
|
||||
property: "color",
|
||||
variable: `fg-${el.innerText}`,
|
||||
});
|
||||
}
|
||||
|
||||
// board text
|
||||
for (const group of document.querySelectorAll(
|
||||
".notion-board-view .notion-board-group"
|
||||
)) {
|
||||
// get color name from card
|
||||
const card = group.querySelector('a[style*="background"]'),
|
||||
innerText = card.innerText.replace("Drag image to reposition\n", "");
|
||||
if (!innerText || /\s/.test(innerText)) continue;
|
||||
const el = group.querySelector('[style*="height: 32px"]'),
|
||||
groupStyle = group
|
||||
.getAttribute("style")
|
||||
.match(/background(?:-color)?:\s*([^;]+);?/)[1];
|
||||
overrideStyle({
|
||||
element: el,
|
||||
selector: `.notion-board-view :is(
|
||||
.notion-board-group[style*="${groupStyle}"] [style*="height: 32px"],
|
||||
[style*="${groupStyle}"] > [style*="color"]:nth-child(2),
|
||||
[style*="${groupStyle}"] > div > svg
|
||||
)`,
|
||||
property: "color",
|
||||
// light_gray text doesn't exist
|
||||
variable: `fg-${innerText === "light_gray" ? "secondary" : innerText}`,
|
||||
specificity: ["mode"],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const styleBackgrounds = () => {
|
||||
const primary = cssVariable({
|
||||
name: "bg-primary",
|
||||
value: darkMode ? "rgb(25, 25, 25)" : "white",
|
||||
}),
|
||||
secondary = cssVariable({
|
||||
name: "bg-secondary",
|
||||
value: darkMode ? "rgb(32, 32, 32)" : "rgb(251, 251, 250)",
|
||||
});
|
||||
|
||||
overrideStyle({
|
||||
property: "background",
|
||||
variable: primary,
|
||||
valueAliases: darkMode ? [] : ["rgb(255, 255, 255)", "rgb(247, 247, 247)"],
|
||||
postProcessor(selector, cssProps) {
|
||||
return [`${selector}:not(.notion-timeline-view)`, cssProps];
|
||||
},
|
||||
});
|
||||
overrideStyle({
|
||||
property: "background",
|
||||
variable: secondary,
|
||||
valueAliases: darkMode
|
||||
? ["rgb(37, 37, 37)", "rgb(47, 47, 47)"]
|
||||
: ["rgb(253, 253, 253)"],
|
||||
});
|
||||
// patch: remove overlay from settings sidebar
|
||||
// to match notion-enhancer menu sidebar colour
|
||||
cssBody += `.notion-overlay-container .notion-space-settings > div > div > [style*="height: 100%; background: rgba(255, 255, 255, 0.03);"] { background: transparent !important }`;
|
||||
|
||||
// cards
|
||||
overrideStyle({
|
||||
selector: `.notion-timeline-item,
|
||||
.notion-calendar-view .notion-collection-item > a,
|
||||
.notion-gallery-view .notion-collection-item > a`,
|
||||
property: "background",
|
||||
variable: secondary,
|
||||
});
|
||||
|
||||
// popups
|
||||
overrideStyle({
|
||||
selector: `.notion-overlay-container [style*="border-radius: 4px;"
|
||||
][style*="position: relative; max-width: calc(100vw - 24px); box-shadow:"],
|
||||
[style*="font-size: 12px;"][style*="box-shadow:"][
|
||||
style*="border-radius: 3px; max-width: calc(100% - 16px); min-height: 24px; overflow: hidden;"
|
||||
][style*="position: absolute; right: 8px; bottom: 8px; z-index:"],
|
||||
[style*="height: 32px;"][style*="font-size: 14px; line-height: 1.2; border-radius: 5px; box-shadow:"],
|
||||
[style*="transition: background"][style*="cursor: pointer;"][
|
||||
style*="border-radius: 3px; height: 24px; width: 24px;"][style*="box-shadow:"],
|
||||
[style*="right: 6px; top: 4px;"][style*="border-radius: 4px;"][style*="gap: 1px;"][style*="box-shadow:"]`,
|
||||
property: "background",
|
||||
variable: secondary,
|
||||
});
|
||||
|
||||
// modals
|
||||
overrideStyle({
|
||||
selector: `.notion-overlay-container [data-overlay] :is(
|
||||
[style*="height: 100%; width: 275px;"][style*="flex-direction: column;"],
|
||||
.notion-space-settings [style*="flex-grow: 1"] > [style*="background-color"])`,
|
||||
property: "background",
|
||||
variable: primary,
|
||||
specificity: ["mode"],
|
||||
});
|
||||
overrideStyle({
|
||||
selector: `.notion-overlay-container [data-overlay] :is(
|
||||
[style*="height: 100%; width: 275px;"][style*="flex-direction: column;"] + [style*="width: 100%;"],
|
||||
.notion-space-settings [style*="height: 100%; background:"][style*="max-width: 250px;"])`,
|
||||
property: "background",
|
||||
variable: secondary,
|
||||
specificity: ["mode"],
|
||||
});
|
||||
|
||||
// timeline fades
|
||||
overrideStyle({
|
||||
selector: `.notion-timeline-view`,
|
||||
property: "background",
|
||||
variable: primary,
|
||||
specificity: ["mode"],
|
||||
});
|
||||
cssBody += `[style*="linear-gradient(to left, ${
|
||||
darkMode ? primary.value : "white"
|
||||
} 20%, rgba(${
|
||||
darkMode ? primary.value.slice(4, -1) : "255, 255, 255"
|
||||
}, 0) 100%)"] { background-image: linear-gradient(to left,
|
||||
var(--theme--bg-primary, ${primary.value}) 20%, transparent
|
||||
100%) !important; }
|
||||
[style*="linear-gradient(to right, ${
|
||||
darkMode ? primary.value : "white"
|
||||
} 20%, rgba(${
|
||||
darkMode ? primary.value.slice(4, -1) : "255, 255, 255"
|
||||
}, 0) 100%)"] { background-image: linear-gradient(to right,
|
||||
var(--theme--bg-primary, ${primary.value}) 20%, transparent
|
||||
100%) !important; }
|
||||
`;
|
||||
|
||||
// hovered elements, inputs and unchecked toggle backgrounds
|
||||
overrideStyle({
|
||||
property: "background",
|
||||
variable: cssVariable({
|
||||
name: "bg-hover",
|
||||
value: darkMode ? "rgba(255, 255, 255, 0.055)" : "rgba(55, 53, 47, 0.08)",
|
||||
}),
|
||||
valueAliases: darkMode
|
||||
? []
|
||||
: [
|
||||
"rgba(242, 241, 238, 0.6)",
|
||||
"rgb(225, 225, 225)",
|
||||
"rgb(239, 239, 238)",
|
||||
],
|
||||
postProcessor(selector, cssProps) {
|
||||
selector += `, ${bodySelector} [style*="height: 14px; width: 26px; border-radius: 44px;"][style*="rgba"]`;
|
||||
if (darkMode) {
|
||||
selector += `, ${bodySelector} :is([style*="background: rgb(47, 47, 47)"],
|
||||
[style*="background-color: rgb(47, 47, 47)"])[style*="transition: background"]:hover`;
|
||||
}
|
||||
return [selector, cssProps];
|
||||
},
|
||||
});
|
||||
|
||||
// modal shadow
|
||||
overrideStyle({
|
||||
selector: `.notion-overlay-container [data-overlay]
|
||||
> div > [style*="position: absolute"]:first-child`,
|
||||
property: "background",
|
||||
variable: cssVariable({
|
||||
name: "bg-overlay",
|
||||
value: darkMode ? "rgba(15, 15, 15, 0.8)" : "rgba(15, 15, 15, 0.6)",
|
||||
}),
|
||||
specificity: ["mode"],
|
||||
});
|
||||
};
|
||||
|
||||
const styleColoredBackgrounds = () => {
|
||||
for (const targetSelector of [
|
||||
// database tags
|
||||
`[style*="height: 20px; border-radius: 3px; padding-left: 6px;"]`,
|
||||
`.notion-collection_view-block [style*="height: 14px; border-radius: 3px; padding-left: 6px;"]`,
|
||||
`:is(.notion-timeline-item-properties [style*="height: 18px; border-radius: 3px; padding-left: 8px;"],
|
||||
.notion-collection_view-block .notion-collection-item a > .notion-focusable)`,
|
||||
// inline highlights
|
||||
`.notion-selectable .notion-enable-hover[style*="background:"]`,
|
||||
// block highlights and hovered board items
|
||||
`:is(.notion-text-block > [style*="background:"],
|
||||
.notion-collection_view-block .notion-collection-item a > .notion-focusable)`,
|
||||
]) {
|
||||
for (const el of document.querySelectorAll(targetSelector)) {
|
||||
if (!el.innerText || /\s/.test(el.innerText)) continue;
|
||||
overrideStyle({
|
||||
element: el,
|
||||
selector: targetSelector,
|
||||
property: "background",
|
||||
variable: `bg-${el.innerText}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// board cards
|
||||
for (const group of document.querySelectorAll(
|
||||
".notion-board-view .notion-board-group"
|
||||
)) {
|
||||
const card = group.querySelector('a[style*="background"]'),
|
||||
innerText = card.innerText.replace("Drag image to reposition\n", "");
|
||||
if (!innerText || /\s/.test(innerText)) continue;
|
||||
const groupStyle = group
|
||||
.getAttribute("style")
|
||||
.match(/background(?:-color)?:\s*([^;]+);?/)[1];
|
||||
// in light mode pages in board views all have bg "white"
|
||||
// by default, must be styled based on parent
|
||||
overrideStyle({
|
||||
element: card,
|
||||
selector: `.notion-board-view .notion-board-group[style*="${groupStyle}"] a`,
|
||||
property: "background",
|
||||
variable: `bg-${innerText}`,
|
||||
specificity: ["mode"],
|
||||
});
|
||||
overrideStyle({
|
||||
element: group,
|
||||
selector: `.notion-board-view [style*="${groupStyle}"]:is(
|
||||
.notion-board-group,
|
||||
[style*="border-top-left-radius: 5px;"]
|
||||
)`,
|
||||
property: "background",
|
||||
variable: `dim-${innerText}`,
|
||||
specificity: ["mode"],
|
||||
});
|
||||
}
|
||||
|
||||
// use dim for callout blocks
|
||||
for (const el of document.querySelectorAll(
|
||||
'.notion-callout-block > div > [style*="background:"]'
|
||||
)) {
|
||||
if (!el.innerText || /\s/.test(el.innerText)) continue;
|
||||
overrideStyle({
|
||||
element: el,
|
||||
selector: ".notion-callout-block > div > div",
|
||||
property: "background",
|
||||
variable: `dim-${el.innerText}`,
|
||||
});
|
||||
}
|
||||
// use yellow for notification highlights
|
||||
overrideStyle({
|
||||
property: "background",
|
||||
variable: cssVariable({
|
||||
name: "bg-yellow",
|
||||
value: "rgba(255, 212, 0, 0.14)",
|
||||
}),
|
||||
specificity: ["value"],
|
||||
});
|
||||
// use light gray for taglikes e.g. file property values
|
||||
overrideStyle({
|
||||
selector: `[style*="height: 18px; border-radius: 3px; background"]`,
|
||||
property: "background",
|
||||
variable: "bg-light_gray",
|
||||
});
|
||||
};
|
||||
|
||||
const styleTooltips = () => {
|
||||
cssBody += `.notion-overlay-container [style*="border-radius: 3px; background:"
|
||||
][style*="max-width: calc(100vw - 24px); box-shadow:"
|
||||
][style*="padding: 4px 8px; font-size: 12px; line-height: 1.4; font-weight: 500;"] {
|
||||
background: rgb(15, 15, 15) !important;
|
||||
color: rgba(255, 255, 255, 0.9) !important;
|
||||
}
|
||||
.notion-overlay-container [style*="border-radius: 3px; background:"
|
||||
][style*="max-width: calc(100vw - 24px); box-shadow:"
|
||||
][style*="padding: 4px 8px; font-size: 12px; line-height: 1.4; font-weight: 500;"]
|
||||
> [style*="color"] { color: rgb(127, 127, 127) !important; }`;
|
||||
};
|
||||
|
||||
const styleAccents = () => {
|
||||
const primary = cssVariable({
|
||||
name: "accent-primary",
|
||||
value: "rgb(35, 131, 226)",
|
||||
}),
|
||||
primaryHover = cssVariable({
|
||||
name: "accent-primary_hover",
|
||||
value: "rgb(0, 117, 211)",
|
||||
}),
|
||||
primaryContrast = cssVariable({
|
||||
name: "accent-primary_contrast",
|
||||
value: "rgb(255, 255, 255)",
|
||||
}),
|
||||
primaryTransparent = cssVariable({
|
||||
name: "accent-primary_transparent",
|
||||
value: "rgba(35, 131, 226, 0.14)",
|
||||
});
|
||||
overrideStyle({
|
||||
property: "color",
|
||||
variable: primary,
|
||||
specificity: ["value"],
|
||||
});
|
||||
overrideStyle({
|
||||
property: "background",
|
||||
variable: primary,
|
||||
specificity: ["value"],
|
||||
cssProps: {
|
||||
fill: primaryContrast,
|
||||
color: primaryContrast,
|
||||
},
|
||||
});
|
||||
overrideStyle({
|
||||
property: "background",
|
||||
variable: primaryHover,
|
||||
specificity: ["value"],
|
||||
cssProps: {
|
||||
fill: primaryContrast,
|
||||
color: primaryContrast,
|
||||
},
|
||||
});
|
||||
overrideStyle({
|
||||
selector: `.notion-table-selection-overlay [style*="border: 2px solid"]`,
|
||||
property: "border-color",
|
||||
variable: primary,
|
||||
specificity: [],
|
||||
});
|
||||
overrideStyle({
|
||||
selector: `
|
||||
[style*="background: ${primary.value}"] svg[style*="fill"],
|
||||
[style*="background-color: ${primary.value}"] svg[style*="fill"]
|
||||
`,
|
||||
property: "fill",
|
||||
variable: primaryContrast,
|
||||
specificity: [],
|
||||
});
|
||||
overrideStyle({
|
||||
selector: `[style*="border-radius: 44px;"] > [style*="border-radius: 44px; background: white;"]`,
|
||||
property: "background",
|
||||
variable: primaryContrast,
|
||||
specificity: [],
|
||||
});
|
||||
overrideStyle({
|
||||
selector: `
|
||||
*::selection,
|
||||
.notion-selectable-halo,
|
||||
#notion-app .rdp-day:not(.rdp-day_disabled):not(.rdp-day_selected
|
||||
):not(.rdp-day_value):not(.rdp-day_start):not(.rdp-day_end):hover,
|
||||
[style*="background: ${primaryTransparent.value.split(".")[0]}."],
|
||||
[style*="background:${primaryTransparent.value.split(".")[0]}."],
|
||||
[style*="background-color: ${primaryTransparent.value.split(".")[0]}."],
|
||||
[style*="background-color:${primaryTransparent.value.split(".")[0]}."]
|
||||
`,
|
||||
property: "background",
|
||||
variable: primaryTransparent,
|
||||
specificity: [],
|
||||
});
|
||||
|
||||
const secondary = cssVariable({
|
||||
name: "accent-secondary",
|
||||
value: "rgb(235, 87, 87)",
|
||||
}),
|
||||
secondaryAliases = [
|
||||
"rgb(180, 65, 60)",
|
||||
"rgb(211, 79, 67)",
|
||||
"rgb(205, 73, 69)",
|
||||
],
|
||||
secondaryHover = cssVariable({
|
||||
name: "accent-secondary_hover",
|
||||
value: "rgba(235, 87, 87, 0.1)",
|
||||
}),
|
||||
secondaryContrast = cssVariable({
|
||||
name: "accent-secondary_contrast",
|
||||
value: "white",
|
||||
});
|
||||
overrideStyle({
|
||||
property: "color",
|
||||
variable: secondary,
|
||||
valueAliases: secondaryAliases,
|
||||
specificity: ["value"],
|
||||
});
|
||||
overrideStyle({
|
||||
property: "background",
|
||||
variable: secondary,
|
||||
valueAliases: secondaryAliases,
|
||||
specificity: ["value"],
|
||||
cssProps: {
|
||||
fill: secondaryContrast,
|
||||
color: secondaryContrast,
|
||||
},
|
||||
postProcessor(selector, cssProps) {
|
||||
return [
|
||||
`#notion-app .rdp-day_today:not(.rdp-day_selected):not(.rdp-day_value
|
||||
):not(.rdp-day_start):not(.rdp-day_end)::after, ${selector}`,
|
||||
cssProps,
|
||||
];
|
||||
},
|
||||
});
|
||||
overrideStyle({
|
||||
property: "background",
|
||||
variable: secondary,
|
||||
valueAliases: secondaryAliases,
|
||||
specificity: ["value"],
|
||||
cssProps: {
|
||||
fill: secondaryContrast,
|
||||
color: secondaryContrast,
|
||||
},
|
||||
postProcessor(selector, cssProps) {
|
||||
delete cssProps["background"];
|
||||
return [
|
||||
`#notion-app .rdp-day_today:not(.rdp-day_selected):not(.rdp-day_value):not(.rdp-day_start
|
||||
):not(.rdp-day_end), :is(${selector}) + :is([style*="fill: ${secondaryContrast.value};"],
|
||||
[style*="color: ${secondaryContrast.value};"]), :is(${selector})
|
||||
:is([style*="fill: ${secondaryContrast.value};"], [style*="color: ${secondaryContrast.value};"])`,
|
||||
cssProps,
|
||||
];
|
||||
},
|
||||
});
|
||||
overrideStyle({
|
||||
property: "background",
|
||||
variable: secondaryHover,
|
||||
specificity: ["value"],
|
||||
});
|
||||
|
||||
// box-shadows are complicated, style manually
|
||||
cssBody += `.notion-focusable-within:focus-within {
|
||||
box-shadow:
|
||||
var(--theme--accent-primary, ${primary.value}) 0px 0px 0px 1px inset,
|
||||
var(--theme--accent-primary, ${primary.value}) 0px 0px 0px 2px
|
||||
!important;
|
||||
}
|
||||
.notion-focusable:focus-visible {
|
||||
box-shadow:
|
||||
var(--theme--accent-primary, ${primary.value}) 0px 0px 0px 1px inset,
|
||||
var(--theme--accent-primary, ${primary.value}) 0px 0px 0px 2px
|
||||
!important;
|
||||
}
|
||||
${["box-shadow: rgb(35, 131, 226) 0px 0px 0px 2px inset"]
|
||||
.map((shadow) => {
|
||||
return `[style*="${shadow}"] { ${shadow.replace(
|
||||
/rgba?\([^\)]+\)/g,
|
||||
`var(--theme--accent-primary, ${primary.value})`
|
||||
)} !important; }`;
|
||||
})
|
||||
.join("")}
|
||||
${[
|
||||
"border: 1px solid rgb(110, 54, 48)",
|
||||
"border: 1px solid rgba(235, 87, 87, 0.5)",
|
||||
"border: 2px solid rgb(110, 54, 48)",
|
||||
"border: 2px solid rgb(227, 134, 118)",
|
||||
"border-right: 1px solid rgb(180, 65, 60)",
|
||||
"border-right: 1px solid rgb(211, 79, 67)",
|
||||
]
|
||||
.map((border) => `[style*="${border}"]`)
|
||||
.join(", ")} { border-color: ${secondary.ref}; }`;
|
||||
};
|
||||
|
||||
const styleScrollbars = () => {
|
||||
const scrollbarTrack = cssVariable({
|
||||
name: "scrollbar-track",
|
||||
value: darkMode ? "rgba(202, 204, 206, 0.04)" : "#EDECE9",
|
||||
});
|
||||
overrideStyle({
|
||||
selector: "::-webkit-scrollbar-track",
|
||||
property: "background",
|
||||
variable: scrollbarTrack,
|
||||
specificity: ["mode"],
|
||||
});
|
||||
overrideStyle({
|
||||
selector: "::-webkit-scrollbar-corner",
|
||||
property: "background",
|
||||
variable: scrollbarTrack,
|
||||
specificity: ["mode"],
|
||||
});
|
||||
overrideStyle({
|
||||
selector: "::-webkit-scrollbar-thumb",
|
||||
property: "background",
|
||||
variable: cssVariable({
|
||||
name: "scrollbar-thumb",
|
||||
value: darkMode ? "#474c50" : "#D3D1CB",
|
||||
}),
|
||||
specificity: ["mode"],
|
||||
});
|
||||
overrideStyle({
|
||||
selector: "::-webkit-scrollbar-thumb:hover",
|
||||
property: "background",
|
||||
variable: cssVariable({
|
||||
name: "scrollbar-thumb_hover",
|
||||
value: darkMode ? "rgba(202, 204, 206, 0.3)" : "#AEACA6",
|
||||
}),
|
||||
specificity: ["mode"],
|
||||
});
|
||||
};
|
||||
|
||||
const styleCode = () => {
|
||||
overrideStyle({
|
||||
selector: `.notion-text-block .notion-enable-hover[style*="mono"]`,
|
||||
property: "color",
|
||||
variable: "code-inline_fg",
|
||||
});
|
||||
overrideStyle({
|
||||
selector: `.notion-text-block .notion-enable-hover[style*="mono"]`,
|
||||
property: "background",
|
||||
variable: "code-inline_bg",
|
||||
});
|
||||
|
||||
overrideStyle({
|
||||
selector: `.notion-code-block > [style*="mono"]`,
|
||||
property: "color",
|
||||
variable: "code-block_fg",
|
||||
});
|
||||
overrideStyle({
|
||||
selector: `.notion-code-block > div > [style*="background"]`,
|
||||
property: "background",
|
||||
variable: "code-block_bg",
|
||||
});
|
||||
|
||||
const aliases = {},
|
||||
code = document.querySelector(".notion-code-block .token");
|
||||
for (const token of [
|
||||
// standard tokens from https://prismjs.com/tokens.html
|
||||
"keyword",
|
||||
"builtin",
|
||||
"class-name",
|
||||
"function",
|
||||
"boolean",
|
||||
"number",
|
||||
"string",
|
||||
"char",
|
||||
"symbol",
|
||||
"regex",
|
||||
"url",
|
||||
"operator",
|
||||
"variable",
|
||||
"constant",
|
||||
"property",
|
||||
"punctuation",
|
||||
"important",
|
||||
"comment",
|
||||
"tag",
|
||||
"attr-name",
|
||||
"attr-value",
|
||||
"namespace",
|
||||
"prolog",
|
||||
"doctype",
|
||||
"cdata",
|
||||
"entity",
|
||||
"atrule",
|
||||
"selector",
|
||||
"inserted",
|
||||
"deleted",
|
||||
]) {
|
||||
code.className = `token ${token}`;
|
||||
overrideStyle({
|
||||
target: code,
|
||||
selector: `.notion-code-block .token.${token}`,
|
||||
property: "color",
|
||||
variable: `code-${token.replace(/-/g, "_")}`,
|
||||
variableAliases: aliases,
|
||||
specificity: ["mode"],
|
||||
});
|
||||
}
|
||||
|
||||
// patch: remove individual backgrounds from prism tokens
|
||||
cssBody += `.token:is(
|
||||
.operator, .entity, .url,
|
||||
:is(.language-css, .style) .string
|
||||
) { background: transparent !important; }`;
|
||||
};
|
||||
|
||||
styleText();
|
||||
styleBorders();
|
||||
styleColoredText();
|
||||
styleBackgrounds();
|
||||
styleColoredBackgrounds();
|
||||
styleTooltips();
|
||||
styleAccents();
|
||||
styleScrollbars();
|
||||
styleCode();
|
||||
|
||||
console.log(
|
||||
`body${modeSelector} { ${cssRoot} } ${Object.entries(cssRefs)
|
||||
.map(([body, selectors]) => `${[...new Set(selectors)].join(",")}{${body}}`)
|
||||
.join("")} ${cssBody}`.replace(/\s+/g, " ")
|
||||
);
|
76
scripts/patch-desktop-app.mjs
Executable file
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
// patch scripts within notion's sources to
|
||||
// activate and respond to the notion-enhancer
|
||||
const injectTriggerOnce = (file, content) =>
|
||||
content +
|
||||
(!/require\(['|"]notion-enhancer['|"]\)/.test(content)
|
||||
? `\n\nrequire("notion-enhancer")("${file}",exports,(js)=>eval(js));`
|
||||
: ""),
|
||||
replaceIfNotFound = ({ string, mode = "replace" }, search, replacement) =>
|
||||
string.includes(replacement)
|
||||
? string
|
||||
: string.replace(
|
||||
search,
|
||||
typeof replacement === "string" && mode === "append"
|
||||
? `$&${replacement}`
|
||||
: typeof replacement === "string" && mode === "prepend"
|
||||
? `${replacement}$&`
|
||||
: replacement
|
||||
);
|
||||
|
||||
const patches = {
|
||||
// prettier-ignore
|
||||
".webpack/main/index.js": (file, content) => {
|
||||
content = injectTriggerOnce(file, content);
|
||||
const replace = (...args) =>
|
||||
(content = replaceIfNotFound(
|
||||
{ string: content, mode: "replace" },
|
||||
...args
|
||||
)),
|
||||
prepend = (...args) =>
|
||||
(content = replaceIfNotFound(
|
||||
{ string: content, mode: "prepend" },
|
||||
...args
|
||||
)),
|
||||
append = (...args) =>
|
||||
(content = replaceIfNotFound(
|
||||
{ string: content, mode: "append" },
|
||||
...args
|
||||
));
|
||||
|
||||
// https://github.com/notion-enhancer/notion-enhancer/issues/160:
|
||||
// run the app in windows mode on linux (instead of macos mode)
|
||||
const isWindows =
|
||||
/(?:"win32"===process\.platform(?:(?=,isFullscreen)|(?=&&\w\.BrowserWindow)|(?=&&\(\w\.app\.requestSingleInstanceLock)))/g,
|
||||
isWindowsOrLinux = '["win32","linux"].includes(process.platform)';
|
||||
replace(isWindows, isWindowsOrLinux);
|
||||
|
||||
// restore node integration in the renderer process
|
||||
// so the notion-enhancer can be require()-d into it
|
||||
replace(/sandbox:!0/g, `sandbox:!1,nodeIntegration:!0,session:require('electron').session.fromPartition("persist:notion")`);
|
||||
|
||||
// expose the app's config + cache + preferences to the global namespace
|
||||
// e.g. to enable development mode or check if keep in background is enabled
|
||||
prepend(/\w\.exports=JSON\.parse\('\{"env":"production"/, "globalThis.__notionConfig=");
|
||||
prepend(/\w\.updatePreferences=\w\.updatePreferences/, "globalThis.__updatePreferences=");
|
||||
prepend(/\w\.Store=\(0,\w\.configureStore\)/, "globalThis.__notionStore=");
|
||||
|
||||
return content;
|
||||
},
|
||||
".webpack/renderer/tabs/preload.js": injectTriggerOnce,
|
||||
".webpack/renderer/tab_browser_view/preload.js": injectTriggerOnce,
|
||||
};
|
||||
|
||||
const decoder = new TextDecoder(),
|
||||
encoder = new TextEncoder();
|
||||
export default (file, content) => {
|
||||
if (!patches[file]) return content;
|
||||
content = decoder.decode(content);
|
||||
content = patches[file](file, content);
|
||||
return encoder.encode(content);
|
||||
};
|
55
scripts/vendor-dependencies.mjs
Normal file
@ -0,0 +1,55 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import fsp from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const esmVersion = "135",
|
||||
esTarget = "es2022",
|
||||
esmBundle = ({ name, version, path = "", exports = [] }) => {
|
||||
const scopedName = name;
|
||||
if (name.startsWith("@")) name = name.split("/")[1];
|
||||
path ||= `${name}.bundle.mjs`;
|
||||
let bundleSrc = `https://esm.sh/v${esmVersion}/${scopedName}@${version}/${esTarget}/${path}`;
|
||||
if (exports.length) bundleSrc += `?bundle&exports=${exports.join()}`;
|
||||
return { [`${scopedName.replace(/\//g, "-")}.mjs`]: bundleSrc };
|
||||
};
|
||||
|
||||
const uno = "0.59.4",
|
||||
coloris = "https://cdn.jsdelivr.net/gh/mdbassit/coloris@v0.24.0/dist",
|
||||
dependencies = {
|
||||
...esmBundle({ name: "htm", version: "3.1.1" }),
|
||||
...esmBundle({
|
||||
name: "lucide",
|
||||
version: "0.372.0",
|
||||
path: "dist/umd/lucide.mjs",
|
||||
}),
|
||||
...esmBundle({
|
||||
name: "@unocss/core",
|
||||
version: uno,
|
||||
exports: ["createGenerator", "expandVariantGroup"],
|
||||
}),
|
||||
...esmBundle({
|
||||
name: "@unocss/preset-uno",
|
||||
version: uno,
|
||||
exports: ["presetUno"],
|
||||
}),
|
||||
"@unocss-preflight-tailwind.css": `https://esm.sh/@unocss/reset@${uno}/tailwind.css`,
|
||||
"coloris.min.js": `${coloris}/coloris.min.js`,
|
||||
"coloris.min.css": `${coloris}/coloris.min.css`,
|
||||
};
|
||||
|
||||
const output = fileURLToPath(new URL("../src/vendor", import.meta.url)),
|
||||
write = (file, data) => fsp.writeFile(resolve(`${output}/${file}`), data);
|
||||
if (existsSync(output)) await fsp.rm(output, { recursive: true });
|
||||
await fsp.mkdir(output);
|
||||
for (const file in dependencies) {
|
||||
const source = dependencies[file],
|
||||
res = await (await fetch(source)).text();
|
||||
await write(file, res);
|
||||
}
|
307
src/api/interface.mjs
Normal file
@ -0,0 +1,307 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import htm from "../vendor/htm.mjs";
|
||||
import lucide from "../vendor/lucide.mjs";
|
||||
import {
|
||||
createGenerator,
|
||||
expandVariantGroup,
|
||||
} from "../vendor/@unocss-core.mjs";
|
||||
import { presetUno } from "../vendor/@unocss-preset-uno.mjs";
|
||||
import "../assets/icons.svg.js";
|
||||
|
||||
// prettier-ignore
|
||||
// https://developer.mozilla.org/en-US/docs/Web/SVG/Element
|
||||
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
|
||||
const svgElements = ["animate","animateMotion","animateTransform","circle","clipPath","defs","desc","discard","ellipse","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence","filter","foreignObject","g","hatch","hatchpath","image","line","linearGradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialGradient","rect","script","set","stop","style","svg","switch","symbol","text","textPath","title","tspan","use","view"],
|
||||
htmlAttributes = ["accept","accept-charset","accesskey","action","align","allow","alt","async","autocapitalize","autocomplete","autofocus","autoplay","background","bgcolor","border","buffered","capture","challenge","charset","checked","cite","class","code","codebase","color","cols","colspan","content","contenteditable","contextmenu","controls","coords","crossorigin","csp","data","data-*","datetime","decoding","default","defer","dir","dirname","disabled","download","draggable","enctype","enterkeyhint","for","form","formaction","formenctype","formmethod","formnovalidate","formtarget","headers","height","hidden","high","href","hreflang","http-equiv","icon","id","importance","integrity","inputmode","ismap","itemprop","keytype","kind","label","lang","loading","list","loop","low","max","maxlength","minlength","media","method","min","multiple","muted","name","novalidate","open","optimum","pattern","ping","placeholder","playsinline","poster","preload","radiogroup","readonly","referrerpolicy","rel","required","reversed","role","rows","rowspan","sandbox","scope","selected","shape","size","sizes","slot","span","spellcheck","src","srcdoc","srclang","srcset","start","step","style","tabindex","target","title","translate","type","usemap","value","width","wrap","accent-height","accumulate","additive","alignment-baseline","alphabetic","amplitude","arabic-form","ascent","attributeName","attributeType","azimuth","baseFrequency","baseline-shift","baseProfile","bbox","begin","bias","by","calcMode","cap-height","clip","clipPathUnits","clip-path","clip-rule","color-interpolation","color-interpolation-filters","color-profile","color-rendering","contentScriptType","contentStyleType","cursor","cx","cy","d","decelerate","descent","diffuseConstant","direction","display","divisor","dominant-baseline","dur","dx","dy","edgeMode","elevation","enable-background","end","exponent","fill","fill-opacity","fill-rule","filter","filterRes","filterUnits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","format","from","fr","fx","fy","g1","g2","glyph-name","glyph-orientation-horizontal","glyph-orientation-vertical","glyphRef","gradientTransform","gradientUnits","hanging","horiz-adv-x","horiz-origin-x","ideographic","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kernelMatrix","kernelUnitLength","kerning","keyPoints","keySplines","keyTimes","lengthAdjust","letter-spacing","lighting-color","limitingConeAngle","local","marker-end","marker-mid","marker-start","markerHeight","markerUnits","markerWidth","mask","maskContentUnits","maskUnits","mathematical","mode","numOctaves","offset","opacity","operator","order","orient","orientation","origin","overflow","overline-position","overline-thickness","panose-1","paint-order","path","pathLength","patternContentUnits","patternTransform","patternUnits","pointer-events","points","pointsAtX","pointsAtY","pointsAtZ","preserveAlpha","preserveAspectRatio","primitiveUnits","r","radius","referrerPolicy","refX","refY","rendering-intent","repeatCount","repeatDur","requiredExtensions","requiredFeatures","restart","result","rotate","rx","ry","scale","seed","shape-rendering","slope","spacing","specularConstant","specularExponent","speed","spreadMethod","startOffset","stdDeviation","stemh","stemv","stitchTiles","stop-color","stop-opacity","strikethrough-position","strikethrough-thickness","string","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","surfaceScale","systemLanguage","tableValues","targetX","targetY","text-anchor","text-decoration","text-rendering","textLength","to","transform","transform-origin","u1","u2","underline-position","underline-thickness","unicode","unicode-bidi","unicode-range","units-per-em","v-alphabetic","v-hanging","v-ideographic","v-mathematical","values","vector-effect","version","vert-adv-y","vert-origin-x","vert-origin-y","viewBox","viewTarget","visibility","widths","word-spacing","writing-mode","x","x-height","x1","x2","xChannelSelector","xlink:actuate","xlink:arcrole","xlink:href","xlink:role","xlink:show","xlink:title","xlink:type","xml:base","xml:lang","xml:space","y","y1","y2","yChannelSelector","z","zoomAndPan"];
|
||||
|
||||
// accelerators approximately match electron accelerators.
|
||||
// logic used when recording hotkeys in menu matches logic used
|
||||
// when triggering hotkeys => detection should be reliable.
|
||||
// default hotkeys using "alt" may trigger an altcode or
|
||||
// accented character on some keyboard layouts (not recommended).
|
||||
let keyListeners = [];
|
||||
const modifierAliases = [
|
||||
["metaKey", ["meta", "os", "win", "cmd", "command"]],
|
||||
["ctrlKey", ["ctrl", "control"]],
|
||||
["shiftKey", ["shift"]],
|
||||
["altKey", ["alt"]],
|
||||
],
|
||||
addKeyListener = (accelerator, callback, waitForKeyup = false) => {
|
||||
if (typeof accelerator === "string") accelerator = accelerator.split("+");
|
||||
accelerator = accelerator.map((key) => key.toLowerCase());
|
||||
keyListeners.push([accelerator, callback, waitForKeyup]);
|
||||
},
|
||||
removeKeyListener = (callback) => {
|
||||
keyListeners = keyListeners.filter(([, c]) => c !== callback);
|
||||
},
|
||||
handleKeypress = (event, keyListeners) => {
|
||||
for (const [accelerator, callback] of keyListeners) {
|
||||
const acceleratorModifiers = [],
|
||||
combinationTriggered =
|
||||
accelerator.every((key) => {
|
||||
for (const [modifier, aliases] of modifierAliases) {
|
||||
if (aliases.includes(key)) {
|
||||
acceleratorModifiers.push(modifier);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (key === "space") key = " ";
|
||||
if (key === "plus") key = "equal";
|
||||
if (key === "minus") key = "-";
|
||||
if (key === "\\") key = "backslash";
|
||||
if (key === ",") key = "comma";
|
||||
if (key === ".") key = "period";
|
||||
const keyPressed = [
|
||||
event.key.toLowerCase(),
|
||||
event.code.toLowerCase(),
|
||||
].includes(key);
|
||||
return keyPressed;
|
||||
}) &&
|
||||
modifierAliases.every(([modifier]) => {
|
||||
// required && used -> matches accelerator
|
||||
// !required && !used -> matches accelerator
|
||||
// (required && !used) || (!required && used) -> no match
|
||||
// differentiates e.g.ctrl + x from ctrl + shift + x
|
||||
return acceleratorModifiers.includes(modifier) === event[modifier];
|
||||
});
|
||||
if (combinationTriggered) callback(event);
|
||||
}
|
||||
},
|
||||
onKeyup = (event) => {
|
||||
const keyupListeners = keyListeners //
|
||||
.filter(([, , waitForKeyup]) => waitForKeyup);
|
||||
handleKeypress(event, keyupListeners);
|
||||
},
|
||||
onKeydown = (event) => {
|
||||
const keydownListeners = keyListeners //
|
||||
.filter(([, , waitForKeyup]) => !waitForKeyup);
|
||||
handleKeypress(event, keydownListeners);
|
||||
};
|
||||
document.removeEventListener("keyup", onKeyup);
|
||||
document.removeEventListener("keydown", onKeydown);
|
||||
document.addEventListener("keyup", onKeyup);
|
||||
document.addEventListener("keydown", onKeydown);
|
||||
|
||||
// mutation listeners observe updates to the dom.
|
||||
// by default, the criteria for matching a selector
|
||||
// is very broad. custom opts can be passed when
|
||||
// adding a listener to reduce handler calls
|
||||
let documentObserver,
|
||||
observerDefaults = {
|
||||
// whether to observe attribute updates
|
||||
attributes: true,
|
||||
// whether to observe innerText updates
|
||||
characterData: true,
|
||||
// whether to observe added/removed nodes
|
||||
childList: true,
|
||||
// whether to observe nested nodes
|
||||
subtree: true,
|
||||
},
|
||||
mutationListeners = [];
|
||||
const _mutations = [],
|
||||
addMutationListener = (selector, callback, opts) => {
|
||||
opts = { ...observerDefaults, ...opts };
|
||||
mutationListeners.push([selector, callback, opts]);
|
||||
},
|
||||
removeMutationListener = (callback) => {
|
||||
mutationListeners = mutationListeners.filter(([, c]) => c !== callback);
|
||||
},
|
||||
selectorMutated = (mutation, selector, opts) => {
|
||||
if (!opts.attributes && mutation.type === "attributes") return false;
|
||||
if (!opts.characterData && mutation.type === "characterData") return false;
|
||||
const target =
|
||||
mutation.type === "characterData"
|
||||
? mutation.target.parentElement
|
||||
: mutation.target;
|
||||
if (!target) return false;
|
||||
const matchesTarget = target.matches(selector),
|
||||
matchesParent = opts.subtree && target.matches(`${selector} *`),
|
||||
matchesChild = opts.subtree && target.querySelector(selector),
|
||||
matchesAdded =
|
||||
opts.childList &&
|
||||
[...(mutation.addedNodes || [])].some((node) => {
|
||||
if (!(node instanceof HTMLElement)) node = node.parentElement;
|
||||
return node?.querySelector(selector);
|
||||
});
|
||||
return matchesTarget || matchesParent || matchesChild || matchesAdded;
|
||||
},
|
||||
handleMutations = () => {
|
||||
let mutation;
|
||||
while ((mutation = _mutations.shift())) {
|
||||
for (const [selector, callback, subtree] of mutationListeners)
|
||||
if (selectorMutated(mutation, selector, subtree)) callback(mutation);
|
||||
}
|
||||
},
|
||||
attachObserver = () => {
|
||||
if (document.readyState !== "complete") return;
|
||||
document.removeEventListener("readystatechange", attachObserver);
|
||||
(documentObserver ??= new MutationObserver((mutations, _observer) => {
|
||||
if (!_mutations.length) requestIdleCallback(handleMutations);
|
||||
_mutations.push(...mutations);
|
||||
})).disconnect();
|
||||
documentObserver.observe(document.body, observerDefaults);
|
||||
};
|
||||
document.addEventListener("readystatechange", attachObserver);
|
||||
attachObserver();
|
||||
|
||||
const kebabToPascalCase = (string) =>
|
||||
string[0].toUpperCase() +
|
||||
string.replace(/-[a-z]/g, (match) => match.slice(1).toUpperCase()).slice(1),
|
||||
hToString = (type, props, ...children) =>
|
||||
`<${type}${Object.entries(props)
|
||||
.map(([attr, value]) => ` ${attr}="${value}"`)
|
||||
.join("")}>${children
|
||||
.map((child) => (Array.isArray(child) ? hToString(...child) : child))
|
||||
.join("")}</${type}>`,
|
||||
// combines instance-provided element props
|
||||
// with a template of element props such that
|
||||
// island/component/template props handlers
|
||||
// and styles can be preserved and extended
|
||||
// rather than overwritten
|
||||
extendProps = (props, extend) => {
|
||||
for (const key in extend) {
|
||||
const { [key]: value } = props;
|
||||
if (typeof extend[key] === "function") {
|
||||
props[key] = (...args) => {
|
||||
extend[key](...args);
|
||||
if (typeof value === "function") value(...args);
|
||||
};
|
||||
} else if (key === "class") {
|
||||
props[key] = value ? `${value} ${extend[key]}` : extend[key];
|
||||
} else props[key] = extend[key] ?? value;
|
||||
}
|
||||
return props;
|
||||
},
|
||||
// enables use of the jsx-like htm syntax
|
||||
// for building components and interfaces
|
||||
// with tagged templates. instantiates dom
|
||||
// elements directly, does not use a vdom.
|
||||
// e.g. html`<div class=${className}></div>`
|
||||
h = function (type, props, ...children) {
|
||||
// disables element caching
|
||||
this[0] = 3;
|
||||
children = children.flat(Infinity);
|
||||
if (typeof type === "function") {
|
||||
// html`<${Component} attr="value">Click Me<//>`
|
||||
return type(props ?? {}, ...children);
|
||||
}
|
||||
const elem = svgElements.includes(type)
|
||||
? document.createElementNS("http://www.w3.org/2000/svg", type)
|
||||
: document.createElement(type);
|
||||
for (const prop in props ?? {}) {
|
||||
if (typeof props[prop] === "undefined") continue;
|
||||
if (["class", "className"].includes(prop)) {
|
||||
// collapse multiline classes &
|
||||
// expand utility variant class groups
|
||||
props[prop] = props[prop].replace(/\s+/g, " ");
|
||||
props[prop] = expandVariantGroup(props[prop]).trim();
|
||||
elem.setAttribute("un-cloak", "");
|
||||
}
|
||||
if (htmlAttributes.includes(prop) || prop.includes("-")) {
|
||||
if (typeof props[prop] === "boolean") {
|
||||
if (!props[prop]) continue;
|
||||
elem.setAttribute(prop, "");
|
||||
} else elem.setAttribute(prop, props[prop]);
|
||||
} else elem[prop] = props[prop];
|
||||
}
|
||||
if (type === "style") {
|
||||
elem.append(children.join("").replace(/\s+/g, " "));
|
||||
} else elem.append(...children);
|
||||
return elem;
|
||||
},
|
||||
html = htm.bind(h);
|
||||
|
||||
const iconPattern = /^i-((?:\w|-)+)(?:\?(mask|bg|auto))?$/,
|
||||
svgToUri = (svg) => {
|
||||
// https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0
|
||||
const xlmns = ~svg.indexOf("xmlns")
|
||||
? "<svg"
|
||||
: '<svg xmlns="http://www.w3.org/2000/svg"';
|
||||
return `url("data:image/svg+xml;utf8,${svg
|
||||
.replace("<svg", xlmns)
|
||||
.replace(/"/g, "'")
|
||||
.replace(/%/g, "%25")
|
||||
.replace(/#/g, "%23")
|
||||
.replace(/{/g, "%7B")
|
||||
.replace(/}/g, "%7D")
|
||||
.replace(/</g, "%3C")
|
||||
.replace(/>/g, "%3E")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()}")`;
|
||||
},
|
||||
// prefer custom preset over @unocss/preset-icons:
|
||||
// limits icons to single set, avoids loading over
|
||||
// cdn (otherwise could cause issues when submitting
|
||||
// to the chrome webstore). also makes custom icon
|
||||
// handling straightforward
|
||||
presetIcons = ([, icon, mode]) => {
|
||||
let svg,
|
||||
mask = mode === "mask";
|
||||
if (icon === "notion-enhancer") {
|
||||
const { iconColour, iconMonochrome } = globalThis.__enhancerApi;
|
||||
svg = mask ? iconMonochrome : iconColour;
|
||||
} else {
|
||||
icon = kebabToPascalCase(icon);
|
||||
if (!lucide[icon]) return;
|
||||
const [type, props, children] = lucide[icon];
|
||||
svg = hToString(type, props, ...children);
|
||||
}
|
||||
mask ||= mode !== "bg" && svg.includes("currentColor");
|
||||
return {
|
||||
// https://antfu.me/posts/icons-in-pure-css
|
||||
display: "inline-block",
|
||||
height: "1em",
|
||||
width: "1em",
|
||||
[mask ? "mask" : "background"]: `${svgToUri(svg)} no-repeat`,
|
||||
[mask ? "mask-size" : "background-size"]: "100% 100%",
|
||||
"background-color": mask ? "currentColor" : "transparent",
|
||||
};
|
||||
};
|
||||
|
||||
let _renderedTokens = -1;
|
||||
const _tokens = new Set(),
|
||||
_stylesheet = html`<style id="__unocss"></style>`,
|
||||
preflight = `[un-cloak]{display:none!important}
|
||||
.notion-emoji{display:inline-block!important}`,
|
||||
uno = createGenerator({
|
||||
presets: [presetUno()],
|
||||
preflights: [{ getCSS: () => preflight }],
|
||||
rules: [[iconPattern, presetIcons, { layer: "icons" }]],
|
||||
layers: { preflights: -2, icons: -1, default: 1 },
|
||||
}),
|
||||
extractTokens = ($root) => {
|
||||
if (!$root?.classList) return;
|
||||
for (const t of $root.classList) _tokens.add(t);
|
||||
for (const $ of $root.children) extractTokens($);
|
||||
$root.removeAttribute("un-cloak");
|
||||
},
|
||||
renderStylesheet = async () => {
|
||||
if (_renderedTokens === _tokens.size) return;
|
||||
_renderedTokens = _tokens.size;
|
||||
const res = await uno.generate(_tokens);
|
||||
if (!document.contains(_stylesheet)) document.head.append(_stylesheet);
|
||||
if (_stylesheet.innerHTML !== res.css) _stylesheet.innerHTML = res.css;
|
||||
};
|
||||
addMutationListener("*", (mutation) => {
|
||||
if (mutation.type === "childList") {
|
||||
for (const node of mutation.addedNodes) extractTokens(node);
|
||||
} else if (mutation.type === "attributes") extractTokens(mutation.target);
|
||||
else return;
|
||||
renderStylesheet();
|
||||
});
|
||||
renderStylesheet();
|
||||
|
||||
Object.assign((globalThis.__enhancerApi ??= {}), {
|
||||
html,
|
||||
extendProps,
|
||||
addKeyListener,
|
||||
removeKeyListener,
|
||||
addMutationListener,
|
||||
removeMutationListener,
|
||||
});
|
367
src/api/notion.js
Normal file
@ -0,0 +1,367 @@
|
||||
/**
|
||||
* notion-enhancer: api
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* a basic wrapper around notion's content apis
|
||||
* @namespace notion
|
||||
*/
|
||||
|
||||
import { web, fs, fmt } from './index.mjs';
|
||||
|
||||
const standardiseUUID = (uuid) => {
|
||||
if (uuid?.length === 32 && !uuid.includes('-')) {
|
||||
uuid = uuid.replace(
|
||||
/([\d\w]{8})([\d\w]{4})([\d\w]{4})([\d\w]{4})([\d\w]{12})/,
|
||||
'$1-$2-$3-$4-$5'
|
||||
);
|
||||
}
|
||||
return uuid;
|
||||
};
|
||||
|
||||
/**
|
||||
* unofficial content api: get a block by id
|
||||
* (requires user to be signed in or content to be public).
|
||||
* why not use the official api?
|
||||
* 1. cors blocking prevents use on the client
|
||||
* 2. the majority of blocks are still 'unsupported'
|
||||
* @param {string} id - uuidv4 record id
|
||||
* @param {string=} table - record type (default: 'block').
|
||||
* may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
|
||||
* @returns {Promise<object>} record data. type definitions can be found here:
|
||||
* https://github.com/NotionX/react-notion-x/tree/master/packages/notion-types/src
|
||||
*/
|
||||
export const get = async (id, table = 'block') => {
|
||||
id = standardiseUUID(id);
|
||||
const json = await fs.getJSON('https://www.notion.so/api/v3/getRecordValues', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ requests: [{ table, id }] }),
|
||||
method: 'POST',
|
||||
});
|
||||
return json?.results?.[0]?.value || json;
|
||||
};
|
||||
|
||||
/**
|
||||
* get the id of the current user (requires user to be signed in)
|
||||
* @returns {string} uuidv4 user id
|
||||
*/
|
||||
export const getUserID = () =>
|
||||
JSON.parse(localStorage['LRU:KeyValueStore2:current-user-id'] || {}).value;
|
||||
|
||||
/**
|
||||
* get the id of the currently open page
|
||||
* @returns {string} uuidv4 page id
|
||||
*/
|
||||
export const getPageID = () =>
|
||||
standardiseUUID(
|
||||
web.queryParams().get('p') || location.pathname.split(/(-|\/)/g).reverse()[0]
|
||||
);
|
||||
|
||||
let _spaceID;
|
||||
/**
|
||||
* get the id of the currently open workspace (requires user to be signed in)
|
||||
* @returns {string} uuidv4 space id
|
||||
*/
|
||||
export const getSpaceID = async () => {
|
||||
if (!_spaceID) _spaceID = (await get(getPageID())).space_id;
|
||||
return _spaceID;
|
||||
};
|
||||
|
||||
/**
|
||||
* unofficial content api: search all blocks in a space
|
||||
* (requires user to be signed in or content to be public).
|
||||
* why not use the official api?
|
||||
* 1. cors blocking prevents use on the client
|
||||
* 2. the majority of blocks are still 'unsupported'
|
||||
* @param {string=} query - query to search blocks in the space for
|
||||
* @param {number=} limit - the max number of results to return (default: 20)
|
||||
* @param {string=} spaceID - uuidv4 workspace id
|
||||
* @returns {object} the number of total results, the list of matches, and related record values.
|
||||
* type definitions can be found here: https://github.com/NotionX/react-notion-x/blob/master/packages/notion-types/src/api.ts
|
||||
*/
|
||||
export const search = async (query = '', limit = 20, spaceID = getSpaceID()) => {
|
||||
spaceID = standardiseUUID(await spaceID);
|
||||
const json = await fs.getJSON('https://www.notion.so/api/v3/search', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: 'BlocksInSpace',
|
||||
query,
|
||||
spaceId: spaceID,
|
||||
limit,
|
||||
filters: {
|
||||
isDeletedOnly: false,
|
||||
excludeTemplates: false,
|
||||
isNavigableOnly: false,
|
||||
requireEditPermissions: false,
|
||||
ancestors: [],
|
||||
createdBy: [],
|
||||
editedBy: [],
|
||||
lastEditedTime: {},
|
||||
createdTime: {},
|
||||
},
|
||||
sort: 'Relevance',
|
||||
source: 'quick_find',
|
||||
}),
|
||||
method: 'POST',
|
||||
});
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
* unofficial content api: update a property/the content of an existing record
|
||||
* (requires user to be signed in or content to be public).
|
||||
* TEST THIS THOROUGHLY. misuse can corrupt a record, leading the notion client
|
||||
* to be unable to parse and render content properly and throw errors.
|
||||
* why not use the official api?
|
||||
* 1. cors blocking prevents use on the client
|
||||
* 2. the majority of blocks are still 'unsupported'
|
||||
* @param {object} pointer - the record being updated
|
||||
* @param {object} recordValue - the new raw data values to set to the record.
|
||||
* for examples, use notion.get to fetch an existing block record.
|
||||
* to use this to update content, set pointer.path to ['properties', 'title]
|
||||
* and recordValue to an array of rich text segments. a segment is an array
|
||||
* where the first value is the displayed text and the second value
|
||||
* is an array of decorations. a decoration is an array where the first value
|
||||
* is a modifier and the second value specifies it. e.g.
|
||||
* [
|
||||
* ['bold text', [['b']]],
|
||||
* [' '],
|
||||
* ['an italicised link', [['i'], ['a', 'https://github.com']]],
|
||||
* [' '],
|
||||
* ['highlighted text', [['h', 'pink_background']]],
|
||||
* ]
|
||||
* more examples can be creating a block with the desired content/formatting,
|
||||
* then find the value of blockRecord.properties.title using notion.get.
|
||||
* type definitions can be found here: https://github.com/NotionX/react-notion-x/blob/master/packages/notion-types/src/core.ts
|
||||
* @param {string} pointer.recordID - uuidv4 record id
|
||||
* @param {string=} pointer.recordTable - record type (default: 'block').
|
||||
* may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
|
||||
* @param {string=} pointer.property - the record property to update.
|
||||
* for record content, it will be the default: 'title'.
|
||||
* for page properties, it will be the property id (the key used in pageRecord.properties).
|
||||
* other possible values are unknown/untested
|
||||
* @param {string=} pointer.spaceID - uuidv4 workspace id
|
||||
* @param {string=} pointer.path - the path to the key to be set within the record
|
||||
* (default: [], the root of the record's values)
|
||||
* @returns {boolean|object} true if success, else an error object
|
||||
*/
|
||||
export const set = async (
|
||||
{ recordID, recordTable = 'block', spaceID = getSpaceID(), path = [] },
|
||||
recordValue = {}
|
||||
) => {
|
||||
spaceID = standardiseUUID(await spaceID);
|
||||
recordID = standardiseUUID(recordID);
|
||||
const json = await fs.getJSON('https://www.notion.so/api/v3/saveTransactions', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
requestId: fmt.uuidv4(),
|
||||
transactions: [
|
||||
{
|
||||
id: fmt.uuidv4(),
|
||||
spaceId: spaceID,
|
||||
operations: [
|
||||
{
|
||||
pointer: {
|
||||
table: recordTable,
|
||||
id: recordID,
|
||||
spaceId: spaceID,
|
||||
},
|
||||
path,
|
||||
command: path.length ? 'set' : 'update',
|
||||
args: recordValue,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
method: 'POST',
|
||||
});
|
||||
return json.errorId ? json : true;
|
||||
};
|
||||
|
||||
/**
|
||||
* unofficial content api: create and add a new block to a page
|
||||
* (requires user to be signed in or content to be public).
|
||||
* TEST THIS THOROUGHLY. misuse can corrupt a record, leading the notion client
|
||||
* to be unable to parse and render content properly and throw errors.
|
||||
* why not use the official api?
|
||||
* 1. cors blocking prevents use on the client
|
||||
* 2. the majority of blocks are still 'unsupported'
|
||||
* @param {object} insert - the new record.
|
||||
* @param {object} pointer - where to insert the new block
|
||||
* for examples, use notion.get to fetch an existing block record.
|
||||
* type definitions can be found here: https://github.com/NotionX/react-notion-x/blob/master/packages/notion-types/src/block.ts
|
||||
* may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
|
||||
* @param {object=} insert.recordValue - the new raw data values to set to the record.
|
||||
* @param {object=} insert.recordTable - record type (default: 'block').
|
||||
* may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
|
||||
* @param {string=} pointer.prepend - insert before pointer.siblingID. if false, will be appended after
|
||||
* @param {string=} pointer.siblingID - uuidv4 sibling id. if unset, the record will be
|
||||
* inserted at the end of the page start (or the start if pointer.prepend is true)
|
||||
* @param {string=} pointer.parentID - uuidv4 parent id
|
||||
* @param {string=} pointer.parentTable - parent record type (default: 'block').
|
||||
* @param {string=} pointer.spaceID - uuidv4 space id
|
||||
* @param {string=} pointer.userID - uuidv4 user id
|
||||
* instead of the end
|
||||
* @returns {string|object} error object or uuidv4 of the new record
|
||||
*/
|
||||
export const create = async (
|
||||
{ recordValue = {}, recordTable = 'block' } = {},
|
||||
{
|
||||
prepend = false,
|
||||
siblingID = undefined,
|
||||
parentID = getPageID(),
|
||||
parentTable = 'block',
|
||||
spaceID = getSpaceID(),
|
||||
userID = getUserID(),
|
||||
} = {}
|
||||
) => {
|
||||
spaceID = standardiseUUID(await spaceID);
|
||||
parentID = standardiseUUID(parentID);
|
||||
siblingID = standardiseUUID(siblingID);
|
||||
const recordID = standardiseUUID(recordValue?.id ?? fmt.uuidv4()),
|
||||
path = [],
|
||||
args = {
|
||||
type: 'text',
|
||||
id: recordID,
|
||||
version: 0,
|
||||
created_time: new Date().getTime(),
|
||||
last_edited_time: new Date().getTime(),
|
||||
parent_id: parentID,
|
||||
parent_table: parentTable,
|
||||
alive: true,
|
||||
created_by_table: 'notion_user',
|
||||
created_by_id: userID,
|
||||
last_edited_by_table: 'notion_user',
|
||||
last_edited_by_id: userID,
|
||||
space_id: spaceID,
|
||||
permissions: [{ type: 'user_permission', role: 'editor', user_id: userID }],
|
||||
};
|
||||
if (parentTable === 'space') {
|
||||
parentID = spaceID;
|
||||
args.parent_id = spaceID;
|
||||
path.push('pages');
|
||||
args.type = 'page';
|
||||
} else if (parentTable === 'collection_view') {
|
||||
path.push('page_sort');
|
||||
args.type = 'page';
|
||||
} else {
|
||||
path.push('content');
|
||||
}
|
||||
const json = await fs.getJSON('https://www.notion.so/api/v3/saveTransactions', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
requestId: fmt.uuidv4(),
|
||||
transactions: [
|
||||
{
|
||||
id: fmt.uuidv4(),
|
||||
spaceId: spaceID,
|
||||
operations: [
|
||||
{
|
||||
pointer: {
|
||||
table: parentTable,
|
||||
id: parentID,
|
||||
spaceId: spaceID,
|
||||
},
|
||||
path,
|
||||
command: prepend ? 'listBefore' : 'listAfter',
|
||||
args: {
|
||||
...(siblingID ? { after: siblingID } : {}),
|
||||
id: recordID,
|
||||
},
|
||||
},
|
||||
{
|
||||
pointer: {
|
||||
table: recordTable,
|
||||
id: recordID,
|
||||
spaceId: spaceID,
|
||||
},
|
||||
path: [],
|
||||
command: 'set',
|
||||
args: {
|
||||
...args,
|
||||
...recordValue,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
method: 'POST',
|
||||
});
|
||||
return json.errorId ? json : recordID;
|
||||
};
|
||||
|
||||
/**
|
||||
* unofficial content api: upload a file to notion's aws servers
|
||||
* (requires user to be signed in or content to be public).
|
||||
* TEST THIS THOROUGHLY. misuse can corrupt a record, leading the notion client
|
||||
* to be unable to parse and render content properly and throw errors.
|
||||
* why not use the official api?
|
||||
* 1. cors blocking prevents use on the client
|
||||
* 2. the majority of blocks are still 'unsupported'
|
||||
* @param {File} file - the file to upload
|
||||
* @param {object=} pointer - where the file should be accessible from
|
||||
* @param {string=} pointer.pageID - uuidv4 page id
|
||||
* @param {string=} pointer.spaceID - uuidv4 space id
|
||||
* @returns {string|object} error object or the url of the uploaded file
|
||||
*/
|
||||
export const upload = async (file, { pageID = getPageID(), spaceID = getSpaceID() } = {}) => {
|
||||
spaceID = standardiseUUID(await spaceID);
|
||||
pageID = standardiseUUID(pageID);
|
||||
const json = await fs.getJSON('https://www.notion.so/api/v3/getUploadFileUrl', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
bucket: 'secure',
|
||||
name: file.name,
|
||||
contentType: file.type,
|
||||
record: {
|
||||
table: 'block',
|
||||
id: pageID,
|
||||
spaceId: spaceID,
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (json.errorId) return json;
|
||||
fetch(json.signedPutUrl, {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': file.type },
|
||||
body: file,
|
||||
});
|
||||
return json.url;
|
||||
};
|
||||
|
||||
/**
|
||||
* redirect through notion to a resource's signed aws url for display outside of notion
|
||||
* (requires user to be signed in or content to be public)
|
||||
* @param src source url for file
|
||||
* @param {string} recordID uuidv4 record/block/file id
|
||||
* @param {string=} recordTable record type (default: 'block').
|
||||
* may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
|
||||
* @returns {string} url signed if necessary, else string as-is
|
||||
*/
|
||||
export const sign = (src, recordID, recordTable = 'block') => {
|
||||
if (src.startsWith('/')) src = `https://notion.so${src}`;
|
||||
if (src.includes('secure.notion-static.com')) {
|
||||
src = new URL(src);
|
||||
src = `https://www.notion.so/signed/${encodeURIComponent(
|
||||
src.origin + src.pathname
|
||||
)}?table=${recordTable}&id=${recordID}`;
|
||||
}
|
||||
return src;
|
||||
};
|
87
src/api/registry.js
Normal file
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const _isManifestValid = (modManifest) => {
|
||||
const { platform } = globalThis.__enhancerApi,
|
||||
hasRequiredFields =
|
||||
modManifest.id &&
|
||||
modManifest.name &&
|
||||
modManifest.version &&
|
||||
modManifest.description &&
|
||||
modManifest.authors,
|
||||
meetsThemeRequirements =
|
||||
!modManifest._src.startsWith("themes/") ||
|
||||
((modManifest.tags?.includes("dark") ||
|
||||
modManifest.tags?.includes("light")) &&
|
||||
modManifest.thumbnail),
|
||||
targetsCurrentPlatform =
|
||||
!modManifest.platforms || //
|
||||
modManifest.platforms.includes(platform);
|
||||
return hasRequiredFields && meetsThemeRequirements && targetsCurrentPlatform;
|
||||
};
|
||||
|
||||
let _mods;
|
||||
const getMods = async (asyncFilter) => {
|
||||
const { readJson } = globalThis.__enhancerApi;
|
||||
// prettier-ignore
|
||||
_mods ??= (await Promise.all((await readJson("registry.json")).map(async (_src) => {
|
||||
const modManifest = { ...(await readJson(`${_src}/mod.json`)), _src };
|
||||
return _isManifestValid(modManifest) ? modManifest : undefined;
|
||||
}))).filter((mod) => mod);
|
||||
// prettier-ignore
|
||||
return (await Promise.all(_mods.map(async (mod) => {
|
||||
return !asyncFilter || (await asyncFilter(mod)) ? mod : undefined;
|
||||
}))).filter((mod) => mod);
|
||||
},
|
||||
getProfile = async () => {
|
||||
const db = globalThis.__enhancerApi.initDatabase();
|
||||
let activeProfile = await db.get("activeProfile");
|
||||
activeProfile ??= (await db.get("profileIds"))?.[0];
|
||||
return activeProfile ?? "default";
|
||||
};
|
||||
|
||||
const isEnabled = async (id) => {
|
||||
const { version, initDatabase } = globalThis.__enhancerApi,
|
||||
mod = (await getMods()).find((mod) => mod.id === id);
|
||||
if (mod._src === "core") return true;
|
||||
const agreedToTerms = await initDatabase().get("agreedToTerms"),
|
||||
enabledInProfile = await initDatabase([
|
||||
await getProfile(),
|
||||
"enabledMods",
|
||||
]).get(id);
|
||||
return agreedToTerms === version && enabledInProfile;
|
||||
},
|
||||
setEnabled = async (id, enabled) => {
|
||||
return await globalThis.__enhancerApi
|
||||
.initDatabase([await getProfile(), "enabledMods"])
|
||||
.set(id, enabled);
|
||||
};
|
||||
|
||||
const modDatabase = async (id) => {
|
||||
const optionDefaults = (await getMods())
|
||||
.find((mod) => mod.id === id)
|
||||
?.options?.map?.((opt) => {
|
||||
let value = opt.value;
|
||||
value ??= opt.values?.[0]?.value;
|
||||
value ??= opt.values?.[0];
|
||||
return [opt.key, value];
|
||||
})
|
||||
?.filter?.(([, value]) => typeof value !== "undefined");
|
||||
return globalThis.__enhancerApi.initDatabase(
|
||||
[await getProfile(), id],
|
||||
Object.fromEntries(optionDefaults ?? [])
|
||||
);
|
||||
};
|
||||
|
||||
Object.assign((globalThis.__enhancerApi ??= {}), {
|
||||
getMods,
|
||||
getProfile,
|
||||
isEnabled,
|
||||
setEnabled,
|
||||
modDatabase,
|
||||
});
|
70
src/api/state.js
Normal file
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// batch event callbacks to avoid over-handling
|
||||
// and any conflicts / perf.issues that may
|
||||
// otherwise result. initial call is immediate,
|
||||
// following calls are delayed. a wait time of
|
||||
// ~200ms is recommended (the avg. human visual
|
||||
// reaction time is ~180-200ms)
|
||||
const sleep = async (ms) => {
|
||||
return new Promise((res, rej) => setTimeout(res, ms));
|
||||
},
|
||||
debounce = (callback, ms = 200) => {
|
||||
let delay, update;
|
||||
const next = () =>
|
||||
sleep(ms).then(() => {
|
||||
if (!update) return (delay = undefined);
|
||||
update(), (update = undefined);
|
||||
delay = next();
|
||||
});
|
||||
return (...args) => {
|
||||
if (delay) update = callback.bind(this, ...args);
|
||||
return delay || ((delay = next()), callback(...args));
|
||||
};
|
||||
};
|
||||
|
||||
// provides basic key/value reactivity:
|
||||
// this is shared between all active mods,
|
||||
// i.e. mods can read and update other mods'
|
||||
// reactive states. this enables interop
|
||||
// between a mod's component islands and
|
||||
// supports inter-mod communication if so
|
||||
// required. caution should be used in
|
||||
// naming keys to avoid conflicts
|
||||
const _subscribers = [],
|
||||
dumpState = () => (document.__enhancerState ??= {}),
|
||||
getKeysFromState = (keys) => keys.map((key) => dumpState()[key]),
|
||||
setState = (state) => {
|
||||
Object.assign(dumpState(), state);
|
||||
const updates = Object.keys(state);
|
||||
_subscribers
|
||||
.filter(([keys]) => updates.some((key) => keys.includes(key)))
|
||||
.forEach(([keys, callback]) => callback(getKeysFromState(keys)));
|
||||
},
|
||||
// useState(["keyA", "keyB"]) => returns [valueA, valueB]
|
||||
// useState(["keyA", "keyB"], callback) => registers callback
|
||||
// to be triggered after each update to either keyA or keyB,
|
||||
// with [valueA, valueB] passed to the callback's first arg
|
||||
useState = (keys, callback) => {
|
||||
const state = getKeysFromState(keys);
|
||||
if (callback) {
|
||||
callback = debounce(callback);
|
||||
_subscribers.push([keys, callback]);
|
||||
callback(state);
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
Object.assign((globalThis.__enhancerApi ??= {}), {
|
||||
sleep,
|
||||
debounce,
|
||||
setState,
|
||||
useState,
|
||||
dumpState,
|
||||
});
|
154
src/api/system.js
Normal file
@ -0,0 +1,154 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const IS_ELECTRON = typeof module !== "undefined",
|
||||
IS_RENDERER = IS_ELECTRON && process.type === "renderer";
|
||||
|
||||
// expected values: 'linux', 'win32', 'darwin' (== macos), 'firefox'
|
||||
// and 'chromium' (inc. chromium-based browsers like edge and brave)
|
||||
// other possible values: 'aix', 'freebsd', 'openbsd', 'sunos'
|
||||
const platform = IS_ELECTRON
|
||||
? process.platform
|
||||
: navigator.userAgent.includes("Firefox")
|
||||
? "firefox"
|
||||
: "chromium",
|
||||
// currently installed version of the notion-enhancer
|
||||
version = IS_ELECTRON
|
||||
? require("notion-enhancer/package.json").version
|
||||
: chrome.runtime.getManifest().version,
|
||||
// packages a url to access notion-enhancer assets and sources,
|
||||
// proxies via api in desktop app to bypass service worker cache
|
||||
enhancerUrl = (target = "") =>
|
||||
IS_ELECTRON
|
||||
? "https://www.notion.so/api/__notion-enhancer/" +
|
||||
target.replace(/^\//, "")
|
||||
: chrome.runtime.getURL(target),
|
||||
// require a file from the root of notion's app/ folder,
|
||||
// only available in an electron main process
|
||||
notionRequire = (target) =>
|
||||
IS_ELECTRON && !IS_RENDERER ? require(`../../../${target}`) : undefined;
|
||||
|
||||
let __port;
|
||||
const connectToPort = () => {
|
||||
if (__port) return;
|
||||
__port = chrome.runtime.connect();
|
||||
__port.onDisconnect.addListener(() => (__port = null));
|
||||
},
|
||||
onMessage = (channel, listener) => {
|
||||
// from worker to client
|
||||
if (IS_RENDERER) {
|
||||
const { ipcRenderer } = require("electron");
|
||||
ipcRenderer.on(channel, (event, message) => listener(message));
|
||||
} else if (!IS_ELECTRON) {
|
||||
const onMessage = (msg) => {
|
||||
if (msg?.channel !== channel || msg?.invocation) return;
|
||||
listener(msg.message);
|
||||
};
|
||||
connectToPort();
|
||||
__port.onMessage.addListener(onMessage);
|
||||
chrome.runtime.onMessage.addListener(onMessage);
|
||||
}
|
||||
},
|
||||
sendMessage = (channel, message) => {
|
||||
// to worker from client
|
||||
if (IS_RENDERER) {
|
||||
const { ipcRenderer } = require("electron");
|
||||
ipcRenderer.send(channel, message);
|
||||
} else if (!IS_ELECTRON) {
|
||||
connectToPort();
|
||||
__port.postMessage({ channel, message });
|
||||
}
|
||||
},
|
||||
invokeInWorker = (channel, message) => {
|
||||
// sends a payload to the worker/main
|
||||
// process and waits for a response
|
||||
if (IS_RENDERER) {
|
||||
const { ipcRenderer } = require("electron");
|
||||
return ipcRenderer.invoke(channel, message);
|
||||
} else if (!IS_ELECTRON) {
|
||||
// polyfills the electron.ipcRenderer.invoke method in
|
||||
// the browser: uses a long-lived ipc connection to
|
||||
// pass messages and handle responses asynchronously
|
||||
let fulfilled;
|
||||
connectToPort();
|
||||
const id = crypto.randomUUID();
|
||||
return new Promise((res, rej) => {
|
||||
__port.onMessage.addListener((msg) => {
|
||||
if (msg?.invocation !== id || fulfilled) return;
|
||||
fulfilled = true;
|
||||
res(msg.message);
|
||||
});
|
||||
__port.postMessage({ channel, message, invocation: id });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const readFile = (file) => {
|
||||
if (IS_ELECTRON) {
|
||||
// read directly from filesys if possible,
|
||||
// treating notion-enhancer/src as fs root
|
||||
if (!file.startsWith("http")) {
|
||||
const fsp = require("fs/promises"),
|
||||
{ resolve } = require("path");
|
||||
return fsp.readFile(resolve(`${__dirname}/../${file}`), "utf-8");
|
||||
}
|
||||
} else file = file.startsWith("http") ? file : enhancerUrl(file);
|
||||
return fetch(file).then((res) => res.text());
|
||||
},
|
||||
readJson = (file) => {
|
||||
// as above, uses require instead of readFile
|
||||
// and res.json() instead of res.text() to return
|
||||
// json content of file in object form
|
||||
if (IS_ELECTRON) {
|
||||
if (!file.startsWith("http")) {
|
||||
const { resolve } = require("path");
|
||||
return require(resolve(`${__dirname}/../${file}`));
|
||||
}
|
||||
} else file = file.startsWith("http") ? file : enhancerUrl(file);
|
||||
return fetch(file).then((res) => res.json());
|
||||
};
|
||||
|
||||
const initDatabase = (namespace, fallbacks = {}) => {
|
||||
// all db operations are performed via ipc:
|
||||
// with nodeintegration disabled, sqlite cannot
|
||||
// be require()-d from the renderer process
|
||||
const query = (query, args = {}) =>
|
||||
IS_ELECTRON && !IS_RENDERER
|
||||
? globalThis.__enhancerApi.queryDatabase(namespace, query, args)
|
||||
: invokeInWorker("notion-enhancer", {
|
||||
action: "query-database",
|
||||
data: { namespace, query, args },
|
||||
});
|
||||
return {
|
||||
get: (key) => query("get", { key, fallbacks }),
|
||||
set: (key, value) => query("set", { key, value }),
|
||||
remove: (keys) => query("remove", { keys }),
|
||||
export: () => query("export"),
|
||||
import: (obj) => query("import", { obj }),
|
||||
};
|
||||
},
|
||||
reloadApp = () => {
|
||||
if (IS_ELECTRON && !IS_RENDERER) {
|
||||
const { app } = require("electron");
|
||||
app.relaunch(), app.exit();
|
||||
} else sendMessage("notion-enhancer", "reload-app");
|
||||
};
|
||||
|
||||
Object.assign((globalThis.__enhancerApi ??= {}), {
|
||||
platform,
|
||||
version,
|
||||
enhancerUrl,
|
||||
notionRequire,
|
||||
onMessage,
|
||||
sendMessage,
|
||||
invokeInWorker,
|
||||
readFile,
|
||||
readJson,
|
||||
initDatabase,
|
||||
reloadApp,
|
||||
});
|
BIN
src/assets/colour-x128.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
src/assets/colour-x16.png
Normal file
After Width: | Height: | Size: 595 B |
BIN
src/assets/colour-x256.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
src/assets/colour-x32.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
src/assets/colour-x48.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/colour-x512.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
src/assets/colour-x64.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
13
src/assets/icons.svg.js
Normal file
157
src/core/client.mjs
Normal file
@ -0,0 +1,157 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
// telemetry endpoint not ready, disabled for current release
|
||||
|
||||
import { checkForUpdate } from "./updateCheck.mjs";
|
||||
// import { sendTelemetryPing } from "./sendTelemetry.mjs";
|
||||
import { Modal, Frame } from "./islands/Modal.mjs";
|
||||
import { MenuButton } from "./islands/MenuButton.mjs";
|
||||
import { Tooltip } from "./islands/Tooltip.mjs";
|
||||
import { Panel } from "./islands/Panel.mjs";
|
||||
|
||||
const shouldLoadThemeOverrides = async (api, db) => {
|
||||
const { getMods, isEnabled } = api,
|
||||
loadThemeOverrides = await db.get("loadThemeOverrides");
|
||||
if (loadThemeOverrides === "Enabled") return true;
|
||||
if (loadThemeOverrides === "Disabled") return false;
|
||||
// prettier-ignore
|
||||
// loadThemeOverrides === "Auto"
|
||||
return (await getMods(async (mod) => {
|
||||
if (!mod._src.startsWith("themes/")) return false;
|
||||
return await isEnabled(mod.id);
|
||||
})).length;
|
||||
},
|
||||
loadThemeOverrides = async (api, db) => {
|
||||
const { html, enhancerUrl } = api;
|
||||
if (!(await shouldLoadThemeOverrides(api, db))) return;
|
||||
document.head.append(html`<link
|
||||
rel="stylesheet"
|
||||
href=${enhancerUrl("core/theme.css")}
|
||||
/>`);
|
||||
},
|
||||
insertCustomStyles = async (api, db) => {
|
||||
const { html } = api,
|
||||
customStyles = (await db.get("customStyles"))?.content;
|
||||
if (!customStyles) return;
|
||||
const $customStyles = html`<style
|
||||
id="__custom"
|
||||
innerHTML=${customStyles}
|
||||
></style>`;
|
||||
return document.head.append($customStyles);
|
||||
};
|
||||
|
||||
const insertMenu = async (api, db) => {
|
||||
const inviteMember = `.notion-sidebar-container .notion-sidebar [role="button"]:has(.inviteMember)`,
|
||||
{ html, addMutationListener, removeMutationListener } = api,
|
||||
{ addKeyListener, platform, enhancerUrl, onMessage } = api,
|
||||
menuButtonIconStyle = await db.get("menuButtonIconStyle"),
|
||||
menuButtonLabel = await db.get("menuButtonLabel"),
|
||||
openMenuHotkey = await db.get("openMenuHotkey"),
|
||||
menuPing = {
|
||||
channel: "notion-enhancer",
|
||||
hotkey: openMenuHotkey,
|
||||
icon: menuButtonIconStyle,
|
||||
};
|
||||
|
||||
let _contentWindow;
|
||||
const updateMenuTheme = () => {
|
||||
const darkMode = document.body.classList.contains("dark"),
|
||||
notionTheme = darkMode ? "dark" : "light";
|
||||
menuPing.theme = notionTheme;
|
||||
_contentWindow?.postMessage?.(menuPing, "*");
|
||||
};
|
||||
|
||||
const $modal = html`<${Modal}>
|
||||
<${Frame}
|
||||
title="notion-enhancer menu"
|
||||
src="${enhancerUrl("core/menu/index.html")}"
|
||||
onload=${function () {
|
||||
// pass notion-enhancer api to electron menu process
|
||||
if (["linux", "win32", "darwin"].includes(platform)) {
|
||||
const apiKey = "__enhancerApi";
|
||||
this.contentWindow[apiKey] = { ...globalThis[apiKey] };
|
||||
}
|
||||
_contentWindow = this.contentWindow;
|
||||
updateMenuTheme();
|
||||
}}
|
||||
/>
|
||||
<//>`,
|
||||
$button = html`<${MenuButton}
|
||||
onclick=${$modal.open}
|
||||
notifications=${(await checkForUpdate()) ? 1 : 0}
|
||||
themeOverridesLoaded=${await shouldLoadThemeOverrides(api, db)}
|
||||
icon="notion-enhancer${menuButtonIconStyle === "Monochrome"
|
||||
? "?mask"
|
||||
: " text-[16px]"}"
|
||||
>${menuButtonLabel}
|
||||
<//>`;
|
||||
const appendToDom = () => {
|
||||
if (!document.body.contains($modal)) document.body.append($modal);
|
||||
else if (!document.body.contains($button)) {
|
||||
document.querySelector(inviteMember)?.after($button);
|
||||
} else removeMutationListener(appendToDom);
|
||||
};
|
||||
html`<${Tooltip}>
|
||||
<b>Configure the notion-enhancer and its mods</b>
|
||||
<//>`.attach($button, "right");
|
||||
addMutationListener(inviteMember, appendToDom);
|
||||
addMutationListener(".notion-app-inner", updateMenuTheme, { subtree: false });
|
||||
appendToDom();
|
||||
|
||||
addKeyListener(openMenuHotkey, (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$modal.open();
|
||||
});
|
||||
addEventListener("message", (event) => {
|
||||
// from embedded menu
|
||||
if (event.data?.channel !== "notion-enhancer") return;
|
||||
if (event.data?.action === "close-menu") $modal.close();
|
||||
if (event.data?.action === "open-menu") $modal.open();
|
||||
});
|
||||
onMessage("notion-enhancer", (message) => {
|
||||
// from worker
|
||||
if (message === "open-menu") $modal.open();
|
||||
});
|
||||
};
|
||||
|
||||
const insertPanel = async (api, db) => {
|
||||
const notionFrame = ".notion-frame",
|
||||
togglePanelHotkey = await db.get("togglePanelHotkey"),
|
||||
{ html, setState, addMutationListener, removeMutationListener } = api;
|
||||
|
||||
const $panel = html`<${Panel}
|
||||
hotkey="${togglePanelHotkey}"
|
||||
...${Object.assign(
|
||||
...["Width", "Open", "View"].map((key) => ({
|
||||
[`_get${key}`]: () => db.get(`panel${key}`),
|
||||
[`_set${key}`]: async (value) => {
|
||||
await db.set(`panel${key}`, value);
|
||||
setState({ rerender: true });
|
||||
},
|
||||
}))
|
||||
)}
|
||||
/>`,
|
||||
appendToDom = () => {
|
||||
const $frame = document.querySelector(notionFrame);
|
||||
if (!$frame) return;
|
||||
$frame.append($panel);
|
||||
$frame.style.flexDirection = "row";
|
||||
removeMutationListener(appendToDom);
|
||||
};
|
||||
addMutationListener(notionFrame, appendToDom);
|
||||
appendToDom();
|
||||
};
|
||||
|
||||
export default async (api, db) =>
|
||||
Promise.all([
|
||||
insertMenu(api, db),
|
||||
insertPanel(api, db),
|
||||
insertCustomStyles(api, db),
|
||||
loadThemeOverrides(api, db),
|
||||
// sendTelemetryPing(),
|
||||
]).then(() => api.sendMessage("notion-enhancer", "load-complete"));
|
49
src/core/electron.cjs
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const getPreference = (key) => {
|
||||
const { preferences = {} } = globalThis.__notionStore?.getState()?.app;
|
||||
return preferences[key];
|
||||
},
|
||||
setPreference = (key, value) => {
|
||||
const action = globalThis.__updatePreferences?.({ [key]: value });
|
||||
globalThis.__notionStore?.dispatch?.(action);
|
||||
};
|
||||
|
||||
module.exports = async ({}, db) => {
|
||||
const toggleWindowHotkey = await db.get("toggleWindowHotkey"),
|
||||
developerMode = await db.get("developerMode");
|
||||
|
||||
// enable developer mode, access extra debug tools
|
||||
Object.assign((globalThis.__notionConfig ??= {}), {
|
||||
env: developerMode ? "development" : "production",
|
||||
});
|
||||
|
||||
// listen for the global window toggle hotkey
|
||||
const { app, globalShortcut, BrowserWindow } = require("electron");
|
||||
app.whenReady().then(() => {
|
||||
globalShortcut.register(toggleWindowHotkey, () => {
|
||||
const windows = BrowserWindow.getAllWindows()
|
||||
// filter out quick search window
|
||||
.filter((win) => win.fullScreenable),
|
||||
focused = windows.some((win) => win.isFocused() && win.isVisible());
|
||||
windows.forEach((win) =>
|
||||
// check if notion is set to run in the background
|
||||
getPreference("isHideLastWindowOnCloseEnabled")
|
||||
? focused
|
||||
? win.hide()
|
||||
: win.show()
|
||||
: focused
|
||||
? win.minimize()
|
||||
: win.isMinimized()
|
||||
? win.restore()
|
||||
: win.focus()
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
54
src/core/islands/FloatingButton.mjs
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
let __$wrapper;
|
||||
const setupWrapper = () => {
|
||||
const notionAi = ".notion-help-button, .notion-ai-button",
|
||||
{ html, addMutationListener } = globalThis.__enhancerApi,
|
||||
{ removeMutationListener } = globalThis.__enhancerApi;
|
||||
return (__$wrapper ??= new Promise((res) => {
|
||||
const addToDom = () => {
|
||||
const $notionAi = document.querySelector(notionAi);
|
||||
if (!$notionAi) return;
|
||||
const $wrapper = html`<div
|
||||
class="notion-enhancer--floating-buttons z-50 gap-[12px]
|
||||
flex absolute bottom-[calc(16px+env(safe-area-inset-bottom))]"
|
||||
></div>`;
|
||||
removeMutationListener(addToDom);
|
||||
$notionAi.after($wrapper);
|
||||
res($wrapper);
|
||||
};
|
||||
addMutationListener(notionAi, addToDom);
|
||||
addToDom();
|
||||
}));
|
||||
},
|
||||
addFloatingButton = async ($btn) => {
|
||||
if (document.contains($btn)) return;
|
||||
(await setupWrapper()).prepend($btn);
|
||||
// button positioning is calculated by panel
|
||||
},
|
||||
removeFloatingButton = ($btn) => $btn.remove();
|
||||
|
||||
function FloatingButton({ icon, ...props }, ...children) {
|
||||
const { html, extendProps } = globalThis.__enhancerApi;
|
||||
extendProps(props, {
|
||||
tabindex: 0,
|
||||
class: `notion-enhancer--floating-button
|
||||
size-[36px] flex items-center justify-center rounded-full
|
||||
text-([20px] [color:var(--theme--fg-primary)]) select-none cursor-pointer
|
||||
bg-[color:var(--theme--bg-secondary)] hover:bg-[color:var(--theme--bg-hover)]
|
||||
shadow-[rgba(15,15,15,0.2)_0px_0px_0px_1px,rgba(15,15,15,0.2)_0px_2px_4px]`,
|
||||
});
|
||||
return html`<button ...${props}>${children}</button>`;
|
||||
}
|
||||
|
||||
if (globalThis.document) setupWrapper();
|
||||
Object.assign((globalThis.__enhancerApi ??= {}), {
|
||||
addFloatingButton,
|
||||
removeFloatingButton,
|
||||
});
|
||||
|
||||
export { addFloatingButton, FloatingButton };
|
43
src/core/islands/MenuButton.mjs
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
function MenuButton(
|
||||
{ icon, notifications, themeOverridesLoaded, ...props },
|
||||
...children
|
||||
) {
|
||||
const { html, extendProps } = globalThis.__enhancerApi;
|
||||
extendProps(props, {
|
||||
tabindex: 0,
|
||||
role: "button",
|
||||
class: `notion-enhancer--menu-button flex select-none
|
||||
cursor-pointer rounded-[6px] text-[14px] font-medium
|
||||
transition hover:bg-[color:var(--theme--bg-hover)]
|
||||
w-full h-[30px] px-[10px] py-[4px] items-center`,
|
||||
});
|
||||
return html`<div ...${props}>
|
||||
<div class="flex items-center justify-center text-[18px] mr-[10px]">
|
||||
<i class="i-${icon}"></i>
|
||||
</div>
|
||||
<div>${children}</div>
|
||||
|
||||
<div class="ml-auto my-auto${notifications > 0 ? "" : " hidden"}">
|
||||
<!-- accents are squashed into one variable for theming:
|
||||
use rgb to match notion if overrides not loaded -->
|
||||
<div
|
||||
class="flex justify-center size-[16px] font-semibold mb-[2px]
|
||||
text-([10px] [color:var(--theme--accent-secondary\\_contrast)])
|
||||
bg-[color:var(--theme--accent-secondary)] rounded-[3px]
|
||||
dark:bg-[color:${themeOverridesLoaded
|
||||
? "var(--theme--accent-secondary)"
|
||||
: "rgb(180,65,60)"}]"
|
||||
>
|
||||
<span class="ml-[-0.5px]">${notifications}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export { MenuButton };
|
67
src/core/islands/Modal.mjs
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
function Modal(props, ...children) {
|
||||
const { html, extendProps, addKeyListener } = globalThis.__enhancerApi;
|
||||
extendProps(props, {
|
||||
class: `notion-enhancer--menu-modal z-[999]
|
||||
fixed inset-0 w-screen h-screen group/modal
|
||||
transition pointer-events-none opacity-0
|
||||
open:(pointer-events-auto opacity-100)`,
|
||||
});
|
||||
const $modal = html`<div ...${props}>
|
||||
<div
|
||||
class="fixed inset-0 bg-[color:var(--theme--bg-overlay)]"
|
||||
onclick=${() => $modal.close()}
|
||||
></div>
|
||||
<div
|
||||
class="fixed inset-0 flex w-screen h-screen
|
||||
items-center justify-center pointer-events-none"
|
||||
>
|
||||
${children}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
let _openQueued;
|
||||
$modal.open = async () => {
|
||||
_openQueued = true;
|
||||
while (!document.contains($modal)) {
|
||||
if (!_openQueued) return;
|
||||
// dont trigger open until menu is in dom,
|
||||
// to ensure transition is shown when menu
|
||||
// does initially open
|
||||
await new Promise(requestAnimationFrame);
|
||||
}
|
||||
$modal.setAttribute("open", "");
|
||||
setTimeout(() => $modal.onopen?.(), 200);
|
||||
};
|
||||
$modal.close = () => {
|
||||
_openQueued = false;
|
||||
$modal.removeAttribute("open");
|
||||
if ($modal.contains(document.activeElement)) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
setTimeout(() => $modal.onclose?.(), 200);
|
||||
};
|
||||
addKeyListener("Escape", () => {
|
||||
if (document.activeElement?.nodeName === "INPUT") return;
|
||||
$modal.close();
|
||||
});
|
||||
return $modal;
|
||||
}
|
||||
|
||||
function Frame(props) {
|
||||
const { html, extendProps } = globalThis.__enhancerApi;
|
||||
extendProps(props, {
|
||||
class: `rounded-[12px] w-[1150px] h-[calc(100vh-100px)] opacity-0
|
||||
max-w-[calc(100vw-100px)] max-h-[715px] overflow-hidden scale-95
|
||||
bg-[color:var(--theme--bg-primary)] drop-shadow-xl transition
|
||||
group-open/modal:(pointer-events-auto opacity-100 scale-100)`,
|
||||
});
|
||||
return html`<iframe ...${props}></iframe>`;
|
||||
}
|
||||
|
||||
export { Modal, Frame };
|
433
src/core/islands/Panel.mjs
Normal file
@ -0,0 +1,433 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (c) 2021 CloudHill <rl.cloudhill@gmail.com> (https://github.com/CloudHill)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { Tooltip } from "./Tooltip.mjs";
|
||||
import { TopbarButton } from "./TopbarButton.mjs";
|
||||
import { Select } from "../menu/islands/Select.mjs";
|
||||
|
||||
const coreId = "0f0bf8b6-eae6-4273-b307-8fc43f2ee082",
|
||||
topbarId = "e0700ce3-a9ae-45f5-92e5-610ded0e348d",
|
||||
tweaksId = "5174a483-c88d-4bf8-a95f-35cd330b76e2";
|
||||
|
||||
// note: these islands are not reusable.
|
||||
// panel views can be added via addPanelView,
|
||||
// do not instantiate additional panels
|
||||
|
||||
let panelViews = [],
|
||||
// "$icon" may either be an actual dom element,
|
||||
// or an icon name from the lucide icons set
|
||||
addPanelView = ({ title, $icon, $view }) => {
|
||||
panelViews.push([{ title, $icon }, $view]);
|
||||
panelViews.sort(([{ title: a }], [{ title: b }]) => a.localeCompare(b));
|
||||
const { setState } = globalThis.__enhancerApi;
|
||||
setState?.({ panelViews });
|
||||
},
|
||||
removePanelView = ($view) => {
|
||||
panelViews = panelViews.filter(([, v]) => v !== $view);
|
||||
const { setState } = globalThis.__enhancerApi;
|
||||
setState?.({ panelViews });
|
||||
};
|
||||
|
||||
function View({ _get }) {
|
||||
const { html, useState } = globalThis.__enhancerApi,
|
||||
$container = html`<div
|
||||
class="overflow-(y-auto x-hidden)
|
||||
h-[calc(100%-46px)] min-w-[var(--panel--width)]"
|
||||
></div>`;
|
||||
useState(["rerender"], async () => {
|
||||
const openView = await _get?.(),
|
||||
$view =
|
||||
panelViews.find(([{ title }]) => {
|
||||
return title === openView;
|
||||
})?.[1] || panelViews[0]?.[1];
|
||||
if (!$container.contains($view)) {
|
||||
$container.innerHTML = "";
|
||||
$container.append($view);
|
||||
}
|
||||
});
|
||||
return $container;
|
||||
}
|
||||
|
||||
function Switcher({ _get, _set, minWidth, maxWidth }) {
|
||||
const { html, useState } = globalThis.__enhancerApi,
|
||||
$select = html`<${Select}
|
||||
popupMode="dropdown"
|
||||
class="w-full text-left"
|
||||
maxWidth=${maxWidth - 56}
|
||||
minWidth=${minWidth - 56}
|
||||
...${{ _get, _set }}
|
||||
/>`;
|
||||
useState(["panelViews"], ([panelViews = []]) => {
|
||||
const values = panelViews.map(([{ title, $icon }]) => {
|
||||
// panel switcher internally uses the select island,
|
||||
// which expects an option value rather than a title
|
||||
return { value: title, $icon };
|
||||
});
|
||||
$select.setValues(values);
|
||||
});
|
||||
return html`<div
|
||||
class="relative flex items-center grow
|
||||
font-medium p-[8.5px] ml-[4px] select-none"
|
||||
>
|
||||
${$select}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function Panel({
|
||||
hotkey,
|
||||
_getWidth,
|
||||
_setWidth,
|
||||
_getOpen,
|
||||
_setOpen,
|
||||
_getView,
|
||||
_setView,
|
||||
minWidth = 256,
|
||||
maxWidth = 640,
|
||||
transitionDuration = 300,
|
||||
}) {
|
||||
const { modDatabase, isEnabled } = globalThis.__enhancerApi,
|
||||
{ html, useState, addKeyListener, MODS_LOADED } = globalThis.__enhancerApi,
|
||||
{ addMutationListener, removeMutationListener } = globalThis.__enhancerApi,
|
||||
$panelToggle = html`<button
|
||||
aria-label="Toggle side panel"
|
||||
class="select-none size-[24px] duration-[20ms]
|
||||
transition inline-flex items-center justify-center mr-[10px]
|
||||
rounded-[3px] hover:bg-[color:var(--theme--bg-hover)]"
|
||||
>
|
||||
<i
|
||||
class="i-chevrons-left size-[20px]
|
||||
text-[color:var(--theme--fg-secondary)] transition-transform
|
||||
group-[&[data-pinned]]/panel:rotate-180 duration-[${transitionDuration}ms]"
|
||||
/>
|
||||
</button>`,
|
||||
$panel = html`<div
|
||||
class="notion-enhancer--panel group/panel order-2
|
||||
shrink-0 [&[data-pinned]]:w-[var(--panel--width,0)]"
|
||||
>
|
||||
<style>
|
||||
.notion-frame {
|
||||
flex-direction: row !important;
|
||||
}
|
||||
/* prevent page load skeletons overlapping with panel */
|
||||
.notion-frame [role="progressbar"] {
|
||||
padding-right: var(--panel--width);
|
||||
}
|
||||
.notion-frame [role="progressbar"] > div {
|
||||
overflow-x: clip;
|
||||
}
|
||||
</style>
|
||||
<aside
|
||||
class="border-(l-1 [color:var(--theme--fg-border)]) w-0
|
||||
group-[&[data-pinned]]/panel:(w-[var(--panel--width,0)]) h-[calc(100vh-45px)] bottom-0)
|
||||
absolute right-0 z-20 bg-[color:var(--theme--bg-primary)] group-[&[data-peeked]]/panel:(
|
||||
w-[var(--panel--width,0)] h-[calc(100vh-120px)] bottom-[60px] rounded-l-[8px] border-(t-1 b-1))"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between items-center
|
||||
border-(b [color:var(--theme--fg-border)])"
|
||||
>
|
||||
<${Switcher}
|
||||
...${{ _get: _getView, _set: _setView, minWidth, maxWidth }}
|
||||
/>
|
||||
${$panelToggle}
|
||||
</div>
|
||||
<${View} ...${{ _get: _getView }} />
|
||||
</aside>
|
||||
</div>`;
|
||||
|
||||
const notionTopbar = ".notion-topbar",
|
||||
topbarFavorite = ".notion-topbar-favorite-button",
|
||||
$topbarToggle = html`<${TopbarButton}
|
||||
aria-label="Toggle side panel"
|
||||
icon="panel-right"
|
||||
/>`,
|
||||
addToTopbar = () => {
|
||||
if (document.contains($topbarToggle)) return;
|
||||
document.querySelector(topbarFavorite)?.after($topbarToggle);
|
||||
};
|
||||
$panelToggle.onclick = $topbarToggle.onclick = () => $panel.toggle();
|
||||
addMutationListener(notionTopbar, addToTopbar, { subtree: false });
|
||||
addToTopbar();
|
||||
|
||||
isEnabled(topbarId).then(async (topbarEnabled) => {
|
||||
if (!topbarEnabled) return;
|
||||
const topbarDatabase = await modDatabase(topbarId),
|
||||
panelButton = await topbarDatabase.get("panelButton"),
|
||||
panelIcon = await topbarDatabase.get("panelIcon");
|
||||
if (panelButton === "Text") {
|
||||
$topbarToggle.innerHTML = `<span>${$topbarToggle.ariaLabel}</span>`;
|
||||
} else if (panelIcon?.content) $topbarToggle.innerHTML = panelIcon.content;
|
||||
});
|
||||
|
||||
let preDragWidth, dragStartX, _animatedAt;
|
||||
const getWidth = async (width) => {
|
||||
if (width && !isNaN(width)) {
|
||||
width = Math.max(width, minWidth);
|
||||
width = Math.min(width, maxWidth);
|
||||
} else width = await _getWidth?.();
|
||||
if (isNaN(width)) width = minWidth;
|
||||
return width;
|
||||
},
|
||||
setInteractive = (interactive) => {
|
||||
$panel
|
||||
.querySelectorAll("[tabindex]")
|
||||
.forEach(($el) => ($el.tabIndex = interactive ? 1 : -1));
|
||||
},
|
||||
isAnimated = () => {
|
||||
if (!_animatedAt) return false;
|
||||
return Date.now() - _animatedAt <= transitionDuration;
|
||||
},
|
||||
isDragging = () => !isNaN(preDragWidth) && !isNaN(dragStartX),
|
||||
isPinned = () => $panel.hasAttribute("data-pinned"),
|
||||
isPeeked = () => $panel.hasAttribute("data-peeked"),
|
||||
isClosed = () => !isPinned() && !isPeeked();
|
||||
|
||||
const closedWidth = { width: "0px" },
|
||||
openWidth = { width: "var(--panel--width, 0px)" },
|
||||
peekAnimation = {
|
||||
height: "calc(100vh - 120px)",
|
||||
bottom: "60px",
|
||||
borderTopWidth: "1px",
|
||||
borderBottomWidth: "1px",
|
||||
borderTopLeftRadius: "8px",
|
||||
borderBottomLeftRadius: "8px",
|
||||
boxShadow: document.body.classList.contains("dark")
|
||||
? "rgba(15, 15, 15, 0.1) 0px 0px 0px 1px, rgba(15, 15, 15, 0.2) 0px 3px 6px, rgba(15, 15, 15, 0.4) 0px 9px 24px"
|
||||
: "rgba(15, 15, 15, 0.05) 0px 0px 0px 1px, rgba(15, 15, 15, 0.1) 0px 3px 6px, rgba(15, 15, 15, 0.2) 0px 9px 24px",
|
||||
},
|
||||
pinAnimation = {
|
||||
height: "calc(100vh - 45px)",
|
||||
bottom: "0px",
|
||||
borderTopWidth: "0px",
|
||||
borderBottomWidth: "0px",
|
||||
borderTopLeftRadius: "0px",
|
||||
borderBottomLeftRadius: "0px",
|
||||
boxShadow: "none",
|
||||
};
|
||||
|
||||
const animationState = { ...closedWidth },
|
||||
animate = ($target, keyframes) => {
|
||||
const opts = {
|
||||
fill: "forwards",
|
||||
duration: transitionDuration,
|
||||
easing: "ease",
|
||||
};
|
||||
$target.animate(keyframes, opts);
|
||||
},
|
||||
animatePanel = (to) => {
|
||||
_animatedAt = Date.now();
|
||||
animate($panel.lastElementChild, [animationState, to]);
|
||||
Object.assign(animationState, to);
|
||||
};
|
||||
|
||||
isEnabled(tweaksId).then(async (tweaksEnabled) => {
|
||||
if (!tweaksEnabled) return;
|
||||
const tweaksDatabase = await modDatabase(tweaksId),
|
||||
snappyTransitions = await tweaksDatabase.get("snappyTransitions");
|
||||
if (snappyTransitions) transitionDuration = 0;
|
||||
});
|
||||
|
||||
// dragging the resize handle horizontally will
|
||||
// adjust the width of the panel correspondingly
|
||||
const $resizeHandle = html`<div
|
||||
class="absolute opacity-0 h-full w-[3px] left-[-2px]
|
||||
active:cursor-text bg-[color:var(--theme--fg-border)] z-20
|
||||
transition duration-300 hover:(cursor-col-resize opacity-100)
|
||||
group-[&[data-peeked]]/panel:(w-[8px] left-[-1px] rounded-l-[7px])"
|
||||
>
|
||||
<div
|
||||
class="ml-[2px] bg-[color:var(--theme--bg-primary)]
|
||||
group-[&[data-peeked]]/panel:(my-px h-[calc(100%-2px)] rounded-l-[6px])"
|
||||
></div>
|
||||
</div>`,
|
||||
startDrag = async (event) => {
|
||||
dragStartX = event.clientX;
|
||||
preDragWidth = await getWidth();
|
||||
document.addEventListener("mousemove", onDrag);
|
||||
document.addEventListener("mouseup", endDrag);
|
||||
},
|
||||
onDrag = (event) => {
|
||||
event.preventDefault();
|
||||
if (!isDragging()) return;
|
||||
$panel.resize(preDragWidth + (dragStartX - event.clientX));
|
||||
},
|
||||
endDrag = (event) => {
|
||||
document.removeEventListener("mousemove", onDrag);
|
||||
document.removeEventListener("mouseup", endDrag);
|
||||
if (!isDragging()) return;
|
||||
$panel.resize(preDragWidth + (dragStartX - event.clientX));
|
||||
// toggle panel if not resized
|
||||
if (dragStartX - event.clientX === 0) $panel.toggle();
|
||||
preDragWidth = dragStartX = undefined;
|
||||
};
|
||||
$resizeHandle.addEventListener("mousedown", startDrag);
|
||||
$panel.lastElementChild.prepend($resizeHandle);
|
||||
|
||||
// add tooltips to panel pin/unpin toggles
|
||||
const $resizeTooltipClick = html`<span></span>`,
|
||||
$resizeTooltip = html`<${Tooltip}
|
||||
onbeforeshow=${() => {
|
||||
$resizeTooltipClick.innerText = isPinned() ? "close" : "lock open";
|
||||
}}
|
||||
><b>Drag</b> to resize<br />
|
||||
<b>Click</b> to ${$resizeTooltipClick}
|
||||
<//>`,
|
||||
$toggleTooltipClick = html`<b></b>`,
|
||||
$toggleTooltip = html`<${Tooltip}
|
||||
onbeforeshow=${() => {
|
||||
$toggleTooltipClick.innerText = isPinned()
|
||||
? "Close sidebar"
|
||||
: "Lock sidebar open";
|
||||
}}
|
||||
>${$toggleTooltipClick}<br />
|
||||
${hotkey}
|
||||
<//>`;
|
||||
$resizeTooltip.attach($resizeHandle, "left");
|
||||
$toggleTooltip.attach($topbarToggle, "bottom");
|
||||
$toggleTooltip.attach($panelToggle, "bottom");
|
||||
|
||||
// hovering over the peek trigger will temporarily
|
||||
// pop out an interactive preview of the panel
|
||||
let _peekDebounce, _peekPanelOnHover;
|
||||
const $peekTrigger = html`<div
|
||||
class="absolute z-10 right-0 h-[calc(100vh-120px)] bottom-[60px] w-[96px]
|
||||
group-[&[data-peeked]]/panel:(w-[calc(var(--panel--width,0)+8px)])
|
||||
group-[&[data-pinned]]/panel:(w-[calc(var(--panel--width,0)+8px)])"
|
||||
></div>`;
|
||||
modDatabase(coreId).then(async (db) => {
|
||||
_peekPanelOnHover = await db.get("peekPanelOnHover");
|
||||
if (_peekPanelOnHover) $panel.prepend($peekTrigger);
|
||||
});
|
||||
$panel.addEventListener("mouseout", () => {
|
||||
if (isDragging() || isAnimated() || isPinned()) return;
|
||||
if (!$panel.matches(":hover")) $panel.close();
|
||||
});
|
||||
$panel.addEventListener("mouseover", () => {
|
||||
_peekDebounce ??= setTimeout(() => {
|
||||
if (isClosed() && $panel.matches(":hover")) $panel.peek();
|
||||
_peekDebounce = undefined;
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// moves ai/q&a button out of the way of open panel.
|
||||
// normally would place outside of an island, but in
|
||||
// this case is necessary for syncing up animations
|
||||
const notionAi = ".notion-help-button, .notion-ai-button",
|
||||
floatingButtons = ".notion-enhancer--floating-buttons",
|
||||
repositionCorner = async (offset) => {
|
||||
const $help = document.querySelector(notionAi),
|
||||
$floating = document.querySelector(floatingButtons);
|
||||
offset ??= await getWidth();
|
||||
if (isNaN(offset)) offset = minWidth;
|
||||
if (!isPinned()) offset = 0;
|
||||
// offset help from panel edge
|
||||
offset += 26;
|
||||
for (const $btn of [$help, $floating]) {
|
||||
if (!$btn) continue;
|
||||
const computedStyles = getComputedStyle($btn),
|
||||
visible = computedStyles.getPropertyValue("display") !== "none";
|
||||
if (!visible) continue;
|
||||
const width = computedStyles.getPropertyValue("width"),
|
||||
from = computedStyles.getPropertyValue("right"),
|
||||
to = offset + "px";
|
||||
// offset floating buttons from help
|
||||
offset += 12 + parseInt(width);
|
||||
if (from === to) continue;
|
||||
$btn.style.setProperty("right", to);
|
||||
animate($btn, [({ right: from }, { right: to })]);
|
||||
}
|
||||
if ($help || $floating) removeMutationListener(repositionCorner);
|
||||
};
|
||||
const corner = `${notionAi}, ${floatingButtons}`;
|
||||
addMutationListener(corner, repositionCorner, { subtree: false });
|
||||
MODS_LOADED.then(() => repositionCorner());
|
||||
|
||||
$panel.pin = () => {
|
||||
if (isPinned() || !panelViews.length) return;
|
||||
if (isClosed()) Object.assign(animationState, pinAnimation);
|
||||
animatePanel({ ...openWidth, ...pinAnimation });
|
||||
animate($panel, [closedWidth, openWidth]);
|
||||
$panel.removeAttribute("data-peeked");
|
||||
$panel.dataset.pinned = true;
|
||||
$topbarToggle.setAttribute("data-active", true);
|
||||
setInteractive(true);
|
||||
_setOpen(true);
|
||||
$panel.resize();
|
||||
};
|
||||
$panel.peek = () => {
|
||||
if (!_peekPanelOnHover) return;
|
||||
if (isPeeked() || !panelViews.length) return;
|
||||
if (isClosed()) Object.assign(animationState, peekAnimation);
|
||||
animatePanel({ ...openWidth, ...peekAnimation });
|
||||
// closing on mouseout is disabled mid-animation,
|
||||
// queue close in case mouse is no longer peeking
|
||||
// after the initial animation is complete
|
||||
setTimeout(() => {
|
||||
if (!isDragging() && !$panel.matches(":hover")) $panel.close();
|
||||
}, transitionDuration);
|
||||
$panel.removeAttribute("data-pinned");
|
||||
$panel.dataset.peeked = true;
|
||||
setInteractive(true);
|
||||
$panel.resize();
|
||||
};
|
||||
$panel.close = async () => {
|
||||
if (isClosed()) return;
|
||||
if (panelViews.length) _setOpen(false);
|
||||
$topbarToggle.removeAttribute("data-active");
|
||||
const width = (animationState.width = `${await getWidth()}px`);
|
||||
// only animate container close if it is actually taking up space,
|
||||
// otherwise will unnaturally grow + retrigger peek on peek mouseout
|
||||
if (isPinned()) animate($panel, [{ width }, closedWidth]);
|
||||
if (!$panel.matches(":hover") || !_peekPanelOnHover) {
|
||||
$panel.removeAttribute("data-pinned");
|
||||
$panel.removeAttribute("data-peeked");
|
||||
animatePanel(closedWidth);
|
||||
setInteractive(false);
|
||||
$panel.resize();
|
||||
} else $panel.peek();
|
||||
};
|
||||
$panel.toggle = () => {
|
||||
if (isPinned()) $panel.close();
|
||||
else $panel.pin();
|
||||
};
|
||||
// resizing handles visual resizes (inc. setting width to 0
|
||||
// if closed) and actual resizes on drag (inc. saving to db)
|
||||
$panel.resize = async (width) => {
|
||||
$resizeTooltip.hide();
|
||||
width = await getWidth(width);
|
||||
_setWidth?.(width);
|
||||
// works in conjunction with animations, acts as fallback
|
||||
// plus updates dependent styles e.g. page skeleton padding
|
||||
if (isClosed()) width = 0;
|
||||
const $parent = $panel.parentElement || $panel;
|
||||
$parent.style.setProperty("--panel--width", `${width}px`);
|
||||
if ($parent !== $panel) $panel.style.removeProperty("--panel--width");
|
||||
repositionCorner(width);
|
||||
};
|
||||
|
||||
useState(["panelViews"], async ([panelViews = []]) => {
|
||||
$topbarToggle.style.display = panelViews.length ? "" : "none";
|
||||
if (panelViews.length && (await _getOpen())) $panel.pin();
|
||||
else $panel.close();
|
||||
});
|
||||
|
||||
if (!hotkey) return $panel;
|
||||
addKeyListener(hotkey, (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$panel.toggle();
|
||||
});
|
||||
|
||||
return $panel;
|
||||
}
|
||||
|
||||
Object.assign((globalThis.__enhancerApi ??= {}), {
|
||||
addPanelView,
|
||||
removePanelView,
|
||||
});
|
||||
|
||||
export { Panel };
|
94
src/core/islands/Tooltip.mjs
Normal file
@ -0,0 +1,94 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
function Tooltip(props, ...children) {
|
||||
const { html, extendProps } = globalThis.__enhancerApi;
|
||||
extendProps(props, {
|
||||
role: "dialog",
|
||||
class: `absolute group/tooltip z-[999] text-center pointer-events-none`,
|
||||
});
|
||||
|
||||
const notionApp = ".notion-app-inner",
|
||||
$tooltip = html`<div ...${props}>
|
||||
<div
|
||||
class="bg-[color:var(--theme--bg-secondary)]
|
||||
text-([color:var(--theme--fg-secondary)] [12px] nowrap)
|
||||
leading-[1.4] font-medium py-[4px] px-[8px] rounded-[4px]
|
||||
drop-shadow-md transition duration-100 opacity-0
|
||||
group-open/tooltip:(pointer-events-auto opacity-100)
|
||||
[&>b]:text-[color:var(--theme--fg-primary)]"
|
||||
>
|
||||
${children}
|
||||
</div>
|
||||
</div>`;
|
||||
// can pass each coord as a number or a function
|
||||
$tooltip.show = (x, y) => {
|
||||
const $notionApp = document.querySelector(notionApp);
|
||||
if (!document.contains($tooltip)) $notionApp?.append($tooltip);
|
||||
if ($tooltip.hasAttribute("open")) return;
|
||||
$tooltip.onbeforeshow?.();
|
||||
const edgePadding = 12,
|
||||
{ clientHeight, clientWidth } = document.documentElement;
|
||||
requestAnimationFrame(() => {
|
||||
if (typeof x === "function") x = x();
|
||||
if (typeof y === "function") y = y();
|
||||
if (x < edgePadding) x = $tooltip.clientWidth + edgePadding;
|
||||
if (x + $tooltip.clientWidth > clientWidth - edgePadding)
|
||||
x = clientWidth - $tooltip.clientWidth - edgePadding;
|
||||
if (y < edgePadding) y = $tooltip.clientHeight + edgePadding;
|
||||
if (y + $tooltip.clientHeight > clientHeight - edgePadding)
|
||||
y = clientHeight - $tooltip.clientHeight - edgePadding;
|
||||
$tooltip.style.left = `${x}px`;
|
||||
$tooltip.style.top = `${y}px`;
|
||||
$tooltip.setAttribute("open", true);
|
||||
$tooltip.onshow?.();
|
||||
});
|
||||
};
|
||||
$tooltip.hide = () => {
|
||||
$tooltip.onbeforehide?.();
|
||||
$tooltip.removeAttribute("open");
|
||||
setTimeout(() => {
|
||||
$tooltip.onhide?.();
|
||||
}, 200);
|
||||
};
|
||||
$tooltip.attach = ($target, alignment = "") => {
|
||||
$target.addEventListener("mouseover", (event) => {
|
||||
setTimeout(() => {
|
||||
if (!$target.matches(":hover")) return;
|
||||
const x = () => {
|
||||
const rect = $target.getBoundingClientRect();
|
||||
if (["top", "bottom"].includes(alignment)) {
|
||||
return rect.left + rect.width / 2 - $tooltip.clientWidth / 2;
|
||||
} else if (alignment === "left") {
|
||||
return rect.left - $tooltip.clientWidth - 6;
|
||||
} else if (alignment === "right") {
|
||||
return rect.right + 6;
|
||||
} else return event.clientX;
|
||||
},
|
||||
y = () => {
|
||||
const rect = $target.getBoundingClientRect();
|
||||
if (["left", "right"].includes(alignment)) {
|
||||
// match mouse alignment if hovering over large
|
||||
// target e.g. panel resize handle, otherwise centre
|
||||
return rect.height > $tooltip.clientHeight * 2
|
||||
? event.clientY - $tooltip.clientHeight / 2
|
||||
: rect.top + rect.height / 2 - $tooltip.clientHeight / 2;
|
||||
} else if (alignment === "top") {
|
||||
return rect.top - $tooltip.clientHeight - 6;
|
||||
} else if (alignment === "bottom") {
|
||||
return rect.bottom + 6;
|
||||
} else return event.clientY;
|
||||
};
|
||||
$tooltip.show(x, y);
|
||||
}, 200);
|
||||
});
|
||||
$target.addEventListener("mouseout", $tooltip.hide);
|
||||
};
|
||||
|
||||
return $tooltip;
|
||||
}
|
||||
|
||||
export { Tooltip };
|
29
src/core/islands/TopbarButton.mjs
Normal file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
function TopbarButton({ icon, ...props }, ...children) {
|
||||
const { html, extendProps } = globalThis.__enhancerApi;
|
||||
extendProps(props, {
|
||||
tabindex: 0,
|
||||
class: `notion-enhancer--topbar-button
|
||||
text-[color:var(--theme--fg-primary)] mr-[2px]
|
||||
select-none h-[28px] w-[33px] duration-[20ms]
|
||||
transition inline-flex items-center justify-center
|
||||
rounded-[3px] hover:bg-[color:var(--theme--bg-hover)]
|
||||
has-[span]:w-auto [&>span]:(text-[14px] leading-[1.2] px-[8px])
|
||||
[&[data-active]]:bg-[color:var(--theme--bg-hover)]
|
||||
[&>i]:size-[20px]`,
|
||||
});
|
||||
|
||||
// [role="button"] == `-webkit-app-region: no-drag`
|
||||
return html`<div role="button" ...${props}>
|
||||
${props.innerHTML || children.length
|
||||
? children
|
||||
: html`<i class="i-${icon}" />`}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export { TopbarButton };
|
59
src/core/menu/index.html
Normal file
@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base target="_blank" />
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>notion-enhancer menu</title>
|
||||
<link rel="stylesheet" href="./menu.css" />
|
||||
<link rel="stylesheet" href="../../vendor/coloris.min.css" />
|
||||
<script src="../../vendor/coloris.min.js" type="module"></script>
|
||||
<script src="./menu.mjs" type="module" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="skeleton">
|
||||
<div class="row row-group">
|
||||
<div class="shimmer" style="width: 110px"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="shimmer icon"></div>
|
||||
<div class="shimmer" style="width: 60px"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="shimmer icon"></div>
|
||||
<div class="shimmer" style="width: 72px"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="shimmer icon"></div>
|
||||
<div class="shimmer" style="width: 78px"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="shimmer icon"></div>
|
||||
<div class="shimmer" style="width: 96px"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="shimmer icon"></div>
|
||||
<div class="shimmer" style="width: 79px"></div>
|
||||
</div>
|
||||
<div class="row row-group">
|
||||
<div class="shimmer" style="width: 53px"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="shimmer icon"></div>
|
||||
<div class="shimmer" style="width: 30px"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="shimmer icon"></div>
|
||||
<div class="shimmer" style="width: 48px"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="shimmer icon"></div>
|
||||
<div class="shimmer" style="width: 65px"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="shimmer icon"></div>
|
||||
<div class="shimmer" style="width: 75px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
192
src/core/menu/islands/Banner.mjs
Normal file
@ -0,0 +1,192 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { Popup } from "./Popup.mjs";
|
||||
import { Button } from "./Button.mjs";
|
||||
import { Description } from "./Description.mjs";
|
||||
|
||||
const updateGuide =
|
||||
"https://notion-enhancer.github.io/getting-started/updating/",
|
||||
tsAndCs = "https://notion-enhancer.github.io/about/terms-and-conditions/";
|
||||
|
||||
const rectToStyle = (rect) =>
|
||||
["width", "height", "top", "bottom", "left", "right"]
|
||||
.filter((prop) => rect[prop])
|
||||
.map((prop) => `${prop}: ${rect[prop]};`)
|
||||
.join("");
|
||||
|
||||
function Star({ from, ...rect }) {
|
||||
const { html } = globalThis.__enhancerApi;
|
||||
return html`<svg
|
||||
viewBox="0 0 24 24"
|
||||
class="absolute fill-none skew-y-2${from
|
||||
? ` hidden ${from}:inline-block`
|
||||
: ""}"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style=${rectToStyle(rect)}
|
||||
>
|
||||
<path
|
||||
d="M11.3255 22.5826C11.3255 22.8897 11.5745 23.1387 11.8816 23.1387C12.1887 23.1387 12.4377 22.8897 12.4377 22.5826C12.4377 19.3351 13.2489 16.7277 14.8868 14.848C16.5218 12.9717 19.044 11.7477 22.6145 11.3906C22.9201 11.3601 23.1431 11.0875 23.1125 10.7819C23.082 10.4763 22.8094 10.2533 22.5038 10.2839C18.9253 10.6417 16.4423 9.91532 14.8501 8.40653C13.2524 6.89252 12.4377 4.48364 12.4377 1.22746C12.4377 0.920325 12.1887 0.67134 11.8816 0.67134C11.5745 0.67134 11.3255 0.920325 11.3255 1.22746C11.3255 4.47517 10.516 7.08239 8.87909 8.96186C7.24516 10.8379 4.72305 12.062 1.1487 12.4194C0.843091 12.45 0.620117 12.7225 0.650678 13.0281C0.681239 13.3337 0.953763 13.5567 1.25938 13.5261C4.84181 13.1679 7.32467 13.8944 8.91581 15.4031C10.5125 16.9171 11.3255 19.3261 11.3255 22.5826Z"
|
||||
fill="#FDCC80"
|
||||
/>
|
||||
<path
|
||||
d="M11.3255 22.5826C11.3255 22.8897 11.5745 23.1387 11.8816 23.1387C12.1887 23.1387 12.4377 22.8897 12.4377 22.5826C12.4377 19.3351 13.2489 16.7277 14.8868 14.848C16.5218 12.9717 19.044 11.7477 22.6145 11.3906C22.9201 11.3601 23.1431 11.0875 23.1125 10.7819C23.082 10.4763 22.8094 10.2533 22.5038 10.2839C18.9253 10.6417 16.4423 9.91532 14.8501 8.40653C13.2524 6.89252 12.4377 4.48364 12.4377 1.22746C12.4377 0.920325 12.1887 0.67134 11.8816 0.67134C11.5745 0.67134 11.3255 0.920325 11.3255 1.22746C11.3255 4.47517 10.516 7.08239 8.87909 8.96186C7.24516 10.8379 4.72305 12.062 1.1487 12.4194C0.843091 12.45 0.620117 12.7225 0.650678 13.0281C0.681239 13.3337 0.953763 13.5567 1.25938 13.5261C4.84181 13.1679 7.32467 13.8944 8.91581 15.4031C10.5125 16.9171 11.3255 19.3261 11.3255 22.5826Z"
|
||||
fill="url(#paint0_linear_3_70)"
|
||||
/>
|
||||
<path
|
||||
d="M11.3255 22.5826C11.3255 22.8897 11.5745 23.1387 11.8816 23.1387C12.1887 23.1387 12.4377 22.8897 12.4377 22.5826C12.4377 19.3351 13.2489 16.7277 14.8868 14.848C16.5218 12.9717 19.044 11.7477 22.6145 11.3906C22.9201 11.3601 23.1431 11.0875 23.1125 10.7819C23.082 10.4763 22.8094 10.2533 22.5038 10.2839C18.9253 10.6417 16.4423 9.91532 14.8501 8.40653C13.2524 6.89252 12.4377 4.48364 12.4377 1.22746C12.4377 0.920325 12.1887 0.67134 11.8816 0.67134C11.5745 0.67134 11.3255 0.920325 11.3255 1.22746C11.3255 4.47517 10.516 7.08239 8.87909 8.96186C7.24516 10.8379 4.72305 12.062 1.1487 12.4194C0.843091 12.45 0.620117 12.7225 0.650678 13.0281C0.681239 13.3337 0.953763 13.5567 1.25938 13.5261C4.84181 13.1679 7.32467 13.8944 8.91581 15.4031C10.5125 16.9171 11.3255 19.3261 11.3255 22.5826Z"
|
||||
stroke="#FDCC80"
|
||||
stroke-width="1.11225"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_3_70"
|
||||
x1="11.8816"
|
||||
y1="1.22746"
|
||||
x2="11.8816"
|
||||
y2="22.5826"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#FFE171" />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function Circle(rect) {
|
||||
const { html } = globalThis.__enhancerApi;
|
||||
return html`<div
|
||||
class="absolute rounded-full
|
||||
border-(~ purple-500) bg-purple-400"
|
||||
style=${rectToStyle(rect)}
|
||||
></div>`;
|
||||
}
|
||||
|
||||
function Banner({ updateAvailable, isDevelopmentBuild }) {
|
||||
const { html, useState } = globalThis.__enhancerApi,
|
||||
{ version, initDatabase } = globalThis.__enhancerApi,
|
||||
$version = html`<button
|
||||
class="text-[12px] py-[2px] px-[6px] mt-[2px]
|
||||
font-medium leading-tight tracking-wide rounded-[3px]
|
||||
relative bg-purple-500 from-white/[0.18] to-white/[0.16]
|
||||
bg-[linear-gradient(225deg,var(--tw-gradient-stops))]"
|
||||
>
|
||||
<div
|
||||
class="notion-enhancer--menu-update-indicator
|
||||
absolute size-[12px] right-[-6px] top-[-6px]
|
||||
${updateAvailable ? "" : "hidden"}"
|
||||
>
|
||||
<span
|
||||
class="block rounded-full h-full w-full
|
||||
absolute bg-purple-500/75 animate-ping"
|
||||
></span>
|
||||
<span
|
||||
class="block rounded-full h-full w-full
|
||||
relative bg-purple-500"
|
||||
></span>
|
||||
</div>
|
||||
<span class="relative">v${version}</span>
|
||||
</button>`,
|
||||
$popup = html`<${Popup} trigger=${$version}>
|
||||
<p
|
||||
class="typography py-[2px] px-[8px] text-[14px]"
|
||||
innerHTML=${updateAvailable
|
||||
? `<b>v${updateAvailable}</b> is available! <a href="${updateGuide}">Update now.</a>`
|
||||
: isDevelopmentBuild
|
||||
? "This is a development build of the notion-enhancer. It may be unstable."
|
||||
: "You're up to date!"}
|
||||
/>
|
||||
<//>`;
|
||||
$version.append($popup);
|
||||
if (updateAvailable) {
|
||||
useState(["focus", "view"], ([, view = "welcome"]) => {
|
||||
if (view !== "welcome") return;
|
||||
// delayed appearance = movement attracts eye
|
||||
setTimeout(() => $popup.open(), 400);
|
||||
});
|
||||
}
|
||||
|
||||
const $welcome = html`<div
|
||||
class="relative flex overflow-hidden h-[192px] rounded-t-[4px]
|
||||
border-(~ purple-400) bg-purple-500 from-white/20 to-transparent
|
||||
text-white bg-[linear-gradient(225deg,var(--tw-gradient-stops))]"
|
||||
>
|
||||
<${Circle} width="128px" height="128px" bottom="-64px" left="-64px" />
|
||||
<${Circle} width="144px" height="144px" top="-108px" left="80px" />
|
||||
<${Circle} width="208px" height="208px" bottom="-64px" right="-16px" />
|
||||
<${Circle} width="144px" height="144px" bottom="-72px" right="144px" />
|
||||
<${Star} width="36px" height="36px" top="136px" left="190px" />
|
||||
<${Star} width="48px" height="48px" top="32px" left="336px" />
|
||||
<${Star} width="64px" height="64px" top="90px" left="448px" from="lg" />
|
||||
<h1
|
||||
class="z-10 px-[32px] md:px-[48px] lg:px-[64px]
|
||||
font-bold leading-tight tracking-tight my-auto"
|
||||
>
|
||||
<a href="https://notion-enhancer.github.io/">
|
||||
<span class="text-[26px]">Welcome to</span><br />
|
||||
<span class="text-[28px]">the notion-enhancer</span>
|
||||
</a>
|
||||
</h1>
|
||||
<div
|
||||
class="absolute bottom-0 right-0 py-[24px]
|
||||
px-[32px] md:px-[48px] lg:px-[64px]"
|
||||
>
|
||||
<div class="relative flex-(~ col)">
|
||||
<i class="i-notion-enhancer text-[42px] mx-auto mb-[8px]"></i>
|
||||
${$version}
|
||||
</div>
|
||||
</div>
|
||||
</div>`,
|
||||
$sponsorship = html`<div
|
||||
class="py-[18px] px-[16px] rounded-b-[4px]
|
||||
border-(~ [color:var(--theme--fg-border)]) bg-[color:var(--theme--bg-secondary)]"
|
||||
>
|
||||
<div class="flex items-center gap-[16px]">
|
||||
<p class="text-[14px] font-semibold">
|
||||
Enjoying the notion-enhancer?<br />
|
||||
Support future development:
|
||||
</p>
|
||||
<${Button}
|
||||
icon="coffee"
|
||||
variant="brand"
|
||||
class="grow justify-center"
|
||||
href="https://www.buymeacoffee.com/dragonwocky"
|
||||
>Buy me a coffee
|
||||
<//>
|
||||
<${Button}
|
||||
icon="calendar-heart"
|
||||
variant="brand"
|
||||
class="grow justify-center"
|
||||
href="https://github.com/sponsors/dragonwocky"
|
||||
>Sponsor me
|
||||
<//>
|
||||
</div>
|
||||
<!-- Disclaimer: draft of potential perks, to be confirmed before full release. -->
|
||||
<${Description} class="mt-[6px]">
|
||||
<!-- Sponsors help make open-source development sustainable and receive
|
||||
access to priority support channels, private developer previews, and
|
||||
role cosmetics on Discord. A one-time donation is equivalent to 1 month
|
||||
of sponsor perks. To learn more about perks, read the
|
||||
<a href=${tsAndCs} class="ml-[3px]">Terms & Conditions</a>. -->
|
||||
<//>
|
||||
</div>`;
|
||||
initDatabase()
|
||||
.get("agreedToTerms")
|
||||
.then((agreedToTerms) => {
|
||||
// only show sponsorship if already agree to terms
|
||||
// and opening menu after having reloaded since agreeing
|
||||
$welcome.style.borderRadius = agreedToTerms === version ? "" : "4px";
|
||||
$sponsorship.style.display = agreedToTerms === version ? "" : "none";
|
||||
});
|
||||
|
||||
return html`<section class="notion-enhancer--menu-banner">
|
||||
${$welcome}${$sponsorship}
|
||||
</section>`;
|
||||
}
|
||||
|
||||
export { Banner };
|
45
src/core/menu/islands/Button.mjs
Normal file
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
function Button({ icon, variant, tagName, ...props }, ...children) {
|
||||
const { html, extendProps } = globalThis.__enhancerApi;
|
||||
extendProps(props, {
|
||||
class: `notion-enhancer--menu-button shrink-0
|
||||
flex gap-[8px] items-center px-[12px] rounded-[4px]
|
||||
h-[${variant === "sm" ? "28" : "32"}px] select-none
|
||||
transition duration-[20ms] ${
|
||||
variant === "primary"
|
||||
? `text-[color:var(--theme--accent-primary\\_contrast)]
|
||||
font-medium bg-[color:var(--theme--accent-primary)]
|
||||
hover:bg-[color:var(--theme--accent-primary\\_hover)]`
|
||||
: variant === "secondary"
|
||||
? `text-[color:var(--theme--accent-secondary)]
|
||||
border-(~ [color:var(--theme--accent-secondary)])
|
||||
hover:bg-[color:var(--theme--accent-secondary\\_hover)]`
|
||||
: variant === "brand"
|
||||
? `text-white border-(~ purple-400)
|
||||
bg-purple-500 hover:(from-white/20 to-transparent
|
||||
bg-[linear-gradient(225deg,var(--tw-gradient-stops))])`
|
||||
: `border-(~ [color:var(--theme--fg-border)])
|
||||
not-disabled:hover:bg-[color:var(--theme--bg-hover)]
|
||||
disabled:text-[color:var(--theme--fg-secondary)]`
|
||||
}`,
|
||||
});
|
||||
tagName ??= props["href"] ? "a" : "button";
|
||||
return html`<${tagName} ...${props}>
|
||||
${icon
|
||||
? html`<i
|
||||
class="i-${icon}
|
||||
text-[${variant === "sm" && children.length ? "13" : "17"}px]"
|
||||
></i>`
|
||||
: ""}
|
||||
<span class="text-[${variant === "sm" ? "13" : "14"}px] empty:hidden">
|
||||
${children}
|
||||
</span>
|
||||
<//>`;
|
||||
}
|
||||
|
||||
export { Button };
|
47
src/core/menu/islands/Checkbox.mjs
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
function Checkbox({ _get, _set, _requireReload = true, ...props }) {
|
||||
let _initialValue;
|
||||
const { html, extendProps, setState, useState } = globalThis.__enhancerApi,
|
||||
$input = html`<input
|
||||
type="checkbox"
|
||||
class="hidden [&:checked+div]:(px-px bg-[color:var(--theme--accent-primary)])
|
||||
[&:not(:checked)+div>i]:text-transparent [&:not(:checked)+div]:(border-(~
|
||||
[color:var(--theme--fg-primary)]) hover:bg-[color:var(--theme--bg-hover)])"
|
||||
...${props}
|
||||
/>`;
|
||||
extendProps($input, { onchange: () => _set?.($input.checked) });
|
||||
useState(["rerender"], async () => {
|
||||
const checked = (await _get?.()) ?? $input.checked;
|
||||
$input.checked = checked;
|
||||
if (_requireReload) {
|
||||
_initialValue ??= checked;
|
||||
if (checked !== _initialValue) setState({ databaseUpdated: true });
|
||||
}
|
||||
});
|
||||
|
||||
return html`<label
|
||||
tabindex="0"
|
||||
class="notion-enhancer--menu-checkbox cursor-pointer"
|
||||
onkeydown=${(event) => {
|
||||
if ([" ", "Enter"].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
$input.click();
|
||||
}
|
||||
}}
|
||||
>
|
||||
${$input}
|
||||
<div class="flex items-center h-[16px] transition duration-200">
|
||||
<i
|
||||
class="i-check size-[14px]
|
||||
text-[color:var(--theme--accent-primary\\_contrast)]"
|
||||
></i>
|
||||
</div>
|
||||
</label>`;
|
||||
}
|
||||
|
||||
export { Checkbox };
|
16
src/core/menu/islands/Description.mjs
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
function Description(props, ...children) {
|
||||
const { html, extendProps } = globalThis.__enhancerApi;
|
||||
extendProps(props, {
|
||||
class: `notion-enhancer--menu-description typography
|
||||
leading-[16px] text-([12px] [color:var(--theme--fg-secondary)])`,
|
||||
});
|
||||
return html`<p ...${props}>${children}</p>`;
|
||||
}
|
||||
|
||||
export { Description };
|
59
src/core/menu/islands/Footer.mjs
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { Button } from "./Button.mjs";
|
||||
|
||||
function Footer({ categories, transitionDuration = 150 }) {
|
||||
const { html, setState, useState, reloadApp } = globalThis.__enhancerApi,
|
||||
$reload = html`<${Button}
|
||||
class="ml-auto"
|
||||
variant="primary"
|
||||
icon="refresh-cw"
|
||||
onclick=${reloadApp}
|
||||
style="display: none"
|
||||
>Reload & Apply Changes
|
||||
<//>`,
|
||||
$categories = categories.map(({ id, title, mods }) => {
|
||||
return [
|
||||
mods.map((mod) => mod.id),
|
||||
html`<${Button}
|
||||
icon="chevron-left"
|
||||
onclick=${() => setState({ transition: "slide-to-left", view: id })}
|
||||
>${title}
|
||||
<//>`,
|
||||
];
|
||||
});
|
||||
|
||||
useState(["view"], ([view]) => {
|
||||
let [footerOpen] = useState(["databaseUpdated"]);
|
||||
footerOpen ||= $categories.some(([ids]) => ids.some((id) => id === view));
|
||||
setState({ footerOpen });
|
||||
});
|
||||
useState(["databaseUpdated"], ([databaseUpdated]) => {
|
||||
$reload.style.display = databaseUpdated ? "" : "none";
|
||||
if (databaseUpdated) setState({ footerOpen: true });
|
||||
});
|
||||
useState(["footerOpen"], ([footerOpen]) => {
|
||||
// only toggle buttons if footer is open,
|
||||
// otherwise leave as is during transition
|
||||
if (!footerOpen) return;
|
||||
const [view] = useState(["view"]);
|
||||
for (const [ids, $btn] of $categories) {
|
||||
const viewInCategory = ids.some((id) => id === view);
|
||||
$btn.style.display = viewInCategory ? "" : "none";
|
||||
}
|
||||
});
|
||||
|
||||
return html`<footer
|
||||
class="notion-enhancer--menu-footer px-[60px] py-[16px]
|
||||
flex w-full bg-[color:var(--theme--bg-primary)] h-[64px]
|
||||
border-t-(~ [color:var(--theme--fg-border)])"
|
||||
>
|
||||
${$categories.map(([, $btn]) => $btn)}${$reload}
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
export { Footer };
|
17
src/core/menu/islands/Heading.mjs
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
function Heading(props, ...children) {
|
||||
const { html, extendProps } = globalThis.__enhancerApi;
|
||||
extendProps(props, {
|
||||
class: `notion-enhancer--menu-heading text-[16px]
|
||||
font-semibold mb-[16px] mt-[48px] first:mt-0 pb-[12px]
|
||||
border-b-(~ [color:var(--theme--fg-border)])`,
|
||||
});
|
||||
return html`<h4 ...${props}>${children}</h4>`;
|
||||
}
|
||||
|
||||
export { Heading };
|
200
src/core/menu/islands/Input.mjs
Normal file
@ -0,0 +1,200 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
const updateHotkey = (event) => {
|
||||
const keys = [];
|
||||
for (const modifier of ["metaKey", "ctrlKey", "altKey", "shiftKey"]) {
|
||||
if (!event[modifier]) continue;
|
||||
const alias = modifier[0].toUpperCase() + modifier.slice(1, -3);
|
||||
keys.push(alias);
|
||||
}
|
||||
// retain keyboard navigation of menu
|
||||
if (["Tab", "Escape"].includes(event.key) && !keys.length) {
|
||||
return;
|
||||
} else event.preventDefault();
|
||||
if (!keys.length && ["Backspace", "Delete"].includes(event.key)) {
|
||||
event.target.value = "";
|
||||
} else if (event.key) {
|
||||
let key = event.key;
|
||||
if (key === " ") key = "Space";
|
||||
if (["+", "="].includes(key)) key = "Plus";
|
||||
if (key === "-") key = "Minus";
|
||||
if (key === "|") key = "\\";
|
||||
if (event.code === "Comma") key = ",";
|
||||
if (event.code === "Period") key = ".";
|
||||
if (key === "Control") key = "Ctrl";
|
||||
// avoid e.g. Shift+Shift, force inclusion of non-modifier
|
||||
if (keys.includes(key)) return;
|
||||
keys.push(key.length === 1 ? key.toUpperCase() : key);
|
||||
event.target.value = keys.join("+");
|
||||
}
|
||||
event.target.dispatchEvent(new Event("input"));
|
||||
event.target.dispatchEvent(new Event("change"));
|
||||
},
|
||||
updateContrast = ($input, $icon) => {
|
||||
$input.style.background = $input.value;
|
||||
const [r, g, b, a = 1] = $input.value
|
||||
.replace(/^rgba?\(/, "")
|
||||
.replace(/\)$/, "")
|
||||
.split(",")
|
||||
.map((n) => parseFloat(n));
|
||||
if (a > 0.5) {
|
||||
// pick a contrasting foreground for an rgb background
|
||||
// using the percieved brightness constants from http://alienryderflex.com/hsp.html
|
||||
const brightness = 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b);
|
||||
$input.style.color = Math.sqrt(brightness) > 165.75 ? "#000" : "#fff";
|
||||
} else $input.style.color = "#000";
|
||||
$icon.style.color = $input.style.color;
|
||||
$icon.style.opacity = "0.7";
|
||||
},
|
||||
readUpload = async (event) => {
|
||||
const file = event.target.files[0],
|
||||
reader = new FileReader();
|
||||
return new Promise((res) => {
|
||||
reader.onload = async (progress) => {
|
||||
const content = progress.currentTarget.result,
|
||||
upload = { filename: file.name, content };
|
||||
res(upload);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
};
|
||||
|
||||
function Input({
|
||||
type,
|
||||
icon,
|
||||
variant,
|
||||
extensions,
|
||||
class: className,
|
||||
_get,
|
||||
_set,
|
||||
_requireReload = true,
|
||||
...props
|
||||
}) {
|
||||
let $filename, $clear;
|
||||
const { html, extendProps, setState, useState } = globalThis.__enhancerApi;
|
||||
Coloris({ format: "rgb" });
|
||||
|
||||
type ??= "text";
|
||||
if (type === "text") icon ??= "text-cursor";
|
||||
if (type === "number") icon ??= "hash";
|
||||
if (type === "hotkey") icon ??= "command";
|
||||
if (type === "color") icon ??= "pipette";
|
||||
|
||||
if (type === "file") {
|
||||
icon ??= "file-up";
|
||||
$filename = html`<span class="ml-[6px]">Upload a file</span>`;
|
||||
$clear = html`<button
|
||||
style="display: none"
|
||||
class="h-[14px] transition duration-[20ms]
|
||||
flex text-[color:var(--theme--fg-secondary)]
|
||||
hover:text-[color:var(--theme--fg-primary)]"
|
||||
onclick=${() => _set?.({ filename: "", content: "" })}
|
||||
>
|
||||
<i class="i-x size-[14px]"></i>
|
||||
</button>`;
|
||||
props.accept = extensions
|
||||
?.map((ext) => (ext.startsWith(".") ? ext : `.${ext}`))
|
||||
.join(",");
|
||||
}
|
||||
|
||||
const $input = html`<input
|
||||
type=${["hotkey", "color"].includes(type) ? "text" : type}
|
||||
class="h-full w-full pb-px text-[14px] leading-[1.2]
|
||||
${variant === "lg" ? "pl-[12px] pr-[40px]" : "pl-[8px] pr-[32px]"}
|
||||
appearance-none bg-transparent ${type === "file" ? "hidden" : ""}
|
||||
${type === "hotkey" ? "text-[color:var(--theme--fg-secondary)]" : ""}
|
||||
${type === "color"
|
||||
? "font-medium"
|
||||
: "border-(~ [color:var(--theme--fg-border)])"}"
|
||||
data-coloris=${type === "color"}
|
||||
...${props}
|
||||
/>`,
|
||||
$icon = html`<span
|
||||
class="${variant === "lg" ? "pr-[12px]" : "pr-[8px]"}
|
||||
absolute flex items-center h-full pointer-events-none
|
||||
text-[color:var(--theme--fg-secondary)] right-0 top-0"
|
||||
><i class="i-${icon} size-[16px]"></i>
|
||||
</span>`;
|
||||
|
||||
let _initialValue;
|
||||
extendProps($input, {
|
||||
onclick: () => {
|
||||
// change text to "uploading..." until file has uploaded
|
||||
// to reassure users experiencing latency while file is processed
|
||||
if (type === "file") $filename.innerText = "Uploading...";
|
||||
},
|
||||
onchange: (event) => {
|
||||
if (_set && type === "file") {
|
||||
readUpload(event)
|
||||
.then(_set)
|
||||
// refocus iframe after file has uploaded,
|
||||
// sometimes switching back after opening the
|
||||
// native file upload menu causes a loss of focus
|
||||
.then(() => window.focus());
|
||||
} else _set?.($input.value);
|
||||
},
|
||||
onrerender: async () => {
|
||||
const value = (await _get?.()) ?? $input.value ?? "";
|
||||
if (type === "file") {
|
||||
$filename.innerText = value?.filename || "Upload a file";
|
||||
$clear.style.display = value?.filename ? "" : "none";
|
||||
if (_requireReload) {
|
||||
_initialValue ??= value?.content || "";
|
||||
if ((value?.content || "") !== _initialValue) {
|
||||
setState({ databaseUpdated: true });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($input.value !== value) $input.value = value;
|
||||
if (_requireReload) {
|
||||
_initialValue ??= value;
|
||||
if (value !== _initialValue) setState({ databaseUpdated: true });
|
||||
}
|
||||
if (type === "color") updateContrast($input, $icon);
|
||||
}
|
||||
},
|
||||
onkeydown: type === "hotkey" ? updateHotkey : undefined,
|
||||
oninput: type === "color" ? () => _set?.($input.value) : undefined,
|
||||
});
|
||||
useState(["rerender"], () => $input.onrerender?.());
|
||||
|
||||
return type === "file"
|
||||
? html`<div
|
||||
class="notion-enhancer--menu-file-input shrink-0
|
||||
flex items-center gap-[8px] ${className ?? ""}"
|
||||
>
|
||||
<label
|
||||
tabindex="0"
|
||||
class="flex items-center cursor-pointer select-none
|
||||
px-[8px] bg-[color:var(--theme--bg-secondary)]
|
||||
h-[28px] rounded-[4px] transition duration-[20ms]
|
||||
text-([14px] [color:var(--theme--fg-secondary)])
|
||||
border-(~ [color:var(--theme--fg-border)])
|
||||
hover:bg-[color:var(--theme--bg-hover)]"
|
||||
onkeydown=${(event) => {
|
||||
if ([" ", "Enter"].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
$input.click();
|
||||
}
|
||||
}}
|
||||
>${$input}${$icon.children[0]}${$filename}
|
||||
</label>
|
||||
${$clear}
|
||||
</div>`
|
||||
: html`<label
|
||||
class="notion-enhancer--menu-input
|
||||
${variant === "lg" ? "h-[32px]" : "h-[28px]"}
|
||||
relative overflow-hidden rounded-[4px] w-full inline-block
|
||||
focus-within:ring-(~ [color:var(--theme--accent-primary)])
|
||||
${className ?? ""} ${type === "color"
|
||||
? "bg-([image:repeating-linear-gradient(45deg,#aaa_25%,transparent_25%,transparent_75%,#aaa_75%,#aaa),repeating-linear-gradient(45deg,#aaa_25%,#fff_25%,#fff_75%,#aaa_75%,#aaa)] [position:0_0,4px_4px] [size:8px_8px])"
|
||||
: "bg-[color:var(--theme--bg-hover)]"}"
|
||||
>${$input}${$icon}
|
||||
</label>`;
|
||||
}
|
||||
|
||||
export { Input };
|
72
src/core/menu/islands/List.mjs
Normal file
@ -0,0 +1,72 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { Description } from "./Description.mjs";
|
||||
import { Input } from "./Input.mjs";
|
||||
import { Mod } from "./Mod.mjs";
|
||||
|
||||
function Search({ items, itemType }) {
|
||||
const { html, addKeyListener } = globalThis.__enhancerApi,
|
||||
$search = html`<${Input}
|
||||
type="text"
|
||||
icon="search"
|
||||
variant="lg"
|
||||
placeholder="Search ${items.length} ${items.length === 1
|
||||
? itemType.replace(/s$/, "")
|
||||
: itemType} (Press '/' to focus)"
|
||||
oninput=${(event) => {
|
||||
const query = event.target.value.toLowerCase();
|
||||
for (const $item of items) {
|
||||
const matches = $item.innerText.toLowerCase().includes(query);
|
||||
$item.style.display = matches ? "" : "none";
|
||||
}
|
||||
}}
|
||||
/>`;
|
||||
addKeyListener("/", (event) => {
|
||||
if (document.activeElement?.nodeName === "INPUT") return;
|
||||
// offsetParent == null if parent has "display: none;"
|
||||
if ($search.offsetParent) {
|
||||
event.preventDefault();
|
||||
$search.focus();
|
||||
}
|
||||
});
|
||||
return $search;
|
||||
}
|
||||
|
||||
function List({ id, mods, description }) {
|
||||
const { html, setState } = globalThis.__enhancerApi,
|
||||
{ isEnabled, setEnabled } = globalThis.__enhancerApi,
|
||||
$mods = mods.map((mod) => {
|
||||
const _get = () => isEnabled(mod.id),
|
||||
_set = async (enabled) => {
|
||||
await setEnabled(mod.id, enabled);
|
||||
// only one theme may be enabled per
|
||||
// mode at a time => auto-disable other
|
||||
// enabled themes of matching mode
|
||||
if (enabled && id === "themes") {
|
||||
const isDark = mod.tags.includes("dark"),
|
||||
isLight = mod.tags.includes("light");
|
||||
for (const other of mods) {
|
||||
if (other.id === mod.id) continue;
|
||||
const otherDark = other.tags.includes("dark"),
|
||||
otherLight = other.tags.includes("light");
|
||||
if ((isDark && otherDark) || (isLight && otherLight)) {
|
||||
await setEnabled(other.id, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
setState({ rerender: true });
|
||||
};
|
||||
return html`<${Mod} ...${{ ...mod, _get, _set }} />`;
|
||||
});
|
||||
return html`<div class="flex-(~ col) gap-y-[14px]">
|
||||
<${Search} items=${$mods} itemType=${id} />
|
||||
<${Description} innerHTML=${description} />
|
||||
${$mods}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export { List };
|
84
src/core/menu/islands/Mod.mjs
Normal file
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { Description } from "./Description.mjs";
|
||||
import { Toggle } from "./Toggle.mjs";
|
||||
|
||||
function Mod({
|
||||
id,
|
||||
name,
|
||||
version,
|
||||
description,
|
||||
thumbnail,
|
||||
tags = [],
|
||||
authors,
|
||||
options = [],
|
||||
_get,
|
||||
_set,
|
||||
_src,
|
||||
}) {
|
||||
const { html, setState, enhancerUrl } = globalThis.__enhancerApi,
|
||||
toggleId = Math.random().toString(36).slice(2, 5);
|
||||
|
||||
return html`<label
|
||||
for=${toggleId}
|
||||
class="notion-enhancer--menu-mod flex items-stretch rounded-[4px]
|
||||
bg-[color:var(--theme--bg-secondary)] w-full py-[18px] px-[16px]
|
||||
border border-[color:var(--theme--fg-border)] cursor-pointer
|
||||
duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]
|
||||
transition [&+.notion-enhancer--menu-option]:mt-[24px]"
|
||||
>
|
||||
${thumbnail
|
||||
? html`<img
|
||||
src="${enhancerUrl(`${_src}/${thumbnail}`)}"
|
||||
class="rounded-[4px] mr-[12px] h-[74px] aspect-video my-auto"
|
||||
/>`
|
||||
: ""}
|
||||
<div class="flex-(~ col) w-full">
|
||||
<div class="flex flex-wrap items-center gap-[8px] text-[14px] mb-[5px]">
|
||||
<h3 class="my-0">${name}</h3>
|
||||
${[`v${version}`, ...tags].map((tag) => {
|
||||
return html`<span
|
||||
class="text-([12px] [color:var(--theme--fg-secondary)]
|
||||
nowrap) leading-tight tracking-wide py-[2px] px-[6px]
|
||||
rounded-[3px] bg-[color:var(--theme--bg-hover)]"
|
||||
>${tag}
|
||||
</span>`;
|
||||
})}
|
||||
</div>
|
||||
<${Description} class="mb-[6px] max-w-[80%]" innerHTML=${description} />
|
||||
<div class="mt-auto flex gap-x-[8px] text-[12px] leading-[16px]">
|
||||
${authors.map((author) => {
|
||||
return html`<a href=${author.homepage} class="flex items-center">
|
||||
<img src=${author.avatar} alt="" class="h-[12px] rounded-full" />
|
||||
<span class="ml-[6px]">${author.name}</span>
|
||||
</a>`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex ml-auto">
|
||||
${options.length
|
||||
? html`<button
|
||||
class="flex items-center p-[4px] rounded-[4px] transition
|
||||
text-[color:var(--theme--fg-secondary)] my-auto mr-[8px]
|
||||
duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]
|
||||
active:text-[color:var(--theme--fg-primary)]"
|
||||
onclick=${() => {
|
||||
setState({ transition: "slide-to-right", view: id });
|
||||
}}
|
||||
>
|
||||
<i class="i-settings size-[18px]"></i>
|
||||
</button>`
|
||||
: ""}
|
||||
<div class="my-auto scale-[1.15]">
|
||||
<${Toggle} id=${toggleId} ...${{ _get, _set }} />
|
||||
</div>
|
||||
</div>
|
||||
</label>`;
|
||||
}
|
||||
|
||||
export { Mod };
|
108
src/core/menu/islands/Onboarding.mjs
Normal file
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { Heading } from "./Heading.mjs";
|
||||
import { Description } from "./Description.mjs";
|
||||
import { Checkbox } from "./Checkbox.mjs";
|
||||
import { Button } from "./Button.mjs";
|
||||
import { Tile } from "./Tile.mjs";
|
||||
|
||||
const privacyPolicy = "https://notion-enhancer.github.io/about/privacy-policy/",
|
||||
tsAndCs = "https://notion-enhancer.github.io/about/terms-and-conditions/";
|
||||
|
||||
function Onboarding() {
|
||||
const { html, setState, useState } = globalThis.__enhancerApi,
|
||||
{ version, initDatabase } = globalThis.__enhancerApi,
|
||||
$submitAgreement = html`<${Button}
|
||||
icon="arrow-right"
|
||||
class="ml-auto"
|
||||
disabled
|
||||
>Continue
|
||||
<//>`,
|
||||
$agreeToTerms = html`<div class="mt-[32px]">
|
||||
<${Heading} class="mb-[8px]">
|
||||
Thanks for installing the notion-enhancer!
|
||||
<//>
|
||||
<${Description}>
|
||||
In order for the notion-enhancer to function, it may access, collect,
|
||||
process and/or store data on your device (including workspace content,
|
||||
device metadata, and notion-enhancer configuration) as described in its
|
||||
privacy policy. Unless otherwise stated, the notion-enhancer will never
|
||||
transmit personally identifiable information from your device.
|
||||
<br /><br />
|
||||
The notion-enhancer is free and open-source software distributed under
|
||||
the <a href="${tsAndCs}#license">MIT License</a> without warranty of any
|
||||
kind. In no event shall the authors be liable for any consequences of
|
||||
the software's use. Before continuing, you must read and agree to the
|
||||
notion-enhancer's privacy policy and terms & conditions.
|
||||
<//>
|
||||
<div class="flex items-center my-[14px] gap-[8px]">
|
||||
<!-- _requireReload=${false} prevents the footer from
|
||||
suggesting a reload of the app when the box is checked -->
|
||||
<${Checkbox}
|
||||
_set=${(checked) => ($submitAgreement.disabled = !checked)}
|
||||
_requireReload=${false}
|
||||
/>
|
||||
<p class="typography text-[14px] mr-[16px]">
|
||||
I have read and agree to the
|
||||
<a class="mx-[4px]" href=${privacyPolicy}>Privacy Policy</a>
|
||||
and <a href=${tsAndCs}>Terms & Conditions</a>.
|
||||
</p>
|
||||
${$submitAgreement}
|
||||
</div>
|
||||
</div>`;
|
||||
$submitAgreement.onclick = async () => {
|
||||
if ($submitAgreement.disabled) return;
|
||||
await initDatabase().set("agreedToTerms", version);
|
||||
setState({ rerender: true });
|
||||
};
|
||||
|
||||
const $regularGreeting = html`<div
|
||||
class="mt-[16px] grid-(~ cols-3) gap-[16px]"
|
||||
>
|
||||
<${Tile}
|
||||
href="https://notion-enhancer.github.io/getting-started/basic-usage/"
|
||||
icon="graduation-cap"
|
||||
title="Stuck?"
|
||||
>Check out the usage guide.
|
||||
<//>
|
||||
<${Tile}
|
||||
href="https://notion-enhancer.github.io/documentation/mods/"
|
||||
icon="package-plus"
|
||||
title="Something missing?"
|
||||
>Build your own extension.
|
||||
<//>
|
||||
<${Tile}
|
||||
href="https://github.com/notion-enhancer/notion-enhancer/issues"
|
||||
icon="bug"
|
||||
title="Something broken?"
|
||||
>Report a bug.
|
||||
<//>
|
||||
</div>`,
|
||||
$featuredSponsors = html`
|
||||
<div class="mt-[32px]">
|
||||
<${Heading} class="mb-[8px]">Featured Sponsors<//>
|
||||
<${Description}>
|
||||
A few awesome companies out there have teamed up with me to provide
|
||||
you with the notion-enhancer, free forever. Check them out!
|
||||
<//>
|
||||
<div class="mt-[16px] grid-(~ cols-1) gap-[16px]"></div>
|
||||
<${Description} class="mt-[12px]">
|
||||
<a href="mailto:thedragonring.bod@gmail.com">Join this list.</a>
|
||||
<//>
|
||||
</div>
|
||||
`;
|
||||
useState(["rerender"], async () => {
|
||||
const agreedToTerms = await initDatabase().get("agreedToTerms");
|
||||
$agreeToTerms.style.display = agreedToTerms === version ? "none" : "";
|
||||
$regularGreeting.style.display = agreedToTerms === version ? "" : "none";
|
||||
$featuredSponsors.style.display = agreedToTerms === version ? "" : "none";
|
||||
});
|
||||
|
||||
return html`${$agreeToTerms}${$regularGreeting}`;
|
||||
}
|
||||
|
||||
export { Onboarding };
|
100
src/core/menu/islands/Options.mjs
Normal file
@ -0,0 +1,100 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { Heading } from "./Heading.mjs";
|
||||
import { Description } from "./Description.mjs";
|
||||
import { Input } from "./Input.mjs";
|
||||
import { Select } from "./Select.mjs";
|
||||
import { Toggle } from "./Toggle.mjs";
|
||||
|
||||
const camelToSentenceCase = (string) =>
|
||||
string[0].toUpperCase() +
|
||||
string.replace(/[A-Z]/g, (match) => ` ${match.toLowerCase()}`).slice(1),
|
||||
filterOptionsForRender = (options) => {
|
||||
const { platform } = globalThis.__enhancerApi;
|
||||
options = options.reduce((options, opt) => {
|
||||
// option must have key, headings may use label
|
||||
if (!opt.key && (opt.type !== "heading" || !opt.label)) return options;
|
||||
// ignore platform-specific options
|
||||
if (opt.platforms && !opt.platforms.includes(platform)) return options;
|
||||
// replace consective headings
|
||||
opt._autoremoveIfSectionEmpty ??= true;
|
||||
const prev = options[options.length - 1],
|
||||
canReplacePrev =
|
||||
prev?._autoremoveIfSectionEmpty && prev?.type === opt.type;
|
||||
if (opt.type === "heading" && canReplacePrev) {
|
||||
options[options.length - 1] = opt;
|
||||
} else options.push(opt);
|
||||
return options;
|
||||
}, []);
|
||||
// remove trailing heading
|
||||
return options.at(-1)?.type === "heading" &&
|
||||
options.at(-1)?._autoremoveIfSectionEmpty
|
||||
? options.slice(0, -1)
|
||||
: options;
|
||||
};
|
||||
|
||||
function Option({ _get, _set, ...opt }) {
|
||||
const { html } = globalThis.__enhancerApi;
|
||||
return html`<${opt.type === "toggle" ? "label" : "div"}
|
||||
class="notion-enhancer--menu-option flex items-center justify-between
|
||||
mb-[18px] ${opt.type === "toggle" ? "cursor-pointer" : ""}"
|
||||
>
|
||||
<div class="flex-(~ col) ${opt.type === "text" ? "w-full" : "mr-[10%]"}">
|
||||
<h5 class="text-[14px] mb-[2px] mt-0">${opt.label}</h5>
|
||||
${opt.type === "text"
|
||||
? html`<${Input}
|
||||
type="text"
|
||||
class="mt-[4px] mb-[8px]"
|
||||
...${{ _get, _set }}
|
||||
/>`
|
||||
: ""}
|
||||
${["string", "undefined"].includes(typeof opt.description)
|
||||
? html`<${Description} innerHTML=${opt.description} />`
|
||||
: html`<${Description}>${opt.description}<//>`}
|
||||
</div>
|
||||
${["number", "hotkey", "color"].includes(opt.type)
|
||||
? html`<${Input}
|
||||
type=${opt.type}
|
||||
class="shrink-0 !w-[192px]"
|
||||
...${{ _get, _set }}
|
||||
/>`
|
||||
: opt.type === "file"
|
||||
? html`<${Input}
|
||||
type="file"
|
||||
extensions=${opt.extensions}
|
||||
...${{ _get, _set }}
|
||||
/>`
|
||||
: opt.type === "select"
|
||||
? html`<${Select} values=${opt.values} ...${{ _get, _set }} />`
|
||||
: opt.type === "toggle"
|
||||
? html`<${Toggle} ...${{ _get, _set }} />`
|
||||
: ""}
|
||||
<//>`;
|
||||
}
|
||||
|
||||
function Options({ mod }) {
|
||||
const { html, modDatabase, setState } = globalThis.__enhancerApi;
|
||||
return filterOptionsForRender(mod.options).map((opt) => {
|
||||
opt.label ??= camelToSentenceCase(opt.key);
|
||||
if (opt.type === "heading") {
|
||||
return typeof opt.description === "string"
|
||||
? html`<div class="mb-[18px]">
|
||||
<${Heading}>${opt.label}<//>
|
||||
<${Description} innerHTML=${opt.description} />
|
||||
</div>`
|
||||
: html`<${Heading}>${opt.label}<//>`;
|
||||
}
|
||||
const _get = async () => (await modDatabase(mod.id)).get(opt.key),
|
||||
_set = async (value) => {
|
||||
await (await modDatabase(mod.id)).set(opt.key, value);
|
||||
setState({ rerender: true });
|
||||
};
|
||||
return html`<${Option} ...${{ _get, _set, ...opt }} />`;
|
||||
});
|
||||
}
|
||||
|
||||
export { Options, Option };
|
82
src/core/menu/islands/Popup.mjs
Normal file
@ -0,0 +1,82 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
function Popup(
|
||||
{ trigger, mode = "left", width = 250, maxWidth, ...props },
|
||||
...children
|
||||
) {
|
||||
const { html, extendProps, setState, useState } = globalThis.__enhancerApi,
|
||||
// known values for mode:
|
||||
// dropdown => panel switcher
|
||||
isDropdown = mode === "dropdown",
|
||||
// left => menu option select
|
||||
isLeft = mode === "left";
|
||||
extendProps(props, {
|
||||
class: `notion-enhancer--menu-popup group/popup
|
||||
absolute top-0 left-0 z-20 text-left font-normal
|
||||
flex-(~ col) justify-center pointer-events-none
|
||||
items-end w-full ${isDropdown ? "" : "h-full"}`,
|
||||
});
|
||||
|
||||
const $popup = html`<div ...${props}>
|
||||
<div
|
||||
class="relative ${isDropdown ? "w-full" : ""}
|
||||
${isLeft ? "right-[calc(100%+8px)]" : ""}"
|
||||
>
|
||||
<div
|
||||
class="bg-[color:var(--theme--bg-secondary)]
|
||||
rounded-[4px] overflow-y-auto drop-shadow-xl max-h-[70vh]
|
||||
${isDropdown ? "w-full" : "w-[250px] max-w-[calc(100vw-24px)]"}
|
||||
transition duration-200 opacity-0 scale-95 py-[6px] px-[4px]
|
||||
group-open/popup:( pointer-events-auto opacity-100 scale-100)"
|
||||
>
|
||||
${children}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
$popup.open = () => {
|
||||
$popup.setAttribute("open", true);
|
||||
$popup.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = 0));
|
||||
setState({ popupOpen: true });
|
||||
$popup.onopen?.();
|
||||
};
|
||||
$popup.close = () => {
|
||||
$popup.onbeforeclose?.();
|
||||
$popup.removeAttribute("open");
|
||||
$popup.style.pointerEvents = "auto";
|
||||
$popup.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = -1));
|
||||
setTimeout(() => {
|
||||
$popup.style.pointerEvents = "";
|
||||
setState({ popupOpen: false });
|
||||
$popup.onclose?.();
|
||||
}, 200);
|
||||
};
|
||||
$popup.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = -1));
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
if (!$popup.hasAttribute("open")) return;
|
||||
if ($popup.contains(event.target) || $popup === event.target) return;
|
||||
if (trigger?.contains(event.target) || trigger === event.target) return;
|
||||
$popup.close();
|
||||
});
|
||||
useState(["rerender"], () => {
|
||||
if ($popup.hasAttribute("open")) $popup.close();
|
||||
});
|
||||
|
||||
if (!trigger) return $popup;
|
||||
extendProps(trigger, {
|
||||
onclick: $popup.open,
|
||||
onkeydown(event) {
|
||||
if ([" ", "Enter"].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
$popup.open();
|
||||
}
|
||||
},
|
||||
});
|
||||
return $popup;
|
||||
}
|
||||
|
||||
export { Popup };
|
237
src/core/menu/islands/Profiles.mjs
Normal file
@ -0,0 +1,237 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { Heading } from "./Heading.mjs";
|
||||
import { Description } from "./Description.mjs";
|
||||
import { Checkbox } from "./Checkbox.mjs";
|
||||
import { Button } from "./Button.mjs";
|
||||
import { Input } from "./Input.mjs";
|
||||
import { Popup } from "./Popup.mjs";
|
||||
|
||||
function Profile({ id }) {
|
||||
const { html, setState } = globalThis.__enhancerApi,
|
||||
{ getProfile, initDatabase } = globalThis.__enhancerApi,
|
||||
profile = initDatabase([id]),
|
||||
db = initDatabase();
|
||||
|
||||
const getName = async () => {
|
||||
let profileName = await profile.get("profileName");
|
||||
if (id === "default") profileName ??= "default";
|
||||
return profileName ?? "";
|
||||
},
|
||||
setName = async (name) => {
|
||||
// name only has effect in menu
|
||||
// doesn't need to trigger reload
|
||||
await profile.set("profileName", name);
|
||||
},
|
||||
isActive = async () => {
|
||||
return id === (await getProfile());
|
||||
},
|
||||
setActive = async () => {
|
||||
if (await isActive()) return;
|
||||
await db.set("activeProfile", id);
|
||||
setState({ rerender: true });
|
||||
};
|
||||
|
||||
const $successName = html`<span
|
||||
class="py-[2px] px-[4px] rounded-[3px]
|
||||
bg-[color:var(--theme--bg-hover)]"
|
||||
></span>`,
|
||||
$uploadSuccess = html`<${Popup}
|
||||
onopen=${async () => ($successName.innerText = await getName())}
|
||||
>
|
||||
<p class="py-[2px] px-[8px] text-[14px]">
|
||||
The profile ${$successName} has been updated successfully.
|
||||
</p>
|
||||
<//>`,
|
||||
$uploadError = html`<${Popup}>
|
||||
<p
|
||||
class="py-[2px] px-[8px] text-[14px]
|
||||
text-[color:var(--theme--accent-secondary)]"
|
||||
>
|
||||
An error was encountered attempting to parse the uploaded file.
|
||||
</p>
|
||||
<//>`,
|
||||
uploadProfile = (event) => {
|
||||
const file = event.target.files[0],
|
||||
reader = new FileReader();
|
||||
reader.onload = async (progress) => {
|
||||
try {
|
||||
let res = progress.currentTarget.result;
|
||||
res = JSON.parse(res);
|
||||
delete res["profileName"];
|
||||
await profile.import(res);
|
||||
setState({ rerender: true });
|
||||
$uploadSuccess.open();
|
||||
setTimeout(() => $uploadSuccess.close(), 2000);
|
||||
} catch (err) {
|
||||
$uploadError.open();
|
||||
setTimeout(() => $uploadError.close(), 2000);
|
||||
}
|
||||
// clear input value to allow repeat uploads
|
||||
event.target.value = "";
|
||||
};
|
||||
reader.readAsText(file);
|
||||
},
|
||||
downloadProfile = async () => {
|
||||
const now = new Date(),
|
||||
year = now.getFullYear().toString(),
|
||||
month = (now.getMonth() + 1).toString().padStart(2, "0"),
|
||||
day = now.getDate().toString().padStart(2, "0"),
|
||||
hour = now.getHours().toString().padStart(2, "0"),
|
||||
min = now.getMinutes().toString().padStart(2, "0"),
|
||||
sec = now.getSeconds().toString().padStart(2, "0"),
|
||||
date = year + month + day + hour + min + sec;
|
||||
const $a = html`<a
|
||||
class="hidden"
|
||||
download="notion-enhancer_${await getName()}_${date}.json"
|
||||
href="data:text/json;charset=utf-8,${encodeURIComponent(
|
||||
JSON.stringify(await profile.export())
|
||||
)}"
|
||||
/>`;
|
||||
document.body.append($a);
|
||||
$a.click();
|
||||
$a.remove();
|
||||
},
|
||||
$uploadInput = html`<input
|
||||
type="file"
|
||||
class="hidden"
|
||||
accept=".json"
|
||||
onchange=${uploadProfile}
|
||||
/>`;
|
||||
|
||||
const deleteProfile = async () => {
|
||||
let profileIds = await db.get("profileIds");
|
||||
if (!profileIds?.length) profileIds = ["default"];
|
||||
// clear profile data
|
||||
const keys = Object.keys(await profile.export());
|
||||
await profile.remove(keys);
|
||||
// remove profile from list
|
||||
const index = profileIds.indexOf(id);
|
||||
if (index > -1) profileIds.splice(index, 1);
|
||||
await db.set("profileIds", profileIds);
|
||||
if (await isActive()) await db.remove("activeProfile");
|
||||
setState({ rerender: true });
|
||||
},
|
||||
$delete = html`<button
|
||||
class="h-[14px] transition duration-[20ms]
|
||||
text-[color:var(--theme--fg-secondary)]
|
||||
hover:text-[color:var(--theme--fg-primary)]"
|
||||
>
|
||||
<i class="i-x size-[14px]"></i>
|
||||
</button>`,
|
||||
$confirmName = $successName.cloneNode(true),
|
||||
$confirm = html`<${Popup}
|
||||
trigger=${$delete}
|
||||
onopen=${async () => ($confirmName.innerText = await getName())}
|
||||
>
|
||||
<p class="text-[14px] py-[2px] px-[8px]">
|
||||
Are you sure you want to delete the profile ${$confirmName} permanently?
|
||||
</p>
|
||||
<div class="flex-(~ col) gap-[8px] py-[6px] px-[8px]">
|
||||
<${Button}
|
||||
tabindex="0"
|
||||
icon="trash"
|
||||
class="justify-center"
|
||||
variant="secondary"
|
||||
onclick=${deleteProfile}
|
||||
>
|
||||
Delete
|
||||
<//>
|
||||
<${Button}
|
||||
tabindex="0"
|
||||
class="justify-center"
|
||||
onclick=${() => $confirm.close()}
|
||||
>
|
||||
Cancel
|
||||
<//>
|
||||
</div>
|
||||
<//>`;
|
||||
|
||||
return html`<li class="flex items-center my-[14px] gap-[8px]" id=${id}>
|
||||
<${Checkbox}
|
||||
...${{ _get: isActive, _set: setActive, _requireReload: false }}
|
||||
onchange=${(event) => (event.target.checked = true)}
|
||||
/>
|
||||
<${Input}
|
||||
icon="file-cog"
|
||||
...${{ _get: getName, _set: setName, _requireReload: false }}
|
||||
/>
|
||||
<${Button}
|
||||
icon="import"
|
||||
variant="sm"
|
||||
tagName="label"
|
||||
class="relative"
|
||||
onkeydown=${(event) => {
|
||||
if ([" ", "Enter"].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
$uploadInput.click();
|
||||
}
|
||||
}}
|
||||
>${$uploadInput} Import ${$uploadSuccess}${$uploadError}
|
||||
<//>
|
||||
<${Button} variant="sm" icon="upload" onclick=${downloadProfile}>Export<//>
|
||||
<div class="relative flex">${$delete}${$confirm}</div>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
function Profiles() {
|
||||
const { html, setState, useState, initDatabase } = globalThis.__enhancerApi,
|
||||
$input = html`<${Input} icon="file-cog" />`,
|
||||
$list = html`<ul></ul>`;
|
||||
|
||||
const db = initDatabase(),
|
||||
refreshProfiles = async () => {
|
||||
let profileIds = await db.get("profileIds");
|
||||
if (!profileIds?.length) profileIds = ["default"];
|
||||
const $profiles = profileIds.map((id) => {
|
||||
return document.getElementById(id) || html`<${Profile} id=${id} />`;
|
||||
});
|
||||
// replace rows one-by-one to avoid layout shift
|
||||
for (let i = 0; i < $profiles.length || i < $list.children.length; i++) {
|
||||
if ($profiles[i] === $list.children[i]) continue;
|
||||
if ($list.children[i]) {
|
||||
if ($profiles[i]) {
|
||||
$list.children[i].replaceWith($profiles[i]);
|
||||
} else $list.children[i].remove();
|
||||
} else $list.append($profiles[i]);
|
||||
}
|
||||
},
|
||||
addProfile = async () => {
|
||||
if (!$input.children[0].value) return;
|
||||
const name = $input.children[0].value,
|
||||
id = crypto.randomUUID();
|
||||
let profileIds = await db.get("profileIds");
|
||||
if (!profileIds?.length) profileIds = ["default"];
|
||||
await db.set("profileIds", [...profileIds, id]);
|
||||
await initDatabase([id]).set("profileName", name);
|
||||
$input.children[0].value = "";
|
||||
setState({ rerender: true });
|
||||
};
|
||||
useState(["rerender"], () => refreshProfiles());
|
||||
$input.onkeydown = (event) => {
|
||||
if (event.key === "Enter") addProfile();
|
||||
};
|
||||
|
||||
return html`
|
||||
<${Heading}>Profiles<//>
|
||||
<${Description}>
|
||||
Profiles can be used to preserve and switch between notion-enhancer
|
||||
configurations.
|
||||
<//>
|
||||
<div>
|
||||
${$list}
|
||||
<div class="flex items-center my-[14px] gap-[8px]">
|
||||
${$input}
|
||||
<${Button} variant="sm" icon="plus" onclick=${addProfile}>
|
||||
Add Profile
|
||||
<//>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export { Profiles };
|
146
src/core/menu/islands/Select.mjs
Normal file
@ -0,0 +1,146 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { Popup } from "./Popup.mjs";
|
||||
|
||||
function Option({ $icon = "", value = "", _get, _set }) {
|
||||
const { html, useState } = globalThis.__enhancerApi;
|
||||
return html`<div
|
||||
tabindex="0"
|
||||
role="option"
|
||||
class="select-none cursor-pointer rounded-[3px]
|
||||
flex items-center w-full h-[28px] px-[12px] leading-[1.2]
|
||||
transition duration-[20ms] focus:bg-[color:var(--theme--bg-hover)]"
|
||||
onmouseover=${(event) => event.target.focus()}
|
||||
onclick=${() => _set?.(value)}
|
||||
onkeydown=${(event) => {
|
||||
if (["Enter", " "].includes(event.key)) _set?.(value);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="mr-[6px] inline-flex items-center gap-[6px]
|
||||
text-[14px] text-ellipsis overflow-hidden"
|
||||
>
|
||||
${$icon}<span>${value}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function Select({
|
||||
_get,
|
||||
_set,
|
||||
_requireReload = true,
|
||||
values = [],
|
||||
popupMode = "left",
|
||||
maxWidth = 256,
|
||||
minWidth = 48,
|
||||
...props
|
||||
}) {
|
||||
const { html, extendProps, setState, useState } = globalThis.__enhancerApi,
|
||||
$selected = html`<i class="ml-auto i-check size-[16px]"></i>`,
|
||||
// dir="rtl" overflows to the left during transition
|
||||
$select = html`<div
|
||||
dir="rtl"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="appearance-none bg-transparent rounded-[4px]
|
||||
h-[28px] max-w-[${maxWidth}px] min-w-[${minWidth}px]
|
||||
cursor-pointer text-[14px] overflow-hidden pr-[28px]
|
||||
transition duration-[20ms] leading-[28px] pl-[8px]
|
||||
hover:bg-[color:var(--theme--bg-hover)]"
|
||||
></div>`,
|
||||
$popup = html`<div></div>`,
|
||||
onKeydown = (event) => {
|
||||
const intercept = () => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
if (event.key === "Escape") {
|
||||
intercept(setState({ rerender: true }));
|
||||
} else if (!options.length) return;
|
||||
// prettier-ignore
|
||||
const $next = options.find(({ $option }) => $option === event.target)
|
||||
?.$option.nextElementSibling ?? options.at(0).$option,
|
||||
$prev = options.find(({ $option }) => $option === event.target)
|
||||
?.$option.previousElementSibling ?? options.at(-1).$option;
|
||||
// overflow to opposite end of list from dir of travel
|
||||
if (event.key === "ArrowUp") intercept($prev.focus());
|
||||
if (event.key === "ArrowDown") intercept($next.focus());
|
||||
// re-enable natural tab behaviour in notion interface
|
||||
if (event.key === "Tab") event.stopPropagation();
|
||||
};
|
||||
|
||||
let options = [];
|
||||
const valueToOption = (opt) => {
|
||||
if (["string", "number"].includes(typeof opt)) opt = { value: opt };
|
||||
if (!(opt?.$icon instanceof Element)) {
|
||||
if (typeof opt?.$icon === "string") {
|
||||
opt.$icon = html`<i class="i-${opt.$icon} size-[16px]" />`;
|
||||
} else delete opt.$icon;
|
||||
}
|
||||
const $icon = opt.$icon?.cloneNode(true);
|
||||
return {
|
||||
...opt,
|
||||
$option: html`<${Option} ...${{ ...opt, _get, _set }} />`,
|
||||
$value: html`<div class="inline-flex text-nowrap items-center gap-[6px]">
|
||||
<!-- swap icon/value order for correct display when dir="rtl" -->
|
||||
<span>${opt.label || opt.value}</span>${$icon ?? ""}
|
||||
</div>`,
|
||||
};
|
||||
};
|
||||
$select.setValues = (values) => {
|
||||
options = values.map(valueToOption);
|
||||
$popup.innerHTML = "";
|
||||
$popup.append(...options.map(({ $option }) => $option));
|
||||
};
|
||||
$select.setValues(values);
|
||||
|
||||
let _initialValue;
|
||||
const getSelected = async () => {
|
||||
const value = (await _get?.()) ?? $select.innerText,
|
||||
option = options.find((opt) => opt.value === value);
|
||||
if (!option) _set?.(options[0].value);
|
||||
return option || options[0];
|
||||
};
|
||||
useState(["rerender"], async () => {
|
||||
if (!options.length) return;
|
||||
const { value, $value, $option } = await getSelected();
|
||||
$select.innerHTML = "";
|
||||
$select.append($value);
|
||||
$option.append($selected);
|
||||
if (_requireReload) {
|
||||
_initialValue ??= value;
|
||||
if (value !== _initialValue) setState({ databaseUpdated: true });
|
||||
}
|
||||
});
|
||||
|
||||
extendProps(props, { class: "notion-enhancer--menu-select relative" });
|
||||
return html`<div ...${props} setValues=${$select.setValues}>
|
||||
${$select}<${Popup}
|
||||
tabindex="0"
|
||||
trigger=${$select}
|
||||
mode=${popupMode}
|
||||
onopen=${() => document.addEventListener("keydown", onKeydown, true)}
|
||||
onbeforeclose=${() => {
|
||||
document.removeEventListener("keydown", onKeydown, true);
|
||||
$select.style.width = `${$select.offsetWidth}px`;
|
||||
$select.style.background = "transparent";
|
||||
}}
|
||||
onclose=${() => {
|
||||
$select.style.width = "";
|
||||
$select.style.background = "";
|
||||
}}
|
||||
>${$popup}
|
||||
<//>
|
||||
<i
|
||||
class="i-chevron-down pointer-events-none
|
||||
absolute right-[6px] top-[6px] size-[16px]
|
||||
text-[color:var(--theme--fg-secondary)]"
|
||||
></i>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export { Select };
|
110
src/core/menu/islands/Sidebar.mjs
Normal file
@ -0,0 +1,110 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { Description } from "./Description.mjs";
|
||||
|
||||
function SidebarHeading({}, ...children) {
|
||||
const { html } = globalThis.__enhancerApi;
|
||||
return html`<h2
|
||||
class="flex items-center font-semibold leading-none
|
||||
text-([12px] [color:var(--theme--fg-secondary)])
|
||||
h-[24px] px-[12px] mb-px mt-[18px] first:mt-[10px]"
|
||||
>
|
||||
${children}
|
||||
</h2>`;
|
||||
}
|
||||
|
||||
function SidebarButton({ id, icon, ...props }, ...children) {
|
||||
const { html, extendProps, setState, useState } = globalThis.__enhancerApi,
|
||||
$btn = html`<${props["href"] ? "a" : "button"}
|
||||
class="flex items-center select-none cursor-pointer text-[14px]
|
||||
transition hover:bg-[color:var(--theme--bg-hover)] disabled:hidden
|
||||
min-h-[27px] w-full my-px last:mb-[12px] px-[12px] rounded-[4px]"
|
||||
...${props}
|
||||
>
|
||||
${icon
|
||||
? html`<i
|
||||
class="i-${icon} ${icon.startsWith("notion-enhancer")
|
||||
? "size-[17px] ml-[1.5px] mr-[9.5px]"
|
||||
: "size-[18px] ml-px mr-[9px]"}"
|
||||
></i>`
|
||||
: ""}
|
||||
<span class="leading-[20px]">${children}</span>
|
||||
<//>`;
|
||||
|
||||
if (!props["href"]) {
|
||||
extendProps($btn, {
|
||||
onclick: () => setState({ transition: "fade", view: id }),
|
||||
});
|
||||
useState(["view"], ([view = "welcome"]) => {
|
||||
const active = view.toLowerCase() === id.toLowerCase();
|
||||
$btn.style.background = active ? "var(--theme--bg-hover)" : "";
|
||||
$btn.style.fontWeight = active ? "600" : "";
|
||||
});
|
||||
}
|
||||
return $btn;
|
||||
}
|
||||
|
||||
function Sidebar({ items, categories }) {
|
||||
const { html, useState } = globalThis.__enhancerApi,
|
||||
{ version, initDatabase, isEnabled } = globalThis.__enhancerApi,
|
||||
$agreeToUnlock = html`<span
|
||||
class="pt-[2px] pb-[5px] px-[15px] text-[12px]
|
||||
inline-block text-[color:var(--theme--fg-red)]"
|
||||
>To unlock the notion-enhancer's full functionality, agree to the privacy
|
||||
policy and terms & conditions on the welcome page.
|
||||
</span>`,
|
||||
$sidebar = html`<aside
|
||||
class="notion-enhancer--menu-sidebar h-full
|
||||
px-[4px] overflow-y-auto flex-(~ col) row-span-1
|
||||
bg-[color:var(--theme--bg-secondary)]"
|
||||
>
|
||||
${items.map((item) => {
|
||||
if (Array.isArray(item)) {
|
||||
const [title, desc] = Array.isArray(item) ? item : [item];
|
||||
return html`
|
||||
<${SidebarHeading}>${title}<//>
|
||||
<${Description}>${desc}<//>
|
||||
`;
|
||||
} else if (typeof item === "object") {
|
||||
const { title, ...props } = item;
|
||||
return html`<${SidebarButton} ...${props}>${title}<//>`;
|
||||
} else return html`<${SidebarHeading}>${item}<//>`;
|
||||
})}${$agreeToUnlock}
|
||||
</aside>`;
|
||||
useState(["rerender"], async () => {
|
||||
const agreedToTerms = await initDatabase().get("agreedToTerms");
|
||||
$agreeToUnlock.style.display = agreedToTerms === version ? "none" : "";
|
||||
[...$sidebar.children].forEach(($btn) => {
|
||||
if (!$btn.disableUntilAgreedToTerms) return;
|
||||
$btn.disabled = agreedToTerms !== version;
|
||||
});
|
||||
});
|
||||
|
||||
for (const { title, mods } of categories) {
|
||||
const $title = html`<${SidebarHeading}>${title}<//>`,
|
||||
$mods = mods
|
||||
.filter((mod) => mod.options?.length)
|
||||
.map((mod) => [
|
||||
mod.id,
|
||||
html`<${SidebarButton} id=${mod.id}>${mod.name}<//>`,
|
||||
]);
|
||||
$sidebar.append($title, ...$mods.map(([, $btn]) => $btn));
|
||||
|
||||
useState(["rerender"], async () => {
|
||||
let sectionVisible = false;
|
||||
for (const [id, $btn] of $mods) {
|
||||
$btn.disabled = !(await isEnabled(id));
|
||||
sectionVisible ||= !$btn.disabled;
|
||||
}
|
||||
$title.style.display = sectionVisible ? "" : "none";
|
||||
});
|
||||
}
|
||||
|
||||
return $sidebar;
|
||||
}
|
||||
|
||||
export { Sidebar };
|
53
src/core/menu/islands/Telemetry.mjs
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
// telemetry endpoint not ready, disabled for current release
|
||||
|
||||
import { collectTelemetryData } from "../../sendTelemetry.mjs";
|
||||
import { Option } from "./Options.mjs";
|
||||
|
||||
const privacyPolicy = "https://notion-enhancer.github.io/about/privacy-policy/";
|
||||
function Telemetry() {
|
||||
const { html, setState, useState, initDatabase } = globalThis.__enhancerApi,
|
||||
_get = async () => {
|
||||
// defaults to true, must be explicitly set to false to disable
|
||||
return (await initDatabase().get("telemetryEnabled")) ?? true;
|
||||
},
|
||||
_set = async (value) => {
|
||||
await initDatabase().set("telemetryEnabled", value);
|
||||
setState({ rerender: true });
|
||||
};
|
||||
|
||||
const $ = {
|
||||
platform: html`<code></code>`,
|
||||
version: html`<code></code>`,
|
||||
timezone: html`<code></code>`,
|
||||
enabled_mods: html`<code></code>`,
|
||||
};
|
||||
useState(["rerender"], async () => {
|
||||
const telemetryData = await collectTelemetryData();
|
||||
for (const key in telemetryData) {
|
||||
$[key].innerText = JSON.stringify(telemetryData[key]);
|
||||
}
|
||||
});
|
||||
|
||||
return html`<${Option}
|
||||
type="toggle"
|
||||
label="Telemetry"
|
||||
description=${html`If telemetry is enabled, usage data will be collected at
|
||||
a regular interval from your device in order to better understand how and
|
||||
where the notion-enhancer is used. This data is anonymous and includes
|
||||
only your platform (${$.platform}), notion-enhancer version
|
||||
(${$.version}), timezone (${$.timezone}), and enabled mods
|
||||
(${$.enabled_mods}). You can opt in or out of telemetry at any time. This
|
||||
setting syncs across configuration profiles. For more information, read
|
||||
the notion-enhancer's
|
||||
<a href=${privacyPolicy} class="ml-[3px]">privacy policy</a>.`}
|
||||
...${{ _get, _set }}
|
||||
/>`;
|
||||
}
|
||||
|
||||
export { Telemetry };
|
27
src/core/menu/islands/Tile.mjs
Normal file
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
function Tile({ icon, title, tagName, ...props }, ...children) {
|
||||
const { html, extendProps } = globalThis.__enhancerApi;
|
||||
extendProps(props, {
|
||||
class: `flex items-center gap-[12px] rounded-[4px]
|
||||
border-(~ [color:var(--theme--fg-border)]) px-[16px]
|
||||
bg-[color:var(--theme--bg-secondary)] py-[12px]
|
||||
hover:bg-[color:var(--theme--bg-hover)]`,
|
||||
});
|
||||
tagName ??= props["href"] ? "a" : "button";
|
||||
return html`<${tagName} ...${props}>
|
||||
<i class="i-${icon} text-[28px]"></i>
|
||||
<div>
|
||||
<h3 class="text-[14px] font-semibold">${title}</h3>
|
||||
<div class="text-([12px] [color:var(--theme--fg-secondary)])">
|
||||
${children}
|
||||
</div>
|
||||
</div>
|
||||
<//>`;
|
||||
}
|
||||
|
||||
export { Tile };
|
53
src/core/menu/islands/Toggle.mjs
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
function Toggle({ _get, _set, _requireReload = true, ...props }) {
|
||||
let _initialValue;
|
||||
const { html, extendProps, setState, useState } = globalThis.__enhancerApi,
|
||||
$input = html`<input
|
||||
type="checkbox"
|
||||
class="hidden [&:checked+div>div]:(
|
||||
bg-[color:var(--theme--accent-primary)]
|
||||
after:translate-x-[12px])"
|
||||
...${props}
|
||||
/>`;
|
||||
extendProps($input, { onchange: () => _set?.($input.checked) });
|
||||
useState(["rerender"], async () => {
|
||||
const checked = (await _get?.()) ?? $input.checked;
|
||||
$input.checked = checked;
|
||||
if (_requireReload) {
|
||||
_initialValue ??= checked;
|
||||
if (checked !== _initialValue) setState({ databaseUpdated: true });
|
||||
}
|
||||
});
|
||||
|
||||
return html`<div class="notion-enhancer--menu-toggle shrink-0">
|
||||
${$input}
|
||||
<div
|
||||
tabindex="0"
|
||||
class="w-[30px] h-[18px] rounded-[44px] cursor-pointer
|
||||
transition duration-200 bg-[color:var(--theme--bg-hover)]"
|
||||
onkeydown=${(event) => {
|
||||
if ([" ", "Enter"].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
$input.click();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="w-full h-full rounded-[44px] text-[12px]
|
||||
p-[2px] hover:bg-[color:var(--theme--bg-hover)]
|
||||
transition duration-200 after:(
|
||||
inline-block size-[14px] rounded-[44px]
|
||||
bg-[color:var(--theme--accent-primary\\_contrast)]
|
||||
transition duration-200 content-empty
|
||||
)"
|
||||
></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export { Toggle };
|
92
src/core/menu/islands/View.mjs
Normal file
@ -0,0 +1,92 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
function View({ id }, ...children) {
|
||||
const { html, setState, useState } = globalThis.__enhancerApi,
|
||||
// set padding on last child to maintain pad on overflow
|
||||
$view = html`<article
|
||||
id=${id}
|
||||
class="notion-enhancer--menu-view min-h-full w-full
|
||||
absolute px-[60px] py-[36px] min-w-[580px]"
|
||||
>
|
||||
${children}
|
||||
</article>`;
|
||||
|
||||
useState(["view"], ([view = "welcome"]) => {
|
||||
const [transition] = useState(["transition"]),
|
||||
isVisible = $view.style.display !== "none",
|
||||
nowActive = view.toLowerCase() === id.toLowerCase();
|
||||
|
||||
switch (transition) {
|
||||
case "fade": {
|
||||
const duration = 100,
|
||||
cssTransition = `opacity ${duration}ms`;
|
||||
if (isVisible && !nowActive) {
|
||||
$view.parentElement.style.overflow = "hidden";
|
||||
requestAnimationFrame(() => {
|
||||
$view.style.transition = cssTransition;
|
||||
$view.style.opacity = "0";
|
||||
setTimeout(() => ($view.style.display = "none"), duration);
|
||||
});
|
||||
} else if (!isVisible && nowActive) {
|
||||
setTimeout(() => {
|
||||
$view.style.opacity = "0";
|
||||
$view.style.display = "";
|
||||
requestAnimationFrame(() => {
|
||||
$view.style.transition = cssTransition;
|
||||
$view.style.opacity = "1";
|
||||
$view.parentElement.style.overflow = "";
|
||||
});
|
||||
}, duration);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "slide-to-left":
|
||||
case "slide-to-right": {
|
||||
const duration = 200,
|
||||
cssTransition = `opacity ${duration}ms, transform ${duration}ms`;
|
||||
if (isVisible && !nowActive) {
|
||||
$view.parentElement.style.overflow = "hidden";
|
||||
requestAnimationFrame(() => {
|
||||
$view.style.transition = cssTransition;
|
||||
$view.style.transform = `translateX(${
|
||||
transition === "slide-to-right" ? "-100%" : "100%"
|
||||
})`;
|
||||
$view.style.opacity = "0";
|
||||
setTimeout(() => {
|
||||
$view.style.display = "none";
|
||||
$view.style.transform = "";
|
||||
}, duration);
|
||||
});
|
||||
} else if (!isVisible && nowActive) {
|
||||
$view.style.transform = `translateX(${
|
||||
transition === "slide-to-right" ? "100%" : "-100%"
|
||||
})`;
|
||||
$view.style.opacity = "0";
|
||||
$view.style.display = "";
|
||||
requestAnimationFrame(() => {
|
||||
$view.style.transition = cssTransition;
|
||||
$view.style.transform = "";
|
||||
$view.style.opacity = "1";
|
||||
setTimeout(() => {
|
||||
$view.parentElement.style.overflow = "";
|
||||
}, duration);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
$view.style.transition = "";
|
||||
$view.style.opacity = nowActive ? "1" : "0";
|
||||
$view.style.display = nowActive ? "" : "none";
|
||||
}
|
||||
});
|
||||
return $view;
|
||||
}
|
||||
|
||||
export { View };
|
140
src/core/menu/menu.css
Normal file
@ -0,0 +1,140 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--theme--accent-primary_transparent);
|
||||
}
|
||||
*:focus-visible {
|
||||
outline: 3px solid var(--theme--accent-primary);
|
||||
}
|
||||
*:focus-visible[role="option"] {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-track,
|
||||
::-webkit-scrollbar-corner {
|
||||
background: var(--theme--scrollbar-track);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--theme--scrollbar-thumb);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--theme--scrollbar-thumb_hover);
|
||||
}
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
grid-template-columns: 240px auto;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
color: var(--theme--fg-primary);
|
||||
font-family: var(--font--sans);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body > #skeleton {
|
||||
background: rgba(86, 86, 86, 0.1);
|
||||
}
|
||||
body > #skeleton .row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 15px;
|
||||
margin: 2px 0;
|
||||
height: 27px;
|
||||
}
|
||||
body > #skeleton .shimmer {
|
||||
height: 14px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
background: rgba(86, 86, 86, 0.1);
|
||||
}
|
||||
body > #skeleton .shimmer.icon {
|
||||
margin-left: 1px;
|
||||
margin-right: 9px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
body > #skeleton .shimmer::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
animation: 1s linear infinite skeleton-shimmer;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0,
|
||||
rgba(86, 86, 86, 0.1) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
body > #skeleton .row-group {
|
||||
height: 24px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
body > #skeleton .row-group:first-child {
|
||||
margin-top: 10px;
|
||||
}
|
||||
body > #skeleton .row-group .shimmer {
|
||||
height: 11px;
|
||||
}
|
||||
|
||||
.typography mark {
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
background-color: var(--theme--bg-hover);
|
||||
color: inherit;
|
||||
}
|
||||
.typography code {
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
background-color: var(--theme--code-inline_bg);
|
||||
color: var(--theme--code-inline_fg);
|
||||
}
|
||||
.typography kbd {
|
||||
padding: 2px 4px;
|
||||
border-radius: 6px;
|
||||
border: solid 1px var(--theme--fg-border);
|
||||
box-shadow: inset 0 -1px 0 var(--theme--fg-border);
|
||||
}
|
||||
.typography a {
|
||||
text-decoration: underline;
|
||||
transition: 100ms ease-in;
|
||||
}
|
||||
.typography a:hover {
|
||||
color: var(--theme--accent-secondary);
|
||||
}
|
||||
|
||||
/* https://coloris.js.org/ */
|
||||
.clr-picker {
|
||||
background-color: var(--theme--bg-secondary) !important;
|
||||
}
|
||||
.clr-color {
|
||||
background-color: var(--theme--bg-hover) !important;
|
||||
border-color: var(--theme--fg-border) !important;
|
||||
color: var(--theme--fg-primary) !important;
|
||||
}
|
||||
.clr-preview:after,
|
||||
.clr-preview:before {
|
||||
border-color: var(--theme--fg-border) !important;
|
||||
}
|
277
src/core/menu/menu.mjs
Normal file
@ -0,0 +1,277 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { checkForUpdate, isDevelopmentBuild } from "../updateCheck.mjs";
|
||||
import { Sidebar } from "./islands/Sidebar.mjs";
|
||||
import { Footer } from "./islands/Footer.mjs";
|
||||
import { Banner } from "./islands/Banner.mjs";
|
||||
import { Onboarding } from "./islands/Onboarding.mjs";
|
||||
import { View } from "./islands/View.mjs";
|
||||
import { List } from "./islands/List.mjs";
|
||||
import { Mod } from "./islands/Mod.mjs";
|
||||
import { Options } from "./islands/Options.mjs";
|
||||
import { Profiles } from "./islands/Profiles.mjs";
|
||||
import { Description } from "./islands/Description.mjs";
|
||||
|
||||
let _apiImport, //
|
||||
_renderStarted,
|
||||
_stateHookedInto,
|
||||
_hotkeyRegistered;
|
||||
const categories = [
|
||||
{
|
||||
icon: "palette",
|
||||
id: "themes",
|
||||
title: "Themes",
|
||||
description: `Themes override Notion's colour schemes. Dark themes require
|
||||
Notion to be in dark mode and light themes require Notion to be in light
|
||||
mode. To switch between dark mode and light mode, go to <mark>Settings &
|
||||
members → My notifications & settings → My settings → Appearance</mark>.`,
|
||||
},
|
||||
{
|
||||
icon: "zap",
|
||||
id: "extensions",
|
||||
title: "Extensions",
|
||||
description: `Extensions add to the functionality and layout of the Notion
|
||||
client, interacting with and modifying existing interfaces.`,
|
||||
},
|
||||
// {
|
||||
// icon: "plug",
|
||||
// id: "integrations",
|
||||
// title: "Integrations",
|
||||
// description: `<span class="text-[color:var(--theme--fg-red)]">
|
||||
// Integrations access and modify Notion content. They interact directly with
|
||||
// <mark>https://www.notion.so/api/v3</mark>. Use at your own risk.</span>`,
|
||||
// },
|
||||
],
|
||||
sidebar = [
|
||||
"notion-enhancer",
|
||||
{
|
||||
id: "welcome",
|
||||
title: "Welcome",
|
||||
icon: "notion-enhancer",
|
||||
},
|
||||
{
|
||||
icon: "message-circle",
|
||||
title: "Community",
|
||||
href: "https://discord.gg/sFWPXtA",
|
||||
},
|
||||
{
|
||||
icon: "clock",
|
||||
title: "Changelog",
|
||||
href: "https://notion-enhancer.github.io/about/changelog/",
|
||||
},
|
||||
{
|
||||
icon: "book",
|
||||
title: "Documentation",
|
||||
href: "https://notion-enhancer.github.io/",
|
||||
},
|
||||
{
|
||||
icon: "github",
|
||||
title: "Source Code",
|
||||
href: "https://github.com/notion-enhancer",
|
||||
},
|
||||
"Settings",
|
||||
{
|
||||
id: "core",
|
||||
title: "Core",
|
||||
icon: "sliders-horizontal",
|
||||
disableUntilAgreedToTerms: true,
|
||||
},
|
||||
...categories.map((c) => ({
|
||||
id: c.id,
|
||||
title: c.title,
|
||||
icon: c.icon,
|
||||
disableUntilAgreedToTerms: true,
|
||||
})),
|
||||
];
|
||||
|
||||
const renderMenu = async () => {
|
||||
const { html, setState, useState } = globalThis.__enhancerApi,
|
||||
{ getMods, isEnabled, setEnabled } = globalThis.__enhancerApi,
|
||||
[theme, icon] = useState(["theme", "icon"]);
|
||||
if (!theme || !icon || _renderStarted) return;
|
||||
if (icon === "Monochrome") sidebar[1].icon += "?mask";
|
||||
_renderStarted = true;
|
||||
|
||||
const mods = await getMods();
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
const { id } = categories[i];
|
||||
categories[i].mods = mods.filter(({ _src }) => _src.startsWith(`${id}/`));
|
||||
categories[i].view = html`<${View} id=${id}>
|
||||
<${List} ...${categories[i]} />
|
||||
<//>`;
|
||||
}
|
||||
for (let i = 0; i < mods.length; i++) {
|
||||
const options = mods[i].options?.filter((opt) => opt.type !== "heading");
|
||||
if (mods[i]._src === "core" || !options?.length) continue;
|
||||
const _get = () => isEnabled(mods[i].id),
|
||||
_set = async (enabled) => {
|
||||
await setEnabled(mods[i].id, enabled);
|
||||
setState({ rerender: true });
|
||||
};
|
||||
mods[i].view = html`<${View} id=${mods[i].id}>
|
||||
<!-- passing an empty options array hides the settings button -->
|
||||
<${Mod} ...${{ ...mods[i], options: [], _get, _set }} />
|
||||
<${Options} mod=${mods[i]} />
|
||||
<//>`;
|
||||
}
|
||||
|
||||
const $sidebar = html`<${Sidebar}
|
||||
items=${sidebar}
|
||||
categories=${categories}
|
||||
/>`,
|
||||
$main = html`
|
||||
<main
|
||||
class="flex-(~ col) overflow-hidden transition-[height]"
|
||||
style="height: calc(100% + 65px)"
|
||||
>
|
||||
<!-- wrappers necessary for transitions and breakpoints -->
|
||||
<div class="grow overflow-auto">
|
||||
<div class="relative h-full w-full">
|
||||
<${View} id="welcome">
|
||||
<${Banner}
|
||||
updateAvailable=${await checkForUpdate()}
|
||||
isDevelopmentBuild=${await isDevelopmentBuild()}
|
||||
/>
|
||||
<${Onboarding} />
|
||||
<div
|
||||
class="p-6 rounded-[4px] mt-[16px] text-[14px]
|
||||
border border-[color:var(--theme--fg-red)]
|
||||
bg-[color:var(--theme--dim-red)] typography"
|
||||
>
|
||||
Hi there! Before you go any further, <b>please note that this update is
|
||||
not feature complete.</b> As part of an internal overhaul and the Chrome
|
||||
extension's upgrade to manifest v3, all themes and extensions must be
|
||||
ported manually across to the new version.
|
||||
<br />
|
||||
<br />
|
||||
The following extensions have not been updated yet but will be
|
||||
soon:
|
||||
<ul class="list-disc pl-6">
|
||||
<li>indentation lines</li>
|
||||
<li>view scale</li>
|
||||
<li>emoji sets</li>
|
||||
<li>simpler databases</li>
|
||||
<li>icon sets</li>
|
||||
<li>quick note</li>
|
||||
</ul>
|
||||
<br />
|
||||
The theming system is incomplete and only mostly recolours the
|
||||
app's interface. The following themes have not been updated
|
||||
yet but will be soon:
|
||||
<ul class="list-disc pl-6">
|
||||
<li>dark+</li>
|
||||
<li>light+</li>
|
||||
<li>nord</li>
|
||||
<li>dracula</li>
|
||||
<li>neutral</li>
|
||||
<li>cherry cola</li>
|
||||
<li>gruvbox dark</li>
|
||||
<li>gruvbox light</li>
|
||||
<li>pastel dark</li>
|
||||
<li>pinky boom</li>
|
||||
<li>playful purple</li>
|
||||
</ul>
|
||||
<br />
|
||||
In the meantime, the styling for these themes can be
|
||||
found <a href="https://github.com/notion-enhancer/repo"
|
||||
>here</a> and
|
||||
copy/pasted into your custom styles alongside the <a
|
||||
href="https://github.com/notion-enhancer/repo/blob/dev/theming/theme.css"
|
||||
>old theming system</a>, if you wish.
|
||||
<br />
|
||||
<br />
|
||||
The following extensions have been deprecated as their feature
|
||||
offerings are now available within Notion by default. Some
|
||||
features that belonged to these extensions have been merged
|
||||
into the notion-enhancer's core or into the tweaks extension:
|
||||
<ul class="list-disc pl-6">
|
||||
<li>integrated titlebar</li>
|
||||
<li>collapsible properties</li>
|
||||
<li>collapsible headers</li>
|
||||
<li>tray</li>
|
||||
<li>tabs</li>
|
||||
<li>weekly view</li>
|
||||
<li>truncated titles</li>
|
||||
<li>global block links</li>
|
||||
</ul>
|
||||
<br />
|
||||
A full changelog and updated documentation will be made
|
||||
available on the website as soon as possible. This release is
|
||||
being made available early in order to comply with Chrome's
|
||||
deprecation of manifest v2.
|
||||
</div>
|
||||
<//>
|
||||
<${View} id="core">
|
||||
<${Options} mod=${mods.find(({ _src }) => _src === "core")} />
|
||||
<${Profiles} />
|
||||
<//>
|
||||
${[...categories, ...mods]
|
||||
.filter(({ view }) => view)
|
||||
.map(({ view }) => view)}
|
||||
</div>
|
||||
</div>
|
||||
<${Footer} categories=${categories} />
|
||||
</main>
|
||||
`;
|
||||
useState(["footerOpen"], ([footerOpen]) => {
|
||||
$main.style.height = footerOpen ? "100%" : "calc(100% + 65px)";
|
||||
});
|
||||
|
||||
const $skeleton = document.querySelector("#skeleton");
|
||||
$skeleton.replaceWith($sidebar, $main);
|
||||
},
|
||||
registerHotkey = ([hotkey]) => {
|
||||
const { addKeyListener, setState, useState } = globalThis.__enhancerApi;
|
||||
if (!hotkey || _hotkeyRegistered) return;
|
||||
_hotkeyRegistered = true;
|
||||
addKeyListener(hotkey, (event) => {
|
||||
event.preventDefault();
|
||||
const msg = { channel: "notion-enhancer", action: "open-menu" };
|
||||
parent?.postMessage(msg, "*");
|
||||
});
|
||||
addKeyListener("Escape", () => {
|
||||
const [popupOpen] = useState(["popupOpen"]);
|
||||
if (document.activeElement?.tagName === "INPUT") {
|
||||
document.activeElement.blur();
|
||||
} else if (!popupOpen) {
|
||||
const msg = { channel: "notion-enhancer", action: "close-menu" };
|
||||
parent?.postMessage(msg, "*");
|
||||
} else setState({ rerender: true });
|
||||
});
|
||||
},
|
||||
updateTheme = ([theme]) => {
|
||||
if (theme === "dark") document.body.classList.add("dark");
|
||||
if (theme === "light") document.body.classList.remove("dark");
|
||||
};
|
||||
|
||||
const importApi = () => {
|
||||
return (_apiImport ??= (async () => {
|
||||
const api = globalThis.__enhancerApi;
|
||||
if (typeof api === "undefined") await import("../../api/system.js");
|
||||
await import("../../load.mjs").then((i) => i.default);
|
||||
})());
|
||||
},
|
||||
hookIntoState = () => {
|
||||
if (_stateHookedInto) return;
|
||||
_stateHookedInto = true;
|
||||
const { useState } = globalThis.__enhancerApi;
|
||||
useState(["theme"], updateTheme);
|
||||
useState(["hotkey"], registerHotkey);
|
||||
useState(["rerender"], renderMenu);
|
||||
};
|
||||
|
||||
addEventListener("message", async (event) => {
|
||||
if (event.data?.channel !== "notion-enhancer") return;
|
||||
await importApi().then(hookIntoState);
|
||||
const { setState, useState } = globalThis.__enhancerApi;
|
||||
setState({
|
||||
rerender: true,
|
||||
hotkey: event.data?.hotkey ?? useState(["hotkey"])[0],
|
||||
theme: event.data?.theme ?? useState(["theme"])[0],
|
||||
icon: event.data?.icon ?? useState(["icon"])[0],
|
||||
});
|
||||
});
|
88
src/core/mod.json
Normal file
@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "notion-enhancer",
|
||||
"version": "0.11.1",
|
||||
"id": "0f0bf8b6-eae6-4273-b307-8fc43f2ee082",
|
||||
"description": "Customise the all-in-one productivity workspace Notion.",
|
||||
"tags": ["core"],
|
||||
"authors": [
|
||||
{
|
||||
"name": "dragonwocky",
|
||||
"homepage": "https://dragonwocky.me/",
|
||||
"avatar": "https://dragonwocky.me/avatar.jpg"
|
||||
}
|
||||
],
|
||||
"options": [
|
||||
{ "type": "heading", "label": "Hotkeys" },
|
||||
{
|
||||
"type": "hotkey",
|
||||
"key": "openMenuHotkey",
|
||||
"description": "Opens the notion-enhancer menu from within Notion.",
|
||||
"value": "Ctrl+Shift+,"
|
||||
},
|
||||
{
|
||||
"type": "hotkey",
|
||||
"key": "togglePanelHotkey",
|
||||
"description": "Toggles the side panel used by some notion-enhancer extensions to display additional information and interfaces within the Notion app.",
|
||||
"value": "Ctrl+Shift+\\"
|
||||
},
|
||||
{
|
||||
"type": "hotkey",
|
||||
"key": "toggleWindowHotkey",
|
||||
"description": "Toggles focus of the Notion window anywhere, even when your Notion app isn't active.",
|
||||
"value": "Ctrl+Shift+A"
|
||||
},
|
||||
{ "type": "heading", "label": "Appearance" },
|
||||
{
|
||||
"type": "file",
|
||||
"key": "customStyles",
|
||||
"description": "Adds the styles from an uploaded .css file to Notion. Use this if you would like to customise the current theme or <a href=\"https://notion-enhancer.github.io/advanced/tweaks\">otherwise tweak Notion's appearance</a>.",
|
||||
"extensions": ["css"]
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"key": "loadThemeOverrides",
|
||||
"description": "Loads the styling required for a theme to customise Notion's interface. Disabling this may increase client performance, but will also disable all themes.",
|
||||
"values": ["Auto", "Enabled", "Disabled"]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "menuButtonLabel",
|
||||
"description": "Sets the text to label the notion-enhancer button added to Notion's sidebar with.",
|
||||
"value": "notion-enhancer"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"key": "menuButtonIconStyle",
|
||||
"description": "Sets whether the icon beside the notion-enhancer button added to Notion's sidebar should be coloured or monochrome. The latter style will match the theme's icon colour for users who would like the icon to be less noticeable.",
|
||||
"values": ["Colour", "Monochrome"]
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "peekPanelOnHover",
|
||||
"description": "Pops the side panel out to preview its content when hovering near the right edge of the window, in the same way that Notion's left-hand sidebar will slide out on hover. Disable this if you prefer to view the panel only by pinning it.",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"label": "Advanced",
|
||||
"_autoremoveIfSectionEmpty": false
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"label": "Custom JavaScript",
|
||||
"key": "customScript",
|
||||
"description": "Executes the uploaded userscript within Notion. Requires <a href='https://developer.chrome.com/docs/extensions/reference/api/userScripts#developer_mode_for_extension_users'>developer mode</a> to be enabled in your browser's extension settings to run in Chromium-based browsers.",
|
||||
"extensions": ["js"]
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "developerMode",
|
||||
"description": "Activates built-in debugging tools accessible through the application menu.",
|
||||
"platforms": ["linux", "win32", "darwin"],
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"clientStyles": ["variables.css", "../vendor/@unocss-preflight-tailwind.css"],
|
||||
"clientScripts": ["client.mjs"],
|
||||
"electronScripts": [[".webpack/main/index.js", "electron.cjs"]]
|
||||
}
|
47
src/core/sendTelemetry.mjs
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
// telemetry endpoint not ready, disabled for current release
|
||||
|
||||
const pingEndpoint = "https://notion-enhancer.deno.dev/api/ping",
|
||||
collectTelemetryData = async () => {
|
||||
const { platform, version } = globalThis.__enhancerApi,
|
||||
{ getMods, isEnabled } = globalThis.__enhancerApi,
|
||||
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
// prettier-ignore
|
||||
enabled_mods = (await getMods(async (mod) => {
|
||||
if (mod._src === "core") return false;
|
||||
return await isEnabled(mod.id);
|
||||
})).map(mod => mod.id);
|
||||
return { platform, version, timezone, enabled_mods };
|
||||
},
|
||||
sendTelemetryPing = async () => {
|
||||
// const db = __enhancerApi.initDatabase(),
|
||||
// { version } = globalThis.__enhancerApi,
|
||||
// agreedToTerms = await db.get("agreedToTerms"),
|
||||
// telemetryEnabled = (await db.get("telemetryEnabled")) ?? true;
|
||||
// if (!telemetryEnabled || agreedToTerms !== version) return;
|
||||
|
||||
// const lastTelemetryPing = await db.get("lastTelemetryPing");
|
||||
// if (lastTelemetryPing) {
|
||||
// const msSincePing = Date.now() - new Date(lastTelemetryPing);
|
||||
// // send ping only once a week
|
||||
// if (msSincePing / 8.64e7 < 7) return;
|
||||
// }
|
||||
|
||||
// try {
|
||||
// const telemetryData = await collectTelemetryData(),
|
||||
// pingTimestamp = await fetch(pingEndpoint, {
|
||||
// method: "POST",
|
||||
// body: JSON.stringify(telemetryData),
|
||||
// }).then((res) => res.text());
|
||||
// await db.set("lastTelemetryPing", pingTimestamp);
|
||||
// } catch (err) {
|
||||
// console.error(err);
|
||||
// }
|
||||
};
|
||||
|
||||
export { collectTelemetryData, sendTelemetryPing };
|
1
src/core/theme.css
Normal file
49
src/core/updateCheck.mjs
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
let _release;
|
||||
const repo = "notion-enhancer/notion-enhancer",
|
||||
endpoint = `https://api.github.com/repos/${repo}/releases/latest`,
|
||||
getRelease = async () => {
|
||||
const { version, readJson } = globalThis.__enhancerApi;
|
||||
try {
|
||||
_release ??= (await readJson(endpoint))?.tag_name.replace(/^v/, "");
|
||||
} catch {}
|
||||
_release ??= version;
|
||||
return _release;
|
||||
};
|
||||
|
||||
const parseVersion = (semver) => {
|
||||
while (semver.split("-")[0].split(".").length < 3) semver = `0.${semver}`;
|
||||
let [major, minor, patch, build] = semver.split("."),
|
||||
prerelease = patch.split("-")[1]?.split(".")[0];
|
||||
patch = patch.split("-")[0];
|
||||
return [major, minor, patch, prerelease, build]
|
||||
.map((v) => v ?? "")
|
||||
.map((v) => (/^\d+$/.test(v) ? parseInt(v) : v));
|
||||
},
|
||||
// is a < b
|
||||
greaterThan = (a, b) => {
|
||||
if (a && !b) return true;
|
||||
if (!a && b) return false;
|
||||
a = parseVersion(a);
|
||||
b = parseVersion(b);
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] > b[i]) return true;
|
||||
else if (a[i] < b[i]) return false;
|
||||
}
|
||||
};
|
||||
|
||||
const checkForUpdate = async () => {
|
||||
const { version } = globalThis.__enhancerApi;
|
||||
return greaterThan(await getRelease(), version) ? _release : false;
|
||||
},
|
||||
isDevelopmentBuild = async () => {
|
||||
const { version } = globalThis.__enhancerApi;
|
||||
return !(await checkForUpdate()) && version !== _release;
|
||||
};
|
||||
|
||||
export { checkForUpdate, isDevelopmentBuild, greaterThan };
|
181
src/core/variables.css
Normal file
@ -0,0 +1,181 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
body.dark {
|
||||
--theme--fg-primary: rgba(255, 255, 255, 0.81);
|
||||
--theme--fg-secondary: rgb(155, 155, 155);
|
||||
--theme--fg-border: rgb(47, 47, 47);
|
||||
--theme--fg-gray: rgba(155, 155, 155, 1);
|
||||
--theme--fg-brown: rgba(186, 133, 111, 1);
|
||||
--theme--fg-orange: rgba(199, 125, 72, 1);
|
||||
--theme--fg-yellow: rgba(202, 152, 73, 1);
|
||||
--theme--fg-green: rgba(82, 158, 114, 1);
|
||||
--theme--fg-blue: rgba(94, 135, 201, 1);
|
||||
--theme--fg-purple: rgba(157, 104, 211, 1);
|
||||
--theme--fg-pink: rgba(209, 87, 150, 1);
|
||||
--theme--fg-red: rgba(223, 84, 82, 1);
|
||||
|
||||
--theme--bg-primary: rgb(25, 25, 25);
|
||||
--theme--bg-secondary: rgb(32, 32, 32);
|
||||
--theme--bg-hover: rgba(255, 255, 255, 0.055);
|
||||
--theme--bg-overlay: rgba(15, 15, 15, 0.8);
|
||||
--theme--bg-light_gray: rgb(55, 55, 55);
|
||||
--theme--bg-gray: rgb(90, 90, 90);
|
||||
--theme--bg-brown: rgb(96, 59, 44);
|
||||
--theme--bg-orange: rgb(133, 76, 29);
|
||||
--theme--bg-yellow: rgb(137, 99, 42);
|
||||
--theme--bg-green: rgb(43, 89, 63);
|
||||
--theme--bg-blue: rgb(40, 69, 108);
|
||||
--theme--bg-purple: rgb(73, 47, 100);
|
||||
--theme--bg-pink: rgb(105, 49, 76);
|
||||
--theme--bg-red: rgb(110, 54, 48);
|
||||
|
||||
--theme--dim-light_gray: rgb(28, 28, 28);
|
||||
--theme--dim-gray: rgb(32, 32, 32);
|
||||
--theme--dim-brown: rgb(35, 30, 28);
|
||||
--theme--dim-orange: rgb(37, 31, 27);
|
||||
--theme--dim-yellow: rgb(35, 31, 26);
|
||||
--theme--dim-green: rgb(29, 34, 32);
|
||||
--theme--dim-blue: rgb(27, 31, 34);
|
||||
--theme--dim-purple: rgb(31, 29, 33);
|
||||
--theme--dim-pink: rgb(35, 28, 31);
|
||||
--theme--dim-red: rgb(36, 30, 29);
|
||||
|
||||
--theme--accent-primary: rgb(35, 131, 226);
|
||||
--theme--accent-primary_hover: rgb(0, 117, 211);
|
||||
--theme--accent-primary_contrast: rgb(255, 255, 255);
|
||||
--theme--accent-primary_transparent: rgba(35, 131, 226, 0.14);
|
||||
--theme--accent-secondary: rgb(235, 87, 87);
|
||||
--theme--accent-secondary_hover: rgba(235, 87, 87, 0.1);
|
||||
--theme--accent-secondary_contrast: white;
|
||||
|
||||
--theme--scrollbar-track: rgba(202, 204, 206, 0.04);
|
||||
--theme--scrollbar-thumb: #474c50;
|
||||
--theme--scrollbar-thumb_hover: rgba(202, 204, 206, 0.3);
|
||||
|
||||
--theme--code-inline_fg: #eb5757;
|
||||
--theme--code-inline_bg: rgba(135, 131, 120, 0.15);
|
||||
--theme--code-block_fg: rgba(255, 255, 255, 0.81);
|
||||
--theme--code-block_bg: rgba(255, 255, 255, 0.03);
|
||||
--theme--code-keyword: rgb(209, 148, 158);
|
||||
--theme--code-builtin: rgb(189, 224, 82);
|
||||
--theme--code-class_name: rgba(255, 255, 255, 0.81);
|
||||
--theme--code-function: var(--theme--code-class_name);
|
||||
--theme--code-boolean: var(--theme--code-keyword);
|
||||
--theme--code-number: var(--theme--code-keyword);
|
||||
--theme--code-string: var(--theme--code-builtin);
|
||||
--theme--code-char: var(--theme--code-builtin);
|
||||
--theme--code-symbol: var(--theme--code-keyword);
|
||||
--theme--code-regex: rgb(238, 153, 0);
|
||||
--theme--code-url: rgb(245, 184, 61);
|
||||
--theme--code-operator: var(--theme--code-url);
|
||||
--theme--code-variable: var(--theme--code-url);
|
||||
--theme--code-constant: var(--theme--code-keyword);
|
||||
--theme--code-property: var(--theme--code-keyword);
|
||||
--theme--code-punctuation: var(--theme--code-class_name);
|
||||
--theme--code-important: var(--theme--code-regex);
|
||||
--theme--code-comment: rgb(153, 128, 102);
|
||||
--theme--code-tag: var(--theme--code-keyword);
|
||||
--theme--code-attr_name: var(--theme--code-builtin);
|
||||
--theme--code-attr_value: var(--theme--code-keyword);
|
||||
--theme--code-namespace: var(--theme--code-class_name);
|
||||
--theme--code-prolog: var(--theme--code-comment);
|
||||
--theme--code-doctype: var(--theme--code-comment);
|
||||
--theme--code-cdata: var(--theme--code-comment);
|
||||
--theme--code-entity: var(--theme--code-url);
|
||||
--theme--code-atrule: var(--theme--code-keyword);
|
||||
--theme--code-selector: var(--theme--code-builtin);
|
||||
--theme--code-inserted: var(--theme--code-builtin);
|
||||
--theme--code-deleted: rgb(255, 0, 0);
|
||||
}
|
||||
|
||||
body:not(.dark) {
|
||||
--theme--fg-primary: rgb(55, 53, 47);
|
||||
--theme--fg-secondary: rgba(25, 23, 17, 0.6);
|
||||
--theme--fg-border: rgb(233, 233, 231);
|
||||
--theme--fg-gray: rgba(120, 119, 116, 1);
|
||||
--theme--fg-brown: rgba(159, 107, 83, 1);
|
||||
--theme--fg-orange: rgba(217, 115, 13, 1);
|
||||
--theme--fg-yellow: rgba(203, 145, 47, 1);
|
||||
--theme--fg-green: rgba(68, 131, 97, 1);
|
||||
--theme--fg-blue: rgba(51, 126, 169, 1);
|
||||
--theme--fg-purple: rgba(144, 101, 176, 1);
|
||||
--theme--fg-pink: rgba(193, 76, 138, 1);
|
||||
--theme--fg-red: rgba(212, 76, 71, 1);
|
||||
|
||||
--theme--bg-primary: white;
|
||||
--theme--bg-secondary: rgb(251, 251, 250);
|
||||
--theme--bg-hover: rgba(55, 53, 47, 0.08);
|
||||
--theme--bg-overlay: rgba(15, 15, 15, 0.6);
|
||||
--theme--bg-light_gray: rgba(227, 226, 224, 0.5);
|
||||
--theme--bg-gray: rgb(227, 226, 224);
|
||||
--theme--bg-brown: rgb(238, 224, 218);
|
||||
--theme--bg-orange: rgb(250, 222, 201);
|
||||
--theme--bg-yellow: rgb(253, 236, 200);
|
||||
--theme--bg-green: rgb(219, 237, 219);
|
||||
--theme--bg-blue: rgb(211, 229, 239);
|
||||
--theme--bg-purple: rgb(232, 222, 238);
|
||||
--theme--bg-pink: rgb(245, 224, 233);
|
||||
--theme--bg-red: rgb(255, 226, 221);
|
||||
|
||||
--theme--dim-light_gray: rgba(249, 249, 245, 0.5);
|
||||
--theme--dim-gray: rgba(247, 247, 245, 0.7);
|
||||
--theme--dim-brown: rgba(250, 246, 245, 0.7);
|
||||
--theme--dim-orange: rgba(252, 245, 242, 0.7);
|
||||
--theme--dim-yellow: rgba(250, 247, 237, 0.7);
|
||||
--theme--dim-green: rgba(244, 248, 243, 0.7);
|
||||
--theme--dim-blue: rgba(241, 248, 251, 0.7);
|
||||
--theme--dim-purple: rgba(249, 246, 252, 0.7);
|
||||
--theme--dim-pink: rgba(251, 245, 251, 0.7);
|
||||
--theme--dim-red: rgba(253, 245, 243, 0.7);
|
||||
|
||||
--theme--accent-primary: rgb(35, 131, 226);
|
||||
--theme--accent-primary_hover: rgb(0, 117, 211);
|
||||
--theme--accent-primary_contrast: rgb(255, 255, 255);
|
||||
--theme--accent-primary_transparent: rgba(35, 131, 226, 0.14);
|
||||
--theme--accent-secondary: rgb(235, 87, 87);
|
||||
--theme--accent-secondary_hover: rgba(235, 87, 87, 0.1);
|
||||
--theme--accent-secondary_contrast: white;
|
||||
|
||||
--theme--scrollbar-track: #edece9;
|
||||
--theme--scrollbar-thumb: #d3d1cb;
|
||||
--theme--scrollbar-thumb_hover: #aeaca6;
|
||||
|
||||
--theme--code-inline_fg: #eb5757;
|
||||
--theme--code-inline_bg: rgba(135, 131, 120, 0.15);
|
||||
--theme--code-block_fg: rgb(55, 53, 47);
|
||||
--theme--code-block_bg: rgb(247, 246, 243);
|
||||
--theme--code-keyword: rgb(0, 119, 170);
|
||||
--theme--code-builtin: rgb(102, 153, 0);
|
||||
--theme--code-class_name: rgb(221, 74, 104);
|
||||
--theme--code-function: var(--theme--code-class_name);
|
||||
--theme--code-boolean: rgb(153, 0, 85);
|
||||
--theme--code-number: var(--theme--code-boolean);
|
||||
--theme--code-string: var(--theme--code-builtin);
|
||||
--theme--code-char: var(--theme--code-builtin);
|
||||
--theme--code-symbol: var(--theme--code-boolean);
|
||||
--theme--code-regex: rgb(238, 153, 0);
|
||||
--theme--code-url: rgb(154, 110, 58);
|
||||
--theme--code-operator: var(--theme--code-url);
|
||||
--theme--code-variable: var(--theme--code-regex);
|
||||
--theme--code-constant: var(--theme--code-boolean);
|
||||
--theme--code-property: var(--theme--code-boolean);
|
||||
--theme--code-punctuation: rgb(153, 153, 153);
|
||||
--theme--code-important: var(--theme--code-regex);
|
||||
--theme--code-comment: rgb(112, 128, 144);
|
||||
--theme--code-tag: var(--theme--code-boolean);
|
||||
--theme--code-attr_name: var(--theme--code-builtin);
|
||||
--theme--code-attr_value: var(--theme--code-keyword);
|
||||
--theme--code-namespace: rgb(55, 53, 47);
|
||||
--theme--code-prolog: var(--theme--code-comment);
|
||||
--theme--code-doctype: var(--theme--code-comment);
|
||||
--theme--code-cdata: var(--theme--code-comment);
|
||||
--theme--code-entity: var(--theme--code-url);
|
||||
--theme--code-atrule: var(--theme--code-keyword);
|
||||
--theme--code-selector: var(--theme--code-builtin);
|
||||
--theme--code-inserted: var(--theme--code-builtin);
|
||||
--theme--code-deleted: var(--theme--code-boolean);
|
||||
}
|
10
src/extensions/emoji-sets/client.css
Normal file
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* notion-enhancer: emoji sets
|
||||
* (c) 2021 Arecsu
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
[aria-label][role='image'][style*='Apple Color Emoji']:not([data-emoji-sets-unsupported]) {
|
||||
margin-left: 2.5px !important;
|
||||
}
|
71
src/extensions/emoji-sets/client.mjs
Normal file
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* notion-enhancer: emoji sets
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
export default async function ({ web, env }, db) {
|
||||
const style = await db.get(['style']),
|
||||
// real emojis are used on macos instead of the twitter set
|
||||
nativeEmojiSelector = `[aria-label][role="image"][style*="Apple Color Emoji"]:not([data-emoji-sets-unsupported])`,
|
||||
imgEmojiSelector = '.notion-emoji:not([data-emoji-sets-unsupported])',
|
||||
imgEmojiOverlaySelector = `${imgEmojiSelector} + [src*="notion-emojis"]`;
|
||||
|
||||
await Promise.any([web.whenReady([nativeEmojiSelector]), web.whenReady([imgEmojiSelector])]);
|
||||
|
||||
const nativeEmojis = document.querySelectorAll(nativeEmojiSelector).length,
|
||||
imgEmojis = document.querySelectorAll(imgEmojiSelector).length;
|
||||
|
||||
const unsupportedEmojis = [],
|
||||
emojiReqs = new Map(),
|
||||
getEmoji = async (emoji) => {
|
||||
emoji = encodeURIComponent(emoji);
|
||||
if (unsupportedEmojis.includes(emoji)) return undefined;
|
||||
try {
|
||||
if (!emojiReqs.get(emoji)) {
|
||||
emojiReqs.set(emoji, fetch(`https://emojicdn.elk.sh/${emoji}?style=${style}`));
|
||||
}
|
||||
const res = await emojiReqs.get(emoji);
|
||||
if (!res.ok) throw new Error();
|
||||
return `url("https://emojicdn.elk.sh/${emoji}?style=${style}") 100% 100% / 100%`;
|
||||
} catch {
|
||||
unsupportedEmojis.push(emoji);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
if (nativeEmojis) {
|
||||
const updateEmojis = async () => {
|
||||
const $emojis = document.querySelectorAll(nativeEmojiSelector);
|
||||
for (const $emoji of $emojis) {
|
||||
const emojiSrc = await getEmoji($emoji.ariaLabel);
|
||||
if (emojiSrc) {
|
||||
$emoji.style.background = emojiSrc;
|
||||
$emoji.style.width = '1em';
|
||||
$emoji.style.height = '1em';
|
||||
$emoji.style.display = 'inline-block';
|
||||
$emoji.innerText = '';
|
||||
} else $emoji.dataset.emojiSetsUnsupported = true;
|
||||
}
|
||||
};
|
||||
web.addDocumentObserver(updateEmojis, [nativeEmojiSelector]);
|
||||
}
|
||||
|
||||
if (style !== 'twitter' && imgEmojis) {
|
||||
const updateEmojis = async () => {
|
||||
const $emojis = document.querySelectorAll(imgEmojiSelector);
|
||||
for (const $emoji of $emojis) {
|
||||
const emojiSrc = await getEmoji($emoji.ariaLabel);
|
||||
if (emojiSrc) {
|
||||
$emoji.style.background = emojiSrc;
|
||||
$emoji.style.opacity = 1;
|
||||
if ($emoji.nextElementSibling?.matches?.(imgEmojiOverlaySelector)) {
|
||||
$emoji.nextElementSibling.style.opacity = 0;
|
||||
}
|
||||
} else $emoji.dataset.emojiSetsUnsupported = true;
|
||||
}
|
||||
};
|
||||
updateEmojis();
|
||||
web.addDocumentObserver(updateEmojis, [imgEmojiSelector, imgEmojiOverlaySelector]);
|
||||
}
|
||||
}
|
BIN
src/extensions/emoji-sets/emoji-sets.jpg
Normal file
After Width: | Height: | Size: 71 KiB |
46
src/extensions/emoji-sets/mod.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "emoji sets",
|
||||
"id": "a2401ee1-93ba-4b8c-9781-7f570bf5d71e",
|
||||
"version": "0.4.0",
|
||||
"description": "pick from a variety of emoji styles to use.",
|
||||
"preview": "emoji-sets.jpg",
|
||||
"tags": ["extension", "customisation"],
|
||||
"authors": [
|
||||
{
|
||||
"name": "dragonwocky",
|
||||
"email": "thedragonring.bod@gmail.com",
|
||||
"homepage": "https://dragonwocky.me/",
|
||||
"avatar": "https://dragonwocky.me/avatar.jpg"
|
||||
}
|
||||
],
|
||||
"js": {
|
||||
"client": ["client.mjs"]
|
||||
},
|
||||
"css": {
|
||||
"client": ["client.css"]
|
||||
},
|
||||
"options": [
|
||||
{
|
||||
"type": "select",
|
||||
"key": "style",
|
||||
"label": "emoji style",
|
||||
"tooltip": "**initial use may involve some lag and load-time for emojis until they have all been cached**",
|
||||
"values": [
|
||||
"twitter",
|
||||
"apple",
|
||||
"google",
|
||||
"microsoft",
|
||||
"samsung",
|
||||
"whatsapp",
|
||||
"facebook",
|
||||
"messenger",
|
||||
"joypixels",
|
||||
"openmoji",
|
||||
"emojidex",
|
||||
"lg",
|
||||
"htc",
|
||||
"mozilla"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
25
src/extensions/focus/client.css
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* notion-enhancer: focus
|
||||
* (c) 2020 Arecsu
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
/* hide topbar and ai */
|
||||
.notion-sidebar-container[aria-hidden] ~ div {
|
||||
:is(.notion-topbar, .notion-help-button, .notion-ai-button) {
|
||||
opacity: 0 !important;
|
||||
transition: opacity 200ms ease-in-out !important;
|
||||
}
|
||||
.notion-topbar:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* hide tabs */
|
||||
body > #root.sidebar-collapsed {
|
||||
transition: opacity 200ms ease-in-out;
|
||||
&:not(:hover) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
23
src/extensions/focus/client.mjs
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* notion-enhancer: focus
|
||||
* (c) 2020 Arecsu
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
export default async (api, db) => {
|
||||
// tabs can only be hidden in the desktop app
|
||||
const { platform, sendMessage, addMutationListener } = api;
|
||||
if (!["linux", "win32", "darwin"].includes(platform)) return;
|
||||
|
||||
let _state;
|
||||
const sidebar = ".notion-sidebar-container",
|
||||
onUpdate = () => {
|
||||
const $sidebar = document.querySelector(sidebar),
|
||||
state = $sidebar.hasAttribute("aria-hidden") ? "collapsed" : "pinned";
|
||||
if (state === _state) return;
|
||||
sendMessage("notion-enhancer:focus", "sidebar-" + (_state = state));
|
||||
};
|
||||
addMutationListener(sidebar, onUpdate, { childList: false, subtree: false });
|
||||
onUpdate();
|
||||
};
|
17
src/extensions/focus/electron.cjs
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* notion-enhancer: focus
|
||||
* (c) 2020 Arecsu
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
module.exports = async (api, db) => {
|
||||
const { ipcMain, BrowserWindow } = require("electron"),
|
||||
channel = "notion-enhancer:focus";
|
||||
ipcMain.on(channel, ({ sender }, message) => {
|
||||
const views = BrowserWindow.fromWebContents(sender).getBrowserViews();
|
||||
for (const view of views) view.webContents.send(channel, message);
|
||||
});
|
||||
};
|
25
src/extensions/focus/mod.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "Focus",
|
||||
"version": "0.4.0",
|
||||
"id": "5a08598d-bfac-4167-9ae8-2bd0e2ef141e",
|
||||
"description": "Enter focus mode when the left sidebar is closed, hiding Notion's extraneous interface elements (e.g. the topbar) until they are hovered over.",
|
||||
"tags": ["productivity", "focus-mode"],
|
||||
"authors": [
|
||||
{
|
||||
"name": "dragonwocky",
|
||||
"homepage": "https://dragonwocky.me/",
|
||||
"avatar": "https://dragonwocky.me/avatar.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Arecsu",
|
||||
"homepage": "https://github.com/Arecsu",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/12679098"
|
||||
}
|
||||
],
|
||||
"clientStyles": ["client.css"],
|
||||
"clientScripts": ["client.mjs"],
|
||||
"electronScripts": [
|
||||
[".webpack/main/index.js", "electron.cjs"],
|
||||
[".webpack/renderer/tabs/preload.js", "tabs.cjs"]
|
||||
]
|
||||
}
|
18
src/extensions/focus/tabs.cjs
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* notion-enhancer: focus
|
||||
* (c) 2020 Arecsu
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
module.exports = async (api, db) => {
|
||||
const { onMessage } = api,
|
||||
focusClass = "sidebar-collapsed";
|
||||
onMessage("notion-enhancer:focus", (message) => {
|
||||
const $root = document.querySelector("#root");
|
||||
if (message === "sidebar-pinned") $root?.classList.remove(focusClass);
|
||||
if (message === "sidebar-collapsed") $root?.classList.add(focusClass);
|
||||
});
|
||||
};
|
52
src/extensions/fonts/client.css
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* notion-enhancer: fonts
|
||||
* (c) 2021 TorchAtlas (https://github.com/torchatlas/)
|
||||
* (c) 2021 admiraldus (https://github.com/admiraldus)
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
:root {
|
||||
--font--sans: ui-sans-serif, -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif,
|
||||
"Segoe UI Emoji", "Segoe UI Symbol";
|
||||
--font--serif: Lyon-Text, Georgia, ui-serif, serif;
|
||||
--font--mono: iawriter-mono, Nitti, Menlo, Courier, monospace;
|
||||
--font--code: "SFMono-Regular", Menlo, Consolas, "PT Mono",
|
||||
"Liberation Mono", Courier, monospace;
|
||||
--font--math: KaTeX_Main, Times New Roman, serif;
|
||||
--font--quotes: inherit;
|
||||
--font--headings: inherit;
|
||||
}
|
||||
|
||||
[style*="Segoe UI"] {
|
||||
font-family: var(--font--sans) !important;
|
||||
}
|
||||
|
||||
[style*="Georgia"] {
|
||||
font-family: var(--font--serif) !important;
|
||||
}
|
||||
|
||||
[style*="iawriter-mono"] {
|
||||
font-family: var(--font--mono) !important;
|
||||
}
|
||||
|
||||
[style*=SFMono-Regular] {
|
||||
font-family: var(--font--code) !important;
|
||||
}
|
||||
|
||||
[placeholder='Untitled'],
|
||||
[placeholder='Heading 1'],
|
||||
[placeholder='Heading 2'],
|
||||
[placeholder='Heading 3'] {
|
||||
font-family: var(--font--headings) !important;
|
||||
}
|
||||
|
||||
.notion-quote-block {
|
||||
font-family: var(--font--quotes) !important;
|
||||
}
|
||||
|
||||
.katex,
|
||||
.katex * {
|
||||
font-family: var(--font--math) !important;
|
||||
}
|
23
src/extensions/fonts/client.mjs
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* notion-enhancer: fonts
|
||||
* (c) 2021 TorchAtlas (https://github.com/torchatlas/)
|
||||
* (c) 2021 admiraldus (https://github.com/admiraldus
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
export default async (api, db) => {
|
||||
const $root = document.documentElement;
|
||||
for (const style of [
|
||||
"sans",
|
||||
"serif",
|
||||
"mono",
|
||||
"code",
|
||||
"math",
|
||||
"quotes",
|
||||
"headings",
|
||||
]) {
|
||||
const font = await db.get(style);
|
||||
if (font) $root.style.setProperty(`--font--${style}`, font);
|
||||
}
|
||||
};
|
66
src/extensions/fonts/mod.json
Normal file
@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "Fonts",
|
||||
"version": "0.5.0",
|
||||
"id": "e0d8d148-45e7-4d79-8313-e7b2ad8abe16",
|
||||
"description": "Replace Notion's default fonts with any font installed on your system.",
|
||||
"tags": ["customisation", "font-chooser"],
|
||||
"authors": [
|
||||
{
|
||||
"name": "dragonwocky",
|
||||
"homepage": "https://dragonwocky.me/",
|
||||
"avatar": "https://dragonwocky.me/avatar.jpg"
|
||||
},
|
||||
{
|
||||
"name": "TorchAtlas",
|
||||
"homepage": "https://github.com/torchatlas/",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/12666855"
|
||||
}
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"type": "text",
|
||||
"key": "sans",
|
||||
"label": "Sans serif",
|
||||
"description": "Sets the font used across Notion's interface and as the default page font. Leave this blank to use Notion's default sans serif font.",
|
||||
"value": "ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, \"Apple Color Emoji\", Arial, sans-serif, \"Segoe UI Emoji\", \"Segoe UI Symbol\""
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "serif",
|
||||
"description": "Sets the font used on serif-styled pages (configurable via the <i class='i-ellipsis -mb-px'></i> <i>Style, export and more...</i> menu). Leave this blank to use Notion's default serif font.",
|
||||
"value": "Lyon-Text, Georgia, ui-serif, serif"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "mono",
|
||||
"description": "Sets the font used on mono-styled pages (configurable via the <i class='i-ellipsis -mb-px'></i> <i>Style, export and more...</i> menu). Leave this blank to use Notion's default monospaced font.",
|
||||
"value": "iawriter-mono, Nitti, Menlo, Courier, monospace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "code",
|
||||
"description": "Sets the font used for code blocks and inline code. Leave this blank to use Notion's default code font.",
|
||||
"value": "\"SFMono-Regular\", Menlo, Consolas, \"PT Mono\", \"Liberation Mono\", Courier, monospace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "math",
|
||||
"description": "Sets the font used for math equations. Leave this blank to use Notion's default math font.",
|
||||
"value": "KaTeX_Main, Times New Roman, serif"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "quotes",
|
||||
"description": "Sets the font used for quote blocks. Leave this blank to inherit the page's font style.",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "headings",
|
||||
"description": "Sets the font used for page headings. Leave this blank to inherit the page's font style.",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"clientStyles": ["client.css"],
|
||||
"clientScripts": ["client.mjs"]
|
||||
}
|
84
src/extensions/indent-guides/client.css
Normal file
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* notion-enhancer: indent guides
|
||||
* (c) 2020 Alexa Baldon <alnbaldon@gmail.com> (https://github.com/runargs)
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
body {
|
||||
--guide--style: solid;
|
||||
--guide--color: var(--theme--fg-border);
|
||||
--guide--opacity: 0;
|
||||
}
|
||||
|
||||
/* add indent guides to nested blocks */
|
||||
.notion-header-block,
|
||||
.notion-sub_header-block,
|
||||
.notion-sub_sub_header-block,
|
||||
.notion-toggle-block,
|
||||
.notion-to_do-block,
|
||||
.notion-bulleted_list-block,
|
||||
.notion-numbered_list-block {
|
||||
--guide--offset: 32px;
|
||||
--guide--indent: 14px;
|
||||
position: relative;
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: calc(100% - var(--guide--offset));
|
||||
top: var(--guide--offset);
|
||||
margin-inline-start: var(--guide--indent);
|
||||
border-left: 1px var(--guide--style) var(--guide--color);
|
||||
opacity: var(--guide--opacity);
|
||||
}
|
||||
}
|
||||
|
||||
.notion-header-block {
|
||||
--guide--offset: 47px;
|
||||
}
|
||||
.notion-sub_header-block {
|
||||
--guide--offset: 40px;
|
||||
}
|
||||
.notion-header-block,
|
||||
.notion-sub_header-block,
|
||||
.notion-sub_sub_header-block,
|
||||
.notion-toggle-block {
|
||||
--guide--indent: 13.4px;
|
||||
}
|
||||
|
||||
/* add indent guides to toc blocks & the outliner */
|
||||
.notion-table_of_contents-block
|
||||
[contenteditable="false"]
|
||||
a
|
||||
> div:not([style*="margin-left: 0"]),
|
||||
.notion-enhancer--outliner-heading:not(.pl-\[18px\]) {
|
||||
position: relative;
|
||||
--guide--indent: -16px;
|
||||
&:before {
|
||||
content: "";
|
||||
top: 0;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
margin-inline-start: var(--guide--indent);
|
||||
border-left: 1px var(--guide--style) var(--guide--color);
|
||||
opacity: var(--guide--opacity);
|
||||
}
|
||||
}
|
||||
|
||||
.notion-enhancer--outliner-heading:not(.pl-\[18px\]) {
|
||||
--guide--indent: -12px;
|
||||
}
|
||||
|
||||
/* add solid background to drag handles,
|
||||
otherwise guides show through underneath */
|
||||
[role="button"]:is([aria-label="Drag"], [aria-label^="Click to add below"]) {
|
||||
position: relative;
|
||||
&:before {
|
||||
content: "";
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--theme--bg-primary);
|
||||
}
|
||||
}
|
66
src/extensions/indent-guides/client.mjs
Normal file
@ -0,0 +1,66 @@
|
||||
/**
|
||||
* notion-enhancer: indent guides
|
||||
* (c) 2020 Alexa Baldon <alnbaldon@gmail.com> (https://github.com/runargs)
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
export default async function (api, db) {
|
||||
const { html } = api,
|
||||
guideStyle = await db.get("guideStyle"),
|
||||
rainbowMode = await db.get("rainbowMode");
|
||||
document.body.style.setProperty("--guide--style", guideStyle.toLowerCase());
|
||||
|
||||
const nestedTargets = [],
|
||||
outlineTargets = [];
|
||||
for (const [listType, selectors] of [
|
||||
["to-doList", [".notion-to_do-block"]],
|
||||
["bulletedList", [".notion-bulleted_list-block"]],
|
||||
["numberedList", [".notion-numbered_list-block"]],
|
||||
["toggleList", [".notion-toggle-block"]],
|
||||
[
|
||||
"toggleHeadings",
|
||||
[
|
||||
".notion-header-block",
|
||||
".notion-sub_header-block",
|
||||
".notion-sub_sub_header-block",
|
||||
],
|
||||
],
|
||||
]) {
|
||||
if (await db.get(listType)) nestedTargets.push(...selectors);
|
||||
}
|
||||
if (await db.get("tableOfContents"))
|
||||
outlineTargets.push(".notion-table_of_contents-block");
|
||||
if (await db.get("outliner"))
|
||||
outlineTargets.push(".notion-enhancer--outliner-heading");
|
||||
|
||||
let css = `${[...nestedTargets, ...outlineTargets].join(",")} {
|
||||
--guide--opacity: 1;
|
||||
}`;
|
||||
if (rainbowMode) {
|
||||
const opacity = `--guide--opacity: 0.5;`,
|
||||
selector = `:is(${nestedTargets.join(",")})`,
|
||||
colours = ["green", "blue", "purple", "pink", "red", "orange", "yellow"];
|
||||
colours.push(...colours, ...colours, ...colours, "gray");
|
||||
for (let i = 0; i < colours.length; i++) {
|
||||
css += `${(selector + " ").repeat(i + 1)} {
|
||||
--guide--color: var(--theme--fg-${colours[i]});
|
||||
${opacity}
|
||||
}`;
|
||||
}
|
||||
css += `
|
||||
.notion-table_of_contents-block [contenteditable="false"] a
|
||||
> div[style*="margin-left: 24px"],
|
||||
.notion-enhancer--outliner-heading.pl-\\[36px\\] {
|
||||
--guide--color: var(--theme--fg-${colours[0]});
|
||||
${opacity}
|
||||
}
|
||||
.notion-table_of_contents-block [contenteditable="false"] a
|
||||
> div[style*="margin-left: 48px"],
|
||||
.notion-enhancer--outliner-heading.pl-\\[54px\\] {
|
||||
--guide--color: var(--theme--fg-${colours[1]});
|
||||
${opacity}
|
||||
}`;
|
||||
}
|
||||
document.head.append(html`<style innerHTML=${css}></style>`);
|
||||
}
|
79
src/extensions/indent-guides/mod.json
Normal file
@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "Indent Guides",
|
||||
"id": "35815b3b-3916-4dc6-8769-c9c2448f8b57",
|
||||
"version": "0.3.0",
|
||||
"description": "Marks list indentation with vertical lines to make it easy to follow.",
|
||||
"tags": ["extension", "usability", "indentation-lines"],
|
||||
"authors": [
|
||||
{
|
||||
"name": "dragonwocky",
|
||||
"homepage": "https://dragonwocky.me/",
|
||||
"avatar": "https://dragonwocky.me/avatar.jpg"
|
||||
},
|
||||
{
|
||||
"name": "runargs",
|
||||
"email": "alnbaldon@gmail.com",
|
||||
"homepage": "http://github.com/runargs",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/39810066"
|
||||
}
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"type": "select",
|
||||
"key": "guideStyle",
|
||||
"description": "The type of line to use for indent guides.",
|
||||
"values": ["Solid", "Dashed", "Dotted"]
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "rainbowMode",
|
||||
"description": "By default, indent guides are coloured based on the current theme. Rainbow mode uses alternating colours at each indent level to better connect corresponding blocks in longer lists.",
|
||||
"value": false
|
||||
},
|
||||
{ "type": "heading", "label": "List Types" },
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "to-doList",
|
||||
"description": "Shows indent guides for Notion's to-do list blocks.",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "bulletedList",
|
||||
"description": "Shows indent guides for Notion's bulleted list blocks.",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "numberedList",
|
||||
"description": "Shows indent guides for Notion's numbered list blocks.",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "toggleList",
|
||||
"description": "Shows indent guides for Notion's toggle list blocks.",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "toggleHeadings",
|
||||
"description": "Shows indent guides for Notion's toggle heading blocks.",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "tableOfContents",
|
||||
"description": "Shows indent guides for Notion's table of contents blocks.",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "outliner",
|
||||
"description": "Shows indent guides for the Outliner's table of contents in the side panel.",
|
||||
"value": true
|
||||
}
|
||||
],
|
||||
"clientStyles": ["client.css"],
|
||||
"clientScripts": ["client.mjs"]
|
||||
}
|
105
src/extensions/line-numbers/client.mjs
Executable file
@ -0,0 +1,105 @@
|
||||
/**
|
||||
* notion-enhancer: line numbers
|
||||
* (c) 2020 CloudHill <rl.cloudhill@gmail.com> (https://github.com/CloudHill)
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
function LineNumbers({ decorationStyle = "None" }) {
|
||||
const { html } = globalThis.__enhancerApi,
|
||||
decorations = {
|
||||
Border: `pr-[16px] border-r-([2px]
|
||||
[color:var(--theme--bg-hover)])`,
|
||||
Background: `pr-[4px] before:(absolute block
|
||||
h-full w-[calc(100%-24px)] rounded-[4px] right-0
|
||||
content-empty bg-[var(--theme--bg-hover)] z-[-1])`,
|
||||
};
|
||||
return html`<div
|
||||
class="notion-enhancer--line-numbers mt-[34px]
|
||||
text-([85%] [var(--theme--fg-secondary)] right)
|
||||
font-[var(--font--code)] overflow-hidden select-none
|
||||
relative flex-grow ${decorations[decorationStyle] || ""}"
|
||||
></div>`;
|
||||
}
|
||||
|
||||
export default async (api, db) => {
|
||||
const { html, addMutationListener } = api,
|
||||
decorationStyle = await db.get("decorationStyle"),
|
||||
numberSingleLines = await db.get("numberSingleLines"),
|
||||
codeBlockSelector = ".notion-code-block.line-numbers > .notranslate";
|
||||
|
||||
// get character width in pixels
|
||||
const getCharWidth = ($elem) => {
|
||||
const $char = html`<span style="width:1ch"> </span>`;
|
||||
$elem.append($char);
|
||||
const charWidth = getComputedStyle($char).getPropertyValue("width");
|
||||
$char.remove();
|
||||
return parseFloat(charWidth);
|
||||
},
|
||||
// get line width in pixels
|
||||
getLineWidth = ($elem) =>
|
||||
parseFloat(getComputedStyle($elem).getPropertyValue("width")) -
|
||||
parseFloat(getComputedStyle($elem).getPropertyValue("padding-left")) -
|
||||
parseFloat(getComputedStyle($elem).getPropertyValue("padding-right")),
|
||||
// get line height in pixels
|
||||
getLineHeight = ($elem) =>
|
||||
parseFloat(getComputedStyle($elem).getPropertyValue("line-height")),
|
||||
// update inline styles without unnecessary dom updates
|
||||
applyStyles = ($elem, styles) => {
|
||||
for (const property in styles) {
|
||||
if ($elem.style[property] === styles[property]) continue;
|
||||
$elem.style[property] = styles[property];
|
||||
}
|
||||
};
|
||||
|
||||
const numberLines = () => {
|
||||
for (const $code of document.querySelectorAll(codeBlockSelector)) {
|
||||
const wrap = $code.style.wordBreak === "break-all",
|
||||
lines = $code.innerText.split("\n"),
|
||||
numLines = Math.max(lines.length - 1, 1),
|
||||
numChars = lines.map((line) => line.length).join(","),
|
||||
numDigits = (Math.log(numLines) * Math.LOG10E + 1) | 0;
|
||||
|
||||
if ($code.dataset.lines === wrap + "," + numChars) continue;
|
||||
$code.dataset.lines = wrap + "," + numChars;
|
||||
|
||||
// do not add to single-line blocks if disabled
|
||||
const visible = numberSingleLines || numLines > 1,
|
||||
width = visible
|
||||
? decorationStyle === "Border"
|
||||
? `calc(100% - 50px - ${numDigits}ch)`
|
||||
: `calc(100% - 32px - ${numDigits}ch)`
|
||||
: "",
|
||||
paddingLeft = visible && decorationStyle === "Border" ? "16px" : "32px";
|
||||
// shrink block to allow space for numbers
|
||||
applyStyles($code.parentElement, { justifyContent: "flex-end" });
|
||||
applyStyles($code, { minWidth: width, maxWidth: width, paddingLeft });
|
||||
|
||||
// calculate heights of wrapped lines and render line nums
|
||||
let totalHeight = 0;
|
||||
const lineHeight = getLineHeight($code),
|
||||
charsPerLine = Math.floor(getLineWidth($code) / getCharWidth($code));
|
||||
$code._$lineNumbers ||= html`<${LineNumbers}...${{ decorationStyle }} />`;
|
||||
for (let i = 1; i <= numLines; i++) {
|
||||
const $n = $code._$lineNumbers.children[i - 1] || html`<p>${i}</p>`;
|
||||
if (!$code._$lineNumbers.contains($n)) $code._$lineNumbers.append($n);
|
||||
const wrappedHeight =
|
||||
wrap && lines[i - 1].length > charsPerLine
|
||||
? Math.ceil(lines[i - 1].length / charsPerLine) * lineHeight
|
||||
: lineHeight;
|
||||
applyStyles($n, { height: `${wrappedHeight}px` });
|
||||
totalHeight += wrappedHeight;
|
||||
}
|
||||
applyStyles($code._$lineNumbers, {
|
||||
display: visible ? "" : "none",
|
||||
height: `${totalHeight}px`,
|
||||
});
|
||||
|
||||
if (visible && !document.contains($code._$lineNumbers)) {
|
||||
$code.before($code._$lineNumbers);
|
||||
} else if (!visible) $code._$lineNumbers.style.display = "none";
|
||||
}
|
||||
};
|
||||
|
||||
addMutationListener(codeBlockSelector, numberLines);
|
||||
};
|
34
src/extensions/line-numbers/mod.json
Executable file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "Line Numbers",
|
||||
"id": "d61dc8a7-b195-465b-935f-53eea9efe74e",
|
||||
"version": "0.5.0",
|
||||
"description": "Adds line numbers to code blocks.",
|
||||
"tags": ["code-line-numbers"],
|
||||
"authors": [
|
||||
{
|
||||
"name": "dragonwocky",
|
||||
"homepage": "https://dragonwocky.me/",
|
||||
"avatar": "https://dragonwocky.me/avatar.jpg"
|
||||
},
|
||||
{
|
||||
"name": "CloudHill",
|
||||
"homepage": "https://github.com/CloudHill",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/54142180"
|
||||
}
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "numberSingleLines",
|
||||
"description": "Adds line numbers to code blocks with only one line.",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"key": "decorationStyle",
|
||||
"description": "Decorates line numbers with additional styling to distinguish them from code block content.",
|
||||
"values": ["Border", "Background", "None"]
|
||||
}
|
||||
],
|
||||
"clientScripts": ["client.mjs"]
|
||||
}
|
9
src/extensions/no-peeking/client.css
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* notion-enhancer: no peeking
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
.notion-peek-renderer {
|
||||
display: none;
|
||||
}
|
26
src/extensions/no-peeking/client.mjs
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* notion-enhancer: no peeking
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
export default async (api) => {
|
||||
const { addMutationListener } = api,
|
||||
peekRenderer = ".notion-peek-renderer",
|
||||
openInFullPage = `[aria-label="Open in full page"]`,
|
||||
pageId = () => location.pathname.split(/-|\//g).at(-1),
|
||||
peekId = () => new URLSearchParams(location.search).get("p");
|
||||
|
||||
let _pageId = pageId();
|
||||
const skipPeek = () => {
|
||||
const $openInFullPage = document.querySelector(openInFullPage);
|
||||
if (peekId() === _pageId) {
|
||||
_pageId = pageId();
|
||||
history.back();
|
||||
} else if (peekId() && $openInFullPage) {
|
||||
_pageId = peekId();
|
||||
$openInFullPage.click();
|
||||
} else _pageId = pageId();
|
||||
};
|
||||
addMutationListener(peekRenderer, skipPeek);
|
||||
};
|
16
src/extensions/no-peeking/mod.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "No Peeking",
|
||||
"id": "cb6fd684-f113-4a7a-9423-8f0f0cff069f",
|
||||
"version": "0.3.0",
|
||||
"description": "Globally force pages opening in side peek or center peek to open as full pages instead.",
|
||||
"tags": ["automation"],
|
||||
"authors": [
|
||||
{
|
||||
"name": "dragonwocky",
|
||||
"homepage": "https://dragonwocky.me/",
|
||||
"avatar": "https://dragonwocky.me/avatar.jpg"
|
||||
}
|
||||
],
|
||||
"clientStyles": ["client.css"],
|
||||
"clientScripts": ["client.mjs"]
|
||||
}
|
135
src/extensions/outliner/client.mjs
Normal file
@ -0,0 +1,135 @@
|
||||
/**
|
||||
* notion-enhancer: outliner
|
||||
* (c) 2021 CloudHill <rl.cloudhill@gmail.com> (https://github.com/CloudHill)
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { Heading } from "./islands/Heading.mjs";
|
||||
import { PanelDescription } from "./islands/PanelDescription.mjs";
|
||||
|
||||
export default async (api, db) => {
|
||||
const { html, debounce, addMutationListener, addPanelView } = api,
|
||||
behavior = (await db.get("smoothScrolling")) ? "smooth" : "auto",
|
||||
scroller = ".notion-frame .notion-scroller",
|
||||
equation = ".notion-text-equation-token",
|
||||
annotation = (await db.get("equationRendering"))
|
||||
? ".katex-html"
|
||||
: ".katex-mathml annotation",
|
||||
page = ".notion-page-content",
|
||||
headings = [
|
||||
".notion-header-block",
|
||||
".notion-sub_header-block",
|
||||
".notion-sub_sub_header-block",
|
||||
],
|
||||
$toc = html`<div></div>`;
|
||||
addPanelView({
|
||||
title: "Outliner",
|
||||
// prettier-ignore
|
||||
$icon: html`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
||||
<circle cx="5" cy="7" r="2.8"/>
|
||||
<circle cx="5" cy="17" r="2.79"/>
|
||||
<path d="M21,5.95H11c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h10c0.55,0,1,0.45,1,1v0C22,5.5,21.55,5.95,21,5.95z"/>
|
||||
<path d="M17,10.05h-6c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h6c0.55,0,1,0.45,1,1v0C18,9.6,17.55,10.05,17,10.05z"/>
|
||||
<path d="M21,15.95H11c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h10c0.55,0,1,0.45,1,1v0C22,15.5,21.55,15.95,21,15.95z" />
|
||||
<path d="M17,20.05h-6c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h6c0.55,0,1,0.45,1,1v0C18,19.6,17.55,20.05,17,20.05z"/>
|
||||
</svg>`,
|
||||
$view: html`<section>
|
||||
<${PanelDescription}>Click on a heading to jump to it.<//>
|
||||
${$toc}
|
||||
</section>`,
|
||||
});
|
||||
|
||||
const replaceFloatingOutline = await db.get("replaceFloatingOutline");
|
||||
if (replaceFloatingOutline) {
|
||||
document.head.append(html`<style>
|
||||
.hide-scrollbar.ignore-scrolling-container:has(
|
||||
div:empty[style*="width"]
|
||||
) {
|
||||
display: none !important;
|
||||
}
|
||||
</style>`);
|
||||
}
|
||||
|
||||
let $page, $scroller;
|
||||
const getHeadings = () => {
|
||||
if (!$page) return [];
|
||||
return [...$page.querySelectorAll(headings.join(", "))];
|
||||
},
|
||||
getHeadingLevel = ($heading) => {
|
||||
for (let i = 0; i < headings.length; i++)
|
||||
if ($heading.matches(headings[i])) return i + 1;
|
||||
},
|
||||
getHeadingTitle = ($heading) => {
|
||||
if (!$heading.innerText) return "Untitled";
|
||||
let title = "";
|
||||
for (const node of $heading.querySelector("h2, h3, h4").childNodes) {
|
||||
if (node.nodeType === 3) title += node.textContent;
|
||||
else if (node.matches(equation)) {
|
||||
// https://github.com/notion-enhancer/repo/issues/39
|
||||
const $katex = node.querySelector(annotation);
|
||||
title += $katex.textContent;
|
||||
} else title += node.innerText;
|
||||
}
|
||||
return title;
|
||||
},
|
||||
getBlockOffset = ($block) => {
|
||||
let offset = 0;
|
||||
while (!$block?.matches("[data-content-editable-root]")) {
|
||||
offset += $block.offsetTop;
|
||||
$block = $block.offsetParent;
|
||||
}
|
||||
return offset;
|
||||
},
|
||||
updateHeadings = debounce(() => {
|
||||
$toc.innerHTML = "";
|
||||
if (!$page) return;
|
||||
let indent = 0,
|
||||
prev_level = 0;
|
||||
const $frag = document.createDocumentFragment();
|
||||
for (const $heading of getHeadings()) {
|
||||
const level = getHeadingLevel($heading);
|
||||
if (level === 1) indent = 1;
|
||||
else if (level > prev_level) indent = Math.min(indent + 1, level);
|
||||
else if (level < prev_level) indent = Math.max(indent - 1, level);
|
||||
prev_level = level;
|
||||
$heading._$outline = html`<${Heading}
|
||||
...${{ indent }}
|
||||
onclick=${() => {
|
||||
if (!$scroller) return;
|
||||
const top = getBlockOffset($heading) - 24;
|
||||
$scroller.scrollTo({ top, behavior });
|
||||
}}
|
||||
>${getHeadingTitle($heading)}
|
||||
<//>`;
|
||||
$frag.append($heading._$outline);
|
||||
}
|
||||
$toc.append($frag);
|
||||
onScroll();
|
||||
});
|
||||
|
||||
const $progressMarker = html`<span
|
||||
class="absolute block left-[6px] top-[calc(50%-1px)]
|
||||
size-[6px] rounded-full bg-[color:var(--theme--fg-secondary)]"
|
||||
></span>`,
|
||||
onScroll = () => {
|
||||
if (!$scroller) return;
|
||||
const $h = getHeadings().find(($h) => {
|
||||
return $scroller.scrollTop < getBlockOffset($h) - 16;
|
||||
})?._$outline;
|
||||
if ($h) $h.prepend($progressMarker);
|
||||
},
|
||||
setup = () => {
|
||||
if (document.contains($page)) return;
|
||||
$page = document.querySelector(page);
|
||||
$scroller = document.querySelector(scroller);
|
||||
$scroller?.removeEventListener("scroll", onScroll);
|
||||
$scroller?.addEventListener("scroll", onScroll);
|
||||
updateHeadings();
|
||||
};
|
||||
|
||||
const semanticHeadings = '[class$="header-block"] :is(h2, h3, h4)';
|
||||
addMutationListener(`${page} ${semanticHeadings}`, updateHeadings);
|
||||
addMutationListener(`${page}, ${scroller}`, setup, { subtree: false });
|
||||
setup();
|
||||
};
|
23
src/extensions/outliner/islands/Heading.mjs
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* notion-enhancer: outliner
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
function Heading({ indent, ...props }, ...children) {
|
||||
const { html } = globalThis.__enhancerApi;
|
||||
return html`<div
|
||||
role="button"
|
||||
class="notion-enhancer--outliner-heading block
|
||||
relative cursor-pointer select-none text-[14px]
|
||||
decoration-(2 [color:var(--theme--fg-border)])
|
||||
hover:bg-[color:var(--theme--bg-hover)]
|
||||
py-[6px] pr-[2px] pl-[${indent * 18}px]
|
||||
underline-(~ offset-4) last:mb-[24px]"
|
||||
...${props}
|
||||
>
|
||||
${children}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export { Heading };
|
16
src/extensions/outliner/islands/PanelDescription.mjs
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* notion-enhancer: outliner
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
function PanelDescription(props, ...children) {
|
||||
const { html, extendProps } = globalThis.__enhancerApi;
|
||||
extendProps(props, {
|
||||
class: `py-[12px] px-[18px] text-(
|
||||
[13px] [color:var(--theme--fg-secondary)])`,
|
||||
});
|
||||
return html` <p ...${props}>${children}</p>`;
|
||||
}
|
||||
|
||||
export { PanelDescription };
|
44
src/extensions/outliner/mod.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "Outliner",
|
||||
"version": "0.5.0",
|
||||
"id": "87e077cc-5402-451c-ac70-27cc4ae65546",
|
||||
"description": "Adds a table of contents to the side panel to overview and navigate the current page's headings and subheadings.",
|
||||
"tags": [
|
||||
"panel"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "dragonwocky",
|
||||
"homepage": "https://dragonwocky.me/",
|
||||
"avatar": "https://dragonwocky.me/avatar.jpg"
|
||||
},
|
||||
{
|
||||
"name": "CloudHill",
|
||||
"homepage": "https://github.com/CloudHill",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/54142180"
|
||||
}
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "smoothScrolling",
|
||||
"description": "Animates scrolling to a heading smoothly. Disable this to jump to a heading instantly when clicking it in the Outliner's table of contents.",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "equationRendering",
|
||||
"description": "Attempts to render special symbols from inline equations in headings. Note that position- and size-based formatting will be lost when displaying equations in the Outliner's table of contents. Disable this to display the raw TeX equation instead.",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "replaceFloatingOutline",
|
||||
"description": "Disables Notion's builtin floating table of contents for a complete switch to the Outliner.",
|
||||
"value": true
|
||||
}
|
||||
],
|
||||
"clientScripts": [
|
||||
"client.mjs"
|
||||
]
|
||||
}
|
15
src/extensions/right-to-left/client.css
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* notion-enhancer: right to left
|
||||
* (c) 2021 obahareth <omar@omar.engineer> (https://omar.engineer)
|
||||
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
/* indent rtl toc header levels,
|
||||
* https://github.com/notion-enhancer/notion-enhancer/issues/616 */
|
||||
.notion-table_of_contents-block div[style*="margin-left: 24px"] {
|
||||
margin-inline-start: 24px;
|
||||
}
|
||||
.notion-table_of_contents-block div[style*="margin-left: 48px"] {
|
||||
margin-inline-start: 48px;
|
||||
}
|