mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 02:06:29 +00:00
Compare commits
1717 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
414e0dbc05 | ||
|
|
081a3da9e1 | ||
|
|
626175b2b8 | ||
|
|
54db0c718a | ||
|
|
ae9175d1b1 | ||
|
|
16e1f0f882 | ||
|
|
179b830d14 | ||
|
|
aeb27f7bff | ||
|
|
0b2b89da0f | ||
|
|
804991b300 | ||
|
|
529f271e07 | ||
|
|
402d577ce0 | ||
|
|
4bb28a358e | ||
|
|
200a9275d2 | ||
|
|
9cb086cbd9 | ||
|
|
3a836aaf34 | ||
|
|
e14a6f2374 | ||
|
|
28794bff79 | ||
|
|
ecc9443c9a | ||
|
|
a1c499c026 | ||
|
|
4e8321268c | ||
|
|
8510325732 | ||
|
|
49102be894 | ||
|
|
1a97488dd7 | ||
|
|
b90c7e5318 | ||
|
|
ece1e37c16 | ||
|
|
3a6e3f5cae | ||
|
|
8e502eec80 | ||
|
|
042ebe8316 | ||
|
|
eee47b1f76 | ||
|
|
7978704f1b | ||
|
|
9392ac438c | ||
|
|
cbcfb57e35 | ||
|
|
29fa512a90 | ||
|
|
059d1fb4f2 | ||
|
|
4d3a538213 | ||
|
|
108a0db799 | ||
|
|
04f632570d | ||
|
|
92b919da6c | ||
|
|
4f0f611e52 | ||
|
|
90f7cf0996 | ||
|
|
ce43e80bdf | ||
|
|
c397752b7a | ||
|
|
2470364571 | ||
|
|
cab122ba8b | ||
|
|
56b01b5a85 | ||
|
|
cbcc9fb411 | ||
|
|
5bbc60d48a | ||
|
|
8fb8aab7b8 | ||
|
|
5b76216a64 | ||
|
|
7f5e372bdc | ||
|
|
01913a9a40 | ||
|
|
62f9fcf171 | ||
|
|
34c667f6b2 | ||
|
|
7a6de387e3 | ||
|
|
b6efbcedef | ||
|
|
3c70b2e5a0 | ||
|
|
62e108d460 | ||
|
|
b7f426b03f | ||
|
|
5ca13b70aa | ||
|
|
4644d107c6 | ||
|
|
06013b2a6a | ||
|
|
7380fc8500 | ||
|
|
81b550e66c | ||
|
|
f5972c273b | ||
|
|
c259ec2bed | ||
|
|
86a04b0dcb | ||
|
|
8b7c0c957c | ||
|
|
0cfe971a48 | ||
|
|
8884db858c | ||
|
|
0fe9dd4fbb | ||
|
|
ae57e99173 | ||
|
|
c238e1b5cb | ||
|
|
55ef998c55 | ||
|
|
166815ccbf | ||
|
|
34a16298d4 | ||
|
|
1613e72d47 | ||
|
|
e933ad89dc | ||
|
|
148fb25a15 | ||
|
|
fe0544dbc9 | ||
|
|
0aaea28e74 | ||
|
|
f4764afbf4 | ||
|
|
2a2b662a6f | ||
|
|
deade1f9f8 | ||
|
|
2b73c17cca | ||
|
|
369f8b6093 | ||
|
|
5391ca161d | ||
|
|
5b224d3b54 | ||
|
|
dad6a93944 | ||
|
|
10ec5c1342 | ||
|
|
8edfc15f45 | ||
|
|
792e41f161 | ||
|
|
9fbdf895af | ||
|
|
dbc0fedf43 | ||
|
|
cb298b8cad | ||
|
|
2459a80e15 | ||
|
|
0a973c4db3 | ||
|
|
a84fddd5fa | ||
|
|
1cd26d2e71 | ||
|
|
f88ac891ff | ||
|
|
17aa9ae85f | ||
|
|
bb46a9ba4c | ||
|
|
4a6c16df8d | ||
|
|
95787fafc6 | ||
|
|
c51d79e4d7 | ||
|
|
d5232a1b0e | ||
|
|
e093c7f982 | ||
|
|
3f53bddf7e | ||
|
|
e03d8175e3 | ||
|
|
7ab2e9f6ca | ||
|
|
6316ddc6a6 | ||
|
|
11c46ede5d | ||
|
|
2ec5238d97 | ||
|
|
b7f359f6cf | ||
|
|
8ef485221c | ||
|
|
afddb6fc33 | ||
|
|
f28ee1bc4b | ||
|
|
b997f0a5a7 | ||
|
|
48dc47ce57 | ||
|
|
f1a90f4a11 | ||
|
|
990d7edba4 | ||
|
|
13a1da91be | ||
|
|
930b58d2a8 | ||
|
|
b88b1065ee | ||
|
|
5fbb802b32 | ||
|
|
a40f8d25ef | ||
|
|
0a28798d54 | ||
|
|
6047987c25 | ||
|
|
2ce96186f2 | ||
|
|
26357bd4bc | ||
|
|
e94b45a7e4 | ||
|
|
4be1eb7a28 | ||
|
|
f4bbbb640d | ||
|
|
94b4e70b0f | ||
|
|
b31e3f7783 | ||
|
|
6375ac857a | ||
|
|
5eac08430f | ||
|
|
ef0b109ad4 | ||
|
|
7fbe6c0955 | ||
|
|
c86e43597c | ||
|
|
123a73a5e6 | ||
|
|
706b5d253f | ||
|
|
8fb8c7a40b | ||
|
|
21220f93b1 | ||
|
|
705e64377b | ||
|
|
5c3f0cd060 | ||
|
|
a238be5b7c | ||
|
|
2351bb78da | ||
|
|
6bbc5a91fe | ||
|
|
2f52233bd0 | ||
|
|
92eccb635a | ||
|
|
1f8acc3afc | ||
|
|
f8eaa9e796 | ||
|
|
7c9fecd943 | ||
|
|
3df8cbb357 | ||
|
|
993cb9cb0b | ||
|
|
5bd4a3f761 | ||
|
|
fcad84ced3 | ||
|
|
0bd48981ec | ||
|
|
0bf7e8b705 | ||
|
|
875c451221 | ||
|
|
c8256bea3a | ||
|
|
7d10b951b7 | ||
|
|
34e9238cc4 | ||
|
|
7d97784a58 | ||
|
|
1265e7c4a1 | ||
|
|
44ece2bf34 | ||
|
|
fde2a8055d | ||
|
|
f9643c2503 | ||
|
|
e6a97e5cb3 | ||
|
|
73b5546ae9 | ||
|
|
f98719ee73 | ||
|
|
7666182ae3 | ||
|
|
05af30f336 | ||
|
|
20de08b625 | ||
|
|
e60f4f4a64 | ||
|
|
6af25d932c | ||
|
|
3f49a8a15a | ||
|
|
bfa8db7b55 | ||
|
|
d6f2e7588c | ||
|
|
a594332ffb | ||
|
|
ceb18ebf1c | ||
|
|
b9038e254e | ||
|
|
b351e42538 | ||
|
|
8910c26ee2 | ||
|
|
7549a7bbbe | ||
|
|
17fbe6e232 | ||
|
|
ccdac8f604 | ||
|
|
88a828c9ef | ||
|
|
ae3291b90e | ||
|
|
2c6f0452b8 | ||
|
|
4651acd6f4 | ||
|
|
bba7babce3 | ||
|
|
73dc6a4a92 | ||
|
|
992f5a525a | ||
|
|
b38d5789f3 | ||
|
|
47b5945e17 | ||
|
|
73544b0f06 | ||
|
|
e7d9311e23 | ||
|
|
c97c65b707 | ||
|
|
1c02b4e62a | ||
|
|
d23156d11a | ||
|
|
bd013adb4d | ||
|
|
c0368ce713 | ||
|
|
80283b5f55 | ||
|
|
4078645958 | ||
|
|
955ade0b8a | ||
|
|
4b158af9f6 | ||
|
|
642fae3ac7 | ||
|
|
d249967aee | ||
|
|
7b6b7f05e0 | ||
|
|
35b9bf5d34 | ||
|
|
59f0cc4f98 | ||
|
|
a29ca73fb4 | ||
|
|
59b658f059 | ||
|
|
3397b3108f | ||
|
|
cae7baa5e1 | ||
|
|
4af71fd1dd | ||
|
|
4194b61373 | ||
|
|
c91fd6783d | ||
|
|
89bbed1dfd | ||
|
|
2aeb53920c | ||
|
|
fe51c232b6 | ||
|
|
57b054794c | ||
|
|
8318c56046 | ||
|
|
0d52417ee7 | ||
|
|
6f3b1b8d6f | ||
|
|
a460d7722e | ||
|
|
d770208d4c | ||
|
|
0434109908 | ||
|
|
289d3a4e6b | ||
|
|
ffb9be63c7 | ||
|
|
bf2b53cbce | ||
|
|
1d9bf65c31 | ||
|
|
4744b918d3 | ||
|
|
588b1809a9 | ||
|
|
dc1c19293d | ||
|
|
1f548959e3 | ||
|
|
8cae5670fc | ||
|
|
07c0982d4f | ||
|
|
2f9e4b3198 | ||
|
|
89dba149a3 | ||
|
|
aa71b4c1b8 | ||
|
|
43110f8f2a | ||
|
|
e48540713d | ||
|
|
cfd13139e0 | ||
|
|
ac5cdf384f | ||
|
|
e9d858d902 | ||
|
|
1beae4403a | ||
|
|
dedf36f704 | ||
|
|
1477de3899 | ||
|
|
0d947c7dd8 | ||
|
|
ebfd8f40e3 | ||
|
|
3159cc0ded | ||
|
|
10dcbfb891 | ||
|
|
19dc16e14a | ||
|
|
95586b3156 | ||
|
|
0637daf645 | ||
|
|
fdcd62617d | ||
|
|
0f3e5ee4ed | ||
|
|
7b171ecc67 | ||
|
|
7a4052ede3 | ||
|
|
3f53a1f629 | ||
|
|
31daec5fe2 | ||
|
|
0d7155bda6 | ||
|
|
35beec3e39 | ||
|
|
ff4b96b622 | ||
|
|
9b60814292 | ||
|
|
3c4fa83161 | ||
|
|
e8564f6540 | ||
|
|
a22e97d4bd | ||
|
|
046e6af489 | ||
|
|
f805e8a688 | ||
|
|
2fddc32eb7 | ||
|
|
6018cd5d81 | ||
|
|
3533903be3 | ||
|
|
d867292f66 | ||
|
|
7691b662d6 | ||
|
|
86270dd856 | ||
|
|
012e2dde4f | ||
|
|
ad7a3c49f9 | ||
|
|
e8abd43c8a | ||
|
|
3192ce9d39 | ||
|
|
d09de09fef | ||
|
|
4689ddeb98 | ||
|
|
e300b33a4f | ||
|
|
0ca87ea407 | ||
|
|
2886da4f63 | ||
|
|
bf9ecb02e5 | ||
|
|
852617726c | ||
|
|
c2aa35104c | ||
|
|
95e237d4a3 | ||
|
|
59e5c547e9 | ||
|
|
06bd2b2b79 | ||
|
|
faede48217 | ||
|
|
ad0ac19d3d | ||
|
|
3154110de1 | ||
|
|
5248c05e61 | ||
|
|
8311030bec | ||
|
|
c429fc6b2c | ||
|
|
590aa9ab17 | ||
|
|
f9a7c2d457 | ||
|
|
b4506168fb | ||
|
|
f203ab3aaf | ||
|
|
c197dd0a4b | ||
|
|
457e596851 | ||
|
|
d274563b2c | ||
|
|
2003bea3cf | ||
|
|
f9b3284852 | ||
|
|
9bca133d88 | ||
|
|
03fc453608 | ||
|
|
3027cc81b3 | ||
|
|
2415fbf676 | ||
|
|
725c6a7ba9 | ||
|
|
c33da0cf8e | ||
|
|
d915d19425 | ||
|
|
f3370242bf | ||
|
|
0e312ba929 | ||
|
|
6440395197 | ||
|
|
5433abddaf | ||
|
|
0ccb465288 | ||
|
|
8fd4deb3eb | ||
|
|
fe8045c51d | ||
|
|
b890c59134 | ||
|
|
f39caeb967 | ||
|
|
7ab482184b | ||
|
|
78b12ae686 | ||
|
|
caa5deac4e | ||
|
|
af3083825e | ||
|
|
5255708ff2 | ||
|
|
9331f2034b | ||
|
|
fc6a5c22bf | ||
|
|
51c397d177 | ||
|
|
7c9596308e | ||
|
|
15dc424ade | ||
|
|
49243a8010 | ||
|
|
93e188d118 | ||
|
|
df3195fc1e | ||
|
|
da6b8c30a0 | ||
|
|
70468b6b7d | ||
|
|
2511512d94 | ||
|
|
4418617d3b | ||
|
|
6e480ba146 | ||
|
|
b4f5913a80 | ||
|
|
6a3062709c | ||
|
|
d66bc1faef | ||
|
|
bef7d45c3e | ||
|
|
bb9489a8d3 | ||
|
|
700eeb8f5a | ||
|
|
7e2f0049b6 | ||
|
|
b2388544d8 | ||
|
|
d772551c60 | ||
|
|
31dca6f06b | ||
|
|
83f68fe153 | ||
|
|
08a2ae0fd3 | ||
|
|
53d3f51c74 | ||
|
|
f7cdafb087 | ||
|
|
5b17808569 | ||
|
|
a7328e21f1 | ||
|
|
e64370e9a2 | ||
|
|
d5b37b2418 | ||
|
|
4da08d93fd | ||
|
|
c39e5c67f5 | ||
|
|
00d5cf13c9 | ||
|
|
1120bcfc0c | ||
|
|
8e506cb7c2 | ||
|
|
145b66d375 | ||
|
|
82e4a8bbc3 | ||
|
|
bca9bfb960 | ||
|
|
d8e19d9c17 | ||
|
|
95d74d1ca2 | ||
|
|
ebdd6d77f7 | ||
|
|
d56bcc4fdf | ||
|
|
4bb18cfc9a | ||
|
|
6f30692534 | ||
|
|
e249c1ec65 | ||
|
|
c2b4c77003 | ||
|
|
e64733827a | ||
|
|
ea81b0d414 | ||
|
|
000cf2a864 | ||
|
|
dc13b919b3 | ||
|
|
a0c8ec3171 | ||
|
|
80c13f7c4f | ||
|
|
1a6f3d808b | ||
|
|
ec8fac1199 | ||
|
|
98c93d3248 | ||
|
|
a053706c24 | ||
|
|
e1a75a13e9 | ||
|
|
ee6f4de183 | ||
|
|
475885b3ef | ||
|
|
2d2b2d4c6c | ||
|
|
4d00454539 | ||
|
|
bf590b5614 | ||
|
|
3ef33c065c | ||
|
|
8e89fb8b92 | ||
|
|
8e81cfcf89 | ||
|
|
515736262d | ||
|
|
bd266dc602 | ||
|
|
9b3306157c | ||
|
|
f8e6a939ca | ||
|
|
8c48ee6fc1 | ||
|
|
5e476054d7 | ||
|
|
2fc8547384 | ||
|
|
2af2d71540 | ||
|
|
6aaf9d9eb2 | ||
|
|
42a9caf5a3 | ||
|
|
e6c1d7a383 | ||
|
|
02100bbc0a | ||
|
|
ce7c5f5d40 | ||
|
|
69f1ad6eb3 | ||
|
|
8320fb5024 | ||
|
|
4b79bca6bf | ||
|
|
4a5fd41249 | ||
|
|
4e90a93b30 | ||
|
|
9861fbf7c8 | ||
|
|
c762b9ae00 | ||
|
|
cc667a6edf | ||
|
|
419c57ed3f | ||
|
|
601f0b0de8 | ||
|
|
7b1c6c10b7 | ||
|
|
5a85c257cf | ||
|
|
beceb851c2 | ||
|
|
31485d3387 | ||
|
|
fc552e030a | ||
|
|
bf4c9f920a | ||
|
|
4ebd503664 | ||
|
|
0907bc80ef | ||
|
|
2cf46a3332 | ||
|
|
41868f28e6 | ||
|
|
964b7b62de | ||
|
|
0d34a03fe0 | ||
|
|
a62faa471c | ||
|
|
66f3ce2cb2 | ||
|
|
43c49f54d2 | ||
|
|
a15dfffa44 | ||
|
|
59985dee72 | ||
|
|
6b7132f134 | ||
|
|
e313b5e59d | ||
|
|
bb26d9a0a8 | ||
|
|
5c2c99282d | ||
|
|
94e6f89d07 | ||
|
|
3804a746df | ||
|
|
5c2d7e2d2a | ||
|
|
c34dd462b6 | ||
|
|
9141b1a641 | ||
|
|
0fea85e2f2 | ||
|
|
0ca41fbdb4 | ||
|
|
a58c191ded | ||
|
|
77089a1178 | ||
|
|
2cfb883bad | ||
|
|
ec8c8bb669 | ||
|
|
0b54f01107 | ||
|
|
67be198bee | ||
|
|
32a4a1aae1 | ||
|
|
dac7372839 | ||
|
|
521c261a37 | ||
|
|
27367488c2 | ||
|
|
0d5c3b1be6 | ||
|
|
a67d5ffacb | ||
|
|
687440a7c7 | ||
|
|
bafdc24a6d | ||
|
|
047f9c93c5 | ||
|
|
116fafc117 | ||
|
|
060c92091c | ||
|
|
5802525b73 | ||
|
|
c3580caabc | ||
|
|
08c027acc5 | ||
|
|
4468792346 | ||
|
|
1b16c68cf9 | ||
|
|
b99c1e3b32 | ||
|
|
eb2994e3c2 | ||
|
|
d88dd26186 | ||
|
|
59fcc58e9c | ||
|
|
acba61f36a | ||
|
|
a3a55a8bb4 | ||
|
|
22929d84fc | ||
|
|
9ea9d30947 | ||
|
|
7f08428fe2 | ||
|
|
0d80a7d961 | ||
|
|
2899264b54 | ||
|
|
923de0aa0d | ||
|
|
2b729dad15 | ||
|
|
b9b5bae78a | ||
|
|
a9acde07d1 | ||
|
|
a46b8d3079 | ||
|
|
8b92e2cbb7 | ||
|
|
881f5a5110 | ||
|
|
4e986a6384 | ||
|
|
ce5e1babb7 | ||
|
|
b85790d2fa | ||
|
|
6bc3e7fcf1 | ||
|
|
1ca968201d | ||
|
|
f2a03e4cc7 | ||
|
|
a752730718 | ||
|
|
9d20fd91ec | ||
|
|
70a6a3acb8 | ||
|
|
7f52eed4d5 | ||
|
|
105119e1a4 | ||
|
|
169e30e029 | ||
|
|
a5fa3e9e7a | ||
|
|
8985062d34 | ||
|
|
56eb9c76ae | ||
|
|
e5b6762bf3 | ||
|
|
e8bccaef88 | ||
|
|
afdb038244 | ||
|
|
56942d55eb | ||
|
|
9d742c8435 | ||
|
|
6ee4e48de2 | ||
|
|
184f3dc04b | ||
|
|
2027f60014 | ||
|
|
076edd375f | ||
|
|
ab1aa56059 | ||
|
|
46f7dfdfeb | ||
|
|
fcaa5e21cf | ||
|
|
1e202db50f | ||
|
|
9405b95825 | ||
|
|
f05e256afc | ||
|
|
731ffd4a22 | ||
|
|
8f4566b7e1 | ||
|
|
95aec54f60 | ||
|
|
f14ce0d68e | ||
|
|
cc1c7f3820 | ||
|
|
2df901288a | ||
|
|
821a7c780e | ||
|
|
6e2e48fa64 | ||
|
|
2864ac88f5 | ||
|
|
4a292d6518 | ||
|
|
e934182e86 | ||
|
|
d8fa73287b | ||
|
|
35938c09e8 | ||
|
|
9eaa90c691 | ||
|
|
049835d426 | ||
|
|
af91c40406 | ||
|
|
4940ad6825 | ||
|
|
d02b740300 | ||
|
|
9cb443dc2f | ||
|
|
1c7cba2951 | ||
|
|
473b80710d | ||
|
|
2247c0835d | ||
|
|
b7b715ba3d | ||
|
|
6c43fb2325 | ||
|
|
a6fe3c27d4 | ||
|
|
d47ff96b13 | ||
|
|
a0def654bd | ||
|
|
4873b40e49 | ||
|
|
0a758f20a7 | ||
|
|
5e58d457a3 | ||
|
|
0f745361ad | ||
|
|
bf6cae9a0e | ||
|
|
ab640a7676 | ||
|
|
820171e19e | ||
|
|
f1e9d0ab81 | ||
|
|
0646484c83 | ||
|
|
a27b79c213 | ||
|
|
773a9b4b7f | ||
|
|
07b838ef7b | ||
|
|
85217a7171 | ||
|
|
886d7b7227 | ||
|
|
6987b762dd | ||
|
|
f32ac81f84 | ||
|
|
87ea66bb92 | ||
|
|
ff6fd62932 | ||
|
|
76728448ff | ||
|
|
3b7225e0fa | ||
|
|
d6280f4397 | ||
|
|
8df867046f | ||
|
|
331c822816 | ||
|
|
6219173945 | ||
|
|
6207e02e7f | ||
|
|
537ba537dc | ||
|
|
3e919241e6 | ||
|
|
2324327e7e | ||
|
|
b8374494ea | ||
|
|
a480ca7b55 | ||
|
|
bfd67fb7f1 | ||
|
|
e6bd6a5077 | ||
|
|
350af844ca | ||
|
|
a49d53179a | ||
|
|
e68069fd2f | ||
|
|
a193ba3e6c | ||
|
|
9034b3ab54 | ||
|
|
f39b7594ab | ||
|
|
f79734391e | ||
|
|
e54f516418 | ||
|
|
e86464535d | ||
|
|
e8553caa65 | ||
|
|
044e6b7180 | ||
|
|
0266770657 | ||
|
|
2d6f7c08e8 | ||
|
|
0a27819a7f | ||
|
|
3185c25ee1 | ||
|
|
820802cdc2 | ||
|
|
2c169e6f15 | ||
|
|
49be1320f9 | ||
|
|
6bc4ecce48 | ||
|
|
0290d23832 | ||
|
|
e001c97e01 | ||
|
|
976d1bb4f3 | ||
|
|
dee6495b08 | ||
|
|
4a77f250f1 | ||
|
|
c697f19642 | ||
|
|
f7d1f9e949 | ||
|
|
27b9952f8e | ||
|
|
961dab4a97 | ||
|
|
930b6bc927 | ||
|
|
ed7d8258cf | ||
|
|
762425125f | ||
|
|
03bdf0c653 | ||
|
|
7002026190 | ||
|
|
b35395e6bd | ||
|
|
b74fb76d03 | ||
|
|
72d2df465b | ||
|
|
2592c943f7 | ||
|
|
7b3a3ba200 | ||
|
|
8369832585 | ||
|
|
d94a674343 | ||
|
|
260611ffd6 | ||
|
|
690549b57f | ||
|
|
5d9aeb4c04 | ||
|
|
eddfdea2ca | ||
|
|
a40385f87f | ||
|
|
0123526c98 | ||
|
|
275e3317a3 | ||
|
|
d0b835a825 | ||
|
|
f450260ff8 | ||
|
|
5f7b119e5c | ||
|
|
bb9eb494e9 | ||
|
|
6ea2b5e1d9 | ||
|
|
b21baf1ce5 | ||
|
|
4c3fd461e4 | ||
|
|
8365a60d5d | ||
|
|
804ac1aa96 | ||
|
|
49fd2cfc4d | ||
|
|
2bb2a52f27 | ||
|
|
8d71b28afa | ||
|
|
86eac7054d | ||
|
|
dccb92d72b | ||
|
|
2e628de9c6 | ||
|
|
2d243abc12 | ||
|
|
ae08bf4d7a | ||
|
|
cbff5fb585 | ||
|
|
2650cc2f1c | ||
|
|
ec560ceab1 | ||
|
|
ad17cb8837 | ||
|
|
4f98d9641a | ||
|
|
17535ccd4c | ||
|
|
110c1ea337 | ||
|
|
7e087bfbab | ||
|
|
85e0d4b922 | ||
|
|
265262ccbf | ||
|
|
345008e9b6 | ||
|
|
849aa05d76 | ||
|
|
5ff5ec6a71 | ||
|
|
684c3f64aa | ||
|
|
4ff73ede59 | ||
|
|
7302d83f60 | ||
|
|
f9836fd66e | ||
|
|
c0f05695fe | ||
|
|
e50a1172d6 | ||
|
|
7a4234e73c | ||
|
|
4b9640e5a3 | ||
|
|
4b58054100 | ||
|
|
8951dbf4ed | ||
|
|
5047bc94eb | ||
|
|
75fb29c594 | ||
|
|
bd75f7fd2c | ||
|
|
8b8cb3c9b4 | ||
|
|
b479b21c37 | ||
|
|
525c490704 | ||
|
|
2e3599d005 | ||
|
|
f005bb1e46 | ||
|
|
21f96febdb | ||
|
|
805829be78 | ||
|
|
a7bd3f253f | ||
|
|
81e8a290f0 | ||
|
|
f46a967b6e | ||
|
|
b78d9534aa | ||
|
|
bdefaf7427 | ||
|
|
43550c7dec | ||
|
|
1b07d393f2 | ||
|
|
44fc356775 | ||
|
|
5623c68170 | ||
|
|
d5f82943cf | ||
|
|
96a020341d | ||
|
|
99b5f75763 | ||
|
|
a128ff7cd8 | ||
|
|
59329cc5da | ||
|
|
ecd5daaf55 | ||
|
|
514232d720 | ||
|
|
93abfe3202 | ||
|
|
4d9e0e3bd7 | ||
|
|
75d9bf8e38 | ||
|
|
ccb9752254 | ||
|
|
0f2ca19c6c | ||
|
|
d5a77aade4 | ||
|
|
3c89e6a128 | ||
|
|
a36be79977 | ||
|
|
617f0e0f9c | ||
|
|
0db1a94105 | ||
|
|
f29f97a00f | ||
|
|
032b5a59f2 | ||
|
|
1e5bc395ac | ||
|
|
fef4afe660 | ||
|
|
679ed1eacf | ||
|
|
78a9320017 | ||
|
|
927981bf30 | ||
|
|
70ea2f6c14 | ||
|
|
a6779414ff | ||
|
|
b62fcf523a | ||
|
|
d0bbea5a97 | ||
|
|
212d7027fc | ||
|
|
be1a5b09da | ||
|
|
3de2db4459 | ||
|
|
f2405a5b34 | ||
|
|
94abb8f959 | ||
|
|
2e30db05bc | ||
|
|
c93f3cc5dd | ||
|
|
068f8f2ba9 | ||
|
|
0980c3b012 | ||
|
|
5288d6768f | ||
|
|
34491f4ea4 | ||
|
|
8c73ca8854 | ||
|
|
0786d8eab6 | ||
|
|
209518c815 | ||
|
|
b2753b6457 | ||
|
|
4775a920c0 | ||
|
|
0e94dc8740 | ||
|
|
cf68d202d5 | ||
|
|
d29ba2bf16 | ||
|
|
720686cef5 | ||
|
|
0625c65cf0 | ||
|
|
acaefe22d1 | ||
|
|
df1f083ebf | ||
|
|
5a1dfc2ca9 | ||
|
|
d84894f1bf | ||
|
|
0fdc444c4c | ||
|
|
4d216c6f13 | ||
|
|
1c0af8eede | ||
|
|
f443f9264a | ||
|
|
d9b2981327 | ||
|
|
68e36d2a6d | ||
|
|
68b91bf98c | ||
|
|
b91ddfad05 | ||
|
|
a110cbfb5d | ||
|
|
d69f45b0c9 | ||
|
|
07df26d5c4 | ||
|
|
d24bcb7f86 | ||
|
|
9f383ba491 | ||
|
|
c38a76d587 | ||
|
|
994ca5dd02 | ||
|
|
9281f8f6cb | ||
|
|
324f579474 | ||
|
|
10bd2d4547 | ||
|
|
d1942868e4 | ||
|
|
a409b3e48d | ||
|
|
21004aab6a | ||
|
|
eaf88c6491 | ||
|
|
17dcd1b6f1 | ||
|
|
244a06eea6 | ||
|
|
afb13af7a1 | ||
|
|
ff9b935e98 | ||
|
|
4250d6fe52 | ||
|
|
5bc2094f10 | ||
|
|
552653c0ed | ||
|
|
c9bbc61c95 | ||
|
|
41a04aa3f1 | ||
|
|
cd421c4662 | ||
|
|
063e2e02bd | ||
|
|
0c3019b52e | ||
|
|
2ee2494dc4 | ||
|
|
df6ff1fffe | ||
|
|
0d2e6a6a12 | ||
|
|
79ed55a76f | ||
|
|
cbe58b9437 | ||
|
|
afc729b1c3 | ||
|
|
5d0cb0302e | ||
|
|
29e0d121cd | ||
|
|
48ca13f82c | ||
|
|
278061e4f1 | ||
|
|
186a815821 | ||
|
|
2fea9eb874 | ||
|
|
f8b6453be9 | ||
|
|
be625f8884 | ||
|
|
2271def5d3 | ||
|
|
275a8ea7cc | ||
|
|
dc236f33b1 | ||
|
|
678d739e75 | ||
|
|
8fe05a4c24 | ||
|
|
77adfdb9f0 | ||
|
|
30c94028fb | ||
|
|
b5bf0780fa | ||
|
|
ad838d82ee | ||
|
|
d5ec0c0cdd | ||
|
|
ff29f1e0c6 | ||
|
|
1f58698a04 | ||
|
|
a275f331d0 | ||
|
|
0862c6e059 | ||
|
|
46fddfd26c | ||
|
|
c547ccb4cb | ||
|
|
86e82fb149 | ||
|
|
00a284bfbd | ||
|
|
f6fbec0a2e | ||
|
|
b24c06bee6 | ||
|
|
95e7f4f645 | ||
|
|
4b75d501f5 | ||
|
|
b28d5c8b25 | ||
|
|
86e61661e6 | ||
|
|
cdd5717e63 | ||
|
|
42bd7fb4fb | ||
|
|
66e478a001 | ||
|
|
c8259abcac | ||
|
|
d6d16a63a4 | ||
|
|
df8d1f0714 | ||
|
|
fbbc6411c3 | ||
|
|
bc1e94fcab | ||
|
|
418439e3d5 | ||
|
|
46cbd04ba7 | ||
|
|
0ade6d9ece | ||
|
|
823599192f | ||
|
|
9496ab88f7 | ||
|
|
290e6ab170 | ||
|
|
a9e3572f4f | ||
|
|
92b2b7dde3 | ||
|
|
dad1e40234 | ||
|
|
b75c8b0373 | ||
|
|
fc4c471a87 | ||
|
|
24de71f240 | ||
|
|
805c39e60c | ||
|
|
10e75041e8 | ||
|
|
2364348df4 | ||
|
|
e643443660 | ||
|
|
d72e876b23 | ||
|
|
f3612774ba | ||
|
|
67d8b02c49 | ||
|
|
7abf53009a | ||
|
|
90bb4632b6 | ||
|
|
630da00235 | ||
|
|
245e0dd7ee | ||
|
|
824e288a80 | ||
|
|
99228f2e60 | ||
|
|
0f71139eba | ||
|
|
2bbe7056d1 | ||
|
|
005d8f84fd | ||
|
|
908cd7a890 | ||
|
|
d4865adf6a | ||
|
|
20411a2fd5 | ||
|
|
19f8930f5a | ||
|
|
ffc4ca1dd4 | ||
|
|
0bc9c6fb5a | ||
|
|
983f453afd | ||
|
|
60a0293495 | ||
|
|
4807d22763 | ||
|
|
e8d451bcbb | ||
|
|
4809bb6f06 | ||
|
|
d6d694cf66 | ||
|
|
132e3c3088 | ||
|
|
3c7ff78549 | ||
|
|
adbdc2cb1c | ||
|
|
e52b74bbc9 | ||
|
|
575f1b4b33 | ||
|
|
a528c99900 | ||
|
|
e6047ed383 | ||
|
|
381b7d960a | ||
|
|
045a8bde22 | ||
|
|
e528182c2a | ||
|
|
b86cdb461a | ||
|
|
7265f76770 | ||
|
|
2ed092279d | ||
|
|
7d71819be0 | ||
|
|
e848c74511 | ||
|
|
968ed146b2 | ||
|
|
5e03f12875 | ||
|
|
09f8e5f25a | ||
|
|
89f4ed3006 | ||
|
|
8d14a9557d | ||
|
|
513042d769 | ||
|
|
c28980c2a9 | ||
|
|
1c31ff4e98 | ||
|
|
7c9d3904b3 | ||
|
|
e23707f0a0 | ||
|
|
a952b18b96 | ||
|
|
35cc07bf63 | ||
|
|
118bf18434 | ||
|
|
34da15208b | ||
|
|
e0dc62c00b | ||
|
|
b58df2f172 | ||
|
|
90846fab81 | ||
|
|
efd80d5c0c | ||
|
|
e37e28a22e | ||
|
|
a1e71b318c | ||
|
|
aee6541d45 | ||
|
|
50ad5e3791 | ||
|
|
b34d72f21a | ||
|
|
e5ae420ef6 | ||
|
|
7269750264 | ||
|
|
33fe3ff733 | ||
|
|
aa77180957 | ||
|
|
3d80b6a4cd | ||
|
|
d76f9243a3 | ||
|
|
88f1a0d5cd | ||
|
|
26c435d6da | ||
|
|
e66abdea2d | ||
|
|
c7d2eeb71a | ||
|
|
0e8d681954 | ||
|
|
433d110cf0 | ||
|
|
4a514cd7bd | ||
|
|
417fee16bd | ||
|
|
49a821d9ee | ||
|
|
6a5ce098e0 | ||
|
|
1af73eebea | ||
|
|
b7ba29ac92 | ||
|
|
133c2ec308 | ||
|
|
a70fe1bba8 | ||
|
|
6dcd653eac | ||
|
|
db2f90b1ce | ||
|
|
60e5665133 | ||
|
|
7aa982849f | ||
|
|
d94f7626c2 | ||
|
|
549c289f81 | ||
|
|
b35953d1f9 | ||
|
|
941c4aeb19 | ||
|
|
11f7fcbaef | ||
|
|
6b8488ae0f | ||
|
|
ee5268a07e | ||
|
|
28bc331318 | ||
|
|
098153b6ba | ||
|
|
878f31e91e | ||
|
|
b40d09fa86 | ||
|
|
eb48f48f6b | ||
|
|
7961008500 | ||
|
|
ff87c6b226 | ||
|
|
0c2807a08b | ||
|
|
f0cf369317 | ||
|
|
5e9954c060 | ||
|
|
0c7bdf20af | ||
|
|
9c1179a6f9 | ||
|
|
43cb290c80 | ||
|
|
f8b7b7df9f | ||
|
|
b73f0a8012 | ||
|
|
75b2c7bd2e | ||
|
|
5fd9866eef | ||
|
|
4e7204bdbc | ||
|
|
ff7024e38f | ||
|
|
8c11a0b42d | ||
|
|
2df295dc1d | ||
|
|
b695d27817 | ||
|
|
8e2fd300f6 | ||
|
|
1722e103fc | ||
|
|
71464112ce | ||
|
|
003d8a1b21 | ||
|
|
adf81175f3 | ||
|
|
39274ce483 | ||
|
|
1daf07edeb | ||
|
|
cd2dc471c7 | ||
|
|
6229ca7ac9 | ||
|
|
10043cc755 | ||
|
|
e60a5430b4 | ||
|
|
f7e0cb655f | ||
|
|
04097ecfcd | ||
|
|
2222192bcd | ||
|
|
ae93c38d46 | ||
|
|
9ff9dcbe6d | ||
|
|
94d442af7d | ||
|
|
6a711d6a71 | ||
|
|
9d8e71aeb3 | ||
|
|
cfeeba209e | ||
|
|
455029851a | ||
|
|
d0f7baaad0 | ||
|
|
e3fb236139 | ||
|
|
42296e421a | ||
|
|
c6f4ed7c8f | ||
|
|
e1fb36d64d | ||
|
|
48cf695e11 | ||
|
|
5b0e0c71a0 | ||
|
|
c1a76b6fb4 | ||
|
|
945a6306ec | ||
|
|
313bacf9dc | ||
|
|
9027f48dda | ||
|
|
eea8f7cdf4 | ||
|
|
ffef239aa7 | ||
|
|
50fc15feea | ||
|
|
2d7a37c872 | ||
|
|
db468fc095 | ||
|
|
9a9f0035c2 | ||
|
|
b9270cd040 | ||
|
|
65b1bd18c4 | ||
|
|
c87ecc3d40 | ||
|
|
e39e1648f9 | ||
|
|
091d2618a2 | ||
|
|
d039b17715 | ||
|
|
e350dca72c | ||
|
|
c07e334f03 | ||
|
|
1ccfd8e392 | ||
|
|
d22c40f0a5 | ||
|
|
3d37db6e54 | ||
|
|
22bd92916b | ||
|
|
de303cf072 | ||
|
|
b22aad0678 | ||
|
|
bf02f9b256 | ||
|
|
6440ab423e | ||
|
|
fceab73226 | ||
|
|
fded0ad3e8 | ||
|
|
294eb64686 | ||
|
|
851b28c482 | ||
|
|
63fa2f1149 | ||
|
|
816d8decbd | ||
|
|
85b4eedcd6 | ||
|
|
901f40a669 | ||
|
|
bae5f202a5 | ||
|
|
131f6780c2 | ||
|
|
a0e067cacf | ||
|
|
8cdcf537be | ||
|
|
575f5f160c | ||
|
|
d8fbac584c | ||
|
|
b73e1b04dc | ||
|
|
3dd49c287d | ||
|
|
6ce6a7036b | ||
|
|
c120633f14 | ||
|
|
6e84b24309 | ||
|
|
343e35bb54 | ||
|
|
09cf94d807 | ||
|
|
a5531e20f2 | ||
|
|
0f311120af | ||
|
|
614506cada | ||
|
|
7891d14a0a | ||
|
|
2df12b6891 | ||
|
|
97b42d6be1 | ||
|
|
39baadeb04 | ||
|
|
a6f5452a85 | ||
|
|
29dc3bd550 | ||
|
|
e103605956 | ||
|
|
c510c2e540 | ||
|
|
41d65e4132 | ||
|
|
b6cb532568 | ||
|
|
a034ea3a05 | ||
|
|
adeb45a9ce | ||
|
|
4ada755793 | ||
|
|
c4be052a49 | ||
|
|
54c2d7bac9 | ||
|
|
233ab17992 | ||
|
|
e251ec64dc | ||
|
|
32bd6d76ee | ||
|
|
3fc17634aa | ||
|
|
555d725e7b | ||
|
|
35416796b5 | ||
|
|
6c737fe25f | ||
|
|
1b58e320aa | ||
|
|
658a90bf15 | ||
|
|
d092e75f3c | ||
|
|
162fae19cc | ||
|
|
58f5035ec6 | ||
|
|
c5076e4e95 | ||
|
|
28ef1e625c | ||
|
|
97441ccacb | ||
|
|
6b29aed6c4 | ||
|
|
31eb9caee4 | ||
|
|
64cf34e673 | ||
|
|
8996ebb819 | ||
|
|
a36c62044b | ||
|
|
13418109ea | ||
|
|
fe7c05aaa5 | ||
|
|
116e27e0db | ||
|
|
6b135afe1a | ||
|
|
7a9c4951a2 | ||
|
|
041a51a70f | ||
|
|
7b4ff9906e | ||
|
|
11ab5c7598 | ||
|
|
64d53c611b | ||
|
|
657d69a0fb | ||
|
|
62d39e6715 | ||
|
|
a27d8192ee | ||
|
|
41b69afe03 | ||
|
|
54b5af0741 | ||
|
|
d4060f8a5a | ||
|
|
2935d41aba | ||
|
|
af6cd10e28 | ||
|
|
435c80d870 | ||
|
|
775cec32da | ||
|
|
e8cc7abadc | ||
|
|
c1051afdc0 | ||
|
|
573d3ce11e | ||
|
|
0a89dcc6d8 | ||
|
|
d45033ae8e | ||
|
|
313e8b8c98 | ||
|
|
25685dc8b0 | ||
|
|
d6a78dfe28 | ||
|
|
de45852790 | ||
|
|
c6a505cb44 | ||
|
|
44427a40b7 | ||
|
|
8a2ac08c0a | ||
|
|
7babf66d5f | ||
|
|
50cd0b794b | ||
|
|
720f07f62c | ||
|
|
6daffbcafa | ||
|
|
b76729e836 | ||
|
|
b2294e0fc9 | ||
|
|
3559737e8e | ||
|
|
40b7ce607b | ||
|
|
1de67a00cb | ||
|
|
6b4b44dba8 | ||
|
|
c424cc5d33 | ||
|
|
5b71d010b4 | ||
|
|
b9457d3e33 | ||
|
|
60e267409c | ||
|
|
e6a2521143 | ||
|
|
ae36ed2b46 | ||
|
|
fb2ed81fd3 | ||
|
|
a74651b515 | ||
|
|
6904c192e4 | ||
|
|
6540d2670c | ||
|
|
966ba06bc4 | ||
|
|
3b4921b848 | ||
|
|
b11e10ac07 | ||
|
|
2ec7ba04f5 | ||
|
|
095910d156 | ||
|
|
c39463aea8 | ||
|
|
87e47c7ffb | ||
|
|
359f6734c5 | ||
|
|
1d3e71cf49 | ||
|
|
1ab449cecf | ||
|
|
44dd609134 | ||
|
|
1e8e161a33 | ||
|
|
c56d232e58 | ||
|
|
8315b75587 | ||
|
|
562b0592af | ||
|
|
1fd1bed01a | ||
|
|
4f116cba34 | ||
|
|
9d1d57f183 | ||
|
|
6feeee8933 | ||
|
|
5541c0dc38 | ||
|
|
4a8054faed | ||
|
|
bbced7be25 | ||
|
|
893a92c87b | ||
|
|
4055ce19cd | ||
|
|
bcf27233bc | ||
|
|
45111e1610 | ||
|
|
2af86dfa3e | ||
|
|
1b474e1c28 | ||
|
|
c4370694cc | ||
|
|
91f24d96b9 | ||
|
|
bb0b74e889 | ||
|
|
525ab900bd | ||
|
|
31c04de7b6 | ||
|
|
dea0c4287b | ||
|
|
cec4b3132c | ||
|
|
f3ed22dd51 | ||
|
|
6aa9104076 | ||
|
|
e7fd18967b | ||
|
|
8a5558db55 | ||
|
|
4767f15e9b | ||
|
|
b7ca4668e9 | ||
|
|
70e637fada | ||
|
|
459b0ff030 | ||
|
|
2903788fd4 | ||
|
|
af0fdb9277 | ||
|
|
41a58583dc | ||
|
|
c80a26fe0b | ||
|
|
806c3bbaf9 | ||
|
|
fe1c197138 | ||
|
|
b577ca2bc2 | ||
|
|
70a97a6a2a | ||
|
|
034f46792b | ||
|
|
3dc1b59753 | ||
|
|
98b761f1d1 | ||
|
|
712301436d | ||
|
|
4243afb033 | ||
|
|
0f43485606 | ||
|
|
b91b88f16e | ||
|
|
2f2c500e4a | ||
|
|
7065fad69b | ||
|
|
6fcbca6b10 | ||
|
|
93b15f2a7a | ||
|
|
df9d8ff735 | ||
|
|
fda17e044e | ||
|
|
b4e54fc149 | ||
|
|
48514d1020 | ||
|
|
49a4ec5e16 | ||
|
|
c3e92b3b81 | ||
|
|
e78492983a | ||
|
|
0f3230110c | ||
|
|
65573bd4db | ||
|
|
928df018dc | ||
|
|
b2187b72ab | ||
|
|
aae2bddd32 | ||
|
|
b6eddf0821 | ||
|
|
fac0abaed6 | ||
|
|
a6bd239592 | ||
|
|
7845bbd881 | ||
|
|
68bc440749 | ||
|
|
6dbe3cec69 | ||
|
|
850c339bb3 | ||
|
|
57835d0e32 | ||
|
|
ac2c50c8bc | ||
|
|
7296cbe4ec | ||
|
|
16061a7eba | ||
|
|
566fe92589 | ||
|
|
83cef13f1c | ||
|
|
4bb9533049 | ||
|
|
d07c62e266 | ||
|
|
8beb661af4 | ||
|
|
5a201dd1b9 | ||
|
|
aa0ad3bb70 | ||
|
|
f7fb531902 | ||
|
|
c65db4e2b0 | ||
|
|
b32b38bb0d | ||
|
|
d6171dc502 | ||
|
|
7b5a7aabed | ||
|
|
77eb19af40 | ||
|
|
e68d535fa2 | ||
|
|
6e5f6cc739 | ||
|
|
bbeeeccb31 | ||
|
|
e9525fae22 | ||
|
|
672d409bf2 | ||
|
|
6624178864 | ||
|
|
4a66c6717c | ||
|
|
dd76bc027b | ||
|
|
74ee6ae6ce | ||
|
|
1ae3f295f3 | ||
|
|
3cb2ce41fe | ||
|
|
5534319e93 | ||
|
|
c7373c15a5 | ||
|
|
dbf1d6403b | ||
|
|
743b220953 | ||
|
|
95d74c6f5b | ||
|
|
f04b7db9fc | ||
|
|
900fa023fb | ||
|
|
ad9da44afb | ||
|
|
c827717202 | ||
|
|
7d3caa3c2e | ||
|
|
fde7fbccac | ||
|
|
56f06fa7d5 | ||
|
|
c0fba82e73 | ||
|
|
b6304a04e6 | ||
|
|
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 | ||
|
|
a641a7b3e4 | ||
|
|
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 |
6
.babelrc
6
.babelrc
@@ -3,6 +3,12 @@
|
||||
"env": {
|
||||
"development": {
|
||||
"presets": ["react-hmre"]
|
||||
},
|
||||
"test": {
|
||||
"presets": ["react", "es2015"],
|
||||
"plugins": [
|
||||
[ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33
.boostnoterc.sample
Normal file
33
.boostnoterc.sample
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"amaEnabled": true,
|
||||
"editor": {
|
||||
"fontFamily": "Monaco, Consolas",
|
||||
"fontSize": "14",
|
||||
"indentSize": "2",
|
||||
"indentType": "space",
|
||||
"keyMap": "vim",
|
||||
"switchPreview": "BLUR",
|
||||
"theme": "monokai"
|
||||
},
|
||||
"hotkey": {
|
||||
"toggleFinder": "Cmd + Alt + S",
|
||||
"toggleMain": "Cmd + Alt + L"
|
||||
},
|
||||
"isSideNavFolded": false,
|
||||
"listStyle": "DEFAULT",
|
||||
"listWidth": 174,
|
||||
"navWidth": 200,
|
||||
"preview": {
|
||||
"codeBlockTheme": "dracula",
|
||||
"fontFamily": "Lato",
|
||||
"fontSize": "14",
|
||||
"lineNumber": true
|
||||
},
|
||||
"sortBy": "UPDATED_AT",
|
||||
"ui": {
|
||||
"defaultNote": "ALWAYS_ASK",
|
||||
"disableDirectWrite": false,
|
||||
"theme": "default"
|
||||
},
|
||||
"zoom": 1
|
||||
}
|
||||
3
.eslintignore
Normal file
3
.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
compiled/
|
||||
dist/
|
||||
16
.eslintrc
Normal file
16
.eslintrc
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": ["standard", "standard-jsx", "plugin:react/recommended"],
|
||||
"plugins": ["react"],
|
||||
"rules": {
|
||||
"no-useless-escape": 0,
|
||||
"prefer-const": "warn",
|
||||
"no-unused-vars": "warn",
|
||||
"no-undef": "warn",
|
||||
"no-lone-blocks": "warn",
|
||||
"react/prop-types": 0,
|
||||
"react/no-string-refs": 0,
|
||||
"react/no-find-dom-node": "warn",
|
||||
"react/no-render-return-value": "warn",
|
||||
"react/no-deprecated": "warn"
|
||||
}
|
||||
}
|
||||
BIN
.snapcraft/travis_snapcraft.cfg
Normal file
BIN
.snapcraft/travis_snapcraft.cfg
Normal file
Binary file not shown.
20
.travis.yml
Normal file
20
.travis.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- stable
|
||||
- lts/*
|
||||
script:
|
||||
- npm run lint && npm run test
|
||||
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@5.2 && grunt pre-build; fi'
|
||||
after_success:
|
||||
- openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv
|
||||
-in .snapcraft/travis_snapcraft.cfg -out .snapcraft/snapcraft.cfg -d
|
||||
sudo: required
|
||||
services:
|
||||
- docker
|
||||
deploy:
|
||||
'on':
|
||||
branch: master
|
||||
provider: script
|
||||
script: if [ ${TRAVIS_NODE_VERSION} = "stable" ];then docker run -v $(pwd):$(pwd) -t snapcore/snapcraft sh -c "apt update -qq
|
||||
&& cd $(pwd) && snapcraft && snapcraft push *.snap --release edge"; fi
|
||||
skip_cleanup: true
|
||||
49
Backers.md
Normal file
49
Backers.md
Normal file
@@ -0,0 +1,49 @@
|
||||
<h1 align="center">Sponsors & Backers</h1>
|
||||
|
||||
Boostnote is an open source project. It's an independent project with its ongoing development made possible entirely thanks to the support by these awesome [backers](https://github.com/BoostIO/Boostnote/blob/master/Backers.md). If you'd like to join them, please consider:
|
||||
|
||||
- [Become a backer or sponsor on Open Collective.](https://opencollective.com/boostnoteio)
|
||||
|
||||
---
|
||||
|
||||
## Backers via OpenCollective
|
||||
<a href="https://opencollective.com/boostnoteio#backers" target="_blank"><img src="https://opencollective.com/boostnoteio/backers.svg?width=890"></a>
|
||||
|
||||
- [Ralph03](https://opencollective.com/ralph03) - $24
|
||||
|
||||
- [Nikolas Dan](https://opencollective.com/nikolas-dan) - $20
|
||||
|
||||
- [tatoosh11](https://twitter.com/ta11) - $10
|
||||
|
||||
- [Alexander Borovkov](https://opencollective.com/alexander-borovkov) - $10
|
||||
|
||||
- [Yeojong Kim](https://twitter.com/yeojoy) - $5
|
||||
|
||||
- [Scotia Draven](https://opencollective.com/scotia-draven) - $5
|
||||
|
||||
- [spoonhoop](https://opencollective.com/spoonhoop) - $5
|
||||
|
||||
## Backers via Bountysource
|
||||
https://salt.bountysource.com/teams/boostnote
|
||||
|
||||
- Kuzz - $65
|
||||
|
||||
- Intense Raiden - $45
|
||||
|
||||
- ravy22 - $25
|
||||
|
||||
- trentpolack - $20
|
||||
|
||||
- hikariru - $10
|
||||
|
||||
- kolchan11 - $10
|
||||
|
||||
- RonWalker22 - $10
|
||||
|
||||
- hocchuc - $5
|
||||
|
||||
- Adam - $5
|
||||
|
||||
- Steve - $5
|
||||
|
||||
- evmin - $5
|
||||
10
ISSUE_TEMPLATE.md
Normal file
10
ISSUE_TEMPLATE.md
Normal file
@@ -0,0 +1,10 @@
|
||||
<!--
|
||||
Please paste some **screenshots** with the **developer tool** open (console tab) when you report a bug.
|
||||
|
||||
If your issue is regarding boostnote mobile, move to https://github.com/BoostIO/boostnote-mobile.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Love Boostnote? Please consider supporting us via OpenCollective:
|
||||
👉 https://opencollective.com/boostnoteio
|
||||
-->
|
||||
6
LICENSE
6
LICENSE
@@ -1,6 +1,8 @@
|
||||
Boostnote - the simplest note app
|
||||
GPL-3.0
|
||||
|
||||
Copyright (C) 2016 MAISIN&CO.
|
||||
Boostnote - an open source note-taking app made for programmers just like you.
|
||||
|
||||
Copyright (C) 2017 Maisin&Co., Inc.
|
||||
|
||||
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
|
||||
|
||||
@@ -1,206 +1,181 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import modes from '../lib/modes'
|
||||
import _ from 'lodash'
|
||||
import CodeMirror from 'codemirror'
|
||||
import path from 'path'
|
||||
import copyImage from 'browser/main/lib/dataApi/copyImage'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import fs from 'fs'
|
||||
|
||||
const ace = window.ace
|
||||
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 = (e) => {
|
||||
e.stopPropagation()
|
||||
this.blurHandler = (editor, e) => {
|
||||
if (e == null) return null
|
||||
let el = e.relatedTarget
|
||||
let isStillFocused = false
|
||||
while (el != null) {
|
||||
if (el === this.refs.root) {
|
||||
isStillFocused = true
|
||||
break
|
||||
return
|
||||
}
|
||||
el = el.parentNode
|
||||
}
|
||||
|
||||
if (!isStillFocused && this.props.onBlur != null) this.props.onBlur(e)
|
||||
this.props.onBlur != null && this.props.onBlur(e)
|
||||
}
|
||||
|
||||
this.killedBuffer = ''
|
||||
this.execHandler = (e) => {
|
||||
console.info('ACE COMMAND >> %s', e.command.name)
|
||||
switch (e.command.name) {
|
||||
case 'gotolinestart':
|
||||
e.preventDefault()
|
||||
{
|
||||
let position = this.editor.getCursorPosition()
|
||||
this.editor.navigateTo(position.row, 0)
|
||||
}
|
||||
break
|
||||
case 'gotolineend':
|
||||
e.preventDefault()
|
||||
let position = this.editor.getCursorPosition()
|
||||
this.editor.navigateTo(position.row, this.editor.getSession().getLine(position.row).length)
|
||||
break
|
||||
case 'jumptomatching':
|
||||
e.preventDefault()
|
||||
this.editor.navigateUp()
|
||||
break
|
||||
case 'removetolineend':
|
||||
e.preventDefault()
|
||||
let range = this.editor.getSelectionRange()
|
||||
let session = this.editor.getSession()
|
||||
if (range.isEmpty()) {
|
||||
range.setEnd(range.start.row, session.getLine(range.start.row).length)
|
||||
this.killedBuffer = session.getTextRange(range)
|
||||
if (this.killedBuffer.length > 0) {
|
||||
console.log('remove to lineend')
|
||||
session.remove(range)
|
||||
} else {
|
||||
if (session.getLength() === range.start.row) {
|
||||
return
|
||||
}
|
||||
range.setStart(range.start.row, range.end.col)
|
||||
range.setEnd(range.start.row + 1, 0)
|
||||
this.killedBuffer = '\n'
|
||||
session.remove(range)
|
||||
}
|
||||
} else {
|
||||
this.killedBuffer = session.getTextRange(range)
|
||||
session.remove(range)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.afterExecHandler = (e) => {
|
||||
switch (e.command.name) {
|
||||
case 'find':
|
||||
Array.prototype.forEach.call(ReactDOM.findDOMNode(this).querySelectorAll('.ace_search_field, .ace_searchbtn, .ace_replacebtn, .ace_searchbtn_close'), (el) => {
|
||||
el.removeEventListener('blur', this.blurHandler)
|
||||
el.addEventListener('blur', this.blurHandler)
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
this.state = {
|
||||
}
|
||||
|
||||
this.silentChange = false
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.readOnly !== this.props.readOnly) {
|
||||
this.editor.setReadOnly(!!nextProps.readOnly)
|
||||
this.pasteHandler = (editor, e) => this.handlePaste(editor, e)
|
||||
this.loadStyleHandler = (e) => {
|
||||
this.editor.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
let { mode, value, theme, fontSize } = this.props
|
||||
this.value = value
|
||||
let el = ReactDOM.findDOMNode(this)
|
||||
let editor = this.editor = ace.edit(el)
|
||||
editor.$blockScrolling = Infinity
|
||||
editor.renderer.setShowGutter(true)
|
||||
editor.setTheme('ace/theme/' + theme)
|
||||
editor.moveCursorTo(0, 0)
|
||||
editor.setReadOnly(!!this.props.readOnly)
|
||||
editor.setFontSize(fontSize)
|
||||
|
||||
editor.on('blur', this.blurHandler)
|
||||
|
||||
editor.commands.addCommand({
|
||||
name: 'Emacs cursor up',
|
||||
bindKey: {mac: 'Ctrl-P'},
|
||||
exec: function (editor) {
|
||||
editor.navigateUp(1)
|
||||
if (editor.getCursorPosition().row < editor.getFirstVisibleRow()) editor.scrollToLine(editor.getCursorPosition().row, false, false)
|
||||
},
|
||||
readOnly: true
|
||||
})
|
||||
editor.commands.addCommand({
|
||||
name: 'Emacs kill buffer',
|
||||
bindKey: {mac: 'Ctrl-Y'},
|
||||
exec: function (editor) {
|
||||
editor.insert(this.killedBuffer)
|
||||
}.bind(this),
|
||||
readOnly: true
|
||||
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',
|
||||
dragDrop: false,
|
||||
extraKeys: {
|
||||
Tab: function (cm) {
|
||||
const cursor = cm.getCursor()
|
||||
const line = cm.getLine(cursor.line)
|
||||
if (cm.somethingSelected()) cm.indentSelection('add')
|
||||
else {
|
||||
const tabs = cm.getOption('indentWithTabs')
|
||||
if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)\] )?$/)) {
|
||||
cm.execCommand('goLineStart')
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab')
|
||||
}
|
||||
cm.execCommand('goLineEnd')
|
||||
} else {
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'Cmd-T': function (cm) {
|
||||
// Do nothing
|
||||
},
|
||||
Enter: 'newlineAndIndentContinueMarkdownList',
|
||||
'Ctrl-C': (cm) => {
|
||||
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
|
||||
document.execCommand('copy')
|
||||
}
|
||||
return CodeMirror.Pass
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
editor.commands.on('exec', this.execHandler)
|
||||
editor.commands.on('afterExec', this.afterExecHandler)
|
||||
this.setMode(this.props.mode)
|
||||
|
||||
var session = editor.getSession()
|
||||
mode = _.find(modes, {name: mode})
|
||||
let syntaxMode = mode != null
|
||||
? mode.mode
|
||||
: 'text'
|
||||
session.setMode('ace/mode/' + syntaxMode)
|
||||
this.editor.on('blur', this.blurHandler)
|
||||
this.editor.on('change', this.changeHandler)
|
||||
this.editor.on('paste', this.pasteHandler)
|
||||
|
||||
session.setUseSoftTabs(this.props.indentType === 'space')
|
||||
session.setTabSize(this.props.indentSize)
|
||||
session.setOption('useWorker', true)
|
||||
session.setUseWrapMode(true)
|
||||
session.setValue(_.isString(value) ? value : '')
|
||||
let editorTheme = document.getElementById('editorTheme')
|
||||
editorTheme.addEventListener('load', this.loadStyleHandler)
|
||||
|
||||
session.on('change', this.changeHandler)
|
||||
CodeMirror.Vim.defineEx('quit', 'q', this.quitEditor)
|
||||
CodeMirror.Vim.defineEx('q!', 'q!', this.quitEditor)
|
||||
CodeMirror.Vim.defineEx('wq', 'wq', this.quitEditor)
|
||||
CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor)
|
||||
CodeMirror.Vim.map('ZZ', ':q', 'normal')
|
||||
}
|
||||
|
||||
quitEditor () {
|
||||
document.querySelector('textarea').blur()
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.editor.getSession().removeListener('change', this.changeHandler)
|
||||
this.editor.removeListener('blur', this.blurHandler)
|
||||
this.editor.commands.removeListener('exec', this.execHandler)
|
||||
this.editor.commands.removeListener('afterExec', this.afterExecHandler)
|
||||
this.editor.off('blur', this.blurHandler)
|
||||
this.editor.off('change', this.changeHandler)
|
||||
this.editor.off('paste', this.pasteHandler)
|
||||
let editorTheme = document.getElementById('editorTheme')
|
||||
editorTheme.removeEventListener('load', this.loadStyleHandler)
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
let { value } = this.props
|
||||
this.value = value
|
||||
let editor = this.editor
|
||||
let session = this.editor.getSession()
|
||||
|
||||
let needRefresh = false
|
||||
if (prevProps.mode !== this.props.mode) {
|
||||
let mode = _.find(modes, {name: this.props.mode})
|
||||
let syntaxMode = mode != null
|
||||
? mode.mode
|
||||
: 'text'
|
||||
session.setMode('ace/mode/' + syntaxMode)
|
||||
this.setMode(this.props.mode)
|
||||
}
|
||||
if (prevProps.theme !== this.props.theme) {
|
||||
editor.setTheme('ace/theme/' + this.props.theme)
|
||||
this.editor.setOption('theme', this.props.theme)
|
||||
// editor should be refreshed after css loaded
|
||||
}
|
||||
if (prevProps.fontSize !== this.props.fontSize) {
|
||||
editor.setFontSize(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) {
|
||||
session.setTabSize(this.props.indentSize)
|
||||
this.editor.setOption('indentUnit', this.props.indentSize)
|
||||
this.editor.setOption('tabSize', this.props.indentSize)
|
||||
}
|
||||
if (prevProps.indentType !== this.props.indentType) {
|
||||
session.setUseSoftTabs(this.props.indentType === 'space')
|
||||
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.value = this.editor.getValue()
|
||||
this.props.onChange(e)
|
||||
}
|
||||
}
|
||||
|
||||
getFirstVisibleRow () {
|
||||
return this.editor.getFirstVisibleRow()
|
||||
}
|
||||
|
||||
getCursorPosition () {
|
||||
return this.editor.getCursorPosition()
|
||||
}
|
||||
|
||||
moveCursorTo (row, col) {
|
||||
this.editor.moveCursorTo(row, col)
|
||||
}
|
||||
|
||||
scrollToLine (num) {
|
||||
this.editor.scrollToLine(num, false, false)
|
||||
}
|
||||
|
||||
focus () {
|
||||
@@ -212,15 +187,62 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
reload () {
|
||||
let session = this.editor.getSession()
|
||||
session.removeListener('change', this.changeHandler)
|
||||
session.setValue(this.props.value)
|
||||
session.getUndoManager().reset()
|
||||
session.on('change', this.changeHandler)
|
||||
// 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)
|
||||
this.editor.refresh()
|
||||
}
|
||||
|
||||
setValue (value) {
|
||||
let cursor = this.editor.getCursor()
|
||||
this.editor.setValue(value)
|
||||
this.editor.setCursor(cursor)
|
||||
}
|
||||
|
||||
handleDropImage (e) {
|
||||
e.preventDefault()
|
||||
const imagePath = e.dataTransfer.files[0].path
|
||||
const filename = path.basename(imagePath)
|
||||
|
||||
copyImage(imagePath, this.props.storageKey).then((imagePath) => {
|
||||
const imageMd = `})`
|
||||
this.insertImageMd(imageMd)
|
||||
})
|
||||
}
|
||||
|
||||
insertImageMd (imageMd) {
|
||||
this.editor.replaceSelection(imageMd)
|
||||
}
|
||||
|
||||
handlePaste (editor, e) {
|
||||
const dataTransferItem = e.clipboardData.items[0]
|
||||
if (!dataTransferItem.type.match('image')) return
|
||||
|
||||
const blob = dataTransferItem.getAsFile()
|
||||
let reader = new FileReader()
|
||||
let base64data
|
||||
|
||||
reader.readAsDataURL(blob)
|
||||
reader.onloadend = () => {
|
||||
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
|
||||
base64data += base64data.replace('+', ' ')
|
||||
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
||||
const imageName = Math.random().toString(36).slice(-16)
|
||||
const storagePath = findStorage(this.props.storageKey).path
|
||||
const imageDir = path.join(storagePath, 'images')
|
||||
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
|
||||
const imagePath = path.join(imageDir, `${imageName}.png`)
|
||||
fs.writeFile(imagePath, binaryData, 'binary')
|
||||
const imageMd = `})`
|
||||
this.insertImageMd(imageMd)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { className, fontFamily } = this.props
|
||||
let { className, fontFamily, fontSize } = this.props
|
||||
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
|
||||
? [fontFamily].concat(defaultEditorFontFamily)
|
||||
: defaultEditorFontFamily
|
||||
@@ -233,8 +255,10 @@ export default class CodeEditor extends React.Component {
|
||||
ref='root'
|
||||
tabIndex='-1'
|
||||
style={{
|
||||
fontFamily: fontFamily.join(', ')
|
||||
fontFamily: fontFamily.join(', '),
|
||||
fontSize: fontSize
|
||||
}}
|
||||
onDrop={(e) => this.handleDropImage(e)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -252,10 +276,9 @@ CodeEditor.propTypes = {
|
||||
CodeEditor.defaultProps = {
|
||||
readOnly: false,
|
||||
theme: 'xcode',
|
||||
keyMap: 'sublime',
|
||||
fontSize: 14,
|
||||
fontFamily: 'Monaco, Consolas',
|
||||
indentSize: 4,
|
||||
indentType: 'space'
|
||||
}
|
||||
|
||||
export default CodeEditor
|
||||
|
||||
@@ -3,24 +3,67 @@ import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './MarkdownEditor.styl'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
const _ = require('lodash')
|
||||
|
||||
class MarkdownEditor extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
// char codes for ctrl + w
|
||||
this.escapeFromEditor = [17, 87]
|
||||
|
||||
// ctrl + shift + ;
|
||||
this.supportMdSelectionBold = [16, 17, 186]
|
||||
|
||||
this.state = {
|
||||
status: 'PREVIEW'
|
||||
status: 'PREVIEW',
|
||||
renderValue: props.value,
|
||||
keyPressed: new Set(),
|
||||
isLocked: false
|
||||
}
|
||||
|
||||
this.lockEditorCode = () => this.handleLockEditor()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.value = this.refs.code.value
|
||||
eventEmitter.on('editor:lock', this.lockEditorCode)
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
this.value = this.refs.code.value
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
if (props.value !== this.props.value) {
|
||||
this.queueRendering(props.value)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.cancelQueue()
|
||||
eventEmitter.off('editor:lock', this.lockEditorCode)
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -38,23 +81,26 @@ class MarkdownEditor extends React.Component {
|
||||
if (newStatus === 'CODE') {
|
||||
this.refs.code.focus()
|
||||
} else {
|
||||
this.refs.code.blur()
|
||||
this.refs.preview.focus()
|
||||
}
|
||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleBlur (e) {
|
||||
if (this.state.isLocked) return
|
||||
this.setState({ keyPressed: new Set() })
|
||||
let { config } = this.props
|
||||
if (config.editor.switchPreview === 'BLUR') {
|
||||
let cursorPosition = this.refs.code.getCursorPosition()
|
||||
let cursorPosition = this.refs.code.editor.getCursor()
|
||||
this.setState({
|
||||
status: 'PREVIEW'
|
||||
}, () => {
|
||||
this.refs.preview.focus()
|
||||
this.refs.preview.scrollTo(cursorPosition.row)
|
||||
this.refs.preview.scrollTo(cursorPosition.line)
|
||||
})
|
||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +116,30 @@ class MarkdownEditor extends React.Component {
|
||||
}, () => {
|
||||
this.refs.code.focus()
|
||||
})
|
||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||
}
|
||||
}
|
||||
|
||||
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'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,14 +153,62 @@ class MarkdownEditor extends React.Component {
|
||||
} else {
|
||||
this.refs.code.focus()
|
||||
}
|
||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||
}
|
||||
|
||||
reload () {
|
||||
this.refs.code.reload()
|
||||
this.cancelQueue()
|
||||
this.renderPreview(this.props.value)
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
let { config } = this.props
|
||||
if (this.state.status !== 'CODE') return false
|
||||
const keyPressed = this.state.keyPressed
|
||||
keyPressed.add(e.keyCode)
|
||||
this.setState({ keyPressed })
|
||||
let isNoteHandlerKey = (el) => { return keyPressed.has(el) }
|
||||
// These conditions are for ctrl-e and ctrl-w
|
||||
if (keyPressed.size === this.escapeFromEditor.length &&
|
||||
!this.state.isLocked && this.state.status === 'CODE' &&
|
||||
this.escapeFromEditor.every(isNoteHandlerKey)) {
|
||||
this.handleContextMenu()
|
||||
if (config.editor.switchPreview === 'BLUR') document.activeElement.blur()
|
||||
}
|
||||
if (keyPressed.size === this.supportMdSelectionBold.length && this.supportMdSelectionBold.every(isNoteHandlerKey)) {
|
||||
this.addMdAroundWord('**')
|
||||
}
|
||||
}
|
||||
|
||||
addMdAroundWord (mdElement) {
|
||||
if (this.refs.code.editor.getSelection()) {
|
||||
return this.addMdAroundSelection(mdElement)
|
||||
}
|
||||
const currentCaret = this.refs.code.editor.getCursor()
|
||||
const word = this.refs.code.editor.findWordAt(currentCaret)
|
||||
const cmDoc = this.refs.code.editor.getDoc()
|
||||
cmDoc.replaceRange(mdElement, word.anchor)
|
||||
cmDoc.replaceRange(mdElement, { line: word.head.line, ch: word.head.ch + mdElement.length })
|
||||
}
|
||||
|
||||
addMdAroundSelection (mdElement) {
|
||||
this.refs.code.editor.replaceSelection(`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`)
|
||||
}
|
||||
|
||||
handleKeyUp (e) {
|
||||
const keyPressed = this.state.keyPressed
|
||||
keyPressed.delete(e.keyCode)
|
||||
this.setState({ keyPressed })
|
||||
}
|
||||
|
||||
handleLockEditor () {
|
||||
this.setState({ isLocked: !this.state.isLocked })
|
||||
}
|
||||
|
||||
render () {
|
||||
let { className, value, config } = this.props
|
||||
let { className, value, config, storageKey } = this.props
|
||||
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||
@@ -99,6 +217,8 @@ class MarkdownEditor extends React.Component {
|
||||
let previewStyle = {}
|
||||
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
|
||||
|
||||
const storage = findStorage(storageKey)
|
||||
|
||||
return (
|
||||
<div className={className == null
|
||||
? 'MarkdownEditor'
|
||||
@@ -106,16 +226,23 @@ class MarkdownEditor extends React.Component {
|
||||
}
|
||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||
tabIndex='-1'
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
onKeyUp={(e) => this.handleKeyUp(e)}
|
||||
>
|
||||
<CodeEditor styleName='codeEditor'
|
||||
<CodeEditor styleName={this.state.status === 'CODE'
|
||||
? 'codeEditor'
|
||||
: 'codeEditor--hide'
|
||||
}
|
||||
ref='code'
|
||||
mode='markdown'
|
||||
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}
|
||||
storageKey={storageKey}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
onBlur={(e) => this.handleBlur(e)}
|
||||
/>
|
||||
@@ -124,17 +251,23 @@ class MarkdownEditor extends React.Component {
|
||||
: '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={value}
|
||||
value={this.state.renderValue}
|
||||
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
|
||||
onMouseDown={(e) => this.handlePreviewMouseDown(e)}
|
||||
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
||||
showCopyNotification={config.ui.showCopyNotification}
|
||||
storagePath={storage.path}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -4,8 +4,14 @@
|
||||
.codeEditor
|
||||
absolute top bottom left right
|
||||
|
||||
.hide
|
||||
z-index 0
|
||||
opacity 0
|
||||
pointer-events none
|
||||
|
||||
.codeEditor--hide
|
||||
@extend .codeEditor
|
||||
@extend .hide
|
||||
|
||||
.preview
|
||||
display block
|
||||
@@ -17,7 +23,5 @@
|
||||
|
||||
.preview--hide
|
||||
@extend .preview
|
||||
z-index 0
|
||||
opacity 0
|
||||
pointer-events none
|
||||
@extend .hide
|
||||
|
||||
|
||||
@@ -1,21 +1,107 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import markdown from 'browser/lib/markdown'
|
||||
import _ from 'lodash'
|
||||
import hljsTheme from 'browser/lib/hljsThemes'
|
||||
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'
|
||||
import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import mdurl from 'mdurl'
|
||||
|
||||
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 { shell } = require('electron')
|
||||
const goExternal = function (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
shell.openExternal(e.target.href)
|
||||
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;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
src: url('${appPath}/resources/fonts/Lato-Black.woff2') format('woff2'), /* Modern Browsers */
|
||||
url('${appPath}/resources/fonts/Lato-Black.woff') format('woff'), /* Modern Browsers */
|
||||
url('${appPath}/resources/fonts/Lato-Black.ttf') format('truetype');
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
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);
|
||||
}
|
||||
.lineNumber {
|
||||
${lineNumber && 'display: block !important;'}
|
||||
font-family: ${codeBlockFontFamily.join(', ')};
|
||||
}
|
||||
|
||||
.clipboardButton {
|
||||
color: rgba(147,147,149,0.8);;
|
||||
fill: rgba(147,147,149,1);;
|
||||
border-radius: 50%;
|
||||
margin: 0px 10px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clipboardButton:hover {
|
||||
transition: 0.2s;
|
||||
color: #939395;
|
||||
fill: #939395;
|
||||
background-color: rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
border: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
padding-bottom: 4px;
|
||||
margin: 1em 0 8px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding-bottom: 0.2em;
|
||||
margin: 1em 0 0.37em;
|
||||
}
|
||||
|
||||
body p {
|
||||
white-space: normal;
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
const { shell } = require('electron')
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
|
||||
if (!OSX) {
|
||||
defaultFontFamily.unshift('\'Microsoft YaHei\'')
|
||||
defaultFontFamily.unshift('Microsoft YaHei')
|
||||
defaultFontFamily.unshift('meiryo')
|
||||
}
|
||||
const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||
@@ -27,6 +113,33 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.contextMenuHandler = (e) => this.handleContextMenu(e)
|
||||
this.mouseDownHandler = (e) => this.handleMouseDown(e)
|
||||
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
||||
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
|
||||
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
||||
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
||||
this.printHandler = () => this.handlePrint()
|
||||
|
||||
this.linkClickHandler = this.handlelinkClick.bind(this)
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -34,84 +147,248 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
handlePrint () {
|
||||
this.refs.root.contentWindow.print()
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fixDecodedURI (node) {
|
||||
if (node && node.children.length === 1 && typeof node.children[0] === 'string') {
|
||||
const { innerText, href } = node
|
||||
|
||||
node.innerText = mdurl.decode(href) === innerText
|
||||
? href
|
||||
: innerText
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.refs.root.setAttribute('sandbox', 'allow-same-origin')
|
||||
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)
|
||||
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler)
|
||||
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
||||
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
||||
eventEmitter.on('print', this.printHandler)
|
||||
}
|
||||
|
||||
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)
|
||||
this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler)
|
||||
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler)
|
||||
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
||||
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
||||
eventEmitter.off('print', this.printHandler)
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.value !== this.props.value ||
|
||||
prevProps.fontFamily !== this.props.fontFamily ||
|
||||
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
|
||||
) this.rewriteIframe()
|
||||
prevProps.lineNumber !== this.props.lineNumber ||
|
||||
prevProps.showCopyNotification !== this.props.showCopyNotification ||
|
||||
prevProps.theme !== this.props.theme) {
|
||||
this.applyStyle()
|
||||
this.rewriteIframe()
|
||||
}
|
||||
}
|
||||
|
||||
rewriteIframe () {
|
||||
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
el.removeEventListener('click', goExternal)
|
||||
})
|
||||
|
||||
let { value, fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme } = this.props
|
||||
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
|
||||
codeBlockTheme = hljsTheme().some((theme) => theme.name === codeBlockTheme) ? codeBlockTheme : 'xcode'
|
||||
|
||||
this.refs.root.contentWindow.document.head.innerHTML = `
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
src: url('../resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
|
||||
url('../resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
|
||||
url('../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(', ')};
|
||||
}
|
||||
.lineNumber {
|
||||
${lineNumber && 'display: block !important;'}
|
||||
font-family: ${codeBlockFontFamily.join(', ')};
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="../node_modules/highlight.js/styles/${codeBlockTheme}.css">
|
||||
<link rel="stylesheet" href="../resources/katex.min.css">
|
||||
`
|
||||
this.refs.root.contentWindow.document.body.innerHTML = markdown(value)
|
||||
this.setCodeTheme(codeBlockTheme)
|
||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
|
||||
}
|
||||
|
||||
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
el.addEventListener('mousedown', goExternal)
|
||||
setCodeTheme (theme) {
|
||||
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
|
||||
? theme
|
||||
: 'elegant'
|
||||
this.getWindow().document.getElementById('codeTheme').href = theme.startsWith('solarized')
|
||||
? `${appPath}/node_modules/codemirror/theme/solarized.css`
|
||||
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
|
||||
}
|
||||
|
||||
rewriteIframe () {
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
el.removeEventListener('click', this.anchorClickHandler)
|
||||
})
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
||||
el.removeEventListener('click', this.checkboxClickHandler)
|
||||
})
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
el.removeEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
|
||||
let { value, theme, indentSize, codeBlockTheme, showCopyNotification, storagePath } = this.props
|
||||
|
||||
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
||||
|
||||
const codeBlocks = value.match(/(```)(.|[\n])*?(```)/g)
|
||||
if (codeBlocks !== null) {
|
||||
codeBlocks.forEach((codeBlock) => {
|
||||
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock))
|
||||
})
|
||||
}
|
||||
this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value)
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.taskListItem'), (el) => {
|
||||
el.parentNode.parentNode.style.listStyleType = 'none'
|
||||
})
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
this.fixDecodedURI(el)
|
||||
el.addEventListener('click', this.anchorClickHandler)
|
||||
})
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
||||
el.addEventListener('click', this.checkboxClickHandler)
|
||||
})
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => {
|
||||
el.src = markdown.normalizeLinkText(el.src)
|
||||
if (!/\/:storage/.test(el.src)) return
|
||||
el.src = `file:///${markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}`
|
||||
})
|
||||
|
||||
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
|
||||
? codeBlockTheme
|
||||
: 'default'
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.code code'), (el) => {
|
||||
let syntax = CodeMirror.findModeByName(el.className)
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
CodeMirror.requireMode(syntax.mode, () => {
|
||||
let content = htmlTextHelper.decodeEntities(el.innerHTML)
|
||||
const copyIcon = document.createElement('i')
|
||||
copyIcon.innerHTML = '<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
|
||||
copyIcon.onclick = (e) => {
|
||||
copy(content)
|
||||
if (showCopyNotification) {
|
||||
this.notify('Saved to Clipboard!', {
|
||||
body: 'Paste it wherever you want!',
|
||||
silent: true
|
||||
})
|
||||
}
|
||||
}
|
||||
el.parentNode.appendChild(copyIcon)
|
||||
el.innerHTML = ''
|
||||
if (codeBlockTheme.indexOf('solarized') === 0) {
|
||||
const [refThema, color] = codeBlockTheme.split(' ')
|
||||
el.parentNode.className += ` cm-s-${refThema} cm-s-${color} CodeMirror`
|
||||
} else {
|
||||
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(htmlTextHelper.decodeEntities(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(htmlTextHelper.decodeEntities(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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -124,19 +401,39 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
scrollTo (targetRow) {
|
||||
let lineAnchors = this.getWindow().document.querySelectorAll('a.lineAnchor')
|
||||
let blocks = this.getWindow().document.querySelectorAll('body>[data-line]')
|
||||
|
||||
for (let index = 0; index < lineAnchors.length; index++) {
|
||||
let lineAnchor = lineAnchors[index]
|
||||
let row = parseInt(lineAnchor.getAttribute('data-key'))
|
||||
if (row > targetRow) {
|
||||
let targetAnchor = lineAnchors[index - 1]
|
||||
this.getWindow().scrollTo(0, targetAnchor.offsetTop)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preventImageDroppedHandler (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
notify (title, options) {
|
||||
if (global.process.platform === 'win32') {
|
||||
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
||||
}
|
||||
return new window.Notification(title, options)
|
||||
}
|
||||
|
||||
handlelinkClick (e) {
|
||||
const noteHash = e.target.href.split('/').pop()
|
||||
const regexIsNoteLink = /^(.{20})-(.{20})$/
|
||||
if (regexIsNoteLink.test(noteHash)) {
|
||||
eventEmitter.emit('list:jump', noteHash)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { className, style, tabIndex } = this.props
|
||||
return (
|
||||
@@ -157,7 +454,8 @@ MarkdownPreview.propTypes = {
|
||||
onDoubleClick: PropTypes.func,
|
||||
onMouseUp: PropTypes.func,
|
||||
onMouseDown: PropTypes.func,
|
||||
onMouseMove: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.string
|
||||
value: PropTypes.string,
|
||||
showCopyNotification: PropTypes.bool,
|
||||
storagePath: PropTypes.string
|
||||
}
|
||||
|
||||
18
browser/components/ModalEscButton.js
Normal file
18
browser/components/ModalEscButton.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React, {PropTypes} from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './ModalEscButton.styl'
|
||||
|
||||
const ModalEscButton = ({
|
||||
handleEscButtonClick
|
||||
}) => (
|
||||
<button styleName='escButton' onClick={handleEscButtonClick}>
|
||||
<div styleName='esc-mark'>×</div>
|
||||
<div styleName='esc-text'>esc</div>
|
||||
</button>
|
||||
)
|
||||
|
||||
ModalEscButton.propTypes = {
|
||||
handleEscButtonClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(ModalEscButton, styles)
|
||||
16
browser/components/ModalEscButton.styl
Normal file
16
browser/components/ModalEscButton.styl
Normal file
@@ -0,0 +1,16 @@
|
||||
.escButton
|
||||
height 50px
|
||||
position absolute
|
||||
background-color transparent
|
||||
color $ui-inactive-text-color
|
||||
border none
|
||||
top 1px
|
||||
right 10px
|
||||
text-align center
|
||||
width top-bar--height
|
||||
height top-bar-height
|
||||
|
||||
.esc-mark
|
||||
font-size 28px
|
||||
margin-top -5px
|
||||
margin-bottom -7px
|
||||
@@ -1,82 +0,0 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
|
||||
export default class ModeIcon extends React.Component {
|
||||
getClassName () {
|
||||
var mode = this.props.mode
|
||||
switch (mode) {
|
||||
// Script
|
||||
case 'javascript':
|
||||
return 'devicon-javascript-plain'
|
||||
case 'jsx':
|
||||
return 'devicon-react-original'
|
||||
case 'coffee':
|
||||
return 'devicon-coffeescript-original'
|
||||
case 'ruby':
|
||||
return 'devicon-ruby-plain'
|
||||
case 'erlang':
|
||||
return 'devicon-erlang-plain'
|
||||
case 'php':
|
||||
return 'devicon-php-plain'
|
||||
|
||||
// HTML
|
||||
case 'html':
|
||||
return 'devicon-html5-plain'
|
||||
|
||||
// Stylesheet
|
||||
case 'css':
|
||||
return 'devicon-css3-plain'
|
||||
case 'less':
|
||||
return 'devicon-less-plain-wordmark'
|
||||
case 'sass':
|
||||
case 'scss':
|
||||
return 'devicon-sass-original'
|
||||
|
||||
// Compile
|
||||
case 'c':
|
||||
return 'devicon-c-plain'
|
||||
case 'cpp':
|
||||
return 'devicon-cplusplus-plain'
|
||||
case 'csharp':
|
||||
return 'devicon-csharp-plain'
|
||||
case 'objc':
|
||||
return 'devicon-apple-original'
|
||||
case 'golang':
|
||||
return 'devicon-go-plain'
|
||||
case 'java':
|
||||
return 'devicon-java-plain'
|
||||
|
||||
// Framework
|
||||
case 'django':
|
||||
return 'devicon-django-plain'
|
||||
|
||||
// Config
|
||||
case 'dockerfile':
|
||||
return 'devicon-docker-plain'
|
||||
case 'gitignore':
|
||||
return 'devicon-git-plain'
|
||||
|
||||
// Shell
|
||||
case 'sh':
|
||||
case 'batchfile':
|
||||
case 'powershell':
|
||||
return 'fa fa-fw fa-terminal'
|
||||
|
||||
case 'text':
|
||||
case 'markdown':
|
||||
return 'fa fa-fw fa-file-text-o'
|
||||
}
|
||||
return 'fa fa-fw fa-code'
|
||||
}
|
||||
|
||||
render () {
|
||||
let className = `ModeIcon ${this.getClassName()} ${this.props.className}`
|
||||
return (
|
||||
<i className={className}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ModeIcon.propTypes = {
|
||||
className: PropTypes.string,
|
||||
mode: PropTypes.string
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ModeIcon from './ModeIcon'
|
||||
import modes from '../lib/modes'
|
||||
import _ from 'lodash'
|
||||
|
||||
const IDLE_MODE = 'IDLE_MODE'
|
||||
const EDIT_MODE = 'EDIT_MODE'
|
||||
|
||||
export default class ModeSelect extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
mode: IDLE_MODE,
|
||||
search: '',
|
||||
focusIndex: 0
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.blurHandler = e => {
|
||||
let searchElement = ReactDOM.findDOMNode(this.refs.search)
|
||||
if (this.state.mode === EDIT_MODE && document.activeElement !== searchElement) {
|
||||
this.handleBlur(e)
|
||||
}
|
||||
}
|
||||
window.addEventListener('click', this.blurHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('click', this.blurHandler)
|
||||
let searchElement = ReactDOM.findDOMNode(this.refs.search)
|
||||
if (searchElement != null && this.searchKeyDownListener != null) {
|
||||
searchElement.removeEventListener('keydown', this.searchKeyDownListener)
|
||||
}
|
||||
}
|
||||
|
||||
handleIdleSelectClick (e) {
|
||||
this.setState({mode: EDIT_MODE, search: this.props.value}, () => {
|
||||
ReactDOM.findDOMNode(this.refs.search).select()
|
||||
})
|
||||
}
|
||||
|
||||
handleModeOptionClick (modeName) {
|
||||
return e => {
|
||||
this.props.onChange(modeName)
|
||||
this.setState({
|
||||
mode: IDLE_MODE,
|
||||
search: '',
|
||||
focusIndex: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
// up
|
||||
case 38:
|
||||
e.preventDefault()
|
||||
if (this.state.focusIndex > 0) this.setState({focusIndex: this.state.focusIndex - 1})
|
||||
break
|
||||
// down
|
||||
case 40:
|
||||
e.preventDefault()
|
||||
{
|
||||
let search = _.escapeRegExp(this.state.search)
|
||||
let filteredModes = modes
|
||||
.filter(mode => {
|
||||
let nameMatched = mode.name.match(search)
|
||||
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
|
||||
return nameMatched || aliasMatched
|
||||
})
|
||||
if (filteredModes.length === this.state.focusIndex + 1) this.setState({focusIndex: filteredModes.length - 1})
|
||||
else this.setState({focusIndex: this.state.focusIndex + 1})
|
||||
}
|
||||
break
|
||||
// enter
|
||||
case 13:
|
||||
e.preventDefault()
|
||||
{
|
||||
let search = _.escapeRegExp(this.state.search)
|
||||
let filteredModes = modes
|
||||
.filter(mode => {
|
||||
let nameMatched = mode.name.match(search)
|
||||
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
|
||||
return nameMatched || aliasMatched
|
||||
})
|
||||
let targetMode = filteredModes[this.state.focusIndex]
|
||||
if (targetMode != null) {
|
||||
this.props.onChange(targetMode.name)
|
||||
this.setIdle()
|
||||
}
|
||||
}
|
||||
break
|
||||
// esc
|
||||
case 27:
|
||||
case 9:
|
||||
e.stopPropagation()
|
||||
this.setIdle()
|
||||
}
|
||||
if (this.props.onKeyDown) this.props.onKeyDown(e)
|
||||
}
|
||||
|
||||
handleSearchChange (e) {
|
||||
this.setState({
|
||||
search: e.target.value,
|
||||
focusIndex: 0
|
||||
})
|
||||
}
|
||||
|
||||
handleBlur (e) {
|
||||
if (e.target !== ReactDOM.findDOMNode(this.refs.search)) {
|
||||
this.setIdle()
|
||||
}
|
||||
}
|
||||
|
||||
setIdle () {
|
||||
this.setState({
|
||||
mode: IDLE_MODE,
|
||||
search: '',
|
||||
focusIndex: 0
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
let className = this.props.className != null
|
||||
? `ModeSelect ${this.props.className}`
|
||||
: this.props.className
|
||||
|
||||
if (this.state.mode === IDLE_MODE) {
|
||||
let mode = _.findWhere(modes, {name: this.props.value})
|
||||
let modeName = mode != null ? mode.name : 'text'
|
||||
let modeLabel = mode != null ? mode.label : this.props.value
|
||||
|
||||
return (
|
||||
<div className={className + ' idle'} onClick={e => this.handleIdleSelectClick(e)}>
|
||||
<ModeIcon mode={modeName}/>{modeLabel}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
let search = _.escapeRegExp(this.state.search)
|
||||
let filteredOptions = modes
|
||||
.filter(mode => {
|
||||
let nameMatched = mode.name.match(search)
|
||||
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
|
||||
return nameMatched || aliasMatched
|
||||
})
|
||||
.map((mode, index) => {
|
||||
return (
|
||||
<div key={mode.name} className={index === this.state.focusIndex ? 'ModeSelect-options-item active' : 'ModeSelect-options-item'} onClick={e => this.handleModeOptionClick(mode.name)(e)}><ModeIcon mode={mode.name}/>{mode.label}</div>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={className + ' edit'}>
|
||||
<input onBlur={e => this.handleBlur(e)} onKeyDown={e => this.handleSearchKeyDown(e)} ref='search' onChange={e => this.handleSearchChange(e)} value={this.state.search} type='text'/>
|
||||
<div ref='options' className='ModeSelect-options hide'>
|
||||
{filteredOptions}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ModeSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onKeyDown: PropTypes.func
|
||||
}
|
||||
29
browser/components/NavToggleButton.js
Normal file
29
browser/components/NavToggleButton.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @fileoverview Micro component for toggle SideNav
|
||||
*/
|
||||
import React, { PropTypes } from 'react'
|
||||
import styles from './NavToggleButton.styl'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
|
||||
/**
|
||||
* @param {boolean} isFolded
|
||||
* @param {Function} handleToggleButtonClick
|
||||
*/
|
||||
|
||||
const NavToggleButton = ({isFolded, handleToggleButtonClick}) => (
|
||||
<button styleName='navToggle'
|
||||
onClick={(e) => handleToggleButtonClick(e)}
|
||||
>
|
||||
{isFolded
|
||||
? <i className='fa fa-angle-double-right' />
|
||||
: <i className='fa fa-angle-double-left' />
|
||||
}
|
||||
</button>
|
||||
)
|
||||
|
||||
NavToggleButton.propTypes = {
|
||||
isFolded: PropTypes.bool.isRequired,
|
||||
handleToggleButtonClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(NavToggleButton, styles)
|
||||
22
browser/components/NavToggleButton.styl
Normal file
22
browser/components/NavToggleButton.styl
Normal file
@@ -0,0 +1,22 @@
|
||||
.navToggle
|
||||
navButtonColor()
|
||||
display block
|
||||
position absolute
|
||||
left 5px
|
||||
bottom 5px
|
||||
border-radius 16.5px
|
||||
height 34px
|
||||
width 34px
|
||||
line-height 32px
|
||||
padding 0
|
||||
|
||||
body[data-theme="white"]
|
||||
navWhiteButtonColor()
|
||||
|
||||
body[data-theme="dark"]
|
||||
.navToggle
|
||||
&:hover
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
transition 0.15s
|
||||
color $ui-dark-text-color
|
||||
|
||||
112
browser/components/NoteItem.js
Normal file
112
browser/components/NoteItem.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* @fileoverview Note item component.
|
||||
*/
|
||||
import React, { PropTypes } from 'react'
|
||||
import { isArray } from 'lodash'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import { getTodoStatus } from 'browser/lib/getTodoStatus'
|
||||
import styles from './NoteItem.styl'
|
||||
import TodoProcess from './TodoProcess'
|
||||
|
||||
/**
|
||||
* @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 {Function} handleDragStart
|
||||
* @param {string} dateDisplay
|
||||
*/
|
||||
const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteContextMenu, handleDragStart, pathname }) => (
|
||||
<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}`)}
|
||||
onDragStart={e => handleDragStart(e, note)}
|
||||
draggable='true'
|
||||
>
|
||||
<div styleName='item-wrapper'>
|
||||
{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' />
|
||||
}
|
||||
<div styleName='item-title'>
|
||||
{note.title.trim().length > 0
|
||||
? note.title
|
||||
: <span styleName='item-title-empty'>Empty</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div styleName='item-bottom-time'>{dateDisplay}</div>
|
||||
{note.isStarred
|
||||
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
|
||||
}
|
||||
{note.isPinned && !pathname.match(/\/home|\/starred|\/trash/)
|
||||
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
|
||||
}
|
||||
{note.type === 'MARKDOWN_NOTE'
|
||||
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
||||
: ''
|
||||
}
|
||||
<div styleName='item-bottom'>
|
||||
<div styleName='item-bottom-tagList'>
|
||||
{note.tags.length > 0
|
||||
? TagElementList(note.tags)
|
||||
: <span styleName='item-bottom-tagList-empty' />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</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,
|
||||
isTrashed: PropTypes.bool.isRequired
|
||||
}),
|
||||
handleNoteClick: PropTypes.func.isRequired,
|
||||
handleNoteContextMenu: PropTypes.func.isRequired,
|
||||
handleDragStart: PropTypes.func.isRequired,
|
||||
handleDragEnd: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(NoteItem, styles)
|
||||
232
browser/components/NoteItem.styl
Normal file
232
browser/components/NoteItem.styl
Normal file
@@ -0,0 +1,232 @@
|
||||
$control-height = 30px
|
||||
|
||||
.root
|
||||
absolute left bottom
|
||||
top $topBar-height - 1
|
||||
background-color $ui-noteList-backgroundColor
|
||||
|
||||
.item
|
||||
position relative
|
||||
padding 0 20px
|
||||
user-select none
|
||||
cursor pointer
|
||||
background-color $ui-noteList-backgroundColor
|
||||
transition 0.2s
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 20%)
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-text-color
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha(white, 0.6)
|
||||
color $ui-text-color
|
||||
.item-star
|
||||
color $ui-favorite-star-button-color
|
||||
&:active
|
||||
background-color alpha($ui-button--active-backgroundColor, 40%)
|
||||
color $ui-text-color
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-text-color
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha(white, 0.6)
|
||||
color $ui-text-color
|
||||
|
||||
.item-wrapper
|
||||
padding 15px 0
|
||||
border-bottom $ui-border
|
||||
position relative
|
||||
|
||||
.item--active
|
||||
@extend .item
|
||||
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||
color $ui-text-color
|
||||
.item-title
|
||||
.item-title-empty
|
||||
.item-bottom-tagList-empty
|
||||
.item-bottom-time
|
||||
.item-title-icon
|
||||
color $ui-text-color
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha(white, 0.6)
|
||||
color $ui-text-color
|
||||
.item-wrapper
|
||||
border-color transparent
|
||||
.item-star
|
||||
color $ui-favorite-star-button-color
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 40%)
|
||||
color #e74c3c
|
||||
.menu-button-label
|
||||
color $ui-text-color
|
||||
&:active, &:active:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 40%)
|
||||
color #e74c3c
|
||||
.menu-button-label
|
||||
color $ui-text-color
|
||||
|
||||
.item-title-icon
|
||||
position relative
|
||||
font-size 12px
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-title
|
||||
font-size 15px
|
||||
font-weight 700
|
||||
position relative
|
||||
top -12px
|
||||
left 20px
|
||||
padding-right 15px
|
||||
padding-bottom 4px
|
||||
overflow ellipsis
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-title-empty
|
||||
font-weight normal
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-bottom
|
||||
position relative
|
||||
bottom 0px
|
||||
margin-top 10px
|
||||
font-size 12px
|
||||
line-height 20px
|
||||
overflow ellipsis
|
||||
display flex
|
||||
|
||||
.item-bottom-tagList
|
||||
flex 1
|
||||
overflow ellipsis
|
||||
line-height 25px
|
||||
padding-left 2px
|
||||
margin-right 40px
|
||||
|
||||
.item-bottom-tagList-item
|
||||
font-size 11px
|
||||
margin-right 8px
|
||||
padding 0
|
||||
box-sizing border-box
|
||||
border-radius 2px
|
||||
padding 4px
|
||||
vertical-align middle
|
||||
background-color white
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-bottom-time
|
||||
color $ui-inactive-text-color
|
||||
font-size 13px
|
||||
padding-left 2px
|
||||
padding-bottom 2px
|
||||
|
||||
.item-star
|
||||
position absolute
|
||||
right -6px
|
||||
bottom 23px
|
||||
width 16px
|
||||
height 16px
|
||||
color alpha($ui-favorite-star-button-color, 60%)
|
||||
font-size 12px
|
||||
padding 0
|
||||
border-radius 17px
|
||||
|
||||
.item-pin
|
||||
position absolute
|
||||
right 0px
|
||||
bottom 2px
|
||||
width 34px
|
||||
height 34px
|
||||
color #E54D42
|
||||
font-size 14px
|
||||
padding 0
|
||||
border-radius 17px
|
||||
|
||||
body[data-theme="white"]
|
||||
.item
|
||||
background-color $ui-white-noteList-backgroundColor
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||
&:active
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.item--active
|
||||
@extend .item
|
||||
background-color $ui-button--active-backgroundColor
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||
|
||||
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
|
||||
&:hover
|
||||
transition 0.15s
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
color $ui-dark-text-color
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-dark-text-color
|
||||
.item-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(#fff, 20%)
|
||||
color $ui-dark-text-color
|
||||
&:active
|
||||
transition 0.15s
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-dark-text-color
|
||||
.item-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-dark-text-color
|
||||
|
||||
.item-wrapper
|
||||
border-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||
|
||||
.item--active
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
.item-wrapper
|
||||
border-color transparent
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
color $ui-dark-text-color
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-dark-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||
color #c0392b
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha(#fff, 20%)
|
||||
|
||||
.item-title
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-title-icon
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-title-empty
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 40%)
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-bottom-tagList-empty
|
||||
color $ui-inactive-text-color
|
||||
vertical-align middle
|
||||
53
browser/components/NoteItemSimple.js
Normal file
53
browser/components/NoteItemSimple.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @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
|
||||
* @param {Function} handleDragStart
|
||||
*/
|
||||
const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu, handleDragStart }) => (
|
||||
<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}`)}
|
||||
onDragStart={e => handleDragStart(e, note)}
|
||||
draggable='true'
|
||||
>
|
||||
<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,
|
||||
handleDragStart: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(NoteItemSimple, styles)
|
||||
145
browser/components/NoteItemSimple.styl
Normal file
145
browser/components/NoteItemSimple.styl
Normal file
@@ -0,0 +1,145 @@
|
||||
$control-height = 30px
|
||||
|
||||
.root
|
||||
absolute left bottom
|
||||
top $topBar-height - 1
|
||||
background-color $ui-noteList-backgroundColor
|
||||
|
||||
.item-simple
|
||||
position relative
|
||||
padding 0 20px
|
||||
user-select none
|
||||
cursor pointer
|
||||
background-color $ui-noteList-backgroundColor
|
||||
transition 0.2s
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 20%)
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
color $ui-text-color
|
||||
&:active
|
||||
background-color alpha($ui-button--active-backgroundColor, 40%)
|
||||
color $ui-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
color $ui-text-color
|
||||
|
||||
.item-simple--active
|
||||
@extend .item-simple
|
||||
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||
color $ui-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
border-color transparent
|
||||
color $ui-text-color
|
||||
.item-simple-title-icon
|
||||
color $ui-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 40%)
|
||||
color #e74c3c
|
||||
.menu-button-label
|
||||
color $ui-text-color
|
||||
&:active, &:active:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 40%)
|
||||
color #e74c3c
|
||||
.menu-button-label
|
||||
color $ui-text-color
|
||||
|
||||
.item-simple-title
|
||||
font-size 13px
|
||||
height 40px
|
||||
box-sizing border-box
|
||||
line-height 24px
|
||||
padding-top 8px
|
||||
overflow ellipsis
|
||||
color $ui-inactive-text-color
|
||||
border-bottom $ui-border
|
||||
position relative
|
||||
|
||||
.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="white"]
|
||||
.item-simple
|
||||
background-color $ui-white-noteList-backgroundColor
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||
&:active
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.item-simple--active
|
||||
@extend .item-simple
|
||||
background-color $ui-button--active-backgroundColor
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||
|
||||
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
|
||||
&:hover
|
||||
transition 0.15s
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
color $ui-dark-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-dark-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(#fff, 20%)
|
||||
color $ui-dark-text-color
|
||||
&:active
|
||||
transition 0.15s
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-dark-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-dark-text-color
|
||||
|
||||
.item-simple--active
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
.item-simple-wrapper
|
||||
border-color transparent
|
||||
.item-simple-title
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
color $ui-dark-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-dark-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||
color #c0392b
|
||||
.item-simple-bottom-tagList-item
|
||||
background-color alpha(#fff, 20%)
|
||||
|
||||
.item-simple-title
|
||||
color $ui-inactive-text-color
|
||||
border-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||
|
||||
.item-simple-title-icon
|
||||
color $ui-darkinactive-text-color
|
||||
|
||||
.item-simple-title-empty
|
||||
color $ui-dark-inactive-text-color
|
||||
@@ -1,24 +0,0 @@
|
||||
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
|
||||
}
|
||||
55
browser/components/RealtimeNotification.js
Normal file
55
browser/components/RealtimeNotification.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './RealtimeNotification.styl'
|
||||
|
||||
const electron = require('electron')
|
||||
const { shell } = electron
|
||||
|
||||
class RealtimeNotification extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
notifications: []
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.fetchNotifications()
|
||||
}
|
||||
|
||||
fetchNotifications () {
|
||||
const notificationsUrl = 'https://raw.githubusercontent.com/BoostIO/notification/master/notification.json'
|
||||
fetch(notificationsUrl)
|
||||
.then(response => {
|
||||
return response.json()
|
||||
})
|
||||
.then(json => {
|
||||
this.setState({notifications: json.notifications})
|
||||
})
|
||||
}
|
||||
|
||||
handleLinkClick (e) {
|
||||
shell.openExternal(e.currentTarget.href)
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { notifications } = this.state
|
||||
const link = notifications.length > 0
|
||||
? <a styleName='notification-link' href={notifications[0].linkUrl}
|
||||
onClick={(e) => this.handleLinkClick(e)}
|
||||
>
|
||||
Info: {notifications[0].text}
|
||||
</a>
|
||||
: ''
|
||||
|
||||
return (
|
||||
<div styleName='notification-area' style={this.props.style}>{link}</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RealtimeNotification.propTypes = {}
|
||||
|
||||
export default CSSModules(RealtimeNotification, styles)
|
||||
34
browser/components/RealtimeNotification.styl
Normal file
34
browser/components/RealtimeNotification.styl
Normal file
@@ -0,0 +1,34 @@
|
||||
.notification-area
|
||||
z-index 1000
|
||||
font-size 12px
|
||||
position absolute
|
||||
bottom 20px
|
||||
width 100%
|
||||
float left
|
||||
height 30px
|
||||
background-color none
|
||||
|
||||
.notification-link
|
||||
position absolute
|
||||
text-decoration none
|
||||
color #282A36
|
||||
font-size 14px
|
||||
border 1px solid #6FA8E6
|
||||
background-color alpha(#6FA8E6, 0.2)
|
||||
padding 5px 12px
|
||||
border-radius 2px
|
||||
transition 0.2s
|
||||
&:hover
|
||||
color #1378BD
|
||||
|
||||
body[data-theme="dark"]
|
||||
.notification-area
|
||||
background-color none
|
||||
|
||||
.notification-link
|
||||
color #fff
|
||||
border 1px solid alpha(#5CB85C, 0.6)
|
||||
background-color alpha(#5CB85C, 0.2)
|
||||
transition 0.2s
|
||||
&:hover
|
||||
color #5CB85C
|
||||
78
browser/components/SideNavFilter.js
Normal file
78
browser/components/SideNavFilter.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @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, isTrashedActive, handleTrashedButtonClick, counterDelNote,
|
||||
counterTotalNote, counterStarredNote
|
||||
}) => (
|
||||
<div styleName={isFolded ? 'menu--folded' : 'menu'}>
|
||||
|
||||
<button styleName={isHomeActive ? 'menu-button--active' : 'menu-button'}
|
||||
onClick={handleAllNotesButtonClick}
|
||||
>
|
||||
<div styleName='iconWrap'>
|
||||
<img src={isHomeActive
|
||||
? '../resources/icon/icon-all-active.svg'
|
||||
: '../resources/icon/icon-all.svg'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span styleName='menu-button-label'>All Notes</span>
|
||||
<span styleName='counters'>{counterTotalNote}</span>
|
||||
</button>
|
||||
|
||||
<button styleName={isStarredActive ? 'menu-button-star--active' : 'menu-button'}
|
||||
onClick={handleStarredButtonClick}
|
||||
>
|
||||
<div styleName='iconWrap'>
|
||||
<img src={isStarredActive
|
||||
? '../resources/icon/icon-star-active.svg'
|
||||
: '../resources/icon/icon-star.svg'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span styleName='menu-button-label'>Starred</span>
|
||||
<span styleName='counters'>{counterStarredNote}</span>
|
||||
</button>
|
||||
|
||||
<button styleName={isTrashedActive ? 'menu-button-trash--active' : 'menu-button'}
|
||||
onClick={handleTrashedButtonClick}
|
||||
>
|
||||
<div styleName='iconWrap'>
|
||||
<img src={isTrashedActive
|
||||
? '../resources/icon/icon-trash-active.svg'
|
||||
: '../resources/icon/icon-trash.svg'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span styleName='menu-button-label'>Trash</span>
|
||||
<span styleName='counters'>{counterDelNote}</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
)
|
||||
|
||||
SideNavFilter.propTypes = {
|
||||
isFolded: PropTypes.bool,
|
||||
isHomeActive: PropTypes.bool.isRequired,
|
||||
handleAllNotesButtonClick: PropTypes.func.isRequired,
|
||||
isStarredActive: PropTypes.bool.isRequired,
|
||||
isTrashedActive: PropTypes.bool.isRequired,
|
||||
handleStarredButtonClick: PropTypes.func.isRequired,
|
||||
handleTrashdButtonClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(SideNavFilter, styles)
|
||||
181
browser/components/SideNavFilter.styl
Normal file
181
browser/components/SideNavFilter.styl
Normal file
@@ -0,0 +1,181 @@
|
||||
.menu
|
||||
margin-bottom 30px
|
||||
|
||||
.menu-button
|
||||
navButtonColor()
|
||||
height 36px
|
||||
padding 0 15px 0 20px
|
||||
font-size 14px
|
||||
width 100%
|
||||
text-align left
|
||||
overflow ellipsis
|
||||
display flex
|
||||
align-items center
|
||||
&:hover, &:active, &:active:hover
|
||||
color #1EC38B
|
||||
background-color alpha($ui-button-default--active-backgroundColor, 20%)
|
||||
|
||||
.iconWrap
|
||||
width 20px
|
||||
text-align center
|
||||
|
||||
.counters
|
||||
float right
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.menu-button--active
|
||||
@extend .menu-button
|
||||
SideNavFilter()
|
||||
color #1EC38B
|
||||
background-color alpha($ui-button-default--active-backgroundColor, 20%)
|
||||
.menu-button-label, .counters
|
||||
color #1EC38B
|
||||
&:hover
|
||||
color #1EC38B
|
||||
|
||||
.menu-button-star--active
|
||||
@extend .menu-button
|
||||
SideNavFilter()
|
||||
color #1EC38B
|
||||
background-color alpha($ui-button-default--active-backgroundColor, 20%)
|
||||
.menu-button-label, .counters
|
||||
color #1EC38B
|
||||
|
||||
.menu-button-trash--active
|
||||
@extend .menu-button
|
||||
SideNavFilter()
|
||||
color #1EC38B
|
||||
background-color alpha($ui-button-default--active-backgroundColor, 20%)
|
||||
.menu-button-label, .counters
|
||||
color #1EC38B
|
||||
|
||||
.menu-button-label
|
||||
margin-left 10px
|
||||
flex 1
|
||||
|
||||
.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
|
||||
z-index 10
|
||||
line-height 32px
|
||||
border-top-right-radius 2px
|
||||
border-bottom-right-radius 2px
|
||||
pointer-events none
|
||||
opacity 0
|
||||
font-size 13px
|
||||
.counters
|
||||
display none
|
||||
|
||||
body[data-theme="white"]
|
||||
.menu-button
|
||||
navWhiteButtonColor()
|
||||
|
||||
.counters
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.menu-button--active
|
||||
@extend .menu-button
|
||||
color #e74c3c
|
||||
background-color $ui-button--active-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 50%)
|
||||
color #e74c3c
|
||||
.menu-button-label
|
||||
color $ui-text-color
|
||||
&:active, &:active:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 50%)
|
||||
color #e74c3c
|
||||
.menu-button-label
|
||||
color $ui-text-color
|
||||
|
||||
.menu-button-star--active
|
||||
@extend .menu-button
|
||||
color #F9BF3B
|
||||
background-color $ui-button--active-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 50%)
|
||||
color #F9BF3B
|
||||
.menu-button-label
|
||||
color $ui-text-color
|
||||
&:active, &:active:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 50%)
|
||||
color #F9BF3B
|
||||
.menu-button-label
|
||||
color $ui-text-color
|
||||
|
||||
.menu-button-trash--active
|
||||
@extend .menu-button
|
||||
color #5D9E36
|
||||
background-color $ui-button--active-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 50%)
|
||||
color #5D9E36
|
||||
.menu-button-label
|
||||
color $ui-text-color
|
||||
&:active, &:active:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 50%)
|
||||
color #5D9E36
|
||||
.menu-button-label
|
||||
color $ui-text-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.menu-button
|
||||
&:active
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
color $ui-dark-text-color
|
||||
|
||||
.menu-button--active
|
||||
color #c0392b
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-dark-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||
color #c0392b
|
||||
.menu-button-label
|
||||
color $ui-dark-text-color
|
||||
|
||||
.menu-button-star--active
|
||||
color $ui-favorite-star-button-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-dark-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||
color $ui-favorite-star-button-color
|
||||
.menu-button-label
|
||||
color $ui-dark-text-color
|
||||
|
||||
.menu-button-trash--active
|
||||
color #5D9E36
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-dark-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||
color #5D9E36
|
||||
.menu-button-label
|
||||
color $ui-dark-text-color
|
||||
132
browser/components/SnippetTab.js
Normal file
132
browser/components/SnippetTab.js
Normal file
@@ -0,0 +1,132 @@
|
||||
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)}
|
||||
onDoubleClick={(e) => this.handleRenameClick(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 $ui-dark-text-color
|
||||
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 $ui-dark-text-color
|
||||
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 $ui-dark-text-color
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
|
||||
.input
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
64
browser/components/StorageItem.js
Normal file
64
browser/components/StorageItem.js
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @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
|
||||
* @param {Function} handleDrop
|
||||
* @param {Function} handleDragEnter
|
||||
* @param {Function} handleDragOut
|
||||
* @return {React.Component}
|
||||
*/
|
||||
const StorageItem = ({
|
||||
isActive, handleButtonClick, handleContextMenu, folderName,
|
||||
folderColor, isFolded, noteCount, handleDrop, handleDragEnter, handleDragLeave
|
||||
}) => (
|
||||
<button styleName={isActive
|
||||
? 'folderList-item--active'
|
||||
: 'folderList-item'
|
||||
}
|
||||
onClick={handleButtonClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
onDrop={handleDrop}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
<span styleName={isFolded
|
||||
? 'folderList-item-name--folded' : 'folderList-item-name'
|
||||
}>
|
||||
<text style={{color: folderColor, paddingRight: '10px'}}>{isActive ? <i className='fa fa-folder-open-o' /> : <i className='fa fa-folder-o' />}</text>{isFolded ? 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,
|
||||
handleDragEnter: PropTypes.func.isRequired,
|
||||
handleDragLeave: PropTypes.func.isRequired,
|
||||
noteCount: PropTypes.number
|
||||
}
|
||||
|
||||
export default CSSModules(StorageItem, styles)
|
||||
111
browser/components/StorageItem.styl
Normal file
111
browser/components/StorageItem.styl
Normal file
@@ -0,0 +1,111 @@
|
||||
.root
|
||||
width 100%
|
||||
user-select none
|
||||
|
||||
.folderList-item
|
||||
display flex
|
||||
width 100%
|
||||
height 34px
|
||||
background-color transparent
|
||||
color $ui-inactive-text-color
|
||||
padding 0
|
||||
text-align left
|
||||
border none
|
||||
overflow ellipsis
|
||||
font-size 14px
|
||||
&:first-child
|
||||
margin-top 0
|
||||
&:hover
|
||||
color #1EC38B;
|
||||
background-color alpha($ui-button-default--active-backgroundColor, 20%)
|
||||
transition background-color 0.15s
|
||||
&:active
|
||||
color $$ui-button-default-color
|
||||
background-color alpha($ui-button-default--active-backgroundColor, 20%)
|
||||
|
||||
.folderList-item--active
|
||||
@extend .folderList-item
|
||||
color #1EC38B
|
||||
background-color alpha($ui-button-default--active-backgroundColor, 20%)
|
||||
&:hover
|
||||
color #1EC38B;
|
||||
background-color alpha($ui-button-default--active-backgroundColor, 50%)
|
||||
|
||||
.folderList-item-name
|
||||
display block
|
||||
flex 1
|
||||
padding 0 12px
|
||||
height 26px
|
||||
line-height 26px
|
||||
border-width 0 0 0 2px
|
||||
border-style solid
|
||||
border-color transparent
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
|
||||
.folderList-item-noteCount
|
||||
float right
|
||||
line-height 26px
|
||||
padding-right 15px
|
||||
font-size 13px
|
||||
|
||||
.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 17px
|
||||
text
|
||||
display none
|
||||
|
||||
body[data-theme="white"]
|
||||
.folderList-item
|
||||
color $ui-inactive-text-color
|
||||
&:hover
|
||||
color $ui-text-color
|
||||
background-color alpha($ui-button--active-backgroundColor, 20%)
|
||||
transition background-color 0.15s
|
||||
&:active
|
||||
color $ui-text-color
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.folderList-item--active
|
||||
@extend .folderList-item
|
||||
color $ui-text-color
|
||||
background-color $ui-button--active-backgroundColor
|
||||
&:hover
|
||||
color $ui-text-color
|
||||
background-color alpha($ui-button--active-backgroundColor, 50%)
|
||||
|
||||
body[data-theme="dark"]
|
||||
.folderList-item
|
||||
&:hover
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
color $ui-dark-text-color
|
||||
&:active
|
||||
color $ui-dark-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.folderList-item--active
|
||||
@extend .folderList-item
|
||||
color $ui-dark-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
&:active
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||
23
browser/components/StorageList.js
Normal file
23
browser/components/StorageList.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @fileoverview Micro component for showing StorageList
|
||||
*/
|
||||
import React, { PropTypes } from 'react'
|
||||
import styles from './StorageList.styl'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
|
||||
/**
|
||||
* @param {Array} storgaeList
|
||||
*/
|
||||
|
||||
const StorageList = ({storageList}) => (
|
||||
<div styleName='storageList'>
|
||||
{storageList.length > 0 ? storageList : (
|
||||
<div styleName='storgaeList-empty'>No storage mount.</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
StorageList.propTypes = {
|
||||
storgaeList: PropTypes.arrayOf(PropTypes.element).isRequired
|
||||
}
|
||||
export default CSSModules(StorageList, styles)
|
||||
20
browser/components/StorageList.styl
Normal file
20
browser/components/StorageList.styl
Normal file
@@ -0,0 +1,20 @@
|
||||
.storageList
|
||||
absolute left right
|
||||
bottom 37px
|
||||
top 180px
|
||||
overflow-y auto
|
||||
|
||||
.storageList-empty
|
||||
padding 0 10px
|
||||
margin-top 15px
|
||||
line-height 24px
|
||||
color $ui-inactive-text-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.storageList-empty
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
.root-folded
|
||||
.storageList-empty
|
||||
white-space nowrap
|
||||
transform rotate(90deg)
|
||||
27
browser/components/TagListItem.js
Normal file
27
browser/components/TagListItem.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @fileoverview Micro component for showing TagList.
|
||||
*/
|
||||
import React, { PropTypes } from 'react'
|
||||
import styles from './TagListItem.styl'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Function} handleClickTagListItem
|
||||
* @param {bool} isActive
|
||||
*/
|
||||
|
||||
const TagListItem = ({name, handleClickTagListItem, isActive}) => (
|
||||
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
|
||||
<span styleName='tagList-item-name'>
|
||||
{`# ${name}`}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
TagListItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
handleClickTagListItem: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(TagListItem, styles)
|
||||
84
browser/components/TagListItem.styl
Normal file
84
browser/components/TagListItem.styl
Normal file
@@ -0,0 +1,84 @@
|
||||
.tagList-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 13px
|
||||
&:first-child
|
||||
margin-top 0
|
||||
&:hover
|
||||
color $ui-button-default-color
|
||||
background-color alpha($ui-button-default--active-backgroundColor, 20%)
|
||||
transition background-color 0.15s
|
||||
&:active, &:active:hover
|
||||
color $ui-button-default-color
|
||||
background-color $ui-button-default--active-backgroundColor
|
||||
|
||||
.tagList-item-active
|
||||
background-color $ui-button-default--active-backgroundColor
|
||||
display flex
|
||||
width 100%
|
||||
height 26px
|
||||
padding 0
|
||||
margin-bottom 5px
|
||||
text-align left
|
||||
border none
|
||||
overflow ellipsis
|
||||
font-size 13px
|
||||
color $ui-button-default-color
|
||||
&:hover
|
||||
background-color alpha($ui-button-default--active-backgroundColor, 60%)
|
||||
transition 0.2s
|
||||
|
||||
.tagList-item-name
|
||||
display block
|
||||
flex 1
|
||||
padding 0 15px
|
||||
height 26px
|
||||
line-height 26px
|
||||
border-width 0 0 0 2px
|
||||
border-style solid
|
||||
border-color transparent
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
|
||||
body[data-theme="white"]
|
||||
.tagList-item
|
||||
color $ui-inactive-text-color
|
||||
&:hover
|
||||
color $ui-text-color
|
||||
background-color alpha($ui-button--active-backgroundColor, 20%)
|
||||
&:active
|
||||
color $ui-text-color
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.tagList-item-active
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||
|
||||
body[data-theme="dark"]
|
||||
.tagList-item
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
&:active
|
||||
color $ui-dark-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.tagList-item-active
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
&:active
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||
27
browser/components/TodoListPercentage.js
Normal file
27
browser/components/TodoListPercentage.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @fileoverview Percentage of todo achievement.
|
||||
*/
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './TodoListPercentage.styl'
|
||||
|
||||
/**
|
||||
* @param {number} percentageOfTodo
|
||||
*/
|
||||
|
||||
const TodoListPercentage = ({
|
||||
percentageOfTodo
|
||||
}) => (
|
||||
<div styleName='percentageBar' style={{display: isNaN(percentageOfTodo) ? 'none' : ''}}>
|
||||
<div styleName='progressBar' style={{width: `${percentageOfTodo}%`}}>
|
||||
<p styleName='percentageText'>{percentageOfTodo}%</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
TodoListPercentage.propTypes = {
|
||||
percentageOfTodo: PropTypes.number.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(TodoListPercentage, styles)
|
||||
32
browser/components/TodoListPercentage.styl
Normal file
32
browser/components/TodoListPercentage.styl
Normal file
@@ -0,0 +1,32 @@
|
||||
.percentageBar
|
||||
position absolute
|
||||
top 50px
|
||||
right 0px
|
||||
left 0px
|
||||
background-color #DADFE1
|
||||
width 100%
|
||||
height: 17px
|
||||
font-size: 12px
|
||||
z-index 100
|
||||
border-radius 2px
|
||||
|
||||
.progressBar
|
||||
background-color: #1EC38B
|
||||
height 17px
|
||||
border-radius 2px
|
||||
transition 0.4s cubic-bezier(0.4, 0.4, 0, 1)
|
||||
|
||||
.percentageText
|
||||
color #f4f4f4
|
||||
padding: 2px 43%
|
||||
font-weight 600
|
||||
|
||||
body[data-theme="dark"]
|
||||
.percentageBar
|
||||
background-color #363A3D
|
||||
|
||||
.progressBar
|
||||
background-color: alpha(#939395, 50%)
|
||||
|
||||
.percentageText
|
||||
color $ui-dark-text-color
|
||||
33
browser/components/TodoProcess.js
Normal file
33
browser/components/TodoProcess.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @fileoverview Percentage of todo achievement.
|
||||
*/
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './TodoProcess.styl'
|
||||
|
||||
const TodoProcess = ({
|
||||
todoStatus: {
|
||||
total: totalTodo,
|
||||
completed: completedTodo
|
||||
}
|
||||
}) => (
|
||||
<div styleName='todo-process' style={{display: totalTodo > 0 ? '' : 'none'}}>
|
||||
<div styleName='todo-process-text'>
|
||||
<i className='fa fa-fw fa-check-square-o' />
|
||||
{completedTodo} of {totalTodo}
|
||||
</div>
|
||||
<div styleName='todo-process-bar'>
|
||||
<div styleName='todo-process-bar--inner' style={{width: parseInt(completedTodo / totalTodo * 100) + '%'}} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
TodoProcess.propTypes = {
|
||||
todoStatus: {
|
||||
total: PropTypes.number.isRequired,
|
||||
completed: PropTypes.number.isRequired
|
||||
}
|
||||
}
|
||||
|
||||
export default CSSModules(TodoProcess, styles)
|
||||
45
browser/components/TodoProcess.styl
Normal file
45
browser/components/TodoProcess.styl
Normal file
@@ -0,0 +1,45 @@
|
||||
.todo-process
|
||||
font-size 12px
|
||||
display flex
|
||||
padding-top 15px
|
||||
width 85%
|
||||
|
||||
.todo-process-text
|
||||
display inline-block
|
||||
padding-right 10px
|
||||
white-space nowrap
|
||||
text-overflow ellipsis
|
||||
color $ui-inactive-text-color
|
||||
i
|
||||
color $ui-inactive-text-color
|
||||
padding-right 5px
|
||||
|
||||
.todo-process-bar
|
||||
display inline-block
|
||||
margin auto
|
||||
height 4px
|
||||
border-radius 10px
|
||||
background-color #DADFE1
|
||||
border-radius 2px
|
||||
flex-grow 1
|
||||
border 1px solid alpha(#6C7A89, 10%)
|
||||
|
||||
.todo-process-bar--inner
|
||||
height 100%
|
||||
border-radius 5px
|
||||
background-color #6C7A89
|
||||
transition 0.3s
|
||||
|
||||
|
||||
body[data-theme="dark"]
|
||||
.todo-process
|
||||
color $ui-dark-text-color
|
||||
|
||||
.todo-process-bar
|
||||
background-color #363A3D
|
||||
|
||||
.todo-process-text
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.todo-process-bar--inner
|
||||
background-color: alpha(#939395, 50%)
|
||||
@@ -54,7 +54,7 @@ body
|
||||
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
|
||||
@@ -68,6 +68,15 @@ body
|
||||
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
|
||||
div.math-rendered
|
||||
text-align center
|
||||
.math-failed
|
||||
@@ -102,12 +111,6 @@ a
|
||||
background-color alpha(#FFC95C, 0.3)
|
||||
&:visited
|
||||
color brandColor
|
||||
&.lineAnchor
|
||||
padding 0
|
||||
margin 0
|
||||
display block
|
||||
font-size 0
|
||||
height 0
|
||||
hr
|
||||
border-top none
|
||||
border-bottom solid 1px borderColor
|
||||
@@ -115,7 +118,7 @@ hr
|
||||
h1, h2, h3, h4, h5, h6
|
||||
font-weight bold
|
||||
h1
|
||||
font-size 2.25em
|
||||
font-size 2.55em
|
||||
padding-bottom 0.3em
|
||||
line-height 1.2em
|
||||
border-bottom solid 1px borderColor
|
||||
@@ -147,9 +150,6 @@ h6
|
||||
line-height 1.4em
|
||||
margin 1em 0 1em
|
||||
color #777
|
||||
|
||||
*:not(a.lineAnchor) + p, *:not(a.lineAnchor) + blockquote, *:not(a.lineAnchor) + ul, *:not(a.lineAnchor) + ol, *:not(a.lineAnchor) + pre
|
||||
margin-top 1em
|
||||
p
|
||||
line-height 1.6em
|
||||
margin 0 0 1em
|
||||
@@ -174,6 +174,8 @@ ul
|
||||
margin-bottom 1em
|
||||
li
|
||||
display list-item
|
||||
p
|
||||
margin 0
|
||||
&>li>ul, &>li>ol
|
||||
margin 0
|
||||
&>li>ul
|
||||
@@ -186,43 +188,58 @@ ol
|
||||
margin-bottom 1em
|
||||
li
|
||||
display list-item
|
||||
p
|
||||
margin 0
|
||||
&>li>ul, &>li>ol
|
||||
margin 0
|
||||
code
|
||||
color #CC305F
|
||||
padding 0.2em 0.4em
|
||||
background-color #f7f7f7
|
||||
border-radius 3px
|
||||
font-size 0.85em
|
||||
font-size 1em
|
||||
text-decoration none
|
||||
margin-right 2px
|
||||
*:not(a.lineAnchor) + code
|
||||
margin-left 2px
|
||||
pre
|
||||
padding 0.5em !important
|
||||
border solid 1px alpha(borderColor, 0.5)
|
||||
border solid 1px #D1D1D1
|
||||
border-radius 5px
|
||||
overflow-x auto
|
||||
margin 0 0 1em
|
||||
line-height 1.35
|
||||
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
|
||||
margin 0
|
||||
background-color inherit
|
||||
margin 0
|
||||
padding 0
|
||||
border none
|
||||
border-radius 0
|
||||
pre
|
||||
border none
|
||||
margin -5px
|
||||
&>span.lineNumber
|
||||
display none
|
||||
float left
|
||||
font-size 0.85em
|
||||
margin 0 0.5em 0 -0.5em
|
||||
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 1em
|
||||
padding 0 .5em 0
|
||||
table
|
||||
display block
|
||||
width 100%
|
||||
@@ -236,6 +253,7 @@ table
|
||||
line-height 1.6
|
||||
border-width 1px 0 2px 1px
|
||||
border-color borderColor
|
||||
font-weight bold
|
||||
&:last-child
|
||||
border-right solid 1px borderColor
|
||||
tbody
|
||||
@@ -251,3 +269,64 @@ table
|
||||
border-color borderColor
|
||||
&:last-child
|
||||
border-right solid 1px borderColor
|
||||
kbd
|
||||
background-color #fafbfc
|
||||
border solid 1px borderColor
|
||||
border-bottom-color btnColor
|
||||
border-radius 3px
|
||||
box-shadow inset 0 -1px 0 #959da5
|
||||
display inline-block
|
||||
font-size .8em
|
||||
line-height 1
|
||||
padding 3px 5px
|
||||
|
||||
themeDarkBackground = darken(#21252B, 10%)
|
||||
themeDarkText = #f9f9f9
|
||||
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
|
||||
color #EA6730
|
||||
border-color darken(themeDarkBorder, 10%)
|
||||
background-color lighten(themeDarkPreview, 5%)
|
||||
|
||||
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
|
||||
kbd
|
||||
background-color themeDarkBorder
|
||||
color themeDarkText
|
||||
@@ -1,44 +0,0 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import ModeIcon from 'browser/components/ModeIcon'
|
||||
|
||||
export default class FinderDetail extends React.Component {
|
||||
render () {
|
||||
let { activeArticle } = this.props
|
||||
|
||||
if (activeArticle != null) {
|
||||
return (
|
||||
<div className='FinderDetail'>
|
||||
<div className='header'>
|
||||
<div className='left'>
|
||||
<ModeIcon mode={activeArticle.mode}/> {activeArticle.title}
|
||||
</div>
|
||||
<div className='right'>
|
||||
<button onClick={this.props.saveToClipboard} className='clipboardBtn'>
|
||||
<i className='fa fa-clipboard fa-fw'/>
|
||||
<span className='tooltip'>Copy to clipboard (Enter)</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='content'>
|
||||
{activeArticle.mode === 'markdown'
|
||||
? <MarkdownPreview content={activeArticle.content}/>
|
||||
: <CodeEditor readOnly article={activeArticle}/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className='FinderDetail'>
|
||||
<div className='nothing'>Nothing selected</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FinderDetail.propTypes = {
|
||||
activeArticle: PropTypes.shape(),
|
||||
saveToClipboard: PropTypes.func
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
|
||||
export default class FinderInput extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div className='FinderInput'>
|
||||
<input ref='input' value={this.props.value} onChange={this.props.handleSearchChange} type='text'/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FinderInput.propTypes = {
|
||||
handleSearchChange: PropTypes.func,
|
||||
value: PropTypes.string
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ModeIcon from 'browser/components/ModeIcon'
|
||||
import { selectArticle } from './actions'
|
||||
|
||||
export default class FinderList extends React.Component {
|
||||
componentDidUpdate () {
|
||||
var index = this.props.articles.indexOf(this.props.activeArticle)
|
||||
var el = ReactDOM.findDOMNode(this)
|
||||
var li = el.querySelectorAll('li')[index]
|
||||
|
||||
if (li == null) {
|
||||
return
|
||||
}
|
||||
|
||||
var overflowBelow = el.clientHeight + el.scrollTop < li.offsetTop + li.clientHeight
|
||||
if (overflowBelow) {
|
||||
el.scrollTop = li.offsetTop + li.clientHeight - el.clientHeight
|
||||
}
|
||||
var overflowAbove = el.scrollTop > li.offsetTop
|
||||
if (overflowAbove) {
|
||||
el.scrollTop = li.offsetTop
|
||||
}
|
||||
}
|
||||
|
||||
handleArticleClick (article) {
|
||||
return (e) => {
|
||||
let { dispatch } = this.props
|
||||
dispatch(selectArticle(article.key))
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let articleElements = this.props.articles.map(function (article) {
|
||||
if (article == null) {
|
||||
return (
|
||||
<li className={isActive ? 'active' : ''}>
|
||||
<div className='articleItem'>Undefined</div>
|
||||
<div className='divider'/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
var isActive = this.props.activeArticle != null && (article.key === this.props.activeArticle.key)
|
||||
return (
|
||||
<li key={'article-' + article.key} onClick={this.handleArticleClick(article)} className={isActive ? 'active' : ''}>
|
||||
<div className='articleItem'>
|
||||
<ModeIcon mode={article.mode}/> {article.title}</div>
|
||||
<div className='divider'/>
|
||||
</li>
|
||||
)
|
||||
}.bind(this))
|
||||
|
||||
return (
|
||||
<div className='FinderList'>
|
||||
<ul>
|
||||
{articleElements}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FinderList.propTypes = {
|
||||
articles: PropTypes.array,
|
||||
activeArticle: PropTypes.shape({
|
||||
type: PropTypes.string,
|
||||
key: PropTypes.string
|
||||
}),
|
||||
dispatch: PropTypes.func
|
||||
}
|
||||
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 110px + 32px + 10px + 10px + 20px
|
||||
overflow-y auto
|
||||
|
||||
.result-list
|
||||
user-select none
|
||||
absolute top bottom
|
||||
left $nav-width
|
||||
width $list-width
|
||||
box-sizing border-box
|
||||
overflow-y auto
|
||||
box-shadow 2px 0 15px -8px #b1b1b1
|
||||
z-index 1
|
||||
|
||||
.result-detail
|
||||
absolute top bottom right
|
||||
left $nav-width + $list-width
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
background-color $ui-dark-backgroundColor
|
||||
.search
|
||||
border-color $ui-dark-borderColor
|
||||
.search-input
|
||||
color $ui-dark-text-color
|
||||
|
||||
.result
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
.result-nav
|
||||
background-color $ui-dark-backgroundColor
|
||||
label
|
||||
color $ui-dark-text-color
|
||||
|
||||
.result-nav-menu
|
||||
navDarkButtonColor()
|
||||
|
||||
.result-nav-menu--active
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-button--active-color
|
||||
&:hover
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.result-list
|
||||
border-color $ui-dark-borderColor
|
||||
box-shadow none
|
||||
top 0
|
||||
|
||||
.result-detail
|
||||
absolute top bottom right
|
||||
left $nav-width + $list-width
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
209
browser/finder/NoteDetail.js
Normal file
209
browser/finder/NoteDetail.js
Normal file
@@ -0,0 +1,209 @@
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './NoteDetail.styl'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import CodeMirror from 'codemirror'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
|
||||
const electron = require('electron')
|
||||
const { clipboard } = electron
|
||||
const path = require('path')
|
||||
|
||||
function pass (name) {
|
||||
switch (name) {
|
||||
case 'ejs':
|
||||
return 'Embedded Javascript'
|
||||
case 'html_ruby':
|
||||
return 'Embedded Ruby'
|
||||
case 'objectivec':
|
||||
return 'Objective C'
|
||||
case 'text':
|
||||
return 'Plain Text'
|
||||
default:
|
||||
return name
|
||||
}
|
||||
}
|
||||
function notify (title, options) {
|
||||
if (global.process.platform === 'win32') {
|
||||
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
||||
}
|
||||
return new window.Notification(title, options)
|
||||
}
|
||||
|
||||
class NoteDetail extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
snippetIndex: 0
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.note !== this.props.note) {
|
||||
this.setState({
|
||||
snippetIndex: 0
|
||||
}, () => {
|
||||
if (nextProps.note.type === 'SNIPPET_NOTE') {
|
||||
nextProps.note.snippets.forEach((snippet, index) => {
|
||||
this.refs['code-' + index].reload()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
selectPriorSnippet () {
|
||||
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
|
||||
|
||||
const storage = findStorage(note.storage)
|
||||
|
||||
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}
|
||||
storageKey={note.storage}
|
||||
/>
|
||||
: <CodeEditor styleName='tabView-content'
|
||||
mode={snippet.mode}
|
||||
value={snippet.content}
|
||||
theme={config.editor.theme}
|
||||
fontFamily={config.editor.fontFamily}
|
||||
fontSize={editorFontSize}
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorIndentSize}
|
||||
keyMap={config.editor.keyMap}
|
||||
readOnly
|
||||
ref={'code-' + index}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
})
|
||||
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='description'>
|
||||
<textarea styleName='description-textarea'
|
||||
style={{
|
||||
fontFamily: config.preview.fontFamily,
|
||||
fontSize: parseInt(config.preview.fontSize, 10)
|
||||
}}
|
||||
ref='description'
|
||||
placeholder='Description...'
|
||||
value={note.description}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div styleName='tabList'>
|
||||
{tabList}
|
||||
</div>
|
||||
{viewList}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<MarkdownPreview styleName='root'
|
||||
theme={config.ui.theme}
|
||||
fontSize={config.preview.fontSize}
|
||||
fontFamily={config.preview.fontFamily}
|
||||
codeBlockTheme={config.preview.codeBlockTheme}
|
||||
codeBlockFontFamily={config.editor.fontFamily}
|
||||
lineNumber={config.preview.lineNumber}
|
||||
indentSize={editorIndentSize}
|
||||
value={note.content}
|
||||
showCopyNotification={config.ui.showCopyNotification}
|
||||
storagePath={storage.path}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NoteDetail.propTypes = {
|
||||
}
|
||||
|
||||
export default CSSModules(NoteDetail, styles)
|
||||
98
browser/finder/NoteDetail.styl
Normal file
98
browser/finder/NoteDetail.styl
Normal file
@@ -0,0 +1,98 @@
|
||||
@import('../main/Detail/DetailVars.styl')
|
||||
|
||||
.root
|
||||
absolute top bottom left right
|
||||
bottom 30px
|
||||
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
|
||||
90
browser/finder/NoteList.js
Normal file
90
browser/finder/NoteList.js
Normal file
@@ -0,0 +1,90 @@
|
||||
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)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
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
|
||||
@@ -1,39 +0,0 @@
|
||||
export const SELECT_ARTICLE = 'SELECT_ARTICLE'
|
||||
export const SEARCH_ARTICLE = 'SEARCH_ARTICLE'
|
||||
export const REFRESH_DATA = 'REFRESH_DATA'
|
||||
|
||||
export function selectArticle (key) {
|
||||
return {
|
||||
type: SELECT_ARTICLE,
|
||||
data: { key }
|
||||
}
|
||||
}
|
||||
|
||||
export function searchArticle (input) {
|
||||
return {
|
||||
type: SEARCH_ARTICLE,
|
||||
data: { input }
|
||||
}
|
||||
}
|
||||
|
||||
export function refreshData (data) {
|
||||
console.log('refreshing data')
|
||||
let { folders, articles } = data
|
||||
|
||||
return {
|
||||
type: REFRESH_DATA,
|
||||
data: {
|
||||
articles,
|
||||
folders
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
SELECT_ARTICLE,
|
||||
SEARCH_ARTICLE,
|
||||
REFRESH_DATA,
|
||||
selectArticle,
|
||||
searchArticle,
|
||||
refreshData
|
||||
}
|
||||
@@ -1,85 +1,88 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { connect, Provider } from 'react-redux'
|
||||
import reducer from './reducer'
|
||||
import { createStore } from 'redux'
|
||||
import FinderInput from './FinderInput'
|
||||
import FinderList from './FinderList'
|
||||
import FinderDetail from './FinderDetail'
|
||||
import actions, { selectArticle, searchArticle } from './actions'
|
||||
import _ from 'lodash'
|
||||
import dataStore from 'browser/lib/dataStore'
|
||||
import fetchConfig from '../lib/fetchConfig'
|
||||
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'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
require('!!style!css!stylus?sourceMap!../main/global.styl')
|
||||
require('../lib/customMeta')
|
||||
|
||||
const electron = require('electron')
|
||||
const { clipboard, ipcRenderer, remote } = electron
|
||||
const path = require('path')
|
||||
|
||||
let config = fetchConfig()
|
||||
applyConfig(config)
|
||||
|
||||
ipcRenderer.on('config-apply', function (e, newConfig) {
|
||||
config = newConfig
|
||||
applyConfig(config)
|
||||
})
|
||||
|
||||
function applyConfig () {
|
||||
let body = document.body
|
||||
body.setAttribute('data-theme', config['theme-ui'])
|
||||
|
||||
let hljsCss = document.getElementById('hljs-css')
|
||||
hljsCss.setAttribute('href', '../node_modules/highlight.js/styles/' + config['theme-code'] + '.css')
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
window.addEventListener('keydown', function (e) {
|
||||
if (e.keyCode === 73 && e.metaKey && e.altKey) {
|
||||
remote.getCurrentWindow().toggleDevTools()
|
||||
}
|
||||
})
|
||||
}
|
||||
const { remote } = electron
|
||||
const { Menu } = remote
|
||||
|
||||
function hideFinder () {
|
||||
ipcRenderer.send('hide-finder')
|
||||
}
|
||||
|
||||
function notify (title, options) {
|
||||
if (process.platform === 'win32') {
|
||||
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
||||
let finderWindow = remote.getCurrentWindow()
|
||||
if (global.process.platform === 'win32') {
|
||||
finderWindow.blur()
|
||||
finderWindow.hide()
|
||||
}
|
||||
return new window.Notification(title, options)
|
||||
if (global.process.platform === 'darwin') {
|
||||
Menu.sendActionToFirstResponder('hide:')
|
||||
}
|
||||
remote.getCurrentWindow().hide()
|
||||
}
|
||||
|
||||
require('!!style!css!stylus?sourceMap!../styles/finder/index.styl')
|
||||
|
||||
const FOLDER_FILTER = 'FOLDER_FILTER'
|
||||
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
|
||||
const TEXT_FILTER = 'TEXT_FILTER'
|
||||
const TAG_FILTER = 'TAG_FILTER'
|
||||
|
||||
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.keyDownHandler = e => this.handleKeyDown(e)
|
||||
document.addEventListener('keydown', this.keyDownHandler)
|
||||
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
|
||||
this.focusHandler = e => {
|
||||
let { dispatch } = this.props
|
||||
|
||||
dispatch(searchArticle(''))
|
||||
dispatch(selectArticle(null))
|
||||
}
|
||||
this.refs.search.focus()
|
||||
window.addEventListener('focus', this.focusHandler)
|
||||
window.addEventListener('blur', this.blurHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('keydown', this.keyDownHandler)
|
||||
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()
|
||||
@@ -91,7 +94,9 @@ class FinderMain extends React.Component {
|
||||
}
|
||||
|
||||
if (e.keyCode === 13) {
|
||||
this.saveToClipboard()
|
||||
this.refs.detail.saveToClipboard()
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('COPY_FINDER')
|
||||
hideFinder()
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.keyCode === 27) {
|
||||
@@ -101,26 +106,13 @@ class FinderMain extends React.Component {
|
||||
if (e.keyCode === 91 || e.metaKey) {
|
||||
return
|
||||
}
|
||||
|
||||
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
|
||||
}
|
||||
|
||||
saveToClipboard () {
|
||||
let { activeArticle } = this.props
|
||||
clipboard.writeText(activeArticle.content)
|
||||
|
||||
ipcRenderer.send('copy-finder')
|
||||
notify('Saved to Clipboard!', {
|
||||
body: 'Paste it wherever you want!',
|
||||
silent: true
|
||||
})
|
||||
hideFinder()
|
||||
}
|
||||
|
||||
handleSearchChange (e) {
|
||||
let { dispatch } = this.props
|
||||
|
||||
dispatch(searchArticle(e.target.value))
|
||||
this.setState({
|
||||
search: e.target.value,
|
||||
index: 0
|
||||
})
|
||||
}
|
||||
|
||||
selectArticle (article) {
|
||||
@@ -128,156 +120,236 @@ class FinderMain extends React.Component {
|
||||
}
|
||||
|
||||
selectPrevious () {
|
||||
let { activeArticle, dispatch } = this.props
|
||||
let index = this.refs.finderList.props.articles.indexOf(activeArticle)
|
||||
let previousArticle = this.refs.finderList.props.articles[index - 1]
|
||||
if (previousArticle != null) dispatch(selectArticle(previousArticle.key))
|
||||
if (this.state.index > 0) {
|
||||
this.setState({
|
||||
index: this.state.index - 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
selectNext () {
|
||||
let { activeArticle, dispatch } = this.props
|
||||
let index = this.refs.finderList.props.articles.indexOf(activeArticle)
|
||||
let previousArticle = this.refs.finderList.props.articles[index + 1]
|
||||
if (previousArticle != null) dispatch(selectArticle(previousArticle.key))
|
||||
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 { articles, activeArticle, status, dispatch } = this.props
|
||||
let saveToClipboard = () => this.saveToClipboard()
|
||||
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'>
|
||||
<FinderInput
|
||||
handleSearchChange={e => this.handleSearchChange(e)}
|
||||
ref='finderInput'
|
||||
onChange={this.handleChange}
|
||||
value={status.search}
|
||||
/>
|
||||
<FinderList
|
||||
ref='finderList'
|
||||
activeArticle={activeArticle}
|
||||
articles={articles}
|
||||
dispatch={dispatch}
|
||||
selectArticle={article => this.selectArticle(article)}
|
||||
/>
|
||||
<FinderDetail
|
||||
activeArticle={activeArticle}
|
||||
saveToClipboard={saveToClipboard}
|
||||
/>
|
||||
<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 = {
|
||||
articles: PropTypes.array,
|
||||
activeArticle: PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
tags: PropTypes.array,
|
||||
title: PropTypes.string,
|
||||
content: PropTypes.string
|
||||
}),
|
||||
status: PropTypes.shape(),
|
||||
dispatch: PropTypes.func
|
||||
}
|
||||
|
||||
// Ignore invalid key
|
||||
function ignoreInvalidKey (key) {
|
||||
return key.length > 0 && !key.match(/^\/\/$/) && !key.match(/^\/$/) && !key.match(/^#$/)
|
||||
}
|
||||
|
||||
// Build filter object by key
|
||||
function buildFilter (key) {
|
||||
if (key.match(/^\/\/.+/)) {
|
||||
return {type: FOLDER_EXACT_FILTER, value: key.match(/^\/\/(.+)$/)[1]}
|
||||
}
|
||||
if (key.match(/^\/.+/)) {
|
||||
return {type: FOLDER_FILTER, value: key.match(/^\/(.+)$/)[1]}
|
||||
}
|
||||
if (key.match(/^#(.+)/)) {
|
||||
return {type: TAG_FILTER, value: key.match(/^#(.+)$/)[1]}
|
||||
}
|
||||
return {type: TEXT_FILTER, value: key}
|
||||
}
|
||||
|
||||
function isContaining (target, needle) {
|
||||
return target.match(new RegExp(_.escapeRegExp(needle), 'i'))
|
||||
}
|
||||
|
||||
function startsWith (target, needle) {
|
||||
return target.match(new RegExp('^' + _.escapeRegExp(needle), 'i'))
|
||||
}
|
||||
|
||||
function remap (state) {
|
||||
let { articles, folders, status } = state
|
||||
|
||||
let filters = status.search.split(' ')
|
||||
.map(key => key.trim())
|
||||
.filter(ignoreInvalidKey)
|
||||
.map(buildFilter)
|
||||
|
||||
let folderExactFilters = filters.filter(filter => filter.type === FOLDER_EXACT_FILTER)
|
||||
let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER)
|
||||
let textFilters = filters.filter(filter => filter.type === TEXT_FILTER)
|
||||
let tagFilters = filters.filter(filter => filter.type === TAG_FILTER)
|
||||
|
||||
let targetFolders
|
||||
if (folders != null) {
|
||||
let exactTargetFolders = folders.filter(folder => {
|
||||
return _.find(folderExactFilters, filter => filter.value.toLowerCase() === folder.name.toLowerCase())
|
||||
})
|
||||
let fuzzyTargetFolders = folders.filter(folder => {
|
||||
return _.find(folderFilters, filter => startsWith(folder.name.replace(/_/g, ''), filter.value.replace(/_/g, '')))
|
||||
})
|
||||
targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders)
|
||||
|
||||
if (targetFolders.length > 0) {
|
||||
articles = articles.filter(article => {
|
||||
return _.findWhere(targetFolders, {key: article.FolderKey})
|
||||
})
|
||||
}
|
||||
|
||||
if (textFilters.length > 0) {
|
||||
articles = textFilters.reduce((articles, textFilter) => {
|
||||
return articles.filter(article => {
|
||||
return isContaining(article.title, textFilter.value) || isContaining(article.content, textFilter.value)
|
||||
})
|
||||
}, articles)
|
||||
}
|
||||
|
||||
if (tagFilters.length > 0) {
|
||||
articles = tagFilters.reduce((articles, tagFilter) => {
|
||||
return articles.filter(article => {
|
||||
return _.find(article.tags, tag => isContaining(tag, tagFilter.value))
|
||||
})
|
||||
}, articles)
|
||||
}
|
||||
}
|
||||
|
||||
let activeArticle = _.findWhere(articles, {key: status.articleKey})
|
||||
if (activeArticle == null) activeArticle = articles[0]
|
||||
|
||||
return {
|
||||
articles,
|
||||
activeArticle,
|
||||
status
|
||||
}
|
||||
}
|
||||
|
||||
var Finder = connect(remap)(FinderMain)
|
||||
var store = createStore(reducer)
|
||||
var Finder = connect((x) => x)(CSSModules(FinderMain, styles))
|
||||
|
||||
function refreshData () {
|
||||
let data = dataStore.getData(true)
|
||||
store.dispatch(actions.refreshData(data))
|
||||
}
|
||||
|
||||
window.onfocus = e => {
|
||||
refreshData()
|
||||
// let data = dataStore.getData(true)
|
||||
}
|
||||
|
||||
ReactDOM.render((
|
||||
<Provider store={store}>
|
||||
<Finder/>
|
||||
<Finder />
|
||||
</Provider>
|
||||
), document.getElementById('content'), function () {
|
||||
refreshData()
|
||||
|
||||
124
browser/finder/ipcClient.js
Normal file
124
browser/finder/ipcClient.js
Normal file
@@ -0,0 +1,124 @@
|
||||
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 if (config.ui.theme === 'white') {
|
||||
document.body.setAttribute('data-theme', 'white')
|
||||
} else {
|
||||
document.body.setAttribute('data-theme', 'default')
|
||||
}
|
||||
|
||||
let editorTheme = document.getElementById('editorTheme')
|
||||
if (editorTheme == null) {
|
||||
editorTheme = document.createElement('link')
|
||||
editorTheme.setAttribute('id', 'editorTheme')
|
||||
editorTheme.setAttribute('rel', 'stylesheet')
|
||||
document.head.appendChild(editorTheme)
|
||||
}
|
||||
|
||||
config.editor.theme = consts.THEMES.some((theme) => theme === config.editor.theme)
|
||||
? config.editor.theme
|
||||
: 'default'
|
||||
|
||||
if (config.editor.theme !== 'default') {
|
||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css')
|
||||
}
|
||||
|
||||
store.default.dispatch({
|
||||
type: 'SET_CONFIG',
|
||||
config: config
|
||||
})
|
||||
})
|
||||
|
||||
nodeIpc.of.node.on('quit-finder-app', function () {
|
||||
nodeIpc.of.node.emit('quit-finder-app-confirm')
|
||||
killFinder()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const ipc = {}
|
||||
|
||||
module.exports = ipc
|
||||
@@ -1,47 +0,0 @@
|
||||
import { combineReducers } from 'redux'
|
||||
import { SELECT_ARTICLE, SEARCH_ARTICLE, REFRESH_DATA } from './actions'
|
||||
|
||||
let initialArticles = []
|
||||
let initialFolders = []
|
||||
let initialStatus = {
|
||||
articleKey: null,
|
||||
search: ''
|
||||
}
|
||||
|
||||
function status (state = initialStatus, action) {
|
||||
switch (action.type) {
|
||||
case SELECT_ARTICLE:
|
||||
state.articleKey = action.data.key
|
||||
return Object.assign({}, state)
|
||||
case SEARCH_ARTICLE:
|
||||
state.search = action.data.input
|
||||
return Object.assign({}, state)
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function articles (state = initialArticles, action) {
|
||||
switch (action.type) {
|
||||
case REFRESH_DATA:
|
||||
return action.data.articles
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function folders (state = initialFolders, action) {
|
||||
switch (action.type) {
|
||||
case REFRESH_DATA:
|
||||
console.log(action)
|
||||
return action.data.folders
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
status,
|
||||
folders,
|
||||
articles
|
||||
})
|
||||
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
|
||||
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
|
||||
21
browser/lib/RcParser.js
Normal file
21
browser/lib/RcParser.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import path from 'path'
|
||||
import sander from 'sander'
|
||||
|
||||
const BOOSTNOTERC = '.boostnoterc'
|
||||
const homePath = global.process.env.HOME || global.process.env.USERPROFILE
|
||||
const _boostnotercPath = path.join(homePath, BOOSTNOTERC)
|
||||
|
||||
export function parse (boostnotercPath = _boostnotercPath) {
|
||||
if (!sander.existsSync(boostnotercPath)) return {}
|
||||
try {
|
||||
return JSON.parse(sander.readFileSync(boostnotercPath).toString())
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
console.warn('Your .boostnoterc is broken so it\'s not used.')
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
parse
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
import dataStore from './dataStore'
|
||||
import { request, SERVER_URL } from './api'
|
||||
import clientKey from './clientKey'
|
||||
|
||||
const electron = require('electron')
|
||||
const version = electron.remote.app.getVersion()
|
||||
|
||||
function isSameDate (a, b) {
|
||||
a = moment(a).utcOffset(+540).format('YYYYMMDD')
|
||||
b = moment(b).utcOffset(+540).format('YYYYMMDD')
|
||||
|
||||
return a === b
|
||||
}
|
||||
|
||||
export function init () {
|
||||
let records = getAllRecords()
|
||||
if (records == null) {
|
||||
saveAllRecords([])
|
||||
}
|
||||
emit(null)
|
||||
|
||||
postRecords()
|
||||
if (window != null) {
|
||||
window.addEventListener('online', postRecords)
|
||||
window.setInterval(postRecords, 1000 * 60 * 60 * 24)
|
||||
}
|
||||
}
|
||||
|
||||
export function getAllRecords () {
|
||||
return JSON.parse(localStorage.getItem('activityRecords'))
|
||||
}
|
||||
|
||||
export function saveAllRecords (records) {
|
||||
localStorage.setItem('activityRecords', JSON.stringify(records))
|
||||
}
|
||||
|
||||
/*
|
||||
Post all records(except today)
|
||||
and remove all posted records
|
||||
*/
|
||||
export function postRecords (data) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log('post failed - NOT PRODUCTION ')
|
||||
return
|
||||
}
|
||||
|
||||
let records = getAllRecords()
|
||||
records = records.filter(record => {
|
||||
return !isSameDate(new Date(), record.date)
|
||||
})
|
||||
|
||||
if (records.length === 0) {
|
||||
console.log('No records to post')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('posting...', records)
|
||||
let input = {
|
||||
clientKey: clientKey.get(),
|
||||
records
|
||||
}
|
||||
return request.post(SERVER_URL + 'apis/activity')
|
||||
.send(input)
|
||||
.then(res => {
|
||||
let records = getAllRecords()
|
||||
let todayRecord = _.find(records, record => {
|
||||
return isSameDate(new Date(), record.date)
|
||||
})
|
||||
if (todayRecord != null) saveAllRecords([todayRecord])
|
||||
else saveAllRecords([])
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
export function emit (type, data = {}) {
|
||||
let records = getAllRecords()
|
||||
|
||||
let index = _.findIndex(records, record => {
|
||||
return isSameDate(new Date(), record.date)
|
||||
})
|
||||
|
||||
let todayRecord
|
||||
if (index < 0) {
|
||||
todayRecord = {date: new Date()}
|
||||
records.push(todayRecord)
|
||||
}
|
||||
else todayRecord = records[index]
|
||||
switch (type) {
|
||||
case 'ARTICLE_CREATE':
|
||||
case 'ARTICLE_UPDATE':
|
||||
case 'ARTICLE_DESTROY':
|
||||
case 'FOLDER_CREATE':
|
||||
case 'FOLDER_UPDATE':
|
||||
case 'FOLDER_DESTROY':
|
||||
case 'FINDER_OPEN':
|
||||
case 'FINDER_COPY':
|
||||
case 'MAIN_DETAIL_COPY':
|
||||
case 'ARTICLE_SHARE':
|
||||
todayRecord[type] = todayRecord[type] == null
|
||||
? 1
|
||||
: todayRecord[type] + 1
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
// Count ARTICLE_CREATE and ARTICLE_UPDATE again by syntax
|
||||
if (type === 'ARTICLE_UPDATE' && data.mode != null) {
|
||||
let recordKey = type + '_BY_SYNTAX'
|
||||
if (todayRecord[recordKey] == null) todayRecord[recordKey] = {}
|
||||
|
||||
todayRecord[recordKey][data.mode] = todayRecord[recordKey][data.mode] == null
|
||||
? 1
|
||||
: todayRecord[recordKey][data.mode] + 1
|
||||
}
|
||||
|
||||
let storeData = dataStore.getData()
|
||||
todayRecord.FOLDER_COUNT = storeData && _.isArray(storeData.folders) ? storeData.folders.length : 0
|
||||
todayRecord.ARTICLE_COUNT = storeData && _.isArray(storeData.articles) ? storeData.articles.length : 0
|
||||
todayRecord.CLIENT_VERSION = version
|
||||
|
||||
todayRecord.SYNTAX_COUNT = storeData && _.isArray(storeData.articles) ? storeData.articles.reduce((sum, article) => {
|
||||
if (sum[article.mode] == null) sum[article.mode] = 1
|
||||
else sum[article.mode]++
|
||||
return sum
|
||||
}, {}) : 0
|
||||
|
||||
saveAllRecords(records)
|
||||
}
|
||||
|
||||
export default {
|
||||
init,
|
||||
emit,
|
||||
postRecords
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import superagent from 'superagent'
|
||||
import superagentPromise from 'superagent-promise'
|
||||
|
||||
export const SERVER_URL = 'https://b00st.io/'
|
||||
// export const SERVER_URL = 'http://localhost:3333/'
|
||||
|
||||
export const request = superagentPromise(superagent, Promise)
|
||||
|
||||
export function shareViaPublicURL (input) {
|
||||
return request
|
||||
.post(SERVER_URL + 'apis/share')
|
||||
// .set({
|
||||
// Authorization: 'Bearer ' + auth.token()
|
||||
// })
|
||||
.send(input)
|
||||
}
|
||||
|
||||
export default {
|
||||
SERVER_URL,
|
||||
shareViaPublicURL
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import _ from 'lodash'
|
||||
import keygen from './keygen'
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
getClientKey()
|
||||
|
||||
export default {
|
||||
get: getClientKey,
|
||||
set: setClientKey
|
||||
}
|
||||
@@ -1,3 +1,17 @@
|
||||
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('.'))
|
||||
})
|
||||
themes.splice(themes.indexOf('solarized'), 1, 'solarized dark', 'solarized light')
|
||||
|
||||
const consts = {
|
||||
FOLDER_COLORS: [
|
||||
'#E10051',
|
||||
@@ -16,7 +30,8 @@ const consts = {
|
||||
'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']})
|
||||
@@ -1,156 +0,0 @@
|
||||
import keygen from './keygen'
|
||||
import _ from 'lodash'
|
||||
|
||||
const electron = require('electron')
|
||||
const remote = electron.remote
|
||||
const jetpack = require('fs-jetpack')
|
||||
const path = require('path')
|
||||
|
||||
let defaultContent = 'Boost is a brand new note App for programmers.\n\n> 下に日本語版があります。\n\n# \u25CEfeature\n\nBoost has some preponderant functions for efficient engineer\'s task.See some part of it.\n\n1. classify information by\u300CFolders\u300D\n2. deal with great variety of syntax\n3. Finder function\n\n\uFF0A\u3000\uFF0A\u3000\uFF0A\u3000\uFF0A\n\n# 1. classify information by \u300CFolders\u300D- access the information you needed easily.\n\n\u300CFolders\u300D which on the left side bar. Press plus button now. flexible way of classification.\n- Create Folder every language or flamework\n- Make Folder for your own casual memos\n\n# 2. Deal with a great variety of syntax \u2013 instead of your brain\nSave handy all information related with programming\n- Use markdown and gather api specification\n- Well using module and snippet\n\nSave them on Boost, you don\'t need to rewrite or re-search same code again.\n\n# 3. Load Finder function \u2013 now you don\'t need to spell command by hand typing.\n\n**Shift +ctrl+tab** press buttons at same time.\nThen, the window will show up for search Boost contents that instant.\n\nUsing cursor key to chose, press enter, cmd+v to paste and\u2026 please check it out by your own eye.\n\n- Such command spl or linux which programmers often use but troublesome to hand type\n\n- (Phrases commonly used for e-mail or customer support)\n\nWe support preponderant efficiency\n\n\uFF0A\u3000\uFF0A\u3000\uFF0A\u3000\uFF0A\n\n## \u25CEfor more information\nFrequently updated with this blog ( http:\/\/blog-jp.b00st.io )\n\nHave wonderful programmer life!\n\n## Hack your memory**\n\n\n\n# 日本語版\n\n**Boost**は全く新しいエンジニアライクのノートアプリです。\n\n# ◎特徴\nBoostはエンジニアの仕事を圧倒的に効率化するいくつかの機能を備えています。\nその一部をご紹介します。\n1. Folderで情報を分類\n2. 豊富なsyantaxに対応\n3. Finder機能\n\n\n* * * *\n\n# 1. Folderで情報を分類、欲しい情報にすぐアクセス。\n左側のバーに存在する「Folders」。\n今すぐプラスボタンを押しましょう。\n分類の仕方も自由自在です。\n- 言語やフレームワークごとにFolderを作成\n- 自分用のカジュアルなメモをまとめる場としてFolderを作成\n\n\n# 2. 豊富なsyntaxに対応、自分の脳の代わりに。\nプログラミングに関する情報を全て、手軽に保存しましょう。\n- mdで、apiの仕様をまとめる\n- よく使うモジュールやスニペット\n\nBoostに保存しておくことで、何度も同じコードを書いたり調べたりする必要がなくなります。\n\n# 3. Finder機能を搭載、もうコマンドを手打ちする必要はありません。\n**「shift+ctrl+tab」** を同時に押してみてください。\nここでは、一瞬でBoostの中身を検索するウィンドウを表示させることができます。\n\n矢印キーで選択、Enterを押し、cmd+vでペーストすると…続きはご自身の目でお確かめください。\n- sqlやlinux等の、よく使うが手打ちが面倒なコマンド\n- (メールやカスタマーサポート等でよく使うフレーズ)\n\n私たちは、圧倒的な効率性を支援します。\n\* * * *\n\n\n## ◎詳しくは\nこちらのブログ( http://blog-jp.b00st.io )にて随時更新しています。\n\nそれでは素晴らしいエンジニアライフを!\n\n## Hack your memory**'
|
||||
|
||||
let data = null
|
||||
|
||||
function getLocalPath () {
|
||||
return path.join(remote.app.getPath('userData'), 'local.json')
|
||||
}
|
||||
|
||||
function forgeInitialRepositories () {
|
||||
let defaultRepo = {
|
||||
key: keygen(),
|
||||
name: 'local',
|
||||
type: 'userData',
|
||||
user: {
|
||||
name: 'New user'
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
defaultRepo.user.name = remote.process.env.USER
|
||||
} else if (process.platform === 'win32') {
|
||||
defaultRepo.user.name = remote.process.env.USERNAME
|
||||
}
|
||||
|
||||
return [defaultRepo]
|
||||
}
|
||||
|
||||
function getRepositories () {
|
||||
let raw = localStorage.getItem('repositories')
|
||||
try {
|
||||
let parsed = JSON.parse(raw)
|
||||
if (!_.isArray(parsed)) {
|
||||
throw new Error('repositories data is corrupted. re-init data.')
|
||||
}
|
||||
return parsed
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
let newRepos = forgeInitialRepositories()
|
||||
saveRepositories(newRepos)
|
||||
return newRepos
|
||||
}
|
||||
}
|
||||
|
||||
function saveRepositories (repos) {
|
||||
localStorage.setItem('repositories', JSON.stringify(repos))
|
||||
}
|
||||
|
||||
export function getUser (repoName) {
|
||||
if (repoName == null) {
|
||||
return getRepositories()[0]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function saveUser (repoName, user) {
|
||||
let repos = getRepositories()
|
||||
if (repoName == null) {
|
||||
Object.assign(repos[0].user, user)
|
||||
}
|
||||
saveRepositories(repos)
|
||||
}
|
||||
|
||||
export function init () {
|
||||
// set repositories info
|
||||
getRepositories()
|
||||
data = jetpack.read(getLocalPath(), 'json')
|
||||
if (data == null) {
|
||||
let defaultFolder = {
|
||||
name: 'default',
|
||||
key: keygen()
|
||||
}
|
||||
let defaultArticle = {
|
||||
title: 'About Boost',
|
||||
tags: ['boost', 'intro'],
|
||||
content: defaultContent,
|
||||
mode: 'markdown',
|
||||
key: keygen(),
|
||||
FolderKey: defaultFolder.key,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
|
||||
data = {
|
||||
articles: [defaultArticle],
|
||||
folders: [defaultFolder],
|
||||
version: '0.4'
|
||||
}
|
||||
saveData()
|
||||
}
|
||||
}
|
||||
|
||||
export function getData (forceRead) {
|
||||
if (forceRead) {
|
||||
try {
|
||||
data = jetpack.read(getLocalPath(), 'json')
|
||||
} catch (e) {}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
let timer = null
|
||||
let isSaving = false
|
||||
let saveAgain = false
|
||||
function saveData () {
|
||||
timer = null
|
||||
isSaving = true
|
||||
jetpack.writeAsync(getLocalPath(), data)
|
||||
.then(function () {
|
||||
isSaving = false
|
||||
if (saveAgain) {
|
||||
saveAgain = false
|
||||
queueSave()
|
||||
}
|
||||
})
|
||||
}
|
||||
function queueSave () {
|
||||
if (!isSaving) {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
timer = setTimeout(saveData, 500)
|
||||
} else {
|
||||
saveAgain = true
|
||||
}
|
||||
}
|
||||
|
||||
export function setArticles (articles) {
|
||||
if (!_.isArray(articles)) throw new Error('Articles must be an array')
|
||||
let data = getData()
|
||||
data.articles = articles
|
||||
queueSave()
|
||||
}
|
||||
|
||||
export function setFolders (folders) {
|
||||
if (!_.isArray(folders)) throw new Error('Folders must be an array')
|
||||
let data = getData()
|
||||
data.folders = folders
|
||||
queueSave()
|
||||
}
|
||||
|
||||
export default {
|
||||
getUser,
|
||||
saveUser,
|
||||
init,
|
||||
getData,
|
||||
setArticles,
|
||||
setFolders
|
||||
}
|
||||
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 formatDate (date) {
|
||||
const m = moment(date)
|
||||
if (!m.isValid()) {
|
||||
throw Error('Invalid argument.')
|
||||
}
|
||||
|
||||
return m.format('MMM D, gggg H:mm')
|
||||
}
|
||||
33
browser/lib/findNoteTitle.js
Normal file
33
browser/lib/findNoteTitle.js
Normal file
@@ -0,0 +1,33 @@
|
||||
export function findNoteTitle (value) {
|
||||
let splitted = value.split('\n')
|
||||
let title = null
|
||||
let isInsideCodeBlock = false
|
||||
|
||||
splitted.some((line, index) => {
|
||||
let trimmedLine = line.trim()
|
||||
let trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
|
||||
if (trimmedLine.match('```')) {
|
||||
isInsideCodeBlock = !isInsideCodeBlock
|
||||
}
|
||||
if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) {
|
||||
title = trimmedLine
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
if (title === null) {
|
||||
title = ''
|
||||
splitted.some((line) => {
|
||||
if (line.trim().length > 0) {
|
||||
title = line.trim()
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
export default {
|
||||
findNoteTitle
|
||||
}
|
||||
14
browser/lib/findStorage.js
Normal file
14
browser/lib/findStorage.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const _ = require('lodash')
|
||||
|
||||
export function findStorage (storageKey) {
|
||||
const cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
|
||||
const storage = _.find(cachedStorageList, {key: storageKey})
|
||||
if (storage === undefined) throw new Error('Target storage doesn\'t exist.')
|
||||
|
||||
return storage
|
||||
}
|
||||
|
||||
export default {
|
||||
findStorage
|
||||
}
|
||||
25
browser/lib/getTodoStatus.js
Normal file
25
browser/lib/getTodoStatus.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export function getTodoStatus (content) {
|
||||
let splitted = content.split('\n')
|
||||
let numberOfTodo = 0
|
||||
let numberOfCompletedTodo = 0
|
||||
|
||||
splitted.forEach((line) => {
|
||||
let trimmedLine = line.trim()
|
||||
if (trimmedLine.match(/^[\+\-\*] \[\s|x\] ./)) {
|
||||
numberOfTodo++
|
||||
}
|
||||
if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) {
|
||||
numberOfCompletedTodo++
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
total: numberOfTodo,
|
||||
completed: numberOfCompletedTodo
|
||||
}
|
||||
}
|
||||
|
||||
export function getTodoPercentageOfCompleted (content) {
|
||||
const state = getTodoStatus(content)
|
||||
return Math.floor(state.completed / state.total * 100)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
const hljsThemeList = [
|
||||
{caption: 'Default', name: 'default'},
|
||||
{caption: 'Agate', name: 'agate'},
|
||||
{caption: 'Androidstudio', name: 'androidstudio'},
|
||||
{caption: 'Arduino Light', name: 'arduino-light'},
|
||||
{caption: 'Arta', name: 'arta'},
|
||||
{caption: 'Ascetic', name: 'ascetic'},
|
||||
{caption: 'Atelier Cave Dark', name: 'atelier-cave-dark'},
|
||||
{caption: 'Atelier Cave Light', name: 'atelier-cave-light'},
|
||||
{caption: 'Atelier Dune Dark', name: 'atelier-dune-dark'},
|
||||
{caption: 'Atelier Dune Light', name: 'atelier-dune-light'},
|
||||
{caption: 'Atelier Estuary Dark', name: 'atelier-estuary-dark'},
|
||||
{caption: 'Atelier Estuary Light', name: 'atelier-estuary-light'},
|
||||
{caption: 'Atelier Forest Dark', name: 'atelier-forest-dark'},
|
||||
{caption: 'Atelier Forest Light', name: 'atelier-forest-light'},
|
||||
{caption: 'Atelier Heath Dark', name: 'atelier-heath-dark'},
|
||||
{caption: 'Atelier Heath Light', name: 'atelier-heath-light'},
|
||||
{caption: 'Atelier Lakeside Dark', name: 'atelier-lakeside-dark'},
|
||||
{caption: 'Atelier Lakeside Light', name: 'atelier-lakeside-light'},
|
||||
{caption: 'Atelier Plateau Dark', name: 'atelier-plateau-dark'},
|
||||
{caption: 'Atelier Plateau Light', name: 'atelier-plateau-light'},
|
||||
{caption: 'Atelier Savanna Dark', name: 'atelier-savanna-dark'},
|
||||
{caption: 'Atelier Savanna Light', name: 'atelier-savanna-light'},
|
||||
{caption: 'Atelier Seaside Dark', name: 'atelier-seaside-dark'},
|
||||
{caption: 'Atelier Seaside Light', name: 'atelier-seaside-light'},
|
||||
{caption: 'Atelier Sulphurpool Dark', name: 'atelier-sulphurpool-dark'},
|
||||
{caption: 'Atelier Sulphurpool Light', name: 'atelier-sulphurpool-light'},
|
||||
{caption: 'Brown Paper', name: 'brown-paper'},
|
||||
{caption: 'Codepen Embed', name: 'codepen-embed'},
|
||||
{caption: 'Color Brewer', name: 'color-brewer'},
|
||||
{caption: 'Dark', name: 'dark'},
|
||||
{caption: 'Darkula', name: 'darkula'},
|
||||
{caption: 'Docco', name: 'docco'},
|
||||
{caption: 'Dracula', name: 'dracula'},
|
||||
{caption: 'Far', name: 'far'},
|
||||
{caption: 'Foundation', name: 'foundation'},
|
||||
{caption: 'Github Gist', name: 'github-gist'},
|
||||
{caption: 'Github', name: 'github'},
|
||||
{caption: 'Googlecode', name: 'googlecode'},
|
||||
{caption: 'Grayscale', name: 'grayscale'},
|
||||
{caption: 'Gruvbox Dark', name: 'gruvbox.dark'},
|
||||
{caption: 'Gruvbox Light', name: 'gruvbox.light'},
|
||||
{caption: 'Hopscotch', name: 'hopscotch'},
|
||||
{caption: 'Hybrid', name: 'hybrid'},
|
||||
{caption: 'Idea', name: 'idea'},
|
||||
{caption: 'Ir Black', name: 'ir-black'},
|
||||
{caption: 'Kimbie Dark', name: 'kimbie.dark'},
|
||||
{caption: 'Kimbie Light', name: 'kimbie.light'},
|
||||
{caption: 'Magula', name: 'magula'},
|
||||
{caption: 'Mono Blue', name: 'mono-blue'},
|
||||
{caption: 'Monokai Sublime', name: 'monokai-sublime'},
|
||||
{caption: 'Monokai', name: 'monokai'},
|
||||
{caption: 'Obsidian', name: 'obsidian'},
|
||||
{caption: 'Paraiso Dark', name: 'paraiso-dark'},
|
||||
{caption: 'Paraiso Light', name: 'paraiso-light'},
|
||||
{caption: 'Pojoaque', name: 'pojoaque'},
|
||||
{caption: 'Qtcreator Dark', name: 'qtcreator_dark'},
|
||||
{caption: 'Qtcreator Light', name: 'qtcreator_light'},
|
||||
{caption: 'Railscasts', name: 'railscasts'},
|
||||
{caption: 'Rainbow', name: 'rainbow'},
|
||||
{caption: 'School Book', name: 'school-book'},
|
||||
{caption: 'Solarized Dark', name: 'solarized-dark'},
|
||||
{caption: 'Solarized Light', name: 'solarized-light'},
|
||||
{caption: 'Sunburst', name: 'sunburst'},
|
||||
{caption: 'Tomorrow Night Blue', name: 'tomorrow-night-blue'},
|
||||
{caption: 'Tomorrow Night Bright', name: 'tomorrow-night-bright'},
|
||||
{caption: 'Tomorrow Night Eighties', name: 'tomorrow-night-eighties'},
|
||||
{caption: 'Tomorrow Night', name: 'tomorrow-night'},
|
||||
{caption: 'Tomorrow', name: 'tomorrow'},
|
||||
{caption: 'Vs', name: 'vs'},
|
||||
{caption: 'Xcode', name: 'xcode'},
|
||||
{caption: 'Xt 256', name: 'xt256'},
|
||||
{caption: 'Zenburn', name: 'zenburn'}
|
||||
]
|
||||
|
||||
export default function hljsTheme () {
|
||||
return hljsThemeList
|
||||
}
|
||||
45
browser/lib/htmlTextHelper.js
Normal file
45
browser/lib/htmlTextHelper.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @fileoverview Text trimmer for html.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @return {string}
|
||||
*/
|
||||
|
||||
export function decodeEntities (text) {
|
||||
var entities = [
|
||||
['apos', '\''],
|
||||
['amp', '&'],
|
||||
['lt', '<'],
|
||||
['gt', '>'],
|
||||
['#63', '\\?'],
|
||||
['#36', '\\$']
|
||||
]
|
||||
|
||||
for (var i = 0, max = entities.length; i < max; ++i) {
|
||||
text = text.replace(new RegExp(`&${entities[i][0]};`, 'g'), entities[i][1])
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
export function encodeEntities (text) {
|
||||
const entities = [
|
||||
['\'', 'apos'],
|
||||
['<', 'lt'],
|
||||
['>', 'gt'],
|
||||
['\\?', '#63'],
|
||||
['\\$', '#36']
|
||||
]
|
||||
|
||||
entities.forEach((entity) => {
|
||||
text = text.replace(new RegExp(entity[0], 'g'), `&${entity[1]};`)
|
||||
})
|
||||
return text
|
||||
}
|
||||
|
||||
export default {
|
||||
decodeEntities,
|
||||
encodeEntities
|
||||
}
|
||||
@@ -2,6 +2,6 @@ const crypto = require('crypto')
|
||||
const _ = require('lodash')
|
||||
|
||||
module.exports = function (length) {
|
||||
if (!_.isFinite(length)) length = 6
|
||||
if (!_.isFinite(length)) length = 10
|
||||
return crypto.randomBytes(length).toString('hex')
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import markdownit from 'markdown-it'
|
||||
import emoji from 'markdown-it-emoji'
|
||||
import math from '@rokt33r/markdown-it-math'
|
||||
import hljs from 'highlight.js'
|
||||
import _ from 'lodash'
|
||||
|
||||
// FIXME We should not depend on global variable.
|
||||
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>' + i + '</span>')
|
||||
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
|
||||
}
|
||||
return '<span class="lineNumber">' + lines.join('') + '</span>'
|
||||
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
|
||||
}
|
||||
|
||||
var md = markdownit({
|
||||
@@ -19,20 +20,18 @@ var md = markdownit({
|
||||
linkify: true,
|
||||
html: true,
|
||||
xhtmlOut: true,
|
||||
breaks: true,
|
||||
highlight: function (str, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return '<pre class="hljs">' +
|
||||
createGutter(str) +
|
||||
'<code>' +
|
||||
hljs.highlight(lang, str).value +
|
||||
'</code></pre>'
|
||||
} catch (e) {}
|
||||
if (lang === 'flowchart') {
|
||||
return `<pre class="flowchart">${str}</pre>`
|
||||
}
|
||||
return '<pre class="hljs">' +
|
||||
if (lang === 'sequence') {
|
||||
return `<pre class="sequence">${str}</pre>`
|
||||
}
|
||||
return '<pre class="code">' +
|
||||
createGutter(str) +
|
||||
'<code>' +
|
||||
str.replace(/\&/g, '&').replace(/\</g, '<').replace(/\>/g, '>').replace(/\"/g, '"') +
|
||||
'<code class="' + lang + '">' +
|
||||
str +
|
||||
'</code></pre>'
|
||||
}
|
||||
})
|
||||
@@ -59,21 +58,100 @@ md.use(math, {
|
||||
return output
|
||||
}
|
||||
})
|
||||
md.use(require('markdown-it-checkbox'))
|
||||
|
||||
let originalRenderToken = md.renderer.renderToken
|
||||
md.renderer.renderToken = function renderToken (tokens, idx, options) {
|
||||
let token = tokens[idx]
|
||||
|
||||
let result = originalRenderToken.call(md.renderer, tokens, idx, options)
|
||||
if (token.map != null) {
|
||||
return result + '<a class=\'lineAnchor\' data-key=\'' + token.map[0] + '\'></a>'
|
||||
md.use(require('markdown-it-imsize'))
|
||||
md.use(require('markdown-it-footnote'))
|
||||
md.use(require('markdown-it-multimd-table'))
|
||||
md.use(require('markdown-it-named-headers'), {
|
||||
slugify: (header) => {
|
||||
return encodeURI(header.trim()
|
||||
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
|
||||
.replace(/\s+/g, '-'))
|
||||
.replace(/\-+$/, '')
|
||||
}
|
||||
})
|
||||
md.use(require('markdown-it-kbd'))
|
||||
md.use(require('markdown-it-plantuml'))
|
||||
|
||||
// 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
|
||||
}
|
||||
// FIXME We should not depend on global variable.
|
||||
window.md = md
|
||||
|
||||
export default function markdown (content) {
|
||||
if (content == null) content = ''
|
||||
|
||||
return md.render(content.toString())
|
||||
function normalizeLinkText (linkText) {
|
||||
return md.normalizeLinkText(linkText)
|
||||
}
|
||||
|
||||
const markdown = {
|
||||
render: function markdown (content) {
|
||||
if (!_.isString(content)) content = ''
|
||||
const renderedContent = md.render(content)
|
||||
return renderedContent
|
||||
},
|
||||
normalizeLinkText
|
||||
}
|
||||
|
||||
export default markdown
|
||||
|
||||
39
browser/lib/markdownTextHelper.js
Normal file
39
browser/lib/markdownTextHelper.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @fileoverview Text trimmer for markdown note.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @return {string}
|
||||
*/
|
||||
export function strip (input) {
|
||||
let 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(/(`{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
|
||||
}
|
||||
|
||||
export default {
|
||||
strip
|
||||
}
|
||||
@@ -1,753 +0,0 @@
|
||||
const modes = [
|
||||
{
|
||||
name: 'text',
|
||||
label: 'Plain text',
|
||||
mode: 'text'
|
||||
},
|
||||
{
|
||||
name: 'abap',
|
||||
label: 'ABAP',
|
||||
alias: [],
|
||||
mode: 'abap'
|
||||
},
|
||||
{
|
||||
name: 'abc',
|
||||
label: 'ABC',
|
||||
alias: [],
|
||||
mode: 'abc'
|
||||
},
|
||||
{
|
||||
name: 'actionscript',
|
||||
label: 'ActionScript',
|
||||
alias: ['as'],
|
||||
mode: 'actionscript'
|
||||
},
|
||||
{
|
||||
name: 'ada',
|
||||
label: 'Ada',
|
||||
alias: [],
|
||||
mode: 'ada'
|
||||
},
|
||||
{
|
||||
name: 'apache_conf',
|
||||
label: 'Apache config',
|
||||
alias: ['apache', 'conf'],
|
||||
mode: 'apache_conf'
|
||||
},
|
||||
{
|
||||
name: 'applescript',
|
||||
label: 'AppleScript',
|
||||
alias: ['scpt'],
|
||||
mode: 'applescript'
|
||||
},
|
||||
{
|
||||
name: 'asciidoc',
|
||||
label: 'AsciiDoc',
|
||||
alias: ['ascii', 'doc', 'txt'],
|
||||
mode: 'asciidoc'
|
||||
},
|
||||
{
|
||||
name: 'assembly_x86',
|
||||
label: 'Assembly x86',
|
||||
alias: ['assembly', 'x86', 'asm'],
|
||||
mode: 'assembly_x86'
|
||||
},
|
||||
{
|
||||
name: 'autohotkey',
|
||||
label: 'AutoHotkey',
|
||||
alias: ['ahk'],
|
||||
mode: 'autohotkey'
|
||||
},
|
||||
{
|
||||
name: 'batchfile',
|
||||
label: 'Batch file',
|
||||
alias: ['dos', 'windows', 'bat', 'cmd', 'btm'],
|
||||
mode: 'batchfile'
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
label: 'C',
|
||||
alias: ['c', 'h', 'clang', 'clang'],
|
||||
mode: 'c_cpp'
|
||||
},
|
||||
{
|
||||
name: 'cirru',
|
||||
label: 'Cirru',
|
||||
alias: [],
|
||||
mode: 'cirru'
|
||||
},
|
||||
{
|
||||
name: 'clojure',
|
||||
label: 'Clojure',
|
||||
alias: ['clj', 'cljs', 'cljc', 'edn'],
|
||||
mode: 'clojure'
|
||||
},
|
||||
{
|
||||
name: 'cobol',
|
||||
label: 'COBOL',
|
||||
alias: ['cbl', 'cob', 'cpy'],
|
||||
mode: 'cobol'
|
||||
},
|
||||
{
|
||||
name: 'coffee',
|
||||
label: 'CoffeeScript',
|
||||
alias: ['coffee'],
|
||||
mode: 'coffee'
|
||||
},
|
||||
{
|
||||
name: 'coldfusion',
|
||||
label: 'ColdFusion',
|
||||
alias: ['cfm', 'cfc'],
|
||||
mode: 'coldfusion'
|
||||
},
|
||||
{
|
||||
name: 'cpp',
|
||||
label: 'C++',
|
||||
alias: ['cc', 'cpp', 'cxx', 'hh', 'c++', 'cplusplus'],
|
||||
mode: 'c_cpp'
|
||||
},
|
||||
{
|
||||
name: 'csharp',
|
||||
label: 'C#',
|
||||
alias: ['cs', 'c#'],
|
||||
mode: 'csharp'
|
||||
},
|
||||
{
|
||||
name: 'css',
|
||||
label: 'CSS',
|
||||
alias: ['cascade', 'stylesheet'],
|
||||
mode: 'css'
|
||||
},
|
||||
{
|
||||
name: 'curly',
|
||||
label: 'Curly',
|
||||
alias: [],
|
||||
mode: 'curly'
|
||||
},
|
||||
{
|
||||
name: 'd',
|
||||
label: 'D',
|
||||
alias: ['dlang'],
|
||||
mode: 'd'
|
||||
},
|
||||
{
|
||||
name: 'dockerfile',
|
||||
label: 'DockerFile',
|
||||
alias: ['docker'],
|
||||
mode: 'docker'
|
||||
},
|
||||
{
|
||||
name: 'dart',
|
||||
label: 'Dart',
|
||||
alias: [],
|
||||
mode: 'dart'
|
||||
},
|
||||
{
|
||||
name: 'diff',
|
||||
label: 'Diff',
|
||||
alias: [],
|
||||
mode: 'diff'
|
||||
},
|
||||
{
|
||||
name: 'django',
|
||||
label: 'Django',
|
||||
alias: [],
|
||||
mode: 'djt'
|
||||
},
|
||||
{
|
||||
name: 'dot',
|
||||
label: 'DOT',
|
||||
alias: ['gv'],
|
||||
mode: 'dot'
|
||||
},
|
||||
{
|
||||
name: 'eiffel',
|
||||
label: 'Eiffel',
|
||||
alias: [],
|
||||
mode: 'eiffel'
|
||||
},
|
||||
{
|
||||
name: 'ejs',
|
||||
label: 'EJS',
|
||||
alias: [],
|
||||
mode: 'ejs'
|
||||
},
|
||||
{
|
||||
name: 'elixir',
|
||||
label: 'Elixir',
|
||||
alias: ['ex', 'exs'],
|
||||
mode: 'elixir'
|
||||
},
|
||||
{
|
||||
name: 'elm',
|
||||
label: 'Elm',
|
||||
alias: [],
|
||||
mode: 'elm'
|
||||
},
|
||||
{
|
||||
name: 'erlang',
|
||||
label: 'Erlang',
|
||||
alias: ['erl', 'hrl'],
|
||||
mode: 'erlang'
|
||||
},
|
||||
{
|
||||
name: 'forth',
|
||||
label: 'Forth',
|
||||
alias: ['fs', 'fth'],
|
||||
mode: 'forth'
|
||||
},
|
||||
{
|
||||
name: 'freemaker',
|
||||
label: 'Freemaker',
|
||||
alias: ['ftl'],
|
||||
mode: 'ftl'
|
||||
},
|
||||
{
|
||||
name: 'gcode',
|
||||
label: 'G-code',
|
||||
alias: ['mpt', 'mpf', 'nc'],
|
||||
mode: 'gcode'
|
||||
},
|
||||
{
|
||||
name: 'gherkin',
|
||||
label: 'Gherkin',
|
||||
alias: ['cucumber'],
|
||||
mode: 'gherkin'
|
||||
},
|
||||
{
|
||||
name: 'gitignore',
|
||||
label: 'Gitignore',
|
||||
alias: ['git'],
|
||||
mode: 'gitignore'
|
||||
},
|
||||
{
|
||||
name: 'glsl',
|
||||
label: 'GLSL',
|
||||
alias: ['opengl', 'shading'],
|
||||
mode: 'glsl'
|
||||
},
|
||||
{
|
||||
name: 'golang',
|
||||
label: 'Go',
|
||||
alias: ['go'],
|
||||
mode: 'golang'
|
||||
},
|
||||
{
|
||||
name: 'groovy',
|
||||
label: 'Groovy',
|
||||
alias: [],
|
||||
mode: 'grooby'
|
||||
},
|
||||
{
|
||||
name: 'haml',
|
||||
label: 'Haml',
|
||||
alias: [],
|
||||
mode: 'haml'
|
||||
},
|
||||
{
|
||||
name: 'handlebars',
|
||||
label: 'Handlebars',
|
||||
alias: ['hbs'],
|
||||
mode: 'handlebars'
|
||||
},
|
||||
{
|
||||
name: 'haskell',
|
||||
label: 'Haskell',
|
||||
alias: ['hs', 'lhs'],
|
||||
mode: 'haskell'
|
||||
},
|
||||
{
|
||||
name: 'haxe',
|
||||
label: 'Haxe',
|
||||
alias: ['hx', 'hxml'],
|
||||
mode: 'haxe'
|
||||
},
|
||||
{
|
||||
name: 'html',
|
||||
label: 'HTML',
|
||||
alias: [],
|
||||
mode: 'html'
|
||||
},
|
||||
{
|
||||
name: 'html_ruby',
|
||||
label: 'HTML (Ruby)',
|
||||
alias: ['erb', 'rhtml'],
|
||||
mode: 'html_ruby'
|
||||
},
|
||||
{
|
||||
name: 'jsx',
|
||||
label: 'JSX',
|
||||
alias: ['es', 'babel', 'js', 'jsx', 'react'],
|
||||
mode: 'jsx'
|
||||
},
|
||||
{
|
||||
name: 'typescript',
|
||||
label: 'TypeScript',
|
||||
alias: ['ts'],
|
||||
mode: 'typescript'
|
||||
},
|
||||
{
|
||||
name: 'ini',
|
||||
label: 'INI file',
|
||||
alias: [],
|
||||
mode: 'ini'
|
||||
},
|
||||
{
|
||||
name: 'io',
|
||||
label: 'Io',
|
||||
alias: [],
|
||||
mode: 'io'
|
||||
},
|
||||
{
|
||||
name: 'jack',
|
||||
label: 'Jack',
|
||||
alias: [],
|
||||
mode: 'jack'
|
||||
},
|
||||
{
|
||||
name: 'jade',
|
||||
label: 'Jade',
|
||||
alias: [],
|
||||
mode: 'jade'
|
||||
},
|
||||
{
|
||||
name: 'java',
|
||||
label: 'Java',
|
||||
alias: [],
|
||||
mode: 'java'
|
||||
},
|
||||
{
|
||||
name: 'javascript',
|
||||
label: 'JavaScript',
|
||||
alias: ['js', 'jscript', 'babel', 'es'],
|
||||
mode: 'javascript'
|
||||
},
|
||||
{
|
||||
name: 'json',
|
||||
label: 'JSON',
|
||||
alias: [],
|
||||
mode: 'json'
|
||||
},
|
||||
{
|
||||
name: 'jsoniq',
|
||||
label: 'JSONiq',
|
||||
alias: ['query'],
|
||||
mode: 'jsoniq'
|
||||
},
|
||||
{
|
||||
name: 'jsp',
|
||||
label: 'JSP',
|
||||
alias: [],
|
||||
mode: 'jsp'
|
||||
},
|
||||
{
|
||||
name: 'julia',
|
||||
label: 'Julia',
|
||||
alias: [],
|
||||
mode: 'julia'
|
||||
},
|
||||
{
|
||||
name: 'latex',
|
||||
label: 'Latex',
|
||||
alias: ['tex'],
|
||||
mode: 'latex'
|
||||
},
|
||||
{
|
||||
name: 'lean',
|
||||
label: 'Lean',
|
||||
alias: [],
|
||||
mode: 'lean'
|
||||
},
|
||||
{
|
||||
name: 'less',
|
||||
label: 'Less',
|
||||
alias: [],
|
||||
mode: 'less'
|
||||
},
|
||||
{
|
||||
name: 'liquid',
|
||||
label: 'Liquid',
|
||||
alias: [],
|
||||
mode: 'liquid'
|
||||
},
|
||||
{
|
||||
name: 'lisp',
|
||||
label: 'Lisp',
|
||||
alias: ['lsp'],
|
||||
mode: 'lisp'
|
||||
},
|
||||
{
|
||||
name: 'livescript',
|
||||
label: 'LiveScript',
|
||||
alias: ['ls'],
|
||||
mode: 'livescript'
|
||||
},
|
||||
{
|
||||
name: 'logiql',
|
||||
label: 'LogiQL',
|
||||
alias: [],
|
||||
mode: 'logiql'
|
||||
},
|
||||
{
|
||||
name: 'lsl',
|
||||
label: 'LSL',
|
||||
alias: [],
|
||||
mode: 'lsl'
|
||||
},
|
||||
{
|
||||
name: 'lua',
|
||||
label: 'Lua',
|
||||
alias: [],
|
||||
mode: 'lua'
|
||||
},
|
||||
{
|
||||
name: 'luapage',
|
||||
label: 'Luapage',
|
||||
alias: [],
|
||||
mode: 'luapage'
|
||||
},
|
||||
{
|
||||
name: 'lucene',
|
||||
label: 'Lucene',
|
||||
alias: [],
|
||||
mode: 'lucene'
|
||||
},
|
||||
{
|
||||
name: 'makefile',
|
||||
label: 'Makefile',
|
||||
alias: [],
|
||||
mode: 'makefile'
|
||||
},
|
||||
{
|
||||
name: 'markdown',
|
||||
label: 'Markdown',
|
||||
alias: ['md'],
|
||||
mode: 'markdown'
|
||||
},
|
||||
{
|
||||
name: 'mask',
|
||||
label: 'Mask',
|
||||
alias: [],
|
||||
mode: 'mask'
|
||||
},
|
||||
{
|
||||
name: 'matlab',
|
||||
label: 'MATLAB',
|
||||
alias: [],
|
||||
mode: 'matlab'
|
||||
},
|
||||
{
|
||||
name: 'maze',
|
||||
label: 'Maze',
|
||||
alias: [],
|
||||
mode: 'maze'
|
||||
},
|
||||
{
|
||||
name: 'mel',
|
||||
label: 'MEL',
|
||||
alias: [],
|
||||
mode: 'mel'
|
||||
},
|
||||
{
|
||||
name: 'mipsassembler',
|
||||
label: 'MIPS assembly',
|
||||
alias: [],
|
||||
mode: 'mipsassembler'
|
||||
},
|
||||
{
|
||||
name: 'mushcode',
|
||||
label: 'MUSHCode',
|
||||
alias: [],
|
||||
mode: 'mushcode'
|
||||
},
|
||||
{
|
||||
name: 'mysql',
|
||||
label: 'MySQL',
|
||||
alias: [],
|
||||
mode: 'mysql'
|
||||
},
|
||||
{
|
||||
name: 'nix',
|
||||
label: 'Nix',
|
||||
alias: [],
|
||||
mode: 'nix'
|
||||
},
|
||||
{
|
||||
name: 'objectivec',
|
||||
label: 'Objective C',
|
||||
alias: ['objc'],
|
||||
mode: 'objectivec'
|
||||
},
|
||||
{
|
||||
name: 'ocaml',
|
||||
label: 'OCaml',
|
||||
alias: [],
|
||||
mode: 'ocaml'
|
||||
},
|
||||
{
|
||||
name: 'pascal',
|
||||
label: 'Pascal',
|
||||
alias: [],
|
||||
mode: 'pascal'
|
||||
},
|
||||
{
|
||||
name: 'perl',
|
||||
label: 'Perl',
|
||||
alias: [],
|
||||
mode: 'perl'
|
||||
},
|
||||
{
|
||||
name: 'pgsql',
|
||||
label: 'Postgres SQL',
|
||||
alias: ['postgres'],
|
||||
mode: 'pgsql'
|
||||
},
|
||||
{
|
||||
name: 'php',
|
||||
label: 'PHP',
|
||||
alias: [],
|
||||
mode: 'php'
|
||||
},
|
||||
{
|
||||
name: 'powershell',
|
||||
label: 'PowerShell',
|
||||
alias: ['ps1'],
|
||||
mode: 'powershell'
|
||||
},
|
||||
{
|
||||
name: 'praat',
|
||||
label: 'Praat',
|
||||
alias: [],
|
||||
mode: 'praat'
|
||||
},
|
||||
{
|
||||
name: 'prolog',
|
||||
label: 'Prolog',
|
||||
alias: ['pl', 'pro'],
|
||||
mode: 'prolog'
|
||||
},
|
||||
{
|
||||
name: 'properties',
|
||||
label: 'Properties',
|
||||
alias: [],
|
||||
mode: 'properties'
|
||||
},
|
||||
{
|
||||
name: 'protobuf',
|
||||
label: 'Protocol Buffers',
|
||||
alias: ['protocol', 'buffers'],
|
||||
mode: 'protobuf'
|
||||
},
|
||||
{
|
||||
name: 'python',
|
||||
label: 'Python',
|
||||
alias: ['py'],
|
||||
mode: 'python'
|
||||
},
|
||||
{
|
||||
name: 'r',
|
||||
label: 'R',
|
||||
alias: ['rlang'],
|
||||
mode: 'r'
|
||||
},
|
||||
{
|
||||
name: 'rdoc',
|
||||
label: 'RDoc',
|
||||
alias: [],
|
||||
mode: 'rdoc'
|
||||
},
|
||||
{
|
||||
name: 'ruby',
|
||||
label: 'Ruby',
|
||||
alias: ['rb'],
|
||||
mode: 'ruby'
|
||||
},
|
||||
{
|
||||
name: 'rust',
|
||||
label: 'Rust',
|
||||
alias: [],
|
||||
mode: 'rust'
|
||||
},
|
||||
{
|
||||
name: 'sass',
|
||||
label: 'Sass',
|
||||
alias: [],
|
||||
mode: 'sass'
|
||||
},
|
||||
{
|
||||
name: 'scad',
|
||||
label: 'SCAD',
|
||||
alias: [],
|
||||
mode: 'scad'
|
||||
},
|
||||
{
|
||||
name: 'scala',
|
||||
label: 'Scala',
|
||||
alias: [],
|
||||
mode: 'scala'
|
||||
},
|
||||
{
|
||||
name: 'scheme',
|
||||
label: 'Scheme',
|
||||
alias: ['scm', 'ss'],
|
||||
mode: 'scheme'
|
||||
},
|
||||
{
|
||||
name: 'scss',
|
||||
label: 'Scss',
|
||||
alias: [],
|
||||
mode: 'scss'
|
||||
},
|
||||
{
|
||||
name: 'sh',
|
||||
label: 'Shell',
|
||||
alias: ['shell'],
|
||||
mode: 'sh'
|
||||
},
|
||||
{
|
||||
name: 'sjs',
|
||||
label: 'StratifiedJS',
|
||||
alias: ['stratified'],
|
||||
mode: 'sjs'
|
||||
},
|
||||
{
|
||||
name: 'smarty',
|
||||
label: 'Smarty',
|
||||
alias: [],
|
||||
mode: 'smarty'
|
||||
},
|
||||
{
|
||||
name: 'snippets',
|
||||
label: 'Snippets',
|
||||
alias: [],
|
||||
mode: 'snippets'
|
||||
},
|
||||
{
|
||||
name: 'soy_template',
|
||||
label: 'Soy Template',
|
||||
alias: ['soy'],
|
||||
mode: 'soy_template'
|
||||
},
|
||||
{
|
||||
name: 'space',
|
||||
label: 'Space',
|
||||
alias: [],
|
||||
mode: 'space'
|
||||
},
|
||||
{
|
||||
name: 'sql',
|
||||
label: 'SQL',
|
||||
alias: [],
|
||||
mode: 'sql'
|
||||
},
|
||||
{
|
||||
name: 'sqlserver',
|
||||
label: 'SQL Server',
|
||||
alias: [],
|
||||
mode: 'sqlserver'
|
||||
},
|
||||
{
|
||||
name: 'stylus',
|
||||
label: 'Stylus',
|
||||
alias: [],
|
||||
mode: 'stylus'
|
||||
},
|
||||
{
|
||||
name: 'svg',
|
||||
label: 'SVG',
|
||||
alias: [],
|
||||
mode: 'svg'
|
||||
},
|
||||
{
|
||||
name: 'swift',
|
||||
label: 'Swift',
|
||||
alias: [],
|
||||
mode: 'swift'
|
||||
},
|
||||
{
|
||||
name: 'swig',
|
||||
label: 'SWIG',
|
||||
alias: [],
|
||||
mode: 'swig'
|
||||
},
|
||||
{
|
||||
name: 'tcl',
|
||||
label: 'Tcl',
|
||||
alias: [],
|
||||
mode: 'tcl'
|
||||
},
|
||||
{
|
||||
name: 'tex',
|
||||
label: 'TeX',
|
||||
alias: [],
|
||||
mode: 'tex'
|
||||
},
|
||||
{
|
||||
name: 'textile',
|
||||
label: 'Textile',
|
||||
alias: [],
|
||||
mode: 'textile'
|
||||
},
|
||||
{
|
||||
name: 'toml',
|
||||
label: 'TOML',
|
||||
alias: [],
|
||||
mode: 'toml'
|
||||
},
|
||||
{
|
||||
name: 'twig',
|
||||
label: 'Twig',
|
||||
alias: [],
|
||||
mode: 'twig'
|
||||
},
|
||||
{
|
||||
name: 'vala',
|
||||
label: 'Vala',
|
||||
alias: [],
|
||||
mode: 'vala'
|
||||
},
|
||||
{
|
||||
name: 'vbscript',
|
||||
label: 'VBScript',
|
||||
alias: ['vbs', 'vbe'],
|
||||
mode: 'vbscript'
|
||||
},
|
||||
{
|
||||
name: 'velocity',
|
||||
label: 'Velocity',
|
||||
alias: [],
|
||||
mode: 'velocity'
|
||||
},
|
||||
{
|
||||
name: 'verilog',
|
||||
label: 'Verilog',
|
||||
alias: [],
|
||||
mode: 'verilog'
|
||||
},
|
||||
{
|
||||
name: 'vhdl',
|
||||
label: 'VHDL',
|
||||
alias: [],
|
||||
mode: 'vhdl'
|
||||
},
|
||||
{
|
||||
name: 'xml',
|
||||
label: 'XML',
|
||||
alias: [],
|
||||
mode: 'xml'
|
||||
},
|
||||
{
|
||||
name: 'xquery',
|
||||
label: 'XQuery',
|
||||
alias: [],
|
||||
mode: 'xquery'
|
||||
},
|
||||
{
|
||||
name: 'yaml',
|
||||
label: 'YAML',
|
||||
alias: [],
|
||||
mode: 'yaml'
|
||||
}
|
||||
]
|
||||
|
||||
export default modes
|
||||
43
browser/lib/search.js
Normal file
43
browser/lib/search.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
export default function searchFromNotes (notes, search) {
|
||||
if (search.trim().length === 0) return []
|
||||
const searchBlocks = search.split(' ').filter(block => { return block !== '' })
|
||||
|
||||
let foundNotes = findByWord(notes, searchBlocks[0])
|
||||
searchBlocks.forEach((block) => {
|
||||
foundNotes = findByWord(foundNotes, block)
|
||||
if (block.match(/^#.+/)) {
|
||||
foundNotes = foundNotes.concat(findByTag(notes, block))
|
||||
}
|
||||
})
|
||||
return foundNotes
|
||||
}
|
||||
|
||||
function findByTag (notes, block) {
|
||||
const tag = block.match(/#(.+)/)[1]
|
||||
let regExp = new RegExp(_.escapeRegExp(tag), 'i')
|
||||
return notes.filter((note) => {
|
||||
if (!_.isArray(note.tags)) return false
|
||||
return note.tags.some((_tag) => {
|
||||
return _tag.match(regExp)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function findByWord (notes, block) {
|
||||
let regExp = new RegExp(_.escapeRegExp(block), 'i')
|
||||
return 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
|
||||
})
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
.root
|
||||
absolute top bottom right
|
||||
border-width 1px 0
|
||||
border-style solid
|
||||
border-color $ui-borderColor
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
|
||||
.empty
|
||||
height 320px
|
||||
@@ -11,7 +11,14 @@
|
||||
|
||||
.empty-message
|
||||
width 100%
|
||||
font-size 42px
|
||||
line-height 72px
|
||||
font-size 36px
|
||||
font-weight 600
|
||||
line-height 56px
|
||||
text-align center
|
||||
color $ui-inactive-text-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
background-color $ui-dark-backgroundColor
|
||||
.empty-message
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
11
browser/main/Detail/DetailVars.styl
Normal file
11
browser/main/Detail/DetailVars.styl
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Varibales for note detail space.
|
||||
*/
|
||||
|
||||
// Margin on the left side and the right side for NoteDetail component.
|
||||
$note-detail-left-margin = 100px
|
||||
$note-detail-right-margin = 120px
|
||||
$snippet-note-detail-left-margin = 60px
|
||||
$snippet-note-detail-right-margin = 80px
|
||||
|
||||
$note-detail-box-shadow = 2px 0 15px -8px #b1b1b1 inset
|
||||
@@ -128,8 +128,8 @@ class FolderSelect extends React.Component {
|
||||
}
|
||||
|
||||
nextOption () {
|
||||
let { storages } = this.props
|
||||
let { optionIndex } = this.state
|
||||
let { folders } = this.props
|
||||
|
||||
optionIndex++
|
||||
if (optionIndex >= folders.length) optionIndex = 0
|
||||
@@ -184,12 +184,12 @@ class FolderSelect extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
let { className, storages, value } = this.props
|
||||
let { className, data, value } = this.props
|
||||
let splitted = value.split('-')
|
||||
let storageKey = splitted.shift()
|
||||
let folderKey = splitted.shift()
|
||||
let options = []
|
||||
storages.forEach((storage, index) => {
|
||||
data.storageMap.forEach((storage, index) => {
|
||||
storage.folders.forEach((folder) => {
|
||||
options.push({
|
||||
storage: storage,
|
||||
@@ -200,6 +200,11 @@ class FolderSelect extends React.Component {
|
||||
|
||||
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 (
|
||||
@@ -254,16 +259,13 @@ class FolderSelect extends React.Component {
|
||||
{optionList}
|
||||
</div>
|
||||
</div>
|
||||
: <div styleName='idle'>
|
||||
: <div styleName='idle' style={{color: currentOption.folder.color}}>
|
||||
<div styleName='idle-label'>
|
||||
<span styleName='idle-label-name'
|
||||
style={{borderColor: currentOption.folder.color}}
|
||||
>
|
||||
<i className='fa fa-folder' />
|
||||
<span styleName='idle-label-name'>
|
||||
{currentOption.folder.name}
|
||||
<span styleName='idle-label-name-surfix'>in {currentOption.storage.name}</span>
|
||||
</span>
|
||||
</div>
|
||||
<i styleName='idle-caret' className='fa fa-fw fa-caret-down'/>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,40 @@
|
||||
.root
|
||||
position relative
|
||||
border solid 1px transparent
|
||||
line-height 34px
|
||||
vertical-align middle
|
||||
border-radius 2px
|
||||
transition 0.15s
|
||||
user-select none
|
||||
margin-right 10px
|
||||
&:hover
|
||||
background-color white
|
||||
border-color $ui-borderColor
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
|
||||
.root--search, .root--focus
|
||||
@extend .root
|
||||
background-color white
|
||||
background-color $ui-noteDetail-backgroundColor = #fff
|
||||
border-color $ui-input--focus-borderColor
|
||||
width 154px
|
||||
height 30px
|
||||
&:hover
|
||||
background-color white
|
||||
border-color $ui-input--focus-borderColor
|
||||
border-color $ui-input--focus-borderColor = #fff
|
||||
|
||||
.idle
|
||||
position relative
|
||||
cursor pointer
|
||||
|
||||
.idle-label
|
||||
absolute left top
|
||||
padding 0 0 0 5px
|
||||
right 20px
|
||||
overflow ellipsis
|
||||
display flex
|
||||
align-items center
|
||||
|
||||
.idle-label-name
|
||||
border-left solid 4px transparent
|
||||
padding 2px 5px
|
||||
font-size 13px
|
||||
font-weight 600
|
||||
margin-left 4px
|
||||
|
||||
.idle-label-name-surfix
|
||||
font-size 10px
|
||||
font-size 15px
|
||||
color $ui-inactive-text-color
|
||||
margin-left 5px
|
||||
.idle-caret
|
||||
@@ -39,38 +42,42 @@
|
||||
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
|
||||
top 0
|
||||
font-size 14px
|
||||
outline none
|
||||
border none
|
||||
height 20px
|
||||
line-height 20px
|
||||
width 100%
|
||||
background-color transparent
|
||||
padding 0 10px
|
||||
|
||||
.search-optionList
|
||||
position fixed
|
||||
border $ui-border
|
||||
position absolute
|
||||
top 30px
|
||||
max-height 450px
|
||||
min-width 150px
|
||||
overflow auto
|
||||
z-index 200
|
||||
border $ui-border
|
||||
background-color white
|
||||
border-radius 2px
|
||||
padding 10px 6px
|
||||
|
||||
.search-optionList-item
|
||||
width 140px
|
||||
height 34px
|
||||
width 250px
|
||||
display flex
|
||||
align-items center
|
||||
box-sizing border-box
|
||||
padding 0 5px
|
||||
padding 0
|
||||
margin-bottom 10px
|
||||
overflow ellipsis
|
||||
cursor pointer
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
|
||||
.search-optionList-item--active
|
||||
@extend .search-optionList-item
|
||||
@@ -80,9 +87,55 @@
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
.search-optionList-item-name
|
||||
border-left solid 4px transparent
|
||||
padding 2px 5px
|
||||
border-left solid 3px transparent
|
||||
padding 6px
|
||||
.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
|
||||
|
||||
19
browser/main/Detail/InfoButton.js
Normal file
19
browser/main/Detail/InfoButton.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './InfoButton.styl'
|
||||
|
||||
const InfoButton = ({
|
||||
onClick
|
||||
}) => (
|
||||
<button styleName='control-infoButton'
|
||||
onClick={(e) => onClick(e)}
|
||||
>
|
||||
<img className='infoButton' src='../resources/icon/icon-info.svg' />
|
||||
</button>
|
||||
)
|
||||
|
||||
InfoButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(InfoButton, styles)
|
||||
11
browser/main/Detail/InfoButton.styl
Normal file
11
browser/main/Detail/InfoButton.styl
Normal file
@@ -0,0 +1,11 @@
|
||||
.control-infoButton
|
||||
top 10px
|
||||
margin-bottom 10px
|
||||
topBarButtonLight()
|
||||
|
||||
.infoButton
|
||||
padding 0px
|
||||
|
||||
body[data-theme="dark"]
|
||||
.control-infoButton
|
||||
topBarButtonDark()
|
||||
90
browser/main/Detail/InfoPanel.js
Normal file
90
browser/main/Detail/InfoPanel.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './InfoPanel.styl'
|
||||
|
||||
const InfoPanel = ({
|
||||
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, wordCount, letterCount, type, print
|
||||
}) => (
|
||||
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
|
||||
<div>
|
||||
<p styleName='modification-date'>{updatedAt}</p>
|
||||
<p styleName='modification-date-desc'>MODIFICATION DATE</p>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
{type === 'SNIPPET_NOTE'
|
||||
? ''
|
||||
: <div styleName='count-wrap'>
|
||||
<div styleName='count-number'>
|
||||
<p styleName='infoPanel-defaul-count'>{wordCount}</p>
|
||||
<p styleName='infoPanel-sub-count'>Words</p>
|
||||
</div>
|
||||
<div styleName='count-number'>
|
||||
<p styleName='infoPanel-defaul-count'>{letterCount}</p>
|
||||
<p styleName='infoPanel-sub-count'>Letters</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{type === 'SNIPPET_NOTE'
|
||||
? ''
|
||||
: <hr />
|
||||
}
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'>{storageName}</p>
|
||||
<p styleName='infoPanel-sub'>STORAGE</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'>{folderName}</p>
|
||||
<p styleName='infoPanel-sub'>FOLDER</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'>{createdAt}</p>
|
||||
<p styleName='infoPanel-sub'>CREATION DATE</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input styleName='infoPanel-noteLink' value={noteLink} onClick={(e) => { e.target.select() }} />
|
||||
<p styleName='infoPanel-sub'>NOTE LINK</p>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div id='export-wrap'>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
||||
<i className='fa fa-file-code-o fa-fw' />
|
||||
<p>.md</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
||||
<i className='fa fa-file-text-o fa-fw' />
|
||||
<p>.txt</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => print(e)}>
|
||||
<i className='fa fa-print fa-fw' />
|
||||
<p>Print</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
InfoPanel.propTypes = {
|
||||
storageName: PropTypes.string.isRequired,
|
||||
folderName: PropTypes.string.isRequired,
|
||||
noteLink: PropTypes.string.isRequired,
|
||||
updatedAt: PropTypes.string.isRequired,
|
||||
createdAt: PropTypes.string.isRequired,
|
||||
exportAsMd: PropTypes.func.isRequired,
|
||||
exportAsTxt: PropTypes.func.isRequired,
|
||||
wordCount: PropTypes.number,
|
||||
letterCount: PropTypes.number,
|
||||
type: PropTypes.string.isRequired,
|
||||
print: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(InfoPanel, styles)
|
||||
163
browser/main/Detail/InfoPanel.styl
Normal file
163
browser/main/Detail/InfoPanel.styl
Normal file
@@ -0,0 +1,163 @@
|
||||
.control-infoPanel
|
||||
position fixed
|
||||
pointer-events none
|
||||
top 50px
|
||||
z-index 200
|
||||
line-height normal
|
||||
border-radius 4px
|
||||
opacity 0
|
||||
transition 0.2s
|
||||
|
||||
.control-infoButton-panel
|
||||
z-index 200
|
||||
margin-top 0px
|
||||
right 0
|
||||
position absolute
|
||||
padding 20px 25px 0 25px
|
||||
width 300px
|
||||
height 350px
|
||||
overflow auto
|
||||
background-color $ui-noteList-backgroundColor
|
||||
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
|
||||
border-radius 2px
|
||||
|
||||
.modification-date
|
||||
font-size 18px
|
||||
line-height 30px
|
||||
color $ui-text-color
|
||||
|
||||
.modification-date-desc
|
||||
font-size 18px
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.control-infoButton-panel-trash
|
||||
z-index 200
|
||||
margin-top 0px
|
||||
right 0px
|
||||
position absolute
|
||||
padding 20px 25px 0 25px
|
||||
width 300px
|
||||
background-color $ui-noteList-backgroundColor
|
||||
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
|
||||
border-radius 2px
|
||||
|
||||
.count-wrap
|
||||
display flex
|
||||
position relative
|
||||
width 100%
|
||||
|
||||
.count-number
|
||||
position relative
|
||||
display block
|
||||
width 50%
|
||||
overflow hidden
|
||||
margin 0
|
||||
padding 0
|
||||
|
||||
.infoPanel-defaul-count
|
||||
font-size 16px
|
||||
line-height 30px
|
||||
color $ui-text-color
|
||||
|
||||
.infoPanel-sub-count
|
||||
font-size 16px
|
||||
color $ui-inactive-text-color
|
||||
padding-bottom 8px
|
||||
|
||||
.infoPanel-default
|
||||
font-size 14px
|
||||
line-height 30px
|
||||
color $ui-text-color
|
||||
|
||||
.infoPanel-sub
|
||||
font-size 14px
|
||||
color $ui-inactive-text-color
|
||||
padding-bottom 8px
|
||||
|
||||
.infoPanel-noteLink
|
||||
padding-right 5px
|
||||
width 200px
|
||||
height 25px
|
||||
margin-bottom 6px
|
||||
|
||||
.infoPanel-trash
|
||||
color #EA4447
|
||||
font-weight 600
|
||||
font-size 14px
|
||||
width 70px
|
||||
background-color rgba(226,33,113,0.1)
|
||||
border none
|
||||
outline none
|
||||
border-radius 2px
|
||||
margin-right 5px
|
||||
padding 2px 5px
|
||||
|
||||
[id=export-wrap]
|
||||
height 90px
|
||||
display flex
|
||||
justify-content center
|
||||
margin 20px 0 10px 0
|
||||
button
|
||||
outline none
|
||||
font-size 48px
|
||||
color #A0A0A0
|
||||
background-color transparent
|
||||
border none
|
||||
margin 0 5px
|
||||
border-radius 5px
|
||||
&:hover
|
||||
transition 0.2s
|
||||
background-color alpha($ui-button--hover-backgroundColor, 30%)
|
||||
color $ui-inactive-text-color
|
||||
p
|
||||
font-size 13px
|
||||
color #A0A0A0
|
||||
font-weight light
|
||||
&:hover
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.export--enable
|
||||
cursor pointer
|
||||
|
||||
.export--unable
|
||||
cursor not-allowed
|
||||
|
||||
body[data-theme="dark"]
|
||||
.control-infoButton-panel
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
.control-infoButton-panel-trash
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
.modification-date
|
||||
color $ui-dark-text-color
|
||||
|
||||
.modification-date-desc
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-defaul-count
|
||||
color $ui-dark-text-color
|
||||
|
||||
.infoPanel-sub-count
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-default
|
||||
color $ui-dark-text-color
|
||||
|
||||
.infoPanel-sub
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-noteLink
|
||||
background-color alpha($ui-dark-borderColor, 60%)
|
||||
color $ui-dark-text-color
|
||||
|
||||
[id=export-wrap]
|
||||
button
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-dark-borderColor, 20%)
|
||||
color $ui-dark-text-color
|
||||
p
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
59
browser/main/Detail/InfoPanelTrashed.js
Normal file
59
browser/main/Detail/InfoPanelTrashed.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './InfoPanel.styl'
|
||||
|
||||
const InfoPanelTrashed = ({
|
||||
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt
|
||||
}) => (
|
||||
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
|
||||
<div>
|
||||
<p styleName='modification-date'>{updatedAt}</p>
|
||||
<p styleName='modification-date-desc'>MODIFICATION DATE</p>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'>{storageName}</p>
|
||||
<p styleName='infoPanel-sub'>STORAGE</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'><text styleName='infoPanel-trash'>Trash</text>{folderName}</p>
|
||||
<p styleName='infoPanel-sub'>FOLDER</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'>{createdAt}</p>
|
||||
<p styleName='infoPanel-sub'>CREATION DATE</p>
|
||||
</div>
|
||||
|
||||
<div id='export-wrap'>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
||||
<i className='fa fa-file-code-o fa-fw' />
|
||||
<p>.md</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
||||
<i className='fa fa-file-text-o fa-fw' />
|
||||
<p>.txt</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--unable'>
|
||||
<i className='fa fa-file-pdf-o fa-fw' />
|
||||
<p>.pdf</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
InfoPanelTrashed.propTypes = {
|
||||
storageName: PropTypes.string.isRequired,
|
||||
folderName: PropTypes.string.isRequired,
|
||||
updatedAt: PropTypes.string.isRequired,
|
||||
createdAt: PropTypes.string.isRequired,
|
||||
exportAsMd: PropTypes.func.isRequired,
|
||||
exportAsTxt: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(InfoPanelTrashed, styles)
|
||||
@@ -2,83 +2,83 @@ import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './MarkdownNoteDetail.styl'
|
||||
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
||||
import TodoListPercentage from 'browser/components/TodoListPercentage'
|
||||
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/markdownTextHelper'
|
||||
import StatusBar from '../StatusBar'
|
||||
import _ from 'lodash'
|
||||
import { findNoteTitle } from 'browser/lib/findNoteTitle'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import TrashButton from './TrashButton'
|
||||
import PermanentDeleteButton from './PermanentDeleteButton'
|
||||
import InfoButton from './InfoButton'
|
||||
import InfoPanel from './InfoPanel'
|
||||
import InfoPanelTrashed from './InfoPanelTrashed'
|
||||
import { formatDate } from 'browser/lib/date-formatter'
|
||||
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
|
||||
import striptags from 'striptags'
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
const Menu = remote.Menu
|
||||
const MenuItem = remote.MenuItem
|
||||
const { Menu, MenuItem, dialog } = remote
|
||||
|
||||
class MarkdownNoteDetail extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isMovingNote: false,
|
||||
note: Object.assign({
|
||||
title: '',
|
||||
content: '',
|
||||
isMovingNote: false,
|
||||
isDeleting: false
|
||||
}, props.note)
|
||||
content: ''
|
||||
}, props.note),
|
||||
isLockButtonShown: false,
|
||||
isLocked: false
|
||||
}
|
||||
this.dispatchTimer = null
|
||||
|
||||
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.refs.content.focus()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
ee.on('topbar:togglelockbutton', this.toggleLockButton)
|
||||
}
|
||||
|
||||
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),
|
||||
isDeleting: false
|
||||
note: Object.assign({}, nextProps.note)
|
||||
}, () => {
|
||||
this.refs.content.reload()
|
||||
this.refs.tags.reset()
|
||||
if (this.refs.tags) this.refs.tags.reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
findTitle (value) {
|
||||
let splitted = value.split('\n')
|
||||
let title = null
|
||||
componentWillUnmount () {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
}
|
||||
|
||||
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
|
||||
componentDidUnmount () {
|
||||
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
let { note } = this.state
|
||||
|
||||
note.content = this.refs.content.value
|
||||
note.tags = this.refs.tags.value
|
||||
note.title = this.findTitle(note.content)
|
||||
if (this.refs.tags) note.tags = this.refs.tags.value
|
||||
note.title = markdown.strip(striptags(findNoteTitle(note.content)))
|
||||
note.updatedAt = new Date()
|
||||
|
||||
this.setState({
|
||||
@@ -89,15 +89,26 @@ class MarkdownNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
save () {
|
||||
let { note, dispatch } = this.props
|
||||
clearTimeout(this.saveQueue)
|
||||
this.saveQueue = setTimeout(() => {
|
||||
this.saveNow()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: this.state.note
|
||||
})
|
||||
saveNow () {
|
||||
let { note, dispatch } = this.props
|
||||
clearTimeout(this.saveQueue)
|
||||
this.saveQueue = null
|
||||
|
||||
dataApi
|
||||
.updateNote(note.storage, note.folder, note.key, this.state.note)
|
||||
.updateNote(note.storage, note.key, this.state.note)
|
||||
.then((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
|
||||
})
|
||||
}
|
||||
|
||||
handleFolderChange (e) {
|
||||
@@ -108,7 +119,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
let newFolderKey = splitted.shift()
|
||||
|
||||
dataApi
|
||||
.moveNote(note.storage, note.folder, note.key, newStorageKey, newFolderKey)
|
||||
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
|
||||
.then((newNote) => {
|
||||
this.setState({
|
||||
isMovingNote: true,
|
||||
@@ -117,13 +128,13 @@ class MarkdownNoteDetail extends React.Component {
|
||||
let { dispatch, location } = this.props
|
||||
dispatch({
|
||||
type: 'MOVE_NOTE',
|
||||
note: note,
|
||||
newNote: newNote
|
||||
originNote: note,
|
||||
note: newNote
|
||||
})
|
||||
hashHistory.replace({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: newNote.uniqueKey
|
||||
key: newNote.storage + '-' + newNote.key
|
||||
}
|
||||
})
|
||||
this.setState({
|
||||
@@ -135,6 +146,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
|
||||
handleStarButtonClick (e) {
|
||||
let { note } = this.state
|
||||
if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
|
||||
|
||||
note.isStarred = !note.isStarred
|
||||
|
||||
@@ -149,128 +161,239 @@ class MarkdownNoteDetail extends React.Component {
|
||||
|
||||
}
|
||||
|
||||
handleShareButtonClick (e) {
|
||||
let menu = new Menu()
|
||||
menu.append(new MenuItem({
|
||||
label: 'Export as a File',
|
||||
click: (e) => this.handlePreferencesButtonClick(e)
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
label: 'Export to Web',
|
||||
disabled: true,
|
||||
click: (e) => this.handlePreferencesButtonClick(e)
|
||||
}))
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
exportAsMd () {
|
||||
ee.emit('export:save-md')
|
||||
}
|
||||
|
||||
handleContextButtonClick (e) {
|
||||
let menu = new Menu()
|
||||
menu.append(new MenuItem({
|
||||
label: 'Delete',
|
||||
click: (e) => this.handleDeleteMenuClick(e)
|
||||
}))
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
exportAsTxt () {
|
||||
ee.emit('export:save-text')
|
||||
}
|
||||
|
||||
handleDeleteMenuClick (e) {
|
||||
this.setState({
|
||||
isDeleting: true
|
||||
})
|
||||
}
|
||||
handleTrashButtonClick (e) {
|
||||
let { note } = this.state
|
||||
const { isTrashed } = note
|
||||
|
||||
handleDeleteConfirmButtonClick (e) {
|
||||
let { note, dispatch } = this.props
|
||||
dataApi
|
||||
.removeNote(note.storage, note.folder, note.key)
|
||||
.then(() => {
|
||||
let dispatchHandler = () => {
|
||||
dispatch({
|
||||
type: 'REMOVE_NOTE',
|
||||
note: note
|
||||
})
|
||||
}
|
||||
ee.once('list:moved', dispatchHandler)
|
||||
ee.emit('list:next')
|
||||
if (isTrashed) {
|
||||
let dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Confirm note deletion',
|
||||
detail: 'This will permanently remove this note.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
if (dialogueButtonIndex === 1) return
|
||||
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)
|
||||
})
|
||||
} else {
|
||||
note.isTrashed = true
|
||||
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
ee.emit('list:next')
|
||||
}
|
||||
|
||||
handleDeleteCancelButtonClick (e) {
|
||||
handleUndoButtonClick (e) {
|
||||
let { note } = this.state
|
||||
|
||||
note.isTrashed = false
|
||||
|
||||
this.setState({
|
||||
isDeleting: false
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
this.refs.content.reload()
|
||||
ee.emit('list:next')
|
||||
})
|
||||
}
|
||||
|
||||
handleFullScreenButton (e) {
|
||||
ee.emit('editor:fullscreen')
|
||||
}
|
||||
|
||||
handleLockButtonMouseDown (e) {
|
||||
e.preventDefault()
|
||||
ee.emit('editor:lock')
|
||||
this.setState({ isLocked: !this.state.isLocked })
|
||||
if (this.state.isLocked) this.focus()
|
||||
}
|
||||
|
||||
getToggleLockButton () {
|
||||
return this.state.isLocked ? '../resources/icon/icon-lock.svg' : '../resources/icon/icon-unlock.svg'
|
||||
}
|
||||
|
||||
handleDeleteKeyDown (e) {
|
||||
if (e.keyCode === 27) this.handleDeleteCancelButtonClick(e)
|
||||
}
|
||||
|
||||
handleToggleLockButton (event, noteStatus) {
|
||||
// first argument event is not used
|
||||
if (this.props.config.editor.switchPreview === 'BLUR' && noteStatus === 'CODE') {
|
||||
this.setState({isLockButtonShown: true})
|
||||
} else {
|
||||
this.setState({isLockButtonShown: false})
|
||||
}
|
||||
}
|
||||
|
||||
handleFocus (e) {
|
||||
this.focus()
|
||||
}
|
||||
|
||||
handleInfoButtonClick (e) {
|
||||
const infoPanel = document.querySelector('.infoPanel')
|
||||
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
|
||||
}
|
||||
|
||||
print (e) {
|
||||
ee.emit('print')
|
||||
}
|
||||
|
||||
render () {
|
||||
let { storages, config } = this.props
|
||||
let { data, config, location } = this.props
|
||||
let { note } = this.state
|
||||
let storageKey = note.storage
|
||||
let folderKey = note.folder
|
||||
|
||||
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]
|
||||
|
||||
const trashTopBar = <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<i styleName='undo-button'
|
||||
className='fa fa-undo fa-fw'
|
||||
onClick={(e) => this.handleUndoButtonClick(e)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
<InfoButton
|
||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
||||
/>
|
||||
<InfoPanelTrashed
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.exportAsMd}
|
||||
exportAsTxt={this.exportAsTxt}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const detailTopBar = <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<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)}
|
||||
/>
|
||||
<TodoListPercentage
|
||||
percentageOfTodo={getTodoPercentageOfCompleted(note.content)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<InfoButton
|
||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
||||
/>
|
||||
|
||||
<StarButton
|
||||
onClick={(e) => this.handleStarButtonClick(e)}
|
||||
isActive={note.isStarred}
|
||||
/>
|
||||
|
||||
{(() => {
|
||||
const imgSrc = `${this.getToggleLockButton()}`
|
||||
const lockButtonComponent =
|
||||
<button styleName='control-lockButton'
|
||||
onFocus={(e) => this.handleFocus(e)}
|
||||
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
|
||||
>
|
||||
<img styleName='iconInfo' src={imgSrc} />
|
||||
</button>
|
||||
|
||||
return (
|
||||
this.state.isLockButtonShown ? lockButtonComponent : ''
|
||||
)
|
||||
})()}
|
||||
|
||||
<button styleName='control-fullScreenButton'
|
||||
onMouseDown={(e) => this.handleFullScreenButton(e)}
|
||||
>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-sidebar.svg' />
|
||||
</button>
|
||||
|
||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
|
||||
<InfoPanel
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
noteLink={`[${note.title}](${location.query.key})`}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.exportAsMd}
|
||||
exportAsTxt={this.exportAsTxt}
|
||||
wordCount={note.content.split(' ').length}
|
||||
letterCount={note.content.replace(/\r?\n/g, '').length}
|
||||
type={note.type}
|
||||
print={this.print}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div className='NoteDetail'
|
||||
style={this.props.style}
|
||||
styleName='root'
|
||||
>
|
||||
{this.state.isDeleting
|
||||
? <div styleName='info'>
|
||||
<div styleName='info-delete'>
|
||||
|
||||
<span styleName='info-delete-message'>
|
||||
Are you sure to delete this note?
|
||||
</span>
|
||||
<button styleName='info-delete-cancelButton'
|
||||
onClick={(e) => this.handleDeleteCancelButtonClick(e)}
|
||||
>Cancel</button>
|
||||
<button styleName='info-delete-confirmButton'
|
||||
onClick={(e) => this.handleDeleteConfirmButtonClick(e)}
|
||||
>Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
: <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<div styleName='info-left-top'>
|
||||
<FolderSelect styleName='info-left-top-folderSelect'
|
||||
value={this.state.note.storage + '-' + this.state.note.folder}
|
||||
ref='folder'
|
||||
storages={storages}
|
||||
onChange={(e) => this.handleFolderChange(e)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-left-bottom'>
|
||||
<TagSelect
|
||||
styleName='info-left-bottom-tagSelect'
|
||||
ref='tags'
|
||||
value={this.state.note.tags}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<StarButton styleName='info-right-button'
|
||||
onClick={(e) => this.handleStarButtonClick(e)}
|
||||
isActive={note.isStarred}
|
||||
/>
|
||||
<button styleName='info-right-button'
|
||||
onClick={(e) => this.handleShareButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-share-alt fa-fw'/>
|
||||
</button>
|
||||
<button styleName='info-right-button'
|
||||
onClick={(e) => this.handleContextButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-ellipsis-v'/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
||||
|
||||
<div styleName='body'>
|
||||
<MarkdownEditor
|
||||
ref='content'
|
||||
styleName='body-noteEditor'
|
||||
config={config}
|
||||
value={this.state.note.content}
|
||||
storageKey={this.state.note.storage}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<StatusBar
|
||||
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
||||
date={note.updatedAt}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,90 +1,56 @@
|
||||
$info-height = 75px
|
||||
@import('NoteDetailInfo')
|
||||
@import('DetailVars')
|
||||
|
||||
.root
|
||||
absolute top bottom right
|
||||
border-width 0 0 1px
|
||||
border-style solid
|
||||
border-color $ui-borderColor
|
||||
absolute top right bottom
|
||||
border-left 1px solid alpha(#DEDEDE, 60%)
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
box-shadow none
|
||||
padding 20px 40px
|
||||
|
||||
.info
|
||||
absolute top left right
|
||||
height $info-height
|
||||
border-bottom $ui-border
|
||||
background-color $ui-backgroundColor
|
||||
.lock-button
|
||||
padding-bottom 3px
|
||||
|
||||
.info-delete
|
||||
height 80px
|
||||
clearfix()
|
||||
.control-lockButton
|
||||
top 160px
|
||||
margin-bottom 10px
|
||||
topBarButtonLight()
|
||||
|
||||
.info-delete-message
|
||||
height 80px
|
||||
line-height 80px
|
||||
padding 0 25px
|
||||
float left
|
||||
.trashed-infopanel
|
||||
top 40px
|
||||
position relative
|
||||
|
||||
.info-delete-confirmButton
|
||||
float right
|
||||
margin 25px 5px 0
|
||||
height 30px
|
||||
padding 0 25px
|
||||
border-radius 2px
|
||||
border none
|
||||
color $ui-text-color
|
||||
colorDangerButton()
|
||||
|
||||
.info-delete-cancelButton
|
||||
float right
|
||||
height 30px
|
||||
margin 25px 5px 0
|
||||
padding 0 25px
|
||||
border $ui-border
|
||||
border-radius 2px
|
||||
color $ui-text-color
|
||||
colorDefaultButton()
|
||||
|
||||
.info-left
|
||||
float left
|
||||
padding 0 5px
|
||||
|
||||
.info-left-top
|
||||
height 40px
|
||||
line-height 40px
|
||||
|
||||
.info-left-top-folderSelect
|
||||
display inline-block
|
||||
height 34px
|
||||
width 200px
|
||||
vertical-align middle
|
||||
|
||||
.info-left-bottom
|
||||
height 30px
|
||||
|
||||
.info-left-bottom-tagSelect
|
||||
height 30px
|
||||
line-height 30px
|
||||
|
||||
.info-right
|
||||
float right
|
||||
|
||||
.info-right-button
|
||||
width 34px
|
||||
height 34px
|
||||
border-radius 17px
|
||||
navButtonColor()
|
||||
border $ui-border
|
||||
font-size 14px
|
||||
margin 8px 2px
|
||||
padding 0
|
||||
&:active
|
||||
border-color $ui-button--focus-borderColor
|
||||
&:hover .left-control-newPostButton-tooltip
|
||||
display block
|
||||
&:focus
|
||||
border-color $ui-button--focus-borderColor
|
||||
.control-fullScreenButton
|
||||
top 80px
|
||||
topBarButtonLight()
|
||||
|
||||
.body
|
||||
absolute bottom left right
|
||||
top $info-height
|
||||
|
||||
absolute left right
|
||||
left 0
|
||||
right 0
|
||||
top $info-height + $info-margin-under-border
|
||||
bottom $statusBar-height
|
||||
max-width 600px
|
||||
margin 0 auto
|
||||
.body-noteEditor
|
||||
absolute top bottom left right
|
||||
|
||||
body[data-theme="white"]
|
||||
.root
|
||||
box-shadow $note-detail-box-shadow
|
||||
border none
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
box-shadow none
|
||||
border none
|
||||
|
||||
.control-lockButton
|
||||
topBarButtonDark()
|
||||
|
||||
.control-lockButton-tooltip
|
||||
darkTooltip()
|
||||
|
||||
.control-fullScreenButton
|
||||
topBarButtonDark()
|
||||
|
||||
98
browser/main/Detail/NoteDetailInfo.styl
Normal file
98
browser/main/Detail/NoteDetailInfo.styl
Normal file
@@ -0,0 +1,98 @@
|
||||
@import('DetailVars')
|
||||
|
||||
$info-height = 50px
|
||||
$info-margin-under-border = 30px
|
||||
|
||||
.info
|
||||
absolute top left right
|
||||
left 0
|
||||
right 0
|
||||
height $info-height
|
||||
border-bottom 1px solid #eee
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
width 100%
|
||||
display flex
|
||||
align-items center
|
||||
|
||||
.info-left
|
||||
padding 0 10px
|
||||
width 100%
|
||||
display flex
|
||||
align-items center
|
||||
|
||||
|
||||
.info-left-top-folderSelect
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
|
||||
.info-left-button
|
||||
width 34px
|
||||
height 34px
|
||||
navButtonColor()
|
||||
color $ui-favorite-star-button-color
|
||||
font-size 14px
|
||||
line-height 0
|
||||
margin 13px 2px
|
||||
padding 0
|
||||
border-radius 17px
|
||||
&:hover .info-left-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--color
|
||||
|
||||
.info-right
|
||||
position absolute
|
||||
right 40px
|
||||
top 60px
|
||||
bottom 1px
|
||||
padding-left 30px
|
||||
z-index 101
|
||||
|
||||
.undo-button
|
||||
width 34px
|
||||
height 34px
|
||||
border-radius 17px
|
||||
font-size 14px
|
||||
margin 5px 0px
|
||||
border none
|
||||
color $ui-button-color
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
fill $ui-button-color
|
||||
background-color transparent
|
||||
cursor pointer
|
||||
&:active
|
||||
border-color $ui-button--active-backgroundColor
|
||||
&:hover
|
||||
background-color alpha($ui-button--hover-backgroundColor, 60%)
|
||||
transition 0.2s
|
||||
.control-lockButton-tooltip
|
||||
opacity 1
|
||||
|
||||
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
|
||||
|
||||
.undo-button
|
||||
topBarButtonDark()
|
||||
19
browser/main/Detail/PermanentDeleteButton.js
Normal file
19
browser/main/Detail/PermanentDeleteButton.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './TrashButton.styl'
|
||||
|
||||
const PermanentDeleteButton = ({
|
||||
onClick
|
||||
}) => (
|
||||
<button styleName='control-trashButton--in-trash'
|
||||
onClick={(e) => onClick(e)}
|
||||
>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
||||
</button>
|
||||
)
|
||||
|
||||
PermanentDeleteButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(PermanentDeleteButton, styles)
|
||||
@@ -2,40 +2,65 @@ 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 modes from 'browser/lib/modes'
|
||||
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'
|
||||
import { findNoteTitle } from 'browser/lib/findNoteTitle'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import TrashButton from './TrashButton'
|
||||
import PermanentDeleteButton from './PermanentDeleteButton'
|
||||
import InfoButton from './InfoButton'
|
||||
import InfoPanel from './InfoPanel'
|
||||
import InfoPanelTrashed from './InfoPanelTrashed'
|
||||
import { formatDate } from 'browser/lib/date-formatter'
|
||||
|
||||
function pass (name) {
|
||||
switch (name) {
|
||||
case 'ejs':
|
||||
return 'Embedded Javascript'
|
||||
case 'html_ruby':
|
||||
return 'Embedded Ruby'
|
||||
case 'objectivec':
|
||||
return 'Objective C'
|
||||
case 'text':
|
||||
return 'Plain Text'
|
||||
default:
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
const Menu = remote.Menu
|
||||
const MenuItem = remote.MenuItem
|
||||
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))
|
||||
}),
|
||||
isDeleting: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.refs.description.focus()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.note.key !== this.props.note.key) {
|
||||
if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
let nextNote = Object.assign({
|
||||
description: ''
|
||||
}, nextProps.note, {
|
||||
@@ -43,53 +68,28 @@ class SnippetNoteDetail extends React.Component {
|
||||
})
|
||||
this.setState({
|
||||
snippetIndex: 0,
|
||||
note: nextNote,
|
||||
isDeleting: false
|
||||
note: nextNote
|
||||
}, () => {
|
||||
let { snippets } = this.state.note
|
||||
snippets.forEach((snippet, index) => {
|
||||
this.refs['code-' + index].reload()
|
||||
})
|
||||
this.refs.tags.reset()
|
||||
if (this.refs.tags) this.refs.tags.reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
componentWillUnmount () {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
let { note } = this.state
|
||||
|
||||
note.tags = this.refs.tags.value
|
||||
if (this.refs.tags) note.tags = this.refs.tags.value
|
||||
note.description = this.refs.description.value
|
||||
note.updatedAt = new Date()
|
||||
note.title = this.findTitle(note.description)
|
||||
note.title = findNoteTitle(note.description)
|
||||
|
||||
this.setState({
|
||||
note
|
||||
@@ -99,15 +99,26 @@ class SnippetNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
save () {
|
||||
let { note, dispatch } = this.props
|
||||
clearTimeout(this.saveQueue)
|
||||
this.saveQueue = setTimeout(() => {
|
||||
this.saveNow()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: this.state.note
|
||||
})
|
||||
saveNow () {
|
||||
let { note, dispatch } = this.props
|
||||
clearTimeout(this.saveQueue)
|
||||
this.saveQueue = null
|
||||
|
||||
dataApi
|
||||
.updateNote(note.storage, note.folder, note.key, this.state.note)
|
||||
.updateNote(note.storage, note.key, this.state.note)
|
||||
.then((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
|
||||
})
|
||||
}
|
||||
|
||||
handleFolderChange (e) {
|
||||
@@ -118,7 +129,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
let newFolderKey = splitted.shift()
|
||||
|
||||
dataApi
|
||||
.moveNote(note.storage, note.folder, note.key, newStorageKey, newFolderKey)
|
||||
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
|
||||
.then((newNote) => {
|
||||
this.setState({
|
||||
isMovingNote: true,
|
||||
@@ -127,13 +138,13 @@ class SnippetNoteDetail extends React.Component {
|
||||
let { dispatch, location } = this.props
|
||||
dispatch({
|
||||
type: 'MOVE_NOTE',
|
||||
note: note,
|
||||
newNote: newNote
|
||||
originNote: note,
|
||||
note: newNote
|
||||
})
|
||||
hashHistory.replace({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: newNote.uniqueKey
|
||||
key: newNote.storage + '-' + newNote.key
|
||||
}
|
||||
})
|
||||
this.setState({
|
||||
@@ -145,6 +156,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
|
||||
handleStarButtonClick (e) {
|
||||
let { note } = this.state
|
||||
if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
|
||||
|
||||
note.isStarred = !note.isStarred
|
||||
|
||||
@@ -159,131 +171,136 @@ class SnippetNoteDetail extends React.Component {
|
||||
|
||||
}
|
||||
|
||||
handleShareButtonClick (e) {
|
||||
let menu = new Menu()
|
||||
menu.append(new MenuItem({
|
||||
label: 'Export as a File',
|
||||
disabled: true,
|
||||
click: (e) => this.handlePreferencesButtonClick(e)
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
label: 'Export to Web',
|
||||
disabled: true,
|
||||
click: (e) => this.handlePreferencesButtonClick(e)
|
||||
}))
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
}
|
||||
|
||||
handleContextButtonClick (e) {
|
||||
let menu = new Menu()
|
||||
menu.append(new MenuItem({
|
||||
label: 'Delete',
|
||||
click: (e) => this.handleDeleteMenuClick(e)
|
||||
}))
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
}
|
||||
|
||||
handleDeleteMenuClick (e) {
|
||||
this.setState({
|
||||
isDeleting: true
|
||||
})
|
||||
}
|
||||
|
||||
handleDeleteConfirmButtonClick (e) {
|
||||
let { note, dispatch } = this.props
|
||||
dataApi
|
||||
.removeNote(note.storage, note.folder, note.key)
|
||||
.then(() => {
|
||||
let dispatchHandler = () => {
|
||||
dispatch({
|
||||
type: 'REMOVE_NOTE',
|
||||
note: note
|
||||
})
|
||||
}
|
||||
ee.once('list:moved', dispatchHandler)
|
||||
ee.emit('list:next')
|
||||
})
|
||||
}
|
||||
|
||||
handleDeleteCancelButtonClick (e) {
|
||||
this.setState({
|
||||
isDeleting: false
|
||||
})
|
||||
}
|
||||
|
||||
handleTabPlusButtonClick (e) {
|
||||
handleTrashButtonClick (e) {
|
||||
let { note } = this.state
|
||||
const { isTrashed } = note
|
||||
|
||||
note.snippets = note.snippets.concat([{
|
||||
name: '',
|
||||
mode: 'text',
|
||||
content: ''
|
||||
}])
|
||||
|
||||
this.setState({
|
||||
note
|
||||
})
|
||||
}
|
||||
|
||||
handleTabButtonClick (index) {
|
||||
return (e) => {
|
||||
this.setState({
|
||||
snippetIndex: index
|
||||
if (isTrashed) {
|
||||
let dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Confirm note deletion',
|
||||
detail: 'This will permanently remove this note.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleTabDeleteButtonClick (index) {
|
||||
return (e) => {
|
||||
if (this.state.note.snippets.length > 1) {
|
||||
let snippets = this.state.note.snippets.slice()
|
||||
snippets.splice(index, 1)
|
||||
this.state.note.snippets = snippets
|
||||
this.setState({
|
||||
note: this.state.note
|
||||
if (dialogueButtonIndex === 1) return
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleNameInputChange (index) {
|
||||
return (e) => {
|
||||
let snippets = this.state.note.snippets.slice()
|
||||
snippets[index].name = e.target.value
|
||||
this.state.note.snippets = snippets
|
||||
} else {
|
||||
note.isTrashed = true
|
||||
|
||||
this.setState({
|
||||
note: this.state.note
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
ee.emit('list:next')
|
||||
}
|
||||
|
||||
handleModeButtonClick (index) {
|
||||
return (e) => {
|
||||
let menu = new Menu()
|
||||
modes.forEach((mode) => {
|
||||
menu.append(new MenuItem({
|
||||
label: mode.label,
|
||||
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
|
||||
}))
|
||||
})
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
handleUndoButtonClick (e) {
|
||||
let { note } = this.state
|
||||
|
||||
note.isTrashed = false
|
||||
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
ee.emit('list:next')
|
||||
})
|
||||
}
|
||||
|
||||
handleFullScreenButton (e) {
|
||||
ee.emit('editor:fullscreen')
|
||||
}
|
||||
|
||||
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) {
|
||||
const snippets = this.state.note.snippets.slice()
|
||||
snippets.splice(index, 1)
|
||||
const note = Object.assign({}, this.state.note, {snippets})
|
||||
const snippetIndex = this.state.snippetIndex >= snippets.length
|
||||
? snippets.length - 1
|
||||
: this.state.snippetIndex
|
||||
this.setState({ note, snippetIndex }, () => {
|
||||
this.save()
|
||||
this.refs['code-' + this.state.snippetIndex].reload()
|
||||
})
|
||||
}
|
||||
|
||||
renameSnippetByIndex (index, name) {
|
||||
const snippets = this.state.note.snippets.slice()
|
||||
snippets[index].name = name
|
||||
const syntax = CodeMirror.findModeByFileName(name.trim())
|
||||
const mode = syntax != null ? syntax.name : null
|
||||
if (mode != null) {
|
||||
snippets[index].mode = mode
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('SNIPPET_LANG', {
|
||||
name: mode
|
||||
})
|
||||
}
|
||||
this.setState({note: Object.assign(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: Object.assign(this.state.note, {snippets: snippets})})
|
||||
|
||||
this.setState({
|
||||
note: this.state.note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('SELECT_LANG', {
|
||||
name
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +308,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
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: Object.assign(this.state.note, {snippets: snippets})})
|
||||
this.setState({
|
||||
note: this.state.note
|
||||
}, () => {
|
||||
@@ -300,10 +317,182 @@ class SnippetNoteDetail extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { storages, config } = this.props
|
||||
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()
|
||||
}
|
||||
|
||||
handleInfoButtonClick (e) {
|
||||
const infoPanel = document.querySelector('.infoPanel')
|
||||
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
|
||||
}
|
||||
|
||||
showWarning () {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Sorry!',
|
||||
detail: 'md/text import is available only a markdown note.',
|
||||
buttons: ['OK']
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
let { data, config, location } = this.props
|
||||
let { note } = this.state
|
||||
|
||||
let storageKey = note.storage
|
||||
let folderKey = note.folder
|
||||
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||
@@ -311,132 +500,145 @@ class SnippetNoteDetail extends React.Component {
|
||||
|
||||
let tabList = note.snippets.map((snippet, index) => {
|
||||
let isActive = this.state.snippetIndex === index
|
||||
return <div styleName={isActive
|
||||
? 'tabList-item--active'
|
||||
: 'tabList-item'
|
||||
}
|
||||
|
||||
return <SnippetTab
|
||||
key={index}
|
||||
>
|
||||
<button styleName='tabList-item-button'
|
||||
onClick={(e) => this.handleTabButtonClick(index)(e)}
|
||||
>
|
||||
{snippet.name.trim().length > 0
|
||||
? snippet.name
|
||||
: <span styleName='tabList-item-unnamed'>
|
||||
Unnamed
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
{note.snippets.length > 1 &&
|
||||
<button styleName='tabList-item-deleteButton'
|
||||
onClick={(e) => this.handleTabDeleteButtonClick(index)(e)}
|
||||
>
|
||||
<i className='fa fa-times'/>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
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 mode = snippet.mode === 'text'
|
||||
? null
|
||||
: modes.filter((mode) => mode.name === snippet.mode)[0]
|
||||
|
||||
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}}
|
||||
>
|
||||
<div styleName='tabView-top'>
|
||||
<input styleName='tabView-top-name'
|
||||
placeholder='Filename including extensions...'
|
||||
value={snippet.name}
|
||||
onChange={(e) => this.handleNameInputChange(index)(e)}
|
||||
{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}
|
||||
storageKey={storageKey}
|
||||
/>
|
||||
<button styleName='tabView-top-mode'
|
||||
onClick={(e) => this.handleModeButtonClick(index)(e)}
|
||||
>
|
||||
{mode == null
|
||||
? 'Select Syntax...'
|
||||
: mode.label
|
||||
}
|
||||
<i className='fa fa-caret-down'/>
|
||||
</button>
|
||||
</div>
|
||||
<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}
|
||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||
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}
|
||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||
ref={'code-' + index}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
})
|
||||
|
||||
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]
|
||||
|
||||
const trashTopBar = <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<i styleName='undo-button'
|
||||
className='fa fa-undo fa-fw'
|
||||
onClick={(e) => this.handleUndoButtonClick(e)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
<InfoButton
|
||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
||||
/>
|
||||
<InfoPanelTrashed
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.showWarning}
|
||||
exportAsTxt={this.showWarning}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const detailTopBar = <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<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'>
|
||||
<InfoButton
|
||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
||||
/>
|
||||
|
||||
<StarButton
|
||||
onClick={(e) => this.handleStarButtonClick(e)}
|
||||
isActive={note.isStarred}
|
||||
/>
|
||||
|
||||
<button styleName='control-fullScreenButton'
|
||||
onMouseDown={(e) => this.handleFullScreenButton(e)}>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-sidebar.svg' />
|
||||
</button>
|
||||
|
||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
<InfoPanel
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
noteLink={`[${note.title}](${location.query.key})`}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.showWarning}
|
||||
exportAsTxt={this.showWarning}
|
||||
type={note.type}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div className='NoteDetail'
|
||||
style={this.props.style}
|
||||
styleName='root'
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
>
|
||||
{this.state.isDeleting
|
||||
? <div styleName='info'>
|
||||
<div styleName='info-delete'>
|
||||
|
||||
<span styleName='info-delete-message'>
|
||||
Are you sure to delete this note?
|
||||
</span>
|
||||
<button styleName='info-delete-cancelButton'
|
||||
onClick={(e) => this.handleDeleteCancelButtonClick(e)}
|
||||
>Cancel</button>
|
||||
<button styleName='info-delete-confirmButton'
|
||||
onClick={(e) => this.handleDeleteConfirmButtonClick(e)}
|
||||
>Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
: <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<div styleName='info-left-top'>
|
||||
<FolderSelect styleName='info-left-top-folderSelect'
|
||||
value={this.state.note.storage + '-' + this.state.note.folder}
|
||||
ref='folder'
|
||||
storages={storages}
|
||||
onChange={(e) => this.handleFolderChange(e)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-left-bottom'>
|
||||
<TagSelect
|
||||
styleName='info-left-bottom-tagSelect'
|
||||
ref='tags'
|
||||
value={this.state.note.tags}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<StarButton styleName='info-right-button'
|
||||
onClick={(e) => this.handleStarButtonClick(e)}
|
||||
isActive={note.isStarred}
|
||||
/>
|
||||
<button styleName='info-right-button'
|
||||
onClick={(e) => this.handleShareButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-share-alt fa-fw'/>
|
||||
</button>
|
||||
<button styleName='info-right-button'
|
||||
onClick={(e) => this.handleContextButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-ellipsis-v'/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
||||
|
||||
<div styleName='body'>
|
||||
<div styleName='body-description'>
|
||||
<textarea styleName='body-description-textarea'
|
||||
<div styleName='description'>
|
||||
<textarea
|
||||
style={{
|
||||
fontFamily: config.preview.fontFamily,
|
||||
fontSize: parseInt(config.preview.fontSize, 10)
|
||||
@@ -448,15 +650,46 @@ class SnippetNoteDetail extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
<div styleName='tabList'>
|
||||
{tabList}
|
||||
<button styleName='tabList-plusButton'
|
||||
<div styleName='list'>
|
||||
{tabList}
|
||||
</div>
|
||||
<button styleName='plusButton'
|
||||
onClick={(e) => this.handleTabPlusButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-plus'/>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,170 +1,111 @@
|
||||
$info-height = 75px
|
||||
@import('NoteDetailInfo')
|
||||
@import('DetailVars')
|
||||
|
||||
.root
|
||||
absolute top bottom right
|
||||
border-width 0 0 1px
|
||||
border-style solid
|
||||
border-color $ui-borderColor
|
||||
|
||||
.info
|
||||
absolute top left right
|
||||
height $info-height
|
||||
border-bottom $ui-border
|
||||
background-color $ui-backgroundColor
|
||||
|
||||
.info-delete
|
||||
height 80px
|
||||
clearfix()
|
||||
|
||||
.info-delete-message
|
||||
height 80px
|
||||
line-height 80px
|
||||
padding 0 25px
|
||||
float left
|
||||
|
||||
.info-delete-confirmButton
|
||||
float right
|
||||
margin 25px 5px 0
|
||||
height 30px
|
||||
padding 0 25px
|
||||
border-radius 2px
|
||||
border none
|
||||
color $ui-text-color
|
||||
colorDangerButton()
|
||||
|
||||
.info-delete-cancelButton
|
||||
float right
|
||||
height 30px
|
||||
margin 25px 5px 0
|
||||
padding 0 25px
|
||||
border $ui-border
|
||||
border-radius 2px
|
||||
color $ui-text-color
|
||||
colorDefaultButton()
|
||||
|
||||
.info-left
|
||||
float left
|
||||
padding 0 5px
|
||||
|
||||
.info-left-top
|
||||
height 40px
|
||||
line-height 40px
|
||||
|
||||
.info-left-top-folderSelect
|
||||
display inline-block
|
||||
height 34px
|
||||
width 200px
|
||||
vertical-align middle
|
||||
|
||||
.info-left-bottom
|
||||
height 30px
|
||||
|
||||
.info-left-bottom-tagSelect
|
||||
height 30px
|
||||
line-height 30px
|
||||
|
||||
.info-right
|
||||
float right
|
||||
|
||||
.info-right-button
|
||||
width 34px
|
||||
height 34px
|
||||
border-radius 17px
|
||||
navButtonColor()
|
||||
border $ui-border
|
||||
font-size 14px
|
||||
margin 8px 2px
|
||||
padding 0
|
||||
&:active
|
||||
border-color $ui-button--focus-borderColor
|
||||
&:hover .left-control-newPostButton-tooltip
|
||||
display block
|
||||
&:focus
|
||||
border-color $ui-button--focus-borderColor
|
||||
border-left 1px solid alpha(#DEDEDE, 60%)
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
box-shadow none
|
||||
|
||||
.body
|
||||
absolute bottom left right
|
||||
top $info-height
|
||||
absolute left right
|
||||
left $snippet-note-detail-left-margin
|
||||
right $snippet-note-detail-right-margin
|
||||
top $info-height + $info-margin-under-border
|
||||
bottom $statusBar-height
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
.body-description
|
||||
.body .description
|
||||
absolute top left right
|
||||
height 80px
|
||||
border-bottom $ui-border
|
||||
height 50px
|
||||
|
||||
.body-description-textarea
|
||||
.body .description textarea
|
||||
outline none
|
||||
display block
|
||||
height 100%
|
||||
width 100%
|
||||
resize none
|
||||
border none
|
||||
padding 10px
|
||||
border 1px solid $ui-borderColor
|
||||
padding 2px 5px
|
||||
line-height 1.6
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
.tabList
|
||||
absolute left right
|
||||
top 80px
|
||||
top 55px
|
||||
height 30px
|
||||
border-bottom $ui-border
|
||||
display flex
|
||||
background-color $ui-backgroundColor
|
||||
.tabList-item
|
||||
position relative
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
.tabList .list
|
||||
flex 1
|
||||
border-right $ui-border
|
||||
&:hover
|
||||
.tabList-item-deleteButton
|
||||
color $ui-inactive-text-color
|
||||
&:hover
|
||||
background-color darken($ui-backgroundColor, 15%)
|
||||
&:active
|
||||
color white
|
||||
background-color $ui-active-color
|
||||
.tabList-item--active
|
||||
@extend .tabList-item
|
||||
.tabList-item-button
|
||||
border-color $brand-color
|
||||
.tabList-item-button
|
||||
width 100%
|
||||
height 100%
|
||||
navButtonColor()
|
||||
border-left 4px solid transparent
|
||||
.tabList-item-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
|
||||
.tabList-plusButton
|
||||
navButtonColor()
|
||||
display flex
|
||||
overflow hidden
|
||||
|
||||
.tabList .plusButton
|
||||
navWhiteButtonColor()
|
||||
width 30px
|
||||
|
||||
.tabView
|
||||
absolute left right bottom
|
||||
top 110px
|
||||
.tabView-top
|
||||
absolute top left right
|
||||
height 30px
|
||||
border-bottom $ui-border
|
||||
display flex
|
||||
.tabView-top-name
|
||||
flex 1
|
||||
border none
|
||||
padding 0 10px
|
||||
font-size 14px
|
||||
.tabView-top-mode
|
||||
width 110px
|
||||
padding 0
|
||||
border none
|
||||
border-left $ui-border
|
||||
colorDefaultButton()
|
||||
color $ui-inactive-text-color
|
||||
&:hover
|
||||
color $ui-text-color
|
||||
top 100px
|
||||
|
||||
.tabView-content
|
||||
absolute left right bottom
|
||||
top 30px
|
||||
absolute top left right bottom
|
||||
|
||||
.override
|
||||
absolute bottom left
|
||||
bottom 1px
|
||||
left 60px
|
||||
z-index 101
|
||||
button
|
||||
navButtonColor()
|
||||
padding 0 6px
|
||||
&:hover
|
||||
color $ui-active-color
|
||||
&:active .update-icon
|
||||
color $ui-active-color
|
||||
|
||||
.control-fullScreenButton
|
||||
top 80px
|
||||
margin-bottom 10px
|
||||
topBarButtonLight()
|
||||
|
||||
body[data-theme="white"]
|
||||
.root
|
||||
box-shadow $note-detail-box-shadow
|
||||
border none
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
border none
|
||||
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 $ui-dark-text-color
|
||||
border 1px solid $ui-dark-borderColor
|
||||
|
||||
.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
|
||||
&:hover
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
transition 0.15s
|
||||
color $ui-dark-text-color
|
||||
|
||||
.control-fullScreenButton
|
||||
topBarButtonDark()
|
||||
|
||||
@@ -47,10 +47,11 @@ class StarButton extends React.Component {
|
||||
onMouseLeave={(e) => this.handleMouseLeave(e)}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
<i className={this.state.isActive || this.props.isActive
|
||||
? 'fa fa-star'
|
||||
: 'fa fa-star-o'
|
||||
}
|
||||
<img styleName='icon'
|
||||
src={this.state.isActive || this.props.isActive
|
||||
? '../resources/icon/icon-starred.svg'
|
||||
: '../resources/icon/icon-star.svg'
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
.root
|
||||
position relative
|
||||
padding 0
|
||||
transition transform 0.15s
|
||||
top 45px
|
||||
topBarButtonLight()
|
||||
&:hover
|
||||
transform rotate(-72deg)
|
||||
transition 0.2s
|
||||
color alpha($ui-favorite-star-button-color, 0.6)
|
||||
|
||||
.root--active
|
||||
@extend .root
|
||||
color $ui-active-color
|
||||
transform rotate(-72deg)
|
||||
transition 0.15s
|
||||
color $ui-favorite-star-button-color
|
||||
&:hover
|
||||
transition 0.2s
|
||||
color alpha($ui-favorite-star-button-color, 0.6)
|
||||
|
||||
.icon
|
||||
transition transform 0.15s
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
topBarButtonDark()
|
||||
&:hover
|
||||
transition 0.2s
|
||||
color alpha($ui-favorite-star-button-color, 0.6)
|
||||
@@ -2,6 +2,7 @@ import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './TagSelect.styl'
|
||||
import _ from 'lodash'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
|
||||
class TagSelect extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -22,6 +23,10 @@ class TagSelect extends React.Component {
|
||||
|
||||
handleNewTagInputKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
case 9:
|
||||
e.preventDefault()
|
||||
this.submitTag()
|
||||
break
|
||||
case 13:
|
||||
this.submitTag()
|
||||
break
|
||||
@@ -52,8 +57,9 @@ class TagSelect extends React.Component {
|
||||
}
|
||||
|
||||
submitTag () {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
|
||||
let { value } = this.props
|
||||
let newTag = this.refs.newTag.value.trim()
|
||||
let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
|
||||
|
||||
if (newTag.length <= 0) {
|
||||
this.setState({
|
||||
@@ -103,12 +109,12 @@ class TagSelect extends React.Component {
|
||||
<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'/>
|
||||
<img className='tag-removeButton-icon' src='../resources/icon/icon-x.svg' width='8px' />
|
||||
</button>
|
||||
<span styleName='tag-label'>{tag}</span>
|
||||
</span>
|
||||
)
|
||||
})
|
||||
@@ -121,10 +127,7 @@ class TagSelect extends React.Component {
|
||||
}
|
||||
styleName='root'
|
||||
>
|
||||
<i styleName='icon'
|
||||
className='fa fa-tags'
|
||||
/>
|
||||
{tagList}
|
||||
{tagList}
|
||||
<input styleName='newTag'
|
||||
ref='newTag'
|
||||
value={this.state.newTag}
|
||||
@@ -132,7 +135,6 @@ class TagSelect extends React.Component {
|
||||
onChange={(e) => this.handleNewTagInputChange(e)}
|
||||
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
|
||||
/>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,63 +1,66 @@
|
||||
.root
|
||||
position relative
|
||||
display flex
|
||||
align-items center
|
||||
user-select none
|
||||
|
||||
.icon
|
||||
display inline-block
|
||||
width 30px
|
||||
vertical-align middle
|
||||
text-align center
|
||||
color $ui-button-color
|
||||
width 100%
|
||||
overflow-x scroll
|
||||
white-space nowrap
|
||||
|
||||
.root::-webkit-scrollbar
|
||||
display none
|
||||
|
||||
.tag
|
||||
display inline-block
|
||||
margin 0 2px
|
||||
vertical-align middle
|
||||
height 20px
|
||||
background-color white
|
||||
border-radius 3px
|
||||
overflow hidden
|
||||
display flex
|
||||
align-items center
|
||||
margin 0px 2px
|
||||
padding 2px 4px
|
||||
background-color alpha($ui-tag-backgroundColor, 3%)
|
||||
border-radius 4px
|
||||
position relative
|
||||
clearfix()
|
||||
|
||||
.tag-removeButton
|
||||
float left
|
||||
height 20px
|
||||
width 18px
|
||||
margin 0
|
||||
padding 0
|
||||
border-style solid
|
||||
border-color $ui-borderColor
|
||||
border-width 0 0 0 3px
|
||||
line-height 18px
|
||||
border-width 0
|
||||
border-radius 20px
|
||||
background-color transparent
|
||||
color $ui-button-color
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
&:active, &:active:hover
|
||||
color $ui-button--active-color
|
||||
background-color $ui-button--active-backgroundColor
|
||||
border-color $ui-button--focus-borderColor
|
||||
&:focus
|
||||
border-color $ui-button--focus-borderColor
|
||||
position absolute
|
||||
right 6px
|
||||
|
||||
.tag-removeButton-icon
|
||||
width 5px
|
||||
padding-right 4px
|
||||
|
||||
.tag-label
|
||||
float left
|
||||
height 20px
|
||||
line-height 20px
|
||||
padding 0 6px
|
||||
font-size 13px
|
||||
color: $ui-text-color
|
||||
padding 4px 16px 4px 8px
|
||||
|
||||
.newTag
|
||||
display inline-block
|
||||
margin 0 2px
|
||||
vertical-align middle
|
||||
height 24px
|
||||
box-sizing borde-box
|
||||
box-sizing border-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
|
||||
font-size 13px
|
||||
|
||||
body[data-theme="dark"]
|
||||
.tag
|
||||
background-color alpha($ui-dark-tag-backgroundColor, 60%)
|
||||
|
||||
.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 none
|
||||
background-color transparent
|
||||
color $ui-dark-text-color
|
||||
19
browser/main/Detail/TrashButton.js
Normal file
19
browser/main/Detail/TrashButton.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './TrashButton.styl'
|
||||
|
||||
const TrashButton = ({
|
||||
onClick
|
||||
}) => (
|
||||
<button styleName='control-trashButton'
|
||||
onClick={(e) => onClick(e)}
|
||||
>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
||||
</button>
|
||||
)
|
||||
|
||||
TrashButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(TrashButton, styles)
|
||||
15
browser/main/Detail/TrashButton.styl
Normal file
15
browser/main/Detail/TrashButton.styl
Normal file
@@ -0,0 +1,15 @@
|
||||
.control-trashButton
|
||||
top 120px
|
||||
margin-bottom 10px
|
||||
topBarButtonLight()
|
||||
|
||||
.control-trashButton--in-trash
|
||||
top 60px
|
||||
topBarButtonLight()
|
||||
|
||||
.trashButton
|
||||
padding 0px
|
||||
|
||||
body[data-theme="dark"]
|
||||
.control-trashButton
|
||||
topBarButtonDark()
|
||||
@@ -5,6 +5,7 @@ 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'
|
||||
|
||||
@@ -16,7 +17,7 @@ class Detail extends React.Component {
|
||||
this.refs.root != null && this.refs.root.focus()
|
||||
}
|
||||
this.deleteHandler = () => {
|
||||
this.refs.root != null && this.refs.root.handleDeleteMenuClick()
|
||||
this.refs.root != null && this.refs.root.handleTrashButtonClick()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,19 +32,14 @@ class Detail extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
let { location, notes, config } = this.props
|
||||
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 folderKey = splitted.shift()
|
||||
let noteKey = splitted.shift()
|
||||
|
||||
note = _.find(notes, {
|
||||
storage: storageKey,
|
||||
folder: folderKey,
|
||||
key: noteKey
|
||||
})
|
||||
note = data.noteMap.get(storageKey + '-' + noteKey)
|
||||
}
|
||||
|
||||
if (note == null) {
|
||||
@@ -53,8 +49,11 @@ class Detail extends React.Component {
|
||||
tabIndex='0'
|
||||
>
|
||||
<div styleName='empty'>
|
||||
<div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br/>to create a new post</div>
|
||||
<div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br />to create a new note</div>
|
||||
</div>
|
||||
<StatusBar
|
||||
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -67,7 +66,7 @@ class Detail extends React.Component {
|
||||
ref='root'
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'storages',
|
||||
'data',
|
||||
'style',
|
||||
'ignorePreviewPointerEvents',
|
||||
'location'
|
||||
@@ -83,7 +82,7 @@ class Detail extends React.Component {
|
||||
ref='root'
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'storages',
|
||||
'data',
|
||||
'style',
|
||||
'ignorePreviewPointerEvents',
|
||||
'location'
|
||||
@@ -95,7 +94,6 @@ class Detail extends React.Component {
|
||||
|
||||
Detail.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
storages: PropTypes.array,
|
||||
style: PropTypes.shape({
|
||||
left: PropTypes.number
|
||||
}),
|
||||
|
||||
@@ -7,26 +7,56 @@ import TopBar from './TopBar'
|
||||
import NoteList from './NoteList'
|
||||
import Detail from './Detail'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import StatusBar from './StatusBar'
|
||||
import _ from 'lodash'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import modal from 'browser/main/lib/modal'
|
||||
import InitModal from 'browser/main/modals/InitModal'
|
||||
import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
|
||||
class Main extends React.Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
mobileAnalytics.initAwsMobileAnalytics()
|
||||
}
|
||||
|
||||
let { config } = props
|
||||
|
||||
this.state = {
|
||||
isSliderFocused: false,
|
||||
listWidth: config.listWidth
|
||||
isRightSliderFocused: false,
|
||||
listWidth: config.listWidth,
|
||||
navWidth: config.navWidth,
|
||||
isLeftSliderFocused: false,
|
||||
fullScreen: false,
|
||||
noteDetailWidth: 0,
|
||||
mainBodyWidth: 0
|
||||
}
|
||||
|
||||
this.toggleFullScreen = () => this.handleFullScreenButton()
|
||||
}
|
||||
|
||||
getChildContext () {
|
||||
let { status, config } = this.props
|
||||
|
||||
return {
|
||||
status,
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
let { dispatch } = this.props
|
||||
let { dispatch, config } = this.props
|
||||
|
||||
if (config.ui.theme === 'dark') {
|
||||
document.body.setAttribute('data-theme', 'dark')
|
||||
} else if (config.ui.theme === 'white') {
|
||||
document.body.setAttribute('data-theme', 'white')
|
||||
} else {
|
||||
document.body.setAttribute('data-theme', 'default')
|
||||
}
|
||||
|
||||
// Reload all data
|
||||
dataApi.init()
|
||||
@@ -41,19 +71,33 @@ class Main extends React.Component {
|
||||
modal.open(InitModal)
|
||||
}
|
||||
})
|
||||
|
||||
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
|
||||
}
|
||||
|
||||
handleSlideMouseDown (e) {
|
||||
componentWillUnmount () {
|
||||
eventEmitter.off('editor:fullscreen', this.toggleFullScreen)
|
||||
}
|
||||
|
||||
handleLeftSlideMouseDown (e) {
|
||||
e.preventDefault()
|
||||
this.setState({
|
||||
isSliderFocused: true
|
||||
isLeftSliderFocused: true
|
||||
})
|
||||
}
|
||||
|
||||
handleRightSlideMouseDown (e) {
|
||||
e.preventDefault()
|
||||
this.setState({
|
||||
isRightSliderFocused: true
|
||||
})
|
||||
}
|
||||
|
||||
handleMouseUp (e) {
|
||||
if (this.state.isSliderFocused) {
|
||||
// Change width of NoteList component.
|
||||
if (this.state.isRightSliderFocused) {
|
||||
this.setState({
|
||||
isSliderFocused: false
|
||||
isRightSliderFocused: false
|
||||
}, () => {
|
||||
let { dispatch } = this.props
|
||||
let newListWidth = this.state.listWidth
|
||||
@@ -65,10 +109,26 @@ class Main extends React.Component {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 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.isSliderFocused) {
|
||||
if (this.state.isRightSliderFocused) {
|
||||
let offset = this.refs.body.getBoundingClientRect().left
|
||||
let newListWidth = e.pageX - offset
|
||||
if (newListWidth < 10) {
|
||||
@@ -80,11 +140,53 @@ class Main extends React.Component {
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleFullScreenButton (e) {
|
||||
this.setState({ fullScreen: !this.state.fullScreen }, () => {
|
||||
const noteDetail = document.querySelector('.NoteDetail')
|
||||
const noteList = document.querySelector('.NoteList')
|
||||
const mainBody = document.querySelector('#main-body')
|
||||
|
||||
if (this.state.fullScreen) {
|
||||
this.hideLeftLists(noteDetail, noteList, mainBody)
|
||||
} else {
|
||||
this.showLeftLists(noteDetail, noteList, mainBody)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
hideLeftLists (noteDetail, noteList, mainBody) {
|
||||
this.setState({noteDetailWidth: noteDetail.style.left})
|
||||
this.setState({mainBodyWidth: mainBody.style.left})
|
||||
noteDetail.style.left = '0px'
|
||||
mainBody.style.left = '0px'
|
||||
noteList.style.display = 'none'
|
||||
}
|
||||
|
||||
showLeftLists (noteDetail, noteList, mainBody) {
|
||||
noteDetail.style.left = this.state.noteDetailWidth
|
||||
mainBody.style.left = this.state.mainBodyWidth
|
||||
noteList.style.display = 'inline'
|
||||
}
|
||||
|
||||
render () {
|
||||
let { config } = this.props
|
||||
|
||||
// the width of the navigation bar when it is folded/collapsed
|
||||
const foldedNavigationWidth = 44
|
||||
|
||||
return (
|
||||
<div
|
||||
className='Main'
|
||||
@@ -95,20 +197,31 @@ class Main extends React.Component {
|
||||
<SideNav
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'storages',
|
||||
'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'}
|
||||
id='main-body'
|
||||
ref='body'
|
||||
style={{left: config.isSideNavFolded ? foldedNavigationWidth : this.state.navWidth}}
|
||||
>
|
||||
<TopBar style={{width: this.state.listWidth}}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'storages',
|
||||
'config',
|
||||
'notes',
|
||||
'data',
|
||||
'params',
|
||||
'location'
|
||||
])}
|
||||
@@ -116,44 +229,46 @@ class Main extends React.Component {
|
||||
<NoteList style={{width: this.state.listWidth}}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'storages',
|
||||
'notes',
|
||||
'data',
|
||||
'config',
|
||||
'params',
|
||||
'location'
|
||||
])}
|
||||
/>
|
||||
<div styleName={this.state.isSliderFocused ? 'slider--active' : 'slider'}
|
||||
style={{left: this.state.listWidth}}
|
||||
onMouseDown={(e) => this.handleSlideMouseDown(e)}
|
||||
<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 styleName='slider-hitbox' />
|
||||
</div>
|
||||
<Detail
|
||||
style={{left: this.state.listWidth + 1}}
|
||||
style={{left: this.state.listWidth}}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'storages',
|
||||
'notes',
|
||||
'data',
|
||||
'config',
|
||||
'params',
|
||||
'location'
|
||||
])}
|
||||
ignorePreviewPointerEvents={this.state.isSliderFocused}
|
||||
ignorePreviewPointerEvents={this.state.isRightSliderFocused}
|
||||
/>
|
||||
</div>
|
||||
<StatusBar
|
||||
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Main.childContextTypes = {
|
||||
status: PropTypes.shape({
|
||||
updateReady: PropTypes.bool.isRequired
|
||||
}).isRequired,
|
||||
config: PropTypes.shape({}).isRequired
|
||||
}
|
||||
|
||||
Main.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
repositories: PropTypes.array
|
||||
data: PropTypes.shape({}).isRequired
|
||||
}
|
||||
|
||||
export default connect((x) => x)(CSSModules(Main, styles))
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
absolute top left bottom right
|
||||
|
||||
.body
|
||||
absolute right top
|
||||
bottom $statusBar-height - 1
|
||||
absolute right top bottom
|
||||
left $sideNav-width
|
||||
|
||||
.body--expanded
|
||||
@@ -12,15 +11,20 @@
|
||||
|
||||
.slider
|
||||
absolute top bottom
|
||||
top -2px
|
||||
width 0
|
||||
z-index 0
|
||||
|
||||
.slider-right
|
||||
@extend .slider
|
||||
width 1px
|
||||
background-color $ui-borderColor
|
||||
border-width 0
|
||||
border-style solid
|
||||
border-color $ui-borderColor
|
||||
z-index 0
|
||||
|
||||
.slider--active
|
||||
@extend .slider
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.slider-right--active
|
||||
@extend .slider-right
|
||||
|
||||
.slider-hitbox
|
||||
absolute top bottom left right
|
||||
@@ -28,3 +32,12 @@
|
||||
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
|
||||
|
||||
73
browser/main/NewNoteButton/NewNoteButton.styl
Normal file
73
browser/main/NewNoteButton/NewNoteButton.styl
Normal file
@@ -0,0 +1,73 @@
|
||||
.root
|
||||
position relative
|
||||
background-color $ui-noteList-backgroundColor
|
||||
height $topBar-height - 1
|
||||
margin-left: auto;
|
||||
width: 64px;
|
||||
|
||||
.root--expanded
|
||||
@extend .root
|
||||
|
||||
$control-height = 34px
|
||||
|
||||
.control
|
||||
position absolute
|
||||
top 13px
|
||||
right 7px
|
||||
height $control-height
|
||||
display flex
|
||||
|
||||
.control-newNoteButton
|
||||
display block
|
||||
width 32px
|
||||
height $control-height - 2
|
||||
navButtonColor()
|
||||
font-size 16px
|
||||
line-height 28px
|
||||
padding 0
|
||||
&:active
|
||||
border-color $ui-button--active-backgroundColor
|
||||
&:hover .control-newNoteButton-tooltip
|
||||
opacity 1
|
||||
|
||||
.control-newNoteButton-tooltip
|
||||
tooltip()
|
||||
position absolute
|
||||
pointer-events none
|
||||
top 26px
|
||||
right -43px
|
||||
width 124px
|
||||
z-index 200
|
||||
padding 5px
|
||||
line-height normal
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
body[data-theme="white"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-white-noteList-backgroundColor
|
||||
|
||||
.control-newNoteButton
|
||||
background-color $ui-white-noteList-backgroundColor
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
.control
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.control-newNoteButton
|
||||
color $ui-inactive-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
&:hover
|
||||
transition 0.15s
|
||||
color $ui-dark-text-color
|
||||
&:active
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
border-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.control-newNoteButton-tooltip
|
||||
darkTooltip()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user