Compare commits
1142 Commits
search-hig
...
theme-buil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ef9180894 | ||
|
|
d1a2ec580d | ||
|
|
0a057e2b32 | ||
|
|
29611ffdef | ||
|
|
85fa49f53e | ||
|
|
703853bd8a | ||
|
|
9b046c6b6f | ||
|
|
d10e08a158 | ||
|
|
ad67947ad7 | ||
|
|
1574ae7a28 | ||
|
|
56201cb408 | ||
|
|
b3fc476352 | ||
|
|
fdd24a90d5 | ||
|
|
5140fd7108 | ||
|
|
becd852165 | ||
|
|
700f5e4205 | ||
|
|
4245ac1451 | ||
|
|
26e44798ec | ||
|
|
9648913a02 | ||
|
|
7091f64de8 | ||
|
|
e5ec15fd9c | ||
|
|
c067c73b33 | ||
|
|
d304f708c7 | ||
|
|
71cf9955cd | ||
|
|
c5fcfd6654 | ||
|
|
67a1c55420 | ||
|
|
6c54f28c0f | ||
|
|
c576671302 | ||
|
|
6e663bf913 | ||
|
|
0f999947ee | ||
|
|
ab458486a2 | ||
|
|
5e627b567f | ||
|
|
2ef7baab0a | ||
|
|
56ede7654a | ||
|
|
18ccbc0ba3 | ||
|
|
73f50a34f9 | ||
|
|
c0805b1915 | ||
|
|
3c9a62b8ed | ||
|
|
b61c094a5d | ||
|
|
7f7390810f | ||
|
|
09ea2f577b | ||
|
|
cbcd7a9e5a | ||
|
|
bca774dfd0 | ||
|
|
0b56bdb0c4 | ||
|
|
cdf481d581 | ||
|
|
c644f606f3 | ||
|
|
c45e029780 | ||
|
|
59422d3e9c | ||
|
|
f0df45bd82 | ||
|
|
c5ce153c00 | ||
|
|
3e7d68f524 | ||
|
|
e4eea50319 | ||
|
|
d12e14ab34 | ||
|
|
c0a444d0c6 | ||
|
|
e16a5524b8 | ||
|
|
c386e92831 | ||
|
|
ed277a95fb | ||
|
|
c14b09ee1d | ||
|
|
47e0c54ab0 | ||
|
|
52fecc27ce | ||
|
|
056b24e936 | ||
|
|
e34607f244 | ||
|
|
7ba7bf64f0 | ||
|
|
6f51a98477 | ||
|
|
1ff1319e8a | ||
|
|
16c46d3fc7 | ||
|
|
cd7e0f5380 | ||
|
|
8362c39277 | ||
|
|
eedf347708 | ||
|
|
d95aba8cdd | ||
|
|
922fce7bcc | ||
|
|
9e3646ebc5 | ||
|
|
cdf3ea9dab | ||
|
|
403b2809b7 | ||
|
|
6c551eef0a | ||
|
|
d34b354420 | ||
|
|
38a4652c29 | ||
|
|
e38ac19a58 | ||
|
|
228c1a4d3a | ||
|
|
7bca693c44 | ||
|
|
9d3f309fc3 | ||
|
|
6b7eb9a1f2 | ||
|
|
d2165c99f1 | ||
|
|
8b538b833b | ||
|
|
2977fb39e4 | ||
|
|
01cb43ba2e | ||
|
|
583fa3da49 | ||
|
|
4b68a083c5 | ||
|
|
5854a3eef5 | ||
|
|
df5cbacc8a | ||
|
|
05fa30a039 | ||
|
|
2ff73e241d | ||
|
|
fe2c753068 | ||
|
|
19201186c5 | ||
|
|
b2dd6b750d | ||
|
|
25380ec052 | ||
|
|
81cc687e1e | ||
|
|
1dab6be32d | ||
|
|
b9bfeee126 | ||
|
|
a06fa9f00e | ||
|
|
b147afbefe | ||
|
|
7646f35e58 | ||
|
|
beb782b9ad | ||
|
|
6e3f706b3a | ||
|
|
f01d415415 | ||
|
|
b6417d0016 | ||
|
|
f1ab8e92bd | ||
|
|
80ffbc511d | ||
|
|
745bf65641 | ||
|
|
adabf488ee | ||
|
|
a0015af169 | ||
|
|
e86b7c68e6 | ||
|
|
c5b974e055 | ||
|
|
b3695d4a93 | ||
|
|
b5e5450452 | ||
|
|
d19cddccea | ||
|
|
c61649f7fa | ||
|
|
13b8580b2f | ||
|
|
aba2b0ea19 | ||
|
|
0894637b61 | ||
|
|
5970aabb0d | ||
|
|
72a25dcf34 | ||
|
|
6fff30bd89 | ||
|
|
a9a71834b8 | ||
|
|
8ec4865e23 | ||
|
|
111c7ef954 | ||
|
|
35a37458e4 | ||
|
|
92bafbafa7 | ||
|
|
2705caf6bd | ||
|
|
8380c7d994 | ||
|
|
550c0f3854 | ||
|
|
04cd135295 | ||
|
|
e212a080c2 | ||
|
|
533abb58db | ||
|
|
941a542cc6 | ||
|
|
5bcda2cd3a | ||
|
|
869d8ba8f9 | ||
|
|
984e8f8780 | ||
|
|
750b61fb75 | ||
|
|
4a2defa300 | ||
|
|
5c9f7a8bcb | ||
|
|
b66caa9848 | ||
|
|
81cac1e54f | ||
|
|
36a99cd64f | ||
|
|
8c494642e4 | ||
|
|
7ab4088331 | ||
|
|
580af33caf | ||
|
|
bbeeacc480 | ||
|
|
51efef5005 | ||
|
|
ee030c8f61 | ||
|
|
20835e1fb3 | ||
|
|
b542680e7a | ||
|
|
fb32aedd69 | ||
|
|
56f6d34995 | ||
|
|
c68e380a0f | ||
|
|
fc5507bdd3 | ||
|
|
3f0a656a36 | ||
|
|
60667149d4 | ||
|
|
cdb3ede207 | ||
|
|
445494ce95 | ||
|
|
972f52fd01 | ||
|
|
f5e86d668f | ||
|
|
0e817236a1 | ||
|
|
0ea3b3ac2a | ||
|
|
7bfaef82d8 | ||
|
|
f51220c9a8 | ||
|
|
88e9ca721a | ||
|
|
c1ab444646 | ||
|
|
b497b53ef2 | ||
|
|
80decaae8a | ||
|
|
41197cb52c | ||
|
|
d14abcbba7 | ||
|
|
cd36fc8d4a | ||
|
|
e872487724 | ||
|
|
5e7e91caf0 | ||
|
|
a971a29ff2 | ||
|
|
a967482c4f | ||
|
|
f91ada3c48 | ||
|
|
44782d552d | ||
|
|
a51dfb4033 | ||
|
|
ccb18e9335 | ||
|
|
194556a0a1 | ||
|
|
8f828bca80 | ||
|
|
a967234ddb | ||
|
|
bbcf7d1206 | ||
|
|
6904045c7f | ||
|
|
59c4052765 | ||
|
|
d3c47d3321 | ||
|
|
221767ed4d | ||
|
|
4843d1c39e | ||
|
|
ebc28bca62 | ||
|
|
52dcf868b4 | ||
|
|
e7bcad29f7 | ||
|
|
33c939f5ca | ||
|
|
e6b9219d75 | ||
|
|
5774daf3a9 | ||
|
|
b8181c4a1b | ||
|
|
408cd5a9f3 | ||
|
|
42a76c6b03 | ||
|
|
7fa88ff8fb | ||
|
|
cafd854b8c | ||
|
|
8c58c512ed | ||
|
|
3ab104e000 | ||
|
|
55f8a226c8 | ||
|
|
0cca9e1ff8 | ||
|
|
1fb43b7dfe | ||
|
|
e138aef6a9 | ||
|
|
8d7b444664 | ||
|
|
4a5c5fd3bf | ||
|
|
8fb0071cf0 | ||
|
|
bb1cb8dc33 | ||
|
|
533884e534 | ||
|
|
f5c2962951 | ||
|
|
8d285c93d9 | ||
|
|
fc10e957fb | ||
|
|
eb35d41321 | ||
|
|
99808293a7 | ||
|
|
10fafa885a | ||
|
|
9096e7711d | ||
|
|
25cbdbd409 | ||
|
|
0216867fa9 | ||
|
|
f832aba1a4 | ||
|
|
8df9c49fa1 | ||
|
|
b733eb3c44 | ||
|
|
addbd53d7a | ||
|
|
5f42f11fb1 | ||
|
|
30072576ce | ||
|
|
48371b6364 | ||
|
|
90ef877942 | ||
|
|
c91bc819be | ||
|
|
6c25a70a51 | ||
|
|
9e2b9fa6f1 | ||
|
|
5663c507e9 | ||
|
|
d6fce5ad18 | ||
|
|
9125e41e9a | ||
|
|
0498604848 | ||
|
|
26902cb969 | ||
|
|
915f21d59a | ||
|
|
e7f4ba8b57 | ||
|
|
0bdcfff561 | ||
|
|
b330445531 | ||
|
|
053ad1927b | ||
|
|
31ff047a34 | ||
|
|
7f9c24e061 | ||
|
|
9d8689bbce | ||
|
|
5dabd07f38 | ||
|
|
41f0a4e101 | ||
|
|
ce1bc83e38 | ||
|
|
0ef53f17cb | ||
|
|
574d638c94 | ||
|
|
78a543dad4 | ||
|
|
8c857e58b7 | ||
|
|
61c96e7b58 | ||
|
|
ea02552634 | ||
|
|
4438d96d9c | ||
|
|
5c4fee8f98 | ||
|
|
0179a81d52 | ||
|
|
0d5cd2e554 | ||
|
|
47006b8648 | ||
|
|
1a701d2a64 | ||
|
|
adae78d2df | ||
|
|
c6fe3ea91d | ||
|
|
69c152948e | ||
|
|
fb9a1af93c | ||
|
|
dfe7e7d459 | ||
|
|
2e807cdd0b | ||
|
|
6f80d2e7b2 | ||
|
|
e49c2e8ccc | ||
|
|
3fec04466e | ||
|
|
93c2115f4b | ||
|
|
8716d3076a | ||
|
|
5ec5517778 | ||
|
|
8cb0644cd0 | ||
|
|
56947d942d | ||
|
|
3ea810a059 | ||
|
|
723bb67a6f | ||
|
|
f0e68afdf8 | ||
|
|
7c0d8c25d3 | ||
|
|
641d78fdb2 | ||
|
|
1c43da0eb0 | ||
|
|
87de351170 | ||
|
|
31726f6d86 | ||
|
|
548eda9a26 | ||
|
|
dc6fa89c9c | ||
|
|
cdda26f32f | ||
|
|
a8470c9fea | ||
|
|
4fa2bc6bb6 | ||
|
|
8aacf49f07 | ||
|
|
d14c712b79 | ||
|
|
fb90ef3137 | ||
|
|
f733e6e95b | ||
|
|
3ae3465e84 | ||
|
|
9abe85f1c9 | ||
|
|
6194367c1e | ||
|
|
4e793317ca | ||
|
|
eb6927cbce | ||
|
|
b7762ceaae | ||
|
|
53b545f84c | ||
|
|
4195150005 | ||
|
|
0a8c045f6f | ||
|
|
f4fe2c6a65 | ||
|
|
11e109740b | ||
|
|
d882c7cf9f | ||
|
|
eb8b58a559 | ||
|
|
2b94212552 | ||
|
|
04d122c47c | ||
|
|
6c9c99681e | ||
|
|
f0a4a61e1d | ||
|
|
f88c610495 | ||
|
|
f9cb9fa3cc | ||
|
|
3c6622ccf0 | ||
|
|
df82e84c83 | ||
|
|
abd136c7c2 | ||
|
|
c61082cdee | ||
|
|
b9b21a760a | ||
|
|
acc852e23b | ||
|
|
36ae9383ba | ||
|
|
c04df4b4c8 | ||
|
|
5c849c39b4 | ||
|
|
9bae058d16 | ||
|
|
db2d48a083 | ||
|
|
628d3bd9ff | ||
|
|
cdbca8ac22 | ||
|
|
8d2c0762db | ||
|
|
992f4fc697 | ||
|
|
cb1d09904b | ||
|
|
2441a4505e | ||
|
|
79aae86906 | ||
|
|
cd792625ac | ||
|
|
605e045ff7 | ||
|
|
8c17233e67 | ||
|
|
08480188fa | ||
|
|
87402251a5 | ||
|
|
e3a818378d | ||
|
|
6d82b0a5ba | ||
|
|
e3f0d0ada7 | ||
|
|
9e2f54fd0b | ||
|
|
19eb512198 | ||
|
|
86a7fd9b93 | ||
|
|
fa7900aef6 | ||
|
|
561d7a8bbc | ||
|
|
fb616a246e | ||
|
|
c881e74aae | ||
|
|
0f2acb2801 | ||
|
|
9b5e2c108b | ||
|
|
ba900b3a0f | ||
|
|
49df1bcd9e | ||
|
|
5d1ba7fa0a | ||
|
|
a6d5a618b0 | ||
|
|
8f4bd20253 | ||
|
|
8b4270f7df | ||
|
|
9a4877bdbd | ||
|
|
0a45e963a6 | ||
|
|
fb7b306bc5 | ||
|
|
4035751264 | ||
|
|
07bb35395c | ||
|
|
09cd19d3b7 | ||
|
|
a3988fd309 | ||
|
|
574c4cc8a9 | ||
|
|
5041e47902 | ||
|
|
f3a03dc813 | ||
|
|
942c9556f6 | ||
|
|
5374f2b56c | ||
|
|
3f128da55d | ||
|
|
0aa689ca03 | ||
|
|
93e1ae5830 | ||
|
|
ef5aebdb3b | ||
|
|
c152796767 | ||
|
|
b75382f8b6 | ||
|
|
80893a5011 | ||
|
|
0f9ac0e068 | ||
|
|
46e4ac127c | ||
|
|
3da4ced75b | ||
|
|
3f79ae6403 | ||
|
|
2d78b05e8d | ||
|
|
c7c4ae4615 | ||
|
|
ee2cbea0c4 | ||
|
|
782fc575a9 | ||
|
|
151a527444 | ||
|
|
6ab0911970 | ||
|
|
28927e705b | ||
|
|
2a31f70b4a | ||
|
|
0596bd9e84 | ||
|
|
3e95960b45 | ||
|
|
131ecfee89 | ||
|
|
dfccd640b0 | ||
|
|
14d27bb277 | ||
|
|
dd470297c3 | ||
|
|
1e690664f1 | ||
|
|
d68eaa7a41 | ||
|
|
791119fcbc | ||
|
|
87b82865b2 | ||
|
|
ad145c3499 | ||
|
|
5a228ce5fe | ||
|
|
8cb3cb6b19 | ||
|
|
94cc3a6853 | ||
|
|
163dfac5d7 | ||
|
|
efd0d6c2c3 | ||
|
|
742dc56f3c | ||
|
|
19f2d8b0a7 | ||
|
|
8c9d98ab88 | ||
|
|
75ae5d9603 | ||
|
|
8a2453429d | ||
|
|
b54ca08fc8 | ||
|
|
2be801ba3b | ||
|
|
f37cd3fe63 | ||
|
|
1224e48bc8 | ||
|
|
780bebe141 | ||
|
|
f707b43efb | ||
|
|
92e9cd740f | ||
|
|
20bef5e70c | ||
|
|
c9b45875a0 | ||
|
|
f180b79a3e | ||
|
|
438a3c2686 | ||
|
|
64d23d3de0 | ||
|
|
e251c6f71c | ||
|
|
40cb6ae8bf | ||
|
|
06aaa7e2c2 | ||
|
|
ab29a01649 | ||
|
|
49e7bdf202 | ||
|
|
2e0f9d8524 | ||
|
|
092b26be57 | ||
|
|
3525feffcf | ||
|
|
6cb5a5e473 | ||
|
|
0992d4eeab | ||
|
|
828cd6be35 | ||
|
|
1b1a962ec2 | ||
|
|
0a88502e59 | ||
|
|
50ff39845a | ||
|
|
1cb34d0783 | ||
|
|
bbb43bc8b4 | ||
|
|
1521a7d120 | ||
|
|
3b6a2cb9de | ||
|
|
0deaed4ccd | ||
|
|
8ebb58b70c | ||
|
|
f691d113d2 | ||
|
|
a57eaf9be6 | ||
|
|
c5a264270c | ||
|
|
e75c62e6e1 | ||
|
|
6c3583c9e5 | ||
|
|
58ba453f57 | ||
|
|
7f5d5fcb82 | ||
|
|
e218194402 | ||
|
|
c36f532bbb | ||
|
|
b3b7bd44ef | ||
|
|
c85242a610 | ||
|
|
58f7a206d9 | ||
|
|
25bf4fee82 | ||
|
|
c2ab5ded47 | ||
|
|
ad6a0ebfab | ||
|
|
cb9e6578c1 | ||
|
|
17a1475ded | ||
|
|
2cee5a4d89 | ||
|
|
337f88c373 | ||
|
|
53d5073690 | ||
|
|
9cc9660b48 | ||
|
|
e6162b2580 | ||
|
|
abc14d2d1f | ||
|
|
fa11b5fabb | ||
|
|
d904916863 | ||
|
|
0cfc2e1609 | ||
|
|
3742e923c9 | ||
|
|
792a254513 | ||
|
|
17928ba182 | ||
|
|
2da0f164c6 | ||
|
|
4eabe79e7e | ||
|
|
89dd8f5b6c | ||
|
|
b4d3662897 | ||
|
|
675e1e2c6f | ||
|
|
0f7f3bae7e | ||
|
|
c917d95a7b | ||
|
|
3ae9001504 | ||
|
|
cc97b8beed | ||
|
|
954bdebcef | ||
|
|
04b62f4e53 | ||
|
|
a2dcb67b36 | ||
|
|
cc9141c981 | ||
|
|
85484b1762 | ||
|
|
1a0c94896a | ||
|
|
068a99463d | ||
|
|
b413315777 | ||
|
|
7227513783 | ||
|
|
083e4e1150 | ||
|
|
09098410a5 | ||
|
|
2c9f5df530 | ||
|
|
3ad346362b | ||
|
|
e176b1bf76 | ||
|
|
b368232598 | ||
|
|
bcd27e10c1 | ||
|
|
af9b9b7083 | ||
|
|
ea1565048a | ||
|
|
59b081704b | ||
|
|
2761222b36 | ||
|
|
571f15b556 | ||
|
|
283efa6e41 | ||
|
|
b3b1b7470e | ||
|
|
a9e59b17dc | ||
|
|
22d946b677 | ||
|
|
6a748c3f98 | ||
|
|
520296106a | ||
|
|
3b727e6f64 | ||
|
|
b77711a53d | ||
|
|
aebb2d414b | ||
|
|
e0e505371c | ||
|
|
b9040a9358 | ||
|
|
68f10ce308 | ||
|
|
8a475f654f | ||
|
|
1043b7df55 | ||
|
|
8383e09243 | ||
|
|
df58a6cfc4 | ||
|
|
a9df577478 | ||
|
|
e021d93893 | ||
|
|
24e13f488e | ||
|
|
6f88b70937 | ||
|
|
a90f957aa4 | ||
|
|
748953b3eb | ||
|
|
874c892986 | ||
|
|
ab678fa3a9 | ||
|
|
c2351ab2d3 | ||
|
|
3fd3e02256 | ||
|
|
d2fd26abd5 | ||
|
|
e6e775dbeb | ||
|
|
dd84b9ca2f | ||
|
|
269f083dce | ||
|
|
e18867d3f1 | ||
|
|
e221882f8e | ||
|
|
153ee4bce3 | ||
|
|
a2ecdab61c | ||
|
|
eb089e6211 | ||
|
|
1981c3a2f8 | ||
|
|
68b3ebe318 | ||
|
|
8a0b6264d2 | ||
|
|
239174c3e7 | ||
|
|
db1503d828 | ||
|
|
7b1cbba9e7 | ||
|
|
2c3c193629 | ||
|
|
899e56e9cd | ||
|
|
9c9582ae12 | ||
|
|
b918667b63 | ||
|
|
878c901e06 | ||
|
|
fdd3897a60 | ||
|
|
c958657b01 | ||
|
|
e7175f48a0 | ||
|
|
4e1862d653 | ||
|
|
1d5ac37222 | ||
|
|
32d027fff6 | ||
|
|
9434993373 | ||
|
|
2bd0f1a304 | ||
|
|
7f873f240e | ||
|
|
a0229729b7 | ||
|
|
dd368a4170 | ||
|
|
e082007a49 | ||
|
|
6d3e985c9d | ||
|
|
15858db21b | ||
|
|
b69a94a96f | ||
|
|
deced99339 | ||
|
|
b0acce7b4f | ||
|
|
73b965eb7c | ||
|
|
e081e7ea09 | ||
|
|
35db89aa11 | ||
|
|
679a912e2b | ||
|
|
5738f48360 | ||
|
|
627259e129 | ||
|
|
70c8201355 | ||
|
|
064059278a | ||
|
|
959e7466a6 | ||
|
|
69bd277d87 | ||
|
|
5833832123 | ||
|
|
37187403d4 | ||
|
|
b24d52bbbd | ||
|
|
82142d11aa | ||
|
|
19b4d84c40 | ||
|
|
fd98b89900 | ||
|
|
1870406b5e | ||
|
|
64e5f63f70 | ||
|
|
02f75ac1cd | ||
|
|
52849a5bf3 | ||
|
|
f64c91faec | ||
|
|
c7ae14f239 | ||
|
|
9ab084d793 | ||
|
|
6514b98ce8 | ||
|
|
5a5cacee4f | ||
|
|
04451eee5d | ||
|
|
6e7558b364 | ||
|
|
9c02fed3af | ||
|
|
5d7195fb41 | ||
|
|
6cd1178347 | ||
|
|
b7b4a18624 | ||
|
|
de74b8c2cd | ||
|
|
2d07d79057 | ||
|
|
5f6a38c8b1 | ||
|
|
3496e6bfa9 | ||
|
|
fc5b62ba61 | ||
|
|
9d0a206240 | ||
|
|
ee6bbca18c | ||
|
|
e6d5472514 | ||
|
|
1e5f13087b | ||
|
|
abb89ecbc3 | ||
|
|
aa168bec5e | ||
|
|
f9116b22db | ||
|
|
5b957f5f0d | ||
|
|
a578aae3dc | ||
|
|
43e5d2fa2d | ||
|
|
b58d034716 | ||
|
|
3b77d79997 | ||
|
|
ef4c9af7dc | ||
|
|
8ba3cf172e | ||
|
|
3e446ce9dd | ||
|
|
394aa98713 | ||
|
|
a2ac882a05 | ||
|
|
cae36487f9 | ||
|
|
fade5eddbf | ||
|
|
66b2ffce31 | ||
|
|
13cd2d4daa | ||
|
|
8442faeeb1 | ||
|
|
6d18383b22 | ||
|
|
f9bcfe2f7a | ||
|
|
3621f95dbc | ||
|
|
536dead44d | ||
|
|
39d74e80a9 | ||
|
|
46e03dd248 | ||
|
|
152fee2c29 | ||
|
|
492a4d16b2 | ||
|
|
3d7c1fa1e7 | ||
|
|
69bb0e3656 | ||
|
|
4875cfa108 | ||
|
|
a28beb62c6 | ||
|
|
21c8960146 | ||
|
|
2ba638b123 | ||
|
|
8548940cb9 | ||
|
|
909bdfa0cf | ||
|
|
bfb3c56abe | ||
|
|
e06836f701 | ||
|
|
bc16cb79f2 | ||
|
|
176c46e3f2 | ||
|
|
af65055ce9 | ||
|
|
ad23ff19de | ||
|
|
cf65fc4f57 | ||
|
|
9ab83e7fbb | ||
|
|
1acf2aadba | ||
|
|
06833bcac9 | ||
|
|
3bf6b5293a | ||
|
|
c40d33df37 | ||
|
|
fe609f8275 | ||
|
|
3566f47f2d | ||
|
|
4b5526dffd | ||
|
|
4977390394 | ||
|
|
87804de452 | ||
|
|
bc55d2b476 | ||
|
|
3ff34a5c82 | ||
|
|
7e01e90e22 | ||
|
|
d568be6536 | ||
|
|
734e79604f | ||
|
|
227a962042 | ||
|
|
a1640175e5 | ||
|
|
7cf61570ef | ||
|
|
83762c668e | ||
|
|
ecd4a08b1c | ||
|
|
7b66462300 | ||
|
|
41639fd325 | ||
|
|
2fe82841ef | ||
|
|
9b7af87611 | ||
|
|
d99ccbc243 | ||
|
|
0f191eba5c | ||
|
|
26657c7ee3 | ||
|
|
a12749eb82 | ||
|
|
1a56d38116 | ||
|
|
0b9025af11 | ||
|
|
6f5a44cd11 | ||
|
|
5e1cc307b1 | ||
|
|
2197f1ec5e | ||
|
|
1da5fd3661 | ||
|
|
49e79b52b2 | ||
|
|
c9a657298c | ||
|
|
cfd7caf16c | ||
|
|
9744679550 | ||
|
|
9cdc502ce5 | ||
|
|
300a104949 | ||
|
|
efad6520a2 | ||
|
|
cc58351731 | ||
|
|
cf2f4c4bbb | ||
|
|
cc161b844c | ||
|
|
45ebf0670f | ||
|
|
9cfaa90232 | ||
|
|
edf4a248ed | ||
|
|
14d7ec6b4e | ||
|
|
cd2ca94c3d | ||
|
|
f1609aaa46 | ||
|
|
851364c0bf | ||
|
|
913a1743b5 | ||
|
|
dc5572d4b9 | ||
|
|
554560903c | ||
|
|
885e4a325c | ||
|
|
156ce7e711 | ||
|
|
71ecc05bd6 | ||
|
|
98b93d42c1 | ||
|
|
2e7935cacc | ||
|
|
50d64af7f6 | ||
|
|
e452fd3d2f | ||
|
|
64dd481402 | ||
|
|
28f647fa06 | ||
|
|
9eb1061cd1 | ||
|
|
3cab264213 | ||
|
|
fb56e7c866 | ||
|
|
234e368c4d | ||
|
|
4de4693505 | ||
|
|
5563c7d5f8 | ||
|
|
93225e5a30 | ||
|
|
0ab9846732 | ||
|
|
f6f1cf0ae7 | ||
|
|
69615d54db | ||
|
|
a990d0f404 | ||
|
|
4aeff48e74 | ||
|
|
78c1a75ec9 | ||
|
|
9e6664a80e | ||
|
|
b306a09747 | ||
|
|
fab52012ad | ||
|
|
fc39c714b6 | ||
|
|
5fff4bbb0e | ||
|
|
0a29571ad8 | ||
|
|
275fa1b3ca | ||
|
|
79889665e5 | ||
|
|
9888ad327c | ||
|
|
77ab2d7740 | ||
|
|
509dc4b541 | ||
|
|
ff84ee2d42 | ||
|
|
d838c2e8ad | ||
|
|
28d718acf6 | ||
|
|
adf546f29c | ||
|
|
b18cf277f6 | ||
|
|
8ab26eb852 | ||
|
|
aac43274a0 | ||
|
|
2fb2c4247a | ||
|
|
cb1d32d177 | ||
|
|
75e1985a6f | ||
|
|
0093b39f64 | ||
|
|
d19c2ef735 | ||
|
|
db527cfbf3 | ||
|
|
7da8ddb24f | ||
|
|
5d401ac9ac | ||
|
|
9605eafa00 | ||
|
|
783460c685 | ||
|
|
af906d8ace | ||
|
|
fca26a8793 | ||
|
|
27863def9c | ||
|
|
f1d4fd1d5d | ||
|
|
61b7e43d08 | ||
|
|
87ebe1918c | ||
|
|
4c5253c267 | ||
|
|
f5be8a0c86 | ||
|
|
5392582819 | ||
|
|
651ae4cdaa | ||
|
|
d87b5c5386 | ||
|
|
d5331b1997 | ||
|
|
0a6f8fa18e | ||
|
|
c1ba16eb80 | ||
|
|
0cd01b25bd | ||
|
|
f0c1300ec8 | ||
|
|
70eee2a224 | ||
|
|
42470d1d67 | ||
|
|
7fc58a3fa5 | ||
|
|
e186037c03 | ||
|
|
732a0ffea4 | ||
|
|
89f2e9fde1 | ||
|
|
c369541a7f | ||
|
|
0b7a157ab1 | ||
|
|
9b86c6ebb7 | ||
|
|
22cd07e414 | ||
|
|
47f919cb61 | ||
|
|
5b3c889b7c | ||
|
|
77ac53215c | ||
|
|
167c7125d2 | ||
|
|
ba56fd0279 | ||
|
|
8e795b729a | ||
|
|
a4eb597f35 | ||
|
|
0aeade69da | ||
|
|
90a3bd3307 | ||
|
|
7a61e3558e | ||
|
|
dd9c555bff | ||
|
|
50f84780bb | ||
|
|
e12372364d | ||
|
|
969611c9e6 | ||
|
|
06399fe3bd | ||
|
|
35c5a90b4e | ||
|
|
3c70cccd14 | ||
|
|
a3c24bc666 | ||
|
|
16b00d21a6 | ||
|
|
88f90c6240 | ||
|
|
a5c44dcb3b | ||
|
|
c0a2dd139a | ||
|
|
b354a26f0a | ||
|
|
b3de1205d9 | ||
|
|
f18e8ae421 | ||
|
|
a46b863802 | ||
|
|
2befa03cd2 | ||
|
|
763a108abb | ||
|
|
30a069c845 | ||
|
|
ffbf5cdc88 | ||
|
|
4318a65a99 | ||
|
|
bc433c56ae | ||
|
|
e43740c6bc | ||
|
|
6123bccf4b | ||
|
|
08106c20e8 | ||
|
|
eaaecbd5e5 | ||
|
|
29608099db | ||
|
|
7a773937c2 | ||
|
|
fa4a298bfa | ||
|
|
966bedbb49 | ||
|
|
159b6f3ca6 | ||
|
|
de1b0fc466 | ||
|
|
b6a3394dee | ||
|
|
f5ac51db13 | ||
|
|
df94f31998 | ||
|
|
1ec0b65fe8 | ||
|
|
d9a77ff658 | ||
|
|
c45118515c | ||
|
|
f9b8865351 | ||
|
|
1e92b3fc92 | ||
|
|
f34fb858ec | ||
|
|
1fdede5518 | ||
|
|
a454ec4944 | ||
|
|
5afbf103ac | ||
|
|
2ef1b76bfb | ||
|
|
f7d06d3b82 | ||
|
|
e405468471 | ||
|
|
9d2bacdc5f | ||
|
|
b44d4702be | ||
|
|
03803dbc23 | ||
|
|
9ea675ded1 | ||
|
|
c85d623e1c | ||
|
|
e6f1320b1d | ||
|
|
80f49e22e5 | ||
|
|
4884c27a9a | ||
|
|
4976a4dab4 | ||
|
|
5eb0b5faa7 | ||
|
|
5e33cb93c6 | ||
|
|
b4220d349f | ||
|
|
d4752685d0 | ||
|
|
0b06d2a4bd | ||
|
|
a37d9a193f | ||
|
|
b1d465e8bf | ||
|
|
31e17c305f | ||
|
|
72db8724ec | ||
|
|
b64cb22186 | ||
|
|
f5ca1220de | ||
|
|
fb54125ee6 | ||
|
|
be957d058f | ||
|
|
54a3e3c927 | ||
|
|
aabbf7c1c1 | ||
|
|
7773ba0804 | ||
|
|
9f812772fe | ||
|
|
75191afc3e | ||
|
|
1c25baccea | ||
|
|
b1d4ccc27e | ||
|
|
0d714847f3 | ||
|
|
760d4abbd0 | ||
|
|
7c1bb33ea9 | ||
|
|
5ba8ce549e | ||
|
|
a449c614ee | ||
|
|
12e6688f48 | ||
|
|
da47ef159b | ||
|
|
efc678d3cd | ||
|
|
d7a126ab85 | ||
|
|
bc9c976c95 | ||
|
|
26af23771d | ||
|
|
590f2d80d3 | ||
|
|
fdeb34ff0a | ||
|
|
ec76be776c | ||
|
|
4de5691428 | ||
|
|
ae5b339251 | ||
|
|
a6021943e1 | ||
|
|
765fcafc38 | ||
|
|
7a53881eb3 | ||
|
|
854f22f373 | ||
|
|
0566f98323 | ||
|
|
435a358c4c | ||
|
|
3f55422d45 | ||
|
|
9804c1fbb1 | ||
|
|
c3b8c4b630 | ||
|
|
8c4d8622d8 | ||
|
|
86770b32bb | ||
|
|
74c86c1e5f | ||
|
|
1538b74c63 | ||
|
|
5934c518e6 | ||
|
|
71dfd0cf9a | ||
|
|
984bf1f15f | ||
|
|
a14949b75a | ||
|
|
7bbe138d91 | ||
|
|
65dea70ae5 | ||
|
|
a07c8b9e11 | ||
|
|
69b82c61f8 | ||
|
|
fb8df74242 | ||
|
|
3674594832 | ||
|
|
1ee24cde07 | ||
|
|
93baed35f4 | ||
|
|
3e1420d496 | ||
|
|
ebc4324ef1 | ||
|
|
6ba04f4731 | ||
|
|
7359e23b65 | ||
|
|
01387bae94 | ||
|
|
df1d61ecdf | ||
|
|
cb644dbea6 | ||
|
|
fc5ea0ad51 | ||
|
|
8108dbae8a | ||
|
|
b14a9d6eca | ||
|
|
2f4a7f5410 | ||
|
|
dfa718f8ea | ||
|
|
f842700192 | ||
|
|
953d0295dc | ||
|
|
836f607aa0 | ||
|
|
2c6f205ea9 | ||
|
|
8154dd1830 | ||
|
|
686674c855 | ||
|
|
8e5d931be1 | ||
|
|
46509a04b0 | ||
|
|
d6e189134c | ||
|
|
0a05780e4d | ||
|
|
1671c99614 | ||
|
|
b5c8afe8f7 | ||
|
|
7e4ab8e14b | ||
|
|
35210ad02b | ||
|
|
9f853f0b36 | ||
|
|
559698c946 | ||
|
|
50c05efb48 | ||
|
|
94b9d3590d | ||
|
|
092016f942 | ||
|
|
24c899e709 | ||
|
|
e53cc648f0 | ||
|
|
4c541f3995 | ||
|
|
f56cbfc142 | ||
|
|
c21c4f1117 | ||
|
|
abf1f5c61c | ||
|
|
1d5b0793d0 | ||
|
|
5ba72fdbd4 | ||
|
|
c7d26d2444 | ||
|
|
22c146e350 | ||
|
|
1cb0ebd4f8 | ||
|
|
eff2e475ff | ||
|
|
aef475d8fc | ||
|
|
1ed2e55867 | ||
|
|
16ffec1f1e | ||
|
|
912e97dc3c | ||
|
|
ca2f924417 | ||
|
|
468fba8fcf | ||
|
|
b948dcee22 | ||
|
|
a633f4a3fe | ||
|
|
acb384c901 | ||
|
|
043228fe47 | ||
|
|
f29e7936d1 | ||
|
|
d4a7b8d9c9 | ||
|
|
fbabfe468a | ||
|
|
87c7eccee4 | ||
|
|
648eac5555 | ||
|
|
fe8ba8ca90 | ||
|
|
8fa4b5017c | ||
|
|
d2f39df3bf | ||
|
|
2ada289e74 | ||
|
|
1edf416461 | ||
|
|
6fae73b612 | ||
|
|
ec898748b1 | ||
|
|
5be67823ce | ||
|
|
fd868aec95 | ||
|
|
33bf5e26ff | ||
|
|
08363f95e0 | ||
|
|
dc62291fba | ||
|
|
43034cf3a3 | ||
|
|
88f047c312 | ||
|
|
46a635d4f4 | ||
|
|
882217be63 | ||
|
|
87a2650480 | ||
|
|
c41806bd70 | ||
|
|
ae7a6ba88c | ||
|
|
df78b31527 | ||
|
|
4040c62dda | ||
|
|
34bf9d5b45 | ||
|
|
80cd37fe4f | ||
|
|
21a7dbad53 | ||
|
|
c2b010d057 | ||
|
|
82b01851c7 | ||
|
|
0a81f0a09a | ||
|
|
e06cbb39b0 | ||
|
|
b52cc0c590 | ||
|
|
82376c8c47 | ||
|
|
e0b191b9a0 | ||
|
|
6ef6259629 | ||
|
|
52d08f9392 | ||
|
|
6812d3552a | ||
|
|
68ebb132cd | ||
|
|
203aa4824d | ||
|
|
047707dd35 | ||
|
|
7c7645c0c6 | ||
|
|
2d4ca15e10 | ||
|
|
9ca831713c | ||
|
|
56008c15c5 | ||
|
|
2a2811ec54 | ||
|
|
bb0c48f26a | ||
|
|
2f815fcec3 | ||
|
|
cc446f99ee | ||
|
|
9e83e67a0b | ||
|
|
bf589da7b8 | ||
|
|
21b278d42e | ||
|
|
2cd6a397dc | ||
|
|
b3aa56736e | ||
|
|
e52b72b0c6 | ||
|
|
6fa82bbe2e | ||
|
|
6b3c628e6b | ||
|
|
39e14d059e | ||
|
|
494d6633b5 | ||
|
|
690d405103 | ||
|
|
6a05048aed | ||
|
|
f75c010ecb | ||
|
|
f2c0c34c10 | ||
|
|
512517cc64 | ||
|
|
74f1d3cb74 | ||
|
|
f99cbd011a | ||
|
|
18d80cde42 | ||
|
|
6f2127abb8 | ||
|
|
cb1c023815 | ||
|
|
a9a6a00f87 | ||
|
|
060952697a | ||
|
|
a0579278c9 | ||
|
|
678684e24b | ||
|
|
27e3d18987 | ||
|
|
7ffe7f1a0c | ||
|
|
03c037da97 | ||
|
|
b7f035b78e | ||
|
|
66fe109eb9 | ||
|
|
f62cd63fbb | ||
|
|
e1e980cbb2 | ||
|
|
ca5543d6ea | ||
|
|
05cec14c9b | ||
|
|
1123fe0383 | ||
|
|
5855fe34ee | ||
|
|
030e57d0a9 | ||
|
|
b6f922eb92 | ||
|
|
2ab756a847 | ||
|
|
3611bba1d4 | ||
|
|
e07f32fb98 | ||
|
|
5ba065c8f1 | ||
|
|
1fb7541d14 | ||
|
|
0dc0ba21f9 | ||
|
|
e34f1f0b45 | ||
|
|
604d98f772 | ||
|
|
40eb2d083d | ||
|
|
3bca1ca795 | ||
|
|
ae4afd5154 | ||
|
|
27e732665a | ||
|
|
6415e89df9 | ||
|
|
4ef8785489 | ||
|
|
b7af3d92c9 | ||
|
|
82a80269d3 | ||
|
|
6112eedcc4 | ||
|
|
001bdc1dc9 | ||
|
|
fb2de4245d | ||
|
|
9164d8f138 | ||
|
|
fbcfbfc12c | ||
|
|
fb8e3a2364 | ||
|
|
375ef1c06d | ||
|
|
a064254bcf | ||
|
|
95cf31703a | ||
|
|
ff995c8d60 | ||
|
|
1f7b8ce1ed | ||
|
|
f2608a1a8d | ||
|
|
4e858f3230 | ||
|
|
ba7daf9766 | ||
|
|
d2fcd61541 | ||
|
|
417be8dbf0 | ||
|
|
9b145d15af | ||
|
|
6b9596d0d1 | ||
|
|
fa1ad69be7 | ||
|
|
1f29273561 | ||
|
|
4c21d781c4 | ||
|
|
746fc2963e | ||
|
|
289ef2a324 | ||
|
|
91eaf1c71e | ||
|
|
c22bedbfcf | ||
|
|
ef104e4126 | ||
|
|
0d884aa161 | ||
|
|
192fa8fc24 | ||
|
|
0ae24d12d8 | ||
|
|
6ecf2c8ac3 | ||
|
|
da7267de43 | ||
|
|
f134184a43 | ||
|
|
650a34ca73 | ||
|
|
009afcd6aa | ||
|
|
05f71f39a1 | ||
|
|
d3a3dc9200 | ||
|
|
9df44b1f3d | ||
|
|
964180a183 | ||
|
|
4ef51f4d8f | ||
|
|
ee39d940be | ||
|
|
db60299c54 | ||
|
|
7d3c31efc1 | ||
|
|
9e68ccb5a8 | ||
|
|
f05fc861d5 | ||
|
|
085408e5a3 | ||
|
|
6442a08d6f | ||
|
|
f58239e102 | ||
|
|
f4952aa770 | ||
|
|
5747253400 | ||
|
|
57c45a049f | ||
|
|
b3516310b0 | ||
|
|
9438c3ed46 | ||
|
|
35d36d3ec6 | ||
|
|
d852b5c9db | ||
|
|
bbb40a2842 | ||
|
|
e46963512e | ||
|
|
1ca5511e78 | ||
|
|
b6e368d730 | ||
|
|
94dfee53bc | ||
|
|
8996708164 | ||
|
|
b1f79e338f | ||
|
|
40387ca082 | ||
|
|
b02941d5aa | ||
|
|
fad31fbeb3 | ||
|
|
ca08fc0e9d | ||
|
|
7df64837fe | ||
|
|
fa70df366c | ||
|
|
70d4e99dc1 | ||
|
|
49272212bc | ||
|
|
736b2a17e9 | ||
|
|
ade420cf87 | ||
|
|
96ed46bd4a | ||
|
|
e334698e3b | ||
|
|
82d97f019d | ||
|
|
fb56206537 | ||
|
|
22fff296b5 | ||
|
|
954cb1da0d | ||
|
|
8c25de2aaf | ||
|
|
b362dca795 | ||
|
|
45c6daabe7 | ||
|
|
43c5c67bfa | ||
|
|
dde276f77e | ||
|
|
60ba6ba6cf | ||
|
|
f7a0938c38 | ||
|
|
4805e54ef3 | ||
|
|
a67599ce83 | ||
|
|
cd9867273f | ||
|
|
a0898d9223 | ||
|
|
11514051b6 | ||
|
|
0a1059e0a4 | ||
|
|
aec1d601a3 |
@@ -19,7 +19,10 @@ const SCOPES = [
|
||||
"editor",
|
||||
"logger",
|
||||
"theme",
|
||||
"server",
|
||||
"core",
|
||||
"fs",
|
||||
"ui",
|
||||
"clipper",
|
||||
"config",
|
||||
"ci",
|
||||
@@ -27,7 +30,9 @@ const SCOPES = [
|
||||
"docs",
|
||||
"refactor",
|
||||
"misc",
|
||||
"global"
|
||||
"common",
|
||||
"global",
|
||||
"docs"
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -21,4 +21,7 @@ packages/editor/styles/
|
||||
packages/editor/languages/
|
||||
|
||||
# editor mobile
|
||||
packages/editor-mobile/build.bundle
|
||||
packages/editor-mobile/build.bundle
|
||||
|
||||
# snapshots
|
||||
tap-snapshots
|
||||
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
27
.github/actions/setup-node-with-cache/action.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: "Setup node with cache action"
|
||||
description: "Setups node with cache"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
apps/mobile/package-lock.json
|
||||
apps/desktop/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
extensions/web-clipper/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
packages/sodium/package-lock.json
|
||||
packages/clipper/package-lock.json
|
||||
packages/crypto-worker/package-lock.json
|
||||
packages/editor-mobile/package-lock.json
|
||||
packages/editor/package-lock.json
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
53
.github/workflows/android.publish.internal.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Publish @notesnook/android-internal
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: "11"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Make Gradlew Executable
|
||||
run: cd apps/mobile/native/android && chmod +x ./gradlew
|
||||
|
||||
- name: Build unsigned app bundle
|
||||
run: yarn release:android:bundle
|
||||
|
||||
- name: Sign app bundle for Playstore release
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Publish to Playstore
|
||||
id: deploy
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: com.streetwriters.notesnook
|
||||
releaseFile: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
track: internal
|
||||
status: completed
|
||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||
51
.github/workflows/android.publish.yml
vendored
@@ -11,31 +11,20 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
apps/mobile/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
packages/crypto-worker/package-lock.json
|
||||
packages/editor-mobile/package-lock.json
|
||||
packages/editor/package-lock.json
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
architecture: 'x64'
|
||||
java-version: "11"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Make Gradlew Executable
|
||||
run: cd apps/mobile/native/android && chmod +x ./gradlew
|
||||
|
||||
@@ -50,7 +39,18 @@ jobs:
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Publish to Playstore
|
||||
id: deploy
|
||||
uses: r0adkll/upload-google-play@v1.1.1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: com.streetwriters.notesnook
|
||||
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
track: production
|
||||
status: completed
|
||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||
|
||||
- name: Build apks for Github release
|
||||
run: yarn release:android
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
signingKeyBase64: ${{ secrets.PUBLIC_SIGNING_KEY }}
|
||||
alias: ${{ secrets.PUBLIC_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||
|
||||
- name: Rename apk files
|
||||
run: |
|
||||
@@ -92,14 +92,3 @@ jobs:
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
|
||||
|
||||
- name: Publish to Playstore
|
||||
id: deploy
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: com.streetwriters.notesnook
|
||||
releaseFile: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
track: production
|
||||
status: completed
|
||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||
21
.github/workflows/core.tests.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
paths:
|
||||
- "packages/core/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/core.tests.yml"
|
||||
- ".github/workflows/core.tests.yml"
|
||||
pull_request:
|
||||
types:
|
||||
- "ready_for_review"
|
||||
@@ -24,25 +24,12 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
apps/mobile/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
packages/crypto-worker/package-lock.json
|
||||
packages/editor-mobile/package-lock.json
|
||||
packages/editor/package-lock.json
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
npm ci --prefer-offline --no-audit
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=core
|
||||
- name: Add environment variables
|
||||
run: |
|
||||
echo "USER_EMAIL=${{ secrets.USER_EMAIL }}" >> $GITHUB_ENV
|
||||
|
||||
118
.github/workflows/desktop.publish.yml
vendored
@@ -28,26 +28,12 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
apps/mobile/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
packages/crypto-worker/package-lock.json
|
||||
packages/editor-mobile/package-lock.json
|
||||
packages/editor/package-lock.json
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm i --ignore-scripts --prefer-offline --no-audit
|
||||
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
@@ -64,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Collect app metadata
|
||||
id: app_metadata
|
||||
working-directory: ./apps/web/desktop
|
||||
working-directory: ./apps/desktop
|
||||
run: |
|
||||
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
|
||||
|
||||
@@ -91,6 +77,9 @@ jobs:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Setup notarization
|
||||
run: |
|
||||
mkdir -p ~/private_keys/
|
||||
@@ -98,13 +87,13 @@ jobs:
|
||||
|
||||
- name: Collect app metadata
|
||||
id: app_metadata
|
||||
working-directory: ./apps/web/desktop
|
||||
working-directory: ./apps/desktop
|
||||
run: |
|
||||
echo ::set-output name=apple_app_id::$(cat package.json | jq -r .appAppleId)
|
||||
echo ::set-output name=app_bundle_id::$(cat package.json | jq -r .build.appId)
|
||||
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
|
||||
echo ::set-output name=bundle_version::$(cat package.json | jq -r .build.mac.bundleVersion)
|
||||
|
||||
|
||||
- name: Get App Store Version
|
||||
id: appstore
|
||||
uses: streetwriters/appstore-connect-app-version@develop
|
||||
@@ -115,24 +104,20 @@ jobs:
|
||||
private-key-raw: ${{ secrets.api_key }}
|
||||
platform: "MAC_OS"
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/desktop/build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: npm i
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install provisioning profile
|
||||
run: echo "${{ secrets.MAC_PROVISIONING_PROFILE }}" | base64 --decode > embedded.provisionprofile
|
||||
working-directory: ./apps/web/desktop
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build app for Mac App Store
|
||||
if: inputs.publish-apple && steps.appstore.outputs.app-version-latest != steps.app_metadata.outputs.app_version
|
||||
@@ -140,9 +125,9 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.mac_certs }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
||||
run: |
|
||||
npm run build:mas
|
||||
npx nx run release --project @notesnook/desktop -- --variant=mas
|
||||
npx electron-builder --mac mas --universal -p never
|
||||
working-directory: ./apps/web/desktop
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build zip and dmg
|
||||
env:
|
||||
@@ -153,26 +138,26 @@ jobs:
|
||||
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
|
||||
APPLE_API_KEY_ISSUER: ${{ secrets.api_key_issuer_id }}
|
||||
run: |
|
||||
npm run build
|
||||
npx nx run release --project @notesnook/desktop
|
||||
if [ ${{ inputs.publish-github }} == true ]; then
|
||||
npx electron-builder --mac zip dmg --arm64 --x64 -p always
|
||||
else
|
||||
npx electron-builder --mac zip dmg --arm64 --x64 -p never
|
||||
fi
|
||||
working-directory: ./apps/web/desktop
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Deploy to Testflight
|
||||
if: inputs.publish-apple && steps.appstore.outputs.app-version-latest != steps.app_metadata.outputs.app_version
|
||||
env:
|
||||
API_KEY_ID: ${{ secrets.api_key_id }}
|
||||
API_KEY_ISSUER_ID: ${{ secrets.api_key_issuer_id }}
|
||||
run: |
|
||||
package=$(find ./dist/mas-universal/notesnook*.pkg)
|
||||
run: |
|
||||
package=$(find ./output/mas-universal/notesnook*.pkg)
|
||||
|
||||
echo "Uploading ${package} using altool..."
|
||||
|
||||
xcrun altool --upload-package $package -t osx --apiKey $API_KEY_ID --apiIssuer $API_KEY_ISSUER_ID --apple-id ${{ steps.app_metadata.outputs.apple_app_id }} --bundle-id ${{ steps.app_metadata.outputs.app_bundle_id }} --bundle-short-version-string ${{ steps.app_metadata.outputs.app_version }} --bundle-version ${{ steps.app_metadata.outputs.bundle_version }}
|
||||
working-directory: ./apps/web/desktop
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
@@ -183,6 +168,9 @@ jobs:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Setup Snapcraft Auth
|
||||
if: inputs.publish-snap
|
||||
run: echo "SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.snapcraft_token }}" >> $GITHUB_ENV
|
||||
@@ -190,48 +178,44 @@ jobs:
|
||||
- name: Install Snapcraft
|
||||
uses: samuelmeuli/action-snapcraft@v1
|
||||
if: inputs.publish-snap
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/desktop/build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: npm i
|
||||
working-directory: ./apps/web/desktop
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run build
|
||||
working-directory: ./apps/web/desktop
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build snap
|
||||
if: inputs.publish-snap
|
||||
run: |
|
||||
npx electron-builder --linux snap -p never
|
||||
working-directory: ./apps/web/desktop
|
||||
npx electron-builder --linux snap:x64 -p never
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build AppImage deb and rpm
|
||||
- name: Build AppImage
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [ ${{ inputs.publish-github }} == true ]; then
|
||||
npx electron-builder --linux AppImage deb rpm -p always
|
||||
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p always
|
||||
else
|
||||
npx electron-builder --linux AppImage deb rpm -p never
|
||||
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p never
|
||||
fi
|
||||
working-directory: ./apps/web/desktop
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Publish on Snapcraft
|
||||
if: inputs.publish-snap
|
||||
run: |
|
||||
snapcraft upload --release=stable ./dist/notesnook_linux_amd64.snap
|
||||
working-directory: ./apps/web/desktop
|
||||
snapcraft upload --release=stable ./output/notesnook_linux_amd64.snap
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
@@ -243,31 +227,29 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/desktop/build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: npm i
|
||||
working-directory: ./apps/web/desktop
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run build
|
||||
working-directory: ./apps/web/desktop
|
||||
- name: Build
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
|
||||
- name: Build app
|
||||
- name: Publish
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if ($${{ inputs.publish-github }} -eq $true) {
|
||||
npx electron-builder --win --publish always
|
||||
yarn electron-builder --win --publish always
|
||||
} else {
|
||||
npx electron-builder --win --publish never
|
||||
yarn electron-builder --win --publish never
|
||||
}
|
||||
working-directory: ./apps/web/desktop
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
43
.github/workflows/editor.tests.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Test @notesnook/editor
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "packages/editor/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/editor.tests.yml"
|
||||
pull_request:
|
||||
types:
|
||||
- "ready_for_review"
|
||||
- "opened"
|
||||
- "synchronize"
|
||||
- "reopened"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=editor
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build editor
|
||||
run: npx nx build @notesnook/editor
|
||||
|
||||
- name: Run all @notesnook/editor tests
|
||||
run: npx nx test @notesnook/editor
|
||||
35
.github/workflows/help.publish.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Publish Notesnook Help
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "docs/help/**"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Install docgen
|
||||
run: cargo install --git https://github.com/thecodrr/docgen
|
||||
|
||||
- name: Build site
|
||||
run: docgen build --release
|
||||
working-directory: docs/help
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Publish on Cloudflare Pages
|
||||
run: npx --yes wrangler pages deploy --project-name notesnook-help ./docs/help/site/ --branch main
|
||||
2
.github/workflows/nscurl.test.yml
vendored
@@ -11,4 +11,4 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Run ats-diagnostics
|
||||
run: nscurl --ats-diagnostics https://payments.streetwriters.co
|
||||
run: nscurl --ats-diagnostics https://payments.streetwriters.co
|
||||
|
||||
20
.github/workflows/web.publish.yml
vendored
@@ -11,26 +11,12 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
apps/mobile/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
packages/crypto-worker/package-lock.json
|
||||
packages/editor-mobile/package-lock.json
|
||||
packages/editor/package-lock.json
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm i --ignore-scripts --prefer-offline --no-audit
|
||||
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
|
||||
38
.github/workflows/web.tests.yml
vendored
@@ -25,26 +25,12 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
apps/mobile/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
packages/crypto-worker/package-lock.json
|
||||
packages/editor-mobile/package-lock.json
|
||||
packages/editor/package-lock.json
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm i --ignore-scripts --prefer-offline --no-audit
|
||||
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
@@ -76,25 +62,11 @@ jobs:
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
apps/mobile/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
packages/crypto-worker/package-lock.json
|
||||
packages/editor-mobile/package-lock.json
|
||||
packages/editor/package-lock.json
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm i --ignore-scripts --prefer-offline --no-audit
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm install -D @playwright/test otplib
|
||||
|
||||
- name: Validate build extraction
|
||||
|
||||
5
.gitignore
vendored
@@ -6,4 +6,7 @@ dist
|
||||
nx-cloud.env
|
||||
.idea
|
||||
.eslintcache
|
||||
.env.local
|
||||
.env.local
|
||||
.nyc_output
|
||||
site
|
||||
node_modules.backup
|
||||
@@ -5,5 +5,6 @@ module.exports = {
|
||||
printWidth: 80,
|
||||
useTabs: false,
|
||||
tabWidth: 2,
|
||||
bracketSpacing: true
|
||||
bracketSpacing: true,
|
||||
endOfLine: "lf"
|
||||
};
|
||||
|
||||
11
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-playwright.playwright",
|
||||
"firefox-devtools.vscode-firefox-debug",
|
||||
"msjsdiag.vscode-react-native"
|
||||
]
|
||||
}
|
||||
14
.vscode/settings.json
vendored
@@ -1,3 +1,15 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
"files.eol": "\n",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"eslint.run": "onSave",
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"eslint.options": {
|
||||
"cache": true,
|
||||
"cacheStrategy": "content"
|
||||
},
|
||||
"git.rebaseWhenSync": true
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ Thank you so much for considering to contribute to Notesnook! If you have no ide
|
||||
|
||||
Ugh! Bugs!
|
||||
|
||||
> A bug is when software behaves in a way that you didn't expect, and the developer didn't intend.
|
||||
> A bug is when software behaves in a way you didn't expect, which the developer didn't intend.
|
||||
|
||||
To help us understand what's going on, we first want to **make sure you're using the latest version of Notesnook**.
|
||||
To help us understand what's happening, we first want to **make sure you're using the latest version of Notesnook**.
|
||||
|
||||
Once you've **confirmed that the bug still exists in the latest version**, you'll want to check to make sure it's not something we already know about in the [opened GitHub issues](https://github.com/streetwriters/notesnook/issues).
|
||||
|
||||
@@ -28,15 +28,9 @@ If you've thought of a way that Notesnook could be better, we want to hear about
|
||||
|
||||
Before you open a new feature request, please make sure it's not a duplicate. **Duplicate feature requests & bug reports are closed immediately.**
|
||||
|
||||
### Improving documentation
|
||||
|
||||
All app related documentation can be found in the `docs/` directory. All the files in this directory are simple markdown files which you can edit either directly on GitHub, or in your favorite text editor.
|
||||
|
||||
If you find a mistake in the documentation or would like to contribute something new to the documentation, feel free to open a pull request; creating an issue for this is not required.
|
||||
|
||||
### Helping out in the issue tracker
|
||||
|
||||
There are always new issues getting opened that need to be triaged, sorted & organized, so the developers can easily find the most critical and/or relevant bugs to fix. Any help in this regard is appreciated.
|
||||
New issues are always opened that need to be triaged, sorted & organized, so the developers can easily find the most critical and/or relevant bugs to fix. Any help in this regard is appreciated.
|
||||
|
||||
In addition to this, you can help out in the following ways:
|
||||
|
||||
@@ -68,7 +62,7 @@ Once you are done, [open a new pull request](https://docs.github.com/en/pull-req
|
||||
|
||||
1. Fork [the repository](https://github.com/streetwriters/notesnook) and create your branch from `master` (you can name your branch anything).
|
||||
2. Run `npm run bootstrap` in the repository root.
|
||||
3. If you’ve fixed a bug or added code that should be tested, add tests!
|
||||
3. If you've fixed a bug or added code that should be tested, add tests!
|
||||
4. Ensure the test suite passes (`npm run test`).
|
||||
5. Format your code with prettier (`npm run prettier`).
|
||||
6. Make sure your code lints (`npm run lint`). Tip: `npm run linc` to only check changed files.
|
||||
@@ -91,7 +85,11 @@ The DCO is a declaration attached to every contribution made by every developer
|
||||
Signed-off-by: Jane Smith <jane.smith@email.com>
|
||||
```
|
||||
|
||||
You may type this line on your own when writing your commit messages. However, if your [user.name](http://user.name) and [user.email](http://user.email) are set in your git configs, you can use `-s` or `--signoff` to add the `Signed-off-by` line to the end of the commit message.
|
||||
On VSCode, it looks like this:
|
||||
|
||||

|
||||
|
||||
You may type this line on your own when writing your commit messages. However, if your [user.name](http://user.name) and [user.email](http://user.email) are set in your git configs, you can use `-s` or `--signoff` to add the `Signed-off-by` line to the end of the commit message automatically.
|
||||
|
||||
#### Include `<scope>:` in your commit message
|
||||
|
||||
@@ -133,11 +131,11 @@ We use an automatic code formatter called [Prettier](https://prettier.io/). Run
|
||||
|
||||
Then, our linter will catch most issues that may exist in your code. You can check the status of your code styling by simply running `npm run lint`.
|
||||
|
||||
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnb’s Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
|
||||
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnb's Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
|
||||
|
||||
## Git Branch Organization
|
||||
|
||||
Submit all changes directly to the [`master branch`](https://github.com/streetwriters/notesnook). We don’t use separate branches for development or for upcoming releases. This requires us to always keep the `master` branch in a deployable state which means:
|
||||
Submit all changes directly to the [`master branch`](https://github.com/streetwriters/notesnook). We don't use separate branches for development or for upcoming releases. This requires us to always keep the `master` branch in a deployable state which means:
|
||||
|
||||
1. All tests must be passing at all times
|
||||
2. There should be as few breaking changes as possible
|
||||
|
||||
39
README.md
@@ -10,11 +10,11 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Notesnook is a free (as in speech) & open source note taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
|
||||
Notesnook is a free (as in speech) & open-source note-taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
|
||||
|
||||
Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. Our goal is to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
|
||||
Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. We aim to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
|
||||
|
||||
This repository contains all the code required to build & use the Notesnook web, desktop & mobile clients. If you are looking for a full features list or screenshots, please check the [website](https://notesnook.com/).
|
||||
This repository contains all the code required to build & use the Notesnook web, desktop & mobile clients. If you are looking for a full feature list or screenshots, please check the [website](https://notesnook.com/).
|
||||
|
||||
## Developer guide
|
||||
|
||||
@@ -25,25 +25,30 @@ Notesnook is built using the following technologies:
|
||||
1. JavaScript/Typescript — this repo is in a hybrid state. A lot of the newer code is being written in Typescript & the old code is slowly being ported over.
|
||||
2. React — the whole front-end across all platforms is built using React.
|
||||
3. React Native — For mobile apps we are using React Native
|
||||
4. NPM — listed here because we **don't** use Yarn or PNPM or XYZ across any of our projects.
|
||||
5. Lerna & Nx — maintaining monorepos is hard but Nx makes it easier.
|
||||
4. Electron — For desktop app
|
||||
5. NPM — listed here because we **don't** use Yarn or PNPM or XYZ across any of our projects.
|
||||
6. Nx — maintaining monorepos is hard but Nx makes it easier.
|
||||
|
||||
> **Note: Each project in the monorepo contains its own architecture details which you can refer to.**
|
||||
|
||||
### Monorepo structure
|
||||
|
||||
| Name | Path | Description |
|
||||
| -------------------------- | -------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| `@notesnook/web` | [/apps/web](/apps/web) | Web/Desktop clients |
|
||||
| `@notesnook/mobile` | [/apps/mobile](/apps/mobile) | Android/iOS clients |
|
||||
| `@notesnook/core` | [/packages/core](/packages/core) | Shared core between all platforms |
|
||||
| `@notesnook/crypto` | [/packages/crypto](/packages/crypto) | Cryptography library wrapper around libsodium |
|
||||
| `@notesnook/editor` | [/packages/editor](/packages/editor) | Notesnook editor + all extensions |
|
||||
| `@notesnook/editor-mobile` | [/packages/editor-mobile](/packages/editor-mobile) | A very thin wrapper around `@notesnook/editor` for mobile clients |
|
||||
| `@notesnook/logger` | [/packages/logger](/packages/logger) | Simple & pluggable logger |
|
||||
| `@notesnook/crypto-worker` | [/packages/crypto-worker](/packages/crypto-worker) | Helpers to use `@notesnook/crypto` from a Worker |
|
||||
| `@notesnook/streamable-fs` | [/packages/streamable-fs](/packages/streamable-fs) | Streaming interface around an IndexedDB based file system |
|
||||
| `@notesnook/theme` | [/packages/theme](/packages/theme) | The core theme used in web & desktop clients |
|
||||
| Name | Path | Description |
|
||||
| -------------------------- | -------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| `@notesnook/web` | [/apps/web](/apps/web) | Web client |
|
||||
| `@notesnook/desktop` | [/apps/desktop](/apps/desktop) | Desktop client |
|
||||
| `@notesnook/mobile` | [/apps/mobile](/apps/mobile) | Android/iOS clients |
|
||||
| `@notesnook/web-clipper` | [/extensions/web-clipper](/extensions/web-clipper) | Web clipper |
|
||||
| `@notesnook/core` | [/packages/core](/packages/core) | Shared core between all platforms |
|
||||
| `@notesnook/crypto` | [/packages/crypto](/packages/crypto) | Cryptography library wrapper around libsodium |
|
||||
| `@notesnook/clipper` | [/packages/clipper](/packages/clipper) | Web clipper core handling everything related to actual page clipping |
|
||||
| `@notesnook/editor` | [/packages/editor](/packages/editor) | Notesnook editor + all extensions |
|
||||
| `@notesnook/editor-mobile` | [/packages/editor-mobile](/packages/editor-mobile) | A very thin wrapper around `@notesnook/editor` for mobile clients |
|
||||
| `@notesnook/logger` | [/packages/logger](/packages/logger) | Simple & pluggable logger |
|
||||
| `@notesnook/sodium` | [/packages/sodium](/packages/sodium) | Wrapper around libsodium to support Node.js & Browser |
|
||||
| `@notesnook/crypto-worker` | [/packages/crypto-worker](/packages/crypto-worker) | Helpers to use `@notesnook/crypto` from a Worker |
|
||||
| `@notesnook/streamable-fs` | [/packages/streamable-fs](/packages/streamable-fs) | Streaming interface around an IndexedDB based file system |
|
||||
| `@notesnook/theme` | [/packages/theme](/packages/theme) | The core theme used in web & desktop clients |
|
||||
|
||||
### Contributing guidelines
|
||||
|
||||
|
||||
4
apps/desktop/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
build
|
||||
output
|
||||
dist
|
||||
58
apps/desktop/README.md
Normal file
@@ -0,0 +1,58 @@
|
||||
<p align="center">
|
||||
<img style="align:center;" src="/resources/screenshots/web.jpg" alt="Notesnook desktop screenshot" width="600" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">Notesnook Desktop</h1>
|
||||
<h3 align="center">The desktop app is built using Electron & Typescript.</h3>
|
||||
<p align="center">
|
||||
<a href="https://notesnook.com/downloads">Downloads</a> | <a href="#developer-guide">Developer guide</a> | <a href="#build-instructions">How to build?</a>
|
||||
</p>
|
||||
|
||||
## Getting started
|
||||
|
||||
## Build instructions
|
||||
|
||||
> **Before you start, it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
|
||||
|
||||
### Setting up the development environment
|
||||
|
||||
Requirements:
|
||||
|
||||
1. [Node.js](https://nodejs.org/en/download/)
|
||||
2. [git](https://git-scm.com/downloads)
|
||||
3. NPM (not yarn or pnpm)
|
||||
|
||||
Before you can do anything, you'll need to [install Node.js](https://nodejs.org/en/download/) v16 or later on your system.
|
||||
|
||||
Once you have completed the setup, the first step is to `clone` the monorepo:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/streetwriters/notesnook.git
|
||||
|
||||
# change directory
|
||||
cd notesnook
|
||||
```
|
||||
|
||||
Once you are inside the `./notesnook` directory, run the preparation step:
|
||||
|
||||
```bash
|
||||
# this might take a while to complete
|
||||
npm install
|
||||
```
|
||||
|
||||
Now you can finally start the desktop app:
|
||||
|
||||
```bash
|
||||
npm run start:desktop
|
||||
```
|
||||
|
||||
## Developer guide
|
||||
|
||||
### The tech stack
|
||||
|
||||
We try to keep the stack as lean as possible:
|
||||
|
||||
1. Electron
|
||||
2. tRPC: for cross-communication between the web & desktop parts
|
||||
3. zod: for runtime typechecking
|
||||
4. yargs: for CLI argument parsing
|
||||
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 520 B After Width: | Height: | Size: 520 B |
|
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 917 B |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 353 KiB |
BIN
apps/desktop/assets/icons/note-add.dark.ico
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/desktop/assets/icons/note-add.ico
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/desktop/assets/icons/notebook-add.dark.ico
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/desktop/assets/icons/notebook-add.ico
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/desktop/assets/icons/quit.dark.ico
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/desktop/assets/icons/quit.ico
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/desktop/assets/icons/reminder-add.dark.ico
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/desktop/assets/icons/reminder-add.ico
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/desktop/assets/icons/tray-icon.dark.ico
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
apps/desktop/assets/icons/tray-icon.ico
Normal file
|
After Width: | Height: | Size: 36 KiB |
@@ -16,16 +16,12 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/* eslint-disable no-var */
|
||||
|
||||
function withAnimation(_duration = 300) {
|
||||
return;
|
||||
import { BrowserWindow } from "electron";
|
||||
|
||||
declare global {
|
||||
var window: BrowserWindow | null;
|
||||
var RELEASE: boolean;
|
||||
var MAC_APP_STORE: boolean;
|
||||
}
|
||||
|
||||
function withSpringAnimation(_duration = 300) {
|
||||
return;
|
||||
}
|
||||
|
||||
export default {
|
||||
withAnimation,
|
||||
withSpringAnimation
|
||||
};
|
||||
5123
apps/desktop/package-lock.json
generated
Normal file
@@ -2,29 +2,47 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "2.4.0",
|
||||
"version": "2.5.8",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./build/electron.js",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"homepage": "https://notesnook.com/",
|
||||
"repository": "https://github.com/streetwriters/notesnook",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"diary": "^0.3.1",
|
||||
"electron-updater": "^5.3.0",
|
||||
"isomorphic-fetch": "^3.0.0"
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
"@trpc/client": "10.31.0",
|
||||
"@trpc/server": "10.31.0",
|
||||
"electron-trpc": "0.5.2",
|
||||
"electron-updater": "6.1.1",
|
||||
"icojs": "^0.17.1",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"yargs": "^17.6.2",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"electron": "^21.1.1",
|
||||
"electron-builder": "^23.6.0",
|
||||
"electron-builder-notarize": "^1.5.0",
|
||||
"electron-reloader": "^1.2.3",
|
||||
"esbuild": "^0.15.11"
|
||||
"@types/node": "^18.15.0",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "24.5.1",
|
||||
"electron-builder": "24.4.0",
|
||||
"electron-builder-notarize": "^1.5.1",
|
||||
"esbuild": "^0.17.19",
|
||||
"node-fetch": "^3.3.1",
|
||||
"tree-kill": "^1.2.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"dmg-license": "^1.0.11"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "esbuild ./electron.js ./preload.js --external:electron --external:fsevents --minify --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
|
||||
"build:mas": "esbuild ./electron.js ./preload.js --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
|
||||
"pack": "rm -rf ./build && cp -r ../build ./ && npm run build && yarn electron-builder --linux AppImage && ./dist/notesnook_linux_x86_64.AppImage"
|
||||
"start": "node scripts/dev.mjs",
|
||||
"staging": "node scripts/build.mjs --run",
|
||||
"release": "node scripts/build.mjs",
|
||||
"build": "tsc",
|
||||
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --minify --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
|
||||
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"author": {
|
||||
"name": "Streetwriters (Private) Limited",
|
||||
@@ -36,12 +54,17 @@
|
||||
"productName": "Notesnook",
|
||||
"copyright": "Copyright © 2023 Streetwriters (Private) Limited",
|
||||
"artifactName": "notesnook_${os}_${arch}.${ext}",
|
||||
"npmRebuild": false,
|
||||
"asar": false,
|
||||
"files": [
|
||||
"!*.chunk.js.map",
|
||||
"!*.chunk.js.LICENSE.txt",
|
||||
"build/",
|
||||
"!build/screenshots",
|
||||
"!node_modules"
|
||||
"!build/screenshots${/*}",
|
||||
"!build/banner.jpg",
|
||||
"!build/*.ico",
|
||||
"!build/*.png",
|
||||
"!node_modules${/*}"
|
||||
],
|
||||
"afterSign": "electron-builder-notarize",
|
||||
"afterPack": "./scripts/removeLocales.js",
|
||||
@@ -97,8 +120,20 @@
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis",
|
||||
"portable"
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "portable",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "assets/icons/app.ico"
|
||||
},
|
||||
@@ -111,15 +146,44 @@
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
"AppImage",
|
||||
"deb",
|
||||
"rpm",
|
||||
"snap"
|
||||
{
|
||||
"target": "AppImage",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "snap",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"category": "Office",
|
||||
"icon": "assets/icons/app.icns",
|
||||
"description": "Your private note taking space",
|
||||
"executableName": "notesnook"
|
||||
"executableName": "notesnook",
|
||||
"desktop": {
|
||||
"actions": [
|
||||
{
|
||||
"id": "new-note",
|
||||
"name": "New note",
|
||||
"args": "new note"
|
||||
},
|
||||
{
|
||||
"id": "new-notebook",
|
||||
"name": "New notebook",
|
||||
"args": "new notebook"
|
||||
},
|
||||
{
|
||||
"id": "new-reminder",
|
||||
"name": "New reminder",
|
||||
"args": "new reminder"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"snap": {
|
||||
"autoStart": false,
|
||||
@@ -129,9 +193,12 @@
|
||||
"extraResources": [
|
||||
"./assets/**"
|
||||
],
|
||||
"extraMetadata": {
|
||||
"main": "./build/electron.js"
|
||||
},
|
||||
"directories": {
|
||||
"buildResources": "assets",
|
||||
"output": "./dist/"
|
||||
"output": "./output/"
|
||||
},
|
||||
"publish": [
|
||||
{
|
||||
36
apps/desktop/patches/app-builder-lib+24.4.0.patch
Normal file
@@ -0,0 +1,36 @@
|
||||
diff --git a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
index 3ba5d6a..cf8e147 100644
|
||||
--- a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
+++ b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
@@ -105,6 +105,7 @@ class LinuxTargetHelper {
|
||||
StartupWMClass: appInfo.productName,
|
||||
...extra,
|
||||
...targetSpecificOptions.desktop,
|
||||
+ actions: undefined
|
||||
};
|
||||
const description = this.getDescription(targetSpecificOptions);
|
||||
if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
|
||||
@@ -148,6 +149,23 @@ class LinuxTargetHelper {
|
||||
data += `\n${name}=${desktopMeta[name]}`;
|
||||
}
|
||||
data += "\n";
|
||||
+
|
||||
+ if (targetSpecificOptions.desktop.actions) {
|
||||
+ let actionsData = "";
|
||||
+ const validActions = [];
|
||||
+ for (const action of targetSpecificOptions.desktop.actions) {
|
||||
+ if (!action.id || !action.name || !action.args) continue;
|
||||
+ actionsData += "\n";
|
||||
+ actionsData += `[Desktop Action ${action.id}]
|
||||
+Name=${action.name}
|
||||
+Exec=${desktopMeta.Exec} ${action.args}`;
|
||||
+ actionsData += "\n";
|
||||
+
|
||||
+ validActions.push(action.id);
|
||||
+ }
|
||||
+
|
||||
+ data += `Actions=${validActions.join(";")};\n${actionsData}`
|
||||
+ }
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
}
|
||||
23
apps/desktop/patches/electron-trpc+0.5.2.patch
Normal file
@@ -0,0 +1,23 @@
|
||||
diff --git a/node_modules/electron-trpc/dist/main.mjs b/node_modules/electron-trpc/dist/main.mjs
|
||||
index 379cf3b..2644e5d 100644
|
||||
--- a/node_modules/electron-trpc/dist/main.mjs
|
||||
+++ b/node_modules/electron-trpc/dist/main.mjs
|
||||
@@ -221,9 +221,16 @@ class G {
|
||||
i(this, c).includes(r) || (i(this, c).push(r), I(this, T, W).call(this, r));
|
||||
}
|
||||
detachWindow(r) {
|
||||
+
|
||||
y(this, c, i(this, c).filter((n) => n !== r));
|
||||
- for (const [n, t] of i(this, u).entries())
|
||||
- n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
|
||||
+ for (const [n, t] of i(this, u).entries()) {
|
||||
+ try {
|
||||
+ n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
|
||||
+ } catch(e) {
|
||||
+ console.error(e);
|
||||
+ // ignore
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
}
|
||||
c = new WeakMap(), u = new WeakMap(), T = new WeakSet(), W = function(r) {
|
||||
117
apps/desktop/scripts/build.mjs
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { existsSync } from "fs";
|
||||
import yargs from "yargs-parser";
|
||||
import os from "os";
|
||||
import * as childProcess from "child_process";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const args = yargs(process.argv);
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const sodiumNativePrebuildPath = (arch) =>
|
||||
path.join(
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`crypto`,
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`sodium`,
|
||||
`node_modules`,
|
||||
`sodium-native`,
|
||||
`prebuilds`,
|
||||
`${os.platform()}-${arch}`
|
||||
);
|
||||
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
|
||||
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
||||
await exec(`cd ${webAppPath} && npm run build:desktop`);
|
||||
}
|
||||
|
||||
await fs.cp(path.join(webAppPath, "build"), "build", {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
|
||||
if (args.variant === "mas") {
|
||||
await exec(`npm run bundle:mas`);
|
||||
} else {
|
||||
await exec(`npm run bundle`);
|
||||
}
|
||||
|
||||
await exec(`npx tsc`);
|
||||
|
||||
if (existsSync(sodiumNativePrebuildPath("x64"))) {
|
||||
console.log("copying sodium-native-x64");
|
||||
await fs.cp(
|
||||
sodiumNativePrebuildPath("x64"),
|
||||
path.join("build", "prebuilds", `${process.platform}-x64`),
|
||||
{
|
||||
recursive: true,
|
||||
force: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (existsSync(sodiumNativePrebuildPath("ia32"))) {
|
||||
console.log("copying sodium-native-ia32");
|
||||
await fs.cp(
|
||||
sodiumNativePrebuildPath("ia32"),
|
||||
path.join("build", "prebuilds", `${process.platform}-ia32`),
|
||||
{
|
||||
recursive: true,
|
||||
force: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (existsSync(sodiumNativePrebuildPath("arm64"))) {
|
||||
console.log("copying sodium-native-arm64");
|
||||
await fs.cp(
|
||||
sodiumNativePrebuildPath("arm64"),
|
||||
path.join("build", "prebuilds", `${process.platform}-arm64`),
|
||||
{
|
||||
recursive: true,
|
||||
force: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (args.run) {
|
||||
await exec(`npx electron-builder --dir --x64`);
|
||||
if (process.platform === "win32") {
|
||||
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
|
||||
} else if (process.platform === "darwin") {
|
||||
await exec(`./output/darwin-unpacked/Notesnook`);
|
||||
} else {
|
||||
await exec(`./output/linux-unpacked/Notesnook`);
|
||||
}
|
||||
}
|
||||
|
||||
async function exec(cmd) {
|
||||
return childProcess.execSync(cmd, {
|
||||
env: process.env,
|
||||
stdio: "inherit"
|
||||
});
|
||||
}
|
||||
197
apps/desktop/scripts/dev.mjs
Normal file
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import chokidar from "chokidar";
|
||||
import { execSync, spawn } from "child_process";
|
||||
import { fileURLToPath } from "url";
|
||||
import treekill from "tree-kill";
|
||||
import crypto from "crypto";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const sodiumNativePrebuildPath = path.join(
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`crypto`,
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`sodium`,
|
||||
`node_modules`,
|
||||
`sodium-native`,
|
||||
`prebuilds`
|
||||
);
|
||||
const RUNNING_PROCESSES = [];
|
||||
const RESTARTABLE_PROCESSES = [];
|
||||
let lastBundleHash = null;
|
||||
|
||||
await onChange(true);
|
||||
|
||||
console.log("Watching...");
|
||||
|
||||
const watcher = chokidar.watch("src/", { ignoreInitial: true });
|
||||
watcher.on("all", () => {
|
||||
onChange(false);
|
||||
});
|
||||
|
||||
process.on("SIGINT", async (s) => {
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
async function onChange(first) {
|
||||
if (first) {
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
}
|
||||
|
||||
await exec(`npm run bundle`);
|
||||
execAsync(`npx`, [`tsc`]);
|
||||
|
||||
if (await isBundleSame()) {
|
||||
console.log("Bundle is same. Doing nothing.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (first) {
|
||||
await fs.cp(sodiumNativePrebuildPath, "build/prebuilds", {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
}
|
||||
|
||||
if (first) {
|
||||
await spawnAndWaitUntil(
|
||||
["npx", "nx", "start:desktop", "@notesnook/web"],
|
||||
path.join(__dirname, "..", "..", ".."),
|
||||
(data) => data.includes("Network: use --host to expose")
|
||||
);
|
||||
}
|
||||
|
||||
if (!first) {
|
||||
console.log("Restarting...", RESTARTABLE_PROCESSES.length);
|
||||
await killProcesses(RESTARTABLE_PROCESSES);
|
||||
}
|
||||
|
||||
execAsync(
|
||||
"npx",
|
||||
["electron", path.join("build", "electron.js")],
|
||||
true,
|
||||
cleanup
|
||||
);
|
||||
}
|
||||
|
||||
function spawnAndWaitUntil(cmd, cwd, predicate) {
|
||||
return new Promise((resolve) => {
|
||||
console.log(">", ...cmd);
|
||||
|
||||
const s = spawn(cmd[0], cmd.slice(1), {
|
||||
cwd,
|
||||
env: { ...process.env, NO_COLOR: "true" },
|
||||
shell: false
|
||||
});
|
||||
|
||||
RUNNING_PROCESSES.push(s);
|
||||
|
||||
s.stderr.pipe(process.stderr);
|
||||
s.stdout.on("data", (data) => {
|
||||
process.stdout.write(data);
|
||||
if (predicate(data)) resolve(undefined); //
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function exec(cmd) {
|
||||
try {
|
||||
console.log(">", cmd);
|
||||
|
||||
return execSync(cmd, {
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
shell: false
|
||||
});
|
||||
} catch {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
function execAsync(cmd, args, restartable, onExit) {
|
||||
try {
|
||||
console.log(">", cmd, ...args);
|
||||
|
||||
const proc = spawn(cmd, args, {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
shell: false
|
||||
});
|
||||
|
||||
const array = restartable ? RESTARTABLE_PROCESSES : RUNNING_PROCESSES;
|
||||
array.push(proc);
|
||||
proc.on("exit", (code, signal) => {
|
||||
console.log(cmd, ...args, "closed with code", code);
|
||||
if (code === 0 && !signal) {
|
||||
array.splice(array.indexOf(proc), 1);
|
||||
onExit && onExit();
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function killProcesses(processes) {
|
||||
for (const process of processes.slice()) {
|
||||
processes.splice(processes.indexOf(process), 1);
|
||||
await new Promise((resolve, reject) =>
|
||||
treekill(process.pid, (err) => (err ? reject(err) : resolve()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
console.log("Cleaning up");
|
||||
|
||||
await killProcesses(RESTARTABLE_PROCESSES);
|
||||
await killProcesses(RUNNING_PROCESSES);
|
||||
await sleep(1000);
|
||||
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
process.exit();
|
||||
}
|
||||
|
||||
async function isBundleSame() {
|
||||
const bundle = await fs.readFile(
|
||||
path.join(__dirname, "..", "build", "electron.js")
|
||||
);
|
||||
|
||||
const hashSum = crypto.createHash("sha256");
|
||||
hashSum.update(bundle);
|
||||
const hex = hashSum.digest("hex");
|
||||
|
||||
if (!lastBundleHash) {
|
||||
lastBundleHash = hex;
|
||||
return false;
|
||||
}
|
||||
if (hex === lastBundleHash) return true;
|
||||
lastBundleHash = hex;
|
||||
return false;
|
||||
}
|
||||
58
apps/desktop/src/api/bridge.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { EventEmitter } from "events";
|
||||
import TypedEventEmitter from "typed-emitter";
|
||||
|
||||
export type AppEvents = {
|
||||
onCreateItem(name: "note" | "notebook" | "reminder"): void;
|
||||
};
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
const typedEmitter = emitter as TypedEventEmitter<AppEvents>;
|
||||
const t = initTRPC.create();
|
||||
|
||||
export const bridgeRouter = t.router({
|
||||
onCreateItem: createSubscription("onCreateItem")
|
||||
});
|
||||
|
||||
export const bridge: AppEvents = new Proxy({} as AppEvents, {
|
||||
get(_t, name) {
|
||||
if (typeof name === "symbol") return;
|
||||
return (...args: unknown[]) => {
|
||||
emitter.emit(name, ...args);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function createSubscription<TName extends keyof AppEvents>(eventName: TName) {
|
||||
return t.procedure.subscription(() => {
|
||||
return observable<Parameters<AppEvents[TName]>[0]>((emit) => {
|
||||
const listener: AppEvents[TName] = (...args: any[]) => {
|
||||
emit.next(args[0]);
|
||||
};
|
||||
typedEmitter.addListener(eventName, listener);
|
||||
return () => {
|
||||
typedEmitter.removeListener(eventName, listener);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -18,16 +18,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import zlib from "node:zlib";
|
||||
import utils from "node:util";
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
const gzipAsync = utils.promisify(zlib.gzip);
|
||||
const gunzipAsync = utils.promisify(zlib.gunzip);
|
||||
|
||||
export async function gzip(args) {
|
||||
const { data, level } = args;
|
||||
return (await gzipAsync(data, { level })).toString("base64");
|
||||
}
|
||||
|
||||
export async function gunzip(args) {
|
||||
const { data } = args;
|
||||
return (await gunzipAsync(Buffer.from(data, "base64"))).toString("utf-8");
|
||||
}
|
||||
export const compressionRouter = t.router({
|
||||
gzip: t.procedure
|
||||
.input(z.object({ data: z.string(), level: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
const { data, level } = input;
|
||||
return (await gzipAsync(data, { level })).toString("base64");
|
||||
}),
|
||||
gunzip: t.procedure.input(z.string()).query(async ({ input }) => {
|
||||
return (await gunzipAsync(Buffer.from(input, "base64"))).toString("utf-8");
|
||||
})
|
||||
});
|
||||
41
apps/desktop/src/api/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { compressionRouter } from "./compression";
|
||||
import { osIntegrationRouter } from "./os-integration";
|
||||
import { spellCheckerRouter } from "./spell-checker";
|
||||
import { updaterRouter } from "./updater";
|
||||
import { bridgeRouter } from "./bridge";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
export const router = t.router({
|
||||
compress: compressionRouter,
|
||||
integration: osIntegrationRouter,
|
||||
spellChecker: spellCheckerRouter,
|
||||
updater: updaterRouter,
|
||||
bridge: bridgeRouter
|
||||
});
|
||||
|
||||
export const api = router.createCaller({});
|
||||
|
||||
// Export type router type signature,
|
||||
// NOT the router itself.
|
||||
export type AppRouter = typeof router;
|
||||
160
apps/desktop/src/api/os-integration.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { dialog, nativeTheme, Notification, shell } from "electron";
|
||||
import { AutoLaunch } from "../utils/autolaunch";
|
||||
import { config, DesktopIntegration } from "../utils/config";
|
||||
import { bringToFront } from "../utils/bring-to-front";
|
||||
import { getTheme, setTheme, Theme } from "../utils/theme";
|
||||
import { mkdirSync, writeFileSync } from "fs";
|
||||
import { dirname } from "path";
|
||||
import { resolvePath } from "../utils/resolve-path";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { AssetManager } from "../utils/asset-manager";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
const NotificationOptions = z.object({
|
||||
title: z.string().optional(),
|
||||
body: z.string().optional(),
|
||||
silent: z.boolean().optional(),
|
||||
timeoutType: z.union([z.literal("default"), z.literal("never")]).optional(),
|
||||
urgency: z
|
||||
.union([z.literal("normal"), z.literal("critical"), z.literal("low")])
|
||||
.optional(),
|
||||
tag: z.string()
|
||||
});
|
||||
|
||||
export const osIntegrationRouter = t.router({
|
||||
zoomFactor: t.procedure.query(() => config.zoomFactor),
|
||||
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
|
||||
globalThis.window?.webContents.setZoomFactor(factor);
|
||||
config.zoomFactor = factor;
|
||||
}),
|
||||
|
||||
privacyMode: t.procedure.query(() => config.privacyMode),
|
||||
setPrivacyMode: t.procedure
|
||||
.input(z.boolean().optional())
|
||||
.mutation(({ input: privacyMode }) => {
|
||||
if (!globalThis.window || !["win32", "darwin"].includes(process.platform))
|
||||
return;
|
||||
|
||||
globalThis.window.setContentProtection(!!privacyMode);
|
||||
|
||||
if (process.platform === "win32") {
|
||||
globalThis.window.setThumbnailClip(
|
||||
privacyMode
|
||||
? { x: 0, y: 0, width: 1, height: 1 }
|
||||
: { x: 0, y: 0, width: 0, height: 0 }
|
||||
);
|
||||
}
|
||||
config.privacyMode = !!privacyMode;
|
||||
}),
|
||||
|
||||
desktopIntegration: t.procedure.query(() => config.desktopSettings),
|
||||
setDesktopIntegration: t.procedure
|
||||
.input(DesktopIntegration)
|
||||
.mutation(({ input: settings }) => {
|
||||
if (settings.autoStart) {
|
||||
AutoLaunch.enable(!!settings.startMinimized);
|
||||
} else {
|
||||
AutoLaunch.disable();
|
||||
}
|
||||
config.desktopSettings = settings;
|
||||
}),
|
||||
|
||||
selectDirectory: t.procedure
|
||||
.input(
|
||||
z.object({
|
||||
title: z.string().optional(),
|
||||
buttonLabel: z.string().optional(),
|
||||
defaultPath: z.string().optional()
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
if (!globalThis.window) return undefined;
|
||||
|
||||
const { title, buttonLabel, defaultPath } = input;
|
||||
|
||||
const result = await dialog.showOpenDialog(globalThis.window, {
|
||||
title,
|
||||
buttonLabel,
|
||||
properties: ["openDirectory"],
|
||||
defaultPath: defaultPath && resolvePath(defaultPath)
|
||||
});
|
||||
if (result.canceled) return undefined;
|
||||
|
||||
return result.filePaths[0];
|
||||
}),
|
||||
|
||||
saveFile: t.procedure
|
||||
.input(z.object({ data: z.string(), filePath: z.string() }))
|
||||
.query(({ input }) => {
|
||||
const { data, filePath } = input;
|
||||
if (!data || !filePath) return;
|
||||
|
||||
const resolvedPath = resolvePath(filePath);
|
||||
|
||||
mkdirSync(dirname(resolvedPath), { recursive: true });
|
||||
writeFileSync(resolvedPath, data);
|
||||
}),
|
||||
|
||||
showNotification: t.procedure
|
||||
.input(NotificationOptions)
|
||||
.query(({ input }) => {
|
||||
const notification = new Notification({
|
||||
...input,
|
||||
icon: AssetManager.appIcon({
|
||||
size: 64,
|
||||
format: process.platform === "win32" ? "ico" : "png"
|
||||
})
|
||||
});
|
||||
notification.show();
|
||||
if (input.urgency === "critical") {
|
||||
shell.beep();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
notification.once("close", () => resolve(undefined));
|
||||
notification.once("click", () => resolve(input.tag));
|
||||
});
|
||||
}),
|
||||
openPath: t.procedure
|
||||
.input(z.object({ type: z.literal("path"), link: z.string() }))
|
||||
.query(({ input }) => {
|
||||
const { type, link } = input;
|
||||
if (type === "path") return shell.openPath(resolvePath(link));
|
||||
}),
|
||||
bringToFront: t.procedure.query(() => bringToFront()),
|
||||
changeTheme: t.procedure
|
||||
.input(Theme)
|
||||
.mutation(({ input }) => setTheme(input)),
|
||||
|
||||
onThemeChanged: t.procedure.subscription(() =>
|
||||
observable<"dark" | "light">((emit) => {
|
||||
nativeTheme.on("updated", () => {
|
||||
if (getTheme() === "system") {
|
||||
emit.next(nativeTheme.shouldUseDarkColors ? "dark" : "light");
|
||||
}
|
||||
});
|
||||
})
|
||||
)
|
||||
});
|
||||
122
apps/desktop/src/api/spell-checker.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { config } from "../utils/config";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
const LANGUAGES: Record<string, string> = {
|
||||
af: "Afrikaans",
|
||||
bg: "Bulgarian",
|
||||
ca: "Catalan",
|
||||
cs: "Czech",
|
||||
cy: "Welsh",
|
||||
da: "Danish",
|
||||
de: "German",
|
||||
"de-DE": "German (Germany)",
|
||||
el: "Greek",
|
||||
en: "English",
|
||||
"en-AU": "English (Australia)",
|
||||
"en-CA": "English (Canada)",
|
||||
"en-GB": "English (UK)",
|
||||
"en-GB-oxendict": "English (UK Oxford)",
|
||||
"en-US": "English (US)",
|
||||
es: "Spanish",
|
||||
"es-419": "Spanish (Latin America)",
|
||||
"es-AR": "Spanish (Argentina)",
|
||||
"es-ES": "Spanish (Spain)",
|
||||
"es-MX": "Spanish (Mexico)",
|
||||
"es-US": "Spanish (US)",
|
||||
et: "Estonian",
|
||||
fa: "Persian",
|
||||
fo: "Faroese",
|
||||
fr: "French",
|
||||
"fr-FR": "French (France)",
|
||||
he: "Hebrew",
|
||||
hi: "Hindi",
|
||||
hr: "Croatian",
|
||||
hu: "Hungarian",
|
||||
hy: "Armenian",
|
||||
id: "Indonesian",
|
||||
it: "Italian",
|
||||
"it-IT": "Italian (Italy)",
|
||||
ko: "Korean",
|
||||
lt: "Lithuanian",
|
||||
lv: "Latvian",
|
||||
nb: "Norwegian Bokmål",
|
||||
nl: "Dutch",
|
||||
pl: "Polish",
|
||||
pt: "Portuguese",
|
||||
"pt-BR": "Portuguese (Brazil)",
|
||||
"pt-PT": "Portuguese (Portugal)",
|
||||
ro: "Romanian",
|
||||
ru: "Russian",
|
||||
sh: "Serbo-Croatian",
|
||||
sk: "Slovak",
|
||||
sl: "Slovenian",
|
||||
sq: "Albanian",
|
||||
sr: "Serbian",
|
||||
sv: "Swedish",
|
||||
ta: "Tamil",
|
||||
tg: "Tajik",
|
||||
tr: "Turkish",
|
||||
uk: "Ukrainian",
|
||||
vi: "Vietnamese"
|
||||
};
|
||||
|
||||
type Language = { code: string; name: string };
|
||||
|
||||
export const spellCheckerRouter = t.router({
|
||||
isEnabled: t.procedure.query(() => config.isSpellCheckerEnabled),
|
||||
languages: t.procedure.query(
|
||||
() =>
|
||||
<Language[]>(
|
||||
globalThis.window?.webContents.session.availableSpellCheckerLanguages.map(
|
||||
(code) => ({
|
||||
code,
|
||||
name: LANGUAGES[code]
|
||||
})
|
||||
)
|
||||
)
|
||||
),
|
||||
enabledLanguages: t.procedure.query(
|
||||
() =>
|
||||
<Language[]>(
|
||||
globalThis.window?.webContents.session
|
||||
.getSpellCheckerLanguages()
|
||||
.map((code) => ({
|
||||
code,
|
||||
name: LANGUAGES[code]
|
||||
}))
|
||||
)
|
||||
),
|
||||
setLanguages: t.procedure
|
||||
.input(z.array(z.string()))
|
||||
.mutation(({ input: languages }) =>
|
||||
globalThis.window?.webContents.session.setSpellCheckerLanguages(languages)
|
||||
),
|
||||
toggle: t.procedure
|
||||
.input(z.boolean().optional())
|
||||
.mutation(({ input: enabled }) => {
|
||||
globalThis.window?.webContents.session.setSpellCheckerEnabled(!!enabled);
|
||||
config.isSpellCheckerEnabled = !!enabled;
|
||||
})
|
||||
});
|
||||
81
apps/desktop/src/api/updater.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { CancellationToken, autoUpdater } from "electron-updater";
|
||||
import type { AppUpdaterEvents } from "electron-updater/out/AppUpdater";
|
||||
import { z } from "zod";
|
||||
import { config } from "../utils/config";
|
||||
|
||||
type UpdateInfo = { version: string };
|
||||
type Progress = { percent: number };
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
export const updaterRouter = t.router({
|
||||
autoUpdates: t.procedure.query(() => config.automaticUpdates),
|
||||
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
|
||||
download: t.procedure.query(async () => {
|
||||
const cancellationToken = new CancellationToken();
|
||||
await autoUpdater.downloadUpdate(cancellationToken);
|
||||
}),
|
||||
check: t.procedure.query(async () => {
|
||||
await autoUpdater.checkForUpdates();
|
||||
}),
|
||||
|
||||
toggleAutoUpdates: t.procedure
|
||||
.input(z.boolean().optional())
|
||||
.mutation(({ input }) => {
|
||||
config.automaticUpdates =
|
||||
input === undefined ? !config.automaticUpdates : input;
|
||||
}),
|
||||
|
||||
onChecking: createSubscription("checking-for-update"),
|
||||
onDownloaded: createSubscription<"update-downloaded", UpdateInfo>(
|
||||
"update-downloaded"
|
||||
),
|
||||
onDownloadProgress: createSubscription<"download-progress", Progress>(
|
||||
"download-progress"
|
||||
),
|
||||
onNotAvailable: createSubscription<"update-not-available", UpdateInfo>(
|
||||
"update-not-available"
|
||||
),
|
||||
onAvailable: createSubscription<"update-available", UpdateInfo>(
|
||||
"update-available"
|
||||
),
|
||||
onError: createSubscription("error")
|
||||
});
|
||||
|
||||
function createSubscription<
|
||||
TName extends keyof AppUpdaterEvents,
|
||||
TReturnType = Parameters<AppUpdaterEvents[TName]>[0]
|
||||
>(eventName: TName) {
|
||||
return t.procedure.subscription(() => {
|
||||
return observable<TReturnType>((emit) => {
|
||||
const listener: AppUpdaterEvents[TName] = (...args: any[]) => {
|
||||
emit.next(args[0]);
|
||||
};
|
||||
autoUpdater.addListener(eventName, listener);
|
||||
return () => {
|
||||
autoUpdater.removeListener(eventName, listener);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
84
apps/desktop/src/cli.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
|
||||
export type CLIOptions = {
|
||||
note: boolean | string;
|
||||
notebook: boolean | string;
|
||||
reminder: boolean | string;
|
||||
hidden: boolean;
|
||||
};
|
||||
|
||||
export async function parseArguments(): Promise<CLIOptions> {
|
||||
const result: CLIOptions = {
|
||||
note: false,
|
||||
notebook: false,
|
||||
reminder: false,
|
||||
hidden: false
|
||||
};
|
||||
const { hidden } = await yargs(hideBin(process.argv))
|
||||
.boolean("hidden")
|
||||
.command("new", "Create a new item", (yargs) => {
|
||||
return yargs
|
||||
.command("note", "Create a new note", {}, () => {
|
||||
result.note = true;
|
||||
})
|
||||
.command("notebook", "Create a new notebook", {}, () => {
|
||||
result.notebook = true;
|
||||
})
|
||||
.command("reminder", "Add a new reminder", {}, () => {
|
||||
result.reminder = true;
|
||||
});
|
||||
})
|
||||
.command("open", "Open a specific item", (yargs) => {
|
||||
return yargs
|
||||
.command(
|
||||
"note",
|
||||
"Open a note",
|
||||
{ id: { string: true, description: "Id of the note" } },
|
||||
(args) => {
|
||||
result.note = args.id || false;
|
||||
}
|
||||
)
|
||||
.command(
|
||||
"notebook",
|
||||
"Open a notebook",
|
||||
{ id: { string: true, description: "Id of the notebook" } },
|
||||
(args) => {
|
||||
result.notebook = args.id || false;
|
||||
}
|
||||
)
|
||||
.command(
|
||||
"topic",
|
||||
"Open a topic",
|
||||
{
|
||||
id: { string: true, description: "Id of the topic" },
|
||||
notebookId: { string: true, description: "Id of the notebook" }
|
||||
},
|
||||
(args) => {
|
||||
result.notebook = `${args.notebookId}/${args.id}`;
|
||||
}
|
||||
);
|
||||
})
|
||||
.parse();
|
||||
result.hidden = hidden || false;
|
||||
return result;
|
||||
}
|
||||
22
apps/desktop/src/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export * from "./constants";
|
||||
export type { AppRouter } from "./api";
|
||||
export { type UpdateInfo } from "builder-util-runtime";
|
||||
205
apps/desktop/src/main.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { app, BrowserWindow, nativeTheme, session, shell } from "electron";
|
||||
import { isDevelopment } from "./utils";
|
||||
import { registerProtocol, PROTOCOL_URL } from "./utils/protocol";
|
||||
import { configureAutoUpdater } from "./utils/autoupdater";
|
||||
import { getBackgroundColor, getTheme, setTheme } from "./utils/theme";
|
||||
import { setupMenu } from "./utils/menu";
|
||||
import { WindowState } from "./utils/window-state";
|
||||
import { AutoLaunch } from "./utils/autolaunch";
|
||||
import { setupJumplist } from "./utils/jumplist";
|
||||
import { setupTray } from "./utils/tray";
|
||||
import { CLIOptions, parseArguments } from "./cli";
|
||||
import { AssetManager } from "./utils/asset-manager";
|
||||
import { createIPCHandler } from "electron-trpc/main";
|
||||
import { router, api } from "./api";
|
||||
import { config } from "./utils/config";
|
||||
import path from "path";
|
||||
|
||||
if (process.platform == "win32" && process.env.PORTABLE_EXECUTABLE_DIR) {
|
||||
console.log("Portable app: true");
|
||||
const root = path.join(process.env.PORTABLE_EXECUTABLE_DIR, "Notesnook");
|
||||
app.setPath("appData", path.join(root, "AppData"));
|
||||
app.setPath("documents", path.join(root, "Documents"));
|
||||
app.setPath("userData", path.join(root, "UserData"));
|
||||
}
|
||||
|
||||
// only run a single instance
|
||||
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
|
||||
app.exit();
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
app.setAppUserModelId(app.name);
|
||||
}
|
||||
|
||||
app.commandLine.appendSwitch("lang", "en-US");
|
||||
|
||||
async function createWindow() {
|
||||
const cliOptions = await parseArguments();
|
||||
setTheme(getTheme());
|
||||
|
||||
const mainWindowState = new WindowState({});
|
||||
const mainWindow = new BrowserWindow({
|
||||
show: !cliOptions.hidden,
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
darkTheme: getTheme() === "dark",
|
||||
backgroundColor: getBackgroundColor(),
|
||||
opacity: 0,
|
||||
autoHideMenuBar: true,
|
||||
icon: AssetManager.appIcon({
|
||||
size: 512,
|
||||
format: process.platform === "win32" ? "ico" : "png"
|
||||
}),
|
||||
webPreferences: {
|
||||
zoomFactor: config.zoomFactor,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
spellcheck: config.isSpellCheckerEnabled,
|
||||
preload: __dirname + "/preload.js"
|
||||
}
|
||||
});
|
||||
|
||||
globalThis.window = mainWindow;
|
||||
mainWindowState.manage(mainWindow);
|
||||
|
||||
if (cliOptions.hidden && !config.desktopSettings.minimizeToSystemTray)
|
||||
mainWindow.minimize();
|
||||
|
||||
await mainWindow.webContents.loadURL(`${createURL(cliOptions, "/")}`);
|
||||
mainWindow.setOpacity(1);
|
||||
|
||||
if (config.privacyMode) {
|
||||
await api.integration.setPrivacyMode(config.privacyMode);
|
||||
}
|
||||
|
||||
await AssetManager.loadIcons();
|
||||
setupDesktopIntegration();
|
||||
createIPCHandler({ router, windows: [mainWindow] });
|
||||
|
||||
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
|
||||
"http://dictionaries.notesnook.com/"
|
||||
);
|
||||
|
||||
mainWindow.once("closed", () => {
|
||||
globalThis.window = null;
|
||||
});
|
||||
|
||||
setupMenu();
|
||||
setupJumplist();
|
||||
|
||||
if (isDevelopment())
|
||||
mainWindow.webContents.openDevTools({ mode: "right", activate: true });
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
nativeTheme.on("updated", () => {
|
||||
setupTray();
|
||||
setupJumplist();
|
||||
});
|
||||
}
|
||||
|
||||
app.once("ready", async () => {
|
||||
console.info("App ready. Opening window.");
|
||||
|
||||
if (!isDevelopment()) registerProtocol();
|
||||
await createWindow();
|
||||
configureAutoUpdater();
|
||||
});
|
||||
|
||||
app.once("window-all-closed", () => {
|
||||
if (process.platform !== "darwin" || MAC_APP_STORE) {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
if (globalThis.window === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
function createURL(options: CLIOptions, path = "/") {
|
||||
const url = new URL(isDevelopment() ? "http://localhost:3000" : PROTOCOL_URL);
|
||||
|
||||
url.pathname = path;
|
||||
if (options.note === true) url.hash = "/notes/create/1";
|
||||
else if (options.notebook === true) url.hash = "/notebooks/create";
|
||||
else if (options.reminder === true) url.hash = "/reminders/create";
|
||||
else if (typeof options.note === "string")
|
||||
url.hash = `/notes/${options.note}/edit`;
|
||||
else if (typeof options.notebook === "string")
|
||||
url.hash = `/notebooks/${options.notebook}`;
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
function setupDesktopIntegration() {
|
||||
const desktopIntegration = config.desktopSettings;
|
||||
|
||||
if (
|
||||
desktopIntegration.closeToSystemTray ||
|
||||
desktopIntegration.minimizeToSystemTray
|
||||
) {
|
||||
setupTray();
|
||||
}
|
||||
|
||||
// when close to system tray is enabled, it becomes nigh impossible
|
||||
// to "quit" the app. This is necessary in order to fix that.
|
||||
if (desktopIntegration.closeToSystemTray) {
|
||||
app.on("before-quit", () => app.exit(0));
|
||||
}
|
||||
|
||||
globalThis.window?.once("close", (e) => {
|
||||
if (config.desktopSettings.closeToSystemTray) {
|
||||
e.preventDefault();
|
||||
if (process.platform == "darwin") {
|
||||
// on macOS window cannot be minimized/hidden if it is already fullscreen
|
||||
// so we just close it.
|
||||
if (globalThis.window?.isFullScreen()) app.exit(0);
|
||||
else app.hide();
|
||||
} else {
|
||||
globalThis.window?.minimize();
|
||||
globalThis.window?.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
globalThis.window?.on("minimize", () => {
|
||||
if (config.desktopSettings.minimizeToSystemTray) {
|
||||
if (process.platform == "darwin") {
|
||||
app.hide();
|
||||
} else {
|
||||
globalThis.window?.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (desktopIntegration.autoStart) {
|
||||
AutoLaunch.enable(!!desktopIntegration.startMinimized);
|
||||
}
|
||||
}
|
||||
51
apps/desktop/src/preload.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/* eslint-disable no-var */
|
||||
|
||||
import { ELECTRON_TRPC_CHANNEL } from "electron-trpc/main";
|
||||
import { type RendererGlobalElectronTRPC } from "electron-trpc/src/types";
|
||||
import type { NNCrypto } from "@notesnook/crypto";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { platform } from "os";
|
||||
|
||||
declare global {
|
||||
var os: () => "mas" | ReturnType<typeof platform>;
|
||||
var electronTRPC: RendererGlobalElectronTRPC;
|
||||
var NativeNNCrypto: (new () => NNCrypto) | undefined;
|
||||
}
|
||||
|
||||
process.once("loaded", async () => {
|
||||
console.log("HELLO!");
|
||||
const electronTRPC: RendererGlobalElectronTRPC = {
|
||||
sendMessage: (operation) =>
|
||||
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
|
||||
onMessage: (callback) =>
|
||||
ipcRenderer.on(ELECTRON_TRPC_CHANNEL, (_event, args) => callback(args))
|
||||
};
|
||||
globalThis.electronTRPC = electronTRPC;
|
||||
});
|
||||
|
||||
globalThis.NativeNNCrypto =
|
||||
process.platform === "win32" &&
|
||||
process.arch !== "x64" &&
|
||||
process.arch !== "ia32"
|
||||
? undefined
|
||||
: require("@notesnook/crypto").NNCrypto;
|
||||
|
||||
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());
|
||||
117
apps/desktop/src/utils/asset-manager.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { NativeImage, nativeImage } from "electron";
|
||||
import path from "path";
|
||||
import { isDevelopment } from "./index";
|
||||
import { parse, ParsedImage } from "icojs";
|
||||
import { getSystemTheme } from "./theme";
|
||||
import { readFile } from "fs/promises";
|
||||
|
||||
type Formats = "ico" | "png" | "icns";
|
||||
type IconOptions<TFormat extends Formats> = {
|
||||
size?: 16 | 22 | 24 | 32 | 48 | 64 | 128 | 256 | 512 | 1024;
|
||||
format?: TFormat;
|
||||
};
|
||||
|
||||
type IconNames = (typeof icons)[number];
|
||||
type Prefixes = (typeof prefixes)[number];
|
||||
|
||||
type FlexibleIcon<TFormat extends Formats> = TFormat extends "ico"
|
||||
? string
|
||||
: NativeImage;
|
||||
|
||||
const RESOURCES_DIR = isDevelopment()
|
||||
? process.cwd()
|
||||
: process.platform === "darwin"
|
||||
? path.normalize(path.join(path.dirname(process.execPath), "..", "Resources"))
|
||||
: path.join(path.dirname(process.execPath), "resources");
|
||||
|
||||
const prefixes = ["", ".dark"];
|
||||
const icons = [
|
||||
"note-add",
|
||||
"notebook-add",
|
||||
"reminder-add",
|
||||
"quit",
|
||||
"tray-icon"
|
||||
] as const;
|
||||
|
||||
const ALL_ICONS: {
|
||||
id: IconNames;
|
||||
prefix: Prefixes;
|
||||
images: ParsedImage[];
|
||||
}[] = [];
|
||||
|
||||
export class AssetManager {
|
||||
static async loadIcons() {
|
||||
if (ALL_ICONS.length) return;
|
||||
|
||||
for (const prefix of prefixes) {
|
||||
for (const icon of icons) {
|
||||
const icoPath = path.join(
|
||||
RESOURCES_DIR,
|
||||
"assets",
|
||||
"icons",
|
||||
`${icon}${prefix}.ico`
|
||||
);
|
||||
const icoBuffer = await readFile(icoPath);
|
||||
const images = await parse(icoBuffer, "image/png");
|
||||
ALL_ICONS.push({ id: icon, images, prefix });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static appIcon(options: IconOptions<Formats>) {
|
||||
const { size = 32, format = "png" } = options;
|
||||
|
||||
if (format === "ico") return "assets\\icons\\app.ico";
|
||||
if (format === "icns") return "assets/icons/app.icns";
|
||||
|
||||
return `assets/icons/${size}x${size}.png`;
|
||||
}
|
||||
|
||||
static icon<TFormat extends Formats>(
|
||||
name: IconNames,
|
||||
options: IconOptions<TFormat>
|
||||
): FlexibleIcon<TFormat> {
|
||||
const { size = 16, format = "png" } = options;
|
||||
|
||||
const prefix: Prefixes = getSystemTheme() === "dark" ? ".dark" : "";
|
||||
|
||||
const icoPath = path.join(
|
||||
RESOURCES_DIR,
|
||||
"assets",
|
||||
"icons",
|
||||
`${name}${prefix}.ico`
|
||||
);
|
||||
if (format === "ico") return icoPath as FlexibleIcon<TFormat>;
|
||||
|
||||
const icon = ALL_ICONS.find((a) => a.id === name && a.prefix === prefix);
|
||||
if (!icon)
|
||||
return nativeImage.createFromPath(
|
||||
AssetManager.appIcon(options)
|
||||
) as FlexibleIcon<TFormat>;
|
||||
|
||||
return nativeImage.createFromBuffer(
|
||||
Buffer.from(
|
||||
(icon.images.find((i) => i.height === size) || icon.images[0]).buffer
|
||||
)
|
||||
) as FlexibleIcon<TFormat>;
|
||||
}
|
||||
}
|
||||
70
apps/desktop/src/utils/autolaunch.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { app } from "electron";
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
|
||||
import path from "path";
|
||||
|
||||
const LINUX_DESKTOP_ENTRY = (hidden: boolean) => `[Desktop Entry]
|
||||
Type=Application
|
||||
Version=${app.getVersion()}
|
||||
Name=${app.getName()}
|
||||
Comment=${app.getName()} startup script
|
||||
Exec=${process.execPath}${hidden ? " --hidden" : ""}
|
||||
StartupNotify=false
|
||||
Terminal=false`;
|
||||
|
||||
const LINUX_AUTOSTART_DIRECTORY_PATH = path.join(
|
||||
app.getPath("home"),
|
||||
".config",
|
||||
"autostart"
|
||||
);
|
||||
|
||||
export class AutoLaunch {
|
||||
static enable(hidden: boolean) {
|
||||
if (process.platform === "linux") {
|
||||
mkdirSync(LINUX_AUTOSTART_DIRECTORY_PATH, { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(
|
||||
LINUX_AUTOSTART_DIRECTORY_PATH,
|
||||
`${app.getName().toLowerCase()}.desktop`
|
||||
),
|
||||
LINUX_DESKTOP_ENTRY(hidden)
|
||||
);
|
||||
} else {
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin: true,
|
||||
openAsHidden: hidden,
|
||||
args: ["--hidden"]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static disable() {
|
||||
if (process.platform === "linux") {
|
||||
const desktopFilePath = path.join(
|
||||
LINUX_AUTOSTART_DIRECTORY_PATH,
|
||||
`${app.getName().toLowerCase()}.desktop`
|
||||
);
|
||||
if (!existsSync(desktopFilePath)) return;
|
||||
rmSync(desktopFilePath);
|
||||
} else {
|
||||
app.setLoginItemSettings({ openAtLogin: false, openAsHidden: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,20 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { CancellationToken } from "builder-util-runtime";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { EVENTS } from "../../events";
|
||||
import { sendMessageToRenderer } from "../utils";
|
||||
import { config } from "./config";
|
||||
|
||||
export default () => {
|
||||
sendMessageToRenderer(EVENTS.updateDownloadProgress, { progress: 0 });
|
||||
autoUpdater.cancellationToken = new CancellationToken();
|
||||
autoUpdater.downloadUpdate(autoUpdater.cancellationToken);
|
||||
};
|
||||
async function configureAutoUpdater() {
|
||||
autoUpdater.setFeedURL({
|
||||
provider: "generic",
|
||||
url: `https://notesnook.com/releases/${process.platform}/`,
|
||||
useMultipleRangeRequest: false
|
||||
});
|
||||
|
||||
autoUpdater.autoDownload = config.automaticUpdates;
|
||||
autoUpdater.allowDowngrade = false;
|
||||
autoUpdater.allowPrerelease = false;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
}
|
||||
|
||||
export { configureAutoUpdater };
|
||||
34
apps/desktop/src/utils/bring-to-front.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { WindowState } from "./window-state";
|
||||
|
||||
export function bringToFront() {
|
||||
if (!globalThis.window) return;
|
||||
|
||||
if (globalThis.window.isMinimized()) {
|
||||
if (new WindowState({}).isMaximized) {
|
||||
globalThis.window.maximize();
|
||||
} else globalThis.window.restore();
|
||||
}
|
||||
globalThis.window.show();
|
||||
globalThis.window.focus();
|
||||
globalThis.window.moveTop();
|
||||
globalThis.window.webContents.focus();
|
||||
}
|
||||
56
apps/desktop/src/utils/config.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { nativeTheme } from "electron";
|
||||
import { JSONStorage } from "./json-storage";
|
||||
import { z } from "zod";
|
||||
|
||||
export const DesktopIntegration = z.object({
|
||||
autoStart: z.boolean().optional(),
|
||||
startMinimized: z.boolean().optional(),
|
||||
minimizeToSystemTray: z.boolean().optional(),
|
||||
closeToSystemTray: z.boolean().optional()
|
||||
});
|
||||
|
||||
export type DesktopIntegration = z.infer<typeof DesktopIntegration>;
|
||||
|
||||
export const config = {
|
||||
desktopSettings: <DesktopIntegration>{
|
||||
autoStart: false,
|
||||
startMinimized: false,
|
||||
minimizeToSystemTray: false,
|
||||
closeToSystemTray: false
|
||||
},
|
||||
privacyMode: false,
|
||||
isSpellCheckerEnabled: false,
|
||||
zoomFactor: 0,
|
||||
theme: nativeTheme.themeSource,
|
||||
automaticUpdates: true
|
||||
};
|
||||
|
||||
type ConfigKey = keyof typeof config;
|
||||
for (const key in config) {
|
||||
const defaultValue = config[<ConfigKey>key];
|
||||
if (Object.hasOwn(config, key)) {
|
||||
Object.defineProperty(config, key, {
|
||||
get: () => JSONStorage.get(key, defaultValue),
|
||||
set: (value) => JSONStorage.set(key, value)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,15 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { JSONStorage } from "../jsonstorage";
|
||||
import { app } from "electron";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
function getPrivacyMode() {
|
||||
let mode = JSONStorage.get("privacyMode");
|
||||
return mode === undefined ? false : mode;
|
||||
export function isDevelopment() {
|
||||
return process.env.ELECTRON_IS_DEV
|
||||
? Number.parseInt(process.env.ELECTRON_IS_DEV, 10) === 1
|
||||
: !app.isPackaged;
|
||||
}
|
||||
|
||||
function setPrivacyMode(mode) {
|
||||
return JSONStorage.set("privacyMode", mode);
|
||||
export function isFlatpak() {
|
||||
return existsSync("/.flatpak-info");
|
||||
}
|
||||
|
||||
export { setPrivacyMode, getPrivacyMode };
|
||||
@@ -25,12 +25,12 @@ const directory = app.getPath("userData");
|
||||
const filename = "config.json";
|
||||
const filePath = join(directory, filename);
|
||||
class JSONStorage {
|
||||
static get(key, def) {
|
||||
static get<T>(key: string, def?: T): T {
|
||||
const json = this.readJson();
|
||||
return json[key] || def;
|
||||
return json[key] === undefined ? def : json[key];
|
||||
}
|
||||
|
||||
static set(key, value) {
|
||||
static set(key: string, value: unknown) {
|
||||
const json = this.readJson();
|
||||
json[key] = value;
|
||||
this.writeJson(json);
|
||||
@@ -40,10 +40,7 @@ class JSONStorage {
|
||||
this.writeJson({});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
static readJson() {
|
||||
private static readJson() {
|
||||
try {
|
||||
const json = readFileSync(filePath, "utf-8");
|
||||
return JSON.parse(json);
|
||||
@@ -53,10 +50,7 @@ class JSONStorage {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
static writeJson(json) {
|
||||
private static writeJson(json: Record<string, unknown>) {
|
||||
try {
|
||||
writeFileSync(filePath, JSON.stringify(json));
|
||||
} catch (e) {
|
||||
99
apps/desktop/src/utils/jumplist.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { app, Menu } from "electron";
|
||||
import { AssetManager } from "./asset-manager";
|
||||
import { bringToFront } from "./bring-to-front";
|
||||
import { bridge } from "../api/bridge";
|
||||
|
||||
export function setupJumplist() {
|
||||
if (process.platform === "win32") {
|
||||
setJumplistOnWindows();
|
||||
} else if (process.platform === "darwin") {
|
||||
setDockMenuOnMacOs();
|
||||
}
|
||||
}
|
||||
|
||||
function setJumplistOnWindows() {
|
||||
app.setJumpList([
|
||||
{
|
||||
type: "custom",
|
||||
name: "Quick actions",
|
||||
items: [
|
||||
{
|
||||
program: process.execPath,
|
||||
iconIndex: 0,
|
||||
iconPath: AssetManager.icon("note-add", { format: "ico" }),
|
||||
args: "new note",
|
||||
description: "Create a new note",
|
||||
title: "New note",
|
||||
type: "task"
|
||||
},
|
||||
{
|
||||
program: process.execPath,
|
||||
iconIndex: 0,
|
||||
iconPath: AssetManager.icon("notebook-add", { format: "ico" }),
|
||||
args: "new notebook",
|
||||
description: "Create a new notebook",
|
||||
title: "New notebook",
|
||||
type: "task"
|
||||
},
|
||||
{
|
||||
program: process.execPath,
|
||||
iconIndex: 0,
|
||||
iconPath: AssetManager.icon("reminder-add", { format: "ico" }),
|
||||
args: "new reminder",
|
||||
description: "Add a new reminder",
|
||||
title: "New reminder",
|
||||
type: "task"
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
function setDockMenuOnMacOs() {
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: "New note",
|
||||
type: "normal",
|
||||
click: () => {
|
||||
bringToFront();
|
||||
bridge.onCreateItem("note");
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "New notebook",
|
||||
type: "normal",
|
||||
click: () => {
|
||||
bringToFront();
|
||||
bridge.onCreateItem("notebook");
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "New reminder",
|
||||
type: "normal",
|
||||
click: () => {
|
||||
bringToFront();
|
||||
bridge.onCreateItem("reminder");
|
||||
}
|
||||
}
|
||||
]);
|
||||
app.dock.setMenu(contextMenu);
|
||||
}
|
||||
@@ -17,15 +17,54 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Menu, MenuItem, clipboard } from "electron";
|
||||
import { Menu, MenuItem, clipboard, shell } from "electron";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("@playwright/test").BrowserWindow} mainWindow
|
||||
*/
|
||||
function setupMenu(mainWindow) {
|
||||
mainWindow.webContents.on("context-menu", (event, params) => {
|
||||
function setupMenu() {
|
||||
if (!globalThis.window) return;
|
||||
|
||||
globalThis.window.webContents.on("context-menu", (_event, params) => {
|
||||
const menu = new Menu();
|
||||
|
||||
// Add each spelling suggestion
|
||||
for (const suggestion of params.dictionarySuggestions) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: suggestion,
|
||||
click: () =>
|
||||
globalThis.window?.webContents.replaceMisspelling(suggestion)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Allow users to add the misspelled word to the dictionary
|
||||
if (params.misspelledWord) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Add to dictionary",
|
||||
click: () =>
|
||||
globalThis.window?.webContents.session.addWordToSpellCheckerDictionary(
|
||||
params.misspelledWord
|
||||
)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (menu.items.length > 0)
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
type: "separator"
|
||||
})
|
||||
);
|
||||
|
||||
if (params.linkURL.length) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Open in browser",
|
||||
click: () => shell.openExternal(params.linkURL)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (params.isEditable) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
@@ -62,14 +101,35 @@ function setupMenu(mainWindow) {
|
||||
})
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Copy",
|
||||
role: "copy",
|
||||
enabled: params.selectionText.length > 0,
|
||||
accelerator: "CommandOrControl+C"
|
||||
})
|
||||
);
|
||||
if (params.linkURL?.length) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Copy link",
|
||||
click() {
|
||||
clipboard.writeText(params.linkURL);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Copy link text",
|
||||
click() {
|
||||
clipboard.writeText(params.linkText);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (params.selectionText.length) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Copy",
|
||||
role: "copy",
|
||||
accelerator: "CommandOrControl+C"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (params.isEditable)
|
||||
menu.append(
|
||||
@@ -81,29 +141,7 @@ function setupMenu(mainWindow) {
|
||||
})
|
||||
);
|
||||
|
||||
// Add each spelling suggestion
|
||||
// for (const suggestion of params.dictionarySuggestions) {
|
||||
// menu.append(
|
||||
// new MenuItem({
|
||||
// label: suggestion,
|
||||
// click: () => mainWindow.webContents.replaceMisspelling(suggestion),
|
||||
// })
|
||||
// );
|
||||
// }
|
||||
|
||||
// Allow users to add the misspelled word to the dictionary
|
||||
// if (params.misspelledWord) {
|
||||
// menu.append(
|
||||
// new MenuItem({
|
||||
// label: "Add to dictionary",
|
||||
// click: () =>
|
||||
// mainWindow.webContents.session.addWordToSpellCheckerDictionary(
|
||||
// params.misspelledWord
|
||||
// ),
|
||||
// })
|
||||
// );
|
||||
// }
|
||||
menu.popup();
|
||||
if (menu.items.length > 0) menu.popup();
|
||||
});
|
||||
}
|
||||
export { setupMenu };
|
||||
@@ -17,19 +17,18 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { protocol } from "electron";
|
||||
import { isDevelopment, getPath } from "./utils";
|
||||
import { protocol, ProtocolRequest } from "electron";
|
||||
import { isDevelopment } from "./index";
|
||||
import { createReadStream } from "fs";
|
||||
import { extname, normalize } from "path";
|
||||
import { logger } from "./logger";
|
||||
import { Blob } from "buffer";
|
||||
import { URL } from "url";
|
||||
import fetch, { Response } from "node-fetch";
|
||||
|
||||
const FILE_NOT_FOUND = -6;
|
||||
const BASE_PATH = isDevelopment() ? "../public" : "";
|
||||
const HOSTNAME = `app.notesnook.com`;
|
||||
const PROTOCOL = "https";
|
||||
const extensionToMimeType = {
|
||||
const FILE_NOT_FOUND = -6;
|
||||
const SCHEME = "https";
|
||||
const extensionToMimeType: Record<string, string> = {
|
||||
html: "text/html",
|
||||
json: "application/json",
|
||||
js: "application/javascript",
|
||||
@@ -40,23 +39,22 @@ const extensionToMimeType = {
|
||||
|
||||
function registerProtocol() {
|
||||
const protocolInterceptionResult = protocol.interceptStreamProtocol(
|
||||
PROTOCOL,
|
||||
SCHEME,
|
||||
async (request, callback) => {
|
||||
const url = new URL(request.url);
|
||||
if (shouldInterceptRequest(url)) {
|
||||
logger.info("Intercepting request:", request);
|
||||
console.info("Intercepting request:", request.url);
|
||||
|
||||
const loadIndex = !extname(url.pathname);
|
||||
const absoluteFilePath = normalize(
|
||||
const filePath = normalize(
|
||||
`${__dirname}${
|
||||
loadIndex
|
||||
? `${BASE_PATH}/index.html`
|
||||
: `${BASE_PATH}/${url.pathname}`
|
||||
}`
|
||||
);
|
||||
const filePath = getPath(absoluteFilePath);
|
||||
if (!filePath) {
|
||||
logger.error("Local asset file not found at", filePath);
|
||||
console.error("Local asset file not found at", filePath);
|
||||
callback({ error: FILE_NOT_FOUND });
|
||||
return;
|
||||
}
|
||||
@@ -68,70 +66,95 @@ function registerProtocol() {
|
||||
mimeType: extensionToMimeType[fileExtension]
|
||||
});
|
||||
} else {
|
||||
var response;
|
||||
let response: Response;
|
||||
try {
|
||||
const body = await getBody(request);
|
||||
response = await fetch(request.url, {
|
||||
...request,
|
||||
body,
|
||||
headers: {
|
||||
...request.headers,
|
||||
origin: `${PROTOCOL}://${HOSTNAME}/`
|
||||
...request.headers
|
||||
// origin: `${PROTOCOL}://${HOSTNAME}/`
|
||||
},
|
||||
referrer: request.referrer,
|
||||
redirect: "manual"
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
logger.error(`Error sending request to `, request.url, "Error: ", e);
|
||||
console.error(`Error sending request to `, request.url, "Error: ", e);
|
||||
callback({ statusCode: 400 });
|
||||
return;
|
||||
}
|
||||
callback({
|
||||
statusCode: response.status,
|
||||
data: response.body,
|
||||
data: response.body || undefined,
|
||||
headers: Object.fromEntries(response.headers.entries()),
|
||||
mimeType: response.headers.get("Content-Type")
|
||||
mimeType: response.headers.get("Content-Type") || undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`${PROTOCOL} protocol inteception ${
|
||||
console.info(
|
||||
`${SCHEME} protocol inteception ${
|
||||
protocolInterceptionResult ? "successful" : "failed"
|
||||
}.`
|
||||
);
|
||||
|
||||
// protocol.handle(SCHEME, (request) => {
|
||||
// const url = new URL(request.url);
|
||||
// if (shouldInterceptRequest(url)) {
|
||||
// console.info("Intercepting request:", request.url);
|
||||
// const loadIndex = !extname(url.pathname);
|
||||
// const absoluteFilePath = normalize(
|
||||
// `${__dirname}${
|
||||
// loadIndex ? `${BASE_PATH}/index.html` : `${BASE_PATH}/${url.pathname}`
|
||||
// }`
|
||||
// );
|
||||
// const filePath = getPath(absoluteFilePath);
|
||||
// if (!filePath) {
|
||||
// console.error("Local asset file not found at", filePath);
|
||||
// return new Response(undefined, {
|
||||
// status: 404,
|
||||
// statusText: "FILE_NOT_FOUND"
|
||||
// });
|
||||
// }
|
||||
// const fileExtension = extname(filePath).replace(".", "");
|
||||
// const data = createReadStream(filePath);
|
||||
// return new Response(data, {
|
||||
// headers: { "Content-Type": extensionToMimeType[fileExtension] }
|
||||
// });
|
||||
// } else {
|
||||
// return net.fetch(request);
|
||||
// }
|
||||
// });
|
||||
// console.info(`${SCHEME} protocol inteception "successful"`);
|
||||
}
|
||||
|
||||
const bypassedRoutes = ["/notes/index_v14.json", "/notes/welcome-web"];
|
||||
function shouldInterceptRequest(url) {
|
||||
let shouldIntercept = url.hostname === HOSTNAME;
|
||||
const bypassedRoutes: string[] = [];
|
||||
function shouldInterceptRequest(url: URL) {
|
||||
const shouldIntercept = url.hostname === HOSTNAME;
|
||||
return shouldIntercept && !bypassedRoutes.includes(url.pathname);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Electron.ProtocolRequest} request
|
||||
*/
|
||||
async function getBody(request) {
|
||||
/**
|
||||
* @type {Electron.Session}
|
||||
*/
|
||||
const session = global.win.webContents.session;
|
||||
const PROTOCOL_URL = `${SCHEME}://${HOSTNAME}/`;
|
||||
export { registerProtocol, PROTOCOL_URL };
|
||||
|
||||
async function getBody(request: ProtocolRequest) {
|
||||
const session = globalThis?.window?.webContents?.session;
|
||||
|
||||
const blobParts = [];
|
||||
if (!request.uploadData || !request.uploadData.length) return null;
|
||||
for (let data of request.uploadData) {
|
||||
if (data.type === "rawData") {
|
||||
for (const data of request.uploadData) {
|
||||
if (data.bytes) {
|
||||
blobParts.push(new Uint8Array(data.bytes));
|
||||
} else if (data.type === "blob") {
|
||||
} else if (session && data.blobUUID) {
|
||||
const buffer = await session.getBlobData(data.blobUUID);
|
||||
if (!buffer) continue;
|
||||
blobParts.push(new Uint8Array(buffer));
|
||||
}
|
||||
}
|
||||
const blob = new Blob(blobParts);
|
||||
return await blob.arrayBuffer();
|
||||
return blob;
|
||||
}
|
||||
|
||||
const PROTOCOL_URL = `${PROTOCOL}://${HOSTNAME}/`;
|
||||
export { registerProtocol, PROTOCOL_URL };
|
||||
@@ -17,24 +17,17 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { app } from "electron";
|
||||
import { isAbsolute, join } from "path";
|
||||
|
||||
function sendMessageToRenderer(type, payload = {}) {
|
||||
const message = { type, ...payload };
|
||||
logger.info("Sending message to renderer", message);
|
||||
if (global.win) global.win.webContents.send("fromMain", message);
|
||||
}
|
||||
|
||||
function resolvePath(_path) {
|
||||
export function resolvePath(_path: string) {
|
||||
if (isAbsolute(_path)) return _path;
|
||||
|
||||
return join(
|
||||
..._path.split("/").map((segment) => {
|
||||
let resolved = segment;
|
||||
try {
|
||||
resolved = app.getPath(resolved);
|
||||
resolved = app.getPath(resolved as any);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
@@ -42,5 +35,3 @@ function resolvePath(_path) {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export { resolvePath, sendMessageToRenderer };
|
||||
75
apps/desktop/src/utils/theme.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { nativeTheme } from "electron";
|
||||
import { config } from "./config";
|
||||
import { z } from "zod";
|
||||
|
||||
export const Theme = z.union([
|
||||
z.literal("system"),
|
||||
z.literal("light"),
|
||||
z.literal("dark")
|
||||
]);
|
||||
|
||||
export type Theme = z.infer<typeof Theme>;
|
||||
|
||||
function getTheme() {
|
||||
return config.theme;
|
||||
}
|
||||
|
||||
function setTheme(theme: Theme) {
|
||||
changeTheme(theme);
|
||||
if (globalThis.window)
|
||||
globalThis.window.setBackgroundColor(getBackgroundColor());
|
||||
config.theme = theme;
|
||||
}
|
||||
|
||||
function getBackgroundColor() {
|
||||
return nativeTheme.shouldUseDarkColors ? "#0f0f0f" : "#ffffff";
|
||||
}
|
||||
|
||||
function getSystemTheme() {
|
||||
const listeners = nativeTheme.rawListeners("updated");
|
||||
nativeTheme.removeAllListeners("updated");
|
||||
|
||||
const oldThemeSource = nativeTheme.themeSource;
|
||||
nativeTheme.themeSource = "system";
|
||||
const currentTheme = nativeTheme.shouldUseDarkColors ? "dark" : "light";
|
||||
nativeTheme.themeSource = oldThemeSource;
|
||||
|
||||
setTimeout(
|
||||
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
|
||||
1000
|
||||
);
|
||||
return currentTheme;
|
||||
}
|
||||
|
||||
export { getTheme, setTheme, getBackgroundColor, getSystemTheme };
|
||||
|
||||
function changeTheme(theme: Theme) {
|
||||
const listeners = nativeTheme.rawListeners("updated");
|
||||
nativeTheme.removeAllListeners("updated");
|
||||
|
||||
nativeTheme.themeSource = theme;
|
||||
|
||||
setTimeout(
|
||||
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
|
||||
1000
|
||||
);
|
||||
}
|
||||
91
apps/desktop/src/utils/tray.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { app, Menu, Tray } from "electron";
|
||||
import { AssetManager } from "./asset-manager";
|
||||
import { isFlatpak } from "./index";
|
||||
import { bringToFront } from "./bring-to-front";
|
||||
import { bridge } from "../api/bridge";
|
||||
|
||||
let tray: Tray | undefined = undefined;
|
||||
export function destroyTray() {
|
||||
if (tray) tray.destroy();
|
||||
}
|
||||
|
||||
export function setupTray() {
|
||||
if (tray) tray.destroy();
|
||||
|
||||
const trayIconSize =
|
||||
process.platform === "win32" || process.platform === "darwin" ? 16 : 32;
|
||||
|
||||
tray = new Tray(
|
||||
AssetManager.icon("tray-icon", {
|
||||
size: process.platform === "darwin" ? 22 : 32
|
||||
})
|
||||
);
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: "Show app",
|
||||
type: "normal",
|
||||
icon: isFlatpak()
|
||||
? undefined
|
||||
: AssetManager.icon("tray-icon", { size: trayIconSize }),
|
||||
click: bringToFront
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "New note",
|
||||
type: "normal",
|
||||
icon: isFlatpak()
|
||||
? undefined
|
||||
: AssetManager.icon("note-add", { size: trayIconSize }),
|
||||
click: () => {
|
||||
bringToFront();
|
||||
bridge.onCreateItem("note");
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "New notebook",
|
||||
type: "normal",
|
||||
icon: isFlatpak()
|
||||
? undefined
|
||||
: AssetManager.icon("notebook-add", { size: trayIconSize }),
|
||||
click: () => {
|
||||
bringToFront();
|
||||
bridge.onCreateItem("notebook");
|
||||
}
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Quit",
|
||||
icon: isFlatpak()
|
||||
? undefined
|
||||
: AssetManager.icon("quit", { size: trayIconSize }),
|
||||
type: "normal",
|
||||
click: () => {
|
||||
app.exit(0);
|
||||
}
|
||||
}
|
||||
]);
|
||||
tray.on("double-click", bringToFront);
|
||||
if (process.platform !== "darwin") tray.on("click", bringToFront);
|
||||
tray.setToolTip("Notesnook");
|
||||
tray.setContextMenu(contextMenu);
|
||||
}
|
||||
@@ -17,37 +17,64 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { JSONStorage } from "../jsonstorage";
|
||||
import { screen as _screen, remote } from "electron";
|
||||
const screen = _screen || remote.screen;
|
||||
import { JSONStorage } from "./json-storage";
|
||||
import { screen as _screen, BrowserWindow } from "electron";
|
||||
const screen = _screen;
|
||||
|
||||
class WindowState {
|
||||
constructor(options) {
|
||||
this.winRef = null;
|
||||
type WindowStateOptions = {
|
||||
storageKey: string;
|
||||
maximize: boolean;
|
||||
fullScreen: boolean;
|
||||
defaultWidth: number;
|
||||
defaultHeight: number;
|
||||
};
|
||||
|
||||
type SerializableWindowState = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
displayBounds?: Electron.Rectangle;
|
||||
isMaximized?: boolean;
|
||||
isFullScreen?: boolean;
|
||||
};
|
||||
|
||||
export class WindowState {
|
||||
private readonly config: WindowStateOptions;
|
||||
private state: SerializableWindowState;
|
||||
private windowRef: BrowserWindow | undefined;
|
||||
private readonly eventHandlingDelay = 1000;
|
||||
private stateChangeTimer: NodeJS.Timeout | undefined;
|
||||
|
||||
constructor(options?: Partial<WindowStateOptions>) {
|
||||
this.windowRef = undefined;
|
||||
this.stateChangeTimer = undefined;
|
||||
this.eventHandlingDelay = 100;
|
||||
this.config = {
|
||||
storageKey: "windowState",
|
||||
maximize: true,
|
||||
fullScreen: true,
|
||||
defaultWidth: 1024,
|
||||
defaultHeight: 700,
|
||||
...options
|
||||
};
|
||||
|
||||
// Load previous state
|
||||
this.state = JSONStorage.get(this.config.storageKey, {});
|
||||
const defaultState: SerializableWindowState = {
|
||||
width: this.config.defaultWidth,
|
||||
height: this.config.defaultHeight,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
this.state = JSONStorage.get<SerializableWindowState>(
|
||||
this.config.storageKey,
|
||||
defaultState
|
||||
);
|
||||
|
||||
// Check state validity
|
||||
this.validateState();
|
||||
|
||||
// Set state fallback values
|
||||
this.state = {
|
||||
width: this.config.defaultWidth || 800,
|
||||
height: this.config.defaultHeight || 600,
|
||||
...this.state
|
||||
};
|
||||
this.validateState(defaultState);
|
||||
}
|
||||
|
||||
isNormal(win) {
|
||||
isNormal(win: BrowserWindow) {
|
||||
return !win.isMaximized() && !win.isMinimized() && !win.isFullScreen();
|
||||
}
|
||||
|
||||
@@ -76,7 +103,9 @@ class WindowState {
|
||||
};
|
||||
}
|
||||
|
||||
windowWithinBounds(bounds) {
|
||||
windowWithinBounds(bounds: Electron.Rectangle) {
|
||||
if (!this.state) return false;
|
||||
|
||||
return (
|
||||
this.state.x >= bounds.x &&
|
||||
this.state.y >= bounds.y &&
|
||||
@@ -97,12 +126,12 @@ class WindowState {
|
||||
}
|
||||
}
|
||||
|
||||
validateState() {
|
||||
validateState(defaultState: SerializableWindowState) {
|
||||
const isValid =
|
||||
this.state &&
|
||||
(this.hasBounds() || this.state.isMaximized || this.state.isFullScreen);
|
||||
if (!isValid) {
|
||||
this.state = null;
|
||||
this.state = defaultState;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -111,8 +140,8 @@ class WindowState {
|
||||
}
|
||||
}
|
||||
|
||||
updateState(win) {
|
||||
win = win || this.winRef;
|
||||
updateState(win?: BrowserWindow) {
|
||||
win = win || this.windowRef;
|
||||
if (!win) {
|
||||
return;
|
||||
}
|
||||
@@ -133,7 +162,7 @@ class WindowState {
|
||||
}
|
||||
}
|
||||
|
||||
saveState(win) {
|
||||
saveState(win?: BrowserWindow) {
|
||||
// Update window state only if it was provided
|
||||
if (win) {
|
||||
this.updateState(win);
|
||||
@@ -145,9 +174,9 @@ class WindowState {
|
||||
|
||||
stateChangeHandler = () => {
|
||||
// Handles both 'resize' and 'move'
|
||||
clearTimeout(this.stateChangeTimer);
|
||||
if (this.stateChangeTimer) clearTimeout(this.stateChangeTimer);
|
||||
this.stateChangeTimer = setTimeout(
|
||||
() => this.updateState(),
|
||||
() => this.saveState(this.windowRef),
|
||||
this.eventHandlingDelay
|
||||
);
|
||||
};
|
||||
@@ -163,28 +192,28 @@ class WindowState {
|
||||
this.saveState();
|
||||
};
|
||||
|
||||
manage(win) {
|
||||
manage(win: BrowserWindow) {
|
||||
if (this.config.maximize && this.state.isMaximized) {
|
||||
win.maximize();
|
||||
}
|
||||
if (this.config.fullScreen && this.state.isFullScreen) {
|
||||
win.setFullScreen(true);
|
||||
}
|
||||
win.on("resize", this.stateChangeHandler);
|
||||
win.on("move", this.stateChangeHandler);
|
||||
win.on("resized", this.stateChangeHandler);
|
||||
win.on("moved", this.stateChangeHandler);
|
||||
win.on("close", this.closeHandler);
|
||||
win.on("closed", this.closedHandler);
|
||||
this.winRef = win;
|
||||
this.windowRef = win;
|
||||
}
|
||||
|
||||
unmanage() {
|
||||
if (this.winRef) {
|
||||
this.winRef.removeListener("resize", this.stateChangeHandler);
|
||||
this.winRef.removeListener("move", this.stateChangeHandler);
|
||||
clearTimeout(this.stateChangeTimer);
|
||||
this.winRef.removeListener("close", this.closeHandler);
|
||||
this.winRef.removeListener("closed", this.closedHandler);
|
||||
this.winRef = null;
|
||||
if (this.windowRef) {
|
||||
this.windowRef.removeListener("resized", this.stateChangeHandler);
|
||||
this.windowRef.removeListener("moved", this.stateChangeHandler);
|
||||
if (this.stateChangeTimer) clearTimeout(this.stateChangeTimer);
|
||||
this.windowRef.removeListener("close", this.closeHandler);
|
||||
this.windowRef.removeListener("closed", this.closedHandler);
|
||||
this.windowRef = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,5 +245,3 @@ class WindowState {
|
||||
return this.state.isFullScreen;
|
||||
}
|
||||
}
|
||||
|
||||
export { WindowState };
|
||||
8
apps/desktop/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/",
|
||||
"lib": ["ESNext"]
|
||||
},
|
||||
"include": ["src", "global.d.ts"]
|
||||
}
|
||||
@@ -17,15 +17,6 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup npmrc
|
||||
run: |
|
||||
echo "registry=https://npm.pkg.github.com" > .npmrc
|
||||
echo "@streetwriters:registry=https://npm.pkg.github.com" >> .npmrc
|
||||
echo "//npm.pkg.github.com/:_authToken=${{secrets.PACKAGES_TOKEN}}" >> .npmrc
|
||||
|
||||
- name: Setup yarnrc
|
||||
run: echo "\"@streetwriters:registry\" \"https://npm.pkg.github.com\"" > .yarnrc
|
||||
|
||||
- uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: "16"
|
||||
|
||||
@@ -17,15 +17,6 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup npmrc
|
||||
run: |
|
||||
echo "registry=https://npm.pkg.github.com" > .npmrc
|
||||
echo "@streetwriters:registry=https://npm.pkg.github.com" >> .npmrc
|
||||
echo "//npm.pkg.github.com/:_authToken=${{secrets.PACKAGES_TOKEN}}" >> .npmrc
|
||||
|
||||
- name: Setup yarnrc
|
||||
run: echo "\"@streetwriters:registry\" \"https://npm.pkg.github.com\"" > .yarnrc
|
||||
|
||||
- uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: "16"
|
||||
|
||||
4
apps/mobile/.gitignore
vendored
@@ -14,7 +14,9 @@ build_cache/
|
||||
.yarnrc.yml
|
||||
.yarn
|
||||
*.log
|
||||
|
||||
.cxx/
|
||||
*.keystore
|
||||
!debug.keystore
|
||||
|
||||
# Xcode
|
||||
#
|
||||
|
||||
1
apps/mobile/.vscode/settings.json
vendored
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -19,9 +19,9 @@
|
||||
|
||||
## Build instructions
|
||||
|
||||
> **Before you start it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
|
||||
> **Before you start, it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
|
||||
|
||||
### Setting up development environment
|
||||
### Setting up the development environment
|
||||
|
||||
Requirements:
|
||||
|
||||
@@ -57,7 +57,7 @@ npm install
|
||||
|
||||
### Running the app on Android
|
||||
|
||||
[Setup an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) if you haven't already and then run the following command to start the app in the Emulator:
|
||||
[Setup an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) if you haven't already, and then run the following command to start the app in the Emulator:
|
||||
|
||||
```bash
|
||||
npm run start:android
|
||||
@@ -78,11 +78,11 @@ npm run start:ios
|
||||
|
||||
## Developer guide
|
||||
|
||||
> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript so if you can help with that, it'd be great!
|
||||
> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript, so if you can help with that, it'd be great!
|
||||
|
||||
### The tech stack
|
||||
|
||||
We try to keep the stack as lean as possible
|
||||
We try to keep the stack as lean as possible:
|
||||
|
||||
1. React Native
|
||||
2. Typescript/Javascript
|
||||
@@ -95,17 +95,17 @@ We try to keep the stack as lean as possible
|
||||
|
||||
The app codebase is distributed over two primary directories. `native/` and `app/`.
|
||||
|
||||
- `native/`: Includes `android/` and `ios/` folders and everything related to react native core functionality like bundling, development and packaging. Any react-native dependency that has native code i.e android & ios folders, is installed here.
|
||||
- `native/`: Includes `android/` and `ios/` folders and everything related to react native core functionality like bundling, development, and packaging. Any react-native dependency with native code, i.e., android & ios folders, is installed here.
|
||||
|
||||
- `app/`: Includes all the app code other than the native part. All JS only dependencies are installed here.
|
||||
- `components/`: Each component serves a specific purpose in the app UI, for example the `Paragraph` component is used to render paragraphs in the app and a `Header` component is used to render a `header` on all screens.
|
||||
- `common/`: Features that have integral role in app functionality, for example, notesnook core is initialized here.
|
||||
- `app/`: Includes all the app code other than the native part. All JS-only dependencies are installed here.
|
||||
- `components/`: Each component serves a specific purpose in the app UI. For example, the `Paragraph` component is used to render paragraphs in the app, and a `Header` component is used to render a `header` on all screens.
|
||||
- `common/`: Features that are integral to the app's functionality. For example, the notesnook core is initialized here.
|
||||
- `hooks/`: Hooks for different app logic
|
||||
- `navigation/`: Includes app navigation specific code. Here the app navigation, editor & side menu are rendered side by side in fluid tabs.
|
||||
- `navigation/`: Includes app navigation-specific code. Here the app navigation, editor & side menu are rendered side by side in fluid tabs.
|
||||
- `screens`: Navigator screens.
|
||||
- `services`: Parts of code that do a specific function, for example, the `sync` service is responsibe for running Sync from anywhere in the app.
|
||||
- `stores`: We use `zustand` for global state management in the app. There are multiple stores that provide the state for different parts of the app.
|
||||
- `utils`: General purpose stuff such as constant values, utility functions etc.
|
||||
- `services`: Parts of code that do a specific function. For example, the `sync` service runs Sync from anywhere in the app.
|
||||
- `stores`: We use `zustand` for global state management in the app. Multiple stores provide the state for different parts of the app.
|
||||
- `utils`: General purpose stuff such as constant values, utility functions, etc.
|
||||
|
||||
There are several other folders at the root:
|
||||
|
||||
@@ -115,11 +115,11 @@ There are several other folders at the root:
|
||||
|
||||
### Running the tests
|
||||
|
||||
When you are done making the required changes, you will need to run the tests to make sure you didn't break anything. We use Detox as the testing framework & the tests can be started as follows:
|
||||
When you are done making the required changes, you must run the tests to ensure you didn't break anything. We use Detox as the testing framework & the tests can be started as follows:
|
||||
|
||||
### Android
|
||||
|
||||
To run the tests on android, you will need to create an emulator device on your system:
|
||||
To run the tests on Android, you will need to create an emulator device on your system:
|
||||
|
||||
```
|
||||
$ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --package "system-images;android-31;default;x86_64"
|
||||
@@ -127,13 +127,13 @@ $ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --packa
|
||||
|
||||
If you face problems, follow the detailed guide in [Detox documentation](https://wix.github.io/Detox/docs/introduction/android-dev-env). Keep the emulator name set to `Pixel_5_API_31`.
|
||||
|
||||
Once you have created an emulator device, build the android apks
|
||||
Once you have created an emulator device, build the Android apks:
|
||||
|
||||
```
|
||||
npm run build:android
|
||||
```
|
||||
|
||||
Finally run the tests
|
||||
Finally, run the tests:
|
||||
|
||||
```
|
||||
npm run test:android
|
||||
@@ -141,9 +141,9 @@ npm run test:android
|
||||
|
||||
### iOS
|
||||
|
||||
To run e2e tests on iOS simulator, you must be on a Mac with XCode installed.
|
||||
To run e2e tests on the iOS simulator, you must be on a Mac with XCode installed.
|
||||
|
||||
First install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils)
|
||||
First, install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils):
|
||||
|
||||
```
|
||||
brew tap wix/brew
|
||||
@@ -156,7 +156,7 @@ Now build the iOS app for testing:
|
||||
npm run build:ios
|
||||
```
|
||||
|
||||
Finally run the tests:
|
||||
Finally, run the tests:
|
||||
|
||||
```
|
||||
npm run test:ios
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import "react-native";
|
||||
import { it } from "@jest/globals";
|
||||
// Note: test renderer must be required after react-native.
|
||||
import renderer from "react-test-renderer";
|
||||
import Heading from "../app/components/ui/typography/heading";
|
||||
|
||||
@@ -16,44 +16,37 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
THEME_COMPATIBILITY_VERSION,
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import AppLockedOverlay from "./components/app-lock-overlay";
|
||||
import { withErrorBoundry } from "./components/exception-handler";
|
||||
import GlobalSafeAreaProvider from "./components/globalsafearea";
|
||||
import Launcher from "./components/launcher";
|
||||
import { useAppEvents } from "./hooks/use-app-events";
|
||||
import { ApplicationHolder } from "./navigation";
|
||||
import Notifications from "./services/notifications";
|
||||
import SettingsService from "./services/settings";
|
||||
import { TipManager } from "./services/tip-manager";
|
||||
import { useThemeStore } from "./stores/use-theme-store";
|
||||
import { useUserStore } from "./stores/use-user-store";
|
||||
import { View } from "react-native";
|
||||
import { useState } from "react";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
//import { BackgroundSync } from "./services/background-sync";
|
||||
|
||||
NetInfo.configure({
|
||||
reachabilityUrl: "https://notesnook.com",
|
||||
reachabilityTest: (response) => {
|
||||
if (!response) return false;
|
||||
return response?.status >= 200 && response?.status < 300;
|
||||
}
|
||||
});
|
||||
import { themeTrpcClient } from "./screens/settings/theme-selector";
|
||||
|
||||
SettingsService.init();
|
||||
SettingsService.checkOrientation();
|
||||
const App = () => {
|
||||
useAppEvents();
|
||||
const [init, setInit] = useState(false);
|
||||
const init = useAppEvents();
|
||||
useEffect(() => {
|
||||
let { appLockMode } = SettingsService.get();
|
||||
if (appLockMode && appLockMode !== "none") {
|
||||
useUserStore.getState().setVerifyUser(true);
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
//BackgroundSync.start();
|
||||
setInit(true);
|
||||
init();
|
||||
setTimeout(async () => {
|
||||
SettingsService.onFirstLaunch();
|
||||
await Notifications.get();
|
||||
@@ -62,7 +55,10 @@ const App = () => {
|
||||
}
|
||||
TipManager.init();
|
||||
}, 100);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -91,10 +87,49 @@ const App = () => {
|
||||
}}
|
||||
>
|
||||
<ApplicationHolder />
|
||||
{init && <Launcher />}
|
||||
</GestureHandlerRootView>
|
||||
<AppLockedOverlay />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default withErrorBoundry(App, "App");
|
||||
export const withTheme = (Element) => {
|
||||
return function AppWithThemeProvider() {
|
||||
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
|
||||
state.colorScheme,
|
||||
state.darkTheme,
|
||||
state.lightTheme
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
if (!currentTheme) return;
|
||||
themeTrpcClient.updateTheme
|
||||
.query({
|
||||
version: currentTheme.version,
|
||||
compatibilityVersion: THEME_COMPATIBILITY_VERSION,
|
||||
id: currentTheme.id
|
||||
})
|
||||
.then((theme) => {
|
||||
if (theme) {
|
||||
console.log(theme.version, "theme updated");
|
||||
theme.colorScheme === "dark"
|
||||
? useThemeStore.getState().setDarkTheme(theme)
|
||||
: useThemeStore.getState().setLightTheme(theme);
|
||||
}
|
||||
})
|
||||
.catch(console.log);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
useThemeEngineStore
|
||||
.getState()
|
||||
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
|
||||
}, [colorScheme, darkTheme, lightTheme]);
|
||||
|
||||
return <Element />;
|
||||
};
|
||||
};
|
||||
|
||||
export default withTheme(withErrorBoundry(App, "App"));
|
||||
|
||||
BIN
apps/mobile/app/assets/images/notesnook-dark.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
@@ -22,10 +22,17 @@ import "react-native-get-random-values";
|
||||
import * as Keychain from "react-native-keychain";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_UPGRAGE_KEY = "keychain-ios:upgraded";
|
||||
|
||||
const KEYSTORE_CONFIG = Platform.select({
|
||||
ios: {
|
||||
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY
|
||||
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
||||
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
|
||||
service: IOS_KEYCHAIN_SERVICE_NAME
|
||||
},
|
||||
android: {}
|
||||
});
|
||||
@@ -39,12 +46,27 @@ export async function deriveCryptoKey(name, data) {
|
||||
credentials.key,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
|
||||
return credentials.key;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function upgradeIOSKeychain(username, password) {
|
||||
if (Platform.OS !== "ios") return;
|
||||
if (!MMKV.getBool(IOS_KEYCHAIN_UPGRAGE_KEY)) {
|
||||
await Keychain.setInternetCredentials(
|
||||
"notesnook",
|
||||
username,
|
||||
password,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
console.log("IOS KEYCHAIN MIGRATION COMPLETED!");
|
||||
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCryptoKey(_name) {
|
||||
try {
|
||||
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||
@@ -52,6 +74,9 @@ export async function getCryptoKey(_name) {
|
||||
"notesnook",
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
// upgrades ios keychain to use accessGroups
|
||||
// so we have access to keychain in share extension.
|
||||
await upgradeIOSKeychain(credentials.username, credentials.password);
|
||||
return credentials.password;
|
||||
} else {
|
||||
return null;
|
||||
|
||||
@@ -16,8 +16,7 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Database from "@notesnook/core/api/index";
|
||||
import { database } from "@notesnook/common";
|
||||
import { initalize, logger as dbLogger } from "@notesnook/core/logger";
|
||||
import { Platform } from "react-native";
|
||||
import { MMKVLoader } from "react-native-mmkv-storage";
|
||||
@@ -30,52 +29,44 @@ import * as Gzip from "react-native-gzip";
|
||||
const LoggerStorage = new MMKVLoader()
|
||||
.withInstanceID("notesnook_logs")
|
||||
.initialize();
|
||||
|
||||
|
||||
database.host(
|
||||
__DEV__
|
||||
? {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
// API_HOST: "http://192.168.8.101:5264",
|
||||
// AUTH_HOST: "http://192.168.8.101:8264",
|
||||
// SSE_HOST: "http://192.168.8.101:7264",
|
||||
// SUBSCRIPTIONS_HOST: "http://192.168.8.101:9264",
|
||||
// ISSUES_HOST: "http://192.168.8.101:2624"
|
||||
}
|
||||
: {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
}
|
||||
);
|
||||
|
||||
database.setup(
|
||||
Storage,
|
||||
Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
filesystem,
|
||||
{
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
initalize(new KV(LoggerStorage), true);
|
||||
|
||||
export const db = database;
|
||||
export const DatabaseLogger = dbLogger;
|
||||
|
||||
/**
|
||||
* @type {import("@notesnook/core/api/index").default}
|
||||
*/
|
||||
export var db = new Database(
|
||||
Storage,
|
||||
Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
filesystem,
|
||||
{
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
}
|
||||
);
|
||||
|
||||
db.host(
|
||||
__DEV__
|
||||
? {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
// API_HOST: "http://192.168.8.101:5264",
|
||||
// AUTH_HOST: "http://192.168.8.101:8264",
|
||||
// SSE_HOST: "http://192.168.8.101:7264",
|
||||
// SUBSCRIPTIONS_HOST: "http://192.168.8.101:9264",
|
||||
// ISSUES_HOST: "http://192.168.8.101:2624"
|
||||
}
|
||||
: {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
}
|
||||
);
|
||||
|
||||
export async function loadDatabase() {
|
||||
// if (!DB) {
|
||||
// let module = await import(/* webpackChunkName: "notes-core" */ 'notes-core/api/index');
|
||||
// DB = module.default;
|
||||
// }
|
||||
// db = new DB(Storage, Platform.OS === 'ios' ? EventSource : AndroidEventSource, filesystem);
|
||||
// if (DOMParser) {
|
||||
// await DOMParser.prepare();
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import {
|
||||
decrypt,
|
||||
deriveCryptoKey,
|
||||
|
||||
50
apps/mobile/app/common/filesystem/compress.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Dimensions, Platform } from "react-native";
|
||||
import ImageResizer from "@bam.tech/react-native-image-resizer";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
/**
|
||||
* Scale down & compress images to screen width
|
||||
* for loading in editor.
|
||||
* @returns
|
||||
*/
|
||||
export async function compressToBase64(path, type) {
|
||||
const { width, scale } = Dimensions.get("window");
|
||||
const response = await ImageResizer.createResizedImage(
|
||||
path,
|
||||
width * scale,
|
||||
9999,
|
||||
type,
|
||||
80,
|
||||
0,
|
||||
undefined,
|
||||
true,
|
||||
{
|
||||
mode: "contain",
|
||||
onlyScaleDown: true
|
||||
}
|
||||
);
|
||||
const base64 = await RNFetchBlob.fs.readFile(
|
||||
Platform.OS === "ios" ? response.uri?.replace("file://", "") : response.uri,
|
||||
"base64"
|
||||
);
|
||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(response.uri.replace("file://", "")).catch(console.log);
|
||||
return base64;
|
||||
}
|
||||
@@ -17,19 +17,171 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import {
|
||||
getFileNameWithExtension,
|
||||
isImage,
|
||||
isDocument
|
||||
} from "@notesnook/core/utils/filename";
|
||||
import React from "react";
|
||||
import { Platform } from "react-native";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import { subscribe, zip } from "react-native-zip-archive";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
||||
import { presentSheet, ToastEvent } from "../../services/event-manager";
|
||||
import { ToastEvent, presentSheet } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { cacheDir } from "./utils";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
|
||||
export default async function downloadAttachment(hash, global = true) {
|
||||
export const FileDownloadStatus = {
|
||||
Success: 1,
|
||||
Fail: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Download all user's attachments
|
||||
* @returns
|
||||
*/
|
||||
export function downloadAllAttachments() {
|
||||
const attachments = db.attachments.all;
|
||||
return downloadAttachments(attachments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads provided attachments to a .zip file
|
||||
* on user's device.
|
||||
* @param attachments
|
||||
* @param onProgress
|
||||
* @returns
|
||||
*/
|
||||
export async function downloadAttachments(
|
||||
attachments,
|
||||
onProgress,
|
||||
canceled,
|
||||
groupId
|
||||
) {
|
||||
if (!attachments || !attachments.length) return;
|
||||
const result = new Map();
|
||||
|
||||
let outputFolder;
|
||||
if (Platform.OS === "android") {
|
||||
// Ask the user to select a directory to store the file
|
||||
let file = await ScopedStorage.openDocumentTree(true);
|
||||
outputFolder = file.uri;
|
||||
if (!outputFolder) return;
|
||||
} else {
|
||||
outputFolder = await Storage.checkAndCreateDir("/downloads/");
|
||||
}
|
||||
|
||||
// Create the folder to zip;
|
||||
const zipSourceFolder = `${cacheDir}/notesnook-attachments`;
|
||||
const zipOutputFile =
|
||||
Platform.OS === "ios"
|
||||
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
|
||||
: `${cacheDir}/notesnook-attachments.zip`;
|
||||
if (await RNFetchBlob.fs.exists(zipSourceFolder))
|
||||
await RNFetchBlob.fs.unlink(zipSourceFolder);
|
||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
let attachment = attachments[i];
|
||||
const hash = attachment.metadata.hash;
|
||||
try {
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
onProgress?.(
|
||||
i + 1 / attachments.length,
|
||||
`Downloading attachments (${i + 1}/${
|
||||
attachments.length
|
||||
})... Please wait`
|
||||
);
|
||||
// Download to cache
|
||||
let uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true,
|
||||
groupId: groupId
|
||||
});
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
if (!uri) throw new Error("Failed to download file");
|
||||
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
|
||||
const filePath = `${zipSourceFolder}/${attachment.metadata.filename}`;
|
||||
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
|
||||
result.set(hash, {
|
||||
filename: attachment.metadata.filename,
|
||||
status: FileDownloadStatus.Success
|
||||
});
|
||||
} catch (e) {
|
||||
result.set(hash, {
|
||||
filename: attachment.metadata.filename,
|
||||
status: FileDownloadStatus.Fail,
|
||||
reason: e
|
||||
});
|
||||
console.log("Error downloading attachment", e);
|
||||
}
|
||||
}
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
if (result?.size) {
|
||||
let sub;
|
||||
try {
|
||||
onProgress?.(0, `Zipping... Please wait`);
|
||||
// If all goes well, zip the notesnook-attachments folder in cache.
|
||||
|
||||
sub = subscribe(({ progress }) => {
|
||||
onProgress(
|
||||
progress,
|
||||
`Saving zip file (${(progress * 100).toFixed(1)}%)... Please wait`
|
||||
);
|
||||
});
|
||||
await zip(zipSourceFolder, zipOutputFile);
|
||||
sub?.remove();
|
||||
onProgress(1, `Saving zip file... Please wait`);
|
||||
if (Platform.OS === "android") {
|
||||
// Move the zip to user selected directory.
|
||||
const file = await ScopedStorage.createFile(
|
||||
outputFolder,
|
||||
`notesnook-attachments-${Date.now()}.zip`,
|
||||
"application/zip"
|
||||
);
|
||||
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
|
||||
}
|
||||
|
||||
onProgress?.(1, `Done`);
|
||||
releasePermissions(outputFolder);
|
||||
} catch (e) {
|
||||
releasePermissions(outputFolder);
|
||||
sub?.remove();
|
||||
console.log("Error zipping attachments", e);
|
||||
}
|
||||
// Remove source & zip file from cache.
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
if (Platform.OS === "android") {
|
||||
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default async function downloadAttachment(
|
||||
hash,
|
||||
global = true,
|
||||
options = {
|
||||
silent: false,
|
||||
cache: false,
|
||||
throwError: false,
|
||||
groupId: undefined
|
||||
}
|
||||
) {
|
||||
let attachment = db.attachments.attachment(hash);
|
||||
if (!attachment) {
|
||||
console.log("attachment not found");
|
||||
@@ -37,16 +189,23 @@ export default async function downloadAttachment(hash, global = true) {
|
||||
}
|
||||
|
||||
let folder = {};
|
||||
if (Platform.OS === "android") {
|
||||
folder = await ScopedStorage.openDocumentTree();
|
||||
if (!folder) return;
|
||||
} else {
|
||||
folder.uri = await Storage.checkAndCreateDir("/downloads/");
|
||||
if (!options.cache) {
|
||||
if (Platform.OS === "android") {
|
||||
folder = await ScopedStorage.openDocumentTree();
|
||||
if (!folder) return;
|
||||
} else {
|
||||
folder.uri = await Storage.checkAndCreateDir("/downloads/");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await db.fs.downloadFile(
|
||||
console.log(
|
||||
"starting download attachment",
|
||||
attachment.metadata.hash,
|
||||
options.groupId
|
||||
);
|
||||
await db.fs.downloadFile(
|
||||
options.groupId || attachment.metadata.hash,
|
||||
attachment.metadata.hash
|
||||
);
|
||||
if (
|
||||
@@ -54,6 +213,11 @@ export default async function downloadAttachment(hash, global = true) {
|
||||
)
|
||||
return;
|
||||
|
||||
let filename = getFileNameWithExtension(
|
||||
attachment.metadata.filename,
|
||||
attachment.metadata.type
|
||||
);
|
||||
|
||||
let key = await db.attachments.decryptKey(attachment.key);
|
||||
let info = {
|
||||
iv: attachment.iv,
|
||||
@@ -63,46 +227,51 @@ export default async function downloadAttachment(hash, global = true) {
|
||||
hash: attachment.metadata.hash,
|
||||
hashType: attachment.metadata.hashType,
|
||||
mime: attachment.metadata.type,
|
||||
fileName: attachment.metadata.filename,
|
||||
uri: folder.uri,
|
||||
fileName: options.cache ? undefined : filename,
|
||||
uri: options.cache ? undefined : folder.uri,
|
||||
chunkSize: attachment.chunkSize
|
||||
};
|
||||
|
||||
let fileUri = await Sodium.decryptFile(key, info, "file");
|
||||
ToastEvent.show({
|
||||
heading: "Download successful",
|
||||
message: attachment.metadata.filename + " downloaded",
|
||||
type: "success"
|
||||
});
|
||||
let fileUri = await Sodium.decryptFile(
|
||||
key,
|
||||
info,
|
||||
options.cache ? "cache" : "file"
|
||||
);
|
||||
|
||||
if (attachment.dateUploaded) {
|
||||
if (!options.silent) {
|
||||
ToastEvent.show({
|
||||
heading: "Download successful",
|
||||
message: filename + " downloaded",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
attachment.dateUploaded &&
|
||||
!isImage(attachment.metadata?.type) &&
|
||||
!isDocument(attachment.metadata?.type)
|
||||
) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
|
||||
.catch(console.log);
|
||||
}
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
fileUri = folder.uri + `/${attachment.metadata.filename}`;
|
||||
if (Platform.OS === "ios" && !options.cache) {
|
||||
fileUri = folder.uri + `/${filename}`;
|
||||
}
|
||||
if (!options.silent) {
|
||||
presentSheet({
|
||||
title: "File downloaded",
|
||||
paragraph: `${filename} saved to ${
|
||||
Platform.OS === "android"
|
||||
? "selected path"
|
||||
: "File Manager/Notesnook/downloads"
|
||||
}`,
|
||||
icon: "download",
|
||||
context: global ? null : attachment.metadata.hash,
|
||||
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
|
||||
});
|
||||
}
|
||||
console.log("saved file uri: ", fileUri);
|
||||
|
||||
presentSheet({
|
||||
title: "File downloaded",
|
||||
paragraph: `${attachment.metadata.filename} saved to ${
|
||||
Platform.OS === "android"
|
||||
? "selected path"
|
||||
: "File Manager/Notesnook/downloads"
|
||||
}`,
|
||||
icon: "download",
|
||||
context: global ? null : attachment.metadata.hash,
|
||||
component: (
|
||||
<ShareComponent
|
||||
uri={fileUri}
|
||||
name={attachment.metadata.filename}
|
||||
padding={12}
|
||||
/>
|
||||
)
|
||||
});
|
||||
return fileUri;
|
||||
} catch (e) {
|
||||
console.log("download attachment error: ", e);
|
||||
@@ -112,5 +281,8 @@ export default async function downloadAttachment(hash, global = true) {
|
||||
.catch(console.log);
|
||||
}
|
||||
useAttachmentStore.getState().remove(attachment.metadata.hash);
|
||||
if (options.throwError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import hosts from "@notesnook/core/utils/constants";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
@@ -46,6 +46,7 @@ export async function downloadFile(filename, data, cancelToken) {
|
||||
|
||||
if (!downloadUrl) throw new Error("Unable to resolve download url");
|
||||
let totalSize = 0;
|
||||
console.log("Download starting");
|
||||
let request = RNFetchBlob.config({
|
||||
path: path,
|
||||
IOSBackgroundTask: true
|
||||
|
||||
@@ -23,14 +23,16 @@ import {
|
||||
deleteFile,
|
||||
exists,
|
||||
readEncrypted,
|
||||
writeEncrypted
|
||||
writeEncryptedBase64,
|
||||
hashBase64
|
||||
} from "./io";
|
||||
import { uploadFile } from "./upload";
|
||||
import { cancelable } from "./utils";
|
||||
|
||||
export default {
|
||||
readEncrypted,
|
||||
writeEncrypted,
|
||||
writeEncryptedBase64,
|
||||
hashBase64,
|
||||
uploadFile: cancelable(uploadFile),
|
||||
downloadFile: cancelable(downloadFile),
|
||||
deleteFile,
|
||||
|
||||
@@ -19,25 +19,54 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { cacheDir, getRandomId } from "./utils";
|
||||
import { db } from "../database";
|
||||
import { compressToBase64 } from "./compress";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
|
||||
export async function readEncrypted(filename, key, cipherData) {
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
try {
|
||||
let exists = await RNFetchBlob.fs.exists(path);
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
let exists =
|
||||
(await RNFetchBlob.fs.exists(path)) ||
|
||||
(Platform.OS === "ios" && (await RNFetchBlob.fs.exists(appGroupPath)));
|
||||
|
||||
if (!exists) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attachment = db.attachments.attachment(filename);
|
||||
const isPng = /(png)/g.test(attachment?.metadata.type);
|
||||
const isJpeg = /(jpeg|jpg)/g.test(attachment?.metadata.type);
|
||||
|
||||
let output = await Sodium.decryptFile(
|
||||
key,
|
||||
{
|
||||
...cipherData,
|
||||
hash: filename
|
||||
hash: filename,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
},
|
||||
cipherData.outputType === "base64" ? "base64" : "text"
|
||||
cipherData.outputType === "base64"
|
||||
? isPng || isJpeg
|
||||
? "cache"
|
||||
: "base64"
|
||||
: "text"
|
||||
);
|
||||
if (cipherData.outputType === "base64" && (isPng || isJpeg)) {
|
||||
const dCachePath = `${cacheDir}/${output}`;
|
||||
output = await compressToBase64(
|
||||
`file://${dCachePath}`,
|
||||
isPng ? "PNG" : "JPEG"
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
} catch (e) {
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
@@ -46,17 +75,26 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeEncrypted(filename, { data, type, key }) {
|
||||
console.log("file input: ", { type, key });
|
||||
export async function hashBase64(data) {
|
||||
const hash = await Sodium.hashFile({
|
||||
type: "base64",
|
||||
data,
|
||||
uri: ""
|
||||
});
|
||||
return {
|
||||
hash: hash,
|
||||
type: "xxh64"
|
||||
};
|
||||
}
|
||||
|
||||
export async function writeEncryptedBase64({ data, key }) {
|
||||
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||
console.log(filepath);
|
||||
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
|
||||
let output = await Sodium.encryptFile(key, {
|
||||
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
|
||||
type: "url"
|
||||
});
|
||||
RNFetchBlob.fs.unlink(filepath).catch(console.log);
|
||||
|
||||
console.log("encrypted file output: ", output);
|
||||
return {
|
||||
...output,
|
||||
|
||||
@@ -17,16 +17,20 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { cacheDir } from "./utils";
|
||||
import { isImage, isDocument } from "@notesnook/core/utils/filename";
|
||||
import { Platform } from "react-native";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
|
||||
export async function uploadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
let { url, headers } = data;
|
||||
|
||||
console.log("uploading file: ", filename, headers);
|
||||
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
|
||||
|
||||
try {
|
||||
let res = await fetch(url, {
|
||||
method: "PUT",
|
||||
@@ -34,10 +38,22 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
});
|
||||
if (!res.ok) throw new Error(`${res.status}: Unable to resolve upload url`);
|
||||
const uploadUrl = await res.text();
|
||||
if (!uploadUrl) throw new Error("Unable to resolve upload url");
|
||||
if (!uploadUrl) throw new Error("Unable to resolve attachment upload url");
|
||||
let uploadFilePath = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
uploadFilePath = appGroupPath;
|
||||
}
|
||||
DatabaseLogger.info(`Starting upload: ${filename}`);
|
||||
|
||||
let request = RNFetchBlob.config({
|
||||
IOSBackgroundTask: true
|
||||
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
|
||||
})
|
||||
.fetch(
|
||||
"PUT",
|
||||
@@ -45,14 +61,17 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
{
|
||||
"content-type": ""
|
||||
},
|
||||
RNFetchBlob.wrap(`${cacheDir}/${filename}`)
|
||||
RNFetchBlob.wrap(uploadFilePath)
|
||||
)
|
||||
.uploadProgress((sent, total) => {
|
||||
useAttachmentStore
|
||||
.getState()
|
||||
.setProgress(sent, total, filename, 0, "upload");
|
||||
console.log("uploading: ", sent, total);
|
||||
DatabaseLogger.info(
|
||||
`File upload progress: ${filename}, ${sent}/${total}`
|
||||
);
|
||||
});
|
||||
|
||||
cancelToken.cancel = request.cancel;
|
||||
let response = await request;
|
||||
|
||||
@@ -61,17 +80,31 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
let result = status >= 200 && status < 300 && text.length === 0;
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
if (result) {
|
||||
DatabaseLogger.info(
|
||||
`File upload status: ${filename}, ${status}, ${text}`
|
||||
);
|
||||
let attachment = db.attachments.attachment(filename);
|
||||
if (!attachment) return result;
|
||||
if (!attachment.metadata.type.startsWith("image/")) {
|
||||
if (
|
||||
!isImage(attachment.metadata.type) &&
|
||||
!isDocument(attachment.metadata?.type)
|
||||
) {
|
||||
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
|
||||
}
|
||||
} else {
|
||||
const fileInfo = await RNFetchBlob.fs.stat(uploadFilePath);
|
||||
throw new Error(
|
||||
`${status}, ${text}, name: ${fileInfo.filename}, length: ${
|
||||
fileInfo.size
|
||||
}, info: ${JSON.stringify(response.info())}`
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
console.log("upload file: ", e, url, headers);
|
||||
DatabaseLogger.info(`File upload error: ${filename}, ${e}`);
|
||||
DatabaseLogger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
|
||||
export const cacheDir = RNFetchBlob.fs.dirs.CacheDir;
|
||||
|
||||
@@ -62,3 +64,22 @@ export function cancelable(operation) {
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function copyFileAsync(source, dest) {
|
||||
return new Promise((resolve) => {
|
||||
ScopedStorage.copyFile(source, dest, (e, r) => {
|
||||
console.log(e, r);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function releasePermissions(path) {
|
||||
if (Platform.OS === "ios") return;
|
||||
const uris = await ScopedStorage.getPersistedUriPermissions();
|
||||
for (let uri of uris) {
|
||||
if (path.startsWith(uri)) {
|
||||
await ScopedStorage.releasePersistableUriPermission(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,18 +21,18 @@ import React from "react";
|
||||
import { FlatList, View } from "react-native";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useMessageStore } from "../../stores/use-message-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { allowedOnPlatform, renderItem } from "./functions";
|
||||
|
||||
export const Announcement = ({ color }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const { colors } = useThemeColors();
|
||||
const announcements = useMessageStore((state) => state.announcements);
|
||||
let announcement = announcements.length > 0 ? announcements[0] : null;
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
return !announcement || selectionMode ? null : (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.bg,
|
||||
backgroundColor: colors.primary.background,
|
||||
width: "100%",
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 12,
|
||||
@@ -44,28 +44,26 @@ export const Announcement = ({ color }) => {
|
||||
width: "100%",
|
||||
borderRadius: 10,
|
||||
overflow: "hidden",
|
||||
backgroundColor: colors.nav,
|
||||
backgroundColor: colors.secondary.background,
|
||||
paddingBottom: 12
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<FlatList
|
||||
style={{
|
||||
width: "100%",
|
||||
marginTop: 12
|
||||
}}
|
||||
data={announcement?.body.filter((item) =>
|
||||
allowedOnPlatform(item.platforms)
|
||||
)}
|
||||
renderItem={({ item, index }) =>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
marginTop: 12
|
||||
}}
|
||||
>
|
||||
{announcement?.body
|
||||
.filter((item) => allowedOnPlatform(item.platforms))
|
||||
.map((item, index) =>
|
||||
renderItem({
|
||||
item: item,
|
||||
index: index,
|
||||
color: colors[color],
|
||||
inline: true
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||