Compare commits
1655 Commits
v0.10.65
...
android-v1
Author | SHA1 | Date | |
---|---|---|---|
|
c0d679b6c2 | ||
|
eb789b9b9a | ||
|
b1898141c3 | ||
|
3231bfaff0 | ||
|
6bb09c9c30 | ||
|
35d3fe03ab | ||
|
f05929cd17 | ||
|
982c9828da | ||
|
d6eacb2b33 | ||
|
0abe213fc2 | ||
|
a6716d55c5 | ||
|
fa0572de77 | ||
|
6dca4a0d6b | ||
|
eacfe1a9ac | ||
|
c223cdf10a | ||
|
38c42b7a15 | ||
|
56432dc773 | ||
|
d3b4379161 | ||
|
8a6fcdbcae | ||
|
061ce646d2 | ||
|
5ec7c16e3e | ||
|
5d629508c1 | ||
|
0a6f8b0cfe | ||
|
460f826672 | ||
|
cb16a10121 | ||
|
3b6131f1ca | ||
|
57225a36b9 | ||
|
3e313399c2 | ||
|
7947e14792 | ||
|
71098102c5 | ||
|
8e601e80df | ||
|
3b14cfcc54 | ||
|
61a0e43092 | ||
|
d08aaffe41 | ||
|
7d0def30f0 | ||
|
bb45d72a56 | ||
|
3943192c5d | ||
|
18d76807f6 | ||
|
01a30a7ccf | ||
|
3fb35d043b | ||
|
9b51bd484d | ||
|
879b556845 | ||
|
0df2a501dd | ||
|
6f64fdffcc | ||
|
19252af345 | ||
|
897f53b13e | ||
|
45cd8b7e3c | ||
|
922bbdd1b6 | ||
|
c24135577c | ||
|
3240ff40bc | ||
|
58b68cab0c | ||
|
0a0afd7245 | ||
|
de01606bff | ||
|
046474b484 | ||
|
277b2b9298 | ||
|
0b7296ae95 | ||
|
ce87dd55f0 | ||
|
07b724d65b | ||
|
bc1984298f | ||
|
9ed0bdfed2 | ||
|
57628e8986 | ||
|
fc8f53fd0e | ||
|
efd7cc6a0c | ||
|
7bfc3e1256 | ||
|
7f6ca1e527 | ||
|
71d9b1d441 | ||
|
a3d64d0a90 | ||
|
e7ec2ce6cf | ||
|
61dbdd5f7c | ||
|
e6888c451d | ||
|
899219abd2 | ||
|
7a4c7a13eb | ||
|
e8797f49b9 | ||
|
e17f3051f0 | ||
|
06091933e1 | ||
|
b30c65dd89 | ||
|
0eb18d206d | ||
|
3a9948e528 | ||
|
2bcddd38b2 | ||
|
5ff8808f69 | ||
|
28b1d8a324 | ||
|
5c1dd79435 | ||
|
706d59a6cc | ||
|
251f1bba55 | ||
|
cb1fd85ca4 | ||
|
11ddc55911 | ||
|
ee106105d8 | ||
|
19f5a144e5 | ||
|
18717bac79 | ||
|
28fa83c406 | ||
|
258e514a91 | ||
|
f92546d6eb | ||
|
693456164b | ||
|
7cd3e6b1f7 | ||
|
764e63d869 | ||
|
2c6f47f277 | ||
|
e41896d6f3 | ||
|
990591cc80 | ||
|
7b85c33213 | ||
|
4b4d0e8b25 | ||
|
4fb6af3c62 | ||
|
d7ffe7e294 | ||
|
3ff139d445 | ||
|
40443e0134 | ||
|
1f927c1285 | ||
|
5e82e62335 | ||
|
de954827df | ||
|
2cb24bf198 | ||
|
739a6a4a9c | ||
|
dfcf1193dc | ||
|
c72f92e22f | ||
|
f6d01ce7e1 | ||
|
fed9700587 | ||
|
12a3a9a89e | ||
|
590c62c371 | ||
|
df41f64b3c | ||
|
1849355245 | ||
|
fa1b471ea4 | ||
|
0a67f8c947 | ||
|
621d0260f4 | ||
|
f93fca7c5b | ||
|
f4d830c2ef | ||
|
1aa2844efa | ||
|
f22b2adaad | ||
|
b547f9aa13 | ||
|
e4166e9da7 | ||
|
1634fdb421 | ||
|
7f51035f91 | ||
|
70e71cbc2a | ||
|
ffd03bf34c | ||
|
f59a3dee78 | ||
|
3ba3037242 | ||
|
dbb269fef6 | ||
|
e209189faa | ||
|
2d7065cde2 | ||
|
59f5972c93 | ||
|
8bac5275c3 | ||
|
58d748e235 | ||
|
e69ac3e62a | ||
|
7fc8ac4c0f | ||
|
069dce69cd | ||
|
3bdf621026 | ||
|
2f62897fb6 | ||
|
dbdd602f50 | ||
|
d66fa87b2b | ||
|
124a959c8d | ||
|
127dce1cd6 | ||
|
44986a35a4 | ||
|
ea516301fd | ||
|
90b684457a | ||
|
8517e2aa42 | ||
|
b880be8b7c | ||
|
57fd1a7588 | ||
|
5ed458f634 | ||
|
ac12143d00 | ||
|
b6c36d1961 | ||
|
3c2de70baa | ||
|
f6c5620682 | ||
|
79b6f64bd0 | ||
|
ed89f55bff | ||
|
8841a92142 | ||
|
0bd19c97eb | ||
|
2fd026d107 | ||
|
5e7eb37ca7 | ||
|
6b10d5d821 | ||
|
0f4dbfbcbf | ||
|
99493174ec | ||
|
333253fd4f | ||
|
01470e8d3b | ||
|
bda2fe6717 | ||
|
d1f4c5be18 | ||
|
377adea51d | ||
|
cda3d20834 | ||
|
d11870b1eb | ||
|
53bda3eea7 | ||
|
30165e8d6a | ||
|
2202eb6570 | ||
|
720927f488 | ||
|
2858c0fce0 | ||
|
36c3521f40 | ||
|
98a3b99d17 | ||
|
95a06c4531 | ||
|
6ea77b36ce | ||
|
0cd7ebf9d3 | ||
|
a816498fc6 | ||
|
549c1a6767 | ||
|
f87d1f11b0 | ||
|
fb913bc33c | ||
|
53d7a51cb0 | ||
|
12da48c756 | ||
|
a0a6bdb684 | ||
|
eb4aa2c026 | ||
|
a9e789f845 | ||
|
89b76918bd | ||
|
e98575643c | ||
|
7c9e7743f1 | ||
|
435aa4845b | ||
|
9841488ce4 | ||
|
9c907989a5 | ||
|
f684d8e59a | ||
|
a1ad6c9712 | ||
|
b6ca3090df | ||
|
ff2d793fbb | ||
|
fcfb7f1111 | ||
|
6125cde223 | ||
|
c83391e624 | ||
|
a3a818ea74 | ||
|
54a4965503 | ||
|
2233d88c01 | ||
|
9680ab74a3 | ||
|
ef711af5b5 | ||
|
8a619e4b8b | ||
|
bc09d2c640 | ||
|
f82dfde6f4 | ||
|
312c7f2d27 | ||
|
953cc327c6 | ||
|
14cff96713 | ||
|
34b9af2ce0 | ||
|
6a6ee280c3 | ||
|
861387707a | ||
|
830e665366 | ||
|
f14ae68ea0 | ||
|
c7084bf27e | ||
|
fc8ffcbe46 | ||
|
77f089654e | ||
|
e7a12bb0dd | ||
|
22fe3a4e44 | ||
|
afb8b92528 | ||
|
5178f99100 | ||
|
72af564382 | ||
|
0a2b83998c | ||
|
73e79213dc | ||
|
e31ffc9474 | ||
|
fdb8706a5f | ||
|
4c0262bd82 | ||
|
3b2dcb37a6 | ||
|
46a3b020a6 | ||
|
8373392e99 | ||
|
695c2623c2 | ||
|
979e7f2486 | ||
|
e7a9f630ec | ||
|
4e8372174b | ||
|
1b8912d7e9 | ||
|
8c3669588b | ||
|
1b784fe3b0 | ||
|
5ab1d8dfd6 | ||
|
cda8b95bfa | ||
|
9664842b1a | ||
|
09836e1d34 | ||
|
8974e20c7f | ||
|
761a49803e | ||
|
a40028f0c0 | ||
|
d4fca7e313 | ||
|
6748d4d825 | ||
|
0a5ad1d628 | ||
|
4080958e10 | ||
|
95c4a717e3 | ||
|
c5b9353105 | ||
|
17595f7ceb | ||
|
dcf78e8a06 | ||
|
de0c54c3c3 | ||
|
38970e9a52 | ||
|
563f43168b | ||
|
6e235605ed | ||
|
0749e0b675 | ||
|
756f3e627c | ||
|
4b39ed42b1 | ||
|
abe85ca4bd | ||
|
a559565ace | ||
|
d35e3163ca | ||
|
f22ad85681 | ||
|
727bdaeea4 | ||
|
42f7764eed | ||
|
1fbc1073ca | ||
|
66b683e5e7 | ||
|
7d1f61e47b | ||
|
643e5a6a2a | ||
|
a1e7e29279 | ||
|
abf6c3f3f1 | ||
|
32c81ad8c2 | ||
|
0f461c4caa | ||
|
57ed718993 | ||
|
ef1ae63233 | ||
|
81ac200cc0 | ||
|
3a2d62f6c7 | ||
|
7f80f67fd6 | ||
|
cebd8de77a | ||
|
417218fc34 | ||
|
29586437c2 | ||
|
f51d0ad914 | ||
|
35294b5f97 | ||
|
758562cff9 | ||
|
da0678c6fe | ||
|
afe4fd70cc | ||
|
4cef383fe7 | ||
|
b58c30889e | ||
|
1561c0e4d7 | ||
|
32b11c15a4 | ||
|
5e06efc1b9 | ||
|
43bd88703c | ||
|
cdd70230af | ||
|
eaf3eef2d3 | ||
|
81ec8eaf83 | ||
|
23f7e350c6 | ||
|
cea368cd3f | ||
|
50c8f2ae61 | ||
|
ed0ecababb | ||
|
72aa4c40a5 | ||
|
4f6784e2e5 | ||
|
01f015a54f | ||
|
806acad22a | ||
|
1d322d8a39 | ||
|
aef94e6950 | ||
|
456fcec334 | ||
|
3b6937c2f0 | ||
|
7cdd1d41c1 | ||
|
1fc535a740 | ||
|
033b37077a | ||
|
07f6a4a08b | ||
|
8c1b592a51 | ||
|
9460f7a17a | ||
|
106260ed69 | ||
|
123162e946 | ||
|
54e81966e5 | ||
|
9bf6ab60bb | ||
|
4f0ff3cdfc | ||
|
47cfaaa5ab | ||
|
1f49788f21 | ||
|
7e4cf9aeda | ||
|
4b6964b683 | ||
|
3caf398021 | ||
|
8840631266 | ||
|
c4411bb895 | ||
|
f63668350b | ||
|
3fc54d7ffd | ||
|
2c6c20f44f | ||
|
08ee939951 | ||
|
463b1441d3 | ||
|
6754d4ee89 | ||
|
d5d0732bf3 | ||
|
d27cbaa663 | ||
|
70adf10f2e | ||
|
e75417d26e | ||
|
2ded983828 | ||
|
0c708f766b | ||
|
a801f8d8ed | ||
|
26fc26c9fe | ||
|
df4c07d204 | ||
|
cf565d1563 | ||
|
6b425cf543 | ||
|
6188e7a0fa | ||
|
310afb0ad6 | ||
|
7d7e1e1637 | ||
|
424c8a2723 | ||
|
187fb1b85d | ||
|
595fd7a9aa | ||
|
0027cb9036 | ||
|
db6878b978 | ||
|
1c78722573 | ||
|
fea83e28c4 | ||
|
84adf64271 | ||
|
74e2b0d15d | ||
|
df302206dd | ||
|
6d8941c005 | ||
|
971b20062f | ||
|
936f334b61 | ||
|
7e3a290939 | ||
|
c6466a780e | ||
|
43774ad3fb | ||
|
b3ba5b7747 | ||
|
599f4ccef4 | ||
|
a67600d264 | ||
|
ebf4c89ef0 | ||
|
aa7da784fc | ||
|
617ed42d8c | ||
|
5848e7d90d | ||
|
01d032261c | ||
|
54d06646aa | ||
|
81da46035a | ||
|
74d0f75802 | ||
|
f25a352dcb | ||
|
21ef8da45f | ||
|
1f3a1c49df | ||
|
a8b58aaec3 | ||
|
44f9b35d93 | ||
|
711af9beed | ||
|
971339ca9a | ||
|
f5a72ffbaf | ||
|
cf4331c5af | ||
|
07b85388fc | ||
|
553b086ba2 | ||
|
ff89537899 | ||
|
f20792889a | ||
|
ee22a7ff73 | ||
|
4fc4353859 | ||
|
f4f9e25e6b | ||
|
e54f9934b5 | ||
|
f599ae065a | ||
|
5bd9bf6a4e | ||
|
cb9e8d4f76 | ||
|
f64596672e | ||
|
961150b2d3 | ||
|
0cd8e1cbc0 | ||
|
b503aff5e9 | ||
|
54bde47c67 | ||
|
d345f8dc13 | ||
|
979b0c0e78 | ||
|
13525f3327 | ||
|
5d9c2c0904 | ||
|
c748281d86 | ||
|
fa619eba7c | ||
|
17a75f7cf5 | ||
|
a74cfbfb25 | ||
|
af01fed950 | ||
|
218b446915 | ||
|
a68df18cd5 | ||
|
f79326b2d5 | ||
|
b08dcdfd90 | ||
|
ced14e578f | ||
|
0528c6e970 | ||
|
df9c1e0aeb | ||
|
b6619b41df | ||
|
ab9675544c | ||
|
0d9f703c75 | ||
|
f6ee5dd0e7 | ||
|
423d880b92 | ||
|
41017b9ab8 | ||
|
e3314c859f | ||
|
8f794fdbc6 | ||
|
80b0773618 | ||
|
ac848241b9 | ||
|
b4432e2efc | ||
|
52f60a2cf6 | ||
|
03c8438050 | ||
|
0e1c36ccf1 | ||
|
1b2b68c485 | ||
|
5c36f3e78a | ||
|
d4ec8ae823 | ||
|
449a70d840 | ||
|
8375030135 | ||
|
e9f938b0fb | ||
|
b0e57a5990 | ||
|
f47610e6fd | ||
|
c131cb9bb8 | ||
|
023f775bd2 | ||
|
1127eb6e09 | ||
|
0d7437c7d2 | ||
|
6dbc691973 | ||
|
fe53200a3a | ||
|
c7f61271a0 | ||
|
b826e2d97b | ||
|
d8ad42b04a | ||
|
b3ca30b8b6 | ||
|
bdd9da3d22 | ||
|
eb43ddc701 | ||
|
f9c65a148f | ||
|
bd0b9dff51 | ||
|
3822309657 | ||
|
ac2ec65c81 | ||
|
c0943f1776 | ||
|
5a2ab5fae7 | ||
|
281e36fde7 | ||
|
1af1c445c6 | ||
|
e6d2e028ad | ||
|
8d25b8075d | ||
|
58201fd6c3 | ||
|
2b624a9aed | ||
|
792fd7c50d | ||
|
90d37a15bd | ||
|
ef57ee803f | ||
|
e2bfb74895 | ||
|
89b486a3ee | ||
|
d6c6ef20d4 | ||
|
6cb6e9541f | ||
|
a8acecb703 | ||
|
0938297250 | ||
|
a2da2f681c | ||
|
32477e901d | ||
|
1841c4dc11 | ||
|
f81dce3321 | ||
|
e15f84716a | ||
|
d11ecd8fac | ||
|
012e70d668 | ||
|
264ee4f319 | ||
|
4640d6d6e3 | ||
|
9db9d98419 | ||
|
f79d7b9626 | ||
|
a8da469523 | ||
|
3c5eb99c59 | ||
|
8dc14516d6 | ||
|
84efc6a04e | ||
|
86e3038cfe | ||
|
6898b9ca4c | ||
|
9eb62920f7 | ||
|
7cf267254f | ||
|
5a0e3cbbf2 | ||
|
4b376ec5c2 | ||
|
92b71d3eb2 | ||
|
c32d7de7c4 | ||
|
c83a61d45d | ||
|
429f2d5aab | ||
|
ed70cf571c | ||
|
fd77671575 | ||
|
9d915a916e | ||
|
acb90935c7 | ||
|
6301ba0a12 | ||
|
44e1245416 | ||
|
6527d9db83 | ||
|
2bcaf62a2f | ||
|
2db3998f11 | ||
|
04724c58d1 | ||
|
7ed9c2770c | ||
|
48883bfa13 | ||
|
b6d9e695d1 | ||
|
43bab3c1bd | ||
|
dd67602b87 | ||
|
c96a416c2c | ||
|
c4ca9cde32 | ||
|
6c5d208893 | ||
|
795fd8b58c | ||
|
c226940792 | ||
|
bdd0a6106f | ||
|
d1ea7ad3ea | ||
|
a2b1181f7c | ||
|
8cce2f17d5 | ||
|
658b911513 | ||
|
3c95979d94 | ||
|
2e32211a28 | ||
|
ba2874173d | ||
|
ba9598682c | ||
|
30bfd82683 | ||
|
10c6774c28 | ||
|
c4ad9019aa | ||
|
7c99ab9947 | ||
|
feb7778fe4 | ||
|
b45185780f | ||
|
4e032c0c55 | ||
|
2e2b35dfeb | ||
|
526ef7e1d2 | ||
|
a37005446a | ||
|
e012b927dc | ||
|
359b8d5545 | ||
|
23c592b322 | ||
|
9aeddf86f4 | ||
|
0e1887988e | ||
|
394f2df664 | ||
|
2a04378a0d | ||
|
bac68f2c42 | ||
|
0f0ff86ffa | ||
|
8b38752cbf | ||
|
3c24589450 | ||
|
65065a62d8 | ||
|
482e9340bc | ||
|
69d490996e | ||
|
3494937e34 | ||
|
41ba1043be | ||
|
cc57de60c0 | ||
|
60a2b9e5c6 | ||
|
8e1fb666a5 | ||
|
f4ad777bbf | ||
|
2eacf6146a | ||
|
fe2ba34cb4 | ||
|
84daa0db61 | ||
|
b9118a90be | ||
|
ef2ffd4e52 | ||
|
5e3063abe0 | ||
|
f460b2497a | ||
|
c080d7054f | ||
|
61dd4cefbc | ||
|
63d99b2d70 | ||
|
55332d7671 | ||
|
16635defcd | ||
|
595cf3fcad | ||
|
c9b9f82130 | ||
|
f5bca733d7 | ||
|
494e235e18 | ||
|
85219a6004 | ||
|
e4a7851e57 | ||
|
b7529b40b5 | ||
|
74827e5324 | ||
|
2e16cc5433 | ||
|
7f41bc5703 | ||
|
a2380fb752 | ||
|
f6a902809d | ||
|
33a853397d | ||
|
4f02481899 | ||
|
b18076565f | ||
|
853ddc5840 | ||
|
7930ab66c6 | ||
|
c7716c0d59 | ||
|
49cbb254d0 | ||
|
cf9246796d | ||
|
e1dee546dc | ||
|
da6fdad2de | ||
|
567596643c | ||
|
cb617e1b14 | ||
|
facf8afa8b | ||
|
f0dd61a711 | ||
|
e958211a13 | ||
|
0ed170b5bc | ||
|
473d3453a2 | ||
|
fa9d7b0408 | ||
|
d4a28f48c9 | ||
|
ead6fff861 | ||
|
c7d06b35cd | ||
|
fa939e5c76 | ||
|
1bf2601f4f | ||
|
feb0c02c9a | ||
|
40a34a7c05 | ||
|
c62dcd96b0 | ||
|
1364d6786d | ||
|
9f2666aef9 | ||
|
a6a351e68d | ||
|
1db38a9699 | ||
|
c57db1834f | ||
|
3aeb49b469 | ||
|
80b467eead | ||
|
61572f287a | ||
|
f136664c11 | ||
|
0e545baf10 | ||
|
e65e647359 | ||
|
238268884e | ||
|
4c210d0956 | ||
|
5f32c6466a | ||
|
71bd39a8a3 | ||
|
ffb660f0f4 | ||
|
dde23632c1 | ||
|
9d26f13db0 | ||
|
2a4c9c4427 | ||
|
3bfde26b74 | ||
|
a419bc7253 | ||
|
89e0dad88b | ||
|
ff1ee1249b | ||
|
ba9cfd8041 | ||
|
80a51e02a4 | ||
|
a2e2a9a2f5 | ||
|
49e4c37cac | ||
|
11d323d8b7 | ||
|
784ba45f1f | ||
|
e534414874 | ||
|
01f4faf8f1 | ||
|
b33d30ca47 | ||
|
1ba3fae101 | ||
|
9550347e04 | ||
|
398946d39a | ||
|
05faf55e8d | ||
|
4cf5525e20 | ||
|
62e91c44d7 | ||
|
e4ec4ae92b | ||
|
c1f5dfd9cc | ||
|
0c0efeac1f | ||
|
5e0f2642e3 | ||
|
93966b0fa1 | ||
|
e90abf3517 | ||
|
d3fa0dce96 | ||
|
58a7c2fa94 | ||
|
962a8700c2 | ||
|
b5c704e2bb | ||
|
e7b52b19d7 | ||
|
903c2e6d92 | ||
|
abcb1ac760 | ||
|
b6bf76cc4c | ||
|
2bf87655da | ||
|
d4b19f19a1 | ||
|
d8ccc38d5b | ||
|
c8c9f80cc5 | ||
|
577bef5704 | ||
|
4e3b8a06ea | ||
|
363632ffa7 | ||
|
994c99f47f | ||
|
96571baadc | ||
|
4ce2b2c948 | ||
|
5d69f7a0a7 | ||
|
69ddcc6e30 | ||
|
bcb1f36ad8 | ||
|
34c65a686c | ||
|
0b32741a12 | ||
|
dbb321a3cc | ||
|
a6e4f47adf | ||
|
fb6dee32ac | ||
|
984dd6f2c0 | ||
|
02bde2c6e9 | ||
|
782d24cc04 | ||
|
4d0af575e5 | ||
|
be8bda8e73 | ||
|
1242de532e | ||
|
7d7ec7f15e | ||
|
ca112ec5d3 | ||
|
5deb8cf76d | ||
|
a2c9737c17 | ||
|
d3fca3d6cc | ||
|
16554b22c7 | ||
|
d5574098f0 | ||
|
f5a683f25c | ||
|
5f04adb392 | ||
|
edd0f7e255 | ||
|
67145d9104 | ||
|
003e2afff7 | ||
|
6e9d70c5cb | ||
|
4821b4cdf2 | ||
|
734d4db431 | ||
|
317aaed0ac | ||
|
9778098d6c | ||
|
5b1755f988 | ||
|
2a772895dd | ||
|
5fbb01cf2f | ||
|
f9e0870b4e | ||
|
a58f1e9b4b | ||
|
6fc0d89b30 | ||
|
2dcadab7d2 | ||
|
bb3307e156 | ||
|
ecd07f1209 | ||
|
266cb1174f | ||
|
bfb9b77b6e | ||
|
01b1361dcb | ||
|
3a921720d6 | ||
|
cdfd3d9c31 | ||
|
9961fb64bb | ||
|
3137c355cf | ||
|
16abaf60d2 | ||
|
9004b710ea | ||
|
6ebac21c2b | ||
|
99f79faf83 | ||
|
613fa20806 | ||
|
1b5f812278 | ||
|
3a9643c1ea | ||
|
aee7f5a8ac | ||
|
d3cd378922 | ||
|
4f5e7367d0 | ||
|
2280fb5c43 | ||
|
96fb7c2087 | ||
|
6e994fd8b9 | ||
|
a7cde1e269 | ||
|
f8310ba0d5 | ||
|
b239c3faba | ||
|
3c2281dbf9 | ||
|
ac07bf784d | ||
|
067455542f | ||
|
5bfeaa357b | ||
|
fe27a64331 | ||
|
ed638612aa | ||
|
1d7ec83510 | ||
|
75c710232d | ||
|
5af52afadb | ||
|
0f4324c2f8 | ||
|
b48e1dac94 | ||
|
f0ca8e1e31 | ||
|
74b83eb71e | ||
|
28dce0fbb5 | ||
|
c12d402c7e | ||
|
014f5b123c | ||
|
58601dfc04 | ||
|
9fe7f0adae | ||
|
ea1374371f | ||
|
bce4294529 | ||
|
de409b632a | ||
|
a677b2e844 | ||
|
c63bb19cb6 | ||
|
72fd77812e | ||
|
40f3e72bd1 | ||
|
d6d86f2aff | ||
|
c71809438b | ||
|
3e6e1a0a36 | ||
|
f590ce4a34 | ||
|
67608e29c8 | ||
|
d5c2982093 | ||
|
90fad2a3ab | ||
|
bc7c82e3da | ||
|
cb824f7dd7 | ||
|
32c47a96f1 | ||
|
4e3f8893f7 | ||
|
ca3946689a | ||
|
e2ad2dfcaa | ||
|
d6f7893c56 | ||
|
8c65a7cc31 | ||
|
aabb9be7de | ||
|
544f93bf22 | ||
|
f81dbf4a4c | ||
|
fbec8263a3 | ||
|
68d77a69e6 | ||
|
f2ef2446c6 | ||
|
875cb5387a | ||
|
ae9ecdad40 | ||
|
86a0e34975 | ||
|
1141074745 | ||
|
efc46d9989 | ||
|
2b45f745b6 | ||
|
37fb81e9b2 | ||
|
255a4fac93 | ||
|
3e3fb88de8 | ||
|
e4cf03ae46 | ||
|
554a3eb10d | ||
|
61881b528a | ||
|
c2507cbc4e | ||
|
ed0f6d165c | ||
|
8e22d38eb3 | ||
|
2599c425c3 | ||
|
0e15821a81 | ||
|
c1bb51c12b | ||
|
1532b6d159 | ||
|
945018b698 | ||
|
df7b981e5e | ||
|
4fe495675b | ||
|
7828eef2ad | ||
|
694f81b75f | ||
|
8364b6e08d | ||
|
3f4328ce9d | ||
|
9e0bf1acb2 | ||
|
c9e130a771 | ||
|
26331f61e1 | ||
|
694672859a | ||
|
858ead40b9 | ||
|
b07fe5cc34 | ||
|
0317171097 | ||
|
9741a3a53d | ||
|
7937fab5ff | ||
|
f595be07d4 | ||
|
eef106c99b | ||
|
dbe1833f92 | ||
|
520dc0ae21 | ||
|
c9be287f4a | ||
|
711f5dcaba | ||
|
ebc0aa9809 | ||
|
dcaaf50a5a | ||
|
3370b57134 | ||
|
55c5ddedf4 | ||
|
5e8b09f5af | ||
|
1acffce62d | ||
|
8555ecce87 | ||
|
4df5f668dc | ||
|
cceebeebef | ||
|
c4f19465a6 | ||
|
e868102c98 | ||
|
0d4a1837f5 | ||
|
d6a4436313 | ||
|
03b5c6aa5e | ||
|
250cd47e02 | ||
|
943fef32e7 | ||
|
408634671c | ||
|
570b5856ba | ||
|
d114d14e87 | ||
|
32791f502e | ||
|
083ab0c788 | ||
|
003c4c4e26 | ||
|
f08f89ebd4 | ||
|
3c973144c4 | ||
|
82e99ca658 | ||
|
b04d750cec | ||
|
c804e9f541 | ||
|
7753f3f842 | ||
|
c985b7c682 | ||
|
4509919c22 | ||
|
89b164c7ca | ||
|
e52d17b39a | ||
|
5014914dc9 | ||
|
122ab83a84 | ||
|
7a985c2c8a | ||
|
b11ad30a31 | ||
|
5914fc97df | ||
|
e41ae1832d | ||
|
89b50909ed | ||
|
edccd7412f | ||
|
c76beae057 | ||
|
23c5934a7d | ||
|
a078947d6d | ||
|
0faaf660b4 | ||
|
5ba98b4200 | ||
|
c36513b99d | ||
|
97814531fa | ||
|
fd3e335a02 | ||
|
e676fa2b57 | ||
|
122cbbf673 | ||
|
271793b324 | ||
|
134b31933b | ||
|
0ec5518a62 | ||
|
76931370d7 | ||
|
8cf0e4517a | ||
|
e75c62bf0f | ||
|
058285e0b9 | ||
|
795568d8c2 | ||
|
df4933fddd | ||
|
4046a51472 | ||
|
45845f645d | ||
|
d7fd8944f7 | ||
|
3cee671f25 | ||
|
8f2e5faff3 | ||
|
39ddd934f6 | ||
|
9f8a46b9d9 | ||
|
c6698eaea6 | ||
|
8a96cf3434 | ||
|
74d255c056 | ||
|
71aa841265 | ||
|
14a93a9f26 | ||
|
e1fd9c6922 | ||
|
b9db747b5c | ||
|
4a56c76901 | ||
|
6bb3184a72 | ||
|
7fb8fbd450 | ||
|
9d5bba472e | ||
|
e6d821a45f | ||
|
72f0027e21 | ||
|
29a13a9943 | ||
|
3691ae4d13 | ||
|
4dda397c29 | ||
|
b4b058998d | ||
|
10919e415e | ||
|
4966d74864 | ||
|
c70ecb30a5 | ||
|
acc0d17e0f | ||
|
b509b878bf | ||
|
322ec2efa1 | ||
|
1232661b1e | ||
|
c46d123503 | ||
|
8f4060999f | ||
|
0addd86069 | ||
|
760086307b | ||
|
fc6558a64c | ||
|
eca500880d | ||
|
90bcd7c977 | ||
|
cca0c6eaf3 | ||
|
b0736002be | ||
|
51fc2d8e51 | ||
|
d87c192ff1 | ||
|
52ccf398a6 | ||
|
344d0e2687 | ||
|
1bc4d6b423 | ||
|
baa9ca7ea3 | ||
|
e4d477fb4c | ||
|
71319eee28 | ||
|
68b31526f8 | ||
|
0b2b7324d9 | ||
|
43512cf27b | ||
|
4218b65969 | ||
|
7244e12b78 | ||
|
a796ef5c66 | ||
|
9474a05aaa | ||
|
41df355a7e | ||
|
4f3ab87914 | ||
|
5d1a08707c | ||
|
4f822df80e | ||
|
951be5cbf6 | ||
|
b6c2341542 | ||
|
a6e6b49a9d | ||
|
3a4bbd571e | ||
|
feccc6150e | ||
|
a37b599a6b | ||
|
9347683fe3 | ||
|
3551c26e28 | ||
|
cfca0107eb | ||
|
dfbe37fdaf | ||
|
81bc975193 | ||
|
7908fda451 | ||
|
cdbb7c4b0d | ||
|
414e57ec55 | ||
|
1871123066 | ||
|
87bc08bef5 | ||
|
214a39c3d3 | ||
|
ef0cc5e33e | ||
|
3a1fa583ab | ||
|
c1161ae017 | ||
|
1023ec6206 | ||
|
7841421c0d | ||
|
995d8c35dd | ||
|
b179471eff | ||
|
19a126ebfe | ||
|
7e56e5b587 | ||
|
acf0c79341 | ||
|
9fe7e23ffe | ||
|
c94cc93971 | ||
|
b26094eba8 | ||
|
89a5ccdf93 | ||
|
ce2da0e6dc | ||
|
f49d644b6a | ||
|
02ac0b8593 | ||
|
78e5eaf1e2 | ||
|
fc0d227396 | ||
|
f91c52cdf7 | ||
|
3f14878d0f | ||
|
69fd32e7c6 | ||
|
80801cedf0 | ||
|
480e4fa94b | ||
|
717c789836 | ||
|
f099376446 | ||
|
41fa9d093e | ||
|
e2f3f81eb6 | ||
|
5cab7aeb55 | ||
|
fa5f418c22 | ||
|
a25fcacace | ||
|
727ba7300e | ||
|
d25d9b3f44 | ||
|
9d762a4319 | ||
|
18d94c7585 | ||
|
af82345eb8 | ||
|
1e94a22986 | ||
|
e19a8a99ff | ||
|
f975009e24 | ||
|
90640fafc7 | ||
|
42e0e1e5a5 | ||
|
61f64fa933 | ||
|
0d0ffd6d27 | ||
|
023ccffd2e | ||
|
bc26098c7d | ||
|
7257a71a18 | ||
|
8ad8b73585 | ||
|
9a06815db9 | ||
|
66947d4954 | ||
|
3ec22185d5 | ||
|
0f05c23e26 | ||
|
74493fece0 | ||
|
557a96e814 | ||
|
4b23b419a4 | ||
|
8b7f5b1151 | ||
|
29e9ccf216 | ||
|
2c04f5c8bc | ||
|
5430a747e9 | ||
|
13bc185829 | ||
|
ed87581a8a | ||
|
2645ec96a8 | ||
|
d278d830f0 | ||
|
b4dce0ed46 | ||
|
e8416042d4 | ||
|
70adbe5e76 | ||
|
f66be08d1d | ||
|
fad96f5266 | ||
|
c33a7f5f47 | ||
|
28afbcde02 | ||
|
691292d2b3 | ||
|
30ff81064f | ||
|
f9f398ad98 | ||
|
537884bdcd | ||
|
d54400a7cb | ||
|
37e7ea0b52 | ||
|
42c78264fb | ||
|
c52da82447 | ||
|
cca43624e4 | ||
|
dac1cd7668 | ||
|
b4c00db0e3 | ||
|
3ce393a8b2 | ||
|
2b627fe4ab | ||
|
fcf8a1649d | ||
|
8d3b050831 | ||
|
43297ef0a3 | ||
|
551fabdfc9 | ||
|
d6de56b2db | ||
|
9e979804f3 | ||
|
b8e0f182cc | ||
|
9a41b9e192 | ||
|
9b8f520b9f | ||
|
5b6019805c | ||
|
a4106436c4 | ||
|
f6b4eb511e | ||
|
eb67ac17a0 | ||
|
7b760d03ef | ||
|
2805ae2acf | ||
|
5cb5ccc781 | ||
|
0dba2821b6 | ||
|
1db7825b22 | ||
|
8a92d6ad70 | ||
|
138ad9fcad | ||
|
08cb518c25 | ||
|
6d04eab200 | ||
|
8a8cb51e1b | ||
|
5c66042a2d | ||
|
ae75181b02 | ||
|
9dc3238182 | ||
|
0a68749373 | ||
|
1519116291 | ||
|
d023d841e2 | ||
|
d7a1465d8e | ||
|
15848fc696 | ||
|
837ae2c9f2 | ||
|
6789b98ead | ||
|
29f6e74ee3 | ||
|
2780c38c45 | ||
|
4531838217 | ||
|
7bccf7f65d | ||
|
c62a24a9cb | ||
|
c6830499f7 | ||
|
d9f00a2539 | ||
|
def83c9119 | ||
|
b6cb0056c7 | ||
|
1669b5258a | ||
|
5a9e0bfc26 | ||
|
8f3fdb3afe | ||
|
7ab135c099 | ||
|
1cc27f2509 | ||
|
ef700b421c | ||
|
b9af5ac052 | ||
|
173f2d421d | ||
|
9f82c069c9 | ||
|
6ade09c228 | ||
|
5393a1399c | ||
|
fd29f20b2e | ||
|
c011b53d1f | ||
|
26e3a7b68c | ||
|
e70a291698 | ||
|
511bd57726 | ||
|
c6de8598dc | ||
|
7bee25599d | ||
|
773a1ad829 | ||
|
1a1e264fa4 | ||
|
5b99ecefca | ||
|
1bfeed377a | ||
|
86eee376bb | ||
|
6a7d368184 | ||
|
1da19ae98d | ||
|
f52c117b09 | ||
|
2551f96149 | ||
|
c984c19fee | ||
|
ac8e91e82e | ||
|
af50d80541 | ||
|
e355f4e49b | ||
|
738ef2b0fa | ||
|
9746a3964b | ||
|
9efbf74b6f | ||
|
c16ea6b237 | ||
|
b06a3b588f | ||
|
6ff67e0995 | ||
|
1a5c8d126d | ||
|
f632580eed | ||
|
1d73f0cdee | ||
|
99c7111f8c | ||
|
ae9806561a | ||
|
fffdf5b5b7 | ||
|
3de19c3db7 | ||
|
56e074b4ef | ||
|
1a79253780 | ||
|
b67908df11 | ||
|
6a5089f71d | ||
|
f710463b67 | ||
|
6ae0c3aba0 | ||
|
07c6347014 | ||
|
b10999e83e | ||
|
961b5bfd25 | ||
|
d1f1d1068a | ||
|
faade0afe2 | ||
|
a442a49e2f | ||
|
7d3fbbcaba | ||
|
d9bb7c3271 | ||
|
4d1dd17fa2 | ||
|
c5c6c777be | ||
|
1fd1a73fda | ||
|
feeb498a79 | ||
|
1d7f30d441 | ||
|
53da63e371 | ||
|
424443a2d8 | ||
|
08b58f0e4c | ||
|
c2a0d8600f | ||
|
ede3c2ce2f | ||
|
0b93515711 | ||
|
2f13e689b9 | ||
|
ea135a0d28 | ||
|
f67e4a03e4 | ||
|
e9268edeff | ||
|
a8576a55d6 | ||
|
eb500cdf9e | ||
|
7b9dc66121 | ||
|
bba2c68c6f | ||
|
c70d8bea78 | ||
|
176bda66ad | ||
|
78ce10ddf0 | ||
|
29f14681a8 | ||
|
aaf617e41c | ||
|
b99146ed7f | ||
|
d136161650 | ||
|
39051a27a1 | ||
|
30bc9dd551 | ||
|
cc2f665313 | ||
|
37c989ed28 | ||
|
adc5885980 | ||
|
8de5b4219d | ||
|
e096ddebd4 | ||
|
83398dd0bc | ||
|
ddc78ebb41 | ||
|
70b69eb31e | ||
|
3fa891e136 | ||
|
6f7a9f3295 | ||
|
44bf518244 | ||
|
63cb9b4968 | ||
|
a6cecc103c | ||
|
8e8793943b | ||
|
83d9faf2fe | ||
|
f45a4fff8b | ||
|
1e02aa3120 | ||
|
77fec75f23 | ||
|
2c9feae20b | ||
|
a81788b3fa | ||
|
88cfba53a3 | ||
|
d659d975cd | ||
|
277ad90f72 | ||
|
f2e3bedde6 | ||
|
98c0f2315a | ||
|
0926755635 | ||
|
75f8234db5 | ||
|
b8e85e1587 | ||
|
b6add857f9 | ||
|
4e41731c08 | ||
|
6ac3b8a8f9 | ||
|
7ab2b11c9d | ||
|
1eb1c5914c | ||
|
5780951f7d | ||
|
67e790e393 | ||
|
0d472486ca | ||
|
2d218904d0 | ||
|
c3fbcb8feb | ||
|
64031ac6cf | ||
|
caae7f7cf8 | ||
|
d56b247adf | ||
|
11bdfbde61 | ||
|
a59bf55c16 | ||
|
043be1916c | ||
|
42e34b5c3b | ||
|
931083b2e2 | ||
|
09b9df4228 | ||
|
37663bd110 | ||
|
f01c6aa8d1 | ||
|
8838017830 | ||
|
1d6fb8058f | ||
|
bb51729bea | ||
|
507e7e6014 | ||
|
f42908b11c | ||
|
03ec406627 | ||
|
c703521b6c | ||
|
cf97bf9a77 | ||
|
304b9a582f | ||
|
4d5c4b1743 | ||
|
4abe5d07c4 | ||
|
f6633e23f5 | ||
|
a6d6201ecb | ||
|
0115e74163 | ||
|
4314c392f6 | ||
|
685f541bb4 | ||
|
f9634ea283 | ||
|
6252a4d8c8 | ||
|
d13d7bec45 | ||
|
74e284d795 | ||
|
7b82dacbf2 | ||
|
f82162f94a | ||
|
e2d5128624 | ||
|
9074412472 | ||
|
0db9c66317 | ||
|
b22211fada | ||
|
7efeaa3a22 | ||
|
79efac2f71 | ||
|
9d7b7092f5 | ||
|
71e877d369 | ||
|
7e086a7730 | ||
|
3f810c71b0 | ||
|
500fbc5294 | ||
|
6d0f60d9a1 | ||
|
e19c26fdd1 | ||
|
86b86513d9 | ||
|
6ff19063ef | ||
|
6a75451539 | ||
|
70a33f8533 | ||
|
23722719f0 | ||
|
d499251206 | ||
|
d180e7b5e1 | ||
|
9e869a5b1f | ||
|
ab959623aa | ||
|
08d2655f13 | ||
|
20632ae1c1 | ||
|
bef7c38724 | ||
|
d1abf4971d | ||
|
d13c2cf8d7 | ||
|
3146273409 | ||
|
1891eb4998 | ||
|
70b03971f6 | ||
|
38c050b47e | ||
|
0bf5c9ebdd | ||
|
6683da804b | ||
|
7750b954fc | ||
|
edbff5a26a | ||
|
18846c11ed | ||
|
cc02c1d585 | ||
|
26bf7c4d46 | ||
|
2959fa1080 | ||
|
3f4f154949 | ||
|
4c0b472f67 | ||
|
4756238821 | ||
|
f5d26e0d81 | ||
|
e9bb5bee9d | ||
|
2c608bca3c | ||
|
d9c1e30e9b | ||
|
5bc72e2b44 | ||
|
df05d04dad | ||
|
888ac8f4c2 | ||
|
55266e5694 | ||
|
39c73e1649 | ||
|
3bf9d01f0a | ||
|
89ef33f7ca | ||
|
f71fe9a1a6 | ||
|
1008b1835b | ||
|
2ffa5419e2 | ||
|
bd20ecff78 | ||
|
a073514c46 | ||
|
c6ff14226f | ||
|
ee02f8255e | ||
|
5951ed3f55 | ||
|
f6fbf3ba0f | ||
|
9bce52a92a | ||
|
e44975622a | ||
|
92b857d83b | ||
|
671e538740 | ||
|
8a282fd2e1 | ||
|
cda623a95c | ||
|
0f343bccda | ||
|
a513f6f3f0 | ||
|
3b4809714e | ||
|
238b5ab9b9 | ||
|
08d9e9b6aa | ||
|
b22900eb3a | ||
|
89fc2c4779 | ||
|
353b79f5e5 | ||
|
b929b46281 | ||
|
5c4a536dad | ||
|
91e337307c | ||
|
2855b68ed4 | ||
|
45ca6284f9 | ||
|
145ee13356 | ||
|
6f97747199 | ||
|
f3751e4ba6 | ||
|
5cd55cada6 | ||
|
bad4b2ecb8 | ||
|
7aafd63ff3 | ||
|
3227a13035 | ||
|
027f96d100 | ||
|
f242a3c215 | ||
|
ad6c347180 | ||
|
4f0431da55 | ||
|
b873fdd029 | ||
|
c1ff820913 | ||
|
7008daf92a | ||
|
ed914c6907 | ||
|
73e81a54b4 | ||
|
ab8c66a361 | ||
|
4b55fefcb1 | ||
|
0eac8b25e1 | ||
|
aec556ff7d | ||
|
110dc29bd4 | ||
|
b1efea1bd9 | ||
|
8671467ed3 | ||
|
7851b6b429 | ||
|
8eb5f8b74e | ||
|
1830ee9fd2 | ||
|
dd615d6a8f | ||
|
08a518db70 | ||
|
581372de0b | ||
|
fe2c1c197e | ||
|
9854fddeb2 | ||
|
6c3918ebd2 | ||
|
4af496632f | ||
|
485ef1f2c2 | ||
|
cd1e7a1083 | ||
|
4dce9e9e47 | ||
|
dbeff4fd7d | ||
|
aef2e4845d | ||
|
739c6be476 | ||
|
ff502670bf | ||
|
3894dd1191 | ||
|
818537933a | ||
|
ec50808ba1 | ||
|
33fd8325be | ||
|
c72e0a14c0 | ||
|
58bc708014 | ||
|
ede1ed8b22 | ||
|
22e39b4434 | ||
|
9d984596cc | ||
|
fe909f659d | ||
|
ea120eae91 | ||
|
fa819d25b0 | ||
|
65739a5077 | ||
|
228d06e27f | ||
|
d386b83c53 | ||
|
e1b1f31cf1 | ||
|
f80b403dfe | ||
|
5af55d7da3 | ||
|
5e6a389f97 | ||
|
6f26910243 | ||
|
b2056f9e4d | ||
|
dca4bb204b | ||
|
ade39837de | ||
|
2ef2296566 | ||
|
04ed914894 | ||
|
332dd0d859 | ||
|
d9a1f7855d | ||
|
3ec59a835c | ||
|
620225bb2d | ||
|
9d7d469908 | ||
|
16bf0cf646 | ||
|
8079106c3e | ||
|
28143db968 | ||
|
7f1a14fa22 | ||
|
4de1edda05 | ||
|
52f09d2638 | ||
|
97b8cad755 | ||
|
52cb10dd4e | ||
|
135a8a9273 | ||
|
a346116d5f | ||
|
934c3c8001 | ||
|
565c17df37 | ||
|
5ee9a35f7d | ||
|
bd73107853 | ||
|
2e8fe88f53 | ||
|
444c96d5e7 | ||
|
f5ff68b236 | ||
|
d1a83d065a | ||
|
ddb73c8642 | ||
|
4df73cd82c | ||
|
c446e4471d | ||
|
fa22d5bae3 | ||
|
1a610054d3 | ||
|
11517fa037 | ||
|
67a457b9c5 | ||
|
18dc6c826a | ||
|
033d356b56 | ||
|
f9f5974267 | ||
|
6e23fead59 | ||
|
7df6541902 | ||
|
9a40851c77 | ||
|
748acdf03f | ||
|
13d27357a0 | ||
|
c72caad764 | ||
|
2933d09366 | ||
|
f51ad26db7 | ||
|
50ca686727 | ||
|
3612529e52 | ||
|
c053146885 | ||
|
8ccf2ec521 | ||
|
0428917ea2 | ||
|
914a2554ab | ||
|
26bb7dc33b | ||
|
8e5b0eadd9 | ||
|
a96b91cfef | ||
|
03251d4c40 | ||
|
112609c5f1 | ||
|
cc7cbc2ecf | ||
|
946ad7c71a | ||
|
60d2b0c763 | ||
|
d7f3cfd778 | ||
|
a2ae2c766a | ||
|
f52ff730b1 | ||
|
9327a61a36 | ||
|
05997908e5 | ||
|
2a93dea378 | ||
|
fbf7b2cc43 | ||
|
d8b19f7d08 | ||
|
acc4eb5d28 | ||
|
bcd5cd9110 | ||
|
45a4034816 | ||
|
978a08fb06 | ||
|
72dc5a6c99 | ||
|
5340fb8af9 | ||
|
3ce1172c36 | ||
|
e4d48f43d6 | ||
|
3e1ea0eb0a | ||
|
d10d6ba7de | ||
|
cf832354a2 | ||
|
b27519b85a | ||
|
27af0e69ef | ||
|
6283bf6190 | ||
|
48b648e656 | ||
|
367a18db93 | ||
|
f5feb595f6 | ||
|
c6ec0279fc | ||
|
b0b5488c2e | ||
|
3722012da5 | ||
|
c5214b6c44 | ||
|
585ccc2b8b | ||
|
ab8115b89d | ||
|
8d8395c226 | ||
|
8dbec0e8f4 | ||
|
1c1228bb88 | ||
|
7d9eec262a | ||
|
a057fbf3bd | ||
|
518feadc3e | ||
|
b1e351ce77 | ||
|
c6cb2800d7 | ||
|
c31c7a8a67 | ||
|
3d6fe4c2cd | ||
|
da7034ae08 | ||
|
37de5fd4b3 | ||
|
e2cbef1538 | ||
|
4de044dc90 | ||
|
d5dc27d788 | ||
|
e80dd59da2 | ||
|
cbd2075156 | ||
|
4b5c1491d0 | ||
|
ea077852a1 | ||
|
ca20a2a1c2 | ||
|
37c0b6d24a | ||
|
0c14a42b28 | ||
|
f126e0a944 | ||
|
9519bb1218 | ||
|
716b1315a9 | ||
|
6f88c025f5 | ||
|
2f3458e207 | ||
|
bd265e5a9d | ||
|
9d5d197747 | ||
|
ba1a005fcd | ||
|
f09cec794b | ||
|
9619d451f9 | ||
|
240fbf1720 | ||
|
286722fda7 | ||
|
86b5f3582f | ||
|
69ae32a9af | ||
|
504c3d4c0d | ||
|
9e77f01088 | ||
|
409f2ca98d | ||
|
2bfaa0e02c | ||
|
209f2cae55 | ||
|
ce9da90846 | ||
|
43bc7b5619 | ||
|
056741e2c2 | ||
|
01ba0c3078 | ||
|
7d58763fe9 | ||
|
4bb9be8f78 | ||
|
092c09065f | ||
|
fcc2eb6354 | ||
|
94a800aa3c | ||
|
add69aa210 | ||
|
5098c03264 | ||
|
2724167028 | ||
|
eacd0e5cfd | ||
|
e799062059 | ||
|
96a2aa6ad8 | ||
|
82674e8bdb | ||
|
6404d3520d | ||
|
667f5c0cfe | ||
|
fca504d552 | ||
|
af78721568 | ||
|
9a10984e84 | ||
|
462a676442 | ||
|
a2a714461c | ||
|
dfc5845025 | ||
|
e382231d7d | ||
|
2bca9af9a7 | ||
|
8b7ed05d7b | ||
|
bc80405f13 | ||
|
b4b9e285eb | ||
|
35b9835fd5 | ||
|
11ad6c6bef | ||
|
c838548831 | ||
|
dbb5599b0f | ||
|
bf916d5b9b | ||
|
9c610963b2 | ||
|
7b1e913651 | ||
|
e8d7050279 | ||
|
0e01011afc | ||
|
01d3bf8a11 | ||
|
cfa4da39e5 | ||
|
cf233e8e67 | ||
|
b5f1f87839 | ||
|
48061457ac | ||
|
395804ec0d | ||
|
ba3e357413 | ||
|
63c3636151 | ||
|
4dc4c6cf26 | ||
|
015c1f6084 | ||
|
9945850aa7 | ||
|
4868adf18a | ||
|
e2ad0be555 | ||
|
8cf4ae6533 | ||
|
a50aacddda | ||
|
f38069a47d | ||
|
b5f663de8b | ||
|
4fd62bf7bf | ||
|
e4ea21271f | ||
|
57575b7f13 | ||
|
3ef95ee868 | ||
|
6149ca575e | ||
|
7e19f99bbb | ||
|
96922aa20a | ||
|
b68dcaa526 | ||
|
f4f265b1af | ||
|
e74e717e2a | ||
|
e378e03e5e | ||
|
afc42061d5 | ||
|
47096edd11 | ||
|
04a64368c0 | ||
|
2bb0b00136 | ||
|
7177ca21c9 | ||
|
2cf5234e76 | ||
|
71a97bd45b | ||
|
be77f15ef7 | ||
|
f8bcb56964 | ||
|
26d1c0d79d | ||
|
6e75c93f3c | ||
|
6d2a5a7b32 | ||
|
f9a06bb5b9 | ||
|
2879e616c1 | ||
|
e48d2ed64b | ||
|
6e9e3fc9bd | ||
|
eda3be066d | ||
|
e649670bfe | ||
|
6b3bda2941 | ||
|
61890d99ec | ||
|
49c11fb22e | ||
|
f543015714 | ||
|
769df5b818 | ||
|
43dff9362a | ||
|
2b1d5ff726 | ||
|
21f50a14c5 | ||
|
00084e398c | ||
|
9b86eeacb3 | ||
|
17309b0fac | ||
|
bbb9d8d492 | ||
|
e2cfd8a5af | ||
|
5ddd1fc1b2 | ||
|
fea29b95de | ||
|
d4a0f7791a | ||
|
0806530fd9 | ||
|
e6f9e2663d | ||
|
26deb2ceb4 | ||
|
a2219bedd7 | ||
|
e3554c7aec | ||
|
7d12da27ad | ||
|
b9194e94aa | ||
|
5a7fde7d21 | ||
|
8bf85b6b88 | ||
|
9e3af21e66 | ||
|
dd4a640480 | ||
|
5024dcf668 | ||
|
26d9b57923 | ||
|
c1cb020283 | ||
|
8dd41dca96 | ||
|
d763b13e44 | ||
|
7f625051ba | ||
|
100b98bff8 | ||
|
3de7534d1d | ||
|
09db5ff1d8 | ||
|
cc5bd12ba1 | ||
|
dbf51382c8 | ||
|
cc277018b7 | ||
|
ec259f866f | ||
|
18bb02244f | ||
|
243d6e9845 | ||
|
d35bd7906b | ||
|
056c0dbab7 | ||
|
b10b5566f7 | ||
|
ddf9f442e5 | ||
|
6e4effdecf | ||
|
f1e1db6744 | ||
|
4f831f91f9 | ||
|
d601ed737b | ||
|
1e8325be9d | ||
|
28dcdc8984 | ||
|
86ea826e18 | ||
|
a1cb172bc3 | ||
|
b7e74e4547 | ||
|
8a78ee5df6 | ||
|
7781fbad76 | ||
|
1c144c9674 | ||
|
742cff5598 | ||
|
7ac6f39658 |
10
.gitignore
vendored
Executable file → Normal file
@@ -32,4 +32,12 @@ INFO.md
|
||||
sync_staging.sh
|
||||
*.swp
|
||||
_vieux/
|
||||
_mydocs
|
||||
_mydocs
|
||||
.DS_Store
|
||||
Assets/DownloadBadges*.psd
|
||||
node_modules
|
||||
Tools/github_oauth_token.txt
|
||||
_releases
|
||||
ReactNativeClient/lib/csstojs/
|
||||
ElectronClient/app/gui/note-viewer/fonts/
|
||||
Tools/commit_hook.txt
|
61
.travis.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
# Only build tags (Doesn't work - doesn't build anything)
|
||||
if: tag IS present
|
||||
|
||||
rvm: 2.3.3
|
||||
|
||||
# It's important to only build production branches otherwise Electron Builder
|
||||
# might take assets from dev branches and overwrite those of production.
|
||||
# https://docs.travis-ci.com/user/customizing-the-build/#Building-Specific-Branches
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^v\d+\.\d+(\.\d+)?(-\S*)?$/
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
osx_image: xcode9.0
|
||||
language: node_js
|
||||
node_js: "8"
|
||||
env:
|
||||
- ELECTRON_CACHE=$HOME/.cache/electron
|
||||
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
|
||||
|
||||
- os: linux
|
||||
sudo: required
|
||||
dist: trusty
|
||||
language: node_js
|
||||
node_js: "8"
|
||||
env:
|
||||
- ELECTRON_CACHE=$HOME/.cache/electron
|
||||
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
|
||||
|
||||
# cache:
|
||||
# directories:
|
||||
# - node_modules
|
||||
# - $HOME/.cache/electron
|
||||
# - $HOME/.cache/electron-builder
|
||||
|
||||
before_install:
|
||||
# HOMEBREW_NO_AUTO_UPDATE needed so that Homebrew doesn't upgrade to the next
|
||||
# version, which requires Ruby 2.3, which is not available on the Travis VM.
|
||||
|
||||
# Silence apt-get update errors (for example when a module doesn't exist) since
|
||||
# otherwise it will make the whole build fails, even though all we need is yarn.
|
||||
- |
|
||||
if [ "$TRAVIS_OS_NAME" == "osx" ]; then
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 brew install yarn
|
||||
else
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y yarn
|
||||
fi
|
||||
|
||||
script:
|
||||
- |
|
||||
cd Tools
|
||||
npm install
|
||||
cd ../ElectronClient/app
|
||||
rsync -aP --delete ../../ReactNativeClient/lib/ lib/
|
||||
npm install && yarn dist
|
Before Width: | Height: | Size: 79 KiB |
BIN
Assets/Icon.psd
BIN
Assets/Joplin.ico
Normal file
After Width: | Height: | Size: 38 KiB |
62
Assets/JoplinLetter.svg
Normal file
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="116.54575mm"
|
||||
height="131.19589mm"
|
||||
viewBox="0 0 116.54575 131.19589"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
|
||||
sodipodi:docname="JoplinLetter.svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.49497475"
|
||||
inkscape:cx="152.11122"
|
||||
inkscape:cy="-26.090631"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1017"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-2.7903623,-2.175533)">
|
||||
<path
|
||||
style="fill:#000000;stroke-width:0.26458332"
|
||||
d="m 43.790458,133.13317 c -8.32317,-1.11843 -12.937,-2.40956 -18.46857,-5.16822 -10.21924,-5.09644 -18.1023498,-13.95338 -21.1745998,-23.79038 -1.22214,-3.91319 -1.3607,-4.872332 -1.35685,-9.392712 0.003,-3.72804 0.0907,-4.66941 0.59927,-6.44569 1.0664,-3.7246 2.49409,-6.1704 5.19529,-8.90014 3.2574198,-3.29184 6.6565798,-4.77332 11.3929598,-4.96548 4.53189,-0.18388 7.54661,0.59927 10.40386,2.70266 1.82035,1.34007 3.67693,3.96421 4.71565,6.66525 0.65839,1.71204 0.70959,2.1839 0.90042,8.29756 0.19973,6.39855 0.36372,7.6318 1.39223,10.469902 1.40468,3.87611 3.78939,6.56189 7.33039,8.25588 3.20047,1.53108 5.63801,2.00183 9.60817,1.8556 2.58182,-0.0951 3.60332,-0.25442 5.15337,-0.80371 4.61358,-1.63493 8.46322,-5.31381 10.31326,-9.85579 1.91154,-4.693002 1.90785,-4.609372 1.90213,-43.127082 -0.005,-33.78395 -0.0106,-34.14337 -0.54484,-35.32188 -1.30698,-2.882895 -2.68223,-3.398165 -9.66971,-3.622945 l -5.12472,-0.16486 V 10.998334 2.175533 l 31.41927,0.06723 31.419272,0.06723 0.0697,8.755726 0.0697,8.755724 -5.09675,0.1793 c -2.82759,0.0995 -5.60596,0.33101 -6.24051,0.52006 -1.72896,0.5151 -2.82899,1.538795 -3.52569,3.281045 l -0.61059,1.5269 -0.16762,34.7927 c -0.16988,35.26321 -0.19381,36.08914 -1.18496,40.914372 -1.81292,8.82581 -8.301582,17.89221 -16.959672,23.69719 -6.95182,4.66099 -14.48972,7.21214 -24.82645,8.40235 -2.7431,0.31585 -14.57797,0.31433 -16.93333,-0.002 z"
|
||||
id="path21"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
BIN
Assets/LinuxIcons/1024x1024.png
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
Assets/LinuxIcons/128x128.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
BIN
Assets/LinuxIcons/16x16.png
Normal file
After Width: | Height: | Size: 679 B |
BIN
Assets/LinuxIcons/24x24.png
Normal file
After Width: | Height: | Size: 968 B |
BIN
Assets/LinuxIcons/256x256.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
Assets/LinuxIcons/32x32.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 151 KiB |
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 170 KiB |
BIN
Assets/Screenshots/Screenshot_1511192771.png
Normal file
After Width: | Height: | Size: 205 KiB |
BIN
Assets/Screenshots/Screenshot_1511192805.png
Normal file
After Width: | Height: | Size: 300 KiB |
BIN
Assets/Screenshots/Screenshot_1511193130.png
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
Assets/Screenshots/Screenshot_1511193142.png
Normal file
After Width: | Height: | Size: 277 KiB |
BIN
Assets/Screenshots/Screenshot_1511193169.png
Normal file
After Width: | Height: | Size: 133 KiB |
BIN
Assets/Screenshots/Screenshot_1511193181.png
Normal file
After Width: | Height: | Size: 214 KiB |
BIN
Assets/Screenshots/Screenshot_1511193188.png
Normal file
After Width: | Height: | Size: 181 KiB |
BIN
Assets/Screenshots/Screenshot_1511193192.png
Normal file
After Width: | Height: | Size: 133 KiB |
BIN
Assets/Screenshots/iOS/Screenshot_iPad_Paysage.jpg
Normal file
After Width: | Height: | Size: 288 KiB |
BIN
Assets/Screenshots/iOS/Screenshot_iPad_Paysage.png
Normal file
After Width: | Height: | Size: 335 KiB |
BIN
Assets/Screenshots/iOS/Screenshot_iPad_Paysage.psd
Normal file
BIN
Assets/Screenshots/iOS/Screenshot_iPhone_Portrait.jpg
Normal file
After Width: | Height: | Size: 226 KiB |
BIN
Assets/Screenshots/iOS/Screenshot_iPhone_Portrait.png
Normal file
After Width: | Height: | Size: 254 KiB |
BIN
Assets/Screenshots/iOS/Screenshot_iPhone_Portrait.psd
Normal file
BIN
Assets/iOSIcons/29.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
Assets/iOSIcons/29x2.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
Assets/iOSIcons/29x3.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
Assets/iOSIcons/40.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
Assets/iOSIcons/40x2.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
Assets/iOSIcons/40x3.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
Assets/iOSIcons/57.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
Assets/iOSIcons/57x2.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
Assets/iOSIcons/60x2.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
Assets/iOSIcons/60x3.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
Assets/iOSIcons/AppStore.png
Normal file
After Width: | Height: | Size: 74 KiB |
BIN
Assets/macOs.icns
Normal file
BIN
Assets/macOs.iconset/icon_1024x1024.png
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
Assets/macOs.iconset/icon_128x128.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
Assets/macOs.iconset/icon_16x16.png
Normal file
After Width: | Height: | Size: 679 B |
BIN
Assets/macOs.iconset/icon_256x256.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
Assets/macOs.iconset/icon_32x32.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
Assets/macOs.iconset/icon_512x512.png
Normal file
After Width: | Height: | Size: 28 KiB |
78
BUILD.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# General information
|
||||
|
||||
- All the applications share the same library, which, for historical reasons, is in ReactNativeClient/lib. This library is copied to the relevant directories when building each app.
|
||||
- The translations are built by running CliClient/build-translation.sh. You normally don't need to run this if you haven't updated the translation since the compiled files are on the repository.
|
||||
|
||||
## macOS dependencies
|
||||
|
||||
brew install yarn node
|
||||
echo 'export PATH="/usr/local/opt/gettext/bin:$PATH"' >> ~/.bash_profile
|
||||
source ~/.bash_profile
|
||||
|
||||
## Linux and Windows (WSL) dependencies
|
||||
|
||||
- Install yarn - https://yarnpkg.com/lang/en/docs/install/
|
||||
- Install node v8.x (check with `node --version`) - https://nodejs.org/en/
|
||||
- If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
|
||||
|
||||
# Building the tools
|
||||
|
||||
Before building any of the applications, you need to build the tools:
|
||||
|
||||
```
|
||||
cd Tools
|
||||
npm install
|
||||
```
|
||||
|
||||
# Building the Electron application
|
||||
|
||||
```
|
||||
cd ElectronClient/app
|
||||
rsync --delete -a ../../ReactNativeClient/lib/ lib/
|
||||
npm install
|
||||
yarn dist
|
||||
```
|
||||
|
||||
If there's an error `while loading shared libraries: libgconf-2.so.4: cannot open shared object file: No such file or directory`, run `sudo apt-get install libgconf-2-4`
|
||||
|
||||
If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`.
|
||||
|
||||
If you get the error `libtool: unrecognized option '-static'`, follow the instructions [in this post](https://stackoverflow.com/a/38552393/561309) to use the correct libtool version.
|
||||
|
||||
That will create the executable file in the `dist` directory.
|
||||
|
||||
From `/ElectronClient` you can also run `run.sh` to run the app for testing.
|
||||
|
||||
## Building Electron application on Windows
|
||||
|
||||
```
|
||||
cd Tools
|
||||
npm install
|
||||
cd ..\ElectronClient\app
|
||||
xcopy /C /I /H /R /Y /S ..\..\ReactNativeClient\lib lib
|
||||
npm install
|
||||
yarn dist
|
||||
```
|
||||
|
||||
If node-gyp does not works (MSBUILD: error MSB3428: Could not load the Visual C++ component "VCBuild.exe"), you might need to install the `windows-build-tools` using `npm install --global windows-build-tools`.
|
||||
|
||||
If `yarn dist` fails, it may need administrative rights.
|
||||
|
||||
The [building\_win32\_tips on this page](./readme/building_win32_tips.md) might be helpful.
|
||||
|
||||
# Building the Mobile application
|
||||
|
||||
First you need to setup React Native to build projects with native code. For this, follow the instructions on the [Get Started](https://facebook.github.io/react-native/docs/getting-started.html) tutorial, in the "Building Projects with Native Code" tab.
|
||||
|
||||
Then, from `/ReactNativeClient`, run `npm install`, then `react-native run-ios` or `react-native run-android`.
|
||||
|
||||
# Building the Terminal application
|
||||
|
||||
```
|
||||
cd CliClient
|
||||
npm install
|
||||
./build.sh
|
||||
rsync --delete -aP ../ReactNativeClient/locales/ build/locales/
|
||||
```
|
||||
|
||||
Run `run.sh` to start the application for testing.
|
28
CONTRIBUTING.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# User support
|
||||
|
||||
For general discussion about Joplin, user support, software development questions, and to discuss new features, please go to the [Joplin Forum](https://discourse.joplin.cozic.net/). It is possible to login with your GitHub account.
|
||||
|
||||
# Reporting a bug
|
||||
|
||||
Please check first that it [has not already been reported](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). Also consider [enabling debug mode](https://github.com/laurent22/joplin/blob/master/readme/debugging.md) before reporting the issue so that you can provide as much details as possible to help fix it.
|
||||
|
||||
If possible, **please provide a screenshot**. A screenshot showing the problem is often more useful than a paragraph describing it as it can make it immediately clear what the issue is.
|
||||
|
||||
# Feature requests
|
||||
|
||||
Again, please check that it has not already been requested. If it has, simply **up-vote the issue** - the ones with the most up-votes are likely to be implemented. "+1" comments are not tracked.
|
||||
|
||||
# Adding new features
|
||||
|
||||
If you want to add a new feature, consider asking about it before implementing it or checking existing discussions to make sure it is within the scope of the project. Of course you are free to create the pull request directly but it is not guaranteed it is going to be accepted.
|
||||
|
||||
Building the apps is relatively easy - please [see the build instructions](https://github.com/laurent22/joplin/blob/master/BUILD.md) for more details.
|
||||
|
||||
Pull requests that automatically change many files tend to be rejected (for example changes that automatically update the code styling, or to add new lines to many files, or to automatically convert images to a different format) so if you have such a pull request in mind, please discuss it first in the forum.
|
||||
|
||||
# Coding style
|
||||
|
||||
There are only two rules, but not following them means the pull request will not be accepted (it can be accepted once the issues are fixed):
|
||||
|
||||
- **Please use tabs, NOT spaces.**
|
||||
- **Please do not add or remove optional characters, such as spaces or colons.** Please setup your editor so that it only changes what you are working on and is not making automated changes elsewhere. The reason for this is that small white space changes make diff hard to read and can cause needless conflicts.
|
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"presets": ["env", "react"],
|
||||
"plugins": ["syntax-async-functions","transform-runtime"]
|
||||
}
|
4
CliClient/.gitignore
vendored
@@ -13,9 +13,11 @@ tests/fuzzing.*
|
||||
tests/fuzzing -*
|
||||
tests/logs/*
|
||||
tests/cli-integration/
|
||||
tests/tmp/
|
||||
*.mo
|
||||
*.*~
|
||||
tests/sync
|
||||
out.txt
|
||||
linkToLocal.sh
|
||||
yarn-error.log
|
||||
yarn-error.log
|
||||
tests/support/dropbox-auth.txt
|
@@ -1,7 +1,7 @@
|
||||
import { _ } from 'lib/locale.js'
|
||||
import { Logger } from 'lib/logger.js';
|
||||
import { Resource } from 'lib/models/resource.js';
|
||||
import { netUtils } from 'lib/net-utils.js'
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const { netUtils } = require('lib/net-utils.js');
|
||||
|
||||
const http = require("http");
|
||||
const urlParser = require("url");
|
||||
|
@@ -1,18 +1,23 @@
|
||||
import { Logger } from 'lib/logger.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { Tag } from 'lib/models/tag.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
import { Resource } from 'lib/models/resource.js';
|
||||
import { cliUtils } from './cli-utils.js';
|
||||
import { reducer, defaultState } from 'lib/reducer.js';
|
||||
import { reg } from 'lib/registry.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
const { reducer, defaultState } = require('lib/reducer.js');
|
||||
const { splitCommandString } = require('lib/string-utils.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Entities = require('html-entities').AllHtmlEntities;
|
||||
const htmlentities = (new Entities()).encode;
|
||||
|
||||
const chalk = require('chalk');
|
||||
const tk = require('terminal-kit');
|
||||
const TermWrapper = require('tkwidgets/framework/TermWrapper.js');
|
||||
const Renderer = require('tkwidgets/framework/Renderer.js');
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||
|
||||
const BaseWidget = require('tkwidgets/BaseWidget.js');
|
||||
const ListWidget = require('tkwidgets/ListWidget.js');
|
||||
@@ -33,37 +38,55 @@ const ConsoleWidget = require('./gui/ConsoleWidget.js');
|
||||
|
||||
class AppGui {
|
||||
|
||||
constructor(app, store) {
|
||||
this.app_ = app;
|
||||
this.store_ = store;
|
||||
constructor(app, store, keymap) {
|
||||
try {
|
||||
this.app_ = app;
|
||||
this.store_ = store;
|
||||
|
||||
BaseWidget.setLogger(app.logger());
|
||||
BaseWidget.setLogger(app.logger());
|
||||
|
||||
this.term_ = new TermWrapper(tk.terminal);
|
||||
this.term_ = new TermWrapper(tk.terminal);
|
||||
|
||||
this.renderer_ = null;
|
||||
this.logger_ = new Logger();
|
||||
this.buildUi();
|
||||
// Some keys are directly handled by the tkwidget framework
|
||||
// so they need to be remapped in a different way.
|
||||
this.tkWidgetKeys_ = {
|
||||
'focus_next': 'TAB',
|
||||
'focus_previous': 'SHIFT_TAB',
|
||||
'move_up': 'UP',
|
||||
'move_down': 'DOWN',
|
||||
'page_down': 'PAGE_DOWN',
|
||||
'page_up': 'PAGE_UP',
|
||||
};
|
||||
|
||||
this.renderer_ = new Renderer(this.term(), this.rootWidget_);
|
||||
this.renderer_ = null;
|
||||
this.logger_ = new Logger();
|
||||
this.buildUi();
|
||||
|
||||
this.app_.on('modelAction', async (event) => {
|
||||
await this.handleModelAction(event.action);
|
||||
});
|
||||
this.renderer_ = new Renderer(this.term(), this.rootWidget_);
|
||||
|
||||
this.shortcuts_ = this.setupShortcuts();
|
||||
this.app_.on('modelAction', async (event) => {
|
||||
await this.handleModelAction(event.action);
|
||||
});
|
||||
|
||||
this.inputMode_ = AppGui.INPUT_MODE_NORMAL;
|
||||
this.keymap_ = this.setupKeymap(keymap);
|
||||
|
||||
this.commandCancelCalled_ = false;
|
||||
this.inputMode_ = AppGui.INPUT_MODE_NORMAL;
|
||||
|
||||
this.currentShortcutKeys_ = [];
|
||||
this.lastShortcutKeyTime_ = 0;
|
||||
this.commandCancelCalled_ = false;
|
||||
|
||||
// Recurrent sync is setup only when the GUI is started. In
|
||||
// a regular command it's not necessary since the process
|
||||
// exits right away.
|
||||
reg.setupRecurrentSync();
|
||||
this.currentShortcutKeys_ = [];
|
||||
this.lastShortcutKeyTime_ = 0;
|
||||
|
||||
// Recurrent sync is setup only when the GUI is started. In
|
||||
// a regular command it's not necessary since the process
|
||||
// exits right away.
|
||||
reg.setupRecurrentSync();
|
||||
DecryptionWorker.instance().scheduleStart();
|
||||
} catch (error) {
|
||||
this.fullScreen(false);
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
store() {
|
||||
@@ -79,8 +102,16 @@ class AppGui {
|
||||
await this.renderer_.renderRoot();
|
||||
}
|
||||
|
||||
prompt(initialText = '', promptString = ':') {
|
||||
return this.widget('statusBar').prompt(initialText, promptString);
|
||||
termSaveState() {
|
||||
return this.term().saveState();
|
||||
}
|
||||
|
||||
termRestoreState(state) {
|
||||
return this.term().restoreState(state);
|
||||
}
|
||||
|
||||
prompt(initialText = '', promptString = ':', options = null) {
|
||||
return this.widget('statusBar').prompt(initialText, promptString, options);
|
||||
}
|
||||
|
||||
stdoutMaxWidth() {
|
||||
@@ -94,6 +125,7 @@ class AppGui {
|
||||
buildUi() {
|
||||
this.rootWidget_ = new ReduxRootWidget(this.store_);
|
||||
this.rootWidget_.name = 'root';
|
||||
this.rootWidget_.autoShortcutsEnabled = false;
|
||||
|
||||
const folderList = new FolderListWidget();
|
||||
folderList.style = {
|
||||
@@ -112,8 +144,8 @@ class AppGui {
|
||||
|
||||
if (!nextItem) return; // Normally not possible
|
||||
|
||||
let actionType = 'FOLDERS_SELECT';
|
||||
if (nextItem.type_ === BaseModel.TYPE_TAG) actionType = 'TAGS_SELECT';
|
||||
let actionType = 'FOLDER_SELECT';
|
||||
if (nextItem.type_ === BaseModel.TYPE_TAG) actionType = 'TAG_SELECT';
|
||||
if (nextItem.type_ === BaseModel.TYPE_SEARCH) actionType = 'SEARCH_SELECT';
|
||||
|
||||
this.store_.dispatch({
|
||||
@@ -122,12 +154,12 @@ class AppGui {
|
||||
});
|
||||
} else if (item.type_ === Folder.modelType()) {
|
||||
this.store_.dispatch({
|
||||
type: 'FOLDERS_SELECT',
|
||||
type: 'FOLDER_SELECT',
|
||||
id: item ? item.id : null,
|
||||
});
|
||||
} else if (item.type_ === Tag.modelType()) {
|
||||
this.store_.dispatch({
|
||||
type: 'TAGS_SELECT',
|
||||
type: 'TAG_SELECT',
|
||||
id: item ? item.id : null,
|
||||
});
|
||||
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
|
||||
@@ -160,13 +192,13 @@ class AppGui {
|
||||
noteList.on('currentItemChange', async () => {
|
||||
let note = noteList.currentItem;
|
||||
this.store_.dispatch({
|
||||
type: 'NOTES_SELECT',
|
||||
noteId: note ? note.id : null,
|
||||
type: 'NOTE_SELECT',
|
||||
id: note ? note.id : null,
|
||||
});
|
||||
});
|
||||
this.rootWidget_.connect(noteList, (state) => {
|
||||
return {
|
||||
selectedNoteId: state.selectedNoteId,
|
||||
selectedNoteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
|
||||
items: state.notes,
|
||||
};
|
||||
});
|
||||
@@ -180,7 +212,7 @@ class AppGui {
|
||||
};
|
||||
this.rootWidget_.connect(noteText, (state) => {
|
||||
return {
|
||||
noteId: state.selectedNoteId,
|
||||
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
|
||||
notes: state.notes,
|
||||
};
|
||||
});
|
||||
@@ -194,7 +226,7 @@ class AppGui {
|
||||
borderRightWidth: 1,
|
||||
};
|
||||
this.rootWidget_.connect(noteMetadata, (state) => {
|
||||
return { noteId: state.selectedNoteId };
|
||||
return { noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null };
|
||||
});
|
||||
noteMetadata.hide();
|
||||
|
||||
@@ -258,148 +290,31 @@ class AppGui {
|
||||
|
||||
addCommandToConsole(cmd) {
|
||||
if (!cmd) return;
|
||||
const isConfigPassword = cmd.indexOf('config ') >= 0 && cmd.indexOf('password') >= 0;
|
||||
if (isConfigPassword) return;
|
||||
this.stdout(chalk.cyan.bold('> ' + cmd));
|
||||
}
|
||||
|
||||
setupShortcuts() {
|
||||
const shortcuts = {};
|
||||
setupKeymap(keymap) {
|
||||
const output = [];
|
||||
|
||||
shortcuts['TAB'] = {
|
||||
friendlyName: 'Tab',
|
||||
description: () => _('Give focus to next pane'),
|
||||
isDocOnly: true,
|
||||
}
|
||||
for (let i = 0; i < keymap.length; i++) {
|
||||
const item = Object.assign({}, keymap[i]);
|
||||
|
||||
shortcuts['SHIFT_TAB'] = {
|
||||
friendlyName: 'Shift+Tab',
|
||||
description: () => _('Give focus to previous pane'),
|
||||
isDocOnly: true,
|
||||
}
|
||||
if (!item.command) throw new Error('Missing command for keymap item: ' + JSON.stringify(item));
|
||||
|
||||
shortcuts[':'] = {
|
||||
description: () => _('Enter command line mode'),
|
||||
action: async () => {
|
||||
const cmd = await this.widget('statusBar').prompt();
|
||||
if (!cmd) return;
|
||||
this.addCommandToConsole(cmd);
|
||||
await this.processCommand(cmd);
|
||||
},
|
||||
};
|
||||
if (!('type' in item)) item.type = 'exec';
|
||||
|
||||
shortcuts['ESC'] = { // Built into terminal-kit inputField
|
||||
description: () => _('Exit command line mode'),
|
||||
isDocOnly: true,
|
||||
};
|
||||
|
||||
shortcuts['ENTER'] = {
|
||||
description: () => _('Edit the selected note'),
|
||||
action: () => {
|
||||
const w = this.widget('mainWindow').focusedWidget;
|
||||
if (w.name === 'folderList') {
|
||||
this.widget('noteList').focus();
|
||||
} else if (w.name === 'noteList' || w.name === 'noteText') {
|
||||
this.processCommand('edit $n');
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
shortcuts['CTRL_C'] = {
|
||||
description: () => _('Cancel the current command.'),
|
||||
friendlyName: 'Ctrl+C',
|
||||
isDocOnly: true,
|
||||
}
|
||||
|
||||
shortcuts['CTRL_D'] = {
|
||||
description: () => _('Exit the application.'),
|
||||
friendlyName: 'Ctrl+D',
|
||||
isDocOnly: true,
|
||||
}
|
||||
|
||||
shortcuts['DELETE'] = {
|
||||
description: () => _('Delete the currently selected note or notebook.'),
|
||||
action: async () => {
|
||||
if (this.widget('folderList').hasFocus) {
|
||||
const item = this.widget('folderList').selectedJoplinItem;
|
||||
if (item.type_ === BaseModel.TYPE_FOLDER) {
|
||||
await this.processCommand('rmbook ' + item.id);
|
||||
} else if (item.type_ === BaseModel.TYPE_TAG) {
|
||||
this.stdout(_('To delete a tag, untag the associated notes.'));
|
||||
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
|
||||
this.store().dispatch({
|
||||
type: 'SEARCH_REMOVE',
|
||||
id: item.id,
|
||||
});
|
||||
}
|
||||
} else if (this.widget('noteList').hasFocus) {
|
||||
await this.processCommand('rmnote $n');
|
||||
} else {
|
||||
this.stdout(_('Please select the note or notebook to be deleted first.'));
|
||||
}
|
||||
if (item.command in this.tkWidgetKeys_) {
|
||||
item.type = 'tkwidgets';
|
||||
}
|
||||
};
|
||||
|
||||
shortcuts[' '] = {
|
||||
friendlyName: 'SPACE',
|
||||
description: () => _('Set a to-do as completed / not completed'),
|
||||
action: 'todo toggle $n',
|
||||
item.canRunAlongOtherCommands = item.type === 'function' && ['toggle_metadata', 'toggle_console'].indexOf(item.command) >= 0;
|
||||
|
||||
output.push(item);
|
||||
}
|
||||
|
||||
shortcuts['tc'] = {
|
||||
description: () => _('[t]oggle [c]onsole between maximized/minimized/hidden/visible.'),
|
||||
action: () => {
|
||||
if (!this.consoleIsShown()) {
|
||||
this.showConsole();
|
||||
this.minimizeConsole();
|
||||
} else {
|
||||
if (this.consoleIsMaximized()) {
|
||||
this.hideConsole();
|
||||
} else {
|
||||
this.maximizeConsole();
|
||||
}
|
||||
}
|
||||
},
|
||||
canRunAlongOtherCommands: true,
|
||||
}
|
||||
|
||||
shortcuts['/'] = {
|
||||
description: () => _('Search'),
|
||||
action: { type: 'prompt', initialText: 'search ""', cursorPosition: -2 },
|
||||
};
|
||||
|
||||
shortcuts['tm'] = {
|
||||
description: () => _('[t]oggle note [m]etadata.'),
|
||||
action: () => {
|
||||
this.toggleNoteMetadata();
|
||||
},
|
||||
canRunAlongOtherCommands: true,
|
||||
}
|
||||
|
||||
shortcuts['mn'] = {
|
||||
description: () => _('[M]ake a new [n]ote'),
|
||||
action: { type: 'prompt', initialText: 'mknote ""', cursorPosition: -2 },
|
||||
}
|
||||
|
||||
shortcuts['mt'] = {
|
||||
description: () => _('[M]ake a new [t]odo'),
|
||||
action: { type: 'prompt', initialText: 'mktodo ""', cursorPosition: -2 },
|
||||
}
|
||||
|
||||
shortcuts['mb'] = {
|
||||
description: () => _('[M]ake a new note[b]ook'),
|
||||
action: { type: 'prompt', initialText: 'mkbook ""', cursorPosition: -2 },
|
||||
}
|
||||
|
||||
shortcuts['yn'] = {
|
||||
description: () => _('Copy ([Y]ank) the [n]ote to a notebook.'),
|
||||
action: { type: 'prompt', initialText: 'cp $n ""', cursorPosition: -2 },
|
||||
}
|
||||
|
||||
shortcuts['dn'] = {
|
||||
description: () => _('Move the note to a notebook.'),
|
||||
action: { type: 'prompt', initialText: 'mv $n ""', cursorPosition: -2 },
|
||||
}
|
||||
|
||||
return shortcuts;
|
||||
return output;
|
||||
}
|
||||
|
||||
toggleConsole() {
|
||||
@@ -474,8 +389,16 @@ class AppGui {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
shortcuts() {
|
||||
return this.shortcuts_;
|
||||
keymap() {
|
||||
return this.keymap_;
|
||||
}
|
||||
|
||||
keymapItemByKey(key) {
|
||||
for (let i = 0; i < this.keymap_.length; i++) {
|
||||
const item = this.keymap_[i];
|
||||
if (item.keys.indexOf(key) >= 0) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
term() {
|
||||
@@ -506,40 +429,104 @@ class AppGui {
|
||||
}
|
||||
}
|
||||
|
||||
async processCommand(cmd) {
|
||||
async processFunctionCommand(cmd) {
|
||||
|
||||
if (cmd === 'activate') {
|
||||
|
||||
const w = this.widget('mainWindow').focusedWidget;
|
||||
if (w.name === 'folderList') {
|
||||
this.widget('noteList').focus();
|
||||
} else if (w.name === 'noteList' || w.name === 'noteText') {
|
||||
this.processPromptCommand('edit $n');
|
||||
}
|
||||
|
||||
} else if (cmd === 'delete') {
|
||||
|
||||
if (this.widget('folderList').hasFocus) {
|
||||
const item = this.widget('folderList').selectedJoplinItem;
|
||||
|
||||
if (!item) return;
|
||||
|
||||
if (item.type_ === BaseModel.TYPE_FOLDER) {
|
||||
await this.processPromptCommand('rmbook ' + item.id);
|
||||
} else if (item.type_ === BaseModel.TYPE_TAG) {
|
||||
this.stdout(_('To delete a tag, untag the associated notes.'));
|
||||
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
|
||||
this.store().dispatch({
|
||||
type: 'SEARCH_DELETE',
|
||||
id: item.id,
|
||||
});
|
||||
}
|
||||
} else if (this.widget('noteList').hasFocus) {
|
||||
await this.processPromptCommand('rmnote $n');
|
||||
} else {
|
||||
this.stdout(_('Please select the note or notebook to be deleted first.'));
|
||||
}
|
||||
|
||||
} else if (cmd === 'toggle_console') {
|
||||
|
||||
if (!this.consoleIsShown()) {
|
||||
this.showConsole();
|
||||
this.minimizeConsole();
|
||||
} else {
|
||||
if (this.consoleIsMaximized()) {
|
||||
this.hideConsole();
|
||||
} else {
|
||||
this.maximizeConsole();
|
||||
}
|
||||
}
|
||||
|
||||
} else if (cmd === 'toggle_metadata') {
|
||||
|
||||
this.toggleNoteMetadata();
|
||||
|
||||
} else if (cmd === 'enter_command_line_mode') {
|
||||
|
||||
const cmd = await this.widget('statusBar').prompt();
|
||||
if (!cmd) return;
|
||||
this.addCommandToConsole(cmd);
|
||||
await this.processPromptCommand(cmd);
|
||||
|
||||
} else {
|
||||
|
||||
throw new Error('Unknown command: ' + cmd);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
async processPromptCommand(cmd) {
|
||||
if (!cmd) return;
|
||||
cmd = cmd.trim();
|
||||
if (!cmd.length) return;
|
||||
|
||||
this.logger().info('Got command: ' + cmd);
|
||||
// this.logger().debug('Got command: ' + cmd);
|
||||
|
||||
if (cmd === 'q' || cmd === 'wq' || cmd === 'qa') { // Vim bonus
|
||||
await this.app().exit();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let note = this.widget('noteList').currentItem;
|
||||
let folder = this.widget('folderList').currentItem;
|
||||
let args = splitCommandString(cmd);
|
||||
|
||||
let note = this.widget('noteList').currentItem;
|
||||
let folder = this.widget('folderList').currentItem;
|
||||
let args = cliUtils.splitCommandString(cmd);
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] == '$n') {
|
||||
args[i] = note ? note.id : '';
|
||||
} else if (args[i] == '$b') {
|
||||
args[i] = folder ? folder.id : '';
|
||||
} else if (args[i] == '$c') {
|
||||
const item = this.activeListItem();
|
||||
args[i] = item ? item.id : '';
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] == '$n') {
|
||||
args[i] = note ? note.id : '';
|
||||
} else if (args[i] == '$b') {
|
||||
args[i] = folder ? folder.id : '';
|
||||
} else if (args[i] == '$c') {
|
||||
const item = this.activeListItem();
|
||||
args[i] = item ? item.id : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.app().execCommand(args);
|
||||
} catch (error) {
|
||||
this.stdout(error.message);
|
||||
}
|
||||
|
||||
this.widget('console').scrollBottom();
|
||||
|
||||
// Invalidate so that the screen is redrawn in case inputting a command has moved
|
||||
// the GUI up (in particular due to autocompletion), it's moved back to the right position.
|
||||
this.widget('root').invalidate();
|
||||
}
|
||||
|
||||
async updateFolderList() {
|
||||
@@ -654,12 +641,27 @@ class AppGui {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (link.type === 'resource') {
|
||||
const resourceId = link.id;
|
||||
let resource = await Resource.load(resourceId);
|
||||
if (!resource) throw new Error('No resource with ID ' + resourceId); // Should be nearly impossible
|
||||
if (resource.mime) response.setHeader('Content-Type', resource.mime);
|
||||
response.write(await Resource.content(resource));
|
||||
if (link.type === 'item') {
|
||||
const itemId = link.id;
|
||||
let item = await BaseItem.loadItemById(itemId);
|
||||
if (!item) throw new Error('No item with ID ' + itemId); // Should be nearly impossible
|
||||
|
||||
if (item.type_ === BaseModel.TYPE_RESOURCE) {
|
||||
if (item.mime) response.setHeader('Content-Type', item.mime);
|
||||
response.write(await Resource.content(item));
|
||||
} else if (item.type_ === BaseModel.TYPE_NOTE) {
|
||||
const html = [`
|
||||
<!DOCTYPE html>
|
||||
<html class="client-nojs" lang="en" dir="ltr">
|
||||
<head><meta charset="UTF-8"/></head><body>
|
||||
`];
|
||||
html.push('<pre>' + htmlentities(item.title) + '\n\n' + htmlentities(item.body) + '</pre>');
|
||||
html.push('</body></html>');
|
||||
response.write(html.join(''));
|
||||
} else {
|
||||
throw new Error('Unsupported item type: ' + item.type_);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -675,7 +677,7 @@ class AppGui {
|
||||
|
||||
if (resourceIdRegex.test(url)) {
|
||||
noteLinks[index] = {
|
||||
type: 'resource',
|
||||
type: 'item',
|
||||
id: url.substr(2),
|
||||
};
|
||||
} else if (hasProtocol(url, ['http', 'https', 'file', 'ftp'])) {
|
||||
@@ -764,32 +766,34 @@ class AppGui {
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
const shortcutKey = this.currentShortcutKeys_.join('');
|
||||
const cmd = shortcutKey in this.shortcuts_ ? this.shortcuts_[shortcutKey] : null;
|
||||
let keymapItem = this.keymapItemByKey(shortcutKey);
|
||||
|
||||
let processShortcutKeys = !this.app().currentCommand() && cmd;
|
||||
if (cmd && cmd.canRunAlongOtherCommands) processShortcutKeys = true;
|
||||
// If this command is an alias to another command, resolve to the actual command
|
||||
|
||||
let processShortcutKeys = !this.app().currentCommand() && keymapItem;
|
||||
if (keymapItem && keymapItem.canRunAlongOtherCommands) processShortcutKeys = true;
|
||||
if (statusBar.promptActive) processShortcutKeys = false;
|
||||
if (cmd && cmd.isDocOnly) processShortcutKeys = false;
|
||||
|
||||
if (processShortcutKeys) {
|
||||
this.logger().info('Shortcut:', shortcutKey, cmd.description());
|
||||
this.logger().debug('Shortcut:', shortcutKey, keymapItem);
|
||||
|
||||
this.currentShortcutKeys_ = [];
|
||||
if (typeof cmd.action === 'function') {
|
||||
await cmd.action();
|
||||
} else if (typeof cmd.action === 'object') {
|
||||
if (cmd.action.type === 'prompt') {
|
||||
let promptOptions = {};
|
||||
if ('cursorPosition' in cmd.action) promptOptions.cursorPosition = cmd.action.cursorPosition;
|
||||
const commandString = await statusBar.prompt(cmd.action.initialText ? cmd.action.initialText : '', null, promptOptions);
|
||||
this.addCommandToConsole(commandString);
|
||||
await this.processCommand(commandString);
|
||||
} else {
|
||||
throw new Error('Unknown command: ' + JSON.stringify(cmd.action));
|
||||
}
|
||||
} else { // String
|
||||
this.stdout(cmd.action);
|
||||
await this.processCommand(cmd.action);
|
||||
|
||||
if (keymapItem.type === 'function') {
|
||||
this.processFunctionCommand(keymapItem.command);
|
||||
} else if (keymapItem.type === 'prompt') {
|
||||
let promptOptions = {};
|
||||
if ('cursorPosition' in keymapItem) promptOptions.cursorPosition = keymapItem.cursorPosition;
|
||||
const commandString = await statusBar.prompt(keymapItem.command ? keymapItem.command : '', null, promptOptions);
|
||||
this.addCommandToConsole(commandString);
|
||||
await this.processPromptCommand(commandString);
|
||||
} else if (keymapItem.type === 'exec') {
|
||||
this.stdout(keymapItem.command);
|
||||
await this.processPromptCommand(keymapItem.command);
|
||||
} else if (keymapItem.type === 'tkwidgets') {
|
||||
this.widget('root').handleKey(this.tkWidgetKeys_[keymapItem.command]);
|
||||
} else {
|
||||
throw new Error('Unknown command type: ' + JSON.stringify(keymapItem));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -815,4 +819,4 @@ class AppGui {
|
||||
AppGui.INPUT_MODE_NORMAL = 1;
|
||||
AppGui.INPUT_MODE_META = 2;
|
||||
|
||||
module.exports = AppGui;
|
||||
module.exports = AppGui;
|
||||
|
@@ -1,81 +1,51 @@
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
import { reducer, defaultState } from 'lib/reducer.js';
|
||||
import { JoplinDatabase } from 'lib/joplin-database.js';
|
||||
import { Database } from 'lib/database.js';
|
||||
import { FoldersScreenUtils } from 'lib/folders-screen-utils.js';
|
||||
import { DatabaseDriverNode } from 'lib/database-driver-node.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { BaseItem } from 'lib/models/base-item.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
import { Tag } from 'lib/models/tag.js';
|
||||
import { Setting } from 'lib/models/setting.js';
|
||||
import { Logger } from 'lib/logger.js';
|
||||
import { sprintf } from 'sprintf-js';
|
||||
import { reg } from 'lib/registry.js';
|
||||
import { fileExtension } from 'lib/path-utils.js';
|
||||
import { _, setLocale, defaultLocale, closestSupportedLocale } from 'lib/locale.js';
|
||||
import os from 'os';
|
||||
import fs from 'fs-extra';
|
||||
import { cliUtils } from './cli-utils.js';
|
||||
const EventEmitter = require('events');
|
||||
const { BaseApplication } = require('lib/BaseApplication');
|
||||
const { createStore, applyMiddleware } = require('redux');
|
||||
const { reducer, defaultState } = require('lib/reducer.js');
|
||||
const { JoplinDatabase } = require('lib/joplin-database.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
|
||||
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
|
||||
const ResourceService = require('lib/services/ResourceService');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { fileExtension } = require('lib/path-utils.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const { _, setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
|
||||
const os = require('os');
|
||||
const fs = require('fs-extra');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
const Cache = require('lib/Cache');
|
||||
|
||||
class Application {
|
||||
class Application extends BaseApplication {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.showPromptString_ = true;
|
||||
this.logger_ = new Logger();
|
||||
this.dbLogger_ = new Logger();
|
||||
this.commands_ = {};
|
||||
this.commandMetadata_ = null;
|
||||
this.activeCommand_ = null;
|
||||
this.allCommandsLoaded_ = false;
|
||||
this.showStackTraces_ = false;
|
||||
this.gui_ = null;
|
||||
this.eventEmitter_ = new EventEmitter();
|
||||
|
||||
// Note: this is basically a cache of state.selectedFolderId. It should *only*
|
||||
// be derived from the state and not set directly since that would make the
|
||||
// state and UI out of sync.
|
||||
this.currentFolder_ = null;
|
||||
this.cache_ = new Cache();
|
||||
}
|
||||
|
||||
gui() {
|
||||
return this.gui_;
|
||||
}
|
||||
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
store() {
|
||||
return this.store_;
|
||||
}
|
||||
|
||||
currentFolder() {
|
||||
return this.currentFolder_;
|
||||
}
|
||||
|
||||
commandStdoutMaxWidth() {
|
||||
return this.gui().stdoutMaxWidth();
|
||||
}
|
||||
|
||||
async refreshCurrentFolder() {
|
||||
let newFolder = null;
|
||||
|
||||
if (this.currentFolder_) newFolder = await Folder.load(this.currentFolder_.id);
|
||||
if (!newFolder) newFolder = await Folder.defaultFolder();
|
||||
|
||||
this.switchCurrentFolder(newFolder);
|
||||
}
|
||||
|
||||
switchCurrentFolder(folder) {
|
||||
this.dispatch({
|
||||
type: 'FOLDERS_SELECT',
|
||||
id: folder ? folder.id : '',
|
||||
});
|
||||
}
|
||||
|
||||
async guessTypeAndLoadItem(pattern, options = null) {
|
||||
let type = BaseModel.TYPE_NOTE;
|
||||
if (pattern.indexOf('/') === 0) {
|
||||
@@ -149,112 +119,6 @@ class Application {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Handles the initial flags passed to main script and
|
||||
// returns the remaining args.
|
||||
async handleStartFlags_(argv) {
|
||||
let matched = {};
|
||||
argv = argv.slice(0);
|
||||
argv.splice(0, 2); // First arguments are the node executable, and the node JS file
|
||||
|
||||
while (argv.length) {
|
||||
let arg = argv[0];
|
||||
let nextArg = argv.length >= 2 ? argv[1] : null;
|
||||
|
||||
if (arg == '--profile') {
|
||||
if (!nextArg) throw new Error(_('Usage: %s', '--profile <dir-path>'));
|
||||
matched.profileDir = nextArg;
|
||||
argv.splice(0, 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == '--env') {
|
||||
if (!nextArg) throw new Error(_('Usage: %s', '--env <dev|prod>'));
|
||||
matched.env = nextArg;
|
||||
argv.splice(0, 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == '--is-demo') {
|
||||
Setting.setConstant('isDemo', true);
|
||||
argv.splice(0, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == '--update-geolocation-disabled') {
|
||||
Note.updateGeolocationEnabled_ = false;
|
||||
argv.splice(0, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == '--stack-trace-enabled') {
|
||||
this.showStackTraces_ = true;
|
||||
argv.splice(0, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == '--log-level') {
|
||||
if (!nextArg) throw new Error(_('Usage: %s', '--log-level <none|error|warn|info|debug>'));
|
||||
matched.logLevel = Logger.levelStringToId(nextArg);
|
||||
argv.splice(0, 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.length && arg[0] == '-') {
|
||||
throw new Error(_('Unknown flag: %s', arg));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched.logLevel) matched.logLevel = Logger.LEVEL_INFO;
|
||||
if (!matched.env) matched.env = 'prod';
|
||||
|
||||
return {
|
||||
matched: matched,
|
||||
argv: argv,
|
||||
};
|
||||
}
|
||||
|
||||
escapeShellArg(arg) {
|
||||
if (arg.indexOf('"') >= 0 && arg.indexOf("'") >= 0) throw new Error(_('Command line argument "%s" contains both quotes and double-quotes - aborting.', arg)); // Hopeless case
|
||||
let quote = '"';
|
||||
if (arg.indexOf('"') >= 0) quote = "'";
|
||||
if (arg.indexOf(' ') >= 0 || arg.indexOf("\t") >= 0) return quote + arg + quote;
|
||||
return arg;
|
||||
}
|
||||
|
||||
shellArgsToString(args) {
|
||||
let output = [];
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
output.push(this.escapeShellArg(args[i]));
|
||||
}
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
onLocaleChanged() {
|
||||
return;
|
||||
|
||||
let currentCommands = this.vorpal().commands;
|
||||
for (let i = 0; i < currentCommands.length; i++) {
|
||||
let cmd = currentCommands[i];
|
||||
if (cmd._name == 'help') {
|
||||
cmd.description(_('Provides help for a given command.'));
|
||||
} else if (cmd._name == 'exit') {
|
||||
cmd.description(_('Exits the application.'));
|
||||
} else if (cmd.__commandObject) {
|
||||
cmd.description(cmd.__commandObject.description());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
baseModelListener(action) {
|
||||
this.eventEmitter_.emit('modelAction', { action: action });
|
||||
}
|
||||
|
||||
on(eventName, callback) {
|
||||
return this.eventEmitter_.on(eventName, callback);
|
||||
}
|
||||
|
||||
stdout(text) {
|
||||
return this.gui().stdout(text);
|
||||
}
|
||||
@@ -282,13 +146,15 @@ class Application {
|
||||
message += ' (' + options.answers.join('/') + ')';
|
||||
}
|
||||
|
||||
let answer = await this.gui().prompt('', message + ' ');
|
||||
let answer = await this.gui().prompt('', message + ' ', options);
|
||||
|
||||
if (options.type === 'boolean') {
|
||||
if (answer === null) return false; // Pressed ESCAPE
|
||||
if (!answer) answer = options.answers[0];
|
||||
let positiveIndex = options.booleanAnswerDefault == 'y' ? 0 : 1;
|
||||
return answer.toLowerCase() === options.answers[positiveIndex].toLowerCase();
|
||||
} else {
|
||||
return answer;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -297,9 +163,8 @@ class Application {
|
||||
|
||||
async exit(code = 0) {
|
||||
const doExit = async () => {
|
||||
await Setting.saveAll();
|
||||
this.gui().exit();
|
||||
process.exit(code);
|
||||
await super.exit(code);
|
||||
};
|
||||
|
||||
// Give it a few seconds to cancel otherwise exit anyway
|
||||
@@ -307,31 +172,42 @@ class Application {
|
||||
await doExit();
|
||||
}, 5000);
|
||||
|
||||
if (await reg.syncStarted()) {
|
||||
if (await reg.syncTarget().syncStarted()) {
|
||||
this.stdout(_('Cancelling background synchronisation... Please wait.'));
|
||||
const sync = await reg.synchronizer(Setting.value('sync.target'));
|
||||
const sync = await reg.syncTarget().synchronizer();
|
||||
await sync.cancel();
|
||||
}
|
||||
|
||||
await doExit();
|
||||
}
|
||||
|
||||
commands() {
|
||||
if (this.allCommandsLoaded_) return this.commands_;
|
||||
commands(uiType = null) {
|
||||
if (!this.allCommandsLoaded_) {
|
||||
fs.readdirSync(__dirname).forEach((path) => {
|
||||
if (path.indexOf('command-') !== 0) return;
|
||||
const ext = fileExtension(path)
|
||||
if (ext != 'js') return;
|
||||
|
||||
fs.readdirSync(__dirname).forEach((path) => {
|
||||
if (path.indexOf('command-') !== 0) return;
|
||||
const ext = fileExtension(path)
|
||||
if (ext != 'js') return;
|
||||
let CommandClass = require('./' + path);
|
||||
let cmd = new CommandClass();
|
||||
if (!cmd.enabled()) return;
|
||||
cmd = this.setupCommand(cmd);
|
||||
this.commands_[cmd.name()] = cmd;
|
||||
});
|
||||
|
||||
let CommandClass = require('./' + path);
|
||||
let cmd = new CommandClass();
|
||||
if (!cmd.enabled()) return;
|
||||
cmd = this.setupCommand(cmd);
|
||||
this.commands_[cmd.name()] = cmd;
|
||||
});
|
||||
this.allCommandsLoaded_ = true;
|
||||
}
|
||||
|
||||
this.allCommandsLoaded_ = true;
|
||||
if (uiType !== null) {
|
||||
let temp = [];
|
||||
for (let n in this.commands_) {
|
||||
if (!this.commands_.hasOwnProperty(n)) continue;
|
||||
const c = this.commands_[n];
|
||||
if (!c.supportsUi(uiType)) continue;
|
||||
temp[n] = c;
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
|
||||
return this.commands_;
|
||||
}
|
||||
@@ -349,12 +225,8 @@ class Application {
|
||||
async commandMetadata() {
|
||||
if (this.commandMetadata_) return this.commandMetadata_;
|
||||
|
||||
const osTmpdir = require('os-tmpdir');
|
||||
const storage = require('node-persist');
|
||||
await storage.init({ dir: osTmpdir() + '/commandMetadata', ttl: 1000 * 60 * 60 * 24 });
|
||||
|
||||
let output = await storage.getItem('metadata');
|
||||
if (Setting.value('env') != 'dev' && output) {
|
||||
let output = await this.cache_.getItem('metadata');
|
||||
if (output) {
|
||||
this.commandMetadata_ = output;
|
||||
return Object.assign({}, this.commandMetadata_);
|
||||
}
|
||||
@@ -368,12 +240,16 @@ class Application {
|
||||
output[n] = cmd.metadata();
|
||||
}
|
||||
|
||||
await storage.setItem('metadata', output);
|
||||
await this.cache_.setItem('metadata', output, 1000 * 60 * 60 * 24);
|
||||
|
||||
this.commandMetadata_ = output;
|
||||
return Object.assign({}, this.commandMetadata_);
|
||||
}
|
||||
|
||||
hasGui() {
|
||||
return this.gui() && !this.gui().isDummy();
|
||||
}
|
||||
|
||||
findCommandByName(name) {
|
||||
if (this.commands_[name]) return this.commands_[name];
|
||||
|
||||
@@ -381,9 +257,13 @@ class Application {
|
||||
try {
|
||||
CommandClass = require(__dirname + '/command-' + name + '.js');
|
||||
} catch (error) {
|
||||
let e = new Error('No such command: ' + name);
|
||||
e.type = 'notFound';
|
||||
throw e;
|
||||
if (error.message && error.message.indexOf('Cannot find module') >= 0) {
|
||||
let e = new Error(_('No such command: %s', name));
|
||||
e.type = 'notFound';
|
||||
throw e;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
let cmd = new CommandClass();
|
||||
@@ -395,7 +275,7 @@ class Application {
|
||||
dummyGui() {
|
||||
return {
|
||||
isDummy: () => { return true; },
|
||||
prompt: (initialText = '', promptString = '') => { return cliUtils.prompt(initialText, promptString); },
|
||||
prompt: (initialText = '', promptString = '', options = null) => { return cliUtils.prompt(initialText, promptString, options); },
|
||||
showConsole: () => {},
|
||||
maximizeConsole: () => {},
|
||||
stdout: (text) => { console.info(text); },
|
||||
@@ -403,13 +283,16 @@ class Application {
|
||||
exit: () => {},
|
||||
showModalOverlay: (text) => {},
|
||||
hideModalOverlay: () => {},
|
||||
stdoutMaxWidth: () => { return 78; }
|
||||
stdoutMaxWidth: () => { return 100; },
|
||||
forceRender: () => {},
|
||||
termSaveState: () => {},
|
||||
termRestoreState: (state) => {},
|
||||
};
|
||||
}
|
||||
|
||||
async execCommand(argv) {
|
||||
if (!argv.length) return this.execCommand(['help']);
|
||||
reg.logger().info('execCommand()', argv);
|
||||
// reg.logger().debug('execCommand()', argv);
|
||||
const commandName = argv[0];
|
||||
this.activeCommand_ = this.findCommandByName(commandName);
|
||||
|
||||
@@ -429,200 +312,99 @@ class Application {
|
||||
return this.activeCommand_;
|
||||
}
|
||||
|
||||
async refreshNotes(parentType, parentId) {
|
||||
this.logger().debug('Refreshing notes:', parentType, parentId);
|
||||
async loadKeymaps() {
|
||||
const defaultKeyMap = [
|
||||
{ "keys": [":"], "type": "function", "command": "enter_command_line_mode" },
|
||||
{ "keys": ["TAB"], "type": "function", "command": "focus_next" },
|
||||
{ "keys": ["SHIFT_TAB"], "type": "function", "command": "focus_previous" },
|
||||
{ "keys": ["UP"], "type": "function", "command": "move_up" },
|
||||
{ "keys": ["DOWN"], "type": "function", "command": "move_down" },
|
||||
{ "keys": ["PAGE_UP"], "type": "function", "command": "page_up" },
|
||||
{ "keys": ["PAGE_DOWN"], "type": "function", "command": "page_down" },
|
||||
{ "keys": ["ENTER"], "type": "function", "command": "activate" },
|
||||
{ "keys": ["DELETE", "BACKSPACE"], "type": "function", "command": "delete" },
|
||||
{ "keys": [" "], "command": "todo toggle $n" },
|
||||
{ "keys": ["tc"], "type": "function", "command": "toggle_console" },
|
||||
{ "keys": ["tm"], "type": "function", "command": "toggle_metadata" },
|
||||
{ "keys": ["/"], "type": "prompt", "command": "search \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["mn"], "type": "prompt", "command": "mknote \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["mt"], "type": "prompt", "command": "mktodo \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["mb"], "type": "prompt", "command": "mkbook \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["yn"], "type": "prompt", "command": "cp $n \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["dn"], "type": "prompt", "command": "mv $n \"\"", "cursorPosition": -2 }
|
||||
];
|
||||
|
||||
const state = this.store().getState();
|
||||
// Filter the keymap item by command so that items in keymap.json can override
|
||||
// the default ones.
|
||||
const itemsByCommand = {};
|
||||
|
||||
let options = {
|
||||
order: state.notesOrder,
|
||||
uncompletedTodosOnTop: Setting.value('uncompletedTodosOnTop'),
|
||||
};
|
||||
for (let i = 0; i < defaultKeyMap.length; i++) {
|
||||
itemsByCommand[defaultKeyMap[i].command] = defaultKeyMap[i]
|
||||
}
|
||||
|
||||
const source = JSON.stringify({
|
||||
options: options,
|
||||
parentId: parentId,
|
||||
});
|
||||
|
||||
let notes = [];
|
||||
|
||||
if (parentId) {
|
||||
if (parentType === Folder.modelType()) {
|
||||
notes = await Note.previews(parentId, options);
|
||||
} else if (parentType === Tag.modelType()) {
|
||||
notes = await Tag.notes(parentId);
|
||||
} else if (parentType === BaseModel.TYPE_SEARCH) {
|
||||
let fields = Note.previewFields();
|
||||
let search = BaseModel.byId(state.searches, parentId);
|
||||
notes = await Note.previews(null, {
|
||||
fields: fields,
|
||||
anywherePattern: '*' + search.query_pattern + '*',
|
||||
});
|
||||
const filePath = Setting.value('profileDir') + '/keymap.json';
|
||||
if (await fs.pathExists(filePath)) {
|
||||
try {
|
||||
let configString = await fs.readFile(filePath, 'utf-8');
|
||||
configString = configString.replace(/^\s*\/\/.*/, ''); // Strip off comments
|
||||
const keymap = JSON.parse(configString);
|
||||
for (let keymapIndex = 0; keymapIndex < keymap.length; keymapIndex++) {
|
||||
const item = keymap[keymapIndex];
|
||||
itemsByCommand[item.command] = item;
|
||||
}
|
||||
} catch (error) {
|
||||
let msg = error.message ? error.message : '';
|
||||
msg = 'Could not load keymap ' + filePath + '\n' + msg;
|
||||
error.message = msg;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
this.store().dispatch({
|
||||
type: 'NOTES_UPDATE_ALL',
|
||||
notes: notes,
|
||||
notesSource: source,
|
||||
});
|
||||
|
||||
this.store().dispatch({
|
||||
type: 'NOTES_SELECT',
|
||||
noteId: notes.length ? notes[0].id : null,
|
||||
});
|
||||
}
|
||||
|
||||
reducerActionToString(action) {
|
||||
let o = [action.type];
|
||||
if (action.id) o.push(action.id);
|
||||
if (action.noteId) o.push(action.noteId);
|
||||
if (action.folderId) o.push(action.folderId);
|
||||
if (action.tagId) o.push(action.tagId);
|
||||
if (action.tag) o.push(action.tag.id);
|
||||
if (action.folder) o.push(action.folder.id);
|
||||
if (action.notesSource) o.push(JSON.stringify(action.notesSource));
|
||||
return o.join(', ');
|
||||
}
|
||||
|
||||
generalMiddleware() {
|
||||
const middleware = store => next => async (action) => {
|
||||
this.logger().debug('Reducer action', this.reducerActionToString(action));
|
||||
|
||||
const result = next(action);
|
||||
const newState = store.getState();
|
||||
|
||||
if (action.type == 'FOLDERS_SELECT' || action.type === 'FOLDER_DELETE') {
|
||||
Setting.setValue('activeFolderId', newState.selectedFolderId);
|
||||
this.currentFolder_ = newState.selectedFolderId ? await Folder.load(newState.selectedFolderId) : null;
|
||||
await this.refreshNotes(Folder.modelType(), newState.selectedFolderId);
|
||||
}
|
||||
|
||||
if (action.type == 'TAGS_SELECT') {
|
||||
await this.refreshNotes(Tag.modelType(), action.id);
|
||||
}
|
||||
|
||||
if (action.type == 'SEARCH_SELECT') {
|
||||
await this.refreshNotes(BaseModel.TYPE_SEARCH, action.id);
|
||||
}
|
||||
|
||||
if (this.gui() && action.type == 'SETTINGS_UPDATE_ONE' && action.key == 'sync.interval' || action.type == 'SETTINGS_UPDATE_ALL') {
|
||||
reg.setupRecurrentSync();
|
||||
}
|
||||
|
||||
return result;
|
||||
const output = [];
|
||||
for (let n in itemsByCommand) {
|
||||
if (!itemsByCommand.hasOwnProperty(n)) continue;
|
||||
output.push(itemsByCommand[n]);
|
||||
}
|
||||
|
||||
return middleware;
|
||||
return output;
|
||||
}
|
||||
|
||||
dispatch(action) {
|
||||
if (this.store()) return this.store().dispatch(action);
|
||||
}
|
||||
|
||||
async start() {
|
||||
let argv = process.argv;
|
||||
let startFlags = await this.handleStartFlags_(argv);
|
||||
argv = startFlags.argv;
|
||||
let initArgs = startFlags.matched;
|
||||
if (argv.length) this.showPromptString_ = false;
|
||||
|
||||
if (process.argv[1].indexOf('joplindev') >= 0) {
|
||||
if (!initArgs.profileDir) initArgs.profileDir = '/mnt/d/Temp/TestNotes2';
|
||||
initArgs.logLevel = Logger.LEVEL_DEBUG;
|
||||
initArgs.env = 'dev';
|
||||
}
|
||||
|
||||
Setting.setConstant('appName', initArgs.env == 'dev' ? 'joplindev' : 'joplin');
|
||||
|
||||
const profileDir = initArgs.profileDir ? initArgs.profileDir : os.homedir() + '/.config/' + Setting.value('appName');
|
||||
const resourceDir = profileDir + '/resources';
|
||||
const tempDir = profileDir + '/tmp';
|
||||
|
||||
Setting.setConstant('env', initArgs.env);
|
||||
Setting.setConstant('profileDir', profileDir);
|
||||
Setting.setConstant('resourceDir', resourceDir);
|
||||
Setting.setConstant('tempDir', tempDir);
|
||||
|
||||
await fs.mkdirp(profileDir, 0o755);
|
||||
await fs.mkdirp(resourceDir, 0o755);
|
||||
await fs.mkdirp(tempDir, 0o755);
|
||||
|
||||
this.logger_.addTarget('file', { path: profileDir + '/log.txt' });
|
||||
this.logger_.setLevel(initArgs.logLevel);
|
||||
|
||||
reg.setLogger(this.logger_);
|
||||
reg.dispatch = (o) => {};
|
||||
|
||||
this.dbLogger_.addTarget('file', { path: profileDir + '/log-database.txt' });
|
||||
this.dbLogger_.setLevel(initArgs.logLevel);
|
||||
|
||||
if (Setting.value('env') === 'dev') {
|
||||
this.dbLogger_.setLevel(Logger.LEVEL_WARN);
|
||||
}
|
||||
|
||||
const packageJson = require('./package.json');
|
||||
this.logger_.info(sprintf('Starting %s %s (%s)...', packageJson.name, packageJson.version, Setting.value('env')));
|
||||
this.logger_.info('Profile directory: ' + profileDir);
|
||||
|
||||
this.database_ = new JoplinDatabase(new DatabaseDriverNode());
|
||||
//this.database_.setLogExcludedQueryTypes(['SELECT']);
|
||||
this.database_.setLogger(this.dbLogger_);
|
||||
await this.database_.open({ name: profileDir + '/database.sqlite' });
|
||||
|
||||
reg.setDb(this.database_);
|
||||
BaseModel.db_ = this.database_;
|
||||
async start(argv) {
|
||||
argv = await super.start(argv);
|
||||
|
||||
cliUtils.setStdout((object) => {
|
||||
return this.stdout(object);
|
||||
});
|
||||
|
||||
await Setting.load();
|
||||
|
||||
if (Setting.value('firstStart')) {
|
||||
let locale = process.env.LANG;
|
||||
if (!locale) locale = defaultLocale();
|
||||
locale = locale.split('.');
|
||||
locale = locale[0];
|
||||
reg.logger().info('First start: detected locale as ' + locale);
|
||||
Setting.setValue('locale', closestSupportedLocale(locale));
|
||||
Setting.setValue('firstStart', 0)
|
||||
}
|
||||
|
||||
setLocale(Setting.value('locale'));
|
||||
|
||||
let currentFolderId = Setting.value('activeFolderId');
|
||||
let currentFolder = null;
|
||||
if (currentFolderId) currentFolder = await Folder.load(currentFolderId);
|
||||
if (!currentFolder) currentFolder = await Folder.defaultFolder();
|
||||
Setting.setValue('activeFolderId', currentFolder ? currentFolder.id : '');
|
||||
|
||||
// If we have some arguments left at this point, it's a command
|
||||
// so execute it.
|
||||
if (argv.length) {
|
||||
this.gui_ = this.dummyGui();
|
||||
|
||||
this.currentFolder_ = await Folder.load(Setting.value('activeFolderId'));
|
||||
|
||||
try {
|
||||
await this.execCommand(argv);
|
||||
} catch (error) {
|
||||
if (this.showStackTraces_) {
|
||||
console.info(error);
|
||||
console.error(error);
|
||||
} else {
|
||||
console.info(error.message);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
} else { // Otherwise open the GUI
|
||||
this.store_ = createStore(reducer, applyMiddleware(this.generalMiddleware()));
|
||||
BaseModel.dispatch = this.store().dispatch;
|
||||
FoldersScreenUtils.dispatch = this.store().dispatch;
|
||||
this.initRedux();
|
||||
|
||||
const keymap = await this.loadKeymaps();
|
||||
|
||||
const AppGui = require('./app-gui.js');
|
||||
this.gui_ = new AppGui(this, this.store());
|
||||
this.gui_ = new AppGui(this, this.store(), keymap);
|
||||
this.gui_.setLogger(this.logger_);
|
||||
await this.gui_.start();
|
||||
|
||||
// Since the settings need to be loaded before the store is created, it will never
|
||||
// receive the SETTINGS_UPDATE_ALL even, which mean state.settings will not be
|
||||
// receive the SETTING_UPDATE_ALL even, which mean state.settings will not be
|
||||
// initialised. So we manually call dispatchUpdateAll() to force an update.
|
||||
Setting.dispatchUpdateAll();
|
||||
|
||||
@@ -630,13 +412,15 @@ class Application {
|
||||
|
||||
const tags = await Tag.allWithNotes();
|
||||
|
||||
ResourceService.runInBackground();
|
||||
|
||||
this.dispatch({
|
||||
type: 'TAGS_UPDATE_ALL',
|
||||
tags: tags,
|
||||
type: 'TAG_UPDATE_ALL',
|
||||
items: tags,
|
||||
});
|
||||
|
||||
this.store().dispatch({
|
||||
type: 'FOLDERS_SELECT',
|
||||
type: 'FOLDER_SELECT',
|
||||
id: Setting.value('activeFolderId'),
|
||||
});
|
||||
}
|
||||
@@ -652,4 +436,4 @@ function app() {
|
||||
return application_;
|
||||
}
|
||||
|
||||
export { app };
|
||||
module.exports = { app };
|
199
CliClient/app/autocompletion.js
Normal file
@@ -0,0 +1,199 @@
|
||||
var { app } = require('./app.js');
|
||||
var Note = require('lib/models/Note.js');
|
||||
var Folder = require('lib/models/Folder.js');
|
||||
var Tag = require('lib/models/Tag.js');
|
||||
var { cliUtils } = require('./cli-utils.js');
|
||||
var yargParser = require('yargs-parser');
|
||||
var fs = require('fs-extra');
|
||||
|
||||
async function handleAutocompletionPromise(line) {
|
||||
// Auto-complete the command name
|
||||
const names = await app().commandNames();
|
||||
let words = getArguments(line);
|
||||
//If there is only one word and it is not already a command name then you
|
||||
//should look for commmands it could be
|
||||
if (words.length == 1) {
|
||||
if (names.indexOf(words[0]) === -1) {
|
||||
let x = names.filter((n) => n.indexOf(words[0]) === 0);
|
||||
if (x.length === 1) {
|
||||
return x[0] + ' ';
|
||||
}
|
||||
return x.length > 0 ? x.map((a) => a + ' ') : line;
|
||||
} else {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
//There is more than one word and it is a command
|
||||
const metadata = (await app().commandMetadata())[words[0]];
|
||||
//If for some reason this command does not have any associated metadata
|
||||
//just don't autocomplete. However, this should not happen.
|
||||
if (metadata === undefined) {
|
||||
return line;
|
||||
}
|
||||
//complete an option
|
||||
let next = words.length > 1 ? words[words.length - 1] : '';
|
||||
let l = [];
|
||||
if (next[0] === '-') {
|
||||
for (let i = 0; i<metadata.options.length; i++) {
|
||||
const options = metadata.options[i][0].split(' ');
|
||||
//if there are multiple options then they will be separated by comma and
|
||||
//space. The comma should be removed
|
||||
if (options[0][options[0].length - 1] === ',') {
|
||||
options[0] = options[0].slice(0, -1);
|
||||
}
|
||||
if (words.includes(options[0]) || words.includes(options[1])) {
|
||||
continue;
|
||||
}
|
||||
//First two elements are the flag and the third is the description
|
||||
//Only autocomplete long
|
||||
if (options.length > 1 && options[1].indexOf(next) === 0) {
|
||||
l.push(options[1]);
|
||||
} else if (options[0].indexOf(next) === 0) {
|
||||
l.push(options[0]);
|
||||
}
|
||||
}
|
||||
if (l.length === 0) {
|
||||
return line;
|
||||
}
|
||||
let ret = l.map(a=>toCommandLine(a));
|
||||
ret.prefix = toCommandLine(words.slice(0, -1)) + ' ';
|
||||
return ret;
|
||||
}
|
||||
//Complete an argument
|
||||
//Determine the number of positional arguments by counting the number of
|
||||
//words that don't start with a - less one for the command name
|
||||
const positionalArgs = words.filter((a)=>a.indexOf('-') !== 0).length - 1;
|
||||
|
||||
let cmdUsage = yargParser(metadata.usage)['_'];
|
||||
cmdUsage.splice(0, 1);
|
||||
|
||||
if (cmdUsage.length >= positionalArgs) {
|
||||
|
||||
let argName = cmdUsage[positionalArgs - 1];
|
||||
argName = cliUtils.parseCommandArg(argName).name;
|
||||
|
||||
const currentFolder = app().currentFolder();
|
||||
|
||||
if (argName == 'note' || argName == 'note-pattern') {
|
||||
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: next + '*' }) : [];
|
||||
l.push(...notes.map((n) => n.title));
|
||||
}
|
||||
|
||||
if (argName == 'notebook') {
|
||||
const folders = await Folder.search({ titlePattern: next + '*' });
|
||||
l.push(...folders.map((n) => n.title));
|
||||
}
|
||||
|
||||
if (argName == 'item') {
|
||||
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: next + '*' }) : [];
|
||||
const folders = await Folder.search({ titlePattern: next + '*' });
|
||||
l.push(...notes.map((n) => n.title), folders.map((n) => n.title));
|
||||
}
|
||||
|
||||
if (argName == 'tag') {
|
||||
let tags = await Tag.search({ titlePattern: next + '*' });
|
||||
l.push(...tags.map((n) => n.title));
|
||||
}
|
||||
|
||||
if (argName == 'file') {
|
||||
let files = await fs.readdir('.');
|
||||
l.push(...files);
|
||||
}
|
||||
|
||||
if (argName == 'tag-command') {
|
||||
let c = filterList(['add', 'remove', 'list'], next);
|
||||
l.push(...c);
|
||||
}
|
||||
|
||||
if (argName == 'todo-command') {
|
||||
let c = filterList(['toggle', 'clear'], next);
|
||||
l.push(...c);
|
||||
}
|
||||
}
|
||||
if (l.length === 1) {
|
||||
return toCommandLine([...words.slice(0, -1), l[0]]);
|
||||
} else if (l.length > 1) {
|
||||
let ret = l.map(a=>toCommandLine(a));
|
||||
ret.prefix = toCommandLine(words.slice(0, -1)) + ' ';
|
||||
return ret;
|
||||
}
|
||||
return line;
|
||||
|
||||
}
|
||||
function handleAutocompletion(str, callback) {
|
||||
handleAutocompletionPromise(str).then(function(res) {
|
||||
callback(undefined, res);
|
||||
});
|
||||
}
|
||||
function toCommandLine(args) {
|
||||
if (Array.isArray(args)) {
|
||||
return args.map(function(a) {
|
||||
if(a.indexOf('"') !== -1 || a.indexOf(' ') !== -1) {
|
||||
return "'" + a + "'";
|
||||
} else if (a.indexOf("'") !== -1) {
|
||||
return '"' + a + '"';
|
||||
} else {
|
||||
return a;
|
||||
}
|
||||
}).join(' ');
|
||||
} else {
|
||||
if(args.indexOf('"') !== -1 || args.indexOf(' ') !== -1) {
|
||||
return "'" + args + "' ";
|
||||
} else if (args.indexOf("'") !== -1) {
|
||||
return '"' + args + '" ';
|
||||
} else {
|
||||
return args + ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
function getArguments(line) {
|
||||
let inSingleQuotes = false;
|
||||
let inDoubleQuotes = false;
|
||||
let currentWord = '';
|
||||
let parsed = [];
|
||||
for(let i = 0; i<line.length; i++) {
|
||||
if(line[i] === '"') {
|
||||
if(inDoubleQuotes) {
|
||||
inDoubleQuotes = false;
|
||||
//maybe push word to parsed?
|
||||
//currentWord += '"';
|
||||
} else {
|
||||
inDoubleQuotes = true;
|
||||
//currentWord += '"';
|
||||
}
|
||||
} else if(line[i] === "'") {
|
||||
if(inSingleQuotes) {
|
||||
inSingleQuotes = false;
|
||||
//maybe push word to parsed?
|
||||
//currentWord += "'";
|
||||
} else {
|
||||
inSingleQuotes = true;
|
||||
//currentWord += "'";
|
||||
}
|
||||
} else if (/\s/.test(line[i]) &&
|
||||
!(inDoubleQuotes || inSingleQuotes)) {
|
||||
if (currentWord !== '') {
|
||||
parsed.push(currentWord);
|
||||
currentWord = '';
|
||||
}
|
||||
} else {
|
||||
currentWord += line[i];
|
||||
}
|
||||
}
|
||||
if (!(inSingleQuotes || inDoubleQuotes) && /\s/.test(line[line.length - 1])) {
|
||||
parsed.push('');
|
||||
} else {
|
||||
parsed.push(currentWord);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
function filterList(list, next) {
|
||||
let output = [];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i].indexOf(next) !== 0) continue;
|
||||
output.push(list[i]);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
module.exports = { handleAutocompletion };
|
@@ -1,5 +1,5 @@
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { reg } from 'lib/registry.js';
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
|
||||
class BaseCommand {
|
||||
|
||||
@@ -12,6 +12,10 @@ class BaseCommand {
|
||||
throw new Error('Usage not defined');
|
||||
}
|
||||
|
||||
encryptionCheck(item) {
|
||||
if (item && item.encryption_applied) throw new Error(_('Cannot change encrypted item'));
|
||||
}
|
||||
|
||||
description() {
|
||||
throw new Error('Description not defined');
|
||||
}
|
||||
@@ -28,10 +32,6 @@ class BaseCommand {
|
||||
return this.compatibleUis().indexOf(ui) >= 0;
|
||||
}
|
||||
|
||||
aliases() {
|
||||
return [];
|
||||
}
|
||||
|
||||
options() {
|
||||
return [];
|
||||
}
|
||||
@@ -96,4 +96,4 @@ class BaseCommand {
|
||||
|
||||
}
|
||||
|
||||
export { BaseCommand };
|
||||
module.exports = { BaseCommand };
|
@@ -1,10 +1,7 @@
|
||||
require('source-map-support').install();
|
||||
require('babel-plugin-transform-runtime');
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { fileExtension, basename, dirname } from 'lib/path-utils.js';
|
||||
import wrap_ from 'word-wrap';
|
||||
import { _, setLocale, languageCode } from 'lib/locale.js';
|
||||
const fs = require('fs-extra');
|
||||
const { fileExtension, basename, dirname } = require('lib/path-utils.js');
|
||||
const wrap_ = require('word-wrap');
|
||||
const { _, setLocale, languageCode } = require('lib/locale.js');
|
||||
|
||||
const rootDir = dirname(dirname(__dirname));
|
||||
const MAX_WIDTH = 78;
|
||||
@@ -105,7 +102,7 @@ function getFooter() {
|
||||
|
||||
output.push('WEBSITE');
|
||||
output.push('');
|
||||
output.push(INDENT + 'http://joplin.cozic.net');
|
||||
output.push(INDENT + 'https://joplin.cozic.net');
|
||||
|
||||
output.push('');
|
||||
|
||||
|
@@ -1,148 +0,0 @@
|
||||
"use strict"
|
||||
|
||||
require('source-map-support').install();
|
||||
require('babel-plugin-transform-runtime');
|
||||
|
||||
const processArgs = process.argv.splice(2, process.argv.length);
|
||||
|
||||
const silentLog = processArgs.indexOf('--silent') >= 0;
|
||||
|
||||
import { basename, dirname } from 'lib/path-utils.js';
|
||||
import fs from 'fs-extra';
|
||||
import gettextParser from 'gettext-parser';
|
||||
|
||||
const rootDir = dirname(dirname(__dirname));
|
||||
const cliDir = rootDir + '/CliClient';
|
||||
const cliLocalesDir = cliDir + '/locales';
|
||||
const rnDir = rootDir + '/ReactNativeClient';
|
||||
|
||||
function execCommand(command) {
|
||||
if (!silentLog) console.info('Running: ' + command);
|
||||
|
||||
const exec = require('child_process').exec
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let childProcess = exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
if (error.signal == 'SIGTERM') {
|
||||
resolve('Process was killed');
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
} else {
|
||||
resolve(stdout.trim());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function parsePoFile(filePath) {
|
||||
const content = fs.readFileSync(filePath);
|
||||
return gettextParser.po.parse(content);
|
||||
}
|
||||
|
||||
function serializeTranslation(translation) {
|
||||
let output = {};
|
||||
const translations = translation.translations[''];
|
||||
for (let n in translations) {
|
||||
if (!translations.hasOwnProperty(n)) continue;
|
||||
if (n == '') continue;
|
||||
const t = translations[n];
|
||||
if (t.comments && t.comments.flag && t.comments.flag.indexOf('fuzzy') >= 0) {
|
||||
output[n] = t['msgid'];
|
||||
} else {
|
||||
output[n] = t['msgstr'][0];
|
||||
}
|
||||
}
|
||||
return JSON.stringify(output);
|
||||
}
|
||||
|
||||
function saveToFile(filePath, data) {
|
||||
fs.writeFileSync(filePath, data);
|
||||
}
|
||||
|
||||
function buildLocale(inputFile, outputFile) {
|
||||
const r = parsePoFile(inputFile);
|
||||
const translation = serializeTranslation(r);
|
||||
saveToFile(outputFile, translation);
|
||||
}
|
||||
|
||||
async function removePoHeaderDate(filePath) {
|
||||
await execCommand('sed -i -e\'/POT-Creation-Date:/d\' "' + filePath + '"');
|
||||
await execCommand('sed -i -e\'/PO-Revision-Date:/d\' "' + filePath + '"');
|
||||
}
|
||||
|
||||
async function createPotFile(potFilePath, sources) {
|
||||
let baseArgs = [];
|
||||
baseArgs.push('--from-code=utf-8');
|
||||
baseArgs.push('--output="' + potFilePath + '"');
|
||||
baseArgs.push('--language=JavaScript');
|
||||
baseArgs.push('--copyright-holder="Laurent Cozic"');
|
||||
baseArgs.push('--package-name=Joplin-CLI');
|
||||
baseArgs.push('--package-version=1.0.0');
|
||||
baseArgs.push('--no-location');
|
||||
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
let args = baseArgs.slice();
|
||||
if (i > 0) args.push('--join-existing');
|
||||
args.push(sources[i]);
|
||||
const result = await execCommand('xgettext ' + args.join(' '));
|
||||
if (result) console.error(result);
|
||||
await removePoHeaderDate(potFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
async function mergePotToPo(potFilePath, poFilePath) {
|
||||
const command = 'msgmerge -U "' + poFilePath + '" "' + potFilePath + '"';
|
||||
const result = await execCommand(command);
|
||||
if (result) console.error(result);
|
||||
await removePoHeaderDate(poFilePath);
|
||||
}
|
||||
|
||||
function buildIndex(locales) {
|
||||
let output = [];
|
||||
output.push('var locales = {};');
|
||||
for (let i = 0; i < locales.length; i++) {
|
||||
const locale = locales[i];
|
||||
output.push("locales['" + locale + "'] = require('./" + locale + ".json');");
|
||||
}
|
||||
output.push('module.exports = { locales: locales };');
|
||||
return output.join("\n");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let potFilePath = cliLocalesDir + '/joplin.pot';
|
||||
let jsonLocalesDir = cliDir + '/build/locales';
|
||||
const defaultLocale = 'en_GB';
|
||||
|
||||
await createPotFile(potFilePath, [
|
||||
cliDir + '/app/*.js',
|
||||
rnDir + '/lib/*.js',
|
||||
rnDir + '/lib/models/*.js',
|
||||
rnDir + '/lib/services/*.js',
|
||||
rnDir + '/lib/components/*.js',
|
||||
rnDir + '/lib/components/screens/*.js',
|
||||
]);
|
||||
|
||||
await execCommand('cp "' + potFilePath + '" ' + '"' + cliLocalesDir + '/' + defaultLocale + '.po"');
|
||||
|
||||
fs.mkdirpSync(jsonLocalesDir, 0o755);
|
||||
|
||||
let locales = [defaultLocale, 'fr_FR'];
|
||||
for (let i = 0; i < locales.length; i++) {
|
||||
const locale = locales[i];
|
||||
const poFilePäth = cliLocalesDir + '/' + locale + '.po';
|
||||
const jsonFilePath = jsonLocalesDir + '/' + locale + '.json';
|
||||
if (locale != defaultLocale) await mergePotToPo(potFilePath, poFilePäth);
|
||||
buildLocale(poFilePäth, jsonFilePath);
|
||||
}
|
||||
|
||||
saveToFile(jsonLocalesDir + '/index.js', buildIndex(locales));
|
||||
|
||||
const rnJsonLocaleDir = rnDir + '/locales';
|
||||
await execCommand('rsync -a "' + jsonLocalesDir + '/" "' + rnJsonLocaleDir + '"');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
});
|
@@ -1,211 +0,0 @@
|
||||
require('source-map-support').install();
|
||||
require('babel-plugin-transform-runtime');
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { fileExtension, basename, dirname } from 'lib/path-utils.js';
|
||||
import { _, setLocale, languageCode } from 'lib/locale.js';
|
||||
import marked from 'lib/marked.js';
|
||||
|
||||
const headerHtml = `
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Joplin - a free, open source, te taking and to-do application with synchronisation capabilities</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
|
||||
<style>
|
||||
body {
|
||||
background-color: #F1F1F1;
|
||||
color: #333333;
|
||||
}
|
||||
table {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
td, th {
|
||||
padding: .5em 1em .5em 0;
|
||||
}
|
||||
h1, h2 {
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.3em;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
font-weight: 600;
|
||||
font-size: 2em;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
code {
|
||||
color: black;
|
||||
background-color: #eee;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
pre code {
|
||||
border: none;
|
||||
}
|
||||
.title-icon {
|
||||
height: 2em;
|
||||
}
|
||||
.sub-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.container {
|
||||
background-color: white;
|
||||
padding: 0;
|
||||
box-shadow: 0 10px 20px #888888;
|
||||
}
|
||||
table.screenshots {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
table.screenshots th {
|
||||
height: 3em;
|
||||
text-align: center;
|
||||
}
|
||||
table.screenshots th,
|
||||
table.screenshots td {
|
||||
border: 1px solid #C2C2C2;
|
||||
}
|
||||
.mobile-screenshot {
|
||||
height: 40em;
|
||||
padding: 1em;
|
||||
}
|
||||
.cli-screenshot-wrapper {
|
||||
background-color: black;
|
||||
vertical-align: top;
|
||||
padding: 1em 2em 1em 1em;
|
||||
}
|
||||
.cli-screenshot {
|
||||
font-family: "Monaco", "Inconsolata", "CONSOLAS", "Deja Vu Sans Mono", "Droid Sans Mono", "Andale Mono", monospace;
|
||||
background-color: black;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
.cli-screenshot .prompt {
|
||||
color: #48C2F0;
|
||||
}
|
||||
.header {
|
||||
position: relative;
|
||||
padding-left: 2em;
|
||||
padding-right: 2em;
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
color: white;
|
||||
background-color: #2B2B3D;
|
||||
}
|
||||
.content {
|
||||
padding-left: 2em;
|
||||
padding-right: 2em;
|
||||
padding-bottom: 2em;
|
||||
padding-top: 2em;
|
||||
}
|
||||
.forkme {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top:0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div class="header">
|
||||
<a class="forkme" href="https://github.com/laurent22/joplin"><img src="docs/images/ForkMe.png"/></a>
|
||||
<h1 id="joplin"><img class="title-icon" src="docs/images/Icon512.png">oplin</h1>
|
||||
<p class="sub-title">A free, open source, note taking and to-do application with synchronisation capabilities.</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
`;
|
||||
|
||||
const footerHtml = `
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
// const screenshotHtml = `
|
||||
// <table class="screenshots">
|
||||
// <tr>
|
||||
// <th>
|
||||
// Mobile
|
||||
// </th>
|
||||
// <th>
|
||||
// Command line
|
||||
// </th>
|
||||
// </tr>
|
||||
// <tr>
|
||||
// <td>
|
||||
// <img class="mobile-screenshot" src="docs/images/Mobile.png"/>
|
||||
// </td>
|
||||
// <td class="cli-screenshot-wrapper">
|
||||
// <pre class="cli-screenshot">
|
||||
// <span class="prompt">joplin:/My notebook$</span> ls -n 12
|
||||
// [ ] 8am conference call ☎
|
||||
// [ ] Make vet appointment
|
||||
// [ ] Go pick up parcel
|
||||
// [ ] Pay flat rent 💸
|
||||
// [X] Book ferry 🚢
|
||||
// [X] Deploy Joplin app
|
||||
// Open source stuff
|
||||
// Swimming pool time table 🏊
|
||||
// Grocery shopping list 📝
|
||||
// Work itinerary
|
||||
// Tuesday random note
|
||||
// Vacation plans ☀️
|
||||
// </pre>
|
||||
// </td>
|
||||
// </tr>
|
||||
// </table>
|
||||
// `;
|
||||
|
||||
const gaHtml = `
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-103586105-1', 'auto');
|
||||
ga('send', 'pageview');
|
||||
|
||||
</script>
|
||||
`;
|
||||
|
||||
const rootDir = dirname(dirname(__dirname));
|
||||
|
||||
function markdownToHtml(md) {
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
// Remove the header because it's going to be added back as HTML
|
||||
md = md.replace(/# Joplin/, '');
|
||||
|
||||
let output = marked(md, {
|
||||
gfm: true,
|
||||
break: true,
|
||||
renderer: renderer,
|
||||
});
|
||||
|
||||
//output = output.replace(/<!-- \[SCREENSHOTS\] -->/, screenshotHtml);
|
||||
|
||||
return headerHtml + output + gaHtml + footerHtml;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const md = fs.readFileSync(rootDir + '/README.md', 'utf8');
|
||||
const html = markdownToHtml(md);
|
||||
|
||||
fs.writeFileSync(rootDir + '/index.html', html);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
});
|
@@ -1,18 +1,15 @@
|
||||
"use strict"
|
||||
|
||||
require('source-map-support').install();
|
||||
require('babel-plugin-transform-runtime');
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { Logger } from 'lib/logger.js';
|
||||
import { dirname } from 'lib/path-utils.js';
|
||||
import { DatabaseDriverNode } from 'lib/database-driver-node.js';
|
||||
import { JoplinDatabase } from 'lib/joplin-database.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
import { Setting } from 'lib/models/setting.js';
|
||||
import { sprintf } from 'sprintf-js';
|
||||
const fs = require('fs-extra');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { dirname } = require('lib/path-utils.js');
|
||||
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
|
||||
const { JoplinDatabase } = require('lib/joplin-database.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const exec = require('child_process').exec
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
|
@@ -1,79 +1,10 @@
|
||||
import yargParser from 'yargs-parser';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { time } from 'lib/time-utils.js';
|
||||
const yargParser = require('yargs-parser');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const stringPadding = require('string-padding');
|
||||
|
||||
const cliUtils = {};
|
||||
|
||||
cliUtils.splitCommandString = function(command) {
|
||||
let args = [];
|
||||
let state = "start"
|
||||
let current = ""
|
||||
let quote = "\""
|
||||
let escapeNext = false;
|
||||
for (let i = 0; i < command.length; i++) {
|
||||
let c = command[i]
|
||||
|
||||
if (state == "quotes") {
|
||||
if (c != quote) {
|
||||
current += c
|
||||
} else {
|
||||
args.push(current)
|
||||
current = ""
|
||||
state = "start"
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (escapeNext) {
|
||||
current += c;
|
||||
escapeNext = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == "\\") {
|
||||
escapeNext = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '"' || c == '\'') {
|
||||
state = "quotes"
|
||||
quote = c
|
||||
continue
|
||||
}
|
||||
|
||||
if (state == "arg") {
|
||||
if (c == ' ' || c == '\t') {
|
||||
args.push(current)
|
||||
current = ""
|
||||
state = "start"
|
||||
} else {
|
||||
current += c
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (c != ' ' && c != "\t") {
|
||||
state = "arg"
|
||||
current += c
|
||||
}
|
||||
}
|
||||
|
||||
if (state == "quotes") {
|
||||
throw new Error("Unclosed quote in command line: " + command)
|
||||
}
|
||||
|
||||
if (current != "") {
|
||||
args.push(current)
|
||||
}
|
||||
|
||||
if (args.length <= 0) {
|
||||
throw new Error("Empty command line")
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
cliUtils.printArray = function(logFunction, rows, headers = null) {
|
||||
if (!rows.length) return '';
|
||||
|
||||
@@ -247,38 +178,39 @@ cliUtils.promptConfirm = function(message, answers = null) {
|
||||
});
|
||||
}
|
||||
|
||||
cliUtils.promptInput = function(message) {
|
||||
const readline = require('readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
rl.question(message + ' ', (answer) => {
|
||||
rl.close();
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Note: initialText is there to have the same signature as statusBar.prompt() so that
|
||||
// it can be a drop-in replacement, however initialText is not used (and cannot be
|
||||
// with readline.question?).
|
||||
cliUtils.prompt = function(initialText = '', promptString = ':') {
|
||||
cliUtils.prompt = function(initialText = '', promptString = ':', options = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
const readline = require('readline');
|
||||
const Writable = require('stream').Writable;
|
||||
|
||||
const mutableStdout = new Writable({
|
||||
write: function(chunk, encoding, callback) {
|
||||
if (!this.muted)
|
||||
process.stdout.write(chunk, encoding);
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
output: mutableStdout,
|
||||
terminal: true,
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
mutableStdout.muted = false;
|
||||
|
||||
rl.question(promptString, (answer) => {
|
||||
rl.close();
|
||||
if (!!options.secure) this.stdout_('');
|
||||
resolve(answer);
|
||||
});
|
||||
|
||||
mutableStdout.muted = !!options.secure;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -315,4 +247,4 @@ cliUtils.redrawDone = function() {
|
||||
redrawStarted_ = false;
|
||||
}
|
||||
|
||||
export { cliUtils };
|
||||
module.exports = { cliUtils };
|
289
CliClient/app/command-apidoc.js
Normal file
@@ -0,0 +1,289 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
const EncryptionService = require('lib/services/EncryptionService');
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||
const MasterKey = require('lib/models/MasterKey');
|
||||
const BaseItem = require('lib/models/BaseItem');
|
||||
const BaseModel = require('lib/BaseModel');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { toTitleCase } = require('lib/string-utils.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const markdownUtils = require('lib/markdownUtils');
|
||||
const { Database } = require('lib/database.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
usage() {
|
||||
return 'apidoc';
|
||||
}
|
||||
|
||||
description() {
|
||||
return 'Build the API doc';
|
||||
}
|
||||
|
||||
createPropertiesTable(tableFields) {
|
||||
const headers = [
|
||||
{ name: 'name', label: 'Name' },
|
||||
{ name: 'type', label: 'Type', filter: (value) => {
|
||||
return Database.enumName('fieldType', value);
|
||||
}},
|
||||
{ name: 'description', label: 'Description' },
|
||||
];
|
||||
|
||||
return markdownUtils.createMarkdownTable(headers, tableFields);
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
const models = [
|
||||
{
|
||||
type: BaseModel.TYPE_NOTE,
|
||||
},
|
||||
{
|
||||
type: BaseModel.TYPE_FOLDER,
|
||||
},
|
||||
{
|
||||
type: BaseModel.TYPE_RESOURCE,
|
||||
},
|
||||
{
|
||||
type: BaseModel.TYPE_TAG,
|
||||
},
|
||||
];
|
||||
|
||||
const lines = [];
|
||||
|
||||
lines.push('# Joplin API');
|
||||
lines.push('');
|
||||
|
||||
lines.push('When the Web Clipper service is enabled, Joplin exposes a [REST API](https://en.wikipedia.org/wiki/Representational_state_transfer) which allows third-party applications to access Joplin\'s data and to create, modify or delete notes, notebooks, resources or tags.');
|
||||
lines.push('');
|
||||
lines.push('In order to use it, you\'ll first need to find on which port the service is running. To do so, open the Web Clipper Options in Joplin and if the service is running it should tell you on which port. Normally it runs on port **41184**. If you want to find it programmatically, you may follow this kind of algorithm:');
|
||||
lines.push('');
|
||||
lines.push('```javascript');
|
||||
lines.push('let port = null;');
|
||||
lines.push('for (let portToTest = 41184; portToTest <= 41194; portToTest++) {');
|
||||
lines.push(' const result = pingPort(portToTest); // Call GET /ping');
|
||||
lines.push(' if (result == \'JoplinClipperServer\') {');
|
||||
lines.push(' port = portToTest; // Found the port');
|
||||
lines.push(' break;');
|
||||
lines.push(' }');
|
||||
lines.push('}');
|
||||
lines.push('```');
|
||||
lines.push('');
|
||||
|
||||
lines.push('# Authorisation')
|
||||
lines.push('');
|
||||
lines.push('To prevent unauthorised applications from accessing the API, the calls must be authentified. To do so, you must provide a token as a query parameter for each API call. You can get this token from the Joplin desktop application, on the Web Clipper Options screen.');
|
||||
lines.push('');
|
||||
lines.push('This would be an example of valid cURL call using a token:');
|
||||
lines.push('');
|
||||
lines.push('\tcurl http://localhost:41184/notes?token=ABCD123ABCD123ABCD123ABCD123ABCD123');
|
||||
lines.push('');
|
||||
lines.push('In the documentation below, the token will not be specified every time however you will need to include it.');
|
||||
lines.push('');
|
||||
|
||||
lines.push('# Using the API');
|
||||
lines.push('');
|
||||
lines.push('All the calls, unless noted otherwise, receives and send **JSON data**. For example to create a new note:');
|
||||
lines.push('');
|
||||
lines.push('\tcurl --data \'{ "title": "My note", "body": "Some note in **Markdown**"}\' http://localhost:41184/notes');
|
||||
lines.push('');
|
||||
lines.push('In the documentation below, the calls may include special parameters such as :id or :note_id. You would replace this with the item ID or note ID.');
|
||||
lines.push('');
|
||||
lines.push('For example, for the endpoint `DELETE /tags/:id/notes/:note_id`, to remove the tag with ID "ABCD1234" from the note with ID "EFGH789", you would run for example:');
|
||||
lines.push('');
|
||||
lines.push('\tcurl -X DELETE http://localhost:41184/tags/ABCD1234/notes/EFGH789');
|
||||
lines.push('');
|
||||
lines.push('The four verbs supported by the API are the following ones:');
|
||||
lines.push('');
|
||||
lines.push('* **GET**: To retrieve items (notes, notebooks, etc.).');
|
||||
lines.push('* **POST**: To create new items. In general most item properties are optional. If you omit any, a default value will be used.');
|
||||
lines.push('* **PUT**: To update an item. Note in a REST API, traditionally PUT is used to completely replace an item, however in this API it will only replace the properties that are provided. For example if you PUT {"title": "my new title"}, only the "title" property will be changed. The other properties will be left untouched (they won\'t be cleared nor changed).');
|
||||
lines.push('* **DELETE**: To delete items.');
|
||||
lines.push('');
|
||||
|
||||
lines.push('# Filtering data');
|
||||
lines.push('');
|
||||
lines.push('You can change the fields that will be returned by the API using the `fields=` query parameter, which takes a list of comma separated fields. For example, to get the longitude and latitude of a note, use this:');
|
||||
lines.push('');
|
||||
lines.push('\tcurl http://localhost:41184/notes/ABCD123?fields=longitude,latitude');
|
||||
lines.push('');
|
||||
lines.push('To get the IDs only of all the tags:');
|
||||
lines.push('');
|
||||
lines.push('\tcurl http://localhost:41184/tags?fields=id');
|
||||
lines.push('');
|
||||
|
||||
lines.push('# About the property types');
|
||||
lines.push('');
|
||||
lines.push('* Text is UTF-8.');
|
||||
lines.push('* All date/time are Unix timestamps in milliseconds.');
|
||||
lines.push('* Booleans are integer values 0 or 1.');
|
||||
lines.push('');
|
||||
|
||||
lines.push('# Testing if the service is available');
|
||||
lines.push('');
|
||||
lines.push('Call **GET /ping** to check if the service is available. It should return "JoplinClipperServer" if it works.');
|
||||
lines.push('');
|
||||
|
||||
for (let i = 0; i < models.length; i++) {
|
||||
const model = models[i];
|
||||
const ModelClass = BaseItem.getClassByItemType(model.type);
|
||||
const tableName = ModelClass.tableName();
|
||||
let tableFields = reg.db().tableFields(tableName, { includeDescription: true });
|
||||
const singular = tableName.substr(0, tableName.length - 1);
|
||||
|
||||
if (model.type === BaseModel.TYPE_NOTE) {
|
||||
tableFields = tableFields.slice();
|
||||
tableFields.push({
|
||||
name: 'body_html',
|
||||
type: Database.enumId('fieldType', 'text'),
|
||||
description: 'Note body, in HTML format',
|
||||
});
|
||||
tableFields.push({
|
||||
name: 'base_url',
|
||||
type: Database.enumId('fieldType', 'text'),
|
||||
description: 'If `body_html` is provided and contains relative URLs, provide the `base_url` parameter too so that all the URLs can be converted to absolute ones. The base URL is basically where the HTML was fetched from, minus the query (everything after the \'?\'). For example if the original page was `https://stackoverflow.com/search?q=%5Bjava%5D+test`, the base URL is `https://stackoverflow.com/search`.',
|
||||
});
|
||||
tableFields.push({
|
||||
name: 'image_data_url',
|
||||
type: Database.enumId('fieldType', 'text'),
|
||||
description: 'An image to attach to the note, in [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format.',
|
||||
});
|
||||
tableFields.push({
|
||||
name: 'crop_rect',
|
||||
type: Database.enumId('fieldType', 'text'),
|
||||
description: 'If an image is provided, you can also specify an optional rectangle that will be used to crop the image. In format `{ x: x, y: y, width: width, height: height }`',
|
||||
});
|
||||
tableFields.push({
|
||||
name: 'tags',
|
||||
type: Database.enumId('fieldType', 'text'),
|
||||
description: 'Comma-separated list of tags. eg. `tag1,tag2`.',
|
||||
});
|
||||
}
|
||||
|
||||
lines.push('# ' + toTitleCase(tableName));
|
||||
lines.push('');
|
||||
|
||||
if (model.type === BaseModel.TYPE_FOLDER) {
|
||||
lines.push('This is actually a notebook. Internally notebooks are called "folders".');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push('## Properties');
|
||||
lines.push('');
|
||||
lines.push(this.createPropertiesTable(tableFields));
|
||||
lines.push('');
|
||||
|
||||
lines.push('## GET /' + tableName);
|
||||
lines.push('');
|
||||
lines.push('Gets all ' + tableName);
|
||||
lines.push('');
|
||||
|
||||
if (model.type === BaseModel.TYPE_FOLDER) {
|
||||
lines.push('The folders are returned as a tree. The sub-notebooks of a notebook, if any, are under the `children` key.');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push('## GET /' + tableName + '/:id');
|
||||
lines.push('');
|
||||
lines.push('Gets ' + singular + ' with ID :id');
|
||||
lines.push('');
|
||||
|
||||
if (model.type === BaseModel.TYPE_TAG) {
|
||||
lines.push('## GET /tags/:id/notes');
|
||||
lines.push('');
|
||||
lines.push('Gets all the notes with this tag.');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (model.type === BaseModel.TYPE_NOTE) {
|
||||
lines.push('## GET /notes/:id/tags');
|
||||
lines.push('');
|
||||
lines.push('Gets all the tags attached to this note.');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (model.type === BaseModel.TYPE_FOLDER) {
|
||||
lines.push('## GET /folders/:id/notes');
|
||||
lines.push('');
|
||||
lines.push('Gets all the notes inside this folder.');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (model.type === BaseModel.TYPE_RESOURCE) {
|
||||
lines.push('## GET /resources/:id/file');
|
||||
lines.push('');
|
||||
lines.push('Gets the actual file associated with this resource.');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push('## POST /' + tableName);
|
||||
lines.push('');
|
||||
lines.push('Creates a new ' + singular);
|
||||
lines.push('');
|
||||
|
||||
if (model.type === BaseModel.TYPE_RESOURCE) {
|
||||
lines.push('Creating a new resource is special because you also need to upload the file. Unlike other API calls, this one must have the "multipart/form-data" Content-Type. The file data must be passed to the "data" form field, and the other properties to the "props" form field. An example of a valid call with cURL would be:');
|
||||
lines.push('');
|
||||
lines.push('\tcurl -F \'data=@/path/to/file.jpg\' -F \'props={"title":"my resource title"}\' http://localhost:41184/resources');
|
||||
lines.push('');
|
||||
lines.push('The "data" field is required, while the "props" one is not. If not specified, default values will be used.');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (model.type === BaseModel.TYPE_TAG) {
|
||||
lines.push('## POST /tags/:id/notes');
|
||||
lines.push('');
|
||||
lines.push('Post a note to this endpoint to add the tag to the note. The note data must at least contain an ID property (all other properties will be ignored).');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (model.type === BaseModel.TYPE_NOTE) {
|
||||
lines.push('You can either specify the note body as Markdown by setting the `body` parameter, or in HTML by setting the `body_html`.');
|
||||
lines.push('');
|
||||
lines.push('Examples:');
|
||||
lines.push('');
|
||||
lines.push('* Create a note from some Markdown text');
|
||||
lines.push('');
|
||||
lines.push(' curl --data \'{ "title": "My note", "body": "Some note in **Markdown**"}\' http://127.0.0.1:41184/notes');
|
||||
lines.push('');
|
||||
lines.push('* Create a note from some HTML');
|
||||
lines.push('');
|
||||
lines.push(' curl --data \'{ "title": "My note", "body_html": "Some note in <b>HTML</b>"}\' http://127.0.0.1:41184/notes');
|
||||
lines.push('');
|
||||
lines.push('* Create a note and attach an image to it:');
|
||||
lines.push('');
|
||||
lines.push(' curl --data \'{ "title": "Image test", "body": "Here is Joplin icon:", "image_data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAANZJREFUeNoAyAA3/wFwtO3K6gUB/vz2+Prw9fj/+/r+/wBZKAAExOgF4/MC9ff+MRH6Ui4E+/0Bqc/zutj6AgT+/Pz7+vv7++nu82c4DlMqCvLs8goA/gL8/fz09fb59vXa6vzZ6vjT5fbn6voD/fwC8vX4UiT9Zi//APHyAP8ACgUBAPv5APz7BPj2+DIaC2o3E+3o6ywaC5fT6gD6/QD9/QEVf9kD+/dcLQgJA/7v8vqfwOf18wA1IAIEVycAyt//v9XvAPv7APz8LhoIAPz9Ri4OAgwARgx4W/6fVeEAAAAASUVORK5CYII="}\' http://127.0.0.1:41184/notes');
|
||||
lines.push('');
|
||||
lines.push('### Creating a note with a specific ID');
|
||||
lines.push('');
|
||||
lines.push('When a new note is created, it is automatically assigned a new unique ID so **normally you do not need to set the ID**. However, if for some reason you want to set it, you can supply it as the `id` property. It needs to be a 32 characters long hexadecimal string. **Make sure it is unique**, for example by generating it using whatever GUID function is available in your programming language.');
|
||||
lines.push('');
|
||||
lines.push(' curl --data \'{ "id": "00a87474082744c1a8515da6aa5792d2", "title": "My note with custom ID"}\' http://127.0.0.1:41184/notes');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push('## PUT /' + tableName + '/:id');
|
||||
lines.push('');
|
||||
lines.push('Sets the properties of the ' + singular + ' with ID :id');
|
||||
lines.push('');
|
||||
|
||||
lines.push('## DELETE /' + tableName + '/:id');
|
||||
lines.push('');
|
||||
lines.push('Deletes the ' + singular + ' with ID :id');
|
||||
lines.push('');
|
||||
|
||||
if (model.type === BaseModel.TYPE_TAG) {
|
||||
lines.push('## DELETE /tags/:id/notes/:note_id');
|
||||
lines.push('');
|
||||
lines.push('Remove the tag from the note.');
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
|
||||
this.stdout(lines.join('\n'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Command;
|
@@ -1,16 +1,9 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
import { Resource } from 'lib/models/resource.js';
|
||||
import { uuid } from 'lib/uuid.js';
|
||||
import { filename } from 'lib/path-utils.js';
|
||||
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const fs = require('fs-extra');
|
||||
const mime = require('mime/lite');
|
||||
const sharp = require('sharp');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -22,49 +15,16 @@ class Command extends BaseCommand {
|
||||
return _('Attaches the given file to the note.');
|
||||
}
|
||||
|
||||
resizeImage_(filePath, targetPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
sharp(filePath)
|
||||
.resize(Resource.IMAGE_MAX_DIMENSION, Resource.IMAGE_MAX_DIMENSION)
|
||||
.max()
|
||||
.withoutEnlargement()
|
||||
.toFile(targetPath, (err, info) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(info);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
let title = args['note'];
|
||||
|
||||
let note = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
|
||||
this.encryptionCheck(note);
|
||||
if (!note) throw new Error(_('Cannot find "%s".', title));
|
||||
|
||||
const localFilePath = args['file'];
|
||||
if (!(await fs.pathExists(localFilePath))) throw new Error(_('Cannot access %s', localFilePath));
|
||||
|
||||
let resource = Resource.new();
|
||||
resource.id = uuid.create();
|
||||
resource.mime = mime.getType(localFilePath);
|
||||
resource.title = filename(localFilePath);
|
||||
|
||||
let targetPath = Resource.fullPath(resource);
|
||||
|
||||
if (resource.mime == 'image/jpeg' || resource.mime == 'image/jpg' || resource.mime == 'image/png') {
|
||||
const result = await this.resizeImage_(localFilePath, targetPath);
|
||||
this.logger().info(result);
|
||||
} else {
|
||||
await fs.copy(localFilePath, targetPath, { overwrite: true });
|
||||
}
|
||||
|
||||
await Resource.save(resource, { isNew: true });
|
||||
|
||||
note.body += "\n\n" + Resource.markdownTag(resource);
|
||||
await Note.save(note);
|
||||
await shim.attachFileToNote(note, localFilePath);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -21,10 +21,6 @@ class Command extends BaseCommand {
|
||||
];
|
||||
}
|
||||
|
||||
enabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
let title = args['note'];
|
||||
|
||||
@@ -33,6 +29,9 @@ class Command extends BaseCommand {
|
||||
|
||||
const content = args.options.verbose ? await Note.serialize(item) : await Note.serializeForEdit(item);
|
||||
this.stdout(content);
|
||||
|
||||
app().gui().showConsole();
|
||||
app().gui().maximizeConsole();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { _, setLocale } from 'lib/locale.js';
|
||||
import { app } from './app.js';
|
||||
import { Setting } from 'lib/models/setting.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { _, setLocale } = require('lib/locale.js');
|
||||
const { app } = require('./app.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -23,7 +23,11 @@ class Command extends BaseCommand {
|
||||
const verbose = args.options.verbose;
|
||||
|
||||
const renderKeyValue = (name) => {
|
||||
const value = Setting.value(name);
|
||||
const md = Setting.settingMetadata(name);
|
||||
let value = Setting.value(name);
|
||||
if (typeof value === 'object' || Array.isArray(value)) value = JSON.stringify(value);
|
||||
if (md.secure && value) value = '********';
|
||||
|
||||
if (Setting.isEnum(name)) {
|
||||
return _('%s = %s (%s)', name, value, Setting.enumOptionsDoc(name));
|
||||
} else {
|
||||
@@ -33,6 +37,7 @@ class Command extends BaseCommand {
|
||||
|
||||
if (!args.name && !args.value) {
|
||||
let keys = Setting.keys(!verbose, 'cli');
|
||||
keys.sort();
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const value = Setting.value(keys[i]);
|
||||
if (!verbose && !value) continue;
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
import { time } from 'lib/time-utils.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -16,8 +16,9 @@ class Command extends BaseCommand {
|
||||
return _('Marks a to-do as done.');
|
||||
}
|
||||
|
||||
static async handleAction(args, isCompleted) {
|
||||
static async handleAction(commandInstance, args, isCompleted) {
|
||||
const note = await app().loadItem(BaseModel.TYPE_NOTE, args.note);
|
||||
commandInstance.encryptionCheck(note);
|
||||
if (!note) throw new Error(_('Cannot find "%s".', args.note));
|
||||
if (!note.is_todo) throw new Error(_('Note is not a to-do: "%s"', args.note));
|
||||
|
||||
@@ -32,7 +33,7 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
await Command.handleAction(args, true);
|
||||
await Command.handleAction(this, args, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
import { Tag } from 'lib/models/tag.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
218
CliClient/app/command-e2ee.js
Normal file
@@ -0,0 +1,218 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
const EncryptionService = require('lib/services/EncryptionService');
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||
const MasterKey = require('lib/models/MasterKey');
|
||||
const BaseItem = require('lib/models/BaseItem');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
usage() {
|
||||
return 'e2ee <command> [path]';
|
||||
}
|
||||
|
||||
description() {
|
||||
return _('Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status` and `target-status`.');
|
||||
}
|
||||
|
||||
options() {
|
||||
return [
|
||||
// This is here mostly for testing - shouldn't be used
|
||||
['-p, --password <password>', 'Use this password as master password (For security reasons, it is not recommended to use this option).'],
|
||||
['-v, --verbose', 'More verbose output for the `target-status` command'],
|
||||
];
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
// change-password
|
||||
|
||||
const options = args.options;
|
||||
|
||||
if (args.command === 'enable') {
|
||||
const password = options.password ? options.password.toString() : await this.prompt(_('Enter master password:'), { type: 'string', secure: true });
|
||||
if (!password) {
|
||||
this.stdout(_('Operation cancelled'));
|
||||
return;
|
||||
}
|
||||
|
||||
await EncryptionService.instance().generateMasterKeyAndEnableEncryption(password);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.command === 'disable') {
|
||||
await EncryptionService.instance().disableEncryption();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.command === 'decrypt') {
|
||||
while (true) {
|
||||
try {
|
||||
if (args.path) {
|
||||
const plainText = await EncryptionService.instance().decryptString(args.path);
|
||||
this.stdout(plainText);
|
||||
return;
|
||||
} else {
|
||||
if (process.stdin.isTTY) {
|
||||
this.stdout(_('Starting decryption... Please wait as it may take several minutes depending on how much there is to decrypt.'));
|
||||
await DecryptionWorker.instance().start();
|
||||
this.stdout(_('Completed decryption.'));
|
||||
return;
|
||||
} else {
|
||||
// var repl = require("repl");
|
||||
// var r = repl.start("node> ");
|
||||
|
||||
const text = await new Promise((accept, reject) => {
|
||||
var buffer = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', function(chunk) {
|
||||
buffer += chunk;
|
||||
// process.stdout.write(chunk);
|
||||
});
|
||||
process.stdin.on('end', function() {
|
||||
accept(buffer.trim());
|
||||
});
|
||||
});
|
||||
|
||||
if (text.length > 0) {
|
||||
var cipherText = text;
|
||||
try {
|
||||
var item = await BaseItem.unserialize(text);
|
||||
cipherText = item.encryption_cipher_text;
|
||||
} catch (error) {
|
||||
// we already got the pure cipher text
|
||||
}
|
||||
const plainText = await EncryptionService.instance().decryptString(cipherText);
|
||||
this.stdout(plainText);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'masterKeyNotLoaded') {
|
||||
const masterKeyId = error.masterKeyId;
|
||||
const password = await this.prompt(_('Enter master password:'), { type: 'string', secure: true });
|
||||
if (!password) {
|
||||
this.stdout(_('Operation cancelled'));
|
||||
return;
|
||||
}
|
||||
Setting.setObjectKey('encryption.passwordCache', masterKeyId, password);
|
||||
await EncryptionService.instance().loadMasterKeysFromSettings();
|
||||
continue;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.command === 'status') {
|
||||
this.stdout(_('Encryption is: %s', Setting.value('encryption.enabled') ? _('Enabled') : _('Disabled')));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.command === 'target-status') {
|
||||
const fs = require('fs-extra');
|
||||
const pathUtils = require('lib/path-utils.js');
|
||||
const fsDriver = new (require('lib/fs-driver-node.js').FsDriverNode)();
|
||||
|
||||
const targetPath = args.path;
|
||||
if (!targetPath) throw new Error('Please specify the sync target path.');
|
||||
|
||||
const dirPaths = function(targetPath) {
|
||||
let paths = [];
|
||||
fs.readdirSync(targetPath).forEach((path) => {
|
||||
paths.push(path);
|
||||
});
|
||||
return paths;
|
||||
}
|
||||
|
||||
let itemCount = 0;
|
||||
let resourceCount = 0;
|
||||
let encryptedItemCount = 0;
|
||||
let encryptedResourceCount = 0;
|
||||
let otherItemCount = 0;
|
||||
|
||||
let encryptedPaths = [];
|
||||
let decryptedPaths = [];
|
||||
|
||||
let paths = dirPaths(targetPath);
|
||||
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
const path = paths[i];
|
||||
const fullPath = targetPath + '/' + path;
|
||||
const stat = await fs.stat(fullPath);
|
||||
|
||||
// this.stdout(fullPath);
|
||||
|
||||
if (path === '.resource') {
|
||||
let resourcePaths = dirPaths(fullPath);
|
||||
for (let j = 0; j < resourcePaths.length; j++) {
|
||||
const resourcePath = resourcePaths[j];
|
||||
resourceCount++;
|
||||
const fullResourcePath = fullPath + '/' + resourcePath;
|
||||
const isEncrypted = await EncryptionService.instance().fileIsEncrypted(fullResourcePath);
|
||||
if (isEncrypted) {
|
||||
encryptedResourceCount++;
|
||||
encryptedPaths.push(fullResourcePath);
|
||||
} else {
|
||||
decryptedPaths.push(fullResourcePath);
|
||||
}
|
||||
}
|
||||
} else if (stat.isDirectory()) {
|
||||
continue;
|
||||
} else {
|
||||
const content = await fs.readFile(fullPath, 'utf8');
|
||||
const item = await BaseItem.unserialize(content);
|
||||
const ItemClass = BaseItem.itemClass(item);
|
||||
|
||||
if (!ItemClass.encryptionSupported()) {
|
||||
otherItemCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
itemCount++;
|
||||
|
||||
const isEncrypted = await EncryptionService.instance().itemIsEncrypted(item);
|
||||
|
||||
if (isEncrypted) {
|
||||
encryptedItemCount++;
|
||||
encryptedPaths.push(fullPath);
|
||||
} else {
|
||||
decryptedPaths.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.stdout('Encrypted items: ' + encryptedItemCount + '/' + itemCount);
|
||||
this.stdout('Encrypted resources: ' + encryptedResourceCount + '/' + resourceCount);
|
||||
this.stdout('Other items (never encrypted): ' + otherItemCount);
|
||||
|
||||
if (options.verbose) {
|
||||
this.stdout('');
|
||||
this.stdout('# Encrypted paths');
|
||||
this.stdout('');
|
||||
for (let i = 0; i < encryptedPaths.length; i++) {
|
||||
const path = encryptedPaths[i];
|
||||
this.stdout(path);
|
||||
}
|
||||
|
||||
this.stdout('');
|
||||
this.stdout('# Decrypted paths');
|
||||
this.stdout('');
|
||||
for (let i = 0; i < decryptedPaths.length; i++) {
|
||||
const path = decryptedPaths[i];
|
||||
this.stdout(path);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Command;
|
@@ -1,14 +1,14 @@
|
||||
import fs from 'fs-extra';
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { uuid } from 'lib/uuid.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
import { Setting } from 'lib/models/setting.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { cliUtils } from './cli-utils.js';
|
||||
import { time } from 'lib/time-utils.js';
|
||||
const fs = require('fs-extra');
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { uuid } = require('lib/uuid.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -44,6 +44,8 @@ class Command extends BaseCommand {
|
||||
if (!app().currentFolder()) throw new Error(_('No active notebook.'));
|
||||
let note = await app().loadItem(BaseModel.TYPE_NOTE, title);
|
||||
|
||||
this.encryptionCheck(note);
|
||||
|
||||
if (!note) {
|
||||
const ok = await this.prompt(_('Note does not exist: "%s". Create it?', title));
|
||||
if (!ok) return;
|
||||
@@ -75,13 +77,15 @@ class Command extends BaseCommand {
|
||||
this.logger().info('Disabling fullscreen...');
|
||||
|
||||
app().gui().showModalOverlay(_('Starting to edit note. Close the editor to get back to the prompt.'));
|
||||
app().gui().forceRender();
|
||||
const termState = app().gui().term().saveState();
|
||||
await app().gui().forceRender();
|
||||
const termState = app().gui().termSaveState();
|
||||
|
||||
const spawnSync = require('child_process').spawnSync;
|
||||
spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
|
||||
const result = spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
|
||||
|
||||
app().gui().term().restoreState(termState);
|
||||
if (result.error) this.stdout(_('Error opening note in editor: %s', result.error.message));
|
||||
|
||||
app().gui().termRestoreState(termState);
|
||||
app().gui().hideModalOverlay();
|
||||
app().gui().forceRender();
|
||||
|
||||
@@ -98,8 +102,8 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
this.dispatch({
|
||||
type: 'NOTES_SELECT',
|
||||
noteId: note.id,
|
||||
type: 'NOTE_SELECT',
|
||||
id: note.id,
|
||||
});
|
||||
|
||||
await onFinishedEditing();
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -12,6 +12,10 @@ class Command extends BaseCommand {
|
||||
return _('Exits the application.');
|
||||
}
|
||||
|
||||
compatibleUis() {
|
||||
return ['gui'];
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
await app().exit();
|
||||
}
|
||||
|
36
CliClient/app/command-export-sync-status.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const { app } = require('./app.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { ReportService } = require('lib/services/report.js');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
usage() {
|
||||
return 'export-sync-status';
|
||||
}
|
||||
|
||||
description() {
|
||||
return 'Export sync status';
|
||||
}
|
||||
|
||||
hidden() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
const service = new ReportService();
|
||||
const csv = await service.basicItemList({ format: 'csv' });
|
||||
const filePath = Setting.value('profileDir') + '/syncReport-' + (new Date()).getTime() + '.csv';
|
||||
await fs.writeFileSync(filePath, csv);
|
||||
this.stdout('Sync status exported to ' + filePath);
|
||||
|
||||
app().gui().showConsole();
|
||||
app().gui().maximizeConsole();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Command;
|
@@ -1,24 +1,30 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { Exporter } from 'lib/services/exporter.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
import { reg } from 'lib/registry.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import fs from 'fs-extra';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const InteropService = require('lib/services/InteropService.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
usage() {
|
||||
return 'export <directory>';
|
||||
return 'export <path>';
|
||||
}
|
||||
|
||||
description() {
|
||||
return _('Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.');
|
||||
return _('Exports Joplin data to the given path. By default, it will export the complete database including notebooks, notes, tags and resources.');
|
||||
}
|
||||
|
||||
options() {
|
||||
const service = new InteropService();
|
||||
const formats = service.modules()
|
||||
.filter(m => m.type === 'exporter')
|
||||
.map(m => m.format + (m.description ? ' (' + m.description + ')' : ''));
|
||||
|
||||
return [
|
||||
['--format <format>', _('Destination format: %s', formats.join(', '))],
|
||||
['--note <note>', _('Exports only the given note.')],
|
||||
['--notebook <notebook>', _('Exports only the given notebook.')],
|
||||
];
|
||||
@@ -26,13 +32,9 @@ class Command extends BaseCommand {
|
||||
|
||||
async action(args) {
|
||||
let exportOptions = {};
|
||||
exportOptions.destDir = args.directory;
|
||||
exportOptions.writeFile = (filePath, data) => {
|
||||
return fs.writeFile(filePath, data);
|
||||
};
|
||||
exportOptions.copyFile = (source, dest) => {
|
||||
return fs.copy(source, dest, { overwrite: true });
|
||||
};
|
||||
exportOptions.path = args.path;
|
||||
|
||||
exportOptions.format = args.options.format ? args.options.format : 'jex';
|
||||
|
||||
if (args.options.note) {
|
||||
|
||||
@@ -48,10 +50,10 @@ class Command extends BaseCommand {
|
||||
|
||||
}
|
||||
|
||||
const exporter = new Exporter();
|
||||
const result = await exporter.export(exportOptions);
|
||||
const service = new InteropService();
|
||||
const result = await service.export(exportOptions);
|
||||
|
||||
reg.logger().info('Export result: ', result);
|
||||
result.warnings.map((w) => this.stdout(w));
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { renderCommandHelp } from './help-utils.js';
|
||||
import { Database } from 'lib/database.js';
|
||||
import { Setting } from 'lib/models/setting.js';
|
||||
import { wrap } from 'lib/string-utils.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { cliUtils } from './cli-utils.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { renderCommandHelp } = require('./help-utils.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { wrap } = require('lib/string-utils.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -18,7 +18,7 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
allCommands() {
|
||||
const commands = app().commands();
|
||||
const commands = app().commands(app().uiType());
|
||||
let output = [];
|
||||
for (let n in commands) {
|
||||
if (!commands.hasOwnProperty(n)) continue;
|
||||
@@ -36,21 +36,22 @@ class Command extends BaseCommand {
|
||||
async action(args) {
|
||||
const stdoutWidth = app().commandStdoutMaxWidth();
|
||||
|
||||
if (args.command === 'shortcuts') {
|
||||
if (args.command === 'shortcuts' || args.command === 'keymap') {
|
||||
this.stdout(_('For information on how to customise the shortcuts please visit %s', 'https://joplin.cozic.net/terminal/#shortcuts'));
|
||||
this.stdout('');
|
||||
|
||||
if (app().gui().isDummy()) {
|
||||
throw new Error(_('Shortcuts are not available in CLI mode.'));
|
||||
}
|
||||
|
||||
const shortcuts = app().gui().shortcuts();
|
||||
const keymap = app().gui().keymap();
|
||||
|
||||
let rows = [];
|
||||
|
||||
for (let n in shortcuts) {
|
||||
if (!shortcuts.hasOwnProperty(n)) continue;
|
||||
const shortcut = shortcuts[n];
|
||||
if (!shortcut.description) continue;
|
||||
n = shortcut.friendlyName ? shortcut.friendlyName : n;
|
||||
rows.push([n, shortcut.description()]);
|
||||
for (let i = 0; i < keymap.length; i++) {
|
||||
const item = keymap[i];
|
||||
const keys = item.keys.map((k) => k === ' ' ? '(SPACE)' : k);
|
||||
rows.push([keys.join(', '), item.command]);
|
||||
}
|
||||
|
||||
cliUtils.printArray(this.stdout.bind(this), rows);
|
||||
@@ -65,20 +66,20 @@ class Command extends BaseCommand {
|
||||
} else {
|
||||
const commandNames = this.allCommands().map((a) => a.name());
|
||||
|
||||
this.stdout(_('Type `help [command]` for more information about a command.'));
|
||||
this.stdout(_('Type `help [command]` for more information about a command; or type `help all` for the complete usage information.'));
|
||||
this.stdout('');
|
||||
this.stdout(_('The possible commands are:'));
|
||||
this.stdout('');
|
||||
this.stdout(commandNames.join(', '));
|
||||
this.stdout('');
|
||||
this.stdout(_('In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.'));
|
||||
this.stdout(_('In any command, a note or notebook can be referred to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.'));
|
||||
this.stdout('');
|
||||
this.stdout(_('To move from one pane to another, press Tab or Shift+Tab.'));
|
||||
this.stdout(_('Use the arrows and page up/down to scroll the lists and text areas (including this console).'));
|
||||
this.stdout(_('To maximise/minimise the console, press "TC".'));
|
||||
this.stdout(_('To maximise/minimise the console, press "tc".'));
|
||||
this.stdout(_('To enter command line mode, press ":"'));
|
||||
this.stdout(_('To exit command line mode, press ESCAPE'));
|
||||
this.stdout(_('For the complete list of available keyboard shortcuts, type `help shortcuts`'));
|
||||
this.stdout(_('For the list of keyboard shortcuts and config options, type `help keymap`'));
|
||||
}
|
||||
|
||||
app().gui().showConsole();
|
||||
|
@@ -1,68 +0,0 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { importEnex } from 'import-enex';
|
||||
import { filename, basename } from 'lib/path-utils.js';
|
||||
import { cliUtils } from './cli-utils.js';
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
usage() {
|
||||
return 'import-enex <file> [notebook]';
|
||||
}
|
||||
|
||||
description() {
|
||||
return _('Imports an Evernote notebook file (.enex file).');
|
||||
}
|
||||
|
||||
options() {
|
||||
return [
|
||||
['-f, --force', _('Do not ask for confirmation.')],
|
||||
];
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
let filePath = args.file;
|
||||
let folder = null;
|
||||
let folderTitle = args['notebook'];
|
||||
let force = args.options.force === true;
|
||||
|
||||
if (!folderTitle) folderTitle = filename(filePath);
|
||||
folder = await Folder.loadByField('title', folderTitle);
|
||||
const msg = folder ? _('File "%s" will be imported into existing notebook "%s". Continue?', basename(filePath), folderTitle) : _('New notebook "%s" will be created and file "%s" will be imported into it. Continue?', folderTitle, basename(filePath));
|
||||
const ok = force ? true : await this.prompt(msg);
|
||||
if (!ok) return;
|
||||
|
||||
let lastProgress = '';
|
||||
|
||||
let options = {
|
||||
onProgress: (progressState) => {
|
||||
let line = [];
|
||||
line.push(_('Found: %d.', progressState.loaded));
|
||||
line.push(_('Created: %d.', progressState.created));
|
||||
if (progressState.updated) line.push(_('Updated: %d.', progressState.updated));
|
||||
if (progressState.skipped) line.push(_('Skipped: %d.', progressState.skipped));
|
||||
if (progressState.resourcesCreated) line.push(_('Resources: %d.', progressState.resourcesCreated));
|
||||
if (progressState.notesTagged) line.push(_('Tagged: %d.', progressState.notesTagged));
|
||||
lastProgress = line.join(' ');
|
||||
cliUtils.redraw(lastProgress);
|
||||
},
|
||||
onError: (error) => {
|
||||
let s = error.trace ? error.trace : error.toString();
|
||||
this.stdout(s);
|
||||
},
|
||||
}
|
||||
|
||||
folder = !folder ? await Folder.save({ title: folderTitle }) : folder;
|
||||
|
||||
app().gui().showConsole();
|
||||
this.stdout(_('Importing notes...'));
|
||||
await importEnex(folder.id, filePath, options);
|
||||
cliUtils.redrawDone();
|
||||
this.stdout(_('The notes have been imported: %s', lastProgress));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Command;
|
75
CliClient/app/command-import.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const InteropService = require('lib/services/InteropService.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { filename, basename, fileExtension } = require('lib/path-utils.js');
|
||||
const { importEnex } = require('lib/import-enex');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
usage() {
|
||||
return 'import <path> [notebook]';
|
||||
}
|
||||
|
||||
description() {
|
||||
return _('Imports data into Joplin.');
|
||||
}
|
||||
|
||||
options() {
|
||||
const service = new InteropService();
|
||||
const formats = service.modules().filter(m => m.type === 'importer').map(m => m.format);
|
||||
|
||||
return [
|
||||
['--format <format>', _('Source format: %s', (['auto'].concat(formats)).join(', '))],
|
||||
['-f, --force', _('Do not ask for confirmation.')],
|
||||
];
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
let folder = await app().loadItem(BaseModel.TYPE_FOLDER, args.notebook);
|
||||
|
||||
if (args.notebook && !folder) throw new Error(_('Cannot find "%s".', args.notebook));
|
||||
|
||||
const importOptions = {};
|
||||
importOptions.path = args.path;
|
||||
importOptions.format = args.options.format ? args.options.format : 'auto';
|
||||
importOptions.destinationFolderId = folder ? folder.id : null;
|
||||
|
||||
let lastProgress = '';
|
||||
|
||||
// onProgress/onError supported by Enex import only
|
||||
|
||||
importOptions.onProgress = (progressState) => {
|
||||
let line = [];
|
||||
line.push(_('Found: %d.', progressState.loaded));
|
||||
line.push(_('Created: %d.', progressState.created));
|
||||
if (progressState.updated) line.push(_('Updated: %d.', progressState.updated));
|
||||
if (progressState.skipped) line.push(_('Skipped: %d.', progressState.skipped));
|
||||
if (progressState.resourcesCreated) line.push(_('Resources: %d.', progressState.resourcesCreated));
|
||||
if (progressState.notesTagged) line.push(_('Tagged: %d.', progressState.notesTagged));
|
||||
lastProgress = line.join(' ');
|
||||
cliUtils.redraw(lastProgress);
|
||||
};
|
||||
|
||||
importOptions.onError = (error) => {
|
||||
let s = error.trace ? error.trace : error.toString();
|
||||
this.stdout(s);
|
||||
};
|
||||
|
||||
app().gui().showConsole();
|
||||
this.stdout(_('Importing notes...'));
|
||||
const service = new InteropService();
|
||||
const result = await service.import(importOptions);
|
||||
result.warnings.map((w) => this.stdout(w));
|
||||
cliUtils.redrawDone();
|
||||
if (lastProgress) this.stdout(_('The notes have been imported: %s', lastProgress));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Command;
|
@@ -1,13 +1,13 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { Setting } from 'lib/models/setting.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
import { sprintf } from 'sprintf-js';
|
||||
import { time } from 'lib/time-utils.js';
|
||||
import { cliUtils } from './cli-utils.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -22,7 +22,7 @@ class Command extends BaseCommand {
|
||||
enabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
options() {
|
||||
return [
|
||||
['-n, --limit <num>', _('Displays only the first top <num> notes.')],
|
||||
@@ -93,7 +93,7 @@ class Command extends BaseCommand {
|
||||
row.push(await Folder.noteCount(item.id));
|
||||
}
|
||||
|
||||
row.push(time.unixMsToLocalDateTime(item.user_updated_time));
|
||||
row.push(time.formatMsToLocal(item.user_updated_time));
|
||||
}
|
||||
|
||||
let title = item.title;
|
||||
@@ -123,4 +123,4 @@ class Command extends BaseCommand {
|
||||
|
||||
}
|
||||
|
||||
module.exports = Command;
|
||||
module.exports = Command;
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { reg } from 'lib/registry.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -14,10 +14,6 @@ class Command extends BaseCommand {
|
||||
return _('Creates a new notebook.');
|
||||
}
|
||||
|
||||
aliases() {
|
||||
return ['mkdir'];
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
let folder = await Folder.save({ title: args['new-notebook'] }, { userSideValidation: true });
|
||||
app().switchCurrentFolder(folder);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -20,6 +20,7 @@ class Command extends BaseCommand {
|
||||
const name = args['name'];
|
||||
|
||||
const item = await app().loadItem('folderOrNote', pattern);
|
||||
this.encryptionCheck(item);
|
||||
if (!item) throw new Error(_('Cannot find "%s".', pattern));
|
||||
|
||||
const newItem = {
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { BaseItem } from 'lib/models/base-item.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { cliUtils } from './cli-utils.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -29,7 +29,7 @@ class Command extends BaseCommand {
|
||||
|
||||
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
|
||||
if (!folder) throw new Error(_('Cannot find "%s".', pattern));
|
||||
const ok = force ? true : await this.prompt(_('Delete notebook "%s"?', folder.title), { booleanAnswerDefault: 'n' });
|
||||
const ok = force ? true : await this.prompt(_('Delete notebook? All notes and sub-notebooks within this notebook will also be deleted.'), { booleanAnswerDefault: 'n' });
|
||||
if (!ok) return;
|
||||
|
||||
await Folder.delete(folder.id);
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { BaseItem } from 'lib/models/base-item.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { cliUtils } from './cli-utils.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
import { sprintf } from 'sprintf-js';
|
||||
import { time } from 'lib/time-utils.js';
|
||||
import { uuid } from 'lib/uuid.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { uuid } = require('lib/uuid.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
import { BaseItem } from 'lib/models/base-item.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -12,16 +13,16 @@ class Command extends BaseCommand {
|
||||
return 'set <note> <name> [value]';
|
||||
}
|
||||
|
||||
enabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
description() {
|
||||
return _('Sets the property <name> of the given <note> to the given [value].');
|
||||
}
|
||||
const fields = Note.fields();
|
||||
const s = [];
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const f = fields[i];
|
||||
if (f.name === 'id') continue;
|
||||
s.push(f.name + ' (' + Database.enumName('fieldType', f.type) + ')');
|
||||
}
|
||||
|
||||
hidden() {
|
||||
return true;
|
||||
return _('Sets the property <name> of the given <note> to the given [value]. Possible properties are:\n\n%s', s.join(', '));
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
@@ -34,6 +35,8 @@ class Command extends BaseCommand {
|
||||
if (!notes.length) throw new Error(_('Cannot find "%s".', title));
|
||||
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
this.encryptionCheck(notes[i]);
|
||||
|
||||
let newNote = {
|
||||
id: notes[i].id,
|
||||
type_: notes[i].type_,
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { Database } from 'lib/database.js';
|
||||
import { app } from './app.js';
|
||||
import { Setting } from 'lib/models/setting.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { ReportService } from 'lib/services/report.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const { app } = require('./app.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { ReportService } = require('lib/services/report.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,22 +1,23 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { OneDriveApiNodeUtils } from './onedrive-api-node-utils.js';
|
||||
import { Setting } from 'lib/models/setting.js';
|
||||
import { BaseItem } from 'lib/models/base-item.js';
|
||||
import { Synchronizer } from 'lib/synchronizer.js';
|
||||
import { reg } from 'lib/registry.js';
|
||||
import { cliUtils } from './cli-utils.js';
|
||||
import md5 from 'md5';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { OneDriveApiNodeUtils } = require('./onedrive-api-node-utils.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const ResourceFetcher = require('lib/services/ResourceFetcher');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
const md5 = require('md5');
|
||||
const locker = require('proper-lockfile');
|
||||
const fs = require('fs-extra');
|
||||
const osTmpdir = require('os-tmpdir');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.syncTarget_ = null;
|
||||
this.syncTargetId_ = null;
|
||||
this.releaseLockFn_ = null;
|
||||
this.oneDriveApiUtils_ = null;
|
||||
}
|
||||
@@ -32,7 +33,6 @@ class Command extends BaseCommand {
|
||||
options() {
|
||||
return [
|
||||
['--target <target>', _('Sync to provided target (defaults to sync.target config value)')],
|
||||
['--random-failures', 'For debugging purposes. Do not use.'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -62,11 +62,63 @@ class Command extends BaseCommand {
|
||||
});
|
||||
}
|
||||
|
||||
async doAuth() {
|
||||
const syncTarget = reg.syncTarget(this.syncTargetId_);
|
||||
const syncTargetMd = SyncTargetRegistry.idToMetadata(this.syncTargetId_);
|
||||
|
||||
if (this.syncTargetId_ === 3 || this.syncTargetId_ === 4) { // OneDrive
|
||||
this.oneDriveApiUtils_ = new OneDriveApiNodeUtils(syncTarget.api());
|
||||
const auth = await this.oneDriveApiUtils_.oauthDance({
|
||||
log: (...s) => { return this.stdout(...s); }
|
||||
});
|
||||
this.oneDriveApiUtils_ = null;
|
||||
|
||||
Setting.setValue('sync.' + this.syncTargetId_ + '.auth', auth ? JSON.stringify(auth) : null);
|
||||
if (!auth) {
|
||||
this.stdout(_('Authentication was not completed (did not receive an authentication token).'));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (syncTargetMd.name === 'dropbox') { // Dropbox
|
||||
const api = await syncTarget.api();
|
||||
const loginUrl = api.loginUrl();
|
||||
this.stdout(_('To allow Joplin to synchronise with Dropbox, please follow the steps below:'));
|
||||
this.stdout(_('Step 1: Open this URL in your browser to authorise the application:'));
|
||||
this.stdout(loginUrl);
|
||||
const authCode = await this.prompt(_('Step 2: Enter the code provided by Dropbox:'), { type: 'string' });
|
||||
if (!authCode) {
|
||||
this.stdout(_('Authentication was not completed (did not receive an authentication token).'));
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = await api.execAuthToken(authCode);
|
||||
Setting.setValue('sync.' + this.syncTargetId_ + '.auth', response.access_token);
|
||||
api.setAuthToken(response.access_token);
|
||||
return true;
|
||||
}
|
||||
|
||||
this.stdout(_('Not authentified with %s. Please provide any missing credentials.', syncTargetMd.label));
|
||||
return false;
|
||||
}
|
||||
|
||||
cancelAuth() {
|
||||
if (this.oneDriveApiUtils_) {
|
||||
this.oneDriveApiUtils_.cancelOAuthDance();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
doingAuth() {
|
||||
return !!this.oneDriveApiUtils_;
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
this.releaseLockFn_ = null;
|
||||
|
||||
// Lock is unique per profile/database
|
||||
const lockFilePath = osTmpdir() + '/synclock_' + md5(Setting.value('profileDir'));
|
||||
// TODO: use SQLite database to do lock?
|
||||
const lockFilePath = require('os').tmpdir() + '/synclock_' + md5(escape(Setting.value('profileDir'))); // https://github.com/pvorb/node-md5/issues/41
|
||||
if (!await fs.pathExists(lockFilePath)) await fs.writeFile(lockFilePath, 'synclock');
|
||||
|
||||
try {
|
||||
@@ -91,25 +143,20 @@ class Command extends BaseCommand {
|
||||
};
|
||||
|
||||
try {
|
||||
this.syncTarget_ = Setting.value('sync.target');
|
||||
if (args.options.target) this.syncTarget_ = args.options.target;
|
||||
this.syncTargetId_ = Setting.value('sync.target');
|
||||
if (args.options.target) this.syncTargetId_ = args.options.target;
|
||||
|
||||
if (this.syncTarget_ == Setting.SYNC_TARGET_ONEDRIVE && !reg.syncHasAuth(this.syncTarget_)) {
|
||||
const syncTarget = reg.syncTarget(this.syncTargetId_);
|
||||
|
||||
if (!await syncTarget.isAuthenticated()) {
|
||||
app().gui().showConsole();
|
||||
app().gui().maximizeConsole();
|
||||
this.oneDriveApiUtils_ = new OneDriveApiNodeUtils(reg.oneDriveApi());
|
||||
const auth = await this.oneDriveApiUtils_.oauthDance({
|
||||
log: (...s) => { return this.stdout(...s); }
|
||||
});
|
||||
this.oneDriveApiUtils_ = null;
|
||||
Setting.setValue('sync.3.auth', auth ? JSON.stringify(auth) : null);
|
||||
if (!auth) {
|
||||
this.stdout(_('Authentication was not completed (did not receive an authentication token).'));
|
||||
return cleanUp();
|
||||
}
|
||||
|
||||
const authDone = await this.doAuth();
|
||||
if (!authDone) return cleanUp();
|
||||
}
|
||||
|
||||
let sync = await reg.synchronizer(this.syncTarget_);
|
||||
const sync = await syncTarget.synchronizer();
|
||||
|
||||
let options = {
|
||||
onProgress: (report) => {
|
||||
@@ -120,16 +167,15 @@ class Command extends BaseCommand {
|
||||
cliUtils.redrawDone();
|
||||
this.stdout(msg);
|
||||
},
|
||||
randomFailures: args.options['random-failures'] === true,
|
||||
};
|
||||
|
||||
this.stdout(_('Synchronisation target: %s (%s)', Setting.enumOptionLabel('sync.target', this.syncTarget_), this.syncTarget_));
|
||||
this.stdout(_('Synchronisation target: %s (%s)', Setting.enumOptionLabel('sync.target', this.syncTargetId_), this.syncTargetId_));
|
||||
|
||||
if (!sync) throw new Error(_('Cannot initialize synchroniser.'));
|
||||
|
||||
this.stdout(_('Starting synchronisation...'));
|
||||
|
||||
const contextKey = 'sync.' + this.syncTarget_ + '.context';
|
||||
const contextKey = 'sync.' + this.syncTargetId_ + '.context';
|
||||
let context = Setting.value(contextKey);
|
||||
|
||||
context = context ? JSON.parse(context) : {};
|
||||
@@ -146,6 +192,15 @@ class Command extends BaseCommand {
|
||||
}
|
||||
}
|
||||
|
||||
// When using the tool in command line mode, the ResourceFetcher service is
|
||||
// not going to be running in the background, so the resources need to be
|
||||
// explicitely downloaded below.
|
||||
if (!app().hasGui()) {
|
||||
this.stdout(_('Downloading resources...'));
|
||||
await ResourceFetcher.instance().fetchAll();
|
||||
await ResourceFetcher.instance().waitForAllFinished();
|
||||
}
|
||||
|
||||
await app().refreshCurrentFolder();
|
||||
} catch (error) {
|
||||
cleanUp();
|
||||
@@ -156,26 +211,28 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
if (this.oneDriveApiUtils_) {
|
||||
this.oneDriveApiUtils_.cancelOAuthDance();
|
||||
if (this.doingAuth()) {
|
||||
this.cancelAuth();
|
||||
return;
|
||||
}
|
||||
|
||||
const target = this.syncTarget_ ? this.syncTarget_ : Setting.value('sync.target');
|
||||
const syncTargetId = this.syncTargetId_ ? this.syncTargetId_ : Setting.value('sync.target');
|
||||
|
||||
cliUtils.redrawDone();
|
||||
|
||||
this.stdout(_('Cancelling... Please wait.'));
|
||||
|
||||
if (reg.syncHasAuth(target)) {
|
||||
let sync = await reg.synchronizer(target);
|
||||
const syncTarget = reg.syncTarget(syncTargetId);
|
||||
|
||||
if (await syncTarget.isAuthenticated()) {
|
||||
const sync = await syncTarget.synchronizer();
|
||||
if (sync) await sync.cancel();
|
||||
} else {
|
||||
if (this.releaseLockFn_) this.releaseLockFn_();
|
||||
this.releaseLockFn_ = null;
|
||||
}
|
||||
|
||||
this.syncTarget_ = null;
|
||||
this.syncTargetId_ = null;
|
||||
}
|
||||
|
||||
cancellable() {
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { Tag } from 'lib/models/tag.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -11,11 +12,19 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
description() {
|
||||
return _('<tag-command> can be "add", "remove" or "list" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.');
|
||||
return _('<tag-command> can be "add", "remove" or "list" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags (use -l for long option).');
|
||||
}
|
||||
|
||||
|
||||
options() {
|
||||
return [
|
||||
['-l, --long', _('Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE')],
|
||||
];
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
let tag = null;
|
||||
let options = args.options;
|
||||
|
||||
if (args.tag) tag = await app().loadItem(BaseModel.TYPE_TAG, args.tag);
|
||||
let notes = [];
|
||||
if (args.note) {
|
||||
@@ -28,7 +37,7 @@ class Command extends BaseCommand {
|
||||
|
||||
if (command == 'add') {
|
||||
if (!notes.length) throw new Error(_('Cannot find "%s".', args.note));
|
||||
if (!tag) tag = await Tag.save({ title: args.tag });
|
||||
if (!tag) tag = await Tag.save({ title: args.tag }, { userSideValidation: true });
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
await Tag.addNote(tag.id, notes[i].id);
|
||||
}
|
||||
@@ -41,7 +50,28 @@ class Command extends BaseCommand {
|
||||
} else if (command == 'list') {
|
||||
if (tag) {
|
||||
let notes = await Tag.notes(tag.id);
|
||||
notes.map((note) => { this.stdout(note.title); });
|
||||
notes.map((note) => {
|
||||
let line = '';
|
||||
if (options.long) {
|
||||
line += BaseModel.shortId(note.id);
|
||||
line += ' ';
|
||||
line += time.formatMsToLocal(note.user_updated_time);
|
||||
line += ' ';
|
||||
}
|
||||
if (note.is_todo) {
|
||||
line += '[';
|
||||
if (note.todo_completed) {
|
||||
line += 'X';
|
||||
} else {
|
||||
line += ' ';
|
||||
}
|
||||
line += '] ';
|
||||
} else {
|
||||
line += ' ';
|
||||
}
|
||||
line += note.title;
|
||||
this.stdout(line);
|
||||
});
|
||||
} else {
|
||||
let tags = await Tag.all();
|
||||
tags.map((tag) => { this.stdout(tag.title); });
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
import { time } from 'lib/time-utils.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -25,6 +25,8 @@ class Command extends BaseCommand {
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
const note = notes[i];
|
||||
|
||||
this.encryptionCheck(note);
|
||||
|
||||
let toSave = {
|
||||
id: note.id,
|
||||
};
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
import { time } from 'lib/time-utils.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
const CommandDone = require('./command-done.js');
|
||||
|
||||
@@ -19,7 +19,7 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
await CommandDone.handleAction(args, false);
|
||||
await CommandDone.handleAction(this, args, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { app } from './app.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -18,8 +18,8 @@ class Command extends BaseCommand {
|
||||
return { data: autocompleteFolders };
|
||||
}
|
||||
|
||||
enabled() {
|
||||
return false;
|
||||
compatibleUis() {
|
||||
return ['cli'];
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { BaseCommand } from './base-command.js';
|
||||
import { Setting } from 'lib/models/setting.js'
|
||||
import { _ } from 'lib/locale.js';
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,20 +0,0 @@
|
||||
const fs = require('fs-extra');
|
||||
|
||||
class FsDriverNode {
|
||||
|
||||
appendFileSync(path, string) {
|
||||
return fs.appendFileSync(path, string);
|
||||
}
|
||||
|
||||
writeBinaryFile(path, content) {
|
||||
let buffer = new Buffer(content);
|
||||
return fs.writeFile(path, buffer);
|
||||
}
|
||||
|
||||
readFile(path) {
|
||||
return fs.readFile(path);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports.FsDriverNode = FsDriverNode;
|