mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 10:16:26 +00:00
Compare commits
873 Commits
v0.8.18
...
update-iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58eeb90158 | ||
|
|
40d90a53b2 | ||
|
|
7c3aaff635 | ||
|
|
4bb7929229 | ||
|
|
bdd5b7b3a7 | ||
|
|
a2ddb56540 | ||
|
|
7c0097951c | ||
|
|
c42e1a0ab4 | ||
|
|
720475dae5 | ||
|
|
259df880ba | ||
|
|
3bc21cdb09 | ||
|
|
9e8ef70510 | ||
|
|
5fd822a24d | ||
|
|
8ee4dbbb5c | ||
|
|
9522a4d5d9 | ||
|
|
eefecdefbe | ||
|
|
84e670e71c | ||
|
|
c22b69234f | ||
|
|
214ed388f2 | ||
|
|
0bd3445370 | ||
|
|
002dc9b017 | ||
|
|
ebf5a03f56 | ||
|
|
2797faafe5 | ||
|
|
84ac739993 | ||
|
|
13c37f046f | ||
|
|
2631cc3747 | ||
|
|
5873e8e896 | ||
|
|
6dc633c2a1 | ||
|
|
8b07126285 | ||
|
|
93a120543a | ||
|
|
af0aa4a567 | ||
|
|
58fd1f0c46 | ||
|
|
030041932e | ||
|
|
8dbf456398 | ||
|
|
3a90a078ce | ||
|
|
c30957fc9f | ||
|
|
f6db946c9a | ||
|
|
c6c0d4c62a | ||
|
|
57befc4ccb | ||
|
|
e716af75ed | ||
|
|
efe2bea64b | ||
|
|
9b926326ef | ||
|
|
de71033fe2 | ||
|
|
2c10bf251d | ||
|
|
8af50aa5bd | ||
|
|
05bedfe3d4 | ||
|
|
a1929dac8a | ||
|
|
834ecc643a | ||
|
|
60baabf7e7 | ||
|
|
185a149d74 | ||
|
|
5d62dd2002 | ||
|
|
9eaa6b5cec | ||
|
|
6ff03bbb95 | ||
|
|
9fac6bca64 | ||
|
|
88856b788a | ||
|
|
eda4e46d9f | ||
|
|
35bcbbbae4 | ||
|
|
22e2c3da1f | ||
|
|
b526d48946 | ||
|
|
91a95b7c20 | ||
|
|
d634e1124a | ||
|
|
309e159df1 | ||
|
|
ffae53326a | ||
|
|
ddd1522e19 | ||
|
|
4bc0cccb24 | ||
|
|
72fbefa300 | ||
|
|
30378eeb50 | ||
|
|
03293c0d25 | ||
|
|
2e3f6e39f6 | ||
|
|
e4d4041c6b | ||
|
|
166a5c10a7 | ||
|
|
1fec81cc3e | ||
|
|
9c247bcb22 | ||
|
|
fc88a49acc | ||
|
|
8ccf490e9b | ||
|
|
ea768f982e | ||
|
|
172ea82954 | ||
|
|
10500c3c1c | ||
|
|
225916fbba | ||
|
|
2fce78422b | ||
|
|
8132dd6847 | ||
|
|
4caee1e103 | ||
|
|
ca0b03e97c | ||
|
|
f03178bb8d | ||
|
|
d083a86138 | ||
|
|
8216b992ea | ||
|
|
8e74ee7dde | ||
|
|
b207fe14df | ||
|
|
92cfa21be6 | ||
|
|
5fd482428a | ||
|
|
98b09f7edc | ||
|
|
7b83a34777 | ||
|
|
aeb63ec901 | ||
|
|
436093e0b6 | ||
|
|
d7ee06ce6d | ||
|
|
e72003009d | ||
|
|
cfe8235a36 | ||
|
|
8c1ac9c5b3 | ||
|
|
77e3a7d43a | ||
|
|
53728a0f4a | ||
|
|
06f33d9a63 | ||
|
|
ed2698ecc3 | ||
|
|
0cb5554ae5 | ||
|
|
89850c0b22 | ||
|
|
d78b94f4e8 | ||
|
|
c2c50817f1 | ||
|
|
680c2a2904 | ||
|
|
a1085e3863 | ||
|
|
37f6a05170 | ||
|
|
0d296c3b25 | ||
|
|
73caa2508e | ||
|
|
69e012a6f0 | ||
|
|
21251a1915 | ||
|
|
67143ba2d5 | ||
|
|
d399cba4c0 | ||
|
|
9cad7cd025 | ||
|
|
a593842265 | ||
|
|
2f4eb595f6 | ||
|
|
bfcf349ffe | ||
|
|
2bd78cd47f | ||
|
|
713615e28b | ||
|
|
2b2f17525e | ||
|
|
cd6233a3d7 | ||
|
|
f76224bd17 | ||
|
|
8afa373726 | ||
|
|
199f2202e0 | ||
|
|
86a6311f75 | ||
|
|
0ca4e6ca4f | ||
|
|
50bce4892f | ||
|
|
ca345cf008 | ||
|
|
e0e1290fae | ||
|
|
a84b2611e4 | ||
|
|
ec31fab344 | ||
|
|
1b96eee4de | ||
|
|
a6af5de3e1 | ||
|
|
52b3068330 | ||
|
|
0934c08dfe | ||
|
|
f0428fde66 | ||
|
|
b3f57a67c4 | ||
|
|
8bc2b1262b | ||
|
|
fb24efd3de | ||
|
|
ce594b0b5a | ||
|
|
7b39ab4ec4 | ||
|
|
f717ed9f66 | ||
|
|
d3091a5384 | ||
|
|
2a6d950a4b | ||
|
|
b03b9d5334 | ||
|
|
c9cb31bd02 | ||
|
|
905d6860fc | ||
|
|
6f52744b0f | ||
|
|
007d3e52c5 | ||
|
|
f10fa632ca | ||
|
|
266323b90b | ||
|
|
60707a8f45 | ||
|
|
b89896a4e7 | ||
|
|
d69fd12fb9 | ||
|
|
0bdcfa6028 | ||
|
|
55b8488901 | ||
|
|
8ddbf2067b | ||
|
|
fa5cebda6d | ||
|
|
4fd01b4234 | ||
|
|
0f354f4f06 | ||
|
|
f94a197828 | ||
|
|
a26ff660b0 | ||
|
|
50cc648799 | ||
|
|
a7946805ae | ||
|
|
f3b2969b42 | ||
|
|
d6c3490165 | ||
|
|
6ee594d4d1 | ||
|
|
7e1596de30 | ||
|
|
67bba043ed | ||
|
|
26d7f4923d | ||
|
|
e9218d1088 | ||
|
|
03fd1e29e3 | ||
|
|
ff59af6b51 | ||
|
|
73ba8b8b13 | ||
|
|
ffc3fb770c | ||
|
|
d58ea70a95 | ||
|
|
90e8dd038d | ||
|
|
56d1e3edaa | ||
|
|
83a9e54896 | ||
|
|
ab038b1f31 | ||
|
|
f5a9d3928c | ||
|
|
2bc0bce1b5 | ||
|
|
372933fd99 | ||
|
|
9112347e95 | ||
|
|
30548a68e4 | ||
|
|
ce052d1691 | ||
|
|
765ba8c867 | ||
|
|
a20c0cd49e | ||
|
|
e06ca9a056 | ||
|
|
d04048c749 | ||
|
|
2d0f7589ea | ||
|
|
dc60be404a | ||
|
|
9ff5cc51f9 | ||
|
|
e9de8f42e5 | ||
|
|
5bd0499ae4 | ||
|
|
99e706bcd2 | ||
|
|
239edb0605 | ||
|
|
bf3f5a5971 | ||
|
|
92be3f32d6 | ||
|
|
106f5a53ff | ||
|
|
8c43f3d567 | ||
|
|
2e09501c8a | ||
|
|
a2592e48c8 | ||
|
|
291d76674b | ||
|
|
78957cf128 | ||
|
|
e88694b049 | ||
|
|
5e7bdf7354 | ||
|
|
2e3e0bc1d8 | ||
|
|
b33e6b232c | ||
|
|
df6b083670 | ||
|
|
05009d40c4 | ||
|
|
ea27a3b449 | ||
|
|
2831b0bd2a | ||
|
|
32e22dd507 | ||
|
|
2ee9951853 | ||
|
|
b1912135ed | ||
|
|
bed3d42923 | ||
|
|
c4ec69a43f | ||
|
|
24b004bb2d | ||
|
|
84925b24b5 | ||
|
|
c02b91dfd4 | ||
|
|
066d97220f | ||
|
|
61ed47dda0 | ||
|
|
68c0f210cc | ||
|
|
6c542750f4 | ||
|
|
25440a26ee | ||
|
|
ab393b1f6d | ||
|
|
e643147b69 | ||
|
|
6c4aa71cbc | ||
|
|
9930ba8748 | ||
|
|
fbb8b4687b | ||
|
|
01b1c49738 | ||
|
|
c9c28eda1b | ||
|
|
744bcba599 | ||
|
|
d76db726c4 | ||
|
|
71ec528a87 | ||
|
|
fbbc93900e | ||
|
|
33b45737c9 | ||
|
|
4a55f78a48 | ||
|
|
a82a79e25c | ||
|
|
6ec2124a9c | ||
|
|
1f1ef1440e | ||
|
|
a7d0a4bdac | ||
|
|
d2129ffac6 | ||
|
|
a4782f0663 | ||
|
|
16794b9d78 | ||
|
|
a76aed2d4e | ||
|
|
d2163dacf9 | ||
|
|
158305346f | ||
|
|
e692432242 | ||
|
|
a7b85b123e | ||
|
|
ddcd722598 | ||
|
|
358458a937 | ||
|
|
8925f7c381 | ||
|
|
ff2e39901a | ||
|
|
90ff0f43ea | ||
|
|
442c352c8d | ||
|
|
e91b7fb082 | ||
|
|
88de66a31f | ||
|
|
d3b3e45800 | ||
|
|
50d2f90621 | ||
|
|
8ccf6cb8a3 | ||
|
|
2e9b478824 | ||
|
|
89b2d54725 | ||
|
|
3d0af2d8ca | ||
|
|
813b433f4d | ||
|
|
0bce96b0c6 | ||
|
|
1d4f1764fc | ||
|
|
2994420160 | ||
|
|
65d8d7282f | ||
|
|
47af3f09fc | ||
|
|
f4024f4683 | ||
|
|
ee0ed6df7a | ||
|
|
d3fbba3572 | ||
|
|
4a6b22f5b7 | ||
|
|
d070305002 | ||
|
|
a8500150b0 | ||
|
|
02fb1d01ad | ||
|
|
497dee038f | ||
|
|
a4af77f91e | ||
|
|
f2a4e1d230 | ||
|
|
8560901f80 | ||
|
|
daea604c60 | ||
|
|
7aedb59f26 | ||
|
|
0be1c2f464 | ||
|
|
0dbfaf0e79 | ||
|
|
4147399cda | ||
|
|
cc667ac738 | ||
|
|
022915ffc9 | ||
|
|
4b9cf775ff | ||
|
|
56879924bd | ||
|
|
6142f2d641 | ||
|
|
0d53f799b7 | ||
|
|
65e77e9669 | ||
|
|
2c8f3b56ae | ||
|
|
8ab190affc | ||
|
|
eafccc4fc4 | ||
|
|
ce440351a5 | ||
|
|
be94edde0f | ||
|
|
4fcc9af933 | ||
|
|
e2b4ac6ee8 | ||
|
|
c151049cc2 | ||
|
|
ac778d3f65 | ||
|
|
1aed0cb4b9 | ||
|
|
5836b65aad | ||
|
|
46f750efba | ||
|
|
b33c9e23ce | ||
|
|
14694f1cb0 | ||
|
|
75575348cd | ||
|
|
f6ad0a235c | ||
|
|
bbf6c60888 | ||
|
|
f5915f3e10 | ||
|
|
a32cfc8aff | ||
|
|
c90a878c9e | ||
|
|
b46b958105 | ||
|
|
6943b06a88 | ||
|
|
27a9def88c | ||
|
|
11f8cfe0e6 | ||
|
|
e1e3cc7999 | ||
|
|
254c8816f1 | ||
|
|
9a445e34fd | ||
|
|
ee78e113de | ||
|
|
0dfb14962a | ||
|
|
d493df4295 | ||
|
|
f0144233f9 | ||
|
|
90f21f4ed1 | ||
|
|
646ebe592e | ||
|
|
b098a15e9c | ||
|
|
4f98995fe4 | ||
|
|
56231edc3a | ||
|
|
871ab428c2 | ||
|
|
a9b75f752e | ||
|
|
9590559b81 | ||
|
|
24bd2eddf1 | ||
|
|
f4ba06c401 | ||
|
|
cd405d1df9 | ||
|
|
9d6dbc1a6f | ||
|
|
6d57712fca | ||
|
|
191f2cacbf | ||
|
|
080448af3a | ||
|
|
6e2272d043 | ||
|
|
71078dea4f | ||
|
|
333f0be879 | ||
|
|
3b0f664a3b | ||
|
|
2a23d19321 | ||
|
|
5ee4237510 | ||
|
|
bdb906c26d | ||
|
|
02095ac155 | ||
|
|
80d1ca81ac | ||
|
|
3bbabbc80b | ||
|
|
f8ff3d4bf5 | ||
|
|
3a40f9ebd6 | ||
|
|
cf776088e6 | ||
|
|
7ab81608e8 | ||
|
|
017e40fcb9 | ||
|
|
29309fbaa3 | ||
|
|
b766b08bf5 | ||
|
|
e796e00963 | ||
|
|
10136df977 | ||
|
|
44ec107ce8 | ||
|
|
c7fb5b0475 | ||
|
|
785735ccf5 | ||
|
|
f0736ccf8d | ||
|
|
a24a7e03be | ||
|
|
be235e5204 | ||
|
|
57ec598672 | ||
|
|
2627c09cda | ||
|
|
36e63bb8a9 | ||
|
|
5ea24f650b | ||
|
|
8c792ce7a1 | ||
|
|
055969f5c6 | ||
|
|
145ae10a79 | ||
|
|
bdb9349b52 | ||
|
|
8b11b57ec5 | ||
|
|
29888c89ad | ||
|
|
281fb2afd3 | ||
|
|
fbb7839f83 | ||
|
|
d3f9c170ac | ||
|
|
4f9a0b0040 | ||
|
|
aae584106a | ||
|
|
2a784deb4b | ||
|
|
5f5a7880a6 | ||
|
|
ab8b6d806d | ||
|
|
6c4206a2dd | ||
|
|
0d61d9cee4 | ||
|
|
48bdb9e818 | ||
|
|
cfe447a403 | ||
|
|
2c30f0e487 | ||
|
|
147211478c | ||
|
|
35b7674e69 | ||
|
|
55075b2a96 | ||
|
|
6577599959 | ||
|
|
0c4e72e507 | ||
|
|
9bf96e943d | ||
|
|
2811843e70 | ||
|
|
842ece2a8a | ||
|
|
222de03ea3 | ||
|
|
d31ae9f3fc | ||
|
|
1dd7644e12 | ||
|
|
2cee54f70a | ||
|
|
a8431fae96 | ||
|
|
03d11b7b58 | ||
|
|
316e2eeefb | ||
|
|
c21e19337a | ||
|
|
b9cab0dae8 | ||
|
|
33b3299ca2 | ||
|
|
00ba38beba | ||
|
|
433fce286e | ||
|
|
b36322bba4 | ||
|
|
c147e0a789 | ||
|
|
8bf5d02624 | ||
|
|
35616c1ccd | ||
|
|
7c8939ecb8 | ||
|
|
ccb0302d3f | ||
|
|
0cfd048013 | ||
|
|
f72b4f0249 | ||
|
|
b5cb209f14 | ||
|
|
1af374439d | ||
|
|
bbcd674516 | ||
|
|
3cba71b7a8 | ||
|
|
6a1e9c5818 | ||
|
|
ede41d01b4 | ||
|
|
826a67b550 | ||
|
|
f3d59a9b61 | ||
|
|
65434453b8 | ||
|
|
b8658abbad | ||
|
|
0387b33f6c | ||
|
|
41042fe64e | ||
|
|
1efc3bb15d | ||
|
|
0816a9410b | ||
|
|
cb823eaaa6 | ||
|
|
44f027bf18 | ||
|
|
571d159a1f | ||
|
|
a30d977727 | ||
|
|
847ad2d781 | ||
|
|
da9067a672 | ||
|
|
5ed5950ed5 | ||
|
|
f53c182cfb | ||
|
|
bb2978b370 | ||
|
|
66ae4a5163 | ||
|
|
f435595ad8 | ||
|
|
4bec4596fe | ||
|
|
10b86aec49 | ||
|
|
ecabd37cbb | ||
|
|
f3cabfcb4d | ||
|
|
46fc010cf9 | ||
|
|
e370c76521 | ||
|
|
1c283f88d0 | ||
|
|
cfffa1b10a | ||
|
|
8aaae4de49 | ||
|
|
4f6c35713e | ||
|
|
8fb0b5b572 | ||
|
|
12262671ee | ||
|
|
3165c62985 | ||
|
|
1ca1166f74 | ||
|
|
7b4d2f3b97 | ||
|
|
ccce75a047 | ||
|
|
410e82e375 | ||
|
|
bd385ec062 | ||
|
|
638227ef25 | ||
|
|
e70b35126b | ||
|
|
4d41c7f37f | ||
|
|
d01a7b16a1 | ||
|
|
326d873279 | ||
|
|
74c32bed29 | ||
|
|
a222fd0786 | ||
|
|
c633ba9497 | ||
|
|
40b5472866 | ||
|
|
a413e273ca | ||
|
|
0f82085cae | ||
|
|
fa1ab04409 | ||
|
|
f370508c93 | ||
|
|
7472019422 | ||
|
|
aa3597881a | ||
|
|
a36841e501 | ||
|
|
fe9afc8952 | ||
|
|
63d39a81aa | ||
|
|
b11dc2ca20 | ||
|
|
10f5ad6127 | ||
|
|
4800a88cf6 | ||
|
|
9d8bc40594 | ||
|
|
7d3d96a6e0 | ||
|
|
4c99429d76 | ||
|
|
96fbd7572c | ||
|
|
b012bba6d6 | ||
|
|
45d90b2530 | ||
|
|
a8b946a07a | ||
|
|
18392c4f23 | ||
|
|
88e7869284 | ||
|
|
3c8332c448 | ||
|
|
207f95693a | ||
|
|
de15120bf8 | ||
|
|
1dcf11e1c4 | ||
|
|
b74ba22c44 | ||
|
|
fa2d34dcfc | ||
|
|
0280a5f09e | ||
|
|
a35f876f4f | ||
|
|
c22b7f81c1 | ||
|
|
9344fd78d8 | ||
|
|
e89c0a3e61 | ||
|
|
5aa7ef5738 | ||
|
|
88ac6c6fc6 | ||
|
|
a537f9762b | ||
|
|
aab192223a | ||
|
|
6439810d03 | ||
|
|
8ae7d96cc7 | ||
|
|
c5c78763d1 | ||
|
|
012908e32d | ||
|
|
1f1e545538 | ||
|
|
5b8519b2b8 | ||
|
|
6502158716 | ||
|
|
7c525ab7a6 | ||
|
|
778e3d9799 | ||
|
|
e5f986a959 | ||
|
|
d76ecd5423 | ||
|
|
6bcb6398f8 | ||
|
|
6c341d8fa5 | ||
|
|
0d5e57eeab | ||
|
|
aef603ed8c | ||
|
|
97600e526b | ||
|
|
f3f6095d81 | ||
|
|
fb7280127c | ||
|
|
60df509fc6 | ||
|
|
ced237fbda | ||
|
|
9473a26892 | ||
|
|
e5825e898d | ||
|
|
8d59bd9f71 | ||
|
|
8f90bb83e7 | ||
|
|
ba888a7165 | ||
|
|
9591a7cac2 | ||
|
|
e8fd53d8b7 | ||
|
|
3b7c36b1c9 | ||
|
|
2750a3ef30 | ||
|
|
7d59f51244 | ||
|
|
a3ec6f470a | ||
|
|
8e85a33dac | ||
|
|
77542597f5 | ||
|
|
2a26e8a010 | ||
|
|
8b4c1a6c38 | ||
|
|
7d7e277cc6 | ||
|
|
cb1da609a4 | ||
|
|
e4fbdb35a3 | ||
|
|
c3586372e0 | ||
|
|
808d193543 | ||
|
|
0b65b70af2 | ||
|
|
deab34abd6 | ||
|
|
646cded650 | ||
|
|
9047320301 | ||
|
|
f16b054f01 | ||
|
|
fea856202d | ||
|
|
4f15cc3f08 | ||
|
|
1769dd959e | ||
|
|
1253b81a01 | ||
|
|
3b524f6aba | ||
|
|
051ccad29a | ||
|
|
c82eba05d1 | ||
|
|
74af199afc | ||
|
|
c2afdba659 | ||
|
|
e6ae45f133 | ||
|
|
129ef6766b | ||
|
|
3194a7b1a2 | ||
|
|
46a733ba5b | ||
|
|
466844fc55 | ||
|
|
0cc793e3fe | ||
|
|
0fdfb385a4 | ||
|
|
419a4c6b2d | ||
|
|
55e9441547 | ||
|
|
7abff6ded4 | ||
|
|
a648d310a5 | ||
|
|
4b56fa56b5 | ||
|
|
18bf700936 | ||
|
|
67ddff736c | ||
|
|
1bba125a2a | ||
|
|
187acad592 | ||
|
|
5e07c7b3e1 | ||
|
|
cd942cf8b6 | ||
|
|
c43589fe6a | ||
|
|
d4594eff3b | ||
|
|
809a0e846b | ||
|
|
7f00ce2598 | ||
|
|
d7d77dbfe9 | ||
|
|
7a124c74cc | ||
|
|
f8549f4643 | ||
|
|
0476ff70da | ||
|
|
5ec541c3c1 | ||
|
|
7b920348f3 | ||
|
|
51b1ef41a1 | ||
|
|
12447effc9 | ||
|
|
dc1b059a9d | ||
|
|
a676ebf46c | ||
|
|
338f9fb5a9 | ||
|
|
98c9132d4a | ||
|
|
a054f12492 | ||
|
|
c42ac1df1b | ||
|
|
d9d46cda1c | ||
|
|
f678a17505 | ||
|
|
3da4bb69ce | ||
|
|
3e2b876c59 | ||
|
|
10daf2599d | ||
|
|
56d6eb7077 | ||
|
|
9b54272f8e | ||
|
|
685e003db8 | ||
|
|
701fc0bc80 | ||
|
|
e5518ac0fa | ||
|
|
8e1bf48cd1 | ||
|
|
8dd82e1a3b | ||
|
|
4418bfe965 | ||
|
|
39c4d710bc | ||
|
|
51a8c47afd | ||
|
|
922570bb5c | ||
|
|
ca282d5635 | ||
|
|
ff67043210 | ||
|
|
31da231c1c | ||
|
|
eb698a7430 | ||
|
|
03be809ba9 | ||
|
|
69601bf15a | ||
|
|
7cad3d403b | ||
|
|
cc84af3346 | ||
|
|
33ef54a162 | ||
|
|
ac43ff886a | ||
|
|
a64f73ca0c | ||
|
|
314477d2fc | ||
|
|
c63fc93daa | ||
|
|
f04ad1e702 | ||
|
|
2183c4bda6 | ||
|
|
6ba91c1515 | ||
|
|
dc03bb76e6 | ||
|
|
d0559c16b5 | ||
|
|
eb693f7b48 | ||
|
|
ef5639ff4b | ||
|
|
34a335797c | ||
|
|
edda3a4d23 | ||
|
|
184839423f | ||
|
|
c0f3600a52 | ||
|
|
2b1302aa07 | ||
|
|
d894bb4aab | ||
|
|
470c071344 | ||
|
|
4bd639c6c4 | ||
|
|
4cb7e63421 | ||
|
|
9f14a503d8 | ||
|
|
d5da6de86c | ||
|
|
4c2b233722 | ||
|
|
ca5b1eea13 | ||
|
|
614e9b6d55 | ||
|
|
27b2530b8d | ||
|
|
2259167200 | ||
|
|
2e05214828 | ||
|
|
cfb996039b | ||
|
|
44c4d56214 | ||
|
|
8e6be91f7c | ||
|
|
00816fb2c8 | ||
|
|
535356b77f | ||
|
|
5e558746ce | ||
|
|
f7bd52ac0c | ||
|
|
9165f518a9 | ||
|
|
01605aa221 | ||
|
|
8b0b29c424 | ||
|
|
7a116966fa | ||
|
|
e7e8f11a74 | ||
|
|
f235d832d5 | ||
|
|
7730b5e20b | ||
|
|
8c3ba4ce48 | ||
|
|
e9a126f586 | ||
|
|
097e7d2ff2 | ||
|
|
81265f1238 | ||
|
|
2b507e6e20 | ||
|
|
747d3a8f13 | ||
|
|
30f6f07434 | ||
|
|
6de5488a15 | ||
|
|
5413647166 | ||
|
|
e83fe73b18 | ||
|
|
87a289ec65 | ||
|
|
8a0a118dba | ||
|
|
687126ce87 | ||
|
|
8a05d577da | ||
|
|
4c3ebfc0f8 | ||
|
|
6093f25f9a | ||
|
|
ecab68d676 | ||
|
|
1cb4f37c95 | ||
|
|
14318528b9 | ||
|
|
9c0e1f8f1a | ||
|
|
2034ce9e4d | ||
|
|
657489caf6 | ||
|
|
94be3d1fe5 | ||
|
|
f6eae41cee | ||
|
|
69c64434e3 | ||
|
|
256cabfce1 | ||
|
|
e8b8272cf9 | ||
|
|
bd5ab4881c | ||
|
|
9630744bdb | ||
|
|
ab3ad0eb97 | ||
|
|
2393889028 | ||
|
|
c36ecb1ed1 | ||
|
|
3e9b28ff0c | ||
|
|
d4eec461a9 | ||
|
|
b584f37087 | ||
|
|
5c75644db2 | ||
|
|
72d9e3e00b | ||
|
|
b3d3362f34 | ||
|
|
1cbaf55cee | ||
|
|
7771875d57 | ||
|
|
85937d8e23 | ||
|
|
1480986a3a | ||
|
|
bc968736df | ||
|
|
ad80b8e8b6 | ||
|
|
d44d220c55 | ||
|
|
f6bcef0789 | ||
|
|
a28ec752e8 | ||
|
|
48ea5746d9 | ||
|
|
f473d31970 | ||
|
|
9cd1d4b466 | ||
|
|
4f02065eaf | ||
|
|
4b85e3e8fb | ||
|
|
e4e8c2205e | ||
|
|
18649dd074 | ||
|
|
9f9463f0e8 | ||
|
|
6cf9bc5de2 | ||
|
|
297b4346c5 | ||
|
|
767a203439 | ||
|
|
c564c253b1 | ||
|
|
b4e138e21b | ||
|
|
8ca01921c4 | ||
|
|
c8b97ffde3 | ||
|
|
abc84e5710 | ||
|
|
d732d195f3 | ||
|
|
789975abb0 | ||
|
|
ed1eab7fcc | ||
|
|
29b31c114a | ||
|
|
c8cf353c21 | ||
|
|
e82e2c71f1 | ||
|
|
dd729c406f | ||
|
|
3127e85900 | ||
|
|
304eeb3158 | ||
|
|
bf781c6b50 | ||
|
|
da1098e441 | ||
|
|
85065357e2 | ||
|
|
1f5f6c3b0e | ||
|
|
4f7026969f | ||
|
|
16d264ebfa | ||
|
|
04ffbe945b | ||
|
|
974a1a1e7d | ||
|
|
ca2c07244f | ||
|
|
f90786d1c0 | ||
|
|
bdf55568c7 | ||
|
|
e262d2f19b | ||
|
|
aabfe820ac | ||
|
|
3bba5442bd | ||
|
|
6ce1922fb3 | ||
|
|
9367a404ef | ||
|
|
7c8e19c681 | ||
|
|
7ccc5eb9b8 | ||
|
|
b4b6d3d07c | ||
|
|
9007bac7b2 | ||
|
|
df13ca3c92 | ||
|
|
d86935acaa | ||
|
|
72b450d526 | ||
|
|
4b7afeeb4f | ||
|
|
0e0e779cbe | ||
|
|
89b2f48a06 | ||
|
|
c80bdb8d0c | ||
|
|
50d89a8ec9 | ||
|
|
f5ccaa7b48 | ||
|
|
e682ee8541 | ||
|
|
caaa7a9e74 | ||
|
|
6ef0e325e2 | ||
|
|
ea064deeb8 | ||
|
|
8e81609a39 | ||
|
|
83da07a941 | ||
|
|
977e80c829 | ||
|
|
8ba0d10f40 | ||
|
|
2581091652 | ||
|
|
e72a7ceaea | ||
|
|
a17ddf6d54 | ||
|
|
b5e2d21f33 | ||
|
|
d09f8dff18 | ||
|
|
bdb3406dcb | ||
|
|
f54b49db1a | ||
|
|
0cc9f006c5 | ||
|
|
db1398b65f | ||
|
|
e8192e6c3f | ||
|
|
775200bdd6 | ||
|
|
820a2a093c | ||
|
|
d3995b9b10 | ||
|
|
f6867f9338 | ||
|
|
3e2548fcd5 | ||
|
|
745d250787 | ||
|
|
b1063eb38f | ||
|
|
9032a1debb | ||
|
|
795fe8ae1d | ||
|
|
e5a908af68 | ||
|
|
6ce16c1cc0 | ||
|
|
6ff2a5ac94 | ||
|
|
fcea16e43a | ||
|
|
7b8e42382e | ||
|
|
a372b5ea39 | ||
|
|
1aafee2a7c | ||
|
|
7afe3d5181 | ||
|
|
6fba62d062 | ||
|
|
6906c0ab0d | ||
|
|
5d46adf8fd | ||
|
|
eda1f11d42 | ||
|
|
6431a8255d | ||
|
|
48fd1d11e2 | ||
|
|
4c3e62efad | ||
|
|
52a15a5d92 | ||
|
|
57f4aa5995 | ||
|
|
ab9ab004b7 | ||
|
|
ac624eb98f | ||
|
|
1a4c37820d | ||
|
|
685206950b | ||
|
|
eececf8a93 | ||
|
|
9bc3d65554 | ||
|
|
f9b854ce39 | ||
|
|
1416968fe5 | ||
|
|
efc183c709 | ||
|
|
07a2442718 | ||
|
|
f549c50a58 | ||
|
|
8d6ce1a2f7 | ||
|
|
c5b33e025e | ||
|
|
4cdfc738c0 | ||
|
|
46d46f21e4 | ||
|
|
4983605b23 | ||
|
|
9e8d04d806 | ||
|
|
042f935133 | ||
|
|
ed9d3639e2 | ||
|
|
728f105830 | ||
|
|
6f359fa6a8 | ||
|
|
57a88743bc | ||
|
|
667f022086 | ||
|
|
b742a3a4cd | ||
|
|
8d5c9742f8 | ||
|
|
c2a49efe73 | ||
|
|
8c8a0ab46d | ||
|
|
959b75bddd | ||
|
|
d29d5105f1 | ||
|
|
38e82872a5 | ||
|
|
15d9ce1d36 | ||
|
|
67f7cdb36c | ||
|
|
6a9d4ae0fd | ||
|
|
6a761c3fb5 | ||
|
|
3baf97e69f | ||
|
|
694dc60481 | ||
|
|
e3c6f0452c | ||
|
|
ed2401a87b | ||
|
|
cb59458c79 | ||
|
|
125a493400 | ||
|
|
83910b55d2 | ||
|
|
f4fd131100 | ||
|
|
cfdc880d8c | ||
|
|
7303e8bdd2 | ||
|
|
ecde465d9f | ||
|
|
5c5e70a805 | ||
|
|
4e41d9be55 | ||
|
|
d06d94449c | ||
|
|
1af2c83c63 | ||
|
|
6c75136777 | ||
|
|
31351e34e1 | ||
|
|
a058a774e9 | ||
|
|
e6db28485c | ||
|
|
391bb096d6 | ||
|
|
2b496dc2e5 | ||
|
|
a6e0b30576 | ||
|
|
1ac31264b7 | ||
|
|
ca4b8224fd | ||
|
|
d1e5781c24 | ||
|
|
c86e451198 | ||
|
|
b4a7b547f0 | ||
|
|
07b395c24a | ||
|
|
53b9630fa5 | ||
|
|
f820c3089e | ||
|
|
7a4258bb20 |
2
.babelrc
2
.babelrc
@@ -5,7 +5,7 @@
|
||||
"presets": ["react-hmre"]
|
||||
},
|
||||
"test": {
|
||||
"presets": ["react", "es2015"],
|
||||
"presets": ["env" ,"react", "es2015"],
|
||||
"plugins": [
|
||||
[ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ]
|
||||
]
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"theme": "monokai"
|
||||
},
|
||||
"hotkey": {
|
||||
"toggleFinder": "Cmd + Alt + S",
|
||||
"toggleMain": "Cmd + Alt + L"
|
||||
},
|
||||
"isSideNavFolded": false,
|
||||
@@ -24,6 +23,7 @@
|
||||
"lineNumber": true
|
||||
},
|
||||
"sortBy": "UPDATED_AT",
|
||||
"sortTagsBy": "ALPHABETICAL",
|
||||
"ui": {
|
||||
"defaultNote": "ALWAYS_ASK",
|
||||
"disableDirectWrite": false,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
compiled/
|
||||
dist/
|
||||
extra_scripts/
|
||||
@@ -3,7 +3,9 @@
|
||||
"plugins": ["react"],
|
||||
"rules": {
|
||||
"no-useless-escape": 0,
|
||||
"prefer-const": "warn",
|
||||
"prefer-const": ["warn", {
|
||||
"destructuring": "all"
|
||||
}],
|
||||
"no-unused-vars": "warn",
|
||||
"no-undef": "warn",
|
||||
"no-lone-blocks": "warn",
|
||||
@@ -17,5 +19,8 @@
|
||||
"FileReader": true,
|
||||
"localStorage": true,
|
||||
"fetch": true
|
||||
},
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
}
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,3 +8,5 @@ node_modules/*
|
||||
/compiled
|
||||
/secret
|
||||
*.log
|
||||
.idea
|
||||
.vscode
|
||||
@@ -1,9 +1,9 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- stable
|
||||
- lts/*
|
||||
- 7
|
||||
script:
|
||||
- npm run lint && npm run test
|
||||
- yarn jest
|
||||
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@5.2 && grunt pre-build; fi'
|
||||
after_success:
|
||||
- openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
# Current behavior
|
||||
|
||||
<!--
|
||||
Please paste some **screenshots** with the **developer tool** open (console tab) when you report a bug.
|
||||
|
||||
If your issue is regarding boostnote mobile, move to https://github.com/BoostIO/boostnote-mobile.
|
||||
-->
|
||||
|
||||
# Expected behavior
|
||||
|
||||
# Steps to reproduce
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
# Environment
|
||||
|
||||
- Version :
|
||||
- OS Version and name :
|
||||
|
||||
<!--
|
||||
Love Boostnote? Please consider supporting us via OpenCollective:
|
||||
👉 https://opencollective.com/boostnoteio
|
||||
Love Boostnote? Please consider supporting us on IssueHunt:
|
||||
👉 https://issuehunt.io/repos/53266139
|
||||
-->
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -2,7 +2,7 @@ GPL-3.0
|
||||
|
||||
Boostnote - an open source note-taking app made for programmers just like you.
|
||||
|
||||
Copyright (C) 2017 Maisin&Co., Inc.
|
||||
Copyright (C) 2017 - 2018 BoostIO
|
||||
|
||||
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
|
||||
|
||||
7
__mocks__/electron.js
Normal file
7
__mocks__/electron.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
require: jest.genMockFunction(),
|
||||
match: jest.genMockFunction(),
|
||||
app: jest.genMockFunction(),
|
||||
remote: jest.genMockFunction(),
|
||||
dialog: jest.genMockFunction()
|
||||
}
|
||||
@@ -2,36 +2,33 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import _ from 'lodash'
|
||||
import CodeMirror from 'codemirror'
|
||||
import path from 'path'
|
||||
import copyImage from 'browser/main/lib/dataApi/copyImage'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import 'codemirror-mode-elixir'
|
||||
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
|
||||
import convertModeName from 'browser/lib/convertModeName'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import iconv from 'iconv-lite'
|
||||
import crypto from 'crypto'
|
||||
import consts from 'browser/lib/consts'
|
||||
import fs from 'fs'
|
||||
const { ipcRenderer } = require('electron')
|
||||
|
||||
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
||||
|
||||
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||
|
||||
function pass (name) {
|
||||
switch (name) {
|
||||
case 'ejs':
|
||||
return 'Embedded Javascript'
|
||||
case 'html_ruby':
|
||||
return 'Embedded Ruby'
|
||||
case 'objectivec':
|
||||
return 'Objective C'
|
||||
case 'text':
|
||||
return 'Plain Text'
|
||||
default:
|
||||
return name
|
||||
}
|
||||
}
|
||||
const buildCMRulers = (rulers, enableRulers) =>
|
||||
enableRulers ? rulers.map(ruler => ({column: ruler})) : []
|
||||
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
||||
this.changeHandler = (e) => this.handleChange(e)
|
||||
this.focusHandler = () => {
|
||||
ipcRenderer.send('editor:focused', true)
|
||||
}
|
||||
this.blurHandler = (editor, e) => {
|
||||
ipcRenderer.send('editor:focused', false)
|
||||
if (e == null) return null
|
||||
let el = e.relatedTarget
|
||||
while (el != null) {
|
||||
@@ -41,34 +38,92 @@ export default class CodeEditor extends React.Component {
|
||||
el = el.parentNode
|
||||
}
|
||||
this.props.onBlur != null && this.props.onBlur(e)
|
||||
|
||||
const {storageKey, noteKey} = this.props
|
||||
attachmentManagement.deleteAttachmentsNotPresentInNote(this.editor.getValue(), storageKey, noteKey)
|
||||
}
|
||||
this.pasteHandler = (editor, e) => this.handlePaste(editor, e)
|
||||
this.loadStyleHandler = (e) => {
|
||||
this.editor.refresh()
|
||||
}
|
||||
this.searchHandler = (e, msg) => this.handleSearch(msg)
|
||||
this.searchState = null
|
||||
}
|
||||
|
||||
handleSearch (msg) {
|
||||
const cm = this.editor
|
||||
const component = this
|
||||
|
||||
if (component.searchState) cm.removeOverlay(component.searchState)
|
||||
if (msg.length < 3) return
|
||||
|
||||
cm.operation(function () {
|
||||
component.searchState = makeOverlay(msg, 'searching')
|
||||
cm.addOverlay(component.searchState)
|
||||
|
||||
function makeOverlay (query, style) {
|
||||
query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), 'gi')
|
||||
return {
|
||||
token: function (stream) {
|
||||
query.lastIndex = stream.pos
|
||||
var match = query.exec(stream.string)
|
||||
if (match && match.index === stream.pos) {
|
||||
stream.pos += match[0].length || 1
|
||||
return style
|
||||
} else if (match) {
|
||||
stream.pos = match.index
|
||||
} else {
|
||||
stream.skipToEnd()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { rulers, enableRulers } = this.props
|
||||
const expandSnippet = this.expandSnippet.bind(this)
|
||||
|
||||
const defaultSnippet = [
|
||||
{
|
||||
id: crypto.randomBytes(16).toString('hex'),
|
||||
name: 'Dummy text',
|
||||
prefix: ['lorem', 'ipsum'],
|
||||
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
|
||||
}
|
||||
]
|
||||
if (!fs.existsSync(consts.SNIPPET_FILE)) {
|
||||
fs.writeFileSync(consts.SNIPPET_FILE, JSON.stringify(defaultSnippet, null, 4), 'utf8')
|
||||
}
|
||||
|
||||
this.value = this.props.value
|
||||
this.editor = CodeMirror(this.refs.root, {
|
||||
rulers: buildCMRulers(rulers, enableRulers),
|
||||
value: this.props.value,
|
||||
lineNumbers: true,
|
||||
lineNumbers: this.props.displayLineNumbers,
|
||||
lineWrapping: true,
|
||||
theme: this.props.theme,
|
||||
indentUnit: this.props.indentSize,
|
||||
tabSize: this.props.indentSize,
|
||||
indentWithTabs: this.props.indentType !== 'space',
|
||||
keyMap: this.props.keyMap,
|
||||
scrollPastEnd: this.props.scrollPastEnd,
|
||||
inputStyle: 'textarea',
|
||||
dragDrop: false,
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
autoCloseBrackets: true,
|
||||
extraKeys: {
|
||||
Tab: function (cm) {
|
||||
const cursor = cm.getCursor()
|
||||
const line = cm.getLine(cursor.line)
|
||||
const cursorPosition = cursor.ch
|
||||
const charBeforeCursor = line.substr(cursorPosition - 1, 1)
|
||||
if (cm.somethingSelected()) cm.indentSelection('add')
|
||||
else {
|
||||
const tabs = cm.getOption('indentWithTabs')
|
||||
if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)\] )?$/)) {
|
||||
if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)] )?$/)) {
|
||||
cm.execCommand('goLineStart')
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
@@ -76,6 +131,16 @@ export default class CodeEditor extends React.Component {
|
||||
cm.execCommand('insertSoftTab')
|
||||
}
|
||||
cm.execCommand('goLineEnd')
|
||||
} else if (!charBeforeCursor.match(/\t|\s|\r|\n/) && cursor.ch > 1) {
|
||||
// text expansion on tab key if the char before is alphabet
|
||||
const snippets = JSON.parse(fs.readFileSync(consts.SNIPPET_FILE, 'utf8'))
|
||||
if (expandSnippet(line, cursor, cm, snippets) === false) {
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
@@ -88,7 +153,7 @@ export default class CodeEditor extends React.Component {
|
||||
'Cmd-T': function (cm) {
|
||||
// Do nothing
|
||||
},
|
||||
Enter: 'newlineAndIndentContinueMarkdownList',
|
||||
Enter: 'boostNewLineAndIndentContinueMarkdownList',
|
||||
'Ctrl-C': (cm) => {
|
||||
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
|
||||
document.execCommand('copy')
|
||||
@@ -100,9 +165,14 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
this.setMode(this.props.mode)
|
||||
|
||||
this.editor.on('focus', this.focusHandler)
|
||||
this.editor.on('blur', this.blurHandler)
|
||||
this.editor.on('change', this.changeHandler)
|
||||
this.editor.on('paste', this.pasteHandler)
|
||||
eventEmitter.on('top:search', this.searchHandler)
|
||||
|
||||
eventEmitter.emit('code:init')
|
||||
this.editor.on('scroll', this.scrollHandler)
|
||||
|
||||
const editorTheme = document.getElementById('editorTheme')
|
||||
editorTheme.addEventListener('load', this.loadStyleHandler)
|
||||
@@ -114,20 +184,91 @@ export default class CodeEditor extends React.Component {
|
||||
CodeMirror.Vim.map('ZZ', ':q', 'normal')
|
||||
}
|
||||
|
||||
expandSnippet (line, cursor, cm, snippets) {
|
||||
const wordBeforeCursor = this.getWordBeforeCursor(line, cursor.line, cursor.ch)
|
||||
const templateCursorString = ':{}'
|
||||
for (let i = 0; i < snippets.length; i++) {
|
||||
if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) {
|
||||
if (snippets[i].content.indexOf(templateCursorString) !== -1) {
|
||||
const snippetLines = snippets[i].content.split('\n')
|
||||
let cursorLineNumber = 0
|
||||
let cursorLinePosition = 0
|
||||
for (let j = 0; j < snippetLines.length; j++) {
|
||||
const cursorIndex = snippetLines[j].indexOf(templateCursorString)
|
||||
if (cursorIndex !== -1) {
|
||||
cursorLineNumber = j
|
||||
cursorLinePosition = cursorIndex
|
||||
cm.replaceRange(
|
||||
snippets[i].content.replace(templateCursorString, ''),
|
||||
wordBeforeCursor.range.from,
|
||||
wordBeforeCursor.range.to
|
||||
)
|
||||
cm.setCursor({ line: cursor.line + cursorLineNumber, ch: cursorLinePosition })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cm.replaceRange(
|
||||
snippets[i].content,
|
||||
wordBeforeCursor.range.from,
|
||||
wordBeforeCursor.range.to
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
getWordBeforeCursor (line, lineNumber, cursorPosition) {
|
||||
let wordBeforeCursor = ''
|
||||
const originCursorPosition = cursorPosition
|
||||
const emptyChars = /\t|\s|\r|\n/
|
||||
|
||||
// to prevent the word to expand is long that will crash the whole app
|
||||
// the safeStop is there to stop user to expand words that longer than 20 chars
|
||||
const safeStop = 20
|
||||
|
||||
while (cursorPosition > 0) {
|
||||
const currentChar = line.substr(cursorPosition - 1, 1)
|
||||
// if char is not an empty char
|
||||
if (!emptyChars.test(currentChar)) {
|
||||
wordBeforeCursor = currentChar + wordBeforeCursor
|
||||
} else if (wordBeforeCursor.length >= safeStop) {
|
||||
throw new Error('Your snippet trigger is too long !')
|
||||
} else {
|
||||
break
|
||||
}
|
||||
cursorPosition--
|
||||
}
|
||||
|
||||
return {
|
||||
text: wordBeforeCursor,
|
||||
range: {
|
||||
from: {line: lineNumber, ch: originCursorPosition},
|
||||
to: {line: lineNumber, ch: cursorPosition}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quitEditor () {
|
||||
document.querySelector('textarea').blur()
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.editor.off('focus', this.focusHandler)
|
||||
this.editor.off('blur', this.blurHandler)
|
||||
this.editor.off('change', this.changeHandler)
|
||||
this.editor.off('paste', this.pasteHandler)
|
||||
eventEmitter.off('top:search', this.searchHandler)
|
||||
this.editor.off('scroll', this.scrollHandler)
|
||||
const editorTheme = document.getElementById('editorTheme')
|
||||
editorTheme.removeEventListener('load', this.loadStyleHandler)
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
let needRefresh = false
|
||||
const {rulers, enableRulers} = this.props
|
||||
if (prevProps.mode !== this.props.mode) {
|
||||
this.setMode(this.props.mode)
|
||||
}
|
||||
@@ -145,6 +286,10 @@ export default class CodeEditor extends React.Component {
|
||||
needRefresh = true
|
||||
}
|
||||
|
||||
if (prevProps.enableRulers !== enableRulers || prevProps.rulers !== rulers) {
|
||||
this.editor.setOption('rulers', buildCMRulers(rulers, enableRulers))
|
||||
}
|
||||
|
||||
if (prevProps.indentSize !== this.props.indentSize) {
|
||||
this.editor.setOption('indentUnit', this.props.indentSize)
|
||||
this.editor.setOption('tabSize', this.props.indentSize)
|
||||
@@ -153,13 +298,21 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.setOption('indentWithTabs', this.props.indentType !== 'space')
|
||||
}
|
||||
|
||||
if (prevProps.displayLineNumbers !== this.props.displayLineNumbers) {
|
||||
this.editor.setOption('lineNumbers', this.props.displayLineNumbers)
|
||||
}
|
||||
|
||||
if (prevProps.scrollPastEnd !== this.props.scrollPastEnd) {
|
||||
this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd)
|
||||
}
|
||||
|
||||
if (needRefresh) {
|
||||
this.editor.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
setMode (mode) {
|
||||
let syntax = CodeMirror.findModeByName(pass(mode))
|
||||
let syntax = CodeMirror.findModeByName(convertModeName(mode))
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
|
||||
this.editor.setOption('mode', syntax.mime)
|
||||
@@ -203,51 +356,150 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.setCursor(cursor)
|
||||
}
|
||||
|
||||
handleDropImage (e) {
|
||||
e.preventDefault()
|
||||
const imagePath = e.dataTransfer.files[0].path
|
||||
const filename = path.basename(imagePath)
|
||||
|
||||
copyImage(imagePath, this.props.storageKey).then((imagePath) => {
|
||||
const imageMd = `})`
|
||||
this.insertImageMd(imageMd)
|
||||
})
|
||||
handleDropImage (dropEvent) {
|
||||
dropEvent.preventDefault()
|
||||
const {storageKey, noteKey} = this.props
|
||||
attachmentManagement.handleAttachmentDrop(this, storageKey, noteKey, dropEvent)
|
||||
}
|
||||
|
||||
insertImageMd (imageMd) {
|
||||
insertAttachmentMd (imageMd) {
|
||||
this.editor.replaceSelection(imageMd)
|
||||
}
|
||||
|
||||
handlePaste (editor, e) {
|
||||
const dataTransferItem = e.clipboardData.items[0]
|
||||
if (!dataTransferItem.type.match('image')) return
|
||||
|
||||
const blob = dataTransferItem.getAsFile()
|
||||
const reader = new FileReader()
|
||||
let base64data
|
||||
|
||||
reader.readAsDataURL(blob)
|
||||
reader.onloadend = () => {
|
||||
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
|
||||
base64data += base64data.replace('+', ' ')
|
||||
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
||||
const imageName = Math.random().toString(36).slice(-16)
|
||||
const storagePath = findStorage(this.props.storageKey).path
|
||||
const imageDir = path.join(storagePath, 'images')
|
||||
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
|
||||
const imagePath = path.join(imageDir, `${imageName}.png`)
|
||||
fs.writeFile(imagePath, binaryData, 'binary')
|
||||
const imageMd = `})`
|
||||
this.insertImageMd(imageMd)
|
||||
const clipboardData = e.clipboardData
|
||||
const {storageKey, noteKey} = this.props
|
||||
const dataTransferItem = clipboardData.items[0]
|
||||
const pastedTxt = clipboardData.getData('text')
|
||||
const isURL = (str) => {
|
||||
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
|
||||
return matcher.test(str)
|
||||
}
|
||||
const isInLinkTag = (editor) => {
|
||||
const startCursor = editor.getCursor('start')
|
||||
const prevChar = editor.getRange(
|
||||
{line: startCursor.line, ch: startCursor.ch - 2},
|
||||
{line: startCursor.line, ch: startCursor.ch}
|
||||
)
|
||||
const endCursor = editor.getCursor('end')
|
||||
const nextChar = editor.getRange(
|
||||
{line: endCursor.line, ch: endCursor.ch},
|
||||
{line: endCursor.line, ch: endCursor.ch + 1}
|
||||
)
|
||||
return prevChar === '](' && nextChar === ')'
|
||||
}
|
||||
if (dataTransferItem.type.match('image')) {
|
||||
attachmentManagement.handlePastImageEvent(this, storageKey, noteKey, dataTransferItem)
|
||||
} else if (this.props.fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
|
||||
this.handlePasteUrl(e, editor, pastedTxt)
|
||||
}
|
||||
if (attachmentManagement.isAttachmentLink(pastedTxt)) {
|
||||
attachmentManagement.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
|
||||
.then((modifiedText) => {
|
||||
this.editor.replaceSelection(modifiedText)
|
||||
})
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
if (this.props.onScroll) {
|
||||
this.props.onScroll(e)
|
||||
}
|
||||
}
|
||||
|
||||
handlePasteUrl (e, editor, pastedTxt) {
|
||||
e.preventDefault()
|
||||
const taggedUrl = `<${pastedTxt}>`
|
||||
editor.replaceSelection(taggedUrl)
|
||||
|
||||
const isImageReponse = (response) => {
|
||||
return response.headers.has('content-type') &&
|
||||
response.headers.get('content-type').match(/^image\/.+$/)
|
||||
}
|
||||
const replaceTaggedUrl = (replacement) => {
|
||||
const value = editor.getValue()
|
||||
const cursor = editor.getCursor()
|
||||
const newValue = value.replace(taggedUrl, replacement)
|
||||
const newCursor = Object.assign({}, cursor, { ch: cursor.ch + newValue.length - value.length })
|
||||
editor.setValue(newValue)
|
||||
editor.setCursor(newCursor)
|
||||
}
|
||||
|
||||
fetch(pastedTxt, {
|
||||
method: 'get'
|
||||
}).then((response) => {
|
||||
if (isImageReponse(response)) {
|
||||
return this.mapImageResponse(response, pastedTxt)
|
||||
} else {
|
||||
return this.mapNormalResponse(response, pastedTxt)
|
||||
}
|
||||
}).then((replacement) => {
|
||||
replaceTaggedUrl(replacement)
|
||||
}).catch((e) => {
|
||||
replaceTaggedUrl(pastedTxt)
|
||||
})
|
||||
}
|
||||
|
||||
mapNormalResponse (response, pastedTxt) {
|
||||
return this.decodeResponse(response).then((body) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const parsedBody = (new window.DOMParser()).parseFromString(body, 'text/html')
|
||||
const linkWithTitle = `[${parsedBody.title}](${pastedTxt})`
|
||||
resolve(linkWithTitle)
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
mapImageResponse (response, pastedTxt) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const url = response.url
|
||||
const name = url.substring(url.lastIndexOf('/') + 1)
|
||||
const imageLinkWithName = ``
|
||||
resolve(imageLinkWithName)
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
decodeResponse (response) {
|
||||
const headers = response.headers
|
||||
const _charset = headers.has('content-type')
|
||||
? this.extractContentTypeCharset(headers.get('content-type'))
|
||||
: undefined
|
||||
return response.arrayBuffer().then((buff) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const charset = _charset !== undefined && iconv.encodingExists(_charset) ? _charset : 'utf-8'
|
||||
resolve(iconv.decode(new Buffer(buff), charset).toString())
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
extractContentTypeCharset (contentType) {
|
||||
return contentType.split(';').filter((str) => {
|
||||
return str.trim().toLowerCase().startsWith('charset')
|
||||
}).map((str) => {
|
||||
return str.replace(/['"]/g, '').split('=')[1]
|
||||
})[0]
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, fontSize } = this.props
|
||||
let fontFamily = this.props.className
|
||||
const {className, fontSize} = this.props
|
||||
let fontFamily = this.props.fontFamily
|
||||
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
|
||||
? [fontFamily].concat(defaultEditorFontFamily)
|
||||
: defaultEditorFontFamily
|
||||
const width = this.props.width
|
||||
return (
|
||||
<div
|
||||
className={className == null
|
||||
@@ -258,7 +510,8 @@ export default class CodeEditor extends React.Component {
|
||||
tabIndex='-1'
|
||||
style={{
|
||||
fontFamily: fontFamily.join(', '),
|
||||
fontSize: fontSize
|
||||
fontSize: fontSize,
|
||||
width: width
|
||||
}}
|
||||
onDrop={(e) => this.handleDropImage(e)}
|
||||
/>
|
||||
@@ -268,6 +521,8 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
CodeEditor.propTypes = {
|
||||
value: PropTypes.string,
|
||||
enableRulers: PropTypes.bool,
|
||||
rulers: PropTypes.arrayOf(Number),
|
||||
mode: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
onBlur: PropTypes.func,
|
||||
|
||||
@@ -92,7 +92,9 @@ class MarkdownEditor extends React.Component {
|
||||
if (this.state.isLocked) return
|
||||
this.setState({ keyPressed: new Set() })
|
||||
const { config } = this.props
|
||||
if (config.editor.switchPreview === 'BLUR') {
|
||||
if (config.editor.switchPreview === 'BLUR' ||
|
||||
(config.editor.switchPreview === 'DBL_CLICK' && this.state.status === 'CODE')
|
||||
) {
|
||||
const cursorPosition = this.refs.code.editor.getCursor()
|
||||
this.setState({
|
||||
status: 'PREVIEW'
|
||||
@@ -104,6 +106,20 @@ class MarkdownEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleDoubleClick (e) {
|
||||
if (this.state.isLocked) return
|
||||
this.setState({keyPressed: new Set()})
|
||||
const { config } = this.props
|
||||
if (config.editor.switchPreview === 'DBL_CLICK') {
|
||||
this.setState({
|
||||
status: 'CODE'
|
||||
}, () => {
|
||||
this.refs.code.focus()
|
||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handlePreviewMouseDown (e) {
|
||||
this.previewMouseDownedAt = new Date()
|
||||
}
|
||||
@@ -207,7 +223,7 @@ class MarkdownEditor extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, value, config, storageKey } = this.props
|
||||
const {className, value, config, storageKey, noteKey} = this.props
|
||||
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
@@ -242,7 +258,13 @@ class MarkdownEditor extends React.Component {
|
||||
fontSize={editorFontSize}
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorIndentSize}
|
||||
enableRulers={config.editor.enableRulers}
|
||||
rulers={config.editor.rulers}
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
storageKey={storageKey}
|
||||
noteKey={noteKey}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
onBlur={(e) => this.handleBlur(e)}
|
||||
/>
|
||||
@@ -259,8 +281,14 @@ class MarkdownEditor extends React.Component {
|
||||
codeBlockFontFamily={config.editor.fontFamily}
|
||||
lineNumber={config.preview.lineNumber}
|
||||
indentSize={editorIndentSize}
|
||||
scrollPastEnd={config.preview.scrollPastEnd}
|
||||
smartQuotes={config.preview.smartQuotes}
|
||||
smartArrows={config.preview.smartArrows}
|
||||
breaks={config.preview.breaks}
|
||||
sanitize={config.preview.sanitize}
|
||||
ref='preview'
|
||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||
onDoubleClick={(e) => this.handleDoubleClick(e)}
|
||||
tabIndex='0'
|
||||
value={this.state.renderValue}
|
||||
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
|
||||
@@ -268,6 +296,9 @@ class MarkdownEditor extends React.Component {
|
||||
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
||||
showCopyNotification={config.ui.showCopyNotification}
|
||||
storagePath={storage.path}
|
||||
noteKey={noteKey}
|
||||
customCSS={config.preview.customCSS}
|
||||
allowCustomCSS={config.preview.allowCustomCSS}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
284
browser/components/MarkdownPreview.js
Normal file → Executable file
284
browser/components/MarkdownPreview.js
Normal file → Executable file
@@ -1,19 +1,24 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import markdown from 'browser/lib/markdown'
|
||||
import Markdown from 'browser/lib/markdown'
|
||||
import _ from 'lodash'
|
||||
import CodeMirror from 'codemirror'
|
||||
import 'codemirror-mode-elixir'
|
||||
import consts from 'browser/lib/consts'
|
||||
import Raphael from 'raphael'
|
||||
import flowchart from 'flowchart'
|
||||
import SequenceDiagram from 'js-sequence-diagrams'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import fs from 'fs'
|
||||
import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
||||
import convertModeName from 'browser/lib/convertModeName'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import mdurl from 'mdurl'
|
||||
import exportNote from 'browser/main/lib/dataApi/exportNote'
|
||||
import { escapeHtmlCharacters } from 'browser/lib/utils'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
||||
|
||||
const { app } = remote
|
||||
const path = require('path')
|
||||
const dialog = remote.dialog
|
||||
@@ -22,8 +27,12 @@ const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
||||
const appPath = 'file://' + (process.env.NODE_ENV === 'production'
|
||||
? app.getAppPath()
|
||||
: path.resolve())
|
||||
const CSS_FILES = [
|
||||
`${appPath}/node_modules/katex/dist/katex.min.css`,
|
||||
`${appPath}/node_modules/codemirror/lib/codemirror.css`
|
||||
]
|
||||
|
||||
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber) {
|
||||
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS) {
|
||||
return `
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
@@ -43,18 +52,31 @@ function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber) {
|
||||
font-weight: 700;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Material Icons'),
|
||||
local('MaterialIcons-Regular'),
|
||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff2') format('woff2'),
|
||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
|
||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
|
||||
}
|
||||
${allowCustomCSS ? customCSS : ''}
|
||||
${markdownStyle}
|
||||
|
||||
body {
|
||||
font-family: '${fontFamily.join("','")}';
|
||||
font-size: ${fontSize}px;
|
||||
${scrollPastEnd && 'padding-bottom: 90vh;'}
|
||||
}
|
||||
code {
|
||||
font-family: ${codeBlockFontFamily.join(', ')};
|
||||
font-family: '${codeBlockFontFamily.join("','")}';
|
||||
background-color: rgba(0,0,0,0.04);
|
||||
}
|
||||
.lineNumber {
|
||||
${lineNumber && 'display: block !important;'}
|
||||
font-family: ${codeBlockFontFamily.join(', ')};
|
||||
font-family: '${codeBlockFontFamily.join("','")}';
|
||||
}
|
||||
|
||||
.clipboardButton {
|
||||
@@ -94,6 +116,16 @@ h2 {
|
||||
body p {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body[data-theme="${theme}"] {
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
}
|
||||
.clipboardButton {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
@@ -106,7 +138,6 @@ if (!OSX) {
|
||||
defaultFontFamily.unshift('meiryo')
|
||||
}
|
||||
const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||
|
||||
export default class MarkdownPreview extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
@@ -114,39 +145,46 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.contextMenuHandler = (e) => this.handleContextMenu(e)
|
||||
this.mouseDownHandler = (e) => this.handleMouseDown(e)
|
||||
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
||||
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
|
||||
this.DoubleClickHandler = (e) => this.handleDoubleClick(e)
|
||||
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
||||
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
||||
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
||||
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
|
||||
this.printHandler = () => this.handlePrint()
|
||||
|
||||
this.linkClickHandler = this.handlelinkClick.bind(this)
|
||||
this.initMarkdown = this.initMarkdown.bind(this)
|
||||
this.initMarkdown()
|
||||
}
|
||||
|
||||
handlePreviewAnchorClick (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const anchor = e.target.closest('a')
|
||||
const href = anchor.getAttribute('href')
|
||||
if (_.isString(href) && href.match(/^#/)) {
|
||||
const targetElement = this.refs.root.contentWindow.document.getElementById(href.substring(1, href.length))
|
||||
if (targetElement != null) {
|
||||
this.getWindow().scrollTo(0, targetElement.offsetTop)
|
||||
}
|
||||
} else {
|
||||
shell.openExternal(href)
|
||||
}
|
||||
initMarkdown () {
|
||||
const { smartQuotes, sanitize, breaks } = this.props
|
||||
this.markdown = new Markdown({
|
||||
typographer: smartQuotes,
|
||||
sanitize,
|
||||
breaks
|
||||
})
|
||||
}
|
||||
|
||||
handleCheckboxClick (e) {
|
||||
this.props.onCheckboxClick(e)
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
if (this.props.onScroll) {
|
||||
this.props.onScroll(e)
|
||||
}
|
||||
}
|
||||
|
||||
handleContextMenu (e) {
|
||||
this.props.onContextMenu(e)
|
||||
}
|
||||
|
||||
handleDoubleClick (e) {
|
||||
if (this.props.onDoubleClick != null) this.props.onDoubleClick(e)
|
||||
}
|
||||
|
||||
handleMouseDown (e) {
|
||||
if (e.target != null) {
|
||||
switch (e.target.tagName) {
|
||||
@@ -159,6 +197,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
handleMouseUp (e) {
|
||||
if (!this.props.onMouseUp) return
|
||||
if (e.target != null && e.target.tagName === 'A') {
|
||||
return null
|
||||
}
|
||||
@@ -173,25 +212,75 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.exportAsDocument('md')
|
||||
}
|
||||
|
||||
handleSaveAsHtml () {
|
||||
this.exportAsDocument('html', (noteContent, exportTasks) => {
|
||||
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams()
|
||||
|
||||
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS)
|
||||
let body = this.markdown.render(escapeHtmlCharacters(noteContent))
|
||||
|
||||
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(noteContent, this.props.storagePath)
|
||||
|
||||
files.forEach((file) => {
|
||||
file = file.replace('file://', '')
|
||||
exportTasks.push({
|
||||
src: file,
|
||||
dst: 'css'
|
||||
})
|
||||
})
|
||||
attachmentsAbsolutePaths.forEach((attachment) => {
|
||||
exportTasks.push({
|
||||
src: attachment,
|
||||
dst: attachmentManagement.DESTINATION_FOLDER
|
||||
})
|
||||
})
|
||||
body = attachmentManagement.removeStorageAndNoteReferences(body, this.props.noteKey)
|
||||
|
||||
let styles = ''
|
||||
files.forEach((file) => {
|
||||
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
|
||||
})
|
||||
|
||||
return `<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
|
||||
<style id="style">${inlineStyles}</style>
|
||||
${styles}
|
||||
</head>
|
||||
<body>${body}</body>
|
||||
</html>`
|
||||
})
|
||||
}
|
||||
|
||||
handlePrint () {
|
||||
this.refs.root.contentWindow.print()
|
||||
}
|
||||
|
||||
exportAsDocument (fileType) {
|
||||
exportAsDocument (fileType, contentFormatter) {
|
||||
const options = {
|
||||
filters: [
|
||||
{ name: 'Documents', extensions: [fileType] }
|
||||
{name: 'Documents', extensions: [fileType]}
|
||||
],
|
||||
properties: ['openFile', 'createDirectory']
|
||||
}
|
||||
|
||||
dialog.showSaveDialog(remote.getCurrentWindow(), options,
|
||||
(filename) => {
|
||||
if (filename) {
|
||||
fs.writeFile(filename, this.props.value, (err) => {
|
||||
if (err) throw err
|
||||
(filename) => {
|
||||
if (filename) {
|
||||
const content = this.props.value
|
||||
const storage = this.props.storagePath
|
||||
|
||||
exportNote(storage, content, filename, contentFormatter)
|
||||
.then((res) => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {type: 'info', message: `Exported to ${filename}`})
|
||||
}).catch((err) => {
|
||||
dialog.showErrorBox('Export error', err ? err.message || err : 'Unexpected error during export')
|
||||
throw err
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fixDecodedURI (node) {
|
||||
@@ -208,21 +297,29 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.refs.root.setAttribute('sandbox', 'allow-scripts')
|
||||
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
|
||||
|
||||
this.refs.root.contentWindow.document.head.innerHTML = `
|
||||
let styles = `
|
||||
<style id='style'></style>
|
||||
<link rel="stylesheet" href="${appPath}/node_modules/katex/dist/katex.min.css">
|
||||
<link rel="stylesheet" href="${appPath}/node_modules/codemirror/lib/codemirror.css">
|
||||
<link rel="stylesheet" id="codeTheme">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
`
|
||||
|
||||
CSS_FILES.forEach((file) => {
|
||||
styles += `<link rel="stylesheet" href="${file}">`
|
||||
})
|
||||
|
||||
this.refs.root.contentWindow.document.head.innerHTML = styles
|
||||
this.rewriteIframe()
|
||||
this.applyStyle()
|
||||
|
||||
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('dblclick', this.DoubleClickHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('scroll', this.scrollHandler)
|
||||
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
||||
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
||||
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
|
||||
eventEmitter.on('print', this.printHandler)
|
||||
}
|
||||
|
||||
@@ -230,54 +327,70 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
|
||||
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler)
|
||||
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
|
||||
this.refs.root.contentWindow.document.removeEventListener('dblclick', this.DoubleClickHandler)
|
||||
this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler)
|
||||
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler)
|
||||
this.refs.root.contentWindow.document.removeEventListener('scroll', this.scrollHandler)
|
||||
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
||||
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
||||
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
|
||||
eventEmitter.off('print', this.printHandler)
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.value !== this.props.value) this.rewriteIframe()
|
||||
if (prevProps.smartQuotes !== this.props.smartQuotes ||
|
||||
prevProps.sanitize !== this.props.sanitize ||
|
||||
prevProps.smartArrows !== this.props.smartArrows ||
|
||||
prevProps.breaks !== this.props.breaks) {
|
||||
this.initMarkdown()
|
||||
this.rewriteIframe()
|
||||
}
|
||||
if (prevProps.fontFamily !== this.props.fontFamily ||
|
||||
prevProps.fontSize !== this.props.fontSize ||
|
||||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
|
||||
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
|
||||
prevProps.lineNumber !== this.props.lineNumber ||
|
||||
prevProps.showCopyNotification !== this.props.showCopyNotification ||
|
||||
prevProps.theme !== this.props.theme) {
|
||||
prevProps.theme !== this.props.theme ||
|
||||
prevProps.scrollPastEnd !== this.props.scrollPastEnd ||
|
||||
prevProps.allowCustomCSS !== this.props.allowCustomCSS ||
|
||||
prevProps.customCSS !== this.props.customCSS) {
|
||||
this.applyStyle()
|
||||
this.rewriteIframe()
|
||||
}
|
||||
}
|
||||
|
||||
applyStyle () {
|
||||
const { fontSize, lineNumber, codeBlockTheme } = this.props
|
||||
getStyleParams () {
|
||||
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS } = this.props
|
||||
let { fontFamily, codeBlockFontFamily } = this.props
|
||||
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
||||
? [fontFamily].concat(defaultFontFamily)
|
||||
: defaultFontFamily
|
||||
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily)
|
||||
: defaultFontFamily
|
||||
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
|
||||
? [codeBlockFontFamily].concat(defaultCodeBlockFontFamily)
|
||||
: defaultCodeBlockFontFamily
|
||||
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
|
||||
: defaultCodeBlockFontFamily
|
||||
|
||||
this.setCodeTheme(codeBlockTheme)
|
||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
|
||||
return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS}
|
||||
}
|
||||
|
||||
setCodeTheme (theme) {
|
||||
applyStyle () {
|
||||
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams()
|
||||
|
||||
this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme)
|
||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS)
|
||||
}
|
||||
|
||||
GetCodeThemeLink (theme) {
|
||||
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
|
||||
? theme
|
||||
: 'elegant'
|
||||
this.getWindow().document.getElementById('codeTheme').href = theme.startsWith('solarized')
|
||||
return theme.startsWith('solarized')
|
||||
? `${appPath}/node_modules/codemirror/theme/solarized.css`
|
||||
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
|
||||
}
|
||||
|
||||
rewriteIframe () {
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
el.removeEventListener('click', this.anchorClickHandler)
|
||||
})
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
||||
el.removeEventListener('click', this.checkboxClickHandler)
|
||||
})
|
||||
@@ -286,7 +399,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
el.removeEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
|
||||
const { theme, indentSize, showCopyNotification, storagePath } = this.props
|
||||
const { theme, indentSize, showCopyNotification, storagePath, noteKey } = this.props
|
||||
let { value, codeBlockTheme } = this.props
|
||||
|
||||
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
||||
@@ -297,37 +410,25 @@ export default class MarkdownPreview extends React.Component {
|
||||
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock))
|
||||
})
|
||||
}
|
||||
this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value)
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.taskListItem'), (el) => {
|
||||
el.parentNode.parentNode.style.listStyleType = 'none'
|
||||
})
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
this.fixDecodedURI(el)
|
||||
el.addEventListener('click', this.anchorClickHandler)
|
||||
})
|
||||
let renderedHTML = this.markdown.render(value)
|
||||
attachmentManagement.migrateAttachments(renderedHTML, storagePath, noteKey)
|
||||
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(renderedHTML, storagePath)
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
||||
el.addEventListener('click', this.checkboxClickHandler)
|
||||
})
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
this.fixDecodedURI(el)
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => {
|
||||
el.src = markdown.normalizeLinkText(el.src)
|
||||
if (!/\/:storage/.test(el.src)) return
|
||||
el.src = `file:///${markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}`
|
||||
})
|
||||
|
||||
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
|
||||
? codeBlockTheme
|
||||
: 'default'
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.code code'), (el) => {
|
||||
let syntax = CodeMirror.findModeByName(el.className)
|
||||
let syntax = CodeMirror.findModeByName(convertModeName(el.className))
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
CodeMirror.requireMode(syntax.mode, () => {
|
||||
const content = htmlTextHelper.decodeEntities(el.innerHTML)
|
||||
@@ -346,9 +447,9 @@ export default class MarkdownPreview extends React.Component {
|
||||
el.innerHTML = ''
|
||||
if (codeBlockTheme.indexOf('solarized') === 0) {
|
||||
const [refThema, color] = codeBlockTheme.split(' ')
|
||||
el.parentNode.className += ` cm-s-${refThema} cm-s-${color} CodeMirror`
|
||||
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
|
||||
} else {
|
||||
el.parentNode.className += ` cm-s-${codeBlockTheme} CodeMirror`
|
||||
el.parentNode.className += ` cm-s-${codeBlockTheme}`
|
||||
}
|
||||
CodeMirror.runMode(content, syntax.mime, el, {
|
||||
tabSize: indentSize
|
||||
@@ -369,7 +470,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
el.innerHTML = ''
|
||||
diagram.drawSVG(el, opts)
|
||||
_.forEach(el.querySelectorAll('a'), (el) => {
|
||||
el.addEventListener('click', this.anchorClickHandler)
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
@@ -385,7 +486,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
el.innerHTML = ''
|
||||
diagram.drawSVG(el, {theme: 'simple'})
|
||||
_.forEach(el.querySelectorAll('a'), (el) => {
|
||||
el.addEventListener('click', this.anchorClickHandler)
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
@@ -430,11 +531,44 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
handlelinkClick (e) {
|
||||
const noteHash = e.target.href.split('/').pop()
|
||||
const regexIsNoteLink = /^(.{20})-(.{20})$/
|
||||
if (regexIsNoteLink.test(noteHash)) {
|
||||
eventEmitter.emit('list:jump', noteHash)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const href = e.target.href
|
||||
const linkHash = href.split('/').pop()
|
||||
|
||||
const regexNoteInternalLink = /main.html#(.+)/
|
||||
if (regexNoteInternalLink.test(linkHash)) {
|
||||
const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1])
|
||||
const targetElement = this.refs.root.contentWindow.document.getElementById(targetId)
|
||||
|
||||
if (targetElement != null) {
|
||||
this.getWindow().scrollTo(0, targetElement.offsetTop)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// this will match the new uuid v4 hash and the old hash
|
||||
// e.g.
|
||||
// :note:1c211eb7dcb463de6490 and
|
||||
// :note:7dd23275-f2b4-49cb-9e93-3454daf1af9c
|
||||
const regexIsNoteLink = /^:note:([a-zA-Z0-9-]{20,36})$/
|
||||
if (regexIsNoteLink.test(linkHash)) {
|
||||
eventEmitter.emit('list:jump', linkHash.replace(':note:', ''))
|
||||
return
|
||||
}
|
||||
|
||||
// this will match the old link format storage.key-note.key
|
||||
// e.g.
|
||||
// 877f99c3268608328037-1c211eb7dcb463de6490
|
||||
const regexIsLegacyNoteLink = /^(.{20})-(.{20})$/
|
||||
if (regexIsLegacyNoteLink.test(linkHash)) {
|
||||
eventEmitter.emit('list:jump', linkHash.split('-')[1])
|
||||
return
|
||||
}
|
||||
|
||||
// other case
|
||||
shell.openExternal(href)
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -457,8 +591,12 @@ MarkdownPreview.propTypes = {
|
||||
onDoubleClick: PropTypes.func,
|
||||
onMouseUp: PropTypes.func,
|
||||
onMouseDown: PropTypes.func,
|
||||
onContextMenu: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
showCopyNotification: PropTypes.bool,
|
||||
storagePath: PropTypes.string
|
||||
storagePath: PropTypes.string,
|
||||
smartQuotes: PropTypes.bool,
|
||||
smartArrows: PropTypes.bool,
|
||||
breaks: PropTypes.bool
|
||||
}
|
||||
|
||||
200
browser/components/MarkdownSplitEditor.js
Normal file
200
browser/components/MarkdownSplitEditor.js
Normal file
@@ -0,0 +1,200 @@
|
||||
import React from 'react'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import _ from 'lodash'
|
||||
|
||||
import styles from './MarkdownSplitEditor.styl'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
|
||||
class MarkdownSplitEditor extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.value = props.value
|
||||
this.focus = () => this.refs.code.focus()
|
||||
this.reload = () => this.refs.code.reload()
|
||||
this.userScroll = true
|
||||
this.state = {
|
||||
isSliderFocused: false,
|
||||
codeEditorWidthInPercent: 50
|
||||
}
|
||||
}
|
||||
|
||||
handleOnChange () {
|
||||
this.value = this.refs.code.value
|
||||
this.props.onChange()
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document')
|
||||
const codeDoc = _.get(this, 'refs.code.editor.doc')
|
||||
let srcTop, srcHeight, targetTop, targetHeight
|
||||
|
||||
if (this.userScroll) {
|
||||
if (e.doc) {
|
||||
srcTop = _.get(e, 'doc.scrollTop')
|
||||
srcHeight = _.get(e, 'doc.height')
|
||||
targetTop = _.get(previewDoc, 'body.scrollTop')
|
||||
targetHeight = _.get(previewDoc, 'body.scrollHeight')
|
||||
} else {
|
||||
srcTop = _.get(previewDoc, 'body.scrollTop')
|
||||
srcHeight = _.get(previewDoc, 'body.scrollHeight')
|
||||
targetTop = _.get(codeDoc, 'scrollTop')
|
||||
targetHeight = _.get(codeDoc, 'height')
|
||||
}
|
||||
|
||||
const distance = (targetHeight * srcTop / srcHeight) - targetTop
|
||||
const framerate = 1000 / 60
|
||||
const frames = 20
|
||||
const refractory = frames * framerate
|
||||
|
||||
this.userScroll = false
|
||||
|
||||
let frame = 0
|
||||
let scrollPos, time
|
||||
const timer = setInterval(() => {
|
||||
time = frame / frames
|
||||
scrollPos = time < 0.5
|
||||
? 2 * time * time // ease in
|
||||
: -1 + (4 - 2 * time) * time // ease out
|
||||
if (e.doc) _.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance)
|
||||
else _.get(this, 'refs.code.editor').scrollTo(0, targetTop + scrollPos * distance)
|
||||
if (frame >= frames) {
|
||||
clearInterval(timer)
|
||||
setTimeout(() => { this.userScroll = true }, refractory)
|
||||
}
|
||||
frame++
|
||||
}, framerate)
|
||||
}
|
||||
}
|
||||
|
||||
handleCheckboxClick (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const idMatch = /checkbox-([0-9]+)/
|
||||
const checkedMatch = /\[x\]/i
|
||||
const uncheckedMatch = /\[ \]/
|
||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||
const lines = this.refs.code.value
|
||||
.split('\n')
|
||||
|
||||
const targetLine = lines[lineIndex]
|
||||
|
||||
if (targetLine.match(checkedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
|
||||
}
|
||||
if (targetLine.match(uncheckedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
|
||||
}
|
||||
this.refs.code.setValue(lines.join('\n'))
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseMove (e) {
|
||||
if (this.state.isSliderFocused) {
|
||||
const rootRect = this.refs.root.getBoundingClientRect()
|
||||
const rootWidth = rootRect.width
|
||||
const offset = rootRect.left
|
||||
let newCodeEditorWidthInPercent = (e.pageX - offset) / rootWidth * 100
|
||||
|
||||
// limit minSize to 10%, maxSize to 90%
|
||||
if (newCodeEditorWidthInPercent <= 10) {
|
||||
newCodeEditorWidthInPercent = 10
|
||||
}
|
||||
|
||||
if (newCodeEditorWidthInPercent >= 90) {
|
||||
newCodeEditorWidthInPercent = 90
|
||||
}
|
||||
|
||||
this.setState({
|
||||
codeEditorWidthInPercent: newCodeEditorWidthInPercent
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseUp (e) {
|
||||
e.preventDefault()
|
||||
this.setState({
|
||||
isSliderFocused: false
|
||||
})
|
||||
}
|
||||
|
||||
handleMouseDown (e) {
|
||||
e.preventDefault()
|
||||
this.setState({
|
||||
isSliderFocused: true
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const {config, value, storageKey, noteKey} = this.props
|
||||
const storage = findStorage(storageKey)
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||
const previewStyle = {}
|
||||
previewStyle.width = (100 - this.state.codeEditorWidthInPercent) + '%'
|
||||
if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused) previewStyle.pointerEvents = 'none'
|
||||
return (
|
||||
<div styleName='root' ref='root'
|
||||
onMouseMove={e => this.handleMouseMove(e)}
|
||||
onMouseUp={e => this.handleMouseUp(e)}>
|
||||
<CodeEditor
|
||||
styleName='codeEditor'
|
||||
ref='code'
|
||||
width={this.state.codeEditorWidthInPercent + '%'}
|
||||
mode='GitHub Flavored Markdown'
|
||||
value={value}
|
||||
theme={config.editor.theme}
|
||||
keyMap={config.editor.keyMap}
|
||||
fontFamily={config.editor.fontFamily}
|
||||
fontSize={editorFontSize}
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorIndentSize}
|
||||
enableRulers={config.editor.enableRulers}
|
||||
rulers={config.editor.rulers}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
storageKey={storageKey}
|
||||
noteKey={noteKey}
|
||||
onChange={this.handleOnChange.bind(this)}
|
||||
onScroll={this.handleScroll.bind(this)}
|
||||
/>
|
||||
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
|
||||
<div styleName='slider-hitbox' />
|
||||
</div>
|
||||
<MarkdownPreview
|
||||
style={previewStyle}
|
||||
styleName='preview'
|
||||
theme={config.ui.theme}
|
||||
keyMap={config.editor.keyMap}
|
||||
fontSize={config.preview.fontSize}
|
||||
fontFamily={config.preview.fontFamily}
|
||||
codeBlockTheme={config.preview.codeBlockTheme}
|
||||
codeBlockFontFamily={config.editor.fontFamily}
|
||||
lineNumber={config.preview.lineNumber}
|
||||
scrollPastEnd={config.preview.scrollPastEnd}
|
||||
smartQuotes={config.preview.smartQuotes}
|
||||
smartArrows={config.preview.smartArrows}
|
||||
breaks={config.preview.breaks}
|
||||
sanitize={config.preview.sanitize}
|
||||
ref='preview'
|
||||
tabInde='0'
|
||||
value={value}
|
||||
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
||||
onScroll={this.handleScroll.bind(this)}
|
||||
showCopyNotification={config.ui.showCopyNotification}
|
||||
storagePath={storage.path}
|
||||
noteKey={noteKey}
|
||||
customCSS={config.preview.customCSS}
|
||||
allowCustomCSS={config.preview.allowCustomCSS}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CSSModules(MarkdownSplitEditor, styles)
|
||||
16
browser/components/MarkdownSplitEditor.styl
Normal file
16
browser/components/MarkdownSplitEditor.styl
Normal file
@@ -0,0 +1,16 @@
|
||||
.root
|
||||
width 100%
|
||||
height 100%
|
||||
font-size 30px
|
||||
display flex
|
||||
.slider
|
||||
absolute top bottom
|
||||
top -2px
|
||||
width 0
|
||||
z-index 0
|
||||
.slider-hitbox
|
||||
absolute top bottom left right
|
||||
width 7px
|
||||
left -3px
|
||||
z-index 10
|
||||
cursor col-resize
|
||||
@@ -9,6 +9,10 @@
|
||||
width 34px
|
||||
line-height 32px
|
||||
padding 0
|
||||
&:hover
|
||||
border: 1px solid #1EC38B;
|
||||
background-color: alpha(#1EC38B, 30%)
|
||||
border-radius: 50%;
|
||||
|
||||
body[data-theme="white"]
|
||||
navWhiteButtonColor()
|
||||
|
||||
@@ -8,6 +8,7 @@ import CSSModules from 'browser/lib/CSSModules'
|
||||
import { getTodoStatus } from 'browser/lib/getTodoStatus'
|
||||
import styles from './NoteItem.styl'
|
||||
import TodoProcess from './TodoProcess'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
/**
|
||||
* @description Tag element component.
|
||||
@@ -46,14 +47,25 @@ const TagElementList = (tags) => {
|
||||
* @param {Function} handleDragStart
|
||||
* @param {string} dateDisplay
|
||||
*/
|
||||
const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteContextMenu, handleDragStart, pathname }) => (
|
||||
const NoteItem = ({
|
||||
isActive,
|
||||
note,
|
||||
dateDisplay,
|
||||
handleNoteClick,
|
||||
handleNoteContextMenu,
|
||||
handleDragStart,
|
||||
pathname,
|
||||
storageName,
|
||||
folderName,
|
||||
viewType
|
||||
}) => (
|
||||
<div styleName={isActive
|
||||
? 'item--active'
|
||||
: 'item'
|
||||
}
|
||||
key={`${note.storage}-${note.key}`}
|
||||
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
|
||||
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
|
||||
? 'item--active'
|
||||
: 'item'
|
||||
}
|
||||
key={note.key}
|
||||
onClick={e => handleNoteClick(e, note.key)}
|
||||
onContextMenu={e => handleNoteContextMenu(e, note.key)}
|
||||
onDragStart={e => handleDragStart(e, note)}
|
||||
draggable='true'
|
||||
>
|
||||
@@ -65,26 +77,36 @@ const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteCont
|
||||
<div styleName='item-title'>
|
||||
{note.title.trim().length > 0
|
||||
? note.title
|
||||
: <span styleName='item-title-empty'>Empty</span>
|
||||
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>
|
||||
}
|
||||
</div>
|
||||
{['ALL', 'STORAGE'].includes(viewType) && <div styleName='item-middle'>
|
||||
<div styleName='item-middle-time'>{dateDisplay}</div>
|
||||
<div styleName='item-middle-app-meta'>
|
||||
<div title={viewType === 'ALL' ? storageName : viewType === 'STORAGE' ? folderName : null} styleName='item-middle-app-meta-label'>
|
||||
{viewType === 'ALL' && storageName}
|
||||
{viewType === 'STORAGE' && folderName}
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<div styleName='item-bottom-time'>{dateDisplay}</div>
|
||||
{note.isStarred
|
||||
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
|
||||
}
|
||||
{note.isPinned && !pathname.match(/\/home|\/starred|\/trash/)
|
||||
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
|
||||
}
|
||||
{note.type === 'MARKDOWN_NOTE'
|
||||
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
||||
: ''
|
||||
}
|
||||
<div styleName='item-bottom'>
|
||||
<div styleName='item-bottom-tagList'>
|
||||
{note.tags.length > 0
|
||||
? TagElementList(note.tags)
|
||||
: <span styleName='item-bottom-tagList-empty' />
|
||||
: <span style={{ fontStyle: 'italic', opacity: 0.5 }} styleName='item-bottom-tagList-empty'>{i18n.__('No tags')}</span>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
{note.isStarred
|
||||
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
|
||||
}
|
||||
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
||||
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
|
||||
}
|
||||
{note.type === 'MARKDOWN_NOTE'
|
||||
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,7 +124,11 @@ NoteItem.propTypes = {
|
||||
title: PropTypes.string.isrequired,
|
||||
tags: PropTypes.array,
|
||||
isStarred: PropTypes.bool.isRequired,
|
||||
isTrashed: PropTypes.bool.isRequired
|
||||
isTrashed: PropTypes.bool.isRequired,
|
||||
blog: {
|
||||
blogLink: PropTypes.string,
|
||||
blogId: PropTypes.number
|
||||
}
|
||||
}),
|
||||
handleNoteClick: PropTypes.func.isRequired,
|
||||
handleNoteContextMenu: PropTypes.func.isRequired,
|
||||
|
||||
@@ -90,6 +90,26 @@ $control-height = 30px
|
||||
font-weight normal
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-middle
|
||||
font-size 13px
|
||||
padding-left 2px
|
||||
padding-bottom 2px
|
||||
|
||||
.item-middle-time
|
||||
color $ui-inactive-text-color
|
||||
display inline-block
|
||||
|
||||
.item-middle-app-meta
|
||||
float right
|
||||
.item-middle-app-meta-label
|
||||
opacity 0.4
|
||||
color $ui-inactive-text-color
|
||||
padding 0 3px
|
||||
white-space nowrap
|
||||
text-overflow ellipsis
|
||||
overflow hidden
|
||||
max-width 200px
|
||||
|
||||
.item-bottom
|
||||
position relative
|
||||
bottom 0px
|
||||
@@ -97,7 +117,7 @@ $control-height = 30px
|
||||
font-size 12px
|
||||
line-height 20px
|
||||
overflow ellipsis
|
||||
display flex
|
||||
display block
|
||||
|
||||
.item-bottom-tagList
|
||||
flex 1
|
||||
@@ -125,10 +145,8 @@ $control-height = 30px
|
||||
|
||||
.item-star
|
||||
position absolute
|
||||
right -6px
|
||||
bottom 23px
|
||||
width 16px
|
||||
height 16px
|
||||
right 2px
|
||||
top 5px
|
||||
color alpha($ui-favorite-star-button-color, 60%)
|
||||
font-size 12px
|
||||
padding 0
|
||||
@@ -136,10 +154,8 @@ $control-height = 30px
|
||||
|
||||
.item-pin
|
||||
position absolute
|
||||
right 0px
|
||||
bottom 2px
|
||||
width 34px
|
||||
height 34px
|
||||
right 25px
|
||||
top 7px
|
||||
color #E54D42
|
||||
font-size 14px
|
||||
padding 0
|
||||
@@ -192,7 +208,7 @@ body[data-theme="dark"]
|
||||
.item-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-dark-text-color
|
||||
color $ui-dark-text-color
|
||||
|
||||
.item-wrapper
|
||||
border-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||
@@ -231,3 +247,150 @@ body[data-theme="dark"]
|
||||
.item-bottom-tagList-empty
|
||||
color $ui-inactive-text-color
|
||||
vertical-align middle
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
|
||||
.item
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
&:hover
|
||||
transition 0.15s
|
||||
// background-color alpha($ui-solarized-dark-noteList-backgroundColor, 20%)
|
||||
color $ui-solarized-dark-text-color
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-solarized-dark-text-color
|
||||
.item-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha($ui-solarized-dark-noteList-backgroundColor, 20%)
|
||||
color $ui-solarized-dark-text-color
|
||||
&:active
|
||||
transition 0.15s
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-solarized-dark-text-color
|
||||
.item-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha($ui-solarized-dark-noteList-backgroundColor, 10%)
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.item-wrapper
|
||||
border-color alpha($ui-solarized-dark-button--active-backgroundColor, 60%)
|
||||
|
||||
.item--active
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
.item-wrapper
|
||||
border-color transparent
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
color $ui-solarized-dark-text-color
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-solarized-dark-text-color
|
||||
&:hover
|
||||
// background-color alpha($ui-solarized-dark-button--active-backgroundColor, 60%)
|
||||
color #c0392b
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha(#fff, 20%)
|
||||
|
||||
.item-title
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-title-icon
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-title-empty
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 40%)
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-bottom-tagList-empty
|
||||
color $ui-inactive-text-color
|
||||
vertical-align middle
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.item
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
&:hover
|
||||
transition 0.15s
|
||||
// background-color alpha($ui-monokai-noteList-backgroundColor, 20%)
|
||||
color $ui-monokai-text-color
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-monokai-text-color
|
||||
.item-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha($ui-monokai-noteList-backgroundColor, 20%)
|
||||
color $ui-monokai-text-color
|
||||
&:active
|
||||
transition 0.15s
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-monokai-text-color
|
||||
.item-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha($ui-monokai-noteList-backgroundColor, 10%)
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.item-wrapper
|
||||
border-color alpha($ui-monokai-button-backgroundColor, 60%)
|
||||
|
||||
.item--active
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
.item-wrapper
|
||||
border-color transparent
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
color $ui-monokai-text-color
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
// background-color alpha($ui-monokai-button--active-backgroundColor, 60%)
|
||||
color #c0392b
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha(#fff, 20%)
|
||||
|
||||
.item-title
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-title-icon
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-title-empty
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 40%)
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-bottom-tagList-empty
|
||||
color $ui-inactive-text-color
|
||||
vertical-align middle
|
||||
|
||||
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './NoteItemSimple.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
/**
|
||||
* @description Note item component when using simple display mode.
|
||||
@@ -14,14 +15,23 @@ import styles from './NoteItemSimple.styl'
|
||||
* @param {Function} handleNoteContextMenu
|
||||
* @param {Function} handleDragStart
|
||||
*/
|
||||
const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu, handleDragStart }) => (
|
||||
const NoteItemSimple = ({
|
||||
isActive,
|
||||
isAllNotesView,
|
||||
note,
|
||||
handleNoteClick,
|
||||
handleNoteContextMenu,
|
||||
handleDragStart,
|
||||
pathname,
|
||||
storage
|
||||
}) => (
|
||||
<div styleName={isActive
|
||||
? 'item-simple--active'
|
||||
: 'item-simple'
|
||||
}
|
||||
key={`${note.storage}-${note.key}`}
|
||||
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
|
||||
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
|
||||
? 'item-simple--active'
|
||||
: 'item-simple'
|
||||
}
|
||||
key={note.key}
|
||||
onClick={e => handleNoteClick(e, note.key)}
|
||||
onContextMenu={e => handleNoteContextMenu(e, note.key)}
|
||||
onDragStart={e => handleDragStart(e, note)}
|
||||
draggable='true'
|
||||
>
|
||||
@@ -30,10 +40,19 @@ const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu
|
||||
? <i styleName='item-simple-title-icon' className='fa fa-fw fa-code' />
|
||||
: <i styleName='item-simple-title-icon' className='fa fa-fw fa-file-text-o' />
|
||||
}
|
||||
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
||||
? <i styleName='item-pin' className='fa fa-thumb-tack' />
|
||||
: ''
|
||||
}
|
||||
{note.title.trim().length > 0
|
||||
? note.title
|
||||
: <span styleName='item-simple-title-empty'>Empty</span>
|
||||
: <span styleName='item-simple-title-empty'>{i18n.__('Empty note')}</span>
|
||||
}
|
||||
{isAllNotesView && <div styleName='item-simple-right'>
|
||||
<span styleName='item-simple-right-storageName'>
|
||||
{storage.name}
|
||||
</span>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -50,6 +50,7 @@ $control-height = 30px
|
||||
.item-simple-title
|
||||
font-size 13px
|
||||
height 40px
|
||||
padding-right 20px
|
||||
box-sizing border-box
|
||||
line-height 24px
|
||||
padding-top 8px
|
||||
@@ -67,6 +68,15 @@ $control-height = 30px
|
||||
font-weight normal
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-pin
|
||||
position absolute
|
||||
right 0px
|
||||
top 12px
|
||||
color #E54D42
|
||||
font-size 14px
|
||||
padding 0
|
||||
border-radius 17px
|
||||
|
||||
body[data-theme="white"]
|
||||
.item-simple
|
||||
background-color $ui-white-noteList-backgroundColor
|
||||
@@ -94,6 +104,7 @@ body[data-theme="dark"]
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
color $ui-dark-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
@@ -107,6 +118,7 @@ body[data-theme="dark"]
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
@@ -114,7 +126,7 @@ body[data-theme="dark"]
|
||||
.item-simple-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-dark-text-color
|
||||
color $ui-dark-text-color
|
||||
|
||||
.item-simple--active
|
||||
border-color $ui-dark-borderColor
|
||||
@@ -122,6 +134,7 @@ body[data-theme="dark"]
|
||||
.item-simple-wrapper
|
||||
border-color transparent
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
color $ui-dark-text-color
|
||||
@@ -143,3 +156,133 @@ body[data-theme="dark"]
|
||||
|
||||
.item-simple-title-empty
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
|
||||
.item-simple
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
&:hover
|
||||
transition 0.15s
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||
color $ui-solarized-dark-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-solarized-dark-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(#fff, 20%)
|
||||
color $ui-solarized-dark-text-color
|
||||
&:active
|
||||
transition 0.15s
|
||||
// background-color $ui-solarized-dark-button--active-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-solarized-dark-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.item-simple--active
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-tag-backgroundColor
|
||||
.item-simple-wrapper
|
||||
border-color transparent
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
color $ui-dark-text-color
|
||||
.item-simple-bottom-time
|
||||
color $ui-solarized-dark-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-solarized-dark-text-color
|
||||
&:hover
|
||||
// background-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||
color #c0392b
|
||||
.item-simple-bottom-tagList-item
|
||||
background-color alpha(#fff, 20%)
|
||||
.item-simple-title
|
||||
color $ui-dark-text-color
|
||||
border-bottom $ui-dark-borderColor
|
||||
.item-simple-right
|
||||
float right
|
||||
.item-simple-right-storageName
|
||||
padding-left 4px
|
||||
opacity 0.4
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.item-simple
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
&:hover
|
||||
transition 0.15s
|
||||
background-color alpha($ui-monokai-button-backgroundColor, 60%)
|
||||
color $ui-monokai-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-solarized-dark-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(#fff, 20%)
|
||||
color $ui-monokai-text-color
|
||||
&:active
|
||||
transition 0.15s
|
||||
background-color $ui-monokai-button--active-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-monokai-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.item-simple--active
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-button--active-backgroundColor
|
||||
.item-simple-wrapper
|
||||
border-color transparent
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
color $ui-monokai-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
// background-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||
color #c0392b
|
||||
.item-simple-bottom-tagList-item
|
||||
background-color alpha(#fff, 20%)
|
||||
.item-simple-title
|
||||
color $ui-dark-text-color
|
||||
border-bottom $ui-dark-borderColor
|
||||
.item-simple-right
|
||||
float right
|
||||
.item-simple-right-storageName
|
||||
padding-left 4px
|
||||
opacity 0.4
|
||||
|
||||
@@ -29,3 +29,26 @@ body[data-theme="dark"]
|
||||
transition 0.2s
|
||||
&:hover
|
||||
color #5CB85C
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.notification-area
|
||||
background-color none
|
||||
|
||||
.notification-link
|
||||
color $ui-solarized-dark-text-color
|
||||
border none
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
&:hover
|
||||
color #5CB85C
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.notification-area
|
||||
background-color none
|
||||
|
||||
.notification-link
|
||||
color $ui-monokai-text-color
|
||||
border none
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
&:hover
|
||||
color #5CB85C
|
||||
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './SideNavFilter.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
/**
|
||||
* @param {boolean} isFolded
|
||||
@@ -17,7 +18,7 @@ import styles from './SideNavFilter.styl'
|
||||
const SideNavFilter = ({
|
||||
isFolded, isHomeActive, handleAllNotesButtonClick,
|
||||
isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick, counterDelNote,
|
||||
counterTotalNote, counterStarredNote
|
||||
counterTotalNote, counterStarredNote, handleFilterButtonContextMenu
|
||||
}) => (
|
||||
<div styleName={isFolded ? 'menu--folded' : 'menu'}>
|
||||
|
||||
@@ -26,12 +27,12 @@ const SideNavFilter = ({
|
||||
>
|
||||
<div styleName='iconWrap'>
|
||||
<img src={isHomeActive
|
||||
? '../resources/icon/icon-all-active.svg'
|
||||
: '../resources/icon/icon-all.svg'
|
||||
}
|
||||
? '../resources/icon/icon-all-active.svg'
|
||||
: '../resources/icon/icon-all.svg'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span styleName='menu-button-label'>All Notes</span>
|
||||
<span styleName='menu-button-label'>{i18n.__('All Notes')}</span>
|
||||
<span styleName='counters'>{counterTotalNote}</span>
|
||||
</button>
|
||||
|
||||
@@ -40,26 +41,26 @@ const SideNavFilter = ({
|
||||
>
|
||||
<div styleName='iconWrap'>
|
||||
<img src={isStarredActive
|
||||
? '../resources/icon/icon-star-active.svg'
|
||||
: '../resources/icon/icon-star.svg'
|
||||
}
|
||||
? '../resources/icon/icon-star-active.svg'
|
||||
: '../resources/icon/icon-star-sidenav.svg'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span styleName='menu-button-label'>Starred</span>
|
||||
<span styleName='menu-button-label'>{i18n.__('Starred')}</span>
|
||||
<span styleName='counters'>{counterStarredNote}</span>
|
||||
</button>
|
||||
|
||||
<button styleName={isTrashedActive ? 'menu-button-trash--active' : 'menu-button'}
|
||||
onClick={handleTrashedButtonClick}
|
||||
onClick={handleTrashedButtonClick} onContextMenu={handleFilterButtonContextMenu}
|
||||
>
|
||||
<div styleName='iconWrap'>
|
||||
<img src={isTrashedActive
|
||||
? '../resources/icon/icon-trash-active.svg'
|
||||
: '../resources/icon/icon-trash.svg'
|
||||
}
|
||||
? '../resources/icon/icon-trash-active.svg'
|
||||
: '../resources/icon/icon-trash-sidenav.svg'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span styleName='menu-button-label'>Trash</span>
|
||||
<span styleName='menu-button-label'>{i18n.__('Trash')}</span>
|
||||
<span styleName='counters'>{counterDelNote}</span>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
.iconWrap
|
||||
width 20px
|
||||
text-align center
|
||||
|
||||
|
||||
.counters
|
||||
float right
|
||||
color $ui-inactive-text-color
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
.menu--folded
|
||||
@extend .menu
|
||||
.menu-button, .menu-button--active
|
||||
.menu-button, .menu-button--active, .menu-button-star--active, .menu-button-trash--active
|
||||
text-align center
|
||||
padding 0 12px
|
||||
&:hover .menu-button-label
|
||||
@@ -68,10 +68,9 @@
|
||||
.menu-button-label
|
||||
position fixed
|
||||
display inline-block
|
||||
height 32px
|
||||
height 36px
|
||||
left 44px
|
||||
padding 0 10px
|
||||
margin-top -8px
|
||||
margin-left 0
|
||||
overflow ellipsis
|
||||
z-index 10
|
||||
@@ -92,7 +91,6 @@ body[data-theme="white"]
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.menu-button--active
|
||||
@extend .menu-button
|
||||
color #e74c3c
|
||||
background-color $ui-button--active-backgroundColor
|
||||
.menu-button-label
|
||||
@@ -109,7 +107,6 @@ body[data-theme="white"]
|
||||
color $ui-text-color
|
||||
|
||||
.menu-button-star--active
|
||||
@extend .menu-button
|
||||
color #F9BF3B
|
||||
background-color $ui-button--active-backgroundColor
|
||||
.menu-button-label
|
||||
@@ -126,7 +123,6 @@ body[data-theme="white"]
|
||||
color $ui-text-color
|
||||
|
||||
.menu-button-trash--active
|
||||
@extend .menu-button
|
||||
color #5D9E36
|
||||
background-color $ui-button--active-backgroundColor
|
||||
.menu-button-label
|
||||
@@ -182,4 +178,89 @@ body[data-theme="dark"]
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||
color #5D9E36
|
||||
.menu-button-label
|
||||
color $ui-dark-text-color
|
||||
color $ui-dark-text-color
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.menu-button
|
||||
&:active
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
&:hover
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.menu-button--active
|
||||
color $ui-solarized-dark-text-color
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-solarized-dark-text-color
|
||||
&:hover
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
.menu-button-label
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.menu-button-star--active
|
||||
color $ui-solarized-dark-text-color
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-solarized-dark-text-color
|
||||
&:hover
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
.menu-button-label
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.menu-button-trash--active
|
||||
color $ui-solarized-dark-text-color
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-solarized-dark-text-color
|
||||
&:hover
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
.menu-button-label
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.menu-button
|
||||
&:active
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.menu-button--active
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.menu-button-star--active
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.menu-button-trash--active
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
@@ -2,6 +2,7 @@ import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './SnippetTab.styl'
|
||||
import context from 'browser/lib/context'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
class SnippetTab extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -28,7 +29,7 @@ class SnippetTab extends React.Component {
|
||||
handleContextMenu (e) {
|
||||
context.popup([
|
||||
{
|
||||
label: 'Rename',
|
||||
label: i18n.__('Rename'),
|
||||
click: (e) => this.handleRenameClick(e)
|
||||
}
|
||||
])
|
||||
@@ -54,10 +55,10 @@ class SnippetTab extends React.Component {
|
||||
this.handleRename()
|
||||
break
|
||||
case 27:
|
||||
this.setState({
|
||||
name: this.props.snippet.name,
|
||||
this.setState((prevState, props) => ({
|
||||
name: props.snippet.name,
|
||||
isRenaming: false
|
||||
})
|
||||
}))
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -85,6 +86,15 @@ class SnippetTab extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleDragStart (e) {
|
||||
e.dataTransfer.dropEffect = 'move'
|
||||
this.props.onDragStart(e)
|
||||
}
|
||||
|
||||
handleDrop (e) {
|
||||
this.props.onDrop(e)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isActive, snippet, isDeletable } = this.props
|
||||
return (
|
||||
@@ -98,11 +108,14 @@ class SnippetTab extends React.Component {
|
||||
onClick={(e) => this.handleClick(e)}
|
||||
onDoubleClick={(e) => this.handleRenameClick(e)}
|
||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||
onDragStart={(e) => this.handleDragStart(e)}
|
||||
onDrop={(e) => this.handleDrop(e)}
|
||||
draggable='true'
|
||||
>
|
||||
{snippet.name.trim().length > 0
|
||||
? snippet.name
|
||||
: <span styleName='button-unnamed'>
|
||||
Unnamed
|
||||
{i18n.__('Unnamed')}
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
@@ -127,6 +140,7 @@ class SnippetTab extends React.Component {
|
||||
}
|
||||
|
||||
SnippetTab.propTypes = {
|
||||
|
||||
}
|
||||
|
||||
export default CSSModules(SnippetTab, styles)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.root
|
||||
position relative
|
||||
flex 1
|
||||
min-width 70px
|
||||
overflow hidden
|
||||
&:hover
|
||||
.deleteButton
|
||||
@@ -21,7 +22,7 @@
|
||||
height 29px
|
||||
overflow ellipsis
|
||||
text-align left
|
||||
padding-right 30px
|
||||
padding-right 23px
|
||||
border none
|
||||
background-color transparent
|
||||
transition 0.15s
|
||||
@@ -38,7 +39,7 @@
|
||||
text-align center
|
||||
border none
|
||||
padding 0
|
||||
color transparent
|
||||
color $ui-inactive-text-color
|
||||
background-color transparent
|
||||
border-radius 2px
|
||||
|
||||
@@ -89,3 +90,50 @@ body[data-theme="dark"]
|
||||
.input
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
.deleteButton
|
||||
color alpha($ui-dark-text-color, 30%)
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
color $ui-solarized-dark-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-solarized-dark-text-color
|
||||
&:hover
|
||||
background-color darken($ui-solarized-dark-noteDetail-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-solarized-dark-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.root--active
|
||||
color $ui-solarized-dark-text-color
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-solarized-dark-text-color
|
||||
&:hover
|
||||
background-color darken($ui-solarized-dark-noteDetail-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-solarized-dark-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.button
|
||||
border none
|
||||
color $ui-solarized-dark-text-color
|
||||
background-color transparent
|
||||
transition color background-color 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
color $ui-solarized-dark-text-color
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
|
||||
.input
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.deleteButton
|
||||
color alpha($ui-solarized-dark-text-color, 30%)
|
||||
@@ -6,6 +6,18 @@ import React from 'react'
|
||||
import styles from './StorageItem.styl'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import _ from 'lodash'
|
||||
import { SortableHandle } from 'react-sortable-hoc'
|
||||
|
||||
const DraggableIcon = SortableHandle(({ className }) => (
|
||||
<i className={`fa ${className}`} />
|
||||
))
|
||||
|
||||
const FolderIcon = ({ className, color, isActive }) => {
|
||||
const iconStyle = isActive ? 'fa-folder-open-o' : 'fa-folder-o'
|
||||
return (
|
||||
<i className={`fa ${iconStyle} ${className}`} style={{ color: color }} />
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} isActive
|
||||
@@ -21,34 +33,54 @@ import _ from 'lodash'
|
||||
* @return {React.Component}
|
||||
*/
|
||||
const StorageItem = ({
|
||||
isActive, handleButtonClick, handleContextMenu, folderName,
|
||||
folderColor, isFolded, noteCount, handleDrop, handleDragEnter, handleDragLeave
|
||||
}) => (
|
||||
<button styleName={isActive
|
||||
? 'folderList-item--active'
|
||||
: 'folderList-item'
|
||||
}
|
||||
onClick={handleButtonClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
onDrop={handleDrop}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
<span styleName={isFolded
|
||||
? 'folderList-item-name--folded' : 'folderList-item-name'
|
||||
}>
|
||||
<text style={{color: folderColor, paddingRight: '10px'}}>{isActive ? <i className='fa fa-folder-open-o' /> : <i className='fa fa-folder-o' />}</text>{isFolded ? _.truncate(folderName, {length: 1, omission: ''}) : folderName}
|
||||
</span>
|
||||
{(!isFolded && _.isNumber(noteCount)) &&
|
||||
<span styleName='folderList-item-noteCount'>{noteCount}</span>
|
||||
}
|
||||
{isFolded &&
|
||||
<span styleName='folderList-item-tooltip'>
|
||||
{folderName}
|
||||
styles,
|
||||
isActive,
|
||||
handleButtonClick,
|
||||
handleContextMenu,
|
||||
folderName,
|
||||
folderColor,
|
||||
isFolded,
|
||||
noteCount,
|
||||
handleDrop,
|
||||
handleDragEnter,
|
||||
handleDragLeave
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
styleName={isActive ? 'folderList-item--active' : 'folderList-item'}
|
||||
onClick={handleButtonClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
onDrop={handleDrop}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
{!isFolded && (
|
||||
<DraggableIcon className={styles['folderList-item-reorder']} />
|
||||
)}
|
||||
<span
|
||||
styleName={
|
||||
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
|
||||
}
|
||||
>
|
||||
<FolderIcon
|
||||
styleName='folderList-item-icon'
|
||||
color={folderColor}
|
||||
isActive={isActive}
|
||||
/>
|
||||
{isFolded
|
||||
? _.truncate(folderName, { length: 1, omission: '' })
|
||||
: folderName}
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
)
|
||||
{!isFolded &&
|
||||
_.isNumber(noteCount) && (
|
||||
<span styleName='folderList-item-noteCount'>{noteCount}</span>
|
||||
)}
|
||||
{isFolded && (
|
||||
<span styleName='folderList-item-tooltip'>{folderName}</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
StorageItem.propTypes = {
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
border none
|
||||
overflow ellipsis
|
||||
font-size 14px
|
||||
align-items: center
|
||||
&:first-child
|
||||
margin-top 0
|
||||
&:hover
|
||||
@@ -22,7 +23,7 @@
|
||||
&:active
|
||||
color $$ui-button-default-color
|
||||
background-color alpha($ui-button-default--active-backgroundColor, 20%)
|
||||
|
||||
|
||||
.folderList-item--active
|
||||
@extend .folderList-item
|
||||
color #1EC38B
|
||||
@@ -34,9 +35,7 @@
|
||||
.folderList-item-name
|
||||
display block
|
||||
flex 1
|
||||
padding 0 12px
|
||||
height 26px
|
||||
line-height 26px
|
||||
padding-right: 10px
|
||||
border-width 0 0 0 2px
|
||||
border-style solid
|
||||
border-color transparent
|
||||
@@ -59,8 +58,8 @@
|
||||
opacity 0
|
||||
border-top-right-radius 2px
|
||||
border-bottom-right-radius 2px
|
||||
height 26px
|
||||
line-height 26px
|
||||
height 34px
|
||||
line-height 32px
|
||||
|
||||
.folderList-item:hover, .folderList-item--active:hover
|
||||
.folderList-item-tooltip
|
||||
@@ -69,9 +68,20 @@
|
||||
.folderList-item-name--folded
|
||||
@extend .folderList-item-name
|
||||
padding-left 7px
|
||||
text
|
||||
.folderList-item-icon
|
||||
font-size 9px
|
||||
|
||||
.folderList-item-icon
|
||||
padding-right: 10px
|
||||
|
||||
.folderList-item-reorder
|
||||
font-size: 9px
|
||||
padding: 10px 8px 10px 9px;
|
||||
color: rgba(147, 147, 149, 0.3)
|
||||
cursor: ns-resize
|
||||
&:before
|
||||
content: "\f142 \f142"
|
||||
|
||||
body[data-theme="white"]
|
||||
.folderList-item
|
||||
color $ui-inactive-text-color
|
||||
@@ -108,4 +118,42 @@ body[data-theme="dark"]
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.folderList-item
|
||||
&:hover
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
&:active
|
||||
color $ui-solarized-dark-text-color
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
|
||||
.folderList-item--active
|
||||
@extend .folderList-item
|
||||
color $ui-solarized-dark-text-color
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
&:active
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
&:hover
|
||||
color $ui-solarized-dark-text-color
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.folderList-item
|
||||
&:hover
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
&:active
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
|
||||
.folderList-item--active
|
||||
@extend .folderList-item
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
&:active
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
&:hover
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
@@ -10,8 +10,8 @@ import CSSModules from 'browser/lib/CSSModules'
|
||||
* @param {Array} storgaeList
|
||||
*/
|
||||
|
||||
const StorageList = ({storageList}) => (
|
||||
<div styleName='storageList'>
|
||||
const StorageList = ({storageList, isFolded}) => (
|
||||
<div styleName={isFolded ? 'storageList-folded' : 'storageList'}>
|
||||
{storageList.length > 0 ? storageList : (
|
||||
<div styleName='storgaeList-empty'>No storage mount.</div>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
top 180px
|
||||
overflow-y auto
|
||||
|
||||
.storageList-folded
|
||||
@extend .storageList
|
||||
width 44px
|
||||
|
||||
.storageList-empty
|
||||
padding 0 10px
|
||||
margin-top 15px
|
||||
|
||||
@@ -9,15 +9,26 @@ import CSSModules from 'browser/lib/CSSModules'
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Function} handleClickTagListItem
|
||||
* @param {Function} handleClickNarrowToTag
|
||||
* @param {bool} isActive
|
||||
* @param {bool} isRelated
|
||||
*/
|
||||
|
||||
const TagListItem = ({name, handleClickTagListItem, isActive}) => (
|
||||
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
|
||||
<span styleName='tagList-item-name'>
|
||||
{`# ${name}`}
|
||||
</span>
|
||||
</button>
|
||||
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isActive, isRelated, count}) => (
|
||||
<div styleName='tagList-itemContainer'>
|
||||
{isRelated
|
||||
? <button styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} onClick={() => handleClickNarrowToTag(name)}>
|
||||
<i className={isActive ? 'fa fa-minus-circle' : 'fa fa-plus-circle'} />
|
||||
</button>
|
||||
: <div styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} />
|
||||
}
|
||||
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
|
||||
<span styleName='tagList-item-name'>
|
||||
{`# ${name}`}
|
||||
<span styleName='tagList-item-count'>{count}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
TagListItem.propTypes = {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
.tagList-itemContainer
|
||||
display flex
|
||||
|
||||
.tagList-item
|
||||
display flex
|
||||
flex 1
|
||||
width 100%
|
||||
height 26px
|
||||
background-color transparent
|
||||
@@ -20,9 +24,16 @@
|
||||
color $ui-button-default-color
|
||||
background-color $ui-button-default--active-backgroundColor
|
||||
|
||||
.tagList-itemNarrow
|
||||
composes tagList-item
|
||||
flex none
|
||||
width 20px
|
||||
padding 0 4px
|
||||
|
||||
.tagList-item-active
|
||||
background-color $ui-button-default--active-backgroundColor
|
||||
display flex
|
||||
flex 1
|
||||
width 100%
|
||||
height 26px
|
||||
padding 0
|
||||
@@ -36,10 +47,16 @@
|
||||
background-color alpha($ui-button-default--active-backgroundColor, 60%)
|
||||
transition 0.2s
|
||||
|
||||
.tagList-itemNarrow-active
|
||||
composes tagList-item-active
|
||||
flex none
|
||||
width 20px
|
||||
padding 0 4px
|
||||
|
||||
.tagList-item-name
|
||||
display block
|
||||
flex 1
|
||||
padding 0 15px
|
||||
padding 0 8px 0 4px
|
||||
height 26px
|
||||
line-height 26px
|
||||
border-width 0 0 0 2px
|
||||
@@ -48,6 +65,12 @@
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
|
||||
.tagList-item-count
|
||||
float right
|
||||
line-height 26px
|
||||
padding-right 15px
|
||||
font-size 13px
|
||||
|
||||
body[data-theme="white"]
|
||||
.tagList-item
|
||||
color $ui-inactive-text-color
|
||||
@@ -63,6 +86,8 @@ body[data-theme="white"]
|
||||
color $ui-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||
.tagList-item-count
|
||||
color $ui-text-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.tagList-item
|
||||
@@ -81,4 +106,6 @@ body[data-theme="dark"]
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||
.tagList-item-count
|
||||
color $ui-dark-button--active-color
|
||||
|
||||
@@ -16,7 +16,9 @@ const TodoListPercentage = ({
|
||||
}) => (
|
||||
<div styleName='percentageBar' style={{display: isNaN(percentageOfTodo) ? 'none' : ''}}>
|
||||
<div styleName='progressBar' style={{width: `${percentageOfTodo}%`}}>
|
||||
<p styleName='percentageText'>{percentageOfTodo}%</p>
|
||||
<div styleName='progressBarInner'>
|
||||
<p styleName='percentageText'>{percentageOfTodo}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.percentageBar
|
||||
position absolute
|
||||
top 50px
|
||||
top 72px
|
||||
right 0px
|
||||
left 0px
|
||||
background-color #DADFE1
|
||||
@@ -16,17 +16,46 @@
|
||||
border-radius 2px
|
||||
transition 0.4s cubic-bezier(0.4, 0.4, 0, 1)
|
||||
|
||||
.progressBarInner
|
||||
padding 0 10px
|
||||
min-width 1px
|
||||
height 100%
|
||||
display -webkit-box
|
||||
display box
|
||||
justify-content center
|
||||
align-items center
|
||||
|
||||
|
||||
.percentageText
|
||||
color #f4f4f4
|
||||
padding: 2px 43%
|
||||
font-weight 600
|
||||
|
||||
body[data-theme="dark"]
|
||||
.percentageBar
|
||||
background-color #363A3D
|
||||
background-color #444444
|
||||
|
||||
.progressBar
|
||||
background-color: alpha(#939395, 50%)
|
||||
background-color: #1EC38B
|
||||
|
||||
.percentageText
|
||||
color $ui-dark-text-color
|
||||
color $ui-dark-text-color
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.percentageBar
|
||||
background-color #002b36
|
||||
|
||||
.progressBar
|
||||
background-color: #2aa198
|
||||
|
||||
.percentageText
|
||||
color #fdf6e3
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.percentageBar
|
||||
background-color #f92672
|
||||
|
||||
.progressBar
|
||||
background-color: #373831
|
||||
|
||||
.percentageText
|
||||
color #fdf6e3
|
||||
@@ -58,7 +58,7 @@ body
|
||||
.katex
|
||||
font 400 1.2em 'KaTeX_Main'
|
||||
line-height 1.2em
|
||||
white-space nowrap
|
||||
white-space initial
|
||||
text-indent 0
|
||||
.katex .mfrac>.vlist>span:nth-child(2)
|
||||
top 0 !important
|
||||
@@ -76,7 +76,10 @@ body
|
||||
justify-content left
|
||||
li
|
||||
label.taskListItem
|
||||
margin-left -2em
|
||||
margin-left -1.8em
|
||||
&.checked
|
||||
text-decoration line-through
|
||||
opacity 0.5
|
||||
div.math-rendered
|
||||
text-align center
|
||||
.math-failed
|
||||
@@ -102,7 +105,6 @@ a
|
||||
border-radius 5px
|
||||
margin -5px
|
||||
transition .1s
|
||||
display inline-block
|
||||
img
|
||||
vertical-align sub
|
||||
&:hover
|
||||
@@ -117,6 +119,7 @@ hr
|
||||
margin 15px 0
|
||||
h1, h2, h3, h4, h5, h6
|
||||
font-weight bold
|
||||
word-wrap break-word
|
||||
h1
|
||||
font-size 2.55em
|
||||
padding-bottom 0.3em
|
||||
@@ -154,6 +157,7 @@ p
|
||||
line-height 1.6em
|
||||
margin 0 0 1em
|
||||
white-space pre-line
|
||||
word-wrap break-word
|
||||
img
|
||||
max-width 100%
|
||||
strong, b
|
||||
@@ -174,6 +178,8 @@ ul
|
||||
margin-bottom 1em
|
||||
li
|
||||
display list-item
|
||||
&.taskListItem
|
||||
list-style none
|
||||
p
|
||||
margin 0
|
||||
&>li>ul, &>li>ol
|
||||
@@ -193,7 +199,6 @@ ol
|
||||
&>li>ul, &>li>ol
|
||||
margin 0
|
||||
code
|
||||
color #CC305F
|
||||
padding 0.2em 0.4em
|
||||
background-color #f7f7f7
|
||||
border-radius 3px
|
||||
@@ -214,6 +219,7 @@ pre
|
||||
background-color white
|
||||
&.CodeMirror
|
||||
height initial
|
||||
flex-wrap wrap
|
||||
&>code
|
||||
flex 1
|
||||
overflow-x auto
|
||||
@@ -223,6 +229,13 @@ pre
|
||||
padding 0
|
||||
border none
|
||||
border-radius 0
|
||||
&>span.filename
|
||||
width 100%
|
||||
border-radius: 5px 0px 0px 0px
|
||||
margin -8px 100% 8px -8px
|
||||
padding 0px 6px
|
||||
background-color #777;
|
||||
color white
|
||||
&>span.lineNumber
|
||||
display none
|
||||
font-size 1em
|
||||
@@ -280,6 +293,82 @@ kbd
|
||||
line-height 1
|
||||
padding 3px 5px
|
||||
|
||||
$admonition
|
||||
box-shadow 0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2)
|
||||
position relative
|
||||
margin 1.5625em 0
|
||||
padding 0 1.2rem
|
||||
border-left .4rem solid #448aff
|
||||
border-radius .2rem
|
||||
overflow auto
|
||||
|
||||
html .admonition>:last-child
|
||||
margin-bottom 1.2rem
|
||||
|
||||
.admonition .admonition
|
||||
margin 1em 0
|
||||
|
||||
.admonition p
|
||||
margin-top: 0.5em
|
||||
|
||||
$admonition-icon
|
||||
position absolute
|
||||
left 1.2rem
|
||||
font-family: "Material Icons"
|
||||
font-size: 24px
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizeLegibility;
|
||||
|
||||
/* Support for Firefox. */
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
/* Support for IE. */
|
||||
font-feature-settings: 'liga';
|
||||
|
||||
$admonition-title
|
||||
margin 0 -1.2rem
|
||||
padding .8rem 1.2rem .8rem 4rem
|
||||
border-bottom .1rem solid rgba(68,138,255,.1)
|
||||
background-color rgba(68,138,255,.1)
|
||||
font-weight 700
|
||||
|
||||
.admonition>.admonition-title:last-child
|
||||
margin-bottom 0
|
||||
|
||||
admonition_types = {
|
||||
note: {border-color: #448aff, title-color: rgba(68,138,255,.1), icon: "note"},
|
||||
hint: {border-color: #00bfa5, title-color: rgba(0,191,165,.1), icon: "info"},
|
||||
danger: {border-color: #ff1744, title-color: rgba(255,23,68,.1), icon: "block"},
|
||||
caution: {border-color: #ff9100, title-color: rgba(255,145,0,.1), icon: "warning"},
|
||||
error: {border-color: #ff1744, title-color: rgba(255,23,68,.1), icon: "error"},
|
||||
attention: {border-color: #64dd17, title-color: rgba(100,221,23,.1), icon: "priority_high"}
|
||||
}
|
||||
|
||||
for name, val in admonition_types
|
||||
.admonition.{name}
|
||||
@extend $admonition
|
||||
border-left-color: val[border-color]
|
||||
|
||||
.admonition.{name}>.admonition-title
|
||||
@extend $admonition-title
|
||||
border-bottom-color: .1rem solid val[title-color]
|
||||
background-color: val[title-color]
|
||||
|
||||
.admonition.{name}>.admonition-title:before
|
||||
@extend $admonition-icon
|
||||
color: val[border-color]
|
||||
content: val[icon]
|
||||
|
||||
themeDarkBackground = darken(#21252B, 10%)
|
||||
themeDarkText = #f9f9f9
|
||||
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
||||
@@ -329,4 +418,60 @@ body[data-theme="dark"]
|
||||
border-right solid 1px themeDarkTableBorder
|
||||
kbd
|
||||
background-color themeDarkBorder
|
||||
color themeDarkText
|
||||
color themeDarkText
|
||||
|
||||
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
|
||||
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
|
||||
themeSolarizedDarkTableHead = themeSolarizedDarkTableEven
|
||||
themeSolarizedDarkTableBorder = themeDarkBorder
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
color $ui-solarized-dark-text-color
|
||||
border-color themeDarkBorder
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
table
|
||||
thead
|
||||
tr
|
||||
background-color themeSolarizedDarkTableHead
|
||||
th
|
||||
border-color themeSolarizedDarkTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeSolarizedDarkTableBorder
|
||||
tbody
|
||||
tr:nth-child(2n + 1)
|
||||
background-color themeSolarizedDarkTableOdd
|
||||
tr:nth-child(2n)
|
||||
background-color themeSolarizedDarkTableEven
|
||||
td
|
||||
border-color themeSolarizedDarkTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeSolarizedDarkTableBorder
|
||||
|
||||
themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor
|
||||
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
|
||||
themeMonokaiTableHead = themeMonokaiTableEven
|
||||
themeMonokaiTableBorder = themeDarkBorder
|
||||
|
||||
body[data-theme="monokai"]
|
||||
color $ui-monokai-text-color
|
||||
border-color themeDarkBorder
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
table
|
||||
thead
|
||||
tr
|
||||
background-color themeMonokaiTableHead
|
||||
th
|
||||
border-color themeMonokaiTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeMonokaiTableBorder
|
||||
tbody
|
||||
tr:nth-child(2n + 1)
|
||||
background-color themeMonokaiTableOdd
|
||||
tr:nth-child(2n)
|
||||
background-color themeMonokaiTableEven
|
||||
td
|
||||
border-color themeMonokaiTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeMonokaiTableBorder
|
||||
kbd
|
||||
background-color themeDarkBackground
|
||||
@@ -1,118 +0,0 @@
|
||||
$search-height = 50px
|
||||
$nav-width = 175px
|
||||
$list-width = 250px
|
||||
|
||||
.root
|
||||
absolute top left right bottom
|
||||
|
||||
.search
|
||||
height $search-height
|
||||
padding 10px
|
||||
box-sizing border-box
|
||||
border-bottom $ui-border
|
||||
text-align center
|
||||
|
||||
.search-input
|
||||
height 30px
|
||||
width 100%
|
||||
margin 0 auto
|
||||
font-size 18px
|
||||
border none
|
||||
outline none
|
||||
text-align center
|
||||
background-color transparent
|
||||
|
||||
.result
|
||||
absolute left right bottom
|
||||
top $search-height
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
.result-nav
|
||||
user-select none
|
||||
absolute left top bottom
|
||||
width $nav-width
|
||||
background-color $ui-backgroundColor
|
||||
|
||||
.result-nav-filter
|
||||
margin-bottom 10px
|
||||
|
||||
.result-nav-filter-option
|
||||
height 25px
|
||||
line-height 25px
|
||||
padding 0 10px
|
||||
label
|
||||
cursor pointer
|
||||
|
||||
.result-nav-menu
|
||||
navButtonColor()
|
||||
height 32px
|
||||
padding 0 10px
|
||||
font-size 14px
|
||||
width 100%
|
||||
outline none
|
||||
text-align left
|
||||
line-height 32px
|
||||
box-sizing border-box
|
||||
cursor pointer
|
||||
|
||||
.result-nav-menu--active
|
||||
@extend .result-nav-menu
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
&:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.result-nav-storageList
|
||||
absolute bottom left right
|
||||
top 110px + 32px + 10px + 10px + 20px
|
||||
overflow-y auto
|
||||
|
||||
.result-list
|
||||
user-select none
|
||||
absolute top bottom
|
||||
left $nav-width
|
||||
width $list-width
|
||||
box-sizing border-box
|
||||
overflow-y auto
|
||||
box-shadow 2px 0 15px -8px #b1b1b1
|
||||
z-index 1
|
||||
|
||||
.result-detail
|
||||
absolute top bottom right
|
||||
left $nav-width + $list-width
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
background-color $ui-dark-backgroundColor
|
||||
.search
|
||||
border-color $ui-dark-borderColor
|
||||
.search-input
|
||||
color $ui-dark-text-color
|
||||
|
||||
.result
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
.result-nav
|
||||
background-color $ui-dark-backgroundColor
|
||||
label
|
||||
color $ui-dark-text-color
|
||||
|
||||
.result-nav-menu
|
||||
navDarkButtonColor()
|
||||
|
||||
.result-nav-menu--active
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-button--active-color
|
||||
&:hover
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.result-list
|
||||
border-color $ui-dark-borderColor
|
||||
box-shadow none
|
||||
top 0
|
||||
|
||||
.result-detail
|
||||
absolute top bottom right
|
||||
left $nav-width + $list-width
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
@@ -1,209 +0,0 @@
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './NoteDetail.styl'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import CodeMirror from 'codemirror'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
|
||||
const electron = require('electron')
|
||||
const { clipboard } = electron
|
||||
const path = require('path')
|
||||
|
||||
function pass (name) {
|
||||
switch (name) {
|
||||
case 'ejs':
|
||||
return 'Embedded Javascript'
|
||||
case 'html_ruby':
|
||||
return 'Embedded Ruby'
|
||||
case 'objectivec':
|
||||
return 'Objective C'
|
||||
case 'text':
|
||||
return 'Plain Text'
|
||||
default:
|
||||
return name
|
||||
}
|
||||
}
|
||||
function notify (title, options) {
|
||||
if (global.process.platform === 'win32') {
|
||||
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
||||
}
|
||||
return new window.Notification(title, options)
|
||||
}
|
||||
|
||||
class NoteDetail extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
snippetIndex: 0
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.note !== this.props.note) {
|
||||
this.setState({
|
||||
snippetIndex: 0
|
||||
}, () => {
|
||||
if (nextProps.note.type === 'SNIPPET_NOTE') {
|
||||
nextProps.note.snippets.forEach((snippet, index) => {
|
||||
this.refs['code-' + index].reload()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
selectPriorSnippet () {
|
||||
const { note } = this.props
|
||||
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
|
||||
this.setState({
|
||||
snippetIndex: (this.state.snippetIndex + note.snippets.length - 1) % note.snippets.length
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
selectNextSnippet () {
|
||||
const { note } = this.props
|
||||
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
|
||||
this.setState({
|
||||
snippetIndex: (this.state.snippetIndex + 1) % note.snippets.length
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
saveToClipboard () {
|
||||
const { note } = this.props
|
||||
|
||||
if (note.type === 'MARKDOWN_NOTE') {
|
||||
clipboard.writeText(note.content)
|
||||
} else {
|
||||
clipboard.writeText(note.snippets[this.state.snippetIndex].content)
|
||||
}
|
||||
|
||||
notify('Saved to Clipboard!', {
|
||||
body: 'Paste it wherever you want!',
|
||||
silent: true
|
||||
})
|
||||
}
|
||||
|
||||
handleTabButtonClick (e, index) {
|
||||
this.setState({
|
||||
snippetIndex: index
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { note, config } = this.props
|
||||
if (note == null) {
|
||||
return (
|
||||
<div styleName='root' />
|
||||
)
|
||||
}
|
||||
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||
|
||||
const storage = findStorage(note.storage)
|
||||
|
||||
if (note.type === 'SNIPPET_NOTE') {
|
||||
const tabList = note.snippets.map((snippet, index) => {
|
||||
const isActive = this.state.snippetIndex === index
|
||||
return <div styleName={isActive
|
||||
? 'tabList-item--active'
|
||||
: 'tabList-item'
|
||||
}
|
||||
key={index}
|
||||
>
|
||||
<button styleName='tabList-item-button'
|
||||
onClick={(e) => this.handleTabButtonClick(e, index)}
|
||||
>
|
||||
{snippet.name.trim().length > 0
|
||||
? snippet.name
|
||||
: <span styleName='tabList-item-unnamed'>
|
||||
Unnamed
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
})
|
||||
|
||||
const viewList = note.snippets.map((snippet, index) => {
|
||||
const isActive = this.state.snippetIndex === index
|
||||
|
||||
let syntax = CodeMirror.findModeByName(pass(snippet.mode))
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
|
||||
return <div styleName='tabView'
|
||||
key={index}
|
||||
style={{zIndex: isActive ? 5 : 4}}
|
||||
>
|
||||
{snippet.mode === 'markdown'
|
||||
? <MarkdownEditor styleName='tabView-content'
|
||||
config={config}
|
||||
value={snippet.content}
|
||||
ref={'code-' + index}
|
||||
storageKey={note.storage}
|
||||
/>
|
||||
: <CodeEditor styleName='tabView-content'
|
||||
mode={snippet.mode}
|
||||
value={snippet.content}
|
||||
theme={config.editor.theme}
|
||||
fontFamily={config.editor.fontFamily}
|
||||
fontSize={editorFontSize}
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorIndentSize}
|
||||
keyMap={config.editor.keyMap}
|
||||
readOnly
|
||||
ref={'code-' + index}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
})
|
||||
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='description'>
|
||||
<textarea styleName='description-textarea'
|
||||
style={{
|
||||
fontFamily: config.preview.fontFamily,
|
||||
fontSize: parseInt(config.preview.fontSize, 10)
|
||||
}}
|
||||
ref='description'
|
||||
placeholder='Description...'
|
||||
value={note.description}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div styleName='tabList'>
|
||||
{tabList}
|
||||
</div>
|
||||
{viewList}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<MarkdownPreview styleName='root'
|
||||
theme={config.ui.theme}
|
||||
fontSize={config.preview.fontSize}
|
||||
fontFamily={config.preview.fontFamily}
|
||||
codeBlockTheme={config.preview.codeBlockTheme}
|
||||
codeBlockFontFamily={config.editor.fontFamily}
|
||||
lineNumber={config.preview.lineNumber}
|
||||
indentSize={editorIndentSize}
|
||||
value={note.content}
|
||||
showCopyNotification={config.ui.showCopyNotification}
|
||||
storagePath={storage.path}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NoteDetail.propTypes = {
|
||||
}
|
||||
|
||||
export default CSSModules(NoteDetail, styles)
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
@import('../main/Detail/DetailVars.styl')
|
||||
|
||||
.root
|
||||
absolute top bottom left right
|
||||
bottom 30px
|
||||
margin 0 25px
|
||||
height 100%
|
||||
width 365px
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
.description
|
||||
absolute top left right
|
||||
height 80px
|
||||
box-sizing border-box
|
||||
|
||||
.description-textarea
|
||||
display block
|
||||
height 100%
|
||||
width 100%
|
||||
resize none
|
||||
border none
|
||||
padding 10px
|
||||
line-height 1.6
|
||||
box-sizing border-box
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
.tabList
|
||||
absolute left right
|
||||
top 80px
|
||||
box-sizing border-box
|
||||
height 30px
|
||||
display flex
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
.tabList-item
|
||||
position relative
|
||||
flex 1
|
||||
overflow hidden
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColorg
|
||||
|
||||
.tabList-item--active
|
||||
@extend .tabList-item
|
||||
border-bottom $ui-border
|
||||
|
||||
.tabList-item-button
|
||||
width 100%
|
||||
height 29px
|
||||
overflow ellipsis
|
||||
text-align left
|
||||
padding-right 30px
|
||||
padding-left 10px
|
||||
border none
|
||||
background-color transparent
|
||||
transition 0.15s
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
|
||||
.tabView
|
||||
absolute left right bottom
|
||||
top 130px
|
||||
|
||||
.tabView-content
|
||||
absolute top left right bottom
|
||||
box-sizing border-box
|
||||
height 100%
|
||||
width 100%
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
|
||||
.description
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
|
||||
.description-textarea
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
color white
|
||||
|
||||
.tabList
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
|
||||
.tabList-item
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
|
||||
.tabList-item-button
|
||||
border none
|
||||
color $ui-dark-text-color
|
||||
background-color transparent
|
||||
transition color background-color 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
color white
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
@@ -1,90 +0,0 @@
|
||||
import React from 'react'
|
||||
import NoteItem from 'browser/components/NoteItem'
|
||||
import moment from 'moment'
|
||||
|
||||
class NoteList extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
range: 0
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (this.props.search !== nextProps.search) {
|
||||
this.resetScroll()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
const { index } = this.props
|
||||
|
||||
if (index > -1) {
|
||||
const list = this.refs.root
|
||||
const item = list.childNodes[index]
|
||||
if (item == null) return null
|
||||
|
||||
const overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
|
||||
if (overflowBelow) {
|
||||
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
|
||||
}
|
||||
const overflowAbove = list.scrollTop > item.offsetTop
|
||||
if (overflowAbove) {
|
||||
list.scrollTop = item.offsetTop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetScroll () {
|
||||
this.refs.root.scrollTop = 0
|
||||
this.setState({
|
||||
range: 0
|
||||
})
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
const { notes } = this.props
|
||||
|
||||
if (e.target.offsetHeight + e.target.scrollTop > e.target.scrollHeight - 100 && notes.length > this.state.range * 10 + 10) {
|
||||
this.setState({
|
||||
range: this.state.range + 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { notes, index } = this.props
|
||||
|
||||
const notesList = notes
|
||||
.slice(0, 10 + 10 * this.state.range)
|
||||
.map((note, _index) => {
|
||||
const isActive = (index === _index)
|
||||
const key = `${note.storage}-${note.key}`
|
||||
const dateDisplay = moment(note.updatedAt).fromNow()
|
||||
|
||||
return (
|
||||
<NoteItem
|
||||
isActive={isActive}
|
||||
note={note}
|
||||
dateDisplay={dateDisplay}
|
||||
key={key}
|
||||
handleNoteClick={(e) => this.props.handleNoteClick(e, _index)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<div className={this.props.className}
|
||||
onScroll={(e) => this.handleScroll(e)}
|
||||
ref='root'
|
||||
>
|
||||
{notesList}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NoteList.propTypes = {
|
||||
}
|
||||
|
||||
export default NoteList
|
||||
@@ -1,77 +0,0 @@
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './StorageSection.styl'
|
||||
import StorageItem from 'browser/components/StorageItem'
|
||||
|
||||
class StorageSection extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isOpen: true
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleButtonClick (e) {
|
||||
this.setState({
|
||||
isOpen: !this.state.isOpen
|
||||
})
|
||||
}
|
||||
|
||||
handleHeaderClick (e) {
|
||||
const { storage } = this.props
|
||||
this.props.handleStorageButtonClick(e, storage.key)
|
||||
}
|
||||
|
||||
handleFolderClick (e, folder) {
|
||||
const { storage } = this.props
|
||||
this.props.handleFolderButtonClick(e, storage.key, folder.key)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { storage, filter } = this.props
|
||||
const folderList = storage.folders
|
||||
.map(folder => (
|
||||
<StorageItem
|
||||
key={folder.key}
|
||||
isActive={filter.type === 'FOLDER' && filter.folder === folder.key && filter.storage === storage.key}
|
||||
handleButtonClick={(e) => this.handleFolderClick(e, folder)}
|
||||
folderName={folder.name}
|
||||
folderColor={folder.color}
|
||||
isFolded={false}
|
||||
/>
|
||||
))
|
||||
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='header'>
|
||||
<button styleName='header-toggleButton'
|
||||
onClick={(e) => this.handleToggleButtonClick(e)}
|
||||
>
|
||||
<i className={this.state.isOpen
|
||||
? 'fa fa-caret-down'
|
||||
: 'fa fa-caret-right'
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
<button styleName={filter.type === 'STORAGE' && filter.storage === storage.key
|
||||
? 'header-name--active'
|
||||
: 'header-name'
|
||||
}
|
||||
onClick={(e) => this.handleHeaderClick(e)}
|
||||
>{storage.name}</button>
|
||||
</div>
|
||||
{this.state.isOpen &&
|
||||
<div styleName='folderList'>
|
||||
{folderList}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
StorageSection.propTypes = {
|
||||
}
|
||||
|
||||
export default CSSModules(StorageSection, styles)
|
||||
@@ -1,85 +0,0 @@
|
||||
.root
|
||||
position relative
|
||||
|
||||
.header
|
||||
height 26px
|
||||
.header-toggleButton
|
||||
absolute top left
|
||||
width 25px
|
||||
height 26px
|
||||
navButtonColor()
|
||||
border none
|
||||
outline none
|
||||
.header-name
|
||||
display block
|
||||
height 26px
|
||||
navButtonColor()
|
||||
padding 0 10px 0 25px
|
||||
font-size 14px
|
||||
width 100%
|
||||
text-align left
|
||||
line-height 26px
|
||||
box-sizing border-box
|
||||
cursor pointer
|
||||
outline none
|
||||
|
||||
.header-name--active
|
||||
@extend .header-name
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
&:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.folderList-item
|
||||
display block
|
||||
width 100%
|
||||
height 26px
|
||||
navButtonColor()
|
||||
padding 0 10px 0 25px
|
||||
font-size 14px
|
||||
width 100%
|
||||
text-align left
|
||||
line-height 26px
|
||||
box-sizing border-box
|
||||
cursor pointer
|
||||
outline none
|
||||
padding 0 10px
|
||||
margin 2px 0
|
||||
height 26px
|
||||
line-height 26px
|
||||
border-width 0 0 0 6px
|
||||
border-style solid
|
||||
border-color transparent
|
||||
|
||||
.folderList-item--active
|
||||
@extend .folderList-item
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
&:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
body[data-theme="dark"]
|
||||
.header-toggleButton
|
||||
navDarkButtonColor()
|
||||
.header-name
|
||||
navDarkButtonColor()
|
||||
|
||||
.header-name--active
|
||||
@extend .header-name
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
&:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.folderList-item
|
||||
navDarkButtonColor()
|
||||
border-width 0 0 0 6px
|
||||
border-style solid
|
||||
border-color transparent
|
||||
|
||||
.folderList-item--active
|
||||
@extend .folderList-item
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
&:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
@@ -1,357 +0,0 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { connect, Provider } from 'react-redux'
|
||||
import _ from 'lodash'
|
||||
import store from './store'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './FinderMain.styl'
|
||||
import StorageSection from './StorageSection'
|
||||
import NoteList from './NoteList'
|
||||
import NoteDetail from './NoteDetail'
|
||||
import SideNavFilter from 'browser/components/SideNavFilter'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
require('!!style!css!stylus?sourceMap!../main/global.styl')
|
||||
require('../lib/customMeta')
|
||||
require('./ipcClient.js')
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
const { Menu } = remote
|
||||
|
||||
function hideFinder () {
|
||||
const finderWindow = remote.getCurrentWindow()
|
||||
if (global.process.platform === 'win32') {
|
||||
finderWindow.blur()
|
||||
finderWindow.hide()
|
||||
}
|
||||
if (global.process.platform === 'darwin') {
|
||||
Menu.sendActionToFirstResponder('hide:')
|
||||
}
|
||||
remote.getCurrentWindow().hide()
|
||||
}
|
||||
|
||||
require('!!style!css!stylus?sourceMap!../styles/finder/index.styl')
|
||||
|
||||
class FinderMain extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
search: '',
|
||||
index: 0,
|
||||
filter: {
|
||||
includeSnippet: true,
|
||||
includeMarkdown: false,
|
||||
type: 'ALL',
|
||||
storage: null,
|
||||
folder: null
|
||||
}
|
||||
}
|
||||
|
||||
this.focusHandler = (e) => this.handleWindowFocus(e)
|
||||
this.blurHandler = (e) => this.handleWindowBlur(e)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.refs.search.focus()
|
||||
window.addEventListener('focus', this.focusHandler)
|
||||
window.addEventListener('blur', this.blurHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('focus', this.focusHandler)
|
||||
window.removeEventListener('blur', this.blurHandler)
|
||||
}
|
||||
|
||||
handleWindowFocus (e) {
|
||||
this.refs.search.focus()
|
||||
}
|
||||
|
||||
handleWindowBlur (e) {
|
||||
this.setState({
|
||||
search: ''
|
||||
})
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
this.refs.search.focus()
|
||||
if (e.keyCode === 9) {
|
||||
if (e.shiftKey) {
|
||||
this.refs.detail.selectPriorSnippet()
|
||||
} else {
|
||||
this.refs.detail.selectNextSnippet()
|
||||
}
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.keyCode === 38) {
|
||||
this.selectPrevious()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
if (e.keyCode === 40) {
|
||||
this.selectNext()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
if (e.keyCode === 13) {
|
||||
this.refs.detail.saveToClipboard()
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('COPY_FINDER')
|
||||
hideFinder()
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.keyCode === 27) {
|
||||
hideFinder()
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.keyCode === 91 || e.metaKey) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchChange (e) {
|
||||
this.setState({
|
||||
search: e.target.value,
|
||||
index: 0
|
||||
})
|
||||
}
|
||||
|
||||
selectArticle (article) {
|
||||
this.setState({currentArticle: article})
|
||||
}
|
||||
|
||||
selectPrevious () {
|
||||
if (this.state.index > 0) {
|
||||
this.setState({
|
||||
index: this.state.index - 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
selectNext () {
|
||||
if (this.state.index < this.noteCount - 1) {
|
||||
this.setState({
|
||||
index: this.state.index + 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleOnlySnippetCheckboxChange (e) {
|
||||
const { filter } = this.state
|
||||
filter.includeSnippet = e.target.checked
|
||||
this.setState({
|
||||
filter: filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleOnlyMarkdownCheckboxChange (e) {
|
||||
const { filter } = this.state
|
||||
filter.includeMarkdown = e.target.checked
|
||||
this.refs.list.resetScroll()
|
||||
this.setState({
|
||||
filter: filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleAllNotesButtonClick (e) {
|
||||
const { filter } = this.state
|
||||
filter.type = 'ALL'
|
||||
this.refs.list.resetScroll()
|
||||
this.setState({
|
||||
filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleStarredButtonClick (e) {
|
||||
const { filter } = this.state
|
||||
filter.type = 'STARRED'
|
||||
this.refs.list.resetScroll()
|
||||
this.setState({
|
||||
filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleStorageButtonClick (e, storage) {
|
||||
const { filter } = this.state
|
||||
filter.type = 'STORAGE'
|
||||
filter.storage = storage
|
||||
this.refs.list.resetScroll()
|
||||
this.setState({
|
||||
filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleFolderButtonClick (e, storage, folder) {
|
||||
const { filter } = this.state
|
||||
filter.type = 'FOLDER'
|
||||
filter.storage = storage
|
||||
filter.folder = folder
|
||||
this.refs.list.resetScroll()
|
||||
this.setState({
|
||||
filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleNoteClick (e, index) {
|
||||
this.setState({
|
||||
index
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { data, config } = this.props
|
||||
const { filter, search } = this.state
|
||||
const storageList = []
|
||||
for (const key in data.storageMap) {
|
||||
const storage = data.storageMap[key]
|
||||
const item = (
|
||||
<StorageSection
|
||||
filter={filter}
|
||||
storage={storage}
|
||||
key={storage.key}
|
||||
handleStorageButtonClick={(e, storage) => this.handleStorageButtonClick(e, storage)}
|
||||
handleFolderButtonClick={(e, storage, folder) => this.handleFolderButtonClick(e, storage, folder)}
|
||||
/>
|
||||
)
|
||||
storageList.push(item)
|
||||
}
|
||||
let notes = []
|
||||
let noteIds
|
||||
|
||||
switch (filter.type) {
|
||||
case 'STORAGE':
|
||||
noteIds = data.storageNoteMap[filter.storage]
|
||||
break
|
||||
case 'FOLDER':
|
||||
noteIds = data.folderNoteMap[filter.storage + '-' + filter.folder]
|
||||
break
|
||||
case 'STARRED':
|
||||
noteIds = data.starredSet
|
||||
}
|
||||
if (noteIds != null) {
|
||||
noteIds.forEach((id) => {
|
||||
notes.push(data.noteMap[id])
|
||||
})
|
||||
} else {
|
||||
for (const key in data.noteMap) {
|
||||
notes.push(data.noteMap[key])
|
||||
}
|
||||
}
|
||||
|
||||
if (!filter.includeSnippet && filter.includeMarkdown) {
|
||||
notes = notes.filter((note) => note.type === 'MARKDOWN_NOTE')
|
||||
} else if (filter.includeSnippet && !filter.includeMarkdown) {
|
||||
notes = notes.filter((note) => note.type === 'SNIPPET_NOTE')
|
||||
}
|
||||
|
||||
if (search.trim().length > 0) {
|
||||
const needle = new RegExp(_.escapeRegExp(search.trim()), 'i')
|
||||
notes = notes.filter((note) => note.title.match(needle))
|
||||
}
|
||||
notes = notes
|
||||
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
|
||||
|
||||
const activeNote = notes[this.state.index]
|
||||
this.noteCount = notes.length
|
||||
|
||||
return (
|
||||
<div className='Finder'
|
||||
styleName='root'
|
||||
ref='-1'
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
>
|
||||
<div styleName='search'>
|
||||
<input
|
||||
styleName='search-input'
|
||||
ref='search'
|
||||
value={search}
|
||||
placeholder='Search...'
|
||||
onChange={(e) => this.handleSearchChange(e)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='result'>
|
||||
<div styleName='result-nav'>
|
||||
<div styleName='result-nav-filter'>
|
||||
<div styleName='result-nav-filter-option'>
|
||||
<label>
|
||||
<input type='checkbox'
|
||||
checked={filter.includeSnippet}
|
||||
onChange={(e) => this.handleOnlySnippetCheckboxChange(e)}
|
||||
/> Only Snippets</label>
|
||||
</div>
|
||||
<div styleName='result-nav-filter-option'>
|
||||
<label>
|
||||
<input type='checkbox'
|
||||
checked={filter.includeMarkdown}
|
||||
onChange={(e) => this.handleOnlyMarkdownCheckboxChange(e)}
|
||||
/> Only Markdown</label>
|
||||
</div>
|
||||
</div>
|
||||
<SideNavFilter
|
||||
isHomeActive={filter.type === 'ALL'}
|
||||
handleAllNotesButtonClick={(e) => this.handleAllNotesButtonClick(e)}
|
||||
isStarredActive={filter.type === 'STARRED'}
|
||||
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
|
||||
/>
|
||||
<div styleName='result-nav-storageList'>
|
||||
{storageList}
|
||||
</div>
|
||||
</div>
|
||||
<NoteList styleName='result-list'
|
||||
storageMap={data.storageMap}
|
||||
notes={notes}
|
||||
ref='list'
|
||||
search={search}
|
||||
index={this.state.index}
|
||||
handleNoteClick={(e, _index) => this.handleNoteClick(e, _index)}
|
||||
/>
|
||||
<div styleName='result-detail'>
|
||||
<NoteDetail
|
||||
note={activeNote}
|
||||
config={config}
|
||||
ref='detail'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FinderMain.propTypes = {
|
||||
dispatch: PropTypes.func
|
||||
}
|
||||
|
||||
var Finder = connect((x) => x)(CSSModules(FinderMain, styles))
|
||||
|
||||
function refreshData () {
|
||||
// let data = dataStore.getData(true)
|
||||
}
|
||||
|
||||
ReactDOM.render((
|
||||
<Provider store={store}>
|
||||
<Finder />
|
||||
</Provider>
|
||||
), document.getElementById('content'), function () {
|
||||
refreshData()
|
||||
})
|
||||
@@ -1,124 +0,0 @@
|
||||
const nodeIpc = require('node-ipc')
|
||||
const { remote, ipcRenderer } = require('electron')
|
||||
const { app, Menu } = remote
|
||||
const path = require('path')
|
||||
const store = require('./store')
|
||||
const consts = require('browser/lib/consts')
|
||||
|
||||
nodeIpc.config.id = 'finder'
|
||||
nodeIpc.config.retry = 1500
|
||||
nodeIpc.config.silent = true
|
||||
|
||||
function killFinder () {
|
||||
const finderWindow = remote.getCurrentWindow()
|
||||
finderWindow.removeAllListeners()
|
||||
if (global.process.platform === 'darwin') {
|
||||
// Only OSX has another app process.
|
||||
nodeIpc.of.node.emit('quit-from-finder')
|
||||
} else {
|
||||
finderWindow.close()
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFinder () {
|
||||
const finderWindow = remote.getCurrentWindow()
|
||||
if (global.process.platform === 'darwin') {
|
||||
if (finderWindow.isVisible()) {
|
||||
finderWindow.hide()
|
||||
Menu.sendActionToFirstResponder('hide:')
|
||||
} else {
|
||||
nodeIpc.of.node.emit('request-data-from-finder')
|
||||
finderWindow.show()
|
||||
}
|
||||
} else {
|
||||
if (finderWindow.isVisible()) {
|
||||
finderWindow.blur()
|
||||
finderWindow.hide()
|
||||
} else {
|
||||
nodeIpc.of.node.emit('request-data-from-finder')
|
||||
finderWindow.show()
|
||||
finderWindow.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nodeIpc.connectTo(
|
||||
'node',
|
||||
path.join(app.getPath('userData'), 'boostnote.service'),
|
||||
function () {
|
||||
nodeIpc.of.node.on('error', function (err) {
|
||||
console.log(err)
|
||||
})
|
||||
nodeIpc.of.node.on('connect', function () {
|
||||
console.log('Conncted successfully')
|
||||
})
|
||||
nodeIpc.of.node.on('disconnect', function () {
|
||||
console.log('disconnected')
|
||||
})
|
||||
|
||||
nodeIpc.of.node.on('open-finder', function () {
|
||||
toggleFinder()
|
||||
})
|
||||
|
||||
ipcRenderer.on('open-finder-from-tray', function () {
|
||||
toggleFinder()
|
||||
})
|
||||
ipcRenderer.on('open-main-from-tray', function () {
|
||||
nodeIpc.of.node.emit('open-main-from-finder')
|
||||
})
|
||||
|
||||
ipcRenderer.on('quit-from-tray', function () {
|
||||
nodeIpc.of.node.emit('quit-from-finder')
|
||||
killFinder()
|
||||
})
|
||||
|
||||
nodeIpc.of.node.on('throttle-data', function (payload) {
|
||||
console.log('Received data from Main renderer')
|
||||
store.default.dispatch({
|
||||
type: 'THROTTLE_DATA',
|
||||
data: payload
|
||||
})
|
||||
})
|
||||
|
||||
nodeIpc.of.node.on('config-renew', function (payload) {
|
||||
const { config } = payload
|
||||
if (config.ui.theme === 'dark') {
|
||||
document.body.setAttribute('data-theme', 'dark')
|
||||
} else if (config.ui.theme === 'white') {
|
||||
document.body.setAttribute('data-theme', 'white')
|
||||
} else {
|
||||
document.body.setAttribute('data-theme', 'default')
|
||||
}
|
||||
|
||||
let editorTheme = document.getElementById('editorTheme')
|
||||
if (editorTheme == null) {
|
||||
editorTheme = document.createElement('link')
|
||||
editorTheme.setAttribute('id', 'editorTheme')
|
||||
editorTheme.setAttribute('rel', 'stylesheet')
|
||||
document.head.appendChild(editorTheme)
|
||||
}
|
||||
|
||||
config.editor.theme = consts.THEMES.some((theme) => theme === config.editor.theme)
|
||||
? config.editor.theme
|
||||
: 'default'
|
||||
|
||||
if (config.editor.theme !== 'default') {
|
||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css')
|
||||
}
|
||||
|
||||
store.default.dispatch({
|
||||
type: 'SET_CONFIG',
|
||||
config: config
|
||||
})
|
||||
})
|
||||
|
||||
nodeIpc.of.node.on('quit-finder-app', function () {
|
||||
nodeIpc.of.node.emit('quit-finder-app-confirm')
|
||||
killFinder()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const ipc = {}
|
||||
|
||||
module.exports = ipc
|
||||
@@ -1,51 +0,0 @@
|
||||
import { combineReducers, createStore } from 'redux'
|
||||
import { routerReducer } from 'react-router-redux'
|
||||
import { DEFAULT_CONFIG } from 'browser/main/lib/ConfigManager'
|
||||
|
||||
const defaultData = {
|
||||
storageMap: {},
|
||||
noteMap: {},
|
||||
starredSet: [],
|
||||
storageNoteMap: {},
|
||||
folderNoteMap: {},
|
||||
tagNoteMap: {}
|
||||
}
|
||||
|
||||
function data (state = defaultData, action) {
|
||||
switch (action.type) {
|
||||
case 'THROTTLE_DATA':
|
||||
console.log(action)
|
||||
state = action.data
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
function config (state = DEFAULT_CONFIG, action) {
|
||||
switch (action.type) {
|
||||
case 'INIT_CONFIG':
|
||||
case 'SET_CONFIG':
|
||||
return Object.assign({}, state, action.config)
|
||||
case 'SET_IS_SIDENAV_FOLDED':
|
||||
state.isSideNavFolded = action.isFolded
|
||||
return Object.assign({}, state)
|
||||
case 'SET_ZOOM':
|
||||
state.zoom = action.zoom
|
||||
return Object.assign({}, state)
|
||||
case 'SET_LIST_WIDTH':
|
||||
state.listWidth = action.listWidth
|
||||
return Object.assign({}, state)
|
||||
case 'SET_UI':
|
||||
return Object.assign({}, state, action.config)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
const reducer = combineReducers({
|
||||
data,
|
||||
config,
|
||||
routing: routerReducer
|
||||
})
|
||||
|
||||
const store = createStore(reducer)
|
||||
|
||||
export default store
|
||||
78
browser/lib/Languages.js
Normal file
78
browser/lib/Languages.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const languages = [
|
||||
{
|
||||
name: 'Albanian',
|
||||
locale: 'sq'
|
||||
},
|
||||
{
|
||||
name: 'Chinese (zh-CN)',
|
||||
locale: 'zh-CN'
|
||||
},
|
||||
{
|
||||
name: 'Chinese (zh-TW)',
|
||||
locale: 'zh-TW'
|
||||
},
|
||||
{
|
||||
name: 'Danish',
|
||||
locale: 'da'
|
||||
},
|
||||
{
|
||||
name: 'English',
|
||||
locale: 'en'
|
||||
},
|
||||
{
|
||||
name: 'French',
|
||||
locale: 'fr'
|
||||
},
|
||||
{
|
||||
name: 'German',
|
||||
locale: 'de'
|
||||
},
|
||||
{
|
||||
name: 'Hungarian',
|
||||
locale: 'hu'
|
||||
},
|
||||
{
|
||||
name: 'Japanese',
|
||||
locale: 'ja'
|
||||
},
|
||||
{
|
||||
name: 'Korean',
|
||||
locale: 'ko'
|
||||
},
|
||||
{
|
||||
name: 'Norwegian',
|
||||
locale: 'no'
|
||||
},
|
||||
{
|
||||
name: 'Polish',
|
||||
locale: 'pl'
|
||||
},
|
||||
{
|
||||
name: 'Portuguese',
|
||||
locale: 'pt'
|
||||
},
|
||||
{
|
||||
name: 'Russian',
|
||||
locale: 'ru'
|
||||
},
|
||||
{
|
||||
name: 'Spanish',
|
||||
locale: 'es-ES'
|
||||
}, {
|
||||
name: 'Turkish',
|
||||
locale: 'tr'
|
||||
}
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
getLocales () {
|
||||
return languages.reduce(function (localeList, locale) {
|
||||
localeList.push(locale.locale)
|
||||
return localeList
|
||||
}, [])
|
||||
},
|
||||
getLanguages () {
|
||||
return languages
|
||||
}
|
||||
}
|
||||
|
||||
23
browser/lib/confirmDeleteNote.js
Normal file
23
browser/lib/confirmDeleteNote.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import electron from 'electron'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
const { remote } = electron
|
||||
const { dialog } = remote
|
||||
|
||||
export function confirmDeleteNote (confirmDeletion, permanent) {
|
||||
if (confirmDeletion || permanent) {
|
||||
const alertConfig = {
|
||||
ype: 'warning',
|
||||
message: i18n.__('Confirm note deletion'),
|
||||
detail: i18n.__('This will permanently remove this note.'),
|
||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||
}
|
||||
|
||||
const dialogButtonIndex = dialog.showMessageBox(
|
||||
remote.getCurrentWindow(), alertConfig
|
||||
)
|
||||
|
||||
return dialogButtonIndex === 0
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -12,6 +12,10 @@ const themes = fs.readdirSync(themePath)
|
||||
})
|
||||
themes.splice(themes.indexOf('solarized'), 1, 'solarized dark', 'solarized light')
|
||||
|
||||
const snippetFile = process.env.NODE_ENV !== 'test'
|
||||
? path.join(app.getPath('userData'), 'snippets.json')
|
||||
: '' // return nothing as we specified different path to snippets.json in test
|
||||
|
||||
const consts = {
|
||||
FOLDER_COLORS: [
|
||||
'#E10051',
|
||||
@@ -31,7 +35,8 @@ const consts = {
|
||||
'Dodger Blue',
|
||||
'Violet Eggplant'
|
||||
],
|
||||
THEMES: ['default'].concat(themes)
|
||||
THEMES: ['default'].concat(themes),
|
||||
SNIPPET_FILE: snippetFile
|
||||
}
|
||||
|
||||
module.exports = consts
|
||||
|
||||
@@ -2,7 +2,7 @@ const { remote } = require('electron')
|
||||
const { Menu, MenuItem } = remote
|
||||
|
||||
function popup (templates) {
|
||||
let menu = new Menu()
|
||||
const menu = new Menu()
|
||||
templates.forEach((item) => {
|
||||
menu.append(new MenuItem(item))
|
||||
})
|
||||
|
||||
14
browser/lib/convertModeName.js
Normal file
14
browser/lib/convertModeName.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export default function convertModeName (name) {
|
||||
switch (name) {
|
||||
case 'ejs':
|
||||
return 'Embedded Javascript'
|
||||
case 'html_ruby':
|
||||
return 'Embedded Ruby'
|
||||
case 'objectivec':
|
||||
return 'Objective C'
|
||||
case 'text':
|
||||
return 'Plain Text'
|
||||
default:
|
||||
return name
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import CodeMirror from 'codemirror'
|
||||
import 'codemirror-mode-elixir'
|
||||
|
||||
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']})
|
||||
CodeMirror.modeInfo.push({name: 'Elixir', mime: 'text/x-elixir', mode: 'elixir', ext: ['ex']})
|
||||
|
||||
@@ -5,7 +5,7 @@ export function getTodoStatus (content) {
|
||||
|
||||
splitted.forEach((line) => {
|
||||
const trimmedLine = line.trim()
|
||||
if (trimmedLine.match(/^[\+\-\*] \[\s|x\] ./)) {
|
||||
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./)) {
|
||||
numberOfTodo++
|
||||
}
|
||||
if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) {
|
||||
|
||||
17
browser/lib/i18n.js
Normal file
17
browser/lib/i18n.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const path = require('path')
|
||||
const { remote } = require('electron')
|
||||
const { app } = remote
|
||||
const { getLocales } = require('./Languages.js')
|
||||
|
||||
// load package for localization
|
||||
const i18n = new (require('i18n-2'))({
|
||||
// setup some locales - other locales default to the first locale
|
||||
locales: getLocales(),
|
||||
extension: '.json',
|
||||
directory: process.env.NODE_ENV === 'production'
|
||||
? path.join(app.getAppPath(), './locales')
|
||||
: path.resolve('./locales'),
|
||||
devMode: false
|
||||
})
|
||||
|
||||
export default i18n
|
||||
@@ -1,7 +1,11 @@
|
||||
const crypto = require('crypto')
|
||||
const _ = require('lodash')
|
||||
const uuidv4 = require('uuid/v4')
|
||||
|
||||
module.exports = function (length) {
|
||||
if (!_.isFinite(length)) length = 10
|
||||
module.exports = function (uuid) {
|
||||
if (typeof uuid === typeof true && uuid) {
|
||||
return uuidv4()
|
||||
}
|
||||
const length = 10
|
||||
return crypto.randomBytes(length).toString('hex')
|
||||
}
|
||||
|
||||
23
browser/lib/markdown-it-sanitize-html.js
Normal file
23
browser/lib/markdown-it-sanitize-html.js
Normal file
@@ -0,0 +1,23 @@
|
||||
'use strict'
|
||||
|
||||
import sanitizeHtml from 'sanitize-html'
|
||||
|
||||
module.exports = function sanitizePlugin (md, options) {
|
||||
options = options || {}
|
||||
|
||||
md.core.ruler.after('linkify', 'sanitize_inline', state => {
|
||||
for (let tokenIdx = 0; tokenIdx < state.tokens.length; tokenIdx++) {
|
||||
if (state.tokens[tokenIdx].type === 'html_block') {
|
||||
state.tokens[tokenIdx].content = sanitizeHtml(state.tokens[tokenIdx].content, options)
|
||||
}
|
||||
if (state.tokens[tokenIdx].type === 'inline') {
|
||||
const inlineTokens = state.tokens[tokenIdx].children
|
||||
for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
|
||||
if (inlineTokens[childIdx].type === 'html_inline') {
|
||||
inlineTokens[childIdx].content = sanitizeHtml(inlineTokens[childIdx].content, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,157 +1,248 @@
|
||||
import markdownit from 'markdown-it'
|
||||
import sanitize from './markdown-it-sanitize-html'
|
||||
import emoji from 'markdown-it-emoji'
|
||||
import math from '@rokt33r/markdown-it-math'
|
||||
import smartArrows from 'markdown-it-smartarrows'
|
||||
import _ from 'lodash'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import katex from 'katex'
|
||||
import { lastFindInArray } from './utils'
|
||||
|
||||
// FIXME We should not depend on global variable.
|
||||
const katex = window.katex
|
||||
|
||||
function createGutter (str) {
|
||||
const lc = (str.match(/\n/g) || []).length
|
||||
function createGutter (str, firstLineNumber) {
|
||||
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
|
||||
const lastLineNumber = (str.match(/\n/g) || []).length + firstLineNumber - 1
|
||||
const lines = []
|
||||
for (let i = 1; i <= lc; i++) {
|
||||
for (let i = firstLineNumber; i <= lastLineNumber; i++) {
|
||||
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
|
||||
}
|
||||
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
|
||||
}
|
||||
|
||||
var md = markdownit({
|
||||
typographer: true,
|
||||
linkify: true,
|
||||
html: true,
|
||||
xhtmlOut: true,
|
||||
breaks: true,
|
||||
highlight: function (str, lang) {
|
||||
if (lang === 'flowchart') {
|
||||
return `<pre class="flowchart">${str}</pre>`
|
||||
}
|
||||
if (lang === 'sequence') {
|
||||
return `<pre class="sequence">${str}</pre>`
|
||||
}
|
||||
return '<pre class="code">' +
|
||||
createGutter(str) +
|
||||
'<code class="' + lang + '">' +
|
||||
str +
|
||||
'</code></pre>'
|
||||
}
|
||||
})
|
||||
md.use(emoji, {
|
||||
shortcuts: {}
|
||||
})
|
||||
md.use(math, {
|
||||
inlineRenderer: function (str) {
|
||||
let output = ''
|
||||
try {
|
||||
output = katex.renderToString(str.trim())
|
||||
} catch (err) {
|
||||
output = `<span class="katex-error">${err.message}</span>`
|
||||
}
|
||||
return output
|
||||
},
|
||||
blockRenderer: function (str) {
|
||||
let output = ''
|
||||
try {
|
||||
output = katex.renderToString(str.trim(), {displayMode: true})
|
||||
} catch (err) {
|
||||
output = `<div class="katex-error">${err.message}</div>`
|
||||
}
|
||||
return output
|
||||
}
|
||||
})
|
||||
md.use(require('markdown-it-imsize'))
|
||||
md.use(require('markdown-it-footnote'))
|
||||
md.use(require('markdown-it-multimd-table'))
|
||||
md.use(require('markdown-it-named-headers'), {
|
||||
slugify: (header) => {
|
||||
return encodeURI(header.trim()
|
||||
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
|
||||
.replace(/\s+/g, '-'))
|
||||
.replace(/\-+$/, '')
|
||||
}
|
||||
})
|
||||
md.use(require('markdown-it-kbd'))
|
||||
md.use(require('markdown-it-plantuml'))
|
||||
class Markdown {
|
||||
constructor (options = {}) {
|
||||
const config = ConfigManager.get()
|
||||
const defaultOptions = {
|
||||
typographer: config.preview.smartQuotes,
|
||||
linkify: true,
|
||||
html: true,
|
||||
xhtmlOut: true,
|
||||
breaks: config.preview.breaks,
|
||||
highlight: function (str, lang) {
|
||||
const delimiter = ':'
|
||||
const langInfo = lang.split(delimiter)
|
||||
const langType = langInfo[0]
|
||||
const fileName = langInfo[1] || ''
|
||||
const firstLineNumber = parseInt(langInfo[2], 10)
|
||||
|
||||
// Override task item
|
||||
md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
||||
let content, terminate, i, l, token
|
||||
let nextLine = startLine + 1
|
||||
const terminatorRules = state.md.block.ruler.getRules('paragraph')
|
||||
const endLine = state.lineMax
|
||||
if (langType === 'flowchart') {
|
||||
return `<pre class="flowchart">${str}</pre>`
|
||||
}
|
||||
if (langType === 'sequence') {
|
||||
return `<pre class="sequence">${str}</pre>`
|
||||
}
|
||||
return '<pre class="code CodeMirror">' +
|
||||
'<span class="filename">' + fileName + '</span>' +
|
||||
createGutter(str, firstLineNumber) +
|
||||
'<code class="' + langType + '">' +
|
||||
str +
|
||||
'</code></pre>'
|
||||
},
|
||||
sanitize: 'STRICT'
|
||||
}
|
||||
|
||||
// jump line-by-line until empty one or EOF
|
||||
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
|
||||
// this would be a code block normally, but after paragraph
|
||||
// it's considered a lazy continuation regardless of what's there
|
||||
if (state.sCount[nextLine] - state.blkIndent > 3) { continue }
|
||||
const updatedOptions = Object.assign(defaultOptions, options)
|
||||
this.md = markdownit(updatedOptions)
|
||||
|
||||
// quirk for blockquotes, this line should already be checked by that rule
|
||||
if (state.sCount[nextLine] < 0) { continue }
|
||||
if (updatedOptions.sanitize !== 'NONE') {
|
||||
const allowedTags = ['iframe', 'input', 'b',
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8', 'br', 'b', 'i', 'strong', 'em', 'a', 'pre', 'code', 'img', 'tt',
|
||||
'div', 'ins', 'del', 'sup', 'sub', 'p', 'ol', 'ul', 'table', 'thead', 'tbody', 'tfoot', 'blockquote',
|
||||
'dl', 'dt', 'dd', 'kbd', 'q', 'samp', 'var', 'hr', 'ruby', 'rt', 'rp', 'li', 'tr', 'td', 'th', 's', 'strike', 'summary', 'details'
|
||||
]
|
||||
const allowedAttributes = [
|
||||
'abbr', 'accept', 'accept-charset',
|
||||
'accesskey', 'action', 'align', 'alt', 'axis',
|
||||
'border', 'cellpadding', 'cellspacing', 'char',
|
||||
'charoff', 'charset', 'checked',
|
||||
'clear', 'cols', 'colspan', 'color',
|
||||
'compact', 'coords', 'datetime', 'dir',
|
||||
'disabled', 'enctype', 'for', 'frame',
|
||||
'headers', 'height', 'hreflang',
|
||||
'hspace', 'ismap', 'label', 'lang',
|
||||
'maxlength', 'media', 'method',
|
||||
'multiple', 'name', 'nohref', 'noshade',
|
||||
'nowrap', 'open', 'prompt', 'readonly', 'rel', 'rev',
|
||||
'rows', 'rowspan', 'rules', 'scope',
|
||||
'selected', 'shape', 'size', 'span',
|
||||
'start', 'summary', 'tabindex', 'target',
|
||||
'title', 'type', 'usemap', 'valign', 'value',
|
||||
'vspace', 'width', 'itemprop'
|
||||
]
|
||||
|
||||
// Some tags can terminate paragraph without empty line.
|
||||
terminate = false
|
||||
for (i = 0, l = terminatorRules.length; i < l; i++) {
|
||||
if (terminatorRules[i](state, nextLine, endLine, true)) {
|
||||
terminate = true
|
||||
break
|
||||
if (updatedOptions.sanitize === 'ALLOW_STYLES') {
|
||||
allowedTags.push('style')
|
||||
allowedAttributes.push('style')
|
||||
}
|
||||
|
||||
// Sanitize use rinput before other plugins
|
||||
this.md.use(sanitize, {
|
||||
allowedTags,
|
||||
allowedAttributes: {
|
||||
'*': allowedAttributes,
|
||||
'a': ['href'],
|
||||
'div': ['itemscope', 'itemtype'],
|
||||
'blockquote': ['cite'],
|
||||
'del': ['cite'],
|
||||
'ins': ['cite'],
|
||||
'q': ['cite'],
|
||||
'img': ['src', 'width', 'height'],
|
||||
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
|
||||
'input': ['type', 'id', 'checked']
|
||||
},
|
||||
allowedIframeHostnames: ['www.youtube.com']
|
||||
})
|
||||
}
|
||||
if (terminate) { break }
|
||||
|
||||
this.md.use(emoji, {
|
||||
shortcuts: {}
|
||||
})
|
||||
this.md.use(math, {
|
||||
inlineOpen: config.preview.latexInlineOpen,
|
||||
inlineClose: config.preview.latexInlineClose,
|
||||
blockOpen: config.preview.latexBlockOpen,
|
||||
blockClose: config.preview.latexBlockClose,
|
||||
inlineRenderer: function (str) {
|
||||
let output = ''
|
||||
try {
|
||||
output = katex.renderToString(str.trim())
|
||||
} catch (err) {
|
||||
output = `<span class="katex-error">${err.message}</span>`
|
||||
}
|
||||
return output
|
||||
},
|
||||
blockRenderer: function (str) {
|
||||
let output = ''
|
||||
try {
|
||||
output = katex.renderToString(str.trim(), { displayMode: true })
|
||||
} catch (err) {
|
||||
output = `<div class="katex-error">${err.message}</div>`
|
||||
}
|
||||
return output
|
||||
}
|
||||
})
|
||||
this.md.use(require('markdown-it-imsize'))
|
||||
this.md.use(require('markdown-it-footnote'))
|
||||
this.md.use(require('markdown-it-multimd-table'))
|
||||
this.md.use(require('markdown-it-named-headers'), {
|
||||
slugify: (header) => {
|
||||
return encodeURI(header.trim()
|
||||
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
|
||||
.replace(/\s+/g, '-'))
|
||||
.replace(/\-+$/, '')
|
||||
}
|
||||
})
|
||||
this.md.use(require('markdown-it-kbd'))
|
||||
this.md.use(require('markdown-it-admonition'))
|
||||
|
||||
const deflate = require('markdown-it-plantuml/lib/deflate')
|
||||
this.md.use(require('markdown-it-plantuml'), '', {
|
||||
generateSource: function (umlCode) {
|
||||
const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
|
||||
const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/svg'
|
||||
const s = unescape(encodeURIComponent(umlCode))
|
||||
const zippedCode = deflate.encode64(
|
||||
deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9)
|
||||
)
|
||||
return `${serverAddress}/${zippedCode}`
|
||||
}
|
||||
})
|
||||
|
||||
// Override task item
|
||||
this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
||||
let content, terminate, i, l, token
|
||||
let nextLine = startLine + 1
|
||||
const terminatorRules = state.md.block.ruler.getRules('paragraph')
|
||||
const endLine = state.lineMax
|
||||
|
||||
// jump line-by-line until empty one or EOF
|
||||
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
|
||||
// this would be a code block normally, but after paragraph
|
||||
// it's considered a lazy continuation regardless of what's there
|
||||
if (state.sCount[nextLine] - state.blkIndent > 3) { continue }
|
||||
|
||||
// quirk for blockquotes, this line should already be checked by that rule
|
||||
if (state.sCount[nextLine] < 0) { continue }
|
||||
|
||||
// Some tags can terminate paragraph without empty line.
|
||||
terminate = false
|
||||
for (i = 0, l = terminatorRules.length; i < l; i++) {
|
||||
if (terminatorRules[i](state, nextLine, endLine, true)) {
|
||||
terminate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (terminate) { break }
|
||||
}
|
||||
|
||||
content = state.getLines(startLine, nextLine, state.blkIndent, false).trim()
|
||||
|
||||
state.line = nextLine
|
||||
|
||||
token = state.push('paragraph_open', 'p', 1)
|
||||
token.map = [startLine, state.line]
|
||||
|
||||
if (state.parentType === 'list') {
|
||||
const match = content.match(/^\[( |x)\] ?(.+)/i)
|
||||
if (match) {
|
||||
const liToken = lastFindInArray(state.tokens, token => token.type === 'list_item_open')
|
||||
if (liToken) {
|
||||
if (!liToken.attrs) {
|
||||
liToken.attrs = []
|
||||
}
|
||||
liToken.attrs.push(['class', 'taskListItem'])
|
||||
}
|
||||
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
|
||||
}
|
||||
}
|
||||
|
||||
token = state.push('inline', '', 0)
|
||||
token.content = content
|
||||
token.map = [startLine, state.line]
|
||||
token.children = []
|
||||
|
||||
token = state.push('paragraph_close', 'p', -1)
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if (config.preview.smartArrows) {
|
||||
this.md.use(smartArrows)
|
||||
}
|
||||
|
||||
// Add line number attribute for scrolling
|
||||
const originalRender = this.md.renderer.render
|
||||
this.md.renderer.render = (tokens, options, env) => {
|
||||
tokens.forEach((token) => {
|
||||
switch (token.type) {
|
||||
case 'heading_open':
|
||||
case 'paragraph_open':
|
||||
case 'blockquote_open':
|
||||
case 'table_open':
|
||||
token.attrPush(['data-line', token.map[0]])
|
||||
}
|
||||
})
|
||||
const result = originalRender.call(this.md.renderer, tokens, options, env)
|
||||
return result
|
||||
}
|
||||
// FIXME We should not depend on global variable.
|
||||
window.md = this.md
|
||||
}
|
||||
|
||||
content = state.getLines(startLine, nextLine, state.blkIndent, false).trim()
|
||||
|
||||
state.line = nextLine
|
||||
|
||||
token = state.push('paragraph_open', 'p', 1)
|
||||
token.map = [ startLine, state.line ]
|
||||
|
||||
if (state.parentType === 'list') {
|
||||
const match = content.match(/^\[( |x)\] ?(.+)/i)
|
||||
if (match) {
|
||||
content = `<label class='taskListItem' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
|
||||
}
|
||||
}
|
||||
|
||||
token = state.push('inline', '', 0)
|
||||
token.content = content
|
||||
token.map = [ startLine, state.line ]
|
||||
token.children = []
|
||||
|
||||
token = state.push('paragraph_close', 'p', -1)
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// Add line number attribute for scrolling
|
||||
const originalRender = md.renderer.render
|
||||
md.renderer.render = function render (tokens, options, env) {
|
||||
tokens.forEach((token) => {
|
||||
switch (token.type) {
|
||||
case 'heading_open':
|
||||
case 'paragraph_open':
|
||||
case 'blockquote_open':
|
||||
case 'table_open':
|
||||
token.attrPush(['data-line', token.map[0]])
|
||||
}
|
||||
})
|
||||
const result = originalRender.call(md.renderer, tokens, options, env)
|
||||
return result
|
||||
}
|
||||
// FIXME We should not depend on global variable.
|
||||
window.md = md
|
||||
|
||||
function normalizeLinkText (linkText) {
|
||||
return md.normalizeLinkText(linkText)
|
||||
}
|
||||
|
||||
const markdown = {
|
||||
render: function markdown (content) {
|
||||
render (content) {
|
||||
if (!_.isString(content)) content = ''
|
||||
const renderedContent = md.render(content)
|
||||
return renderedContent
|
||||
},
|
||||
normalizeLinkText
|
||||
return this.md.render(content)
|
||||
}
|
||||
}
|
||||
|
||||
export default markdown
|
||||
export default Markdown
|
||||
|
||||
|
||||
0
browser/lib/markdown2.js
Normal file
0
browser/lib/markdown2.js
Normal file
@@ -4,39 +4,28 @@ export default function searchFromNotes (notes, search) {
|
||||
if (search.trim().length === 0) return []
|
||||
const searchBlocks = search.split(' ').filter(block => { return block !== '' })
|
||||
|
||||
let foundNotes = findByWord(notes, searchBlocks[0])
|
||||
let foundNotes = notes
|
||||
searchBlocks.forEach((block) => {
|
||||
foundNotes = findByWord(foundNotes, block)
|
||||
if (block.match(/^#.+/)) {
|
||||
foundNotes = foundNotes.concat(findByTag(notes, block))
|
||||
}
|
||||
foundNotes = findByWordOrTag(foundNotes, block)
|
||||
})
|
||||
return foundNotes
|
||||
}
|
||||
|
||||
function findByTag (notes, block) {
|
||||
const tag = block.match(/#(.+)/)[1]
|
||||
const regExp = new RegExp(_.escapeRegExp(tag), 'i')
|
||||
function findByWordOrTag (notes, block) {
|
||||
let tag = block
|
||||
if (tag.match(/^#.+/)) {
|
||||
tag = tag.match(/#(.+)/)[1]
|
||||
}
|
||||
const tagRegExp = new RegExp(_.escapeRegExp(tag), 'i')
|
||||
const wordRegExp = new RegExp(_.escapeRegExp(block), 'i')
|
||||
return notes.filter((note) => {
|
||||
if (!_.isArray(note.tags)) return false
|
||||
return note.tags.some((_tag) => {
|
||||
return _tag.match(regExp)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function findByWord (notes, block) {
|
||||
const regExp = new RegExp(_.escapeRegExp(block), 'i')
|
||||
return notes.filter((note) => {
|
||||
if (_.isArray(note.tags) && note.tags.some((_tag) => {
|
||||
return _tag.match(regExp)
|
||||
})) {
|
||||
if (_.isArray(note.tags) && note.tags.some((_tag) => _tag.match(tagRegExp))) {
|
||||
return true
|
||||
}
|
||||
if (note.type === 'SNIPPET_NOTE') {
|
||||
return note.description.match(regExp)
|
||||
return note.description.match(wordRegExp)
|
||||
} else if (note.type === 'MARKDOWN_NOTE') {
|
||||
return note.content.match(regExp)
|
||||
return note.content.match(wordRegExp)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
78
browser/lib/utils.js
Normal file
78
browser/lib/utils.js
Normal file
@@ -0,0 +1,78 @@
|
||||
export function lastFindInArray (array, callback) {
|
||||
for (let i = array.length - 1; i >= 0; --i) {
|
||||
if (callback(array[i], i, array)) {
|
||||
return array[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function escapeHtmlCharacters (text) {
|
||||
const matchHtmlRegExp = /["'&<>]/
|
||||
const str = '' + text
|
||||
const match = matchHtmlRegExp.exec(str)
|
||||
|
||||
if (!match) {
|
||||
return str
|
||||
}
|
||||
|
||||
let escape
|
||||
let html = ''
|
||||
let index = 0
|
||||
let lastIndex = 0
|
||||
|
||||
for (index = match.index; index < str.length; index++) {
|
||||
switch (str.charCodeAt(index)) {
|
||||
case 34: // "
|
||||
escape = '"'
|
||||
break
|
||||
case 38: // &
|
||||
escape = '&'
|
||||
break
|
||||
case 39: // '
|
||||
escape = '''
|
||||
break
|
||||
case 60: // <
|
||||
escape = '<'
|
||||
break
|
||||
case 62: // >
|
||||
escape = '>'
|
||||
break
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if (lastIndex !== index) {
|
||||
html += str.substring(lastIndex, index)
|
||||
}
|
||||
|
||||
lastIndex = index + 1
|
||||
html += escape
|
||||
}
|
||||
|
||||
return lastIndex !== index
|
||||
? html + str.substring(lastIndex, index)
|
||||
: html
|
||||
}
|
||||
|
||||
export function isObjectEqual (a, b) {
|
||||
const aProps = Object.getOwnPropertyNames(a)
|
||||
const bProps = Object.getOwnPropertyNames(b)
|
||||
|
||||
if (aProps.length !== bProps.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (var i = 0; i < aProps.length; i++) {
|
||||
const propName = aProps[i]
|
||||
if (a[propName] !== b[propName]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export default {
|
||||
lastFindInArray,
|
||||
escapeHtmlCharacters,
|
||||
isObjectEqual
|
||||
}
|
||||
@@ -20,5 +20,20 @@
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
background-color $ui-dark-backgroundColor
|
||||
border-left 1px solid $ui-dark-borderColor
|
||||
.empty-message
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
border-left 1px solid $ui-solarized-dark-borderColor
|
||||
.empty-message
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
border-left 1px solid $ui-monokai-borderColor
|
||||
.empty-message
|
||||
color $ui-monokai-text-color
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './FolderSelect.styl'
|
||||
import _ from 'lodash'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
class FolderSelect extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -249,7 +250,7 @@ class FolderSelect extends React.Component {
|
||||
<input styleName='search-input'
|
||||
ref='search'
|
||||
value={this.state.search}
|
||||
placeholder='Folder...'
|
||||
placeholder={i18n.__('Folder...')}
|
||||
onChange={(e) => this.handleSearchInputChange(e)}
|
||||
onBlur={(e) => this.handleSearchInputBlur(e)}
|
||||
onKeyDown={(e) => this.handleSearchInputKeyDown(e)}
|
||||
|
||||
@@ -3,20 +3,14 @@
|
||||
border solid 1px transparent
|
||||
vertical-align middle
|
||||
border-radius 2px
|
||||
height 30px
|
||||
transition 0.15s
|
||||
user-select none
|
||||
margin-right 10px
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
|
||||
.root--search, .root--focus
|
||||
@extend .root
|
||||
background-color $ui-noteDetail-backgroundColor = #fff
|
||||
border-color $ui-input--focus-borderColor
|
||||
width 154px
|
||||
height 30px
|
||||
&:hover
|
||||
border-color $ui-input--focus-borderColor = #fff
|
||||
|
||||
.idle
|
||||
position relative
|
||||
@@ -139,3 +133,29 @@ body[data-theme="dark"]
|
||||
color $ui-dark-button--active-color
|
||||
.search-optionList-item-name-surfix
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
color $ui-dark-text-color
|
||||
&:hover
|
||||
color white
|
||||
background-color $ui-monokai-button--hover-backgroundColor
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
.search-optionList
|
||||
color white
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
|
||||
.search-optionList-item
|
||||
&:hover
|
||||
background-color lighten($ui-monokai-button--hover-backgroundColor, 15%)
|
||||
|
||||
.search-optionList-item--active
|
||||
background-color $ui-monokai-button--active-backgroundColor
|
||||
color $ui-monokai-button--active-color
|
||||
&:hover
|
||||
background-color $ui-monokai-button--active-backgroundColor
|
||||
color $ui-monokai-button--active-color
|
||||
.search-optionList-item-name-surfix
|
||||
color $ui-monokai-inactive-text-color
|
||||
|
||||
20
browser/main/Detail/FullscreenButton.js
Normal file
20
browser/main/Detail/FullscreenButton.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './FullscreenButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const FullscreenButton = ({
|
||||
onClick
|
||||
}) => (
|
||||
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
||||
<span styleName='tooltip'>{i18n.__('Fullscreen')}</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
FullscreenButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(FullscreenButton, styles)
|
||||
22
browser/main/Detail/FullscreenButton.styl
Normal file
22
browser/main/Detail/FullscreenButton.styl
Normal file
@@ -0,0 +1,22 @@
|
||||
.control-fullScreenButton
|
||||
top 80px
|
||||
topBarButtonRight()
|
||||
&:hover .tooltip
|
||||
opacity 1
|
||||
|
||||
.tooltip
|
||||
tooltip()
|
||||
position absolute
|
||||
pointer-events none
|
||||
top 50px
|
||||
right 70px
|
||||
z-index 200
|
||||
padding 5px
|
||||
line-height normal
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
body[data-theme="dark"]
|
||||
.control-fullScreenButton
|
||||
topBarButtonDark()
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './InfoButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const InfoButton = ({
|
||||
onClick
|
||||
@@ -10,6 +11,7 @@ const InfoButton = ({
|
||||
onClick={(e) => onClick(e)}
|
||||
>
|
||||
<img className='infoButton' src='../resources/icon/icon-info.svg' />
|
||||
<span styleName='tooltip'>{i18n.__('Info')}</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
.control-infoButton
|
||||
top 10px
|
||||
margin-bottom 10px
|
||||
topBarButtonLight()
|
||||
topBarButtonRight()
|
||||
&:hover .tooltip
|
||||
opacity 1
|
||||
|
||||
.tooltip
|
||||
tooltip()
|
||||
position absolute
|
||||
pointer-events none
|
||||
top 50px
|
||||
right 20px
|
||||
z-index 200
|
||||
padding 5px
|
||||
line-height normal
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
.infoButton
|
||||
padding 0px
|
||||
|
||||
@@ -2,77 +2,98 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './InfoPanel.styl'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const InfoPanel = ({
|
||||
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, wordCount, letterCount, type, print
|
||||
}) => (
|
||||
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
|
||||
<div>
|
||||
<p styleName='modification-date'>{updatedAt}</p>
|
||||
<p styleName='modification-date-desc'>MODIFICATION DATE</p>
|
||||
</div>
|
||||
class InfoPanel extends React.Component {
|
||||
copyNoteLink () {
|
||||
const {noteLink} = this.props
|
||||
this.refs.noteLink.select()
|
||||
copy(noteLink)
|
||||
}
|
||||
|
||||
<hr />
|
||||
|
||||
{type === 'SNIPPET_NOTE'
|
||||
? ''
|
||||
: <div styleName='count-wrap'>
|
||||
<div styleName='count-number'>
|
||||
<p styleName='infoPanel-defaul-count'>{wordCount}</p>
|
||||
<p styleName='infoPanel-sub-count'>Words</p>
|
||||
render () {
|
||||
const {
|
||||
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, wordCount, letterCount, type, print
|
||||
} = this.props
|
||||
return (
|
||||
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
|
||||
<div>
|
||||
<p styleName='modification-date'>{updatedAt}</p>
|
||||
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
|
||||
</div>
|
||||
<div styleName='count-number'>
|
||||
<p styleName='infoPanel-defaul-count'>{letterCount}</p>
|
||||
<p styleName='infoPanel-sub-count'>Letters</p>
|
||||
|
||||
<hr />
|
||||
|
||||
{type === 'SNIPPET_NOTE'
|
||||
? ''
|
||||
: <div styleName='count-wrap'>
|
||||
<div styleName='count-number'>
|
||||
<p styleName='infoPanel-defaul-count'>{wordCount}</p>
|
||||
<p styleName='infoPanel-sub-count'>{i18n.__('Words')}</p>
|
||||
</div>
|
||||
<div styleName='count-number'>
|
||||
<p styleName='infoPanel-defaul-count'>{letterCount}</p>
|
||||
<p styleName='infoPanel-sub-count'>{i18n.__('Letters')}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{type === 'SNIPPET_NOTE'
|
||||
? ''
|
||||
: <hr />
|
||||
}
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'>{storageName}</p>
|
||||
<p styleName='infoPanel-sub'>{i18n.__('STORAGE')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'>{folderName}</p>
|
||||
<p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'>{createdAt}</p>
|
||||
<p styleName='infoPanel-sub'>{i18n.__('CREATION DATE')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input styleName='infoPanel-noteLink' ref='noteLink' value={noteLink} onClick={(e) => { e.target.select() }} />
|
||||
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
|
||||
<i className='fa fa-clipboard' />
|
||||
</button>
|
||||
<p styleName='infoPanel-sub'>{i18n.__('NOTE LINK')}</p>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div id='export-wrap'>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
||||
<i className='fa fa-file-code-o' />
|
||||
<p>{i18n.__('.md')}</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
||||
<i className='fa fa-file-text-o' />
|
||||
<p>{i18n.__('.txt')}</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
|
||||
<i className='fa fa-html5' />
|
||||
<p>{i18n.__('.html')}</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => print(e)}>
|
||||
<i className='fa fa-print' />
|
||||
<p>{i18n.__('Print')}</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{type === 'SNIPPET_NOTE'
|
||||
? ''
|
||||
: <hr />
|
||||
}
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'>{storageName}</p>
|
||||
<p styleName='infoPanel-sub'>STORAGE</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'>{folderName}</p>
|
||||
<p styleName='infoPanel-sub'>FOLDER</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'>{createdAt}</p>
|
||||
<p styleName='infoPanel-sub'>CREATION DATE</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input styleName='infoPanel-noteLink' value={noteLink} onClick={(e) => { e.target.select() }} />
|
||||
<p styleName='infoPanel-sub'>NOTE LINK</p>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div id='export-wrap'>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
||||
<i className='fa fa-file-code-o fa-fw' />
|
||||
<p>.md</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
||||
<i className='fa fa-file-text-o fa-fw' />
|
||||
<p>.txt</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => print(e)}>
|
||||
<i className='fa fa-print fa-fw' />
|
||||
<p>Print</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
InfoPanel.propTypes = {
|
||||
storageName: PropTypes.string.isRequired,
|
||||
@@ -82,6 +103,7 @@ InfoPanel.propTypes = {
|
||||
createdAt: PropTypes.string.isRequired,
|
||||
exportAsMd: PropTypes.func.isRequired,
|
||||
exportAsTxt: PropTypes.func.isRequired,
|
||||
exportAsHtml: PropTypes.func.isRequired,
|
||||
wordCount: PropTypes.number,
|
||||
letterCount: PropTypes.number,
|
||||
type: PropTypes.string.isRequired,
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
.control-infoButton-panel
|
||||
z-index 200
|
||||
margin-top 0px
|
||||
right 0
|
||||
top: 50px
|
||||
right 25px
|
||||
position absolute
|
||||
padding 20px 25px 0 25px
|
||||
width 300px
|
||||
height 350px
|
||||
overflow auto
|
||||
background-color $ui-noteList-backgroundColor
|
||||
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
|
||||
@@ -41,12 +41,12 @@
|
||||
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
|
||||
border-radius 2px
|
||||
|
||||
.count-wrap
|
||||
.count-wrap
|
||||
display flex
|
||||
position relative
|
||||
width 100%
|
||||
|
||||
.count-number
|
||||
.count-number
|
||||
position relative
|
||||
display block
|
||||
width 50%
|
||||
@@ -70,26 +70,41 @@
|
||||
color $ui-text-color
|
||||
|
||||
.infoPanel-sub
|
||||
font-size 14px
|
||||
font-size 12px
|
||||
font-weight 600
|
||||
color $ui-inactive-text-color
|
||||
padding-bottom 8px
|
||||
|
||||
.infoPanel-noteLink
|
||||
padding-right 5px
|
||||
width 200px
|
||||
width 210px
|
||||
height 25px
|
||||
margin-bottom 6px
|
||||
margin 6px 0
|
||||
|
||||
.infoPanel-copyButton
|
||||
outline none
|
||||
font-size 16px
|
||||
color #A0A0A0
|
||||
background-color transparent
|
||||
border none
|
||||
margin 0 5px
|
||||
border-radius 5px
|
||||
cursor pointer
|
||||
&:hover
|
||||
transition 0.2s
|
||||
background-color alpha($ui-button--hover-backgroundColor, 30%)
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-trash
|
||||
color #EA4447
|
||||
font-weight 600
|
||||
color #EA4447
|
||||
font-weight 600
|
||||
font-size 14px
|
||||
width 70px
|
||||
background-color rgba(226,33,113,0.1)
|
||||
border none
|
||||
border none
|
||||
outline none
|
||||
border-radius 2px
|
||||
margin-right 5px
|
||||
border-radius 2px
|
||||
margin-right 5px
|
||||
padding 2px 5px
|
||||
|
||||
[id=export-wrap]
|
||||
@@ -160,4 +175,84 @@ body[data-theme="dark"]
|
||||
p
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
color $ui-dark-text-color
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.control-infoButton-panel
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
|
||||
.control-infoButton-panel-trash
|
||||
background-color $ui-solarized-ark-noteList-backgroundColor
|
||||
|
||||
.modification-date
|
||||
color $ui-solarized-ark-text-color
|
||||
|
||||
.modification-date-desc
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-defaul-count
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.infoPanel-sub-count
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-default
|
||||
color $ui-solarized-ark-text-color
|
||||
|
||||
.infoPanel-sub
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-noteLink
|
||||
background-color alpha($ui-solarized-dark-borderColor, 20%)
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
[id=export-wrap]
|
||||
button
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-solarized-dark-borderColor, 20%)
|
||||
color $ui-solarized-ark-text-color
|
||||
p
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-solarized-ark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.control-infoButton-panel
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.control-infoButton-panel-trash
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.modification-date
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.modification-date-desc
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-defaul-count
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.infoPanel-sub-count
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-default
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.infoPanel-sub
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-noteLink
|
||||
background-color alpha($ui-monokai-borderColor, 20%)
|
||||
color $ui-monokai-text-color
|
||||
|
||||
[id=export-wrap]
|
||||
button
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-monokai-borderColor, 20%)
|
||||
color $ui-monokai-text-color
|
||||
p
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-monokai-text-color
|
||||
|
||||
@@ -2,46 +2,52 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './InfoPanel.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const InfoPanelTrashed = ({
|
||||
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt
|
||||
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml
|
||||
}) => (
|
||||
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
|
||||
<div>
|
||||
<p styleName='modification-date'>{updatedAt}</p>
|
||||
<p styleName='modification-date-desc'>MODIFICATION DATE</p>
|
||||
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'>{storageName}</p>
|
||||
<p styleName='infoPanel-sub'>STORAGE</p>
|
||||
<p styleName='infoPanel-sub'>{i18n.__('STORAGE')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'><text styleName='infoPanel-trash'>Trash</text>{folderName}</p>
|
||||
<p styleName='infoPanel-sub'>FOLDER</p>
|
||||
<p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'>{createdAt}</p>
|
||||
<p styleName='infoPanel-sub'>CREATION DATE</p>
|
||||
<p styleName='infoPanel-sub'>{i18n.__('CREATION DATE')}</p>
|
||||
</div>
|
||||
|
||||
<div id='export-wrap'>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
||||
<i className='fa fa-file-code-o fa-fw' />
|
||||
<i className='fa fa-file-code-o' />
|
||||
<p>.md</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
||||
<i className='fa fa-file-text-o fa-fw' />
|
||||
<i className='fa fa-file-text-o' />
|
||||
<p>.txt</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
|
||||
<i className='fa fa-html5' />
|
||||
<p>.html</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--unable'>
|
||||
<i className='fa fa-file-pdf-o fa-fw' />
|
||||
<i className='fa fa-file-pdf-o' />
|
||||
<p>.pdf</p>
|
||||
</button>
|
||||
</div>
|
||||
@@ -54,7 +60,8 @@ InfoPanelTrashed.propTypes = {
|
||||
updatedAt: PropTypes.string.isRequired,
|
||||
createdAt: PropTypes.string.isRequired,
|
||||
exportAsMd: PropTypes.func.isRequired,
|
||||
exportAsTxt: PropTypes.func.isRequired
|
||||
exportAsTxt: PropTypes.func.isRequired,
|
||||
exportAsHtml: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(InfoPanelTrashed, styles)
|
||||
|
||||
182
browser/main/Detail/MarkdownNoteDetail.js
Normal file → Executable file
182
browser/main/Detail/MarkdownNoteDetail.js
Normal file → Executable file
@@ -3,6 +3,7 @@ import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './MarkdownNoteDetail.styl'
|
||||
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
||||
import MarkdownSplitEditor from 'browser/components/MarkdownSplitEditor'
|
||||
import TodoListPercentage from 'browser/components/TodoListPercentage'
|
||||
import StarButton from './StarButton'
|
||||
import TagSelect from './TagSelect'
|
||||
@@ -15,18 +16,19 @@ import StatusBar from '../StatusBar'
|
||||
import _ from 'lodash'
|
||||
import { findNoteTitle } from 'browser/lib/findNoteTitle'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import TrashButton from './TrashButton'
|
||||
import FullscreenButton from './FullscreenButton'
|
||||
import RestoreButton from './RestoreButton'
|
||||
import PermanentDeleteButton from './PermanentDeleteButton'
|
||||
import InfoButton from './InfoButton'
|
||||
import ToggleModeButton from './ToggleModeButton'
|
||||
import InfoPanel from './InfoPanel'
|
||||
import InfoPanelTrashed from './InfoPanelTrashed'
|
||||
import { formatDate } from 'browser/lib/date-formatter'
|
||||
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
|
||||
import striptags from 'striptags'
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
const { dialog } = remote
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
|
||||
class MarkdownNoteDetail extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -39,7 +41,8 @@ class MarkdownNoteDetail extends React.Component {
|
||||
content: ''
|
||||
}, props.note),
|
||||
isLockButtonShown: false,
|
||||
isLocked: false
|
||||
isLocked: false,
|
||||
editorType: props.config.editor.type
|
||||
}
|
||||
this.dispatchTimer = null
|
||||
|
||||
@@ -52,10 +55,14 @@ class MarkdownNoteDetail extends React.Component {
|
||||
|
||||
componentDidMount () {
|
||||
ee.on('topbar:togglelockbutton', this.toggleLockButton)
|
||||
ee.on('topbar:togglemodebutton', () => {
|
||||
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
|
||||
this.handleSwitchMode(reversedType)
|
||||
})
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) {
|
||||
if (nextProps.note.key !== this.props.note.key && !this.state.isMovingNote) {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
this.setState({
|
||||
note: Object.assign({}, nextProps.note)
|
||||
@@ -67,24 +74,26 @@ class MarkdownNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
}
|
||||
|
||||
componentDidUnmount () {
|
||||
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
||||
handleUpdateTag () {
|
||||
const { note } = this.state
|
||||
if (this.refs.tags) note.tags = this.refs.tags.value
|
||||
this.updateNote(note)
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
handleUpdateContent () {
|
||||
const { note } = this.state
|
||||
|
||||
note.content = this.refs.content.value
|
||||
if (this.refs.tags) note.tags = this.refs.tags.value
|
||||
note.title = markdown.strip(striptags(findNoteTitle(note.content)))
|
||||
note.updatedAt = new Date()
|
||||
this.updateNote(note)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
updateNote (note) {
|
||||
note.updatedAt = new Date()
|
||||
this.setState({note}, () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
@@ -135,7 +144,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
hashHistory.replace({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: newNote.storage + '-' + newNote.key
|
||||
key: newNote.key
|
||||
}
|
||||
})
|
||||
this.setState({
|
||||
@@ -170,41 +179,45 @@ class MarkdownNoteDetail extends React.Component {
|
||||
ee.emit('export:save-text')
|
||||
}
|
||||
|
||||
exportAsHtml () {
|
||||
ee.emit('export:save-html')
|
||||
}
|
||||
|
||||
handleTrashButtonClick (e) {
|
||||
const { note } = this.state
|
||||
const { isTrashed } = note
|
||||
const { confirmDeletion } = this.props.config.ui
|
||||
|
||||
if (isTrashed) {
|
||||
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Confirm note deletion',
|
||||
detail: 'This will permanently remove this note.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
if (dialogueButtonIndex === 1) return
|
||||
const { note, dispatch } = this.props
|
||||
dataApi
|
||||
.deleteNote(note.storage, note.key)
|
||||
.then((data) => {
|
||||
const dispatchHandler = () => {
|
||||
dispatch({
|
||||
type: 'DELETE_NOTE',
|
||||
storageKey: data.storageKey,
|
||||
noteKey: data.noteKey
|
||||
})
|
||||
}
|
||||
ee.once('list:moved', dispatchHandler)
|
||||
})
|
||||
if (confirmDeleteNote(confirmDeletion, true)) {
|
||||
const {note, dispatch} = this.props
|
||||
dataApi
|
||||
.deleteNote(note.storage, note.key)
|
||||
.then((data) => {
|
||||
const dispatchHandler = () => {
|
||||
dispatch({
|
||||
type: 'DELETE_NOTE',
|
||||
storageKey: data.storageKey,
|
||||
noteKey: data.noteKey
|
||||
})
|
||||
}
|
||||
ee.once('list:next', dispatchHandler)
|
||||
})
|
||||
.then(() => ee.emit('list:next'))
|
||||
}
|
||||
} else {
|
||||
note.isTrashed = true
|
||||
if (confirmDeleteNote(confirmDeletion, false)) {
|
||||
note.isTrashed = true
|
||||
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
|
||||
ee.emit('list:next')
|
||||
}
|
||||
}
|
||||
ee.emit('list:next')
|
||||
}
|
||||
|
||||
handleUndoButtonClick (e) {
|
||||
@@ -233,7 +246,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
getToggleLockButton () {
|
||||
return this.state.isLocked ? '../resources/icon/icon-lock.svg' : '../resources/icon/icon-unlock.svg'
|
||||
return this.state.isLocked ? '../resources/icon/icon-previewoff-on.svg' : '../resources/icon/icon-previewoff-off.svg'
|
||||
}
|
||||
|
||||
handleDeleteKeyDown (e) {
|
||||
@@ -262,9 +275,45 @@ class MarkdownNoteDetail extends React.Component {
|
||||
ee.emit('print')
|
||||
}
|
||||
|
||||
render () {
|
||||
const { data, config, location } = this.props
|
||||
handleSwitchMode (type) {
|
||||
this.setState({ editorType: type }, () => {
|
||||
this.focus()
|
||||
const newConfig = Object.assign({}, this.props.config)
|
||||
newConfig.editor.type = type
|
||||
ConfigManager.set(newConfig)
|
||||
})
|
||||
}
|
||||
|
||||
renderEditor () {
|
||||
const { config, ignorePreviewPointerEvents } = this.props
|
||||
const { note } = this.state
|
||||
if (this.state.editorType === 'EDITOR_PREVIEW') {
|
||||
return <MarkdownEditor
|
||||
ref='content'
|
||||
styleName='body-noteEditor'
|
||||
config={config}
|
||||
value={note.content}
|
||||
storageKey={note.storage}
|
||||
noteKey={note.key}
|
||||
onChange={this.handleUpdateContent.bind(this)}
|
||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||
/>
|
||||
} else {
|
||||
return <MarkdownSplitEditor
|
||||
ref='content'
|
||||
config={config}
|
||||
value={note.content}
|
||||
storageKey={note.storage}
|
||||
noteKey={note.key}
|
||||
onChange={this.handleUpdateContent.bind(this)}
|
||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { data, location } = this.props
|
||||
const { note, editorType } = this.state
|
||||
const storageKey = note.storage
|
||||
const folderKey = note.folder
|
||||
|
||||
@@ -281,10 +330,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
|
||||
const trashTopBar = <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<i styleName='undo-button'
|
||||
className='fa fa-undo fa-fw'
|
||||
onClick={(e) => this.handleUndoButtonClick(e)}
|
||||
/>
|
||||
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
@@ -296,6 +342,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
folderName={currentOption.folder.name}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsHtml={this.exportAsHtml}
|
||||
exportAsMd={this.exportAsMd}
|
||||
exportAsTxt={this.exportAsTxt}
|
||||
/>
|
||||
@@ -316,17 +363,12 @@ class MarkdownNoteDetail extends React.Component {
|
||||
<TagSelect
|
||||
ref='tags'
|
||||
value={this.state.note.tags}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
/>
|
||||
<TodoListPercentage
|
||||
percentageOfTodo={getTodoPercentageOfCompleted(note.content)}
|
||||
onChange={this.handleUpdateTag.bind(this)}
|
||||
/>
|
||||
<TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<InfoButton
|
||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
||||
/>
|
||||
|
||||
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
|
||||
<StarButton
|
||||
onClick={(e) => this.handleStarButtonClick(e)}
|
||||
isActive={note.isStarred}
|
||||
@@ -340,6 +382,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
|
||||
>
|
||||
<img styleName='iconInfo' src={imgSrc} />
|
||||
{this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>}
|
||||
</button>
|
||||
|
||||
return (
|
||||
@@ -347,22 +390,23 @@ class MarkdownNoteDetail extends React.Component {
|
||||
)
|
||||
})()}
|
||||
|
||||
<button styleName='control-fullScreenButton'
|
||||
onMouseDown={(e) => this.handleFullScreenButton(e)}
|
||||
>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-sidebar.svg' />
|
||||
</button>
|
||||
<FullscreenButton onClick={(e) => this.handleFullScreenButton(e)} />
|
||||
|
||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
|
||||
<InfoButton
|
||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
||||
/>
|
||||
|
||||
<InfoPanel
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
noteLink={`[${note.title}](${location.query.key})`}
|
||||
noteLink={`[${note.title}](:note:${location.query.key})`}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.exportAsMd}
|
||||
exportAsTxt={this.exportAsTxt}
|
||||
exportAsHtml={this.exportAsHtml}
|
||||
wordCount={note.content.split(' ').length}
|
||||
letterCount={note.content.replace(/\r?\n/g, '').length}
|
||||
type={note.type}
|
||||
@@ -380,15 +424,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
||||
|
||||
<div styleName='body'>
|
||||
<MarkdownEditor
|
||||
ref='content'
|
||||
styleName='body-noteEditor'
|
||||
config={config}
|
||||
value={this.state.note.content}
|
||||
storageKey={this.state.note.storage}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
||||
/>
|
||||
{this.renderEditor()}
|
||||
</div>
|
||||
|
||||
<StatusBar
|
||||
|
||||
@@ -7,30 +7,42 @@
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
box-shadow none
|
||||
padding 20px 40px
|
||||
overflow hidden
|
||||
|
||||
.lock-button
|
||||
padding-bottom 3px
|
||||
|
||||
.control-lockButton
|
||||
top 160px
|
||||
margin-bottom 10px
|
||||
topBarButtonLight()
|
||||
topBarButtonRight()
|
||||
position absolute
|
||||
right 225px
|
||||
&:hover .tooltip
|
||||
opacity 1
|
||||
|
||||
.tooltip
|
||||
tooltip()
|
||||
position absolute
|
||||
pointer-events none
|
||||
top 35px
|
||||
right -10px
|
||||
width 50px
|
||||
z-index 200
|
||||
padding 5px
|
||||
line-height normal
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
.trashed-infopanel
|
||||
top 40px
|
||||
position relative
|
||||
|
||||
.control-fullScreenButton
|
||||
top 80px
|
||||
topBarButtonLight()
|
||||
|
||||
.body
|
||||
absolute left right
|
||||
left 0
|
||||
right 0
|
||||
top $info-height + $info-margin-under-border
|
||||
bottom $statusBar-height
|
||||
margin 0 45px
|
||||
margin 0 30px
|
||||
.body-noteEditor
|
||||
absolute top bottom left right
|
||||
|
||||
@@ -43,7 +55,7 @@ body[data-theme="dark"]
|
||||
.root
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
box-shadow none
|
||||
border none
|
||||
border-left 1px solid $ui-dark-borderColor
|
||||
|
||||
.control-lockButton
|
||||
topBarButtonDark()
|
||||
@@ -53,3 +65,14 @@ body[data-theme="dark"]
|
||||
|
||||
.control-fullScreenButton
|
||||
topBarButtonDark()
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
border-left 1px solid $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
border-left 1px solid $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@import('DetailVars')
|
||||
|
||||
$info-height = 50px
|
||||
$info-height = 60px
|
||||
$info-margin-under-border = 30px
|
||||
|
||||
.info
|
||||
@@ -8,11 +8,11 @@ $info-margin-under-border = 30px
|
||||
left 0
|
||||
right 0
|
||||
height $info-height
|
||||
border-bottom 1px solid #eee
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
width 100%
|
||||
display flex
|
||||
align-items center
|
||||
padding 0 20px
|
||||
|
||||
.info-left
|
||||
padding 0 10px
|
||||
@@ -20,7 +20,6 @@ $info-margin-under-border = 30px
|
||||
display flex
|
||||
align-items center
|
||||
|
||||
|
||||
.info-left-top-folderSelect
|
||||
display flex
|
||||
align-items center
|
||||
@@ -45,12 +44,9 @@ $info-margin-under-border = 30px
|
||||
color $ui-button--color
|
||||
|
||||
.info-right
|
||||
position absolute
|
||||
right 40px
|
||||
top 60px
|
||||
bottom 1px
|
||||
padding-left 30px
|
||||
z-index 101
|
||||
display inline-flex
|
||||
margin-top 3px
|
||||
|
||||
.undo-button
|
||||
width 34px
|
||||
@@ -95,4 +91,14 @@ body[data-theme="dark"]
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
|
||||
.undo-button
|
||||
topBarButtonDark()
|
||||
topBarButtonDark()
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.info
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.info
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './TrashButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const PermanentDeleteButton = ({
|
||||
onClick
|
||||
@@ -10,6 +11,7 @@ const PermanentDeleteButton = ({
|
||||
onClick={(e) => onClick(e)}
|
||||
>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
||||
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
|
||||
22
browser/main/Detail/RestoreButton.js
Normal file
22
browser/main/Detail/RestoreButton.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './RestoreButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const RestoreButton = ({
|
||||
onClick
|
||||
}) => (
|
||||
<button styleName='control-restoreButton'
|
||||
onClick={onClick}
|
||||
>
|
||||
<i className='fa fa-undo fa-fw' styleName='iconRestore' />
|
||||
<span styleName='tooltip'>{i18n.__('Restore')}</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
RestoreButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(RestoreButton, styles)
|
||||
22
browser/main/Detail/RestoreButton.styl
Normal file
22
browser/main/Detail/RestoreButton.styl
Normal file
@@ -0,0 +1,22 @@
|
||||
.control-restoreButton
|
||||
top 115px
|
||||
topBarButtonRight()
|
||||
&:hover .tooltip
|
||||
opacity 1
|
||||
|
||||
.tooltip
|
||||
tooltip()
|
||||
position absolute
|
||||
pointer-events none
|
||||
top 50px
|
||||
left 25px
|
||||
z-index 200
|
||||
padding 5px
|
||||
line-height normal
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
body[data-theme="dark"]
|
||||
.control-restoreButton
|
||||
topBarButtonDark()
|
||||
@@ -8,37 +8,27 @@ import StarButton from './StarButton'
|
||||
import TagSelect from './TagSelect'
|
||||
import FolderSelect from './FolderSelect'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import { hashHistory } from 'react-router'
|
||||
import {hashHistory} from 'react-router'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import CodeMirror from 'codemirror'
|
||||
import 'codemirror-mode-elixir'
|
||||
import SnippetTab from 'browser/components/SnippetTab'
|
||||
import StatusBar from '../StatusBar'
|
||||
import context from 'browser/lib/context'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import _ from 'lodash'
|
||||
import { findNoteTitle } from 'browser/lib/findNoteTitle'
|
||||
import {findNoteTitle} from 'browser/lib/findNoteTitle'
|
||||
import convertModeName from 'browser/lib/convertModeName'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import TrashButton from './TrashButton'
|
||||
import RestoreButton from './RestoreButton'
|
||||
import PermanentDeleteButton from './PermanentDeleteButton'
|
||||
import InfoButton from './InfoButton'
|
||||
import InfoPanel from './InfoPanel'
|
||||
import InfoPanelTrashed from './InfoPanelTrashed'
|
||||
import { formatDate } from 'browser/lib/date-formatter'
|
||||
|
||||
function pass (name) {
|
||||
switch (name) {
|
||||
case 'ejs':
|
||||
return 'Embedded Javascript'
|
||||
case 'html_ruby':
|
||||
return 'Embedded Ruby'
|
||||
case 'objectivec':
|
||||
return 'Objective C'
|
||||
case 'text':
|
||||
return 'Plain Text'
|
||||
default:
|
||||
return name
|
||||
}
|
||||
}
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
@@ -51,16 +41,34 @@ class SnippetNoteDetail extends React.Component {
|
||||
this.state = {
|
||||
isMovingNote: false,
|
||||
snippetIndex: 0,
|
||||
showArrows: false,
|
||||
enableLeftArrow: false,
|
||||
enableRightArrow: false,
|
||||
note: Object.assign({
|
||||
description: ''
|
||||
}, props.note, {
|
||||
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
|
||||
})
|
||||
}
|
||||
|
||||
this.scrollToNextTabThreshold = 0.7
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const visibleTabs = this.visibleTabs
|
||||
const allTabs = this.allTabs
|
||||
|
||||
if (visibleTabs.offsetWidth < allTabs.scrollWidth) {
|
||||
this.setState({
|
||||
showArrows: visibleTabs.offsetWidth < allTabs.scrollWidth,
|
||||
enableRightArrow: allTabs.offsetLeft !== visibleTabs.offsetWidth - allTabs.scrollWidth,
|
||||
enableLeftArrow: allTabs.offsetLeft !== 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) {
|
||||
if (nextProps.note.key !== this.props.note.key && !this.state.isMovingNote) {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
const nextNote = Object.assign({
|
||||
description: ''
|
||||
@@ -76,6 +84,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
this.refs['code-' + index].reload()
|
||||
})
|
||||
if (this.refs.tags) this.refs.tags.reset()
|
||||
this.setState(this.getArrowsState())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -145,7 +154,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
hashHistory.replace({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: newNote.storage + '-' + newNote.key
|
||||
key: newNote.key
|
||||
}
|
||||
})
|
||||
this.setState({
|
||||
@@ -175,38 +184,38 @@ class SnippetNoteDetail extends React.Component {
|
||||
handleTrashButtonClick (e) {
|
||||
const { note } = this.state
|
||||
const { isTrashed } = note
|
||||
const { confirmDeletion } = this.props.config.ui
|
||||
|
||||
if (isTrashed) {
|
||||
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Confirm note deletion',
|
||||
detail: 'This will permanently remove this note.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
if (dialogueButtonIndex === 1) return
|
||||
const { note, dispatch } = this.props
|
||||
dataApi
|
||||
.deleteNote(note.storage, note.key)
|
||||
.then((data) => {
|
||||
const dispatchHandler = () => {
|
||||
dispatch({
|
||||
type: 'DELETE_NOTE',
|
||||
storageKey: data.storageKey,
|
||||
noteKey: data.noteKey
|
||||
})
|
||||
}
|
||||
ee.once('list:moved', dispatchHandler)
|
||||
})
|
||||
if (confirmDeleteNote(confirmDeletion, true)) {
|
||||
const {note, dispatch} = this.props
|
||||
dataApi
|
||||
.deleteNote(note.storage, note.key)
|
||||
.then((data) => {
|
||||
const dispatchHandler = () => {
|
||||
dispatch({
|
||||
type: 'DELETE_NOTE',
|
||||
storageKey: data.storageKey,
|
||||
noteKey: data.noteKey
|
||||
})
|
||||
}
|
||||
ee.once('list:next', dispatchHandler)
|
||||
})
|
||||
.then(() => ee.emit('list:next'))
|
||||
}
|
||||
} else {
|
||||
note.isTrashed = true
|
||||
if (confirmDeleteNote(confirmDeletion, false)) {
|
||||
note.isTrashed = true
|
||||
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
|
||||
ee.emit('list:next')
|
||||
}
|
||||
}
|
||||
ee.emit('list:next')
|
||||
}
|
||||
|
||||
handleUndoButtonClick (e) {
|
||||
@@ -226,6 +235,51 @@ class SnippetNoteDetail extends React.Component {
|
||||
ee.emit('editor:fullscreen')
|
||||
}
|
||||
|
||||
handleTabMoveLeftButtonClick (e) {
|
||||
{
|
||||
const left = this.visibleTabs.scrollLeft
|
||||
|
||||
const tabs = this.allTabs.querySelectorAll('div')
|
||||
const lastVisibleTab = Array.from(tabs).find((tab) => {
|
||||
return tab.offsetLeft + tab.offsetWidth >= left
|
||||
})
|
||||
|
||||
if (lastVisibleTab) {
|
||||
const visiblePart = lastVisibleTab.offsetWidth + lastVisibleTab.offsetLeft - left
|
||||
const isFullyVisible = visiblePart > lastVisibleTab.offsetWidth * this.scrollToNextTabThreshold
|
||||
const scrollToTab = (isFullyVisible && lastVisibleTab.previousSibling)
|
||||
? lastVisibleTab.previousSibling
|
||||
: lastVisibleTab
|
||||
|
||||
// FIXME use `scrollIntoView()` instead of custom method after update to Electron2.0 (with Chrome 61 its possible animate the scroll)
|
||||
this.moveToTab(scrollToTab)
|
||||
// scrollToTab.scrollIntoView({behavior: 'smooth', inline: 'start', block: 'start'})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleTabMoveRightButtonClick (e) {
|
||||
const left = this.visibleTabs.scrollLeft
|
||||
const width = this.visibleTabs.offsetWidth
|
||||
|
||||
const tabs = this.allTabs.querySelectorAll('div')
|
||||
const lastVisibleTab = Array.from(tabs).find((tab) => {
|
||||
return tab.offsetLeft + tab.offsetWidth >= width + left
|
||||
})
|
||||
|
||||
if (lastVisibleTab) {
|
||||
const visiblePart = width + left - lastVisibleTab.offsetLeft
|
||||
const isFullyVisible = visiblePart > lastVisibleTab.offsetWidth * this.scrollToNextTabThreshold
|
||||
const scrollToTab = (isFullyVisible && lastVisibleTab.nextSibling)
|
||||
? lastVisibleTab.nextSibling
|
||||
: lastVisibleTab
|
||||
|
||||
// FIXME use `scrollIntoView()` instead of custom method after update to Electron2.0 (with Chrome 61 its possible animate the scroll)
|
||||
this.moveToTab(scrollToTab)
|
||||
// scrollToTab.scrollIntoView({behavior: 'smooth', inline: 'end', block: 'end'})
|
||||
}
|
||||
}
|
||||
|
||||
handleTabPlusButtonClick (e) {
|
||||
this.addSnippet()
|
||||
}
|
||||
@@ -236,14 +290,35 @@ class SnippetNoteDetail extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleTabDragStart (e, index) {
|
||||
e.dataTransfer.setData('text/plain', index)
|
||||
}
|
||||
|
||||
handleTabDrop (e, index) {
|
||||
const oldIndex = parseInt(e.dataTransfer.getData('text'))
|
||||
|
||||
const snippets = this.state.note.snippets.slice()
|
||||
const draggedSnippet = snippets[oldIndex]
|
||||
snippets[oldIndex] = snippets[index]
|
||||
snippets[index] = draggedSnippet
|
||||
const snippetIndex = index
|
||||
|
||||
const note = Object.assign({}, this.state.note, {snippets})
|
||||
this.setState({ note, snippetIndex }, () => {
|
||||
this.save()
|
||||
this.refs['code-' + index].reload()
|
||||
this.refs['code-' + oldIndex].reload()
|
||||
})
|
||||
}
|
||||
|
||||
handleTabDeleteButtonClick (e, index) {
|
||||
if (this.state.note.snippets.length > 1) {
|
||||
if (this.state.note.snippets[index].content.trim().length > 0) {
|
||||
const dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Delete a snippet',
|
||||
detail: 'This work cannot be undone.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
message: i18n.__('Delete a snippet'),
|
||||
detail: i18n.__('This work cannot be undone.'),
|
||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||
})
|
||||
if (dialogIndex === 0) {
|
||||
this.deleteSnippetByIndex(index)
|
||||
@@ -264,6 +339,21 @@ class SnippetNoteDetail extends React.Component {
|
||||
this.setState({ note, snippetIndex }, () => {
|
||||
this.save()
|
||||
this.refs['code-' + this.state.snippetIndex].reload()
|
||||
|
||||
if (this.visibleTabs.offsetWidth > this.allTabs.scrollWidth) {
|
||||
console.log('no need for arrows')
|
||||
this.moveTabBarBy(0)
|
||||
} else {
|
||||
const lastTab = this.allTabs.lastChild
|
||||
if (lastTab.offsetLeft + lastTab.offsetWidth < this.visibleTabs.offsetWidth) {
|
||||
console.log('need to scroll')
|
||||
const width = this.visibleTabs.offsetWidth
|
||||
const newLeft = lastTab.offsetLeft + lastTab.offsetWidth - width
|
||||
this.moveTabBarBy(newLeft > 0 ? -newLeft : 0)
|
||||
} else {
|
||||
this.setState(this.getArrowsState())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -278,11 +368,11 @@ class SnippetNoteDetail extends React.Component {
|
||||
name: mode
|
||||
})
|
||||
}
|
||||
this.setState({note: Object.assign(this.state.note, {snippets: snippets})})
|
||||
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
|
||||
|
||||
this.setState({
|
||||
note: this.state.note
|
||||
}, () => {
|
||||
this.setState(state => ({
|
||||
note: state.note
|
||||
}), () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
@@ -291,11 +381,11 @@ class SnippetNoteDetail extends React.Component {
|
||||
return (e) => {
|
||||
const snippets = this.state.note.snippets.slice()
|
||||
snippets[index].mode = name
|
||||
this.setState({note: Object.assign(this.state.note, {snippets: snippets})})
|
||||
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
|
||||
|
||||
this.setState({
|
||||
note: this.state.note
|
||||
}, () => {
|
||||
this.setState(state => ({
|
||||
note: state.note
|
||||
}), () => {
|
||||
this.save()
|
||||
})
|
||||
|
||||
@@ -309,10 +399,10 @@ class SnippetNoteDetail extends React.Component {
|
||||
return (e) => {
|
||||
const snippets = this.state.note.snippets.slice()
|
||||
snippets[index].content = this.refs['code-' + index].value
|
||||
this.setState({note: Object.assign(this.state.note, {snippets: snippets})})
|
||||
this.setState({
|
||||
note: this.state.note
|
||||
}, () => {
|
||||
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
|
||||
this.setState(state => ({
|
||||
note: state.note
|
||||
}), () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
@@ -320,6 +410,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
|
||||
handleKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
// tab key
|
||||
case 9:
|
||||
if (e.ctrlKey && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
@@ -332,6 +423,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
this.focusEditor()
|
||||
}
|
||||
break
|
||||
// L key
|
||||
case 76:
|
||||
{
|
||||
const isSuper = global.process.platform === 'darwin'
|
||||
@@ -343,6 +435,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
}
|
||||
}
|
||||
break
|
||||
// T key
|
||||
case 84:
|
||||
{
|
||||
const isSuper = global.process.platform === 'darwin'
|
||||
@@ -359,7 +452,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
|
||||
handleModeButtonClick (e, index) {
|
||||
const menu = new Menu()
|
||||
CodeMirror.modeInfo.forEach((mode) => {
|
||||
CodeMirror.modeInfo.sort(function (a, b) { return a.name.localeCompare(b.name) }).forEach((mode) => {
|
||||
menu.append(new MenuItem({
|
||||
label: mode.name,
|
||||
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
|
||||
@@ -434,6 +527,51 @@ class SnippetNoteDetail extends React.Component {
|
||||
this.refs.description.focus()
|
||||
}
|
||||
|
||||
moveToTab (tab) {
|
||||
const easeOutCubic = t => (--t) * t * t + 1
|
||||
const startScrollPosition = this.visibleTabs.scrollLeft
|
||||
const animationTiming = 300
|
||||
const scrollMoreCoeff = 1.4 // introduce coefficient, because we want to scroll a bit further to see next tab
|
||||
|
||||
let scrollBy = (tab.offsetLeft - startScrollPosition)
|
||||
|
||||
if (tab.offsetLeft > startScrollPosition) {
|
||||
// if tab is on the right side and we want to show the whole tab in visible area,
|
||||
// we need to include width of the tab and visible area in the formula
|
||||
// ___________________________________________
|
||||
// |____|_______|________|________|_show_this_|
|
||||
// ↑_____________________↑
|
||||
// visible area
|
||||
scrollBy += (tab.offsetWidth - this.visibleTabs.offsetWidth)
|
||||
}
|
||||
|
||||
let startTime = null
|
||||
const scrollAnimation = time => {
|
||||
startTime = startTime || time
|
||||
const elapsed = (time - startTime) / animationTiming
|
||||
|
||||
this.visibleTabs.scrollLeft = startScrollPosition + easeOutCubic(elapsed) * scrollBy * scrollMoreCoeff
|
||||
if (elapsed < 1) {
|
||||
window.requestAnimationFrame(scrollAnimation)
|
||||
} else {
|
||||
this.setState(this.getArrowsState())
|
||||
}
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(scrollAnimation)
|
||||
}
|
||||
|
||||
getArrowsState () {
|
||||
const allTabs = this.allTabs
|
||||
const visibleTabs = this.visibleTabs
|
||||
|
||||
const showArrows = visibleTabs.offsetWidth < allTabs.scrollWidth
|
||||
const enableRightArrow = visibleTabs.scrollLeft !== allTabs.scrollWidth - visibleTabs.offsetWidth
|
||||
const enableLeftArrow = visibleTabs.scrollLeft !== 0
|
||||
|
||||
return {showArrows, enableRightArrow, enableLeftArrow}
|
||||
}
|
||||
|
||||
addSnippet () {
|
||||
const { note } = this.state
|
||||
|
||||
@@ -444,26 +582,32 @@ class SnippetNoteDetail extends React.Component {
|
||||
}])
|
||||
const snippetIndex = note.snippets.length - 1
|
||||
|
||||
this.setState({
|
||||
this.setState(Object.assign({
|
||||
note,
|
||||
snippetIndex
|
||||
}, () => {
|
||||
}, this.getArrowsState()), () => {
|
||||
if (this.state.showArrows) {
|
||||
const tabs = this.allTabs.querySelectorAll('div')
|
||||
if (tabs) {
|
||||
this.moveToTab(tabs[snippetIndex])
|
||||
}
|
||||
}
|
||||
this.refs['tab-' + snippetIndex].startRenaming()
|
||||
})
|
||||
}
|
||||
|
||||
jumpNextTab () {
|
||||
this.setState({
|
||||
snippetIndex: (this.state.snippetIndex + 1) % this.state.note.snippets.length
|
||||
}, () => {
|
||||
this.setState(state => ({
|
||||
snippetIndex: (state.snippetIndex + 1) % state.note.snippets.length
|
||||
}), () => {
|
||||
this.focusEditor()
|
||||
})
|
||||
}
|
||||
|
||||
jumpPrevTab () {
|
||||
this.setState({
|
||||
snippetIndex: (this.state.snippetIndex - 1 + this.state.note.snippets.length) % this.state.note.snippets.length
|
||||
}, () => {
|
||||
this.setState(state => ({
|
||||
snippetIndex: (state.snippetIndex - 1 + state.note.snippets.length) % state.note.snippets.length
|
||||
}), () => {
|
||||
this.focusEditor()
|
||||
})
|
||||
}
|
||||
@@ -481,9 +625,9 @@ class SnippetNoteDetail extends React.Component {
|
||||
showWarning () {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Sorry!',
|
||||
detail: 'md/text import is available only a markdown note.',
|
||||
buttons: ['OK']
|
||||
message: i18n.__('Sorry!'),
|
||||
detail: i18n.__('md/text import is available only a markdown note.'),
|
||||
buttons: [i18n.__('OK')]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -511,13 +655,15 @@ class SnippetNoteDetail extends React.Component {
|
||||
onDelete={(e) => this.handleTabDeleteButtonClick(e, index)}
|
||||
onRename={(name) => this.renameSnippetByIndex(index, name)}
|
||||
isDeletable={note.snippets.length > 1}
|
||||
onDragStart={(e) => this.handleTabDragStart(e, index)}
|
||||
onDrop={(e) => this.handleTabDrop(e, index)}
|
||||
/>
|
||||
})
|
||||
|
||||
const viewList = note.snippets.map((snippet, index) => {
|
||||
const isActive = this.state.snippetIndex === index
|
||||
|
||||
let syntax = CodeMirror.findModeByName(pass(snippet.mode))
|
||||
let syntax = CodeMirror.findModeByName(convertModeName(snippet.mode))
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
|
||||
return <div styleName='tabView'
|
||||
@@ -541,7 +687,10 @@ class SnippetNoteDetail extends React.Component {
|
||||
fontSize={editorFontSize}
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorIndentSize}
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
keyMap={config.editor.keyMap}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||
ref={'code-' + index}
|
||||
/>
|
||||
@@ -562,10 +711,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
|
||||
const trashTopBar = <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<i styleName='undo-button'
|
||||
className='fa fa-undo fa-fw'
|
||||
onClick={(e) => this.handleUndoButtonClick(e)}
|
||||
/>
|
||||
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
@@ -579,6 +725,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.showWarning}
|
||||
exportAsTxt={this.showWarning}
|
||||
exportAsHtml={this.showWarning}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -601,25 +748,27 @@ class SnippetNoteDetail extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<InfoButton
|
||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
||||
/>
|
||||
|
||||
<StarButton
|
||||
onClick={(e) => this.handleStarButtonClick(e)}
|
||||
isActive={note.isStarred}
|
||||
/>
|
||||
|
||||
<button styleName='control-fullScreenButton'
|
||||
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')}
|
||||
onMouseDown={(e) => this.handleFullScreenButton(e)}>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-sidebar.svg' />
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
||||
<span styleName='tooltip'>{i18n.__('Fullscreen')}</span>
|
||||
</button>
|
||||
|
||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
|
||||
<InfoButton
|
||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
||||
/>
|
||||
|
||||
<InfoPanel
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
noteLink={`[${note.title}](${location.query.key})`}
|
||||
noteLink={`[${note.title}](:note:${location.query.key})`}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.showWarning}
|
||||
@@ -645,16 +794,32 @@ class SnippetNoteDetail extends React.Component {
|
||||
fontSize: parseInt(config.preview.fontSize, 10)
|
||||
}}
|
||||
ref='description'
|
||||
placeholder='Description...'
|
||||
placeholder={i18n.__('Description...')}
|
||||
value={this.state.note.description}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='tabList'>
|
||||
<div styleName='list'>
|
||||
{tabList}
|
||||
<button styleName='tabButton'
|
||||
hidden={!this.state.showArrows}
|
||||
disabled={!this.state.enableLeftArrow}
|
||||
onClick={(e) => this.handleTabMoveLeftButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-chevron-left' />
|
||||
</button>
|
||||
<div styleName='list' onScroll={(e) => { this.setState(this.getArrowsState()) }} ref={(tabs) => { this.visibleTabs = tabs }}>
|
||||
<div styleName='allTabs' ref={(tabs) => { this.allTabs = tabs }}>
|
||||
{tabList}
|
||||
</div>
|
||||
</div>
|
||||
<button styleName='plusButton'
|
||||
<button styleName='tabButton'
|
||||
hidden={!this.state.showArrows}
|
||||
disabled={!this.state.enableRightArrow}
|
||||
onClick={(e) => this.handleTabMoveRightButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-chevron-right' />
|
||||
</button>
|
||||
<button styleName='tabButton'
|
||||
onClick={(e) => this.handleTabPlusButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-plus' />
|
||||
@@ -668,7 +833,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
onClick={(e) => this.handleModeButtonClick(e, this.state.snippetIndex)}
|
||||
>
|
||||
{this.state.note.snippets[this.state.snippetIndex].mode == null
|
||||
? 'Select Syntax...'
|
||||
? i18n.__('Select Syntax...')
|
||||
: this.state.note.snippets[this.state.snippetIndex].mode
|
||||
}
|
||||
<i className='fa fa-caret-down' />
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
|
||||
.body
|
||||
absolute left right
|
||||
left $snippet-note-detail-left-margin
|
||||
right $snippet-note-detail-right-margin
|
||||
margin 0 30px
|
||||
top $info-height + $info-margin-under-border
|
||||
bottom $statusBar-height
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
@@ -36,13 +35,26 @@
|
||||
height 30px
|
||||
display flex
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
overflow hidden
|
||||
|
||||
.tabList .list
|
||||
flex 1
|
||||
display flex
|
||||
overflow hidden
|
||||
overflow-x scroll
|
||||
position relative
|
||||
|
||||
.tabList .plusButton
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.allTabs
|
||||
display flex
|
||||
position relative
|
||||
overflow visible
|
||||
left 0
|
||||
transition left 0.1s
|
||||
|
||||
.tabList .tabButton
|
||||
navWhiteButtonColor()
|
||||
width 30px
|
||||
|
||||
@@ -69,7 +81,22 @@
|
||||
.control-fullScreenButton
|
||||
top 80px
|
||||
margin-bottom 10px
|
||||
topBarButtonLight()
|
||||
topBarButtonRight()
|
||||
&:hover .tooltip
|
||||
opacity 1
|
||||
|
||||
.tooltip
|
||||
tooltip()
|
||||
position absolute
|
||||
pointer-events none
|
||||
top 50px
|
||||
right 70px
|
||||
z-index 200
|
||||
padding 5px
|
||||
line-height normal
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
body[data-theme="white"]
|
||||
.root
|
||||
@@ -78,7 +105,7 @@ body[data-theme="white"]
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
border none
|
||||
border-left 1px solid $ui-dark-borderColor
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
box-shadow none
|
||||
|
||||
@@ -109,3 +136,37 @@ body[data-theme="dark"]
|
||||
|
||||
.control-fullScreenButton
|
||||
topBarButtonDark()
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
border-left 1px solid $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
|
||||
.body
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
|
||||
.body .description textarea
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
border 1px solid $ui-solarized-dark-borderColor
|
||||
|
||||
.tabList
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
border-left 1px solid $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
.body
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
.body .description textarea
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
border 1px solid $ui-monokai-borderColor
|
||||
|
||||
.tabList
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
@@ -3,6 +3,7 @@ import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './StarButton.styl'
|
||||
import _ from 'lodash'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
class StarButton extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -46,14 +47,14 @@ class StarButton extends React.Component {
|
||||
onMouseDown={(e) => this.handleMouseDown(e)}
|
||||
onMouseUp={(e) => this.handleMouseUp(e)}
|
||||
onMouseLeave={(e) => this.handleMouseLeave(e)}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
onClick={this.props.onClick}>
|
||||
<img styleName='icon'
|
||||
src={this.state.isActive || this.props.isActive
|
||||
? '../resources/icon/icon-starred.svg'
|
||||
: '../resources/icon/icon-star.svg'
|
||||
}
|
||||
/>
|
||||
<span styleName='tooltip'>{i18n.__('Star')}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
.root
|
||||
top 45px
|
||||
topBarButtonLight()
|
||||
topBarButtonRight()
|
||||
&:hover
|
||||
transition 0.2s
|
||||
color alpha($ui-favorite-star-button-color, 0.6)
|
||||
&:hover .tooltip
|
||||
opacity 1
|
||||
|
||||
.tooltip
|
||||
tooltip()
|
||||
position absolute
|
||||
pointer-events none
|
||||
top 50px
|
||||
right 115px
|
||||
width 40px
|
||||
z-index 200
|
||||
padding 5px
|
||||
line-height normal
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
.root--active
|
||||
@extend .root
|
||||
|
||||
@@ -4,6 +4,7 @@ import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './TagSelect.styl'
|
||||
import _ from 'lodash'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
class TagSelect extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -43,16 +44,9 @@ class TagSelect extends React.Component {
|
||||
}
|
||||
|
||||
removeLastTag () {
|
||||
let { value } = this.props
|
||||
|
||||
value = _.isArray(value)
|
||||
? value.slice()
|
||||
: []
|
||||
value.pop()
|
||||
value = _.uniq(value)
|
||||
|
||||
this.value = value
|
||||
this.props.onChange()
|
||||
this.removeTagByCallback((value) => {
|
||||
value.pop()
|
||||
})
|
||||
}
|
||||
|
||||
reset () {
|
||||
@@ -64,7 +58,8 @@ class TagSelect extends React.Component {
|
||||
submitTag () {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
|
||||
let { value } = this.props
|
||||
const newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
|
||||
let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
|
||||
newTag = newTag.charAt(0) === '#' ? newTag.substring(1) : newTag
|
||||
|
||||
if (newTag.length <= 0) {
|
||||
this.setState({
|
||||
@@ -94,15 +89,22 @@ class TagSelect extends React.Component {
|
||||
}
|
||||
|
||||
handleTagRemoveButtonClick (tag) {
|
||||
return (e) => {
|
||||
let { value } = this.props
|
||||
|
||||
this.removeTagByCallback((value, tag) => {
|
||||
value.splice(value.indexOf(tag), 1)
|
||||
value = _.uniq(value)
|
||||
}, tag)
|
||||
}
|
||||
|
||||
this.value = value
|
||||
this.props.onChange()
|
||||
}
|
||||
removeTagByCallback (callback, tag = null) {
|
||||
let { value } = this.props
|
||||
|
||||
value = _.isArray(value)
|
||||
? value.slice()
|
||||
: []
|
||||
callback(value, tag)
|
||||
value = _.uniq(value)
|
||||
|
||||
this.value = value
|
||||
this.props.onChange()
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -116,7 +118,7 @@ class TagSelect extends React.Component {
|
||||
>
|
||||
<span styleName='tag-label'>#{tag}</span>
|
||||
<button styleName='tag-removeButton'
|
||||
onClick={(e) => this.handleTagRemoveButtonClick(tag)(e)}
|
||||
onClick={(e) => this.handleTagRemoveButtonClick(tag)}
|
||||
>
|
||||
<img className='tag-removeButton-icon' src='../resources/icon/icon-x.svg' width='8px' />
|
||||
</button>
|
||||
@@ -136,7 +138,7 @@ class TagSelect extends React.Component {
|
||||
<input styleName='newTag'
|
||||
ref='newTag'
|
||||
value={this.state.newTag}
|
||||
placeholder='Add tag...'
|
||||
placeholder={i18n.__('Add tag...')}
|
||||
onChange={(e) => this.handleNewTagInputChange(e)}
|
||||
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
|
||||
onBlur={(e) => this.handleNewTagBlur(e)}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
width 100%
|
||||
overflow-x scroll
|
||||
white-space nowrap
|
||||
margin-top 31px
|
||||
position absolute
|
||||
|
||||
.root::-webkit-scrollbar
|
||||
display none
|
||||
@@ -63,4 +65,36 @@ body[data-theme="dark"]
|
||||
.newTag
|
||||
border-color none
|
||||
background-color transparent
|
||||
color $ui-dark-text-color
|
||||
color $ui-dark-text-color
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.tag
|
||||
background-color $ui-solarized-dark-tag-backgroundColor
|
||||
|
||||
.tag-removeButton
|
||||
border-color $ui-button--focus-borderColor
|
||||
background-color transparent
|
||||
|
||||
.tag-label
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.newTag
|
||||
border-color none
|
||||
background-color transparent
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.tag
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
|
||||
.tag-removeButton
|
||||
border-color $ui-button--focus-borderColor
|
||||
background-color transparent
|
||||
|
||||
.tag-label
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.newTag
|
||||
border-color none
|
||||
background-color transparent
|
||||
color $ui-monokai-text-color
|
||||
|
||||
26
browser/main/Detail/ToggleModeButton.js
Normal file
26
browser/main/Detail/ToggleModeButton.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './ToggleModeButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const ToggleModeButton = ({
|
||||
onClick, editorType
|
||||
}) => (
|
||||
<div styleName='control-toggleModeButton'>
|
||||
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}>
|
||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
|
||||
</div>
|
||||
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
|
||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
|
||||
</div>
|
||||
<span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
ToggleModeButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
editorType: PropTypes.string.Required
|
||||
}
|
||||
|
||||
export default CSSModules(ToggleModeButton, styles)
|
||||
65
browser/main/Detail/ToggleModeButton.styl
Normal file
65
browser/main/Detail/ToggleModeButton.styl
Normal file
@@ -0,0 +1,65 @@
|
||||
.control-toggleModeButton
|
||||
height 25px
|
||||
border-radius 50px
|
||||
background-color #F4F4F4
|
||||
width 52px
|
||||
display flex
|
||||
align-items center
|
||||
position: relative
|
||||
top 2px
|
||||
.active
|
||||
background-color #1EC38B
|
||||
width 33px
|
||||
height 24px
|
||||
box-shadow 2px 0px 7px #eee
|
||||
z-index 1
|
||||
|
||||
div
|
||||
width 40px
|
||||
height 100%
|
||||
border-radius 50%
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
cursor pointer
|
||||
|
||||
&:hover .tooltip
|
||||
opacity 1
|
||||
|
||||
.tooltip
|
||||
tooltip()
|
||||
position absolute
|
||||
pointer-events none
|
||||
top 33px
|
||||
left -10px
|
||||
z-index 200
|
||||
width 80px
|
||||
padding 5px
|
||||
line-height normal
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
body[data-theme="dark"]
|
||||
.control-fullScreenButton
|
||||
topBarButtonDark()
|
||||
|
||||
.control-toggleModeButton
|
||||
background-color #3A404C
|
||||
.active
|
||||
background-color #1EC38B
|
||||
box-shadow 2px 0px 7px #444444
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.control-toggleModeButton
|
||||
background-color #002B36
|
||||
.active
|
||||
background-color #1EC38B
|
||||
box-shadow 2px 0px 7px #222222
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.control-toggleModeButton
|
||||
background-color #272822
|
||||
.active
|
||||
background-color #1EC38B
|
||||
box-shadow 2px 0px 7px #222222
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './TrashButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const TrashButton = ({
|
||||
onClick
|
||||
@@ -10,6 +11,7 @@ const TrashButton = ({
|
||||
onClick={(e) => onClick(e)}
|
||||
>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
||||
<span styleName='tooltip'>{i18n.__('Trash')}</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
.control-trashButton
|
||||
top 120px
|
||||
margin-bottom 10px
|
||||
topBarButtonLight()
|
||||
top 115px
|
||||
topBarButtonRight()
|
||||
&:hover .tooltip
|
||||
opacity 1
|
||||
|
||||
.tooltip
|
||||
tooltip()
|
||||
position absolute
|
||||
pointer-events none
|
||||
top 50px
|
||||
right 50px
|
||||
z-index 200
|
||||
padding 5px
|
||||
line-height normal
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
.control-trashButton--in-trash
|
||||
top 60px
|
||||
topBarButtonLight()
|
||||
topBarButtonRight()
|
||||
|
||||
.trashButton
|
||||
padding 0px
|
||||
|
||||
@@ -7,6 +7,8 @@ import MarkdownNoteDetail from './MarkdownNoteDetail'
|
||||
import SnippetNoteDetail from './SnippetNoteDetail'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import StatusBar from '../StatusBar'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import debounceRender from 'react-debounce-render'
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
@@ -36,11 +38,8 @@ class Detail extends React.Component {
|
||||
const { location, data, config } = this.props
|
||||
let note = null
|
||||
if (location.query.key != null) {
|
||||
const splitted = location.query.key.split('-')
|
||||
const storageKey = splitted.shift()
|
||||
const noteKey = splitted.shift()
|
||||
|
||||
note = data.noteMap.get(storageKey + '-' + noteKey)
|
||||
const noteKey = location.query.key
|
||||
note = data.noteMap.get(noteKey)
|
||||
}
|
||||
|
||||
if (note == null) {
|
||||
@@ -50,7 +49,7 @@ class Detail extends React.Component {
|
||||
tabIndex='0'
|
||||
>
|
||||
<div styleName='empty'>
|
||||
<div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br />to create a new note</div>
|
||||
<div styleName='empty-message'>{OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')} + N<br />{i18n.__('to create a new note')}</div>
|
||||
</div>
|
||||
<StatusBar
|
||||
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
||||
@@ -101,4 +100,4 @@ Detail.propTypes = {
|
||||
ignorePreviewPointerEvents: PropTypes.bool
|
||||
}
|
||||
|
||||
export default CSSModules(Detail, styles)
|
||||
export default debounceRender(CSSModules(Detail, styles))
|
||||
|
||||
@@ -10,10 +10,16 @@ import Detail from './Detail'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import _ from 'lodash'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import modal from 'browser/main/lib/modal'
|
||||
import InitModal from 'browser/main/modals/InitModal'
|
||||
import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import { hashHistory } from 'react-router'
|
||||
import store from 'browser/main/store'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { getLocales } from 'browser/lib/Languages'
|
||||
import applyShortcuts from 'browser/main/lib/shortcutManager'
|
||||
const path = require('path')
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
|
||||
class Main extends React.Component {
|
||||
|
||||
@@ -48,17 +54,113 @@ class Main extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
init () {
|
||||
dataApi
|
||||
.addStorage({
|
||||
name: 'My Storage',
|
||||
path: path.join(remote.app.getPath('home'), 'Boostnote')
|
||||
})
|
||||
.then((data) => {
|
||||
return data
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.storage.folders[0] != null) {
|
||||
return data
|
||||
} else {
|
||||
return dataApi
|
||||
.createFolder(data.storage.key, {
|
||||
color: '#1278BD',
|
||||
name: 'Default'
|
||||
})
|
||||
.then((_data) => {
|
||||
return {
|
||||
storage: _data.storage,
|
||||
notes: data.notes
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
console.log(data)
|
||||
store.dispatch({
|
||||
type: 'ADD_STORAGE',
|
||||
storage: data.storage,
|
||||
notes: data.notes
|
||||
})
|
||||
|
||||
const defaultSnippetNote = dataApi
|
||||
.createNote(data.storage.key, {
|
||||
type: 'SNIPPET_NOTE',
|
||||
folder: data.storage.folders[0].key,
|
||||
title: 'Snippet note example',
|
||||
description: 'Snippet note example\nYou can store a series of snippets as a single note, like Gist.',
|
||||
snippets: [
|
||||
{
|
||||
name: 'example.html',
|
||||
mode: 'html',
|
||||
content: '<html>\n<body>\n<h1 id=\'hello\'>Enjoy Boostnote!</h1>\n</body>\n</html>'
|
||||
},
|
||||
{
|
||||
name: 'example.js',
|
||||
mode: 'javascript',
|
||||
content: 'var boostnote = document.getElementById(\'enjoy\').innerHTML\n\nconsole.log(boostnote)'
|
||||
}
|
||||
]
|
||||
})
|
||||
.then((note) => {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
})
|
||||
const defaultMarkdownNote = dataApi
|
||||
.createNote(data.storage.key, {
|
||||
type: 'MARKDOWN_NOTE',
|
||||
folder: data.storage.folders[0].key,
|
||||
title: 'Welcome to Boostnote!',
|
||||
content: '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)'
|
||||
})
|
||||
.then((note) => {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
})
|
||||
|
||||
return Promise.resolve(defaultSnippetNote)
|
||||
.then(defaultMarkdownNote)
|
||||
.then(() => data.storage)
|
||||
})
|
||||
.then((storage) => {
|
||||
hashHistory.push('/storages/' + storage.key)
|
||||
})
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, config } = this.props
|
||||
|
||||
if (config.ui.theme === 'dark') {
|
||||
document.body.setAttribute('data-theme', 'dark')
|
||||
} else if (config.ui.theme === 'white') {
|
||||
document.body.setAttribute('data-theme', 'white')
|
||||
const supportedThemes = [
|
||||
'dark',
|
||||
'white',
|
||||
'solarized-dark',
|
||||
'monokai'
|
||||
]
|
||||
|
||||
if (supportedThemes.indexOf(config.ui.theme) !== -1) {
|
||||
document.body.setAttribute('data-theme', config.ui.theme)
|
||||
} else {
|
||||
document.body.setAttribute('data-theme', 'default')
|
||||
}
|
||||
|
||||
if (getLocales().indexOf(config.ui.language) !== -1) {
|
||||
i18n.setLocale(config.ui.language)
|
||||
} else {
|
||||
i18n.setLocale('en')
|
||||
}
|
||||
applyShortcuts()
|
||||
// Reload all data
|
||||
dataApi.init()
|
||||
.then((data) => {
|
||||
@@ -69,7 +171,7 @@ class Main extends React.Component {
|
||||
})
|
||||
|
||||
if (data.storages.length < 1) {
|
||||
modal.open(InitModal)
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -71,3 +71,11 @@ body[data-theme="dark"]
|
||||
|
||||
.control-newNoteButton-tooltip
|
||||
darkTooltip()
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
@@ -6,6 +6,7 @@ import _ from 'lodash'
|
||||
import modal from 'browser/main/lib/modal'
|
||||
import NewNoteModal from 'browser/main/modals/NewNoteModal'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { dialog } = remote
|
||||
@@ -56,9 +57,9 @@ class NewNoteButton extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
if (storage == null) this.showMessageBox('No storage to create a note')
|
||||
if (storage == null) this.showMessageBox(i18n.__('No storage to create a note'))
|
||||
const folder = _.find(storage.folders, {key: params.folderKey}) || storage.folders[0]
|
||||
if (folder == null) this.showMessageBox('No folder to create a note')
|
||||
if (folder == null) this.showMessageBox(i18n.__('No folder to create a note'))
|
||||
|
||||
return {
|
||||
storage,
|
||||
@@ -86,7 +87,7 @@ class NewNoteButton extends React.Component {
|
||||
onClick={(e) => this.handleNewNoteButtonClick(e)}>
|
||||
<img styleName='iconTag' src='../resources/icon/icon-newnote.svg' />
|
||||
<span styleName='control-newNoteButton-tooltip'>
|
||||
Make a Note {OSX ? '⌘' : '^'} + n
|
||||
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -88,4 +88,53 @@ body[data-theme="dark"]
|
||||
.control-button--active
|
||||
color $ui-dark-text-color
|
||||
&:active
|
||||
color $ui-dark-text-color
|
||||
color $ui-dark-text-color
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
|
||||
.control
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
|
||||
.control-sortBy-select
|
||||
&:hover
|
||||
transition 0.2s
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.control-button
|
||||
color $ui-solarized-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.control-button--active
|
||||
color $ui-solarized-dark-text-color
|
||||
&:active
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.control
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
.control-sortBy-select
|
||||
&:hover
|
||||
transition 0.2s
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.control-button
|
||||
color $ui-monokai-inactive-text-color
|
||||
&:hover
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.control-button--active
|
||||
color $ui-monokai-text-color
|
||||
&:active
|
||||
color $ui-monokai-text-color
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
/* global electron */
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import debounceRender from 'react-debounce-render'
|
||||
import styles from './NoteList.styl'
|
||||
import moment from 'moment'
|
||||
import _ from 'lodash'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import NoteItem from 'browser/components/NoteItem'
|
||||
import NoteItemSimple from 'browser/components/NoteItemSimple'
|
||||
import searchFromNotes from 'browser/lib/search'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { hashHistory } from 'react-router'
|
||||
import markdown from 'browser/lib/markdown'
|
||||
import { findNoteTitle } from 'browser/lib/findNoteTitle'
|
||||
import store from 'browser/main/store'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import Markdown from '../../lib/markdown'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { Menu, MenuItem, dialog } = remote
|
||||
const WP_POST_PATH = '/wp/v2/posts'
|
||||
|
||||
function sortByCreatedAt (a, b) {
|
||||
return new Date(b.createdAt) - new Date(a.createdAt)
|
||||
@@ -33,7 +39,7 @@ function sortByUpdatedAt (a, b) {
|
||||
}
|
||||
|
||||
function findNoteByKey (notes, noteKey) {
|
||||
return notes.find((note) => `${note.storage}-${note.key}` === noteKey)
|
||||
return notes.find((note) => note.key === noteKey)
|
||||
}
|
||||
|
||||
function findNotesByKeys (notes, noteKeys) {
|
||||
@@ -41,7 +47,7 @@ function findNotesByKeys (notes, noteKeys) {
|
||||
}
|
||||
|
||||
function getNoteKey (note) {
|
||||
return `${note.storage}-${note.key}`
|
||||
return note.key
|
||||
}
|
||||
|
||||
class NoteList extends React.Component {
|
||||
@@ -68,6 +74,11 @@ class NoteList extends React.Component {
|
||||
this.deleteNote = this.deleteNote.bind(this)
|
||||
this.focusNote = this.focusNote.bind(this)
|
||||
this.pinToTop = this.pinToTop.bind(this)
|
||||
this.getNoteStorage = this.getNoteStorage.bind(this)
|
||||
this.getNoteFolder = this.getNoteFolder.bind(this)
|
||||
this.getViewType = this.getViewType.bind(this)
|
||||
this.restoreNote = this.restoreNote.bind(this)
|
||||
this.copyNoteLink = this.copyNoteLink.bind(this)
|
||||
|
||||
// TODO: not Selected noteKeys but SelectedNote(for reusing)
|
||||
this.state = {
|
||||
@@ -111,14 +122,27 @@ class NoteList extends React.Component {
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { location } = this.props
|
||||
const { selectedNoteKeys } = this.state
|
||||
const visibleNoteKeys = this.notes.map(note => note.key)
|
||||
const note = this.notes[0]
|
||||
const prevKey = prevProps.location.query.key
|
||||
const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && note.key
|
||||
|
||||
if (this.notes.length > 0 && location.query.key == null) {
|
||||
if (note && location.query.key == null) {
|
||||
const { router } = this.context
|
||||
if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes()
|
||||
|
||||
// A visible note is an active note
|
||||
if (!selectedNoteKeys.includes(noteKey)) {
|
||||
if (selectedNoteKeys.length === 1) selectedNoteKeys.pop()
|
||||
selectedNoteKeys.push(noteKey)
|
||||
ee.emit('list:moved')
|
||||
}
|
||||
|
||||
router.replace({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: this.notes[0].storage + '-' + this.notes[0].key
|
||||
key: noteKey
|
||||
}
|
||||
})
|
||||
return
|
||||
@@ -171,9 +195,8 @@ class NoteList extends React.Component {
|
||||
if (this.notes == null || this.notes.length === 0) {
|
||||
return
|
||||
}
|
||||
let { router } = this.context
|
||||
let { location } = this.props
|
||||
let { selectedNoteKeys, shiftKeyDown } = this.state
|
||||
let { selectedNoteKeys } = this.state
|
||||
const { shiftKeyDown } = this.state
|
||||
|
||||
let targetIndex = this.getTargetIndex()
|
||||
|
||||
@@ -199,9 +222,8 @@ class NoteList extends React.Component {
|
||||
if (this.notes == null || this.notes.length === 0) {
|
||||
return
|
||||
}
|
||||
let { router } = this.context
|
||||
let { location } = this.props
|
||||
let { selectedNoteKeys, shiftKeyDown } = this.state
|
||||
let { selectedNoteKeys } = this.state
|
||||
const { shiftKeyDown } = this.state
|
||||
|
||||
let targetIndex = this.getTargetIndex()
|
||||
const isTargetLastNote = targetIndex === this.notes.length - 1
|
||||
@@ -235,47 +257,47 @@ class NoteList extends React.Component {
|
||||
return
|
||||
}
|
||||
|
||||
const { router } = this.context
|
||||
const { location } = this.props
|
||||
|
||||
let targetIndex = this.getTargetIndex()
|
||||
|
||||
if (targetIndex < 0) targetIndex = 0
|
||||
|
||||
const selectedNoteKeys = []
|
||||
const nextNoteKey = this.getNoteKeyFromTargetIndex(targetIndex)
|
||||
selectedNoteKeys.push(nextNoteKey)
|
||||
|
||||
this.focusNote(selectedNoteKeys, nextNoteKey)
|
||||
const selectedNoteKeys = [noteHash]
|
||||
this.focusNote(selectedNoteKeys, noteHash)
|
||||
|
||||
ee.emit('list:moved')
|
||||
}
|
||||
|
||||
handleNoteListKeyDown (e) {
|
||||
const { shiftKeyDown } = this.state
|
||||
if (e.metaKey || e.ctrlKey) return true
|
||||
|
||||
// A key
|
||||
if (e.keyCode === 65 && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
ee.emit('top:new-note')
|
||||
}
|
||||
|
||||
// D key
|
||||
if (e.keyCode === 68) {
|
||||
e.preventDefault()
|
||||
this.deleteNote()
|
||||
}
|
||||
|
||||
// E key
|
||||
if (e.keyCode === 69) {
|
||||
e.preventDefault()
|
||||
ee.emit('detail:focus')
|
||||
}
|
||||
|
||||
if (e.keyCode === 38) {
|
||||
// L or S key
|
||||
if (e.keyCode === 76 || e.keyCode === 83) {
|
||||
e.preventDefault()
|
||||
ee.emit('top:focus-search')
|
||||
}
|
||||
|
||||
// UP or K key
|
||||
if (e.keyCode === 38 || e.keyCode === 75) {
|
||||
e.preventDefault()
|
||||
this.selectPriorNote()
|
||||
}
|
||||
|
||||
if (e.keyCode === 40) {
|
||||
// DOWN or J key
|
||||
if (e.keyCode === 40 || e.keyCode === 74) {
|
||||
e.preventDefault()
|
||||
this.selectNextNote()
|
||||
}
|
||||
@@ -294,7 +316,7 @@ class NoteList extends React.Component {
|
||||
getNotes () {
|
||||
const { data, params, location } = this.props
|
||||
|
||||
if (location.pathname.match(/\/home/) || location.pathname.match(/\alltags/)) {
|
||||
if (location.pathname.match(/\/home/) || location.pathname.match(/alltags/)) {
|
||||
const allNotes = data.noteMap.map((note) => note)
|
||||
this.contextNotes = allNotes
|
||||
return allNotes
|
||||
@@ -307,8 +329,10 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
if (location.pathname.match(/\/searched/)) {
|
||||
const searchInputText = document.getElementsByClassName('searchInput')[0].value
|
||||
if (searchInputText === '') {
|
||||
const searchInputText = params.searchword
|
||||
const allNotes = data.noteMap.map((note) => note)
|
||||
this.contextNotes = allNotes
|
||||
if (searchInputText === undefined || searchInputText === '') {
|
||||
return this.sortByPin(this.contextNotes)
|
||||
}
|
||||
return searchFromNotes(this.contextNotes, searchInputText)
|
||||
@@ -321,11 +345,10 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
if (location.pathname.match(/\/tags/)) {
|
||||
const listOfTags = params.tagname.split(' ')
|
||||
return data.noteMap.map(note => {
|
||||
return note
|
||||
}).filter(note => {
|
||||
return note.tags.includes(params.tagname)
|
||||
})
|
||||
}).filter(note => listOfTags.every(tag => note.tags.includes(tag)))
|
||||
}
|
||||
|
||||
return this.getContextNotes()
|
||||
@@ -365,9 +388,10 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
handleNoteClick (e, uniqueKey) {
|
||||
let { router } = this.context
|
||||
let { location } = this.props
|
||||
let { shiftKeyDown, selectedNoteKeys } = this.state
|
||||
const { router } = this.context
|
||||
const { location } = this.props
|
||||
let { selectedNoteKeys } = this.state
|
||||
const { shiftKeyDown } = this.state
|
||||
|
||||
if (shiftKeyDown && selectedNoteKeys.includes(uniqueKey)) {
|
||||
const newSelectedNoteKeys = selectedNoteKeys.filter((noteKey) => noteKey !== uniqueKey)
|
||||
@@ -425,20 +449,27 @@ class NoteList extends React.Component {
|
||||
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Sorry!',
|
||||
detail: 'md/text import is available only a markdown note.',
|
||||
buttons: ['OK', 'Cancel']
|
||||
message: i18n.__('Sorry!'),
|
||||
detail: i18n.__('md/text import is available only a markdown note.'),
|
||||
buttons: [i18n.__('OK'), i18n.__('Cancel')]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleDragStart (e, note) {
|
||||
const { selectedNoteKeys } = this.state
|
||||
let { selectedNoteKeys } = this.state
|
||||
const noteKey = getNoteKey(note)
|
||||
|
||||
if (!selectedNoteKeys.includes(noteKey)) {
|
||||
selectedNoteKeys = []
|
||||
selectedNoteKeys.push(noteKey)
|
||||
}
|
||||
|
||||
const notes = this.notes.map((note) => Object.assign({}, note))
|
||||
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||
const noteData = JSON.stringify(selectedNotes)
|
||||
e.dataTransfer.setData('note', noteData)
|
||||
this.setState({ selectedNoteKeys: [] })
|
||||
this.selectNextNote()
|
||||
}
|
||||
|
||||
handleNoteContextMenu (e, uniqueKey) {
|
||||
@@ -451,45 +482,110 @@ class NoteList extends React.Component {
|
||||
this.handleNoteClick(e, uniqueKey)
|
||||
}
|
||||
|
||||
const pinLabel = note.isPinned ? 'Remove pin' : 'Pin to Top'
|
||||
const deleteLabel = 'Delete Note'
|
||||
const pinLabel = note.isPinned ? i18n.__('Remove pin') : i18n.__('Pin to Top')
|
||||
const deleteLabel = i18n.__('Delete Note')
|
||||
const cloneNote = i18n.__('Clone Note')
|
||||
const restoreNote = i18n.__('Restore Note')
|
||||
const copyNoteLink = i18n.__('Copy Note Link')
|
||||
const publishLabel = i18n.__('Publish Blog')
|
||||
const updateLabel = i18n.__('Update Blog')
|
||||
const openBlogLabel = i18n.__('Open Blog')
|
||||
|
||||
const menu = new Menu()
|
||||
if (!location.pathname.match(/\/home|\/starred|\/trash/)) {
|
||||
|
||||
if (location.pathname.match(/\/trash/)) {
|
||||
menu.append(new MenuItem({
|
||||
label: pinLabel,
|
||||
click: this.pinToTop
|
||||
label: restoreNote,
|
||||
click: this.restoreNote
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
label: deleteLabel,
|
||||
click: this.deleteNote
|
||||
}))
|
||||
} else {
|
||||
if (!location.pathname.match(/\/starred/)) {
|
||||
menu.append(new MenuItem({
|
||||
label: pinLabel,
|
||||
click: this.pinToTop
|
||||
}))
|
||||
}
|
||||
menu.append(new MenuItem({
|
||||
label: deleteLabel,
|
||||
click: this.deleteNote
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
label: cloneNote,
|
||||
click: this.cloneNote.bind(this)
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
label: copyNoteLink,
|
||||
click: this.copyNoteLink(note)
|
||||
}))
|
||||
if (note.type === 'MARKDOWN_NOTE') {
|
||||
if (note.blog && note.blog.blogLink && note.blog.blogId) {
|
||||
menu.append(new MenuItem({
|
||||
label: updateLabel,
|
||||
click: this.publishMarkdown.bind(this)
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
label: openBlogLabel,
|
||||
click: () => this.openBlog.bind(this)(note)
|
||||
}))
|
||||
} else {
|
||||
menu.append(new MenuItem({
|
||||
label: publishLabel,
|
||||
click: this.publishMarkdown.bind(this)
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
menu.append(new MenuItem({
|
||||
label: deleteLabel,
|
||||
click: this.deleteNote
|
||||
}))
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
pinToTop () {
|
||||
updateSelectedNotes (updateFunc, cleanSelection = true) {
|
||||
const { selectedNoteKeys } = this.state
|
||||
const { dispatch } = this.props
|
||||
const notes = this.notes.map((note) => Object.assign({}, note))
|
||||
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||
|
||||
if (!_.isFunction(updateFunc)) {
|
||||
console.warn('Update function is not defined. No update will happen')
|
||||
updateFunc = (note) => { return note }
|
||||
}
|
||||
|
||||
Promise.all(
|
||||
selectedNotes.map((note) => {
|
||||
note.isPinned = !note.isPinned
|
||||
return dataApi
|
||||
.updateNote(note.storage, note.key, note)
|
||||
})
|
||||
)
|
||||
.then((updatedNotes) => {
|
||||
updatedNotes.forEach((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note
|
||||
selectedNotes.map((note) => {
|
||||
note = updateFunc(note)
|
||||
return dataApi
|
||||
.updateNote(note.storage, note.key, note)
|
||||
})
|
||||
})
|
||||
)
|
||||
.then((updatedNotes) => {
|
||||
updatedNotes.forEach((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
if (cleanSelection) {
|
||||
this.selectNextNote()
|
||||
}
|
||||
}
|
||||
|
||||
pinToTop () {
|
||||
this.updateSelectedNotes((note) => {
|
||||
note.isPinned = !note.isPinned
|
||||
return note
|
||||
})
|
||||
}
|
||||
|
||||
restoreNote () {
|
||||
this.updateSelectedNotes((note) => {
|
||||
note.isTrashed = false
|
||||
return note
|
||||
})
|
||||
this.setState({ selectedNoteKeys: [] })
|
||||
}
|
||||
|
||||
deleteNote () {
|
||||
@@ -498,22 +594,15 @@ class NoteList extends React.Component {
|
||||
const notes = this.notes.map((note) => Object.assign({}, note))
|
||||
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||
const firstNote = selectedNotes[0]
|
||||
const { confirmDeletion } = this.props.config.ui
|
||||
|
||||
if (firstNote.isTrashed) {
|
||||
const noteExp = selectedNotes.length > 1 ? 'notes' : 'note'
|
||||
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Confirm note deletion',
|
||||
detail: `This will permanently remove ${selectedNotes.length} ${noteExp}.`,
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
if (dialogueButtonIndex === 1) return
|
||||
if (!confirmDeleteNote(confirmDeletion, true)) return
|
||||
|
||||
Promise.all(
|
||||
selectedNoteKeys.map((uniqueKey) => {
|
||||
const storageKey = uniqueKey.split('-')[0]
|
||||
const noteKey = uniqueKey.split('-')[1]
|
||||
selectedNotes.map((note) => {
|
||||
return dataApi
|
||||
.deleteNote(storageKey, noteKey)
|
||||
.deleteNote(note.storage, note.key)
|
||||
})
|
||||
)
|
||||
.then((data) => {
|
||||
@@ -530,6 +619,8 @@ class NoteList extends React.Component {
|
||||
})
|
||||
console.log('Notes were all deleted')
|
||||
} else {
|
||||
if (!confirmDeleteNote(confirmDeletion, false)) return
|
||||
|
||||
Promise.all(
|
||||
selectedNotes.map((note) => {
|
||||
note.isTrashed = true
|
||||
@@ -555,6 +646,157 @@ class NoteList extends React.Component {
|
||||
this.setState({ selectedNoteKeys: [] })
|
||||
}
|
||||
|
||||
cloneNote () {
|
||||
const { selectedNoteKeys } = this.state
|
||||
const { dispatch, location } = this.props
|
||||
const { storage, folder } = this.resolveTargetFolder()
|
||||
const notes = this.notes.map((note) => Object.assign({}, note))
|
||||
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||
const firstNote = selectedNotes[0]
|
||||
const eventName = firstNote.type === 'MARKDOWN_NOTE' ? 'ADD_MARKDOWN' : 'ADD_SNIPPET'
|
||||
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent(eventName)
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||
dataApi
|
||||
.createNote(storage.key, {
|
||||
type: firstNote.type,
|
||||
folder: folder.key,
|
||||
title: firstNote.title + ' ' + i18n.__('copy'),
|
||||
content: firstNote.content
|
||||
})
|
||||
.then((note) => {
|
||||
attachmentManagement.cloneAttachments(firstNote, note)
|
||||
return note
|
||||
})
|
||||
.then((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
|
||||
this.setState({
|
||||
selectedNoteKeys: [note.key]
|
||||
})
|
||||
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: {key: note.key}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
copyNoteLink (note) {
|
||||
const noteLink = `[${note.title}](:note:${note.key})`
|
||||
return copy(noteLink)
|
||||
}
|
||||
|
||||
save (note) {
|
||||
const { dispatch } = this.props
|
||||
dataApi
|
||||
.updateNote(note.storage, note.key, note)
|
||||
.then((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
publishMarkdown () {
|
||||
if (this.pendingPublish) {
|
||||
clearTimeout(this.pendingPublish)
|
||||
}
|
||||
this.pendingPublish = setTimeout(() => {
|
||||
this.publishMarkdownNow()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
publishMarkdownNow () {
|
||||
const {selectedNoteKeys} = this.state
|
||||
const notes = this.notes.map((note) => Object.assign({}, note))
|
||||
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||
const firstNote = selectedNotes[0]
|
||||
const config = ConfigManager.get()
|
||||
const {address, token, authMethod, username, password} = config.blog
|
||||
let authToken = ''
|
||||
if (authMethod === 'USER') {
|
||||
authToken = `Basic ${window.btoa(`${username}:${password}`)}`
|
||||
} else {
|
||||
authToken = `Bearer ${token}`
|
||||
}
|
||||
const contentToRender = firstNote.content.replace(`# ${firstNote.title}`, '')
|
||||
const markdown = new Markdown()
|
||||
const data = {
|
||||
title: firstNote.title,
|
||||
content: markdown.render(contentToRender),
|
||||
status: 'publish'
|
||||
}
|
||||
|
||||
let url = ''
|
||||
let method = ''
|
||||
if (firstNote.blog && firstNote.blog.blogId) {
|
||||
url = `${address}${WP_POST_PATH}/${firstNote.blog.blogId}`
|
||||
method = 'PUT'
|
||||
} else {
|
||||
url = `${address}${WP_POST_PATH}`
|
||||
method = 'POST'
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
fetch(url, {
|
||||
method: method,
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Authorization': authToken,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(res => res.json())
|
||||
.then(response => {
|
||||
if (_.isNil(response.link) || _.isNil(response.id)) {
|
||||
return Promise.reject()
|
||||
}
|
||||
firstNote.blog = {
|
||||
blogLink: response.link,
|
||||
blogId: response.id
|
||||
}
|
||||
this.save(firstNote)
|
||||
this.confirmPublish(firstNote)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
this.confirmPublishError()
|
||||
})
|
||||
}
|
||||
|
||||
confirmPublishError () {
|
||||
const { remote } = electron
|
||||
const { dialog } = remote
|
||||
const alertError = {
|
||||
type: 'warning',
|
||||
message: i18n.__('Publish Failed'),
|
||||
detail: i18n.__('Check and update your blog setting and try again.'),
|
||||
buttons: [i18n.__('Confirm')]
|
||||
}
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), alertError)
|
||||
}
|
||||
|
||||
confirmPublish (note) {
|
||||
const buttonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: i18n.__('Publish Succeeded'),
|
||||
detail: `${note.title} is published at ${note.blog.blogLink}`,
|
||||
buttons: [i18n.__('Confirm'), i18n.__('Open Blog')]
|
||||
})
|
||||
|
||||
if (buttonIndex === 1) {
|
||||
this.openBlog(note)
|
||||
}
|
||||
}
|
||||
|
||||
openBlog (note) {
|
||||
const { shell } = electron
|
||||
shell.openExternal(note.blog.blogLink)
|
||||
}
|
||||
|
||||
importFromFile () {
|
||||
const options = {
|
||||
filters: [
|
||||
@@ -584,22 +826,29 @@ class NoteList extends React.Component {
|
||||
filepaths.forEach((filepath) => {
|
||||
fs.readFile(filepath, (err, data) => {
|
||||
if (err) throw Error('File reading error: ', err)
|
||||
const content = data.toString()
|
||||
const newNote = {
|
||||
content: content,
|
||||
folder: folder.key,
|
||||
title: markdown.strip(findNoteTitle(content)),
|
||||
type: 'MARKDOWN_NOTE'
|
||||
}
|
||||
dataApi.createNote(storage.key, newNote)
|
||||
.then((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: {key: getNoteKey(note)}
|
||||
|
||||
fs.stat(filepath, (err, {mtime, birthtime}) => {
|
||||
if (err) throw Error('File stat reading error: ', err)
|
||||
|
||||
const content = data.toString()
|
||||
const newNote = {
|
||||
content: content,
|
||||
folder: folder.key,
|
||||
title: path.basename(filepath, path.extname(filepath)),
|
||||
type: 'MARKDOWN_NOTE',
|
||||
createdAt: birthtime,
|
||||
updatedAt: mtime
|
||||
}
|
||||
dataApi.createNote(storage.key, newNote)
|
||||
.then((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: {key: getNoteKey(note)}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -640,19 +889,38 @@ class NoteList extends React.Component {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: message,
|
||||
buttons: ['OK']
|
||||
buttons: [i18n.__('OK')]
|
||||
})
|
||||
}
|
||||
|
||||
getNoteStorage (note) { // note.storage = storage key
|
||||
return this.props.data.storageMap.toJS()[note.storage]
|
||||
}
|
||||
|
||||
getNoteFolder (note) { // note.folder = folder key
|
||||
return _.find(this.getNoteStorage(note).folders, ({ key }) => key === note.folder)
|
||||
}
|
||||
|
||||
getViewType () {
|
||||
const { pathname } = this.props.location
|
||||
const folder = /\/folders\/[a-zA-Z0-9]+/.test(pathname)
|
||||
const storage = /\/storages\/[a-zA-Z0-9]+/.test(pathname) && !folder
|
||||
const allNotes = pathname === '/home'
|
||||
if (allNotes) return 'ALL'
|
||||
if (folder) return 'FOLDER'
|
||||
if (storage) return 'STORAGE'
|
||||
}
|
||||
|
||||
render () {
|
||||
let { location, notes, config, dispatch } = this.props
|
||||
let { selectedNoteKeys } = this.state
|
||||
let sortFunc = config.sortBy === 'CREATED_AT'
|
||||
const { location, config } = this.props
|
||||
let { notes } = this.props
|
||||
const { selectedNoteKeys } = this.state
|
||||
const sortFunc = config.sortBy === 'CREATED_AT'
|
||||
? sortByCreatedAt
|
||||
: config.sortBy === 'ALPHABETICAL'
|
||||
? sortByAlphabetical
|
||||
: sortByUpdatedAt
|
||||
const sortedNotes = location.pathname.match(/\/home|\/starred|\/trash/)
|
||||
const sortedNotes = location.pathname.match(/\/starred|\/trash/)
|
||||
? this.getNotes().sort(sortFunc)
|
||||
: this.sortByPin(this.getNotes().sort(sortFunc))
|
||||
this.notes = notes = sortedNotes.filter((note) => {
|
||||
@@ -660,7 +928,7 @@ class NoteList extends React.Component {
|
||||
if (note.isTrashed !== true || location.pathname === '/trashed') return true
|
||||
})
|
||||
|
||||
moment.locale('en', {
|
||||
moment.updateLocale('en', {
|
||||
relativeTime: {
|
||||
future: 'in %s',
|
||||
past: '%s ago',
|
||||
@@ -679,20 +947,30 @@ class NoteList extends React.Component {
|
||||
}
|
||||
})
|
||||
|
||||
const viewType = this.getViewType()
|
||||
|
||||
const autoSelectFirst =
|
||||
notes.length === 1 ||
|
||||
selectedNoteKeys.length === 0 ||
|
||||
notes.every(note => !selectedNoteKeys.includes(note.key))
|
||||
|
||||
const noteList = notes
|
||||
.map(note => {
|
||||
.map((note, index) => {
|
||||
if (note == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isDefault = config.listStyle === 'DEFAULT'
|
||||
const uniqueKey = getNoteKey(note)
|
||||
const isActive = selectedNoteKeys.includes(uniqueKey)
|
||||
|
||||
const isActive =
|
||||
selectedNoteKeys.includes(uniqueKey) ||
|
||||
notes.length === 1 ||
|
||||
(autoSelectFirst && index === 0)
|
||||
const dateDisplay = moment(
|
||||
config.sortBy === 'CREATED_AT'
|
||||
? note.createdAt : note.updatedAt
|
||||
).fromNow('D')
|
||||
const key = `${note.storage}-${note.key}`
|
||||
|
||||
if (isDefault) {
|
||||
return (
|
||||
@@ -705,6 +983,9 @@ class NoteList extends React.Component {
|
||||
handleNoteClick={this.handleNoteClick.bind(this)}
|
||||
handleDragStart={this.handleDragStart.bind(this)}
|
||||
pathname={location.pathname}
|
||||
folderName={this.getNoteFolder(note).name}
|
||||
storageName={this.getNoteStorage(note).name}
|
||||
viewType={viewType}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -717,6 +998,10 @@ class NoteList extends React.Component {
|
||||
handleNoteContextMenu={this.handleNoteContextMenu.bind(this)}
|
||||
handleNoteClick={this.handleNoteClick.bind(this)}
|
||||
handleDragStart={this.handleDragStart.bind(this)}
|
||||
pathname={location.pathname}
|
||||
folderName={this.getNoteFolder(note).name}
|
||||
storageName={this.getNoteStorage(note).name}
|
||||
viewType={viewType}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -731,16 +1016,17 @@ class NoteList extends React.Component {
|
||||
<div styleName='control-sortBy'>
|
||||
<i className='fa fa-angle-down' />
|
||||
<select styleName='control-sortBy-select'
|
||||
title={i18n.__('Select filter mode')}
|
||||
value={config.sortBy}
|
||||
onChange={(e) => this.handleSortByChange(e)}
|
||||
>
|
||||
<option value='UPDATED_AT'>Updated</option>
|
||||
<option value='CREATED_AT'>Created</option>
|
||||
<option value='ALPHABETICAL'>Alphabetically</option>
|
||||
<option title='Sort by update time' value='UPDATED_AT'>{i18n.__('Updated')}</option>
|
||||
<option title='Sort by create time' value='CREATED_AT'>{i18n.__('Created')}</option>
|
||||
<option title='Sort alphabetically' value='ALPHABETICAL'>{i18n.__('Alphabetically')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div styleName='control-button-area'>
|
||||
<button styleName={config.listStyle === 'DEFAULT'
|
||||
<button title={i18n.__('Default View')} styleName={config.listStyle === 'DEFAULT'
|
||||
? 'control-button--active'
|
||||
: 'control-button'
|
||||
}
|
||||
@@ -748,7 +1034,7 @@ class NoteList extends React.Component {
|
||||
>
|
||||
<img styleName='iconTag' src='../resources/icon/icon-column.svg' />
|
||||
</button>
|
||||
<button styleName={config.listStyle === 'SMALL'
|
||||
<button title={i18n.__('Compressed View')} styleName={config.listStyle === 'SMALL'
|
||||
? 'control-button--active'
|
||||
: 'control-button'
|
||||
}
|
||||
@@ -782,4 +1068,4 @@ NoteList.propTypes = {
|
||||
})
|
||||
}
|
||||
|
||||
export default CSSModules(NoteList, styles)
|
||||
export default debounceRender(CSSModules(NoteList, styles))
|
||||
|
||||
25
browser/main/SideNav/ListButton.js
Normal file
25
browser/main/SideNav/ListButton.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './SwitchButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const ListButton = ({
|
||||
onClick, isTagActive
|
||||
}) => (
|
||||
<button styleName={isTagActive ? 'non-active-button' : 'active-button'} onClick={onClick}>
|
||||
<img src={isTagActive
|
||||
? '../resources/icon/icon-list.svg'
|
||||
: '../resources/icon/icon-list-active.svg'
|
||||
}
|
||||
/>
|
||||
<span styleName='tooltip'>{i18n.__('Notes')}</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
ListButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
isTagActive: PropTypes.bool.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(ListButton, styles)
|
||||
20
browser/main/SideNav/PreferenceButton.js
Normal file
20
browser/main/SideNav/PreferenceButton.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './PreferenceButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const PreferenceButton = ({
|
||||
onClick
|
||||
}) => (
|
||||
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
|
||||
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' />
|
||||
<span styleName='tooltip'>{i18n.__('Preferences')}</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
PreferenceButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(PreferenceButton, styles)
|
||||
52
browser/main/SideNav/PreferenceButton.styl
Normal file
52
browser/main/SideNav/PreferenceButton.styl
Normal file
@@ -0,0 +1,52 @@
|
||||
.top-menu-preference
|
||||
navButtonColor()
|
||||
position absolute
|
||||
top 22px
|
||||
right 10px
|
||||
width 2em
|
||||
background-color transparent
|
||||
&:hover
|
||||
color $ui-button-default--active-backgroundColor
|
||||
background-color transparent
|
||||
.tooltip
|
||||
opacity 1
|
||||
&:active, &:active:hover
|
||||
color $ui-button-default--active-backgroundColor
|
||||
|
||||
body[data-theme="white"]
|
||||
.top-menu-preference
|
||||
navWhiteButtonColor()
|
||||
background-color transparent
|
||||
&:hover
|
||||
color #0B99F1
|
||||
background-color transparent
|
||||
&:active, &:active:hover
|
||||
color #0B99F1
|
||||
background-color transparent
|
||||
|
||||
body[data-theme="dark"]
|
||||
.top-menu-preference
|
||||
navDarkButtonColor()
|
||||
background-color transparent
|
||||
&:active
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
background-color transparent
|
||||
&:hover
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
background-color transparent
|
||||
|
||||
|
||||
|
||||
.tooltip
|
||||
tooltip()
|
||||
position absolute
|
||||
pointer-events none
|
||||
top 26px
|
||||
left -20px
|
||||
z-index 200
|
||||
padding 5px
|
||||
line-height normal
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
white-space nowrap
|
||||
@@ -11,19 +11,6 @@
|
||||
.top
|
||||
padding-bottom 15px
|
||||
|
||||
.top-menu-preference
|
||||
navButtonColor()
|
||||
position absolute
|
||||
top 22px
|
||||
right 10px
|
||||
width 2em
|
||||
background-color transparent
|
||||
&:hover
|
||||
color $ui-button-default--active-backgroundColor
|
||||
background-color transparent
|
||||
&:active, &:active:hover
|
||||
color $ui-button-default--active-backgroundColor
|
||||
|
||||
.switch-buttons
|
||||
background-color transparent
|
||||
border 0
|
||||
@@ -31,21 +18,7 @@
|
||||
display flex
|
||||
text-align center
|
||||
|
||||
.non-active-button
|
||||
color $ui-inactive-text-color
|
||||
font-size 16px
|
||||
border 0
|
||||
background-color transparent
|
||||
transition 0.2s
|
||||
display flex
|
||||
text-align center
|
||||
margin-right 4px;
|
||||
&:hover
|
||||
color alpha(#239F86, 60%)
|
||||
|
||||
.active-button
|
||||
@extend .non-active-button
|
||||
color $ui-button-default--active-backgroundColor
|
||||
|
||||
.top-menu-label
|
||||
margin-left 5px
|
||||
@@ -57,11 +30,33 @@
|
||||
display flex
|
||||
flex-direction column
|
||||
|
||||
.tag-title
|
||||
padding-left 15px
|
||||
padding-bottom 13px
|
||||
p
|
||||
color $ui-button-default-color
|
||||
.tag-control
|
||||
display flex
|
||||
height 30px
|
||||
line-height 25px
|
||||
overflow hidden
|
||||
.tag-control-title
|
||||
padding-left 15px
|
||||
padding-bottom 13px
|
||||
flex 1
|
||||
p
|
||||
color $ui-button-default-color
|
||||
.tag-control-sortTagsBy
|
||||
user-select none
|
||||
font-size 12px
|
||||
color $ui-inactive-text-color
|
||||
margin-left 12px
|
||||
margin-right 12px
|
||||
.tag-control-sortTagsBy-select
|
||||
appearance: none;
|
||||
margin-left 5px
|
||||
color $ui-inactive-text-color
|
||||
padding 0
|
||||
border none
|
||||
background-color transparent
|
||||
outline none
|
||||
cursor pointer
|
||||
font-size 12px
|
||||
|
||||
.tagList
|
||||
overflow-y auto
|
||||
@@ -109,57 +104,21 @@ body[data-theme="white"]
|
||||
background-color #f9f9f9
|
||||
color $ui-text-color
|
||||
|
||||
.top-menu-preference
|
||||
navWhiteButtonColor()
|
||||
background-color transparent
|
||||
&:hover
|
||||
color #0B99F1
|
||||
background-color transparent
|
||||
&:active, &:active:hover
|
||||
color #0B99F1
|
||||
background-color transparent
|
||||
|
||||
.non-active-button
|
||||
color $ui-inactive-text-color
|
||||
&:hover
|
||||
color alpha(#0B99F1, 60%)
|
||||
|
||||
.tag-title
|
||||
p
|
||||
color $ui-text-color
|
||||
|
||||
.non-active-button
|
||||
&:hover
|
||||
color alpha(#0B99F1, 60%)
|
||||
|
||||
.active-button
|
||||
@extend .non-active-button
|
||||
color #0B99F1
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root, .root--folded
|
||||
border-color $ui-dark-borderColor
|
||||
border-right 1px solid $ui-dark-borderColor
|
||||
background-color $ui-dark-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
.top
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.top-menu-preference
|
||||
navDarkButtonColor()
|
||||
background-color transparent
|
||||
&:active
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
background-color transparent
|
||||
&:hover
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
background-color transparent
|
||||
body[data-theme="solarized-dark"]
|
||||
.root, .root--folded
|
||||
background-color $ui-solarized-dark-backgroundColor
|
||||
border-right 1px solid $ui-solarized-dark-borderColor
|
||||
|
||||
.non-active-button
|
||||
color alpha($ui-dark-text-color, 60%)
|
||||
&:hover
|
||||
color alpha(#0B99F1, 60%)
|
||||
|
||||
.tag-title
|
||||
p
|
||||
color alpha($ui-dark-text-color, 60%)
|
||||
body[data-theme="monokai"]
|
||||
.root, .root--folded
|
||||
background-color $ui-monokai-backgroundColor
|
||||
border-right 1px solid $ui-monokai-borderColor
|
||||
|
||||
@@ -8,11 +8,14 @@ import CreateFolderModal from 'browser/main/modals/CreateFolderModal'
|
||||
import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import StorageItemChild from 'browser/components/StorageItem'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import _ from 'lodash'
|
||||
import { SortableElement } from 'react-sortable-hoc'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { Menu, MenuItem, dialog } = remote
|
||||
const { Menu, dialog } = remote
|
||||
const escapeStringRegexp = require('escape-string-regexp')
|
||||
const path = require('path')
|
||||
|
||||
class StorageItem extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -24,27 +27,29 @@ class StorageItem extends React.Component {
|
||||
}
|
||||
|
||||
handleHeaderContextMenu (e) {
|
||||
const menu = new Menu()
|
||||
menu.append(new MenuItem({
|
||||
label: 'Add Folder',
|
||||
click: (e) => this.handleAddFolderButtonClick(e)
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
type: 'separator'
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
label: 'Unlink Storage',
|
||||
click: (e) => this.handleUnlinkStorageClick(e)
|
||||
}))
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: i18n.__('Add Folder'),
|
||||
click: (e) => this.handleAddFolderButtonClick(e)
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: i18n.__('Unlink Storage'),
|
||||
click: (e) => this.handleUnlinkStorageClick(e)
|
||||
}
|
||||
])
|
||||
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
handleUnlinkStorageClick (e) {
|
||||
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Unlink Storage',
|
||||
detail: 'This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
message: i18n.__('Unlink Storage'),
|
||||
detail: i18n.__('This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)'),
|
||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||
})
|
||||
|
||||
if (index === 0) {
|
||||
@@ -89,18 +94,36 @@ class StorageItem extends React.Component {
|
||||
}
|
||||
|
||||
handleFolderButtonContextMenu (e, folder) {
|
||||
const menu = new Menu()
|
||||
menu.append(new MenuItem({
|
||||
label: 'Rename Folder',
|
||||
click: (e) => this.handleRenameFolderClick(e, folder)
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
type: 'separator'
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
label: 'Delete Folder',
|
||||
click: (e) => this.handleFolderDeleteClick(e, folder)
|
||||
}))
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: i18n.__('Rename Folder'),
|
||||
click: (e) => this.handleRenameFolderClick(e, folder)
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export Folder'),
|
||||
submenu: [
|
||||
{
|
||||
label: i18n.__('Export as txt'),
|
||||
click: (e) => this.handleExportFolderClick(e, folder, 'txt')
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export as md'),
|
||||
click: (e) => this.handleExportFolderClick(e, folder, 'md')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: i18n.__('Delete Folder'),
|
||||
click: (e) => this.handleFolderDeleteClick(e, folder)
|
||||
}
|
||||
])
|
||||
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
@@ -112,12 +135,37 @@ class StorageItem extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleExportFolderClick (e, folder, fileType) {
|
||||
const options = {
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
buttonLabel: i18n.__('Select directory'),
|
||||
title: i18n.__('Select a folder to export the files to'),
|
||||
multiSelections: false
|
||||
}
|
||||
dialog.showOpenDialog(remote.getCurrentWindow(), options,
|
||||
(paths) => {
|
||||
if (paths && paths.length === 1) {
|
||||
const { storage, dispatch } = this.props
|
||||
dataApi
|
||||
.exportFolder(storage.key, folder.key, fileType, paths[0])
|
||||
.then((data) => {
|
||||
dispatch({
|
||||
type: 'EXPORT_FOLDER',
|
||||
storage: data.storage,
|
||||
folderKey: data.folderKey,
|
||||
fileType: data.fileType
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleFolderDeleteClick (e, folder) {
|
||||
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Delete Folder',
|
||||
detail: 'This will delete all notes in the folder and can not be undone.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
message: i18n.__('Delete Folder'),
|
||||
detail: i18n.__('This will delete all notes in the folder and can not be undone.'),
|
||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||
})
|
||||
|
||||
if (index === 0) {
|
||||
@@ -147,33 +195,16 @@ class StorageItem extends React.Component {
|
||||
dropNote (storage, folder, dispatch, location, noteData) {
|
||||
noteData = noteData.filter((note) => folder.key !== note.folder)
|
||||
if (noteData.length === 0) return
|
||||
const newNoteData = noteData.map((note) => Object.assign({}, note, {storage: storage, folder: folder.key}))
|
||||
|
||||
Promise.all(
|
||||
newNoteData.map((note) => dataApi.createNote(storage.key, note))
|
||||
noteData.map((note) => dataApi.moveNote(note.storage, note.key, storage.key, folder.key))
|
||||
)
|
||||
.then((createdNoteData) => {
|
||||
createdNoteData.forEach((note) => {
|
||||
createdNoteData.forEach((newNote) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`error on create notes: ${err}`)
|
||||
})
|
||||
.then(() => {
|
||||
return Promise.all(
|
||||
noteData.map((note) => dataApi.deleteNote(note.storage, note.key))
|
||||
)
|
||||
})
|
||||
.then((deletedNoteData) => {
|
||||
deletedNoteData.forEach((note) => {
|
||||
dispatch({
|
||||
type: 'DELETE_NOTE',
|
||||
storageKey: note.storageKey,
|
||||
noteKey: note.noteKey
|
||||
type: 'MOVE_NOTE',
|
||||
originNote: noteData.find((note) => note.content === newNote.oldContent),
|
||||
note: newNote
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -192,8 +223,10 @@ class StorageItem extends React.Component {
|
||||
render () {
|
||||
const { storage, location, isFolded, data, dispatch } = this.props
|
||||
const { folderNoteMap, trashedSet } = data
|
||||
const folderList = storage.folders.map((folder) => {
|
||||
const isActive = !!(location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key)))
|
||||
const SortableStorageItemChild = SortableElement(StorageItemChild)
|
||||
const folderList = storage.folders.map((folder, index) => {
|
||||
let folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
|
||||
const isActive = !!(location.pathname.match(folderRegex))
|
||||
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
||||
|
||||
let noteCount = 0
|
||||
@@ -206,8 +239,9 @@ class StorageItem extends React.Component {
|
||||
noteCount = noteSet.size - trashedNoteCount
|
||||
}
|
||||
return (
|
||||
<StorageItemChild
|
||||
<SortableStorageItemChild
|
||||
key={folder.key}
|
||||
index={index}
|
||||
isActive={isActive}
|
||||
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
|
||||
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
|
||||
@@ -222,16 +256,16 @@ class StorageItem extends React.Component {
|
||||
)
|
||||
})
|
||||
|
||||
const isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '$'))
|
||||
const isActive = location.pathname.match(new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + '$'))
|
||||
|
||||
return (
|
||||
<div styleName={isFolded ? 'root--folded' : 'root'}
|
||||
key={storage.key}
|
||||
>
|
||||
<div styleName={isActive
|
||||
? 'header--active'
|
||||
: 'header'
|
||||
}
|
||||
? 'header--active'
|
||||
: 'header'
|
||||
}
|
||||
onContextMenu={(e) => this.handleHeaderContextMenu(e)}
|
||||
>
|
||||
<button styleName='header-toggleButton'
|
||||
@@ -240,7 +274,7 @@ class StorageItem extends React.Component {
|
||||
<img src={this.state.isOpen
|
||||
? '../resources/icon/icon-down.svg'
|
||||
: '../resources/icon/icon-right.svg'
|
||||
}
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
height 36px
|
||||
padding-left 25px
|
||||
padding-right 15px
|
||||
line-height 22px
|
||||
line-height 36px
|
||||
cursor pointer
|
||||
font-size 14px
|
||||
border none
|
||||
@@ -147,7 +147,7 @@ body[data-theme="dark"]
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
&:active
|
||||
color $ui-dark-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.header--active
|
||||
.header-addFolderButton
|
||||
@@ -179,4 +179,8 @@ body[data-theme="dark"]
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||
&:active, &:active:hover
|
||||
color $ui-dark-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
60
browser/main/SideNav/SwitchButton.styl
Normal file
60
browser/main/SideNav/SwitchButton.styl
Normal file
@@ -0,0 +1,60 @@
|
||||
.non-active-button
|
||||
color $ui-inactive-text-color
|
||||
font-size 16px
|
||||
border 0
|
||||
background-color transparent
|
||||
transition 0.2s
|
||||
display flex
|
||||
text-align center
|
||||
margin-right 4px
|
||||
position relative
|
||||
&:hover
|
||||
color alpha(#239F86, 60%)
|
||||
.tooltip
|
||||
opacity 1
|
||||
|
||||
.active-button
|
||||
@extend .non-active-button
|
||||
color $ui-button-default--active-backgroundColor
|
||||
|
||||
.tooltip
|
||||
tooltip()
|
||||
position absolute
|
||||
pointer-events none
|
||||
top 22px
|
||||
left -2px
|
||||
z-index 200
|
||||
padding 5px
|
||||
line-height normal
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
white-space nowrap
|
||||
|
||||
body[data-theme="white"]
|
||||
.non-active-button
|
||||
color $ui-inactive-text-color
|
||||
&:hover
|
||||
color alpha(#0B99F1, 60%)
|
||||
|
||||
.tag-title
|
||||
p
|
||||
color $ui-text-color
|
||||
|
||||
.non-active-button
|
||||
&:hover
|
||||
color alpha(#0B99F1, 60%)
|
||||
|
||||
.active-button
|
||||
@extend .non-active-button
|
||||
color #0B99F1
|
||||
|
||||
body[data-theme="dark"]
|
||||
.non-active-button
|
||||
color alpha($ui-dark-text-color, 60%)
|
||||
&:hover
|
||||
color alpha(#0B99F1, 60%)
|
||||
|
||||
.tag-title
|
||||
p
|
||||
color alpha($ui-dark-text-color, 60%)
|
||||
25
browser/main/SideNav/TagButton.js
Normal file
25
browser/main/SideNav/TagButton.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './SwitchButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const TagButton = ({
|
||||
onClick, isTagActive
|
||||
}) => (
|
||||
<button styleName={isTagActive ? 'active-button' : 'non-active-button'} onClick={onClick}>
|
||||
<img src={isTagActive
|
||||
? '../resources/icon/icon-tag-active.svg'
|
||||
: '../resources/icon/icon-tag.svg'
|
||||
}
|
||||
/>
|
||||
<span styleName='tooltip'>{i18n.__('Tags')}</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
TagButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
isTagActive: PropTypes.bool.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(TagButton, styles)
|
||||
@@ -1,6 +1,9 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
const { remote } = require('electron')
|
||||
const { Menu } = remote
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import styles from './SideNav.styl'
|
||||
import { openModal } from 'browser/main/lib/modal'
|
||||
import PreferencesModal from '../modals/PreferencesModal'
|
||||
@@ -11,6 +14,11 @@ import SideNavFilter from 'browser/components/SideNavFilter'
|
||||
import StorageList from 'browser/components/StorageList'
|
||||
import NavToggleButton from 'browser/components/NavToggleButton'
|
||||
import EventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import PreferenceButton from './PreferenceButton'
|
||||
import ListButton from './ListButton'
|
||||
import TagButton from './TagButton'
|
||||
import {SortableContainer} from 'react-sortable-hoc'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
class SideNav extends React.Component {
|
||||
// TODO: should not use electron stuff v0.7
|
||||
@@ -62,8 +70,19 @@ class SideNav extends React.Component {
|
||||
router.push('/alltags')
|
||||
}
|
||||
|
||||
onSortEnd (storage) {
|
||||
return ({oldIndex, newIndex}) => {
|
||||
const { dispatch } = this.props
|
||||
dataApi
|
||||
.reorderFolder(storage.key, oldIndex, newIndex)
|
||||
.then((data) => {
|
||||
dispatch({ type: 'REORDER_FOLDER', storage: data.storage })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
SideNavComponent (isFolded, storageList) {
|
||||
const { location, data } = this.props
|
||||
const { location, data, config } = this.props
|
||||
|
||||
const isHomeActive = !!location.pathname.match(/^\/home$/)
|
||||
const isStarredActive = !!location.pathname.match(/^\/starred$/)
|
||||
@@ -83,20 +102,36 @@ class SideNav extends React.Component {
|
||||
isTrashedActive={isTrashedActive}
|
||||
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
|
||||
handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)}
|
||||
counterTotalNote={data.noteMap._map.size}
|
||||
counterTotalNote={data.noteMap._map.size - data.trashedSet._set.size}
|
||||
counterStarredNote={data.starredSet._set.size}
|
||||
counterDelNote={data.trashedSet._set.size}
|
||||
handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(this)}
|
||||
/>
|
||||
|
||||
<StorageList storageList={storageList} />
|
||||
<StorageList storageList={storageList} isFolded={isFolded} />
|
||||
<NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} />
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
component = (
|
||||
<div styleName='tabBody'>
|
||||
<div styleName='tag-title'>
|
||||
<p>Tags</p>
|
||||
<div styleName='tag-control'>
|
||||
<div styleName='tag-control-title'>
|
||||
<p>{i18n.__('Tags')}</p>
|
||||
</div>
|
||||
<div styleName='tag-control-sortTagsBy'>
|
||||
<i className='fa fa-angle-down' />
|
||||
<select styleName='tag-control-sortTagsBy-select'
|
||||
title={i18n.__('Select filter mode')}
|
||||
value={config.sortTagsBy}
|
||||
onChange={(e) => this.handleSortTagsByChange(e)}
|
||||
>
|
||||
<option title='Sort alphabetically'
|
||||
value='ALPHABETICAL'>{i18n.__('Alphabetically')}</option>
|
||||
<option title='Sort by update time'
|
||||
value='COUNTER'>{i18n.__('Counter')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='tagList'>
|
||||
{this.tagListComponent(data)}
|
||||
@@ -109,26 +144,62 @@ class SideNav extends React.Component {
|
||||
}
|
||||
|
||||
tagListComponent () {
|
||||
const { data, location } = this.props
|
||||
const tagList = data.tagNoteMap.map((tag, key) => {
|
||||
return key
|
||||
})
|
||||
const { data, location, config } = this.props
|
||||
const relatedTags = this.getRelatedTags(this.getActiveTags(location.pathname), data.noteMap)
|
||||
let tagList = _.sortBy(data.tagNoteMap.map(
|
||||
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
|
||||
), ['name']).filter(
|
||||
tag => tag.size > 0
|
||||
)
|
||||
if (config.sortTagsBy === 'COUNTER') {
|
||||
tagList = _.sortBy(tagList, item => (0 - item.size))
|
||||
}
|
||||
if (config.ui.showOnlyRelatedTags && (relatedTags.size > 0)) {
|
||||
tagList = tagList.filter(
|
||||
tag => tag.related
|
||||
)
|
||||
}
|
||||
return (
|
||||
tagList.map(tag => (
|
||||
<TagListItem
|
||||
name={tag}
|
||||
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
||||
isActive={this.getTagActive(location.pathname, tag)}
|
||||
key={tag}
|
||||
/>
|
||||
))
|
||||
tagList.map(tag => {
|
||||
return (
|
||||
<TagListItem
|
||||
name={tag.name}
|
||||
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
||||
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
|
||||
isActive={this.getTagActive(location.pathname, tag.name)}
|
||||
isRelated={tag.related}
|
||||
key={tag.name}
|
||||
count={tag.size}
|
||||
/>
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
getRelatedTags (activeTags, noteMap) {
|
||||
if (activeTags.length === 0) {
|
||||
return new Set()
|
||||
}
|
||||
const relatedNotes = noteMap.map(
|
||||
note => ({key: note.key, tags: note.tags})
|
||||
).filter(
|
||||
note => activeTags.every(tag => note.tags.includes(tag))
|
||||
)
|
||||
const relatedTags = new Set()
|
||||
relatedNotes.forEach(note => note.tags.map(tag => relatedTags.add(tag)))
|
||||
return relatedTags
|
||||
}
|
||||
|
||||
getTagActive (path, tag) {
|
||||
return this.getActiveTags(path).includes(tag)
|
||||
}
|
||||
|
||||
getActiveTags (path) {
|
||||
const pathSegments = path.split('/')
|
||||
const pathTag = pathSegments[pathSegments.length - 1]
|
||||
return pathTag === tag
|
||||
const tags = pathSegments[pathSegments.length - 1]
|
||||
return (tags === 'alltags')
|
||||
? []
|
||||
: tags.split(' ')
|
||||
}
|
||||
|
||||
handleClickTagListItem (name) {
|
||||
@@ -136,19 +207,75 @@ class SideNav extends React.Component {
|
||||
router.push(`/tags/${name}`)
|
||||
}
|
||||
|
||||
handleSortTagsByChange (e) {
|
||||
const { dispatch } = this.props
|
||||
|
||||
const config = {
|
||||
sortTagsBy: e.target.value
|
||||
}
|
||||
|
||||
ConfigManager.set(config)
|
||||
dispatch({
|
||||
type: 'SET_CONFIG',
|
||||
config
|
||||
})
|
||||
}
|
||||
|
||||
handleClickNarrowToTag (tag) {
|
||||
const { router } = this.context
|
||||
const { location } = this.props
|
||||
const listOfTags = this.getActiveTags(location.pathname)
|
||||
const indexOfTag = listOfTags.indexOf(tag)
|
||||
if (indexOfTag > -1) {
|
||||
listOfTags.splice(indexOfTag, 1)
|
||||
} else {
|
||||
listOfTags.push(tag)
|
||||
}
|
||||
router.push(`/tags/${listOfTags.join(' ')}`)
|
||||
}
|
||||
|
||||
emptyTrash (entries) {
|
||||
const { dispatch } = this.props
|
||||
const deletionPromises = entries.map((note) => {
|
||||
return dataApi.deleteNote(note.storage, note.key)
|
||||
})
|
||||
Promise.all(deletionPromises)
|
||||
.then((arrayOfStorageAndNoteKeys) => {
|
||||
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
|
||||
dispatch({ type: 'DELETE_NOTE', storageKey, noteKey })
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Cannot Delete note: ' + err)
|
||||
})
|
||||
console.log('Trash emptied')
|
||||
}
|
||||
|
||||
handleFilterButtonContextMenu (event) {
|
||||
const { data } = this.props
|
||||
const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{ label: i18n.__('Empty Trash'), click: () => this.emptyTrash(trashedNotes) }
|
||||
])
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { data, location, config, dispatch } = this.props
|
||||
|
||||
const isFolded = config.isSideNavFolded
|
||||
|
||||
const storageList = data.storageMap.map((storage, key) => {
|
||||
return <StorageItem
|
||||
const SortableStorageItem = SortableContainer(StorageItem)
|
||||
return <SortableStorageItem
|
||||
key={storage.key}
|
||||
storage={storage}
|
||||
data={data}
|
||||
location={location}
|
||||
isFolded={isFolded}
|
||||
dispatch={dispatch}
|
||||
onSortEnd={this.onSortEnd.bind(this)(storage)}
|
||||
useDragHandle
|
||||
/>
|
||||
})
|
||||
const style = {}
|
||||
@@ -162,27 +289,11 @@ class SideNav extends React.Component {
|
||||
>
|
||||
<div styleName='top'>
|
||||
<div styleName='switch-buttons'>
|
||||
<button styleName={isTagActive ? 'non-active-button' : 'active-button'} onClick={this.handleSwitchFoldersButtonClick.bind(this)}>
|
||||
<img src={isTagActive
|
||||
? '../resources/icon/icon-list.svg'
|
||||
: '../resources/icon/icon-list-active.svg'
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
<button styleName={isTagActive ? 'active-button' : 'non-active-button'} onClick={this.handleSwitchTagsButtonClick.bind(this)}>
|
||||
<img src={isTagActive
|
||||
? '../resources/icon/icon-tag-active.svg'
|
||||
: '../resources/icon/icon-tag.svg'
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
<ListButton onClick={this.handleSwitchFoldersButtonClick.bind(this)} isTagActive={isTagActive} />
|
||||
<TagButton onClick={this.handleSwitchTagsButtonClick.bind(this)} isTagActive={isTagActive} />
|
||||
</div>
|
||||
<div>
|
||||
<button styleName='top-menu-preference'
|
||||
onClick={(e) => this.handleMenuButtonClick(e)}
|
||||
>
|
||||
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' />
|
||||
</button>
|
||||
<PreferenceButton onClick={this.handleMenuButtonClick} />
|
||||
</div>
|
||||
</div>
|
||||
{this.SideNavComponent(isFolded, storageList)}
|
||||
|
||||
@@ -69,3 +69,14 @@ body[data-theme="dark"]
|
||||
navDarkButtonColor()
|
||||
border-color $ui-dark-borderColor
|
||||
border-left 1px solid $ui-dark-borderColor
|
||||
|
||||
body[data-theme="monokai"]
|
||||
navButtonColor()
|
||||
.zoom
|
||||
border-color $ui-dark-borderColor
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
transition 0.15s
|
||||
color $ui-monokai-active-color
|
||||
&:active
|
||||
color $ui-monokai-active-color
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './StatusBar.styl'
|
||||
import ZoomManager from 'browser/main/lib/ZoomManager'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote, ipcRenderer } = electron
|
||||
@@ -14,9 +15,9 @@ class StatusBar extends React.Component {
|
||||
updateApp () {
|
||||
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Update Boostnote',
|
||||
detail: 'New Boostnote is ready to be installed.',
|
||||
buttons: ['Restart & Install', 'Not Now']
|
||||
message: i18n.__('Update Boostnote'),
|
||||
detail: i18n.__('New Boostnote is ready to be installed.'),
|
||||
buttons: [i18n.__('Restart & Install'), i18n.__('Not Now')]
|
||||
})
|
||||
|
||||
if (index === 0) {
|
||||
@@ -62,8 +63,8 @@ class StatusBar extends React.Component {
|
||||
|
||||
{status.updateReady
|
||||
? <button onClick={this.updateApp} styleName='update'>
|
||||
<i styleName='update-icon' className='fa fa-cloud-download' /> Ready to Update!
|
||||
</button>
|
||||
<i styleName='update-icon' className='fa fa-cloud-download' /> {i18n.__('Ready to Update!')}
|
||||
</button>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,32 @@ $control-height = 34px
|
||||
padding-bottom 2px
|
||||
background-color $ui-noteList-backgroundColor
|
||||
|
||||
.control-search-input-clear
|
||||
height 16px
|
||||
width 16px
|
||||
position absolute
|
||||
right 40px
|
||||
top 10px
|
||||
z-index 300
|
||||
border none
|
||||
background-color transparent
|
||||
color #999
|
||||
&:hover .control-search-input-clear-tooltip
|
||||
opacity 1
|
||||
|
||||
.control-search-input-clear-tooltip
|
||||
tooltip()
|
||||
position fixed
|
||||
pointer-events none
|
||||
top 50px
|
||||
left 433px
|
||||
z-index 200
|
||||
padding 5px
|
||||
line-height normal
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
.control-search-optionList
|
||||
position fixed
|
||||
z-index 200
|
||||
@@ -185,3 +211,48 @@ body[data-theme="dark"]
|
||||
|
||||
.control-newPostButton-tooltip
|
||||
darkTooltip()
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
|
||||
.control
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
.control-search
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
|
||||
.control-search-icon
|
||||
absolute top bottom left
|
||||
line-height 32px
|
||||
width 35px
|
||||
color $ui-solarized-dark-inactive-text-color
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
|
||||
.control-search-input
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
input
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.control
|
||||
border-color $ui-monokai-borderColor
|
||||
.control-search
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.control-search-icon
|
||||
absolute top bottom left
|
||||
line-height 32px
|
||||
width 35px
|
||||
color $ui-monokai-inactive-text-color
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.control-search-input
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
input
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user