mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 18:26:26 +00:00
Compare commits
513 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ab81608e8 | ||
|
|
017e40fcb9 | ||
|
|
29309fbaa3 | ||
|
|
b766b08bf5 | ||
|
|
e796e00963 | ||
|
|
10136df977 | ||
|
|
44ec107ce8 | ||
|
|
c7fb5b0475 | ||
|
|
785735ccf5 | ||
|
|
f0736ccf8d | ||
|
|
a24a7e03be | ||
|
|
be235e5204 | ||
|
|
57ec598672 | ||
|
|
2627c09cda | ||
|
|
36e63bb8a9 | ||
|
|
5ea24f650b | ||
|
|
8c792ce7a1 | ||
|
|
055969f5c6 | ||
|
|
bdb9349b52 | ||
|
|
8b11b57ec5 | ||
|
|
29888c89ad | ||
|
|
281fb2afd3 | ||
|
|
fbb7839f83 | ||
|
|
d3f9c170ac | ||
|
|
2a784deb4b | ||
|
|
ab8b6d806d | ||
|
|
6c4206a2dd | ||
|
|
0d61d9cee4 | ||
|
|
48bdb9e818 | ||
|
|
cfe447a403 | ||
|
|
2c30f0e487 | ||
|
|
147211478c | ||
|
|
35b7674e69 | ||
|
|
55075b2a96 | ||
|
|
6577599959 | ||
|
|
0c4e72e507 | ||
|
|
9bf96e943d | ||
|
|
2811843e70 | ||
|
|
842ece2a8a | ||
|
|
222de03ea3 | ||
|
|
d31ae9f3fc | ||
|
|
1dd7644e12 | ||
|
|
2cee54f70a | ||
|
|
a8431fae96 | ||
|
|
03d11b7b58 | ||
|
|
316e2eeefb | ||
|
|
c21e19337a | ||
|
|
b9cab0dae8 | ||
|
|
33b3299ca2 | ||
|
|
00ba38beba | ||
|
|
433fce286e | ||
|
|
b36322bba4 | ||
|
|
c147e0a789 | ||
|
|
8bf5d02624 | ||
|
|
35616c1ccd | ||
|
|
7c8939ecb8 | ||
|
|
ccb0302d3f | ||
|
|
0cfd048013 | ||
|
|
f72b4f0249 | ||
|
|
b5cb209f14 | ||
|
|
1af374439d | ||
|
|
bbcd674516 | ||
|
|
3cba71b7a8 | ||
|
|
6a1e9c5818 | ||
|
|
ede41d01b4 | ||
|
|
826a67b550 | ||
|
|
f3d59a9b61 | ||
|
|
65434453b8 | ||
|
|
b8658abbad | ||
|
|
0387b33f6c | ||
|
|
41042fe64e | ||
|
|
1efc3bb15d | ||
|
|
0816a9410b | ||
|
|
cb823eaaa6 | ||
|
|
44f027bf18 | ||
|
|
571d159a1f | ||
|
|
a30d977727 | ||
|
|
847ad2d781 | ||
|
|
da9067a672 | ||
|
|
5ed5950ed5 | ||
|
|
f53c182cfb | ||
|
|
bb2978b370 | ||
|
|
66ae4a5163 | ||
|
|
f435595ad8 | ||
|
|
4bec4596fe | ||
|
|
10b86aec49 | ||
|
|
ecabd37cbb | ||
|
|
f3cabfcb4d | ||
|
|
46fc010cf9 | ||
|
|
e370c76521 | ||
|
|
1c283f88d0 | ||
|
|
cfffa1b10a | ||
|
|
8aaae4de49 | ||
|
|
4f6c35713e | ||
|
|
8fb0b5b572 | ||
|
|
12262671ee | ||
|
|
3165c62985 | ||
|
|
1ca1166f74 | ||
|
|
7b4d2f3b97 | ||
|
|
ccce75a047 | ||
|
|
410e82e375 | ||
|
|
bd385ec062 | ||
|
|
638227ef25 | ||
|
|
e70b35126b | ||
|
|
4d41c7f37f | ||
|
|
d01a7b16a1 | ||
|
|
326d873279 | ||
|
|
74c32bed29 | ||
|
|
a222fd0786 | ||
|
|
c633ba9497 | ||
|
|
40b5472866 | ||
|
|
a413e273ca | ||
|
|
0f82085cae | ||
|
|
fa1ab04409 | ||
|
|
f370508c93 | ||
|
|
7472019422 | ||
|
|
aa3597881a | ||
|
|
a36841e501 | ||
|
|
fe9afc8952 | ||
|
|
63d39a81aa | ||
|
|
b11dc2ca20 | ||
|
|
10f5ad6127 | ||
|
|
4800a88cf6 | ||
|
|
9d8bc40594 | ||
|
|
7d3d96a6e0 | ||
|
|
4c99429d76 | ||
|
|
96fbd7572c | ||
|
|
b012bba6d6 | ||
|
|
45d90b2530 | ||
|
|
a8b946a07a | ||
|
|
18392c4f23 | ||
|
|
88e7869284 | ||
|
|
3c8332c448 | ||
|
|
207f95693a | ||
|
|
de15120bf8 | ||
|
|
1dcf11e1c4 | ||
|
|
b74ba22c44 | ||
|
|
fa2d34dcfc | ||
|
|
0280a5f09e | ||
|
|
a35f876f4f | ||
|
|
c22b7f81c1 | ||
|
|
9344fd78d8 | ||
|
|
e89c0a3e61 | ||
|
|
5aa7ef5738 | ||
|
|
88ac6c6fc6 | ||
|
|
a537f9762b | ||
|
|
aab192223a | ||
|
|
6439810d03 | ||
|
|
c5c78763d1 | ||
|
|
012908e32d | ||
|
|
1f1e545538 | ||
|
|
5b8519b2b8 | ||
|
|
6502158716 | ||
|
|
7c525ab7a6 | ||
|
|
778e3d9799 | ||
|
|
e5f986a959 | ||
|
|
d76ecd5423 | ||
|
|
6bcb6398f8 | ||
|
|
6c341d8fa5 | ||
|
|
0d5e57eeab | ||
|
|
aef603ed8c | ||
|
|
97600e526b | ||
|
|
f3f6095d81 | ||
|
|
fb7280127c | ||
|
|
60df509fc6 | ||
|
|
ced237fbda | ||
|
|
9473a26892 | ||
|
|
e5825e898d | ||
|
|
8d59bd9f71 | ||
|
|
8f90bb83e7 | ||
|
|
ba888a7165 | ||
|
|
9591a7cac2 | ||
|
|
e8fd53d8b7 | ||
|
|
3b7c36b1c9 | ||
|
|
2750a3ef30 | ||
|
|
7d59f51244 | ||
|
|
a3ec6f470a | ||
|
|
8e85a33dac | ||
|
|
77542597f5 | ||
|
|
2a26e8a010 | ||
|
|
8b4c1a6c38 | ||
|
|
7d7e277cc6 | ||
|
|
cb1da609a4 | ||
|
|
e4fbdb35a3 | ||
|
|
c3586372e0 | ||
|
|
808d193543 | ||
|
|
0b65b70af2 | ||
|
|
deab34abd6 | ||
|
|
646cded650 | ||
|
|
9047320301 | ||
|
|
f16b054f01 | ||
|
|
fea856202d | ||
|
|
4f15cc3f08 | ||
|
|
1769dd959e | ||
|
|
1253b81a01 | ||
|
|
3b524f6aba | ||
|
|
051ccad29a | ||
|
|
c82eba05d1 | ||
|
|
74af199afc | ||
|
|
c2afdba659 | ||
|
|
e6ae45f133 | ||
|
|
129ef6766b | ||
|
|
3194a7b1a2 | ||
|
|
46a733ba5b | ||
|
|
466844fc55 | ||
|
|
0cc793e3fe | ||
|
|
0fdfb385a4 | ||
|
|
419a4c6b2d | ||
|
|
55e9441547 | ||
|
|
7abff6ded4 | ||
|
|
a648d310a5 | ||
|
|
4b56fa56b5 | ||
|
|
18bf700936 | ||
|
|
67ddff736c | ||
|
|
1bba125a2a | ||
|
|
187acad592 | ||
|
|
5e07c7b3e1 | ||
|
|
cd942cf8b6 | ||
|
|
c43589fe6a | ||
|
|
d4594eff3b | ||
|
|
809a0e846b | ||
|
|
7f00ce2598 | ||
|
|
d7d77dbfe9 | ||
|
|
7a124c74cc | ||
|
|
f8549f4643 | ||
|
|
0476ff70da | ||
|
|
5ec541c3c1 | ||
|
|
7b920348f3 | ||
|
|
51b1ef41a1 | ||
|
|
12447effc9 | ||
|
|
dc1b059a9d | ||
|
|
a676ebf46c | ||
|
|
338f9fb5a9 | ||
|
|
98c9132d4a | ||
|
|
a054f12492 | ||
|
|
c42ac1df1b | ||
|
|
d9d46cda1c | ||
|
|
f678a17505 | ||
|
|
3da4bb69ce | ||
|
|
3e2b876c59 | ||
|
|
10daf2599d | ||
|
|
56d6eb7077 | ||
|
|
9b54272f8e | ||
|
|
685e003db8 | ||
|
|
701fc0bc80 | ||
|
|
e5518ac0fa | ||
|
|
8e1bf48cd1 | ||
|
|
8dd82e1a3b | ||
|
|
4418bfe965 | ||
|
|
39c4d710bc | ||
|
|
51a8c47afd | ||
|
|
922570bb5c | ||
|
|
ca282d5635 | ||
|
|
ff67043210 | ||
|
|
31da231c1c | ||
|
|
eb698a7430 | ||
|
|
03be809ba9 | ||
|
|
69601bf15a | ||
|
|
7cad3d403b | ||
|
|
cc84af3346 | ||
|
|
33ef54a162 | ||
|
|
ac43ff886a | ||
|
|
a64f73ca0c | ||
|
|
314477d2fc | ||
|
|
c63fc93daa | ||
|
|
f04ad1e702 | ||
|
|
2183c4bda6 | ||
|
|
6ba91c1515 | ||
|
|
dc03bb76e6 | ||
|
|
d0559c16b5 | ||
|
|
eb693f7b48 | ||
|
|
ef5639ff4b | ||
|
|
34a335797c | ||
|
|
edda3a4d23 | ||
|
|
184839423f | ||
|
|
c0f3600a52 | ||
|
|
2b1302aa07 | ||
|
|
d894bb4aab | ||
|
|
470c071344 | ||
|
|
4bd639c6c4 | ||
|
|
4cb7e63421 | ||
|
|
9f14a503d8 | ||
|
|
d5da6de86c | ||
|
|
4c2b233722 | ||
|
|
ca5b1eea13 | ||
|
|
614e9b6d55 | ||
|
|
27b2530b8d | ||
|
|
2259167200 | ||
|
|
2e05214828 | ||
|
|
cfb996039b | ||
|
|
44c4d56214 | ||
|
|
8e6be91f7c | ||
|
|
00816fb2c8 | ||
|
|
535356b77f | ||
|
|
5e558746ce | ||
|
|
f7bd52ac0c | ||
|
|
9165f518a9 | ||
|
|
01605aa221 | ||
|
|
8b0b29c424 | ||
|
|
7a116966fa | ||
|
|
e7e8f11a74 | ||
|
|
f235d832d5 | ||
|
|
7730b5e20b | ||
|
|
8c3ba4ce48 | ||
|
|
e9a126f586 | ||
|
|
097e7d2ff2 | ||
|
|
81265f1238 | ||
|
|
2b507e6e20 | ||
|
|
747d3a8f13 | ||
|
|
30f6f07434 | ||
|
|
6de5488a15 | ||
|
|
5413647166 | ||
|
|
e83fe73b18 | ||
|
|
87a289ec65 | ||
|
|
8a0a118dba | ||
|
|
687126ce87 | ||
|
|
8a05d577da | ||
|
|
4c3ebfc0f8 | ||
|
|
6093f25f9a | ||
|
|
ecab68d676 | ||
|
|
1cb4f37c95 | ||
|
|
14318528b9 | ||
|
|
9c0e1f8f1a | ||
|
|
2034ce9e4d | ||
|
|
657489caf6 | ||
|
|
94be3d1fe5 | ||
|
|
f6eae41cee | ||
|
|
69c64434e3 | ||
|
|
256cabfce1 | ||
|
|
e8b8272cf9 | ||
|
|
bd5ab4881c | ||
|
|
9630744bdb | ||
|
|
ab3ad0eb97 | ||
|
|
2393889028 | ||
|
|
c36ecb1ed1 | ||
|
|
3e9b28ff0c | ||
|
|
d4eec461a9 | ||
|
|
b584f37087 | ||
|
|
5c75644db2 | ||
|
|
72d9e3e00b | ||
|
|
b3d3362f34 | ||
|
|
1cbaf55cee | ||
|
|
7771875d57 | ||
|
|
85937d8e23 | ||
|
|
1480986a3a | ||
|
|
bc968736df | ||
|
|
ad80b8e8b6 | ||
|
|
d44d220c55 | ||
|
|
f6bcef0789 | ||
|
|
a28ec752e8 | ||
|
|
48ea5746d9 | ||
|
|
f473d31970 | ||
|
|
9cd1d4b466 | ||
|
|
4f02065eaf | ||
|
|
4b85e3e8fb | ||
|
|
e4e8c2205e | ||
|
|
18649dd074 | ||
|
|
9f9463f0e8 | ||
|
|
6cf9bc5de2 | ||
|
|
297b4346c5 | ||
|
|
767a203439 | ||
|
|
c564c253b1 | ||
|
|
b4e138e21b | ||
|
|
8ca01921c4 | ||
|
|
c8b97ffde3 | ||
|
|
abc84e5710 | ||
|
|
d732d195f3 | ||
|
|
789975abb0 | ||
|
|
ed1eab7fcc | ||
|
|
29b31c114a | ||
|
|
c8cf353c21 | ||
|
|
e82e2c71f1 | ||
|
|
dd729c406f | ||
|
|
3127e85900 | ||
|
|
304eeb3158 | ||
|
|
bf781c6b50 | ||
|
|
da1098e441 | ||
|
|
85065357e2 | ||
|
|
1f5f6c3b0e | ||
|
|
4f7026969f | ||
|
|
16d264ebfa | ||
|
|
04ffbe945b | ||
|
|
974a1a1e7d | ||
|
|
ca2c07244f | ||
|
|
f90786d1c0 | ||
|
|
bdf55568c7 | ||
|
|
e262d2f19b | ||
|
|
aabfe820ac | ||
|
|
3bba5442bd | ||
|
|
6ce1922fb3 | ||
|
|
9367a404ef | ||
|
|
7c8e19c681 | ||
|
|
7ccc5eb9b8 | ||
|
|
b4b6d3d07c | ||
|
|
9007bac7b2 | ||
|
|
df13ca3c92 | ||
|
|
d86935acaa | ||
|
|
72b450d526 | ||
|
|
4b7afeeb4f | ||
|
|
0e0e779cbe | ||
|
|
89b2f48a06 | ||
|
|
c80bdb8d0c | ||
|
|
50d89a8ec9 | ||
|
|
f5ccaa7b48 | ||
|
|
e682ee8541 | ||
|
|
caaa7a9e74 | ||
|
|
6ef0e325e2 | ||
|
|
ea064deeb8 | ||
|
|
8e81609a39 | ||
|
|
83da07a941 | ||
|
|
977e80c829 | ||
|
|
8ba0d10f40 | ||
|
|
2581091652 | ||
|
|
e72a7ceaea | ||
|
|
a17ddf6d54 | ||
|
|
b5e2d21f33 | ||
|
|
d09f8dff18 | ||
|
|
bdb3406dcb | ||
|
|
f54b49db1a | ||
|
|
0cc9f006c5 | ||
|
|
db1398b65f | ||
|
|
e8192e6c3f | ||
|
|
775200bdd6 | ||
|
|
820a2a093c | ||
|
|
d3995b9b10 | ||
|
|
f6867f9338 | ||
|
|
3e2548fcd5 | ||
|
|
745d250787 | ||
|
|
b1063eb38f | ||
|
|
9032a1debb | ||
|
|
795fe8ae1d | ||
|
|
e5a908af68 | ||
|
|
6ce16c1cc0 | ||
|
|
6ff2a5ac94 | ||
|
|
fcea16e43a | ||
|
|
7b8e42382e | ||
|
|
a372b5ea39 | ||
|
|
1aafee2a7c | ||
|
|
7afe3d5181 | ||
|
|
6fba62d062 | ||
|
|
6906c0ab0d | ||
|
|
5d46adf8fd | ||
|
|
eda1f11d42 | ||
|
|
6431a8255d | ||
|
|
48fd1d11e2 | ||
|
|
4c3e62efad | ||
|
|
52a15a5d92 | ||
|
|
57f4aa5995 | ||
|
|
ab9ab004b7 | ||
|
|
ac624eb98f | ||
|
|
1a4c37820d | ||
|
|
685206950b | ||
|
|
eececf8a93 | ||
|
|
9bc3d65554 | ||
|
|
f9b854ce39 | ||
|
|
1416968fe5 | ||
|
|
efc183c709 | ||
|
|
07a2442718 | ||
|
|
f549c50a58 | ||
|
|
8d6ce1a2f7 | ||
|
|
c5b33e025e | ||
|
|
4cdfc738c0 | ||
|
|
46d46f21e4 | ||
|
|
4983605b23 | ||
|
|
9e8d04d806 | ||
|
|
042f935133 | ||
|
|
ed9d3639e2 | ||
|
|
728f105830 | ||
|
|
6f359fa6a8 | ||
|
|
57a88743bc | ||
|
|
667f022086 | ||
|
|
b742a3a4cd | ||
|
|
8d5c9742f8 | ||
|
|
c2a49efe73 | ||
|
|
8c8a0ab46d | ||
|
|
959b75bddd | ||
|
|
d29d5105f1 | ||
|
|
38e82872a5 | ||
|
|
15d9ce1d36 | ||
|
|
67f7cdb36c | ||
|
|
6a9d4ae0fd | ||
|
|
6a761c3fb5 | ||
|
|
3baf97e69f | ||
|
|
694dc60481 | ||
|
|
e3c6f0452c | ||
|
|
ed2401a87b | ||
|
|
cb59458c79 | ||
|
|
125a493400 | ||
|
|
83910b55d2 | ||
|
|
f4fd131100 | ||
|
|
cfdc880d8c | ||
|
|
7303e8bdd2 | ||
|
|
ecde465d9f | ||
|
|
5c5e70a805 | ||
|
|
4e41d9be55 | ||
|
|
d06d94449c | ||
|
|
1af2c83c63 | ||
|
|
6c75136777 | ||
|
|
31351e34e1 | ||
|
|
a058a774e9 | ||
|
|
e6db28485c | ||
|
|
391bb096d6 | ||
|
|
2b496dc2e5 | ||
|
|
a6e0b30576 | ||
|
|
1ac31264b7 | ||
|
|
ca4b8224fd | ||
|
|
d1e5781c24 | ||
|
|
c86e451198 | ||
|
|
b4a7b547f0 | ||
|
|
07b395c24a | ||
|
|
53b9630fa5 | ||
|
|
f820c3089e | ||
|
|
7a4258bb20 |
@@ -10,7 +10,6 @@
|
|||||||
"theme": "monokai"
|
"theme": "monokai"
|
||||||
},
|
},
|
||||||
"hotkey": {
|
"hotkey": {
|
||||||
"toggleFinder": "Cmd + Alt + S",
|
|
||||||
"toggleMain": "Cmd + Alt + L"
|
"toggleMain": "Cmd + Alt + L"
|
||||||
},
|
},
|
||||||
"isSideNavFolded": false,
|
"isSideNavFolded": false,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
compiled/
|
compiled/
|
||||||
dist/
|
dist/
|
||||||
|
extra_scripts/
|
||||||
@@ -3,7 +3,9 @@
|
|||||||
"plugins": ["react"],
|
"plugins": ["react"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-useless-escape": 0,
|
"no-useless-escape": 0,
|
||||||
"prefer-const": "warn",
|
"prefer-const": ["warn", {
|
||||||
|
"destructuring": "all"
|
||||||
|
}],
|
||||||
"no-unused-vars": "warn",
|
"no-unused-vars": "warn",
|
||||||
"no-undef": "warn",
|
"no-undef": "warn",
|
||||||
"no-lone-blocks": "warn",
|
"no-lone-blocks": "warn",
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,3 +8,5 @@ node_modules/*
|
|||||||
/compiled
|
/compiled
|
||||||
/secret
|
/secret
|
||||||
*.log
|
*.log
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- stable
|
- 6
|
||||||
- lts/*
|
|
||||||
script:
|
script:
|
||||||
- npm run lint && npm run test
|
- 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'
|
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@5.2 && grunt pre-build; fi'
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -2,7 +2,7 @@ GPL-3.0
|
|||||||
|
|
||||||
Boostnote - an open source note-taking app made for programmers just like you.
|
Boostnote - an open source note-taking app made for programmers just like you.
|
||||||
|
|
||||||
Copyright (C) 2017 Maisin&Co., Inc.
|
Copyright (C) 2017 - 2018 BoostIO
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
|||||||
@@ -2,14 +2,20 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import CodeMirror from 'codemirror'
|
import CodeMirror from 'codemirror'
|
||||||
|
import 'codemirror-mode-elixir'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import copyImage from 'browser/main/lib/dataApi/copyImage'
|
import copyImage from 'browser/main/lib/dataApi/copyImage'
|
||||||
import { findStorage } from 'browser/lib/findStorage'
|
import { findStorage } from 'browser/lib/findStorage'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
|
import iconv from 'iconv-lite'
|
||||||
|
const { ipcRenderer } = require('electron')
|
||||||
|
|
||||||
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
||||||
|
|
||||||
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||||
|
const buildCMRulers = (rulers, enableRulers) =>
|
||||||
|
enableRulers ? rulers.map(ruler => ({ column: ruler })) : []
|
||||||
|
|
||||||
function pass (name) {
|
function pass (name) {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
@@ -30,8 +36,13 @@ export default class CodeEditor extends React.Component {
|
|||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
|
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
||||||
this.changeHandler = (e) => this.handleChange(e)
|
this.changeHandler = (e) => this.handleChange(e)
|
||||||
|
this.focusHandler = () => {
|
||||||
|
ipcRenderer.send('editor:focused', true)
|
||||||
|
}
|
||||||
this.blurHandler = (editor, e) => {
|
this.blurHandler = (editor, e) => {
|
||||||
|
ipcRenderer.send('editor:focused', false)
|
||||||
if (e == null) return null
|
if (e == null) return null
|
||||||
let el = e.relatedTarget
|
let el = e.relatedTarget
|
||||||
while (el != null) {
|
while (el != null) {
|
||||||
@@ -46,21 +57,59 @@ export default class CodeEditor extends React.Component {
|
|||||||
this.loadStyleHandler = (e) => {
|
this.loadStyleHandler = (e) => {
|
||||||
this.editor.refresh()
|
this.editor.refresh()
|
||||||
}
|
}
|
||||||
|
this.searchHandler = (e, msg) => this.handleSearch(msg)
|
||||||
|
this.searchState = null
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSearch (msg) {
|
||||||
|
const cm = this.editor
|
||||||
|
const component = this
|
||||||
|
|
||||||
|
if (component.searchState) cm.removeOverlay(component.searchState)
|
||||||
|
if (msg.length < 3) return
|
||||||
|
|
||||||
|
cm.operation(function () {
|
||||||
|
component.searchState = makeOverlay(msg, 'searching')
|
||||||
|
cm.addOverlay(component.searchState)
|
||||||
|
|
||||||
|
function makeOverlay (query, style) {
|
||||||
|
query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), 'gi')
|
||||||
|
return {
|
||||||
|
token: function (stream) {
|
||||||
|
query.lastIndex = stream.pos
|
||||||
|
var match = query.exec(stream.string)
|
||||||
|
if (match && match.index === stream.pos) {
|
||||||
|
stream.pos += match[0].length || 1
|
||||||
|
return style
|
||||||
|
} else if (match) {
|
||||||
|
stream.pos = match.index
|
||||||
|
} else {
|
||||||
|
stream.skipToEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
const { rulers, enableRulers } = this.props
|
||||||
this.value = this.props.value
|
this.value = this.props.value
|
||||||
|
|
||||||
this.editor = CodeMirror(this.refs.root, {
|
this.editor = CodeMirror(this.refs.root, {
|
||||||
|
rulers: buildCMRulers(rulers, enableRulers),
|
||||||
value: this.props.value,
|
value: this.props.value,
|
||||||
lineNumbers: true,
|
lineNumbers: this.props.displayLineNumbers,
|
||||||
lineWrapping: true,
|
lineWrapping: true,
|
||||||
theme: this.props.theme,
|
theme: this.props.theme,
|
||||||
indentUnit: this.props.indentSize,
|
indentUnit: this.props.indentSize,
|
||||||
tabSize: this.props.indentSize,
|
tabSize: this.props.indentSize,
|
||||||
indentWithTabs: this.props.indentType !== 'space',
|
indentWithTabs: this.props.indentType !== 'space',
|
||||||
keyMap: this.props.keyMap,
|
keyMap: this.props.keyMap,
|
||||||
|
scrollPastEnd: this.props.scrollPastEnd,
|
||||||
inputStyle: 'textarea',
|
inputStyle: 'textarea',
|
||||||
dragDrop: false,
|
dragDrop: false,
|
||||||
|
autoCloseBrackets: true,
|
||||||
extraKeys: {
|
extraKeys: {
|
||||||
Tab: function (cm) {
|
Tab: function (cm) {
|
||||||
const cursor = cm.getCursor()
|
const cursor = cm.getCursor()
|
||||||
@@ -68,7 +117,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
if (cm.somethingSelected()) cm.indentSelection('add')
|
if (cm.somethingSelected()) cm.indentSelection('add')
|
||||||
else {
|
else {
|
||||||
const tabs = cm.getOption('indentWithTabs')
|
const tabs = cm.getOption('indentWithTabs')
|
||||||
if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)\] )?$/)) {
|
if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)] )?$/)) {
|
||||||
cm.execCommand('goLineStart')
|
cm.execCommand('goLineStart')
|
||||||
if (tabs) {
|
if (tabs) {
|
||||||
cm.execCommand('insertTab')
|
cm.execCommand('insertTab')
|
||||||
@@ -88,7 +137,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
'Cmd-T': function (cm) {
|
'Cmd-T': function (cm) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
},
|
},
|
||||||
Enter: 'newlineAndIndentContinueMarkdownList',
|
Enter: 'boostNewLineAndIndentContinueMarkdownList',
|
||||||
'Ctrl-C': (cm) => {
|
'Ctrl-C': (cm) => {
|
||||||
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
|
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
|
||||||
document.execCommand('copy')
|
document.execCommand('copy')
|
||||||
@@ -100,9 +149,14 @@ export default class CodeEditor extends React.Component {
|
|||||||
|
|
||||||
this.setMode(this.props.mode)
|
this.setMode(this.props.mode)
|
||||||
|
|
||||||
|
this.editor.on('focus', this.focusHandler)
|
||||||
this.editor.on('blur', this.blurHandler)
|
this.editor.on('blur', this.blurHandler)
|
||||||
this.editor.on('change', this.changeHandler)
|
this.editor.on('change', this.changeHandler)
|
||||||
this.editor.on('paste', this.pasteHandler)
|
this.editor.on('paste', this.pasteHandler)
|
||||||
|
eventEmitter.on('top:search', this.searchHandler)
|
||||||
|
|
||||||
|
eventEmitter.emit('code:init')
|
||||||
|
this.editor.on('scroll', this.scrollHandler)
|
||||||
|
|
||||||
const editorTheme = document.getElementById('editorTheme')
|
const editorTheme = document.getElementById('editorTheme')
|
||||||
editorTheme.addEventListener('load', this.loadStyleHandler)
|
editorTheme.addEventListener('load', this.loadStyleHandler)
|
||||||
@@ -119,15 +173,19 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
|
this.editor.off('focus', this.focusHandler)
|
||||||
this.editor.off('blur', this.blurHandler)
|
this.editor.off('blur', this.blurHandler)
|
||||||
this.editor.off('change', this.changeHandler)
|
this.editor.off('change', this.changeHandler)
|
||||||
this.editor.off('paste', this.pasteHandler)
|
this.editor.off('paste', this.pasteHandler)
|
||||||
|
eventEmitter.off('top:search', this.searchHandler)
|
||||||
|
this.editor.off('scroll', this.scrollHandler)
|
||||||
const editorTheme = document.getElementById('editorTheme')
|
const editorTheme = document.getElementById('editorTheme')
|
||||||
editorTheme.removeEventListener('load', this.loadStyleHandler)
|
editorTheme.removeEventListener('load', this.loadStyleHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps, prevState) {
|
componentDidUpdate (prevProps, prevState) {
|
||||||
let needRefresh = false
|
let needRefresh = false
|
||||||
|
const { rulers, enableRulers } = this.props
|
||||||
if (prevProps.mode !== this.props.mode) {
|
if (prevProps.mode !== this.props.mode) {
|
||||||
this.setMode(this.props.mode)
|
this.setMode(this.props.mode)
|
||||||
}
|
}
|
||||||
@@ -145,6 +203,10 @@ export default class CodeEditor extends React.Component {
|
|||||||
needRefresh = true
|
needRefresh = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prevProps.enableRulers !== enableRulers || prevProps.rulers !== rulers) {
|
||||||
|
this.editor.setOption('rulers', buildCMRulers(rulers, enableRulers))
|
||||||
|
}
|
||||||
|
|
||||||
if (prevProps.indentSize !== this.props.indentSize) {
|
if (prevProps.indentSize !== this.props.indentSize) {
|
||||||
this.editor.setOption('indentUnit', this.props.indentSize)
|
this.editor.setOption('indentUnit', this.props.indentSize)
|
||||||
this.editor.setOption('tabSize', this.props.indentSize)
|
this.editor.setOption('tabSize', this.props.indentSize)
|
||||||
@@ -153,6 +215,14 @@ export default class CodeEditor extends React.Component {
|
|||||||
this.editor.setOption('indentWithTabs', this.props.indentType !== 'space')
|
this.editor.setOption('indentWithTabs', this.props.indentType !== 'space')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prevProps.displayLineNumbers !== this.props.displayLineNumbers) {
|
||||||
|
this.editor.setOption('lineNumbers', this.props.displayLineNumbers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevProps.scrollPastEnd !== this.props.scrollPastEnd) {
|
||||||
|
this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd)
|
||||||
|
}
|
||||||
|
|
||||||
if (needRefresh) {
|
if (needRefresh) {
|
||||||
this.editor.refresh()
|
this.editor.refresh()
|
||||||
}
|
}
|
||||||
@@ -219,32 +289,110 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlePaste (editor, e) {
|
handlePaste (editor, e) {
|
||||||
const dataTransferItem = e.clipboardData.items[0]
|
const clipboardData = e.clipboardData
|
||||||
if (!dataTransferItem.type.match('image')) return
|
const dataTransferItem = clipboardData.items[0]
|
||||||
|
const pastedTxt = clipboardData.getData('text')
|
||||||
const blob = dataTransferItem.getAsFile()
|
const isURL = (str) => {
|
||||||
const reader = new FileReader()
|
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
|
||||||
let base64data
|
return matcher.test(str)
|
||||||
|
|
||||||
reader.readAsDataURL(blob)
|
|
||||||
reader.onloadend = () => {
|
|
||||||
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
|
|
||||||
base64data += base64data.replace('+', ' ')
|
|
||||||
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
|
||||||
const imageName = Math.random().toString(36).slice(-16)
|
|
||||||
const storagePath = findStorage(this.props.storageKey).path
|
|
||||||
const imageDir = path.join(storagePath, 'images')
|
|
||||||
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
|
|
||||||
const imagePath = path.join(imageDir, `${imageName}.png`)
|
|
||||||
fs.writeFile(imagePath, binaryData, 'binary')
|
|
||||||
const imageMd = `})`
|
|
||||||
this.insertImageMd(imageMd)
|
|
||||||
}
|
}
|
||||||
|
const isInLinkTag = (editor) => {
|
||||||
|
const startCursor = editor.getCursor('start')
|
||||||
|
const prevChar = editor.getRange(
|
||||||
|
{ line: startCursor.line, ch: startCursor.ch - 2 },
|
||||||
|
{ line: startCursor.line, ch: startCursor.ch }
|
||||||
|
)
|
||||||
|
const endCursor = editor.getCursor('end')
|
||||||
|
const nextChar = editor.getRange(
|
||||||
|
{ line: endCursor.line, ch: endCursor.ch },
|
||||||
|
{ line: endCursor.line, ch: endCursor.ch + 1 }
|
||||||
|
)
|
||||||
|
return prevChar === '](' && nextChar === ')'
|
||||||
|
}
|
||||||
|
if (dataTransferItem.type.match('image')) {
|
||||||
|
const blob = dataTransferItem.getAsFile()
|
||||||
|
const reader = new FileReader()
|
||||||
|
let base64data
|
||||||
|
|
||||||
|
reader.readAsDataURL(blob)
|
||||||
|
reader.onloadend = () => {
|
||||||
|
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
|
||||||
|
base64data += base64data.replace('+', ' ')
|
||||||
|
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
||||||
|
const imageName = Math.random().toString(36).slice(-16)
|
||||||
|
const storagePath = findStorage(this.props.storageKey).path
|
||||||
|
const imageDir = path.join(storagePath, 'images')
|
||||||
|
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
|
||||||
|
const imagePath = path.join(imageDir, `${imageName}.png`)
|
||||||
|
fs.writeFile(imagePath, binaryData, 'binary')
|
||||||
|
const imageMd = `})`
|
||||||
|
this.insertImageMd(imageMd)
|
||||||
|
}
|
||||||
|
} else if (this.props.fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
|
||||||
|
this.handlePasteUrl(e, editor, pastedTxt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleScroll (e) {
|
||||||
|
if (this.props.onScroll) {
|
||||||
|
this.props.onScroll(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePasteUrl (e, editor, pastedTxt) {
|
||||||
|
e.preventDefault()
|
||||||
|
const taggedUrl = `<${pastedTxt}>`
|
||||||
|
editor.replaceSelection(taggedUrl)
|
||||||
|
|
||||||
|
fetch(pastedTxt, {
|
||||||
|
method: 'get'
|
||||||
|
}).then((response) => {
|
||||||
|
return this.decodeResponse(response)
|
||||||
|
}).then((response) => {
|
||||||
|
const parsedResponse = (new window.DOMParser()).parseFromString(response, 'text/html')
|
||||||
|
const value = editor.getValue()
|
||||||
|
const cursor = editor.getCursor()
|
||||||
|
const LinkWithTitle = `[${parsedResponse.title}](${pastedTxt})`
|
||||||
|
const newValue = value.replace(taggedUrl, LinkWithTitle)
|
||||||
|
editor.setValue(newValue)
|
||||||
|
editor.setCursor(cursor)
|
||||||
|
}).catch((e) => {
|
||||||
|
const value = editor.getValue()
|
||||||
|
const newValue = value.replace(taggedUrl, pastedTxt)
|
||||||
|
const cursor = editor.getCursor()
|
||||||
|
editor.setValue(newValue)
|
||||||
|
editor.setCursor(cursor)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeResponse (response) {
|
||||||
|
const headers = response.headers
|
||||||
|
const _charset = headers.has('content-type')
|
||||||
|
? this.extractContentTypeCharset(headers.get('content-type'))
|
||||||
|
: undefined
|
||||||
|
return response.arrayBuffer().then((buff) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const charset = _charset !== undefined && iconv.encodingExists(_charset) ? _charset : 'utf-8'
|
||||||
|
resolve(iconv.decode(new Buffer(buff), charset).toString())
|
||||||
|
} catch (e) {
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
extractContentTypeCharset (contentType) {
|
||||||
|
return contentType.split(';').filter((str) => {
|
||||||
|
return str.trim().toLowerCase().startsWith('charset')
|
||||||
|
}).map((str) => {
|
||||||
|
return str.replace(/['"]/g, '').split('=')[1]
|
||||||
|
})[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { className, fontSize } = this.props
|
const { className, fontSize } = this.props
|
||||||
let fontFamily = this.props.className
|
let fontFamily = this.props.fontFamily
|
||||||
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
|
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
|
||||||
? [fontFamily].concat(defaultEditorFontFamily)
|
? [fontFamily].concat(defaultEditorFontFamily)
|
||||||
: defaultEditorFontFamily
|
: defaultEditorFontFamily
|
||||||
@@ -268,6 +416,8 @@ export default class CodeEditor extends React.Component {
|
|||||||
|
|
||||||
CodeEditor.propTypes = {
|
CodeEditor.propTypes = {
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
|
enableRulers: PropTypes.bool,
|
||||||
|
rulers: PropTypes.arrayOf(Number),
|
||||||
mode: PropTypes.string,
|
mode: PropTypes.string,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
onBlur: PropTypes.func,
|
onBlur: PropTypes.func,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import styles from './MarkdownEditor.styl'
|
|||||||
import CodeEditor from 'browser/components/CodeEditor'
|
import CodeEditor from 'browser/components/CodeEditor'
|
||||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
import { findStorage } from 'browser/lib/findStorage'
|
import {findStorage} from 'browser/lib/findStorage'
|
||||||
|
|
||||||
class MarkdownEditor extends React.Component {
|
class MarkdownEditor extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -92,7 +92,9 @@ class MarkdownEditor extends React.Component {
|
|||||||
if (this.state.isLocked) return
|
if (this.state.isLocked) return
|
||||||
this.setState({ keyPressed: new Set() })
|
this.setState({ keyPressed: new Set() })
|
||||||
const { config } = this.props
|
const { config } = this.props
|
||||||
if (config.editor.switchPreview === 'BLUR') {
|
if (config.editor.switchPreview === 'BLUR' ||
|
||||||
|
(config.editor.switchPreview === 'DBL_CLICK' && this.state.status === 'CODE')
|
||||||
|
) {
|
||||||
const cursorPosition = this.refs.code.editor.getCursor()
|
const cursorPosition = this.refs.code.editor.getCursor()
|
||||||
this.setState({
|
this.setState({
|
||||||
status: 'PREVIEW'
|
status: 'PREVIEW'
|
||||||
@@ -104,6 +106,20 @@ class MarkdownEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDoubleClick (e) {
|
||||||
|
if (this.state.isLocked) return
|
||||||
|
this.setState({keyPressed: new Set()})
|
||||||
|
const { config } = this.props
|
||||||
|
if (config.editor.switchPreview === 'DBL_CLICK') {
|
||||||
|
this.setState({
|
||||||
|
status: 'CODE'
|
||||||
|
}, () => {
|
||||||
|
this.refs.code.focus()
|
||||||
|
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handlePreviewMouseDown (e) {
|
handlePreviewMouseDown (e) {
|
||||||
this.previewMouseDownedAt = new Date()
|
this.previewMouseDownedAt = new Date()
|
||||||
}
|
}
|
||||||
@@ -242,7 +258,12 @@ class MarkdownEditor extends React.Component {
|
|||||||
fontSize={editorFontSize}
|
fontSize={editorFontSize}
|
||||||
indentType={config.editor.indentType}
|
indentType={config.editor.indentType}
|
||||||
indentSize={editorIndentSize}
|
indentSize={editorIndentSize}
|
||||||
|
enableRulers={config.editor.enableRulers}
|
||||||
|
rulers={config.editor.rulers}
|
||||||
|
displayLineNumbers={config.editor.displayLineNumbers}
|
||||||
|
scrollPastEnd={config.editor.scrollPastEnd}
|
||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
|
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||||
onChange={(e) => this.handleChange(e)}
|
onChange={(e) => this.handleChange(e)}
|
||||||
onBlur={(e) => this.handleBlur(e)}
|
onBlur={(e) => this.handleBlur(e)}
|
||||||
/>
|
/>
|
||||||
@@ -259,8 +280,12 @@ class MarkdownEditor extends React.Component {
|
|||||||
codeBlockFontFamily={config.editor.fontFamily}
|
codeBlockFontFamily={config.editor.fontFamily}
|
||||||
lineNumber={config.preview.lineNumber}
|
lineNumber={config.preview.lineNumber}
|
||||||
indentSize={editorIndentSize}
|
indentSize={editorIndentSize}
|
||||||
|
scrollPastEnd={config.preview.scrollPastEnd}
|
||||||
|
smartQuotes={config.preview.smartQuotes}
|
||||||
|
sanitize={config.preview.sanitize}
|
||||||
ref='preview'
|
ref='preview'
|
||||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||||
|
onDoubleClick={(e) => this.handleDoubleClick(e)}
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
value={this.state.renderValue}
|
value={this.state.renderValue}
|
||||||
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
|
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
|
||||||
|
|||||||
179
browser/components/MarkdownPreview.js
Normal file → Executable file
179
browser/components/MarkdownPreview.js
Normal file → Executable file
@@ -1,17 +1,18 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import markdown from 'browser/lib/markdown'
|
import Markdown from 'browser/lib/markdown'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import CodeMirror from 'codemirror'
|
import CodeMirror from 'codemirror'
|
||||||
|
import 'codemirror-mode-elixir'
|
||||||
import consts from 'browser/lib/consts'
|
import consts from 'browser/lib/consts'
|
||||||
import Raphael from 'raphael'
|
import Raphael from 'raphael'
|
||||||
import flowchart from 'flowchart'
|
import flowchart from 'flowchart'
|
||||||
import SequenceDiagram from 'js-sequence-diagrams'
|
import SequenceDiagram from 'js-sequence-diagrams'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
import fs from 'fs'
|
|
||||||
import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
import mdurl from 'mdurl'
|
import mdurl from 'mdurl'
|
||||||
|
import exportNote from 'browser/main/lib/dataApi/exportNote'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { app } = remote
|
const { app } = remote
|
||||||
@@ -22,8 +23,12 @@ const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
|||||||
const appPath = 'file://' + (process.env.NODE_ENV === 'production'
|
const appPath = 'file://' + (process.env.NODE_ENV === 'production'
|
||||||
? app.getAppPath()
|
? app.getAppPath()
|
||||||
: path.resolve())
|
: path.resolve())
|
||||||
|
const CSS_FILES = [
|
||||||
|
`${appPath}/node_modules/katex/dist/katex.min.css`,
|
||||||
|
`${appPath}/node_modules/codemirror/lib/codemirror.css`
|
||||||
|
]
|
||||||
|
|
||||||
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber) {
|
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd) {
|
||||||
return `
|
return `
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Lato';
|
font-family: 'Lato';
|
||||||
@@ -47,14 +52,15 @@ ${markdownStyle}
|
|||||||
body {
|
body {
|
||||||
font-family: '${fontFamily.join("','")}';
|
font-family: '${fontFamily.join("','")}';
|
||||||
font-size: ${fontSize}px;
|
font-size: ${fontSize}px;
|
||||||
|
${scrollPastEnd && 'padding-bottom: 90vh;'}
|
||||||
}
|
}
|
||||||
code {
|
code {
|
||||||
font-family: ${codeBlockFontFamily.join(', ')};
|
font-family: '${codeBlockFontFamily.join("','")}';
|
||||||
background-color: rgba(0,0,0,0.04);
|
background-color: rgba(0,0,0,0.04);
|
||||||
}
|
}
|
||||||
.lineNumber {
|
.lineNumber {
|
||||||
${lineNumber && 'display: block !important;'}
|
${lineNumber && 'display: block !important;'}
|
||||||
font-family: ${codeBlockFontFamily.join(', ')};
|
font-family: '${codeBlockFontFamily.join("','")}';
|
||||||
}
|
}
|
||||||
|
|
||||||
.clipboardButton {
|
.clipboardButton {
|
||||||
@@ -114,13 +120,26 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.contextMenuHandler = (e) => this.handleContextMenu(e)
|
this.contextMenuHandler = (e) => this.handleContextMenu(e)
|
||||||
this.mouseDownHandler = (e) => this.handleMouseDown(e)
|
this.mouseDownHandler = (e) => this.handleMouseDown(e)
|
||||||
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
||||||
|
this.DoubleClickHandler = (e) => this.handleDoubleClick(e)
|
||||||
|
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
||||||
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
|
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
|
||||||
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
||||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
this.saveAsTextHandler = () => this.handleSaveAsText()
|
||||||
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
||||||
|
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
|
||||||
this.printHandler = () => this.handlePrint()
|
this.printHandler = () => this.handlePrint()
|
||||||
|
|
||||||
this.linkClickHandler = this.handlelinkClick.bind(this)
|
this.linkClickHandler = this.handlelinkClick.bind(this)
|
||||||
|
this.initMarkdown = this.initMarkdown.bind(this)
|
||||||
|
this.initMarkdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
initMarkdown () {
|
||||||
|
const { smartQuotes, sanitize } = this.props
|
||||||
|
this.markdown = new Markdown({
|
||||||
|
typographer: smartQuotes,
|
||||||
|
sanitize
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePreviewAnchorClick (e) {
|
handlePreviewAnchorClick (e) {
|
||||||
@@ -143,10 +162,20 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.props.onCheckboxClick(e)
|
this.props.onCheckboxClick(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleScroll (e) {
|
||||||
|
if (this.props.onScroll) {
|
||||||
|
this.props.onScroll(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleContextMenu (e) {
|
handleContextMenu (e) {
|
||||||
this.props.onContextMenu(e)
|
this.props.onContextMenu(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDoubleClick (e) {
|
||||||
|
if (this.props.onDoubleClick != null) this.props.onDoubleClick(e)
|
||||||
|
}
|
||||||
|
|
||||||
handleMouseDown (e) {
|
handleMouseDown (e) {
|
||||||
if (e.target != null) {
|
if (e.target != null) {
|
||||||
switch (e.target.tagName) {
|
switch (e.target.tagName) {
|
||||||
@@ -159,6 +188,7 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleMouseUp (e) {
|
handleMouseUp (e) {
|
||||||
|
if (!this.props.onMouseUp) return
|
||||||
if (e.target != null && e.target.tagName === 'A') {
|
if (e.target != null && e.target.tagName === 'A') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -173,25 +203,66 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.exportAsDocument('md')
|
this.exportAsDocument('md')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSaveAsHtml () {
|
||||||
|
this.exportAsDocument('html', (noteContent, exportTasks) => {
|
||||||
|
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams()
|
||||||
|
|
||||||
|
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
|
||||||
|
const body = this.markdown.render(noteContent)
|
||||||
|
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
file = file.replace('file://', '')
|
||||||
|
exportTasks.push({
|
||||||
|
src: file,
|
||||||
|
dst: 'css'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
let styles = ''
|
||||||
|
files.forEach((file) => {
|
||||||
|
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
|
||||||
|
})
|
||||||
|
|
||||||
|
return `<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
|
||||||
|
<style id="style">${inlineStyles}</style>
|
||||||
|
${styles}
|
||||||
|
</head>
|
||||||
|
<body>${body}</body>
|
||||||
|
</html>`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
handlePrint () {
|
handlePrint () {
|
||||||
this.refs.root.contentWindow.print()
|
this.refs.root.contentWindow.print()
|
||||||
}
|
}
|
||||||
|
|
||||||
exportAsDocument (fileType) {
|
exportAsDocument (fileType, contentFormatter) {
|
||||||
const options = {
|
const options = {
|
||||||
filters: [
|
filters: [
|
||||||
{ name: 'Documents', extensions: [fileType] }
|
{name: 'Documents', extensions: [fileType]}
|
||||||
],
|
],
|
||||||
properties: ['openFile', 'createDirectory']
|
properties: ['openFile', 'createDirectory']
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.showSaveDialog(remote.getCurrentWindow(), options,
|
dialog.showSaveDialog(remote.getCurrentWindow(), options,
|
||||||
(filename) => {
|
(filename) => {
|
||||||
if (filename) {
|
if (filename) {
|
||||||
fs.writeFile(filename, this.props.value, (err) => {
|
const content = this.props.value
|
||||||
if (err) throw err
|
const storage = this.props.storagePath
|
||||||
|
|
||||||
|
exportNote(storage, content, filename, contentFormatter)
|
||||||
|
.then((res) => {
|
||||||
|
dialog.showMessageBox(remote.getCurrentWindow(), {type: 'info', message: `Exported to ${filename}`})
|
||||||
|
}).catch((err) => {
|
||||||
|
dialog.showErrorBox('Export error', err ? err.message || err : 'Unexpected error during export')
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fixDecodedURI (node) {
|
fixDecodedURI (node) {
|
||||||
@@ -208,21 +279,29 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.refs.root.setAttribute('sandbox', 'allow-scripts')
|
this.refs.root.setAttribute('sandbox', 'allow-scripts')
|
||||||
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
|
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
|
||||||
|
|
||||||
this.refs.root.contentWindow.document.head.innerHTML = `
|
let styles = `
|
||||||
<style id='style'></style>
|
<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">
|
<link rel="stylesheet" id="codeTheme">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
`
|
`
|
||||||
|
|
||||||
|
CSS_FILES.forEach((file) => {
|
||||||
|
styles += `<link rel="stylesheet" href="${file}">`
|
||||||
|
})
|
||||||
|
|
||||||
|
this.refs.root.contentWindow.document.head.innerHTML = styles
|
||||||
this.rewriteIframe()
|
this.rewriteIframe()
|
||||||
this.applyStyle()
|
this.applyStyle()
|
||||||
|
|
||||||
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
|
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
|
||||||
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
|
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
|
||||||
|
this.refs.root.contentWindow.document.addEventListener('dblclick', this.DoubleClickHandler)
|
||||||
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler)
|
||||||
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler)
|
||||||
|
this.refs.root.contentWindow.document.addEventListener('scroll', this.scrollHandler)
|
||||||
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
||||||
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
||||||
|
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
|
||||||
eventEmitter.on('print', this.printHandler)
|
eventEmitter.on('print', this.printHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,46 +309,60 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
|
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('mousedown', this.mouseDownHandler)
|
||||||
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
|
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
|
||||||
|
this.refs.root.contentWindow.document.removeEventListener('dblclick', this.DoubleClickHandler)
|
||||||
this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler)
|
||||||
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler)
|
||||||
|
this.refs.root.contentWindow.document.removeEventListener('scroll', this.scrollHandler)
|
||||||
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
||||||
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
||||||
|
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
|
||||||
eventEmitter.off('print', this.printHandler)
|
eventEmitter.off('print', this.printHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
if (prevProps.value !== this.props.value) this.rewriteIframe()
|
if (prevProps.value !== this.props.value) this.rewriteIframe()
|
||||||
|
if (prevProps.smartQuotes !== this.props.smartQuotes || prevProps.sanitize !== this.props.sanitize) {
|
||||||
|
this.initMarkdown()
|
||||||
|
this.rewriteIframe()
|
||||||
|
}
|
||||||
if (prevProps.fontFamily !== this.props.fontFamily ||
|
if (prevProps.fontFamily !== this.props.fontFamily ||
|
||||||
prevProps.fontSize !== this.props.fontSize ||
|
prevProps.fontSize !== this.props.fontSize ||
|
||||||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
|
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
|
||||||
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
|
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
|
||||||
prevProps.lineNumber !== this.props.lineNumber ||
|
prevProps.lineNumber !== this.props.lineNumber ||
|
||||||
prevProps.showCopyNotification !== this.props.showCopyNotification ||
|
prevProps.showCopyNotification !== this.props.showCopyNotification ||
|
||||||
prevProps.theme !== this.props.theme) {
|
prevProps.theme !== this.props.theme ||
|
||||||
|
prevProps.scrollPastEnd !== this.props.scrollPastEnd) {
|
||||||
this.applyStyle()
|
this.applyStyle()
|
||||||
this.rewriteIframe()
|
this.rewriteIframe()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyStyle () {
|
getStyleParams () {
|
||||||
const { fontSize, lineNumber, codeBlockTheme } = this.props
|
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd } = this.props
|
||||||
let { fontFamily, codeBlockFontFamily } = this.props
|
let { fontFamily, codeBlockFontFamily } = this.props
|
||||||
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
||||||
? [fontFamily].concat(defaultFontFamily)
|
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily)
|
||||||
: defaultFontFamily
|
: defaultFontFamily
|
||||||
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
|
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
|
||||||
? [codeBlockFontFamily].concat(defaultCodeBlockFontFamily)
|
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
|
||||||
: defaultCodeBlockFontFamily
|
: defaultCodeBlockFontFamily
|
||||||
|
|
||||||
this.setCodeTheme(codeBlockTheme)
|
return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd}
|
||||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setCodeTheme (theme) {
|
applyStyle () {
|
||||||
|
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd} = this.getStyleParams()
|
||||||
|
|
||||||
|
this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme)
|
||||||
|
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
GetCodeThemeLink (theme) {
|
||||||
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
|
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
|
||||||
? theme
|
? theme
|
||||||
: 'elegant'
|
: 'elegant'
|
||||||
this.getWindow().document.getElementById('codeTheme').href = theme.startsWith('solarized')
|
return theme.startsWith('solarized')
|
||||||
? `${appPath}/node_modules/codemirror/theme/solarized.css`
|
? `${appPath}/node_modules/codemirror/theme/solarized.css`
|
||||||
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
|
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
|
||||||
}
|
}
|
||||||
@@ -297,11 +390,7 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock))
|
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value)
|
this.refs.root.contentWindow.document.body.innerHTML = this.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) => {
|
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||||
this.fixDecodedURI(el)
|
this.fixDecodedURI(el)
|
||||||
@@ -317,9 +406,9 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
})
|
})
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => {
|
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => {
|
||||||
el.src = markdown.normalizeLinkText(el.src)
|
el.src = this.markdown.normalizeLinkText(el.src)
|
||||||
if (!/\/:storage/.test(el.src)) return
|
if (!/\/:storage/.test(el.src)) return
|
||||||
el.src = `file:///${markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}`
|
el.src = `file:///${this.markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}`
|
||||||
})
|
})
|
||||||
|
|
||||||
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
|
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
|
||||||
@@ -346,9 +435,9 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
el.innerHTML = ''
|
el.innerHTML = ''
|
||||||
if (codeBlockTheme.indexOf('solarized') === 0) {
|
if (codeBlockTheme.indexOf('solarized') === 0) {
|
||||||
const [refThema, color] = codeBlockTheme.split(' ')
|
const [refThema, color] = codeBlockTheme.split(' ')
|
||||||
el.parentNode.className += ` cm-s-${refThema} cm-s-${color} CodeMirror`
|
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
|
||||||
} else {
|
} else {
|
||||||
el.parentNode.className += ` cm-s-${codeBlockTheme} CodeMirror`
|
el.parentNode.className += ` cm-s-${codeBlockTheme}`
|
||||||
}
|
}
|
||||||
CodeMirror.runMode(content, syntax.mime, el, {
|
CodeMirror.runMode(content, syntax.mime, el, {
|
||||||
tabSize: indentSize
|
tabSize: indentSize
|
||||||
@@ -431,9 +520,20 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
|
|
||||||
handlelinkClick (e) {
|
handlelinkClick (e) {
|
||||||
const noteHash = e.target.href.split('/').pop()
|
const noteHash = e.target.href.split('/').pop()
|
||||||
const regexIsNoteLink = /^(.{20})-(.{20})$/
|
// this will match the new uuid v4 hash and the old hash
|
||||||
|
// e.g.
|
||||||
|
// :note:1c211eb7dcb463de6490 and
|
||||||
|
// :note:7dd23275-f2b4-49cb-9e93-3454daf1af9c
|
||||||
|
const regexIsNoteLink = /^:note:([a-zA-Z0-9-]{20,36})$/
|
||||||
if (regexIsNoteLink.test(noteHash)) {
|
if (regexIsNoteLink.test(noteHash)) {
|
||||||
eventEmitter.emit('list:jump', noteHash)
|
eventEmitter.emit('list:jump', noteHash.replace(':note:', ''))
|
||||||
|
}
|
||||||
|
// this will match the old link format storage.key-note.key
|
||||||
|
// e.g.
|
||||||
|
// 877f99c3268608328037-1c211eb7dcb463de6490
|
||||||
|
const regexIsLegacyNoteLink = /^(.{20})-(.{20})$/
|
||||||
|
if (regexIsLegacyNoteLink.test(noteHash)) {
|
||||||
|
eventEmitter.emit('list:jump', noteHash.split('-')[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,5 +560,6 @@ MarkdownPreview.propTypes = {
|
|||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
showCopyNotification: PropTypes.bool,
|
showCopyNotification: PropTypes.bool,
|
||||||
storagePath: PropTypes.string
|
storagePath: PropTypes.string,
|
||||||
|
smartQuotes: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|||||||
147
browser/components/MarkdownSplitEditor.js
Normal file
147
browser/components/MarkdownSplitEditor.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import CodeEditor from 'browser/components/CodeEditor'
|
||||||
|
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||||
|
import { findStorage } from 'browser/lib/findStorage'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
import styles from './MarkdownSplitEditor.styl'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
|
||||||
|
class MarkdownSplitEditor extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
this.value = props.value
|
||||||
|
this.focus = () => this.refs.code.focus()
|
||||||
|
this.reload = () => this.refs.code.reload()
|
||||||
|
this.userScroll = true
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnChange () {
|
||||||
|
this.value = this.refs.code.value
|
||||||
|
this.props.onChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleScroll (e) {
|
||||||
|
const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document')
|
||||||
|
const codeDoc = _.get(this, 'refs.code.editor.doc')
|
||||||
|
let srcTop, srcHeight, targetTop, targetHeight
|
||||||
|
|
||||||
|
if (this.userScroll) {
|
||||||
|
if (e.doc) {
|
||||||
|
srcTop = _.get(e, 'doc.scrollTop')
|
||||||
|
srcHeight = _.get(e, 'doc.height')
|
||||||
|
targetTop = _.get(previewDoc, 'body.scrollTop')
|
||||||
|
targetHeight = _.get(previewDoc, 'body.scrollHeight')
|
||||||
|
} else {
|
||||||
|
srcTop = _.get(previewDoc, 'body.scrollTop')
|
||||||
|
srcHeight = _.get(previewDoc, 'body.scrollHeight')
|
||||||
|
targetTop = _.get(codeDoc, 'scrollTop')
|
||||||
|
targetHeight = _.get(codeDoc, 'height')
|
||||||
|
}
|
||||||
|
|
||||||
|
const distance = (targetHeight * srcTop / srcHeight) - targetTop
|
||||||
|
const framerate = 1000 / 60
|
||||||
|
const frames = 20
|
||||||
|
const refractory = frames * framerate
|
||||||
|
|
||||||
|
this.userScroll = false
|
||||||
|
|
||||||
|
let frame = 0
|
||||||
|
let scrollPos, time
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
time = frame / frames
|
||||||
|
scrollPos = time < 0.5
|
||||||
|
? 2 * time * time // ease in
|
||||||
|
: -1 + (4 - 2 * time) * time // ease out
|
||||||
|
if (e.doc) _.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance)
|
||||||
|
else _.get(this, 'refs.code.editor').scrollTo(0, targetTop + scrollPos * distance)
|
||||||
|
if (frame >= frames) {
|
||||||
|
clearInterval(timer)
|
||||||
|
setTimeout(() => { this.userScroll = true }, refractory)
|
||||||
|
}
|
||||||
|
frame++
|
||||||
|
}, framerate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCheckboxClick (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
const idMatch = /checkbox-([0-9]+)/
|
||||||
|
const checkedMatch = /\[x\]/i
|
||||||
|
const uncheckedMatch = /\[ \]/
|
||||||
|
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||||
|
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||||
|
const lines = this.refs.code.value
|
||||||
|
.split('\n')
|
||||||
|
|
||||||
|
const targetLine = lines[lineIndex]
|
||||||
|
|
||||||
|
if (targetLine.match(checkedMatch)) {
|
||||||
|
lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
|
||||||
|
}
|
||||||
|
if (targetLine.match(uncheckedMatch)) {
|
||||||
|
lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
|
||||||
|
}
|
||||||
|
this.refs.code.setValue(lines.join('\n'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { config, value, storageKey } = this.props
|
||||||
|
const storage = findStorage(storageKey)
|
||||||
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
|
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||||
|
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||||
|
const previewStyle = {}
|
||||||
|
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
|
||||||
|
return (
|
||||||
|
<div styleName='root'>
|
||||||
|
<CodeEditor
|
||||||
|
styleName='codeEditor'
|
||||||
|
ref='code'
|
||||||
|
mode='GitHub Flavored Markdown'
|
||||||
|
value={value}
|
||||||
|
theme={config.editor.theme}
|
||||||
|
keyMap={config.editor.keyMap}
|
||||||
|
fontFamily={config.editor.fontFamily}
|
||||||
|
fontSize={editorFontSize}
|
||||||
|
displayLineNumbers={config.editor.displayLineNumbers}
|
||||||
|
indentType={config.editor.indentType}
|
||||||
|
indentSize={editorIndentSize}
|
||||||
|
enableRulers={config.editor.enableRulers}
|
||||||
|
rulers={config.editor.rulers}
|
||||||
|
scrollPastEnd={config.editor.scrollPastEnd}
|
||||||
|
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||||
|
storageKey={storageKey}
|
||||||
|
onChange={this.handleOnChange.bind(this)}
|
||||||
|
onScroll={this.handleScroll.bind(this)}
|
||||||
|
/>
|
||||||
|
<MarkdownPreview
|
||||||
|
style={previewStyle}
|
||||||
|
styleName='preview'
|
||||||
|
theme={config.ui.theme}
|
||||||
|
keyMap={config.editor.keyMap}
|
||||||
|
fontSize={config.preview.fontSize}
|
||||||
|
fontFamily={config.preview.fontFamily}
|
||||||
|
codeBlockTheme={config.preview.codeBlockTheme}
|
||||||
|
codeBlockFontFamily={config.editor.fontFamily}
|
||||||
|
lineNumber={config.preview.lineNumber}
|
||||||
|
scrollPastEnd={config.preview.scrollPastEnd}
|
||||||
|
smartQuotes={config.preview.smartQuotes}
|
||||||
|
sanitize={config.preview.sanitize}
|
||||||
|
ref='preview'
|
||||||
|
tabInde='0'
|
||||||
|
value={value}
|
||||||
|
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
||||||
|
onScroll={this.handleScroll.bind(this)}
|
||||||
|
showCopyNotification={config.ui.showCopyNotification}
|
||||||
|
storagePath={storage.path}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(MarkdownSplitEditor, styles)
|
||||||
9
browser/components/MarkdownSplitEditor.styl
Normal file
9
browser/components/MarkdownSplitEditor.styl
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.root
|
||||||
|
width 100%
|
||||||
|
height 100%
|
||||||
|
font-size 30px
|
||||||
|
display flex
|
||||||
|
.codeEditor
|
||||||
|
width 50%
|
||||||
|
.preview
|
||||||
|
width 50%
|
||||||
@@ -9,6 +9,10 @@
|
|||||||
width 34px
|
width 34px
|
||||||
line-height 32px
|
line-height 32px
|
||||||
padding 0
|
padding 0
|
||||||
|
&:hover
|
||||||
|
border: 1px solid #1EC38B;
|
||||||
|
background-color: alpha(#1EC38B, 30%)
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
body[data-theme="white"]
|
body[data-theme="white"]
|
||||||
navWhiteButtonColor()
|
navWhiteButtonColor()
|
||||||
|
|||||||
@@ -46,14 +46,25 @@ const TagElementList = (tags) => {
|
|||||||
* @param {Function} handleDragStart
|
* @param {Function} handleDragStart
|
||||||
* @param {string} dateDisplay
|
* @param {string} dateDisplay
|
||||||
*/
|
*/
|
||||||
const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteContextMenu, handleDragStart, pathname }) => (
|
const NoteItem = ({
|
||||||
|
isActive,
|
||||||
|
note,
|
||||||
|
dateDisplay,
|
||||||
|
handleNoteClick,
|
||||||
|
handleNoteContextMenu,
|
||||||
|
handleDragStart,
|
||||||
|
pathname,
|
||||||
|
storageName,
|
||||||
|
folderName,
|
||||||
|
viewType
|
||||||
|
}) => (
|
||||||
<div styleName={isActive
|
<div styleName={isActive
|
||||||
? 'item--active'
|
? 'item--active'
|
||||||
: 'item'
|
: 'item'
|
||||||
}
|
}
|
||||||
key={`${note.storage}-${note.key}`}
|
key={note.key}
|
||||||
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
|
onClick={e => handleNoteClick(e, note.key)}
|
||||||
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
|
onContextMenu={e => handleNoteContextMenu(e, note.key)}
|
||||||
onDragStart={e => handleDragStart(e, note)}
|
onDragStart={e => handleDragStart(e, note)}
|
||||||
draggable='true'
|
draggable='true'
|
||||||
>
|
>
|
||||||
@@ -68,23 +79,33 @@ const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteCont
|
|||||||
: <span styleName='item-title-empty'>Empty</span>
|
: <span styleName='item-title-empty'>Empty</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
{['ALL', 'STORAGE'].includes(viewType) && <div styleName='item-middle'>
|
||||||
|
<div styleName='item-middle-time'>{dateDisplay}</div>
|
||||||
|
<div styleName='item-middle-app-meta'>
|
||||||
|
<div title={viewType === 'ALL' ? storageName : viewType === 'STORAGE' ? folderName : null} styleName='item-middle-app-meta-label'>
|
||||||
|
{viewType === 'ALL' && storageName}
|
||||||
|
{viewType === 'STORAGE' && folderName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
|
||||||
<div styleName='item-bottom-time'>{dateDisplay}</div>
|
|
||||||
{note.isStarred
|
|
||||||
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
|
|
||||||
}
|
|
||||||
{note.isPinned && !pathname.match(/\/home|\/starred|\/trash/)
|
|
||||||
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
|
|
||||||
}
|
|
||||||
{note.type === 'MARKDOWN_NOTE'
|
|
||||||
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
<div styleName='item-bottom'>
|
<div styleName='item-bottom'>
|
||||||
<div styleName='item-bottom-tagList'>
|
<div styleName='item-bottom-tagList'>
|
||||||
{note.tags.length > 0
|
{note.tags.length > 0
|
||||||
? TagElementList(note.tags)
|
? TagElementList(note.tags)
|
||||||
: <span styleName='item-bottom-tagList-empty' />
|
: <span style={{ fontStyle: 'italic', opacity: 0.5 }} styleName='item-bottom-tagList-empty'>No tags</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{note.isStarred
|
||||||
|
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
|
||||||
|
}
|
||||||
|
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
||||||
|
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
|
||||||
|
}
|
||||||
|
{note.type === 'MARKDOWN_NOTE'
|
||||||
|
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
||||||
|
: ''
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,7 +123,11 @@ NoteItem.propTypes = {
|
|||||||
title: PropTypes.string.isrequired,
|
title: PropTypes.string.isrequired,
|
||||||
tags: PropTypes.array,
|
tags: PropTypes.array,
|
||||||
isStarred: PropTypes.bool.isRequired,
|
isStarred: PropTypes.bool.isRequired,
|
||||||
isTrashed: PropTypes.bool.isRequired
|
isTrashed: PropTypes.bool.isRequired,
|
||||||
|
blog: {
|
||||||
|
blogLink: PropTypes.string,
|
||||||
|
blogId: PropTypes.number
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
handleNoteClick: PropTypes.func.isRequired,
|
handleNoteClick: PropTypes.func.isRequired,
|
||||||
handleNoteContextMenu: PropTypes.func.isRequired,
|
handleNoteContextMenu: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -90,6 +90,26 @@ $control-height = 30px
|
|||||||
font-weight normal
|
font-weight normal
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.item-middle
|
||||||
|
font-size 13px
|
||||||
|
padding-left 2px
|
||||||
|
padding-bottom 2px
|
||||||
|
|
||||||
|
.item-middle-time
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
display inline-block
|
||||||
|
|
||||||
|
.item-middle-app-meta
|
||||||
|
float right
|
||||||
|
.item-middle-app-meta-label
|
||||||
|
opacity 0.4
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
padding 0 3px
|
||||||
|
white-space nowrap
|
||||||
|
text-overflow ellipsis
|
||||||
|
overflow hidden
|
||||||
|
max-width 200px
|
||||||
|
|
||||||
.item-bottom
|
.item-bottom
|
||||||
position relative
|
position relative
|
||||||
bottom 0px
|
bottom 0px
|
||||||
@@ -97,7 +117,7 @@ $control-height = 30px
|
|||||||
font-size 12px
|
font-size 12px
|
||||||
line-height 20px
|
line-height 20px
|
||||||
overflow ellipsis
|
overflow ellipsis
|
||||||
display flex
|
display block
|
||||||
|
|
||||||
.item-bottom-tagList
|
.item-bottom-tagList
|
||||||
flex 1
|
flex 1
|
||||||
@@ -125,10 +145,8 @@ $control-height = 30px
|
|||||||
|
|
||||||
.item-star
|
.item-star
|
||||||
position absolute
|
position absolute
|
||||||
right -6px
|
right 2px
|
||||||
bottom 23px
|
top 5px
|
||||||
width 16px
|
|
||||||
height 16px
|
|
||||||
color alpha($ui-favorite-star-button-color, 60%)
|
color alpha($ui-favorite-star-button-color, 60%)
|
||||||
font-size 12px
|
font-size 12px
|
||||||
padding 0
|
padding 0
|
||||||
@@ -136,10 +154,8 @@ $control-height = 30px
|
|||||||
|
|
||||||
.item-pin
|
.item-pin
|
||||||
position absolute
|
position absolute
|
||||||
right 0px
|
right 25px
|
||||||
bottom 2px
|
top 7px
|
||||||
width 34px
|
|
||||||
height 34px
|
|
||||||
color #E54D42
|
color #E54D42
|
||||||
font-size 14px
|
font-size 14px
|
||||||
padding 0
|
padding 0
|
||||||
@@ -192,7 +208,7 @@ body[data-theme="dark"]
|
|||||||
.item-bottom-tagList-item
|
.item-bottom-tagList-item
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
background-color alpha(white, 10%)
|
background-color alpha(white, 10%)
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
.item-wrapper
|
.item-wrapper
|
||||||
border-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
border-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||||
@@ -231,3 +247,77 @@ body[data-theme="dark"]
|
|||||||
.item-bottom-tagList-empty
|
.item-bottom-tagList-empty
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
vertical-align middle
|
vertical-align middle
|
||||||
|
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.root
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
|
||||||
|
.item
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
&:hover
|
||||||
|
transition 0.15s
|
||||||
|
// background-color alpha($ui-solarized-dark-noteList-backgroundColor, 20%)
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.item-title
|
||||||
|
.item-title-icon
|
||||||
|
.item-bottom-time
|
||||||
|
transition 0.15s
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.item-bottom-tagList-item
|
||||||
|
transition 0.15s
|
||||||
|
background-color alpha($ui-solarized-dark-noteList-backgroundColor, 20%)
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:active
|
||||||
|
transition 0.15s
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.item-title
|
||||||
|
.item-title-icon
|
||||||
|
.item-bottom-time
|
||||||
|
transition 0.15s
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.item-bottom-tagList-item
|
||||||
|
transition 0.15s
|
||||||
|
background-color alpha($ui-solarized-dark-noteList-backgroundColor, 10%)
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.item-wrapper
|
||||||
|
border-color alpha($ui-solarized-dark-button--active-backgroundColor, 60%)
|
||||||
|
|
||||||
|
.item--active
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
.item-wrapper
|
||||||
|
border-color transparent
|
||||||
|
.item-title
|
||||||
|
.item-title-icon
|
||||||
|
.item-bottom-time
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.item-bottom-tagList-item
|
||||||
|
background-color alpha(white, 10%)
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:hover
|
||||||
|
// background-color alpha($ui-solarized-dark-button--active-backgroundColor, 60%)
|
||||||
|
color #c0392b
|
||||||
|
.item-bottom-tagList-item
|
||||||
|
background-color alpha(#fff, 20%)
|
||||||
|
|
||||||
|
.item-title
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.item-title-icon
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.item-title-empty
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.item-bottom-tagList-item
|
||||||
|
background-color alpha($ui-dark-button--active-backgroundColor, 40%)
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.item-bottom-tagList-empty
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
vertical-align middle
|
||||||
|
|||||||
@@ -14,14 +14,23 @@ import styles from './NoteItemSimple.styl'
|
|||||||
* @param {Function} handleNoteContextMenu
|
* @param {Function} handleNoteContextMenu
|
||||||
* @param {Function} handleDragStart
|
* @param {Function} handleDragStart
|
||||||
*/
|
*/
|
||||||
const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu, handleDragStart }) => (
|
const NoteItemSimple = ({
|
||||||
|
isActive,
|
||||||
|
isAllNotesView,
|
||||||
|
note,
|
||||||
|
handleNoteClick,
|
||||||
|
handleNoteContextMenu,
|
||||||
|
handleDragStart,
|
||||||
|
pathname,
|
||||||
|
storage
|
||||||
|
}) => (
|
||||||
<div styleName={isActive
|
<div styleName={isActive
|
||||||
? 'item-simple--active'
|
? 'item-simple--active'
|
||||||
: 'item-simple'
|
: 'item-simple'
|
||||||
}
|
}
|
||||||
key={`${note.storage}-${note.key}`}
|
key={note.key}
|
||||||
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
|
onClick={e => handleNoteClick(e, note.key)}
|
||||||
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
|
onContextMenu={e => handleNoteContextMenu(e, note.key)}
|
||||||
onDragStart={e => handleDragStart(e, note)}
|
onDragStart={e => handleDragStart(e, note)}
|
||||||
draggable='true'
|
draggable='true'
|
||||||
>
|
>
|
||||||
@@ -30,10 +39,19 @@ const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu
|
|||||||
? <i styleName='item-simple-title-icon' className='fa fa-fw fa-code' />
|
? <i styleName='item-simple-title-icon' className='fa fa-fw fa-code' />
|
||||||
: <i styleName='item-simple-title-icon' className='fa fa-fw fa-file-text-o' />
|
: <i styleName='item-simple-title-icon' className='fa fa-fw fa-file-text-o' />
|
||||||
}
|
}
|
||||||
|
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
||||||
|
? <i styleName='item-pin' className='fa fa-thumb-tack' />
|
||||||
|
: ''
|
||||||
|
}
|
||||||
{note.title.trim().length > 0
|
{note.title.trim().length > 0
|
||||||
? note.title
|
? note.title
|
||||||
: <span styleName='item-simple-title-empty'>Empty</span>
|
: <span styleName='item-simple-title-empty'>Empty</span>
|
||||||
}
|
}
|
||||||
|
{isAllNotesView && <div styleName='item-simple-right'>
|
||||||
|
<span styleName='item-simple-right-storageName'>
|
||||||
|
{storage.name}
|
||||||
|
</span>
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ $control-height = 30px
|
|||||||
.item-simple-title
|
.item-simple-title
|
||||||
font-size 13px
|
font-size 13px
|
||||||
height 40px
|
height 40px
|
||||||
|
padding-right 20px
|
||||||
box-sizing border-box
|
box-sizing border-box
|
||||||
line-height 24px
|
line-height 24px
|
||||||
padding-top 8px
|
padding-top 8px
|
||||||
@@ -67,6 +68,15 @@ $control-height = 30px
|
|||||||
font-weight normal
|
font-weight normal
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.item-pin
|
||||||
|
position absolute
|
||||||
|
right 0px
|
||||||
|
top 12px
|
||||||
|
color #E54D42
|
||||||
|
font-size 14px
|
||||||
|
padding 0
|
||||||
|
border-radius 17px
|
||||||
|
|
||||||
body[data-theme="white"]
|
body[data-theme="white"]
|
||||||
.item-simple
|
.item-simple
|
||||||
background-color $ui-white-noteList-backgroundColor
|
background-color $ui-white-noteList-backgroundColor
|
||||||
@@ -114,7 +124,7 @@ body[data-theme="dark"]
|
|||||||
.item-simple-bottom-tagList-item
|
.item-simple-bottom-tagList-item
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
background-color alpha(white, 10%)
|
background-color alpha(white, 10%)
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
.item-simple--active
|
.item-simple--active
|
||||||
border-color $ui-dark-borderColor
|
border-color $ui-dark-borderColor
|
||||||
@@ -143,3 +153,62 @@ body[data-theme="dark"]
|
|||||||
|
|
||||||
.item-simple-title-empty
|
.item-simple-title-empty
|
||||||
color $ui-dark-inactive-text-color
|
color $ui-dark-inactive-text-color
|
||||||
|
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.root
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
|
||||||
|
.item-simple
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
&:hover
|
||||||
|
transition 0.15s
|
||||||
|
// background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.item-simple-title
|
||||||
|
.item-simple-title-icon
|
||||||
|
.item-simple-bottom-time
|
||||||
|
transition 0.15s
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.item-simple-bottom-tagList-item
|
||||||
|
transition 0.15s
|
||||||
|
background-color alpha(#fff, 20%)
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:active
|
||||||
|
transition 0.15s
|
||||||
|
background-color $ui-solarized-dark-button--active-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.item-simple-title
|
||||||
|
.item-simple-title-icon
|
||||||
|
.item-simple-bottom-time
|
||||||
|
transition 0.15s
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.item-simple-bottom-tagList-item
|
||||||
|
transition 0.15s
|
||||||
|
background-color alpha(white, 10%)
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.item-simple--active
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
background-color $ui-solarized-dark-button--active-backgroundColor
|
||||||
|
.item-simple-wrapper
|
||||||
|
border-color transparent
|
||||||
|
.item-simple-title
|
||||||
|
.item-simple-title-icon
|
||||||
|
.item-simple-bottom-time
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.item-simple-bottom-tagList-item
|
||||||
|
background-color alpha(white, 10%)
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:hover
|
||||||
|
// background-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||||
|
color #c0392b
|
||||||
|
.item-simple-bottom-tagList-item
|
||||||
|
background-color alpha(#fff, 20%)
|
||||||
|
.item-simple-right
|
||||||
|
float right
|
||||||
|
.item-simple-right-storageName
|
||||||
|
padding-left 4px
|
||||||
|
opacity 0.4
|
||||||
|
|||||||
@@ -29,3 +29,15 @@ body[data-theme="dark"]
|
|||||||
transition 0.2s
|
transition 0.2s
|
||||||
&:hover
|
&:hover
|
||||||
color #5CB85C
|
color #5CB85C
|
||||||
|
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.notification-area
|
||||||
|
background-color none
|
||||||
|
|
||||||
|
.notification-link
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
border none
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
&:hover
|
||||||
|
color #5CB85C
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './SideNavFilter.styl'
|
import styles from './SideNavFilter.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean} isFolded
|
* @param {boolean} isFolded
|
||||||
@@ -17,7 +18,7 @@ import styles from './SideNavFilter.styl'
|
|||||||
const SideNavFilter = ({
|
const SideNavFilter = ({
|
||||||
isFolded, isHomeActive, handleAllNotesButtonClick,
|
isFolded, isHomeActive, handleAllNotesButtonClick,
|
||||||
isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick, counterDelNote,
|
isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick, counterDelNote,
|
||||||
counterTotalNote, counterStarredNote
|
counterTotalNote, counterStarredNote, handleFilterButtonContextMenu
|
||||||
}) => (
|
}) => (
|
||||||
<div styleName={isFolded ? 'menu--folded' : 'menu'}>
|
<div styleName={isFolded ? 'menu--folded' : 'menu'}>
|
||||||
|
|
||||||
@@ -26,12 +27,12 @@ const SideNavFilter = ({
|
|||||||
>
|
>
|
||||||
<div styleName='iconWrap'>
|
<div styleName='iconWrap'>
|
||||||
<img src={isHomeActive
|
<img src={isHomeActive
|
||||||
? '../resources/icon/icon-all-active.svg'
|
? '../resources/icon/icon-all-active.svg'
|
||||||
: '../resources/icon/icon-all.svg'
|
: '../resources/icon/icon-all.svg'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span styleName='menu-button-label'>All Notes</span>
|
<span styleName='menu-button-label'>{i18n.__('All Notes')}</span>
|
||||||
<span styleName='counters'>{counterTotalNote}</span>
|
<span styleName='counters'>{counterTotalNote}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -40,12 +41,12 @@ const SideNavFilter = ({
|
|||||||
>
|
>
|
||||||
<div styleName='iconWrap'>
|
<div styleName='iconWrap'>
|
||||||
<img src={isStarredActive
|
<img src={isStarredActive
|
||||||
? '../resources/icon/icon-star-active.svg'
|
? '../resources/icon/icon-star-active.svg'
|
||||||
: '../resources/icon/icon-star.svg'
|
: '../resources/icon/icon-star-sidenav.svg'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span styleName='menu-button-label'>Starred</span>
|
<span styleName='menu-button-label'>{i18n.__('Starred')}</span>
|
||||||
<span styleName='counters'>{counterStarredNote}</span>
|
<span styleName='counters'>{counterStarredNote}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -54,12 +55,12 @@ const SideNavFilter = ({
|
|||||||
>
|
>
|
||||||
<div styleName='iconWrap'>
|
<div styleName='iconWrap'>
|
||||||
<img src={isTrashedActive
|
<img src={isTrashedActive
|
||||||
? '../resources/icon/icon-trash-active.svg'
|
? '../resources/icon/icon-trash-active.svg'
|
||||||
: '../resources/icon/icon-trash.svg'
|
: '../resources/icon/icon-trash-sidenav.svg'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span styleName='menu-button-label'>Trash</span>
|
<span onContextMenu={handleFilterButtonContextMenu} styleName='menu-button-label'>{i18n.__('Trash')}</span>
|
||||||
<span styleName='counters'>{counterDelNote}</span>
|
<span styleName='counters'>{counterDelNote}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
|
|
||||||
.menu--folded
|
.menu--folded
|
||||||
@extend .menu
|
@extend .menu
|
||||||
.menu-button, .menu-button--active
|
.menu-button, .menu-button--active, .menu-button-star--active, .menu-button-trash--active
|
||||||
text-align center
|
text-align center
|
||||||
padding 0 12px
|
padding 0 12px
|
||||||
&:hover .menu-button-label
|
&:hover .menu-button-label
|
||||||
@@ -92,7 +92,6 @@ body[data-theme="white"]
|
|||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
.menu-button--active
|
.menu-button--active
|
||||||
@extend .menu-button
|
|
||||||
color #e74c3c
|
color #e74c3c
|
||||||
background-color $ui-button--active-backgroundColor
|
background-color $ui-button--active-backgroundColor
|
||||||
.menu-button-label
|
.menu-button-label
|
||||||
@@ -109,7 +108,6 @@ body[data-theme="white"]
|
|||||||
color $ui-text-color
|
color $ui-text-color
|
||||||
|
|
||||||
.menu-button-star--active
|
.menu-button-star--active
|
||||||
@extend .menu-button
|
|
||||||
color #F9BF3B
|
color #F9BF3B
|
||||||
background-color $ui-button--active-backgroundColor
|
background-color $ui-button--active-backgroundColor
|
||||||
.menu-button-label
|
.menu-button-label
|
||||||
@@ -126,7 +124,6 @@ body[data-theme="white"]
|
|||||||
color $ui-text-color
|
color $ui-text-color
|
||||||
|
|
||||||
.menu-button-trash--active
|
.menu-button-trash--active
|
||||||
@extend .menu-button
|
|
||||||
color #5D9E36
|
color #5D9E36
|
||||||
background-color $ui-button--active-backgroundColor
|
background-color $ui-button--active-backgroundColor
|
||||||
.menu-button-label
|
.menu-button-label
|
||||||
@@ -182,4 +179,47 @@ body[data-theme="dark"]
|
|||||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||||
color #5D9E36
|
color #5D9E36
|
||||||
.menu-button-label
|
.menu-button-label
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.menu-button
|
||||||
|
&:active
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.menu-button--active
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
.menu-button-label
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.menu-button-label
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.menu-button-star--active
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
.menu-button-label
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.menu-button-label
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.menu-button-trash--active
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
.menu-button-label
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.menu-button-label
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
@@ -85,6 +85,15 @@ class SnippetTab extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDragStart (e) {
|
||||||
|
e.dataTransfer.dropEffect = 'move'
|
||||||
|
this.props.onDragStart(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDrop (e) {
|
||||||
|
this.props.onDrop(e)
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { isActive, snippet, isDeletable } = this.props
|
const { isActive, snippet, isDeletable } = this.props
|
||||||
return (
|
return (
|
||||||
@@ -98,6 +107,9 @@ class SnippetTab extends React.Component {
|
|||||||
onClick={(e) => this.handleClick(e)}
|
onClick={(e) => this.handleClick(e)}
|
||||||
onDoubleClick={(e) => this.handleRenameClick(e)}
|
onDoubleClick={(e) => this.handleRenameClick(e)}
|
||||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||||
|
onDragStart={(e) => this.handleDragStart(e)}
|
||||||
|
onDrop={(e) => this.handleDrop(e)}
|
||||||
|
draggable='true'
|
||||||
>
|
>
|
||||||
{snippet.name.trim().length > 0
|
{snippet.name.trim().length > 0
|
||||||
? snippet.name
|
? snippet.name
|
||||||
@@ -127,6 +139,7 @@ class SnippetTab extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SnippetTab.propTypes = {
|
SnippetTab.propTypes = {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(SnippetTab, styles)
|
export default CSSModules(SnippetTab, styles)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.root
|
.root
|
||||||
position relative
|
position relative
|
||||||
flex 1
|
flex 1
|
||||||
|
min-width 70px
|
||||||
overflow hidden
|
overflow hidden
|
||||||
&:hover
|
&:hover
|
||||||
.deleteButton
|
.deleteButton
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
height 29px
|
height 29px
|
||||||
overflow ellipsis
|
overflow ellipsis
|
||||||
text-align left
|
text-align left
|
||||||
padding-right 30px
|
padding-right 23px
|
||||||
border none
|
border none
|
||||||
background-color transparent
|
background-color transparent
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
@@ -38,7 +39,7 @@
|
|||||||
text-align center
|
text-align center
|
||||||
border none
|
border none
|
||||||
padding 0
|
padding 0
|
||||||
color transparent
|
color $ui-inactive-text-color
|
||||||
background-color transparent
|
background-color transparent
|
||||||
border-radius 2px
|
border-radius 2px
|
||||||
|
|
||||||
@@ -89,3 +90,50 @@ body[data-theme="dark"]
|
|||||||
.input
|
.input
|
||||||
background-color $ui-dark-button--hover-backgroundColor
|
background-color $ui-dark-button--hover-backgroundColor
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.deleteButton
|
||||||
|
color alpha($ui-dark-text-color, 30%)
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.root
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
&:hover
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
.deleteButton
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:hover
|
||||||
|
background-color darken($ui-solarized-dark-noteDetail-backgroundColor, 15%)
|
||||||
|
&:active
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
background-color $ui-dark-button--active-backgroundColor
|
||||||
|
|
||||||
|
.root--active
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
&:hover
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
.deleteButton
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:hover
|
||||||
|
background-color darken($ui-solarized-dark-noteDetail-backgroundColor, 15%)
|
||||||
|
&:active
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
background-color $ui-dark-button--active-backgroundColor
|
||||||
|
|
||||||
|
.button
|
||||||
|
border none
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
background-color transparent
|
||||||
|
transition color background-color 0.15s
|
||||||
|
border-left 4px solid transparent
|
||||||
|
&:hover
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
.input
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.deleteButton
|
||||||
|
color alpha($ui-solarized-dark-text-color, 30%)
|
||||||
@@ -6,6 +6,18 @@ import React from 'react'
|
|||||||
import styles from './StorageItem.styl'
|
import styles from './StorageItem.styl'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
import { SortableHandle } from 'react-sortable-hoc'
|
||||||
|
|
||||||
|
const DraggableIcon = SortableHandle(({ className }) => (
|
||||||
|
<i className={`fa ${className}`} />
|
||||||
|
))
|
||||||
|
|
||||||
|
const FolderIcon = ({ className, color, isActive }) => {
|
||||||
|
const iconStyle = isActive ? 'fa-folder-open-o' : 'fa-folder-o'
|
||||||
|
return (
|
||||||
|
<i className={`fa ${iconStyle} ${className}`} style={{ color: color }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean} isActive
|
* @param {boolean} isActive
|
||||||
@@ -21,34 +33,54 @@ import _ from 'lodash'
|
|||||||
* @return {React.Component}
|
* @return {React.Component}
|
||||||
*/
|
*/
|
||||||
const StorageItem = ({
|
const StorageItem = ({
|
||||||
isActive, handleButtonClick, handleContextMenu, folderName,
|
styles,
|
||||||
folderColor, isFolded, noteCount, handleDrop, handleDragEnter, handleDragLeave
|
isActive,
|
||||||
}) => (
|
handleButtonClick,
|
||||||
<button styleName={isActive
|
handleContextMenu,
|
||||||
? 'folderList-item--active'
|
folderName,
|
||||||
: 'folderList-item'
|
folderColor,
|
||||||
}
|
isFolded,
|
||||||
onClick={handleButtonClick}
|
noteCount,
|
||||||
onContextMenu={handleContextMenu}
|
handleDrop,
|
||||||
onDrop={handleDrop}
|
handleDragEnter,
|
||||||
onDragEnter={handleDragEnter}
|
handleDragLeave
|
||||||
onDragLeave={handleDragLeave}
|
}) => {
|
||||||
>
|
return (
|
||||||
<span styleName={isFolded
|
<button
|
||||||
? 'folderList-item-name--folded' : 'folderList-item-name'
|
styleName={isActive ? 'folderList-item--active' : 'folderList-item'}
|
||||||
}>
|
onClick={handleButtonClick}
|
||||||
<text style={{color: folderColor, paddingRight: '10px'}}>{isActive ? <i className='fa fa-folder-open-o' /> : <i className='fa fa-folder-o' />}</text>{isFolded ? _.truncate(folderName, {length: 1, omission: ''}) : folderName}
|
onContextMenu={handleContextMenu}
|
||||||
</span>
|
onDrop={handleDrop}
|
||||||
{(!isFolded && _.isNumber(noteCount)) &&
|
onDragEnter={handleDragEnter}
|
||||||
<span styleName='folderList-item-noteCount'>{noteCount}</span>
|
onDragLeave={handleDragLeave}
|
||||||
}
|
>
|
||||||
{isFolded &&
|
{!isFolded && (
|
||||||
<span styleName='folderList-item-tooltip'>
|
<DraggableIcon className={styles['folderList-item-reorder']} />
|
||||||
{folderName}
|
)}
|
||||||
|
<span
|
||||||
|
styleName={
|
||||||
|
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FolderIcon
|
||||||
|
styleName='folderList-item-icon'
|
||||||
|
color={folderColor}
|
||||||
|
isActive={isActive}
|
||||||
|
/>
|
||||||
|
{isFolded
|
||||||
|
? _.truncate(folderName, { length: 1, omission: '' })
|
||||||
|
: folderName}
|
||||||
</span>
|
</span>
|
||||||
}
|
{!isFolded &&
|
||||||
</button>
|
_.isNumber(noteCount) && (
|
||||||
)
|
<span styleName='folderList-item-noteCount'>{noteCount}</span>
|
||||||
|
)}
|
||||||
|
{isFolded && (
|
||||||
|
<span styleName='folderList-item-tooltip'>{folderName}</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
StorageItem.propTypes = {
|
StorageItem.propTypes = {
|
||||||
isActive: PropTypes.bool.isRequired,
|
isActive: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
border none
|
border none
|
||||||
overflow ellipsis
|
overflow ellipsis
|
||||||
font-size 14px
|
font-size 14px
|
||||||
|
align-items: center
|
||||||
&:first-child
|
&:first-child
|
||||||
margin-top 0
|
margin-top 0
|
||||||
&:hover
|
&:hover
|
||||||
@@ -22,7 +23,7 @@
|
|||||||
&:active
|
&:active
|
||||||
color $$ui-button-default-color
|
color $$ui-button-default-color
|
||||||
background-color alpha($ui-button-default--active-backgroundColor, 20%)
|
background-color alpha($ui-button-default--active-backgroundColor, 20%)
|
||||||
|
|
||||||
.folderList-item--active
|
.folderList-item--active
|
||||||
@extend .folderList-item
|
@extend .folderList-item
|
||||||
color #1EC38B
|
color #1EC38B
|
||||||
@@ -34,9 +35,7 @@
|
|||||||
.folderList-item-name
|
.folderList-item-name
|
||||||
display block
|
display block
|
||||||
flex 1
|
flex 1
|
||||||
padding 0 12px
|
padding-right: 10px
|
||||||
height 26px
|
|
||||||
line-height 26px
|
|
||||||
border-width 0 0 0 2px
|
border-width 0 0 0 2px
|
||||||
border-style solid
|
border-style solid
|
||||||
border-color transparent
|
border-color transparent
|
||||||
@@ -69,9 +68,20 @@
|
|||||||
.folderList-item-name--folded
|
.folderList-item-name--folded
|
||||||
@extend .folderList-item-name
|
@extend .folderList-item-name
|
||||||
padding-left 7px
|
padding-left 7px
|
||||||
text
|
.folderList-item-icon
|
||||||
font-size 9px
|
font-size 9px
|
||||||
|
|
||||||
|
.folderList-item-icon
|
||||||
|
padding-right: 10px
|
||||||
|
|
||||||
|
.folderList-item-reorder
|
||||||
|
font-size: 9px
|
||||||
|
padding: 10px 8px 10px 9px;
|
||||||
|
color: rgba(147, 147, 149, 0.3)
|
||||||
|
cursor: ns-resize
|
||||||
|
&:before
|
||||||
|
content: "\f142 \f142"
|
||||||
|
|
||||||
body[data-theme="white"]
|
body[data-theme="white"]
|
||||||
.folderList-item
|
.folderList-item
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
@@ -108,4 +118,23 @@ body[data-theme="dark"]
|
|||||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||||
&:hover
|
&:hover
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.folderList-item
|
||||||
|
&:hover
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:active
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
|
||||||
|
.folderList-item--active
|
||||||
|
@extend .folderList-item
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
&:active
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
&:hover
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
* @param {bool} isActive
|
* @param {bool} isActive
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const TagListItem = ({name, handleClickTagListItem, isActive}) => (
|
const TagListItem = ({name, handleClickTagListItem, isActive, count}) => (
|
||||||
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
|
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
|
||||||
<span styleName='tagList-item-name'>
|
<span styleName='tagList-item-name'>
|
||||||
{`# ${name}`}
|
{`# ${name}`}
|
||||||
|
<span styleName='tagList-item-count'> {count}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -48,6 +48,9 @@
|
|||||||
overflow hidden
|
overflow hidden
|
||||||
text-overflow ellipsis
|
text-overflow ellipsis
|
||||||
|
|
||||||
|
.tagList-item-count
|
||||||
|
padding 0 3px
|
||||||
|
|
||||||
body[data-theme="white"]
|
body[data-theme="white"]
|
||||||
.tagList-item
|
.tagList-item
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
@@ -63,6 +66,8 @@ body[data-theme="white"]
|
|||||||
color $ui-text-color
|
color $ui-text-color
|
||||||
&:hover
|
&:hover
|
||||||
background-color alpha($ui-button--active-backgroundColor, 60%)
|
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||||
|
.tagList-item-count
|
||||||
|
color $ui-text-color
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.tagList-item
|
.tagList-item
|
||||||
@@ -81,4 +86,6 @@ body[data-theme="dark"]
|
|||||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||||
&:hover
|
&:hover
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||||
|
.tagList-item-count
|
||||||
|
color $ui-dark-button--active-color
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ const TodoListPercentage = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<div styleName='percentageBar' style={{display: isNaN(percentageOfTodo) ? 'none' : ''}}>
|
<div styleName='percentageBar' style={{display: isNaN(percentageOfTodo) ? 'none' : ''}}>
|
||||||
<div styleName='progressBar' style={{width: `${percentageOfTodo}%`}}>
|
<div styleName='progressBar' style={{width: `${percentageOfTodo}%`}}>
|
||||||
<p styleName='percentageText'>{percentageOfTodo}%</p>
|
<div styleName='progressBarInner'>
|
||||||
|
<p styleName='percentageText'>{percentageOfTodo}%</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.percentageBar
|
.percentageBar
|
||||||
position absolute
|
position absolute
|
||||||
top 50px
|
top 72px
|
||||||
right 0px
|
right 0px
|
||||||
left 0px
|
left 0px
|
||||||
background-color #DADFE1
|
background-color #DADFE1
|
||||||
@@ -16,17 +16,36 @@
|
|||||||
border-radius 2px
|
border-radius 2px
|
||||||
transition 0.4s cubic-bezier(0.4, 0.4, 0, 1)
|
transition 0.4s cubic-bezier(0.4, 0.4, 0, 1)
|
||||||
|
|
||||||
|
.progressBarInner
|
||||||
|
padding 0 10px
|
||||||
|
min-width 1px
|
||||||
|
height 100%
|
||||||
|
display -webkit-box
|
||||||
|
display box
|
||||||
|
justify-content center
|
||||||
|
align-items center
|
||||||
|
|
||||||
|
|
||||||
.percentageText
|
.percentageText
|
||||||
color #f4f4f4
|
color #f4f4f4
|
||||||
padding: 2px 43%
|
|
||||||
font-weight 600
|
font-weight 600
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.percentageBar
|
.percentageBar
|
||||||
background-color #363A3D
|
background-color #444444
|
||||||
|
|
||||||
.progressBar
|
.progressBar
|
||||||
background-color: alpha(#939395, 50%)
|
background-color: #1EC38B
|
||||||
|
|
||||||
.percentageText
|
.percentageText
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.percentageBar
|
||||||
|
background-color #002b36
|
||||||
|
|
||||||
|
.progressBar
|
||||||
|
background-color: #2aa198
|
||||||
|
|
||||||
|
.percentageText
|
||||||
|
color #fdf6e3
|
||||||
@@ -58,7 +58,7 @@ body
|
|||||||
.katex
|
.katex
|
||||||
font 400 1.2em 'KaTeX_Main'
|
font 400 1.2em 'KaTeX_Main'
|
||||||
line-height 1.2em
|
line-height 1.2em
|
||||||
white-space nowrap
|
white-space initial
|
||||||
text-indent 0
|
text-indent 0
|
||||||
.katex .mfrac>.vlist>span:nth-child(2)
|
.katex .mfrac>.vlist>span:nth-child(2)
|
||||||
top 0 !important
|
top 0 !important
|
||||||
@@ -76,7 +76,10 @@ body
|
|||||||
justify-content left
|
justify-content left
|
||||||
li
|
li
|
||||||
label.taskListItem
|
label.taskListItem
|
||||||
margin-left -2em
|
margin-left -1.8em
|
||||||
|
&.checked
|
||||||
|
text-decoration line-through
|
||||||
|
opacity 0.5
|
||||||
div.math-rendered
|
div.math-rendered
|
||||||
text-align center
|
text-align center
|
||||||
.math-failed
|
.math-failed
|
||||||
@@ -102,7 +105,6 @@ a
|
|||||||
border-radius 5px
|
border-radius 5px
|
||||||
margin -5px
|
margin -5px
|
||||||
transition .1s
|
transition .1s
|
||||||
display inline-block
|
|
||||||
img
|
img
|
||||||
vertical-align sub
|
vertical-align sub
|
||||||
&:hover
|
&:hover
|
||||||
@@ -117,6 +119,7 @@ hr
|
|||||||
margin 15px 0
|
margin 15px 0
|
||||||
h1, h2, h3, h4, h5, h6
|
h1, h2, h3, h4, h5, h6
|
||||||
font-weight bold
|
font-weight bold
|
||||||
|
word-wrap break-word
|
||||||
h1
|
h1
|
||||||
font-size 2.55em
|
font-size 2.55em
|
||||||
padding-bottom 0.3em
|
padding-bottom 0.3em
|
||||||
@@ -154,6 +157,7 @@ p
|
|||||||
line-height 1.6em
|
line-height 1.6em
|
||||||
margin 0 0 1em
|
margin 0 0 1em
|
||||||
white-space pre-line
|
white-space pre-line
|
||||||
|
word-wrap break-word
|
||||||
img
|
img
|
||||||
max-width 100%
|
max-width 100%
|
||||||
strong, b
|
strong, b
|
||||||
@@ -174,6 +178,8 @@ ul
|
|||||||
margin-bottom 1em
|
margin-bottom 1em
|
||||||
li
|
li
|
||||||
display list-item
|
display list-item
|
||||||
|
&.taskListItem
|
||||||
|
list-style none
|
||||||
p
|
p
|
||||||
margin 0
|
margin 0
|
||||||
&>li>ul, &>li>ol
|
&>li>ul, &>li>ol
|
||||||
@@ -214,6 +220,7 @@ pre
|
|||||||
background-color white
|
background-color white
|
||||||
&.CodeMirror
|
&.CodeMirror
|
||||||
height initial
|
height initial
|
||||||
|
flex-wrap wrap
|
||||||
&>code
|
&>code
|
||||||
flex 1
|
flex 1
|
||||||
overflow-x auto
|
overflow-x auto
|
||||||
@@ -223,6 +230,13 @@ pre
|
|||||||
padding 0
|
padding 0
|
||||||
border none
|
border none
|
||||||
border-radius 0
|
border-radius 0
|
||||||
|
&>span.filename
|
||||||
|
width 100%
|
||||||
|
border-radius: 5px 0px 0px 0px
|
||||||
|
margin -8px 100% 8px -8px
|
||||||
|
padding 0px 6px
|
||||||
|
background-color #777;
|
||||||
|
color white
|
||||||
&>span.lineNumber
|
&>span.lineNumber
|
||||||
display none
|
display none
|
||||||
font-size 1em
|
font-size 1em
|
||||||
@@ -329,4 +343,31 @@ body[data-theme="dark"]
|
|||||||
border-right solid 1px themeDarkTableBorder
|
border-right solid 1px themeDarkTableBorder
|
||||||
kbd
|
kbd
|
||||||
background-color themeDarkBorder
|
background-color themeDarkBorder
|
||||||
color themeDarkText
|
color themeDarkText
|
||||||
|
|
||||||
|
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
|
||||||
|
themeSolarizedDarkTableHead = themeSolarizedDarkTableEven
|
||||||
|
themeSolarizedDarkTableBorder = themeDarkBorder
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
border-color themeDarkBorder
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
table
|
||||||
|
thead
|
||||||
|
tr
|
||||||
|
background-color themeSolarizedDarkTableHead
|
||||||
|
th
|
||||||
|
border-color themeSolarizedDarkTableBorder
|
||||||
|
&:last-child
|
||||||
|
border-right solid 1px themeSolarizedDarkTableBorder
|
||||||
|
tbody
|
||||||
|
tr:nth-child(2n + 1)
|
||||||
|
background-color themeSolarizedDarkTableOdd
|
||||||
|
tr:nth-child(2n)
|
||||||
|
background-color themeSolarizedDarkTableEven
|
||||||
|
td
|
||||||
|
border-color themeSolarizedDarkTableBorder
|
||||||
|
&:last-child
|
||||||
|
border-right solid 1px themeSolarizedDarkTableBorder
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
$search-height = 50px
|
|
||||||
$nav-width = 175px
|
|
||||||
$list-width = 250px
|
|
||||||
|
|
||||||
.root
|
|
||||||
absolute top left right bottom
|
|
||||||
|
|
||||||
.search
|
|
||||||
height $search-height
|
|
||||||
padding 10px
|
|
||||||
box-sizing border-box
|
|
||||||
border-bottom $ui-border
|
|
||||||
text-align center
|
|
||||||
|
|
||||||
.search-input
|
|
||||||
height 30px
|
|
||||||
width 100%
|
|
||||||
margin 0 auto
|
|
||||||
font-size 18px
|
|
||||||
border none
|
|
||||||
outline none
|
|
||||||
text-align center
|
|
||||||
background-color transparent
|
|
||||||
|
|
||||||
.result
|
|
||||||
absolute left right bottom
|
|
||||||
top $search-height
|
|
||||||
background-color $ui-noteDetail-backgroundColor
|
|
||||||
|
|
||||||
.result-nav
|
|
||||||
user-select none
|
|
||||||
absolute left top bottom
|
|
||||||
width $nav-width
|
|
||||||
background-color $ui-backgroundColor
|
|
||||||
|
|
||||||
.result-nav-filter
|
|
||||||
margin-bottom 10px
|
|
||||||
|
|
||||||
.result-nav-filter-option
|
|
||||||
height 25px
|
|
||||||
line-height 25px
|
|
||||||
padding 0 10px
|
|
||||||
label
|
|
||||||
cursor pointer
|
|
||||||
|
|
||||||
.result-nav-menu
|
|
||||||
navButtonColor()
|
|
||||||
height 32px
|
|
||||||
padding 0 10px
|
|
||||||
font-size 14px
|
|
||||||
width 100%
|
|
||||||
outline none
|
|
||||||
text-align left
|
|
||||||
line-height 32px
|
|
||||||
box-sizing border-box
|
|
||||||
cursor pointer
|
|
||||||
|
|
||||||
.result-nav-menu--active
|
|
||||||
@extend .result-nav-menu
|
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
color $ui-button--active-color
|
|
||||||
&:hover
|
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
|
|
||||||
.result-nav-storageList
|
|
||||||
absolute bottom left right
|
|
||||||
top 110px + 32px + 10px + 10px + 20px
|
|
||||||
overflow-y auto
|
|
||||||
|
|
||||||
.result-list
|
|
||||||
user-select none
|
|
||||||
absolute top bottom
|
|
||||||
left $nav-width
|
|
||||||
width $list-width
|
|
||||||
box-sizing border-box
|
|
||||||
overflow-y auto
|
|
||||||
box-shadow 2px 0 15px -8px #b1b1b1
|
|
||||||
z-index 1
|
|
||||||
|
|
||||||
.result-detail
|
|
||||||
absolute top bottom right
|
|
||||||
left $nav-width + $list-width
|
|
||||||
background-color $ui-noteDetail-backgroundColor
|
|
||||||
|
|
||||||
body[data-theme="dark"]
|
|
||||||
.root
|
|
||||||
background-color $ui-dark-backgroundColor
|
|
||||||
.search
|
|
||||||
border-color $ui-dark-borderColor
|
|
||||||
.search-input
|
|
||||||
color $ui-dark-text-color
|
|
||||||
|
|
||||||
.result
|
|
||||||
background-color $ui-dark-noteList-backgroundColor
|
|
||||||
|
|
||||||
.result-nav
|
|
||||||
background-color $ui-dark-backgroundColor
|
|
||||||
label
|
|
||||||
color $ui-dark-text-color
|
|
||||||
|
|
||||||
.result-nav-menu
|
|
||||||
navDarkButtonColor()
|
|
||||||
|
|
||||||
.result-nav-menu--active
|
|
||||||
background-color $ui-dark-button--active-backgroundColor
|
|
||||||
color $ui-dark-button--active-color
|
|
||||||
&:hover
|
|
||||||
background-color $ui-dark-button--active-backgroundColor
|
|
||||||
|
|
||||||
.result-list
|
|
||||||
border-color $ui-dark-borderColor
|
|
||||||
box-shadow none
|
|
||||||
top 0
|
|
||||||
|
|
||||||
.result-detail
|
|
||||||
absolute top bottom right
|
|
||||||
left $nav-width + $list-width
|
|
||||||
background-color $ui-dark-noteDetail-backgroundColor
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
|
||||||
import styles from './NoteDetail.styl'
|
|
||||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
|
||||||
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
|
||||||
import CodeEditor from 'browser/components/CodeEditor'
|
|
||||||
import CodeMirror from 'codemirror'
|
|
||||||
import { findStorage } from 'browser/lib/findStorage'
|
|
||||||
|
|
||||||
const electron = require('electron')
|
|
||||||
const { clipboard } = electron
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
function pass (name) {
|
|
||||||
switch (name) {
|
|
||||||
case 'ejs':
|
|
||||||
return 'Embedded Javascript'
|
|
||||||
case 'html_ruby':
|
|
||||||
return 'Embedded Ruby'
|
|
||||||
case 'objectivec':
|
|
||||||
return 'Objective C'
|
|
||||||
case 'text':
|
|
||||||
return 'Plain Text'
|
|
||||||
default:
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function notify (title, options) {
|
|
||||||
if (global.process.platform === 'win32') {
|
|
||||||
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
|
||||||
}
|
|
||||||
return new window.Notification(title, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
class NoteDetail extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
snippetIndex: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
|
||||||
if (nextProps.note !== this.props.note) {
|
|
||||||
this.setState({
|
|
||||||
snippetIndex: 0
|
|
||||||
}, () => {
|
|
||||||
if (nextProps.note.type === 'SNIPPET_NOTE') {
|
|
||||||
nextProps.note.snippets.forEach((snippet, index) => {
|
|
||||||
this.refs['code-' + index].reload()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectPriorSnippet () {
|
|
||||||
const { note } = this.props
|
|
||||||
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
|
|
||||||
this.setState({
|
|
||||||
snippetIndex: (this.state.snippetIndex + note.snippets.length - 1) % note.snippets.length
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectNextSnippet () {
|
|
||||||
const { note } = this.props
|
|
||||||
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
|
|
||||||
this.setState({
|
|
||||||
snippetIndex: (this.state.snippetIndex + 1) % note.snippets.length
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saveToClipboard () {
|
|
||||||
const { note } = this.props
|
|
||||||
|
|
||||||
if (note.type === 'MARKDOWN_NOTE') {
|
|
||||||
clipboard.writeText(note.content)
|
|
||||||
} else {
|
|
||||||
clipboard.writeText(note.snippets[this.state.snippetIndex].content)
|
|
||||||
}
|
|
||||||
|
|
||||||
notify('Saved to Clipboard!', {
|
|
||||||
body: 'Paste it wherever you want!',
|
|
||||||
silent: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTabButtonClick (e, index) {
|
|
||||||
this.setState({
|
|
||||||
snippetIndex: index
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { note, config } = this.props
|
|
||||||
if (note == null) {
|
|
||||||
return (
|
|
||||||
<div styleName='root' />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
|
||||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
|
||||||
|
|
||||||
const storage = findStorage(note.storage)
|
|
||||||
|
|
||||||
if (note.type === 'SNIPPET_NOTE') {
|
|
||||||
const tabList = note.snippets.map((snippet, index) => {
|
|
||||||
const isActive = this.state.snippetIndex === index
|
|
||||||
return <div styleName={isActive
|
|
||||||
? 'tabList-item--active'
|
|
||||||
: 'tabList-item'
|
|
||||||
}
|
|
||||||
key={index}
|
|
||||||
>
|
|
||||||
<button styleName='tabList-item-button'
|
|
||||||
onClick={(e) => this.handleTabButtonClick(e, index)}
|
|
||||||
>
|
|
||||||
{snippet.name.trim().length > 0
|
|
||||||
? snippet.name
|
|
||||||
: <span styleName='tabList-item-unnamed'>
|
|
||||||
Unnamed
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
|
|
||||||
const viewList = note.snippets.map((snippet, index) => {
|
|
||||||
const isActive = this.state.snippetIndex === index
|
|
||||||
|
|
||||||
let syntax = CodeMirror.findModeByName(pass(snippet.mode))
|
|
||||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
|
||||||
|
|
||||||
return <div styleName='tabView'
|
|
||||||
key={index}
|
|
||||||
style={{zIndex: isActive ? 5 : 4}}
|
|
||||||
>
|
|
||||||
{snippet.mode === 'markdown'
|
|
||||||
? <MarkdownEditor styleName='tabView-content'
|
|
||||||
config={config}
|
|
||||||
value={snippet.content}
|
|
||||||
ref={'code-' + index}
|
|
||||||
storageKey={note.storage}
|
|
||||||
/>
|
|
||||||
: <CodeEditor styleName='tabView-content'
|
|
||||||
mode={snippet.mode}
|
|
||||||
value={snippet.content}
|
|
||||||
theme={config.editor.theme}
|
|
||||||
fontFamily={config.editor.fontFamily}
|
|
||||||
fontSize={editorFontSize}
|
|
||||||
indentType={config.editor.indentType}
|
|
||||||
indentSize={editorIndentSize}
|
|
||||||
keyMap={config.editor.keyMap}
|
|
||||||
readOnly
|
|
||||||
ref={'code-' + index}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div styleName='root'>
|
|
||||||
<div styleName='description'>
|
|
||||||
<textarea styleName='description-textarea'
|
|
||||||
style={{
|
|
||||||
fontFamily: config.preview.fontFamily,
|
|
||||||
fontSize: parseInt(config.preview.fontSize, 10)
|
|
||||||
}}
|
|
||||||
ref='description'
|
|
||||||
placeholder='Description...'
|
|
||||||
value={note.description}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div styleName='tabList'>
|
|
||||||
{tabList}
|
|
||||||
</div>
|
|
||||||
{viewList}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MarkdownPreview styleName='root'
|
|
||||||
theme={config.ui.theme}
|
|
||||||
fontSize={config.preview.fontSize}
|
|
||||||
fontFamily={config.preview.fontFamily}
|
|
||||||
codeBlockTheme={config.preview.codeBlockTheme}
|
|
||||||
codeBlockFontFamily={config.editor.fontFamily}
|
|
||||||
lineNumber={config.preview.lineNumber}
|
|
||||||
indentSize={editorIndentSize}
|
|
||||||
value={note.content}
|
|
||||||
showCopyNotification={config.ui.showCopyNotification}
|
|
||||||
storagePath={storage.path}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NoteDetail.propTypes = {
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CSSModules(NoteDetail, styles)
|
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
@import('../main/Detail/DetailVars.styl')
|
|
||||||
|
|
||||||
.root
|
|
||||||
absolute top bottom left right
|
|
||||||
bottom 30px
|
|
||||||
margin 0 25px
|
|
||||||
height 100%
|
|
||||||
width 365px
|
|
||||||
background-color $ui-noteDetail-backgroundColor
|
|
||||||
|
|
||||||
.description
|
|
||||||
absolute top left right
|
|
||||||
height 80px
|
|
||||||
box-sizing border-box
|
|
||||||
|
|
||||||
.description-textarea
|
|
||||||
display block
|
|
||||||
height 100%
|
|
||||||
width 100%
|
|
||||||
resize none
|
|
||||||
border none
|
|
||||||
padding 10px
|
|
||||||
line-height 1.6
|
|
||||||
box-sizing border-box
|
|
||||||
background-color $ui-noteDetail-backgroundColor
|
|
||||||
|
|
||||||
.tabList
|
|
||||||
absolute left right
|
|
||||||
top 80px
|
|
||||||
box-sizing border-box
|
|
||||||
height 30px
|
|
||||||
display flex
|
|
||||||
background-color $ui-noteDetail-backgroundColor
|
|
||||||
|
|
||||||
.tabList-item
|
|
||||||
position relative
|
|
||||||
flex 1
|
|
||||||
overflow hidden
|
|
||||||
&:hover
|
|
||||||
background-color $ui-button--hover-backgroundColorg
|
|
||||||
|
|
||||||
.tabList-item--active
|
|
||||||
@extend .tabList-item
|
|
||||||
border-bottom $ui-border
|
|
||||||
|
|
||||||
.tabList-item-button
|
|
||||||
width 100%
|
|
||||||
height 29px
|
|
||||||
overflow ellipsis
|
|
||||||
text-align left
|
|
||||||
padding-right 30px
|
|
||||||
padding-left 10px
|
|
||||||
border none
|
|
||||||
background-color transparent
|
|
||||||
transition 0.15s
|
|
||||||
&:hover
|
|
||||||
background-color $ui-button--hover-backgroundColor
|
|
||||||
|
|
||||||
.tabView
|
|
||||||
absolute left right bottom
|
|
||||||
top 130px
|
|
||||||
|
|
||||||
.tabView-content
|
|
||||||
absolute top left right bottom
|
|
||||||
box-sizing border-box
|
|
||||||
height 100%
|
|
||||||
width 100%
|
|
||||||
|
|
||||||
body[data-theme="dark"]
|
|
||||||
.root
|
|
||||||
background-color $ui-dark-noteDetail-backgroundColor
|
|
||||||
|
|
||||||
.description
|
|
||||||
border-color $ui-dark-borderColor
|
|
||||||
background-color $ui-dark-noteDetail-backgroundColor
|
|
||||||
|
|
||||||
.description-textarea
|
|
||||||
background-color $ui-dark-noteDetail-backgroundColor
|
|
||||||
color white
|
|
||||||
|
|
||||||
.tabList
|
|
||||||
background-color $ui-dark-noteDetail-backgroundColor
|
|
||||||
|
|
||||||
.tabList-item
|
|
||||||
border-color $ui-dark-borderColor
|
|
||||||
&:hover
|
|
||||||
background-color $ui-dark-button--hover-backgroundColor
|
|
||||||
|
|
||||||
.tabList-item-button
|
|
||||||
border none
|
|
||||||
color $ui-dark-text-color
|
|
||||||
background-color transparent
|
|
||||||
transition color background-color 0.15s
|
|
||||||
border-left 4px solid transparent
|
|
||||||
&:hover
|
|
||||||
color white
|
|
||||||
background-color $ui-dark-button--hover-backgroundColor
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import NoteItem from 'browser/components/NoteItem'
|
|
||||||
import moment from 'moment'
|
|
||||||
|
|
||||||
class NoteList extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
range: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
|
||||||
if (this.props.search !== nextProps.search) {
|
|
||||||
this.resetScroll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
const { index } = this.props
|
|
||||||
|
|
||||||
if (index > -1) {
|
|
||||||
const list = this.refs.root
|
|
||||||
const item = list.childNodes[index]
|
|
||||||
if (item == null) return null
|
|
||||||
|
|
||||||
const overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
|
|
||||||
if (overflowBelow) {
|
|
||||||
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
|
|
||||||
}
|
|
||||||
const overflowAbove = list.scrollTop > item.offsetTop
|
|
||||||
if (overflowAbove) {
|
|
||||||
list.scrollTop = item.offsetTop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetScroll () {
|
|
||||||
this.refs.root.scrollTop = 0
|
|
||||||
this.setState({
|
|
||||||
range: 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleScroll (e) {
|
|
||||||
const { notes } = this.props
|
|
||||||
|
|
||||||
if (e.target.offsetHeight + e.target.scrollTop > e.target.scrollHeight - 100 && notes.length > this.state.range * 10 + 10) {
|
|
||||||
this.setState({
|
|
||||||
range: this.state.range + 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { notes, index } = this.props
|
|
||||||
|
|
||||||
const notesList = notes
|
|
||||||
.slice(0, 10 + 10 * this.state.range)
|
|
||||||
.map((note, _index) => {
|
|
||||||
const isActive = (index === _index)
|
|
||||||
const key = `${note.storage}-${note.key}`
|
|
||||||
const dateDisplay = moment(note.updatedAt).fromNow()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NoteItem
|
|
||||||
isActive={isActive}
|
|
||||||
note={note}
|
|
||||||
dateDisplay={dateDisplay}
|
|
||||||
key={key}
|
|
||||||
handleNoteClick={(e) => this.props.handleNoteClick(e, _index)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<div className={this.props.className}
|
|
||||||
onScroll={(e) => this.handleScroll(e)}
|
|
||||||
ref='root'
|
|
||||||
>
|
|
||||||
{notesList}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NoteList.propTypes = {
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NoteList
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
|
||||||
import styles from './StorageSection.styl'
|
|
||||||
import StorageItem from 'browser/components/StorageItem'
|
|
||||||
|
|
||||||
class StorageSection extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isOpen: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleToggleButtonClick (e) {
|
|
||||||
this.setState({
|
|
||||||
isOpen: !this.state.isOpen
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleHeaderClick (e) {
|
|
||||||
const { storage } = this.props
|
|
||||||
this.props.handleStorageButtonClick(e, storage.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFolderClick (e, folder) {
|
|
||||||
const { storage } = this.props
|
|
||||||
this.props.handleFolderButtonClick(e, storage.key, folder.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { storage, filter } = this.props
|
|
||||||
const folderList = storage.folders
|
|
||||||
.map(folder => (
|
|
||||||
<StorageItem
|
|
||||||
key={folder.key}
|
|
||||||
isActive={filter.type === 'FOLDER' && filter.folder === folder.key && filter.storage === storage.key}
|
|
||||||
handleButtonClick={(e) => this.handleFolderClick(e, folder)}
|
|
||||||
folderName={folder.name}
|
|
||||||
folderColor={folder.color}
|
|
||||||
isFolded={false}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div styleName='root'>
|
|
||||||
<div styleName='header'>
|
|
||||||
<button styleName='header-toggleButton'
|
|
||||||
onClick={(e) => this.handleToggleButtonClick(e)}
|
|
||||||
>
|
|
||||||
<i className={this.state.isOpen
|
|
||||||
? 'fa fa-caret-down'
|
|
||||||
: 'fa fa-caret-right'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button styleName={filter.type === 'STORAGE' && filter.storage === storage.key
|
|
||||||
? 'header-name--active'
|
|
||||||
: 'header-name'
|
|
||||||
}
|
|
||||||
onClick={(e) => this.handleHeaderClick(e)}
|
|
||||||
>{storage.name}</button>
|
|
||||||
</div>
|
|
||||||
{this.state.isOpen &&
|
|
||||||
<div styleName='folderList'>
|
|
||||||
{folderList}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StorageSection.propTypes = {
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CSSModules(StorageSection, styles)
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
.root
|
|
||||||
position relative
|
|
||||||
|
|
||||||
.header
|
|
||||||
height 26px
|
|
||||||
.header-toggleButton
|
|
||||||
absolute top left
|
|
||||||
width 25px
|
|
||||||
height 26px
|
|
||||||
navButtonColor()
|
|
||||||
border none
|
|
||||||
outline none
|
|
||||||
.header-name
|
|
||||||
display block
|
|
||||||
height 26px
|
|
||||||
navButtonColor()
|
|
||||||
padding 0 10px 0 25px
|
|
||||||
font-size 14px
|
|
||||||
width 100%
|
|
||||||
text-align left
|
|
||||||
line-height 26px
|
|
||||||
box-sizing border-box
|
|
||||||
cursor pointer
|
|
||||||
outline none
|
|
||||||
|
|
||||||
.header-name--active
|
|
||||||
@extend .header-name
|
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
color $ui-button--active-color
|
|
||||||
&:hover
|
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
|
|
||||||
.folderList-item
|
|
||||||
display block
|
|
||||||
width 100%
|
|
||||||
height 26px
|
|
||||||
navButtonColor()
|
|
||||||
padding 0 10px 0 25px
|
|
||||||
font-size 14px
|
|
||||||
width 100%
|
|
||||||
text-align left
|
|
||||||
line-height 26px
|
|
||||||
box-sizing border-box
|
|
||||||
cursor pointer
|
|
||||||
outline none
|
|
||||||
padding 0 10px
|
|
||||||
margin 2px 0
|
|
||||||
height 26px
|
|
||||||
line-height 26px
|
|
||||||
border-width 0 0 0 6px
|
|
||||||
border-style solid
|
|
||||||
border-color transparent
|
|
||||||
|
|
||||||
.folderList-item--active
|
|
||||||
@extend .folderList-item
|
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
color $ui-button--active-color
|
|
||||||
&:hover
|
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
|
|
||||||
body[data-theme="dark"]
|
|
||||||
.header-toggleButton
|
|
||||||
navDarkButtonColor()
|
|
||||||
.header-name
|
|
||||||
navDarkButtonColor()
|
|
||||||
|
|
||||||
.header-name--active
|
|
||||||
@extend .header-name
|
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
color $ui-button--active-color
|
|
||||||
&:hover
|
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
|
|
||||||
.folderList-item
|
|
||||||
navDarkButtonColor()
|
|
||||||
border-width 0 0 0 6px
|
|
||||||
border-style solid
|
|
||||||
border-color transparent
|
|
||||||
|
|
||||||
.folderList-item--active
|
|
||||||
@extend .folderList-item
|
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
color $ui-button--active-color
|
|
||||||
&:hover
|
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
@@ -1,357 +0,0 @@
|
|||||||
import PropTypes from 'prop-types'
|
|
||||||
import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom'
|
|
||||||
import { connect, Provider } from 'react-redux'
|
|
||||||
import _ from 'lodash'
|
|
||||||
import store from './store'
|
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
|
||||||
import styles from './FinderMain.styl'
|
|
||||||
import StorageSection from './StorageSection'
|
|
||||||
import NoteList from './NoteList'
|
|
||||||
import NoteDetail from './NoteDetail'
|
|
||||||
import SideNavFilter from 'browser/components/SideNavFilter'
|
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
|
||||||
require('!!style!css!stylus?sourceMap!../main/global.styl')
|
|
||||||
require('../lib/customMeta')
|
|
||||||
require('./ipcClient.js')
|
|
||||||
|
|
||||||
const electron = require('electron')
|
|
||||||
const { remote } = electron
|
|
||||||
const { Menu } = remote
|
|
||||||
|
|
||||||
function hideFinder () {
|
|
||||||
const finderWindow = remote.getCurrentWindow()
|
|
||||||
if (global.process.platform === 'win32') {
|
|
||||||
finderWindow.blur()
|
|
||||||
finderWindow.hide()
|
|
||||||
}
|
|
||||||
if (global.process.platform === 'darwin') {
|
|
||||||
Menu.sendActionToFirstResponder('hide:')
|
|
||||||
}
|
|
||||||
remote.getCurrentWindow().hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
require('!!style!css!stylus?sourceMap!../styles/finder/index.styl')
|
|
||||||
|
|
||||||
class FinderMain extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
search: '',
|
|
||||||
index: 0,
|
|
||||||
filter: {
|
|
||||||
includeSnippet: true,
|
|
||||||
includeMarkdown: false,
|
|
||||||
type: 'ALL',
|
|
||||||
storage: null,
|
|
||||||
folder: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.focusHandler = (e) => this.handleWindowFocus(e)
|
|
||||||
this.blurHandler = (e) => this.handleWindowBlur(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
this.refs.search.focus()
|
|
||||||
window.addEventListener('focus', this.focusHandler)
|
|
||||||
window.addEventListener('blur', this.blurHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
window.removeEventListener('focus', this.focusHandler)
|
|
||||||
window.removeEventListener('blur', this.blurHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleWindowFocus (e) {
|
|
||||||
this.refs.search.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
handleWindowBlur (e) {
|
|
||||||
this.setState({
|
|
||||||
search: ''
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyDown (e) {
|
|
||||||
this.refs.search.focus()
|
|
||||||
if (e.keyCode === 9) {
|
|
||||||
if (e.shiftKey) {
|
|
||||||
this.refs.detail.selectPriorSnippet()
|
|
||||||
} else {
|
|
||||||
this.refs.detail.selectNextSnippet()
|
|
||||||
}
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
if (e.keyCode === 38) {
|
|
||||||
this.selectPrevious()
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.keyCode === 40) {
|
|
||||||
this.selectNext()
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.keyCode === 13) {
|
|
||||||
this.refs.detail.saveToClipboard()
|
|
||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('COPY_FINDER')
|
|
||||||
hideFinder()
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
if (e.keyCode === 27) {
|
|
||||||
hideFinder()
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
if (e.keyCode === 91 || e.metaKey) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSearchChange (e) {
|
|
||||||
this.setState({
|
|
||||||
search: e.target.value,
|
|
||||||
index: 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
selectArticle (article) {
|
|
||||||
this.setState({currentArticle: article})
|
|
||||||
}
|
|
||||||
|
|
||||||
selectPrevious () {
|
|
||||||
if (this.state.index > 0) {
|
|
||||||
this.setState({
|
|
||||||
index: this.state.index - 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectNext () {
|
|
||||||
if (this.state.index < this.noteCount - 1) {
|
|
||||||
this.setState({
|
|
||||||
index: this.state.index + 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOnlySnippetCheckboxChange (e) {
|
|
||||||
const { filter } = this.state
|
|
||||||
filter.includeSnippet = e.target.checked
|
|
||||||
this.setState({
|
|
||||||
filter: filter,
|
|
||||||
index: 0
|
|
||||||
}, () => {
|
|
||||||
this.refs.search.focus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOnlyMarkdownCheckboxChange (e) {
|
|
||||||
const { filter } = this.state
|
|
||||||
filter.includeMarkdown = e.target.checked
|
|
||||||
this.refs.list.resetScroll()
|
|
||||||
this.setState({
|
|
||||||
filter: filter,
|
|
||||||
index: 0
|
|
||||||
}, () => {
|
|
||||||
this.refs.search.focus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAllNotesButtonClick (e) {
|
|
||||||
const { filter } = this.state
|
|
||||||
filter.type = 'ALL'
|
|
||||||
this.refs.list.resetScroll()
|
|
||||||
this.setState({
|
|
||||||
filter,
|
|
||||||
index: 0
|
|
||||||
}, () => {
|
|
||||||
this.refs.search.focus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleStarredButtonClick (e) {
|
|
||||||
const { filter } = this.state
|
|
||||||
filter.type = 'STARRED'
|
|
||||||
this.refs.list.resetScroll()
|
|
||||||
this.setState({
|
|
||||||
filter,
|
|
||||||
index: 0
|
|
||||||
}, () => {
|
|
||||||
this.refs.search.focus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleStorageButtonClick (e, storage) {
|
|
||||||
const { filter } = this.state
|
|
||||||
filter.type = 'STORAGE'
|
|
||||||
filter.storage = storage
|
|
||||||
this.refs.list.resetScroll()
|
|
||||||
this.setState({
|
|
||||||
filter,
|
|
||||||
index: 0
|
|
||||||
}, () => {
|
|
||||||
this.refs.search.focus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFolderButtonClick (e, storage, folder) {
|
|
||||||
const { filter } = this.state
|
|
||||||
filter.type = 'FOLDER'
|
|
||||||
filter.storage = storage
|
|
||||||
filter.folder = folder
|
|
||||||
this.refs.list.resetScroll()
|
|
||||||
this.setState({
|
|
||||||
filter,
|
|
||||||
index: 0
|
|
||||||
}, () => {
|
|
||||||
this.refs.search.focus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNoteClick (e, index) {
|
|
||||||
this.setState({
|
|
||||||
index
|
|
||||||
}, () => {
|
|
||||||
this.refs.search.focus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { data, config } = this.props
|
|
||||||
const { filter, search } = this.state
|
|
||||||
const storageList = []
|
|
||||||
for (const key in data.storageMap) {
|
|
||||||
const storage = data.storageMap[key]
|
|
||||||
const item = (
|
|
||||||
<StorageSection
|
|
||||||
filter={filter}
|
|
||||||
storage={storage}
|
|
||||||
key={storage.key}
|
|
||||||
handleStorageButtonClick={(e, storage) => this.handleStorageButtonClick(e, storage)}
|
|
||||||
handleFolderButtonClick={(e, storage, folder) => this.handleFolderButtonClick(e, storage, folder)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
storageList.push(item)
|
|
||||||
}
|
|
||||||
let notes = []
|
|
||||||
let noteIds
|
|
||||||
|
|
||||||
switch (filter.type) {
|
|
||||||
case 'STORAGE':
|
|
||||||
noteIds = data.storageNoteMap[filter.storage]
|
|
||||||
break
|
|
||||||
case 'FOLDER':
|
|
||||||
noteIds = data.folderNoteMap[filter.storage + '-' + filter.folder]
|
|
||||||
break
|
|
||||||
case 'STARRED':
|
|
||||||
noteIds = data.starredSet
|
|
||||||
}
|
|
||||||
if (noteIds != null) {
|
|
||||||
noteIds.forEach((id) => {
|
|
||||||
notes.push(data.noteMap[id])
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
for (const key in data.noteMap) {
|
|
||||||
notes.push(data.noteMap[key])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!filter.includeSnippet && filter.includeMarkdown) {
|
|
||||||
notes = notes.filter((note) => note.type === 'MARKDOWN_NOTE')
|
|
||||||
} else if (filter.includeSnippet && !filter.includeMarkdown) {
|
|
||||||
notes = notes.filter((note) => note.type === 'SNIPPET_NOTE')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (search.trim().length > 0) {
|
|
||||||
const needle = new RegExp(_.escapeRegExp(search.trim()), 'i')
|
|
||||||
notes = notes.filter((note) => note.title.match(needle))
|
|
||||||
}
|
|
||||||
notes = notes
|
|
||||||
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
|
|
||||||
|
|
||||||
const activeNote = notes[this.state.index]
|
|
||||||
this.noteCount = notes.length
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='Finder'
|
|
||||||
styleName='root'
|
|
||||||
ref='-1'
|
|
||||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
|
||||||
>
|
|
||||||
<div styleName='search'>
|
|
||||||
<input
|
|
||||||
styleName='search-input'
|
|
||||||
ref='search'
|
|
||||||
value={search}
|
|
||||||
placeholder='Search...'
|
|
||||||
onChange={(e) => this.handleSearchChange(e)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div styleName='result'>
|
|
||||||
<div styleName='result-nav'>
|
|
||||||
<div styleName='result-nav-filter'>
|
|
||||||
<div styleName='result-nav-filter-option'>
|
|
||||||
<label>
|
|
||||||
<input type='checkbox'
|
|
||||||
checked={filter.includeSnippet}
|
|
||||||
onChange={(e) => this.handleOnlySnippetCheckboxChange(e)}
|
|
||||||
/> Only Snippets</label>
|
|
||||||
</div>
|
|
||||||
<div styleName='result-nav-filter-option'>
|
|
||||||
<label>
|
|
||||||
<input type='checkbox'
|
|
||||||
checked={filter.includeMarkdown}
|
|
||||||
onChange={(e) => this.handleOnlyMarkdownCheckboxChange(e)}
|
|
||||||
/> Only Markdown</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SideNavFilter
|
|
||||||
isHomeActive={filter.type === 'ALL'}
|
|
||||||
handleAllNotesButtonClick={(e) => this.handleAllNotesButtonClick(e)}
|
|
||||||
isStarredActive={filter.type === 'STARRED'}
|
|
||||||
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
|
|
||||||
/>
|
|
||||||
<div styleName='result-nav-storageList'>
|
|
||||||
{storageList}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<NoteList styleName='result-list'
|
|
||||||
storageMap={data.storageMap}
|
|
||||||
notes={notes}
|
|
||||||
ref='list'
|
|
||||||
search={search}
|
|
||||||
index={this.state.index}
|
|
||||||
handleNoteClick={(e, _index) => this.handleNoteClick(e, _index)}
|
|
||||||
/>
|
|
||||||
<div styleName='result-detail'>
|
|
||||||
<NoteDetail
|
|
||||||
note={activeNote}
|
|
||||||
config={config}
|
|
||||||
ref='detail'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FinderMain.propTypes = {
|
|
||||||
dispatch: PropTypes.func
|
|
||||||
}
|
|
||||||
|
|
||||||
var Finder = connect((x) => x)(CSSModules(FinderMain, styles))
|
|
||||||
|
|
||||||
function refreshData () {
|
|
||||||
// let data = dataStore.getData(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
ReactDOM.render((
|
|
||||||
<Provider store={store}>
|
|
||||||
<Finder />
|
|
||||||
</Provider>
|
|
||||||
), document.getElementById('content'), function () {
|
|
||||||
refreshData()
|
|
||||||
})
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
const nodeIpc = require('node-ipc')
|
|
||||||
const { remote, ipcRenderer } = require('electron')
|
|
||||||
const { app, Menu } = remote
|
|
||||||
const path = require('path')
|
|
||||||
const store = require('./store')
|
|
||||||
const consts = require('browser/lib/consts')
|
|
||||||
|
|
||||||
nodeIpc.config.id = 'finder'
|
|
||||||
nodeIpc.config.retry = 1500
|
|
||||||
nodeIpc.config.silent = true
|
|
||||||
|
|
||||||
function killFinder () {
|
|
||||||
const finderWindow = remote.getCurrentWindow()
|
|
||||||
finderWindow.removeAllListeners()
|
|
||||||
if (global.process.platform === 'darwin') {
|
|
||||||
// Only OSX has another app process.
|
|
||||||
nodeIpc.of.node.emit('quit-from-finder')
|
|
||||||
} else {
|
|
||||||
finderWindow.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleFinder () {
|
|
||||||
const finderWindow = remote.getCurrentWindow()
|
|
||||||
if (global.process.platform === 'darwin') {
|
|
||||||
if (finderWindow.isVisible()) {
|
|
||||||
finderWindow.hide()
|
|
||||||
Menu.sendActionToFirstResponder('hide:')
|
|
||||||
} else {
|
|
||||||
nodeIpc.of.node.emit('request-data-from-finder')
|
|
||||||
finderWindow.show()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (finderWindow.isVisible()) {
|
|
||||||
finderWindow.blur()
|
|
||||||
finderWindow.hide()
|
|
||||||
} else {
|
|
||||||
nodeIpc.of.node.emit('request-data-from-finder')
|
|
||||||
finderWindow.show()
|
|
||||||
finderWindow.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeIpc.connectTo(
|
|
||||||
'node',
|
|
||||||
path.join(app.getPath('userData'), 'boostnote.service'),
|
|
||||||
function () {
|
|
||||||
nodeIpc.of.node.on('error', function (err) {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
nodeIpc.of.node.on('connect', function () {
|
|
||||||
console.log('Conncted successfully')
|
|
||||||
})
|
|
||||||
nodeIpc.of.node.on('disconnect', function () {
|
|
||||||
console.log('disconnected')
|
|
||||||
})
|
|
||||||
|
|
||||||
nodeIpc.of.node.on('open-finder', function () {
|
|
||||||
toggleFinder()
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcRenderer.on('open-finder-from-tray', function () {
|
|
||||||
toggleFinder()
|
|
||||||
})
|
|
||||||
ipcRenderer.on('open-main-from-tray', function () {
|
|
||||||
nodeIpc.of.node.emit('open-main-from-finder')
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcRenderer.on('quit-from-tray', function () {
|
|
||||||
nodeIpc.of.node.emit('quit-from-finder')
|
|
||||||
killFinder()
|
|
||||||
})
|
|
||||||
|
|
||||||
nodeIpc.of.node.on('throttle-data', function (payload) {
|
|
||||||
console.log('Received data from Main renderer')
|
|
||||||
store.default.dispatch({
|
|
||||||
type: 'THROTTLE_DATA',
|
|
||||||
data: payload
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
nodeIpc.of.node.on('config-renew', function (payload) {
|
|
||||||
const { config } = payload
|
|
||||||
if (config.ui.theme === 'dark') {
|
|
||||||
document.body.setAttribute('data-theme', 'dark')
|
|
||||||
} else if (config.ui.theme === 'white') {
|
|
||||||
document.body.setAttribute('data-theme', 'white')
|
|
||||||
} else {
|
|
||||||
document.body.setAttribute('data-theme', 'default')
|
|
||||||
}
|
|
||||||
|
|
||||||
let editorTheme = document.getElementById('editorTheme')
|
|
||||||
if (editorTheme == null) {
|
|
||||||
editorTheme = document.createElement('link')
|
|
||||||
editorTheme.setAttribute('id', 'editorTheme')
|
|
||||||
editorTheme.setAttribute('rel', 'stylesheet')
|
|
||||||
document.head.appendChild(editorTheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
config.editor.theme = consts.THEMES.some((theme) => theme === config.editor.theme)
|
|
||||||
? config.editor.theme
|
|
||||||
: 'default'
|
|
||||||
|
|
||||||
if (config.editor.theme !== 'default') {
|
|
||||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css')
|
|
||||||
}
|
|
||||||
|
|
||||||
store.default.dispatch({
|
|
||||||
type: 'SET_CONFIG',
|
|
||||||
config: config
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
nodeIpc.of.node.on('quit-finder-app', function () {
|
|
||||||
nodeIpc.of.node.emit('quit-finder-app-confirm')
|
|
||||||
killFinder()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const ipc = {}
|
|
||||||
|
|
||||||
module.exports = ipc
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { combineReducers, createStore } from 'redux'
|
|
||||||
import { routerReducer } from 'react-router-redux'
|
|
||||||
import { DEFAULT_CONFIG } from 'browser/main/lib/ConfigManager'
|
|
||||||
|
|
||||||
const defaultData = {
|
|
||||||
storageMap: {},
|
|
||||||
noteMap: {},
|
|
||||||
starredSet: [],
|
|
||||||
storageNoteMap: {},
|
|
||||||
folderNoteMap: {},
|
|
||||||
tagNoteMap: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function data (state = defaultData, action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'THROTTLE_DATA':
|
|
||||||
console.log(action)
|
|
||||||
state = action.data
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
function config (state = DEFAULT_CONFIG, action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'INIT_CONFIG':
|
|
||||||
case 'SET_CONFIG':
|
|
||||||
return Object.assign({}, state, action.config)
|
|
||||||
case 'SET_IS_SIDENAV_FOLDED':
|
|
||||||
state.isSideNavFolded = action.isFolded
|
|
||||||
return Object.assign({}, state)
|
|
||||||
case 'SET_ZOOM':
|
|
||||||
state.zoom = action.zoom
|
|
||||||
return Object.assign({}, state)
|
|
||||||
case 'SET_LIST_WIDTH':
|
|
||||||
state.listWidth = action.listWidth
|
|
||||||
return Object.assign({}, state)
|
|
||||||
case 'SET_UI':
|
|
||||||
return Object.assign({}, state, action.config)
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
const reducer = combineReducers({
|
|
||||||
data,
|
|
||||||
config,
|
|
||||||
routing: routerReducer
|
|
||||||
})
|
|
||||||
|
|
||||||
const store = createStore(reducer)
|
|
||||||
|
|
||||||
export default store
|
|
||||||
@@ -2,7 +2,7 @@ const { remote } = require('electron')
|
|||||||
const { Menu, MenuItem } = remote
|
const { Menu, MenuItem } = remote
|
||||||
|
|
||||||
function popup (templates) {
|
function popup (templates) {
|
||||||
let menu = new Menu()
|
const menu = new Menu()
|
||||||
templates.forEach((item) => {
|
templates.forEach((item) => {
|
||||||
menu.append(new MenuItem(item))
|
menu.append(new MenuItem(item))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
import CodeMirror from 'codemirror'
|
import CodeMirror from 'codemirror'
|
||||||
|
import 'codemirror-mode-elixir'
|
||||||
|
|
||||||
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']})
|
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']})
|
||||||
|
CodeMirror.modeInfo.push({name: 'Elixir', mime: 'text/x-elixir', mode: 'elixir', ext: ['ex']})
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export function getTodoStatus (content) {
|
|||||||
|
|
||||||
splitted.forEach((line) => {
|
splitted.forEach((line) => {
|
||||||
const trimmedLine = line.trim()
|
const trimmedLine = line.trim()
|
||||||
if (trimmedLine.match(/^[\+\-\*] \[\s|x\] ./)) {
|
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./)) {
|
||||||
numberOfTodo++
|
numberOfTodo++
|
||||||
}
|
}
|
||||||
if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) {
|
if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) {
|
||||||
|
|||||||
9
browser/lib/i18n.js
Normal file
9
browser/lib/i18n.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// load package for localization
|
||||||
|
const i18n = new (require('i18n-2'))({
|
||||||
|
// setup some locales - other locales default to the first locale
|
||||||
|
locales: ['en', 'sq', 'zh-CN', 'zh-TW', 'da', 'fr', 'de', 'hu', 'ja', 'ko', 'no', 'pl', 'pt', 'es'],
|
||||||
|
extension: '.json',
|
||||||
|
devMode: false
|
||||||
|
})
|
||||||
|
|
||||||
|
export default i18n
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
|
const uuidv4 = require('uuid/v4')
|
||||||
|
|
||||||
module.exports = function (length) {
|
module.exports = function (uuid) {
|
||||||
if (!_.isFinite(length)) length = 10
|
if (typeof uuid === typeof true && uuid) {
|
||||||
|
return uuidv4()
|
||||||
|
}
|
||||||
|
const length = 10
|
||||||
return crypto.randomBytes(length).toString('hex')
|
return crypto.randomBytes(length).toString('hex')
|
||||||
}
|
}
|
||||||
|
|||||||
23
browser/lib/markdown-it-sanitize-html.js
Normal file
23
browser/lib/markdown-it-sanitize-html.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
import sanitizeHtml from 'sanitize-html'
|
||||||
|
|
||||||
|
module.exports = function sanitizePlugin (md, options) {
|
||||||
|
options = options || {}
|
||||||
|
|
||||||
|
md.core.ruler.after('linkify', 'sanitize_inline', state => {
|
||||||
|
for (let tokenIdx = 0; tokenIdx < state.tokens.length; tokenIdx++) {
|
||||||
|
if (state.tokens[tokenIdx].type === 'html_block') {
|
||||||
|
state.tokens[tokenIdx].content = sanitizeHtml(state.tokens[tokenIdx].content, options)
|
||||||
|
}
|
||||||
|
if (state.tokens[tokenIdx].type === 'inline') {
|
||||||
|
const inlineTokens = state.tokens[tokenIdx].children
|
||||||
|
for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
|
||||||
|
if (inlineTokens[childIdx].type === 'html_inline') {
|
||||||
|
inlineTokens[childIdx].content = sanitizeHtml(inlineTokens[childIdx].content, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,157 +1,244 @@
|
|||||||
import markdownit from 'markdown-it'
|
import markdownit from 'markdown-it'
|
||||||
|
import sanitize from './markdown-it-sanitize-html'
|
||||||
import emoji from 'markdown-it-emoji'
|
import emoji from 'markdown-it-emoji'
|
||||||
import math from '@rokt33r/markdown-it-math'
|
import math from '@rokt33r/markdown-it-math'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
|
import katex from 'katex'
|
||||||
|
import {lastFindInArray} from './utils'
|
||||||
|
|
||||||
// FIXME We should not depend on global variable.
|
function createGutter (str, firstLineNumber) {
|
||||||
const katex = window.katex
|
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
|
||||||
|
const lastLineNumber = (str.match(/\n/g) || []).length + firstLineNumber - 1
|
||||||
function createGutter (str) {
|
|
||||||
const lc = (str.match(/\n/g) || []).length
|
|
||||||
const lines = []
|
const lines = []
|
||||||
for (let i = 1; i <= lc; i++) {
|
for (let i = firstLineNumber; i <= lastLineNumber; i++) {
|
||||||
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
|
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
|
||||||
}
|
}
|
||||||
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
|
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
|
||||||
}
|
}
|
||||||
|
|
||||||
var md = markdownit({
|
class Markdown {
|
||||||
typographer: true,
|
constructor (options = {}) {
|
||||||
linkify: true,
|
const config = ConfigManager.get()
|
||||||
html: true,
|
const defaultOptions = {
|
||||||
xhtmlOut: true,
|
typographer: config.preview.smartQuotes,
|
||||||
breaks: true,
|
linkify: true,
|
||||||
highlight: function (str, lang) {
|
html: true,
|
||||||
if (lang === 'flowchart') {
|
xhtmlOut: true,
|
||||||
return `<pre class="flowchart">${str}</pre>`
|
breaks: true,
|
||||||
}
|
highlight: function (str, lang) {
|
||||||
if (lang === 'sequence') {
|
const delimiter = ':'
|
||||||
return `<pre class="sequence">${str}</pre>`
|
const langInfo = lang.split(delimiter)
|
||||||
}
|
const langType = langInfo[0]
|
||||||
return '<pre class="code">' +
|
const fileName = langInfo[1] || ''
|
||||||
createGutter(str) +
|
const firstLineNumber = parseInt(langInfo[2], 10)
|
||||||
'<code class="' + lang + '">' +
|
|
||||||
str +
|
|
||||||
'</code></pre>'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
md.use(emoji, {
|
|
||||||
shortcuts: {}
|
|
||||||
})
|
|
||||||
md.use(math, {
|
|
||||||
inlineRenderer: function (str) {
|
|
||||||
let output = ''
|
|
||||||
try {
|
|
||||||
output = katex.renderToString(str.trim())
|
|
||||||
} catch (err) {
|
|
||||||
output = `<span class="katex-error">${err.message}</span>`
|
|
||||||
}
|
|
||||||
return output
|
|
||||||
},
|
|
||||||
blockRenderer: function (str) {
|
|
||||||
let output = ''
|
|
||||||
try {
|
|
||||||
output = katex.renderToString(str.trim(), {displayMode: true})
|
|
||||||
} catch (err) {
|
|
||||||
output = `<div class="katex-error">${err.message}</div>`
|
|
||||||
}
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
})
|
|
||||||
md.use(require('markdown-it-imsize'))
|
|
||||||
md.use(require('markdown-it-footnote'))
|
|
||||||
md.use(require('markdown-it-multimd-table'))
|
|
||||||
md.use(require('markdown-it-named-headers'), {
|
|
||||||
slugify: (header) => {
|
|
||||||
return encodeURI(header.trim()
|
|
||||||
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
|
|
||||||
.replace(/\s+/g, '-'))
|
|
||||||
.replace(/\-+$/, '')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
md.use(require('markdown-it-kbd'))
|
|
||||||
md.use(require('markdown-it-plantuml'))
|
|
||||||
|
|
||||||
// Override task item
|
if (langType === 'flowchart') {
|
||||||
md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
return `<pre class="flowchart">${str}</pre>`
|
||||||
let content, terminate, i, l, token
|
}
|
||||||
let nextLine = startLine + 1
|
if (langType === 'sequence') {
|
||||||
const terminatorRules = state.md.block.ruler.getRules('paragraph')
|
return `<pre class="sequence">${str}</pre>`
|
||||||
const endLine = state.lineMax
|
}
|
||||||
|
return '<pre class="code CodeMirror">' +
|
||||||
|
'<span class="filename">' + fileName + '</span>' +
|
||||||
|
createGutter(str, firstLineNumber) +
|
||||||
|
'<code class="' + langType + '">' +
|
||||||
|
str +
|
||||||
|
'</code></pre>'
|
||||||
|
},
|
||||||
|
sanitize: 'STRICT'
|
||||||
|
}
|
||||||
|
|
||||||
// jump line-by-line until empty one or EOF
|
const updatedOptions = Object.assign(defaultOptions, options)
|
||||||
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
|
this.md = markdownit(updatedOptions)
|
||||||
// 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 (updatedOptions.sanitize !== 'NONE') {
|
||||||
if (state.sCount[nextLine] < 0) { continue }
|
const allowedTags = ['iframe', 'input', 'b',
|
||||||
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8', 'br', 'b', 'i', 'strong', 'em', 'a', 'pre', 'code', 'img', 'tt',
|
||||||
|
'div', 'ins', 'del', 'sup', 'sub', 'p', 'ol', 'ul', 'table', 'thead', 'tbody', 'tfoot', 'blockquote',
|
||||||
|
'dl', 'dt', 'dd', 'kbd', 'q', 'samp', 'var', 'hr', 'ruby', 'rt', 'rp', 'li', 'tr', 'td', 'th', 's', 'strike', 'summary', 'details'
|
||||||
|
]
|
||||||
|
const allowedAttributes = [
|
||||||
|
'abbr', 'accept', 'accept-charset',
|
||||||
|
'accesskey', 'action', 'align', 'alt', 'axis',
|
||||||
|
'border', 'cellpadding', 'cellspacing', 'char',
|
||||||
|
'charoff', 'charset', 'checked',
|
||||||
|
'clear', 'cols', 'colspan', 'color',
|
||||||
|
'compact', 'coords', 'datetime', 'dir',
|
||||||
|
'disabled', 'enctype', 'for', 'frame',
|
||||||
|
'headers', 'height', 'hreflang',
|
||||||
|
'hspace', 'ismap', 'label', 'lang',
|
||||||
|
'maxlength', 'media', 'method',
|
||||||
|
'multiple', 'name', 'nohref', 'noshade',
|
||||||
|
'nowrap', 'open', 'prompt', 'readonly', 'rel', 'rev',
|
||||||
|
'rows', 'rowspan', 'rules', 'scope',
|
||||||
|
'selected', 'shape', 'size', 'span',
|
||||||
|
'start', 'summary', 'tabindex', 'target',
|
||||||
|
'title', 'type', 'usemap', 'valign', 'value',
|
||||||
|
'vspace', 'width', 'itemprop'
|
||||||
|
]
|
||||||
|
|
||||||
// Some tags can terminate paragraph without empty line.
|
if (updatedOptions.sanitize === 'ALLOW_STYLES') {
|
||||||
terminate = false
|
allowedTags.push('style')
|
||||||
for (i = 0, l = terminatorRules.length; i < l; i++) {
|
allowedAttributes.push('style')
|
||||||
if (terminatorRules[i](state, nextLine, endLine, true)) {
|
|
||||||
terminate = true
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sanitize use rinput before other plugins
|
||||||
|
this.md.use(sanitize, {
|
||||||
|
allowedTags,
|
||||||
|
allowedAttributes: {
|
||||||
|
'*': allowedAttributes,
|
||||||
|
'a': ['href'],
|
||||||
|
'div': ['itemscope', 'itemtype'],
|
||||||
|
'blockquote': ['cite'],
|
||||||
|
'del': ['cite'],
|
||||||
|
'ins': ['cite'],
|
||||||
|
'q': ['cite'],
|
||||||
|
'img': ['src', 'width', 'height'],
|
||||||
|
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
|
||||||
|
'input': ['type', 'id', 'checked']
|
||||||
|
},
|
||||||
|
allowedIframeHostnames: ['www.youtube.com']
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (terminate) { break }
|
|
||||||
|
this.md.use(emoji, {
|
||||||
|
shortcuts: {}
|
||||||
|
})
|
||||||
|
this.md.use(math, {
|
||||||
|
inlineOpen: config.preview.latexInlineOpen,
|
||||||
|
inlineClose: config.preview.latexInlineClose,
|
||||||
|
blockOpen: config.preview.latexBlockOpen,
|
||||||
|
blockClose: config.preview.latexBlockClose,
|
||||||
|
inlineRenderer: function (str) {
|
||||||
|
let output = ''
|
||||||
|
try {
|
||||||
|
output = katex.renderToString(str.trim())
|
||||||
|
} catch (err) {
|
||||||
|
output = `<span class="katex-error">${err.message}</span>`
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
},
|
||||||
|
blockRenderer: function (str) {
|
||||||
|
let output = ''
|
||||||
|
try {
|
||||||
|
output = katex.renderToString(str.trim(), { displayMode: true })
|
||||||
|
} catch (err) {
|
||||||
|
output = `<div class="katex-error">${err.message}</div>`
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.md.use(require('markdown-it-imsize'))
|
||||||
|
this.md.use(require('markdown-it-footnote'))
|
||||||
|
this.md.use(require('markdown-it-multimd-table'))
|
||||||
|
this.md.use(require('markdown-it-named-headers'), {
|
||||||
|
slugify: (header) => {
|
||||||
|
return encodeURI(header.trim()
|
||||||
|
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
|
||||||
|
.replace(/\s+/g, '-'))
|
||||||
|
.replace(/\-+$/, '')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.md.use(require('markdown-it-kbd'))
|
||||||
|
|
||||||
|
const deflate = require('markdown-it-plantuml/lib/deflate')
|
||||||
|
this.md.use(require('markdown-it-plantuml'), '', {
|
||||||
|
generateSource: function (umlCode) {
|
||||||
|
const s = unescape(encodeURIComponent(umlCode))
|
||||||
|
const zippedCode = deflate.encode64(
|
||||||
|
deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9)
|
||||||
|
)
|
||||||
|
return `http://www.plantuml.com/plantuml/svg/${zippedCode}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Override task item
|
||||||
|
this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
||||||
|
let content, terminate, i, l, token
|
||||||
|
let nextLine = startLine + 1
|
||||||
|
const terminatorRules = state.md.block.ruler.getRules('paragraph')
|
||||||
|
const endLine = state.lineMax
|
||||||
|
|
||||||
|
// jump line-by-line until empty one or EOF
|
||||||
|
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
|
||||||
|
// this would be a code block normally, but after paragraph
|
||||||
|
// it's considered a lazy continuation regardless of what's there
|
||||||
|
if (state.sCount[nextLine] - state.blkIndent > 3) { continue }
|
||||||
|
|
||||||
|
// quirk for blockquotes, this line should already be checked by that rule
|
||||||
|
if (state.sCount[nextLine] < 0) { continue }
|
||||||
|
|
||||||
|
// Some tags can terminate paragraph without empty line.
|
||||||
|
terminate = false
|
||||||
|
for (i = 0, l = terminatorRules.length; i < l; i++) {
|
||||||
|
if (terminatorRules[i](state, nextLine, endLine, true)) {
|
||||||
|
terminate = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (terminate) { break }
|
||||||
|
}
|
||||||
|
|
||||||
|
content = state.getLines(startLine, nextLine, state.blkIndent, false).trim()
|
||||||
|
|
||||||
|
state.line = nextLine
|
||||||
|
|
||||||
|
token = state.push('paragraph_open', 'p', 1)
|
||||||
|
token.map = [startLine, state.line]
|
||||||
|
|
||||||
|
if (state.parentType === 'list') {
|
||||||
|
const match = content.match(/^\[( |x)\] ?(.+)/i)
|
||||||
|
if (match) {
|
||||||
|
const liToken = lastFindInArray(state.tokens, token => token.type === 'list_item_open')
|
||||||
|
if (liToken) {
|
||||||
|
if (!liToken.attrs) {
|
||||||
|
liToken.attrs = []
|
||||||
|
}
|
||||||
|
liToken.attrs.push(['class', 'taskListItem'])
|
||||||
|
}
|
||||||
|
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token = state.push('inline', '', 0)
|
||||||
|
token.content = content
|
||||||
|
token.map = [startLine, state.line]
|
||||||
|
token.children = []
|
||||||
|
|
||||||
|
token = state.push('paragraph_close', 'p', -1)
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add line number attribute for scrolling
|
||||||
|
const originalRender = this.md.renderer.render
|
||||||
|
this.md.renderer.render = (tokens, options, env) => {
|
||||||
|
tokens.forEach((token) => {
|
||||||
|
switch (token.type) {
|
||||||
|
case 'heading_open':
|
||||||
|
case 'paragraph_open':
|
||||||
|
case 'blockquote_open':
|
||||||
|
case 'table_open':
|
||||||
|
token.attrPush(['data-line', token.map[0]])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const result = originalRender.call(this.md.renderer, tokens, options, env)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
// FIXME We should not depend on global variable.
|
||||||
|
window.md = this.md
|
||||||
}
|
}
|
||||||
|
|
||||||
content = state.getLines(startLine, nextLine, state.blkIndent, false).trim()
|
render (content) {
|
||||||
|
|
||||||
state.line = nextLine
|
|
||||||
|
|
||||||
token = state.push('paragraph_open', 'p', 1)
|
|
||||||
token.map = [ startLine, state.line ]
|
|
||||||
|
|
||||||
if (state.parentType === 'list') {
|
|
||||||
const match = content.match(/^\[( |x)\] ?(.+)/i)
|
|
||||||
if (match) {
|
|
||||||
content = `<label class='taskListItem' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
token = state.push('inline', '', 0)
|
|
||||||
token.content = content
|
|
||||||
token.map = [ startLine, state.line ]
|
|
||||||
token.children = []
|
|
||||||
|
|
||||||
token = state.push('paragraph_close', 'p', -1)
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add line number attribute for scrolling
|
|
||||||
const originalRender = md.renderer.render
|
|
||||||
md.renderer.render = function render (tokens, options, env) {
|
|
||||||
tokens.forEach((token) => {
|
|
||||||
switch (token.type) {
|
|
||||||
case 'heading_open':
|
|
||||||
case 'paragraph_open':
|
|
||||||
case 'blockquote_open':
|
|
||||||
case 'table_open':
|
|
||||||
token.attrPush(['data-line', token.map[0]])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const result = originalRender.call(md.renderer, tokens, options, env)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
// FIXME We should not depend on global variable.
|
|
||||||
window.md = md
|
|
||||||
|
|
||||||
function normalizeLinkText (linkText) {
|
|
||||||
return md.normalizeLinkText(linkText)
|
|
||||||
}
|
|
||||||
|
|
||||||
const markdown = {
|
|
||||||
render: function markdown (content) {
|
|
||||||
if (!_.isString(content)) content = ''
|
if (!_.isString(content)) content = ''
|
||||||
const renderedContent = md.render(content)
|
return this.md.render(content)
|
||||||
return renderedContent
|
}
|
||||||
},
|
|
||||||
normalizeLinkText
|
normalizeLinkText (linkText) {
|
||||||
|
return this.md.normalizeLinkText(linkText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default markdown
|
export default Markdown
|
||||||
|
|
||||||
|
|||||||
11
browser/lib/utils.js
Normal file
11
browser/lib/utils.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export function lastFindInArray (array, callback) {
|
||||||
|
for (let i = array.length - 1; i >= 0; --i) {
|
||||||
|
if (callback(array[i], i, array)) {
|
||||||
|
return array[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
lastFindInArray
|
||||||
|
}
|
||||||
@@ -20,5 +20,13 @@
|
|||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.root
|
.root
|
||||||
background-color $ui-dark-backgroundColor
|
background-color $ui-dark-backgroundColor
|
||||||
|
border-left 1px solid $ui-dark-borderColor
|
||||||
.empty-message
|
.empty-message
|
||||||
color $ui-dark-inactive-text-color
|
color $ui-dark-inactive-text-color
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.root
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
border-left 1px solid $ui-solarized-dark-borderColor
|
||||||
|
.empty-message
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React from 'react'
|
|||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './FolderSelect.styl'
|
import styles from './FolderSelect.styl'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
class FolderSelect extends React.Component {
|
class FolderSelect extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -249,7 +250,7 @@ class FolderSelect extends React.Component {
|
|||||||
<input styleName='search-input'
|
<input styleName='search-input'
|
||||||
ref='search'
|
ref='search'
|
||||||
value={this.state.search}
|
value={this.state.search}
|
||||||
placeholder='Folder...'
|
placeholder={i18n.__('Folder...')}
|
||||||
onChange={(e) => this.handleSearchInputChange(e)}
|
onChange={(e) => this.handleSearchInputChange(e)}
|
||||||
onBlur={(e) => this.handleSearchInputBlur(e)}
|
onBlur={(e) => this.handleSearchInputBlur(e)}
|
||||||
onKeyDown={(e) => this.handleSearchInputKeyDown(e)}
|
onKeyDown={(e) => this.handleSearchInputKeyDown(e)}
|
||||||
|
|||||||
@@ -3,20 +3,14 @@
|
|||||||
border solid 1px transparent
|
border solid 1px transparent
|
||||||
vertical-align middle
|
vertical-align middle
|
||||||
border-radius 2px
|
border-radius 2px
|
||||||
|
height 30px
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
user-select none
|
user-select none
|
||||||
margin-right 10px
|
margin-right 10px
|
||||||
&:hover
|
|
||||||
background-color $ui-button--hover-backgroundColor
|
|
||||||
|
|
||||||
.root--search, .root--focus
|
.root--search, .root--focus
|
||||||
@extend .root
|
@extend .root
|
||||||
background-color $ui-noteDetail-backgroundColor = #fff
|
|
||||||
border-color $ui-input--focus-borderColor
|
border-color $ui-input--focus-borderColor
|
||||||
width 154px
|
|
||||||
height 30px
|
|
||||||
&:hover
|
|
||||||
border-color $ui-input--focus-borderColor = #fff
|
|
||||||
|
|
||||||
.idle
|
.idle
|
||||||
position relative
|
position relative
|
||||||
|
|||||||
20
browser/main/Detail/FullscreenButton.js
Normal file
20
browser/main/Detail/FullscreenButton.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './FullscreenButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
|
const FullscreenButton = ({
|
||||||
|
onClick
|
||||||
|
}) => (
|
||||||
|
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
|
||||||
|
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
||||||
|
<span styleName='tooltip'>{i18n.__('Fullscreen')}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
FullscreenButton.propTypes = {
|
||||||
|
onClick: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(FullscreenButton, styles)
|
||||||
22
browser/main/Detail/FullscreenButton.styl
Normal file
22
browser/main/Detail/FullscreenButton.styl
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
.control-fullScreenButton
|
||||||
|
top 80px
|
||||||
|
topBarButtonRight()
|
||||||
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 50px
|
||||||
|
right 70px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.control-fullScreenButton
|
||||||
|
topBarButtonDark()
|
||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './InfoButton.styl'
|
import styles from './InfoButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const InfoButton = ({
|
const InfoButton = ({
|
||||||
onClick
|
onClick
|
||||||
@@ -10,6 +11,7 @@ const InfoButton = ({
|
|||||||
onClick={(e) => onClick(e)}
|
onClick={(e) => onClick(e)}
|
||||||
>
|
>
|
||||||
<img className='infoButton' src='../resources/icon/icon-info.svg' />
|
<img className='infoButton' src='../resources/icon/icon-info.svg' />
|
||||||
|
<span styleName='tooltip'>{i18n.__('Info')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,21 @@
|
|||||||
.control-infoButton
|
.control-infoButton
|
||||||
top 10px
|
top 10px
|
||||||
margin-bottom 10px
|
topBarButtonRight()
|
||||||
topBarButtonLight()
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 50px
|
||||||
|
right 20px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
.infoButton
|
.infoButton
|
||||||
padding 0px
|
padding 0px
|
||||||
|
|||||||
@@ -2,77 +2,98 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './InfoPanel.styl'
|
import styles from './InfoPanel.styl'
|
||||||
|
import copy from 'copy-to-clipboard'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const InfoPanel = ({
|
class InfoPanel extends React.Component {
|
||||||
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, wordCount, letterCount, type, print
|
copyNoteLink () {
|
||||||
}) => (
|
const {noteLink} = this.props
|
||||||
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
|
this.refs.noteLink.select()
|
||||||
<div>
|
copy(noteLink)
|
||||||
<p styleName='modification-date'>{updatedAt}</p>
|
}
|
||||||
<p styleName='modification-date-desc'>MODIFICATION DATE</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr />
|
render () {
|
||||||
|
const {
|
||||||
{type === 'SNIPPET_NOTE'
|
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, wordCount, letterCount, type, print
|
||||||
? ''
|
} = this.props
|
||||||
: <div styleName='count-wrap'>
|
return (
|
||||||
<div styleName='count-number'>
|
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
|
||||||
<p styleName='infoPanel-defaul-count'>{wordCount}</p>
|
<div>
|
||||||
<p styleName='infoPanel-sub-count'>Words</p>
|
<p styleName='modification-date'>{updatedAt}</p>
|
||||||
|
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='count-number'>
|
|
||||||
<p styleName='infoPanel-defaul-count'>{letterCount}</p>
|
<hr />
|
||||||
<p styleName='infoPanel-sub-count'>Letters</p>
|
|
||||||
|
{type === 'SNIPPET_NOTE'
|
||||||
|
? ''
|
||||||
|
: <div styleName='count-wrap'>
|
||||||
|
<div styleName='count-number'>
|
||||||
|
<p styleName='infoPanel-defaul-count'>{wordCount}</p>
|
||||||
|
<p styleName='infoPanel-sub-count'>{i18n.__('Words')}</p>
|
||||||
|
</div>
|
||||||
|
<div styleName='count-number'>
|
||||||
|
<p styleName='infoPanel-defaul-count'>{letterCount}</p>
|
||||||
|
<p styleName='infoPanel-sub-count'>{i18n.__('Letters')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{type === 'SNIPPET_NOTE'
|
||||||
|
? ''
|
||||||
|
: <hr />
|
||||||
|
}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p styleName='infoPanel-default'>{storageName}</p>
|
||||||
|
<p styleName='infoPanel-sub'>{i18n.__('STORAGE')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p styleName='infoPanel-default'>{folderName}</p>
|
||||||
|
<p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p styleName='infoPanel-default'>{createdAt}</p>
|
||||||
|
<p styleName='infoPanel-sub'>{i18n.__('CREATION DATE')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input styleName='infoPanel-noteLink' ref='noteLink' value={noteLink} onClick={(e) => { e.target.select() }} />
|
||||||
|
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
|
||||||
|
<i className='fa fa-clipboard' />
|
||||||
|
</button>
|
||||||
|
<p styleName='infoPanel-sub'>{i18n.__('NOTE LINK')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div id='export-wrap'>
|
||||||
|
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
||||||
|
<i className='fa fa-file-code-o' />
|
||||||
|
<p>{i18n.__('.md')}</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
||||||
|
<i className='fa fa-file-text-o' />
|
||||||
|
<p>{i18n.__('.txt')}</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
|
||||||
|
<i className='fa fa-html5' />
|
||||||
|
<p>{i18n.__('.html')}</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button styleName='export--enable' onClick={(e) => print(e)}>
|
||||||
|
<i className='fa fa-print' />
|
||||||
|
<p>{i18n.__('Print')}</p>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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 = {
|
InfoPanel.propTypes = {
|
||||||
storageName: PropTypes.string.isRequired,
|
storageName: PropTypes.string.isRequired,
|
||||||
@@ -82,6 +103,7 @@ InfoPanel.propTypes = {
|
|||||||
createdAt: PropTypes.string.isRequired,
|
createdAt: PropTypes.string.isRequired,
|
||||||
exportAsMd: PropTypes.func.isRequired,
|
exportAsMd: PropTypes.func.isRequired,
|
||||||
exportAsTxt: PropTypes.func.isRequired,
|
exportAsTxt: PropTypes.func.isRequired,
|
||||||
|
exportAsHtml: PropTypes.func.isRequired,
|
||||||
wordCount: PropTypes.number,
|
wordCount: PropTypes.number,
|
||||||
letterCount: PropTypes.number,
|
letterCount: PropTypes.number,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -11,11 +11,10 @@
|
|||||||
.control-infoButton-panel
|
.control-infoButton-panel
|
||||||
z-index 200
|
z-index 200
|
||||||
margin-top 0px
|
margin-top 0px
|
||||||
right 0
|
right 25px
|
||||||
position absolute
|
position absolute
|
||||||
padding 20px 25px 0 25px
|
padding 20px 25px 0 25px
|
||||||
width 300px
|
width 300px
|
||||||
height 350px
|
|
||||||
overflow auto
|
overflow auto
|
||||||
background-color $ui-noteList-backgroundColor
|
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)
|
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
|
||||||
@@ -41,12 +40,12 @@
|
|||||||
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
|
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
|
border-radius 2px
|
||||||
|
|
||||||
.count-wrap
|
.count-wrap
|
||||||
display flex
|
display flex
|
||||||
position relative
|
position relative
|
||||||
width 100%
|
width 100%
|
||||||
|
|
||||||
.count-number
|
.count-number
|
||||||
position relative
|
position relative
|
||||||
display block
|
display block
|
||||||
width 50%
|
width 50%
|
||||||
@@ -70,26 +69,41 @@
|
|||||||
color $ui-text-color
|
color $ui-text-color
|
||||||
|
|
||||||
.infoPanel-sub
|
.infoPanel-sub
|
||||||
font-size 14px
|
font-size 12px
|
||||||
|
font-weight 600
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
padding-bottom 8px
|
padding-bottom 8px
|
||||||
|
|
||||||
.infoPanel-noteLink
|
.infoPanel-noteLink
|
||||||
padding-right 5px
|
padding-right 5px
|
||||||
width 200px
|
width 210px
|
||||||
height 25px
|
height 25px
|
||||||
margin-bottom 6px
|
margin 6px 0
|
||||||
|
|
||||||
|
.infoPanel-copyButton
|
||||||
|
outline none
|
||||||
|
font-size 16px
|
||||||
|
color #A0A0A0
|
||||||
|
background-color transparent
|
||||||
|
border none
|
||||||
|
margin 0 5px
|
||||||
|
border-radius 5px
|
||||||
|
cursor pointer
|
||||||
|
&:hover
|
||||||
|
transition 0.2s
|
||||||
|
background-color alpha($ui-button--hover-backgroundColor, 30%)
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
.infoPanel-trash
|
.infoPanel-trash
|
||||||
color #EA4447
|
color #EA4447
|
||||||
font-weight 600
|
font-weight 600
|
||||||
font-size 14px
|
font-size 14px
|
||||||
width 70px
|
width 70px
|
||||||
background-color rgba(226,33,113,0.1)
|
background-color rgba(226,33,113,0.1)
|
||||||
border none
|
border none
|
||||||
outline none
|
outline none
|
||||||
border-radius 2px
|
border-radius 2px
|
||||||
margin-right 5px
|
margin-right 5px
|
||||||
padding 2px 5px
|
padding 2px 5px
|
||||||
|
|
||||||
[id=export-wrap]
|
[id=export-wrap]
|
||||||
@@ -160,4 +174,44 @@ body[data-theme="dark"]
|
|||||||
p
|
p
|
||||||
color $ui-dark-inactive-text-color
|
color $ui-dark-inactive-text-color
|
||||||
&:hover
|
&:hover
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.control-infoButton-panel
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
|
||||||
|
.control-infoButton-panel-trash
|
||||||
|
background-color $ui-solarized-ark-noteList-backgroundColor
|
||||||
|
|
||||||
|
.modification-date
|
||||||
|
color $ui-solarized-ark-text-color
|
||||||
|
|
||||||
|
.modification-date-desc
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.infoPanel-defaul-count
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.infoPanel-sub-count
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.infoPanel-default
|
||||||
|
color $ui-solarized-ark-text-color
|
||||||
|
|
||||||
|
.infoPanel-sub
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.infoPanel-noteLink
|
||||||
|
background-color alpha($ui-solarized-dark-borderColor, 20%)
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
[id=export-wrap]
|
||||||
|
button
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
|
&:hover
|
||||||
|
background-color alpha($ui-solarized-dark-borderColor, 20%)
|
||||||
|
color $ui-solarized-ark-text-color
|
||||||
|
p
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
|
&:hover
|
||||||
|
color $ui-solarized-ark-text-color
|
||||||
|
|||||||
@@ -2,46 +2,52 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './InfoPanel.styl'
|
import styles from './InfoPanel.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const InfoPanelTrashed = ({
|
const InfoPanelTrashed = ({
|
||||||
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt
|
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml
|
||||||
}) => (
|
}) => (
|
||||||
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
|
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
|
||||||
<div>
|
<div>
|
||||||
<p styleName='modification-date'>{updatedAt}</p>
|
<p styleName='modification-date'>{updatedAt}</p>
|
||||||
<p styleName='modification-date-desc'>MODIFICATION DATE</p>
|
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p styleName='infoPanel-default'>{storageName}</p>
|
<p styleName='infoPanel-default'>{storageName}</p>
|
||||||
<p styleName='infoPanel-sub'>STORAGE</p>
|
<p styleName='infoPanel-sub'>{i18n.__('STORAGE')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p styleName='infoPanel-default'><text styleName='infoPanel-trash'>Trash</text>{folderName}</p>
|
<p styleName='infoPanel-default'><text styleName='infoPanel-trash'>Trash</text>{folderName}</p>
|
||||||
<p styleName='infoPanel-sub'>FOLDER</p>
|
<p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p styleName='infoPanel-default'>{createdAt}</p>
|
<p styleName='infoPanel-default'>{createdAt}</p>
|
||||||
<p styleName='infoPanel-sub'>CREATION DATE</p>
|
<p styleName='infoPanel-sub'>{i18n.__('CREATION DATE')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id='export-wrap'>
|
<div id='export-wrap'>
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
||||||
<i className='fa fa-file-code-o fa-fw' />
|
<i className='fa fa-file-code-o' />
|
||||||
<p>.md</p>
|
<p>.md</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
||||||
<i className='fa fa-file-text-o fa-fw' />
|
<i className='fa fa-file-text-o' />
|
||||||
<p>.txt</p>
|
<p>.txt</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
|
||||||
|
<i className='fa fa-html5' />
|
||||||
|
<p>.html</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button styleName='export--unable'>
|
<button styleName='export--unable'>
|
||||||
<i className='fa fa-file-pdf-o fa-fw' />
|
<i className='fa fa-file-pdf-o' />
|
||||||
<p>.pdf</p>
|
<p>.pdf</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,7 +60,8 @@ InfoPanelTrashed.propTypes = {
|
|||||||
updatedAt: PropTypes.string.isRequired,
|
updatedAt: PropTypes.string.isRequired,
|
||||||
createdAt: PropTypes.string.isRequired,
|
createdAt: PropTypes.string.isRequired,
|
||||||
exportAsMd: PropTypes.func.isRequired,
|
exportAsMd: PropTypes.func.isRequired,
|
||||||
exportAsTxt: PropTypes.func.isRequired
|
exportAsTxt: PropTypes.func.isRequired,
|
||||||
|
exportAsHtml: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(InfoPanelTrashed, styles)
|
export default CSSModules(InfoPanelTrashed, styles)
|
||||||
|
|||||||
175
browser/main/Detail/MarkdownNoteDetail.js
Normal file → Executable file
175
browser/main/Detail/MarkdownNoteDetail.js
Normal file → Executable file
@@ -3,6 +3,7 @@ import React from 'react'
|
|||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './MarkdownNoteDetail.styl'
|
import styles from './MarkdownNoteDetail.styl'
|
||||||
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
||||||
|
import MarkdownSplitEditor from 'browser/components/MarkdownSplitEditor'
|
||||||
import TodoListPercentage from 'browser/components/TodoListPercentage'
|
import TodoListPercentage from 'browser/components/TodoListPercentage'
|
||||||
import StarButton from './StarButton'
|
import StarButton from './StarButton'
|
||||||
import TagSelect from './TagSelect'
|
import TagSelect from './TagSelect'
|
||||||
@@ -15,19 +16,19 @@ import StatusBar from '../StatusBar'
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { findNoteTitle } from 'browser/lib/findNoteTitle'
|
import { findNoteTitle } from 'browser/lib/findNoteTitle'
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
import TrashButton from './TrashButton'
|
import TrashButton from './TrashButton'
|
||||||
|
import FullscreenButton from './FullscreenButton'
|
||||||
|
import RestoreButton from './RestoreButton'
|
||||||
import PermanentDeleteButton from './PermanentDeleteButton'
|
import PermanentDeleteButton from './PermanentDeleteButton'
|
||||||
import InfoButton from './InfoButton'
|
import InfoButton from './InfoButton'
|
||||||
|
import ToggleModeButton from './ToggleModeButton'
|
||||||
import InfoPanel from './InfoPanel'
|
import InfoPanel from './InfoPanel'
|
||||||
import InfoPanelTrashed from './InfoPanelTrashed'
|
import InfoPanelTrashed from './InfoPanelTrashed'
|
||||||
import { formatDate } from 'browser/lib/date-formatter'
|
import { formatDate } from 'browser/lib/date-formatter'
|
||||||
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
|
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
|
||||||
import striptags from 'striptags'
|
import striptags from 'striptags'
|
||||||
|
|
||||||
const electron = require('electron')
|
|
||||||
const { remote } = electron
|
|
||||||
const { dialog } = remote
|
|
||||||
|
|
||||||
class MarkdownNoteDetail extends React.Component {
|
class MarkdownNoteDetail extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
@@ -39,7 +40,8 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
content: ''
|
content: ''
|
||||||
}, props.note),
|
}, props.note),
|
||||||
isLockButtonShown: false,
|
isLockButtonShown: false,
|
||||||
isLocked: false
|
isLocked: false,
|
||||||
|
editorType: props.config.editor.type
|
||||||
}
|
}
|
||||||
this.dispatchTimer = null
|
this.dispatchTimer = null
|
||||||
|
|
||||||
@@ -67,24 +69,26 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
|
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
||||||
if (this.saveQueue != null) this.saveNow()
|
if (this.saveQueue != null) this.saveNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUnmount () {
|
handleUpdateTag () {
|
||||||
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
const { note } = this.state
|
||||||
|
if (this.refs.tags) note.tags = this.refs.tags.value
|
||||||
|
this.updateNote(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChange (e) {
|
handleUpdateContent () {
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
|
|
||||||
note.content = this.refs.content.value
|
note.content = this.refs.content.value
|
||||||
if (this.refs.tags) note.tags = this.refs.tags.value
|
|
||||||
note.title = markdown.strip(striptags(findNoteTitle(note.content)))
|
note.title = markdown.strip(striptags(findNoteTitle(note.content)))
|
||||||
note.updatedAt = new Date()
|
this.updateNote(note)
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
updateNote (note) {
|
||||||
note
|
note.updatedAt = new Date()
|
||||||
}, () => {
|
this.setState({note}, () => {
|
||||||
this.save()
|
this.save()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -135,7 +139,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
hashHistory.replace({
|
hashHistory.replace({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
query: {
|
||||||
key: newNote.storage + '-' + newNote.key
|
key: newNote.key
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -170,41 +174,45 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
ee.emit('export:save-text')
|
ee.emit('export:save-text')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exportAsHtml () {
|
||||||
|
ee.emit('export:save-html')
|
||||||
|
}
|
||||||
|
|
||||||
handleTrashButtonClick (e) {
|
handleTrashButtonClick (e) {
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
const { isTrashed } = note
|
const { isTrashed } = note
|
||||||
|
const { confirmDeletion } = this.props
|
||||||
|
|
||||||
if (isTrashed) {
|
if (isTrashed) {
|
||||||
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
if (confirmDeletion(true)) {
|
||||||
type: 'warning',
|
const {note, dispatch} = this.props
|
||||||
message: 'Confirm note deletion',
|
dataApi
|
||||||
detail: 'This will permanently remove this note.',
|
.deleteNote(note.storage, note.key)
|
||||||
buttons: ['Confirm', 'Cancel']
|
.then((data) => {
|
||||||
})
|
const dispatchHandler = () => {
|
||||||
if (dialogueButtonIndex === 1) return
|
dispatch({
|
||||||
const { note, dispatch } = this.props
|
type: 'DELETE_NOTE',
|
||||||
dataApi
|
storageKey: data.storageKey,
|
||||||
.deleteNote(note.storage, note.key)
|
noteKey: data.noteKey
|
||||||
.then((data) => {
|
})
|
||||||
const dispatchHandler = () => {
|
}
|
||||||
dispatch({
|
ee.once('list:next', dispatchHandler)
|
||||||
type: 'DELETE_NOTE',
|
})
|
||||||
storageKey: data.storageKey,
|
.then(() => ee.emit('list:next'))
|
||||||
noteKey: data.noteKey
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
ee.once('list:moved', dispatchHandler)
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
note.isTrashed = true
|
if (confirmDeletion()) {
|
||||||
|
note.isTrashed = true
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
note
|
note
|
||||||
}, () => {
|
}, () => {
|
||||||
this.save()
|
this.save()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ee.emit('list:next')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ee.emit('list:next')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUndoButtonClick (e) {
|
handleUndoButtonClick (e) {
|
||||||
@@ -233,7 +241,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getToggleLockButton () {
|
getToggleLockButton () {
|
||||||
return this.state.isLocked ? '../resources/icon/icon-lock.svg' : '../resources/icon/icon-unlock.svg'
|
return this.state.isLocked ? '../resources/icon/icon-previewoff-on.svg' : '../resources/icon/icon-previewoff-off.svg'
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteKeyDown (e) {
|
handleDeleteKeyDown (e) {
|
||||||
@@ -262,9 +270,42 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
ee.emit('print')
|
ee.emit('print')
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
handleSwitchMode (type) {
|
||||||
const { data, config, location } = this.props
|
this.setState({ editorType: type }, () => {
|
||||||
|
const newConfig = Object.assign({}, this.props.config)
|
||||||
|
newConfig.editor.type = type
|
||||||
|
ConfigManager.set(newConfig)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEditor () {
|
||||||
|
const { config, ignorePreviewPointerEvents } = this.props
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
|
if (this.state.editorType === 'EDITOR_PREVIEW') {
|
||||||
|
return <MarkdownEditor
|
||||||
|
ref='content'
|
||||||
|
styleName='body-noteEditor'
|
||||||
|
config={config}
|
||||||
|
value={note.content}
|
||||||
|
storageKey={note.storage}
|
||||||
|
onChange={this.handleUpdateContent.bind(this)}
|
||||||
|
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||||
|
/>
|
||||||
|
} else {
|
||||||
|
return <MarkdownSplitEditor
|
||||||
|
ref='content'
|
||||||
|
config={config}
|
||||||
|
value={note.content}
|
||||||
|
storageKey={note.storage}
|
||||||
|
onChange={this.handleUpdateContent.bind(this)}
|
||||||
|
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { data, location } = this.props
|
||||||
|
const { note, editorType } = this.state
|
||||||
const storageKey = note.storage
|
const storageKey = note.storage
|
||||||
const folderKey = note.folder
|
const folderKey = note.folder
|
||||||
|
|
||||||
@@ -281,10 +322,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
const trashTopBar = <div styleName='info'>
|
const trashTopBar = <div styleName='info'>
|
||||||
<div styleName='info-left'>
|
<div styleName='info-left'>
|
||||||
<i styleName='undo-button'
|
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
|
||||||
className='fa fa-undo fa-fw'
|
|
||||||
onClick={(e) => this.handleUndoButtonClick(e)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||||
@@ -296,6 +334,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
folderName={currentOption.folder.name}
|
folderName={currentOption.folder.name}
|
||||||
updatedAt={formatDate(note.updatedAt)}
|
updatedAt={formatDate(note.updatedAt)}
|
||||||
createdAt={formatDate(note.createdAt)}
|
createdAt={formatDate(note.createdAt)}
|
||||||
|
exportAsHtml={this.exportAsHtml}
|
||||||
exportAsMd={this.exportAsMd}
|
exportAsMd={this.exportAsMd}
|
||||||
exportAsTxt={this.exportAsTxt}
|
exportAsTxt={this.exportAsTxt}
|
||||||
/>
|
/>
|
||||||
@@ -316,17 +355,12 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
<TagSelect
|
<TagSelect
|
||||||
ref='tags'
|
ref='tags'
|
||||||
value={this.state.note.tags}
|
value={this.state.note.tags}
|
||||||
onChange={(e) => this.handleChange(e)}
|
onChange={this.handleUpdateTag.bind(this)}
|
||||||
/>
|
|
||||||
<TodoListPercentage
|
|
||||||
percentageOfTodo={getTodoPercentageOfCompleted(note.content)}
|
|
||||||
/>
|
/>
|
||||||
|
<TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
<InfoButton
|
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
|
||||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StarButton
|
<StarButton
|
||||||
onClick={(e) => this.handleStarButtonClick(e)}
|
onClick={(e) => this.handleStarButtonClick(e)}
|
||||||
isActive={note.isStarred}
|
isActive={note.isStarred}
|
||||||
@@ -340,6 +374,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
|
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
|
||||||
>
|
>
|
||||||
<img styleName='iconInfo' src={imgSrc} />
|
<img styleName='iconInfo' src={imgSrc} />
|
||||||
|
{this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -347,22 +382,23 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
<button styleName='control-fullScreenButton'
|
<FullscreenButton onClick={(e) => this.handleFullScreenButton(e)} />
|
||||||
onMouseDown={(e) => this.handleFullScreenButton(e)}
|
|
||||||
>
|
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-sidebar.svg' />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||||
|
|
||||||
|
<InfoButton
|
||||||
|
onClick={(e) => this.handleInfoButtonClick(e)}
|
||||||
|
/>
|
||||||
|
|
||||||
<InfoPanel
|
<InfoPanel
|
||||||
storageName={currentOption.storage.name}
|
storageName={currentOption.storage.name}
|
||||||
folderName={currentOption.folder.name}
|
folderName={currentOption.folder.name}
|
||||||
noteLink={`[${note.title}](${location.query.key})`}
|
noteLink={`[${note.title}](:note:${location.query.key})`}
|
||||||
updatedAt={formatDate(note.updatedAt)}
|
updatedAt={formatDate(note.updatedAt)}
|
||||||
createdAt={formatDate(note.createdAt)}
|
createdAt={formatDate(note.createdAt)}
|
||||||
exportAsMd={this.exportAsMd}
|
exportAsMd={this.exportAsMd}
|
||||||
exportAsTxt={this.exportAsTxt}
|
exportAsTxt={this.exportAsTxt}
|
||||||
|
exportAsHtml={this.exportAsHtml}
|
||||||
wordCount={note.content.split(' ').length}
|
wordCount={note.content.split(' ').length}
|
||||||
letterCount={note.content.replace(/\r?\n/g, '').length}
|
letterCount={note.content.replace(/\r?\n/g, '').length}
|
||||||
type={note.type}
|
type={note.type}
|
||||||
@@ -380,15 +416,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
||||||
|
|
||||||
<div styleName='body'>
|
<div styleName='body'>
|
||||||
<MarkdownEditor
|
{this.renderEditor()}
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
<StatusBar
|
<StatusBar
|
||||||
@@ -409,7 +437,8 @@ MarkdownNoteDetail.propTypes = {
|
|||||||
style: PropTypes.shape({
|
style: PropTypes.shape({
|
||||||
left: PropTypes.number
|
left: PropTypes.number
|
||||||
}),
|
}),
|
||||||
ignorePreviewPointerEvents: PropTypes.bool
|
ignorePreviewPointerEvents: PropTypes.bool,
|
||||||
|
confirmDeletion: PropTypes.bool.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(MarkdownNoteDetail, styles)
|
export default CSSModules(MarkdownNoteDetail, styles)
|
||||||
|
|||||||
@@ -7,30 +7,42 @@
|
|||||||
background-color $ui-noteDetail-backgroundColor
|
background-color $ui-noteDetail-backgroundColor
|
||||||
box-shadow none
|
box-shadow none
|
||||||
padding 20px 40px
|
padding 20px 40px
|
||||||
|
overflow hidden
|
||||||
|
|
||||||
.lock-button
|
.lock-button
|
||||||
padding-bottom 3px
|
padding-bottom 3px
|
||||||
|
|
||||||
.control-lockButton
|
.control-lockButton
|
||||||
top 160px
|
topBarButtonRight()
|
||||||
margin-bottom 10px
|
position absolute
|
||||||
topBarButtonLight()
|
right 225px
|
||||||
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 35px
|
||||||
|
right -10px
|
||||||
|
width 50px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
.trashed-infopanel
|
.trashed-infopanel
|
||||||
top 40px
|
|
||||||
position relative
|
position relative
|
||||||
|
|
||||||
.control-fullScreenButton
|
|
||||||
top 80px
|
|
||||||
topBarButtonLight()
|
|
||||||
|
|
||||||
.body
|
.body
|
||||||
absolute left right
|
absolute left right
|
||||||
left 0
|
left 0
|
||||||
right 0
|
right 0
|
||||||
top $info-height + $info-margin-under-border
|
top $info-height + $info-margin-under-border
|
||||||
bottom $statusBar-height
|
bottom $statusBar-height
|
||||||
margin 0 45px
|
margin 0 30px
|
||||||
.body-noteEditor
|
.body-noteEditor
|
||||||
absolute top bottom left right
|
absolute top bottom left right
|
||||||
|
|
||||||
@@ -43,7 +55,7 @@ body[data-theme="dark"]
|
|||||||
.root
|
.root
|
||||||
background-color $ui-dark-noteDetail-backgroundColor
|
background-color $ui-dark-noteDetail-backgroundColor
|
||||||
box-shadow none
|
box-shadow none
|
||||||
border none
|
border-left 1px solid $ui-dark-borderColor
|
||||||
|
|
||||||
.control-lockButton
|
.control-lockButton
|
||||||
topBarButtonDark()
|
topBarButtonDark()
|
||||||
@@ -53,3 +65,9 @@ body[data-theme="dark"]
|
|||||||
|
|
||||||
.control-fullScreenButton
|
.control-fullScreenButton
|
||||||
topBarButtonDark()
|
topBarButtonDark()
|
||||||
|
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.root
|
||||||
|
border-left 1px solid $ui-solarized-dark-borderColor
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@import('DetailVars')
|
@import('DetailVars')
|
||||||
|
|
||||||
$info-height = 50px
|
$info-height = 60px
|
||||||
$info-margin-under-border = 30px
|
$info-margin-under-border = 30px
|
||||||
|
|
||||||
.info
|
.info
|
||||||
@@ -8,11 +8,11 @@ $info-margin-under-border = 30px
|
|||||||
left 0
|
left 0
|
||||||
right 0
|
right 0
|
||||||
height $info-height
|
height $info-height
|
||||||
border-bottom 1px solid #eee
|
|
||||||
background-color $ui-noteDetail-backgroundColor
|
background-color $ui-noteDetail-backgroundColor
|
||||||
width 100%
|
width 100%
|
||||||
display flex
|
display flex
|
||||||
align-items center
|
align-items center
|
||||||
|
padding 0 20px
|
||||||
|
|
||||||
.info-left
|
.info-left
|
||||||
padding 0 10px
|
padding 0 10px
|
||||||
@@ -20,7 +20,6 @@ $info-margin-under-border = 30px
|
|||||||
display flex
|
display flex
|
||||||
align-items center
|
align-items center
|
||||||
|
|
||||||
|
|
||||||
.info-left-top-folderSelect
|
.info-left-top-folderSelect
|
||||||
display flex
|
display flex
|
||||||
align-items center
|
align-items center
|
||||||
@@ -45,12 +44,9 @@ $info-margin-under-border = 30px
|
|||||||
color $ui-button--color
|
color $ui-button--color
|
||||||
|
|
||||||
.info-right
|
.info-right
|
||||||
position absolute
|
|
||||||
right 40px
|
|
||||||
top 60px
|
|
||||||
bottom 1px
|
|
||||||
padding-left 30px
|
|
||||||
z-index 101
|
z-index 101
|
||||||
|
display inline-flex
|
||||||
|
margin-top 3px
|
||||||
|
|
||||||
.undo-button
|
.undo-button
|
||||||
width 34px
|
width 34px
|
||||||
@@ -95,4 +91,10 @@ body[data-theme="dark"]
|
|||||||
background-color $ui-dark-noteDetail-backgroundColor
|
background-color $ui-dark-noteDetail-backgroundColor
|
||||||
|
|
||||||
.undo-button
|
.undo-button
|
||||||
topBarButtonDark()
|
topBarButtonDark()
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.info
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './TrashButton.styl'
|
import styles from './TrashButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const PermanentDeleteButton = ({
|
const PermanentDeleteButton = ({
|
||||||
onClick
|
onClick
|
||||||
@@ -10,6 +11,7 @@ const PermanentDeleteButton = ({
|
|||||||
onClick={(e) => onClick(e)}
|
onClick={(e) => onClick(e)}
|
||||||
>
|
>
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
||||||
|
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
22
browser/main/Detail/RestoreButton.js
Normal file
22
browser/main/Detail/RestoreButton.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './RestoreButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
|
const RestoreButton = ({
|
||||||
|
onClick
|
||||||
|
}) => (
|
||||||
|
<button styleName='control-restoreButton'
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<i className='fa fa-undo fa-fw' styleName='iconRestore' />
|
||||||
|
<span styleName='tooltip'>{i18n.__('Restore')}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
RestoreButton.propTypes = {
|
||||||
|
onClick: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(RestoreButton, styles)
|
||||||
22
browser/main/Detail/RestoreButton.styl
Normal file
22
browser/main/Detail/RestoreButton.styl
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
.control-restoreButton
|
||||||
|
top 115px
|
||||||
|
topBarButtonRight()
|
||||||
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 50px
|
||||||
|
left 25px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.control-restoreButton
|
||||||
|
topBarButtonDark()
|
||||||
@@ -8,22 +8,25 @@ import StarButton from './StarButton'
|
|||||||
import TagSelect from './TagSelect'
|
import TagSelect from './TagSelect'
|
||||||
import FolderSelect from './FolderSelect'
|
import FolderSelect from './FolderSelect'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import { hashHistory } from 'react-router'
|
import {hashHistory} from 'react-router'
|
||||||
import ee from 'browser/main/lib/eventEmitter'
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
import CodeMirror from 'codemirror'
|
import CodeMirror from 'codemirror'
|
||||||
|
import 'codemirror-mode-elixir'
|
||||||
import SnippetTab from 'browser/components/SnippetTab'
|
import SnippetTab from 'browser/components/SnippetTab'
|
||||||
import StatusBar from '../StatusBar'
|
import StatusBar from '../StatusBar'
|
||||||
import context from 'browser/lib/context'
|
import context from 'browser/lib/context'
|
||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { findNoteTitle } from 'browser/lib/findNoteTitle'
|
import {findNoteTitle} from 'browser/lib/findNoteTitle'
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
import TrashButton from './TrashButton'
|
import TrashButton from './TrashButton'
|
||||||
|
import RestoreButton from './RestoreButton'
|
||||||
import PermanentDeleteButton from './PermanentDeleteButton'
|
import PermanentDeleteButton from './PermanentDeleteButton'
|
||||||
import InfoButton from './InfoButton'
|
import InfoButton from './InfoButton'
|
||||||
import InfoPanel from './InfoPanel'
|
import InfoPanel from './InfoPanel'
|
||||||
import InfoPanelTrashed from './InfoPanelTrashed'
|
import InfoPanelTrashed from './InfoPanelTrashed'
|
||||||
import { formatDate } from 'browser/lib/date-formatter'
|
import { formatDate } from 'browser/lib/date-formatter'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
function pass (name) {
|
function pass (name) {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
@@ -51,12 +54,30 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
isMovingNote: false,
|
isMovingNote: false,
|
||||||
snippetIndex: 0,
|
snippetIndex: 0,
|
||||||
|
showArrows: false,
|
||||||
|
enableLeftArrow: false,
|
||||||
|
enableRightArrow: false,
|
||||||
note: Object.assign({
|
note: Object.assign({
|
||||||
description: ''
|
description: ''
|
||||||
}, props.note, {
|
}, props.note, {
|
||||||
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
|
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.scrollToNextTabThreshold = 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const visibleTabs = this.visibleTabs
|
||||||
|
const allTabs = this.allTabs
|
||||||
|
|
||||||
|
if (visibleTabs.offsetWidth < allTabs.scrollWidth) {
|
||||||
|
this.setState({
|
||||||
|
showArrows: visibleTabs.offsetWidth < allTabs.scrollWidth,
|
||||||
|
enableRightArrow: allTabs.offsetLeft !== visibleTabs.offsetWidth - allTabs.scrollWidth,
|
||||||
|
enableLeftArrow: allTabs.offsetLeft !== 0
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
@@ -76,6 +97,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
this.refs['code-' + index].reload()
|
this.refs['code-' + index].reload()
|
||||||
})
|
})
|
||||||
if (this.refs.tags) this.refs.tags.reset()
|
if (this.refs.tags) this.refs.tags.reset()
|
||||||
|
this.setState(this.getArrowsState())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,7 +167,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
hashHistory.replace({
|
hashHistory.replace({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
query: {
|
||||||
key: newNote.storage + '-' + newNote.key
|
key: newNote.key
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -175,38 +197,38 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
handleTrashButtonClick (e) {
|
handleTrashButtonClick (e) {
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
const { isTrashed } = note
|
const { isTrashed } = note
|
||||||
|
const { confirmDeletion } = this.props
|
||||||
|
|
||||||
if (isTrashed) {
|
if (isTrashed) {
|
||||||
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
if (confirmDeletion(true)) {
|
||||||
type: 'warning',
|
const {note, dispatch} = this.props
|
||||||
message: 'Confirm note deletion',
|
dataApi
|
||||||
detail: 'This will permanently remove this note.',
|
.deleteNote(note.storage, note.key)
|
||||||
buttons: ['Confirm', 'Cancel']
|
.then((data) => {
|
||||||
})
|
const dispatchHandler = () => {
|
||||||
if (dialogueButtonIndex === 1) return
|
dispatch({
|
||||||
const { note, dispatch } = this.props
|
type: 'DELETE_NOTE',
|
||||||
dataApi
|
storageKey: data.storageKey,
|
||||||
.deleteNote(note.storage, note.key)
|
noteKey: data.noteKey
|
||||||
.then((data) => {
|
})
|
||||||
const dispatchHandler = () => {
|
}
|
||||||
dispatch({
|
ee.once('list:next', dispatchHandler)
|
||||||
type: 'DELETE_NOTE',
|
})
|
||||||
storageKey: data.storageKey,
|
.then(() => ee.emit('list:next'))
|
||||||
noteKey: data.noteKey
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
ee.once('list:moved', dispatchHandler)
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
note.isTrashed = true
|
if (confirmDeletion()) {
|
||||||
|
note.isTrashed = true
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
note
|
note
|
||||||
}, () => {
|
}, () => {
|
||||||
this.save()
|
this.save()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ee.emit('list:next')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ee.emit('list:next')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUndoButtonClick (e) {
|
handleUndoButtonClick (e) {
|
||||||
@@ -226,6 +248,51 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
ee.emit('editor:fullscreen')
|
ee.emit('editor:fullscreen')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleTabMoveLeftButtonClick (e) {
|
||||||
|
{
|
||||||
|
const left = this.visibleTabs.scrollLeft
|
||||||
|
|
||||||
|
const tabs = this.allTabs.querySelectorAll('div')
|
||||||
|
const lastVisibleTab = Array.from(tabs).find((tab) => {
|
||||||
|
return tab.offsetLeft + tab.offsetWidth >= left
|
||||||
|
})
|
||||||
|
|
||||||
|
if (lastVisibleTab) {
|
||||||
|
const visiblePart = lastVisibleTab.offsetWidth + lastVisibleTab.offsetLeft - left
|
||||||
|
const isFullyVisible = visiblePart > lastVisibleTab.offsetWidth * this.scrollToNextTabThreshold
|
||||||
|
const scrollToTab = (isFullyVisible && lastVisibleTab.previousSibling)
|
||||||
|
? lastVisibleTab.previousSibling
|
||||||
|
: lastVisibleTab
|
||||||
|
|
||||||
|
// FIXME use `scrollIntoView()` instead of custom method after update to Electron2.0 (with Chrome 61 its possible animate the scroll)
|
||||||
|
this.moveToTab(scrollToTab)
|
||||||
|
// scrollToTab.scrollIntoView({behavior: 'smooth', inline: 'start', block: 'start'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTabMoveRightButtonClick (e) {
|
||||||
|
const left = this.visibleTabs.scrollLeft
|
||||||
|
const width = this.visibleTabs.offsetWidth
|
||||||
|
|
||||||
|
const tabs = this.allTabs.querySelectorAll('div')
|
||||||
|
const lastVisibleTab = Array.from(tabs).find((tab) => {
|
||||||
|
return tab.offsetLeft + tab.offsetWidth >= width + left
|
||||||
|
})
|
||||||
|
|
||||||
|
if (lastVisibleTab) {
|
||||||
|
const visiblePart = width + left - lastVisibleTab.offsetLeft
|
||||||
|
const isFullyVisible = visiblePart > lastVisibleTab.offsetWidth * this.scrollToNextTabThreshold
|
||||||
|
const scrollToTab = (isFullyVisible && lastVisibleTab.nextSibling)
|
||||||
|
? lastVisibleTab.nextSibling
|
||||||
|
: lastVisibleTab
|
||||||
|
|
||||||
|
// FIXME use `scrollIntoView()` instead of custom method after update to Electron2.0 (with Chrome 61 its possible animate the scroll)
|
||||||
|
this.moveToTab(scrollToTab)
|
||||||
|
// scrollToTab.scrollIntoView({behavior: 'smooth', inline: 'end', block: 'end'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleTabPlusButtonClick (e) {
|
handleTabPlusButtonClick (e) {
|
||||||
this.addSnippet()
|
this.addSnippet()
|
||||||
}
|
}
|
||||||
@@ -236,14 +303,35 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleTabDragStart (e, index) {
|
||||||
|
e.dataTransfer.setData('text/plain', index)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTabDrop (e, index) {
|
||||||
|
const oldIndex = parseInt(e.dataTransfer.getData('text'))
|
||||||
|
|
||||||
|
const snippets = this.state.note.snippets.slice()
|
||||||
|
const draggedSnippet = snippets[oldIndex]
|
||||||
|
snippets[oldIndex] = snippets[index]
|
||||||
|
snippets[index] = draggedSnippet
|
||||||
|
const snippetIndex = index
|
||||||
|
|
||||||
|
const note = Object.assign({}, this.state.note, {snippets})
|
||||||
|
this.setState({ note, snippetIndex }, () => {
|
||||||
|
this.save()
|
||||||
|
this.refs['code-' + index].reload()
|
||||||
|
this.refs['code-' + oldIndex].reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
handleTabDeleteButtonClick (e, index) {
|
handleTabDeleteButtonClick (e, index) {
|
||||||
if (this.state.note.snippets.length > 1) {
|
if (this.state.note.snippets.length > 1) {
|
||||||
if (this.state.note.snippets[index].content.trim().length > 0) {
|
if (this.state.note.snippets[index].content.trim().length > 0) {
|
||||||
const dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
const dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Delete a snippet',
|
message: i18n.__('Delete a snippet'),
|
||||||
detail: 'This work cannot be undone.',
|
detail: i18n.__('This work cannot be undone.'),
|
||||||
buttons: ['Confirm', 'Cancel']
|
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||||
})
|
})
|
||||||
if (dialogIndex === 0) {
|
if (dialogIndex === 0) {
|
||||||
this.deleteSnippetByIndex(index)
|
this.deleteSnippetByIndex(index)
|
||||||
@@ -264,6 +352,21 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
this.setState({ note, snippetIndex }, () => {
|
this.setState({ note, snippetIndex }, () => {
|
||||||
this.save()
|
this.save()
|
||||||
this.refs['code-' + this.state.snippetIndex].reload()
|
this.refs['code-' + this.state.snippetIndex].reload()
|
||||||
|
|
||||||
|
if (this.visibleTabs.offsetWidth > this.allTabs.scrollWidth) {
|
||||||
|
console.log('no need for arrows')
|
||||||
|
this.moveTabBarBy(0)
|
||||||
|
} else {
|
||||||
|
const lastTab = this.allTabs.lastChild
|
||||||
|
if (lastTab.offsetLeft + lastTab.offsetWidth < this.visibleTabs.offsetWidth) {
|
||||||
|
console.log('need to scroll')
|
||||||
|
const width = this.visibleTabs.offsetWidth
|
||||||
|
const newLeft = lastTab.offsetLeft + lastTab.offsetWidth - width
|
||||||
|
this.moveTabBarBy(newLeft > 0 ? -newLeft : 0)
|
||||||
|
} else {
|
||||||
|
this.setState(this.getArrowsState())
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,6 +423,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
|
|
||||||
handleKeyDown (e) {
|
handleKeyDown (e) {
|
||||||
switch (e.keyCode) {
|
switch (e.keyCode) {
|
||||||
|
// tab key
|
||||||
case 9:
|
case 9:
|
||||||
if (e.ctrlKey && !e.shiftKey) {
|
if (e.ctrlKey && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -332,6 +436,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
this.focusEditor()
|
this.focusEditor()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
// L key
|
||||||
case 76:
|
case 76:
|
||||||
{
|
{
|
||||||
const isSuper = global.process.platform === 'darwin'
|
const isSuper = global.process.platform === 'darwin'
|
||||||
@@ -343,6 +448,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
// T key
|
||||||
case 84:
|
case 84:
|
||||||
{
|
{
|
||||||
const isSuper = global.process.platform === 'darwin'
|
const isSuper = global.process.platform === 'darwin'
|
||||||
@@ -359,7 +465,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
|
|
||||||
handleModeButtonClick (e, index) {
|
handleModeButtonClick (e, index) {
|
||||||
const menu = new Menu()
|
const menu = new Menu()
|
||||||
CodeMirror.modeInfo.forEach((mode) => {
|
CodeMirror.modeInfo.sort(function (a, b) { return a.name.localeCompare(b.name) }).forEach((mode) => {
|
||||||
menu.append(new MenuItem({
|
menu.append(new MenuItem({
|
||||||
label: mode.name,
|
label: mode.name,
|
||||||
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
|
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
|
||||||
@@ -434,6 +540,51 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
this.refs.description.focus()
|
this.refs.description.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveToTab (tab) {
|
||||||
|
const easeOutCubic = t => (--t) * t * t + 1
|
||||||
|
const startScrollPosition = this.visibleTabs.scrollLeft
|
||||||
|
const animationTiming = 300
|
||||||
|
const scrollMoreCoeff = 1.4 // introduce coefficient, because we want to scroll a bit further to see next tab
|
||||||
|
|
||||||
|
let scrollBy = (tab.offsetLeft - startScrollPosition)
|
||||||
|
|
||||||
|
if (tab.offsetLeft > startScrollPosition) {
|
||||||
|
// if tab is on the right side and we want to show the whole tab in visible area,
|
||||||
|
// we need to include width of the tab and visible area in the formula
|
||||||
|
// ___________________________________________
|
||||||
|
// |____|_______|________|________|_show_this_|
|
||||||
|
// ↑_____________________↑
|
||||||
|
// visible area
|
||||||
|
scrollBy += (tab.offsetWidth - this.visibleTabs.offsetWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
let startTime = null
|
||||||
|
const scrollAnimation = time => {
|
||||||
|
startTime = startTime || time
|
||||||
|
const elapsed = (time - startTime) / animationTiming
|
||||||
|
|
||||||
|
this.visibleTabs.scrollLeft = startScrollPosition + easeOutCubic(elapsed) * scrollBy * scrollMoreCoeff
|
||||||
|
if (elapsed < 1) {
|
||||||
|
window.requestAnimationFrame(scrollAnimation)
|
||||||
|
} else {
|
||||||
|
this.setState(this.getArrowsState())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.requestAnimationFrame(scrollAnimation)
|
||||||
|
}
|
||||||
|
|
||||||
|
getArrowsState () {
|
||||||
|
const allTabs = this.allTabs
|
||||||
|
const visibleTabs = this.visibleTabs
|
||||||
|
|
||||||
|
const showArrows = visibleTabs.offsetWidth < allTabs.scrollWidth
|
||||||
|
const enableRightArrow = visibleTabs.scrollLeft !== allTabs.scrollWidth - visibleTabs.offsetWidth
|
||||||
|
const enableLeftArrow = visibleTabs.scrollLeft !== 0
|
||||||
|
|
||||||
|
return {showArrows, enableRightArrow, enableLeftArrow}
|
||||||
|
}
|
||||||
|
|
||||||
addSnippet () {
|
addSnippet () {
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
|
|
||||||
@@ -444,10 +595,16 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}])
|
}])
|
||||||
const snippetIndex = note.snippets.length - 1
|
const snippetIndex = note.snippets.length - 1
|
||||||
|
|
||||||
this.setState({
|
this.setState(Object.assign({
|
||||||
note,
|
note,
|
||||||
snippetIndex
|
snippetIndex
|
||||||
}, () => {
|
}, this.getArrowsState()), () => {
|
||||||
|
if (this.state.showArrows) {
|
||||||
|
const tabs = this.allTabs.querySelectorAll('div')
|
||||||
|
if (tabs) {
|
||||||
|
this.moveToTab(tabs[snippetIndex])
|
||||||
|
}
|
||||||
|
}
|
||||||
this.refs['tab-' + snippetIndex].startRenaming()
|
this.refs['tab-' + snippetIndex].startRenaming()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -481,9 +638,9 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
showWarning () {
|
showWarning () {
|
||||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Sorry!',
|
message: i18n.__('Sorry!'),
|
||||||
detail: 'md/text import is available only a markdown note.',
|
detail: i18n.__('md/text import is available only a markdown note.'),
|
||||||
buttons: ['OK']
|
buttons: [i18n.__('OK')]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,6 +668,8 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
onDelete={(e) => this.handleTabDeleteButtonClick(e, index)}
|
onDelete={(e) => this.handleTabDeleteButtonClick(e, index)}
|
||||||
onRename={(name) => this.renameSnippetByIndex(index, name)}
|
onRename={(name) => this.renameSnippetByIndex(index, name)}
|
||||||
isDeletable={note.snippets.length > 1}
|
isDeletable={note.snippets.length > 1}
|
||||||
|
onDragStart={(e) => this.handleTabDragStart(e, index)}
|
||||||
|
onDrop={(e) => this.handleTabDrop(e, index)}
|
||||||
/>
|
/>
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -541,7 +700,10 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
fontSize={editorFontSize}
|
fontSize={editorFontSize}
|
||||||
indentType={config.editor.indentType}
|
indentType={config.editor.indentType}
|
||||||
indentSize={editorIndentSize}
|
indentSize={editorIndentSize}
|
||||||
|
displayLineNumbers={config.editor.displayLineNumbers}
|
||||||
keyMap={config.editor.keyMap}
|
keyMap={config.editor.keyMap}
|
||||||
|
scrollPastEnd={config.editor.scrollPastEnd}
|
||||||
|
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||||
ref={'code-' + index}
|
ref={'code-' + index}
|
||||||
/>
|
/>
|
||||||
@@ -562,10 +724,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
|
|
||||||
const trashTopBar = <div styleName='info'>
|
const trashTopBar = <div styleName='info'>
|
||||||
<div styleName='info-left'>
|
<div styleName='info-left'>
|
||||||
<i styleName='undo-button'
|
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
|
||||||
className='fa fa-undo fa-fw'
|
|
||||||
onClick={(e) => this.handleUndoButtonClick(e)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||||
@@ -579,6 +738,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
createdAt={formatDate(note.createdAt)}
|
createdAt={formatDate(note.createdAt)}
|
||||||
exportAsMd={this.showWarning}
|
exportAsMd={this.showWarning}
|
||||||
exportAsTxt={this.showWarning}
|
exportAsTxt={this.showWarning}
|
||||||
|
exportAsHtml={this.showWarning}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -601,25 +761,27 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
<InfoButton
|
|
||||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StarButton
|
<StarButton
|
||||||
onClick={(e) => this.handleStarButtonClick(e)}
|
onClick={(e) => this.handleStarButtonClick(e)}
|
||||||
isActive={note.isStarred}
|
isActive={note.isStarred}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button styleName='control-fullScreenButton'
|
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')}
|
||||||
onMouseDown={(e) => this.handleFullScreenButton(e)}>
|
onMouseDown={(e) => this.handleFullScreenButton(e)}>
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-sidebar.svg' />
|
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
||||||
|
<span styleName='tooltip'>{i18n.__('Fullscreen')}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||||
|
|
||||||
|
<InfoButton
|
||||||
|
onClick={(e) => this.handleInfoButtonClick(e)}
|
||||||
|
/>
|
||||||
|
|
||||||
<InfoPanel
|
<InfoPanel
|
||||||
storageName={currentOption.storage.name}
|
storageName={currentOption.storage.name}
|
||||||
folderName={currentOption.folder.name}
|
folderName={currentOption.folder.name}
|
||||||
noteLink={`[${note.title}](${location.query.key})`}
|
noteLink={`[${note.title}](:note:${location.query.key})`}
|
||||||
updatedAt={formatDate(note.updatedAt)}
|
updatedAt={formatDate(note.updatedAt)}
|
||||||
createdAt={formatDate(note.createdAt)}
|
createdAt={formatDate(note.createdAt)}
|
||||||
exportAsMd={this.showWarning}
|
exportAsMd={this.showWarning}
|
||||||
@@ -645,16 +807,32 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
fontSize: parseInt(config.preview.fontSize, 10)
|
fontSize: parseInt(config.preview.fontSize, 10)
|
||||||
}}
|
}}
|
||||||
ref='description'
|
ref='description'
|
||||||
placeholder='Description...'
|
placeholder={i18n.__('Description...')}
|
||||||
value={this.state.note.description}
|
value={this.state.note.description}
|
||||||
onChange={(e) => this.handleChange(e)}
|
onChange={(e) => this.handleChange(e)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='tabList'>
|
<div styleName='tabList'>
|
||||||
<div styleName='list'>
|
<button styleName='tabButton'
|
||||||
{tabList}
|
hidden={!this.state.showArrows}
|
||||||
|
disabled={!this.state.enableLeftArrow}
|
||||||
|
onClick={(e) => this.handleTabMoveLeftButtonClick(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-chevron-left' />
|
||||||
|
</button>
|
||||||
|
<div styleName='list' onScroll={(e) => { this.setState(this.getArrowsState()) }} ref={(tabs) => { this.visibleTabs = tabs }}>
|
||||||
|
<div styleName='allTabs' ref={(tabs) => { this.allTabs = tabs }}>
|
||||||
|
{tabList}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button styleName='plusButton'
|
<button styleName='tabButton'
|
||||||
|
hidden={!this.state.showArrows}
|
||||||
|
disabled={!this.state.enableRightArrow}
|
||||||
|
onClick={(e) => this.handleTabMoveRightButtonClick(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-chevron-right' />
|
||||||
|
</button>
|
||||||
|
<button styleName='tabButton'
|
||||||
onClick={(e) => this.handleTabPlusButtonClick(e)}
|
onClick={(e) => this.handleTabPlusButtonClick(e)}
|
||||||
>
|
>
|
||||||
<i className='fa fa-plus' />
|
<i className='fa fa-plus' />
|
||||||
@@ -668,7 +846,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
onClick={(e) => this.handleModeButtonClick(e, this.state.snippetIndex)}
|
onClick={(e) => this.handleModeButtonClick(e, this.state.snippetIndex)}
|
||||||
>
|
>
|
||||||
{this.state.note.snippets[this.state.snippetIndex].mode == null
|
{this.state.note.snippets[this.state.snippetIndex].mode == null
|
||||||
? 'Select Syntax...'
|
? i18n.__('Select Syntax...')
|
||||||
: this.state.note.snippets[this.state.snippetIndex].mode
|
: this.state.note.snippets[this.state.snippetIndex].mode
|
||||||
}
|
}
|
||||||
<i className='fa fa-caret-down' />
|
<i className='fa fa-caret-down' />
|
||||||
@@ -705,7 +883,8 @@ SnippetNoteDetail.propTypes = {
|
|||||||
style: PropTypes.shape({
|
style: PropTypes.shape({
|
||||||
left: PropTypes.number
|
left: PropTypes.number
|
||||||
}),
|
}),
|
||||||
ignorePreviewPointerEvents: PropTypes.bool
|
ignorePreviewPointerEvents: PropTypes.bool,
|
||||||
|
confirmDeletion: PropTypes.bool.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(SnippetNoteDetail, styles)
|
export default CSSModules(SnippetNoteDetail, styles)
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
|
|
||||||
.body
|
.body
|
||||||
absolute left right
|
absolute left right
|
||||||
left $snippet-note-detail-left-margin
|
margin 0 30px
|
||||||
right $snippet-note-detail-right-margin
|
|
||||||
top $info-height + $info-margin-under-border
|
top $info-height + $info-margin-under-border
|
||||||
bottom $statusBar-height
|
bottom $statusBar-height
|
||||||
background-color $ui-noteDetail-backgroundColor
|
background-color $ui-noteDetail-backgroundColor
|
||||||
@@ -36,13 +35,26 @@
|
|||||||
height 30px
|
height 30px
|
||||||
display flex
|
display flex
|
||||||
background-color $ui-noteDetail-backgroundColor
|
background-color $ui-noteDetail-backgroundColor
|
||||||
|
overflow hidden
|
||||||
|
|
||||||
.tabList .list
|
.tabList .list
|
||||||
flex 1
|
flex 1
|
||||||
display flex
|
|
||||||
overflow hidden
|
overflow hidden
|
||||||
|
overflow-x scroll
|
||||||
|
position relative
|
||||||
|
|
||||||
.tabList .plusButton
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allTabs
|
||||||
|
display flex
|
||||||
|
position relative
|
||||||
|
overflow visible
|
||||||
|
left 0
|
||||||
|
transition left 0.1s
|
||||||
|
|
||||||
|
.tabList .tabButton
|
||||||
navWhiteButtonColor()
|
navWhiteButtonColor()
|
||||||
width 30px
|
width 30px
|
||||||
|
|
||||||
@@ -69,7 +81,22 @@
|
|||||||
.control-fullScreenButton
|
.control-fullScreenButton
|
||||||
top 80px
|
top 80px
|
||||||
margin-bottom 10px
|
margin-bottom 10px
|
||||||
topBarButtonLight()
|
topBarButtonRight()
|
||||||
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 50px
|
||||||
|
right 70px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
body[data-theme="white"]
|
body[data-theme="white"]
|
||||||
.root
|
.root
|
||||||
@@ -78,7 +105,7 @@ body[data-theme="white"]
|
|||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.root
|
.root
|
||||||
border none
|
border-left 1px solid $ui-dark-borderColor
|
||||||
background-color $ui-dark-noteDetail-backgroundColor
|
background-color $ui-dark-noteDetail-backgroundColor
|
||||||
box-shadow none
|
box-shadow none
|
||||||
|
|
||||||
@@ -109,3 +136,20 @@ body[data-theme="dark"]
|
|||||||
|
|
||||||
.control-fullScreenButton
|
.control-fullScreenButton
|
||||||
topBarButtonDark()
|
topBarButtonDark()
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.root
|
||||||
|
border-left 1px solid $ui-solarized-dark-borderColor
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
.body
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
.body .description textarea
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
border 1px solid $ui-solarized-dark-borderColor
|
||||||
|
|
||||||
|
.tabList
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
@@ -3,6 +3,7 @@ import React from 'react'
|
|||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './StarButton.styl'
|
import styles from './StarButton.styl'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
class StarButton extends React.Component {
|
class StarButton extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -46,14 +47,14 @@ class StarButton extends React.Component {
|
|||||||
onMouseDown={(e) => this.handleMouseDown(e)}
|
onMouseDown={(e) => this.handleMouseDown(e)}
|
||||||
onMouseUp={(e) => this.handleMouseUp(e)}
|
onMouseUp={(e) => this.handleMouseUp(e)}
|
||||||
onMouseLeave={(e) => this.handleMouseLeave(e)}
|
onMouseLeave={(e) => this.handleMouseLeave(e)}
|
||||||
onClick={this.props.onClick}
|
onClick={this.props.onClick}>
|
||||||
>
|
|
||||||
<img styleName='icon'
|
<img styleName='icon'
|
||||||
src={this.state.isActive || this.props.isActive
|
src={this.state.isActive || this.props.isActive
|
||||||
? '../resources/icon/icon-starred.svg'
|
? '../resources/icon/icon-starred.svg'
|
||||||
: '../resources/icon/icon-star.svg'
|
: '../resources/icon/icon-star.svg'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<span styleName='tooltip'>{i18n.__('Star')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
.root
|
.root
|
||||||
top 45px
|
top 45px
|
||||||
topBarButtonLight()
|
topBarButtonRight()
|
||||||
&:hover
|
&:hover
|
||||||
transition 0.2s
|
transition 0.2s
|
||||||
color alpha($ui-favorite-star-button-color, 0.6)
|
color alpha($ui-favorite-star-button-color, 0.6)
|
||||||
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 50px
|
||||||
|
right 115px
|
||||||
|
width 40px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
.root--active
|
.root--active
|
||||||
@extend .root
|
@extend .root
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
import styles from './TagSelect.styl'
|
import styles from './TagSelect.styl'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
class TagSelect extends React.Component {
|
class TagSelect extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -64,7 +65,8 @@ class TagSelect extends React.Component {
|
|||||||
submitTag () {
|
submitTag () {
|
||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
|
||||||
let { value } = this.props
|
let { value } = this.props
|
||||||
const newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
|
let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
|
||||||
|
newTag = newTag.charAt(0) === '#' ? newTag.substring(1) : newTag
|
||||||
|
|
||||||
if (newTag.length <= 0) {
|
if (newTag.length <= 0) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -136,7 +138,7 @@ class TagSelect extends React.Component {
|
|||||||
<input styleName='newTag'
|
<input styleName='newTag'
|
||||||
ref='newTag'
|
ref='newTag'
|
||||||
value={this.state.newTag}
|
value={this.state.newTag}
|
||||||
placeholder='Add tag...'
|
placeholder={i18n.__('Add tag...')}
|
||||||
onChange={(e) => this.handleNewTagInputChange(e)}
|
onChange={(e) => this.handleNewTagInputChange(e)}
|
||||||
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
|
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
|
||||||
onBlur={(e) => this.handleNewTagBlur(e)}
|
onBlur={(e) => this.handleNewTagBlur(e)}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
width 100%
|
width 100%
|
||||||
overflow-x scroll
|
overflow-x scroll
|
||||||
white-space nowrap
|
white-space nowrap
|
||||||
|
margin-top 31px
|
||||||
|
position absolute
|
||||||
|
|
||||||
.root::-webkit-scrollbar
|
.root::-webkit-scrollbar
|
||||||
display none
|
display none
|
||||||
@@ -63,4 +65,20 @@ body[data-theme="dark"]
|
|||||||
.newTag
|
.newTag
|
||||||
border-color none
|
border-color none
|
||||||
background-color transparent
|
background-color transparent
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.tag
|
||||||
|
background-color $ui-solarized-dark-tag-backgroundColor
|
||||||
|
|
||||||
|
.tag-removeButton
|
||||||
|
border-color $ui-button--focus-borderColor
|
||||||
|
background-color transparent
|
||||||
|
|
||||||
|
.tag-label
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.newTag
|
||||||
|
border-color none
|
||||||
|
background-color transparent
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
26
browser/main/Detail/ToggleModeButton.js
Normal file
26
browser/main/Detail/ToggleModeButton.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './ToggleModeButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
|
const ToggleModeButton = ({
|
||||||
|
onClick, editorType
|
||||||
|
}) => (
|
||||||
|
<div styleName='control-toggleModeButton'>
|
||||||
|
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}>
|
||||||
|
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
|
||||||
|
</div>
|
||||||
|
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
|
||||||
|
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
|
||||||
|
</div>
|
||||||
|
<span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
ToggleModeButton.propTypes = {
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
editorType: PropTypes.string.Required
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(ToggleModeButton, styles)
|
||||||
58
browser/main/Detail/ToggleModeButton.styl
Normal file
58
browser/main/Detail/ToggleModeButton.styl
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
.control-toggleModeButton
|
||||||
|
height 25px
|
||||||
|
border-radius 50px
|
||||||
|
background-color #F4F4F4
|
||||||
|
width 52px
|
||||||
|
display flex
|
||||||
|
align-items center
|
||||||
|
position: relative
|
||||||
|
top 2px
|
||||||
|
.active
|
||||||
|
background-color #1EC38B
|
||||||
|
width 33px
|
||||||
|
height 24px
|
||||||
|
box-shadow 2px 0px 7px #eee
|
||||||
|
z-index 1
|
||||||
|
|
||||||
|
div
|
||||||
|
width 40px
|
||||||
|
height 100%
|
||||||
|
border-radius 50%
|
||||||
|
display flex
|
||||||
|
align-items center
|
||||||
|
justify-content center
|
||||||
|
cursor pointer
|
||||||
|
|
||||||
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 33px
|
||||||
|
left -10px
|
||||||
|
z-index 200
|
||||||
|
width 80px
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.control-fullScreenButton
|
||||||
|
topBarButtonDark()
|
||||||
|
|
||||||
|
.control-toggleModeButton
|
||||||
|
background-color #3A404C
|
||||||
|
.active
|
||||||
|
background-color #1EC38B
|
||||||
|
box-shadow 2px 0px 7px #444444
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.control-toggleModeButton
|
||||||
|
background-color #002B36
|
||||||
|
.active
|
||||||
|
background-color #1EC38B
|
||||||
|
box-shadow 2px 0px 7px #222222
|
||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './TrashButton.styl'
|
import styles from './TrashButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const TrashButton = ({
|
const TrashButton = ({
|
||||||
onClick
|
onClick
|
||||||
@@ -10,6 +11,7 @@ const TrashButton = ({
|
|||||||
onClick={(e) => onClick(e)}
|
onClick={(e) => onClick(e)}
|
||||||
>
|
>
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
||||||
|
<span styleName='tooltip'>{i18n.__('Trash')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,25 @@
|
|||||||
.control-trashButton
|
.control-trashButton
|
||||||
top 120px
|
top 115px
|
||||||
margin-bottom 10px
|
topBarButtonRight()
|
||||||
topBarButtonLight()
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 50px
|
||||||
|
right 50px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
.control-trashButton--in-trash
|
.control-trashButton--in-trash
|
||||||
top 60px
|
top 60px
|
||||||
topBarButtonLight()
|
topBarButtonRight()
|
||||||
|
|
||||||
.trashButton
|
.trashButton
|
||||||
padding 0px
|
padding 0px
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import MarkdownNoteDetail from './MarkdownNoteDetail'
|
|||||||
import SnippetNoteDetail from './SnippetNoteDetail'
|
import SnippetNoteDetail from './SnippetNoteDetail'
|
||||||
import ee from 'browser/main/lib/eventEmitter'
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
import StatusBar from '../StatusBar'
|
import StatusBar from '../StatusBar'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const OSX = global.process.platform === 'darwin'
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
@@ -32,15 +33,32 @@ class Detail extends React.Component {
|
|||||||
ee.off('detail:delete', this.deleteHandler)
|
ee.off('detail:delete', this.deleteHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
confirmDeletion (permanent) {
|
||||||
|
if (this.props.config.ui.confirmDeletion || permanent) {
|
||||||
|
const electron = require('electron')
|
||||||
|
const { remote } = electron
|
||||||
|
const { dialog } = remote
|
||||||
|
|
||||||
|
const alertConfig = {
|
||||||
|
type: 'warning',
|
||||||
|
message: i18n.__('Confirm note deletion'),
|
||||||
|
detail: i18n.__('This will permanently remove this note.'),
|
||||||
|
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), alertConfig)
|
||||||
|
return dialogueButtonIndex === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { location, data, config } = this.props
|
const { location, data, config } = this.props
|
||||||
let note = null
|
let note = null
|
||||||
if (location.query.key != null) {
|
if (location.query.key != null) {
|
||||||
const splitted = location.query.key.split('-')
|
const noteKey = location.query.key
|
||||||
const storageKey = splitted.shift()
|
note = data.noteMap.get(noteKey)
|
||||||
const noteKey = splitted.shift()
|
|
||||||
|
|
||||||
note = data.noteMap.get(storageKey + '-' + noteKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
@@ -50,7 +68,7 @@ class Detail extends React.Component {
|
|||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
>
|
>
|
||||||
<div styleName='empty'>
|
<div styleName='empty'>
|
||||||
<div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br />to create a new note</div>
|
<div styleName='empty-message'>{OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')} + N<br />{i18n.__('to create a new note')}</div>
|
||||||
</div>
|
</div>
|
||||||
<StatusBar
|
<StatusBar
|
||||||
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
||||||
@@ -64,6 +82,7 @@ class Detail extends React.Component {
|
|||||||
<SnippetNoteDetail
|
<SnippetNoteDetail
|
||||||
note={note}
|
note={note}
|
||||||
config={config}
|
config={config}
|
||||||
|
confirmDeletion={(permanent) => this.confirmDeletion(permanent)}
|
||||||
ref='root'
|
ref='root'
|
||||||
{..._.pick(this.props, [
|
{..._.pick(this.props, [
|
||||||
'dispatch',
|
'dispatch',
|
||||||
@@ -80,6 +99,7 @@ class Detail extends React.Component {
|
|||||||
<MarkdownNoteDetail
|
<MarkdownNoteDetail
|
||||||
note={note}
|
note={note}
|
||||||
config={config}
|
config={config}
|
||||||
|
confirmDeletion={(permanent) => this.confirmDeletion(permanent)}
|
||||||
ref='root'
|
ref='root'
|
||||||
{..._.pick(this.props, [
|
{..._.pick(this.props, [
|
||||||
'dispatch',
|
'dispatch',
|
||||||
|
|||||||
@@ -10,10 +10,14 @@ import Detail from './Detail'
|
|||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
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 mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
|
import { hashHistory } from 'react-router'
|
||||||
|
import store from 'browser/main/store'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
const path = require('path')
|
||||||
|
const electron = require('electron')
|
||||||
|
const { remote } = electron
|
||||||
|
|
||||||
class Main extends React.Component {
|
class Main extends React.Component {
|
||||||
|
|
||||||
@@ -48,6 +52,91 @@ class Main extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init () {
|
||||||
|
dataApi
|
||||||
|
.addStorage({
|
||||||
|
name: 'My Storage',
|
||||||
|
path: path.join(remote.app.getPath('home'), 'Boostnote')
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (data.storage.folders[0] != null) {
|
||||||
|
return data
|
||||||
|
} else {
|
||||||
|
return dataApi
|
||||||
|
.createFolder(data.storage.key, {
|
||||||
|
color: '#1278BD',
|
||||||
|
name: 'Default'
|
||||||
|
})
|
||||||
|
.then((_data) => {
|
||||||
|
return {
|
||||||
|
storage: _data.storage,
|
||||||
|
notes: data.notes
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
console.log(data)
|
||||||
|
store.dispatch({
|
||||||
|
type: 'ADD_STORAGE',
|
||||||
|
storage: data.storage,
|
||||||
|
notes: data.notes
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultSnippetNote = dataApi
|
||||||
|
.createNote(data.storage.key, {
|
||||||
|
type: 'SNIPPET_NOTE',
|
||||||
|
folder: data.storage.folders[0].key,
|
||||||
|
title: 'Snippet note example',
|
||||||
|
description: 'Snippet note example\nYou can store a series of snippets as a single note, like Gist.',
|
||||||
|
snippets: [
|
||||||
|
{
|
||||||
|
name: 'example.html',
|
||||||
|
mode: 'html',
|
||||||
|
content: '<html>\n<body>\n<h1 id=\'hello\'>Enjoy Boostnote!</h1>\n</body>\n</html>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'example.js',
|
||||||
|
mode: 'javascript',
|
||||||
|
content: 'var boostnote = document.getElementById(\'enjoy\').innerHTML\n\nconsole.log(boostnote)'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.then((note) => {
|
||||||
|
store.dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note: note
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const defaultMarkdownNote = dataApi
|
||||||
|
.createNote(data.storage.key, {
|
||||||
|
type: 'MARKDOWN_NOTE',
|
||||||
|
folder: data.storage.folders[0].key,
|
||||||
|
title: 'Welcome to Boostnote!',
|
||||||
|
content: '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)'
|
||||||
|
})
|
||||||
|
.then((note) => {
|
||||||
|
store.dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note: note
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return Promise.resolve(defaultSnippetNote)
|
||||||
|
.then(defaultMarkdownNote)
|
||||||
|
.then(() => data.storage)
|
||||||
|
})
|
||||||
|
.then((storage) => {
|
||||||
|
hashHistory.push('/storages/' + storage.key)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch, config } = this.props
|
const { dispatch, config } = this.props
|
||||||
|
|
||||||
@@ -55,9 +144,42 @@ class Main extends React.Component {
|
|||||||
document.body.setAttribute('data-theme', 'dark')
|
document.body.setAttribute('data-theme', 'dark')
|
||||||
} else if (config.ui.theme === 'white') {
|
} else if (config.ui.theme === 'white') {
|
||||||
document.body.setAttribute('data-theme', 'white')
|
document.body.setAttribute('data-theme', 'white')
|
||||||
|
} else if (config.ui.theme === 'solarized-dark') {
|
||||||
|
document.body.setAttribute('data-theme', 'solarized-dark')
|
||||||
} else {
|
} else {
|
||||||
document.body.setAttribute('data-theme', 'default')
|
document.body.setAttribute('data-theme', 'default')
|
||||||
}
|
}
|
||||||
|
if (config.ui.language === 'sq') {
|
||||||
|
i18n.setLocale('sq')
|
||||||
|
} else if (config.ui.language === 'zh-CN') {
|
||||||
|
i18n.setLocale('zh-CN')
|
||||||
|
} else if (config.ui.language === 'zh-TW') {
|
||||||
|
i18n.setLocale('zh-TW')
|
||||||
|
} else if (config.ui.language === 'da') {
|
||||||
|
i18n.setLocale('da')
|
||||||
|
} else if (config.ui.language === 'fr') {
|
||||||
|
i18n.setLocale('fr')
|
||||||
|
} else if (config.ui.language === 'de') {
|
||||||
|
i18n.setLocale('de')
|
||||||
|
} else if (config.ui.language === 'hu') {
|
||||||
|
i18n.setLocale('hu')
|
||||||
|
} else if (config.ui.language === 'ja') {
|
||||||
|
i18n.setLocale('ja')
|
||||||
|
} else if (config.ui.language === 'ko') {
|
||||||
|
i18n.setLocale('ko')
|
||||||
|
} else if (config.ui.language === 'no') {
|
||||||
|
i18n.setLocale('no')
|
||||||
|
} else if (config.ui.language === 'pl') {
|
||||||
|
i18n.setLocale('pl')
|
||||||
|
} else if (config.ui.language === 'pt') {
|
||||||
|
i18n.setLocale('pt')
|
||||||
|
} else if (config.ui.language === 'ru') {
|
||||||
|
i18n.setLocale('ru')
|
||||||
|
} else if (config.ui.language === 'es') {
|
||||||
|
i18n.setLocale('es')
|
||||||
|
} else {
|
||||||
|
i18n.setLocale('en')
|
||||||
|
}
|
||||||
|
|
||||||
// Reload all data
|
// Reload all data
|
||||||
dataApi.init()
|
dataApi.init()
|
||||||
@@ -69,7 +191,7 @@ class Main extends React.Component {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (data.storages.length < 1) {
|
if (data.storages.length < 1) {
|
||||||
modal.open(InitModal)
|
this.init()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -71,3 +71,7 @@ body[data-theme="dark"]
|
|||||||
|
|
||||||
.control-newNoteButton-tooltip
|
.control-newNoteButton-tooltip
|
||||||
darkTooltip()
|
darkTooltip()
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.root, .root--expanded
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
@@ -6,6 +6,7 @@ import _ from 'lodash'
|
|||||||
import modal from 'browser/main/lib/modal'
|
import modal from 'browser/main/lib/modal'
|
||||||
import NewNoteModal from 'browser/main/modals/NewNoteModal'
|
import NewNoteModal from 'browser/main/modals/NewNoteModal'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { dialog } = remote
|
const { dialog } = remote
|
||||||
@@ -56,9 +57,9 @@ class NewNoteButton extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storage == null) this.showMessageBox('No storage to create a note')
|
if (storage == null) this.showMessageBox(i18n.__('No storage to create a note'))
|
||||||
const folder = _.find(storage.folders, {key: params.folderKey}) || storage.folders[0]
|
const folder = _.find(storage.folders, {key: params.folderKey}) || storage.folders[0]
|
||||||
if (folder == null) this.showMessageBox('No folder to create a note')
|
if (folder == null) this.showMessageBox(i18n.__('No folder to create a note'))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
storage,
|
storage,
|
||||||
@@ -86,7 +87,7 @@ class NewNoteButton extends React.Component {
|
|||||||
onClick={(e) => this.handleNewNoteButtonClick(e)}>
|
onClick={(e) => this.handleNewNoteButtonClick(e)}>
|
||||||
<img styleName='iconTag' src='../resources/icon/icon-newnote.svg' />
|
<img styleName='iconTag' src='../resources/icon/icon-newnote.svg' />
|
||||||
<span styleName='control-newNoteButton-tooltip'>
|
<span styleName='control-newNoteButton-tooltip'>
|
||||||
Make a Note {OSX ? '⌘' : '^'} + n
|
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -88,4 +88,29 @@ body[data-theme="dark"]
|
|||||||
.control-button--active
|
.control-button--active
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
&:active
|
&:active
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.root
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
|
||||||
|
.control
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
|
||||||
|
.control-sortBy-select
|
||||||
|
&:hover
|
||||||
|
transition 0.2s
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.control-button
|
||||||
|
color $ui-solarized-dark-inactive-text-color
|
||||||
|
&:hover
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.control-button--active
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:active
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* global electron */
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
@@ -11,14 +12,16 @@ import NoteItem from 'browser/components/NoteItem'
|
|||||||
import NoteItemSimple from 'browser/components/NoteItemSimple'
|
import NoteItemSimple from 'browser/components/NoteItemSimple'
|
||||||
import searchFromNotes from 'browser/lib/search'
|
import searchFromNotes from 'browser/lib/search'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
import { hashHistory } from 'react-router'
|
import { hashHistory } from 'react-router'
|
||||||
import markdown from 'browser/lib/markdown'
|
import copy from 'copy-to-clipboard'
|
||||||
import { findNoteTitle } from 'browser/lib/findNoteTitle'
|
|
||||||
import store from 'browser/main/store'
|
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
|
import Markdown from '../../lib/markdown'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { Menu, MenuItem, dialog } = remote
|
const { Menu, MenuItem, dialog } = remote
|
||||||
|
const WP_POST_PATH = '/wp/v2/posts'
|
||||||
|
|
||||||
function sortByCreatedAt (a, b) {
|
function sortByCreatedAt (a, b) {
|
||||||
return new Date(b.createdAt) - new Date(a.createdAt)
|
return new Date(b.createdAt) - new Date(a.createdAt)
|
||||||
@@ -33,7 +36,7 @@ function sortByUpdatedAt (a, b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findNoteByKey (notes, noteKey) {
|
function findNoteByKey (notes, noteKey) {
|
||||||
return notes.find((note) => `${note.storage}-${note.key}` === noteKey)
|
return notes.find((note) => note.key === noteKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
function findNotesByKeys (notes, noteKeys) {
|
function findNotesByKeys (notes, noteKeys) {
|
||||||
@@ -41,7 +44,7 @@ function findNotesByKeys (notes, noteKeys) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getNoteKey (note) {
|
function getNoteKey (note) {
|
||||||
return `${note.storage}-${note.key}`
|
return note.key
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoteList extends React.Component {
|
class NoteList extends React.Component {
|
||||||
@@ -68,6 +71,11 @@ class NoteList extends React.Component {
|
|||||||
this.deleteNote = this.deleteNote.bind(this)
|
this.deleteNote = this.deleteNote.bind(this)
|
||||||
this.focusNote = this.focusNote.bind(this)
|
this.focusNote = this.focusNote.bind(this)
|
||||||
this.pinToTop = this.pinToTop.bind(this)
|
this.pinToTop = this.pinToTop.bind(this)
|
||||||
|
this.getNoteStorage = this.getNoteStorage.bind(this)
|
||||||
|
this.getNoteFolder = this.getNoteFolder.bind(this)
|
||||||
|
this.getViewType = this.getViewType.bind(this)
|
||||||
|
this.restoreNote = this.restoreNote.bind(this)
|
||||||
|
this.copyNoteLink = this.copyNoteLink.bind(this)
|
||||||
|
|
||||||
// TODO: not Selected noteKeys but SelectedNote(for reusing)
|
// TODO: not Selected noteKeys but SelectedNote(for reusing)
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -111,14 +119,27 @@ class NoteList extends React.Component {
|
|||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
const { location } = this.props
|
const { location } = this.props
|
||||||
|
const { selectedNoteKeys } = this.state
|
||||||
|
const visibleNoteKeys = this.notes.map(note => note.key)
|
||||||
|
const note = this.notes[0]
|
||||||
|
const prevKey = prevProps.location.query.key
|
||||||
|
const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && note.key
|
||||||
|
|
||||||
if (this.notes.length > 0 && location.query.key == null) {
|
if (note && location.query.key == null) {
|
||||||
const { router } = this.context
|
const { router } = this.context
|
||||||
if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes()
|
if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes()
|
||||||
|
|
||||||
|
// A visible note is an active note
|
||||||
|
if (!selectedNoteKeys.includes(noteKey)) {
|
||||||
|
if (selectedNoteKeys.length === 1) selectedNoteKeys.pop()
|
||||||
|
selectedNoteKeys.push(noteKey)
|
||||||
|
ee.emit('list:moved')
|
||||||
|
}
|
||||||
|
|
||||||
router.replace({
|
router.replace({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
query: {
|
||||||
key: this.notes[0].storage + '-' + this.notes[0].key
|
key: noteKey
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -171,9 +192,8 @@ class NoteList extends React.Component {
|
|||||||
if (this.notes == null || this.notes.length === 0) {
|
if (this.notes == null || this.notes.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let { router } = this.context
|
let { selectedNoteKeys } = this.state
|
||||||
let { location } = this.props
|
const { shiftKeyDown } = this.state
|
||||||
let { selectedNoteKeys, shiftKeyDown } = this.state
|
|
||||||
|
|
||||||
let targetIndex = this.getTargetIndex()
|
let targetIndex = this.getTargetIndex()
|
||||||
|
|
||||||
@@ -199,9 +219,8 @@ class NoteList extends React.Component {
|
|||||||
if (this.notes == null || this.notes.length === 0) {
|
if (this.notes == null || this.notes.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let { router } = this.context
|
let { selectedNoteKeys } = this.state
|
||||||
let { location } = this.props
|
const { shiftKeyDown } = this.state
|
||||||
let { selectedNoteKeys, shiftKeyDown } = this.state
|
|
||||||
|
|
||||||
let targetIndex = this.getTargetIndex()
|
let targetIndex = this.getTargetIndex()
|
||||||
const isTargetLastNote = targetIndex === this.notes.length - 1
|
const isTargetLastNote = targetIndex === this.notes.length - 1
|
||||||
@@ -235,47 +254,47 @@ class NoteList extends React.Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { router } = this.context
|
const selectedNoteKeys = [noteHash]
|
||||||
const { location } = this.props
|
this.focusNote(selectedNoteKeys, noteHash)
|
||||||
|
|
||||||
let targetIndex = this.getTargetIndex()
|
|
||||||
|
|
||||||
if (targetIndex < 0) targetIndex = 0
|
|
||||||
|
|
||||||
const selectedNoteKeys = []
|
|
||||||
const nextNoteKey = this.getNoteKeyFromTargetIndex(targetIndex)
|
|
||||||
selectedNoteKeys.push(nextNoteKey)
|
|
||||||
|
|
||||||
this.focusNote(selectedNoteKeys, nextNoteKey)
|
|
||||||
|
|
||||||
ee.emit('list:moved')
|
ee.emit('list:moved')
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNoteListKeyDown (e) {
|
handleNoteListKeyDown (e) {
|
||||||
const { shiftKeyDown } = this.state
|
|
||||||
if (e.metaKey || e.ctrlKey) return true
|
if (e.metaKey || e.ctrlKey) return true
|
||||||
|
|
||||||
|
// A key
|
||||||
if (e.keyCode === 65 && !e.shiftKey) {
|
if (e.keyCode === 65 && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
ee.emit('top:new-note')
|
ee.emit('top:new-note')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// D key
|
||||||
if (e.keyCode === 68) {
|
if (e.keyCode === 68) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.deleteNote()
|
this.deleteNote()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// E key
|
||||||
if (e.keyCode === 69) {
|
if (e.keyCode === 69) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
ee.emit('detail:focus')
|
ee.emit('detail:focus')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.keyCode === 38) {
|
// F or S key
|
||||||
|
if (e.keyCode === 70 || e.keyCode === 83) {
|
||||||
|
e.preventDefault()
|
||||||
|
ee.emit('top:focus-search')
|
||||||
|
}
|
||||||
|
|
||||||
|
// UP or K key
|
||||||
|
if (e.keyCode === 38 || e.keyCode === 75) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.selectPriorNote()
|
this.selectPriorNote()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.keyCode === 40) {
|
// DOWN or J key
|
||||||
|
if (e.keyCode === 40 || e.keyCode === 74) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.selectNextNote()
|
this.selectNextNote()
|
||||||
}
|
}
|
||||||
@@ -294,7 +313,7 @@ class NoteList extends React.Component {
|
|||||||
getNotes () {
|
getNotes () {
|
||||||
const { data, params, location } = this.props
|
const { data, params, location } = this.props
|
||||||
|
|
||||||
if (location.pathname.match(/\/home/) || location.pathname.match(/\alltags/)) {
|
if (location.pathname.match(/\/home/) || location.pathname.match(/alltags/)) {
|
||||||
const allNotes = data.noteMap.map((note) => note)
|
const allNotes = data.noteMap.map((note) => note)
|
||||||
this.contextNotes = allNotes
|
this.contextNotes = allNotes
|
||||||
return allNotes
|
return allNotes
|
||||||
@@ -365,9 +384,10 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleNoteClick (e, uniqueKey) {
|
handleNoteClick (e, uniqueKey) {
|
||||||
let { router } = this.context
|
const { router } = this.context
|
||||||
let { location } = this.props
|
const { location } = this.props
|
||||||
let { shiftKeyDown, selectedNoteKeys } = this.state
|
let { selectedNoteKeys } = this.state
|
||||||
|
const { shiftKeyDown } = this.state
|
||||||
|
|
||||||
if (shiftKeyDown && selectedNoteKeys.includes(uniqueKey)) {
|
if (shiftKeyDown && selectedNoteKeys.includes(uniqueKey)) {
|
||||||
const newSelectedNoteKeys = selectedNoteKeys.filter((noteKey) => noteKey !== uniqueKey)
|
const newSelectedNoteKeys = selectedNoteKeys.filter((noteKey) => noteKey !== uniqueKey)
|
||||||
@@ -425,9 +445,9 @@ class NoteList extends React.Component {
|
|||||||
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
|
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
|
||||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Sorry!',
|
message: i18n.__('Sorry!'),
|
||||||
detail: 'md/text import is available only a markdown note.',
|
detail: i18n.__('md/text import is available only a markdown note.'),
|
||||||
buttons: ['OK', 'Cancel']
|
buttons: [i18n.__('OK'), i18n.__('Cancel')]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -451,45 +471,107 @@ class NoteList extends React.Component {
|
|||||||
this.handleNoteClick(e, uniqueKey)
|
this.handleNoteClick(e, uniqueKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pinLabel = note.isPinned ? 'Remove pin' : 'Pin to Top'
|
const pinLabel = note.isPinned ? i18n.__('Remove pin') : i18n.__('Pin to Top')
|
||||||
const deleteLabel = 'Delete Note'
|
const deleteLabel = i18n.__('Delete Note')
|
||||||
|
const cloneNote = i18n.__('Clone Note')
|
||||||
|
const restoreNote = i18n.__('Restore Note')
|
||||||
|
const copyNoteLink = i18n.__('Copy Note Link')
|
||||||
|
const publishLabel = i18n.__('Publish Blog')
|
||||||
|
const updateLabel = i18n.__('Update Blog')
|
||||||
|
const openBlogLabel = i18n.__('Open Blog')
|
||||||
|
|
||||||
const menu = new Menu()
|
const menu = new Menu()
|
||||||
if (!location.pathname.match(/\/home|\/starred|\/trash/)) {
|
if (!location.pathname.match(/\/starred|\/trash/)) {
|
||||||
menu.append(new MenuItem({
|
menu.append(new MenuItem({
|
||||||
label: pinLabel,
|
label: pinLabel,
|
||||||
click: this.pinToTop
|
click: this.pinToTop
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (location.pathname.match(/\/trash/)) {
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: restoreNote,
|
||||||
|
click: this.restoreNote
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
menu.append(new MenuItem({
|
menu.append(new MenuItem({
|
||||||
label: deleteLabel,
|
label: deleteLabel,
|
||||||
click: this.deleteNote
|
click: this.deleteNote
|
||||||
}))
|
}))
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: cloneNote,
|
||||||
|
click: this.cloneNote.bind(this)
|
||||||
|
}))
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: copyNoteLink,
|
||||||
|
click: this.copyNoteLink(note)
|
||||||
|
}))
|
||||||
|
if (note.type === 'MARKDOWN_NOTE') {
|
||||||
|
if (note.blog && note.blog.blogLink && note.blog.blogId) {
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: updateLabel,
|
||||||
|
click: this.publishMarkdown.bind(this)
|
||||||
|
}))
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: openBlogLabel,
|
||||||
|
click: () => this.openBlog.bind(this)(note)
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: publishLabel,
|
||||||
|
click: this.publishMarkdown.bind(this)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
menu.popup()
|
menu.popup()
|
||||||
}
|
}
|
||||||
|
|
||||||
pinToTop () {
|
updateSelectedNotes (updateFunc, cleanSelection = true) {
|
||||||
const { selectedNoteKeys } = this.state
|
const { selectedNoteKeys } = this.state
|
||||||
const { dispatch } = this.props
|
const { dispatch } = this.props
|
||||||
const notes = this.notes.map((note) => Object.assign({}, note))
|
const notes = this.notes.map((note) => Object.assign({}, note))
|
||||||
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||||
|
|
||||||
|
if (!_.isFunction(updateFunc)) {
|
||||||
|
console.warn('Update function is not defined. No update will happen')
|
||||||
|
updateFunc = (note) => { return note }
|
||||||
|
}
|
||||||
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
selectedNotes.map((note) => {
|
selectedNotes.map((note) => {
|
||||||
note.isPinned = !note.isPinned
|
note = updateFunc(note)
|
||||||
return dataApi
|
return dataApi
|
||||||
.updateNote(note.storage, note.key, note)
|
.updateNote(note.storage, note.key, note)
|
||||||
})
|
|
||||||
)
|
|
||||||
.then((updatedNotes) => {
|
|
||||||
updatedNotes.forEach((note) => {
|
|
||||||
dispatch({
|
|
||||||
type: 'UPDATE_NOTE',
|
|
||||||
note
|
|
||||||
})
|
})
|
||||||
})
|
)
|
||||||
|
.then((updatedNotes) => {
|
||||||
|
updatedNotes.forEach((note) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (cleanSelection) {
|
||||||
|
this.selectNextNote()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pinToTop () {
|
||||||
|
this.updateSelectedNotes((note) => {
|
||||||
|
note.isPinned = !note.isPinned
|
||||||
|
return note
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreNote () {
|
||||||
|
this.updateSelectedNotes((note) => {
|
||||||
|
note.isTrashed = false
|
||||||
|
return note
|
||||||
})
|
})
|
||||||
this.setState({ selectedNoteKeys: [] })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteNote () {
|
deleteNote () {
|
||||||
@@ -503,17 +585,15 @@ class NoteList extends React.Component {
|
|||||||
const noteExp = selectedNotes.length > 1 ? 'notes' : 'note'
|
const noteExp = selectedNotes.length > 1 ? 'notes' : 'note'
|
||||||
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Confirm note deletion',
|
message: i18n.__('Confirm note deletion'),
|
||||||
detail: `This will permanently remove ${selectedNotes.length} ${noteExp}.`,
|
detail: `This will permanently remove ${selectedNotes.length} ${noteExp}.`,
|
||||||
buttons: ['Confirm', 'Cancel']
|
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||||
})
|
})
|
||||||
if (dialogueButtonIndex === 1) return
|
if (dialogueButtonIndex === 1) return
|
||||||
Promise.all(
|
Promise.all(
|
||||||
selectedNoteKeys.map((uniqueKey) => {
|
selectedNotes.map((note) => {
|
||||||
const storageKey = uniqueKey.split('-')[0]
|
|
||||||
const noteKey = uniqueKey.split('-')[1]
|
|
||||||
return dataApi
|
return dataApi
|
||||||
.deleteNote(storageKey, noteKey)
|
.deleteNote(note.storage, note.key)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@@ -555,6 +635,153 @@ class NoteList extends React.Component {
|
|||||||
this.setState({ selectedNoteKeys: [] })
|
this.setState({ selectedNoteKeys: [] })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cloneNote () {
|
||||||
|
const { selectedNoteKeys } = this.state
|
||||||
|
const { dispatch, location } = this.props
|
||||||
|
const { storage, folder } = this.resolveTargetFolder()
|
||||||
|
const notes = this.notes.map((note) => Object.assign({}, note))
|
||||||
|
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||||
|
const firstNote = selectedNotes[0]
|
||||||
|
const eventName = firstNote.type === 'MARKDOWN_NOTE' ? 'ADD_MARKDOWN' : 'ADD_SNIPPET'
|
||||||
|
|
||||||
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent(eventName)
|
||||||
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||||
|
dataApi
|
||||||
|
.createNote(storage.key, {
|
||||||
|
type: firstNote.type,
|
||||||
|
folder: folder.key,
|
||||||
|
title: firstNote.title + ' copy',
|
||||||
|
content: firstNote.content
|
||||||
|
})
|
||||||
|
.then((note) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note: note
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
selectedNoteKeys: [note.key]
|
||||||
|
})
|
||||||
|
|
||||||
|
hashHistory.push({
|
||||||
|
pathname: location.pathname,
|
||||||
|
query: {key: note.key}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
copyNoteLink (note) {
|
||||||
|
const noteLink = `[${note.title}](:note:${note.key})`
|
||||||
|
return copy(noteLink)
|
||||||
|
}
|
||||||
|
|
||||||
|
save (note) {
|
||||||
|
const { dispatch } = this.props
|
||||||
|
dataApi
|
||||||
|
.updateNote(note.storage, note.key, note)
|
||||||
|
.then((note) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note: note
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
publishMarkdown () {
|
||||||
|
if (this.pendingPublish) {
|
||||||
|
clearTimeout(this.pendingPublish)
|
||||||
|
}
|
||||||
|
this.pendingPublish = setTimeout(() => {
|
||||||
|
this.publishMarkdownNow()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
publishMarkdownNow () {
|
||||||
|
const {selectedNoteKeys} = this.state
|
||||||
|
const notes = this.notes.map((note) => Object.assign({}, note))
|
||||||
|
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||||
|
const firstNote = selectedNotes[0]
|
||||||
|
const config = ConfigManager.get()
|
||||||
|
const {address, token, authMethod, username, password} = config.blog
|
||||||
|
let authToken = ''
|
||||||
|
if (authMethod === 'USER') {
|
||||||
|
authToken = `Basic ${window.btoa(`${username}:${password}`)}`
|
||||||
|
} else {
|
||||||
|
authToken = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
const contentToRender = firstNote.content.replace(`# ${firstNote.title}`, '')
|
||||||
|
const markdown = new Markdown()
|
||||||
|
const data = {
|
||||||
|
title: firstNote.title,
|
||||||
|
content: markdown.render(contentToRender),
|
||||||
|
status: 'publish'
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = ''
|
||||||
|
let method = ''
|
||||||
|
if (firstNote.blog && firstNote.blog.blogId) {
|
||||||
|
url = `${address}${WP_POST_PATH}/${firstNote.blog.blogId}`
|
||||||
|
method = 'PUT'
|
||||||
|
} else {
|
||||||
|
url = `${address}${WP_POST_PATH}`
|
||||||
|
method = 'POST'
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
fetch(url, {
|
||||||
|
method: method,
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
headers: {
|
||||||
|
'Authorization': authToken,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}).then(res => res.json())
|
||||||
|
.then(response => {
|
||||||
|
if (_.isNil(response.link) || _.isNil(response.id)) {
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
firstNote.blog = {
|
||||||
|
blogLink: response.link,
|
||||||
|
blogId: response.id
|
||||||
|
}
|
||||||
|
this.save(firstNote)
|
||||||
|
this.confirmPublish(firstNote)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
this.confirmPublishError()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmPublishError () {
|
||||||
|
const { remote } = electron
|
||||||
|
const { dialog } = remote
|
||||||
|
const alertError = {
|
||||||
|
type: 'warning',
|
||||||
|
message: i18n.__('Publish Failed'),
|
||||||
|
detail: i18n.__('Check and update your blog setting and try again.'),
|
||||||
|
buttons: [i18n.__('Confirm')]
|
||||||
|
}
|
||||||
|
dialog.showMessageBox(remote.getCurrentWindow(), alertError)
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmPublish (note) {
|
||||||
|
const buttonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
|
type: 'warning',
|
||||||
|
message: i18n.__('Publish Succeeded'),
|
||||||
|
detail: `${note.title} is published at ${note.blog.blogLink}`,
|
||||||
|
buttons: [i18n.__('Confirm'), i18n.__('Open Blog')]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (buttonIndex === 1) {
|
||||||
|
this.openBlog(note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openBlog (note) {
|
||||||
|
const { shell } = electron
|
||||||
|
shell.openExternal(note.blog.blogLink)
|
||||||
|
}
|
||||||
|
|
||||||
importFromFile () {
|
importFromFile () {
|
||||||
const options = {
|
const options = {
|
||||||
filters: [
|
filters: [
|
||||||
@@ -584,22 +811,29 @@ class NoteList extends React.Component {
|
|||||||
filepaths.forEach((filepath) => {
|
filepaths.forEach((filepath) => {
|
||||||
fs.readFile(filepath, (err, data) => {
|
fs.readFile(filepath, (err, data) => {
|
||||||
if (err) throw Error('File reading error: ', err)
|
if (err) throw Error('File reading error: ', err)
|
||||||
const content = data.toString()
|
|
||||||
const newNote = {
|
fs.stat(filepath, (err, {mtime, birthtime}) => {
|
||||||
content: content,
|
if (err) throw Error('File stat reading error: ', err)
|
||||||
folder: folder.key,
|
|
||||||
title: markdown.strip(findNoteTitle(content)),
|
const content = data.toString()
|
||||||
type: 'MARKDOWN_NOTE'
|
const newNote = {
|
||||||
}
|
content: content,
|
||||||
dataApi.createNote(storage.key, newNote)
|
folder: folder.key,
|
||||||
.then((note) => {
|
title: path.basename(filepath, path.extname(filepath)),
|
||||||
dispatch({
|
type: 'MARKDOWN_NOTE',
|
||||||
type: 'UPDATE_NOTE',
|
createdAt: birthtime,
|
||||||
note: note
|
updatedAt: mtime
|
||||||
})
|
}
|
||||||
hashHistory.push({
|
dataApi.createNote(storage.key, newNote)
|
||||||
pathname: location.pathname,
|
.then((note) => {
|
||||||
query: {key: getNoteKey(note)}
|
dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note: note
|
||||||
|
})
|
||||||
|
hashHistory.push({
|
||||||
|
pathname: location.pathname,
|
||||||
|
query: {key: getNoteKey(note)}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -640,19 +874,38 @@ class NoteList extends React.Component {
|
|||||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: message,
|
message: message,
|
||||||
buttons: ['OK']
|
buttons: [i18n.__('OK')]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNoteStorage (note) { // note.storage = storage key
|
||||||
|
return this.props.data.storageMap.toJS()[note.storage]
|
||||||
|
}
|
||||||
|
|
||||||
|
getNoteFolder (note) { // note.folder = folder key
|
||||||
|
return _.find(this.getNoteStorage(note).folders, ({ key }) => key === note.folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
getViewType () {
|
||||||
|
const { pathname } = this.props.location
|
||||||
|
const folder = /\/folders\/[a-zA-Z0-9]+/.test(pathname)
|
||||||
|
const storage = /\/storages\/[a-zA-Z0-9]+/.test(pathname) && !folder
|
||||||
|
const allNotes = pathname === '/home'
|
||||||
|
if (allNotes) return 'ALL'
|
||||||
|
if (folder) return 'FOLDER'
|
||||||
|
if (storage) return 'STORAGE'
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { location, notes, config, dispatch } = this.props
|
const { location, config } = this.props
|
||||||
let { selectedNoteKeys } = this.state
|
let { notes } = this.props
|
||||||
let sortFunc = config.sortBy === 'CREATED_AT'
|
const { selectedNoteKeys } = this.state
|
||||||
|
const sortFunc = config.sortBy === 'CREATED_AT'
|
||||||
? sortByCreatedAt
|
? sortByCreatedAt
|
||||||
: config.sortBy === 'ALPHABETICAL'
|
: config.sortBy === 'ALPHABETICAL'
|
||||||
? sortByAlphabetical
|
? sortByAlphabetical
|
||||||
: sortByUpdatedAt
|
: sortByUpdatedAt
|
||||||
const sortedNotes = location.pathname.match(/\/home|\/starred|\/trash/)
|
const sortedNotes = location.pathname.match(/\/starred|\/trash/)
|
||||||
? this.getNotes().sort(sortFunc)
|
? this.getNotes().sort(sortFunc)
|
||||||
: this.sortByPin(this.getNotes().sort(sortFunc))
|
: this.sortByPin(this.getNotes().sort(sortFunc))
|
||||||
this.notes = notes = sortedNotes.filter((note) => {
|
this.notes = notes = sortedNotes.filter((note) => {
|
||||||
@@ -679,6 +932,8 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const viewType = this.getViewType()
|
||||||
|
|
||||||
const noteList = notes
|
const noteList = notes
|
||||||
.map(note => {
|
.map(note => {
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
@@ -692,7 +947,6 @@ class NoteList extends React.Component {
|
|||||||
config.sortBy === 'CREATED_AT'
|
config.sortBy === 'CREATED_AT'
|
||||||
? note.createdAt : note.updatedAt
|
? note.createdAt : note.updatedAt
|
||||||
).fromNow('D')
|
).fromNow('D')
|
||||||
const key = `${note.storage}-${note.key}`
|
|
||||||
|
|
||||||
if (isDefault) {
|
if (isDefault) {
|
||||||
return (
|
return (
|
||||||
@@ -705,6 +959,9 @@ class NoteList extends React.Component {
|
|||||||
handleNoteClick={this.handleNoteClick.bind(this)}
|
handleNoteClick={this.handleNoteClick.bind(this)}
|
||||||
handleDragStart={this.handleDragStart.bind(this)}
|
handleDragStart={this.handleDragStart.bind(this)}
|
||||||
pathname={location.pathname}
|
pathname={location.pathname}
|
||||||
|
folderName={this.getNoteFolder(note).name}
|
||||||
|
storageName={this.getNoteStorage(note).name}
|
||||||
|
viewType={viewType}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -717,6 +974,10 @@ class NoteList extends React.Component {
|
|||||||
handleNoteContextMenu={this.handleNoteContextMenu.bind(this)}
|
handleNoteContextMenu={this.handleNoteContextMenu.bind(this)}
|
||||||
handleNoteClick={this.handleNoteClick.bind(this)}
|
handleNoteClick={this.handleNoteClick.bind(this)}
|
||||||
handleDragStart={this.handleDragStart.bind(this)}
|
handleDragStart={this.handleDragStart.bind(this)}
|
||||||
|
pathname={location.pathname}
|
||||||
|
folderName={this.getNoteFolder(note).name}
|
||||||
|
storageName={this.getNoteStorage(note).name}
|
||||||
|
viewType={viewType}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -731,16 +992,17 @@ class NoteList extends React.Component {
|
|||||||
<div styleName='control-sortBy'>
|
<div styleName='control-sortBy'>
|
||||||
<i className='fa fa-angle-down' />
|
<i className='fa fa-angle-down' />
|
||||||
<select styleName='control-sortBy-select'
|
<select styleName='control-sortBy-select'
|
||||||
|
title={i18n.__('Select filter mode')}
|
||||||
value={config.sortBy}
|
value={config.sortBy}
|
||||||
onChange={(e) => this.handleSortByChange(e)}
|
onChange={(e) => this.handleSortByChange(e)}
|
||||||
>
|
>
|
||||||
<option value='UPDATED_AT'>Updated</option>
|
<option title='Sort by update time' value='UPDATED_AT'>{i18n.__('Updated')}</option>
|
||||||
<option value='CREATED_AT'>Created</option>
|
<option title='Sort by create time' value='CREATED_AT'>{i18n.__('Created')}</option>
|
||||||
<option value='ALPHABETICAL'>Alphabetically</option>
|
<option title='Sort alphabetically' value='ALPHABETICAL'>{i18n.__('Alphabetically')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='control-button-area'>
|
<div styleName='control-button-area'>
|
||||||
<button styleName={config.listStyle === 'DEFAULT'
|
<button title={i18n.__('Default View')} styleName={config.listStyle === 'DEFAULT'
|
||||||
? 'control-button--active'
|
? 'control-button--active'
|
||||||
: 'control-button'
|
: 'control-button'
|
||||||
}
|
}
|
||||||
@@ -748,7 +1010,7 @@ class NoteList extends React.Component {
|
|||||||
>
|
>
|
||||||
<img styleName='iconTag' src='../resources/icon/icon-column.svg' />
|
<img styleName='iconTag' src='../resources/icon/icon-column.svg' />
|
||||||
</button>
|
</button>
|
||||||
<button styleName={config.listStyle === 'SMALL'
|
<button title={i18n.__('Compressed View')} styleName={config.listStyle === 'SMALL'
|
||||||
? 'control-button--active'
|
? 'control-button--active'
|
||||||
: 'control-button'
|
: 'control-button'
|
||||||
}
|
}
|
||||||
|
|||||||
25
browser/main/SideNav/ListButton.js
Normal file
25
browser/main/SideNav/ListButton.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './SwitchButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
|
const ListButton = ({
|
||||||
|
onClick, isTagActive
|
||||||
|
}) => (
|
||||||
|
<button styleName={isTagActive ? 'non-active-button' : 'active-button'} onClick={onClick}>
|
||||||
|
<img src={isTagActive
|
||||||
|
? '../resources/icon/icon-list.svg'
|
||||||
|
: '../resources/icon/icon-list-active.svg'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span styleName='tooltip'>{i18n.__('Notes')}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
ListButton.propTypes = {
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
isTagActive: PropTypes.bool.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(ListButton, styles)
|
||||||
20
browser/main/SideNav/PreferenceButton.js
Normal file
20
browser/main/SideNav/PreferenceButton.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './PreferenceButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
|
const PreferenceButton = ({
|
||||||
|
onClick
|
||||||
|
}) => (
|
||||||
|
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
|
||||||
|
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' />
|
||||||
|
<span styleName='tooltip'>{i18n.__('Preferences')}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
PreferenceButton.propTypes = {
|
||||||
|
onClick: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(PreferenceButton, styles)
|
||||||
51
browser/main/SideNav/PreferenceButton.styl
Normal file
51
browser/main/SideNav/PreferenceButton.styl
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
.top-menu-preference
|
||||||
|
navButtonColor()
|
||||||
|
position absolute
|
||||||
|
top 22px
|
||||||
|
right 10px
|
||||||
|
width 2em
|
||||||
|
background-color transparent
|
||||||
|
&:hover
|
||||||
|
color $ui-button-default--active-backgroundColor
|
||||||
|
background-color transparent
|
||||||
|
.tooltip
|
||||||
|
opacity 1
|
||||||
|
&:active, &:active:hover
|
||||||
|
color $ui-button-default--active-backgroundColor
|
||||||
|
|
||||||
|
body[data-theme="white"]
|
||||||
|
.top-menu-preference
|
||||||
|
navWhiteButtonColor()
|
||||||
|
background-color transparent
|
||||||
|
&:hover
|
||||||
|
color #0B99F1
|
||||||
|
background-color transparent
|
||||||
|
&:active, &:active:hover
|
||||||
|
color #0B99F1
|
||||||
|
background-color transparent
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.top-menu-preference
|
||||||
|
navDarkButtonColor()
|
||||||
|
background-color transparent
|
||||||
|
&:active
|
||||||
|
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||||
|
background-color transparent
|
||||||
|
&:hover
|
||||||
|
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||||
|
background-color transparent
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 26px
|
||||||
|
left -20px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
@@ -11,19 +11,6 @@
|
|||||||
.top
|
.top
|
||||||
padding-bottom 15px
|
padding-bottom 15px
|
||||||
|
|
||||||
.top-menu-preference
|
|
||||||
navButtonColor()
|
|
||||||
position absolute
|
|
||||||
top 22px
|
|
||||||
right 10px
|
|
||||||
width 2em
|
|
||||||
background-color transparent
|
|
||||||
&:hover
|
|
||||||
color $ui-button-default--active-backgroundColor
|
|
||||||
background-color transparent
|
|
||||||
&:active, &:active:hover
|
|
||||||
color $ui-button-default--active-backgroundColor
|
|
||||||
|
|
||||||
.switch-buttons
|
.switch-buttons
|
||||||
background-color transparent
|
background-color transparent
|
||||||
border 0
|
border 0
|
||||||
@@ -31,21 +18,7 @@
|
|||||||
display flex
|
display flex
|
||||||
text-align center
|
text-align center
|
||||||
|
|
||||||
.non-active-button
|
|
||||||
color $ui-inactive-text-color
|
|
||||||
font-size 16px
|
|
||||||
border 0
|
|
||||||
background-color transparent
|
|
||||||
transition 0.2s
|
|
||||||
display flex
|
|
||||||
text-align center
|
|
||||||
margin-right 4px;
|
|
||||||
&:hover
|
|
||||||
color alpha(#239F86, 60%)
|
|
||||||
|
|
||||||
.active-button
|
|
||||||
@extend .non-active-button
|
|
||||||
color $ui-button-default--active-backgroundColor
|
|
||||||
|
|
||||||
.top-menu-label
|
.top-menu-label
|
||||||
margin-left 5px
|
margin-left 5px
|
||||||
@@ -109,57 +82,16 @@ body[data-theme="white"]
|
|||||||
background-color #f9f9f9
|
background-color #f9f9f9
|
||||||
color $ui-text-color
|
color $ui-text-color
|
||||||
|
|
||||||
.top-menu-preference
|
|
||||||
navWhiteButtonColor()
|
|
||||||
background-color transparent
|
|
||||||
&:hover
|
|
||||||
color #0B99F1
|
|
||||||
background-color transparent
|
|
||||||
&:active, &:active:hover
|
|
||||||
color #0B99F1
|
|
||||||
background-color transparent
|
|
||||||
|
|
||||||
.non-active-button
|
|
||||||
color $ui-inactive-text-color
|
|
||||||
&:hover
|
|
||||||
color alpha(#0B99F1, 60%)
|
|
||||||
|
|
||||||
.tag-title
|
|
||||||
p
|
|
||||||
color $ui-text-color
|
|
||||||
|
|
||||||
.non-active-button
|
|
||||||
&:hover
|
|
||||||
color alpha(#0B99F1, 60%)
|
|
||||||
|
|
||||||
.active-button
|
|
||||||
@extend .non-active-button
|
|
||||||
color #0B99F1
|
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.root, .root--folded
|
.root, .root--folded
|
||||||
border-color $ui-dark-borderColor
|
border-right 1px solid $ui-dark-borderColor
|
||||||
background-color $ui-dark-backgroundColor
|
background-color $ui-dark-backgroundColor
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
.top
|
.top
|
||||||
border-color $ui-dark-borderColor
|
border-color $ui-dark-borderColor
|
||||||
|
|
||||||
.top-menu-preference
|
body[data-theme="solarized-dark"]
|
||||||
navDarkButtonColor()
|
.root, .root--folded
|
||||||
background-color transparent
|
background-color $ui-solarized-dark-backgroundColor
|
||||||
&:active
|
border-right 1px solid $ui-solarized-dark-borderColor
|
||||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
|
||||||
background-color transparent
|
|
||||||
&:hover
|
|
||||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
|
||||||
background-color transparent
|
|
||||||
|
|
||||||
.non-active-button
|
|
||||||
color alpha($ui-dark-text-color, 60%)
|
|
||||||
&:hover
|
|
||||||
color alpha(#0B99F1, 60%)
|
|
||||||
|
|
||||||
.tag-title
|
|
||||||
p
|
|
||||||
color alpha($ui-dark-text-color, 60%)
|
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import CreateFolderModal from 'browser/main/modals/CreateFolderModal'
|
|||||||
import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
|
import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import StorageItemChild from 'browser/components/StorageItem'
|
import StorageItemChild from 'browser/components/StorageItem'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
import { SortableElement } from 'react-sortable-hoc'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { Menu, MenuItem, dialog } = remote
|
const { Menu, dialog } = remote
|
||||||
|
|
||||||
class StorageItem extends React.Component {
|
class StorageItem extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -24,27 +25,29 @@ class StorageItem extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleHeaderContextMenu (e) {
|
handleHeaderContextMenu (e) {
|
||||||
const menu = new Menu()
|
const menu = Menu.buildFromTemplate([
|
||||||
menu.append(new MenuItem({
|
{
|
||||||
label: 'Add Folder',
|
label: i18n.__('Add Folder'),
|
||||||
click: (e) => this.handleAddFolderButtonClick(e)
|
click: (e) => this.handleAddFolderButtonClick(e)
|
||||||
}))
|
},
|
||||||
menu.append(new MenuItem({
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
}))
|
},
|
||||||
menu.append(new MenuItem({
|
{
|
||||||
label: 'Unlink Storage',
|
label: i18n.__('Unlink Storage'),
|
||||||
click: (e) => this.handleUnlinkStorageClick(e)
|
click: (e) => this.handleUnlinkStorageClick(e)
|
||||||
}))
|
}
|
||||||
|
])
|
||||||
|
|
||||||
menu.popup()
|
menu.popup()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnlinkStorageClick (e) {
|
handleUnlinkStorageClick (e) {
|
||||||
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Unlink Storage',
|
message: i18n.__('Unlink Storage'),
|
||||||
detail: 'This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)',
|
detail: i18n.__('This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)'),
|
||||||
buttons: ['Confirm', 'Cancel']
|
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||||
})
|
})
|
||||||
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
@@ -89,18 +92,36 @@ class StorageItem extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleFolderButtonContextMenu (e, folder) {
|
handleFolderButtonContextMenu (e, folder) {
|
||||||
const menu = new Menu()
|
const menu = Menu.buildFromTemplate([
|
||||||
menu.append(new MenuItem({
|
{
|
||||||
label: 'Rename Folder',
|
label: i18n.__('Rename Folder'),
|
||||||
click: (e) => this.handleRenameFolderClick(e, folder)
|
click: (e) => this.handleRenameFolderClick(e, folder)
|
||||||
}))
|
},
|
||||||
menu.append(new MenuItem({
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
}))
|
},
|
||||||
menu.append(new MenuItem({
|
{
|
||||||
label: 'Delete Folder',
|
label: i18n.__('Export Folder'),
|
||||||
click: (e) => this.handleFolderDeleteClick(e, folder)
|
submenu: [
|
||||||
}))
|
{
|
||||||
|
label: i18n.__('Export as txt'),
|
||||||
|
click: (e) => this.handleExportFolderClick(e, folder, 'txt')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.__('Export as md'),
|
||||||
|
click: (e) => this.handleExportFolderClick(e, folder, 'md')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.__('Delete Folder'),
|
||||||
|
click: (e) => this.handleFolderDeleteClick(e, folder)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
menu.popup()
|
menu.popup()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,12 +133,37 @@ class StorageItem extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleExportFolderClick (e, folder, fileType) {
|
||||||
|
const options = {
|
||||||
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
|
buttonLabel: i18n.__('Select directory'),
|
||||||
|
title: i18n.__('Select a folder to export the files to'),
|
||||||
|
multiSelections: false
|
||||||
|
}
|
||||||
|
dialog.showOpenDialog(remote.getCurrentWindow(), options,
|
||||||
|
(paths) => {
|
||||||
|
if (paths && paths.length === 1) {
|
||||||
|
const { storage, dispatch } = this.props
|
||||||
|
dataApi
|
||||||
|
.exportFolder(storage.key, folder.key, fileType, paths[0])
|
||||||
|
.then((data) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'EXPORT_FOLDER',
|
||||||
|
storage: data.storage,
|
||||||
|
folderKey: data.folderKey,
|
||||||
|
fileType: data.fileType
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
handleFolderDeleteClick (e, folder) {
|
handleFolderDeleteClick (e, folder) {
|
||||||
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Delete Folder',
|
message: i18n.__('Delete Folder'),
|
||||||
detail: 'This will delete all notes in the folder and can not be undone.',
|
detail: i18n.__('This will delete all notes in the folder and can not be undone.'),
|
||||||
buttons: ['Confirm', 'Cancel']
|
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||||
})
|
})
|
||||||
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
@@ -147,33 +193,16 @@ class StorageItem extends React.Component {
|
|||||||
dropNote (storage, folder, dispatch, location, noteData) {
|
dropNote (storage, folder, dispatch, location, noteData) {
|
||||||
noteData = noteData.filter((note) => folder.key !== note.folder)
|
noteData = noteData.filter((note) => folder.key !== note.folder)
|
||||||
if (noteData.length === 0) return
|
if (noteData.length === 0) return
|
||||||
const newNoteData = noteData.map((note) => Object.assign({}, note, {storage: storage, folder: folder.key}))
|
|
||||||
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
newNoteData.map((note) => dataApi.createNote(storage.key, note))
|
noteData.map((note) => dataApi.moveNote(note.storage, note.key, storage.key, folder.key))
|
||||||
)
|
)
|
||||||
.then((createdNoteData) => {
|
.then((createdNoteData) => {
|
||||||
createdNoteData.forEach((note) => {
|
createdNoteData.forEach((newNote) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'UPDATE_NOTE',
|
type: 'MOVE_NOTE',
|
||||||
note: note
|
originNote: noteData.find((note) => note.content === newNote.content),
|
||||||
})
|
note: newNote
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(`error on create notes: ${err}`)
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
return Promise.all(
|
|
||||||
noteData.map((note) => dataApi.deleteNote(note.storage, note.key))
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.then((deletedNoteData) => {
|
|
||||||
deletedNoteData.forEach((note) => {
|
|
||||||
dispatch({
|
|
||||||
type: 'DELETE_NOTE',
|
|
||||||
storageKey: note.storageKey,
|
|
||||||
noteKey: note.noteKey
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -192,7 +221,8 @@ class StorageItem extends React.Component {
|
|||||||
render () {
|
render () {
|
||||||
const { storage, location, isFolded, data, dispatch } = this.props
|
const { storage, location, isFolded, data, dispatch } = this.props
|
||||||
const { folderNoteMap, trashedSet } = data
|
const { folderNoteMap, trashedSet } = data
|
||||||
const folderList = storage.folders.map((folder) => {
|
const SortableStorageItemChild = SortableElement(StorageItemChild)
|
||||||
|
const folderList = storage.folders.map((folder, index) => {
|
||||||
const isActive = !!(location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key)))
|
const isActive = !!(location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key)))
|
||||||
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
||||||
|
|
||||||
@@ -206,8 +236,9 @@ class StorageItem extends React.Component {
|
|||||||
noteCount = noteSet.size - trashedNoteCount
|
noteCount = noteSet.size - trashedNoteCount
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<StorageItemChild
|
<SortableStorageItemChild
|
||||||
key={folder.key}
|
key={folder.key}
|
||||||
|
index={index}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
|
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
|
||||||
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
|
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
|
||||||
@@ -229,9 +260,9 @@ class StorageItem extends React.Component {
|
|||||||
key={storage.key}
|
key={storage.key}
|
||||||
>
|
>
|
||||||
<div styleName={isActive
|
<div styleName={isActive
|
||||||
? 'header--active'
|
? 'header--active'
|
||||||
: 'header'
|
: 'header'
|
||||||
}
|
}
|
||||||
onContextMenu={(e) => this.handleHeaderContextMenu(e)}
|
onContextMenu={(e) => this.handleHeaderContextMenu(e)}
|
||||||
>
|
>
|
||||||
<button styleName='header-toggleButton'
|
<button styleName='header-toggleButton'
|
||||||
@@ -240,7 +271,7 @@ class StorageItem extends React.Component {
|
|||||||
<img src={this.state.isOpen
|
<img src={this.state.isOpen
|
||||||
? '../resources/icon/icon-down.svg'
|
? '../resources/icon/icon-down.svg'
|
||||||
: '../resources/icon/icon-right.svg'
|
: '../resources/icon/icon-right.svg'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -179,4 +179,8 @@ body[data-theme="dark"]
|
|||||||
background-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
background-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||||
&:active, &:active:hover
|
&:active, &:active:hover
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
background-color $ui-dark-button--active-backgroundColor
|
background-color $ui-dark-button--active-backgroundColor
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
59
browser/main/SideNav/SwitchButton.styl
Normal file
59
browser/main/SideNav/SwitchButton.styl
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
.non-active-button
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
font-size 16px
|
||||||
|
border 0
|
||||||
|
background-color transparent
|
||||||
|
transition 0.2s
|
||||||
|
display flex
|
||||||
|
text-align center
|
||||||
|
margin-right 4px
|
||||||
|
position relative
|
||||||
|
&:hover
|
||||||
|
color alpha(#239F86, 60%)
|
||||||
|
.tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.active-button
|
||||||
|
@extend .non-active-button
|
||||||
|
color $ui-button-default--active-backgroundColor
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 22px
|
||||||
|
left -2px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
|
body[data-theme="white"]
|
||||||
|
.non-active-button
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
&:hover
|
||||||
|
color alpha(#0B99F1, 60%)
|
||||||
|
|
||||||
|
.tag-title
|
||||||
|
p
|
||||||
|
color $ui-text-color
|
||||||
|
|
||||||
|
.non-active-button
|
||||||
|
&:hover
|
||||||
|
color alpha(#0B99F1, 60%)
|
||||||
|
|
||||||
|
.active-button
|
||||||
|
@extend .non-active-button
|
||||||
|
color #0B99F1
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.non-active-button
|
||||||
|
color alpha($ui-dark-text-color, 60%)
|
||||||
|
&:hover
|
||||||
|
color alpha(#0B99F1, 60%)
|
||||||
|
|
||||||
|
.tag-title
|
||||||
|
p
|
||||||
|
color alpha($ui-dark-text-color, 60%)
|
||||||
25
browser/main/SideNav/TagButton.js
Normal file
25
browser/main/SideNav/TagButton.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './SwitchButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
|
const TagButton = ({
|
||||||
|
onClick, isTagActive
|
||||||
|
}) => (
|
||||||
|
<button styleName={isTagActive ? 'active-button' : 'non-active-button'} onClick={onClick}>
|
||||||
|
<img src={isTagActive
|
||||||
|
? '../resources/icon/icon-tag-active.svg'
|
||||||
|
: '../resources/icon/icon-tag.svg'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span styleName='tooltip'>{i18n.__('Tags')}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
TagButton.propTypes = {
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
isTagActive: PropTypes.bool.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(TagButton, styles)
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
const { remote } = require('electron')
|
||||||
|
const { Menu } = remote
|
||||||
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import styles from './SideNav.styl'
|
import styles from './SideNav.styl'
|
||||||
import { openModal } from 'browser/main/lib/modal'
|
import { openModal } from 'browser/main/lib/modal'
|
||||||
import PreferencesModal from '../modals/PreferencesModal'
|
import PreferencesModal from '../modals/PreferencesModal'
|
||||||
@@ -11,6 +14,11 @@ import SideNavFilter from 'browser/components/SideNavFilter'
|
|||||||
import StorageList from 'browser/components/StorageList'
|
import StorageList from 'browser/components/StorageList'
|
||||||
import NavToggleButton from 'browser/components/NavToggleButton'
|
import NavToggleButton from 'browser/components/NavToggleButton'
|
||||||
import EventEmitter from 'browser/main/lib/eventEmitter'
|
import EventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
|
import PreferenceButton from './PreferenceButton'
|
||||||
|
import ListButton from './ListButton'
|
||||||
|
import TagButton from './TagButton'
|
||||||
|
import {SortableContainer} from 'react-sortable-hoc'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
class SideNav extends React.Component {
|
class SideNav extends React.Component {
|
||||||
// TODO: should not use electron stuff v0.7
|
// TODO: should not use electron stuff v0.7
|
||||||
@@ -62,6 +70,17 @@ class SideNav extends React.Component {
|
|||||||
router.push('/alltags')
|
router.push('/alltags')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSortEnd (storage) {
|
||||||
|
return ({oldIndex, newIndex}) => {
|
||||||
|
const { dispatch } = this.props
|
||||||
|
dataApi
|
||||||
|
.reorderFolder(storage.key, oldIndex, newIndex)
|
||||||
|
.then((data) => {
|
||||||
|
dispatch({ type: 'REORDER_FOLDER', storage: data.storage })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SideNavComponent (isFolded, storageList) {
|
SideNavComponent (isFolded, storageList) {
|
||||||
const { location, data } = this.props
|
const { location, data } = this.props
|
||||||
|
|
||||||
@@ -83,9 +102,10 @@ class SideNav extends React.Component {
|
|||||||
isTrashedActive={isTrashedActive}
|
isTrashedActive={isTrashedActive}
|
||||||
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
|
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
|
||||||
handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)}
|
handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)}
|
||||||
counterTotalNote={data.noteMap._map.size}
|
counterTotalNote={data.noteMap._map.size - data.trashedSet._set.size}
|
||||||
counterStarredNote={data.starredSet._set.size}
|
counterStarredNote={data.starredSet._set.size}
|
||||||
counterDelNote={data.trashedSet._set.size}
|
counterDelNote={data.trashedSet._set.size}
|
||||||
|
handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(this)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StorageList storageList={storageList} />
|
<StorageList storageList={storageList} />
|
||||||
@@ -96,7 +116,7 @@ class SideNav extends React.Component {
|
|||||||
component = (
|
component = (
|
||||||
<div styleName='tabBody'>
|
<div styleName='tabBody'>
|
||||||
<div styleName='tag-title'>
|
<div styleName='tag-title'>
|
||||||
<p>Tags</p>
|
<p>{i18n.__('Tags')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='tagList'>
|
<div styleName='tagList'>
|
||||||
{this.tagListComponent(data)}
|
{this.tagListComponent(data)}
|
||||||
@@ -110,18 +130,21 @@ class SideNav extends React.Component {
|
|||||||
|
|
||||||
tagListComponent () {
|
tagListComponent () {
|
||||||
const { data, location } = this.props
|
const { data, location } = this.props
|
||||||
const tagList = data.tagNoteMap.map((tag, key) => {
|
const tagList = _.sortBy(data.tagNoteMap.map((tag, name) => {
|
||||||
return key
|
return { name, size: tag.size }
|
||||||
})
|
}), ['name'])
|
||||||
return (
|
return (
|
||||||
tagList.map(tag => (
|
tagList.map(tag => {
|
||||||
<TagListItem
|
return (
|
||||||
name={tag}
|
<TagListItem
|
||||||
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
name={tag.name}
|
||||||
isActive={this.getTagActive(location.pathname, tag)}
|
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
||||||
key={tag}
|
isActive={this.getTagActive(location.pathname, tag)}
|
||||||
/>
|
key={tag.name}
|
||||||
))
|
count={tag.size}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,19 +159,48 @@ class SideNav extends React.Component {
|
|||||||
router.push(`/tags/${name}`)
|
router.push(`/tags/${name}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emptyTrash (entries) {
|
||||||
|
const { dispatch } = this.props
|
||||||
|
const deletionPromises = entries.map((note) => {
|
||||||
|
return dataApi.deleteNote(note.storage, note.key)
|
||||||
|
})
|
||||||
|
Promise.all(deletionPromises)
|
||||||
|
.then((arrayOfStorageAndNoteKeys) => {
|
||||||
|
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
|
||||||
|
dispatch({ type: 'DELETE_NOTE', storageKey, noteKey })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Cannot Delete note: ' + err)
|
||||||
|
})
|
||||||
|
console.log('Trash emptied')
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFilterButtonContextMenu (event) {
|
||||||
|
const { data } = this.props
|
||||||
|
const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
|
||||||
|
const menu = Menu.buildFromTemplate([
|
||||||
|
{ label: i18n.__('Empty Trash'), click: () => this.emptyTrash(trashedNotes) }
|
||||||
|
])
|
||||||
|
menu.popup()
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { data, location, config, dispatch } = this.props
|
const { data, location, config, dispatch } = this.props
|
||||||
|
|
||||||
const isFolded = config.isSideNavFolded
|
const isFolded = config.isSideNavFolded
|
||||||
|
|
||||||
const storageList = data.storageMap.map((storage, key) => {
|
const storageList = data.storageMap.map((storage, key) => {
|
||||||
return <StorageItem
|
const SortableStorageItem = SortableContainer(StorageItem)
|
||||||
|
return <SortableStorageItem
|
||||||
key={storage.key}
|
key={storage.key}
|
||||||
storage={storage}
|
storage={storage}
|
||||||
data={data}
|
data={data}
|
||||||
location={location}
|
location={location}
|
||||||
isFolded={isFolded}
|
isFolded={isFolded}
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
|
onSortEnd={this.onSortEnd.bind(this)(storage)}
|
||||||
|
useDragHandle
|
||||||
/>
|
/>
|
||||||
})
|
})
|
||||||
const style = {}
|
const style = {}
|
||||||
@@ -162,27 +214,11 @@ class SideNav extends React.Component {
|
|||||||
>
|
>
|
||||||
<div styleName='top'>
|
<div styleName='top'>
|
||||||
<div styleName='switch-buttons'>
|
<div styleName='switch-buttons'>
|
||||||
<button styleName={isTagActive ? 'non-active-button' : 'active-button'} onClick={this.handleSwitchFoldersButtonClick.bind(this)}>
|
<ListButton onClick={this.handleSwitchFoldersButtonClick.bind(this)} isTagActive={isTagActive} />
|
||||||
<img src={isTagActive
|
<TagButton onClick={this.handleSwitchTagsButtonClick.bind(this)} isTagActive={isTagActive} />
|
||||||
? '../resources/icon/icon-list.svg'
|
|
||||||
: '../resources/icon/icon-list-active.svg'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button styleName={isTagActive ? 'active-button' : 'non-active-button'} onClick={this.handleSwitchTagsButtonClick.bind(this)}>
|
|
||||||
<img src={isTagActive
|
|
||||||
? '../resources/icon/icon-tag-active.svg'
|
|
||||||
: '../resources/icon/icon-tag.svg'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button styleName='top-menu-preference'
|
<PreferenceButton onClick={this.handleMenuButtonClick} />
|
||||||
onClick={(e) => this.handleMenuButtonClick(e)}
|
|
||||||
>
|
|
||||||
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{this.SideNavComponent(isFolded, storageList)}
|
{this.SideNavComponent(isFolded, storageList)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React from 'react'
|
|||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './StatusBar.styl'
|
import styles from './StatusBar.styl'
|
||||||
import ZoomManager from 'browser/main/lib/ZoomManager'
|
import ZoomManager from 'browser/main/lib/ZoomManager'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const { remote, ipcRenderer } = electron
|
const { remote, ipcRenderer } = electron
|
||||||
@@ -14,9 +15,9 @@ class StatusBar extends React.Component {
|
|||||||
updateApp () {
|
updateApp () {
|
||||||
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Update Boostnote',
|
message: i18n.__('Update Boostnote'),
|
||||||
detail: 'New Boostnote is ready to be installed.',
|
detail: i18n.__('New Boostnote is ready to be installed.'),
|
||||||
buttons: ['Restart & Install', 'Not Now']
|
buttons: [i18n.__('Restart & Install'), i18n.__('Not Now')]
|
||||||
})
|
})
|
||||||
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
@@ -62,8 +63,8 @@ class StatusBar extends React.Component {
|
|||||||
|
|
||||||
{status.updateReady
|
{status.updateReady
|
||||||
? <button onClick={this.updateApp} styleName='update'>
|
? <button onClick={this.updateApp} styleName='update'>
|
||||||
<i styleName='update-icon' className='fa fa-cloud-download' /> Ready to Update!
|
<i styleName='update-icon' className='fa fa-cloud-download' /> {i18n.__('Ready to Update!')}
|
||||||
</button>
|
</button>
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,6 +40,32 @@ $control-height = 34px
|
|||||||
padding-bottom 2px
|
padding-bottom 2px
|
||||||
background-color $ui-noteList-backgroundColor
|
background-color $ui-noteList-backgroundColor
|
||||||
|
|
||||||
|
.control-search-input-clear
|
||||||
|
height 16px
|
||||||
|
width 16px
|
||||||
|
position absolute
|
||||||
|
right 40px
|
||||||
|
top 10px
|
||||||
|
z-index 300
|
||||||
|
border none
|
||||||
|
background-color transparent
|
||||||
|
color #999
|
||||||
|
&:hover .control-search-input-clear-tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.control-search-input-clear-tooltip
|
||||||
|
tooltip()
|
||||||
|
position fixed
|
||||||
|
pointer-events none
|
||||||
|
top 50px
|
||||||
|
left 433px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
.control-search-optionList
|
.control-search-optionList
|
||||||
position fixed
|
position fixed
|
||||||
z-index 200
|
z-index 200
|
||||||
@@ -185,3 +211,26 @@ body[data-theme="dark"]
|
|||||||
|
|
||||||
.control-newPostButton-tooltip
|
.control-newPostButton-tooltip
|
||||||
darkTooltip()
|
darkTooltip()
|
||||||
|
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.root, .root--expanded
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
|
||||||
|
.control
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
.control-search
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
|
||||||
|
.control-search-icon
|
||||||
|
absolute top bottom left
|
||||||
|
line-height 32px
|
||||||
|
width 35px
|
||||||
|
color $ui-solarized-dark-inactive-text-color
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
|
||||||
|
.control-search-input
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
input
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import styles from './TopBar.styl'
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import ee from 'browser/main/lib/eventEmitter'
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
import NewNoteButton from 'browser/main/NewNoteButton'
|
import NewNoteButton from 'browser/main/NewNoteButton'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
class TopBar extends React.Component {
|
class TopBar extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -22,14 +23,29 @@ class TopBar extends React.Component {
|
|||||||
this.focusSearchHandler = () => {
|
this.focusSearchHandler = () => {
|
||||||
this.handleOnSearchFocus()
|
this.handleOnSearchFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.codeInitHandler = this.handleCodeInit.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
ee.on('top:focus-search', this.focusSearchHandler)
|
ee.on('top:focus-search', this.focusSearchHandler)
|
||||||
|
ee.on('code:init', this.codeInitHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
ee.off('top:focus-search', this.focusSearchHandler)
|
ee.off('top:focus-search', this.focusSearchHandler)
|
||||||
|
ee.off('code:init', this.codeInitHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSearchClearButton (e) {
|
||||||
|
const { router } = this.context
|
||||||
|
this.setState({
|
||||||
|
search: '',
|
||||||
|
isSearching: false
|
||||||
|
})
|
||||||
|
this.refs.search.childNodes[0].blur
|
||||||
|
router.push('/searched')
|
||||||
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown (e) {
|
handleKeyDown (e) {
|
||||||
@@ -39,6 +55,23 @@ class TopBar extends React.Component {
|
|||||||
isIME: false
|
isIME: false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Clear search on ESC
|
||||||
|
if (e.keyCode === 27) {
|
||||||
|
return this.handleSearchClearButton(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next note on DOWN key
|
||||||
|
if (e.keyCode === 40) {
|
||||||
|
ee.emit('list:next')
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prev note on UP key
|
||||||
|
if (e.keyCode === 38) {
|
||||||
|
ee.emit('list:prior')
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
// When the key is an alphabet, del, enter or ctr
|
// When the key is an alphabet, del, enter or ctr
|
||||||
if (e.keyCode <= 90 || e.keyCode >= 186 && e.keyCode <= 222) {
|
if (e.keyCode <= 90 || e.keyCode >= 186 && e.keyCode <= 222) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -73,14 +106,16 @@ class TopBar extends React.Component {
|
|||||||
|
|
||||||
handleSearchChange (e) {
|
handleSearchChange (e) {
|
||||||
const { router } = this.context
|
const { router } = this.context
|
||||||
|
const keyword = this.refs.searchInput.value
|
||||||
if (this.state.isAlphabet || this.state.isConfirmTranslation) {
|
if (this.state.isAlphabet || this.state.isConfirmTranslation) {
|
||||||
router.push('/searched')
|
router.push('/searched')
|
||||||
} else {
|
} else {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
search: this.refs.searchInput.value
|
search: keyword
|
||||||
})
|
})
|
||||||
|
ee.emit('top:search', keyword)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchFocus (e) {
|
handleSearchFocus (e) {
|
||||||
@@ -108,13 +143,19 @@ class TopBar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleOnSearchFocus () {
|
handleOnSearchFocus () {
|
||||||
|
const el = this.refs.search.childNodes[0]
|
||||||
if (this.state.isSearching) {
|
if (this.state.isSearching) {
|
||||||
this.refs.search.childNodes[0].blur()
|
el.blur()
|
||||||
} else {
|
} else {
|
||||||
this.refs.search.childNodes[0].focus()
|
el.focus()
|
||||||
|
el.setSelectionRange(0, el.value.length)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleCodeInit () {
|
||||||
|
ee.emit('top:search', this.refs.searchInput.value)
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { config, style, location } = this.props
|
const { config, style, location } = this.props
|
||||||
return (
|
return (
|
||||||
@@ -136,19 +177,19 @@ class TopBar extends React.Component {
|
|||||||
onChange={(e) => this.handleSearchChange(e)}
|
onChange={(e) => this.handleSearchChange(e)}
|
||||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||||
onKeyUp={(e) => this.handleKeyUp(e)}
|
onKeyUp={(e) => this.handleKeyUp(e)}
|
||||||
placeholder='Search'
|
placeholder={i18n.__('Search')}
|
||||||
type='text'
|
type='text'
|
||||||
className='searchInput'
|
className='searchInput'
|
||||||
/>
|
/>
|
||||||
|
{this.state.search !== '' &&
|
||||||
|
<button styleName='control-search-input-clear'
|
||||||
|
onClick={(e) => this.handleSearchClearButton(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-fw fa-times' />
|
||||||
|
<span styleName='control-search-input-clear-tooltip'>{i18n.__('Clear Search')}</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
{this.state.search > 0 &&
|
|
||||||
<button styleName='left-search-clearButton'
|
|
||||||
onClick={(e) => this.handleSearchClearButton(e)}
|
|
||||||
>
|
|
||||||
<i className='fa fa-times' />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{location.pathname === '/trashed' ? ''
|
{location.pathname === '/trashed' ? ''
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
global-reset()
|
global-reset()
|
||||||
|
@import '../styles/vars.styl'
|
||||||
|
|
||||||
DEFAULT_FONTS = 'OpenSans', helvetica, arial, sans-serif
|
DEFAULT_FONTS = 'OpenSans', helvetica, arial, sans-serif
|
||||||
|
|
||||||
@@ -84,15 +85,19 @@ modalBackColor = white
|
|||||||
absolute top left bottom right
|
absolute top left bottom right
|
||||||
background-color modalBackColor
|
background-color modalBackColor
|
||||||
z-index modalZIndex + 1
|
z-index modalZIndex + 1
|
||||||
|
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.ModalBase
|
.ModalBase
|
||||||
.modalBack
|
.modalBack
|
||||||
background-color $ui-dark-backgroundColor
|
background-color $ui-dark-backgroundColor
|
||||||
|
.sortableItemHelper
|
||||||
|
color: $ui-dark-text-color
|
||||||
|
|
||||||
.CodeMirror
|
.CodeMirror
|
||||||
font-family inherit !important
|
font-family inherit !important
|
||||||
line-height 1.4em
|
line-height 1.4em
|
||||||
height 96%
|
height 100%
|
||||||
.CodeMirror > div > textarea
|
.CodeMirror > div > textarea
|
||||||
margin-bottom -1em
|
margin-bottom -1em
|
||||||
.CodeMirror-focused .CodeMirror-selected
|
.CodeMirror-focused .CodeMirror-selected
|
||||||
@@ -107,6 +112,11 @@ body[data-theme="dark"]
|
|||||||
.sortableItemHelper
|
.sortableItemHelper
|
||||||
z-index modalZIndex + 5
|
z-index modalZIndex + 5
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="solarized-dark"]
|
||||||
|
.ModalBase
|
||||||
|
.modalBack
|
||||||
|
background-color $ui-solarized-dark-backgroundColor
|
||||||
.sortableItemHelper
|
.sortableItemHelper
|
||||||
color: $ui-dark-text-color
|
color: $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-rou
|
|||||||
import { syncHistoryWithStore } from 'react-router-redux'
|
import { syncHistoryWithStore } from 'react-router-redux'
|
||||||
require('./lib/ipcClient')
|
require('./lib/ipcClient')
|
||||||
require('../lib/customMeta')
|
require('../lib/customMeta')
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
@@ -46,9 +47,9 @@ function notify (...args) {
|
|||||||
function updateApp () {
|
function updateApp () {
|
||||||
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Update Boostnote',
|
message: i18n.__('Update Boostnote'),
|
||||||
detail: 'New Boostnote is ready to be installed.',
|
detail: i18n.__('New Boostnote is ready to be installed.'),
|
||||||
buttons: ['Restart & Install', 'Not Now']
|
buttons: [i18n.__('Restart & Install'), i18n.__('Not Now')]
|
||||||
})
|
})
|
||||||
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const os = require('os')
|
|||||||
let mobileAnalyticsClient
|
let mobileAnalyticsClient
|
||||||
|
|
||||||
AWS.config.region = 'us-east-1'
|
AWS.config.region = 'us-east-1'
|
||||||
if (process.env.NODE_ENV === 'production' && ConfigManager.default.get().amaEnabled) {
|
if (!getSendEventCond()) {
|
||||||
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
|
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
|
||||||
IdentityPoolId: 'us-east-1:xxxxxxxxxxxxxxxxxxxxxxxxx'
|
IdentityPoolId: 'us-east-1:xxxxxxxxxxxxxxxxxxxxxxxxx'
|
||||||
})
|
})
|
||||||
@@ -34,8 +34,15 @@ function convertPlatformName (platformName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSendEventCond () {
|
||||||
|
const isDev = process.env.NODE_ENV !== 'production'
|
||||||
|
const isDisable = !ConfigManager.default.get().amaEnabled
|
||||||
|
const isOffline = !window.navigator.onLine
|
||||||
|
return isDev || isDisable || isOffline
|
||||||
|
}
|
||||||
|
|
||||||
function initAwsMobileAnalytics () {
|
function initAwsMobileAnalytics () {
|
||||||
if (process.env.NODE_ENV !== 'production' || !ConfigManager.default.get().amaEnabled) return
|
if (getSendEventCond()) return
|
||||||
AWS.config.credentials.get((err) => {
|
AWS.config.credentials.get((err) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
console.log('Cognito Identity ID: ' + AWS.config.credentials.identityId)
|
console.log('Cognito Identity ID: ' + AWS.config.credentials.identityId)
|
||||||
@@ -46,7 +53,7 @@ function initAwsMobileAnalytics () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recordDynamicCustomEvent (type, options = {}) {
|
function recordDynamicCustomEvent (type, options = {}) {
|
||||||
if (process.env.NODE_ENV !== 'production' || !ConfigManager.default.get().amaEnabled) return
|
if (getSendEventCond()) return
|
||||||
try {
|
try {
|
||||||
mobileAnalyticsClient.recordEvent(type, options)
|
mobileAnalyticsClient.recordEvent(type, options)
|
||||||
} catch (analyticsError) {
|
} catch (analyticsError) {
|
||||||
@@ -57,7 +64,7 @@ function recordDynamicCustomEvent (type, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recordStaticCustomEvent () {
|
function recordStaticCustomEvent () {
|
||||||
if (process.env.NODE_ENV !== 'production' || !ConfigManager.default.get().amaEnabled) return
|
if (getSendEventCond()) return
|
||||||
try {
|
try {
|
||||||
mobileAnalyticsClient.recordEvent('UI_COLOR_THEME', {
|
mobileAnalyticsClient.recordEvent('UI_COLOR_THEME', {
|
||||||
uiColorTheme: ConfigManager.default.get().ui.theme
|
uiColorTheme: ConfigManager.default.get().ui.theme
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import RcParser from 'browser/lib/RcParser'
|
import RcParser from 'browser/lib/RcParser'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const OSX = global.process.platform === 'darwin'
|
const OSX = global.process.platform === 'darwin'
|
||||||
const win = global.process.platform === 'win32'
|
const win = global.process.platform === 'win32'
|
||||||
@@ -18,10 +19,10 @@ export const DEFAULT_CONFIG = {
|
|||||||
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
|
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
|
||||||
amaEnabled: true,
|
amaEnabled: true,
|
||||||
hotkey: {
|
hotkey: {
|
||||||
toggleFinder: OSX ? 'Cmd + Alt + S' : 'Super + Alt + S',
|
|
||||||
toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E'
|
toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E'
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
|
language: 'en',
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
showCopyNotification: true,
|
showCopyNotification: true,
|
||||||
disableDirectWrite: false,
|
disableDirectWrite: false,
|
||||||
@@ -34,13 +35,34 @@ export const DEFAULT_CONFIG = {
|
|||||||
fontFamily: win ? 'Segoe UI' : 'Monaco, Consolas',
|
fontFamily: win ? 'Segoe UI' : 'Monaco, Consolas',
|
||||||
indentType: 'space',
|
indentType: 'space',
|
||||||
indentSize: '2',
|
indentSize: '2',
|
||||||
switchPreview: 'BLUR' // Available value: RIGHTCLICK, BLUR
|
enableRulers: false,
|
||||||
|
rulers: [80, 120],
|
||||||
|
displayLineNumbers: true,
|
||||||
|
switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR
|
||||||
|
scrollPastEnd: false,
|
||||||
|
type: 'SPLIT',
|
||||||
|
fetchUrlTitle: true
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
fontSize: '14',
|
fontSize: '14',
|
||||||
fontFamily: win ? 'Segoe UI' : 'Lato',
|
fontFamily: win ? 'Segoe UI' : 'Lato',
|
||||||
codeBlockTheme: 'dracula',
|
codeBlockTheme: 'dracula',
|
||||||
lineNumber: true
|
lineNumber: true,
|
||||||
|
latexInlineOpen: '$',
|
||||||
|
latexInlineClose: '$',
|
||||||
|
latexBlockOpen: '$$',
|
||||||
|
latexBlockClose: '$$',
|
||||||
|
scrollPastEnd: false,
|
||||||
|
smartQuotes: true,
|
||||||
|
sanitize: 'STRICT' // 'STRICT', 'ALLOW_STYLES', 'NONE'
|
||||||
|
},
|
||||||
|
blog: {
|
||||||
|
type: 'wordpress', // Available value: wordpress, add more types in the future plz
|
||||||
|
address: 'http://wordpress.com/wp-json',
|
||||||
|
authMethod: 'JWT', // Available value: JWT, USER
|
||||||
|
token: '',
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,10 +132,44 @@ function set (updates) {
|
|||||||
document.body.setAttribute('data-theme', 'dark')
|
document.body.setAttribute('data-theme', 'dark')
|
||||||
} else if (newConfig.ui.theme === 'white') {
|
} else if (newConfig.ui.theme === 'white') {
|
||||||
document.body.setAttribute('data-theme', 'white')
|
document.body.setAttribute('data-theme', 'white')
|
||||||
|
} else if (newConfig.ui.theme === 'solarized-dark') {
|
||||||
|
document.body.setAttribute('data-theme', 'solarized-dark')
|
||||||
} else {
|
} else {
|
||||||
document.body.setAttribute('data-theme', 'default')
|
document.body.setAttribute('data-theme', 'default')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newConfig.ui.language === 'sq') {
|
||||||
|
i18n.setLocale('sq')
|
||||||
|
} else if (newConfig.ui.language === 'zh-CN') {
|
||||||
|
i18n.setLocale('zh-CN')
|
||||||
|
} else if (newConfig.ui.language === 'zh-TW') {
|
||||||
|
i18n.setLocale('zh-TW')
|
||||||
|
} else if (newConfig.ui.language === 'da') {
|
||||||
|
i18n.setLocale('da')
|
||||||
|
} else if (newConfig.ui.language === 'fr') {
|
||||||
|
i18n.setLocale('fr')
|
||||||
|
} else if (newConfig.ui.language === 'de') {
|
||||||
|
i18n.setLocale('de')
|
||||||
|
} else if (newConfig.ui.language === 'hu') {
|
||||||
|
i18n.setLocale('hu')
|
||||||
|
} else if (newConfig.ui.language === 'ja') {
|
||||||
|
i18n.setLocale('ja')
|
||||||
|
} else if (newConfig.ui.language === 'ko') {
|
||||||
|
i18n.setLocale('ko')
|
||||||
|
} else if (newConfig.ui.language === 'no') {
|
||||||
|
i18n.setLocale('no')
|
||||||
|
} else if (newConfig.ui.language === 'pl') {
|
||||||
|
i18n.setLocale('pl')
|
||||||
|
} else if (newConfig.ui.language === 'pt') {
|
||||||
|
i18n.setLocale('pt')
|
||||||
|
} else if (newConfig.ui.language === 'ru') {
|
||||||
|
i18n.setLocale('ru')
|
||||||
|
} else if (newConfig.ui.language === 'es') {
|
||||||
|
i18n.setLocale('es')
|
||||||
|
} else {
|
||||||
|
i18n.setLocale('en')
|
||||||
|
}
|
||||||
|
|
||||||
let editorTheme = document.getElementById('editorTheme')
|
let editorTheme = document.getElementById('editorTheme')
|
||||||
if (editorTheme == null) {
|
if (editorTheme == null) {
|
||||||
editorTheme = document.createElement('link')
|
editorTheme = document.createElement('link')
|
||||||
@@ -141,6 +197,7 @@ function set (updates) {
|
|||||||
function assignConfigValues (originalConfig, rcConfig) {
|
function assignConfigValues (originalConfig, rcConfig) {
|
||||||
const config = Object.assign({}, DEFAULT_CONFIG, originalConfig, rcConfig)
|
const config = Object.assign({}, DEFAULT_CONFIG, originalConfig, rcConfig)
|
||||||
config.hotkey = Object.assign({}, DEFAULT_CONFIG.hotkey, originalConfig.hotkey, rcConfig.hotkey)
|
config.hotkey = Object.assign({}, DEFAULT_CONFIG.hotkey, originalConfig.hotkey, rcConfig.hotkey)
|
||||||
|
config.blog = Object.assign({}, DEFAULT_CONFIG.blog, originalConfig.blog, rcConfig.blog)
|
||||||
config.ui = Object.assign({}, DEFAULT_CONFIG.ui, originalConfig.ui, rcConfig.ui)
|
config.ui = Object.assign({}, DEFAULT_CONFIG.ui, originalConfig.ui, rcConfig.ui)
|
||||||
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor)
|
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor)
|
||||||
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, originalConfig.preview, rcConfig.preview)
|
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, originalConfig.preview, rcConfig.preview)
|
||||||
|
|||||||
31
browser/main/lib/dataApi/copyFile.js
Executable file
31
browser/main/lib/dataApi/copyFile.js
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Copy a file from source to destination
|
||||||
|
* @param {String} srcPath
|
||||||
|
* @param {String} dstPath
|
||||||
|
* @return {Promise} an image path
|
||||||
|
*/
|
||||||
|
function copyFile (srcPath, dstPath) {
|
||||||
|
if (!path.extname(dstPath)) {
|
||||||
|
dstPath = path.join(dstPath, path.basename(srcPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const dstFolder = path.dirname(dstPath)
|
||||||
|
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
|
||||||
|
|
||||||
|
const input = fs.createReadStream(srcPath)
|
||||||
|
const output = fs.createWriteStream(dstPath)
|
||||||
|
|
||||||
|
output.on('error', reject)
|
||||||
|
input.on('error', reject)
|
||||||
|
input.on('end', () => {
|
||||||
|
resolve(dstPath)
|
||||||
|
})
|
||||||
|
input.pipe(output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = copyFile
|
||||||
@@ -3,19 +3,20 @@ const path = require('path')
|
|||||||
const { findStorage } = require('browser/lib/findStorage')
|
const { findStorage } = require('browser/lib/findStorage')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description To copy an image and return the path.
|
* @description Copy an image and return the path.
|
||||||
* @param {String} filePath
|
* @param {String} filePath
|
||||||
* @param {String} storageKey
|
* @param {String} storageKey
|
||||||
* @return {String} an image path
|
* @param {Boolean} rename create new filename or leave the old one
|
||||||
|
* @return {Promise<any>} an image path
|
||||||
*/
|
*/
|
||||||
function copyImage (filePath, storageKey) {
|
function copyImage (filePath, storageKey, rename = true) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const targetStorage = findStorage(storageKey)
|
const targetStorage = findStorage(storageKey)
|
||||||
|
|
||||||
const inputImage = fs.createReadStream(filePath)
|
const inputImage = fs.createReadStream(filePath)
|
||||||
const imageExt = path.extname(filePath)
|
const imageExt = path.extname(filePath)
|
||||||
const imageName = Math.random().toString(36).slice(-16)
|
const imageName = rename ? Math.random().toString(36).slice(-16) : path.basename(filePath, imageExt)
|
||||||
const basename = `${imageName}${imageExt}`
|
const basename = `${imageName}${imageExt}`
|
||||||
const imageDir = path.join(targetStorage.path, 'images')
|
const imageDir = path.join(targetStorage.path, 'images')
|
||||||
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
|
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
|
||||||
|
|||||||
@@ -52,12 +52,12 @@ function createNote (storageKey, input) {
|
|||||||
return storage
|
return storage
|
||||||
})
|
})
|
||||||
.then(function saveNote (storage) {
|
.then(function saveNote (storage) {
|
||||||
let key = keygen()
|
let key = keygen(true)
|
||||||
let isUnique = false
|
let isUnique = false
|
||||||
while (!isUnique) {
|
while (!isUnique) {
|
||||||
try {
|
try {
|
||||||
sander.statSync(path.join(storage.path, 'notes', key + '.cson'))
|
sander.statSync(path.join(storage.path, 'notes', key + '.cson'))
|
||||||
key = keygen()
|
key = keygen(true)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 'ENOENT') {
|
if (err.code === 'ENOENT') {
|
||||||
isUnique = true
|
isUnique = true
|
||||||
@@ -66,12 +66,16 @@ function createNote (storageKey, input) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const noteData = Object.assign({}, input, {
|
const noteData = Object.assign({},
|
||||||
key,
|
{
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date()
|
||||||
storage: storageKey
|
},
|
||||||
})
|
input, // input may contain more accurate dates
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
storage: storageKey
|
||||||
|
})
|
||||||
|
|
||||||
CSON.writeFileSync(path.join(storage.path, 'notes', key + '.cson'), _.omit(noteData, ['key', 'storage']))
|
CSON.writeFileSync(path.join(storage.path, 'notes', key + '.cson'), _.omit(noteData, ['key', 'storage']))
|
||||||
|
|
||||||
|
|||||||
62
browser/main/lib/dataApi/exportFolder.js
Normal file
62
browser/main/lib/dataApi/exportFolder.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { findStorage } from 'browser/lib/findStorage'
|
||||||
|
import resolveStorageData from './resolveStorageData'
|
||||||
|
import resolveStorageNotes from './resolveStorageNotes'
|
||||||
|
import filenamify from 'filenamify'
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} storageKey
|
||||||
|
* @param {String} folderKey
|
||||||
|
* @param {String} fileType
|
||||||
|
* @param {String} exportDir
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
* ```
|
||||||
|
* {
|
||||||
|
* storage: Object,
|
||||||
|
* folderKey: String,
|
||||||
|
* fileType: String,
|
||||||
|
* exportDir: String
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
function exportFolder (storageKey, folderKey, fileType, exportDir) {
|
||||||
|
let targetStorage
|
||||||
|
try {
|
||||||
|
targetStorage = findStorage(storageKey)
|
||||||
|
} catch (e) {
|
||||||
|
return Promise.reject(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveStorageData(targetStorage)
|
||||||
|
.then(function assignNotes (storage) {
|
||||||
|
return resolveStorageNotes(storage)
|
||||||
|
.then((notes) => {
|
||||||
|
return {
|
||||||
|
storage,
|
||||||
|
notes
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(function exportNotes (data) {
|
||||||
|
const { storage, notes } = data
|
||||||
|
|
||||||
|
notes
|
||||||
|
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
|
||||||
|
.forEach(snippet => {
|
||||||
|
const notePath = path.join(exportDir, `${filenamify(snippet.title, {replacement: '_'})}.${fileType}`)
|
||||||
|
fs.writeFileSync(notePath, snippet.content)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
storage,
|
||||||
|
folderKey,
|
||||||
|
fileType,
|
||||||
|
exportDir
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exportFolder
|
||||||
112
browser/main/lib/dataApi/exportNote.js
Executable file
112
browser/main/lib/dataApi/exportNote.js
Executable file
@@ -0,0 +1,112 @@
|
|||||||
|
import copyFile from 'browser/main/lib/dataApi/copyFile'
|
||||||
|
import {findStorage} from 'browser/lib/findStorage'
|
||||||
|
import filenamify from 'filenamify'
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const LOCAL_STORED_REGEX = /!\[(.*?)]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
|
||||||
|
const IMAGES_FOLDER_NAME = 'images'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export note together with images
|
||||||
|
*
|
||||||
|
* If images is stored in the storage, creates 'images' subfolder in target directory
|
||||||
|
* and copies images to it. Changes links to images in the content of the note
|
||||||
|
*
|
||||||
|
* @param {String} storageKey or storage path
|
||||||
|
* @param {String} noteContent Content to export
|
||||||
|
* @param {String} targetPath Path to exported file
|
||||||
|
* @param {function} outputFormatter
|
||||||
|
* @return {Promise.<*[]>}
|
||||||
|
*/
|
||||||
|
function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
|
||||||
|
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
|
||||||
|
const exportTasks = []
|
||||||
|
|
||||||
|
if (!storagePath) {
|
||||||
|
throw new Error('Storage path is not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
let exportedData = noteContent.replace(LOCAL_STORED_REGEX, (match, dstFilename, srcFilename) => {
|
||||||
|
dstFilename = filenamify(dstFilename, {replacement: '_'})
|
||||||
|
if (!path.extname(dstFilename)) {
|
||||||
|
dstFilename += path.extname(srcFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dstRelativePath = path.join(IMAGES_FOLDER_NAME, dstFilename)
|
||||||
|
|
||||||
|
exportTasks.push({
|
||||||
|
src: path.join(IMAGES_FOLDER_NAME, srcFilename),
|
||||||
|
dst: dstRelativePath
|
||||||
|
})
|
||||||
|
|
||||||
|
return ``
|
||||||
|
})
|
||||||
|
|
||||||
|
if (outputFormatter) {
|
||||||
|
exportedData = outputFormatter(exportedData, exportTasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
|
||||||
|
|
||||||
|
return Promise.all(tasks.map((task) => copyFile(task.src, task.dst)))
|
||||||
|
.then(() => {
|
||||||
|
return saveToFile(exportedData, targetPath)
|
||||||
|
}).catch((err) => {
|
||||||
|
rollbackExport(tasks)
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareTasks (tasks, storagePath, targetPath) {
|
||||||
|
return tasks.map((task) => {
|
||||||
|
if (!path.isAbsolute(task.src)) {
|
||||||
|
task.src = path.join(storagePath, task.src)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path.isAbsolute(task.dst)) {
|
||||||
|
task.dst = path.join(targetPath, task.dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
return task
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveToFile (data, filename) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.writeFile(filename, data, (err) => {
|
||||||
|
if (err) return reject(err)
|
||||||
|
|
||||||
|
resolve(filename)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove exported files
|
||||||
|
* @param tasks Array of copy task objects. Object consists of two mandatory fields – `src` and `dst`
|
||||||
|
*/
|
||||||
|
function rollbackExport (tasks) {
|
||||||
|
const folders = new Set()
|
||||||
|
tasks.forEach((task) => {
|
||||||
|
let fullpath = task.dst
|
||||||
|
|
||||||
|
if (!path.extname(task.dst)) {
|
||||||
|
fullpath = path.join(task.dst, path.basename(task.src))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(fullpath)) {
|
||||||
|
fs.unlink(fullpath)
|
||||||
|
folders.add(path.dirname(fullpath))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
folders.forEach((folder) => {
|
||||||
|
if (fs.readdirSync(folder).length === 0) {
|
||||||
|
fs.rmdir(folder)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default exportNote
|
||||||
@@ -7,6 +7,7 @@ const dataApi = {
|
|||||||
updateFolder: require('./updateFolder'),
|
updateFolder: require('./updateFolder'),
|
||||||
deleteFolder: require('./deleteFolder'),
|
deleteFolder: require('./deleteFolder'),
|
||||||
reorderFolder: require('./reorderFolder'),
|
reorderFolder: require('./reorderFolder'),
|
||||||
|
exportFolder: require('./exportFolder'),
|
||||||
createNote: require('./createNote'),
|
createNote: require('./createNote'),
|
||||||
updateNote: require('./updateNote'),
|
updateNote: require('./updateNote'),
|
||||||
deleteNote: require('./deleteNote'),
|
deleteNote: require('./deleteNote'),
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
const resolveStorageData = require('./resolveStorageData')
|
const resolveStorageData = require('./resolveStorageData')
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
const CSON = require('@rokt33r/season')
|
const CSON = require('@rokt33r/season')
|
||||||
const keygen = require('browser/lib/keygen')
|
const keygen = require('browser/lib/keygen')
|
||||||
const sander = require('sander')
|
const sander = require('sander')
|
||||||
const { findStorage } = require('browser/lib/findStorage')
|
const { findStorage } = require('browser/lib/findStorage')
|
||||||
|
const copyImage = require('./copyImage')
|
||||||
|
|
||||||
function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
||||||
let oldStorage, newStorage
|
let oldStorage, newStorage
|
||||||
@@ -37,12 +39,12 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
|||||||
return resolveStorageData(newStorage)
|
return resolveStorageData(newStorage)
|
||||||
.then(function findNewNoteKey (_newStorage) {
|
.then(function findNewNoteKey (_newStorage) {
|
||||||
newStorage = _newStorage
|
newStorage = _newStorage
|
||||||
newNoteKey = keygen()
|
newNoteKey = keygen(true)
|
||||||
let isUnique = false
|
let isUnique = false
|
||||||
while (!isUnique) {
|
while (!isUnique) {
|
||||||
try {
|
try {
|
||||||
sander.statSync(path.join(newStorage.path, 'notes', newNoteKey + '.cson'))
|
sander.statSync(path.join(newStorage.path, 'notes', newNoteKey + '.cson'))
|
||||||
newNoteKey = keygen()
|
newNoteKey = keygen(true)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 'ENOENT') {
|
if (err.code === 'ENOENT') {
|
||||||
isUnique = true
|
isUnique = true
|
||||||
@@ -65,6 +67,27 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
|||||||
|
|
||||||
return noteData
|
return noteData
|
||||||
})
|
})
|
||||||
|
.then(function moveImages (noteData) {
|
||||||
|
const searchImagesRegex = /!\[.*?]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
|
||||||
|
let match = searchImagesRegex.exec(noteData.content)
|
||||||
|
|
||||||
|
const moveTasks = []
|
||||||
|
while (match != null) {
|
||||||
|
const [, filename] = match
|
||||||
|
const oldPath = path.join(oldStorage.path, 'images', filename)
|
||||||
|
moveTasks.push(
|
||||||
|
copyImage(oldPath, noteData.storage, false)
|
||||||
|
.then(() => {
|
||||||
|
fs.unlinkSync(oldPath)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// find next occurence
|
||||||
|
match = searchImagesRegex.exec(noteData.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(moveTasks).then(() => noteData)
|
||||||
|
})
|
||||||
.then(function writeAndReturn (noteData) {
|
.then(function writeAndReturn (noteData) {
|
||||||
CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage']))
|
CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage']))
|
||||||
return noteData
|
return noteData
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user