mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 18:26:26 +00:00
Compare commits
1135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95d74c6f5b | ||
|
|
f04b7db9fc | ||
|
|
900fa023fb | ||
|
|
ad9da44afb | ||
|
|
c827717202 | ||
|
|
7d3caa3c2e | ||
|
|
fde7fbccac | ||
|
|
56f06fa7d5 | ||
|
|
c0fba82e73 | ||
|
|
5438cd14a0 | ||
|
|
0d642b308d | ||
|
|
0b96472f72 | ||
|
|
675d0ed08c | ||
|
|
9c0f5c31c2 | ||
|
|
09ce59fd04 | ||
|
|
98cd83c4e0 | ||
|
|
1aec386656 | ||
|
|
b03cd9cd99 | ||
|
|
27265e210f | ||
|
|
c392c5d178 | ||
|
|
11fe420fac | ||
|
|
9b17a8fb5b | ||
|
|
27f3fd0032 | ||
|
|
1672d9fa5f | ||
|
|
59c9e11879 | ||
|
|
4523743150 | ||
|
|
2fdbe9de96 | ||
|
|
a617976c78 | ||
|
|
7201a98d78 | ||
|
|
472560e2bf | ||
|
|
96753fe0a0 | ||
|
|
83c2fdd161 | ||
|
|
911ce7572f | ||
|
|
0a24d7d4a7 | ||
|
|
c542062d4d | ||
|
|
eb7a195cce | ||
|
|
23a356164e | ||
|
|
de19c51061 | ||
|
|
221b6a2938 | ||
|
|
cda9d53c8e | ||
|
|
f043b0ffb3 | ||
|
|
28e0590327 | ||
|
|
ec6de1b91b | ||
|
|
2b0bdbf1c8 | ||
|
|
f48864a2e7 | ||
|
|
94c6578675 | ||
|
|
2af2399971 | ||
|
|
ebea01cecf | ||
|
|
5d1db1de31 | ||
|
|
6c528625d8 | ||
|
|
7b326b99af | ||
|
|
2a60ba95e0 | ||
|
|
6b98afaa02 | ||
|
|
cdb079dc81 | ||
|
|
2ac0d93caf | ||
|
|
41977e8726 | ||
|
|
b9e6a56a83 | ||
|
|
2468c8311f | ||
|
|
e8e05b20cd | ||
|
|
5bd0a446f1 | ||
|
|
ad4e50d542 | ||
|
|
13131a0d5c | ||
|
|
f007664745 | ||
|
|
87f9589be3 | ||
|
|
e059106a93 | ||
|
|
ada1b4de6b | ||
|
|
96413b9851 | ||
|
|
9699ef6319 | ||
|
|
dd8f4d60f0 | ||
|
|
372f2e7319 | ||
|
|
1957d87dd7 | ||
|
|
a148d17ba1 | ||
|
|
297553c240 | ||
|
|
f0fcaa6be7 | ||
|
|
cfa40f3ec1 | ||
|
|
832c43de88 | ||
|
|
1665e18edb | ||
|
|
fd1717046b | ||
|
|
7fe7c555bc | ||
|
|
411a7a8e80 | ||
|
|
196f5a7bf7 | ||
|
|
3fa326121a | ||
|
|
cce69bea3a | ||
|
|
f70cf7845d | ||
|
|
bd0a326128 | ||
|
|
897d99e043 | ||
|
|
b0f288e103 | ||
|
|
7d26d46c7b | ||
|
|
5c7804fc40 | ||
|
|
836f3af1ab | ||
|
|
67b89d4fe7 | ||
|
|
bc2d9d0fe2 | ||
|
|
79f33b9405 | ||
|
|
ed9ddee5f1 | ||
|
|
0d004b2f0a | ||
|
|
f41ff77d76 | ||
|
|
ae97a76d2e | ||
|
|
3ca18c04c6 | ||
|
|
2b03e6e956 | ||
|
|
010793a478 | ||
|
|
b136512ece | ||
|
|
9179c199fe | ||
|
|
cfa4dfa817 | ||
|
|
6a73a3af97 | ||
|
|
923c24fa6c | ||
|
|
4b1c8a3238 | ||
|
|
76508fbc3b | ||
|
|
2bfda95ed8 | ||
|
|
6e5082a470 | ||
|
|
a6cec44fc4 | ||
|
|
600fab4f23 | ||
|
|
12377b8caf | ||
|
|
250c6e488d | ||
|
|
3a9b57adae | ||
|
|
74415956ac | ||
|
|
33d7ed25a5 | ||
|
|
df2de5c081 | ||
|
|
7f4c58a84a | ||
|
|
7437b26e3c | ||
|
|
ee6d41859f | ||
|
|
b368c3b5d8 | ||
|
|
b1ae2b0b6f | ||
|
|
27a6d53c7f | ||
|
|
4caf3a81be | ||
|
|
ab578f768f | ||
|
|
e4212e796a | ||
|
|
95b1ff9b41 | ||
|
|
684d2c411e | ||
|
|
42710cfee5 | ||
|
|
277004fd9b | ||
|
|
14f79c4c21 | ||
|
|
f88cd80dca | ||
|
|
19ada1dbf6 | ||
|
|
7754ab1a2e | ||
|
|
de0a8837eb | ||
|
|
042d059d53 | ||
|
|
cba743c895 | ||
|
|
24ee71ac06 | ||
|
|
0ec4ef3363 | ||
|
|
98120a5e40 | ||
|
|
3cb2a6bf92 | ||
|
|
4b7262cb72 | ||
|
|
eac8b13d7b | ||
|
|
4458c58066 | ||
|
|
245d603ae8 | ||
|
|
e3d959522b | ||
|
|
124544452b | ||
|
|
4534625084 | ||
|
|
3e4342eec4 | ||
|
|
0ed2d26129 | ||
|
|
1b538993db | ||
|
|
d67e4009e7 | ||
|
|
7f066c4443 | ||
|
|
2594ca984a | ||
|
|
0e089fadfb | ||
|
|
e56518e13d | ||
|
|
c9bc0c89ff | ||
|
|
b18b1171e7 | ||
|
|
d65464401c | ||
|
|
e9a9e10c81 | ||
|
|
825cd6a93b | ||
|
|
276471979a | ||
|
|
d6903edac7 | ||
|
|
4e1b4bdd6a | ||
|
|
52f0a5639d | ||
|
|
58181d02b2 | ||
|
|
1fea39f1b7 | ||
|
|
ae97ff0f98 | ||
|
|
b2d7fa9e97 | ||
|
|
93e9235bb2 | ||
|
|
094bce20e2 | ||
|
|
2f9d4c447a | ||
|
|
84eb790d93 | ||
|
|
ebd07694db | ||
|
|
2bbac8a6f4 | ||
|
|
e74f5e835f | ||
|
|
69e753cc71 | ||
|
|
b0978c772e | ||
|
|
fb4dfbadf3 | ||
|
|
d19ff3ff17 | ||
|
|
d0990be856 | ||
|
|
268d66b51d | ||
|
|
c1df311e86 | ||
|
|
cc3bd41df2 | ||
|
|
856979b455 | ||
|
|
47925489fd | ||
|
|
8aefb21123 | ||
|
|
e27751c18c | ||
|
|
a3f3fdcc71 | ||
|
|
a757576920 | ||
|
|
c4eb28d241 | ||
|
|
aba6c2eb4f | ||
|
|
ecb91b3155 | ||
|
|
7374b1cc70 | ||
|
|
e8f2972659 | ||
|
|
5bef19a306 | ||
|
|
54d9e02a42 | ||
|
|
4f479a8baf | ||
|
|
e7e6194cac | ||
|
|
113abbb94d | ||
|
|
d9d0651352 | ||
|
|
e27af9f6c1 | ||
|
|
11d820356d | ||
|
|
0945aab232 | ||
|
|
e4744221ee | ||
|
|
5f71b24f8d | ||
|
|
095a29972a | ||
|
|
76bdb708fa | ||
|
|
7c3c08fd96 | ||
|
|
a4a2e09429 | ||
|
|
4ed0ae5e2d | ||
|
|
4cb5e43357 | ||
|
|
b0fecc6b51 | ||
|
|
dd4236ca89 | ||
|
|
5da908c759 | ||
|
|
fcce1d406d | ||
|
|
5a01f39dc7 | ||
|
|
ea1d76f853 | ||
|
|
2356d8a64f | ||
|
|
969f82b903 | ||
|
|
fb90907abf | ||
|
|
cbd4cd940c | ||
|
|
6b18c6182e | ||
|
|
09164aa0c9 | ||
|
|
b83dadddb7 | ||
|
|
b3263b41ff | ||
|
|
1118149b9e | ||
|
|
b6fc24c6e7 | ||
|
|
441edf4667 | ||
|
|
74068eaa3d | ||
|
|
c11bd9e7eb | ||
|
|
b21a82ea6b | ||
|
|
cf455a13d5 | ||
|
|
c492f3529e | ||
|
|
40d7ba4bcc | ||
|
|
686bc49230 | ||
|
|
6550af698a | ||
|
|
aac5cbf53e | ||
|
|
00636db87c | ||
|
|
6231b8ad57 | ||
|
|
f2a41aa049 | ||
|
|
bb87b80a92 | ||
|
|
ebfbe29217 | ||
|
|
fad837e148 | ||
|
|
7dc84c0d6d | ||
|
|
74807fe251 | ||
|
|
bf9773be20 | ||
|
|
18961ff555 | ||
|
|
1f6e0342d6 | ||
|
|
7d2bc58ba2 | ||
|
|
556b53181f | ||
|
|
ca904d69e5 | ||
|
|
f461d459d2 | ||
|
|
6e535c11fd | ||
|
|
49d3262380 | ||
|
|
1ee3dec0cc | ||
|
|
15637642bb | ||
|
|
3c950c2b9e | ||
|
|
7811039651 | ||
|
|
ec5f7b38d0 | ||
|
|
2bb361dc19 | ||
|
|
3445e484ae | ||
|
|
efd6bf2afe | ||
|
|
cd2e6e1b24 | ||
|
|
99b8d24db3 | ||
|
|
8116b569c1 | ||
|
|
da791c5fed | ||
|
|
fbd9c59bfd | ||
|
|
3a1b3d19c5 | ||
|
|
238076f534 | ||
|
|
214d74ae11 | ||
|
|
30324f6113 | ||
|
|
68f0a25873 | ||
|
|
cd5bc4e930 | ||
|
|
1d8f729c95 | ||
|
|
f0d2fb53d4 | ||
|
|
0445c680ba | ||
|
|
12453942c8 | ||
|
|
b18a5be940 | ||
|
|
c2234747e8 | ||
|
|
d6c9ab43ec | ||
|
|
db16a87f74 | ||
|
|
5b0d2ec97b | ||
|
|
b906db3b24 | ||
|
|
df0af2a11f | ||
|
|
706dd3e616 | ||
|
|
bbec58e049 | ||
|
|
381b7d85f4 | ||
|
|
c17c056af3 | ||
|
|
a26a85cd1f | ||
|
|
9b68b1d327 | ||
|
|
6a6631052e | ||
|
|
d4ad3a953a | ||
|
|
7bb63a78c5 | ||
|
|
26c859f14c | ||
|
|
dd5c9bf3f6 | ||
|
|
3db40fea31 | ||
|
|
8f1c198406 | ||
|
|
ac65a3c86e | ||
|
|
79abcd90f6 | ||
|
|
d614abdec6 | ||
|
|
44d754c59d | ||
|
|
247d1db5d5 | ||
|
|
a52cfb2cd2 | ||
|
|
4e5d923388 | ||
|
|
f9598dd619 | ||
|
|
00f1c62646 | ||
|
|
8997728cbc | ||
|
|
99a0704516 | ||
|
|
434e361d5d | ||
|
|
f112b0bf03 | ||
|
|
0ce67ccd35 | ||
|
|
c947feadeb | ||
|
|
2fa381980d | ||
|
|
4e2c42f65c | ||
|
|
38b7d60af7 | ||
|
|
4714593d52 | ||
|
|
7729ed4f72 | ||
|
|
7107777df3 | ||
|
|
53989b918e | ||
|
|
fdd0c84441 | ||
|
|
8f0789bc6d | ||
|
|
2fdea6cd61 | ||
|
|
ed5f3a6202 | ||
|
|
d98e909256 | ||
|
|
6e0def310f | ||
|
|
823da07a5e | ||
|
|
40d2960562 | ||
|
|
6799f45352 | ||
|
|
1209a044ce | ||
|
|
9a8760c120 | ||
|
|
8d2a7716f3 | ||
|
|
5f240ada59 | ||
|
|
1c86dea4be | ||
|
|
92fd7ac09c | ||
|
|
90b490c28b | ||
|
|
8fd03a09de | ||
|
|
041232fbdd | ||
|
|
f595121ab3 | ||
|
|
bd733312f4 | ||
|
|
70db2f78da | ||
|
|
0298aba86f | ||
|
|
60b344eea8 | ||
|
|
eb63b743b1 | ||
|
|
d09ffc9dd8 | ||
|
|
f1b2643941 | ||
|
|
9e9d3ddc57 | ||
|
|
99a134494c | ||
|
|
d1156f963d | ||
|
|
2a6a2694d4 | ||
|
|
cc24e6b801 | ||
|
|
9c85719270 | ||
|
|
394e86f765 | ||
|
|
6bb9366ec8 | ||
|
|
8a9d4df6c7 | ||
|
|
fdc2e91170 | ||
|
|
40bf026e82 | ||
|
|
975e82710e | ||
|
|
1763dbcff1 | ||
|
|
908281356e | ||
|
|
123d5638cf | ||
|
|
79d0006f3f | ||
|
|
240b5daf3e | ||
|
|
cd0bede2f2 | ||
|
|
c5d984732a | ||
|
|
7358e68394 | ||
|
|
27b09e5b73 | ||
|
|
1705511b10 | ||
|
|
e17fa6d0ed | ||
|
|
9091976337 | ||
|
|
58d098503b | ||
|
|
33fe4f5295 | ||
|
|
0646c4f8bd | ||
|
|
960469f07c | ||
|
|
665f81ac9c | ||
|
|
26f05b343e | ||
|
|
f50968f992 | ||
|
|
4c486c399b | ||
|
|
9f4dd909a8 | ||
|
|
2b85aa1b88 | ||
|
|
8e4c3a3b21 | ||
|
|
a4160d2994 | ||
|
|
27e0252ccd | ||
|
|
0a707b3f02 | ||
|
|
6fc421810f | ||
|
|
22cf7443f4 | ||
|
|
8e7b4d2444 | ||
|
|
54437cec19 | ||
|
|
40fc63ea0c | ||
|
|
6bd81fe12a | ||
|
|
519ea1a33f | ||
|
|
f07f309393 | ||
|
|
7132e9ff24 | ||
|
|
34ae3cd704 | ||
|
|
fba972c98e | ||
|
|
a391ac682d | ||
|
|
4ee49d5991 | ||
|
|
0d573651a3 | ||
|
|
52efc23984 | ||
|
|
aefb84df3b | ||
|
|
ba374e08ff | ||
|
|
33a11ac2e5 | ||
|
|
357c4a382d | ||
|
|
d7e8f26ace | ||
|
|
5c312c1939 | ||
|
|
5163ab134e | ||
|
|
73dd0db529 | ||
|
|
8921db89ab | ||
|
|
8d624459d4 | ||
|
|
ec96021b00 | ||
|
|
25f50b8cdf | ||
|
|
0127d5143a | ||
|
|
ffe3b689c4 | ||
|
|
ff123be895 | ||
|
|
8178ec5671 | ||
|
|
67dd089e67 | ||
|
|
8d96368ea6 | ||
|
|
eb163ef03c | ||
|
|
5558403358 | ||
|
|
db3a4d0f01 | ||
|
|
d01fe62757 | ||
|
|
87cfc8f1de | ||
|
|
3a8bef26d3 | ||
|
|
b550c0e9e3 | ||
|
|
8f1ee30553 | ||
|
|
458174a5f5 | ||
|
|
c5414aadd1 | ||
|
|
eacd01e77e | ||
|
|
089b919a68 | ||
|
|
2e5945642d | ||
|
|
68a5b6fc50 | ||
|
|
88538257ac | ||
|
|
fb8041fb4b | ||
|
|
e685c4302d | ||
|
|
103b56ea3d | ||
|
|
0490b115ad | ||
|
|
5e1dd4a9ad | ||
|
|
93dd97a14a | ||
|
|
52d065a38d | ||
|
|
ad33e9f32d | ||
|
|
b9f00c6971 | ||
|
|
dba5ea156a | ||
|
|
631c86865f | ||
|
|
324b8fc74a | ||
|
|
282ca3ea2a | ||
|
|
17223db3ea | ||
|
|
cff3fdae6e | ||
|
|
108e83a402 | ||
|
|
fc237848c8 | ||
|
|
92b86bfa0b | ||
|
|
b0fd17047c | ||
|
|
bf8f82fbd8 | ||
|
|
5468c60eaa | ||
|
|
8736d87b95 | ||
|
|
dd0440519b | ||
|
|
b7400553fc | ||
|
|
9d2f570515 | ||
|
|
7eeba0c082 | ||
|
|
2e6bb21fda | ||
|
|
e30cfdf942 | ||
|
|
b07fb92e5c | ||
|
|
5729125fbb | ||
|
|
4577038abd | ||
|
|
7649c1df9e | ||
|
|
033bd111ad | ||
|
|
2cbe07b373 | ||
|
|
f6ec5c67a7 | ||
|
|
6bb78d3216 | ||
|
|
fa717a357d | ||
|
|
eed6bcc044 | ||
|
|
a01fd739bd | ||
|
|
3b7ed5ffd7 | ||
|
|
b3d9beea6d | ||
|
|
e256c7a7d9 | ||
|
|
ef866f957a | ||
|
|
58d25415db | ||
|
|
15f51a4064 | ||
|
|
7fbe456e79 | ||
|
|
9d8daac4cf | ||
|
|
ce199374d5 | ||
|
|
4af8615624 | ||
|
|
b6bb438507 | ||
|
|
49acd8a4f3 | ||
|
|
3e699a99d5 | ||
|
|
e4238f9283 | ||
|
|
9cd6d6d4c1 | ||
|
|
49a4b5feb4 | ||
|
|
c6eff157de | ||
|
|
80d16233e7 | ||
|
|
65e1a39027 | ||
|
|
d73b567bd4 | ||
|
|
787bb0a9e6 | ||
|
|
7faf0efb20 | ||
|
|
ed70cb8e3d | ||
|
|
2544b5b821 | ||
|
|
90613b7cc8 | ||
|
|
173640d0b4 | ||
|
|
e70d09ebbf | ||
|
|
1ff33378e1 | ||
|
|
286739f770 | ||
|
|
45b1cd3942 | ||
|
|
b2d34ab95d | ||
|
|
f628ed55cf | ||
|
|
70ec9f50ab | ||
|
|
d6d130b8f9 | ||
|
|
40410eb10f | ||
|
|
a6e3dbd825 | ||
|
|
3882df41f1 | ||
|
|
6c67b96e30 | ||
|
|
b6d34472fe | ||
|
|
69d11b5cd0 | ||
|
|
f36f2029e5 | ||
|
|
af7476f79a | ||
|
|
890207daee | ||
|
|
9a5653f1e3 | ||
|
|
4f45ba1680 | ||
|
|
0ccfd36a83 | ||
|
|
8452a95114 | ||
|
|
9b98035ee7 | ||
|
|
6dc75afdc8 | ||
|
|
922ae7a274 | ||
|
|
2d46d12628 | ||
|
|
44f270f408 | ||
|
|
9ff70c4aef | ||
|
|
8c6a54397f | ||
|
|
ce915df2b2 | ||
|
|
84dd772b7c | ||
|
|
959f6c2c58 | ||
|
|
793b2b20e9 | ||
|
|
1c224fedc5 | ||
|
|
4299baec5d | ||
|
|
e931d3efbc | ||
|
|
36d81a6ec9 | ||
|
|
22c2d09d88 | ||
|
|
558659b54d | ||
|
|
eb1a0ba49f | ||
|
|
45212e7e14 | ||
|
|
c691af9712 | ||
|
|
0c2226b3fc | ||
|
|
eeda6b0208 | ||
|
|
d9096424c5 | ||
|
|
d171078ca4 | ||
|
|
cb18b8f347 | ||
|
|
4fdb72f93c | ||
|
|
549930b48c | ||
|
|
409a3de901 | ||
|
|
431ac5a403 | ||
|
|
999d4633ab | ||
|
|
5a26fc812d | ||
|
|
eb210e9072 | ||
|
|
dbfd25bb8e | ||
|
|
4292583d73 | ||
|
|
7c346d62eb | ||
|
|
14abbee3fb | ||
|
|
903979b3f2 | ||
|
|
913e649e5d | ||
|
|
7f8733796e | ||
|
|
1a98afee92 | ||
|
|
60daafe136 | ||
|
|
c7ad06a1f7 | ||
|
|
d07c795511 | ||
|
|
89d1071dc1 | ||
|
|
dfe34a9f22 | ||
|
|
93d3ea70fc | ||
|
|
89a76d9ead | ||
|
|
dc6bd1aae8 | ||
|
|
3412d70737 | ||
|
|
ce52d5cf42 | ||
|
|
da19066fb8 | ||
|
|
be20c2c800 | ||
|
|
c59638aaae | ||
|
|
00691f1225 | ||
|
|
735b79a4e0 | ||
|
|
443b529667 | ||
|
|
c3e62f58ab | ||
|
|
9869545777 | ||
|
|
04e5034f5f | ||
|
|
e99e3eb6c4 | ||
|
|
508615b600 | ||
|
|
4d3d416ecb | ||
|
|
be2897806a | ||
|
|
6ebde04d22 | ||
|
|
9deaa8694e | ||
|
|
5dff50ff92 | ||
|
|
ba4d90765a | ||
|
|
187ccaa95e | ||
|
|
18b6d8289f | ||
|
|
c851f8f006 | ||
|
|
9d60704c9b | ||
|
|
365d7a1afd | ||
|
|
a3d5f23861 | ||
|
|
330ea986d4 | ||
|
|
652c1aae73 | ||
|
|
c91adcd165 | ||
|
|
293c286d22 | ||
|
|
70244f79ba | ||
|
|
6ba49ad294 | ||
|
|
acf8cbfe0e | ||
|
|
f904fd00e5 | ||
|
|
831bec5baf | ||
|
|
f0257b0f87 | ||
|
|
66856d1229 | ||
|
|
27c22a4b09 | ||
|
|
1e7cce9056 | ||
|
|
219af76679 | ||
|
|
dcf773fd88 | ||
|
|
df51e6d429 | ||
|
|
6ea66b85e0 | ||
|
|
10aae90ae2 | ||
|
|
ea8e31561e | ||
|
|
45c627b0a5 | ||
|
|
1d2ca469fc | ||
|
|
e6a0c86f4e | ||
|
|
039429d452 | ||
|
|
46df9398e8 | ||
|
|
744907ac38 | ||
|
|
084677cdca | ||
|
|
891cd3124f | ||
|
|
b9fd3b8b7d | ||
|
|
f6208c1324 | ||
|
|
3a117c0f09 | ||
|
|
a0c83f33ca | ||
|
|
99b9fadd74 | ||
|
|
cf023847ad | ||
|
|
59357b274d | ||
|
|
1582184223 | ||
|
|
db9bcafb82 | ||
|
|
50b9838eec | ||
|
|
ff958b7cd6 | ||
|
|
33bc2aa220 | ||
|
|
03b331e5d5 | ||
|
|
86cc9fb7d8 | ||
|
|
f98efe0b97 | ||
|
|
6c5345df64 | ||
|
|
2d97661d28 | ||
|
|
3efe0c1ce2 | ||
|
|
6099c91216 | ||
|
|
934e4d9607 | ||
|
|
1198f6112e | ||
|
|
ac7e08ae2c | ||
|
|
9c5cbd348b | ||
|
|
8600710d23 | ||
|
|
d06d52deda | ||
|
|
948377ba93 | ||
|
|
d878028dcc | ||
|
|
65c969a321 | ||
|
|
777f7f9305 | ||
|
|
43fb37ab1d | ||
|
|
06734ec886 | ||
|
|
0b8ae93727 | ||
|
|
24cea97e08 | ||
|
|
18536e5db0 | ||
|
|
8c236cb5cb | ||
|
|
797c8ad7fa | ||
|
|
ba8d2dcb8b | ||
|
|
78a095d958 | ||
|
|
627172f6df | ||
|
|
a39f25961c | ||
|
|
e738ae5c8c | ||
|
|
749e85e8e6 | ||
|
|
cfa251b158 | ||
|
|
1ca38b8741 | ||
|
|
d7bc5a7088 | ||
|
|
3b9a2c3ee1 | ||
|
|
260ad77d39 | ||
|
|
5c508a0cd9 | ||
|
|
fd3d1607a4 | ||
|
|
abbc0fbcf1 | ||
|
|
00be41608d | ||
|
|
499c3f2e13 | ||
|
|
627845f6e4 | ||
|
|
07eea76057 | ||
|
|
fecc4e9b79 | ||
|
|
58e9302f15 | ||
|
|
86f649fab1 | ||
|
|
9ac0becfb2 | ||
|
|
eda547b868 | ||
|
|
f75e872415 | ||
|
|
aef0712165 | ||
|
|
bed4b7fd27 | ||
|
|
b53ff5daf3 | ||
|
|
bb0872b4fc | ||
|
|
b65101f4be | ||
|
|
593d242a4c | ||
|
|
db7f339c34 | ||
|
|
9f3575a874 | ||
|
|
1c9c59c512 | ||
|
|
127202b831 | ||
|
|
4f8a04ed21 | ||
|
|
63b2e0560b | ||
|
|
07291d71f2 | ||
|
|
d1ca1ec4d9 | ||
|
|
6907cf9972 | ||
|
|
d4f8d1498d | ||
|
|
0952e4a664 | ||
|
|
6a4e8c95ea | ||
|
|
983bfb7adf | ||
|
|
d7aaf5e210 | ||
|
|
50281132ad | ||
|
|
6a2b22015e | ||
|
|
2f90890f50 | ||
|
|
0fe83a0583 | ||
|
|
ce74e69480 | ||
|
|
ddea2aeb22 | ||
|
|
7bbe69cce9 | ||
|
|
e921e30d64 | ||
|
|
cd4f9d8bb4 | ||
|
|
a0553788b6 | ||
|
|
1a183d78af | ||
|
|
cabcaa892c | ||
|
|
01c9d62a2b | ||
|
|
ba76df863c | ||
|
|
81441a0895 | ||
|
|
da0222f213 | ||
|
|
fb8a2eb2e0 | ||
|
|
cde2e27e04 | ||
|
|
3758ea2cf4 | ||
|
|
e62fc11328 | ||
|
|
3cbfae83c1 | ||
|
|
57667654ef | ||
|
|
eadd66fa91 | ||
|
|
75cd94a39a | ||
|
|
7872bfe19d | ||
|
|
af008e69c2 | ||
|
|
a549abc20f | ||
|
|
116344737a | ||
|
|
93c03f4e88 | ||
|
|
445332c27c | ||
|
|
c42e1892d0 | ||
|
|
b6b526dd57 | ||
|
|
3ef7f19ffc | ||
|
|
9d0d851c2e | ||
|
|
adb35b5bef | ||
|
|
acead09377 | ||
|
|
714cf43f6a | ||
|
|
5df0755252 | ||
|
|
c14827b234 | ||
|
|
ff9ef2af41 | ||
|
|
91ef5edcc3 | ||
|
|
27302c6fcc | ||
|
|
4d975da176 | ||
|
|
5b58d8a1e8 | ||
|
|
3105958afb | ||
|
|
a505227d01 | ||
|
|
673503b76f | ||
|
|
384682421d | ||
|
|
2ddd6e6321 | ||
|
|
86739aa1ac | ||
|
|
45a46cbc7a | ||
|
|
567f453232 | ||
|
|
890f654971 | ||
|
|
572a0ac266 | ||
|
|
d26ffdbe1a | ||
|
|
0bfc9236ed | ||
|
|
32e6394b3f | ||
|
|
09735b7f47 | ||
|
|
ee280d5c7b | ||
|
|
c1b56e4cb6 | ||
|
|
6698d15f20 | ||
|
|
ef35fd02e5 | ||
|
|
8e70e20f9e | ||
|
|
9632bf5b93 | ||
|
|
dde0cab04b | ||
|
|
c8337c7287 | ||
|
|
15560a3bce | ||
|
|
2e3a60cf6e | ||
|
|
08b0c43382 | ||
|
|
4e0e11a611 | ||
|
|
ef41dfca4c | ||
|
|
cfbca4b0fd | ||
|
|
fdea9a68a1 | ||
|
|
47169e19aa | ||
|
|
0b03c8360b | ||
|
|
62f8af1455 | ||
|
|
0934d452bb | ||
|
|
f2f31790b4 | ||
|
|
cf6ecc17cc | ||
|
|
931f9bdce0 | ||
|
|
bec0528a3a | ||
|
|
670f2b1fc3 | ||
|
|
f2f6de717b | ||
|
|
f8ad2eddf3 | ||
|
|
c36a46cad6 | ||
|
|
00360c77d2 | ||
|
|
8a62cd386e | ||
|
|
450327f093 | ||
|
|
e87ec04058 | ||
|
|
f9d41de8f1 | ||
|
|
f80a1a5f6b | ||
|
|
f81caf962d | ||
|
|
d18fcf0a18 | ||
|
|
0187217c86 | ||
|
|
b820bdec09 | ||
|
|
adace2954e | ||
|
|
6eeb8eeba6 | ||
|
|
dd2a8202ef | ||
|
|
d1cfd627bc | ||
|
|
fb97b7443d | ||
|
|
48fcd45d7d | ||
|
|
5cfc418d77 | ||
|
|
f3fbe38247 | ||
|
|
a0a1c84db1 | ||
|
|
54d563f49e | ||
|
|
e8ee8b8a16 | ||
|
|
c6ac44ba14 | ||
|
|
e4d8438801 | ||
|
|
f9539ab50a | ||
|
|
59f83c2432 | ||
|
|
cd789136c0 | ||
|
|
54b5bc441e | ||
|
|
2537b6ba09 | ||
|
|
013a1b4f51 | ||
|
|
d2377bd7c3 | ||
|
|
c17314125e | ||
|
|
09a59480f3 | ||
|
|
63cc2ce70a | ||
|
|
4642e050ba | ||
|
|
27a442ed2e | ||
|
|
325ae00eeb | ||
|
|
152e4129b2 | ||
|
|
2ddcf84625 | ||
|
|
13314700cd | ||
|
|
a7a499a2b1 | ||
|
|
b646313b58 | ||
|
|
f3ce4ca803 | ||
|
|
93d99c0c47 | ||
|
|
ae1fc7572a | ||
|
|
1a527cca10 | ||
|
|
c625513924 | ||
|
|
3f58302a14 | ||
|
|
63b199c9c2 | ||
|
|
fc64c565db | ||
|
|
91e60fa82b | ||
|
|
0cc52c2206 | ||
|
|
2ffe4ba70b | ||
|
|
2afd7e3687 | ||
|
|
a0f8d13c4f | ||
|
|
2571ea021a | ||
|
|
6950e05b6a | ||
|
|
7eb767a268 | ||
|
|
8e64abc4bc | ||
|
|
52df793a74 | ||
|
|
8e44a421a2 | ||
|
|
7f4ccdcac8 | ||
|
|
03e8de2f62 | ||
|
|
8b04eecc90 | ||
|
|
16bcd86792 | ||
|
|
be3c519a57 | ||
|
|
8776cb1cea | ||
|
|
4c94503f9a | ||
|
|
48f57376d3 | ||
|
|
958469f526 | ||
|
|
2a774a7bb6 | ||
|
|
a872ad9d8b | ||
|
|
2499a05473 | ||
|
|
6b66893ea4 | ||
|
|
529c27aed5 | ||
|
|
70fc0afbc4 | ||
|
|
09f81fd0d6 | ||
|
|
af7f2d4d5e | ||
|
|
3bd5d6b9f6 | ||
|
|
57912b5a5a | ||
|
|
a05f5b9737 | ||
|
|
1963b586ac | ||
|
|
3b9ad59849 | ||
|
|
79e0e5668d | ||
|
|
0e8edf0c72 | ||
|
|
24e2544544 | ||
|
|
f3732c76ea | ||
|
|
a4c72a9a86 | ||
|
|
455610e586 | ||
|
|
634d58b3ca | ||
|
|
27bbd77e8c | ||
|
|
d8ae77ded7 | ||
|
|
0648c04728 | ||
|
|
57c26e3b4a | ||
|
|
b03afff994 | ||
|
|
77f9e60177 | ||
|
|
35bb792496 | ||
|
|
8a87304800 | ||
|
|
64bbe053f8 | ||
|
|
d3f420bf6d | ||
|
|
7fcaaa297a | ||
|
|
7c2d2044a9 | ||
|
|
aa32f59dc6 | ||
|
|
182af99e7c | ||
|
|
5b520a7a81 | ||
|
|
364917c910 | ||
|
|
ca7b9c786a | ||
|
|
15c2363098 | ||
|
|
1a11095121 | ||
|
|
2b384b1d15 | ||
|
|
a1d61edb9c | ||
|
|
96a8687896 | ||
|
|
0448773682 | ||
|
|
57998ba727 | ||
|
|
de83447cb3 | ||
|
|
eba19468d5 | ||
|
|
65c78df671 | ||
|
|
a7096aa89f | ||
|
|
15a50ef452 | ||
|
|
04036e5c87 | ||
|
|
2bbb5ef74e | ||
|
|
91eb7feb3c | ||
|
|
978d77142c | ||
|
|
e36478b9ac | ||
|
|
e1fe4dd693 | ||
|
|
b1ee949b1c | ||
|
|
a0e5f8e97e | ||
|
|
e9cfb2c4ee | ||
|
|
190b6edfb1 | ||
|
|
80a0c59f87 | ||
|
|
823fdec705 | ||
|
|
fe87dcced7 | ||
|
|
137eb44516 | ||
|
|
f60d957102 | ||
|
|
8f0b04504f | ||
|
|
2c39d8b1c8 | ||
|
|
d4d1c32288 | ||
|
|
e4f39d2b6a | ||
|
|
e5a2bfbcbd | ||
|
|
de3b76b31d | ||
|
|
53455496bf | ||
|
|
cc2a2f6dfb | ||
|
|
ee4ac7371c | ||
|
|
d5265407b9 | ||
|
|
954b3e9fc5 | ||
|
|
7d9894bef7 | ||
|
|
3b34698e8b | ||
|
|
263cb581c4 | ||
|
|
1c9cb4516c | ||
|
|
ac4ceccb4f | ||
|
|
e731b7882d | ||
|
|
84e0728ff3 | ||
|
|
666bc18e91 | ||
|
|
8f83124a0d | ||
|
|
ee91daad7e | ||
|
|
ee78c0d33b | ||
|
|
1318abd37e | ||
|
|
76a031a8c9 | ||
|
|
09482ebcf3 | ||
|
|
67424f2d3a | ||
|
|
51f530ffbe | ||
|
|
013f96a754 | ||
|
|
df6a018fb6 | ||
|
|
409eaf54c1 | ||
|
|
7e04fd342c | ||
|
|
1fe15bc6a5 | ||
|
|
ff1bffbb55 | ||
|
|
b28b18a19a | ||
|
|
bbc3c85212 | ||
|
|
26a08fac06 | ||
|
|
da9d7a4336 | ||
|
|
46c6555f94 | ||
|
|
3e980fd2d4 | ||
|
|
fb1462f669 | ||
|
|
41e1630aac | ||
|
|
ef84c4e3da | ||
|
|
61cc44cc83 | ||
|
|
c20cbe7d66 | ||
|
|
2f4af3223b | ||
|
|
e4b2c42897 | ||
|
|
746df9277c | ||
|
|
8428588a4c | ||
|
|
83a8f4b911 | ||
|
|
2736024cb7 | ||
|
|
9a32ca893e | ||
|
|
59d3c6c94f | ||
|
|
388027b731 | ||
|
|
8abdedc11d | ||
|
|
9758f5baa8 | ||
|
|
248262a597 | ||
|
|
cc0f2c7c7f | ||
|
|
72f6468d12 | ||
|
|
522c0edd90 | ||
|
|
d338f217fe | ||
|
|
ca79857386 | ||
|
|
60e551e273 | ||
|
|
954e148be3 | ||
|
|
3d0b79f674 | ||
|
|
d9442aa23c | ||
|
|
ba0daf4452 | ||
|
|
8d9cd5bbd1 | ||
|
|
186b877c09 | ||
|
|
5ed2dfccd1 | ||
|
|
911cfd8642 | ||
|
|
3539bd1e79 | ||
|
|
f56df7c16d | ||
|
|
c507dfa6c4 | ||
|
|
f6d2e898dc | ||
|
|
326c7a93fb | ||
|
|
58381b8062 | ||
|
|
0899cea4b4 | ||
|
|
7459e937b5 | ||
|
|
55db0bebbb | ||
|
|
0bdb8142c6 | ||
|
|
88ee94d4b6 | ||
|
|
1df4ed0fe9 | ||
|
|
2a339a2935 | ||
|
|
a1810e6023 | ||
|
|
832ca3347c | ||
|
|
9d2b64e82b | ||
|
|
9a5e4b3f54 | ||
|
|
e5e8032ba1 | ||
|
|
5356e68b51 | ||
|
|
cd94c625a7 | ||
|
|
972a3746a1 | ||
|
|
a9e12e4384 | ||
|
|
1690e6420f | ||
|
|
acdf61f7ab | ||
|
|
2e4fc557ea | ||
|
|
979dcead49 | ||
|
|
116ddf345d | ||
|
|
366805a64f | ||
|
|
1fee2a846a | ||
|
|
3f54eb52b2 | ||
|
|
a3847ce1c9 | ||
|
|
1e7415b692 | ||
|
|
ff950ef28a | ||
|
|
51bd12c6cf | ||
|
|
5fa37dbffb | ||
|
|
c6307e4ad3 | ||
|
|
06a54d451c | ||
|
|
e317075815 | ||
|
|
45541a255b | ||
|
|
3ab423d695 | ||
|
|
345d7b427a | ||
|
|
de6d6b692e | ||
|
|
b2845e2284 | ||
|
|
47383c347c | ||
|
|
4bda84d69c | ||
|
|
b510aa11f5 | ||
|
|
8dab6d5e04 | ||
|
|
85f833c865 | ||
|
|
15133d00c7 | ||
|
|
b93990d10b | ||
|
|
a0bcb8edbe | ||
|
|
bfdf691bed | ||
|
|
f60856b998 | ||
|
|
3308eeaf82 | ||
|
|
19930a2472 | ||
|
|
e75d95b1fc | ||
|
|
4319711dc6 | ||
|
|
9712be909d | ||
|
|
04060ce252 | ||
|
|
da066fe694 | ||
|
|
3b7215b36a | ||
|
|
b88d5cfb06 | ||
|
|
8ab96cf2fb | ||
|
|
63af2fd8b1 | ||
|
|
fcf26f7844 | ||
|
|
657ebc99df | ||
|
|
9500f9a8c9 | ||
|
|
a05bba15e7 | ||
|
|
d88ad0f6be | ||
|
|
4e34f16e33 | ||
|
|
503a806446 | ||
|
|
caf7606893 | ||
|
|
2fb51fe37c | ||
|
|
fa1c48e480 | ||
|
|
3d9a631786 | ||
|
|
0b4cfd6563 | ||
|
|
484dfe6726 | ||
|
|
d8cb93fb10 | ||
|
|
23b8b49c00 | ||
|
|
932997259f | ||
|
|
1bebb66165 | ||
|
|
ac0d81f9b3 | ||
|
|
45b99d13cd | ||
|
|
920704075e | ||
|
|
9e929f80ad | ||
|
|
9696a6cba1 | ||
|
|
211fd8b28a | ||
|
|
c6ef86cbbe | ||
|
|
90c2ff7480 | ||
|
|
49057810fb | ||
|
|
e5e6e2e1b8 | ||
|
|
c8851ecd2a | ||
|
|
bd2d77fef7 | ||
|
|
43403f8bb1 | ||
|
|
0ac7839f11 | ||
|
|
714b5f7b4b | ||
|
|
51fb43d624 | ||
|
|
a79cbb2d5c | ||
|
|
e88c197f86 | ||
|
|
b1be92e6c9 | ||
|
|
d20f005c5d | ||
|
|
5dbfb24f1c | ||
|
|
4df489bd10 | ||
|
|
8c3510413a | ||
|
|
bea9dfdfc7 | ||
|
|
465b315ae0 | ||
|
|
054daac6db | ||
|
|
867ec25e54 | ||
|
|
2e4aaf7345 | ||
|
|
2f754bbb87 | ||
|
|
cdf6ed47dd | ||
|
|
c31432fe3f | ||
|
|
f0b2e91091 | ||
|
|
72a08e8fec | ||
|
|
864001bdff | ||
|
|
bd2816b2ac | ||
|
|
03918527f6 | ||
|
|
6140e93cc8 | ||
|
|
0f8eaaf750 | ||
|
|
7afad6ac49 | ||
|
|
e9308bdd69 | ||
|
|
16b60ada50 | ||
|
|
aa71251edd | ||
|
|
89cfd35d72 | ||
|
|
9ea16a39df | ||
|
|
4fee2586e4 | ||
|
|
b30511eb51 | ||
|
|
05325e7276 | ||
|
|
0dde2eb20f | ||
|
|
c2fcc72e62 | ||
|
|
ec686c9452 | ||
|
|
863de33f63 | ||
|
|
f56dd10106 | ||
|
|
8b10eb130a | ||
|
|
b0d9895e5e | ||
|
|
1d3e3f3c87 | ||
|
|
36eaebcbc7 | ||
|
|
7870c60ab4 | ||
|
|
e0d52d3578 | ||
|
|
9422825aec | ||
|
|
e467862c29 | ||
|
|
6b03ea2fe5 | ||
|
|
472d79cbf2 | ||
|
|
27701bbe1b | ||
|
|
9e1dcf8b64 | ||
|
|
c74de88ca3 | ||
|
|
361e9c629e | ||
|
|
3b907627f7 | ||
|
|
2284fd41b9 |
14
.babelrc
Normal file
14
.babelrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"presets": ["react", "es2015"],
|
||||
"env": {
|
||||
"development": {
|
||||
"presets": ["react-hmre"]
|
||||
},
|
||||
"test": {
|
||||
"presets": ["react", "es2015"],
|
||||
"plugins": [
|
||||
[ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
3
.eslintignore
Normal file
3
.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
compiled/
|
||||
dist/
|
||||
6
.eslintrc
Normal file
6
.eslintrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": ["standard", "standard-jsx"],
|
||||
"rules": {
|
||||
"no-useless-escape": 0
|
||||
}
|
||||
}
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,6 +1,10 @@
|
||||
build/
|
||||
node_modules/
|
||||
electron_build/
|
||||
.DS_Store
|
||||
.env
|
||||
dist/
|
||||
vendor/
|
||||
Desktop.ini
|
||||
Thumbs.db
|
||||
node_modules/*
|
||||
!node_modules/boost
|
||||
/dist
|
||||
/compiled
|
||||
/secret
|
||||
*.log
|
||||
|
||||
6
.travis.yml
Normal file
6
.travis.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 'stable'
|
||||
- 'lts/*'
|
||||
|
||||
script: npm run lint && npm run test
|
||||
129
Gulpfile.js
129
Gulpfile.js
@@ -1,129 +0,0 @@
|
||||
var gulp = require('gulp')
|
||||
var styl = require('gulp-stylus')
|
||||
var autoprefixer = require('gulp-autoprefixer')
|
||||
var del = require('del')
|
||||
var runSequence = require('run-sequence')
|
||||
var plumber = require('gulp-plumber')
|
||||
var notify = require('gulp-notify')
|
||||
var rename = require('gulp-rename')
|
||||
var livereload = require('gulp-livereload')
|
||||
var inject = require('gulp-inject')
|
||||
|
||||
// for Dist
|
||||
var rev = require('gulp-rev')
|
||||
var ngAnnotate = require('gulp-ng-annotate')
|
||||
var templateCache = require('gulp-angular-templatecache')
|
||||
var uglify = require('gulp-uglify')
|
||||
var minifyCss = require('gulp-minify-css')
|
||||
var merge = require('merge-stream')
|
||||
var concat = require('gulp-concat')
|
||||
var minifyHtml = require('gulp-minify-html')
|
||||
|
||||
var config = require('./build.config.js')
|
||||
|
||||
gulp.task('build', function () {
|
||||
var tpls = gulp.src(['src/browser/main/**/*.html','!src/browser/main/index.html','!src/browser/main/index.inject.html'])
|
||||
.pipe(templateCache({}))
|
||||
.pipe(concat('tpls.js'))
|
||||
.pipe(ngAnnotate())
|
||||
.pipe(uglify())
|
||||
.pipe(gulp.dest('build'))
|
||||
var js = gulp.src(['src/browser/main/**/*.js', 'src/browser/shared/**/*.js'])
|
||||
.pipe(concat('app.js'))
|
||||
.pipe(ngAnnotate())
|
||||
.pipe(uglify())
|
||||
.pipe(gulp.dest('build'))
|
||||
var css = gulp.src(['src/browser/main/**/*.css', 'src/browser/shared/**/*.css'])
|
||||
.pipe(concat('all.css'))
|
||||
.pipe(minifyCss())
|
||||
.pipe(gulp.dest('build'))
|
||||
return merge(tpls, js, css)
|
||||
})
|
||||
|
||||
gulp.task('vendor', function () {
|
||||
var vendors = config.vendors
|
||||
|
||||
var vendorFiles = vendors.map(function (vendor) {
|
||||
return vendor.src
|
||||
})
|
||||
|
||||
vendorFiles.push('node_modules/font-awesome/**/font-awesome.css')
|
||||
vendorFiles.push('node_modules/font-awesome/**/fontawesome-webfont.*')
|
||||
vendorFiles.push('node_modules/font-awesome/**/FontAwesome.*')
|
||||
|
||||
return gulp.src(vendorFiles)
|
||||
.pipe(gulp.dest('src/browser/vendor'))
|
||||
})
|
||||
|
||||
gulp.task('styl', function () {
|
||||
return gulp.src('src/browser/main/styles/app.styl')
|
||||
.pipe(plumber({errorHandler: notify.onError('Error: <%= error.message %>')}))
|
||||
.pipe(styl())
|
||||
.pipe(autoprefixer())
|
||||
.pipe(gulp.dest('src/browser/main/styles/'))
|
||||
.pipe(livereload())
|
||||
.pipe(notify('Stylus!!'))
|
||||
})
|
||||
|
||||
gulp.task('styl-popup', function () {
|
||||
return gulp.src('src/browser/popup/styles/app.styl')
|
||||
.pipe(plumber({errorHandler: notify.onError('Error: <%= error.message %>')}))
|
||||
.pipe(styl())
|
||||
.pipe(autoprefixer())
|
||||
.pipe(gulp.dest('src/browser/popup/styles/'))
|
||||
.pipe(livereload())
|
||||
.pipe(notify('Stylus!! @POPUP'))
|
||||
})
|
||||
|
||||
gulp.task('bs', function () {
|
||||
return gulp.src('src/browser/shared/styles/bootstrap.styl')
|
||||
.pipe(plumber({errorHandler: notify.onError('Error: <%= error.message %>')}))
|
||||
.pipe(styl())
|
||||
.pipe(autoprefixer())
|
||||
.pipe(gulp.dest('src/browser/shared/styles'))
|
||||
.pipe(notify('Bootstrap compiled!!'))
|
||||
.pipe(livereload())
|
||||
})
|
||||
|
||||
gulp.task('inject', function (cb) {
|
||||
runSequence(['inject-main', 'inject-popup'], cb)
|
||||
})
|
||||
|
||||
gulp.task('inject-main', function () {
|
||||
return gulp.src('src/browser/main/index.inject.html')
|
||||
.pipe(inject(gulp.src(['src/browser/main/**/*.js', 'src/browser/main/**/*.css', 'src/browser/shared/**/*.js', 'src/browser/shared/**/*.css'], {read: false}), {
|
||||
relative: true
|
||||
}))
|
||||
.pipe(rename(function (path) {
|
||||
path.basename = 'index'
|
||||
}))
|
||||
.pipe(gulp.dest('src/browser/main/'))
|
||||
})
|
||||
|
||||
gulp.task('watch-main', function () {
|
||||
gulp.watch(
|
||||
['src/browser/main/index.inject.html', 'src/browser/main/**/*.js', 'src/browser/main/**/*.css', 'src/browser/shared/**/*.js', 'src/browser/shared/**/*.css'], ['inject-main'])
|
||||
|
||||
gulp.watch('src/browser/main/styles/**/*.styl', ['styl'])
|
||||
gulp.watch('src/browser/popup/styles/**/*.styl', ['styl-popup'])
|
||||
gulp.watch('src/browser/shared/styles/**/*.styl', ['bs'])
|
||||
livereload.listen()
|
||||
})
|
||||
gulp.task('inject-popup', function () {
|
||||
return gulp.src('src/browser/popup/index.inject.html')
|
||||
.pipe(inject(gulp.src(['src/browser/popup/**/*.js', 'src/browser/popup/**/*.css', 'src/browser/shared/**/*.js', 'src/browser/shared/**/*.css'], {read: false}), {
|
||||
relative: true
|
||||
}))
|
||||
.pipe(rename(function (path) {
|
||||
path.basename = 'index'
|
||||
}))
|
||||
.pipe(gulp.dest('src/browser/popup/'))
|
||||
})
|
||||
|
||||
gulp.task('del', function (cb) {
|
||||
del(['build/**/*'], cb)
|
||||
})
|
||||
|
||||
gulp.task('default', function (cb) {
|
||||
runSequence('del', 'build', 'watch', cb)
|
||||
})
|
||||
18
LICENSE
Normal file
18
LICENSE
Normal file
@@ -0,0 +1,18 @@
|
||||
GPL-3.0
|
||||
|
||||
Boostnote - the simplest note app
|
||||
|
||||
Copyright (C) 2016 MAISIN&CO.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
11
appdmg.json
Normal file
11
appdmg.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"title": "Boostnote",
|
||||
"icon": "resources/dmg.icns",
|
||||
"background": "resources/boostnote-install.png",
|
||||
"icon-size": 80,
|
||||
"contents": [
|
||||
{ "x": 448, "y": 344, "type": "link", "path": "/Applications" },
|
||||
{ "x": 192, "y": 344, "type": "file", "path": "dist/Boostnote-darwin-x64/Boostnote.app" }
|
||||
]
|
||||
|
||||
}
|
||||
220
browser/components/CodeEditor.js
Normal file
220
browser/components/CodeEditor.js
Normal file
@@ -0,0 +1,220 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import _ from 'lodash'
|
||||
import CodeMirror from 'codemirror'
|
||||
import path from 'path'
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.changeHandler = (e) => this.handleChange(e)
|
||||
this.blurHandler = (editor, e) => {
|
||||
if (e == null) return null
|
||||
let el = e.relatedTarget
|
||||
while (el != null) {
|
||||
if (el === this.refs.root) {
|
||||
return
|
||||
}
|
||||
el = el.parentNode
|
||||
}
|
||||
this.props.onBlur != null && this.props.onBlur(e)
|
||||
}
|
||||
this.loadStyleHandler = (e) => {
|
||||
this.editor.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.value = this.props.value
|
||||
this.editor = CodeMirror(this.refs.root, {
|
||||
value: this.props.value,
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
theme: this.props.theme,
|
||||
indentUnit: this.props.indentSize,
|
||||
tabSize: this.props.indentSize,
|
||||
indentWithTabs: this.props.indentType !== 'space',
|
||||
keyMap: this.props.keyMap,
|
||||
inputStyle: 'textarea',
|
||||
extraKeys: {
|
||||
Tab: function (cm) {
|
||||
if (cm.somethingSelected()) cm.indentSelection('add')
|
||||
else {
|
||||
if (cm.getOption('indentWithTabs')) {
|
||||
cm.execCommand('insertTab')
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab')
|
||||
}
|
||||
}
|
||||
},
|
||||
'Cmd-T': function (cm) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.setMode(this.props.mode)
|
||||
|
||||
this.editor.on('blur', this.blurHandler)
|
||||
this.editor.on('change', this.changeHandler)
|
||||
|
||||
let editorTheme = document.getElementById('editorTheme')
|
||||
editorTheme.addEventListener('load', this.loadStyleHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.editor.off('blur', this.blurHandler)
|
||||
this.editor.off('change', this.changeHandler)
|
||||
let editorTheme = document.getElementById('editorTheme')
|
||||
editorTheme.removeEventListener('load', this.loadStyleHandler)
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
let needRefresh = false
|
||||
if (prevProps.mode !== this.props.mode) {
|
||||
this.setMode(this.props.mode)
|
||||
}
|
||||
if (prevProps.theme !== this.props.theme) {
|
||||
this.editor.setOption('theme', this.props.theme)
|
||||
// editor should be refreshed after css loaded
|
||||
}
|
||||
if (prevProps.fontSize !== this.props.fontSize) {
|
||||
needRefresh = true
|
||||
}
|
||||
if (prevProps.fontFamily !== this.props.fontFamily) {
|
||||
needRefresh = true
|
||||
}
|
||||
if (prevProps.keyMap !== this.props.keyMap) {
|
||||
needRefresh = true
|
||||
}
|
||||
|
||||
if (prevProps.indentSize !== this.props.indentSize) {
|
||||
this.editor.setOption('indentUnit', this.props.indentSize)
|
||||
this.editor.setOption('tabSize', this.props.indentSize)
|
||||
}
|
||||
if (prevProps.indentType !== this.props.indentType) {
|
||||
this.editor.setOption('indentWithTabs', this.props.indentType !== 'space')
|
||||
}
|
||||
|
||||
if (needRefresh) {
|
||||
this.editor.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
setMode (mode) {
|
||||
let syntax = CodeMirror.findModeByName(pass(mode))
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
|
||||
this.editor.setOption('mode', syntax.mime)
|
||||
CodeMirror.autoLoadMode(this.editor, syntax.mode)
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
this.value = this.editor.getValue()
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(e)
|
||||
}
|
||||
}
|
||||
|
||||
moveCursorTo (row, col) {
|
||||
}
|
||||
|
||||
scrollToLine (num) {
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.editor.focus()
|
||||
}
|
||||
|
||||
blur () {
|
||||
this.editor.blur()
|
||||
}
|
||||
|
||||
reload () {
|
||||
// Change event shouldn't be fired when switch note
|
||||
this.editor.off('change', this.changeHandler)
|
||||
this.value = this.props.value
|
||||
this.editor.setValue(this.props.value)
|
||||
this.editor.clearHistory()
|
||||
this.editor.on('change', this.changeHandler)
|
||||
}
|
||||
|
||||
setValue (value) {
|
||||
let cursor = this.editor.getCursor()
|
||||
this.editor.setValue(value)
|
||||
this.editor.setCursor(cursor)
|
||||
}
|
||||
|
||||
handleDropImage (e) {
|
||||
e.preventDefault()
|
||||
let imagePath = e.dataTransfer.files[0].path
|
||||
let filename = path.basename(imagePath)
|
||||
let imageMd = ``
|
||||
this.insertImage(imageMd)
|
||||
}
|
||||
|
||||
insertImage (imageMd) {
|
||||
const textarea = this.editor.getInputField()
|
||||
textarea.value = textarea.value.substr(0, textarea.selectionStart) + imageMd + textarea.value.substr(textarea.selectionEnd)
|
||||
}
|
||||
|
||||
render () {
|
||||
let { className, fontFamily, fontSize } = this.props
|
||||
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
|
||||
? [fontFamily].concat(defaultEditorFontFamily)
|
||||
: defaultEditorFontFamily
|
||||
return (
|
||||
<div
|
||||
className={className == null
|
||||
? 'CodeEditor'
|
||||
: `CodeEditor ${className}`
|
||||
}
|
||||
ref='root'
|
||||
tabIndex='-1'
|
||||
style={{
|
||||
fontFamily: fontFamily.join(', '),
|
||||
fontSize: fontSize
|
||||
}}
|
||||
onDrop={(e) => this.handleDropImage(e)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CodeEditor.propTypes = {
|
||||
value: PropTypes.string,
|
||||
mode: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
onBlur: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
readOnly: PropTypes.bool
|
||||
}
|
||||
|
||||
CodeEditor.defaultProps = {
|
||||
readOnly: false,
|
||||
theme: 'xcode',
|
||||
keyMap: 'sublime',
|
||||
fontSize: 14,
|
||||
fontFamily: 'Monaco, Consolas',
|
||||
indentSize: 4,
|
||||
indentType: 'space'
|
||||
}
|
||||
234
browser/components/MarkdownEditor.js
Normal file
234
browser/components/MarkdownEditor.js
Normal file
@@ -0,0 +1,234 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './MarkdownEditor.styl'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
|
||||
class MarkdownEditor extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.escapeFromEditor = ['Control', 'w']
|
||||
|
||||
this.state = {
|
||||
status: 'PREVIEW',
|
||||
renderValue: props.value,
|
||||
keyPressed: {}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.value = this.refs.code.value
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
this.value = this.refs.code.value
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
if (props.value !== this.props.value) {
|
||||
this.queueRendering(props.value)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.cancelQueue()
|
||||
}
|
||||
|
||||
queueRendering (value) {
|
||||
clearTimeout(this.renderTimer)
|
||||
this.renderTimer = setTimeout(() => {
|
||||
this.renderPreview(value)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
cancelQueue () {
|
||||
clearTimeout(this.renderTimer)
|
||||
}
|
||||
|
||||
renderPreview (value) {
|
||||
this.setState({
|
||||
renderValue: value
|
||||
})
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
this.value = this.refs.code.value
|
||||
this.props.onChange(e)
|
||||
}
|
||||
|
||||
handleContextMenu (e) {
|
||||
let { config } = this.props
|
||||
if (config.editor.switchPreview === 'RIGHTCLICK') {
|
||||
let newStatus = this.state.status === 'PREVIEW'
|
||||
? 'CODE'
|
||||
: 'PREVIEW'
|
||||
this.setState({
|
||||
status: newStatus
|
||||
}, () => {
|
||||
if (newStatus === 'CODE') {
|
||||
this.refs.code.focus()
|
||||
} else {
|
||||
this.refs.code.blur()
|
||||
this.refs.preview.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleBlur (e) {
|
||||
this.setState({ keyPressed: [] })
|
||||
let { config } = this.props
|
||||
if (config.editor.switchPreview === 'BLUR') {
|
||||
let cursorPosition = this.refs.code.editor.getCursor()
|
||||
this.setState({
|
||||
status: 'PREVIEW'
|
||||
}, () => {
|
||||
this.refs.preview.focus()
|
||||
this.refs.preview.scrollTo(cursorPosition.line)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handlePreviewMouseDown (e) {
|
||||
this.previewMouseDownedAt = new Date()
|
||||
}
|
||||
|
||||
handlePreviewMouseUp (e) {
|
||||
let { config } = this.props
|
||||
if (config.editor.switchPreview === 'BLUR' && new Date() - this.previewMouseDownedAt < 200) {
|
||||
this.setState({
|
||||
status: 'CODE'
|
||||
}, () => {
|
||||
this.refs.code.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleCheckboxClick (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
let idMatch = /checkbox-([0-9]+)/
|
||||
let checkedMatch = /\[x\]/i
|
||||
let uncheckedMatch = /\[ \]/
|
||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||
let lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||
let lines = this.refs.code.value
|
||||
.split('\n')
|
||||
|
||||
let 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'))
|
||||
}
|
||||
}
|
||||
|
||||
focus () {
|
||||
if (this.state.status === 'PREVIEW') {
|
||||
this.setState({
|
||||
status: 'CODE'
|
||||
}, () => {
|
||||
this.refs.code.focus()
|
||||
})
|
||||
} else {
|
||||
this.refs.code.focus()
|
||||
}
|
||||
}
|
||||
|
||||
reload () {
|
||||
this.refs.code.reload()
|
||||
this.cancelQueue()
|
||||
this.renderPreview(this.props.value)
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
const keyPressed = Object.assign(this.state.keyPressed, {
|
||||
[e.key]: true
|
||||
})
|
||||
this.setState({ keyPressed })
|
||||
let isNoteHandlerKey = (el) => { return this.state.keyPressed[el] }
|
||||
if (this.state.status === 'CODE' && this.escapeFromEditor.every(isNoteHandlerKey)) {
|
||||
document.activeElement.blur()
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyUp (e) {
|
||||
const keyPressed = Object.assign(this.state.keyPressed, {
|
||||
[e.key]: false
|
||||
})
|
||||
this.setState({ keyPressed })
|
||||
}
|
||||
|
||||
render () {
|
||||
let { className, value, config } = this.props
|
||||
|
||||
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
|
||||
|
||||
let previewStyle = {}
|
||||
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
|
||||
|
||||
return (
|
||||
<div className={className == null
|
||||
? 'MarkdownEditor'
|
||||
: `MarkdownEditor ${className}`
|
||||
}
|
||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||
tabIndex='-1'
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
onKeyUp={(e) => this.handleKeyUp(e)}
|
||||
>
|
||||
<CodeEditor styleName='codeEditor'
|
||||
ref='code'
|
||||
mode='GitHub Flavored Markdown'
|
||||
value={value}
|
||||
theme={config.editor.theme}
|
||||
keyMap={config.editor.keyMap}
|
||||
fontFamily={config.editor.fontFamily}
|
||||
fontSize={editorFontSize}
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorIndentSize}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
onBlur={(e) => this.handleBlur(e)}
|
||||
/>
|
||||
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
||||
? 'preview'
|
||||
: 'preview--hide'
|
||||
}
|
||||
style={previewStyle}
|
||||
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}
|
||||
indentSize={editorIndentSize}
|
||||
ref='preview'
|
||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||
tabIndex='0'
|
||||
value={this.state.renderValue}
|
||||
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
|
||||
onMouseDown={(e) => this.handlePreviewMouseDown(e)}
|
||||
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MarkdownEditor.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
ignorePreviewPointerEvents: PropTypes.bool
|
||||
}
|
||||
|
||||
export default CSSModules(MarkdownEditor, styles)
|
||||
23
browser/components/MarkdownEditor.styl
Normal file
23
browser/components/MarkdownEditor.styl
Normal file
@@ -0,0 +1,23 @@
|
||||
.root
|
||||
position relative
|
||||
|
||||
.codeEditor
|
||||
absolute top bottom left right
|
||||
|
||||
.codeEditor--hide
|
||||
@extend .codeEditor
|
||||
|
||||
.preview
|
||||
display block
|
||||
absolute top bottom left right
|
||||
z-index 100
|
||||
background-color white
|
||||
height 100%
|
||||
width 100%
|
||||
|
||||
.preview--hide
|
||||
@extend .preview
|
||||
z-index 0
|
||||
opacity 0
|
||||
pointer-events none
|
||||
|
||||
350
browser/components/MarkdownPreview.js
Normal file
350
browser/components/MarkdownPreview.js
Normal file
@@ -0,0 +1,350 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import markdown from 'browser/lib/markdown'
|
||||
import _ from 'lodash'
|
||||
import CodeMirror from 'codemirror'
|
||||
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'
|
||||
|
||||
function decodeHTMLEntities (text) {
|
||||
var entities = [
|
||||
['apos', '\''],
|
||||
['amp', '&'],
|
||||
['lt', '<'],
|
||||
['gt', '>']
|
||||
]
|
||||
|
||||
for (var i = 0, max = entities.length; i < max; ++i) {
|
||||
text = text.replace(new RegExp('&' + entities[i][0] + ';', 'g'), entities[i][1])
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { app } = remote
|
||||
const path = require('path')
|
||||
const dialog = remote.dialog
|
||||
|
||||
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
||||
const appPath = 'file://' + (process.env.NODE_ENV === 'production'
|
||||
? app.getAppPath()
|
||||
: path.resolve())
|
||||
|
||||
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber) {
|
||||
return `
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
src: url('${appPath}/resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
|
||||
url('${appPath}/resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
|
||||
url('${appPath}/resources/fonts/Lato-Regular.ttf') format('truetype');
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
${markdownStyle}
|
||||
body {
|
||||
font-family: ${fontFamily.join(', ')};
|
||||
font-size: ${fontSize}px;
|
||||
}
|
||||
code {
|
||||
font-family: ${codeBlockFontFamily.join(', ')};
|
||||
background-color: rgba(0,0,0,0.04);
|
||||
color: #CC305F;
|
||||
}
|
||||
.lineNumber {
|
||||
${lineNumber && 'display: block !important;'}
|
||||
font-family: ${codeBlockFontFamily.join(', ')};
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
border: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
padding-bottom: 4px;
|
||||
margin: 1em 0 8px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding-bottom: 0.2em;
|
||||
margin: 1em 0 0.37em;
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
const { shell } = require('electron')
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
|
||||
if (!OSX) {
|
||||
defaultFontFamily.unshift('\'Microsoft YaHei\'')
|
||||
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)
|
||||
|
||||
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.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
||||
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
||||
}
|
||||
|
||||
handlePreviewAnchorClick (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
let anchor = e.target.closest('a')
|
||||
let href = anchor.getAttribute('href')
|
||||
if (_.isString(href) && href.match(/^#/)) {
|
||||
let 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)
|
||||
}
|
||||
}
|
||||
|
||||
handleCheckboxClick (e) {
|
||||
this.props.onCheckboxClick(e)
|
||||
}
|
||||
|
||||
handleContextMenu (e) {
|
||||
this.props.onContextMenu(e)
|
||||
}
|
||||
|
||||
handleMouseDown (e) {
|
||||
if (e.target != null) {
|
||||
switch (e.target.tagName) {
|
||||
case 'A':
|
||||
case 'INPUT':
|
||||
return null
|
||||
}
|
||||
}
|
||||
if (this.props.onMouseDown != null) this.props.onMouseDown(e)
|
||||
}
|
||||
|
||||
handleMouseUp (e) {
|
||||
if (e.target != null && e.target.tagName === 'A') {
|
||||
return null
|
||||
}
|
||||
if (this.props.onMouseUp != null) this.props.onMouseUp(e)
|
||||
}
|
||||
|
||||
handleSaveAsText () {
|
||||
this.exportAsDocument('txt')
|
||||
}
|
||||
|
||||
handleSaveAsMd () {
|
||||
this.exportAsDocument('md')
|
||||
}
|
||||
|
||||
exportAsDocument (fileType) {
|
||||
const options = {
|
||||
filters: [
|
||||
{ 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
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.refs.root.setAttribute('sandbox', 'allow-scripts')
|
||||
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
|
||||
|
||||
this.refs.root.contentWindow.document.head.innerHTML = `
|
||||
<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">
|
||||
`
|
||||
this.rewriteIframe()
|
||||
this.applyStyle()
|
||||
|
||||
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
|
||||
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
||||
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
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)
|
||||
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
||||
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.value !== this.props.value) 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.theme !== this.props.theme) {
|
||||
this.applyStyle()
|
||||
this.rewriteIframe()
|
||||
}
|
||||
}
|
||||
|
||||
applyStyle () {
|
||||
let { fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme } = this.props
|
||||
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
||||
? [fontFamily].concat(defaultFontFamily)
|
||||
: defaultFontFamily
|
||||
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
|
||||
? [codeBlockFontFamily].concat(defaultCodeBlockFontFamily)
|
||||
: defaultCodeBlockFontFamily
|
||||
|
||||
this.setCodeTheme(codeBlockTheme)
|
||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
|
||||
}
|
||||
|
||||
setCodeTheme (theme) {
|
||||
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
|
||||
? theme
|
||||
: 'elegant'
|
||||
this.getWindow().document.getElementById('codeTheme').href = `${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)
|
||||
})
|
||||
|
||||
let { value, theme, indentSize, codeBlockTheme } = this.props
|
||||
|
||||
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
||||
this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value)
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
el.addEventListener('click', this.anchorClickHandler)
|
||||
})
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
||||
el.addEventListener('click', this.checkboxClickHandler)
|
||||
})
|
||||
|
||||
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)
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
CodeMirror.requireMode(syntax.mode, () => {
|
||||
let content = el.innerHTML
|
||||
el.innerHTML = ''
|
||||
el.parentNode.className += ` cm-s-${codeBlockTheme} CodeMirror`
|
||||
CodeMirror.runMode(content, syntax.mime, el, {
|
||||
tabSize: indentSize
|
||||
})
|
||||
})
|
||||
})
|
||||
let opts = {}
|
||||
// if (this.props.theme === 'dark') {
|
||||
// opts['font-color'] = '#DDD'
|
||||
// opts['line-color'] = '#DDD'
|
||||
// opts['element-color'] = '#DDD'
|
||||
// opts['fill'] = '#3A404C'
|
||||
// }
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.flowchart'), (el) => {
|
||||
Raphael.setWindow(this.getWindow())
|
||||
try {
|
||||
let diagram = flowchart.parse(decodeHTMLEntities(el.innerHTML))
|
||||
el.innerHTML = ''
|
||||
diagram.drawSVG(el, opts)
|
||||
_.forEach(el.querySelectorAll('a'), (el) => {
|
||||
el.addEventListener('click', this.anchorClickHandler)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
el.className = 'flowchart-error'
|
||||
el.innerHTML = 'Flowchart parse error: ' + e.message
|
||||
}
|
||||
})
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.sequence'), (el) => {
|
||||
Raphael.setWindow(this.getWindow())
|
||||
try {
|
||||
let diagram = SequenceDiagram.parse(decodeHTMLEntities(el.innerHTML))
|
||||
el.innerHTML = ''
|
||||
diagram.drawSVG(el, {theme: 'simple'})
|
||||
_.forEach(el.querySelectorAll('a'), (el) => {
|
||||
el.addEventListener('click', this.anchorClickHandler)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
el.className = 'sequence-error'
|
||||
el.innerHTML = 'Sequence diagram parse error: ' + e.message
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.refs.root.focus()
|
||||
}
|
||||
|
||||
getWindow () {
|
||||
return this.refs.root.contentWindow
|
||||
}
|
||||
|
||||
scrollTo (targetRow) {
|
||||
let blocks = this.getWindow().document.querySelectorAll('body>[data-line]')
|
||||
|
||||
for (let index = 0; index < blocks.length; index++) {
|
||||
let block = blocks[index]
|
||||
let row = parseInt(block.getAttribute('data-line'))
|
||||
if (row > targetRow || index === blocks.length - 1) {
|
||||
block = blocks[index - 1]
|
||||
block != null && this.getWindow().scrollTo(0, block.offsetTop)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { className, style, tabIndex } = this.props
|
||||
return (
|
||||
<iframe className={className != null
|
||||
? 'MarkdownPreview ' + className
|
||||
: 'MarkdownPreview'
|
||||
}
|
||||
style={style}
|
||||
tabIndex={tabIndex}
|
||||
ref='root'
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MarkdownPreview.propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
onDoubleClick: PropTypes.func,
|
||||
onMouseUp: PropTypes.func,
|
||||
onMouseDown: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.string
|
||||
}
|
||||
100
browser/components/NoteItem.js
Normal file
100
browser/components/NoteItem.js
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @fileoverview Note item component.
|
||||
*/
|
||||
import React, { PropTypes } from 'react'
|
||||
import { isArray } from 'lodash'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './NoteItem.styl'
|
||||
|
||||
/**
|
||||
* @description Tag element component.
|
||||
* @param {string} tagName
|
||||
* @return {React.Component}
|
||||
*/
|
||||
const TagElement = ({ tagName }) => (
|
||||
<span styleName='item-bottom-tagList-item' key={tagName}>
|
||||
{tagName}
|
||||
</span>
|
||||
)
|
||||
|
||||
/**
|
||||
* @description Tag element list component.
|
||||
* @param {Array|null} tags
|
||||
* @return {React.Component}
|
||||
*/
|
||||
const TagElementList = (tags) => {
|
||||
if (!isArray(tags)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const tagElements = tags.map(tag => (
|
||||
TagElement({tagName: tag})
|
||||
))
|
||||
|
||||
return tagElements
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Note item component when using normal display mode.
|
||||
* @param {boolean} isActive
|
||||
* @param {Object} note
|
||||
* @param {Function} handleNoteClick
|
||||
* @param {Function} handleNoteContextMenu
|
||||
* @param {string} dateDisplay
|
||||
*/
|
||||
const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteContextMenu }) => (
|
||||
<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}`)}
|
||||
>
|
||||
<div styleName='item-wrapper'>
|
||||
<div styleName='item-bottom-time'>{dateDisplay}</div>
|
||||
|
||||
<div styleName='item-title'>
|
||||
{note.title.trim().length > 0
|
||||
? note.title
|
||||
: <span styleName='item-title-empty'>Empty</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div styleName='item-bottom'>
|
||||
<div styleName='item-bottom-tagList'>
|
||||
{note.tags.length > 0
|
||||
? TagElementList(note.tags)
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{note.type === 'SNIPPET_NOTE'
|
||||
? <i styleName='item-title-icon' className='fa fa-fw fa-code' />
|
||||
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />
|
||||
}
|
||||
|
||||
{note.isStarred
|
||||
? <i styleName='item-star' className='fa fa-star' /> : ''
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
NoteItem.propTypes = {
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
dateDisplay: PropTypes.string.isRequired,
|
||||
note: PropTypes.shape({
|
||||
storage: PropTypes.string.isRequired,
|
||||
key: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isrequired,
|
||||
tags: PropTypes.array,
|
||||
isStarred: PropTypes.bool.isRequired
|
||||
}),
|
||||
handleNoteClick: PropTypes.func.isRequired,
|
||||
handleNoteContextMenu: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(NoteItem, styles)
|
||||
168
browser/components/NoteItem.styl
Normal file
168
browser/components/NoteItem.styl
Normal file
@@ -0,0 +1,168 @@
|
||||
$control-height = 30px
|
||||
|
||||
.root
|
||||
absolute left bottom
|
||||
top $topBar-height - 1
|
||||
background-color $ui-noteList-backgroundColor
|
||||
|
||||
.item
|
||||
position relative
|
||||
padding 0 25px
|
||||
user-select none
|
||||
cursor pointer
|
||||
background-color $ui-noteList-backgroundColor
|
||||
transition background-color 0.15s
|
||||
&:hover
|
||||
background-color alpha($ui-active-color, 20%)
|
||||
&:active
|
||||
background-color $ui-active-color
|
||||
color white
|
||||
.item-title
|
||||
.item-title-empty
|
||||
.item-bottom-tagList-empty
|
||||
.item-bottom-time
|
||||
.item-title-icon
|
||||
color white
|
||||
.item-bottom-tagList-item
|
||||
background-color transparent
|
||||
color white
|
||||
|
||||
.item-wrapper
|
||||
padding 20px 0
|
||||
border-bottom $ui-border
|
||||
|
||||
.item--active
|
||||
@extend .item
|
||||
background-color $ui-active-color
|
||||
color white
|
||||
.item-title
|
||||
.item-title-empty
|
||||
.item-bottom-tagList-empty
|
||||
.item-bottom-time
|
||||
.item-title-icon
|
||||
color white
|
||||
.item-bottom-tagList-item
|
||||
background-color transparent
|
||||
color white
|
||||
.item-wrapper
|
||||
border-color transparent
|
||||
&:hover
|
||||
background-color $ui-active-color
|
||||
|
||||
.item-title
|
||||
font-size 14px
|
||||
height 40px
|
||||
box-sizing border-box
|
||||
line-height 24px
|
||||
padding-top 5px
|
||||
padding-bottom 20px
|
||||
overflow ellipsis
|
||||
color $ui-text-color
|
||||
|
||||
.item-title-icon
|
||||
position absolute
|
||||
top 20px
|
||||
right 25px
|
||||
font-size 14px
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-title-empty
|
||||
font-weight normal
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-bottom
|
||||
position relative
|
||||
bottom 0px
|
||||
margin-top 2px
|
||||
height 20px
|
||||
font-size 12px
|
||||
line-height 20px
|
||||
overflow ellipsis
|
||||
display flex
|
||||
|
||||
.item-bottom-tagList
|
||||
flex 1
|
||||
overflow ellipsis
|
||||
line-height 20px
|
||||
color #FFFFFF
|
||||
|
||||
.item-bottom-tagList-item
|
||||
font-size 12px
|
||||
margin-right 8px
|
||||
padding 0 10px
|
||||
height 20px
|
||||
box-sizing border-box
|
||||
border-radius 20px
|
||||
vertical-align middle
|
||||
background-color $ui-tag-backgroundColor
|
||||
color #FFFFFF
|
||||
|
||||
.item-bottom-tagList-empty
|
||||
color $ui-inactive-text-color
|
||||
vertical-align middle
|
||||
font-size 10px
|
||||
margin-left 5px
|
||||
|
||||
.item-bottom-time
|
||||
color $ui-inactive-text-color
|
||||
font-size 12px
|
||||
|
||||
.item-star
|
||||
position absolute
|
||||
top 20px
|
||||
right 29px
|
||||
width 34px
|
||||
height 34px
|
||||
color $ui-favorite-star-button-color
|
||||
font-size 14px
|
||||
padding 0
|
||||
border-radius 17px
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
.item
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
&:active
|
||||
background-color $ui-active-color
|
||||
&:hover
|
||||
background-color alpha($ui-active-color, 20%)
|
||||
|
||||
.item-wrapper
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.item--active
|
||||
@extend .item
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-active-color
|
||||
.item-wrapper
|
||||
border-color transparent
|
||||
.item-title
|
||||
color white
|
||||
.item-bottom-tagList-item
|
||||
background-color transparent
|
||||
color white
|
||||
.item-bottom-tagList-empty
|
||||
color white
|
||||
&:hover
|
||||
background-color $ui-active-color
|
||||
|
||||
.item-title
|
||||
color $ui-dark-text-color
|
||||
|
||||
.item-title-icon
|
||||
color $ui-darkinactive-text-color
|
||||
|
||||
.item-title-empty
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
.item-bottom-tagList-item
|
||||
background-color $ui-dark-tag-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
.item-bottom-tagList-empty
|
||||
color $ui-inactive-text-color
|
||||
vertical-align middle
|
||||
49
browser/components/NoteItemSimple.js
Normal file
49
browser/components/NoteItemSimple.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @fileoverview Note item component with simple display mode.
|
||||
*/
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './NoteItemSimple.styl'
|
||||
|
||||
/**
|
||||
* @description Note item component when using simple display mode.
|
||||
* @param {boolean} isActive
|
||||
* @param {Object} note
|
||||
* @param {Function} handleNoteClick
|
||||
* @param {Function} handleNoteContextMenu
|
||||
*/
|
||||
const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu }) => (
|
||||
<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}`)}
|
||||
>
|
||||
<div styleName='item-simple-title'>
|
||||
{note.type === 'SNIPPET_NOTE'
|
||||
? <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.title.trim().length > 0
|
||||
? note.title
|
||||
: <span styleName='item-simple-title-empty'>Empty</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
NoteItemSimple.propTypes = {
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
note: PropTypes.shape({
|
||||
storage: PropTypes.string.isRequired,
|
||||
key: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isrequired
|
||||
}),
|
||||
handleNoteClick: PropTypes.func.isRequired,
|
||||
handleNoteContextMenu: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(NoteItemSimple, styles)
|
||||
89
browser/components/NoteItemSimple.styl
Normal file
89
browser/components/NoteItemSimple.styl
Normal file
@@ -0,0 +1,89 @@
|
||||
$control-height = 30px
|
||||
|
||||
.root
|
||||
absolute left bottom
|
||||
top $topBar-height - 1
|
||||
background-color $ui-noteList-backgroundColor
|
||||
|
||||
.item-simple
|
||||
position relative
|
||||
padding 0 25px
|
||||
user-select none
|
||||
cursor pointer
|
||||
background-color $ui-noteList-backgroundColor
|
||||
transition background-color 0.15s
|
||||
&:hover
|
||||
background-color alpha($ui-active-color, 20%)
|
||||
&:active
|
||||
background-color $ui-active-color
|
||||
color white
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
color white
|
||||
|
||||
.item-simple--active
|
||||
@extend .item-simple
|
||||
background-color $ui-active-color
|
||||
color white
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
border-color transparent
|
||||
color white
|
||||
.item-simple-title-icon
|
||||
color white
|
||||
&:hover
|
||||
background-color $ui-active-color
|
||||
|
||||
.item-simple-title
|
||||
font-size 14px
|
||||
height 40px
|
||||
box-sizing border-box
|
||||
line-height 24px
|
||||
padding-top 8px
|
||||
overflow ellipsis
|
||||
color $ui-text-color
|
||||
border-bottom $ui-border
|
||||
|
||||
.item-simple-title-icon
|
||||
font-size 12px
|
||||
color $ui-inactive-text-color
|
||||
padding-right 6px
|
||||
|
||||
.item-simple-title-empty
|
||||
font-weight normal
|
||||
color $ui-inactive-text-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
.item-simple
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
&:active
|
||||
background-color $ui-active-color
|
||||
&:hover
|
||||
background-color alpha($ui-active-color, 20%)
|
||||
|
||||
.item-simple--active
|
||||
@extend .item-simple
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-active-color
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
color white
|
||||
border-color transparent
|
||||
&:hover
|
||||
background-color $ui-active-color
|
||||
|
||||
.item-simple-title
|
||||
color $ui-dark-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.item-simple-title-icon
|
||||
color $ui-darkinactive-text-color
|
||||
|
||||
.item-simple-title-empty
|
||||
color $ui-dark-inactive-text-color
|
||||
24
browser/components/ProfileImage.js
Normal file
24
browser/components/ProfileImage.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import md5 from 'md5'
|
||||
|
||||
export default class ProfileImage extends React.Component {
|
||||
render () {
|
||||
let className = this.props.className == null ? 'ProfileImage' : 'ProfileImage ' + this.props.className
|
||||
let email = this.props.email != null ? this.props.email : ''
|
||||
let src = 'http://www.gravatar.com/avatar/' + md5(email.trim().toLowerCase()) + '?s=' + this.props.size
|
||||
|
||||
return (
|
||||
<img
|
||||
className={className}
|
||||
width={this.props.size}
|
||||
height={this.props.size}
|
||||
src={src} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ProfileImage.propTypes = {
|
||||
email: PropTypes.string,
|
||||
size: PropTypes.string,
|
||||
className: PropTypes.string
|
||||
}
|
||||
44
browser/components/SideNavFilter.js
Normal file
44
browser/components/SideNavFilter.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @fileoverview Filter for all notes.
|
||||
*/
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './SideNavFilter.styl'
|
||||
|
||||
/**
|
||||
* @param {boolean} isFolded
|
||||
* @param {boolean} isHomeActive
|
||||
* @param {Function} handleAllNotesButtonClick
|
||||
* @param {boolean} isStarredActive
|
||||
* @param {Function} handleStarredButtonClick
|
||||
* @return {React.Component}
|
||||
*/
|
||||
const SideNavFilter = ({
|
||||
isFolded, isHomeActive, handleAllNotesButtonClick,
|
||||
isStarredActive, handleStarredButtonClick
|
||||
}) => (
|
||||
<div styleName={isFolded ? 'menu--folded' : 'menu'}>
|
||||
<button styleName={isHomeActive ? 'menu-button--active' : 'menu-button'}
|
||||
onClick={handleAllNotesButtonClick}
|
||||
>
|
||||
<i className='fa fa-book fa-fw' />
|
||||
<span styleName='menu-button-label'>All Notes</span>
|
||||
</button>
|
||||
<button styleName={isStarredActive ? 'menu-button--active' : 'menu-button'}
|
||||
onClick={handleStarredButtonClick}
|
||||
>
|
||||
<i className='fa fa-star fa-fw' />
|
||||
<span styleName='menu-button-label'>Starred</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
SideNavFilter.propTypes = {
|
||||
isFolded: PropTypes.bool,
|
||||
isHomeActive: PropTypes.bool.isRequired,
|
||||
handleAllNotesButtonClick: PropTypes.func.isRequired,
|
||||
isStarredActive: PropTypes.bool.isRequired,
|
||||
handleStarredButtonClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(SideNavFilter, styles)
|
||||
58
browser/components/SideNavFilter.styl
Normal file
58
browser/components/SideNavFilter.styl
Normal file
@@ -0,0 +1,58 @@
|
||||
.menu
|
||||
margin-bottom 30px
|
||||
|
||||
.menu-button
|
||||
navButtonColor()
|
||||
height 32px
|
||||
padding 0 15px
|
||||
font-size 14px
|
||||
width 100%
|
||||
text-align left
|
||||
overflow ellipsis
|
||||
|
||||
.menu-button--active
|
||||
@extend .menu-button
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
&:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.menu-button-label
|
||||
margin-left 5px
|
||||
|
||||
.menu--folded
|
||||
@extend .menu
|
||||
.menu-button, .menu-button--active
|
||||
text-align center
|
||||
&:hover .menu-button-label
|
||||
transition opacity 0.15s
|
||||
opacity 1
|
||||
.menu-button-label
|
||||
position fixed
|
||||
display inline-block
|
||||
height 32px
|
||||
left 44px
|
||||
padding 0 10px
|
||||
margin-top -8px
|
||||
margin-left 0
|
||||
overflow ellipsis
|
||||
background-color $ui-tooltip-backgroundColor
|
||||
z-index 10
|
||||
color white
|
||||
line-height 32px
|
||||
border-top-right-radius 2px
|
||||
border-bottom-right-radius 2px
|
||||
pointer-events none
|
||||
opacity 0
|
||||
font-size 12px
|
||||
|
||||
body[data-theme="dark"]
|
||||
.menu-button
|
||||
navDarkButtonColor()
|
||||
|
||||
.menu-button--active
|
||||
@extend .menu-button
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-button--active-color
|
||||
&:hover
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
131
browser/components/SnippetTab.js
Normal file
131
browser/components/SnippetTab.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './SnippetTab.styl'
|
||||
import context from 'browser/lib/context'
|
||||
|
||||
class SnippetTab extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isRenaming: false,
|
||||
name: props.snippet.name
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUpdate (nextProps) {
|
||||
if (nextProps.snippet.name !== this.props.snippet.name) {
|
||||
this.setState({
|
||||
name: nextProps.snippet.name
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleClick (e) {
|
||||
this.props.onClick(e)
|
||||
}
|
||||
|
||||
handleContextMenu (e) {
|
||||
context.popup([
|
||||
{
|
||||
label: 'Rename',
|
||||
click: (e) => this.handleRenameClick(e)
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
handleRenameClick (e) {
|
||||
this.startRenaming()
|
||||
}
|
||||
|
||||
handleNameInputBlur (e) {
|
||||
this.handleRename()
|
||||
}
|
||||
|
||||
handleNameInputChange (e) {
|
||||
this.setState({
|
||||
name: e.target.value
|
||||
})
|
||||
}
|
||||
|
||||
handleNameInputKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
case 13:
|
||||
this.handleRename()
|
||||
break
|
||||
case 27:
|
||||
this.setState({
|
||||
name: this.props.snippet.name,
|
||||
isRenaming: false
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
handleRename () {
|
||||
this.setState({
|
||||
isRenaming: false
|
||||
}, () => {
|
||||
if (this.props.snippet.name !== this.state.name) {
|
||||
this.props.onRename(this.state.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleDeleteButtonClick (e) {
|
||||
this.props.onDelete(e)
|
||||
}
|
||||
|
||||
startRenaming () {
|
||||
this.setState({
|
||||
isRenaming: true
|
||||
}, () => {
|
||||
this.refs.name.focus()
|
||||
this.refs.name.select()
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
let { isActive, snippet, isDeletable } = this.props
|
||||
return (
|
||||
<div styleName={isActive
|
||||
? 'root--active'
|
||||
: 'root'
|
||||
}
|
||||
>
|
||||
{!this.state.isRenaming
|
||||
? <button styleName='button'
|
||||
onClick={(e) => this.handleClick(e)}
|
||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||
>
|
||||
{snippet.name.trim().length > 0
|
||||
? snippet.name
|
||||
: <span styleName='button-unnamed'>
|
||||
Unnamed
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
: <input styleName='input'
|
||||
ref='name'
|
||||
value={this.state.name}
|
||||
onChange={(e) => this.handleNameInputChange(e)}
|
||||
onBlur={(e) => this.handleNameInputBlur(e)}
|
||||
onKeyDown={(e) => this.handleNameInputKeyDown(e)}
|
||||
/>
|
||||
}
|
||||
{isDeletable &&
|
||||
<button styleName='deleteButton'
|
||||
onClick={(e) => this.handleDeleteButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-times' />
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SnippetTab.propTypes = {
|
||||
}
|
||||
|
||||
export default CSSModules(SnippetTab, styles)
|
||||
91
browser/components/SnippetTab.styl
Normal file
91
browser/components/SnippetTab.styl
Normal file
@@ -0,0 +1,91 @@
|
||||
.root
|
||||
position relative
|
||||
flex 1
|
||||
overflow hidden
|
||||
&:hover
|
||||
.deleteButton
|
||||
color $ui-inactive-text-color
|
||||
&:hover
|
||||
background-color darken($ui-backgroundColor, 15%)
|
||||
&:active
|
||||
color white
|
||||
background-color $ui-active-color
|
||||
|
||||
.root--active
|
||||
@extend .root
|
||||
min-width 100px
|
||||
border-bottom $ui-border
|
||||
|
||||
.button
|
||||
width 100%
|
||||
height 29px
|
||||
overflow ellipsis
|
||||
text-align left
|
||||
padding-right 30px
|
||||
border none
|
||||
background-color transparent
|
||||
transition 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
|
||||
.deleteButton
|
||||
position absolute
|
||||
top 5px
|
||||
height 20px
|
||||
right 5px
|
||||
width 20px
|
||||
text-align center
|
||||
border none
|
||||
padding 0
|
||||
color transparent
|
||||
background-color transparent
|
||||
border-radius 2px
|
||||
|
||||
.input
|
||||
height 29px
|
||||
border $ui-active-color
|
||||
padding 0 5px
|
||||
width 100%
|
||||
outline none
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
color $ui-dark-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
|
||||
&:active
|
||||
color white
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.root--active
|
||||
color $ui-dark-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
|
||||
&:active
|
||||
color white
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.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
|
||||
|
||||
.input
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
color white
|
||||
58
browser/components/StorageItem.js
Normal file
58
browser/components/StorageItem.js
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @fileoverview Micro component for showing storage.
|
||||
*/
|
||||
import React, { PropTypes } from 'react'
|
||||
import styles from './StorageItem.styl'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import { isNumber } from 'lodash'
|
||||
|
||||
/**
|
||||
* @param {boolean} isActive
|
||||
* @param {Function} handleButtonClick
|
||||
* @param {Function} handleContextMenu
|
||||
* @param {string} folderName
|
||||
* @param {string} folderColor
|
||||
* @param {boolean} isFolded
|
||||
* @param {number} noteCount
|
||||
* @return {React.Component}
|
||||
*/
|
||||
const StorageItem = ({
|
||||
isActive, handleButtonClick, handleContextMenu, folderName,
|
||||
folderColor, isFolded, noteCount
|
||||
}) => (
|
||||
<button styleName={isActive
|
||||
? 'folderList-item--active'
|
||||
: 'folderList-item'
|
||||
}
|
||||
onClick={handleButtonClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<span styleName={isFolded
|
||||
? 'folderList-item-name--folded' : 'folderList-item-name'
|
||||
}
|
||||
style={{borderColor: folderColor}}
|
||||
>
|
||||
{isFolded ? folderName.substring(0, 1) : folderName}
|
||||
</span>
|
||||
{(!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,
|
||||
handleButtonClick: PropTypes.func,
|
||||
handleContextMenu: PropTypes.func,
|
||||
folderName: PropTypes.string.isRequired,
|
||||
folderColor: PropTypes.string,
|
||||
isFolded: PropTypes.bool.isRequired,
|
||||
noteCount: PropTypes.number
|
||||
}
|
||||
|
||||
export default CSSModules(StorageItem, styles)
|
||||
70
browser/components/StorageItem.styl
Normal file
70
browser/components/StorageItem.styl
Normal file
@@ -0,0 +1,70 @@
|
||||
.root
|
||||
width 100%
|
||||
user-select none
|
||||
|
||||
.folderList-item
|
||||
display flex
|
||||
width 100%
|
||||
height 26px
|
||||
background-color transparent
|
||||
color $ui-inactive-text-color
|
||||
padding 0
|
||||
margin-bottom 5px
|
||||
text-align left
|
||||
border none
|
||||
overflow ellipsis
|
||||
font-size 14px
|
||||
&:first-child
|
||||
margin-top 0
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
&:active
|
||||
color $ui-button--active-color
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.folderList-item--active
|
||||
@extend .folderList-item
|
||||
color $ui-button--active-color
|
||||
background-color $ui-button--active-backgroundColor
|
||||
&:hover
|
||||
color $ui-button--active-color
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.folderList-item-name
|
||||
display block
|
||||
flex 1
|
||||
padding 0 30px
|
||||
height 26px
|
||||
line-height 26px
|
||||
border-width 0 0 0 3px
|
||||
border-style solid
|
||||
border-color transparent
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
|
||||
.folderList-item-noteCount
|
||||
float right
|
||||
line-height 26px
|
||||
padding-right 15px
|
||||
font-size 12px
|
||||
|
||||
.folderList-item-tooltip
|
||||
tooltip()
|
||||
position fixed
|
||||
padding 0 10px
|
||||
left 44px
|
||||
z-index 10
|
||||
pointer-events none
|
||||
opacity 0
|
||||
border-top-right-radius 2px
|
||||
border-bottom-right-radius 2px
|
||||
height 26px
|
||||
line-height 26px
|
||||
|
||||
.folderList-item:hover, .folderList-item--active:hover
|
||||
.folderList-item-tooltip
|
||||
opacity 1
|
||||
|
||||
.folderList-item-name--folded
|
||||
@extend .folderList-item-name
|
||||
padding-left 14px
|
||||
319
browser/components/markdown.styl
Normal file
319
browser/components/markdown.styl
Normal file
@@ -0,0 +1,319 @@
|
||||
global-reset()
|
||||
|
||||
borderColor = #D0D0D0 // using
|
||||
highlightenBorderColor = darken(borderColor, 20%)
|
||||
invBorderColor = #404849
|
||||
brandBorderColor = #3FB399
|
||||
|
||||
focusBorderColor = #369DCD
|
||||
|
||||
buttonBorderColor = #4C4C4C
|
||||
|
||||
lightButtonColor = #898989
|
||||
|
||||
hoverBackgroundColor= transparentify(#444, 4%) // using
|
||||
|
||||
inactiveTextColor = #888 // using
|
||||
textColor = #4D4D4D // using
|
||||
backgroundColor= white
|
||||
fontSize= 14px // using
|
||||
|
||||
shadowColor= #C5C5C5
|
||||
|
||||
invBackgroundColor = #4C4C4C
|
||||
invTextColor = white
|
||||
|
||||
btnColor = #888
|
||||
btnHighlightenColor = #000
|
||||
|
||||
brandColor = #2BAC8F
|
||||
|
||||
popupShadow = 0 0 5px 0 #888
|
||||
|
||||
|
||||
tableHeadBgColor = white
|
||||
tableOddBgColor = #F9F9F9
|
||||
tableEvenBgColor = white
|
||||
|
||||
facebookColor= #3b5998
|
||||
githubBtn= #201F1F
|
||||
|
||||
// using
|
||||
successBackgroundColor= #E0F0D9
|
||||
successTextColor= #3E753F
|
||||
errorBackgroundColor= #F2DEDE
|
||||
errorTextColor= #A64444
|
||||
infoBackgroundColor= #D9EDF7
|
||||
infoTextColor= #34708E
|
||||
|
||||
popupZIndex= 500
|
||||
|
||||
body
|
||||
font-size 16px
|
||||
padding 15px
|
||||
font-family helvetica, arial, sans-serif
|
||||
line-height 1.6
|
||||
overflow-x hidden
|
||||
user-select all
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
.katex
|
||||
font 400 1.2em 'KaTeX_Main'
|
||||
line-height 1.2em
|
||||
white-space nowrap
|
||||
text-indent 0
|
||||
.katex .mfrac>.vlist>span:nth-child(2)
|
||||
top 0 !important
|
||||
.katex-error
|
||||
background-color errorBackgroundColor
|
||||
color errorTextColor
|
||||
padding 5px
|
||||
margin -5px
|
||||
border-radius 5px
|
||||
.flowchart-error, .sequence-error
|
||||
background-color errorBackgroundColor
|
||||
color errorTextColor
|
||||
padding 5px
|
||||
border-radius 5px
|
||||
justify-content left
|
||||
li
|
||||
label.taskListItem
|
||||
margin-left -2em
|
||||
background-color white
|
||||
div.math-rendered
|
||||
text-align center
|
||||
.math-failed
|
||||
background-color alpha(red, 0.1)
|
||||
color darken(red, 15%)
|
||||
padding 5px
|
||||
margin 5px 0
|
||||
border-radius 5px
|
||||
sup
|
||||
position relative
|
||||
top -.4em
|
||||
font-size 0.8em
|
||||
vertical-align top
|
||||
sub
|
||||
position relative
|
||||
bottom -.4em
|
||||
font-size 0.8em
|
||||
vertical-align top
|
||||
a
|
||||
color brandColor
|
||||
text-decoration none
|
||||
padding 5px
|
||||
border-radius 5px
|
||||
margin -5px
|
||||
transition .1s
|
||||
display inline-block
|
||||
img
|
||||
vertical-align sub
|
||||
&:hover
|
||||
color lighten(brandColor, 5%)
|
||||
text-decoration underline
|
||||
background-color alpha(#FFC95C, 0.3)
|
||||
&:visited
|
||||
color brandColor
|
||||
hr
|
||||
border-top none
|
||||
border-bottom solid 1px borderColor
|
||||
margin 15px 0
|
||||
h1, h2, h3, h4, h5, h6
|
||||
font-weight bold
|
||||
h1
|
||||
font-size 2.25em
|
||||
padding-bottom 0.3em
|
||||
line-height 1.2em
|
||||
border-bottom solid 1px borderColor
|
||||
margin 1em 0 0.44em
|
||||
&:first-child
|
||||
margin-top 0
|
||||
h2
|
||||
font-size 1.75em
|
||||
padding-bottom 0.3em
|
||||
line-height 1.225em
|
||||
border-bottom solid 1px borderColor
|
||||
margin 1em 0 0.57em
|
||||
&:first-child
|
||||
margin-top 0
|
||||
h3
|
||||
font-size 1.5em
|
||||
line-height 1.43em
|
||||
margin 1em 0 0.66em
|
||||
h4
|
||||
font-size 1.25em
|
||||
line-height 1.4em
|
||||
margin 1em 0 0.8em
|
||||
h5
|
||||
font-size 1em
|
||||
line-height 1.4em
|
||||
margin 1em 0 1em
|
||||
h6
|
||||
font-size 1em
|
||||
line-height 1.4em
|
||||
margin 1em 0 1em
|
||||
color #777
|
||||
p
|
||||
line-height 1.6em
|
||||
margin 0 0 1em
|
||||
white-space pre-line
|
||||
img
|
||||
max-width 100%
|
||||
strong, b
|
||||
font-weight bold
|
||||
em, i
|
||||
font-style italic
|
||||
s, del, strike
|
||||
text-decoration line-through
|
||||
u
|
||||
text-decoration underline
|
||||
blockquote
|
||||
border-left solid 4px brandBorderColor
|
||||
margin 0 0 1em
|
||||
padding 0 25px
|
||||
ul
|
||||
list-style-type disc
|
||||
padding-left 2em
|
||||
margin-bottom 1em
|
||||
li
|
||||
display list-item
|
||||
p
|
||||
margin 0
|
||||
&>li>ul, &>li>ol
|
||||
margin 0
|
||||
&>li>ul
|
||||
list-style-type circle
|
||||
&>li>ul
|
||||
list-style-type square
|
||||
ol
|
||||
list-style-type decimal
|
||||
padding-left 2em
|
||||
margin-bottom 1em
|
||||
li
|
||||
display list-item
|
||||
p
|
||||
margin 0
|
||||
&>li>ul, &>li>ol
|
||||
margin 0
|
||||
code
|
||||
padding 0.2em 0.4em
|
||||
background-color #f7f7f7
|
||||
border-radius 3px
|
||||
font-size 1em
|
||||
text-decoration none
|
||||
margin-right 2px
|
||||
pre
|
||||
padding 0.5em !important
|
||||
border solid 1px #D1D1D1
|
||||
border-radius 5px
|
||||
overflow-x auto
|
||||
margin 0 0 1em
|
||||
display flex
|
||||
line-height 1.4em
|
||||
&.flowchart, &.sequence
|
||||
display flex
|
||||
justify-content center
|
||||
background-color white
|
||||
&.CodeMirror
|
||||
height initial
|
||||
&>code
|
||||
flex 1
|
||||
overflow-x auto
|
||||
code
|
||||
background-color inherit
|
||||
margin 0
|
||||
padding 0
|
||||
border none
|
||||
border-radius 0
|
||||
&>span.lineNumber
|
||||
display none
|
||||
font-size 1em
|
||||
padding 0.5em 0
|
||||
margin -0.5em 0.5em -0.5em -0.5em
|
||||
border-right 1px solid
|
||||
text-align right
|
||||
border-top-left-radius 4px
|
||||
border-bottom-left-radius 4px
|
||||
&.CodeMirror-gutters
|
||||
position initial
|
||||
top initial
|
||||
left initial
|
||||
min-height 0 !important
|
||||
&>span
|
||||
display block
|
||||
padding 0 .5em 0
|
||||
table
|
||||
display block
|
||||
width 100%
|
||||
margin 0 0 1em
|
||||
thead
|
||||
tr
|
||||
background-color tableHeadBgColor
|
||||
th
|
||||
border-style solid
|
||||
padding 6px 13px
|
||||
line-height 1.6
|
||||
border-width 1px 0 2px 1px
|
||||
border-color borderColor
|
||||
font-weight bold
|
||||
&:last-child
|
||||
border-right solid 1px borderColor
|
||||
tbody
|
||||
tr:nth-child(2n + 1)
|
||||
background-color tableOddBgColor
|
||||
tr:nth-child(2n)
|
||||
background-color tableEvenBgColor
|
||||
td
|
||||
border-style solid
|
||||
padding 6px 13px
|
||||
line-height 1.6
|
||||
border-width 0 0 1px 1px
|
||||
border-color borderColor
|
||||
&:last-child
|
||||
border-right solid 1px borderColor
|
||||
|
||||
themeDarkBackground = darken(#21252B, 10%)
|
||||
themeDarkText = #DDDDDD
|
||||
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
||||
themeDarkPreview = $ui-dark-noteDetail-backgroundColor
|
||||
themeDarkTableOdd = themeDarkPreview
|
||||
themeDarkTableEven = darken(themeDarkPreview, 10%)
|
||||
themeDarkTableHead = themeDarkTableEven
|
||||
themeDarkTableBorder = themeDarkBorder
|
||||
themeDarkModalButtonDefault = themeDarkPreview
|
||||
themeDarkModalButtonDanger = #BF360C
|
||||
|
||||
body[data-theme="dark"]
|
||||
color themeDarkText
|
||||
border-color themeDarkBorder
|
||||
background-color themeDarkPreview
|
||||
a:hover
|
||||
background-color alpha(lighten(brandColor, 30%), 0.2) !important
|
||||
|
||||
code
|
||||
border-color darken(themeDarkBorder, 10%)
|
||||
background-color lighten(themeDarkPreview, 10%)
|
||||
|
||||
pre
|
||||
border-color lighten(#21252B, 20%)
|
||||
code
|
||||
background-color transparent
|
||||
|
||||
label.taskListItem
|
||||
background-color themeDarkPreview
|
||||
table
|
||||
thead
|
||||
tr
|
||||
background-color themeDarkTableHead
|
||||
th
|
||||
border-color themeDarkTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeDarkTableBorder
|
||||
tbody
|
||||
tr:nth-child(2n + 1)
|
||||
background-color themeDarkTableOdd
|
||||
tr:nth-child(2n)
|
||||
background-color themeDarkTableEven
|
||||
td
|
||||
border-color themeDarkTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeDarkTableBorder
|
||||
118
browser/finder/FinderMain.styl
Normal file
118
browser/finder/FinderMain.styl
Normal file
@@ -0,0 +1,118 @@
|
||||
$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 80px + 32px + 10px + 10px
|
||||
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
|
||||
203
browser/finder/NoteDetail.js
Normal file
203
browser/finder/NoteDetail.js
Normal file
@@ -0,0 +1,203 @@
|
||||
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'
|
||||
|
||||
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 () {
|
||||
let { 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 () {
|
||||
let { note } = this.props
|
||||
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
|
||||
this.setState({
|
||||
snippetIndex: (this.state.snippetIndex + 1) % note.snippets.length
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
saveToClipboard () {
|
||||
let { 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 () {
|
||||
let { 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
|
||||
|
||||
if (note.type === 'SNIPPET_NOTE') {
|
||||
let tabList = note.snippets.map((snippet, index) => {
|
||||
let 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>
|
||||
})
|
||||
|
||||
let viewList = note.snippets.map((snippet, index) => {
|
||||
let 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}
|
||||
/>
|
||||
: <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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NoteDetail.propTypes = {
|
||||
}
|
||||
|
||||
export default CSSModules(NoteDetail, styles)
|
||||
97
browser/finder/NoteDetail.styl
Normal file
97
browser/finder/NoteDetail.styl
Normal file
@@ -0,0 +1,97 @@
|
||||
@import('../main/Detail/DetailVars.styl')
|
||||
|
||||
.root
|
||||
absolute top bottom left right
|
||||
left $note-detail-left-margin
|
||||
right $note-detail-right-margin
|
||||
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
|
||||
91
browser/finder/NoteList.js
Normal file
91
browser/finder/NoteList.js
Normal file
@@ -0,0 +1,91 @@
|
||||
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 () {
|
||||
let { index } = this.props
|
||||
|
||||
if (index > -1) {
|
||||
let list = this.refs.root
|
||||
let item = list.childNodes[index]
|
||||
if (item == null) return null
|
||||
|
||||
let overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
|
||||
if (overflowBelow) {
|
||||
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
|
||||
}
|
||||
let overflowAbove = list.scrollTop > item.offsetTop
|
||||
if (overflowAbove) {
|
||||
list.scrollTop = item.offsetTop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetScroll () {
|
||||
this.refs.root.scrollTop = 0
|
||||
this.setState({
|
||||
range: 0
|
||||
})
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
let { 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 () {
|
||||
let { notes, index } = this.props
|
||||
|
||||
let 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)}
|
||||
handleNoteContextMenu={() => ''}
|
||||
/>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<div className={this.props.className}
|
||||
onScroll={(e) => this.handleScroll(e)}
|
||||
ref='root'
|
||||
>
|
||||
{notesList}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NoteList.propTypes = {
|
||||
}
|
||||
|
||||
export default NoteList
|
||||
77
browser/finder/StorageSection.js
Normal file
77
browser/finder/StorageSection.js
Normal file
@@ -0,0 +1,77 @@
|
||||
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) {
|
||||
let { storage } = this.props
|
||||
this.props.handleStorageButtonClick(e, storage.key)
|
||||
}
|
||||
|
||||
handleFolderClick (e, folder) {
|
||||
let { storage } = this.props
|
||||
this.props.handleFolderButtonClick(e, storage.key, folder.key)
|
||||
}
|
||||
|
||||
render () {
|
||||
let { storage, filter } = this.props
|
||||
let 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)
|
||||
85
browser/finder/StorageSection.styl
Normal file
85
browser/finder/StorageSection.styl
Normal file
@@ -0,0 +1,85 @@
|
||||
.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
|
||||
354
browser/finder/index.js
Normal file
354
browser/finder/index.js
Normal file
@@ -0,0 +1,354 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { connect, Provider } from 'react-redux'
|
||||
import _ from 'lodash'
|
||||
import ipc from './ipcClient'
|
||||
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'
|
||||
require('!!style!css!stylus?sourceMap!../main/global.styl')
|
||||
require('../lib/customMeta')
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
const { Menu } = remote
|
||||
|
||||
function hideFinder () {
|
||||
let 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()
|
||||
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) {
|
||||
let { filter } = this.state
|
||||
filter.includeSnippet = e.target.checked
|
||||
this.setState({
|
||||
filter: filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleOnlyMarkdownCheckboxChange (e) {
|
||||
let { filter } = this.state
|
||||
filter.includeMarkdown = e.target.checked
|
||||
this.refs.list.resetScroll()
|
||||
this.setState({
|
||||
filter: filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleAllNotesButtonClick (e) {
|
||||
let { filter } = this.state
|
||||
filter.type = 'ALL'
|
||||
this.refs.list.resetScroll()
|
||||
this.setState({
|
||||
filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleStarredButtonClick (e) {
|
||||
let { filter } = this.state
|
||||
filter.type = 'STARRED'
|
||||
this.refs.list.resetScroll()
|
||||
this.setState({
|
||||
filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleStorageButtonClick (e, storage) {
|
||||
let { 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) {
|
||||
let { 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 () {
|
||||
let { data, config } = this.props
|
||||
let { filter, search } = this.state
|
||||
let storageList = []
|
||||
for (let key in data.storageMap) {
|
||||
let storage = data.storageMap[key]
|
||||
let 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 (let 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) {
|
||||
let 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))
|
||||
|
||||
let 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()
|
||||
})
|
||||
122
browser/finder/ipcClient.js
Normal file
122
browser/finder/ipcClient.js
Normal file
@@ -0,0 +1,122 @@
|
||||
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 () {
|
||||
let 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 () {
|
||||
let 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 {
|
||||
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
|
||||
51
browser/finder/store.js
Normal file
51
browser/finder/store.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { combineReducers, createStore } from 'redux'
|
||||
import { routerReducer } from 'react-router-redux'
|
||||
import { DEFAULT_CONFIG } from 'browser/main/lib/ConfigManager'
|
||||
|
||||
let 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
|
||||
}
|
||||
|
||||
let reducer = combineReducers({
|
||||
data,
|
||||
config,
|
||||
routing: routerReducer
|
||||
})
|
||||
|
||||
let store = createStore(reducer)
|
||||
|
||||
export default store
|
||||
5
browser/lib/CSSModules.js
Normal file
5
browser/lib/CSSModules.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import CSSModules from 'react-css-modules'
|
||||
|
||||
export default function (component, styles) {
|
||||
return CSSModules(component, styles, {errorWhenNotFound: false})
|
||||
}
|
||||
106
browser/lib/Mutable.js
Normal file
106
browser/lib/Mutable.js
Normal file
@@ -0,0 +1,106 @@
|
||||
class MutableMap {
|
||||
constructor (iterable) {
|
||||
this._map = new Map(iterable)
|
||||
Object.defineProperty(this, 'size', {
|
||||
get: () => this._map.size,
|
||||
set: function (value) {
|
||||
this['size'] = value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get (...args) {
|
||||
return this._map.get(...args)
|
||||
}
|
||||
|
||||
set (...args) {
|
||||
return this._map.set(...args)
|
||||
}
|
||||
|
||||
delete (...args) {
|
||||
return this._map.delete(...args)
|
||||
}
|
||||
|
||||
has (...args) {
|
||||
return this._map.has(...args)
|
||||
}
|
||||
|
||||
clear (...args) {
|
||||
return this._map.clear(...args)
|
||||
}
|
||||
|
||||
forEach (...args) {
|
||||
return this._map.forEach(...args)
|
||||
}
|
||||
|
||||
[Symbol.iterator] () {
|
||||
return this._map[Symbol.iterator]()
|
||||
}
|
||||
|
||||
map (cb) {
|
||||
let result = []
|
||||
for (let [key, value] of this._map) {
|
||||
result.push(cb(value, key))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
toJS () {
|
||||
let result = {}
|
||||
for (let [key, value] of this._map) {
|
||||
if (value instanceof MutableSet || value instanceof MutableMap) {
|
||||
value = value.toJS()
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class MutableSet {
|
||||
constructor (iterable) {
|
||||
this._set = new Set(iterable)
|
||||
Object.defineProperty(this, 'size', {
|
||||
get: () => this._set.size,
|
||||
set: function (value) {
|
||||
this['size'] = value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
add (...args) {
|
||||
return this._set.add(...args)
|
||||
}
|
||||
|
||||
delete (...args) {
|
||||
return this._set.delete(...args)
|
||||
}
|
||||
|
||||
forEach (...args) {
|
||||
return this._set.forEach(...args)
|
||||
}
|
||||
|
||||
[Symbol.iterator] () {
|
||||
return this._set[Symbol.iterator]()
|
||||
}
|
||||
|
||||
map (cb) {
|
||||
let result = []
|
||||
this._set.forEach(function (value, key) {
|
||||
result.push(cb(value, key))
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
toJS () {
|
||||
return Array.from(this._set)
|
||||
}
|
||||
}
|
||||
|
||||
const Mutable = {
|
||||
Map: MutableMap,
|
||||
Set: MutableSet
|
||||
}
|
||||
|
||||
module.exports = Mutable
|
||||
36
browser/lib/consts.js
Normal file
36
browser/lib/consts.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const path = require('path')
|
||||
const fs = require('sander')
|
||||
const { remote } = require('electron')
|
||||
const { app } = remote
|
||||
|
||||
const themePath = process.env.NODE_ENV === 'production'
|
||||
? path.join(app.getAppPath(), './node_modules/codemirror/theme')
|
||||
: require('path').resolve('./node_modules/codemirror/theme')
|
||||
const themes = fs.readdirSync(themePath)
|
||||
.map((themePath) => {
|
||||
return themePath.substring(0, themePath.lastIndexOf('.'))
|
||||
})
|
||||
|
||||
const consts = {
|
||||
FOLDER_COLORS: [
|
||||
'#E10051',
|
||||
'#FF8E00',
|
||||
'#E8D252',
|
||||
'#3FD941',
|
||||
'#30D5C8',
|
||||
'#2BA5F7',
|
||||
'#B013A4'
|
||||
],
|
||||
FOLDER_COLOR_NAMES: [
|
||||
'Razzmatazz (Red)',
|
||||
'Pizazz (Orange)',
|
||||
'Confetti (Yellow)',
|
||||
'Emerald (Green)',
|
||||
'Turquoise',
|
||||
'Dodger Blue',
|
||||
'Violet Eggplant'
|
||||
],
|
||||
THEMES: ['default'].concat(themes)
|
||||
}
|
||||
|
||||
module.exports = consts
|
||||
17
browser/lib/context.js
Normal file
17
browser/lib/context.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const { remote } = require('electron')
|
||||
const { Menu, MenuItem } = remote
|
||||
|
||||
function popup (templates) {
|
||||
let menu = new Menu()
|
||||
templates.forEach((item) => {
|
||||
menu.append(new MenuItem(item))
|
||||
})
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
}
|
||||
|
||||
const context = {
|
||||
popup
|
||||
}
|
||||
|
||||
module.export = context
|
||||
export default context
|
||||
3
browser/lib/customMeta.js
Normal file
3
browser/lib/customMeta.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import CodeMirror from 'codemirror'
|
||||
|
||||
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']})
|
||||
18
browser/lib/date-formatter.js
Normal file
18
browser/lib/date-formatter.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @fileoverview Formatting date string.
|
||||
*/
|
||||
import moment from 'moment'
|
||||
|
||||
/**
|
||||
* @description Return date string. For example, 'Sep.9, 2016 12:00'.
|
||||
* @param {mixed}
|
||||
* @return {string}
|
||||
*/
|
||||
export function getLastUpdated (date) {
|
||||
const m = moment(date)
|
||||
if (!m.isValid()) {
|
||||
throw Error('Invalid argument.')
|
||||
}
|
||||
|
||||
return m.format('MMM D, gggg H:mm')
|
||||
}
|
||||
7
browser/lib/keygen.js
Normal file
7
browser/lib/keygen.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const crypto = require('crypto')
|
||||
const _ = require('lodash')
|
||||
|
||||
module.exports = function (length) {
|
||||
if (!_.isFinite(length)) length = 10
|
||||
return crypto.randomBytes(length).toString('hex')
|
||||
}
|
||||
164
browser/lib/markdown.js
Normal file
164
browser/lib/markdown.js
Normal file
@@ -0,0 +1,164 @@
|
||||
import markdownit from 'markdown-it'
|
||||
import emoji from 'markdown-it-emoji'
|
||||
import math from '@rokt33r/markdown-it-math'
|
||||
import _ from 'lodash'
|
||||
|
||||
const katex = window.katex
|
||||
|
||||
function createGutter (str) {
|
||||
let lc = (str.match(/\n/g) || []).length
|
||||
let lines = []
|
||||
for (let i = 1; i <= lc; 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,
|
||||
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-footnote'))
|
||||
// Override task item
|
||||
md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
||||
let content, terminate, i, l, token
|
||||
let nextLine = startLine + 1
|
||||
let terminatorRules = state.md.block.ruler.getRules('paragraph')
|
||||
let 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') {
|
||||
let 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
|
||||
let 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]])
|
||||
}
|
||||
})
|
||||
let result = originalRender.call(md.renderer, tokens, options, env)
|
||||
return result
|
||||
}
|
||||
window.md = md
|
||||
|
||||
function strip (input) {
|
||||
var output = input
|
||||
try {
|
||||
output = output
|
||||
.replace(/^([\s\t]*)([\*\-\+]|\d\.)\s+/gm, '$1')
|
||||
.replace(/\n={2,}/g, '\n')
|
||||
.replace(/~~/g, '')
|
||||
.replace(/`{3}.*\n/g, '')
|
||||
.replace(/<(.*?)>/g, '$1')
|
||||
.replace(/^[=\-]{2,}\s*$/g, '')
|
||||
.replace(/\[\^.+?\](: .*?$)?/g, '')
|
||||
.replace(/\s{0,2}\[.*?\]: .*?$/g, '')
|
||||
.replace(/!\[.*?\][\[\(].*?[\]\)]/g, '')
|
||||
.replace(/\[(.*?)\][\[\(].*?[\]\)]/g, '$1')
|
||||
.replace(/>/g, '')
|
||||
.replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, '')
|
||||
.replace(/^#{1,6}\s*([^#]*)\s*(#{1,6})?/gm, '$1')
|
||||
.replace(/([\*_]{1,3})(\S.*?\S)\1/g, '$2')
|
||||
.replace(/(`{3,})(.*?)\1/gm, '$2')
|
||||
.replace(/^-{3,}\s*$/g, '')
|
||||
.replace(/`(.+?)`/g, '$1')
|
||||
.replace(/\n{2,}/g, '\n\n')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return input
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
const markdown = {
|
||||
render: function markdown (content) {
|
||||
if (!_.isString(content)) content = ''
|
||||
return md.render(content)
|
||||
},
|
||||
strip
|
||||
}
|
||||
export default markdown
|
||||
24
browser/main/Detail/Detail.styl
Normal file
24
browser/main/Detail/Detail.styl
Normal file
@@ -0,0 +1,24 @@
|
||||
.root
|
||||
absolute top bottom right
|
||||
border-width 1px 0
|
||||
border-style solid
|
||||
border-color $ui-borderColor
|
||||
|
||||
.empty
|
||||
height 320px
|
||||
display flex
|
||||
align-items center
|
||||
|
||||
.empty-message
|
||||
width 100%
|
||||
font-size 42px
|
||||
line-height 72px
|
||||
text-align center
|
||||
color $ui-inactive-text-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-backgroundColor
|
||||
.empty-message
|
||||
color $ui-dark-inactive-text-color
|
||||
9
browser/main/Detail/DetailVars.styl
Normal file
9
browser/main/Detail/DetailVars.styl
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Varibales for note detail space.
|
||||
*/
|
||||
|
||||
// Margin on the left side and the right side for NoteDetail component.
|
||||
$note-detail-left-margin = 25px
|
||||
$note-detail-right-margin = 25px
|
||||
|
||||
$note-detail-box-shadow = 2px 0 15px -8px #b1b1b1 inset
|
||||
289
browser/main/Detail/FolderSelect.js
Normal file
289
browser/main/Detail/FolderSelect.js
Normal file
@@ -0,0 +1,289 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './FolderSelect.styl'
|
||||
import _ from 'lodash'
|
||||
|
||||
class FolderSelect extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
status: 'IDLE',
|
||||
search: '',
|
||||
optionIndex: -1
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.value = this.props.value
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
this.value = this.props.value
|
||||
}
|
||||
|
||||
handleClick (e) {
|
||||
this.setState({
|
||||
status: 'SEARCH',
|
||||
optionIndex: -1
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleFocus (e) {
|
||||
if (this.state.status === 'IDLE') {
|
||||
this.setState({
|
||||
status: 'FOCUS'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleBlur (e) {
|
||||
if (this.state.status === 'FOCUS') {
|
||||
this.setState({
|
||||
status: 'IDLE'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
case 13:
|
||||
if (this.state.status === 'FOCUS') {
|
||||
this.setState({
|
||||
status: 'SEARCH',
|
||||
optionIndex: -1
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
break
|
||||
case 40:
|
||||
case 38:
|
||||
if (this.state.status === 'FOCUS') {
|
||||
this.setState({
|
||||
status: 'SEARCH',
|
||||
optionIndex: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
break
|
||||
case 9:
|
||||
if (e.shiftKey) {
|
||||
e.preventDefault()
|
||||
let tabbable = document.querySelectorAll('a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])')
|
||||
let previousEl = tabbable[Array.prototype.indexOf.call(tabbable, this.refs.root) - 1]
|
||||
if (previousEl != null) previousEl.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchInputBlur (e) {
|
||||
if (e.relatedTarget !== this.refs.root) {
|
||||
this.setState({
|
||||
status: 'IDLE'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchInputChange (e) {
|
||||
let { folders } = this.props
|
||||
let search = this.refs.search.value
|
||||
let optionIndex = search.length > 0
|
||||
? _.findIndex(folders, (folder) => {
|
||||
return folder.name.match(new RegExp('^' + _.escapeRegExp(search), 'i'))
|
||||
})
|
||||
: -1
|
||||
|
||||
this.setState({
|
||||
search: this.refs.search.value,
|
||||
optionIndex: optionIndex
|
||||
})
|
||||
}
|
||||
|
||||
handleSearchInputKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
case 40:
|
||||
e.stopPropagation()
|
||||
this.nextOption()
|
||||
break
|
||||
case 38:
|
||||
e.stopPropagation()
|
||||
this.previousOption()
|
||||
break
|
||||
case 13:
|
||||
e.stopPropagation()
|
||||
this.selectOption()
|
||||
break
|
||||
case 27:
|
||||
e.stopPropagation()
|
||||
this.setState({
|
||||
status: 'FOCUS'
|
||||
}, () => {
|
||||
this.refs.root.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
nextOption () {
|
||||
let { optionIndex } = this.state
|
||||
let { folders } = this.props
|
||||
|
||||
optionIndex++
|
||||
if (optionIndex >= folders.length) optionIndex = 0
|
||||
|
||||
this.setState({
|
||||
optionIndex
|
||||
})
|
||||
}
|
||||
|
||||
previousOption () {
|
||||
let { folders } = this.props
|
||||
let { optionIndex } = this.state
|
||||
|
||||
optionIndex--
|
||||
if (optionIndex < 0) optionIndex = folders.length - 1
|
||||
|
||||
this.setState({
|
||||
optionIndex
|
||||
})
|
||||
}
|
||||
|
||||
selectOption () {
|
||||
let { folders } = this.props
|
||||
let optionIndex = this.state.optionIndex
|
||||
|
||||
let folder = folders[optionIndex]
|
||||
if (folder != null) {
|
||||
this.setState({
|
||||
status: 'FOCUS'
|
||||
}, () => {
|
||||
this.setValue(folder.key)
|
||||
this.refs.root.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleOptionClick (storageKey, folderKey) {
|
||||
return (e) => {
|
||||
e.stopPropagation()
|
||||
this.setState({
|
||||
status: 'FOCUS'
|
||||
}, () => {
|
||||
this.setValue(storageKey + '-' + folderKey)
|
||||
this.refs.root.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
setValue (value) {
|
||||
this.value = value
|
||||
this.props.onChange()
|
||||
}
|
||||
|
||||
render () {
|
||||
let { className, data, value } = this.props
|
||||
let splitted = value.split('-')
|
||||
let storageKey = splitted.shift()
|
||||
let folderKey = splitted.shift()
|
||||
let options = []
|
||||
data.storageMap.forEach((storage, index) => {
|
||||
storage.folders.forEach((folder) => {
|
||||
options.push({
|
||||
storage: storage,
|
||||
folder: folder
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
let currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
|
||||
|
||||
if (this.state.search.trim().length > 0) {
|
||||
let filter = new RegExp('^' + _.escapeRegExp(this.state.search), 'i')
|
||||
options = options.filter((option) => filter.test(option.folder.name))
|
||||
}
|
||||
|
||||
let optionList = options
|
||||
.map((option, index) => {
|
||||
return (
|
||||
<div styleName={index === this.state.optionIndex
|
||||
? 'search-optionList-item--active'
|
||||
: 'search-optionList-item'
|
||||
}
|
||||
key={option.storage.key + '-' + option.folder.key}
|
||||
onClick={(e) => this.handleOptionClick(option.storage.key, option.folder.key)(e)}
|
||||
>
|
||||
<span styleName='search-optionList-item-name'
|
||||
style={{borderColor: option.folder.color}}
|
||||
>
|
||||
{option.folder.name}
|
||||
<span styleName='search-optionList-item-name-surfix'>in {option.storage.name}</span>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={_.isString(className)
|
||||
? 'FolderSelect ' + className
|
||||
: 'FolderSelect'
|
||||
}
|
||||
styleName={this.state.status === 'SEARCH'
|
||||
? 'root--search'
|
||||
: this.state.status === 'FOCUS'
|
||||
? 'root--focus'
|
||||
: 'root'
|
||||
}
|
||||
ref='root'
|
||||
tabIndex='0'
|
||||
onClick={(e) => this.handleClick(e)}
|
||||
onFocus={(e) => this.handleFocus(e)}
|
||||
onBlur={(e) => this.handleBlur(e)}
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
>
|
||||
{this.state.status === 'SEARCH'
|
||||
? <div styleName='search'>
|
||||
<input styleName='search-input'
|
||||
ref='search'
|
||||
value={this.state.search}
|
||||
placeholder='Folder...'
|
||||
onChange={(e) => this.handleSearchInputChange(e)}
|
||||
onBlur={(e) => this.handleSearchInputBlur(e)}
|
||||
onKeyDown={(e) => this.handleSearchInputKeyDown(e)}
|
||||
/>
|
||||
<div styleName='search-optionList'
|
||||
ref='optionList'
|
||||
>
|
||||
{optionList}
|
||||
</div>
|
||||
</div>
|
||||
: <div styleName='idle'>
|
||||
<div styleName='idle-label'>
|
||||
<span styleName='idle-label-name'
|
||||
style={{color: currentOption.folder.color}}
|
||||
>
|
||||
{currentOption.folder.name} /
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FolderSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
folders: PropTypes.arrayOf(PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
color: PropTypes.string
|
||||
}))
|
||||
}
|
||||
|
||||
export default CSSModules(FolderSelect, styles)
|
||||
131
browser/main/Detail/FolderSelect.styl
Normal file
131
browser/main/Detail/FolderSelect.styl
Normal file
@@ -0,0 +1,131 @@
|
||||
.root
|
||||
position relative
|
||||
border solid 1px transparent
|
||||
line-height 34px
|
||||
vertical-align middle
|
||||
border-radius 2px
|
||||
transition 0.15s
|
||||
user-select none
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
|
||||
.root--search, .root--focus
|
||||
@extend .root
|
||||
background-color $ui-noteDetail-backgroundColor = #F4F4F4
|
||||
border-color $ui-input--focus-borderColor
|
||||
width 100px
|
||||
&:hover
|
||||
border-color $ui-input--focus-borderColor
|
||||
|
||||
.idle
|
||||
position relative
|
||||
cursor pointer
|
||||
|
||||
.idle-label
|
||||
right 20px
|
||||
overflow ellipsis
|
||||
|
||||
.idle-label-name
|
||||
font-size 16px
|
||||
padding 2px
|
||||
|
||||
.idle-label-name-surfix
|
||||
font-size 10px
|
||||
color $ui-inactive-text-color
|
||||
margin-left 5px
|
||||
.idle-caret
|
||||
absolute right top
|
||||
height 34px
|
||||
width 20px
|
||||
line-height 34px
|
||||
|
||||
.search
|
||||
absolute top left right bottom
|
||||
line-height 34px
|
||||
|
||||
.search-input
|
||||
vertical-align middle
|
||||
position relative
|
||||
top -2px
|
||||
outline none
|
||||
border none
|
||||
height 20px
|
||||
line-height 20px
|
||||
background-color transparent
|
||||
padding 0 10px
|
||||
|
||||
.search-optionList
|
||||
position fixed
|
||||
max-height 450px
|
||||
overflow auto
|
||||
z-index 200
|
||||
border $ui-border
|
||||
background-color white
|
||||
border-radius 2px
|
||||
|
||||
.search-optionList-item
|
||||
height 34px
|
||||
width 250px
|
||||
box-sizing border-box
|
||||
padding 0 5px
|
||||
overflow ellipsis
|
||||
cursor pointer
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
|
||||
.search-optionList-item--active
|
||||
@extend .search-optionList-item
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
&:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
.search-optionList-item-name
|
||||
border-left solid 4px transparent
|
||||
padding 2px 5px
|
||||
.search-optionList-item-name-surfix
|
||||
font-size 10px
|
||||
color $ui-inactive-text-color
|
||||
margin-left 5px
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
color $ui-dark-text-color
|
||||
&:hover
|
||||
color white
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.root--search, .root--focus
|
||||
@extend .root
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
border-color $ui-input--focus-borderColor
|
||||
&:hover
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
border-color $ui-input--focus-borderColor
|
||||
|
||||
.idle-label-name-surfix
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
.search-input
|
||||
color white
|
||||
background-color transparent
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.search-optionList
|
||||
color white
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
|
||||
.search-optionList-item
|
||||
&:hover
|
||||
background-color lighten($ui-dark-button--hover-backgroundColor, 15%)
|
||||
|
||||
.search-optionList-item--active
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-button--active-color
|
||||
&:hover
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-button--active-color
|
||||
.search-optionList-item-name-surfix
|
||||
color $ui-dark-inactive-text-color
|
||||
27
browser/main/Detail/LastUpdatedString.js
Normal file
27
browser/main/Detail/LastUpdatedString.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @fileoverview Component for show updated date of the detail.
|
||||
*/
|
||||
import React, { PropTypes } from 'react'
|
||||
import { getLastUpdated } from 'browser/lib/date-formatter'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './LastUpdatedString.styl'
|
||||
|
||||
const LastUpdatedString = ({ date }) => {
|
||||
let text = ''
|
||||
|
||||
try {
|
||||
text = `Last updated at ${getLastUpdated(date)}`
|
||||
} catch (e) {
|
||||
text = ''
|
||||
}
|
||||
|
||||
return (
|
||||
<p styleName='info-right-date'>{text}</p>
|
||||
)
|
||||
}
|
||||
|
||||
LastUpdatedString.propTypes = {
|
||||
date: PropTypes.string
|
||||
}
|
||||
|
||||
export default CSSModules(LastUpdatedString, styles)
|
||||
10
browser/main/Detail/LastUpdatedString.styl
Normal file
10
browser/main/Detail/LastUpdatedString.styl
Normal file
@@ -0,0 +1,10 @@
|
||||
.info-right-date
|
||||
display inline
|
||||
line-height 24px
|
||||
padding-right 25px
|
||||
font-size 11px
|
||||
color $ui-button-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.info-right-date
|
||||
color $ui-dark-button-color
|
||||
278
browser/main/Detail/MarkdownNoteDetail.js
Normal file
278
browser/main/Detail/MarkdownNoteDetail.js
Normal file
@@ -0,0 +1,278 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './MarkdownNoteDetail.styl'
|
||||
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
||||
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 ee from 'browser/main/lib/eventEmitter'
|
||||
import markdown from 'browser/lib/markdown'
|
||||
import StatusBar from '../StatusBar'
|
||||
import _ from 'lodash'
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
const { Menu, MenuItem, dialog } = remote
|
||||
|
||||
class MarkdownNoteDetail extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isMovingNote: false,
|
||||
note: Object.assign({
|
||||
title: '',
|
||||
content: ''
|
||||
}, props.note)
|
||||
}
|
||||
this.dispatchTimer = null
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.refs.content.focus()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
this.setState({
|
||||
note: Object.assign({}, nextProps.note)
|
||||
}, () => {
|
||||
this.refs.content.reload()
|
||||
this.refs.tags.reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
}
|
||||
|
||||
findTitle (value) {
|
||||
let splitted = value.split('\n')
|
||||
let title = null
|
||||
|
||||
for (let i = 0; i < splitted.length; i++) {
|
||||
let trimmedLine = splitted[i].trim()
|
||||
if (trimmedLine.match(/^# .+/)) {
|
||||
title = trimmedLine.substring(1, trimmedLine.length).trim()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (title == null) {
|
||||
for (let i = 0; i < splitted.length; i++) {
|
||||
let trimmedLine = splitted[i].trim()
|
||||
if (trimmedLine.length > 0) {
|
||||
title = trimmedLine
|
||||
break
|
||||
}
|
||||
}
|
||||
if (title == null) {
|
||||
title = ''
|
||||
}
|
||||
}
|
||||
|
||||
title = markdown.strip(title)
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
let { note } = this.state
|
||||
|
||||
note.content = this.refs.content.value
|
||||
note.tags = this.refs.tags.value
|
||||
note.title = this.findTitle(note.content)
|
||||
note.updatedAt = new Date()
|
||||
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
|
||||
save () {
|
||||
clearTimeout(this.saveQueue)
|
||||
this.saveQueue = setTimeout(() => {
|
||||
this.saveNow()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
saveNow () {
|
||||
let { note, dispatch } = this.props
|
||||
clearTimeout(this.saveQueue)
|
||||
this.saveQueue = null
|
||||
|
||||
dataApi
|
||||
.updateNote(note.storage, note.key, this.state.note)
|
||||
.then((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
handleFolderChange (e) {
|
||||
let { note } = this.state
|
||||
let value = this.refs.folder.value
|
||||
let splitted = value.split('-')
|
||||
let newStorageKey = splitted.shift()
|
||||
let newFolderKey = splitted.shift()
|
||||
|
||||
dataApi
|
||||
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
|
||||
.then((newNote) => {
|
||||
this.setState({
|
||||
isMovingNote: true,
|
||||
note: Object.assign({}, newNote)
|
||||
}, () => {
|
||||
let { dispatch, location } = this.props
|
||||
dispatch({
|
||||
type: 'MOVE_NOTE',
|
||||
originNote: note,
|
||||
note: newNote
|
||||
})
|
||||
hashHistory.replace({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: newNote.storage + '-' + newNote.key
|
||||
}
|
||||
})
|
||||
this.setState({
|
||||
isMovingNote: false
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
handleStarButtonClick (e) {
|
||||
let { note } = this.state
|
||||
|
||||
note.isStarred = !note.isStarred
|
||||
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
|
||||
exportAsFile () {
|
||||
|
||||
}
|
||||
|
||||
handleContextButtonClick (e) {
|
||||
let menu = new Menu()
|
||||
menu.append(new MenuItem({
|
||||
label: 'Delete',
|
||||
click: (e) => this.handleDeleteMenuClick(e)
|
||||
}))
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
}
|
||||
|
||||
handleDeleteMenuClick (e) {
|
||||
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Delete a note',
|
||||
detail: 'This work cannot be undone.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
if (index === 0) {
|
||||
let { note, dispatch } = this.props
|
||||
dataApi
|
||||
.deleteNote(note.storage, note.key)
|
||||
.then((data) => {
|
||||
let dispatchHandler = () => {
|
||||
dispatch({
|
||||
type: 'DELETE_NOTE',
|
||||
storageKey: data.storageKey,
|
||||
noteKey: data.noteKey
|
||||
})
|
||||
}
|
||||
ee.once('list:moved', dispatchHandler)
|
||||
ee.emit('list:next')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleDeleteKeyDown (e) {
|
||||
if (e.keyCode === 27) this.handleDeleteCancelButtonClick(e)
|
||||
}
|
||||
|
||||
render () {
|
||||
let { data, config } = this.props
|
||||
let { note } = this.state
|
||||
|
||||
return (
|
||||
<div className='NoteDetail'
|
||||
style={this.props.style}
|
||||
styleName='root'
|
||||
>
|
||||
<div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<StarButton styleName='info-left-button'
|
||||
onClick={(e) => this.handleStarButtonClick(e)}
|
||||
isActive={note.isStarred}
|
||||
/>
|
||||
<div styleName='info-left-top'>
|
||||
<FolderSelect styleName='info-left-top-folderSelect'
|
||||
value={this.state.note.storage + '-' + this.state.note.folder}
|
||||
ref='folder'
|
||||
data={data}
|
||||
onChange={(e) => this.handleFolderChange(e)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TagSelect
|
||||
ref='tags'
|
||||
value={this.state.note.tags}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<button styleName='info-right-button'
|
||||
onClick={(e) => this.handleContextButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-ellipsis-v' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='body'>
|
||||
<MarkdownEditor
|
||||
ref='content'
|
||||
styleName='body-noteEditor'
|
||||
config={config}
|
||||
value={this.state.note.content}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<StatusBar
|
||||
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
||||
date={note.updatedAt}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MarkdownNoteDetail.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
repositories: PropTypes.array,
|
||||
note: PropTypes.shape({
|
||||
|
||||
}),
|
||||
style: PropTypes.shape({
|
||||
left: PropTypes.number
|
||||
}),
|
||||
ignorePreviewPointerEvents: PropTypes.bool
|
||||
}
|
||||
|
||||
export default CSSModules(MarkdownNoteDetail, styles)
|
||||
26
browser/main/Detail/MarkdownNoteDetail.styl
Normal file
26
browser/main/Detail/MarkdownNoteDetail.styl
Normal file
@@ -0,0 +1,26 @@
|
||||
@import('NoteDetailInfo')
|
||||
@import('DetailVars')
|
||||
|
||||
.root
|
||||
absolute top right bottom
|
||||
border-width 0 0 1px
|
||||
border-style solid
|
||||
border-color $ui-borderColor
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
box-shadow $note-detail-box-shadow
|
||||
|
||||
.body
|
||||
absolute left right
|
||||
left $note-detail-left-margin
|
||||
right $note-detail-right-margin
|
||||
top $info-height + $info-margin-under-border
|
||||
bottom $statusBar-height
|
||||
|
||||
.body-noteEditor
|
||||
absolute top bottom left right
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
box-shadow none
|
||||
98
browser/main/Detail/NoteDetailInfo.styl
Normal file
98
browser/main/Detail/NoteDetailInfo.styl
Normal file
@@ -0,0 +1,98 @@
|
||||
@import('DetailVars')
|
||||
|
||||
$info-height = 60px
|
||||
$info-margin-under-border = 27px
|
||||
|
||||
.info
|
||||
absolute top left right
|
||||
left $note-detail-left-margin
|
||||
right $note-detail-right-margin
|
||||
height $info-height
|
||||
border-bottom $ui-border
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
.info-left
|
||||
float left
|
||||
padding 0 5px
|
||||
margin 0px 2px
|
||||
|
||||
.info-left-top
|
||||
display inline-block
|
||||
height $info-height
|
||||
line-height $info-height
|
||||
|
||||
.info-left-top-folderSelect
|
||||
display inline-block
|
||||
padding 0 3px
|
||||
height 34px
|
||||
line-height 34px
|
||||
vertical-align middle
|
||||
border-radius 3px
|
||||
|
||||
.info-left-button
|
||||
width 34px
|
||||
height 34px
|
||||
navButtonColor()
|
||||
color $ui-favorite-star-button-color
|
||||
font-size 14px
|
||||
margin 13px 2px
|
||||
padding 0
|
||||
border-radius 17px
|
||||
&:hover .info-right-button-tooltip
|
||||
opacity 1
|
||||
&:focus
|
||||
border-color $ui-favorite-star-button-color
|
||||
&:active, &:active:hover
|
||||
background-color $ui-favorite-star-button-color
|
||||
color $ui-button--active-color
|
||||
|
||||
.info-right
|
||||
position absolute
|
||||
right 0
|
||||
top 0
|
||||
background $ui-noteDetail-backgroundColor
|
||||
bottom 1px
|
||||
padding-left 30px
|
||||
|
||||
.info-right-button
|
||||
width 34px
|
||||
height 34px
|
||||
border-radius 17px
|
||||
font-size 14px
|
||||
margin 13px 7px
|
||||
padding 0
|
||||
border none
|
||||
color $ui-button-color
|
||||
background-color transparent
|
||||
&:hover
|
||||
opacity 1
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
|
||||
|
||||
body[data-theme="dark"]
|
||||
.info
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
|
||||
.info-delete
|
||||
color $ui-dark-text-color
|
||||
|
||||
.info-delete-confirmButton
|
||||
colorDarkDangerButton()
|
||||
color $ui-dark-text-color
|
||||
|
||||
.info-delete-cancelButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-dark-borderColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
.info-right
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
|
||||
.info-right-button
|
||||
navDarkButtonColor()
|
||||
border-color $ui-dark-borderColor
|
||||
&:active
|
||||
border-color $ui-dark-button--focus-borderColor
|
||||
&:focus
|
||||
border-color $ui-button--focus-borderColor
|
||||
627
browser/main/Detail/SnippetNoteDetail.js
Normal file
627
browser/main/Detail/SnippetNoteDetail.js
Normal file
@@ -0,0 +1,627 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './SnippetNoteDetail.styl'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
||||
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 ee from 'browser/main/lib/eventEmitter'
|
||||
import CodeMirror from 'codemirror'
|
||||
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'
|
||||
|
||||
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 electron = require('electron')
|
||||
const { remote } = electron
|
||||
const { Menu, MenuItem, dialog } = remote
|
||||
|
||||
class SnippetNoteDetail extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isMovingNote: false,
|
||||
snippetIndex: 0,
|
||||
note: Object.assign({
|
||||
description: ''
|
||||
}, props.note, {
|
||||
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.note.key !== this.props.note.key) {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
let nextNote = Object.assign({
|
||||
description: ''
|
||||
}, nextProps.note, {
|
||||
snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet))
|
||||
})
|
||||
this.setState({
|
||||
snippetIndex: 0,
|
||||
note: nextNote
|
||||
}, () => {
|
||||
let { snippets } = this.state.note
|
||||
snippets.forEach((snippet, index) => {
|
||||
this.refs['code-' + index].reload()
|
||||
})
|
||||
this.refs.tags.reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
}
|
||||
|
||||
findTitle (value) {
|
||||
let splitted = value.split('\n')
|
||||
let title = null
|
||||
|
||||
for (let i = 0; i < splitted.length; i++) {
|
||||
let trimmedLine = splitted[i].trim()
|
||||
if (trimmedLine.match(/^# .+/)) {
|
||||
title = trimmedLine.substring(1, trimmedLine.length).trim()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (title == null) {
|
||||
for (let i = 0; i < splitted.length; i++) {
|
||||
let trimmedLine = splitted[i].trim()
|
||||
if (trimmedLine.length > 0) {
|
||||
title = trimmedLine
|
||||
break
|
||||
}
|
||||
}
|
||||
if (title == null) {
|
||||
title = ''
|
||||
}
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
let { note } = this.state
|
||||
|
||||
note.tags = this.refs.tags.value
|
||||
note.description = this.refs.description.value
|
||||
note.updatedAt = new Date()
|
||||
note.title = this.findTitle(note.description)
|
||||
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
|
||||
save () {
|
||||
clearTimeout(this.saveQueue)
|
||||
this.saveQueue = setTimeout(() => {
|
||||
this.saveNow()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
saveNow () {
|
||||
let { note, dispatch } = this.props
|
||||
clearTimeout(this.saveQueue)
|
||||
this.saveQueue = null
|
||||
|
||||
dataApi
|
||||
.updateNote(note.storage, note.key, this.state.note)
|
||||
.then((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
handleFolderChange (e) {
|
||||
let { note } = this.state
|
||||
let value = this.refs.folder.value
|
||||
let splitted = value.split('-')
|
||||
let newStorageKey = splitted.shift()
|
||||
let newFolderKey = splitted.shift()
|
||||
|
||||
dataApi
|
||||
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
|
||||
.then((newNote) => {
|
||||
this.setState({
|
||||
isMovingNote: true,
|
||||
note: Object.assign({}, newNote)
|
||||
}, () => {
|
||||
let { dispatch, location } = this.props
|
||||
dispatch({
|
||||
type: 'MOVE_NOTE',
|
||||
originNote: note,
|
||||
note: newNote
|
||||
})
|
||||
hashHistory.replace({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: newNote.storage + '-' + newNote.key
|
||||
}
|
||||
})
|
||||
this.setState({
|
||||
isMovingNote: false
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
handleStarButtonClick (e) {
|
||||
let { note } = this.state
|
||||
|
||||
note.isStarred = !note.isStarred
|
||||
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
|
||||
exportAsFile () {
|
||||
|
||||
}
|
||||
|
||||
handleContextButtonClick (e) {
|
||||
context.popup([{
|
||||
label: 'Delete',
|
||||
click: (e) => this.handleDeleteMenuClick(e)
|
||||
}])
|
||||
}
|
||||
|
||||
handleDeleteMenuClick (e) {
|
||||
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Delete a note',
|
||||
detail: 'This work cannot be undone.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
if (index === 0) {
|
||||
let { note, dispatch } = this.props
|
||||
dataApi
|
||||
.deleteNote(note.storage, note.key)
|
||||
.then((data) => {
|
||||
let dispatchHandler = () => {
|
||||
dispatch({
|
||||
type: 'DELETE_NOTE',
|
||||
storageKey: data.storageKey,
|
||||
noteKey: data.noteKey
|
||||
})
|
||||
}
|
||||
ee.once('list:moved', dispatchHandler)
|
||||
ee.emit('list:next')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleTabPlusButtonClick (e) {
|
||||
this.addSnippet()
|
||||
}
|
||||
|
||||
handleTabButtonClick (e, index) {
|
||||
this.setState({
|
||||
snippetIndex: index
|
||||
})
|
||||
}
|
||||
|
||||
handleTabDeleteButtonClick (e, index) {
|
||||
if (this.state.note.snippets.length > 1) {
|
||||
if (this.state.note.snippets[index].content.trim().length > 0) {
|
||||
let dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Delete a snippet',
|
||||
detail: 'This work cannot be undone.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
if (dialogIndex === 0) {
|
||||
this.deleteSnippetByIndex(index)
|
||||
}
|
||||
} else {
|
||||
this.deleteSnippetByIndex(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deleteSnippetByIndex (index) {
|
||||
let snippets = this.state.note.snippets.slice()
|
||||
snippets.splice(index, 1)
|
||||
this.state.note.snippets = snippets
|
||||
let snippetIndex = this.state.snippetIndex >= snippets.length
|
||||
? snippets.length - 1
|
||||
: this.state.snippetIndex
|
||||
this.setState({
|
||||
note: this.state.note,
|
||||
snippetIndex
|
||||
})
|
||||
}
|
||||
|
||||
renameSnippetByIndex (index, name) {
|
||||
let snippets = this.state.note.snippets.slice()
|
||||
snippets[index].name = name
|
||||
let syntax = CodeMirror.findModeByFileName(name.trim())
|
||||
let mode = syntax != null ? syntax.name : null
|
||||
if (mode != null) snippets[index].mode = mode
|
||||
this.state.note.snippets = snippets
|
||||
|
||||
this.setState({
|
||||
note: this.state.note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
|
||||
handleModeOptionClick (index, name) {
|
||||
return (e) => {
|
||||
let snippets = this.state.note.snippets.slice()
|
||||
snippets[index].mode = name
|
||||
this.state.note.snippets = snippets
|
||||
|
||||
this.setState({
|
||||
note: this.state.note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleCodeChange (index) {
|
||||
return (e) => {
|
||||
let snippets = this.state.note.snippets.slice()
|
||||
snippets[index].content = this.refs['code-' + index].value
|
||||
this.state.note.snippets = snippets
|
||||
this.setState({
|
||||
note: this.state.note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
case 9:
|
||||
if (e.ctrlKey && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
this.jumpNextTab()
|
||||
} else if (e.ctrlKey && e.shiftKey) {
|
||||
e.preventDefault()
|
||||
this.jumpPrevTab()
|
||||
} else if (!e.ctrlKey && !e.shiftKey && e.target === this.refs.description) {
|
||||
e.preventDefault()
|
||||
this.focusEditor()
|
||||
}
|
||||
break
|
||||
case 76:
|
||||
{
|
||||
let isSuper = global.process.platform === 'darwin'
|
||||
? e.metaKey
|
||||
: e.ctrlKey
|
||||
if (isSuper) {
|
||||
e.preventDefault()
|
||||
this.focus()
|
||||
}
|
||||
}
|
||||
break
|
||||
case 84:
|
||||
{
|
||||
let isSuper = global.process.platform === 'darwin'
|
||||
? e.metaKey
|
||||
: e.ctrlKey
|
||||
if (isSuper) {
|
||||
e.preventDefault()
|
||||
this.addSnippet()
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
handleModeButtonClick (e, index) {
|
||||
let menu = new Menu()
|
||||
CodeMirror.modeInfo.forEach((mode) => {
|
||||
menu.append(new MenuItem({
|
||||
label: mode.name,
|
||||
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
|
||||
}))
|
||||
})
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
}
|
||||
|
||||
handleIndentTypeButtonClick (e) {
|
||||
context.popup([
|
||||
{
|
||||
label: 'tab',
|
||||
click: (e) => this.handleIndentTypeItemClick(e, 'tab')
|
||||
},
|
||||
{
|
||||
label: 'space',
|
||||
click: (e) => this.handleIndentTypeItemClick(e, 'space')
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
handleIndentSizeButtonClick (e) {
|
||||
context.popup([
|
||||
{
|
||||
label: '2',
|
||||
click: (e) => this.handleIndentSizeItemClick(e, 2)
|
||||
},
|
||||
{
|
||||
label: '4',
|
||||
click: (e) => this.handleIndentSizeItemClick(e, 4)
|
||||
},
|
||||
{
|
||||
label: '8',
|
||||
click: (e) => this.handleIndentSizeItemClick(e, 8)
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
handleIndentSizeItemClick (e, indentSize) {
|
||||
let { config, dispatch } = this.props
|
||||
let editor = Object.assign({}, config.editor, {
|
||||
indentSize
|
||||
})
|
||||
ConfigManager.set({
|
||||
editor
|
||||
})
|
||||
dispatch({
|
||||
type: 'SET_CONFIG',
|
||||
config: {
|
||||
editor
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleIndentTypeItemClick (e, indentType) {
|
||||
let { config, dispatch } = this.props
|
||||
let editor = Object.assign({}, config.editor, {
|
||||
indentType
|
||||
})
|
||||
ConfigManager.set({
|
||||
editor
|
||||
})
|
||||
dispatch({
|
||||
type: 'SET_CONFIG',
|
||||
config: {
|
||||
editor
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.refs.description.focus()
|
||||
}
|
||||
|
||||
addSnippet () {
|
||||
let { note } = this.state
|
||||
|
||||
note.snippets = note.snippets.concat([{
|
||||
name: '',
|
||||
mode: 'Plain Text',
|
||||
content: ''
|
||||
}])
|
||||
let snippetIndex = note.snippets.length - 1
|
||||
|
||||
this.setState({
|
||||
note,
|
||||
snippetIndex
|
||||
}, () => {
|
||||
this.refs['tab-' + snippetIndex].startRenaming()
|
||||
})
|
||||
}
|
||||
|
||||
jumpNextTab () {
|
||||
this.setState({
|
||||
snippetIndex: (this.state.snippetIndex + 1) % this.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.focusEditor()
|
||||
})
|
||||
}
|
||||
|
||||
focusEditor () {
|
||||
console.log('code-' + this.state.snippetIndex)
|
||||
this.refs['code-' + this.state.snippetIndex].focus()
|
||||
}
|
||||
|
||||
render () {
|
||||
let { data, config } = this.props
|
||||
let { note } = this.state
|
||||
|
||||
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
|
||||
|
||||
let tabList = note.snippets.map((snippet, index) => {
|
||||
let isActive = this.state.snippetIndex === index
|
||||
|
||||
return <SnippetTab
|
||||
key={index}
|
||||
ref={'tab-' + index}
|
||||
snippet={snippet}
|
||||
isActive={isActive}
|
||||
onClick={(e) => this.handleTabButtonClick(e, index)}
|
||||
onDelete={(e) => this.handleTabDeleteButtonClick(e, index)}
|
||||
onRename={(name) => this.renameSnippetByIndex(index, name)}
|
||||
isDeletable={note.snippets.length > 1}
|
||||
/>
|
||||
})
|
||||
|
||||
let viewList = note.snippets.map((snippet, index) => {
|
||||
let 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' || snippet.mode === 'GitHub Flavored Markdown'
|
||||
? <MarkdownEditor styleName='tabView-content'
|
||||
value={snippet.content}
|
||||
config={config}
|
||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||
ref={'code-' + index}
|
||||
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
||||
/>
|
||||
: <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}
|
||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||
ref={'code-' + index}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='NoteDetail'
|
||||
style={this.props.style}
|
||||
styleName='root'
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
>
|
||||
<div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<StarButton styleName='info-left-button'
|
||||
onClick={(e) => this.handleStarButtonClick(e)}
|
||||
isActive={note.isStarred}
|
||||
/>
|
||||
<div styleName='info-left-top'>
|
||||
<FolderSelect styleName='info-left-top-folderSelect'
|
||||
value={this.state.note.storage + '-' + this.state.note.folder}
|
||||
ref='folder'
|
||||
data={data}
|
||||
onChange={(e) => this.handleFolderChange(e)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TagSelect
|
||||
ref='tags'
|
||||
value={this.state.note.tags}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<button styleName='info-right-button'
|
||||
onClick={(e) => this.handleContextButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-ellipsis-v' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='body'>
|
||||
<div styleName='description'>
|
||||
<textarea
|
||||
style={{
|
||||
fontFamily: config.preview.fontFamily,
|
||||
fontSize: parseInt(config.preview.fontSize, 10)
|
||||
}}
|
||||
ref='description'
|
||||
placeholder='Description...'
|
||||
value={this.state.note.description}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='tabList'>
|
||||
<div styleName='list'>
|
||||
{tabList}
|
||||
</div>
|
||||
<button styleName='plusButton'
|
||||
onClick={(e) => this.handleTabPlusButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-plus' />
|
||||
</button>
|
||||
</div>
|
||||
{viewList}
|
||||
</div>
|
||||
|
||||
<div styleName='override'>
|
||||
<button
|
||||
onClick={(e) => this.handleModeButtonClick(e, this.state.snippetIndex)}
|
||||
>
|
||||
{this.state.note.snippets[this.state.snippetIndex].mode == null
|
||||
? 'Select Syntax...'
|
||||
: this.state.note.snippets[this.state.snippetIndex].mode
|
||||
}
|
||||
<i className='fa fa-caret-down' />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => this.handleIndentTypeButtonClick(e)}
|
||||
>
|
||||
Indent: {config.editor.indentType}
|
||||
<i className='fa fa-caret-down' />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => this.handleIndentSizeButtonClick(e)}
|
||||
>
|
||||
size: {config.editor.indentSize}
|
||||
<i className='fa fa-caret-down' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<StatusBar
|
||||
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
||||
date={note.updatedAt}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SnippetNoteDetail.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
repositories: PropTypes.array,
|
||||
note: PropTypes.shape({
|
||||
|
||||
}),
|
||||
style: PropTypes.shape({
|
||||
left: PropTypes.number
|
||||
}),
|
||||
ignorePreviewPointerEvents: PropTypes.bool
|
||||
}
|
||||
|
||||
export default CSSModules(SnippetNoteDetail, styles)
|
||||
95
browser/main/Detail/SnippetNoteDetail.styl
Normal file
95
browser/main/Detail/SnippetNoteDetail.styl
Normal file
@@ -0,0 +1,95 @@
|
||||
@import('NoteDetailInfo')
|
||||
@import('DetailVars')
|
||||
|
||||
.root
|
||||
absolute top bottom right
|
||||
border-width 0 0 1px
|
||||
border-style solid
|
||||
border-color $ui-borderColor
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
box-shadow $note-detail-box-shadow
|
||||
|
||||
.body
|
||||
absolute left right
|
||||
left $note-detail-left-margin
|
||||
right $note-detail-right-margin
|
||||
top $info-height + $info-margin-under-border
|
||||
bottom $statusBar-height
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
.body .description
|
||||
absolute top left right
|
||||
height 80px
|
||||
|
||||
.body .description textarea
|
||||
display block
|
||||
height 100%
|
||||
width 100%
|
||||
resize none
|
||||
border none
|
||||
padding 10px
|
||||
line-height 1.6
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
.tabList
|
||||
absolute left right
|
||||
top 80px
|
||||
height 30px
|
||||
display flex
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
.tabList .list
|
||||
flex 1
|
||||
display flex
|
||||
overflow hidden
|
||||
|
||||
.tabList .plusButton
|
||||
navButtonColor()
|
||||
width 30px
|
||||
|
||||
.tabView
|
||||
absolute left right bottom
|
||||
top 130px
|
||||
|
||||
.tabView-content
|
||||
absolute top left right bottom
|
||||
|
||||
.override
|
||||
absolute bottom left
|
||||
left 60px
|
||||
height 23px
|
||||
z-index 1
|
||||
button
|
||||
navButtonColor()
|
||||
height 24px
|
||||
border-width 0 1px 0 0
|
||||
border-style solid
|
||||
border-color $ui-borderColor
|
||||
&:active .update-icon
|
||||
color white
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
box-shadow none
|
||||
|
||||
.body
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
|
||||
.body .description textarea
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
color white
|
||||
|
||||
.tabList
|
||||
background-color $ui-button--active-backgroundColor
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
|
||||
.tabList .list
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.tabList .plusButton
|
||||
navDarkButtonColor()
|
||||
.override
|
||||
button
|
||||
border-color $ui-dark-borderColor
|
||||
67
browser/main/Detail/StarButton.js
Normal file
67
browser/main/Detail/StarButton.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './StarButton.styl'
|
||||
import _ from 'lodash'
|
||||
|
||||
class StarButton extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isActive: false
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseDown (e) {
|
||||
this.setState({
|
||||
isActive: true
|
||||
})
|
||||
}
|
||||
|
||||
handleMouseUp (e) {
|
||||
this.setState({
|
||||
isActive: false
|
||||
})
|
||||
}
|
||||
|
||||
handleMouseLeave (e) {
|
||||
this.setState({
|
||||
isActive: false
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
let { className } = this.props
|
||||
|
||||
return (
|
||||
<button className={_.isString(className)
|
||||
? 'StarButton ' + className
|
||||
: 'StarButton'
|
||||
}
|
||||
styleName={this.state.isActive || this.props.isActive
|
||||
? 'root--active'
|
||||
: 'root'
|
||||
}
|
||||
onMouseDown={(e) => this.handleMouseDown(e)}
|
||||
onMouseUp={(e) => this.handleMouseUp(e)}
|
||||
onMouseLeave={(e) => this.handleMouseLeave(e)}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
<i styleName='icon'
|
||||
className={this.state.isActive || this.props.isActive
|
||||
? 'fa fa-star'
|
||||
: 'fa fa-star-o'
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
StarButton.propTypes = {
|
||||
isActive: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
export default CSSModules(StarButton, styles)
|
||||
20
browser/main/Detail/StarButton.styl
Normal file
20
browser/main/Detail/StarButton.styl
Normal file
@@ -0,0 +1,20 @@
|
||||
.root
|
||||
left 7px
|
||||
top 0
|
||||
padding 0
|
||||
&:hover
|
||||
.icon
|
||||
transform rotate(-72deg)
|
||||
.tooltip
|
||||
opacity 1
|
||||
|
||||
.root--active
|
||||
@extend .root
|
||||
color $ui-favorite-star-button-color
|
||||
&:hover
|
||||
color $ui-favorite-star-button-color
|
||||
.icon
|
||||
transform rotate(-72deg)
|
||||
|
||||
.icon
|
||||
transition transform 0.15s
|
||||
148
browser/main/Detail/TagSelect.js
Normal file
148
browser/main/Detail/TagSelect.js
Normal file
@@ -0,0 +1,148 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './TagSelect.styl'
|
||||
import _ from 'lodash'
|
||||
|
||||
class TagSelect extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
newTag: ''
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.value = this.props.value
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
this.value = this.props.value
|
||||
}
|
||||
|
||||
handleNewTagInputKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
case 9:
|
||||
e.preventDefault()
|
||||
this.submitTag()
|
||||
break
|
||||
case 13:
|
||||
this.submitTag()
|
||||
break
|
||||
case 8:
|
||||
if (this.refs.newTag.value.length === 0) {
|
||||
this.removeLastTag()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeLastTag () {
|
||||
let { value } = this.props
|
||||
|
||||
value = _.isArray(value)
|
||||
? value.slice()
|
||||
: []
|
||||
value.pop()
|
||||
value = _.uniq(value)
|
||||
|
||||
this.value = value
|
||||
this.props.onChange()
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.setState({
|
||||
newTag: ''
|
||||
})
|
||||
}
|
||||
|
||||
submitTag () {
|
||||
let { value } = this.props
|
||||
let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
|
||||
|
||||
if (newTag.length <= 0) {
|
||||
this.setState({
|
||||
newTag: ''
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
value = _.isArray(value)
|
||||
? value.slice()
|
||||
: []
|
||||
value.push(newTag)
|
||||
value = _.uniq(value)
|
||||
|
||||
this.setState({
|
||||
newTag: ''
|
||||
}, () => {
|
||||
this.value = value
|
||||
this.props.onChange()
|
||||
})
|
||||
}
|
||||
|
||||
handleNewTagInputChange (e) {
|
||||
this.setState({
|
||||
newTag: this.refs.newTag.value
|
||||
})
|
||||
}
|
||||
|
||||
handleTagRemoveButtonClick (tag) {
|
||||
return (e) => {
|
||||
let { value } = this.props
|
||||
|
||||
value.splice(value.indexOf(tag), 1)
|
||||
value = _.uniq(value)
|
||||
|
||||
this.value = value
|
||||
this.props.onChange()
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { value, className } = this.props
|
||||
|
||||
let tagList = _.isArray(value)
|
||||
? value.map((tag) => {
|
||||
return (
|
||||
<span styleName='tag'
|
||||
key={tag}
|
||||
>
|
||||
<span styleName='tag-label'>{tag}</span>
|
||||
<button styleName='tag-removeButton'
|
||||
onClick={(e) => this.handleTagRemoveButtonClick(tag)(e)}
|
||||
>
|
||||
<i className='fa fa-times fa-fw tag-removeButton-icon' />
|
||||
</button>
|
||||
</span>
|
||||
)
|
||||
})
|
||||
: []
|
||||
|
||||
return (
|
||||
<div className={_.isString(className)
|
||||
? 'TagSelect ' + className
|
||||
: 'TagSelect'
|
||||
}
|
||||
styleName='root'
|
||||
>
|
||||
{tagList}
|
||||
<input styleName='newTag'
|
||||
ref='newTag'
|
||||
value={this.state.newTag}
|
||||
placeholder='Add tag...'
|
||||
onChange={(e) => this.handleNewTagInputChange(e)}
|
||||
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TagSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.arrayOf(PropTypes.string),
|
||||
onChange: PropTypes.func
|
||||
|
||||
}
|
||||
|
||||
export default CSSModules(TagSelect, styles)
|
||||
110
browser/main/Detail/TagSelect.styl
Normal file
110
browser/main/Detail/TagSelect.styl
Normal file
@@ -0,0 +1,110 @@
|
||||
.root
|
||||
display inline-block
|
||||
top 19px
|
||||
user-select none
|
||||
vertical-align middle
|
||||
width 300px
|
||||
overflow-x scroll
|
||||
white-space nowrap
|
||||
|
||||
.root::-webkit-scrollbar
|
||||
display none
|
||||
|
||||
.tag
|
||||
display inline-block
|
||||
margin 0 2px
|
||||
padding-left 10px
|
||||
vertical-align middle
|
||||
height 20px
|
||||
background-color $ui-tag-backgroundColor
|
||||
border-radius 20px
|
||||
overflow hidden
|
||||
clearfix()
|
||||
|
||||
.tag-removeButton
|
||||
float right
|
||||
height 20px
|
||||
width 18px
|
||||
margin 0
|
||||
padding 0
|
||||
border-style solid
|
||||
border-width 0
|
||||
border-radius 20px
|
||||
line-height 18px
|
||||
background-color transparent
|
||||
color $ui-button-color
|
||||
|
||||
.tag-removeButton-icon
|
||||
width 5px
|
||||
padding-right 4px
|
||||
|
||||
.tag-label
|
||||
font-size 12px
|
||||
font-weight bold
|
||||
color: #FFFFFF
|
||||
float left
|
||||
height 20px
|
||||
line-height 20px
|
||||
padding 0 6px
|
||||
|
||||
.newTag
|
||||
display inline-block
|
||||
margin 0 2px
|
||||
vertical-align middle
|
||||
height 24px
|
||||
box-sizing borde-box
|
||||
border none
|
||||
border-bottom $ui-border
|
||||
background-color transparent
|
||||
outline none
|
||||
padding 0 4px
|
||||
&:focus
|
||||
border-color $ui-input--focus-borderColor = #369DCD
|
||||
&:disabled
|
||||
background-color $ui-input--disabled-backgroundColor = #DDD
|
||||
|
||||
.add-tag-button
|
||||
display inline
|
||||
margin-left 5px
|
||||
width 20px
|
||||
height 20px
|
||||
border none
|
||||
border-radius 20px
|
||||
padding 0
|
||||
color #FFFFFF
|
||||
&:hover
|
||||
background-color rgba(0, 0, 0, 0.3)
|
||||
&:active, &:active:hover
|
||||
background-color rgba(0, 0, 0, 0.5)
|
||||
color $ui-button--active-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.icon
|
||||
color $ui-dark-button-color
|
||||
|
||||
.tag
|
||||
background-color $ui-dark-tag-backgroundColor
|
||||
|
||||
.tag-removeButton
|
||||
border-color $ui-button--focus-borderColor
|
||||
background-color transparent
|
||||
color $ui-button-color
|
||||
|
||||
.tag-label
|
||||
color $ui-dark-text-color
|
||||
|
||||
.newTag
|
||||
border-color $ui-dark-borderColor
|
||||
background-color transparent
|
||||
color $ui-dark-text-color
|
||||
&:focus
|
||||
border-color $ui-input--focus-borderColor = #369DCD
|
||||
&:disabled
|
||||
background-color $ui-input--disabled-backgroundColor = #DDD
|
||||
|
||||
.add-tag-button
|
||||
&:hover
|
||||
background-color rgba(255, 255, 255, 0.3)
|
||||
&:active, &:active:hover
|
||||
background-color rgba(255, 255, 255, 0.5)
|
||||
color $ui-button--active-color
|
||||
103
browser/main/Detail/index.js
Normal file
103
browser/main/Detail/index.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './Detail.styl'
|
||||
import _ from 'lodash'
|
||||
import MarkdownNoteDetail from './MarkdownNoteDetail'
|
||||
import SnippetNoteDetail from './SnippetNoteDetail'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import StatusBar from '../StatusBar'
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
class Detail extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.focusHandler = () => {
|
||||
this.refs.root != null && this.refs.root.focus()
|
||||
}
|
||||
this.deleteHandler = () => {
|
||||
this.refs.root != null && this.refs.root.handleDeleteMenuClick()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
ee.on('detail:focus', this.focusHandler)
|
||||
ee.on('detail:delete', this.deleteHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
ee.off('detail:focus', this.focusHandler)
|
||||
ee.off('detail:delete', this.deleteHandler)
|
||||
}
|
||||
|
||||
render () {
|
||||
let { location, data, config } = this.props
|
||||
let note = null
|
||||
if (location.query.key != null) {
|
||||
let splitted = location.query.key.split('-')
|
||||
let storageKey = splitted.shift()
|
||||
let noteKey = splitted.shift()
|
||||
|
||||
note = data.noteMap.get(storageKey + '-' + noteKey)
|
||||
}
|
||||
|
||||
if (note == null) {
|
||||
return (
|
||||
<div styleName='root'
|
||||
style={this.props.style}
|
||||
tabIndex='0'
|
||||
>
|
||||
<div styleName='empty'>
|
||||
<div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br />to create a new post</div>
|
||||
</div>
|
||||
<StatusBar
|
||||
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (note.type === 'SNIPPET_NOTE') {
|
||||
return (
|
||||
<SnippetNoteDetail
|
||||
note={note}
|
||||
config={config}
|
||||
ref='root'
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
'style',
|
||||
'ignorePreviewPointerEvents',
|
||||
'location'
|
||||
])}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<MarkdownNoteDetail
|
||||
note={note}
|
||||
config={config}
|
||||
ref='root'
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
'style',
|
||||
'ignorePreviewPointerEvents',
|
||||
'location'
|
||||
])}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Detail.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
style: PropTypes.shape({
|
||||
left: PropTypes.number
|
||||
}),
|
||||
ignorePreviewPointerEvents: PropTypes.bool
|
||||
}
|
||||
|
||||
export default CSSModules(Detail, styles)
|
||||
233
browser/main/Main.js
Normal file
233
browser/main/Main.js
Normal file
@@ -0,0 +1,233 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './Main.styl'
|
||||
import { connect } from 'react-redux'
|
||||
import SideNav from './SideNav'
|
||||
import TopBar from './TopBar'
|
||||
import NoteList from './NoteList'
|
||||
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 mixpanel from 'browser/main/lib/mixpanel'
|
||||
|
||||
function focused () {
|
||||
mixpanel.track('MAIN_FOCUSED')
|
||||
}
|
||||
|
||||
class Main extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
let { config } = props
|
||||
|
||||
this.state = {
|
||||
isRightSliderFocused: false,
|
||||
listWidth: config.listWidth,
|
||||
navWidth: config.navWidth,
|
||||
isLeftSliderFocused: false
|
||||
}
|
||||
}
|
||||
|
||||
getChildContext () {
|
||||
let { status, config } = this.props
|
||||
|
||||
return {
|
||||
status,
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
let { dispatch, config } = this.props
|
||||
|
||||
if (config.ui.theme === 'dark') {
|
||||
document.body.setAttribute('data-theme', 'dark')
|
||||
} else {
|
||||
document.body.setAttribute('data-theme', 'default')
|
||||
}
|
||||
|
||||
// Reload all data
|
||||
dataApi.init()
|
||||
.then((data) => {
|
||||
dispatch({
|
||||
type: 'INIT_ALL',
|
||||
storages: data.storages,
|
||||
notes: data.notes
|
||||
})
|
||||
|
||||
if (data.storages.length < 1) {
|
||||
modal.open(InitModal)
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener('focus', focused)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('focus', focused)
|
||||
}
|
||||
|
||||
handleLeftSlideMouseDown (e) {
|
||||
e.preventDefault()
|
||||
this.setState({
|
||||
isLeftSliderFocused: true
|
||||
})
|
||||
}
|
||||
|
||||
handleRightSlideMouseDown (e) {
|
||||
e.preventDefault()
|
||||
this.setState({
|
||||
isRightSliderFocused: true
|
||||
})
|
||||
}
|
||||
|
||||
handleMouseUp (e) {
|
||||
// Change width of NoteList component.
|
||||
if (this.state.isRightSliderFocused) {
|
||||
this.setState({
|
||||
isRightSliderFocused: false
|
||||
}, () => {
|
||||
let { dispatch } = this.props
|
||||
let newListWidth = this.state.listWidth
|
||||
// TODO: ConfigManager should dispatch itself.
|
||||
ConfigManager.set({listWidth: newListWidth})
|
||||
dispatch({
|
||||
type: 'SET_LIST_WIDTH',
|
||||
listWidth: newListWidth
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Change width of SideNav component.
|
||||
if (this.state.isLeftSliderFocused) {
|
||||
this.setState({
|
||||
isLeftSliderFocused: false
|
||||
}, () => {
|
||||
let { dispatch } = this.props
|
||||
let navWidth = this.state.navWidth
|
||||
// TODO: ConfigManager should dispatch itself.
|
||||
ConfigManager.set({ navWidth })
|
||||
dispatch({
|
||||
type: 'SET_NAV_WIDTH',
|
||||
navWidth
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseMove (e) {
|
||||
if (this.state.isRightSliderFocused) {
|
||||
let offset = this.refs.body.getBoundingClientRect().left
|
||||
let newListWidth = e.pageX - offset
|
||||
if (newListWidth < 10) {
|
||||
newListWidth = 10
|
||||
} else if (newListWidth > 600) {
|
||||
newListWidth = 600
|
||||
}
|
||||
this.setState({
|
||||
listWidth: newListWidth
|
||||
})
|
||||
}
|
||||
if (this.state.isLeftSliderFocused) {
|
||||
let navWidth = e.pageX
|
||||
if (navWidth < 80) {
|
||||
navWidth = 80
|
||||
} else if (navWidth > 600) {
|
||||
navWidth = 600
|
||||
}
|
||||
this.setState({
|
||||
navWidth: navWidth
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { config } = this.props
|
||||
|
||||
return (
|
||||
<div
|
||||
className='Main'
|
||||
styleName='root'
|
||||
onMouseMove={(e) => this.handleMouseMove(e)}
|
||||
onMouseUp={(e) => this.handleMouseUp(e)}
|
||||
>
|
||||
<SideNav
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
'config',
|
||||
'location'
|
||||
])}
|
||||
width={this.state.navWidth}
|
||||
/>
|
||||
{!config.isSideNavFolded &&
|
||||
<div styleName={this.state.isLeftSliderFocused ? 'slider--active' : 'slider'}
|
||||
style={{left: this.state.navWidth}}
|
||||
onMouseDown={(e) => this.handleLeftSlideMouseDown(e)}
|
||||
draggable='false'
|
||||
>
|
||||
<div styleName='slider-hitbox' />
|
||||
</div>
|
||||
}
|
||||
<div styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
|
||||
ref='body'
|
||||
style={{left: config.isSideNavFolded ? 44 : this.state.navWidth}}
|
||||
>
|
||||
<TopBar style={{width: this.state.listWidth}}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'config',
|
||||
'data',
|
||||
'params',
|
||||
'location'
|
||||
])}
|
||||
/>
|
||||
<NoteList style={{width: this.state.listWidth}}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
'config',
|
||||
'params',
|
||||
'location'
|
||||
])}
|
||||
/>
|
||||
<div styleName={this.state.isRightSliderFocused ? 'slider-right--active' : 'slider-right'}
|
||||
style={{left: this.state.listWidth - 1}}
|
||||
onMouseDown={(e) => this.handleRightSlideMouseDown(e)}
|
||||
draggable='false'
|
||||
>
|
||||
<div styleName='slider-hitbox' />
|
||||
</div>
|
||||
<Detail
|
||||
style={{left: this.state.listWidth}}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
'config',
|
||||
'params',
|
||||
'location'
|
||||
])}
|
||||
ignorePreviewPointerEvents={this.state.isRightSliderFocused}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Main.childContextTypes = {
|
||||
status: PropTypes.shape({
|
||||
updateReady: PropTypes.bool.isRequired
|
||||
}).isRequired,
|
||||
config: PropTypes.shape({}).isRequired
|
||||
}
|
||||
|
||||
Main.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
data: PropTypes.shape({}).isRequired
|
||||
}
|
||||
|
||||
export default connect((x) => x)(CSSModules(Main, styles))
|
||||
41
browser/main/Main.styl
Normal file
41
browser/main/Main.styl
Normal file
@@ -0,0 +1,41 @@
|
||||
.root
|
||||
absolute top left bottom right
|
||||
|
||||
.body
|
||||
absolute right top bottom
|
||||
left $sideNav-width
|
||||
|
||||
.body--expanded
|
||||
@extend .body
|
||||
left $sideNav--folded-width
|
||||
|
||||
.slider
|
||||
absolute top bottom
|
||||
top -2px
|
||||
width 0
|
||||
|
||||
.slider-right
|
||||
@extend .slider
|
||||
width 1px
|
||||
|
||||
.slider--active
|
||||
@extend .slider
|
||||
|
||||
.slider-right--active
|
||||
@extend .slider-right
|
||||
|
||||
.slider-hitbox
|
||||
absolute top bottom left right
|
||||
width 7px
|
||||
left -3px
|
||||
z-index 10
|
||||
cursor col-resize
|
||||
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
absolute top left bottom right
|
||||
|
||||
.slider-right
|
||||
.slider-right--active
|
||||
box-shadow none
|
||||
70
browser/main/NoteList/NoteList.styl
Normal file
70
browser/main/NoteList/NoteList.styl
Normal file
@@ -0,0 +1,70 @@
|
||||
$control-height = 30px
|
||||
|
||||
.root
|
||||
absolute left bottom
|
||||
top $topBar-height - 1
|
||||
background-color $ui-noteList-backgroundColor
|
||||
|
||||
.control
|
||||
absolute top left right
|
||||
user-select none
|
||||
height $control-height
|
||||
font-size 12px
|
||||
line-height 25px
|
||||
display flex
|
||||
background-color $ui-noteList-backgroundColor
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.control-sortBy
|
||||
flex 1
|
||||
padding-left 25px
|
||||
|
||||
.control-sortBy-select
|
||||
margin-left 0
|
||||
font-size 12px
|
||||
color $ui-inactive-text-color
|
||||
padding 0
|
||||
border none
|
||||
background-color transparent
|
||||
font-size 10px
|
||||
|
||||
.control-button
|
||||
width 25px
|
||||
padding 0
|
||||
background-color transparent
|
||||
border none
|
||||
color $ui-inactive-text-color
|
||||
transition 0.15s
|
||||
&:active, &:active:hover
|
||||
color $ui-active-color
|
||||
&:hover
|
||||
color $ui-text-color
|
||||
|
||||
.control-button--active
|
||||
@extend .control-button
|
||||
color $ui-active-color
|
||||
&:hover
|
||||
color $ui-active-color
|
||||
|
||||
.list
|
||||
absolute left right bottom
|
||||
top $control-height
|
||||
overflow auto
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
.control
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
.control
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.control-button
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
|
||||
432
browser/main/NoteList/index.js
Normal file
432
browser/main/NoteList/index.js
Normal file
@@ -0,0 +1,432 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
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 ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import NoteItem from 'browser/components/NoteItem'
|
||||
import NoteItemSimple from 'browser/components/NoteItemSimple'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { Menu, MenuItem, dialog } = remote
|
||||
|
||||
function sortByCreatedAt (a, b) {
|
||||
return new Date(b.createdAt) - new Date(a.createdAt)
|
||||
}
|
||||
|
||||
function sortByAlphabetical (a, b) {
|
||||
return a.title.localeCompare(b.title)
|
||||
}
|
||||
|
||||
function sortByUpdatedAt (a, b) {
|
||||
return new Date(b.updatedAt) - new Date(a.updatedAt)
|
||||
}
|
||||
|
||||
class NoteList extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.selectNextNoteHandler = () => {
|
||||
console.log('fired next')
|
||||
this.selectNextNote()
|
||||
}
|
||||
this.selectPriorNoteHandler = () => {
|
||||
this.selectPriorNote()
|
||||
}
|
||||
this.focusHandler = () => {
|
||||
this.refs.list.focus()
|
||||
}
|
||||
this.alertIfSnippetHnalder = () => {
|
||||
this.alertIfSnippet()
|
||||
}
|
||||
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
|
||||
ee.on('list:next', this.selectNextNoteHandler)
|
||||
ee.on('list:prior', this.selectPriorNoteHandler)
|
||||
ee.on('list:focus', this.focusHandler)
|
||||
ee.on('list:isMarkdownNote', this.alertIfSnippetHnalder)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.location.pathname !== this.props.location.pathname) {
|
||||
this.resetScroll()
|
||||
}
|
||||
}
|
||||
|
||||
resetScroll () {
|
||||
this.refs.list.scrollTop = 0
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearInterval(this.refreshTimer)
|
||||
|
||||
ee.off('list:next', this.selectNextNoteHandler)
|
||||
ee.off('list:prior', this.selectPriorNoteHandler)
|
||||
ee.off('list:focus', this.focusHandler)
|
||||
ee.off('list:isMarkdownNote', this.alertIfSnippetHnalder)
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
let { location } = this.props
|
||||
|
||||
if (this.notes.length > 0 && location.query.key == null) {
|
||||
let { router } = this.context
|
||||
router.replace({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: this.notes[0].storage + '-' + this.notes[0].key
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Auto scroll
|
||||
if (_.isString(location.query.key) && prevProps.location.query.key === location.query.key) {
|
||||
let targetIndex = _.findIndex(this.notes, (note) => {
|
||||
return note != null && note.storage + '-' + note.key === location.query.key
|
||||
})
|
||||
if (targetIndex > -1) {
|
||||
let list = this.refs.list
|
||||
let item = list.childNodes[targetIndex]
|
||||
|
||||
if (item == null) return false
|
||||
|
||||
let overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
|
||||
if (overflowBelow) {
|
||||
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
|
||||
}
|
||||
let overflowAbove = list.scrollTop > item.offsetTop
|
||||
if (overflowAbove) {
|
||||
list.scrollTop = item.offsetTop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectPriorNote () {
|
||||
if (this.notes == null || this.notes.length === 0) {
|
||||
return
|
||||
}
|
||||
let { router } = this.context
|
||||
let { location } = this.props
|
||||
|
||||
let targetIndex = _.findIndex(this.notes, (note) => {
|
||||
return note.storage + '-' + note.key === location.query.key
|
||||
})
|
||||
|
||||
if (targetIndex === 0) {
|
||||
return
|
||||
}
|
||||
targetIndex--
|
||||
if (targetIndex < 0) targetIndex = 0
|
||||
|
||||
router.push({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
selectNextNote () {
|
||||
if (this.notes == null || this.notes.length === 0) {
|
||||
return
|
||||
}
|
||||
let { router } = this.context
|
||||
let { location } = this.props
|
||||
|
||||
let targetIndex = _.findIndex(this.notes, (note) => {
|
||||
return note.storage + '-' + note.key === location.query.key
|
||||
})
|
||||
|
||||
if (targetIndex === this.notes.length - 1) {
|
||||
targetIndex = 0
|
||||
} else {
|
||||
targetIndex++
|
||||
if (targetIndex < 0) targetIndex = 0
|
||||
else if (targetIndex > this.notes.length - 1) targetIndex === this.notes.length - 1
|
||||
}
|
||||
|
||||
router.push({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key
|
||||
}
|
||||
})
|
||||
ee.emit('list:moved')
|
||||
}
|
||||
|
||||
handleNoteListKeyDown (e) {
|
||||
if (e.metaKey || e.ctrlKey) return true
|
||||
|
||||
if (e.keyCode === 65 && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
ee.emit('top:new-note')
|
||||
}
|
||||
|
||||
if (e.keyCode === 68) {
|
||||
e.preventDefault()
|
||||
ee.emit('detail:delete')
|
||||
}
|
||||
|
||||
if (e.keyCode === 69) {
|
||||
e.preventDefault()
|
||||
ee.emit('detail:focus')
|
||||
}
|
||||
|
||||
if (e.keyCode === 38) {
|
||||
e.preventDefault()
|
||||
this.selectPriorNote()
|
||||
}
|
||||
|
||||
if (e.keyCode === 40) {
|
||||
e.preventDefault()
|
||||
this.selectNextNote()
|
||||
}
|
||||
}
|
||||
|
||||
getNotes () {
|
||||
let { data, params, location } = this.props
|
||||
|
||||
if (location.pathname.match(/\/home/)) {
|
||||
return data.noteMap.map((note) => note)
|
||||
}
|
||||
|
||||
if (location.pathname.match(/\/starred/)) {
|
||||
return data.starredSet.toJS()
|
||||
.map((uniqueKey) => data.noteMap.get(uniqueKey))
|
||||
}
|
||||
|
||||
let storageKey = params.storageKey
|
||||
let folderKey = params.folderKey
|
||||
let storage = data.storageMap.get(storageKey)
|
||||
if (storage == null) return []
|
||||
|
||||
let folder = _.find(storage.folders, {key: folderKey})
|
||||
if (folder == null) {
|
||||
let storageNoteSet = data.storageNoteMap
|
||||
.get(storage.key)
|
||||
if (storageNoteSet == null) storageNoteSet = []
|
||||
return storageNoteSet
|
||||
.map((uniqueKey) => data.noteMap.get(uniqueKey))
|
||||
}
|
||||
|
||||
let folderNoteKeyList = data.folderNoteMap
|
||||
.get(storage.key + '-' + folder.key)
|
||||
|
||||
return folderNoteKeyList != null
|
||||
? folderNoteKeyList
|
||||
.map((uniqueKey) => data.noteMap.get(uniqueKey))
|
||||
: []
|
||||
}
|
||||
|
||||
handleNoteClick (e, uniqueKey) {
|
||||
let { router } = this.context
|
||||
let { location } = this.props
|
||||
|
||||
router.push({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: uniqueKey
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleNoteContextMenu (e, uniqueKey) {
|
||||
let menu = new Menu()
|
||||
menu.append(new MenuItem({
|
||||
label: 'Delete Note',
|
||||
click: (e) => this.handleDeleteNote(e, uniqueKey)
|
||||
}))
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
handleDeleteNote (e, uniqueKey) {
|
||||
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Delete a note',
|
||||
detail: 'This work cannot be undone.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
if (index === 0) {
|
||||
let { dispatch, location } = this.props
|
||||
let splitted = uniqueKey.split('-')
|
||||
let storageKey = splitted.shift()
|
||||
let noteKey = splitted.shift()
|
||||
|
||||
dataApi
|
||||
.deleteNote(storageKey, noteKey)
|
||||
.then((data) => {
|
||||
let dispatchHandler = () => {
|
||||
dispatch({
|
||||
type: 'DELETE_NOTE',
|
||||
storageKey: data.storageKey,
|
||||
noteKey: data.noteKey
|
||||
})
|
||||
}
|
||||
|
||||
if (location.query.key === uniqueKey) {
|
||||
ee.once('list:moved', dispatchHandler)
|
||||
ee.emit('list:next')
|
||||
} else {
|
||||
dispatchHandler()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleSortByChange (e) {
|
||||
let { dispatch } = this.props
|
||||
|
||||
let config = {
|
||||
sortBy: e.target.value
|
||||
}
|
||||
|
||||
ConfigManager.set(config)
|
||||
dispatch({
|
||||
type: 'SET_CONFIG',
|
||||
config
|
||||
})
|
||||
}
|
||||
|
||||
handleListStyleButtonClick (e, style) {
|
||||
let { dispatch } = this.props
|
||||
|
||||
let config = {
|
||||
listStyle: style
|
||||
}
|
||||
|
||||
ConfigManager.set(config)
|
||||
dispatch({
|
||||
type: 'SET_CONFIG',
|
||||
config
|
||||
})
|
||||
}
|
||||
|
||||
alertIfSnippet() {
|
||||
let { location } = this.props
|
||||
const targetIndex = _.findIndex(this.notes, (note) => {
|
||||
return `${note.storage}-${note.key}` === location.query.key
|
||||
})
|
||||
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.'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { location, notes, config } = this.props
|
||||
let sortFunc = config.sortBy === 'CREATED_AT'
|
||||
? sortByCreatedAt
|
||||
: config.sortBy === 'ALPHABETICAL'
|
||||
? sortByAlphabetical
|
||||
: sortByUpdatedAt
|
||||
this.notes = notes = this.getNotes()
|
||||
.sort(sortFunc)
|
||||
|
||||
let noteList = notes
|
||||
.map(note => {
|
||||
if (note == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isDefault = config.listStyle === 'DEFAULT'
|
||||
const isActive = location.query.key === note.storage + '-' + note.key
|
||||
const dateDisplay = moment(
|
||||
config.sortBy === 'CREATED_AT'
|
||||
? note.createdAt : note.updatedAt
|
||||
).fromNow()
|
||||
const key = `${note.storage}-${note.key}`
|
||||
|
||||
if (isDefault) {
|
||||
return (
|
||||
<NoteItem
|
||||
isActive={isActive}
|
||||
note={note}
|
||||
dateDisplay={dateDisplay}
|
||||
key={key}
|
||||
handleNoteClick={this.handleNoteClick.bind(this)}
|
||||
handleNoteContextMenu={this.handleNoteContextMenu.bind(this)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<NoteItemSimple
|
||||
isActive={isActive}
|
||||
note={note}
|
||||
key={key}
|
||||
handleNoteClick={this.handleNoteClick.bind(this)}
|
||||
handleNoteContextMenu={this.handleNoteContextMenu.bind(this)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='NoteList'
|
||||
styleName='root'
|
||||
style={this.props.style}
|
||||
>
|
||||
<div styleName='control'>
|
||||
<div styleName='control-sortBy'>
|
||||
Sort by
|
||||
<select styleName='control-sortBy-select'
|
||||
value={config.sortBy}
|
||||
onChange={(e) => this.handleSortByChange(e)}
|
||||
>
|
||||
<option value='UPDATED_AT'>Updated Time</option>
|
||||
<option value='CREATED_AT'>Created Time</option>
|
||||
<option value='ALPHABETICAL'>Alphabetical</option>
|
||||
</select>
|
||||
</div>
|
||||
<button styleName={config.listStyle === 'DEFAULT'
|
||||
? 'control-button--active'
|
||||
: 'control-button'
|
||||
}
|
||||
onClick={(e) => this.handleListStyleButtonClick(e, 'DEFAULT')}
|
||||
>
|
||||
<i className='fa fa-th-large' />
|
||||
</button>
|
||||
<button styleName={config.listStyle === 'SMALL'
|
||||
? 'control-button--active'
|
||||
: 'control-button'
|
||||
}
|
||||
onClick={(e) => this.handleListStyleButtonClick(e, 'SMALL')}
|
||||
>
|
||||
<i className='fa fa-list-ul' />
|
||||
</button>
|
||||
</div>
|
||||
<div styleName='list'
|
||||
ref='list'
|
||||
tabIndex='-1'
|
||||
onKeyDown={(e) => this.handleNoteListKeyDown(e)}
|
||||
>
|
||||
{noteList}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
NoteList.contextTypes = {
|
||||
router: PropTypes.shape([])
|
||||
}
|
||||
|
||||
NoteList.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
repositories: PropTypes.array,
|
||||
style: PropTypes.shape({
|
||||
width: PropTypes.number
|
||||
})
|
||||
}
|
||||
|
||||
export default CSSModules(NoteList, styles)
|
||||
119
browser/main/SideNav/SideNav.styl
Normal file
119
browser/main/SideNav/SideNav.styl
Normal file
@@ -0,0 +1,119 @@
|
||||
.root
|
||||
absolute top left bottom
|
||||
width $sideNav-width
|
||||
background-color $ui-backgroundColor
|
||||
user-select none
|
||||
color $ui-text-color
|
||||
|
||||
.top
|
||||
height $topBar-height
|
||||
|
||||
.top-menu
|
||||
navButtonColor()
|
||||
height $topBar-height
|
||||
padding 0 15px
|
||||
font-size 14px
|
||||
width 100%
|
||||
text-align left
|
||||
|
||||
.top-menu-label
|
||||
margin-left 5px
|
||||
overflow ellipsis
|
||||
|
||||
.storageList
|
||||
absolute left right
|
||||
bottom 37px
|
||||
top 160px
|
||||
overflow-y auto
|
||||
|
||||
.storageList-empty
|
||||
padding 0 10px
|
||||
margin-top 15px
|
||||
line-height 24px
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.navToggle
|
||||
navButtonColor()
|
||||
display block
|
||||
position absolute
|
||||
right 5px
|
||||
bottom 5px
|
||||
border-radius 16.5px
|
||||
height 34px
|
||||
width 34px
|
||||
line-height 32px
|
||||
padding 0
|
||||
|
||||
.root--folded
|
||||
@extend .root
|
||||
width 44px
|
||||
.storageList-empty
|
||||
white-space nowrap
|
||||
transform rotate(90deg)
|
||||
.top-menu
|
||||
width 44px - 1
|
||||
text-align center
|
||||
&:hover .top-menu-label
|
||||
transition opacity 0.15s
|
||||
opacity 1
|
||||
.top-menu-label
|
||||
position fixed
|
||||
display inline-block
|
||||
height 30px
|
||||
left 32px
|
||||
padding 0 10px
|
||||
margin-top -8px
|
||||
opacity 0
|
||||
margin-left 0
|
||||
overflow hidden
|
||||
background-color $ui-tooltip-backgroundColor
|
||||
z-index 10
|
||||
color white
|
||||
line-height 30px
|
||||
border-top-right-radius 2px
|
||||
border-bottom-right-radius 2px
|
||||
pointer-events none
|
||||
font-size 12px
|
||||
.menu-button, .menu-button--active
|
||||
text-align center
|
||||
&:hover .menu-button-label
|
||||
transition opacity 0.15s
|
||||
opacity 1
|
||||
|
||||
.menu-button-label
|
||||
position fixed
|
||||
display inline-block
|
||||
height 32px
|
||||
left 44px
|
||||
padding 0 10px
|
||||
margin-top -8px
|
||||
margin-left 0
|
||||
overflow ellipsis
|
||||
background-color $ui-tooltip-backgroundColor
|
||||
z-index 10
|
||||
color white
|
||||
line-height 32px
|
||||
border-top-right-radius 2px
|
||||
border-bottom-right-radius 2px
|
||||
pointer-events none
|
||||
opacity 0
|
||||
font-size 12px
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root, .root--folded
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
.top
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.top-menu
|
||||
navDarkButtonColor()
|
||||
|
||||
.storageList-empty
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
.navToggle
|
||||
navDarkButtonColor()
|
||||
|
||||
215
browser/main/SideNav/StorageItem.js
Normal file
215
browser/main/SideNav/StorageItem.js
Normal file
@@ -0,0 +1,215 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './StorageItem.styl'
|
||||
import { hashHistory } from 'react-router'
|
||||
import modal from 'browser/main/lib/modal'
|
||||
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'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { Menu, MenuItem, dialog } = remote
|
||||
|
||||
class StorageItem extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isOpen: true
|
||||
}
|
||||
}
|
||||
|
||||
handleHeaderContextMenu (e) {
|
||||
let 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)
|
||||
}))
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
handleUnlinkStorageClick (e) {
|
||||
let 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']
|
||||
})
|
||||
|
||||
if (index === 0) {
|
||||
let { storage, dispatch } = this.props
|
||||
dataApi.removeStorage(storage.key)
|
||||
.then(() => {
|
||||
dispatch({
|
||||
type: 'REMOVE_STORAGE',
|
||||
storageKey: storage.key
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleButtonClick (e) {
|
||||
this.setState({
|
||||
isOpen: !this.state.isOpen
|
||||
})
|
||||
}
|
||||
|
||||
handleAddFolderButtonClick (e) {
|
||||
let { storage } = this.props
|
||||
|
||||
modal.open(CreateFolderModal, {
|
||||
storage
|
||||
})
|
||||
}
|
||||
|
||||
handleHeaderInfoClick (e) {
|
||||
let { storage } = this.props
|
||||
hashHistory.push('/storages/' + storage.key)
|
||||
}
|
||||
|
||||
handleFolderButtonClick (folderKey) {
|
||||
return (e) => {
|
||||
let { storage } = this.props
|
||||
hashHistory.push('/storages/' + storage.key + '/folders/' + folderKey)
|
||||
}
|
||||
}
|
||||
|
||||
handleFolderButtonContextMenu (e, folder) {
|
||||
let 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)
|
||||
}))
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
handleRenameFolderClick (e, folder) {
|
||||
let { storage } = this.props
|
||||
modal.open(RenameFolderModal, {
|
||||
storage,
|
||||
folder
|
||||
})
|
||||
}
|
||||
|
||||
handleFolderDeleteClick (e, folder) {
|
||||
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Delete Folder',
|
||||
detail: 'This work will deletes all notes in the folder and can not be undone.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
|
||||
if (index === 0) {
|
||||
let { storage, dispatch } = this.props
|
||||
dataApi
|
||||
.deleteFolder(storage.key, folder.key)
|
||||
.then((data) => {
|
||||
dispatch({
|
||||
type: 'DELETE_FOLDER',
|
||||
storage: data.storage,
|
||||
folderKey: data.folderKey
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { storage, location, isFolded, data } = this.props
|
||||
let { folderNoteMap } = data
|
||||
let folderList = storage.folders.map((folder) => {
|
||||
let isActive = !!(location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key)))
|
||||
let noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
||||
|
||||
let noteCount = noteSet != null
|
||||
? noteSet.size
|
||||
: 0
|
||||
return (
|
||||
<StorageItemChild
|
||||
key={folder.key}
|
||||
isActive={isActive}
|
||||
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
|
||||
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
|
||||
folderName={folder.name}
|
||||
folderColor={folder.color}
|
||||
isFolded={isFolded}
|
||||
noteCount={noteCount}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
let isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '$'))
|
||||
|
||||
return (
|
||||
<div styleName={isFolded ? 'root--folded' : 'root'}
|
||||
key={storage.key}
|
||||
>
|
||||
<div styleName={isActive
|
||||
? 'header--active'
|
||||
: 'header'
|
||||
}
|
||||
onContextMenu={(e) => this.handleHeaderContextMenu(e)}
|
||||
>
|
||||
<button styleName='header-toggleButton'
|
||||
onMouseDown={(e) => this.handleToggleButtonClick(e)}
|
||||
>
|
||||
<i className={this.state.isOpen
|
||||
? 'fa fa-caret-down'
|
||||
: 'fa fa-caret-right'
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{!isFolded &&
|
||||
<button styleName='header-addFolderButton'
|
||||
onClick={(e) => this.handleAddFolderButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-plus' />
|
||||
</button>
|
||||
}
|
||||
|
||||
<button styleName='header-info'
|
||||
onClick={(e) => this.handleHeaderInfoClick(e)}
|
||||
>
|
||||
<span styleName='header-info-name'>
|
||||
{isFolded ? storage.name.substring(0, 1) : storage.name}
|
||||
</span>
|
||||
{isFolded &&
|
||||
<span styleName='header-info--folded-tooltip'>
|
||||
{storage.name}
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
{this.state.isOpen &&
|
||||
<div styleName='folderList' >
|
||||
{folderList}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
StorageItem.propTypes = {
|
||||
isFolded: PropTypes.bool
|
||||
}
|
||||
|
||||
export default CSSModules(StorageItem, styles)
|
||||
123
browser/main/SideNav/StorageItem.styl
Normal file
123
browser/main/SideNav/StorageItem.styl
Normal file
@@ -0,0 +1,123 @@
|
||||
.root
|
||||
width 100%
|
||||
user-select none
|
||||
|
||||
.header
|
||||
position relative
|
||||
height 26px
|
||||
width 100%
|
||||
margin-bottom 5px
|
||||
transition 0.15s
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
&:active
|
||||
.header-toggleButton
|
||||
.header-addFolderButton
|
||||
color white
|
||||
&:active
|
||||
color $ui-active-color
|
||||
|
||||
.header--active
|
||||
@extend .header
|
||||
.header-info
|
||||
color $ui-button--active-color
|
||||
background-color $ui-button--active-backgroundColor
|
||||
.header-toggleButton
|
||||
.header-addFolderButton
|
||||
color white
|
||||
&:active
|
||||
&:hover
|
||||
&:hover:active
|
||||
color white
|
||||
|
||||
.header-toggleButton
|
||||
position absolute
|
||||
left 0
|
||||
width 25px
|
||||
height 26px
|
||||
padding 0
|
||||
border none
|
||||
color $ui-inactive-text-color
|
||||
background-color transparent
|
||||
&:hover
|
||||
color $ui-text-color
|
||||
&:active
|
||||
color $ui-active-color
|
||||
|
||||
.header-info
|
||||
display block
|
||||
width 100%
|
||||
height 30px
|
||||
padding-left 25px
|
||||
padding-right 10px
|
||||
line-height 26px
|
||||
cursor pointer
|
||||
font-size 14px
|
||||
border none
|
||||
overflow ellipsis
|
||||
text-align left
|
||||
background-color transparent
|
||||
color $ui-inactive-text-color
|
||||
&:active
|
||||
color $ui-button--active-color
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.header-info-path
|
||||
font-size 10px
|
||||
margin 0 5px
|
||||
|
||||
.header-addFolderButton
|
||||
position absolute
|
||||
right 0
|
||||
width 25px
|
||||
height 26px
|
||||
padding 0
|
||||
border none
|
||||
color $ui-inactive-text-color
|
||||
background-color transparent
|
||||
&:hover
|
||||
color $ui-text-color
|
||||
&:active
|
||||
color $ui-active-color
|
||||
|
||||
.root--folded
|
||||
@extend .root
|
||||
.header
|
||||
width 100%
|
||||
.header-info
|
||||
overflow ellipsis
|
||||
padding 0 0 0 18px
|
||||
&:hover .header-info--folded-tooltip
|
||||
opacity 1
|
||||
.header-info-path
|
||||
display none
|
||||
.header-toggleButton
|
||||
width 15px
|
||||
.header-info--folded-tooltip
|
||||
tooltip()
|
||||
position fixed
|
||||
padding 0 10px
|
||||
left 44px
|
||||
z-index 10
|
||||
pointer-events none
|
||||
opacity 0
|
||||
border-top-right-radius 2px
|
||||
border-bottom-right-radius 2px
|
||||
.header-info--folded-tooltip-path
|
||||
font-size 10px
|
||||
margin 0 5px
|
||||
|
||||
body[data-theme="dark"]
|
||||
.header-toggleButton
|
||||
.header-addFolderButton
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
&:active
|
||||
color $ui-dark-active-color
|
||||
.header--active
|
||||
.header-toggleButton
|
||||
.header-addFolderButton
|
||||
color white
|
||||
&:active
|
||||
color white
|
||||
111
browser/main/SideNav/index.js
Normal file
111
browser/main/SideNav/index.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './SideNav.styl'
|
||||
import { openModal } from 'browser/main/lib/modal'
|
||||
import PreferencesModal from '../modals/PreferencesModal'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import StorageItem from './StorageItem'
|
||||
import SideNavFilter from 'browser/components/SideNavFilter'
|
||||
|
||||
class SideNav extends React.Component {
|
||||
// TODO: should not use electron stuff v0.7
|
||||
handleMenuButtonClick (e) {
|
||||
openModal(PreferencesModal)
|
||||
}
|
||||
|
||||
handleHomeButtonClick (e) {
|
||||
let { router } = this.context
|
||||
router.push('/home')
|
||||
}
|
||||
|
||||
handleStarredButtonClick (e) {
|
||||
let { router } = this.context
|
||||
router.push('/starred')
|
||||
}
|
||||
|
||||
handleToggleButtonClick (e) {
|
||||
let { dispatch, config } = this.props
|
||||
|
||||
ConfigManager.set({isSideNavFolded: !config.isSideNavFolded})
|
||||
dispatch({
|
||||
type: 'SET_IS_SIDENAV_FOLDED',
|
||||
isFolded: !config.isSideNavFolded
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
let { data, location, config, dispatch } = this.props
|
||||
|
||||
let isFolded = config.isSideNavFolded
|
||||
let isHomeActive = !!location.pathname.match(/^\/home$/)
|
||||
let isStarredActive = !!location.pathname.match(/^\/starred$/)
|
||||
|
||||
let storageList = data.storageMap.map((storage, key) => {
|
||||
return <StorageItem
|
||||
key={storage.key}
|
||||
storage={storage}
|
||||
data={data}
|
||||
location={location}
|
||||
isFolded={isFolded}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
})
|
||||
let style = {}
|
||||
if (!isFolded) style.width = this.props.width
|
||||
return (
|
||||
<div className='SideNav'
|
||||
styleName={isFolded ? 'root--folded' : 'root'}
|
||||
tabIndex='1'
|
||||
style={style}
|
||||
>
|
||||
<div styleName='top'>
|
||||
<button styleName='top-menu'
|
||||
onClick={(e) => this.handleMenuButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-navicon fa-fw' />
|
||||
<span styleName='top-menu-label'>Menu</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<SideNavFilter
|
||||
isFolded={isFolded}
|
||||
isHomeActive={isHomeActive}
|
||||
handleAllNotesButtonClick={(e) => this.handleHomeButtonClick(e)}
|
||||
isStarredActive={isStarredActive}
|
||||
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
|
||||
/>
|
||||
|
||||
<div styleName='storageList'>
|
||||
{storageList.length > 0 ? storageList : (
|
||||
<div styleName='storageList-empty'>No storage mount.</div>
|
||||
)}
|
||||
</div>
|
||||
<button styleName='navToggle'
|
||||
onClick={(e) => this.handleToggleButtonClick(e)}
|
||||
>
|
||||
{isFolded
|
||||
? <i className='fa fa-angle-double-right' />
|
||||
: <i className='fa fa-angle-double-left' />
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SideNav.contextTypes = {
|
||||
router: PropTypes.shape({})
|
||||
}
|
||||
|
||||
SideNav.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
storages: PropTypes.array,
|
||||
config: PropTypes.shape({
|
||||
isSideNavFolded: PropTypes.bool
|
||||
}),
|
||||
location: PropTypes.shape({
|
||||
pathname: PropTypes.string
|
||||
})
|
||||
}
|
||||
|
||||
export default CSSModules(SideNav, styles)
|
||||
60
browser/main/StatusBar/StatusBar.styl
Normal file
60
browser/main/StatusBar/StatusBar.styl
Normal file
@@ -0,0 +1,60 @@
|
||||
@import('../Detail/DetailVars')
|
||||
|
||||
.root
|
||||
absolute bottom left right
|
||||
height $statusBar-height
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
border-top $ui-border
|
||||
display flex
|
||||
box-shadow $note-detail-box-shadow
|
||||
|
||||
.blank
|
||||
flex 1
|
||||
|
||||
.help
|
||||
navButtonColor()
|
||||
height 24px
|
||||
width 24px
|
||||
border-width 0 0 0 1px
|
||||
border-style solid
|
||||
border-color $ui-borderColor
|
||||
&:active .update-icon
|
||||
color white
|
||||
|
||||
.zoom
|
||||
navButtonColor()
|
||||
height 24px
|
||||
border-width 0 1px
|
||||
border-style solid
|
||||
border-color $ui-borderColor
|
||||
|
||||
.update
|
||||
navButtonColor()
|
||||
height 24px
|
||||
border-width 0 0 0 1px
|
||||
border-style solid
|
||||
border-color $ui-borderColor
|
||||
&:active .update-icon
|
||||
color white
|
||||
|
||||
.update-icon
|
||||
color $brand-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
border-color $ui-dark-borderColor
|
||||
box-shadow none
|
||||
|
||||
.zoom
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.help
|
||||
navButtonColor()
|
||||
border-color $ui-dark-borderColor
|
||||
border-left 1px solid $ui-dark-borderColor
|
||||
|
||||
.update
|
||||
navDarkButtonColor()
|
||||
border-color $ui-dark-borderColor
|
||||
border-left 1px solid $ui-dark-borderColor
|
||||
92
browser/main/StatusBar/index.js
Normal file
92
browser/main/StatusBar/index.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './StatusBar.styl'
|
||||
import ZoomManager from 'browser/main/lib/ZoomManager'
|
||||
import LastUpdatedString from '../Detail/LastUpdatedString'
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote, ipcRenderer } = electron
|
||||
const { Menu, MenuItem, dialog } = remote
|
||||
|
||||
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
|
||||
|
||||
class StatusBar extends React.Component {
|
||||
updateApp () {
|
||||
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Update Boostnote',
|
||||
detail: 'New Boostnote is ready to be installed.',
|
||||
buttons: ['Restart & Install', 'Not Now']
|
||||
})
|
||||
|
||||
if (index === 0) {
|
||||
ipcRenderer.send('update-app-confirm')
|
||||
}
|
||||
}
|
||||
|
||||
handleZoomButtonClick (e) {
|
||||
let menu = new Menu()
|
||||
|
||||
zoomOptions.forEach((zoom) => {
|
||||
menu.append(new MenuItem({
|
||||
label: Math.floor(zoom * 100) + '%',
|
||||
click: () => this.handleZoomMenuItemClick(zoom)
|
||||
}))
|
||||
})
|
||||
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
}
|
||||
|
||||
handleZoomMenuItemClick (zoomFactor) {
|
||||
let { dispatch } = this.props
|
||||
ZoomManager.setZoom(zoomFactor)
|
||||
dispatch({
|
||||
type: 'SET_ZOOM',
|
||||
zoom: zoomFactor
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
let { config, status } = this.context
|
||||
|
||||
return (
|
||||
<div className='StatusBar'
|
||||
styleName='root'
|
||||
>
|
||||
<button styleName='zoom'
|
||||
onClick={(e) => this.handleZoomButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-search-plus' />
|
||||
{Math.floor(config.zoom * 100)}%
|
||||
</button>
|
||||
|
||||
<div styleName='blank' />
|
||||
|
||||
{status.updateReady
|
||||
? <button onClick={this.updateApp} styleName='update'>
|
||||
<i styleName='update-icon' className='fa fa-cloud-download' /> Ready to Update!
|
||||
</button>
|
||||
: null
|
||||
}
|
||||
|
||||
<LastUpdatedString date={this.props.date} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
StatusBar.contextTypes = {
|
||||
status: PropTypes.shape({
|
||||
updateReady: PropTypes.bool.isRequired
|
||||
}).isRequired,
|
||||
config: PropTypes.shape({}).isRequired,
|
||||
date: PropTypes.string
|
||||
}
|
||||
|
||||
StatusBar.propTypes = {
|
||||
config: PropTypes.shape({
|
||||
zoom: PropTypes.number
|
||||
})
|
||||
}
|
||||
|
||||
export default CSSModules(StatusBar, styles)
|
||||
169
browser/main/TopBar/TopBar.styl
Normal file
169
browser/main/TopBar/TopBar.styl
Normal file
@@ -0,0 +1,169 @@
|
||||
.root
|
||||
position relative
|
||||
background-color $ui-noteList-backgroundColor
|
||||
height $topBar-height - 1
|
||||
|
||||
.root--expanded
|
||||
@extend .root
|
||||
|
||||
$control-height = 34px
|
||||
|
||||
.control
|
||||
position absolute
|
||||
top 13px
|
||||
left 8px
|
||||
right 8px
|
||||
height $control-height
|
||||
overflow hidden
|
||||
display flex
|
||||
|
||||
.control-search
|
||||
height 32px
|
||||
flex 1
|
||||
background-color white
|
||||
position relative
|
||||
|
||||
.control-search-icon
|
||||
absolute top bottom left
|
||||
line-height 32px
|
||||
width 35px
|
||||
color $ui-inactive-text-color
|
||||
background-color $ui-noteList-backgroundColor
|
||||
|
||||
.control-search-input
|
||||
display block
|
||||
absolute top bottom right
|
||||
left 30px
|
||||
input
|
||||
width 100%
|
||||
height 100%
|
||||
outline none
|
||||
border none
|
||||
background-color $ui-noteList-backgroundColor
|
||||
|
||||
.control-search-optionList
|
||||
position fixed
|
||||
z-index 200
|
||||
width 275px
|
||||
height 175px
|
||||
overflow-y auto
|
||||
background-color $modal-background
|
||||
border-radius 2px
|
||||
box-shadow 2px 2px 10px gray
|
||||
|
||||
.control-search-optionList-item
|
||||
height 50px
|
||||
border-bottom $ui-border
|
||||
transition background-color 0.15s
|
||||
padding 5px
|
||||
cursor pointer
|
||||
overflow ellipsis
|
||||
&:hover
|
||||
background-color alpha($ui-active-color, 10%)
|
||||
|
||||
.control-search-optionList-item-folder
|
||||
border-left 4px solid transparent
|
||||
padding 2px 5px
|
||||
color $ui-text-color
|
||||
overflow ellipsis
|
||||
font-size 12px
|
||||
height 16px
|
||||
margin-bottom 4px
|
||||
|
||||
.control-search-optionList-item-folder-surfix
|
||||
font-size 10px
|
||||
margin-left 5px
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.control-search-optionList-item-type
|
||||
font-size 12px
|
||||
color $ui-inactive-text-color
|
||||
padding-right 3px
|
||||
|
||||
.control-search-optionList-empty
|
||||
height 150px
|
||||
color $ui-inactive-text-color
|
||||
line-height 150px
|
||||
text-align center
|
||||
|
||||
.control-newPostButton
|
||||
display block
|
||||
width 32px
|
||||
height $control-height - 2
|
||||
navButtonColor()
|
||||
border $ui-border
|
||||
border-radius 32px
|
||||
font-size 14px
|
||||
line-height 28px
|
||||
padding 0
|
||||
&:active
|
||||
border-color $ui-button--active-backgroundColor
|
||||
&:hover .control-newPostButton-tooltip
|
||||
opacity 1
|
||||
|
||||
.control-newPostButton-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
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
.control
|
||||
border-color $ui-dark-borderColor
|
||||
.control-search
|
||||
background-color $dark-background-color
|
||||
|
||||
.control-search-icon
|
||||
absolute top bottom left
|
||||
line-height 32px
|
||||
width 35px
|
||||
color $ui-dark-inactive-text-color
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
.control-search-input
|
||||
input
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
.control-search-optionList
|
||||
color white
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
border-color $ui-dark-borderColor
|
||||
box-shadow 2px 2px 10px black
|
||||
|
||||
.control-search-optionList-item
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color lighten($ui-dark-button--hover-backgroundColor, 15%)
|
||||
.control-search-optionList-item-folder
|
||||
color $ui-dark-text-color
|
||||
.control-search-optionList-item-folder-surfix
|
||||
font-size 10px
|
||||
margin-left 5px
|
||||
color $ui-inactive-text-color
|
||||
.control-search-optionList-item-type
|
||||
font-size 12px
|
||||
color $ui-inactive-text-color
|
||||
padding-right 3px
|
||||
.control-search-optionList-empty
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.control-newPostButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
&:active
|
||||
border-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.control-newPostButton-tooltip
|
||||
darkTooltip()
|
||||
349
browser/main/TopBar/index.js
Normal file
349
browser/main/TopBar/index.js
Normal file
@@ -0,0 +1,349 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './TopBar.styl'
|
||||
import _ from 'lodash'
|
||||
import modal from 'browser/main/lib/modal'
|
||||
import NewNoteModal from 'browser/main/modals/NewNoteModal'
|
||||
import { hashHistory } from 'react-router'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
|
||||
const OSX = window.process.platform === 'darwin'
|
||||
|
||||
class TopBar extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
search: '',
|
||||
searchOptions: [],
|
||||
searchPopupOpen: false
|
||||
}
|
||||
|
||||
this.newNoteHandler = () => {
|
||||
this.handleNewPostButtonClick()
|
||||
}
|
||||
|
||||
this.focusSearchHandler = () => {
|
||||
this.handleOnSearchFocus()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
ee.on('top:new-note', this.newNoteHandler)
|
||||
ee.on('top:focus-search', this.focusSearchHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
ee.off('top:new-note', this.newNoteHandler)
|
||||
ee.off('top:focus-search', this.focusSearchHandler)
|
||||
}
|
||||
|
||||
handleNewPostButtonClick (e) {
|
||||
let { config } = this.props
|
||||
|
||||
switch (config.ui.defaultNote) {
|
||||
case 'MARKDOWN_NOTE':
|
||||
this.createNote('MARKDOWN_NOTE')
|
||||
break
|
||||
case 'SNIPPET_NOTE':
|
||||
this.createNote('SNIPPET_NOTE')
|
||||
break
|
||||
case 'ALWAYS_ASK':
|
||||
default:
|
||||
let { dispatch, location } = this.props
|
||||
let { storage, folder } = this.resolveTargetFolder()
|
||||
|
||||
modal.open(NewNoteModal, {
|
||||
storage: storage.key,
|
||||
folder: folder.key,
|
||||
dispatch,
|
||||
location
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resolveTargetFolder () {
|
||||
let { data, params } = this.props
|
||||
let storage = data.storageMap.get(params.storageKey)
|
||||
|
||||
// Find first storage
|
||||
if (storage == null) {
|
||||
for (let kv of data.storageMap) {
|
||||
storage = kv[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (storage == null) window.alert('No storage to create a note')
|
||||
let folder = _.find(storage.folders, {key: params.folderKey})
|
||||
if (folder == null) folder = storage.folders[0]
|
||||
if (folder == null) window.alert('No folder to create a note')
|
||||
|
||||
return {
|
||||
storage,
|
||||
folder
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchChange (e) {
|
||||
this.setState({
|
||||
search: this.refs.searchInput.value
|
||||
})
|
||||
}
|
||||
|
||||
getOptions () {
|
||||
let { data } = this.props
|
||||
let { search } = this.state
|
||||
let notes = data.noteMap.map((note) => note)
|
||||
if (search.trim().length === 0) return []
|
||||
let searchBlocks = search.split(' ')
|
||||
searchBlocks.forEach((block) => {
|
||||
if (block.match(/^!#.+/)) {
|
||||
let tag = block.match(/^!#(.+)/)[1]
|
||||
let regExp = new RegExp(_.escapeRegExp(tag), 'i')
|
||||
notes = notes
|
||||
.filter((note) => {
|
||||
if (!_.isArray(note.tags)) return false
|
||||
return note.tags.some((_tag) => {
|
||||
return _tag.match(regExp)
|
||||
})
|
||||
})
|
||||
} else if (block.match(/^!.+/)) {
|
||||
let block = block.match(/^!(.+)/)[1]
|
||||
let regExp = new RegExp(_.escapeRegExp(block), 'i')
|
||||
notes = notes.filter((note) => {
|
||||
if (!_.isArray(note.tags) || !note.tags.some((_tag) => {
|
||||
return _tag.match(regExp)
|
||||
})) {
|
||||
return true
|
||||
}
|
||||
if (note.type === 'SNIPPET_NOTE') {
|
||||
return !note.description.match(regExp)
|
||||
} else if (note.type === 'MARKDOWN_NOTE') {
|
||||
return !note.content.match(regExp)
|
||||
}
|
||||
return false
|
||||
})
|
||||
} else if (block.match(/^#.+/)) {
|
||||
let tag = block.match(/#(.+)/)[1]
|
||||
let regExp = new RegExp(_.escapeRegExp(tag), 'i')
|
||||
notes = notes
|
||||
.filter((note) => {
|
||||
if (!_.isArray(note.tags)) return false
|
||||
return note.tags.some((_tag) => {
|
||||
return _tag.match(regExp)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
let regExp = new RegExp(_.escapeRegExp(block), 'i')
|
||||
notes = notes.filter((note) => {
|
||||
if (_.isArray(note.tags) && note.tags.some((_tag) => {
|
||||
return _tag.match(regExp)
|
||||
})) {
|
||||
return true
|
||||
}
|
||||
if (note.type === 'SNIPPET_NOTE') {
|
||||
return note.description.match(regExp)
|
||||
} else if (note.type === 'MARKDOWN_NOTE') {
|
||||
return note.content.match(regExp)
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return notes
|
||||
}
|
||||
|
||||
handleOptionClick (uniqueKey) {
|
||||
return (e) => {
|
||||
this.setState({
|
||||
searchPopupOpen: false
|
||||
}, () => {
|
||||
let { location } = this.props
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: uniqueKey
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchFocus (e) {
|
||||
this.setState({
|
||||
searchPopupOpen: true
|
||||
})
|
||||
}
|
||||
handleSearchBlur (e) {
|
||||
e.stopPropagation()
|
||||
|
||||
let el = e.relatedTarget
|
||||
let isStillFocused = false
|
||||
while (el != null) {
|
||||
if (el === this.refs.search) {
|
||||
isStillFocused = true
|
||||
break
|
||||
}
|
||||
el = el.parentNode
|
||||
}
|
||||
if (!isStillFocused) {
|
||||
this.setState({
|
||||
searchPopupOpen: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
createNote (noteType) {
|
||||
let { dispatch, location } = this.props
|
||||
if (noteType !== 'MARKDOWN_NOTE' && noteType !== 'SNIPPET_NOTE') throw new Error('Invalid note type.')
|
||||
|
||||
let { storage, folder } = this.resolveTargetFolder()
|
||||
|
||||
let newNote = noteType === 'MARKDOWN_NOTE'
|
||||
? {
|
||||
type: 'MARKDOWN_NOTE',
|
||||
folder: folder.key,
|
||||
title: '',
|
||||
content: ''
|
||||
}
|
||||
: {
|
||||
type: 'SNIPPET_NOTE',
|
||||
folder: folder.key,
|
||||
title: '',
|
||||
description: '',
|
||||
snippets: [{
|
||||
name: '',
|
||||
mode: 'text',
|
||||
content: ''
|
||||
}]
|
||||
}
|
||||
|
||||
dataApi
|
||||
.createNote(storage.key, newNote)
|
||||
.then((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: {key: note.storage + '-' + note.key}
|
||||
})
|
||||
ee.emit('detail:focus')
|
||||
})
|
||||
}
|
||||
|
||||
setDefaultNote (defaultNote) {
|
||||
let { config, dispatch } = this.props
|
||||
let ui = Object.assign(config.ui)
|
||||
ui.defaultNote = defaultNote
|
||||
ConfigManager.set({
|
||||
ui
|
||||
})
|
||||
|
||||
dispatch({
|
||||
type: 'SET_UI',
|
||||
config: ConfigManager.get()
|
||||
})
|
||||
}
|
||||
|
||||
handleOnSearchFocus () {
|
||||
if (this.state.searchPopupOpen) {
|
||||
this.refs.search.childNodes[0].blur()
|
||||
} else {
|
||||
this.refs.search.childNodes[0].focus()
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { config, style, data } = this.props
|
||||
let searchOptionList = this.getOptions()
|
||||
.map((note) => {
|
||||
let storage = data.storageMap.get(note.storage)
|
||||
let folder = _.find(storage.folders, {key: note.folder})
|
||||
return <div styleName='control-search-optionList-item'
|
||||
key={note.storage + '-' + note.key}
|
||||
onClick={(e) => this.handleOptionClick(note.storage + '-' + note.key)(e)}
|
||||
>
|
||||
<div styleName='control-search-optionList-item-folder'
|
||||
style={{borderColor: folder.color}}>
|
||||
{folder.name}
|
||||
<span styleName='control-search-optionList-item-folder-surfix'>in {storage.name}</span>
|
||||
</div>
|
||||
{note.type === 'SNIPPET_NOTE'
|
||||
? <i styleName='control-search-optionList-item-type' className='fa fa-code' />
|
||||
: <i styleName='control-search-optionList-item-type' className='fa fa-file-text-o' />
|
||||
}
|
||||
{note.title}
|
||||
</div>
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='TopBar'
|
||||
styleName={config.isSideNavFolded ? 'root--expanded' : 'root'}
|
||||
style={style}
|
||||
>
|
||||
<div styleName='control'>
|
||||
<div styleName='control-search'>
|
||||
<i styleName='control-search-icon' className='fa fa-search fa-fw' />
|
||||
<div styleName='control-search-input'
|
||||
onFocus={(e) => this.handleSearchFocus(e)}
|
||||
onBlur={(e) => this.handleSearchBlur(e)}
|
||||
tabIndex='-1'
|
||||
ref='search'
|
||||
>
|
||||
<input
|
||||
ref='searchInput'
|
||||
value={this.state.search}
|
||||
onChange={(e) => this.handleSearchChange(e)}
|
||||
placeholder='Search'
|
||||
type='text'
|
||||
/>
|
||||
{this.state.searchPopupOpen &&
|
||||
<div styleName='control-search-optionList'>
|
||||
{searchOptionList.length > 0
|
||||
? searchOptionList
|
||||
: <div styleName='control-search-optionList-empty'>Empty List</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{this.state.search > 0 &&
|
||||
<button styleName='left-search-clearButton'
|
||||
onClick={(e) => this.handleSearchClearButton(e)}
|
||||
>
|
||||
<i className='fa fa-times' />
|
||||
</button>
|
||||
}
|
||||
|
||||
</div>
|
||||
<button styleName='control-newPostButton'
|
||||
onClick={(e) => this.handleNewPostButtonClick(e)}>
|
||||
<i className='fa fa-plus' />
|
||||
<span styleName='control-newPostButton-tooltip'>
|
||||
Make a Note {OSX ? '⌘' : '^'} + n
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TopBar.contextTypes = {
|
||||
router: PropTypes.shape({
|
||||
push: PropTypes.func
|
||||
})
|
||||
}
|
||||
|
||||
TopBar.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
config: PropTypes.shape({
|
||||
isSideNavFolded: PropTypes.bool
|
||||
})
|
||||
}
|
||||
|
||||
export default CSSModules(TopBar, styles)
|
||||
104
browser/main/global.styl
Normal file
104
browser/main/global.styl
Normal file
@@ -0,0 +1,104 @@
|
||||
global-reset()
|
||||
|
||||
DEFAULT_FONTS = 'Lato', helvetica, arial, sans-serif
|
||||
|
||||
html, body
|
||||
width 100%
|
||||
height 100%
|
||||
overflow hidden
|
||||
|
||||
body
|
||||
font-family DEFAULT_FONTS
|
||||
color textColor
|
||||
font-size fontSize
|
||||
font-weight 400
|
||||
|
||||
button, input, select, textarea
|
||||
font-family DEFAULT_FONTS
|
||||
|
||||
div, span, a, button, input, textarea
|
||||
box-sizing border-box
|
||||
|
||||
a
|
||||
color $brand-color
|
||||
&:hover
|
||||
color lighten($brand-color, 5%)
|
||||
&:visited
|
||||
color $brand-color
|
||||
|
||||
hr
|
||||
border-top none
|
||||
border-bottom solid 1px $border-color
|
||||
margin 15px 0
|
||||
|
||||
button
|
||||
font-weight 400
|
||||
cursor pointer
|
||||
font-size 12px
|
||||
&:focus, &.focus
|
||||
outline none
|
||||
&:disabled
|
||||
cursor not-allowed
|
||||
input
|
||||
&:disabled
|
||||
cursor not-allowed
|
||||
.noSelect
|
||||
noSelect()
|
||||
|
||||
.text-center
|
||||
text-align center
|
||||
|
||||
.form-group
|
||||
margin-bottom 15px
|
||||
&>label
|
||||
display block
|
||||
margin-bottom 5px
|
||||
|
||||
textarea.block-input
|
||||
resize vertical
|
||||
height 125px
|
||||
border-radius 5px
|
||||
padding 5px 10px
|
||||
|
||||
#content
|
||||
fullsize()
|
||||
|
||||
modalZIndex= 1000
|
||||
modalBackColor = transparentify(white, 65%)
|
||||
.ace_focus
|
||||
outline-color rgb(59, 153, 252)
|
||||
outline-offset 0px
|
||||
outline-style auto
|
||||
outline-width 5px
|
||||
.ModalBase
|
||||
fixed top left bottom right
|
||||
z-index modalZIndex
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
|
||||
&.hide
|
||||
display none
|
||||
.modalBack
|
||||
absolute top left bottom right
|
||||
background-color modalBackColor
|
||||
z-index modalZIndex + 1
|
||||
body[data-theme="dark"]
|
||||
.ModalBase
|
||||
.modalBack
|
||||
background-color alpha(black, 60%)
|
||||
|
||||
.CodeMirror
|
||||
font-family inherit !important
|
||||
line-height 1.4em
|
||||
height 100%
|
||||
.CodeMirror > div > textarea
|
||||
margin-bottom -1em
|
||||
.CodeMirror-focused .CodeMirror-selected
|
||||
background #B1D7FE
|
||||
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection
|
||||
background #B1D7FE
|
||||
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection
|
||||
background #B1D7FE
|
||||
::selection
|
||||
background #B1D7FE
|
||||
89
browser/main/index.js
Normal file
89
browser/main/index.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Provider } from 'react-redux'
|
||||
import Main from './Main'
|
||||
import store from './store'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
require('!!style!css!stylus?sourceMap!./global.styl')
|
||||
import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-router'
|
||||
import { syncHistoryWithStore } from 'react-router-redux'
|
||||
require('./lib/ipcClient')
|
||||
require('../lib/customMeta')
|
||||
|
||||
const electron = require('electron')
|
||||
|
||||
const { remote, ipcRenderer } = electron
|
||||
const { dialog } = remote
|
||||
|
||||
document.addEventListener('drop', function (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
})
|
||||
document.addEventListener('dragover', function (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
})
|
||||
|
||||
let el = document.getElementById('content')
|
||||
const history = syncHistoryWithStore(hashHistory, store)
|
||||
|
||||
function notify (...args) {
|
||||
return new window.Notification(...args)
|
||||
}
|
||||
|
||||
function updateApp () {
|
||||
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Update Boostnote',
|
||||
detail: 'New Boostnote is ready to be installed.',
|
||||
buttons: ['Restart & Install', 'Not Now']
|
||||
})
|
||||
|
||||
if (index === 0) {
|
||||
ipcRenderer.send('update-app-confirm')
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render((
|
||||
<Provider store={store}>
|
||||
<Router history={history}>
|
||||
<Route path='/' component={Main}>
|
||||
<IndexRedirect to='/home' />
|
||||
<Route path='home' />
|
||||
<Route path='starred' />
|
||||
<Route path='storages'>
|
||||
<IndexRedirect to='/home' />
|
||||
<Route path=':storageKey'>
|
||||
<IndexRoute />
|
||||
<Route path='folders/:folderKey' />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
</Router>
|
||||
</Provider>
|
||||
), el, function () {
|
||||
let loadingCover = document.getElementById('loadingCover')
|
||||
loadingCover.parentNode.removeChild(loadingCover)
|
||||
|
||||
ipcRenderer.on('update-ready', function () {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_AVAILABLE'
|
||||
})
|
||||
notify('Update ready!', {
|
||||
body: 'New Boostnote is ready to be installed.'
|
||||
})
|
||||
updateApp()
|
||||
})
|
||||
|
||||
ipcRenderer.on('update-found', function () {
|
||||
notify('Update found!', {
|
||||
body: 'Preparing to update...'
|
||||
})
|
||||
})
|
||||
|
||||
ipcRenderer.send('update-check', 'check-update')
|
||||
window.addEventListener('online', function () {
|
||||
if (!store.getState().status.updateReady) {
|
||||
ipcRenderer.send('update-check', 'check-update')
|
||||
}
|
||||
})
|
||||
})
|
||||
31
browser/main/lib/Commander.js
Normal file
31
browser/main/lib/Commander.js
Normal file
@@ -0,0 +1,31 @@
|
||||
let callees = []
|
||||
|
||||
function bind (name, el) {
|
||||
callees.push({
|
||||
name: name,
|
||||
element: el
|
||||
})
|
||||
}
|
||||
|
||||
function release (el) {
|
||||
callees = callees.filter((callee) => callee.element !== el)
|
||||
}
|
||||
|
||||
function fire (command) {
|
||||
console.info('COMMAND >>', command)
|
||||
let splitted = command.split(':')
|
||||
let target = splitted[0]
|
||||
let targetCommand = splitted[1]
|
||||
let targetCallees = callees
|
||||
.filter((callee) => callee.name === target)
|
||||
|
||||
targetCallees.forEach((callee) => {
|
||||
callee.element.fire(targetCommand)
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
bind,
|
||||
release,
|
||||
fire
|
||||
}
|
||||
131
browser/main/lib/ConfigManager.js
Normal file
131
browser/main/lib/ConfigManager.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
const electron = require('electron')
|
||||
const { ipcRenderer } = electron
|
||||
const consts = require('browser/lib/consts')
|
||||
|
||||
let isInitialized = false
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
zoom: 1,
|
||||
isSideNavFolded: false,
|
||||
listWidth: 280,
|
||||
navWidth: 200,
|
||||
sortBy: 'UPDATED_AT', // 'CREATED_AT', 'UPDATED_AT', 'APLHABETICAL'
|
||||
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
|
||||
hotkey: {
|
||||
toggleFinder: OSX ? 'Cmd + Alt + S' : 'Super + Alt + S',
|
||||
toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E',
|
||||
},
|
||||
ui: {
|
||||
theme: 'default',
|
||||
disableDirectWrite: false,
|
||||
defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
|
||||
},
|
||||
editor: {
|
||||
theme: 'default',
|
||||
keyMap: 'sublime',
|
||||
fontSize: '14',
|
||||
fontFamily: 'Monaco, Consolas',
|
||||
indentType: 'space',
|
||||
indentSize: '2',
|
||||
switchPreview: 'BLUR' // Available value: RIGHTCLICK, BLUR
|
||||
},
|
||||
preview: {
|
||||
fontSize: '14',
|
||||
fontFamily: 'Lato',
|
||||
codeBlockTheme: 'elegant',
|
||||
lineNumber: true
|
||||
}
|
||||
}
|
||||
|
||||
function validate (config) {
|
||||
if (!_.isObject(config)) return false
|
||||
if (!_.isNumber(config.zoom) || config.zoom < 0) return false
|
||||
if (!_.isBoolean(config.isSideNavFolded)) return false
|
||||
if (!_.isNumber(config.listWidth) || config.listWidth <= 0) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function _save (config) {
|
||||
console.log(config)
|
||||
window.localStorage.setItem('config', JSON.stringify(config))
|
||||
}
|
||||
|
||||
function get () {
|
||||
let config = window.localStorage.getItem('config')
|
||||
|
||||
try {
|
||||
config = Object.assign({}, DEFAULT_CONFIG, JSON.parse(config))
|
||||
config.hotkey = Object.assign({}, DEFAULT_CONFIG.hotkey, config.hotkey)
|
||||
config.ui = Object.assign({}, DEFAULT_CONFIG.ui, config.ui)
|
||||
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, config.editor)
|
||||
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, config.preview)
|
||||
if (!validate(config)) throw new Error('INVALID CONFIG')
|
||||
} catch (err) {
|
||||
console.warn('Boostnote resets the malformed configuration.')
|
||||
config = DEFAULT_CONFIG
|
||||
_save(config)
|
||||
}
|
||||
|
||||
if (!isInitialized) {
|
||||
isInitialized = true
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
function set (updates) {
|
||||
let currentConfig = get()
|
||||
let newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, updates)
|
||||
if (!validate(newConfig)) throw new Error('INVALID CONFIG')
|
||||
_save(newConfig)
|
||||
|
||||
if (newConfig.ui.theme === 'dark') {
|
||||
document.body.setAttribute('data-theme', 'dark')
|
||||
} 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)
|
||||
}
|
||||
let newTheme = consts.THEMES.some((theme) => theme === newConfig.editor.theme)
|
||||
? newConfig.editor.theme
|
||||
: 'default'
|
||||
|
||||
if (newTheme !== 'default') {
|
||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + newTheme + '.css')
|
||||
}
|
||||
|
||||
ipcRenderer.send('config-renew', {
|
||||
config: get()
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
get,
|
||||
set,
|
||||
validate
|
||||
}
|
||||
30
browser/main/lib/ZoomManager.js
Normal file
30
browser/main/lib/ZoomManager.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import ConfigManager from './ConfigManager'
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
|
||||
_init()
|
||||
|
||||
function _init () {
|
||||
setZoom(getZoom(), true)
|
||||
}
|
||||
|
||||
function _saveZoom (zoomFactor) {
|
||||
ConfigManager.set({zoom: zoomFactor})
|
||||
}
|
||||
|
||||
function setZoom (zoomFactor, noSave = false) {
|
||||
if (!noSave) _saveZoom(zoomFactor)
|
||||
remote.getCurrentWebContents().setZoomFactor(zoomFactor)
|
||||
}
|
||||
|
||||
function getZoom () {
|
||||
let config = ConfigManager.get()
|
||||
|
||||
return config.zoom
|
||||
}
|
||||
|
||||
export default {
|
||||
setZoom,
|
||||
getZoom
|
||||
}
|
||||
85
browser/main/lib/dataApi/addStorage.js
Normal file
85
browser/main/lib/dataApi/addStorage.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const _ = require('lodash')
|
||||
const keygen = require('browser/lib/keygen')
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const resolveStorageNotes = require('./resolveStorageNotes')
|
||||
const consts = require('browser/lib/consts')
|
||||
const path = require('path')
|
||||
const CSON = require('@rokt33r/season')
|
||||
/**
|
||||
* @param {Object}
|
||||
* name, path, type
|
||||
*
|
||||
* 1. check if BoostnoteJSON can be created
|
||||
* if the file doesn't exist or isn't valid, try to rewrite it.
|
||||
* if the rewriting failed, throw Error
|
||||
* 2. save metadata to localStorage
|
||||
* 3. fetch notes & folders
|
||||
* 4. return `{storage: {...} folders: [folder]}`
|
||||
*/
|
||||
function addStorage (input) {
|
||||
if (!_.isString(input.path)) {
|
||||
return Promise.reject(new Error('Path must be a string.'))
|
||||
}
|
||||
let rawStorages
|
||||
try {
|
||||
rawStorages = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(rawStorages)) throw new Error('invalid storages')
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
rawStorages = []
|
||||
}
|
||||
let key = keygen()
|
||||
while (rawStorages.some((storage) => storage.key === key)) {
|
||||
key = keygen()
|
||||
}
|
||||
|
||||
let newStorage = {
|
||||
key,
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
path: input.path
|
||||
}
|
||||
|
||||
return Promise.resolve(newStorage)
|
||||
.then(resolveStorageData)
|
||||
.then(function saveMetadataToLocalStorage (resolvedStorage) {
|
||||
newStorage = resolvedStorage
|
||||
rawStorages.push({
|
||||
key: newStorage.key,
|
||||
type: newStorage.type,
|
||||
name: newStorage.name,
|
||||
path: newStorage.path
|
||||
})
|
||||
|
||||
localStorage.setItem('storages', JSON.stringify(rawStorages))
|
||||
return newStorage
|
||||
})
|
||||
.then(function (storage) {
|
||||
return resolveStorageNotes(storage)
|
||||
.then((notes) => {
|
||||
let unknownCount = 0
|
||||
notes.forEach((note) => {
|
||||
if (!storage.folders.some((folder) => note.folder === folder.key)) {
|
||||
unknownCount++
|
||||
storage.folders.push({
|
||||
key: note.folder,
|
||||
color: consts.FOLDER_COLORS[(unknownCount - 1) % 7],
|
||||
name: 'Unknown ' + unknownCount
|
||||
})
|
||||
}
|
||||
})
|
||||
if (unknownCount > 0) {
|
||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
||||
}
|
||||
return notes
|
||||
})
|
||||
})
|
||||
.then(function returnValue (notes) {
|
||||
return {
|
||||
storage: newStorage,
|
||||
notes
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = addStorage
|
||||
63
browser/main/lib/dataApi/createFolder.js
Normal file
63
browser/main/lib/dataApi/createFolder.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const _ = require('lodash')
|
||||
const keygen = require('browser/lib/keygen')
|
||||
const path = require('path')
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const CSON = require('@rokt33r/season')
|
||||
|
||||
/**
|
||||
* @param {String} storageKey
|
||||
* @param {Object} input
|
||||
* ```
|
||||
* {
|
||||
* color: String,
|
||||
* name: String
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @return {Object}
|
||||
* ```
|
||||
* {
|
||||
* storage: Object
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
function createFolder (storageKey, input) {
|
||||
let rawStorages
|
||||
let targetStorage
|
||||
try {
|
||||
if (input == null) throw new Error('No input found.')
|
||||
if (!_.isString(input.name)) throw new Error('Name must be a string.')
|
||||
if (!_.isString(input.color)) throw new Error('Color must be a string.')
|
||||
|
||||
rawStorages = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(rawStorages)) throw new Error('Target storage doesn\'t exist.')
|
||||
|
||||
targetStorage = _.find(rawStorages, {key: storageKey})
|
||||
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(function createFolder (storage) {
|
||||
let key = keygen()
|
||||
while (storage.folders.some((folder) => folder.key === key)) {
|
||||
key = keygen()
|
||||
}
|
||||
let newFolder = {
|
||||
key,
|
||||
color: input.color,
|
||||
name: input.name
|
||||
}
|
||||
|
||||
storage.folders.push(newFolder)
|
||||
|
||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
||||
|
||||
return {
|
||||
storage
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = createFolder
|
||||
84
browser/main/lib/dataApi/createNote.js
Normal file
84
browser/main/lib/dataApi/createNote.js
Normal file
@@ -0,0 +1,84 @@
|
||||
const sander = require('sander')
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const _ = require('lodash')
|
||||
const keygen = require('browser/lib/keygen')
|
||||
const path = require('path')
|
||||
const CSON = require('@rokt33r/season')
|
||||
|
||||
function validateInput (input) {
|
||||
if (!_.isArray(input.tags)) input.tags = []
|
||||
input.tags = input.tags.filter((tag) => _.isString(tag) && tag.trim().length > 0)
|
||||
if (!_.isString(input.title)) input.title = ''
|
||||
input.isStarred = !!input.isStarred
|
||||
|
||||
switch (input.type) {
|
||||
case 'MARKDOWN_NOTE':
|
||||
if (!_.isString(input.content)) input.content = ''
|
||||
break
|
||||
case 'SNIPPET_NOTE':
|
||||
if (!_.isString(input.description)) input.description = ''
|
||||
if (!_.isArray(input.snippets)) {
|
||||
input.snippets = [{
|
||||
name: '',
|
||||
mode: 'text',
|
||||
content: ''
|
||||
}]
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error('Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.')
|
||||
}
|
||||
}
|
||||
|
||||
function createNote (storageKey, input) {
|
||||
let targetStorage
|
||||
try {
|
||||
if (input == null) throw new Error('No input found.')
|
||||
input = Object.assign({}, input)
|
||||
validateInput(input)
|
||||
|
||||
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
|
||||
|
||||
targetStorage = _.find(cachedStorageList, {key: storageKey})
|
||||
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(function checkFolderExists (storage) {
|
||||
if (_.find(storage.folders, {key: input.folder}) == null) {
|
||||
throw new Error('Target folder doesn\'t exist.')
|
||||
}
|
||||
return storage
|
||||
})
|
||||
.then(function saveNote (storage) {
|
||||
let key = keygen()
|
||||
let isUnique = false
|
||||
while (!isUnique) {
|
||||
try {
|
||||
sander.statSync(path.join(storage.path, 'notes', key + '.cson'))
|
||||
key = keygen()
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
isUnique = true
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
let noteData = Object.assign({}, input, {
|
||||
key,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
storage: storageKey
|
||||
})
|
||||
|
||||
CSON.writeFileSync(path.join(storage.path, 'notes', key + '.cson'), _.omit(noteData, ['key', 'storage']))
|
||||
|
||||
return noteData
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = createNote
|
||||
75
browser/main/lib/dataApi/deleteFolder.js
Normal file
75
browser/main/lib/dataApi/deleteFolder.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const resolveStorageNotes = require('./resolveStorageNotes')
|
||||
const CSON = require('@rokt33r/season')
|
||||
const sander = require('sander')
|
||||
|
||||
/**
|
||||
* @param {String} storageKey
|
||||
* @param {String} folderKey
|
||||
*
|
||||
* @return {Object}
|
||||
* ```
|
||||
* {
|
||||
* storage: Object,
|
||||
* folderKey: String
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
function deleteFolder (storageKey, folderKey) {
|
||||
let rawStorages
|
||||
let targetStorage
|
||||
try {
|
||||
rawStorages = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(rawStorages)) throw new Error('Target storage doesn\'t exist.')
|
||||
|
||||
targetStorage = _.find(rawStorages, {key: storageKey})
|
||||
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(function assignNotes (storage) {
|
||||
return resolveStorageNotes(storage)
|
||||
.then((notes) => {
|
||||
return {
|
||||
storage,
|
||||
notes
|
||||
}
|
||||
})
|
||||
})
|
||||
.then(function deleteFolderAndNotes (data) {
|
||||
let { storage, notes } = data
|
||||
storage.folders = storage.folders
|
||||
.filter(function excludeTargetFolder (folder) {
|
||||
return folder.key !== folderKey
|
||||
})
|
||||
|
||||
let targetNotes = notes.filter(function filterTargetNotes (note) {
|
||||
return note.folder === folderKey
|
||||
})
|
||||
|
||||
let deleteAllNotes = targetNotes
|
||||
.map(function deleteNote (note) {
|
||||
const notePath = path.join(storage.path, 'notes', note.key + '.cson')
|
||||
return sander.unlink(notePath)
|
||||
.catch(function (err) {
|
||||
console.warn('Failed to delete', notePath, err)
|
||||
})
|
||||
})
|
||||
return Promise.all(deleteAllNotes)
|
||||
.then(() => storage)
|
||||
})
|
||||
.then(function (storage) {
|
||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
||||
|
||||
return {
|
||||
storage,
|
||||
folderKey
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = deleteFolder
|
||||
34
browser/main/lib/dataApi/deleteNote.js
Normal file
34
browser/main/lib/dataApi/deleteNote.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const sander = require('sander')
|
||||
|
||||
function deleteNote (storageKey, noteKey) {
|
||||
let targetStorage
|
||||
try {
|
||||
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
|
||||
|
||||
targetStorage = _.find(cachedStorageList, {key: storageKey})
|
||||
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(function deleteNoteFile (storage) {
|
||||
let notePath = path.join(storage.path, 'notes', noteKey + '.cson')
|
||||
|
||||
try {
|
||||
sander.unlinkSync(notePath)
|
||||
} catch (err) {
|
||||
console.warn('Failed to delete note cson', err)
|
||||
}
|
||||
return {
|
||||
noteKey,
|
||||
storageKey
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = deleteNote
|
||||
20
browser/main/lib/dataApi/index.js
Normal file
20
browser/main/lib/dataApi/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const dataApi = {
|
||||
init: require('./init'),
|
||||
addStorage: require('./addStorage'),
|
||||
renameStorage: require('./renameStorage'),
|
||||
removeStorage: require('./removeStorage'),
|
||||
createFolder: require('./createFolder'),
|
||||
updateFolder: require('./updateFolder'),
|
||||
deleteFolder: require('./deleteFolder'),
|
||||
createNote: require('./createNote'),
|
||||
updateNote: require('./updateNote'),
|
||||
deleteNote: require('./deleteNote'),
|
||||
moveNote: require('./moveNote'),
|
||||
migrateFromV5Storage: require('./migrateFromV5Storage'),
|
||||
|
||||
_migrateFromV6Storage: require('./migrateFromV6Storage'),
|
||||
_resolveStorageData: require('./resolveStorageData'),
|
||||
_resolveStorageNotes: require('./resolveStorageNotes')
|
||||
}
|
||||
|
||||
module.exports = dataApi
|
||||
83
browser/main/lib/dataApi/init.js
Normal file
83
browser/main/lib/dataApi/init.js
Normal file
@@ -0,0 +1,83 @@
|
||||
'use strict'
|
||||
const _ = require('lodash')
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const resolveStorageNotes = require('./resolveStorageNotes')
|
||||
const consts = require('browser/lib/consts')
|
||||
const path = require('path')
|
||||
const CSON = require('@rokt33r/season')
|
||||
/**
|
||||
* @return {Object} all storages and notes
|
||||
* ```
|
||||
* {
|
||||
* storages: [...],
|
||||
* notes: [...]
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* This method deals with 3 patterns.
|
||||
* 1. v1
|
||||
* 2. legacy
|
||||
* 3. empty directory
|
||||
*/
|
||||
function init () {
|
||||
let fetchStorages = function () {
|
||||
let rawStorages
|
||||
try {
|
||||
rawStorages = JSON.parse(window.localStorage.getItem('storages'))
|
||||
if (!_.isArray(rawStorages)) throw new Error('Cached data is not valid.')
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse cached data from localStorage', e)
|
||||
rawStorages = []
|
||||
window.localStorage.setItem('storages', JSON.stringify(rawStorages))
|
||||
}
|
||||
return Promise.all(rawStorages
|
||||
.map(resolveStorageData))
|
||||
}
|
||||
|
||||
let fetchNotes = function (storages) {
|
||||
let findNotesFromEachStorage = storages
|
||||
.map((storage) => {
|
||||
return resolveStorageNotes(storage)
|
||||
.then((notes) => {
|
||||
let unknownCount = 0
|
||||
notes.forEach((note) => {
|
||||
if (!storage.folders.some((folder) => note.folder === folder.key)) {
|
||||
unknownCount++
|
||||
storage.folders.push({
|
||||
key: note.folder,
|
||||
color: consts.FOLDER_COLORS[(unknownCount - 1) % 7],
|
||||
name: 'Unknown ' + unknownCount
|
||||
})
|
||||
}
|
||||
})
|
||||
if (unknownCount > 0) {
|
||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
||||
}
|
||||
return notes
|
||||
})
|
||||
})
|
||||
return Promise.all(findNotesFromEachStorage)
|
||||
.then(function concatNoteGroup (noteGroups) {
|
||||
return noteGroups.reduce(function (sum, group) {
|
||||
return sum.concat(group)
|
||||
}, [])
|
||||
})
|
||||
.then(function returnData (notes) {
|
||||
return {
|
||||
storages,
|
||||
notes
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve(fetchStorages())
|
||||
.then((storages) => {
|
||||
return storages
|
||||
.filter((storage) => {
|
||||
if (!_.isObject(storage)) return false
|
||||
return true
|
||||
})
|
||||
})
|
||||
.then(fetchNotes)
|
||||
}
|
||||
module.exports = init
|
||||
110
browser/main/lib/dataApi/migrateFromV5Storage.js
Normal file
110
browser/main/lib/dataApi/migrateFromV5Storage.js
Normal file
@@ -0,0 +1,110 @@
|
||||
const _ = require('lodash')
|
||||
const keygen = require('browser/lib/keygen')
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const consts = require('browser/lib/consts')
|
||||
const CSON = require('@rokt33r/season')
|
||||
const path = require('path')
|
||||
const sander = require('sander')
|
||||
|
||||
function migrateFromV5Storage (storageKey, data) {
|
||||
let targetStorage
|
||||
try {
|
||||
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
|
||||
|
||||
targetStorage = _.find(cachedStorageList, {key: storageKey})
|
||||
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(function (storage) {
|
||||
return importAll(storage, data)
|
||||
})
|
||||
}
|
||||
|
||||
function importAll (storage, data) {
|
||||
let oldArticles = data.articles
|
||||
let notes = []
|
||||
data.folders
|
||||
.forEach(function (oldFolder) {
|
||||
let folderKey = keygen()
|
||||
while (storage.folders.some((folder) => folder.key === folderKey)) {
|
||||
folderKey = keygen()
|
||||
}
|
||||
let newFolder = {
|
||||
key: folderKey,
|
||||
name: oldFolder.name,
|
||||
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7]
|
||||
}
|
||||
|
||||
storage.folders.push(newFolder)
|
||||
|
||||
let articles = oldArticles.filter((article) => article.FolderKey === oldFolder.key)
|
||||
articles.forEach((article) => {
|
||||
let noteKey = keygen()
|
||||
let isUnique = false
|
||||
while (!isUnique) {
|
||||
try {
|
||||
sander.statSync(path.join(storage.path, 'notes', noteKey + '.cson'))
|
||||
noteKey = keygen()
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
isUnique = true
|
||||
} else {
|
||||
console.error('Failed to read `notes` directory.')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (article.mode === 'markdown') {
|
||||
let newNote = {
|
||||
tags: article.tags,
|
||||
createdAt: article.createdAt,
|
||||
updatedAt: article.updatedAt,
|
||||
folder: folderKey,
|
||||
storage: storage.key,
|
||||
type: 'MARKDOWN_NOTE',
|
||||
isStarred: false,
|
||||
title: article.title,
|
||||
content: '# ' + article.title + '\n\n' + article.content,
|
||||
key: noteKey
|
||||
}
|
||||
notes.push(newNote)
|
||||
} else {
|
||||
let newNote = {
|
||||
tags: article.tags,
|
||||
createdAt: article.createdAt,
|
||||
updatedAt: article.updatedAt,
|
||||
folder: folderKey,
|
||||
storage: storage.key,
|
||||
type: 'SNIPPET_NOTE',
|
||||
isStarred: false,
|
||||
title: article.title,
|
||||
description: article.title,
|
||||
key: noteKey,
|
||||
snippets: [{
|
||||
name: article.mode,
|
||||
mode: article.mode,
|
||||
content: article.content
|
||||
}]
|
||||
}
|
||||
notes.push(newNote)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
notes.forEach(function (note) {
|
||||
CSON.writeFileSync(path.join(storage.path, 'notes', note.key + '.cson'), _.omit(note, ['storage', 'key']))
|
||||
})
|
||||
|
||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['version', 'folders']))
|
||||
|
||||
return {
|
||||
storage,
|
||||
notes
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = migrateFromV5Storage
|
||||
89
browser/main/lib/dataApi/migrateFromV6Storage.js
Normal file
89
browser/main/lib/dataApi/migrateFromV6Storage.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const path = require('path')
|
||||
const sander = require('sander')
|
||||
const keygen = require('browser/lib/keygen')
|
||||
const _ = require('lodash')
|
||||
const CSON = require('@rokt33r/season')
|
||||
|
||||
function migrateFromV5Storage (storagePath) {
|
||||
var boostnoteJSONPath = path.join(storagePath, 'boostnote.json')
|
||||
return Promise.resolve()
|
||||
.then(function readBoostnoteJSON () {
|
||||
return sander.readFile(boostnoteJSONPath, {
|
||||
encoding: 'utf-8'
|
||||
})
|
||||
})
|
||||
.then(function verifyVersion (rawData) {
|
||||
var boostnoteJSONData = JSON.parse(rawData)
|
||||
if (boostnoteJSONData.version === '1.0') throw new Error('Target storage seems to be transformed already.')
|
||||
if (!_.isArray(boostnoteJSONData.folders)) throw new Error('the value of folders is not an array.')
|
||||
|
||||
return boostnoteJSONData
|
||||
})
|
||||
.then(function setVersion (boostnoteJSONData) {
|
||||
boostnoteJSONData.version = '1.0'
|
||||
return sander.writeFile(boostnoteJSONPath, JSON.stringify(boostnoteJSONData))
|
||||
.then(() => boostnoteJSONData)
|
||||
})
|
||||
.then(function fetchNotes (boostnoteJSONData) {
|
||||
var fetchNotesFromEachFolder = boostnoteJSONData.folders
|
||||
.map(function (folder) {
|
||||
const folderDataJSONPath = path.join(storagePath, folder.key, 'data.json')
|
||||
return sander
|
||||
.readFile(folderDataJSONPath, {
|
||||
encoding: 'utf-8'
|
||||
})
|
||||
.then(function (rawData) {
|
||||
var data = JSON.parse(rawData)
|
||||
if (!_.isArray(data.notes)) throw new Error('value of notes is not an array.')
|
||||
return data.notes
|
||||
.map(function setFolderToNote (note) {
|
||||
note.folder = folder.key
|
||||
return note
|
||||
})
|
||||
})
|
||||
.catch(function failedToReadDataJSON (err) {
|
||||
console.warn('Failed to fetch notes from ', folderDataJSONPath, err)
|
||||
return []
|
||||
})
|
||||
})
|
||||
|
||||
return Promise.all(fetchNotesFromEachFolder)
|
||||
.then(function flatten (folderNotes) {
|
||||
return folderNotes
|
||||
.reduce(function concatNotes (sum, notes) {
|
||||
return sum.concat(notes)
|
||||
}, [])
|
||||
})
|
||||
.then(function saveNotes (notes) {
|
||||
notes.forEach(function renewKey (note) {
|
||||
var newKey = keygen()
|
||||
while (notes.some((_note) => _note.key === newKey)) {
|
||||
newKey = keygen()
|
||||
}
|
||||
note.key = newKey
|
||||
})
|
||||
|
||||
const noteDirPath = path.join(storagePath, 'notes')
|
||||
notes
|
||||
.map(function saveNote (note) {
|
||||
CSON.writeFileSync(path.join(noteDirPath, note.key) + '.cson', note)
|
||||
})
|
||||
return true
|
||||
})
|
||||
.then(function deleteFolderDir (check) {
|
||||
if (check) {
|
||||
boostnoteJSONData.folders.forEach((folder) => {
|
||||
sander.rimrafSync(path.join(storagePath, folder.key))
|
||||
})
|
||||
}
|
||||
return check
|
||||
})
|
||||
})
|
||||
.catch(function handleError (err) {
|
||||
console.warn(err)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = migrateFromV5Storage
|
||||
|
||||
90
browser/main/lib/dataApi/moveNote.js
Normal file
90
browser/main/lib/dataApi/moveNote.js
Normal file
@@ -0,0 +1,90 @@
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const CSON = require('@rokt33r/season')
|
||||
const keygen = require('browser/lib/keygen')
|
||||
const sander = require('sander')
|
||||
|
||||
function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
||||
let oldStorage, newStorage
|
||||
try {
|
||||
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('Storage doesn\'t exist.')
|
||||
|
||||
oldStorage = _.find(cachedStorageList, {key: storageKey})
|
||||
if (oldStorage == null) throw new Error('Storage doesn\'t exist.')
|
||||
|
||||
newStorage = _.find(cachedStorageList, {key: newStorageKey})
|
||||
if (newStorage == null) throw new Error('Target storage doesn\'t exist.')
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
return resolveStorageData(oldStorage)
|
||||
.then(function saveNote (_oldStorage) {
|
||||
oldStorage = _oldStorage
|
||||
let noteData
|
||||
let notePath = path.join(oldStorage.path, 'notes', noteKey + '.cson')
|
||||
try {
|
||||
noteData = CSON.readFileSync(notePath)
|
||||
} catch (err) {
|
||||
console.warn('Failed to find note cson', err)
|
||||
throw err
|
||||
}
|
||||
let newNoteKey
|
||||
return Promise.resolve()
|
||||
.then(function resolveNewStorage () {
|
||||
if (storageKey === newStorageKey) {
|
||||
newNoteKey = noteKey
|
||||
return oldStorage
|
||||
}
|
||||
return resolveStorageData(newStorage)
|
||||
.then(function findNewNoteKey (_newStorage) {
|
||||
newStorage = _newStorage
|
||||
newNoteKey = keygen()
|
||||
let isUnique = false
|
||||
while (!isUnique) {
|
||||
try {
|
||||
sander.statSync(path.join(newStorage.path, 'notes', newNoteKey + '.cson'))
|
||||
newNoteKey = keygen()
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
isUnique = true
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newStorage
|
||||
})
|
||||
})
|
||||
.then(function checkFolderExistsAndPrepareNoteData (newStorage) {
|
||||
if (_.find(newStorage.folders, {key: newFolderKey}) == null) throw new Error('Target folder doesn\'t exist.')
|
||||
|
||||
noteData.folder = newFolderKey
|
||||
noteData.key = newNoteKey
|
||||
noteData.storage = newStorageKey
|
||||
noteData.updatedAt = new Date()
|
||||
|
||||
return noteData
|
||||
})
|
||||
.then(function writeAndReturn (noteData) {
|
||||
CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage']))
|
||||
return noteData
|
||||
})
|
||||
.then(function deleteOldNote (data) {
|
||||
if (storageKey !== newStorageKey) {
|
||||
try {
|
||||
sander.unlinkSync(path.join(oldStorage.path, 'notes', noteKey + '.cson'))
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = moveNote
|
||||
30
browser/main/lib/dataApi/removeStorage.js
Normal file
30
browser/main/lib/dataApi/removeStorage.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const _ = require('lodash')
|
||||
|
||||
/**
|
||||
* @param {String} key
|
||||
* @return {key}
|
||||
*/
|
||||
function removeStorage (key) {
|
||||
let rawStorages
|
||||
|
||||
try {
|
||||
rawStorages = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(rawStorages)) throw new Error('invalid storages')
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
rawStorages = []
|
||||
}
|
||||
|
||||
rawStorages = rawStorages
|
||||
.filter(function excludeTargetStorage (rawStorage) {
|
||||
return rawStorage.key !== key
|
||||
})
|
||||
|
||||
localStorage.setItem('storages', JSON.stringify(rawStorages))
|
||||
|
||||
return Promise.resolve({
|
||||
storageKey: key
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = removeStorage
|
||||
31
browser/main/lib/dataApi/renameStorage.js
Normal file
31
browser/main/lib/dataApi/renameStorage.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const _ = require('lodash')
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
/**
|
||||
* @param {String} key
|
||||
* @param {String} name
|
||||
* @return {Object} Storage meta data
|
||||
*/
|
||||
function renameStorage (key, name) {
|
||||
if (!_.isString(name)) return Promise.reject(new Error('Name must be a string.'))
|
||||
|
||||
let cachedStorageList
|
||||
try {
|
||||
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
|
||||
} catch (err) {
|
||||
console.log('error got')
|
||||
console.error(err)
|
||||
return Promise.reject(err)
|
||||
}
|
||||
let targetStorage = _.find(cachedStorageList, {key: key})
|
||||
if (targetStorage == null) return Promise.reject('Storage')
|
||||
|
||||
targetStorage.name = name
|
||||
localStorage.setItem('storages', JSON.stringify(cachedStorageList))
|
||||
|
||||
targetStorage.path
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
}
|
||||
|
||||
module.exports = renameStorage
|
||||
44
browser/main/lib/dataApi/resolveStorageData.js
Normal file
44
browser/main/lib/dataApi/resolveStorageData.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const CSON = require('@rokt33r/season')
|
||||
const migrateFromV6Storage = require('./migrateFromV6Storage')
|
||||
|
||||
function resolveStorageData (storageCache) {
|
||||
let storage = {
|
||||
key: storageCache.key,
|
||||
name: storageCache.name,
|
||||
type: storageCache.type,
|
||||
path: storageCache.path
|
||||
}
|
||||
|
||||
const boostnoteJSONPath = path.join(storageCache.path, 'boostnote.json')
|
||||
try {
|
||||
let jsonData = CSON.readFileSync(boostnoteJSONPath)
|
||||
if (!_.isArray(jsonData.folders)) throw new Error('folders should be an array.')
|
||||
storage.folders = jsonData.folders
|
||||
storage.version = jsonData.version
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.warn('boostnote.json file doesn\'t exist the given path')
|
||||
CSON.writeFileSync(boostnoteJSONPath, {folders: [], version: '1.0'})
|
||||
} else {
|
||||
console.error(err)
|
||||
}
|
||||
storage.folders = []
|
||||
storage.version = '1.0'
|
||||
}
|
||||
|
||||
let version = parseInt(storage.version, 10)
|
||||
if (version >= 1) {
|
||||
if (version > 1) {
|
||||
console.log('The repository version is newer than one of current app.')
|
||||
}
|
||||
return Promise.resolve(storage)
|
||||
}
|
||||
|
||||
console.log('Transform Legacy storage', storage.path)
|
||||
return migrateFromV6Storage(storage.path)
|
||||
.then(() => storage)
|
||||
}
|
||||
|
||||
module.exports = resolveStorageData
|
||||
38
browser/main/lib/dataApi/resolveStorageNotes.js
Normal file
38
browser/main/lib/dataApi/resolveStorageNotes.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const sander = require('sander')
|
||||
const path = require('path')
|
||||
const CSON = require('@rokt33r/season')
|
||||
|
||||
function resolveStorageNotes (storage) {
|
||||
const notesDirPath = path.join(storage.path, 'notes')
|
||||
let notePathList
|
||||
try {
|
||||
notePathList = sander.readdirSync(notesDirPath)
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log(notesDirPath, ' doesn\'t exist.')
|
||||
sander.mkdirSync(notesDirPath)
|
||||
} else {
|
||||
console.warn('Failed to find note dir', notesDirPath, err)
|
||||
}
|
||||
notePathList = []
|
||||
}
|
||||
let notes = notePathList
|
||||
.filter(function filterOnlyCSONFile (notePath) {
|
||||
return /\.cson$/.test(notePath)
|
||||
})
|
||||
.map(function parseCSONFile (notePath) {
|
||||
try {
|
||||
let data = CSON.readFileSync(path.join(notesDirPath, notePath))
|
||||
data.key = path.basename(notePath, '.cson')
|
||||
data.storage = storage.key
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error(notePath)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
|
||||
return Promise.resolve(notes)
|
||||
}
|
||||
|
||||
module.exports = resolveStorageNotes
|
||||
56
browser/main/lib/dataApi/updateFolder.js
Normal file
56
browser/main/lib/dataApi/updateFolder.js
Normal file
@@ -0,0 +1,56 @@
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const CSON = require('@rokt33r/season')
|
||||
|
||||
/**
|
||||
* @param {String} storageKey
|
||||
* @param {String} folderKey
|
||||
* @param {Object} input
|
||||
* ```
|
||||
* {
|
||||
* color: String,
|
||||
* name: String
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @return {Object}
|
||||
* ```
|
||||
* {
|
||||
* storage: Object
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
function updateFolder (storageKey, folderKey, input) {
|
||||
let rawStorages
|
||||
let targetStorage
|
||||
try {
|
||||
if (input == null) throw new Error('No input found.')
|
||||
if (!_.isString(input.name)) throw new Error('Name must be a string.')
|
||||
if (!_.isString(input.color)) throw new Error('Color must be a string.')
|
||||
|
||||
rawStorages = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(rawStorages)) throw new Error('Target storage doesn\'t exist.')
|
||||
|
||||
targetStorage = _.find(rawStorages, {key: storageKey})
|
||||
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(function updateFolder (storage) {
|
||||
let targetFolder = _.find(storage.folders, {key: folderKey})
|
||||
if (targetFolder == null) throw new Error('Target folder doesn\'t exist.')
|
||||
targetFolder.name = input.name
|
||||
targetFolder.color = input.color
|
||||
|
||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
||||
|
||||
return {
|
||||
storage
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = updateFolder
|
||||
123
browser/main/lib/dataApi/updateNote.js
Normal file
123
browser/main/lib/dataApi/updateNote.js
Normal file
@@ -0,0 +1,123 @@
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const CSON = require('@rokt33r/season')
|
||||
|
||||
function validateInput (input) {
|
||||
let validatedInput = {}
|
||||
|
||||
if (input.tags != null) {
|
||||
if (!_.isArray(input.tags)) validatedInput.tags = []
|
||||
validatedInput.tags = input.tags
|
||||
.filter((tag) => _.isString(tag) && tag.trim().length > 0)
|
||||
}
|
||||
|
||||
if (input.title != null) {
|
||||
if (!_.isString(input.title)) validatedInput.title = ''
|
||||
else validatedInput.title = input.title
|
||||
}
|
||||
|
||||
if (input.isStarred != null) {
|
||||
validatedInput.isStarred = !!input.isStarred
|
||||
}
|
||||
|
||||
validatedInput.type = input.type
|
||||
switch (input.type) {
|
||||
case 'MARKDOWN_NOTE':
|
||||
if (input.content != null) {
|
||||
if (!_.isString(input.content)) validatedInput.content = ''
|
||||
else validatedInput.content = input.content
|
||||
}
|
||||
return validatedInput
|
||||
case 'SNIPPET_NOTE':
|
||||
if (input.description != null) {
|
||||
if (!_.isString(input.description)) validatedInput.description = ''
|
||||
else validatedInput.description = input.description
|
||||
}
|
||||
if (input.snippets != null) {
|
||||
if (!_.isArray(input.snippets)) {
|
||||
validatedInput.snippets = [{
|
||||
name: '',
|
||||
mode: 'text',
|
||||
content: ''
|
||||
}]
|
||||
} else {
|
||||
validatedInput.snippets = input.snippets
|
||||
}
|
||||
validatedInput.snippets
|
||||
.filter((snippet) => {
|
||||
if (!_.isString(snippet.name)) return false
|
||||
if (!_.isString(snippet.mode)) return false
|
||||
if (!_.isString(snippet.content)) return false
|
||||
return true
|
||||
})
|
||||
}
|
||||
return validatedInput
|
||||
default:
|
||||
throw new Error('Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.')
|
||||
}
|
||||
}
|
||||
|
||||
function updateNote (storageKey, noteKey, input) {
|
||||
let targetStorage
|
||||
try {
|
||||
if (input == null) throw new Error('No input found.')
|
||||
input = validateInput(input)
|
||||
|
||||
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
|
||||
|
||||
targetStorage = _.find(cachedStorageList, {key: storageKey})
|
||||
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(function saveNote (storage) {
|
||||
let noteData
|
||||
let notePath = path.join(storage.path, 'notes', noteKey + '.cson')
|
||||
try {
|
||||
noteData = CSON.readFileSync(notePath)
|
||||
} catch (err) {
|
||||
console.warn('Failed to find note cson', err)
|
||||
noteData = input.type === 'SNIPPET_NOTE'
|
||||
? {
|
||||
type: 'SNIPPET_NOTE',
|
||||
description: [],
|
||||
snippets: [{
|
||||
name: '',
|
||||
mode: 'text',
|
||||
content: ''
|
||||
}]
|
||||
}
|
||||
: {
|
||||
type: 'MARKDOWN_NOTE',
|
||||
content: ''
|
||||
}
|
||||
noteData.title = ''
|
||||
if (storage.folders.length === 0) throw new Error('Failed to restore note: No folder exists.')
|
||||
noteData.folder = storage.folders[0].key
|
||||
noteData.createdAt = new Date()
|
||||
noteData.updatedAt = new Date()
|
||||
noteData.isStarred = false
|
||||
noteData.tags = []
|
||||
}
|
||||
|
||||
if (noteData.type === 'SNIPPET_NOTE') {
|
||||
noteData.title
|
||||
}
|
||||
|
||||
Object.assign(noteData, input, {
|
||||
key: noteKey,
|
||||
updatedAt: new Date(),
|
||||
storage: storageKey
|
||||
})
|
||||
|
||||
CSON.writeFileSync(path.join(storage.path, 'notes', noteKey + '.cson'), _.omit(noteData, ['key', 'storage']))
|
||||
|
||||
return noteData
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = updateNote
|
||||
26
browser/main/lib/eventEmitter.js
Normal file
26
browser/main/lib/eventEmitter.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const electron = require('electron')
|
||||
const { ipcRenderer, remote } = electron
|
||||
|
||||
function on (name, listener) {
|
||||
ipcRenderer.on(name, listener)
|
||||
}
|
||||
|
||||
function off (name, listener) {
|
||||
ipcRenderer.removeListener(name, listener)
|
||||
}
|
||||
|
||||
function once (name, listener) {
|
||||
ipcRenderer.once(name, listener)
|
||||
}
|
||||
|
||||
function emit (name, ...args) {
|
||||
console.log(name)
|
||||
remote.getCurrentWindow().webContents.send(name, ...args)
|
||||
}
|
||||
|
||||
export default {
|
||||
emit,
|
||||
on,
|
||||
off,
|
||||
once
|
||||
}
|
||||
44
browser/main/lib/ipcClient.js
Normal file
44
browser/main/lib/ipcClient.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import ConfigManager from './ConfigManager'
|
||||
import store from 'browser/main/store'
|
||||
|
||||
const nodeIpc = require('node-ipc')
|
||||
const { remote, ipcRenderer } = require('electron')
|
||||
const { app } = remote
|
||||
const path = require('path')
|
||||
|
||||
nodeIpc.config.id = 'main'
|
||||
nodeIpc.config.retry = 1500
|
||||
nodeIpc.config.silent = true
|
||||
|
||||
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')
|
||||
ipcRenderer.send('config-renew', {config: ConfigManager.get()})
|
||||
})
|
||||
nodeIpc.of.node.on('disconnect', function () {
|
||||
console.log('disconnected')
|
||||
})
|
||||
|
||||
nodeIpc.of.node.on('request-data-from-finder', function () {
|
||||
console.log('throttle')
|
||||
var { data } = store.getState()
|
||||
console.log(data.starredSet.toJS())
|
||||
nodeIpc.of.node.emit('throttle-data', {
|
||||
storageMap: data.storageMap.toJS(),
|
||||
noteMap: data.noteMap.toJS(),
|
||||
starredSet: data.starredSet.toJS(),
|
||||
storageNoteMap: data.storageNoteMap.toJS(),
|
||||
folderNoteMap: data.folderNoteMap.toJS(),
|
||||
tagNoteMap: data.tagNoteMap.toJS()
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
module.exports = nodeIpc
|
||||
121
browser/main/lib/mixpanel.js
Normal file
121
browser/main/lib/mixpanel.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import store from 'browser/main/store'
|
||||
|
||||
const _ = require('lodash')
|
||||
const keygen = require('browser/lib/keygen')
|
||||
const Mixpanel = require('mixpanel')
|
||||
const mixpanel = Mixpanel.init('7a0aca437d72dfd07cbcbf58d3b61f27', {key: 'fde4fd23f4d550f1b646bcd7d4374b1f'})
|
||||
const moment = require('moment')
|
||||
const electron = require('electron')
|
||||
|
||||
function _getClientKey () {
|
||||
let clientKey = localStorage.getItem('clientKey')
|
||||
if (!_.isString(clientKey) || clientKey.length !== 40) {
|
||||
clientKey = keygen(20)
|
||||
_setClientKey(clientKey)
|
||||
}
|
||||
|
||||
return clientKey
|
||||
}
|
||||
|
||||
function _setClientKey (newKey) {
|
||||
localStorage.setItem('clientKey', newKey)
|
||||
}
|
||||
|
||||
function _fetch () {
|
||||
let events
|
||||
try {
|
||||
events = JSON.parse(localStorage.getItem('events'))
|
||||
if (!_.isArray(events)) throw new Error('events is not an array.')
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
events = []
|
||||
localStorage.setItem('events', JSON.stringify(events))
|
||||
console.info('Events cache initialzed')
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
function _keep (name, properties) {
|
||||
let events = _fetch()
|
||||
properties.time = new Date()
|
||||
events.push({
|
||||
name,
|
||||
properties
|
||||
})
|
||||
localStorage.setItem('events', JSON.stringify(events))
|
||||
}
|
||||
|
||||
function _keepUnique (name, properties) {
|
||||
let events = _fetch()
|
||||
properties.time = new Date()
|
||||
events = events.filter((event) => event.name !== name)
|
||||
events.push({
|
||||
name,
|
||||
properties
|
||||
})
|
||||
localStorage.setItem('events', JSON.stringify(events))
|
||||
}
|
||||
|
||||
function _flush () {
|
||||
let events = _fetch()
|
||||
let spliced = events.splice(0, 50)
|
||||
localStorage.setItem('events', JSON.stringify(events))
|
||||
|
||||
if (spliced.length > 0) {
|
||||
let parsedEvents = spliced
|
||||
.filter((event) => {
|
||||
if (!_.isObject(event)) return false
|
||||
if (!_.isString(event.name)) return false
|
||||
if (!_.isObject(event.properties)) return false
|
||||
if (!moment(event.properties.time).isValid()) return false
|
||||
if (new Date() - moment(event.properties.time).toDate() > 1000 * 3600 * 24 * 3) return false
|
||||
return true
|
||||
})
|
||||
.map((event) => {
|
||||
return {
|
||||
event: event.name,
|
||||
properties: event.properties
|
||||
}
|
||||
})
|
||||
|
||||
mixpanel.import_batch(parsedEvents, {}, (errs) => {
|
||||
if (errs.length > 0) {
|
||||
let events = _fetch()
|
||||
events = events.concat(spliced)
|
||||
localStorage.setItem('events', JSON.stringify(events))
|
||||
} else {
|
||||
_flush()
|
||||
}
|
||||
})
|
||||
|
||||
let state = store.getState()
|
||||
mixpanel.people.set(_getClientKey(), {
|
||||
storage_count: state.data.storageMap.size,
|
||||
note_count: state.data.noteMap.size,
|
||||
version: electron.remote.app.getVersion()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(_flush, 1000 * 60 * 60)
|
||||
|
||||
function track (name, properties) {
|
||||
switch (name) {
|
||||
case 'MAIN_FOCUSED':
|
||||
properties = Object.assign({}, properties, {
|
||||
distinct_id: _getClientKey()
|
||||
})
|
||||
_keepUnique(name, properties)
|
||||
break
|
||||
default:
|
||||
properties = Object.assign({}, properties, {
|
||||
distinct_id: _getClientKey()
|
||||
})
|
||||
_keep(name, properties)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
_mp: mixpanel,
|
||||
track
|
||||
}
|
||||
58
browser/main/lib/modal.js
Normal file
58
browser/main/lib/modal.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react'
|
||||
import { Provider } from 'react-redux'
|
||||
import ReactDOM from 'react-dom'
|
||||
import store from '../store'
|
||||
|
||||
class ModalBase extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
component: null,
|
||||
componentProps: {},
|
||||
isHidden: true
|
||||
}
|
||||
}
|
||||
|
||||
close () {
|
||||
if (modalBase != null) modalBase.setState({component: null, componentProps: null, isHidden: true})
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className={'ModalBase' + (this.state.isHidden ? ' hide' : '')}>
|
||||
<div onClick={(e) => this.close(e)} className='modalBack' />
|
||||
{this.state.component == null ? null : (
|
||||
<Provider store={store}>
|
||||
<this.state.component {...this.state.componentProps} close={this.close} />
|
||||
</Provider>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let el = document.createElement('div')
|
||||
document.body.appendChild(el)
|
||||
let modalBase = ReactDOM.render(<ModalBase />, el)
|
||||
|
||||
export function openModal (component, props) {
|
||||
if (modalBase == null) { return }
|
||||
|
||||
document.body.setAttribute('data-modal', 'open')
|
||||
modalBase.setState({component: component, componentProps: props, isHidden: false})
|
||||
}
|
||||
|
||||
export function closeModal () {
|
||||
if (modalBase == null) { return }
|
||||
modalBase.close()
|
||||
}
|
||||
|
||||
export function isModalOpen () {
|
||||
return !modalBase.state.isHidden
|
||||
}
|
||||
|
||||
export default {
|
||||
open: openModal,
|
||||
close: closeModal,
|
||||
isOpen: isModalOpen
|
||||
}
|
||||
11
browser/main/lib/notify.js
Normal file
11
browser/main/lib/notify.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const path = require('path')
|
||||
|
||||
function notify (title, options) {
|
||||
if (process.platform === 'win32') {
|
||||
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
||||
options.silent = false
|
||||
}
|
||||
return new window.Notification(title, options)
|
||||
}
|
||||
|
||||
export default notify
|
||||
112
browser/main/modals/CreateFolderModal.js
Normal file
112
browser/main/modals/CreateFolderModal.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './CreateFolderModal.styl'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import store from 'browser/main/store'
|
||||
import consts from 'browser/lib/consts'
|
||||
|
||||
class CreateFolderModal extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
name: ''
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.refs.name.focus()
|
||||
this.refs.name.select()
|
||||
}
|
||||
|
||||
handleCloseButtonClick (e) {
|
||||
this.props.close()
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
this.setState({
|
||||
name: this.refs.name.value
|
||||
})
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
if (e.keyCode === 27) {
|
||||
this.props.close()
|
||||
}
|
||||
}
|
||||
|
||||
handleInputKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
case 13:
|
||||
this.confirm()
|
||||
}
|
||||
}
|
||||
|
||||
handleConfirmButtonClick (e) {
|
||||
this.confirm()
|
||||
}
|
||||
|
||||
confirm () {
|
||||
if (this.state.name.trim().length > 0) {
|
||||
let { storage } = this.props
|
||||
let input = {
|
||||
name: this.state.name.trim(),
|
||||
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7]
|
||||
}
|
||||
|
||||
dataApi.createFolder(storage.key, input)
|
||||
.then((data) => {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_FOLDER',
|
||||
storage: data.storage
|
||||
})
|
||||
this.props.close()
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div styleName='root'
|
||||
tabIndex='-1'
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
>
|
||||
<div styleName='header'>
|
||||
<div styleName='title'>Create new folder</div>
|
||||
</div>
|
||||
<button styleName='close' onClick={(e) => this.handleCloseButtonClick(e)}>
|
||||
<div styleName='close-mark'>X</div>
|
||||
<div styleName='close-text'>esc</div>
|
||||
</button>
|
||||
|
||||
<div styleName='control'>
|
||||
<div styleName='control-folder'>
|
||||
<div styleName='control-folder-label'>Folder name</div>
|
||||
<input styleName='control-folder-input'
|
||||
ref='name'
|
||||
value={this.state.name}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
onKeyDown={(e) => this.handleInputKeyDown(e)}
|
||||
/>
|
||||
</div>
|
||||
<button styleName='control-confirmButton'
|
||||
onClick={(e) => this.handleConfirmButtonClick(e)}
|
||||
>
|
||||
Create Folder
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CreateFolderModal.propTypes = {
|
||||
storage: PropTypes.shape({
|
||||
key: PropTypes.string
|
||||
})
|
||||
}
|
||||
|
||||
export default CSSModules(CreateFolderModal, styles)
|
||||
93
browser/main/modals/CreateFolderModal.styl
Normal file
93
browser/main/modals/CreateFolderModal.styl
Normal file
@@ -0,0 +1,93 @@
|
||||
.root
|
||||
modal()
|
||||
width 700px
|
||||
height 200px
|
||||
overflow hidden
|
||||
position relative
|
||||
padding 0 40px
|
||||
|
||||
.header
|
||||
height 50px
|
||||
margin-bottom 10px
|
||||
margin-top 10px
|
||||
font-size 18px
|
||||
line-height 50px
|
||||
background-color $ui-backgroundColor
|
||||
color $ui-text-color
|
||||
|
||||
.close-mark
|
||||
font-size 15px
|
||||
|
||||
.close
|
||||
height 50px
|
||||
position absolute
|
||||
background-color transparent
|
||||
color $ui-inactive-text-color
|
||||
border none
|
||||
top 7px
|
||||
right 10px
|
||||
text-align center
|
||||
width top-bar--height
|
||||
height top-bar--height
|
||||
|
||||
.control-folder-label
|
||||
text-align left
|
||||
font-size 14px
|
||||
color $ui-text-color
|
||||
|
||||
.control-folder-input
|
||||
display block
|
||||
height 30px
|
||||
width 620px
|
||||
padding 0 5px
|
||||
margin 10px auto 15px
|
||||
border 1px solid #C9C9C9 // TODO: use variable.
|
||||
border-radius 5px
|
||||
background-color transparent
|
||||
outline none
|
||||
vertical-align middle
|
||||
font-size 18px
|
||||
&:disabled
|
||||
background-color $ui-input--disabled-backgroundColor
|
||||
&:focus, &:active
|
||||
border-color $ui-active-color
|
||||
|
||||
.control-confirmButton
|
||||
display block
|
||||
height 30px
|
||||
width 620px
|
||||
border none
|
||||
border-radius 5px
|
||||
padding 0 25px
|
||||
margin 20px auto
|
||||
font-size 14px
|
||||
colorPrimaryButton()
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
modalDark()
|
||||
width 700px
|
||||
height 200px
|
||||
overflow hidden
|
||||
position relative
|
||||
padding 0 40px
|
||||
|
||||
.header
|
||||
background-color transparent
|
||||
border-color $ui-dark-borderColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
.control-folder-label
|
||||
color $ui-dark-text-color
|
||||
|
||||
.control-folder-input
|
||||
border 1px solid #C9C9C9 // TODO: use variable.
|
||||
color white
|
||||
|
||||
.closeButton
|
||||
border-color $ui-dark-borderColor
|
||||
color $ui-dark-text-color
|
||||
colorDarkDefaultButton()
|
||||
|
||||
.description
|
||||
color $ui-inactive-text-color
|
||||
53
browser/main/modals/DeleteArticleModal.js
Normal file
53
browser/main/modals/DeleteArticleModal.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import store from '../store'
|
||||
|
||||
const electron = require('electron')
|
||||
const ipc = electron.ipcRenderer
|
||||
|
||||
export default class DeleteArticleModal extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.confirmHandler = (e) => this.handleYesButtonClick()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
ReactDOM.findDOMNode(this.refs.no).focus()
|
||||
ipc.on('modal-confirm', this.confirmHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
ipc.removeListener('modal-confirm', this.confirmHandler)
|
||||
}
|
||||
|
||||
handleNoButtonClick (e) {
|
||||
this.props.close()
|
||||
}
|
||||
|
||||
handleYesButtonClick (e) {
|
||||
// store.dispatch(destroyArticle(this.props.articleKey))
|
||||
this.props.close()
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='DeleteArticleModal modal'>
|
||||
<div className='title'><i className='fa fa-fw fa-trash' /> Delete an article.</div>
|
||||
|
||||
<div className='message'>Do you really want to delete?</div>
|
||||
|
||||
<div className='control'>
|
||||
<button ref='no' onClick={(e) => this.handleNoButtonClick(e)}><i className='fa fa-fw fa-close' /> No</button>
|
||||
<button ref='yes' onClick={(e) => this.handleYesButtonClick(e)} className='danger'><i className='fa fa-fw fa-check' /> Yes</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DeleteArticleModal.propTypes = {
|
||||
action: PropTypes.object,
|
||||
articleKey: PropTypes.string,
|
||||
close: PropTypes.func
|
||||
}
|
||||
265
browser/main/modals/InitModal.js
Normal file
265
browser/main/modals/InitModal.js
Normal file
@@ -0,0 +1,265 @@
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './InitModal.styl'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import store from 'browser/main/store'
|
||||
import { hashHistory } from 'react-router'
|
||||
import _ from 'lodash'
|
||||
|
||||
const CSON = require('@rokt33r/season')
|
||||
const path = require('path')
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
|
||||
function browseFolder () {
|
||||
let dialog = remote.dialog
|
||||
|
||||
let defaultPath = remote.app.getPath('home')
|
||||
return new Promise((resolve, reject) => {
|
||||
dialog.showOpenDialog({
|
||||
title: 'Select Directory',
|
||||
defaultPath,
|
||||
properties: ['openDirectory', 'createDirectory']
|
||||
}, function (targetPaths) {
|
||||
if (targetPaths == null) return resolve('')
|
||||
resolve(targetPaths[0])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
class InitModal extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
path: path.join(remote.app.getPath('home'), 'Boostnote'),
|
||||
migrationRequested: true,
|
||||
isLoading: true,
|
||||
data: null,
|
||||
legacyStorageExists: false,
|
||||
isSending: false
|
||||
}
|
||||
}
|
||||
|
||||
handleCloseButtonClick (e) {
|
||||
this.props.close()
|
||||
}
|
||||
|
||||
handlePathChange (e) {
|
||||
this.setState({
|
||||
path: e.target.value
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
let data = null
|
||||
try {
|
||||
data = CSON.readFileSync(path.join(remote.app.getPath('userData'), 'local.json'))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
let newState = {
|
||||
isLoading: false
|
||||
}
|
||||
if (data != null) {
|
||||
newState.legacyStorageExists = true
|
||||
newState.data = data
|
||||
}
|
||||
this.setState(newState, () => {
|
||||
this.refs.createButton.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handlePathBrowseButtonClick (e) {
|
||||
browseFolder()
|
||||
.then((targetPath) => {
|
||||
if (targetPath.length > 0) {
|
||||
this.setState({
|
||||
path: targetPath
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('BrowseFAILED')
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
handleSubmitButtonClick (e) {
|
||||
this.setState({
|
||||
isSending: true
|
||||
}, () => {
|
||||
dataApi
|
||||
.addStorage({
|
||||
name: 'My Storage',
|
||||
path: this.state.path
|
||||
})
|
||||
.then((data) => {
|
||||
if (this.state.migrationRequested && _.isObject(this.state.data) && _.isArray(this.state.data.folders) && _.isArray(this.state.data.articles)) {
|
||||
return dataApi.migrateFromV5Storage(data.storage.key, this.state.data)
|
||||
}
|
||||
return data
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.storage.folders[0] != null) {
|
||||
return data
|
||||
} else {
|
||||
return dataApi
|
||||
.createFolder(data.storage.key, {
|
||||
color: '#6AA5E9',
|
||||
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
|
||||
})
|
||||
|
||||
let defaultMarkdownNote = dataApi
|
||||
.createNote(data.storage.key, {
|
||||
type: 'MARKDOWN_NOTE',
|
||||
folder: data.storage.folders[0].key,
|
||||
title: 'Welcome to Boostnote :)',
|
||||
content: '# Welcome to Boostnote :)\nThis is a markdown note.\n\nClick to edit this note.'
|
||||
})
|
||||
.then((note) => {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
})
|
||||
let 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 snippet as a single note like Gist.',
|
||||
snippets: [
|
||||
{
|
||||
name: 'example.html',
|
||||
mode: 'html',
|
||||
content: '<html>\n<body>\n<h1 id=\'hello\'>Hello World</h1>\n</body>\n</html>'
|
||||
},
|
||||
{
|
||||
name: 'example.js',
|
||||
mode: 'javascript',
|
||||
content: 'var html = document.getElementById(\'hello\').innerHTML\n\nconsole.log(html)'
|
||||
}
|
||||
]
|
||||
})
|
||||
.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)
|
||||
this.props.close()
|
||||
})
|
||||
.catch((err) => {
|
||||
this.setState({
|
||||
isSending: false
|
||||
})
|
||||
throw err
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
handleMigrationRequestedChange (e) {
|
||||
this.setState({
|
||||
migrationRequested: e.target.checked
|
||||
})
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
if (e.keyCode === 27) {
|
||||
this.props.close()
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
if (this.state.isLoading) {
|
||||
return <div styleName='root--loading'>
|
||||
<i styleName='spinner' className='fa fa-spin fa-spinner' />
|
||||
<div styleName='loadingMessage'>Preparing initialization...</div>
|
||||
</div>
|
||||
}
|
||||
return (
|
||||
<div styleName='root'
|
||||
tabIndex='-1'
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
>
|
||||
|
||||
<div styleName='header'>
|
||||
<div styleName='header-title'>Initialize Storage</div>
|
||||
</div>
|
||||
<button styleName='closeButton'
|
||||
onClick={(e) => this.handleCloseButtonClick(e)}
|
||||
>Close</button>
|
||||
<div styleName='body'>
|
||||
<div styleName='body-welcome'>
|
||||
Welcome you!
|
||||
</div>
|
||||
<div styleName='body-description'>
|
||||
Boostnote will use this directory as a default storage.
|
||||
</div>
|
||||
<div styleName='body-path'>
|
||||
<input styleName='body-path-input'
|
||||
placeholder='Select Folder'
|
||||
value={this.state.path}
|
||||
onChange={(e) => this.handlePathChange(e)}
|
||||
/>
|
||||
<button styleName='body-path-button'
|
||||
onClick={(e) => this.handlePathBrowseButtonClick(e)}
|
||||
>
|
||||
...
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{this.state.legacyStorageExists &&
|
||||
<div styleName='body-migration'>
|
||||
<label><input type='checkbox' checked={this.state.migrationRequested} onChange={(e) => this.handleMigrationRequestedChange(e)} /> Migrate old data from the legacy app v0.5</label>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div styleName='body-control'>
|
||||
<button styleName='body-control-createButton'
|
||||
ref='createButton'
|
||||
onClick={(e) => this.handleSubmitButtonClick(e)}
|
||||
disabled={this.state.isSending}
|
||||
>
|
||||
{this.state.isSending
|
||||
? <span>
|
||||
<i className='fa fa-spin fa-spinner' /> Loading...
|
||||
</span>
|
||||
: 'Let\'s Go!'
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
InitModal.propTypes = {
|
||||
}
|
||||
|
||||
export default CSSModules(InitModal, styles)
|
||||
84
browser/main/modals/InitModal.styl
Normal file
84
browser/main/modals/InitModal.styl
Normal file
@@ -0,0 +1,84 @@
|
||||
.root
|
||||
modal()
|
||||
max-width 540px
|
||||
overflow hidden
|
||||
position relative
|
||||
.root--loading
|
||||
@extend .root
|
||||
text-align center
|
||||
.spinner
|
||||
font-size 100px
|
||||
margin 35px auto
|
||||
color $ui-text-color
|
||||
.loadingMessage
|
||||
color $ui-text-color
|
||||
margin 15px auto 35px
|
||||
.header
|
||||
height 50px
|
||||
font-size 18px
|
||||
line-height 50px
|
||||
padding 0 15px
|
||||
background-color $ui-backgroundColor
|
||||
border-bottom solid 1px $ui-borderColor
|
||||
color $ui-text-color
|
||||
|
||||
.closeButton
|
||||
position absolute
|
||||
top 10px
|
||||
right 10px
|
||||
height 30px
|
||||
padding 0 25px
|
||||
color $ui-text-color
|
||||
colorDefaultButton()
|
||||
|
||||
.body
|
||||
padding 30px
|
||||
|
||||
.body-welcome
|
||||
text-align center
|
||||
margin-bottom 25px
|
||||
font-size 32px
|
||||
color $ui-text-color
|
||||
|
||||
.body-description
|
||||
font-size 14px
|
||||
color $ui-text-color
|
||||
text-align center
|
||||
margin-bottom 25px
|
||||
|
||||
.body-path
|
||||
margin 0 auto 25px
|
||||
width 280px
|
||||
|
||||
.body-path-input
|
||||
height 30px
|
||||
vertical-align middle
|
||||
width 250px
|
||||
font-size 12px
|
||||
border-style solid
|
||||
border-width 1px 0 1px 1px
|
||||
border-color $border-color
|
||||
border-top-left-radius 2px
|
||||
border-bottom-left-radius 2px
|
||||
padding 0 5px
|
||||
|
||||
.body-path-button
|
||||
height 30px
|
||||
border none
|
||||
border-top-right-radius 2px
|
||||
border-bottom-right-radius 2px
|
||||
colorPrimaryButton()
|
||||
vertical-align middle
|
||||
.body-migration
|
||||
margin 0 auto 25px
|
||||
text-align center
|
||||
|
||||
.body-control
|
||||
text-align center
|
||||
|
||||
.body-control-createButton
|
||||
colorPrimaryButton()
|
||||
border none
|
||||
border-radius 2px
|
||||
height 40px
|
||||
padding 0 25px
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user