Compare commits
879 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e65707b7f | ||
|
|
eddf34ce55 | ||
|
|
0fb43aa33c | ||
|
|
b273b02da4 | ||
|
|
0b997f9673 | ||
|
|
bdb2ae57a8 | ||
|
|
b5e563679a | ||
|
|
992c104262 | ||
|
|
555154031e | ||
|
|
acb083e429 | ||
|
|
4a527d192d | ||
|
|
39c3bf17dd | ||
|
|
afc8c84f41 | ||
|
|
a085e04c4d | ||
|
|
2f82b0db34 | ||
|
|
0124c2b17d | ||
|
|
d2cfbbc9f3 | ||
|
|
c59f48822c | ||
|
|
b2d6584c4a | ||
|
|
8f7cafb240 | ||
|
|
08fd0f15ff | ||
|
|
dbb1bfe587 | ||
|
|
fe4b7a5a85 | ||
|
|
d8df5d76e5 | ||
|
|
b65dcc5ade | ||
|
|
a5c387a19e | ||
|
|
07c38d9a9f | ||
|
|
20ac8a444b | ||
|
|
7b601c9c7f | ||
|
|
8d2f74daa4 | ||
|
|
01e82dca5f | ||
|
|
094bb407ed | ||
|
|
338baa55ec | ||
|
|
d06d20a33e | ||
|
|
d46ba6b92b | ||
|
|
ec2639039d | ||
|
|
3a211ded2e | ||
|
|
c2131e3654 | ||
|
|
594fb59395 | ||
|
|
f44378ec84 | ||
|
|
0f6b366f62 | ||
|
|
8d0a5997ee | ||
|
|
347cb3417e | ||
|
|
337fcebd10 | ||
|
|
e057b130e9 | ||
|
|
19a0765a1a | ||
|
|
b81cd3240b | ||
|
|
1e6105b076 | ||
|
|
d8d89b3463 | ||
|
|
459564cb2d | ||
|
|
084df35184 | ||
|
|
81912babeb | ||
|
|
3943fc7d95 | ||
|
|
3999dc930c | ||
|
|
d4dea16456 | ||
|
|
ed38cb33a5 | ||
|
|
4cf5a0f4c8 | ||
|
|
8f0d526af2 | ||
|
|
6ca3881841 | ||
|
|
d8e765a04f | ||
|
|
40ff572f94 | ||
|
|
cc4275dc03 | ||
|
|
b387f4a0db | ||
|
|
1a096031c4 | ||
|
|
83a60b4091 | ||
|
|
b292407ec2 | ||
|
|
952c337b76 | ||
|
|
e947b887fe | ||
|
|
bd1e5485d7 | ||
|
|
e095c3318b | ||
|
|
d75a08b519 | ||
|
|
d55a616fe0 | ||
|
|
2146cb3576 | ||
|
|
ae260e74f6 | ||
|
|
355410c03c | ||
|
|
718ad51fac | ||
|
|
4242a8679f | ||
|
|
4ff9ff699b | ||
|
|
7a76673274 | ||
|
|
bd03ca5136 | ||
|
|
b3e1e4b909 | ||
|
|
4bb22183df | ||
|
|
e72f8f4245 | ||
|
|
5b52f48bce | ||
|
|
f07a157a2a | ||
|
|
ca40854106 | ||
|
|
d6a8209b31 | ||
|
|
731e1f1f15 | ||
|
|
b4a2a8fb98 | ||
|
|
5a3e4ee5ca | ||
|
|
ab2cf0aeec | ||
|
|
9de6a02b30 | ||
|
|
9fb7892bfe | ||
|
|
546f4556f6 | ||
|
|
471de104bc | ||
|
|
d30be1536d | ||
|
|
6c0678ed61 | ||
|
|
4883b8a190 | ||
|
|
14742ed4ad | ||
|
|
1d8bd56862 | ||
|
|
94b8f9fe1c | ||
|
|
27412211a5 | ||
|
|
f8c4960079 | ||
|
|
b2e0bcf995 | ||
|
|
fcf6639d38 | ||
|
|
d540cb91a9 | ||
|
|
f69cc6f1b1 | ||
|
|
607f2ff407 | ||
|
|
ba6bf8c091 | ||
|
|
7e4c938dfd | ||
|
|
7f36d55320 | ||
|
|
d9634a134c | ||
|
|
4f8868d4b1 | ||
|
|
956546585c | ||
|
|
3ca0a92442 | ||
|
|
213f7e48c9 | ||
|
|
8b66fd522d | ||
|
|
fdf5009999 | ||
|
|
bbdba0ef16 | ||
|
|
a63602df7a | ||
|
|
587120f984 | ||
|
|
e72ca0de7e | ||
|
|
c44c27d3d2 | ||
|
|
df4e201ccd | ||
|
|
c8c0e9ec1a | ||
|
|
9a4a84a367 | ||
|
|
1dc3424411 | ||
|
|
c13745e913 | ||
|
|
25c12309f2 | ||
|
|
4b632da5af | ||
|
|
87c364b8ee | ||
|
|
efa48fbc8a | ||
|
|
21df6c1d21 | ||
|
|
39d2ceb94b | ||
|
|
1dad013d60 | ||
|
|
add7a03f88 | ||
|
|
0cefaa6d48 | ||
|
|
f08e73f359 | ||
|
|
cf9ce26438 | ||
|
|
fd74a5a82e | ||
|
|
3109104928 | ||
|
|
b1ec4df2e4 | ||
|
|
1609e149a8 | ||
|
|
298c483d0e | ||
|
|
dc917b75b1 | ||
|
|
ad32bdab44 | ||
|
|
5e8b2e1c87 | ||
|
|
08b4afd287 | ||
|
|
0de31f643b | ||
|
|
5e815eb3c4 | ||
|
|
48c93a2120 | ||
|
|
ad5de42172 | ||
|
|
32bafedaad | ||
|
|
c67fd11be9 | ||
|
|
8b59c72848 | ||
|
|
c35b2f3bfc | ||
|
|
ac63ad4612 | ||
|
|
e1dea1c752 | ||
|
|
25648e2327 | ||
|
|
18ac04bb0f | ||
|
|
5263ee58b2 | ||
|
|
af542b89f7 | ||
|
|
684b675fca | ||
|
|
c29044eca1 | ||
|
|
a36510fcc8 | ||
|
|
bc21ace416 | ||
|
|
57e521e2ff | ||
|
|
ac6ebb9e8d | ||
|
|
54bef54635 | ||
|
|
593e201f79 | ||
|
|
d7b24253fe | ||
|
|
33961abd86 | ||
|
|
37e0e1d42f | ||
|
|
1121f9c918 | ||
|
|
582203f5da | ||
|
|
8c0f193738 | ||
|
|
ebe42956ad | ||
|
|
b8f8df8927 | ||
|
|
2c66ca4fdd | ||
|
|
49f813e880 | ||
|
|
da6fed80d1 | ||
|
|
b901d9b8c9 | ||
|
|
b41d46ac57 | ||
|
|
4f0189f3e0 | ||
|
|
c956e7a802 | ||
|
|
dcbc8409e0 | ||
|
|
fd58568cf0 | ||
|
|
0f81fa53d2 | ||
|
|
44655dc81c | ||
|
|
749667aefd | ||
|
|
dd94418c26 | ||
|
|
55a5375e46 | ||
|
|
df76de7352 | ||
|
|
1fb1a1b2b1 | ||
|
|
f998edb2aa | ||
|
|
7c2cb9a0c7 | ||
|
|
0690a365da | ||
|
|
a20d05aba8 | ||
|
|
4362ae95ba | ||
|
|
d658814399 | ||
|
|
39e14d70ee | ||
|
|
2e58cfdb75 | ||
|
|
fcaa724c00 | ||
|
|
8806b4141e | ||
|
|
7bd159766b | ||
|
|
4df15d603f | ||
|
|
b453c3efe5 | ||
|
|
56590ef8a4 | ||
|
|
7c133136b9 | ||
|
|
41881639aa | ||
|
|
416003f078 | ||
|
|
bbfcd0efa3 | ||
|
|
150e4332c3 | ||
|
|
49649765c7 | ||
|
|
726b7bfa93 | ||
|
|
265f838868 | ||
|
|
6e2e5b5520 | ||
|
|
100ea2f64a | ||
|
|
4e7ed1ee33 | ||
|
|
8ab6aed1aa | ||
|
|
4ff096014c | ||
|
|
03b60b6ca9 | ||
|
|
e30b832e05 | ||
|
|
e646de85a7 | ||
|
|
70a7a0e344 | ||
|
|
b444abeb3e | ||
|
|
c72f56917d | ||
|
|
192283d6b2 | ||
|
|
6be6fa1966 | ||
|
|
510553b055 | ||
|
|
6c4616892e | ||
|
|
1e79a099b8 | ||
|
|
31a22327f1 | ||
|
|
c1712bebc6 | ||
|
|
cd91541245 | ||
|
|
4c1fc83256 | ||
|
|
34c7a33576 | ||
|
|
23ecfeeb4f | ||
|
|
9703f83eb3 | ||
|
|
0f3cc03d00 | ||
|
|
6f7ba1f9fc | ||
|
|
e1b85e4a1b | ||
|
|
b308dd58cc | ||
|
|
9f4c0479ce | ||
|
|
2c57817dde | ||
|
|
ba85c54d7c | ||
|
|
a80e5c3a65 | ||
|
|
22e2c34da8 | ||
|
|
00a8e4c2c5 | ||
|
|
10d0a4079c | ||
|
|
589f7f3c22 | ||
|
|
d1126a7eb0 | ||
|
|
9f4e72a0e1 | ||
|
|
a024295379 | ||
|
|
dc2b2ec488 | ||
|
|
0c5f5975aa | ||
|
|
dc3f682d2d | ||
|
|
2db8876c66 | ||
|
|
8f6201b0f7 | ||
|
|
4b146c70ad | ||
|
|
0118034b4b | ||
|
|
39217053ca | ||
|
|
fba190c826 | ||
|
|
5e9d528e16 | ||
|
|
c5921d88fc | ||
|
|
eb980b0ea1 | ||
|
|
de5b4216f7 | ||
|
|
495ff57b19 | ||
|
|
57948cf6e3 | ||
|
|
1aebbbcabd | ||
|
|
25b4cb072d | ||
|
|
1cdacc3a08 | ||
|
|
34d9466d09 | ||
|
|
c182c4ce66 | ||
|
|
dbb9bd1282 | ||
|
|
8019d2d6cc | ||
|
|
459cdb2e0b | ||
|
|
a230cd9513 | ||
|
|
0c44a25e85 | ||
|
|
34f3d04370 | ||
|
|
1f3e6b7e16 | ||
|
|
47d49a200a | ||
|
|
e1767d6e52 | ||
|
|
0f8e343cd2 | ||
|
|
23ab487baf | ||
|
|
22e5d38ef5 | ||
|
|
5819ccb528 | ||
|
|
42a2fd77cf | ||
|
|
ab93a8b0b3 | ||
|
|
84437eafa6 | ||
|
|
0107d848e0 | ||
|
|
5eeac96a0d | ||
|
|
9351c115be | ||
|
|
f95a11096c | ||
|
|
4203d179e6 | ||
|
|
78dfc9cb1c | ||
|
|
0bef307d77 | ||
|
|
b0da806f7a | ||
|
|
badecd1d81 | ||
|
|
6418e8ee30 | ||
|
|
09115c9658 | ||
|
|
74e3866bd7 | ||
|
|
408de78c13 | ||
|
|
c0451c18b3 | ||
|
|
f303d26c1e | ||
|
|
1b58a34859 | ||
|
|
82ea416e67 | ||
|
|
efd4fbad70 | ||
|
|
01bd15121b | ||
|
|
a9c2495349 | ||
|
|
e7c50b50ed | ||
|
|
6e25b289d2 | ||
|
|
157267eaf7 | ||
|
|
a317f9137a | ||
|
|
5dad3d22ea | ||
|
|
be85df456b | ||
|
|
2e172a08c7 | ||
|
|
bb1069ca60 | ||
|
|
d8141a1628 | ||
|
|
de9f7c4baf | ||
|
|
fa9b3116f1 | ||
|
|
dcf9d52961 | ||
|
|
1da93e2cc7 | ||
|
|
1d1bab988e | ||
|
|
dcc6ad3af3 | ||
|
|
d57f266789 | ||
|
|
c3395e1eff | ||
|
|
ca59ec2dbe | ||
|
|
79788125f3 | ||
|
|
2154f20fa4 | ||
|
|
afe40b6a89 | ||
|
|
ba4b3bd6b8 | ||
|
|
e423b5d745 | ||
|
|
6de8eca7ea | ||
|
|
9d68cfcaf0 | ||
|
|
225de11e6a | ||
|
|
916581bbd0 | ||
|
|
1cbb35840f | ||
|
|
7a1d769e39 | ||
|
|
8254bf934c | ||
|
|
5e2f20542f | ||
|
|
551a707ee4 | ||
|
|
024b15b4f9 | ||
|
|
1935df4143 | ||
|
|
3f99f90076 | ||
|
|
53cb445dde | ||
|
|
6e46947220 | ||
|
|
9b65e1671b | ||
|
|
d5c741db35 | ||
|
|
11e0780b6e | ||
|
|
f153541570 | ||
|
|
f066af88e7 | ||
|
|
97e1eef799 | ||
|
|
1bcd902817 | ||
|
|
2484568b21 | ||
|
|
085cc47ea5 | ||
|
|
aac36a88f3 | ||
|
|
1f2ebc82b7 | ||
|
|
9781949064 | ||
|
|
b06ef3781a | ||
|
|
b32213cb7b | ||
|
|
ac4c7d2421 | ||
|
|
824a49b80f | ||
|
|
13efd50d80 | ||
|
|
6fb091d20f | ||
|
|
518ab85cae | ||
|
|
f5124ad8b5 | ||
|
|
6f80900aa8 | ||
|
|
06b80e9281 | ||
|
|
51b39d9365 | ||
|
|
f7d2d8fc95 | ||
|
|
f34fb94c1a | ||
|
|
3107224e50 | ||
|
|
e1c481c534 | ||
|
|
945a2dd3eb | ||
|
|
e318945eb1 | ||
|
|
926709568d | ||
|
|
da040e799c | ||
|
|
694976cb6e | ||
|
|
3f7bd1846a | ||
|
|
714898b4c3 | ||
|
|
4efc9b6990 | ||
|
|
73c3beaff1 | ||
|
|
a6bdccd4ef | ||
|
|
8007991e7d | ||
|
|
0e5d1ed1c3 | ||
|
|
91dc2f4f71 | ||
|
|
950bab3023 | ||
|
|
29082a10e6 | ||
|
|
65c1227d88 | ||
|
|
5d121a68cf | ||
|
|
ad07d2b8b1 | ||
|
|
c5aef78085 | ||
|
|
522ebc8aa2 | ||
|
|
199b3761b5 | ||
|
|
daf7b45e45 | ||
|
|
fc71b953c7 | ||
|
|
74ccd4a001 | ||
|
|
3c09f6ca55 | ||
|
|
c7dbe8599d | ||
|
|
ca036b6676 | ||
|
|
5ae87c8e03 | ||
|
|
9774fc4f65 | ||
|
|
d0891fb652 | ||
|
|
e388e2a1c7 | ||
|
|
d9ab354338 | ||
|
|
9ea0a0e214 | ||
|
|
131ddf0355 | ||
|
|
8abe71a0fe | ||
|
|
43e27a7e68 | ||
|
|
b784d277e4 | ||
|
|
cb443157da | ||
|
|
c886015d92 | ||
|
|
3161247da9 | ||
|
|
743a250131 | ||
|
|
4fb4b21a9e | ||
|
|
e1a7d3ebc5 | ||
|
|
72b8b156ac | ||
|
|
134dafb608 | ||
|
|
d5102b6422 | ||
|
|
a2cfb9d423 | ||
|
|
bef70f94ab | ||
|
|
4233fffe74 | ||
|
|
81c214748d | ||
|
|
c6a61dc8c8 | ||
|
|
483092d40d | ||
|
|
88cb02f6bc | ||
|
|
9628188730 | ||
|
|
2e66134bf8 | ||
|
|
424fe4d1e9 | ||
|
|
2b6f9568de | ||
|
|
4b270bce8b | ||
|
|
6b077e67db | ||
|
|
4c79ea0679 | ||
|
|
8c4c4c25aa | ||
|
|
a37dbe2c06 | ||
|
|
5e10cb2d16 | ||
|
|
58316c5c1d | ||
|
|
55f092f161 | ||
|
|
ab5049127a | ||
|
|
5f99067e56 | ||
|
|
3a89e61bd8 | ||
|
|
06edfa2fee | ||
|
|
77bfd72458 | ||
|
|
5ddf19be4d | ||
|
|
6657b47746 | ||
|
|
5690efb07a | ||
|
|
05600cba08 | ||
|
|
e3b4120b2c | ||
|
|
1059245fd9 | ||
|
|
87c8d310b3 | ||
|
|
fdc4999556 | ||
|
|
d28a8db4ff | ||
|
|
ab9e7d10dd | ||
|
|
3ff72b26b9 | ||
|
|
107ae70651 | ||
|
|
04de19033e | ||
|
|
089ac70cd3 | ||
|
|
ae40a9ead9 | ||
|
|
152806b7f6 | ||
|
|
06beb8e704 | ||
|
|
64f2b94685 | ||
|
|
5a42eb98ab | ||
|
|
404b87d78d | ||
|
|
dcb8fbdbf4 | ||
|
|
0fe513d7f5 | ||
|
|
0be05325e4 | ||
|
|
75b39308cd | ||
|
|
35ded81713 | ||
|
|
07c85280cd | ||
|
|
43f1d86be0 | ||
|
|
82f5ed4c44 | ||
|
|
0b53ad4b4d | ||
|
|
56ad41d10c | ||
|
|
249a4564e0 | ||
|
|
efb2413720 | ||
|
|
1226acefd6 | ||
|
|
76f7d7bc90 | ||
|
|
a5cb2641fd | ||
|
|
57fc64af79 | ||
|
|
f8b7b8b698 | ||
|
|
3da6befe10 | ||
|
|
a50d61c3ce | ||
|
|
b7568975e7 | ||
|
|
4b9475310f | ||
|
|
639f726c83 | ||
|
|
7997c486cf | ||
|
|
2569d00bd0 | ||
|
|
2cd80d8fa1 | ||
|
|
eedca4db9b | ||
|
|
1d352a76ce | ||
|
|
17670aabf9 | ||
|
|
3456b3d90e | ||
|
|
f3da5a9026 | ||
|
|
00cc63b7cd | ||
|
|
8df80ce738 | ||
|
|
12e7a783b0 | ||
|
|
be86a15351 | ||
|
|
2c5022e7b4 | ||
|
|
f4a996fcb9 | ||
|
|
fdbf508bbf | ||
|
|
500fafa5b2 | ||
|
|
bfa315c68b | ||
|
|
4972f085a3 | ||
|
|
9c13261929 | ||
|
|
e36dc4a913 | ||
|
|
4cccb56ee3 | ||
|
|
3199af570d | ||
|
|
7dad47b3c8 | ||
|
|
fbd50bad1d | ||
|
|
10469bae7b | ||
|
|
b6a000a001 | ||
|
|
59539e7e90 | ||
|
|
a2c41bc5ec | ||
|
|
c4a06858fb | ||
|
|
15b0f05a05 | ||
|
|
67feee9aa1 | ||
|
|
185fb57b8c | ||
|
|
e9039f8208 | ||
|
|
440d1b3ba0 | ||
|
|
9c7a6c64b0 | ||
|
|
7cc63fe849 | ||
|
|
5647e8219d | ||
|
|
81629fab7a | ||
|
|
992d2033f3 | ||
|
|
d52d4a1278 | ||
|
|
57a44c5952 | ||
|
|
a04161ac7c | ||
|
|
47e46f13c3 | ||
|
|
5535bd91c8 | ||
|
|
8747a00de6 | ||
|
|
c926b86926 | ||
|
|
010ac9aa7c | ||
|
|
4ab0c337f1 | ||
|
|
f814c42fdd | ||
|
|
02aee3e625 | ||
|
|
52a32cfdd1 | ||
|
|
6faa7b2efe | ||
|
|
f8481413c9 | ||
|
|
7d4baa7046 | ||
|
|
0951d01383 | ||
|
|
da34472a6f | ||
|
|
a24eaaed50 | ||
|
|
26813c582f | ||
|
|
6067ac73e2 | ||
|
|
b1d94b67f4 | ||
|
|
452f4e69fd | ||
|
|
e89b6e3ea0 | ||
|
|
977bab4745 | ||
|
|
26c73109fe | ||
|
|
65f911ad51 | ||
|
|
f8ed5ebd6a | ||
|
|
e4cb61bebe | ||
|
|
7d5310af42 | ||
|
|
f68c610c0d | ||
|
|
ccfb6a6d73 | ||
|
|
da55996e22 | ||
|
|
ecd8400a34 | ||
|
|
03914883bc | ||
|
|
9981e1f3bd | ||
|
|
4d1df66025 | ||
|
|
a0f64e188b | ||
|
|
08407a1094 | ||
|
|
445ea3bb2e | ||
|
|
0e0aab98b1 | ||
|
|
721d5eb0c1 | ||
|
|
6d99dbc3a7 | ||
|
|
2be31f649b | ||
|
|
828ac27c03 | ||
|
|
b3d614002f | ||
|
|
2b2000ca10 | ||
|
|
8d7428d099 | ||
|
|
57f8322f31 | ||
|
|
bee7bc4294 | ||
|
|
28702065bc | ||
|
|
c248057081 | ||
|
|
6186f5e138 | ||
|
|
2201d8176d | ||
|
|
2ba6819876 | ||
|
|
a393b2a370 | ||
|
|
59fe713df2 | ||
|
|
4b8efaca9a | ||
|
|
a26100a8d0 | ||
|
|
8c52f4718c | ||
|
|
85b5c3c4ec | ||
|
|
4fd559e4c7 | ||
|
|
a337d0ddc7 | ||
|
|
9e4cb7071e | ||
|
|
c3f1707343 | ||
|
|
1ed058a553 | ||
|
|
0500a8178d | ||
|
|
7d0059f573 | ||
|
|
4e3b882362 | ||
|
|
13cf47873e | ||
|
|
7ee23ec38f | ||
|
|
eebf17c42c | ||
|
|
f84536788b | ||
|
|
4bbfdc2cb2 | ||
|
|
211fec35e3 | ||
|
|
b8214a46ae | ||
|
|
549ef91c81 | ||
|
|
cede65313b | ||
|
|
d897a7400f | ||
|
|
47f059213f | ||
|
|
8af51bbf08 | ||
|
|
53d9f5ddc6 | ||
|
|
06fffdccc8 | ||
|
|
aa13dc68fc | ||
|
|
813876dd90 | ||
|
|
596c7d65c5 | ||
|
|
ce8dcb75bf | ||
|
|
1bd51b5565 | ||
|
|
1f9ec305b4 | ||
|
|
be0f6e57d7 | ||
|
|
b268e9ee74 | ||
|
|
e97774435b | ||
|
|
93586bc5bb | ||
|
|
fe23089714 | ||
|
|
e743986f38 | ||
|
|
a6c9b700ed | ||
|
|
afa3fcb524 | ||
|
|
b9aeb648d6 | ||
|
|
5f5df1e5b7 | ||
|
|
ad885679e4 | ||
|
|
e002bebfbe | ||
|
|
a8a41e2b3d | ||
|
|
31940caa84 | ||
|
|
880334054e | ||
|
|
5f03ad5597 | ||
|
|
1efa3f055d | ||
|
|
8ccf11278b | ||
|
|
8a9e7ab4c3 | ||
|
|
c0fa7c0c51 | ||
|
|
022dfd4709 | ||
|
|
71e08aacc3 | ||
|
|
337eca87f2 | ||
|
|
074aceff8f | ||
|
|
cdc6cf229a | ||
|
|
1f33513dc9 | ||
|
|
b095b91ff2 | ||
|
|
454a62dbb9 | ||
|
|
5f7cc12157 | ||
|
|
97ef1ee201 | ||
|
|
a318568b72 | ||
|
|
5bb9949440 | ||
|
|
c33e91d5d0 | ||
|
|
ca65ef3cb7 | ||
|
|
9ebdbc81d0 | ||
|
|
b64985349e | ||
|
|
625fd9d1a4 | ||
|
|
eac5fdcec0 | ||
|
|
970b4d5d97 | ||
|
|
f741bc818d | ||
|
|
5f04c24187 | ||
|
|
a382bef336 | ||
|
|
4ddf28f344 | ||
|
|
0dc650305a | ||
|
|
697093d1c9 | ||
|
|
622f7a4479 | ||
|
|
c4b607804b | ||
|
|
864f008679 | ||
|
|
25f309bcb0 | ||
|
|
1354361ad9 | ||
|
|
8136c7b072 | ||
|
|
c9243e7249 | ||
|
|
1a487da3d9 | ||
|
|
b52395751c | ||
|
|
cfa6cc9a83 | ||
|
|
f203384b00 | ||
|
|
9ac3be455c | ||
|
|
20b74a9dcd | ||
|
|
3b848a5a86 | ||
|
|
a9b5e865a5 | ||
|
|
ab46a1b99d | ||
|
|
4a08465f5b | ||
|
|
a7960d6cd6 | ||
|
|
3caea77dde | ||
|
|
fdaa3b7f93 | ||
|
|
4f433b4456 | ||
|
|
309a9ad4fb | ||
|
|
b0e7431e72 | ||
|
|
158118d183 | ||
|
|
382e37fc5a | ||
|
|
3390676847 | ||
|
|
544a995312 | ||
|
|
f209d49bb5 | ||
|
|
42ed691fdc | ||
|
|
98d2e9d266 | ||
|
|
6111158896 | ||
|
|
3267fc653c | ||
|
|
7250608767 | ||
|
|
e82063e435 | ||
|
|
6d4c44bc25 | ||
|
|
2bc94d8792 | ||
|
|
4ca3edd789 | ||
|
|
d6859efde2 | ||
|
|
3f8cbfa259 | ||
|
|
5d18c9371d | ||
|
|
631990e3bb | ||
|
|
4ae7338f94 | ||
|
|
0d1e51cb21 | ||
|
|
475fb833ea | ||
|
|
580b030ee4 | ||
|
|
6a7cbc70d6 | ||
|
|
d76f60639c | ||
|
|
e2bea407ee | ||
|
|
558fed31aa | ||
|
|
f6513d40c8 | ||
|
|
259f9baa59 | ||
|
|
db5650e276 | ||
|
|
51ebbbc569 | ||
|
|
5184661652 | ||
|
|
7853a14ce6 | ||
|
|
a01e78ace9 | ||
|
|
f7eb576d0d | ||
|
|
34f1ad8fae | ||
|
|
c60f0991df | ||
|
|
d505fd0795 | ||
|
|
93cf506535 | ||
|
|
bfb37e55d4 | ||
|
|
92afc5cb33 | ||
|
|
75cb611701 | ||
|
|
2ec1dd58a5 | ||
|
|
7d59af54de | ||
|
|
2b5f47b3de | ||
|
|
16eebfb9a4 | ||
|
|
9025218671 | ||
|
|
6bccb546bb | ||
|
|
29d49046a0 | ||
|
|
717af9ffaf | ||
|
|
00060c9f43 | ||
|
|
759ff46c92 | ||
|
|
41957cdceb | ||
|
|
d418e3a1c9 | ||
|
|
f650124428 | ||
|
|
795d109c76 | ||
|
|
6868b3effc | ||
|
|
26747b7013 | ||
|
|
5198f8aa60 | ||
|
|
552da48a32 | ||
|
|
db8a688620 | ||
|
|
3088028d05 | ||
|
|
fd62ef865d | ||
|
|
ed74ed00ed | ||
|
|
741317aaaf | ||
|
|
9b6ecd4e6b | ||
|
|
7863b3358e | ||
|
|
e1be68ec3d | ||
|
|
a054186d4b | ||
|
|
2d5c549c83 | ||
|
|
9f6072dfe1 | ||
|
|
69c44fe1ab | ||
|
|
4fa7b2443e | ||
|
|
25a69592bb | ||
|
|
44e0b26990 | ||
|
|
c4496f8dc8 | ||
|
|
9e296231d9 | ||
|
|
49b3f05d65 | ||
|
|
f124b9c050 | ||
|
|
63a86f7c06 | ||
|
|
fd0f523c64 | ||
|
|
487e605520 | ||
|
|
9e169e1f4b | ||
|
|
9612e7ebcd | ||
|
|
f66162efe7 | ||
|
|
656642697b | ||
|
|
feb70f85f8 | ||
|
|
ab1981559b | ||
|
|
c8852d9a8e | ||
|
|
9ac8dc7fd1 | ||
|
|
c9419d99e6 | ||
|
|
a1f4a83e72 | ||
|
|
a8abd5d427 | ||
|
|
629d1b0630 | ||
|
|
97c368f63a | ||
|
|
3266a444d0 | ||
|
|
1c246f71f8 | ||
|
|
96945dfc4a | ||
|
|
30eb3001ef | ||
|
|
bdd8636390 | ||
|
|
f762d2a271 | ||
|
|
cf2efc2b92 | ||
|
|
7670da4cba | ||
|
|
d87f9f2a21 | ||
|
|
6e690f3fea | ||
|
|
6321002617 | ||
|
|
15ec362428 | ||
|
|
454004e705 | ||
|
|
e14b414fc1 | ||
|
|
c4b47a5915 | ||
|
|
957c252cd7 | ||
|
|
d6a6c21762 | ||
|
|
834580cfdf | ||
|
|
de13cfb555 | ||
|
|
4f87508834 | ||
|
|
682a044f32 | ||
|
|
bdb5d90b1d | ||
|
|
01880f4456 | ||
|
|
39f78ce7e8 | ||
|
|
755c6b92da | ||
|
|
2eab9c2837 | ||
|
|
63861789de | ||
|
|
086c353eff | ||
|
|
4fe5b44655 | ||
|
|
036547e260 | ||
|
|
696f434c90 | ||
|
|
0c654d9346 | ||
|
|
a2c393b06b | ||
|
|
eae2c2b102 | ||
|
|
d9e49e3484 | ||
|
|
a28d4c2f1c | ||
|
|
9af055ec54 | ||
|
|
0d41171e9d | ||
|
|
08af826ae9 | ||
|
|
4fd577d7c5 | ||
|
|
2c8efebe98 | ||
|
|
93c9fb53ac | ||
|
|
5a4d249cf9 | ||
|
|
4cc7bdee37 | ||
|
|
a6af568411 | ||
|
|
576a6a094a | ||
|
|
e671e4b6f5 | ||
|
|
a66b2a4c70 | ||
|
|
f1ae409535 | ||
|
|
a4b56b477d | ||
|
|
d9c389812a | ||
|
|
074ef3645f | ||
|
|
cc3aa413e8 | ||
|
|
7f90c09227 | ||
|
|
f6f4d8ccc9 | ||
|
|
31afce8304 | ||
|
|
2c4ff856cd | ||
|
|
f59974e310 | ||
|
|
70e2c12a6b | ||
|
|
11f3c6ce6f | ||
|
|
e213c4640b | ||
|
|
959c5eaa59 | ||
|
|
66fa510b26 | ||
|
|
f26a3b31ac | ||
|
|
724fbf579e | ||
|
|
f192f8e3cd | ||
|
|
f13c3d19fb | ||
|
|
b51a09efcc | ||
|
|
6004043782 | ||
|
|
f9fd0dc2c3 | ||
|
|
eb5411cd20 | ||
|
|
da3c7a02f0 | ||
|
|
e67d05007f | ||
|
|
b0a9a6a08e | ||
|
|
d848ea35f4 | ||
|
|
350f20effe | ||
|
|
b6dc8f98fe | ||
|
|
1b762ee48d | ||
|
|
cc3d7f1eac | ||
|
|
4107282fbf | ||
|
|
c29ffc3fcd | ||
|
|
f648bcda13 | ||
|
|
aa0044eed2 | ||
|
|
2312a721ae | ||
|
|
b93fc39b00 | ||
|
|
2dc2cd700f | ||
|
|
d69e534f8b | ||
|
|
1de9ddd394 | ||
|
|
77c68d4e11 | ||
|
|
2a0d1dcfce | ||
|
|
5a19cca407 | ||
|
|
4e8773ecde | ||
|
|
4c7dada809 | ||
|
|
65690b15da | ||
|
|
8ba07812ce | ||
|
|
2dd8f35001 | ||
|
|
2d15aa88d4 | ||
|
|
e4257e50f0 | ||
|
|
33ebc07915 | ||
|
|
bc07299626 | ||
|
|
25e8aeef53 | ||
|
|
a2ed34abf3 | ||
|
|
36a7b7b91a | ||
|
|
b4e8b7375f |
106
LICENSE.md
Normal file
106
LICENSE.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# CC0 1.0 Universal
|
||||
|
||||
## Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator and
|
||||
subsequent owner(s) (each and all, an “owner”) of an original work of
|
||||
authorship and/or a database (each, a “Work”).
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for the
|
||||
purpose of contributing to a commons of creative, cultural and scientific works
|
||||
(“Commons”) that the public can reliably and without fear of later claims of
|
||||
infringement build upon, modify, incorporate in other works, reuse and
|
||||
redistribute as freely as possible in any form whatsoever and for any purposes,
|
||||
including without limitation commercial purposes. These owners may contribute
|
||||
to the Commons to promote the ideal of a free culture and the further
|
||||
production of creative, cultural and scientific works, or to gain reputation or
|
||||
greater distribution for their Work in part through the use and efforts of
|
||||
others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any expectation of
|
||||
additional consideration or compensation, the person associating CC0 with a
|
||||
Work (the “Affirmer”), to the extent that he or she is an owner of Copyright
|
||||
and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and
|
||||
publicly distribute the Work under its terms, with knowledge of his or her
|
||||
Copyright and Related Rights in the Work and the meaning and intended legal
|
||||
effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights (“Copyright and
|
||||
Related Rights”). Copyright and Related Rights include, but are not limited
|
||||
to, the following:
|
||||
1. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
2. moral rights retained by the original author(s) and/or performer(s);
|
||||
3. publicity and privacy rights pertaining to a person’s image or likeness
|
||||
depicted in a Work;
|
||||
4. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(i), below;
|
||||
5. rights protecting the extraction, dissemination, use and reuse of data
|
||||
in a Work;
|
||||
6. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation thereof,
|
||||
including any amended or successor version of such directive); and
|
||||
7. other similar, equivalent or corresponding rights throughout the world
|
||||
based on applicable law or treaty, and any national implementations
|
||||
thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention of,
|
||||
applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
|
||||
unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright
|
||||
and Related Rights and associated claims and causes of action, whether now
|
||||
known or unknown (including existing as well as future claims and causes of
|
||||
action), in the Work (i) in all territories worldwide, (ii) for the maximum
|
||||
duration provided by applicable law or treaty (including future time
|
||||
extensions), (iii) in any current or future medium and for any number of
|
||||
copies, and (iv) for any purpose whatsoever, including without limitation
|
||||
commercial, advertising or promotional purposes (the “Waiver”). Affirmer makes
|
||||
the Waiver for the benefit of each member of the public at large and to the
|
||||
detriment of Affirmer’s heirs and successors, fully intending that such Waiver
|
||||
shall not be subject to revocation, rescission, cancellation, termination, or
|
||||
any other legal or equitable action to disrupt the quiet enjoyment of the Work
|
||||
by the public as contemplated by Affirmer’s express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason be
|
||||
judged legally invalid or ineffective under applicable law, then the Waiver
|
||||
shall be preserved to the maximum extent permitted taking into account
|
||||
Affirmer’s express Statement of Purpose. In addition, to the extent the Waiver
|
||||
is so judged Affirmer hereby grants to each affected person a royalty-free, non
|
||||
transferable, non sublicensable, non exclusive, irrevocable and unconditional
|
||||
license to exercise Affirmer’s Copyright and Related Rights in the Work (i) in
|
||||
all territories worldwide, (ii) for the maximum duration provided by applicable
|
||||
law or treaty (including future time extensions), (iii) in any current or
|
||||
future medium and for any number of copies, and (iv) for any purpose
|
||||
whatsoever, including without limitation commercial, advertising or promotional
|
||||
purposes (the “License”). The License shall be deemed effective as of the date
|
||||
CC0 was applied by Affirmer to the Work. Should any part of the License for any
|
||||
reason be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the remainder of the
|
||||
License, and in such case Affirmer hereby affirms that he or she will not (i)
|
||||
exercise any of his or her remaining Copyright and Related Rights in the Work
|
||||
or (ii) assert any associated claims and causes of action with respect to the
|
||||
Work, in either case contrary to Affirmer’s express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
1. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
2. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied, statutory
|
||||
or otherwise, including without limitation warranties of title,
|
||||
merchantability, fitness for a particular purpose, non infringement, or
|
||||
the absence of latent or other defects, accuracy, or the present or
|
||||
absence of errors, whether or not discoverable, all to the greatest
|
||||
extent permissible under applicable law.
|
||||
3. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person’s Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the Work.
|
||||
4. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
||||
|
||||
For more information, please see
|
||||
http://creativecommons.org/publicdomain/zero/1.0/.
|
||||
42
README.md
42
README.md
@@ -1,3 +1,43 @@
|
||||
# Liberama
|
||||
|
||||
Свободный обмен книгами в формате fb2
|
||||
Браузерная онлайн-читалка книг и децентрализованная библиотека.
|
||||
|
||||
Читалка [OmniReader](https://omnireader.ru) является частью данного проекта, размещенной на VPS:
|
||||
|
||||

|
||||

|
||||
|
||||
## VPS
|
||||
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
|
||||
|
||||
## Сборка проекта
|
||||
Необходима версия node.js не ниже 10.
|
||||
|
||||
```
|
||||
$ git clone https://github.com/bookpauk/liberama
|
||||
$ cd liberama
|
||||
$ npm i
|
||||
```
|
||||
|
||||
### Windows
|
||||
```
|
||||
$ npm run build:win
|
||||
```
|
||||
|
||||
### Linux
|
||||
```
|
||||
$ npm run build:linux
|
||||
```
|
||||
|
||||
Результат сборки будет доступен в каталоге `dist/linux|win` в виде исполнимого (standalone) файла
|
||||
|
||||
### Разработка
|
||||
```
|
||||
$ npm run dev
|
||||
```
|
||||
|
||||
## Помочь проекту
|
||||
|
||||
* bitcoin: 3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85
|
||||
* litecoin: MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ
|
||||
* monero: 8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz
|
||||
|
||||
31
build/includer.js
Normal file
31
build/includer.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
//пример в коде:
|
||||
// @@include('./test/testFile.inc');
|
||||
|
||||
function includeRecursive(self, parentFile, source, depth) {
|
||||
depth = (depth ? depth : 0);
|
||||
if (depth > 50)
|
||||
throw new Error('includer: stack too big');
|
||||
const lines = source.split('\n');
|
||||
let result = [];
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
const m = trimmed.match(/^@@[\s]*?include[\s]*?\(['"](.*)['"]\)/);
|
||||
if (m) {
|
||||
const includedFile = path.resolve(path.dirname(parentFile), m[1]);
|
||||
self.addDependency(includedFile);
|
||||
|
||||
const fileContent = fs.readFileSync(includedFile, 'utf8');
|
||||
result = result.concat(includeRecursive(self, includedFile, fileContent, depth + 1));
|
||||
} else {
|
||||
result.push(line);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
exports.default = function includer(source) {
|
||||
return includeRecursive(this, this.resourcePath, source).join('\n');
|
||||
}
|
||||
@@ -24,8 +24,8 @@ async function main() {
|
||||
await fs.ensureDir(tempDownloadDir);
|
||||
|
||||
//sqlite3
|
||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.0.4/node-v64-linux-x64.tar.gz';
|
||||
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v64-linux-x64/node_sqlite3.node`;
|
||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.1.1/node-v72-linux-x64.tar.gz';
|
||||
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v72-linux-x64/node_sqlite3.node`;
|
||||
|
||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
||||
|
||||
@@ -16,6 +16,11 @@ module.exports = {
|
||||
test: /\.vue$/,
|
||||
loader: "vue-loader"
|
||||
},
|
||||
{
|
||||
test: /\.includer$/,
|
||||
resourceQuery: /^\?vue/,
|
||||
use: path.resolve('build/includer.js')
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
|
||||
@@ -9,6 +9,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const {GenerateSW} = require('workbox-webpack-plugin');
|
||||
|
||||
const publicDir = path.resolve(__dirname, '../dist/tmp/public');
|
||||
const clientDir = path.resolve(__dirname, '../client');
|
||||
@@ -53,6 +54,13 @@ module.exports = merge(baseWpConfig, {
|
||||
template: `${clientDir}/index.html.template`,
|
||||
filename: `${publicDir}/index.html`
|
||||
}),
|
||||
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}])
|
||||
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}]),
|
||||
new GenerateSW({
|
||||
cacheId: 'liberama',
|
||||
swDest: `${publicDir}/service-worker.js`,
|
||||
navigateFallback: '/index.html',
|
||||
navigateFallbackDenylist: [new RegExp('^/api'), new RegExp('^/ws'), new RegExp('^/tmp'),],
|
||||
skipWaiting: true,
|
||||
}),
|
||||
]
|
||||
});
|
||||
|
||||
@@ -24,8 +24,8 @@ async function main() {
|
||||
await fs.ensureDir(tempDownloadDir);
|
||||
|
||||
//sqlite3
|
||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.0.4/node-v64-win32-x64.tar.gz';
|
||||
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v64-win32-x64/node_sqlite3.node`;
|
||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.1.1/node-v72-win32-x64.tar.gz';
|
||||
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v72-win32-x64/node_sqlite3.node`;
|
||||
|
||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import wsc from './webSocketConnection';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api'
|
||||
@@ -6,9 +7,23 @@ const api = axios.create({
|
||||
|
||||
class Misc {
|
||||
async loadConfig() {
|
||||
const response = await api.post('/config', {params: [
|
||||
|
||||
const query = {params: [
|
||||
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch',
|
||||
]});
|
||||
]};
|
||||
|
||||
try {
|
||||
await wsc.open();
|
||||
const config = await wsc.message(wsc.send(Object.assign({action: 'get-config'}, query)));
|
||||
if (config.error)
|
||||
throw new Error(config.error);
|
||||
return config;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
//если с WebSocket проблема, работаем по http
|
||||
const response = await api.post('/config', query);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,67 @@
|
||||
import _ from 'lodash';
|
||||
import axios from 'axios';
|
||||
import {Buffer} from 'safe-buffer';
|
||||
|
||||
import * as utils from '../share/utils';
|
||||
import wsc from './webSocketConnection';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api/reader'
|
||||
baseURL: '/api/reader'
|
||||
});
|
||||
|
||||
const workerApi = axios.create({
|
||||
baseURL: '/api/worker'
|
||||
baseURL: '/api/worker'
|
||||
});
|
||||
|
||||
class Reader {
|
||||
async loadBook(url, callback) {
|
||||
const refreshPause = 300;
|
||||
constructor() {
|
||||
}
|
||||
|
||||
async getWorkerStateFinish(workerId, callback) {
|
||||
if (!callback) callback = () => {};
|
||||
|
||||
let response = await api.post('/load-book', {type: 'url', url});
|
||||
let response = {};
|
||||
try {
|
||||
await wsc.open();
|
||||
const requestId = wsc.send({action: 'worker-get-state-finish', workerId});
|
||||
|
||||
const workerId = response.data.workerId;
|
||||
if (!workerId)
|
||||
throw new Error('Неверный ответ api');
|
||||
let prevResponse = false;
|
||||
while (1) {// eslint-disable-line no-constant-condition
|
||||
response = await wsc.message(requestId);
|
||||
|
||||
callback({totalSteps: 4});
|
||||
if (!response.state && prevResponse !== false) {//экономия траффика
|
||||
callback(prevResponse);
|
||||
} else {//были изменения worker state
|
||||
if (!response.state)
|
||||
throw new Error('Неверный ответ api');
|
||||
callback(response);
|
||||
prevResponse = response;
|
||||
}
|
||||
|
||||
if (response.state == 'finish' || response.state == 'error') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
//если с WebSocket проблема, работаем по http
|
||||
const refreshPause = 500;
|
||||
let i = 0;
|
||||
response = {};
|
||||
while (1) {// eslint-disable-line no-constant-condition
|
||||
callback(response.data);
|
||||
const prevProgress = response.progress || 0;
|
||||
const prevState = response.state || 0;
|
||||
response = await workerApi.post('/get-state', {workerId});
|
||||
response = response.data;
|
||||
callback(response);
|
||||
|
||||
if (response.data.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
|
||||
callback({step: 4});
|
||||
const book = await this.loadCachedBook(response.data.path, callback);
|
||||
return Object.assign({}, response.data, {data: book.data});
|
||||
}
|
||||
if (response.data.state == 'error') {
|
||||
let errMes = response.data.error;
|
||||
if (errMes.indexOf('getaddrinfo') >= 0 ||
|
||||
errMes.indexOf('ECONNRESET') >= 0 ||
|
||||
errMes.indexOf('EINVAL') >= 0 ||
|
||||
errMes.indexOf('404') >= 0)
|
||||
errMes = `Ресурс не найден по адресу: ${response.data.url}`;
|
||||
throw new Error(errMes);
|
||||
if (!response.state)
|
||||
throw new Error('Неверный ответ api');
|
||||
|
||||
if (response.state == 'finish' || response.state == 'error') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (i > 0)
|
||||
await utils.sleep(refreshPause);
|
||||
|
||||
@@ -51,30 +70,109 @@ class Reader {
|
||||
throw new Error('Слишком долгое время ожидания');
|
||||
}
|
||||
//проверка воркера
|
||||
const prevProgress = response.data.progress;
|
||||
const prevState = response.data.state;
|
||||
response = await workerApi.post('/get-state', {workerId});
|
||||
i = (prevProgress != response.data.progress || prevState != response.data.state ? 1 : i);
|
||||
i = (prevProgress != response.progress || prevState != response.state ? 1 : i);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async loadBook(opts, callback) {
|
||||
if (!callback) callback = () => {};
|
||||
|
||||
let response = await api.post('/load-book', opts);
|
||||
|
||||
const workerId = response.data.workerId;
|
||||
if (!workerId)
|
||||
throw new Error('Неверный ответ api');
|
||||
|
||||
callback({totalSteps: 4});
|
||||
callback(response.data);
|
||||
|
||||
response = await this.getWorkerStateFinish(workerId, callback);
|
||||
|
||||
if (response) {
|
||||
if (response.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
|
||||
callback({step: 4});
|
||||
const book = await this.loadCachedBook(response.path, callback, response.size);
|
||||
return Object.assign({}, response, {data: book.data});
|
||||
}
|
||||
|
||||
if (response.state == 'error') {
|
||||
let errMes = response.error;
|
||||
if (errMes.indexOf('getaddrinfo') >= 0 ||
|
||||
errMes.indexOf('ECONNRESET') >= 0 ||
|
||||
errMes.indexOf('EINVAL') >= 0 ||
|
||||
errMes.indexOf('404') >= 0)
|
||||
errMes = `Ресурс не найден по адресу: ${response.url}`;
|
||||
throw new Error(errMes);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Пустой ответ сервера');
|
||||
}
|
||||
}
|
||||
|
||||
async loadCachedBook(url, callback){
|
||||
const response = await axios.head(url);
|
||||
async checkCachedBook(url) {
|
||||
let estSize = -1;
|
||||
try {
|
||||
const response = await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
|
||||
|
||||
let estSize = 1000000;
|
||||
if (response.headers['content-length']) {
|
||||
estSize = response.headers['content-length'];
|
||||
if (response.headers['content-length']) {
|
||||
estSize = response.headers['content-length'];
|
||||
}
|
||||
} catch (e) {
|
||||
//восстановим при необходимости файл на сервере из удаленного облака
|
||||
let response = null
|
||||
|
||||
try {
|
||||
await wsc.open();
|
||||
response = await wsc.message(wsc.send({action: 'reader-restore-cached-file', path: url}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
//если с WebSocket проблема, работаем по http
|
||||
response = await api.post('/restore-cached-file', {path: url});
|
||||
response = response.data;
|
||||
}
|
||||
if (response.state == 'error') {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
const workerId = response.workerId;
|
||||
if (!workerId)
|
||||
throw new Error('Неверный ответ api');
|
||||
|
||||
response = await this.getWorkerStateFinish(workerId);
|
||||
if (response.state == 'error') {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
if (response.size && estSize < 0) {
|
||||
estSize = response.size;
|
||||
}
|
||||
}
|
||||
|
||||
return estSize;
|
||||
}
|
||||
|
||||
async loadCachedBook(url, callback, estSize = -1) {
|
||||
if (!callback) callback = () => {};
|
||||
|
||||
callback({state: 'loading', progress: 0});
|
||||
|
||||
//получение размера файла
|
||||
if (estSize && estSize < 0) {
|
||||
estSize = await this.checkCachedBook(url);
|
||||
}
|
||||
|
||||
//получение файла
|
||||
estSize = (estSize > 0 ? estSize : 1000000);
|
||||
const options = {
|
||||
onDownloadProgress: progress => {
|
||||
onDownloadProgress: (progress) => {
|
||||
while (progress.loaded > estSize) estSize *= 1.5;
|
||||
|
||||
if (callback)
|
||||
callback({state: 'loading', progress: Math.round((progress.loaded*100)/estSize)});
|
||||
callback({progress: Math.round((progress.loaded*100)/estSize)});
|
||||
}
|
||||
}
|
||||
//загрузка
|
||||
|
||||
return await axios.get(url, options);
|
||||
}
|
||||
|
||||
@@ -83,9 +181,8 @@ class Reader {
|
||||
maxUploadFileSize = 10*1024*1024;
|
||||
if (file.size > maxUploadFileSize)
|
||||
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('file', file, file.name);
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
@@ -111,13 +208,25 @@ class Reader {
|
||||
}
|
||||
|
||||
async storage(request) {
|
||||
let response = await api.post('/storage', request);
|
||||
let response = null;
|
||||
try {
|
||||
await wsc.open();
|
||||
response = await wsc.message(wsc.send({action: 'reader-storage', body: request}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
//если с WebSocket проблема, работаем по http
|
||||
response = await api.post('/storage', request);
|
||||
response = response.data;
|
||||
}
|
||||
|
||||
const state = response.data.state;
|
||||
const state = response.state;
|
||||
if (!state)
|
||||
throw new Error('Неверный ответ api');
|
||||
if (response.state == 'error') {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
185
client/api/webSocketConnection.js
Normal file
185
client/api/webSocketConnection.js
Normal file
@@ -0,0 +1,185 @@
|
||||
import * as utils from '../share/utils';
|
||||
|
||||
const cleanPeriod = 60*1000;//1 минута
|
||||
|
||||
class WebSocketConnection {
|
||||
//messageLifeTime в минутах (cleanPeriod)
|
||||
constructor(messageLifeTime = 5) {
|
||||
this.ws = null;
|
||||
this.timer = null;
|
||||
this.listeners = [];
|
||||
this.messageQueue = [];
|
||||
this.messageLifeTime = messageLifeTime;
|
||||
this.requestId = 0;
|
||||
|
||||
this.connecting = false;
|
||||
}
|
||||
|
||||
addListener(listener) {
|
||||
if (this.listeners.indexOf(listener) < 0)
|
||||
this.listeners.push(Object.assign({regTime: Date.now()}, listener));
|
||||
}
|
||||
|
||||
//рассылаем сообщение и удаляем те обработчики, которые его получили
|
||||
emit(mes, isError) {
|
||||
const len = this.listeners.length;
|
||||
if (len > 0) {
|
||||
let newListeners = [];
|
||||
for (const listener of this.listeners) {
|
||||
let emitted = false;
|
||||
if (isError) {
|
||||
if (listener.onError)
|
||||
listener.onError(mes);
|
||||
emitted = true;
|
||||
} else {
|
||||
if (listener.onMessage) {
|
||||
if (listener.requestId) {
|
||||
if (listener.requestId === mes.requestId) {
|
||||
listener.onMessage(mes);
|
||||
emitted = true;
|
||||
}
|
||||
} else {
|
||||
listener.onMessage(mes);
|
||||
emitted = true;
|
||||
}
|
||||
} else {
|
||||
emitted = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!emitted)
|
||||
newListeners.push(listener);
|
||||
}
|
||||
this.listeners = newListeners;
|
||||
}
|
||||
|
||||
return this.listeners.length != len;
|
||||
}
|
||||
|
||||
open(url) {
|
||||
return new Promise((resolve, reject) => { (async() => {
|
||||
//Ожидаем окончания процесса подключения, если open уже был вызван
|
||||
let i = 0;
|
||||
while (this.connecting && i < 200) {//10 сек
|
||||
await utils.sleep(50);
|
||||
i++;
|
||||
}
|
||||
if (i >= 200)
|
||||
this.connecting = false;
|
||||
|
||||
//проверим подключение, и если нет, то подключимся заново
|
||||
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
|
||||
resolve(this.ws);
|
||||
} else {
|
||||
this.connecting = true;
|
||||
const protocol = (window.location.protocol == 'https:' ? 'wss:' : 'ws:');
|
||||
|
||||
url = url || `${protocol}//${window.location.host}/ws`;
|
||||
|
||||
this.ws = new WebSocket(url);
|
||||
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||
|
||||
this.ws.onopen = (e) => {
|
||||
this.connecting = false;
|
||||
resolve(e);
|
||||
};
|
||||
|
||||
this.ws.onmessage = (e) => {
|
||||
try {
|
||||
const mes = JSON.parse(e.data);
|
||||
this.messageQueue.push({regTime: Date.now(), mes});
|
||||
|
||||
let newMessageQueue = [];
|
||||
for (const message of this.messageQueue) {
|
||||
if (!this.emit(message.mes)) {
|
||||
newMessageQueue.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
this.messageQueue = newMessageQueue;
|
||||
} catch (e) {
|
||||
this.emit(e.message, true);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onerror = (e) => {
|
||||
this.emit(e.message, true);
|
||||
if (this.connecting) {
|
||||
this.connecting = false;
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
})() });
|
||||
}
|
||||
|
||||
//timeout в минутах (cleanPeriod)
|
||||
message(requestId, timeout = 2) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.addListener({
|
||||
requestId,
|
||||
timeout,
|
||||
onMessage: (mes) => {
|
||||
resolve(mes);
|
||||
},
|
||||
onError: (e) => {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
send(req) {
|
||||
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
|
||||
const requestId = ++this.requestId;
|
||||
this.ws.send(JSON.stringify(Object.assign({requestId}, req)));
|
||||
return requestId;
|
||||
} else {
|
||||
throw new Error('WebSocket connection is not ready');
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
|
||||
this.ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
periodicClean() {
|
||||
try {
|
||||
this.timer = null;
|
||||
|
||||
const now = Date.now();
|
||||
//чистка listeners
|
||||
let newListeners = [];
|
||||
for (const listener of this.listeners) {
|
||||
if (now - listener.regTime < listener.timeout*cleanPeriod - 50) {
|
||||
newListeners.push(listener);
|
||||
} else {
|
||||
if (listener.onError)
|
||||
listener.onError('Время ожидания ответа истекло');
|
||||
}
|
||||
}
|
||||
this.listeners = newListeners;
|
||||
|
||||
//чистка messageQueue
|
||||
let newMessageQueue = [];
|
||||
for (const message of this.messageQueue) {
|
||||
if (now - message.regTime < this.messageLifeTime*cleanPeriod - 50) {
|
||||
newMessageQueue.push(message);
|
||||
}
|
||||
}
|
||||
this.messageQueue = newMessageQueue;
|
||||
} finally {
|
||||
if (this.ws.readyState == WebSocket.OPEN) {
|
||||
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new WebSocketConnection();
|
||||
@@ -1,3 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /?*url=
|
||||
Disallow: /#/
|
||||
5
client/assets/sw-register.js
Normal file
5
client/assets/sw-register.js
Normal file
@@ -0,0 +1,5 @@
|
||||
(function() {
|
||||
if('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/service-worker.js');
|
||||
}
|
||||
})();
|
||||
@@ -1,60 +1,29 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<el-aside v-if="showAsideBar" :width="asideWidth">
|
||||
<div class="app-name"><span v-html="appName"></span></div>
|
||||
<el-button class="el-button-collapse" @click="toggleCollapse" :icon="buttonCollapseIcon"></el-button>
|
||||
<el-menu class="el-menu-vertical" :default-active="rootRoute" :collapse="isCollapse" router>
|
||||
<el-menu-item index="/cardindex">
|
||||
<i class="el-icon-search"></i>
|
||||
<span :class="itemTitleClass('/cardindex')" slot="title">{{ this.itemRuText['/cardindex'] }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/reader">
|
||||
<i class="el-icon-tickets"></i>
|
||||
<span :class="itemTitleClass('/reader')" slot="title">{{ this.itemRuText['/reader'] }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/forum" disabled>
|
||||
<i class="el-icon-message"></i>
|
||||
<span :class="itemTitleClass('/forum')" slot="title">{{ this.itemRuText['/forum'] }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/income">
|
||||
<i class="el-icon-upload"></i>
|
||||
<span :class="itemTitleClass('/income')" slot="title">{{ this.itemRuText['/income'] }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/sources">
|
||||
<i class="el-icon-menu"></i>
|
||||
<span :class="itemTitleClass('/sources')" slot="title">{{ this.itemRuText['/sources'] }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/settings">
|
||||
<i class="el-icon-setting"></i>
|
||||
<span :class="itemTitleClass('/settings')" slot="title">{{ this.itemRuText['/settings'] }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/help">
|
||||
<i class="el-icon-question"></i>
|
||||
<span :class="itemTitleClass('/help')" slot="title">{{ this.itemRuText['/help'] }}</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<el-main v-if="showMain" :style="{padding: (isReaderActive ? 0 : '5px')}">
|
||||
<keep-alive>
|
||||
<router-view></router-view>
|
||||
</keep-alive>
|
||||
</el-main>
|
||||
</el-container>
|
||||
<div class="fit row">
|
||||
<Notify ref="notify"/>
|
||||
<StdDialog ref="stdDialog"/>
|
||||
<keep-alive>
|
||||
<router-view class="col"></router-view>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import Notify from './share/Notify.vue';
|
||||
import StdDialog from './share/StdDialog.vue';
|
||||
import * as utils from '../share/utils';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
Notify,
|
||||
StdDialog,
|
||||
},
|
||||
watch: {
|
||||
rootRoute: function() {
|
||||
this.setAppTitle();
|
||||
this.redirectIfNeeded();
|
||||
},
|
||||
mode: function() {
|
||||
this.setAppTitle();
|
||||
this.redirectIfNeeded();
|
||||
}
|
||||
},
|
||||
@@ -69,7 +38,8 @@ class App extends Vue {
|
||||
'/sources': 'Источники',
|
||||
'/settings': 'Параметры',
|
||||
'/help': 'Справка',
|
||||
}
|
||||
};
|
||||
|
||||
created() {
|
||||
this.commit = this.$store.commit;
|
||||
this.dispatch = this.$store.dispatch;
|
||||
@@ -77,6 +47,28 @@ class App extends Vue {
|
||||
this.uistate = this.$store.state.uistate;
|
||||
this.config = this.$store.state.config;
|
||||
|
||||
//root route
|
||||
let cachedRoute = '';
|
||||
let cachedPath = '';
|
||||
this.$root.rootRoute = () => {
|
||||
if (this.$route.path != cachedPath) {
|
||||
cachedPath = this.$route.path;
|
||||
const m = cachedPath.match(/^(\/[^/]*).*$/i);
|
||||
cachedRoute = (m ? m[1] : this.$route.path);
|
||||
|
||||
}
|
||||
return cachedRoute;
|
||||
}
|
||||
|
||||
this.$router.beforeEach((to, from, next) => {
|
||||
//распознавание хоста, если присутствует домен 3-уровня "b.", то разрешена только определенная страница
|
||||
if (window.location.host.indexOf('b.') == 0 && to.path != '/external-libs' && to.path != '/404') {
|
||||
next('/404');
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
// set-app-title
|
||||
this.$root.$on('set-app-title', this.setAppTitle);
|
||||
|
||||
@@ -100,26 +92,49 @@ class App extends Vue {
|
||||
|
||||
document.addEventListener('keyup', (event) => {
|
||||
this.keyHook(event);
|
||||
});
|
||||
});
|
||||
document.addEventListener('keypress', (event) => {
|
||||
this.keyHook(event);
|
||||
});
|
||||
document.addEventListener('keydown', (event) => {
|
||||
this.keyHook(event);
|
||||
});
|
||||
});
|
||||
window.addEventListener('resize', () => {
|
||||
this.$root.$emit('resize');
|
||||
});
|
||||
}
|
||||
|
||||
routerReady() {
|
||||
return new Promise ((resolve) => {
|
||||
this.$router.onReady(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.$root.notify = this.$refs.notify;
|
||||
this.$root.stdDialog = this.$refs.stdDialog;
|
||||
|
||||
this.dispatch('config/loadConfig');
|
||||
this.$watch('apiError', function(newError) {
|
||||
if (newError) {
|
||||
this.$notify.error({
|
||||
title: 'Ошибка API',
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: newError.response.config.url + '<br>' + newError.response.statusText
|
||||
});
|
||||
let mes = newError.message;
|
||||
if (newError.response && newError.response.config)
|
||||
mes = newError.response.config.url + '<br>' + newError.response.statusText;
|
||||
this.$root.notify.error(mes, 'Ошибка API');
|
||||
}
|
||||
});
|
||||
|
||||
this.setAppTitle();
|
||||
(async() => {
|
||||
//запросим persistent storage
|
||||
if (navigator.storage && navigator.storage.persist) {
|
||||
navigator.storage.persist();
|
||||
}
|
||||
await this.routerReady();
|
||||
this.redirectIfNeeded();
|
||||
})();
|
||||
}
|
||||
|
||||
toggleCollapse() {
|
||||
@@ -133,9 +148,9 @@ class App extends Vue {
|
||||
|
||||
get asideWidth() {
|
||||
if (this.uistate.asideBarCollapse) {
|
||||
return '64px';
|
||||
return 64;
|
||||
} else {
|
||||
return '170px';
|
||||
return 170;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,15 +174,14 @@ class App extends Vue {
|
||||
}
|
||||
|
||||
get rootRoute() {
|
||||
const m = this.$route.path.match(/^(\/[^/]*).*$/i);
|
||||
this.$root.rootRoute = (m ? m[1] : this.$route.path);
|
||||
|
||||
return this.$root.rootRoute;
|
||||
return this.$root.rootRoute();
|
||||
}
|
||||
|
||||
setAppTitle(title) {
|
||||
if (!title) {
|
||||
if (this.mode == 'omnireader') {
|
||||
if (this.mode == 'liberama.top') {
|
||||
document.title = `Liberama Reader - всегда с вами`;
|
||||
} else if (this.mode == 'omnireader') {
|
||||
document.title = `Omni Reader - всегда с вами`;
|
||||
} else if (this.config && this.mode !== null) {
|
||||
document.title = `${this.config.name} - ${this.itemRuText[this.$root.rootRoute]}`;
|
||||
@@ -186,44 +200,33 @@ class App extends Vue {
|
||||
}
|
||||
|
||||
get showAsideBar() {
|
||||
return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader');
|
||||
return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader' && this.mode != 'liberama.top');
|
||||
}
|
||||
|
||||
set showAsideBar(value) {
|
||||
}
|
||||
|
||||
get isReaderActive() {
|
||||
return this.rootRoute == '/reader';
|
||||
}
|
||||
|
||||
get showMain() {
|
||||
return (this.showAsideBar || this.isReaderActive);
|
||||
return (this.rootRoute == '/reader' || this.rootRoute == '/external-libs');
|
||||
}
|
||||
|
||||
redirectIfNeeded() {
|
||||
if ((this.mode == 'reader' || this.mode == 'omnireader') && (this.rootRoute != '/reader')) {
|
||||
//старый url
|
||||
if ((this.mode == 'reader' || this.mode == 'omnireader' || this.mode == 'liberama.top')) {
|
||||
const search = window.location.search.substr(1);
|
||||
const url = search.split('url=')[1] || '';
|
||||
if (url) {
|
||||
window.location = `/#/reader?url=${url}`;
|
||||
} else {
|
||||
this.$router.replace('/reader');
|
||||
|
||||
//распознавание параметра url вида "?url=<link>" и редирект при необходимости
|
||||
if (!this.isReaderActive) {
|
||||
const s = search.split('url=');
|
||||
const url = s[1] || '';
|
||||
const q = utils.parseQuery(s[0] || '');
|
||||
if (url) {
|
||||
q.url = decodeURIComponent(url);
|
||||
}
|
||||
|
||||
window.history.replaceState({}, '', '/');
|
||||
this.$router.replace({ path: '/reader', query: q });
|
||||
}
|
||||
}
|
||||
|
||||
//yandex-метрика для omnireader
|
||||
if (this.config.branch == 'production' && this.mode == 'omnireader' && !this.yaMetricsDone) {
|
||||
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
||||
m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
|
||||
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");// eslint-disable-line no-unexpected-multiline
|
||||
|
||||
ym(52347334, "init", {// eslint-disable-line no-undef
|
||||
id:52347334,
|
||||
clickmap:true,
|
||||
trackLinks:true,
|
||||
accurateTrackBounce:true
|
||||
});
|
||||
|
||||
this.yaMetricsDone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
@@ -237,68 +240,28 @@ class App extends Vue {
|
||||
line-height: 140%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.bold-font {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.el-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.el-aside {
|
||||
line-height: 1;
|
||||
background-color: #ccc;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
padding: 0;
|
||||
background-color: #E6EDF4;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.el-menu-vertical:not(.el-menu--collapse) {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.el-menu--collapse {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.el-button-collapse, .el-button-collapse:focus, .el-button-collapse:active, .el-button-collapse:hover {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
border: 0;
|
||||
}
|
||||
.el-menu-item {
|
||||
font-size: 85%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
body, html, #app {
|
||||
body, html, #app {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font: normal 12pt ReaderDefault;
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
flex: 1;
|
||||
padding: 0 !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
.dborder {
|
||||
border: 2px solid magenta !important;
|
||||
}
|
||||
|
||||
.icon-rotate {
|
||||
vertical-align: middle;
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
|
||||
.notify-button-icon {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Book в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Card в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
<template>
|
||||
<el-container direction="vertical">
|
||||
<el-tabs type="border-card" style="height: 100%;" v-model="selectedTab">
|
||||
<el-tab-pane label="Поиск"></el-tab-pane>
|
||||
<el-tab-pane label="Автор"></el-tab-pane>
|
||||
<el-tab-pane label="Книга"></el-tab-pane>
|
||||
<el-tab-pane label="История"></el-tab-pane>
|
||||
<keep-alive>
|
||||
<router-view></router-view>
|
||||
</keep-alive>
|
||||
</el-tabs>
|
||||
</el-container>
|
||||
<div>
|
||||
<keep-alive>
|
||||
<router-view></router-view>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -18,7 +12,7 @@ import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import _ from 'lodash';
|
||||
|
||||
const rootRoute = '/cardindex';
|
||||
const selfRoute = '/cardindex';
|
||||
const tab2Route = [
|
||||
'/cardindex/search',
|
||||
'/cardindex/card',
|
||||
@@ -51,7 +45,7 @@ class CardIndex extends Vue {
|
||||
if (t !== this.selectedTab)
|
||||
this.selectedTab = t.toString();
|
||||
} else {
|
||||
if (route == rootRoute && lastActiveTab !== null)
|
||||
if (route == selfRoute && lastActiveTab !== null)
|
||||
this.setRouteByTab(lastActiveTab);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел History в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Search в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
<template>
|
||||
<Window ref="window" width="600px" height="95%" @close="close">
|
||||
<template slot="header">
|
||||
Настроить закладки
|
||||
</template>
|
||||
|
||||
<div class="col column fit">
|
||||
<div class="row items-center top-panel bg-grey-3">
|
||||
<q-btn class="q-mr-md" round dense color="blue" icon="la la-check" @click.stop="openSelected" size="16px" :disabled="!selected">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть выбранную закладку</q-tooltip>
|
||||
</q-btn>
|
||||
<q-input class="col" ref="search" rounded outlined dense bg-color="white" placeholder="Найти" v-model="search">
|
||||
<template v-slot:append>
|
||||
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch"/>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="col row">
|
||||
<div class="left-panel column items-center no-wrap bg-grey-3">
|
||||
<q-btn class="q-my-sm" round dense color="blue" icon="la la-plus" @click.stop="addBookmark" size="14px">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить закладку</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-minus" @click.stop="delBookmark" size="14px" :disabled="!ticked.length">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Удалить отмеченные закладки</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-edit" @click.stop="editBookmark" size="14px" :disabled="!selected || selected.indexOf('r-') == 0">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Редактировать закладку</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-arrow-up" @click.stop="moveBookmark(false)" size="14px" :disabled="!ticked.length">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Переместить отмеченные вверх</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-arrow-down" @click.stop="moveBookmark(true)" size="14px" :disabled="!ticked.length">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Переместить отмеченные вниз</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-broom" @click.stop="setDefaultBookmarks" size="14px">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Установить по умолчанию</q-tooltip>
|
||||
</q-btn>
|
||||
<div class="space"/>
|
||||
</div>
|
||||
|
||||
<div class="col fit tree">
|
||||
<div v-show="nodes.length" class="checkbox-tick-all">
|
||||
<q-checkbox v-model="tickAll" @input="makeTickAll" size="36px" label="Выбрать все" />
|
||||
</div>
|
||||
<q-tree
|
||||
class="q-my-xs"
|
||||
:nodes="nodes"
|
||||
node-key="key"
|
||||
tick-strategy="leaf"
|
||||
:selected.sync="selected"
|
||||
:ticked.sync="ticked"
|
||||
:expanded.sync="expanded"
|
||||
selected-color="black"
|
||||
:filter="search"
|
||||
no-nodes-label="Закладок пока нет"
|
||||
no-results-label="Ничего не найдено"
|
||||
>
|
||||
<template v-slot:default-header="p">
|
||||
<div class="q-px-xs" :class="{selected: selected == p.key}">{{ p.node.label }}</div>
|
||||
</template>
|
||||
</q-tree>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import _ from 'lodash';
|
||||
|
||||
import Window from '../../share/Window.vue';
|
||||
import * as lu from '../linkUtils';
|
||||
import rstore from '../../../store/modules/reader';
|
||||
|
||||
const BookmarkSettingsProps = Vue.extend({
|
||||
props: {
|
||||
libs: Object,
|
||||
addBookmarkVisible: Boolean,
|
||||
}
|
||||
});
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
watch: {
|
||||
ticked: function() {
|
||||
this.checkAllTicked();
|
||||
},
|
||||
}
|
||||
})
|
||||
class BookmarkSettings extends BookmarkSettingsProps {
|
||||
search = '';
|
||||
selected = '';
|
||||
ticked = [];
|
||||
expanded = [];
|
||||
tickAll = false;
|
||||
|
||||
created() {
|
||||
this.afterInit = true;
|
||||
}
|
||||
|
||||
mounted() {
|
||||
}
|
||||
|
||||
init() {
|
||||
this.$refs.window.init();
|
||||
}
|
||||
|
||||
get nodes() {
|
||||
const result = [];
|
||||
|
||||
const expanded = [];
|
||||
this.links = {};
|
||||
this.libs.groups.forEach(group => {
|
||||
const rkey = `r-${group.r}`;
|
||||
const g = {label: group.r, key: rkey, children: []};
|
||||
this.links[rkey] = {l: group.r, c: ''};
|
||||
|
||||
group.list.forEach(link => {
|
||||
const key = link.l;
|
||||
g.children.push({
|
||||
label: (link.c ? link.c + ' ': '') + lu.removeOrigin(link.l),
|
||||
key
|
||||
});
|
||||
|
||||
this.links[key] = link;
|
||||
if (link.l == this.libs.startLink && expanded.indexOf(rkey) < 0) {
|
||||
expanded.push(rkey);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
result.push(g);
|
||||
});
|
||||
|
||||
if (this.afterInit) {
|
||||
this.$nextTick(() => {
|
||||
this.expanded = expanded;
|
||||
});
|
||||
this.afterInit = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
makeTickAll() {
|
||||
if (this.tickAll) {
|
||||
const newTicked = [];
|
||||
for (const key of Object.keys(this.links)) {
|
||||
if (key.indexOf('r-') != 0)
|
||||
newTicked.push(key);
|
||||
}
|
||||
this.ticked = newTicked;
|
||||
} else {
|
||||
this.ticked = [];
|
||||
}
|
||||
}
|
||||
|
||||
checkAllTicked() {
|
||||
const ticked = new Set(this.ticked);
|
||||
|
||||
let newTickAll = !!(this.nodes.length);
|
||||
for (const key of Object.keys(this.links)) {
|
||||
if (key.indexOf('r-') != 0 && !ticked.has(key))
|
||||
newTickAll = false;
|
||||
}
|
||||
this.tickAll = newTickAll;
|
||||
}
|
||||
|
||||
resetSearch() {
|
||||
this.search = '';
|
||||
this.$refs.search.focus();
|
||||
}
|
||||
|
||||
openSelected() {
|
||||
if (!this.selected)
|
||||
return;
|
||||
if (this.selected.indexOf('r-') === 0) {//rootLink
|
||||
this.$emit('do-action', {action: 'setRootLink', data: this.links[this.selected].l});
|
||||
} else {//selectedLink
|
||||
this.$emit('do-action', {action: 'setSelectedLink', data: this.links[this.selected].l});
|
||||
}
|
||||
this.close();
|
||||
}
|
||||
|
||||
editBookmark() {
|
||||
this.$emit('do-action', {action: 'editBookmark', data: {link: this.links[this.selected].l, desc: this.links[this.selected].c}});
|
||||
}
|
||||
|
||||
addBookmark() {
|
||||
this.$emit('do-action', {action: 'addBookmark'});
|
||||
}
|
||||
|
||||
async delBookmark() {
|
||||
const newLibs = _.cloneDeep(this.libs);
|
||||
|
||||
if (await this.$root.stdDialog.confirm(`Подтвердите удаление ${this.ticked.length} закладок:`, ' ')) {
|
||||
const ticked = new Set(this.ticked);
|
||||
for (let i = newLibs.groups.length - 1; i >= 0; i--) {
|
||||
const g = newLibs.groups[i];
|
||||
for (let j = g.list.length - 1; j >= 0; j--) {
|
||||
if (ticked.has(g.list[j].l)) {
|
||||
delete g.list[j];
|
||||
}
|
||||
}
|
||||
g.list = g.list.filter(v => v);
|
||||
if (!g.list.length)
|
||||
delete newLibs.groups[i];
|
||||
else {
|
||||
const item = lu.getListItemByLink(g.list, g.s);
|
||||
if (!item)
|
||||
g.s = g.list[0].l;
|
||||
}
|
||||
}
|
||||
|
||||
newLibs.groups = newLibs.groups.filter(v => v);
|
||||
this.ticked = [];
|
||||
this.selected = '';
|
||||
this.$emit('do-action', {action: 'setLibs', data: newLibs});
|
||||
}
|
||||
}
|
||||
|
||||
moveBookmark(down = false) {
|
||||
const newLibs = _.cloneDeep(this.libs);
|
||||
|
||||
const ticked = new Set(this.ticked);
|
||||
let moved = false;
|
||||
let prevFull = false;
|
||||
if (!down) {
|
||||
for (let i = 0; i < newLibs.groups.length; i++) {
|
||||
const g = newLibs.groups[i];
|
||||
let count = 0;
|
||||
for (let j = 0; j < g.list.length; j++) {
|
||||
if (ticked.has(g.list[j].l)) {
|
||||
if (j > 0 && !ticked.has(g.list[j - 1].l)) {
|
||||
[g.list[j], g.list[j - 1]] = [g.list[j - 1], g.list[j]];
|
||||
moved = true;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == g.list.length && !prevFull && i > 0) {
|
||||
const gs = newLibs.groups;
|
||||
[gs[i], gs[i - 1]] = [gs[i - 1], gs[i]];
|
||||
moved = true;
|
||||
} else
|
||||
prevFull = (count == g.list.length);
|
||||
}
|
||||
} else {
|
||||
for (let i = newLibs.groups.length - 1; i >= 0; i--) {
|
||||
const g = newLibs.groups[i];
|
||||
let count = 0;
|
||||
for (let j = g.list.length - 1; j >= 0; j--) {
|
||||
if (ticked.has(g.list[j].l)) {
|
||||
if (j < g.list.length - 1 && !ticked.has(g.list[j + 1].l)) {
|
||||
[g.list[j], g.list[j + 1]] = [g.list[j + 1], g.list[j]];
|
||||
moved = true;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == g.list.length && !prevFull && i < newLibs.groups.length - 1) {
|
||||
const gs = newLibs.groups;
|
||||
[gs[i], gs[i + 1]] = [gs[i + 1], gs[i]];
|
||||
moved = true;
|
||||
} else
|
||||
prevFull = (count == g.list.length);
|
||||
}
|
||||
}
|
||||
|
||||
if (moved)
|
||||
this.$emit('do-action', {action: 'setLibs', data: newLibs});
|
||||
}
|
||||
|
||||
async setDefaultBookmarks() {
|
||||
const result = await this.$root.stdDialog.prompt(`Введите 'да' для сброса всех закладок в предустановленные значения:`, ' ', {
|
||||
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
|
||||
});
|
||||
|
||||
if (result && result.value && result.value.toLowerCase() == 'да') {
|
||||
this.$emit('do-action', {action: 'setLibs', data: _.cloneDeep(
|
||||
Object.assign({helpShowed: true}, rstore.libsDefaults)
|
||||
)});
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.afterInit = false;
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (this.addBookmarkVisible)
|
||||
return false;
|
||||
|
||||
if (event.type == 'keydown' && event.key == 'Escape') {
|
||||
this.close();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.top-panel {
|
||||
height: 50px;
|
||||
border-bottom: 1px solid gray;
|
||||
padding: 0 10px 0 12px;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
width: 60px;
|
||||
height: 100%;
|
||||
border-right: 1px solid gray;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tree {
|
||||
padding: 0px 10px 10px 10px;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.selected {
|
||||
text-shadow: 0 0 20px yellow, 0 0 15px yellow, 0 0 10px yellow, 0 0 10px yellow, 0 0 5px yellow;
|
||||
}
|
||||
|
||||
.checkbox-tick-all {
|
||||
border-bottom: 1px solid #bbbbbb;
|
||||
margin-bottom: 7px;
|
||||
padding: 5px 5px 2px 16px;
|
||||
}
|
||||
|
||||
.space {
|
||||
min-height: 1px;
|
||||
width: 1px;
|
||||
}
|
||||
</style>
|
||||
863
client/components/ExternalLibs/ExternalLibs.vue
Normal file
863
client/components/ExternalLibs/ExternalLibs.vue
Normal file
@@ -0,0 +1,863 @@
|
||||
<template>
|
||||
<Window ref="window" @close="close" margin="2px">
|
||||
<template slot="header">
|
||||
{{ header }}
|
||||
</template>
|
||||
|
||||
<template slot="buttons">
|
||||
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle">
|
||||
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
|
||||
</span>
|
||||
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
|
||||
<q-icon name="la la-plus" size="16px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Увеличить масштаб</q-tooltip>
|
||||
</span>
|
||||
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)">
|
||||
<q-icon name="la la-minus" size="16px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Уменьшить масштаб</q-tooltip>
|
||||
</span>
|
||||
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="showHelp">
|
||||
<q-icon name="la la-question-circle" size="16px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Справка</q-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<div v-show="ready" class="col column" style="min-width: 600px">
|
||||
<div class="row items-center q-px-sm" style="height: 50px">
|
||||
<q-select class="q-mr-sm" ref="rootLink" v-model="rootLink" :options="rootLinkOptions" @input="rootLinkInput"
|
||||
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
||||
style="width: 230px"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
rounded outlined dense emit-value map-options display-value-sanitize options-sanitize
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-plus" @click.stop="addBookmark" size="12px">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить закладку</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn round dense color="blue" icon="la la-bars" @click.stop="bookmarkSettings" size="12px">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Настроить закладки</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
<template v-slot:selected>
|
||||
<div style="overflow: hidden; white-space: nowrap;">{{ rootLinkWithoutProtocol }}</div>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<q-select class="q-mr-sm" ref="selectedLink" v-model="selectedLink" :options="selectedLinkOptions" @input="selectedLinkInput" style="width: 50px"
|
||||
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
rounded outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
|
||||
>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Закладки</q-tooltip>
|
||||
</q-select>
|
||||
|
||||
<q-input class="col q-mr-sm" ref="input" rounded outlined dense bg-color="white" v-model="bookUrl" placeholder="Скопируйте сюда URL книги"
|
||||
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-home" @click="goToLink(selectedLink)" size="12px">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Вернуться на стартовую страницу</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn round dense color="blue" icon="la la-angle-double-down" @click="openBookUrlInFrame" size="12px" :disabled="!bookUrl">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Загрузить URL во фрейм</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<q-btn round dense color="blue" icon="la la-cog" @click.stop="optionsVisible = true" size="12px">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Опции</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-btn rounded color="green-7" no-caps size="14px" @click="submitUrl" :disabled="!bookUrl">Открыть
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть в читалке</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
|
||||
<div class="col fit" ref="frameBox" style="position: relative;">
|
||||
<div ref="frameWrap" class="overflow-hidden">
|
||||
<iframe v-if="frameVisible" ref="frame" :src="frameSrc" frameborder="0"></iframe>
|
||||
</div>
|
||||
<div v-show="transparentLayoutVisible" ref="transparentLayout" class="fit transparent-layout" @click="transparentLayoutClick"></div>
|
||||
</div>
|
||||
|
||||
<Dialog ref="dialogAddBookmark" v-model="addBookmarkVisible">
|
||||
<template slot="header">
|
||||
<div class="row items-center">
|
||||
<q-icon class="q-mr-sm" name="la la-bookmark" size="28px"></q-icon>
|
||||
<div v-if="addBookmarkMode == 'edit'">Редактировать закладку</div>
|
||||
<div v-else>Добавить закладку</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="q-mx-md row">
|
||||
<q-input ref="bookmarkLink" class="col q-mr-sm" outlined dense bg-color="white" v-model="bookmarkLink" @keydown="bookmarkLinkKeyDown"
|
||||
placeholder="Ссылка для закладки" maxlength="2000" @focus="selectAllOnFocus">
|
||||
</q-input>
|
||||
|
||||
<q-select class="q-mr-sm" ref="defaultRootLink" v-model="defaultRootLink" :options="defaultRootLinkOptions" @input="defaultRootLinkInput" style="width: 50px"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
|
||||
>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Предустановленные ссылки</q-tooltip>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<div class="q-mx-md q-mt-md">
|
||||
<q-input class="col q-mr-sm" ref="bookmarkDesc" outlined dense bg-color="white" v-model="bookmarkDesc" @keydown="bookmarkDescKeyDown"
|
||||
placeholder="Описание" style="width: 400px" maxlength="100" @focus="selectAllOnFocus">
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<template slot="footer">
|
||||
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okAddBookmark" :disabled="!bookmarkLink">OK</q-btn>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog ref="options" v-model="optionsVisible">
|
||||
<template slot="header">
|
||||
<div class="row items-center">
|
||||
<q-icon class="q-mr-sm" name="la la-cog" size="28px"></q-icon>
|
||||
Опции
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="q-mx-md column">
|
||||
<q-checkbox v-model="closeAfterSubmit" size="36px" label="Закрыть окно при отправке ссылки в читалку" />
|
||||
<q-checkbox v-model="openInFrameOnEnter" size="36px" label="Открывать ссылку во фрейме при нажатии 'Enter'" />
|
||||
<q-checkbox v-model="openInFrameOnAdd" size="36px" label="Активировать новую закладку после добавления" />
|
||||
</div>
|
||||
|
||||
<template slot="footer">
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="optionsVisible = false">OK</q-btn>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<BookmarkSettings v-if="bookmarkSettingsActive" ref="bookmarkSettings" :libs="libs" :addBookmarkVisible="addBookmarkVisible"
|
||||
@do-action="doAction" @close="closeBookmarkSettings">
|
||||
</BookmarkSettings>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import _ from 'lodash';
|
||||
|
||||
import Window from '../share/Window.vue';
|
||||
import Dialog from '../share/Dialog.vue';
|
||||
import BookmarkSettings from './BookmarkSettings/BookmarkSettings.vue';
|
||||
|
||||
import rstore from '../../store/modules/reader';
|
||||
import * as utils from '../../share/utils';
|
||||
import * as lu from './linkUtils';
|
||||
|
||||
const proxySubst = {
|
||||
'http://flibusta.is': 'http://b.liberama.top:23480',
|
||||
'http://fantasy-worlds.org': 'http://b.liberama.top:23580',
|
||||
};
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
Window,
|
||||
Dialog,
|
||||
BookmarkSettings
|
||||
},
|
||||
watch: {
|
||||
libs: function() {
|
||||
this.loadLibs();
|
||||
},
|
||||
defaultRootLink: function() {
|
||||
this.updateBookmarkLink();
|
||||
},
|
||||
bookUrl: function(newValue) {
|
||||
const value = lu.addProtocol(newValue);
|
||||
const subst = this.makeProxySubst(value, true);
|
||||
if (value != subst) {
|
||||
this.$nextTick(() => {
|
||||
this.bookUrl = subst;
|
||||
});
|
||||
}
|
||||
},
|
||||
bookmarkLink: function(newValue) {
|
||||
const value = lu.addProtocol(newValue);
|
||||
const subst = this.makeProxySubst(value, true);
|
||||
if (value != subst) {
|
||||
this.$nextTick(() => {
|
||||
this.bookmarkLink = subst;
|
||||
});
|
||||
}
|
||||
},
|
||||
closeAfterSubmit: function(newValue) {
|
||||
this.commitProp('closeAfterSubmit', newValue);
|
||||
},
|
||||
openInFrameOnEnter: function(newValue) {
|
||||
this.commitProp('openInFrameOnEnter', newValue);
|
||||
},
|
||||
openInFrameOnAdd: function(newValue) {
|
||||
this.commitProp('openInFrameOnAdd', newValue);
|
||||
},
|
||||
}
|
||||
})
|
||||
class ExternalLibs extends Vue {
|
||||
ready = false;
|
||||
frameVisible = false;
|
||||
rootLink = '';
|
||||
selectedLink = '';
|
||||
frameSrc = '';
|
||||
bookUrl = '';
|
||||
libs = {};
|
||||
fullScreenActive = false;
|
||||
transparentLayoutVisible = false;
|
||||
|
||||
addBookmarkVisible = false;
|
||||
optionsVisible = false;
|
||||
|
||||
addBookmarkMode = '';
|
||||
bookmarkLink = '';
|
||||
bookmarkDesc = '';
|
||||
defaultRootLink = '';
|
||||
|
||||
bookmarkSettingsActive = false;
|
||||
|
||||
closeAfterSubmit = false;
|
||||
openInFrameOnEnter = false;
|
||||
openInFrameOnAdd = false;
|
||||
frameScale = 1;
|
||||
|
||||
created() {
|
||||
this.oldStartLink = '';
|
||||
this.justOpened = true;
|
||||
this.$root.addKeyHook(this.keyHook);
|
||||
|
||||
this.$root.$on('resize', async() => {
|
||||
await utils.sleep(200);
|
||||
this.frameResize();
|
||||
});
|
||||
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
this.fullScreenActive = (document.fullscreenElement !== null);
|
||||
});
|
||||
|
||||
this.debouncedGoToLink = _.debounce((link) => {
|
||||
this.goToLink(link);
|
||||
}, 100, {'maxWait':200});
|
||||
//this.commit = this.$store.commit;
|
||||
//this.commit('reader/setLibs', rstore.libsDefaults);
|
||||
}
|
||||
|
||||
mounted() {
|
||||
//Поправка метода toggleOption компонента select фреймворка quasar, необходимо другое поведение
|
||||
//$emit('input'.. вызывается всегда
|
||||
this.toggleOption = function(opt, keepOpen) {
|
||||
if (this.editable !== true || opt === void 0 || this.isOptionDisabled(opt) === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const optValue = this.getOptionValue(opt);
|
||||
|
||||
if (this.multiple !== true) {
|
||||
if (keepOpen !== true) {
|
||||
this.updateInputValue(this.fillInput === true ? this.getOptionLabel(opt) : '', true, true);
|
||||
this.hidePopup();
|
||||
}
|
||||
|
||||
this.$refs.target !== void 0 && this.$refs.target.focus();
|
||||
this.$emit('input', this.emitValue === true ? optValue : opt);
|
||||
}
|
||||
};
|
||||
|
||||
this.$refs.rootLink.toggleOption = this.toggleOption;
|
||||
this.$refs.selectedLink.toggleOption = this.toggleOption;
|
||||
|
||||
(async() => {
|
||||
//подождем this.mode
|
||||
let i = 0;
|
||||
while(!this.mode && i < 100) {
|
||||
await utils.sleep(100);
|
||||
i++;
|
||||
}
|
||||
|
||||
if (this.mode != 'liberama.top') {
|
||||
this.$router.replace('/404');
|
||||
return;
|
||||
}
|
||||
|
||||
this.$refs.window.init();
|
||||
|
||||
this.opener = null;
|
||||
const host = window.location.host;
|
||||
const openerHost = (host.indexOf('b.') == 0 ? host.substring(2) : host);
|
||||
const openerOrigin1 = `http://${openerHost}`;
|
||||
const openerOrigin2 = `https://${openerHost}`;
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.origin !== openerOrigin1 && event.origin !== openerOrigin2)
|
||||
return;
|
||||
if (!_.isObject(event.data) || event.data.from != 'LibsPage')
|
||||
return;
|
||||
if (event.origin == openerOrigin1)
|
||||
this.opener = window.opener;
|
||||
else
|
||||
this.opener = event.source;
|
||||
this.openerOrigin = event.origin;
|
||||
|
||||
//console.log(event);
|
||||
|
||||
this.recvMessage(event.data);
|
||||
});
|
||||
|
||||
//Ожидаем родителя
|
||||
i = 0;
|
||||
while(!this.opener) {
|
||||
await utils.sleep(1000);
|
||||
i++;
|
||||
if (i >= 5) {
|
||||
await this.$root.stdDialog.alert('Нет связи с читалкой. Окно будет закрыто', 'Ошибка');
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
|
||||
//Проверка закрытия родительского окна
|
||||
while(this.opener) {
|
||||
await this.checkOpener();
|
||||
await utils.sleep(1000);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
recvMessage(d) {
|
||||
if (d.type == 'mes') {
|
||||
switch(d.data) {
|
||||
case 'hello': this.sendMessage({type: 'mes', data: 'ready'}); break;
|
||||
}
|
||||
} else if (d.type == 'libs') {
|
||||
this.ready = true;
|
||||
this.libs = _.cloneDeep(d.data);
|
||||
} else if (d.type == 'notify') {
|
||||
this.$root.notify.success(d.data, '', {position: 'bottom-right'});
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(d) {
|
||||
(async() => {
|
||||
await this.checkOpener();
|
||||
if (this.opener && this.openerOrigin)
|
||||
this.opener.postMessage(Object.assign({}, {from: 'ExternalLibs'}, d), this.openerOrigin);
|
||||
})();
|
||||
}
|
||||
|
||||
async checkOpener() {
|
||||
if (this.opener.closed) {
|
||||
await this.$root.stdDialog.alert('Потеряна связь с читалкой. Окно будет закрыто', 'Ошибка');
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
|
||||
commitLibs(libs) {
|
||||
this.sendMessage({type: 'libs', data: libs});
|
||||
}
|
||||
|
||||
commitProp(prop, value) {
|
||||
let libs = _.cloneDeep(this.libs);
|
||||
libs[prop] = value;
|
||||
this.commitLibs(libs);
|
||||
}
|
||||
|
||||
loadLibs() {
|
||||
const libs = this.libs;
|
||||
|
||||
if (!libs.helpShowed) {
|
||||
this.showHelp();
|
||||
(async() => {
|
||||
await utils.sleep(1000);
|
||||
this.commitProp('helpShowed', true);
|
||||
})();
|
||||
}
|
||||
|
||||
this.selectedLink = libs.startLink;
|
||||
this.closeAfterSubmit = libs.closeAfterSubmit || false;
|
||||
this.openInFrameOnEnter = libs.openInFrameOnEnter || false;
|
||||
this.openInFrameOnAdd = libs.openInFrameOnAdd || false;
|
||||
|
||||
this.frameScale = 1;
|
||||
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.selectedLink);
|
||||
if (index >= 0)
|
||||
this.frameScale = this.libs.groups[index].frameScale || 1;
|
||||
|
||||
this.updateStartLink();
|
||||
}
|
||||
|
||||
doAction(event) {
|
||||
switch (event.action) {
|
||||
case 'setLibs': this.commitLibs(event.data); break;
|
||||
case 'setRootLink': this.rootLink = event.data; this.rootLinkInput(); break;
|
||||
case 'setSelectedLink': this.selectedLink = event.data; this.selectedLinkInput(); break;
|
||||
case 'editBookmark': this.addBookmark('edit', event.data.link, event.data.desc); break;
|
||||
case 'addBookmark': this.addBookmark('add'); break;
|
||||
}
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this.$store.state.config.mode;
|
||||
}
|
||||
|
||||
get header() {
|
||||
let result = (this.ready ? 'Сетевая библиотека' : 'Загрузка...');
|
||||
if (this.ready && this.selectedLink) {
|
||||
result += ` | ${(this.libs.comment ? this.libs.comment + ' ': '') + lu.removeProtocol(this.libs.startLink)}`;
|
||||
}
|
||||
this.$root.$emit('set-app-title', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
get rootLinkWithoutProtocol() {
|
||||
return lu.removeProtocol(this.rootLink);
|
||||
}
|
||||
|
||||
updateSelectedLinkByRoot() {
|
||||
if (!this.ready)
|
||||
return;
|
||||
|
||||
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.rootLink);
|
||||
if (index >= 0)
|
||||
this.selectedLink = this.libs.groups[index].s;
|
||||
else
|
||||
this.selectedLink = '';
|
||||
}
|
||||
|
||||
updateStartLink(force) {
|
||||
if (!this.ready)
|
||||
return;
|
||||
|
||||
let index = -1;
|
||||
try {
|
||||
this.rootLink = lu.getOrigin(this.selectedLink);
|
||||
index = lu.getRootIndexByUrl(this.libs.groups, this.rootLink);
|
||||
} catch(e) {
|
||||
//
|
||||
}
|
||||
|
||||
if (index >= 0) {
|
||||
let libs = _.cloneDeep(this.libs);
|
||||
const com = this.getCommentByLink(libs.groups[index].list, this.selectedLink);
|
||||
if (libs.groups[index].s != this.selectedLink ||
|
||||
libs.startLink != this.selectedLink ||
|
||||
libs.comment != com) {
|
||||
libs.groups[index].s = this.selectedLink;
|
||||
libs.startLink = this.selectedLink;
|
||||
libs.comment = com;
|
||||
this.commitLibs(libs);
|
||||
}
|
||||
|
||||
if (force || this.oldStartLink != libs.startLink) {
|
||||
this.oldStartLink = libs.startLink;
|
||||
this.debouncedGoToLink(this.selectedLink);
|
||||
}
|
||||
} else {
|
||||
this.rootLink = '';
|
||||
this.selectedLink = '';
|
||||
this.debouncedGoToLink(this.selectedLink);
|
||||
}
|
||||
}
|
||||
|
||||
get rootLinkOptions() {
|
||||
let result = [];
|
||||
if (!this.ready)
|
||||
return result;
|
||||
|
||||
this.libs.groups.forEach(group => {
|
||||
result.push({label: lu.removeProtocol(group.r), value: group.r});
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
get defaultRootLinkOptions() {
|
||||
let result = [];
|
||||
|
||||
rstore.libsDefaults.groups.forEach(group => {
|
||||
result.push({label: lu.removeProtocol(group.r), value: group.r});
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
get selectedLinkOptions() {
|
||||
let result = [];
|
||||
if (!this.ready)
|
||||
return result;
|
||||
|
||||
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.rootLink);
|
||||
if (index >= 0) {
|
||||
this.libs.groups[index].list.forEach(link => {
|
||||
result.push({label: (link.c ? link.c + ' ': '') + lu.removeOrigin(link.l), value: link.l});
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
openBookUrlInFrame() {
|
||||
if (this.bookUrl) {
|
||||
this.goToLink(lu.addProtocol(this.bookUrl));
|
||||
}
|
||||
}
|
||||
|
||||
goToLink(link) {
|
||||
if (!this.ready || !link)
|
||||
return;
|
||||
|
||||
if (!link) {
|
||||
this.frameVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.frameSrc = this.makeProxySubst(link);
|
||||
|
||||
this.frameVisible = false;
|
||||
this.$nextTick(() => {
|
||||
this.frameVisible = true;
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.frame) {
|
||||
this.$refs.frame.contentWindow.focus();
|
||||
this.frameResize();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
frameResize() {
|
||||
this.$refs.frameWrap.style = 'width: 1px; height: 1px;';
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.frame) {
|
||||
const w = this.$refs.frameBox.offsetWidth;
|
||||
const h = this.$refs.frameBox.offsetHeight;
|
||||
const normalSize = `width: ${w}px; height: ${h}px;`;
|
||||
this.$refs.frameWrap.style = normalSize;
|
||||
if (this.frameScale != 1) {
|
||||
const s = this.frameScale;
|
||||
this.$refs.frame.style = `width: ${w/s}px; height: ${h/s}px; transform: scale(${s}); transform-origin: 0 0;`;
|
||||
} else {
|
||||
this.$refs.frame.style = normalSize;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
changeScale(delta) {
|
||||
if ((this.frameScale > 0.1 && delta <= 0) || (this.frameScale < 5 && delta >= 0)) {
|
||||
this.frameScale = _.round(this.frameScale + delta, 1);
|
||||
|
||||
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.selectedLink);
|
||||
if (index >= 0) {
|
||||
let libs = _.cloneDeep(this.libs);
|
||||
libs.groups[index].frameScale = this.frameScale;
|
||||
this.commitLibs(libs);
|
||||
}
|
||||
|
||||
this.frameResize();
|
||||
this.$root.notify.success(`Масштаб изменен: ${(this.frameScale*100).toFixed(0)}%`, '', {position: 'bottom-right'});
|
||||
}
|
||||
}
|
||||
|
||||
getCommentByLink(list, link) {
|
||||
const item = lu.getListItemByLink(list, link);
|
||||
return (item ? item.c : '');
|
||||
}
|
||||
|
||||
makeProxySubst(url, reverse = false) {
|
||||
for (const [key, value] of Object.entries(proxySubst)) {
|
||||
if (reverse && value == url.substring(0, value.length)) {
|
||||
return key + url.substring(value.length);
|
||||
} else if (!reverse && key == url.substring(0, key.length)) {
|
||||
return value + url.substring(key.length);
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
selectAllOnFocus(event) {
|
||||
if (event.target.select)
|
||||
event.target.select();
|
||||
}
|
||||
|
||||
rootLinkInput() {
|
||||
this.updateSelectedLinkByRoot();
|
||||
this.updateStartLink(true);
|
||||
}
|
||||
|
||||
selectedLinkInput() {
|
||||
this.updateStartLink(true);
|
||||
}
|
||||
|
||||
submitUrl() {
|
||||
if (this.bookUrl) {
|
||||
this.sendMessage({type: 'submitUrl', data: {
|
||||
url: this.bookUrl,
|
||||
force: true
|
||||
}});
|
||||
this.bookUrl = '';
|
||||
if (this.closeAfterSubmit)
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
addBookmark(mode = 'add', link = '', desc = '') {
|
||||
|
||||
if (mode == 'edit') {
|
||||
this.editBookmarkLink = this.bookmarkLink = link;
|
||||
this.editBookmarkDesc = this.bookmarkDesc = desc;
|
||||
} else {
|
||||
this.bookmarkLink = this.bookUrl;
|
||||
this.bookmarkDesc = '';
|
||||
}
|
||||
|
||||
this.addBookmarkMode = mode;
|
||||
this.addBookmarkVisible = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.bookmarkLink.focus();
|
||||
this.$refs.defaultRootLink.toggleOption = this.toggleOption;
|
||||
});
|
||||
}
|
||||
|
||||
updateBookmarkLink() {
|
||||
const index = lu.getSafeRootIndexByUrl(rstore.libsDefaults.groups, this.defaultRootLink);
|
||||
if (index >= 0) {
|
||||
this.bookmarkLink = rstore.libsDefaults.groups[index].s;
|
||||
this.bookmarkDesc = this.getCommentByLink(rstore.libsDefaults.groups[index].list, this.bookmarkLink);
|
||||
} else {
|
||||
this.bookmarkLink = '';
|
||||
this.bookmarkDesc = '';
|
||||
}
|
||||
}
|
||||
|
||||
defaultRootLinkInput() {
|
||||
this.updateBookmarkLink();
|
||||
}
|
||||
|
||||
bookmarkLinkKeyDown(event) {
|
||||
if (event.key == 'Enter') {
|
||||
this.$refs.bookmarkDesc.focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
bookmarkDescKeyDown(event) {
|
||||
if (event.key == 'Enter') {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.okAddBookmark();
|
||||
}
|
||||
}
|
||||
|
||||
async okAddBookmark() {
|
||||
if (!this.bookmarkLink)
|
||||
return;
|
||||
|
||||
const link = (this.addBookmarkMode == 'edit' ? lu.addProtocol(this.editBookmarkLink) : lu.addProtocol(this.bookmarkLink));
|
||||
let index = -1;
|
||||
try {
|
||||
index = lu.getRootIndexByUrl(this.libs.groups, link);
|
||||
} catch (e) {
|
||||
await this.$root.stdDialog.alert('Неверный формат ссылки', 'Ошибка');
|
||||
return;
|
||||
}
|
||||
|
||||
let libs = _.cloneDeep(this.libs);
|
||||
|
||||
//добавление
|
||||
//есть группа в закладках
|
||||
if (index >= 0) {
|
||||
const item = lu.getListItemByLink(libs.groups[index].list, link);
|
||||
|
||||
//редактирование
|
||||
if (item && this.addBookmarkMode == 'edit') {
|
||||
if (item) {
|
||||
//редактируем
|
||||
item.l = link;
|
||||
item.c = this.bookmarkDesc;
|
||||
this.commitLibs(libs);
|
||||
} else {
|
||||
await this.$root.stdDialog.alert('Не удалось отредактировать закладку', 'Ошибка');
|
||||
}
|
||||
} else if (!item) {
|
||||
//добавляем
|
||||
if (libs.groups[index].list.length >= 100) {
|
||||
await this.$root.stdDialog.alert('Достигнут предел количества закладок для этого сайта', 'Ошибка');
|
||||
return;
|
||||
}
|
||||
|
||||
libs.groups[index].list.push({l: link, c: this.bookmarkDesc});
|
||||
|
||||
if (this.openInFrameOnAdd) {
|
||||
libs.startLink = link;
|
||||
libs.comment = this.bookmarkDesc;
|
||||
}
|
||||
|
||||
this.commitLibs(libs);
|
||||
} else if (item.c != this.bookmarkDesc) {
|
||||
if (await this.$root.stdDialog.confirm(`Такая закладка уже существует с другим описанием.<br>` +
|
||||
`Заменить '${this.$sanitize(item.c)}' на '${this.$sanitize(this.bookmarkDesc)}'?`, ' ')) {
|
||||
item.c = this.bookmarkDesc;
|
||||
this.commitLibs(libs);
|
||||
} else
|
||||
return;
|
||||
} else {
|
||||
await this.$root.stdDialog.alert('Такая закладка уже существует', ' ');
|
||||
return;
|
||||
}
|
||||
} else {//нет группы в закладках
|
||||
if (libs.groups.length >= 100) {
|
||||
await this.$root.stdDialog.alert('Достигнут предел количества различных сайтов в закладках', 'Ошибка');
|
||||
return;
|
||||
}
|
||||
|
||||
//добавляем сначала группу
|
||||
libs.groups.push({r: lu.getOrigin(link), s: link, list: []});
|
||||
|
||||
index = lu.getSafeRootIndexByUrl(libs.groups, link);
|
||||
if (index >= 0)
|
||||
libs.groups[index].list.push({l: link, c: this.bookmarkDesc});
|
||||
|
||||
if (this.openInFrameOnAdd) {
|
||||
libs.startLink = link;
|
||||
libs.comment = this.bookmarkDesc;
|
||||
}
|
||||
|
||||
this.commitLibs(libs);
|
||||
}
|
||||
|
||||
this.addBookmarkVisible = false;
|
||||
}
|
||||
|
||||
fullScreenToggle() {
|
||||
this.fullScreenActive = !this.fullScreenActive;
|
||||
if (this.fullScreenActive) {
|
||||
this.$q.fullscreen.request();
|
||||
} else {
|
||||
this.$q.fullscreen.exit();
|
||||
}
|
||||
}
|
||||
|
||||
transparentLayoutClick() {
|
||||
this.transparentLayoutVisible = false;
|
||||
}
|
||||
|
||||
onSelectPopupShow() {
|
||||
this.transparentLayoutVisible = true;
|
||||
}
|
||||
|
||||
onSelectPopupHide() {
|
||||
this.transparentLayoutVisible = false;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.sendMessage({type: 'close'});
|
||||
}
|
||||
|
||||
bookUrlKeyDown(event) {
|
||||
if (event.key == 'Enter') {
|
||||
if (!this.openInFrameOnEnter) {
|
||||
this.submitUrl();
|
||||
} else {
|
||||
if (this.bookUrl)
|
||||
this.goToLink(this.bookUrl);
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
bookmarkSettings() {
|
||||
this.bookmarkSettingsActive = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.bookmarkSettings.init();
|
||||
});
|
||||
}
|
||||
|
||||
closeBookmarkSettings() {
|
||||
this.bookmarkSettingsActive = false;
|
||||
}
|
||||
|
||||
showHelp() {
|
||||
this.$root.stdDialog.alert(`
|
||||
<p>Окно 'Сетевая библиотека' позволяет открывать ссылки в читалке без переключения между окнами,
|
||||
что особенно актуально для мобильных устройств. Имеется возможность управлять закладками
|
||||
на понравившиеся ресурсы, книги или страницы авторов. Открытие ссылок и навигация происходят во фрейме, но,
|
||||
к сожалению, в нем открываются не все страницы.</p>
|
||||
|
||||
<p>Доступ к сайтам <span style="color: blue">http://flibusta.is</span> и <span style="color: blue">http://fantasy-worlds.org</span> работает через прокси.
|
||||
|
||||
<br><span style="color: red"><b>ПРЕДУПРЕЖДЕНИЕ!</b></span>
|
||||
Доступ предназначен только для просмотра и скачивания книг. Авторизоваться на этих сайтах
|
||||
из фрейма категорически не рекомендуется, т.к. ваше подключение не защищено и данные могут попасть
|
||||
к третьим лицам.
|
||||
</p>
|
||||
|
||||
<p>Из-за проблем с безопасностью, навигация 'вперед-назад' во фрейме осуществляется с помощью контекстного меню правой кнопкой мыши.
|
||||
На мобильных устройствах для этого служит системная клавиша 'Назад (стрелка влево)' и опция 'Вперед (стрелка вправо)' в меню браузера.
|
||||
</p>
|
||||
|
||||
<p>Приятного пользования ;-)
|
||||
</p>
|
||||
`, 'Справка', {iconName: 'la la-info-circle'});
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (this.$root.rootRoute() == '/external-libs') {
|
||||
if (this.$root.stdDialog.active)
|
||||
return false;
|
||||
|
||||
if (this.bookmarkSettingsActive && this.$refs.bookmarkSettings.keyHook(event))
|
||||
return true;
|
||||
|
||||
if (this.addBookmarkVisible || this.optionsVisible)
|
||||
return false;
|
||||
|
||||
if (event.type == 'keydown' && event.key == 'F4') {
|
||||
this.addBookmark();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.type == 'keydown' && event.key == 'Escape' &&
|
||||
(document.activeElement != this.$refs.rootLink.$refs.target || !this.$refs.rootLink.menu) &&
|
||||
(document.activeElement != this.$refs.selectedLink.$refs.target || !this.$refs.selectedLink.menu)
|
||||
) {
|
||||
this.close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.separator {
|
||||
height: 1px;
|
||||
background-color: #A0A0A0;
|
||||
}
|
||||
|
||||
.full-screen-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.full-screen-button:hover {
|
||||
background-color: #69C05F;
|
||||
}
|
||||
|
||||
.transparent-layout {
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
48
client/components/ExternalLibs/linkUtils.js
Normal file
48
client/components/ExternalLibs/linkUtils.js
Normal file
@@ -0,0 +1,48 @@
|
||||
export function addProtocol(url) {
|
||||
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0))
|
||||
return 'http://' + url;
|
||||
return url;
|
||||
}
|
||||
|
||||
export function removeProtocol(url) {
|
||||
return url.replace(/(^\w+:|^)\/\//, '');
|
||||
}
|
||||
|
||||
export function getOrigin(url) {
|
||||
const parsed = new URL(url);
|
||||
return parsed.origin;
|
||||
}
|
||||
|
||||
export function removeOrigin(url) {
|
||||
const parsed = new URL(url);
|
||||
const result = url.substring(parsed.origin.length);
|
||||
return (result ? result : '/');
|
||||
}
|
||||
|
||||
export function getRootIndexByUrl(groups, url) {
|
||||
const origin = getOrigin(url);
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
if (groups[i].r == origin)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
export function getSafeRootIndexByUrl(groups, url) {
|
||||
let index = -1;
|
||||
try {
|
||||
index = getRootIndexByUrl(groups, url);
|
||||
} catch(e) {
|
||||
//
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
export function getListItemByLink(list, link) {
|
||||
for (const item of list) {
|
||||
if (item.l == link)
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Help в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Income в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Страница не найдена
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
247
client/components/Reader/ContentsPage/ContentsPage.vue
Normal file
247
client/components/Reader/ContentsPage/ContentsPage.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<Window width="600px" ref="window" @close="close">
|
||||
<template slot="header">
|
||||
Оглавление/закладки
|
||||
</template>
|
||||
|
||||
<div class="bg-grey-3 row">
|
||||
<q-tabs
|
||||
v-model="selectedTab"
|
||||
active-color="black"
|
||||
active-bg-color="white"
|
||||
indicator-color="white"
|
||||
dense
|
||||
no-caps
|
||||
inline-label
|
||||
class="no-mp bg-grey-4 text-grey-7"
|
||||
>
|
||||
<q-tab name="contents" icon="la la-list" label="Оглавление" />
|
||||
<q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" />
|
||||
</q-tabs>
|
||||
</div>
|
||||
|
||||
<div class="q-mb-sm"/>
|
||||
|
||||
<div class="tab-panel" v-show="selectedTab == 'contents'">
|
||||
<div>
|
||||
<div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
|
||||
<div class="row item q-px-sm no-wrap">
|
||||
<div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
|
||||
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="20px"/>
|
||||
</div>
|
||||
<div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
|
||||
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="20px"/>
|
||||
</div>
|
||||
<div class="col row clickable" @click="setBookPos(item.offset)">
|
||||
<div :style="item.indentStyle"></div>
|
||||
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
|
||||
<div class="column justify-center">{{ item.perc }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="item.expanded" :ref="`subitem${item.key}`" class="subitems-transition">
|
||||
<div v-for="subitem in item.list" :key="subitem.key" class="row subitem q-px-sm no-wrap">
|
||||
<div class="col row clickable" @click="setBookPos(subitem.offset)">
|
||||
<div class="no-expand-button"></div>
|
||||
<div :style="subitem.indentStyle"></div>
|
||||
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="subitem.label"></div>
|
||||
<div class="column justify-center">{{ subitem.perc }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!contents.length" class="column justify-center items-center" style="height: 100px">
|
||||
Оглавление отсутствует
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" v-show="selectedTab == 'bookmarks'">
|
||||
<div class="column justify-center items-center" style="height: 100px">
|
||||
Раздел находится в разработке
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
//import _ from 'lodash';
|
||||
|
||||
import Window from '../../share/Window.vue';
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
watch: {
|
||||
},
|
||||
})
|
||||
class ContentsPage extends Vue {
|
||||
selectedTab = 'contents';
|
||||
contents = [];
|
||||
|
||||
created() {
|
||||
}
|
||||
|
||||
async init(currentBook, parsed) {
|
||||
this.$refs.window.init();
|
||||
|
||||
//закладки
|
||||
|
||||
//далее формаирование оглавления
|
||||
if (this.parsed == parsed)
|
||||
return;
|
||||
|
||||
this.parsed = parsed;
|
||||
this.contents = [];
|
||||
await this.$nextTick();
|
||||
|
||||
const pc = parsed.contents;
|
||||
const newpc = [];
|
||||
//преобразуем все, кроме первого, разделы body в title-subtitle
|
||||
let curSubtitles = [];
|
||||
let prevBodyIndex = -1;
|
||||
for (let i = 0; i < pc.length; i++) {
|
||||
const cont = pc[i];
|
||||
if (prevBodyIndex != cont.bodyIndex)
|
||||
curSubtitles = [];
|
||||
|
||||
prevBodyIndex = cont.bodyIndex;
|
||||
|
||||
if (cont.bodyIndex > 1) {
|
||||
if (cont.inset < 1) {
|
||||
newpc.push(Object.assign({}, cont, {subtitles: curSubtitles}));
|
||||
} else {
|
||||
curSubtitles.push(Object.assign({}, cont, {inset: cont.inset - 1}));
|
||||
}
|
||||
} else {
|
||||
newpc.push(cont);
|
||||
}
|
||||
}
|
||||
|
||||
const prepareLabel = (title, bolder = false) => {
|
||||
let titleParts = title.split('<p>');
|
||||
const textParts = titleParts.filter(v => v).map(v => `<div>${utils.removeHtmlTags(v)}</div>`);
|
||||
if (bolder && textParts.length > 1)
|
||||
textParts[0] = `<b>${textParts[0]}</b>`;
|
||||
return textParts.join('');
|
||||
}
|
||||
|
||||
const getIndentStyle = inset => `width: ${inset*20}px`;
|
||||
|
||||
const getLabelStyle = (inset) => {
|
||||
const fontSizes = ['110%', '100%', '90%', '85%'];
|
||||
inset = (inset > 3 ? 3 : inset);
|
||||
return `font-size: ${fontSizes[inset]}`;
|
||||
};
|
||||
|
||||
//формируем newContents
|
||||
let i = 0;
|
||||
const newContents = [];
|
||||
newpc.forEach((cont) => {
|
||||
const label = prepareLabel(cont.title, true);
|
||||
const indentStyle = getIndentStyle(cont.inset);
|
||||
const labelStyle = getLabelStyle(cont.inset);
|
||||
|
||||
let j = 0;
|
||||
const list = [];
|
||||
cont.subtitles.forEach((sub) => {
|
||||
const l = prepareLabel(sub.title);
|
||||
const s = getIndentStyle(sub.inset + 1);
|
||||
const ls = getLabelStyle(cont.inset + 1);
|
||||
const p = parsed.para[sub.paraIndex];
|
||||
list[j] = {perc: (p.offset/parsed.textLength*100).toFixed(2), label: l, key: j, offset: p.offset, indentStyle: s, labelStyle: ls};
|
||||
j++;
|
||||
});
|
||||
|
||||
const p = parsed.para[cont.paraIndex];
|
||||
newContents[i] = {perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset, indentStyle, labelStyle, expanded: false, list};
|
||||
|
||||
i++;
|
||||
});
|
||||
|
||||
this.contents = newContents;
|
||||
}
|
||||
|
||||
async expandClick(key) {
|
||||
const item = this.contents[key];
|
||||
const expanded = !item.expanded;
|
||||
|
||||
if (!expanded) {
|
||||
const subitems = this.$refs[`subitem${key}`][0];
|
||||
subitems.style.height = '0';
|
||||
await utils.sleep(200);
|
||||
}
|
||||
|
||||
this.$set(this.contents, key, Object.assign({}, item, {expanded}));
|
||||
|
||||
if (expanded) {
|
||||
await this.$nextTick();
|
||||
const subitems = this.$refs[`subitem${key}`][0];
|
||||
subitems.style.height = subitems.scrollHeight + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
async setBookPos(newValue) {
|
||||
this.$emit('book-pos-changed', {bookPos: newValue});
|
||||
this.close();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$emit('do-action', {action: 'contents'});
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
|
||||
this.close();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tab-panel {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
font-size: 90%;
|
||||
padding: 0 10px 0px 10px;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
padding: 10px 0 10px 0;
|
||||
}
|
||||
|
||||
.item, .subitem {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.item:hover, .subitem:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.expand-button, .no-expand-button {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.subitems-transition {
|
||||
height: 0;
|
||||
transition: height 0.2s linear;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.expanded-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +1,13 @@
|
||||
<template>
|
||||
<div ref="main" class="main" @click="close">
|
||||
<div class="mainWindow" @click.stop>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
Скопировать текст
|
||||
</template>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
Скопировать текст
|
||||
</template>
|
||||
|
||||
<div ref="text" class="text" tabindex="-1">
|
||||
<div v-html="text"></div>
|
||||
</div>
|
||||
</Window>
|
||||
<div ref="text" class="text" tabindex="-1">
|
||||
<div v-html="text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -95,11 +91,11 @@ class CopyTextPage extends Vue {
|
||||
|
||||
close() {
|
||||
this.stopInit = true;
|
||||
this.$emit('copy-text-toggle');
|
||||
this.$emit('do-action', {action: 'copyText'});
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (event.type == 'keydown' && (event.code == 'Escape')) {
|
||||
if (event.type == 'keydown' && event.key == 'Escape') {
|
||||
this.close();
|
||||
}
|
||||
return true;
|
||||
@@ -109,23 +105,6 @@ class CopyTextPage extends Vue {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mainWindow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.text {
|
||||
flex: 1;
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<h4>Возможности читалки:</h4>
|
||||
<span class="text-h6 text-bold">Возможности читалки:</span>
|
||||
<ul>
|
||||
<li>загрузка любой страницы интернета</li>
|
||||
<li>синхронизация данных (настроек и читаемых книг) между различными устройствами</li>
|
||||
<li>работа в автономном режиме (без связи)</li>
|
||||
<li>изменение цвета фона, текста, размер и тип шрифта и прочее</li>
|
||||
<li>установка и запоминание текущей позиции и настроек в браузере (в будущем планируется сохранение и на сервер)</li>
|
||||
<li>установка и запоминание текущей позиции и настроек в браузере и на сервере</li>
|
||||
<li>кэширование файлов книг на клиенте и на сервере</li>
|
||||
<li>открытие книг с локального диска</li>
|
||||
<li>плавный скроллинг текста</li>
|
||||
@@ -12,16 +14,27 @@
|
||||
<li>поиск по тексту и копирование фрагмента</li>
|
||||
<li>запоминание недавних книг, скачивание книги из читалки в формате fb2</li>
|
||||
<li>управление кликом и с клавиатуры</li>
|
||||
<li>подключение к интернету не обязательно для чтения книги после ее загрузки</li>
|
||||
<li>регистрация не требуется</li>
|
||||
<li>поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий</li>
|
||||
</ul>
|
||||
|
||||
<p>В качестве URL можно задавать html-страничку с книгой, либо прямую ссылку
|
||||
<p>В качестве URL книги можно задавать html-страничку с книгой, либо прямую ссылку
|
||||
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").</p>
|
||||
<p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие</p>
|
||||
<p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p>
|
||||
|
||||
<div v-html="automationHtml"></div>
|
||||
<div v-show="mode == 'omnireader' || mode == 'liberama.top'">
|
||||
<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
||||
<br><strong>{{ bookmarkText }}</strong>
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyText(bookmarkText, 'Код для адреса закладки успешно скопирован в буфер обмена')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
|
||||
<br>или перетащив на панель закладок следующую ссылку:
|
||||
<br><a style="margin-left: 50px" :href="bookmarkText">{{ (mode == 'omnireader' ? 'Omni' : 'Liberama') }} Reader</a>
|
||||
<br>Тогда, активировав получившуюся закладку на любой странице интернета, вы автоматически загрузите эту страницу в Omni Reader.
|
||||
<br>В Chrome для Android можно вызывать такую закладку по имени прямо в адресной строке браузера (имя стоит сделать попроще).
|
||||
</p>
|
||||
</div>
|
||||
<p>Связаться с разработчиком: <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a></p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -31,21 +44,29 @@
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
import {copyTextToClipboard} from '../../../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class CommonHelpPage extends Vue {
|
||||
created() {
|
||||
this.config = this.$store.state.config;
|
||||
}
|
||||
|
||||
get automationHtml() {
|
||||
if (this.config.mode == 'omnireader') {
|
||||
return `<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
||||
<br><strong>javascript:location.href='http://omnireader.ru/?url='+location.href;</strong>
|
||||
<br>Тогда, нажав на получившуюся кнопку на любой странице интернета, вы автоматически откроете ее в Omni Reader.</p>`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
get mode() {
|
||||
return this.$store.state.config.mode;
|
||||
}
|
||||
|
||||
get bookmarkText() {
|
||||
return `javascript:location.href='https://${window.location.host}/?url='+location.href;`
|
||||
}
|
||||
|
||||
async copyText(text, mes) {
|
||||
const result = await copyTextToClipboard(text);
|
||||
const msg = (result ? mes : 'Копирование не удалось');
|
||||
if (result)
|
||||
this.$root.notify.success(msg);
|
||||
else
|
||||
this.$root.notify.error(msg);
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
@@ -53,14 +74,16 @@ class CommonHelpPage extends Vue {
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
.copy-icon {
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 120%;
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,30 +1,51 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="box">
|
||||
<p class="p">Проект существует исключительно на личном энтузиазме.</p>
|
||||
<p class="p">Чтобы энтузиазма было побольше, вы можете пожертвовать на развитие проекта любую сумму:</p>
|
||||
<p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p>
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/yandex.png">
|
||||
<el-button class="button" @click="donateYandexMoney">Пожертвовать</el-button><br>
|
||||
<div class="para">{{ yandexAddress }}</div>
|
||||
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYandexMoney">Пожертвовать</q-btn><br>
|
||||
<div class="para">{{ yandexAddress }}
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yandexAddress, 'Яндекс кошелек')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/paypal.png">
|
||||
<div class="para">{{ paypalAddress }}
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(paypalAddress, 'Paypal-адрес')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/bitcoin.png">
|
||||
<el-button class="button" @click="copyAddress(bitcoinAddress, 'Bitcoin')">Скопировать</el-button><br>
|
||||
<div class="para">{{ bitcoinAddress }}</div>
|
||||
<div class="para">{{ bitcoinAddress }}
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(bitcoinAddress, 'Bitcoin-адрес')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/litecoin.png">
|
||||
<el-button class="button" @click="copyAddress(litecoinAddress, 'Litecoin')">Скопировать</el-button><br>
|
||||
<div class="para">{{ litecoinAddress }}</div>
|
||||
<div class="para">{{ litecoinAddress }}
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(litecoinAddress, 'Litecoin-адрес')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/monero.png">
|
||||
<el-button class="button" @click="copyAddress(moneroAddress, 'Monero')">Скопировать</el-button><br>
|
||||
<div class="para">{{ moneroAddress }}</div>
|
||||
<div class="para">{{ moneroAddress }}
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(moneroAddress, 'Monero-адрес')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,6 +61,7 @@ export default @Component({
|
||||
})
|
||||
class DonateHelpPage extends Vue {
|
||||
yandexAddress = '410018702323056';
|
||||
paypalAddress = 'bookpauk@gmail.com';
|
||||
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
|
||||
litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ';
|
||||
moneroAddress = '8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz';
|
||||
@@ -53,11 +75,10 @@ class DonateHelpPage extends Vue {
|
||||
|
||||
async copyAddress(address, prefix) {
|
||||
const result = await copyTextToClipboard(address);
|
||||
const msg = (result ? `${prefix}-адрес ${address} успешно скопирован в буфер обмена` : 'Копирование не удалось');
|
||||
if (result)
|
||||
this.$notify.success({message: msg});
|
||||
this.$root.notify.success(`${prefix} ${address} успешно скопирован в буфер обмена`);
|
||||
else
|
||||
this.$notify.error({message: msg});
|
||||
this.$root.notify.error('Копирование не удалось');
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
@@ -65,12 +86,10 @@ class DonateHelpPage extends Vue {
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.p {
|
||||
@@ -80,15 +99,10 @@ class DonateHelpPage extends Vue {
|
||||
}
|
||||
|
||||
.box {
|
||||
flex: 1;
|
||||
max-width: 550px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.address {
|
||||
padding-top: 10px;
|
||||
margin-top: 20px;
|
||||
@@ -98,13 +112,16 @@ h5 {
|
||||
margin: 10px 10px 10px 40px;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 130px;
|
||||
position: relative;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 120%;
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -1,29 +1,24 @@
|
||||
<template>
|
||||
<div ref="main" class="main" @click="close">
|
||||
<div class="mainWindow" @click.stop>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
Справка
|
||||
</template>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
Справка
|
||||
</template>
|
||||
|
||||
<el-tabs type="border-card" v-model="selectedTab">
|
||||
<el-tab-pane class="tab" label="Общее">
|
||||
<CommonHelpPage></CommonHelpPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Клавиатура">
|
||||
<HotkeysHelpPage></HotkeysHelpPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Мышь/тачпад">
|
||||
<MouseHelpPage></MouseHelpPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Помочь проекту" name="donate">
|
||||
<DonateHelpPage></DonateHelpPage>
|
||||
</el-tab-pane>
|
||||
<div class="col column" style="min-width: 600px">
|
||||
<q-btn-toggle
|
||||
v-model="selectedTab"
|
||||
toggle-color="primary"
|
||||
no-caps unelevated
|
||||
:options="buttons"
|
||||
/>
|
||||
<div class="separator"></div>
|
||||
|
||||
</el-tabs>
|
||||
</Window>
|
||||
<keep-alive>
|
||||
<component ref="page" class="col" :is="activePage"
|
||||
></component>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -35,30 +30,58 @@ import Window from '../../share/Window.vue';
|
||||
import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
|
||||
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
|
||||
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
|
||||
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
|
||||
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
||||
|
||||
const pages = {
|
||||
'CommonHelpPage': CommonHelpPage,
|
||||
'HotkeysHelpPage': HotkeysHelpPage,
|
||||
'MouseHelpPage': MouseHelpPage,
|
||||
'VersionHistoryPage': VersionHistoryPage,
|
||||
'DonateHelpPage': DonateHelpPage,
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
['CommonHelpPage', 'Общее'],
|
||||
['MouseHelpPage', 'Мышь/тачскрин'],
|
||||
['HotkeysHelpPage', 'Клавиатура'],
|
||||
['VersionHistoryPage', 'История версий'],
|
||||
['DonateHelpPage', 'Помочь проекту'],
|
||||
];
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
Window,
|
||||
CommonHelpPage,
|
||||
HotkeysHelpPage,
|
||||
MouseHelpPage,
|
||||
DonateHelpPage,
|
||||
},
|
||||
components: Object.assign({ Window }, pages),
|
||||
})
|
||||
class HelpPage extends Vue {
|
||||
selectedTab = null;
|
||||
selectedTab = 'CommonHelpPage';
|
||||
|
||||
close() {
|
||||
this.$emit('help-toggle');
|
||||
this.$emit('do-action', {action: 'help'});
|
||||
}
|
||||
|
||||
get activePage() {
|
||||
if (pages[this.selectedTab])
|
||||
return pages[this.selectedTab];
|
||||
return null;
|
||||
}
|
||||
|
||||
get buttons() {
|
||||
let result = [];
|
||||
for (const tab of tabs)
|
||||
result.push({label: tab[1], value: tab[0]});
|
||||
return result;
|
||||
}
|
||||
|
||||
activateDonateHelpPage() {
|
||||
this.selectedTab = 'donate';
|
||||
this.selectedTab = 'DonateHelpPage';
|
||||
}
|
||||
|
||||
activateVersionHistoryHelpPage() {
|
||||
this.selectedTab = 'VersionHistoryPage';
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (event.type == 'keydown' && (event.code == 'Escape')) {
|
||||
if (event.type == 'keydown' && event.key == 'Escape') {
|
||||
this.close();
|
||||
}
|
||||
return true;
|
||||
@@ -68,33 +91,8 @@ class HelpPage extends Vue {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mainWindow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.el-tabs {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-tab-pane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
.separator {
|
||||
height: 1px;
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,27 +1,13 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<h4>Управление с помощью горячих клавиш:</h4>
|
||||
<ul>
|
||||
<li><b>F1, H</b> - открыть справку</li>
|
||||
<li><b>Escape</b> - показать/скрыть страницу загрузки</li>
|
||||
<li><b>Tab, Q</b> - показать/скрыть панель управления</li>
|
||||
<li><b>PageUp, Left, Shift+Space, Backspace</b> - страницу назад</li>
|
||||
<li><b>PageDown, Right, Space</b> - страницу вперед</li>
|
||||
<li><b>Home</b> - в начало книги</li>
|
||||
<li><b>End</b> - в конец книги</li>
|
||||
<li><b>Up</b> - строчку назад</li>
|
||||
<li><b>Down</b> - строчку вперёд</li>
|
||||
<li><b>A, Shift+A</b> - изменить размер шрифта</li>
|
||||
<li><b>Enter, F, F11, ` (апостроф)</b> - вкл./выкл. полный экран</li>
|
||||
<li><b>Z</b> - вкл./выкл. плавный скроллинг текста</li>
|
||||
<li><b>Shift+Down/Shift+Up</b> - увеличить/уменьшить скорость скроллинга
|
||||
<li><b>P</b> - установить страницу</li>
|
||||
<li><b>Ctrl+F</b> - найти в тексте</li>
|
||||
<li><b>Ctrl+C</b> - скопировать текст со страницы</li>
|
||||
<li><b>R</b> - принудительно обновить книгу в обход кэша</li>
|
||||
<li><b>X</b> - открыть недавние</li>
|
||||
<li><b>S</b> - открыть окно настроек</li>
|
||||
</ul>
|
||||
<div style="font-size: 120%">
|
||||
<div class="text-h6 text-bold">Доступны следующие клавиатурные команды:</div>
|
||||
<br>
|
||||
</div>
|
||||
<div class="q-mb-md" style="width: 550px">
|
||||
<div class="text-right text-italic" style="font-size: 80%">* Изменить сочетания клавиш можно в настройках</div>
|
||||
<UserHotKeys v-model="userHotKeys" readonly/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -30,25 +16,32 @@
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
import UserHotKeys from '../../SettingsPage/UserHotKeys/UserHotKeys.vue';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
UserHotKeys,
|
||||
},
|
||||
})
|
||||
class HotkeysHelpPage extends Vue {
|
||||
created() {
|
||||
}
|
||||
|
||||
get userHotKeys() {
|
||||
return this.$store.state.reader.settings.userHotKeys;
|
||||
}
|
||||
|
||||
set userHotKeys(value) {
|
||||
//no setter
|
||||
}
|
||||
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<h4>Управление с помощью мыши/тачпада:</h4>
|
||||
<span class="text-h6 text-bold">Управление с помощью мыши/тачскрина:</span>
|
||||
<ul>
|
||||
<li><b>ЛКМ/ТАЧ</b> по экрану в одну из областей - активация действия:</li>
|
||||
<div class="click-map-page">
|
||||
<ClickMapPage ref="clickMapPage"></ClickMapPage>
|
||||
</div>
|
||||
|
||||
<li><b>ПКМ</b> - показать/скрыть панель управления</li>
|
||||
<li><b>СКМ</b> - вкл./выкл. плавный скроллинг текста</li>
|
||||
<br>
|
||||
<li>Жесты для тачскрина:</li>
|
||||
<ul>
|
||||
<li style="list-style-type: square">от центра вверх: на весь экран</li>
|
||||
<li style="list-style-type: square">от центра вниз: плавный скроллинг</li>
|
||||
<li style="list-style-type: square">от центра вправо: увеличить скорость скроллинга</li>
|
||||
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
|
||||
</ul>
|
||||
|
||||
</ul>
|
||||
* Для управления с помощью мыши/тачпада необходимо установить галочку "Включить управление кликом" в настройках
|
||||
* Для управления с помощью мыши/тачскрина необходимо установить галочку "Включить управление кликом" в настройках
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -39,17 +49,12 @@ class MouseHelpPage extends Vue {
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.click-map-page {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div id="versionHistoryPage" class="page">
|
||||
<span class="text-h6 text-bold">История версий:</span>
|
||||
<br><br>
|
||||
|
||||
<span class="clickable" v-for="(item, index) in versionHeader" :key="index" @click="showRelease(item)">
|
||||
<p>
|
||||
{{ item }}
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<br>
|
||||
|
||||
<div v-for="item in versionContent" :id="item.key" :key="item.key">
|
||||
<span v-html="item.content"></span>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {versionHistory} from '../../versionHistory';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class VersionHistoryPage extends Vue {
|
||||
versionHeader = [];
|
||||
versionContent = [];
|
||||
|
||||
created() {
|
||||
}
|
||||
|
||||
mounted() {
|
||||
let vh = [];
|
||||
for (const version of versionHistory) {
|
||||
vh.push(version.header);
|
||||
}
|
||||
this.versionHeader = vh;
|
||||
|
||||
let vc = [];
|
||||
for (const version of versionHistory) {
|
||||
vc.push({key: version.header, content: 'Версия ' + version.header + version.content});
|
||||
}
|
||||
this.versionContent = vc;
|
||||
}
|
||||
|
||||
showRelease(id) {
|
||||
let el = document.getElementById(id);
|
||||
if (el) {
|
||||
document.getElementById('versionHistoryPage').scrollTop = el.offsetTop;
|
||||
}
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -1,287 +0,0 @@
|
||||
<template>
|
||||
<div ref="main" class="main" @click="close">
|
||||
<div class="mainWindow" @click.stop>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
Последние 100 открытых книг
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
size="mini"
|
||||
height="1px"
|
||||
stripe
|
||||
border
|
||||
:default-sort = "{prop: 'touchDateTime', order: 'descending'}"
|
||||
:header-cell-style = "headerCellStyle"
|
||||
:row-key = "rowKey"
|
||||
>
|
||||
|
||||
<el-table-column
|
||||
type="index"
|
||||
width="35px"
|
||||
>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="touchDateTime"
|
||||
min-width="90px"
|
||||
sortable
|
||||
>
|
||||
<template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
|
||||
<span style="font-size: 90%">Время<br>просм.</span>
|
||||
</template>
|
||||
<template slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
|
||||
<div class="desc" @click="loadBook(scope.row.url)">
|
||||
{{ scope.row.touchDate }}<br>
|
||||
{{ scope.row.touchTime }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
>
|
||||
<template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
|
||||
<!--el-input ref="input"
|
||||
:value="search" @input="search = $event"
|
||||
size="mini"
|
||||
style="margin: 0; padding: 0; vertical-align: bottom; margin-top: 10px"
|
||||
placeholder="Найти"/-->
|
||||
<div class="el-input el-input--mini">
|
||||
<input class="el-input__inner"
|
||||
ref="input"
|
||||
placeholder="Найти"
|
||||
style="margin: 0; padding: 0; vertical-align: bottom; margin-top: 20px; padding: 0 10px 0 10px"
|
||||
:value="search" @input="search = $event.target.value"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table-column
|
||||
min-width="300px"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<div class="desc" @click="loadBook(scope.row.url)">
|
||||
<span style="color: green">{{ scope.row.desc.author }}</span><br>
|
||||
<span>{{ scope.row.desc.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
min-width="100px"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<a v-show="isUrl(scope.row.url)" :href="scope.row.url" target="_blank">Оригинал</a><br>
|
||||
<a :href="scope.row.path" :download="getFileNameFromPath(scope.row.path)">Скачать FB2</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
width="60px"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
style="width: 30px; padding: 7px 0 7px 0; margin-left: 4px"
|
||||
@click="handleDel(scope.row.key)"><i class="el-icon-close"></i>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
</Window>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import path from 'path';
|
||||
import _ from 'lodash';
|
||||
|
||||
import {formatDate} from '../../../share/utils';
|
||||
import Window from '../../share/Window.vue';
|
||||
import bookManager from '../share/bookManager';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
watch: {
|
||||
search: function() {
|
||||
this.updateTableData();
|
||||
}
|
||||
},
|
||||
})
|
||||
class HistoryPage extends Vue {
|
||||
search = null;
|
||||
tableData = null;
|
||||
|
||||
created() {
|
||||
}
|
||||
|
||||
init() {
|
||||
this.updateTableData();
|
||||
this.$nextTick(() => {
|
||||
this.$refs.input.focus();
|
||||
});
|
||||
}
|
||||
|
||||
rowKey(row) {
|
||||
return row.key;
|
||||
}
|
||||
|
||||
updateTableData() {
|
||||
let result = [];
|
||||
|
||||
const sorted = bookManager.getSortedRecent();
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const book = sorted[i];
|
||||
if (book.deleted)
|
||||
continue;
|
||||
|
||||
let d = new Date();
|
||||
d.setTime(book.touchTime);
|
||||
const t = formatDate(d).split(' ');
|
||||
|
||||
let perc = '';
|
||||
let textLen = '';
|
||||
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
|
||||
if (book.textLength) {
|
||||
perc = ` [${((p/book.textLength)*100).toFixed(2)}%]`;
|
||||
textLen = ` ${Math.round(book.textLength/1000)}k`;
|
||||
}
|
||||
|
||||
const fb2 = (book.fb2 ? book.fb2 : {});
|
||||
|
||||
let title = fb2.bookTitle;
|
||||
if (title)
|
||||
title = `"${title}"`;
|
||||
else
|
||||
title = '';
|
||||
|
||||
let author = '';
|
||||
if (fb2.author) {
|
||||
const authorNames = fb2.author.map(a => _.compact([
|
||||
a.lastName,
|
||||
a.firstName,
|
||||
a.middleName
|
||||
]).join(' '));
|
||||
author = authorNames.join(', ');
|
||||
} else {
|
||||
author = _.compact([
|
||||
fb2.lastName,
|
||||
fb2.firstName,
|
||||
fb2.middleName
|
||||
]).join(' ');
|
||||
}
|
||||
author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url));
|
||||
|
||||
result.push({
|
||||
touchDateTime: book.touchTime,
|
||||
touchDate: t[0],
|
||||
touchTime: t[1],
|
||||
desc: {
|
||||
title: `${title}${perc}${textLen}`,
|
||||
author,
|
||||
},
|
||||
url: book.url,
|
||||
path: book.path,
|
||||
key: book.key,
|
||||
});
|
||||
if (result.length >= 100)
|
||||
break;
|
||||
}
|
||||
|
||||
const search = this.search;
|
||||
result = result.filter(item => {
|
||||
return !search ||
|
||||
item.touchTime.includes(search) ||
|
||||
item.touchDate.includes(search) ||
|
||||
item.desc.title.toLowerCase().includes(search.toLowerCase()) ||
|
||||
item.desc.author.toLowerCase().includes(search.toLowerCase())
|
||||
});
|
||||
|
||||
this.tableData = result;
|
||||
}
|
||||
|
||||
headerCellStyle(cell) {
|
||||
let result = {margin: 0, padding: 0};
|
||||
if (cell.columnIndex > 0) {
|
||||
result['border-bottom'] = 0;
|
||||
}
|
||||
if (cell.rowIndex > 0) {
|
||||
result.height = '0px';
|
||||
result['border-right'] = 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getFileNameFromPath(fb2Path) {
|
||||
return path.basename(fb2Path).substr(0, 10) + '.fb2';
|
||||
}
|
||||
|
||||
openOriginal(url) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
openFb2(path) {
|
||||
window.open(path, '_blank');
|
||||
}
|
||||
|
||||
async handleDel(key) {
|
||||
await bookManager.delRecentBook({key});
|
||||
this.updateTableData();
|
||||
|
||||
if (!bookManager.mostRecentBook())
|
||||
this.close();
|
||||
}
|
||||
|
||||
loadBook(url) {
|
||||
this.$emit('load-book', {url});
|
||||
this.close();
|
||||
}
|
||||
|
||||
isUrl(url) {
|
||||
return (url.indexOf('file://') != 0);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$emit('history-toggle');
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (event.type == 'keydown' && event.code == 'Escape') {
|
||||
this.close();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mainWindow {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.desc {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
131
client/components/Reader/LibsPage/LibsPage.vue
Normal file
131
client/components/Reader/LibsPage/LibsPage.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="hidden"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
import Window from '../../share/Window.vue';
|
||||
import * as utils from '../../../share/utils';
|
||||
//import rstore from '../../../store/modules/reader';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
Window
|
||||
},
|
||||
watch: {
|
||||
libs: function() {
|
||||
this.sendLibs();
|
||||
},
|
||||
}
|
||||
})
|
||||
class LibsPage extends Vue {
|
||||
created() {
|
||||
this.popupWindow = null;
|
||||
this.commit = this.$store.commit;
|
||||
this.messageListener = null;
|
||||
//this.commit('reader/setLibs', rstore.libsDefaults);
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.mode != 'liberama.top')
|
||||
return;
|
||||
|
||||
this.childReady = false;
|
||||
const subdomain = (window.location.protocol != 'http:' ? 'b.' : '');
|
||||
this.origin = `http://${subdomain}${window.location.host}`;
|
||||
|
||||
this.messageListener = (event) => {
|
||||
if (event.origin !== this.origin)
|
||||
return;
|
||||
|
||||
//console.log(event.data);
|
||||
|
||||
this.recvMessage(event.data);
|
||||
};
|
||||
|
||||
this.popupWindow = window.open(`${this.origin}/#/external-libs`);
|
||||
|
||||
if (this.popupWindow) {
|
||||
|
||||
window.addEventListener('message', this.messageListener);
|
||||
|
||||
//Проверка закрытия окна
|
||||
(async() => {
|
||||
while(this.popupWindow) {
|
||||
if (this.popupWindow && this.popupWindow.closed)
|
||||
this.close();
|
||||
await utils.sleep(1000);
|
||||
}
|
||||
})();
|
||||
|
||||
//Установление связи с окном
|
||||
(async() => {
|
||||
let i = 0;
|
||||
while(!this.childReady && this.popupWindow && i < 100) {
|
||||
this.sendMessage({type: 'mes', data: 'hello'});
|
||||
await utils.sleep(100);
|
||||
i++;
|
||||
}
|
||||
this.sendLibs();
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
recvMessage(d) {
|
||||
if (d.type == 'mes') {
|
||||
switch(d.data) {
|
||||
case 'ready':
|
||||
this.childReady = true;
|
||||
break;
|
||||
}
|
||||
} else if (d.type == 'libs') {
|
||||
this.commit('reader/setLibs', d.data);
|
||||
} else if (d.type == 'close') {
|
||||
this.close();
|
||||
} else if (d.type == 'submitUrl') {
|
||||
this.$emit('load-book', d.data);
|
||||
this.sendMessage({type: 'notify', data: 'Ссылка передана в читалку'});
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(d) {
|
||||
if (this.popupWindow)
|
||||
this.popupWindow.postMessage(Object.assign({}, {from: 'LibsPage'}, d), this.origin);
|
||||
}
|
||||
|
||||
done() {
|
||||
window.removeEventListener('message', this.messageListener);
|
||||
if (this.popupWindow) {
|
||||
this.popupWindow.close();
|
||||
this.popupWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this.$store.state.config.mode;
|
||||
}
|
||||
|
||||
get libs() {
|
||||
return this.$store.state.reader.libs;
|
||||
}
|
||||
|
||||
sendLibs() {
|
||||
this.sendMessage({type: 'libs', data: this.libs});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$emit('libs-close');
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.separator {
|
||||
height: 1px;
|
||||
background-color: #A0A0A0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div id="vue-github-corner">
|
||||
<a :href="url" id="github-corner" target="_blank" aria-label="View source on Github" >
|
||||
<svg id="github-corner-svg"
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 250 250"
|
||||
:width="size" :height="size"
|
||||
:style="svgStyle" >
|
||||
<path :d="svgPath1" @mouseenter="flipColor" @mouseleave="flipColor"></path>
|
||||
<path :d="svgPath2" :style="gitStyle" class="octo-arm"></path>
|
||||
<path :d="svgPath3" :style="gitStyle" class="octo-body"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'GithubCorner',
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
default: '/'
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 80
|
||||
},
|
||||
colorScheme: {
|
||||
type: String,
|
||||
default: 'auto'
|
||||
},
|
||||
cornerColor: {
|
||||
type: String,
|
||||
default: '#625D5D'
|
||||
},
|
||||
gitColor: {
|
||||
type: String,
|
||||
default: 'PeachPuff'
|
||||
},
|
||||
leftCorner: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
flipOnHover: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
svgStyle: {
|
||||
fill: this.cornerColor,
|
||||
right: (this.leftCorner ? 'auto' : '0'),
|
||||
left: (this.leftCorner ? '0' : 'auto'),
|
||||
transform: (this.leftCorner ? 'scale(-1, 1)' : 'none')
|
||||
},
|
||||
gitStyle: {
|
||||
fill: this.gitColor
|
||||
},
|
||||
flipped: false,
|
||||
svgPath1: 'M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z',
|
||||
svgPath2: 'M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 ' +
|
||||
'123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2',
|
||||
svgPath3: 'M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 ' +
|
||||
'C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 ' +
|
||||
'176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 ' +
|
||||
'216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 ' +
|
||||
'C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
flipColor: function() {
|
||||
if (this.flipOnHover) {
|
||||
let holdSvgFill = this.svgStyle.fill
|
||||
this.svgStyle.fill = this.gitStyle.fill
|
||||
this.gitStyle.fill = holdSvgFill
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeMount: function() {
|
||||
if (this.colorScheme != 'auto') {
|
||||
let sch = this.colorScheme
|
||||
this.gitStyle.fill = '#fff'
|
||||
|
||||
if (sch.toLowerCase() == 'black') {
|
||||
this.svgStyle.fill = '#151513'
|
||||
}
|
||||
if (sch.toLowerCase() == 'green') {
|
||||
this.svgStyle.fill = '#64CEAA'
|
||||
}
|
||||
if (sch.toLowerCase() == 'red') {
|
||||
this.svgStyle.fill = '#FD6C6C'
|
||||
}
|
||||
if (sch.toLowerCase() == 'blue') {
|
||||
this.svgStyle.fill = '#70B7FD'
|
||||
}
|
||||
if (sch.toLowerCase() == 'white') {
|
||||
this.svgStyle.fill = '#fff'
|
||||
this.gitStyle.fill = '#151513'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#github-corner .octo-arm {
|
||||
transform-origin: 130px 106px
|
||||
}
|
||||
#github-corner:hover .octo-arm {
|
||||
animation: octocat-wave 560ms ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes octocat-wave {
|
||||
0% { transform: rotate(0deg); }
|
||||
20% { transform: rotate(-25deg); }
|
||||
40% { transform: rotate(10deg); }
|
||||
60% { transform: rotate(-25deg); }
|
||||
80% { transform: rotate(10deg); }
|
||||
100% { transform: rotate(0deg); }
|
||||
}
|
||||
|
||||
#github-corner-svg {
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#github-corner-svg, #github-corner-svg .octo-arm, #github-corner-svg .octo-body {
|
||||
transition: fill 1s ease;
|
||||
}
|
||||
</style>
|
||||
@@ -1,31 +1,58 @@
|
||||
<template>
|
||||
<div ref="main" class="main">
|
||||
<div class="part">
|
||||
<span class="greeting bold-font">{{ title }}</span>
|
||||
<div class="space"></div>
|
||||
<div ref="main" class="column no-wrap" style="min-height: 500px">
|
||||
<div v-if="mode != 'liberama.top'" class="relative-position">
|
||||
<GithubCorner url="https://github.com/bookpauk/liberama" cornerColor="#1B695F" gitColor="#EBE2C9"></GithubCorner>
|
||||
</div>
|
||||
<div class="col column justify-center items-center no-wrap overflow-hidden" style="min-height: 230px">
|
||||
<span class="greeting"><b>{{ title }}</b></span>
|
||||
<div class="q-my-sm"></div>
|
||||
<span class="greeting">Добро пожаловать!</span>
|
||||
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz</b></span>
|
||||
<span v-if="isExternalConverter" class="greeting">...а также форматы: <b>rtf, doc, docx, pdf, epub, mobi</b></span>
|
||||
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz<span v-if="isExternalConverter">, rar</span></b></span>
|
||||
<span v-if="isExternalConverter" class="greeting">...а также частично форматы: <b>epub, mobi, rtf, doc, docx, pdf, djvu</b></span>
|
||||
</div>
|
||||
|
||||
<div class="part center">
|
||||
<el-input ref="input" placeholder="URL книги" v-model="bookUrl">
|
||||
<el-button slot="append" icon="el-icon-check" @click="submitUrl"></el-button>
|
||||
</el-input>
|
||||
<div class="space"></div>
|
||||
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
||||
<q-input ref="input" class="full-width q-px-sm" style="max-width: 700px" outlined dense bg-color="white" v-model="bookUrl" placeholder="URL книги">
|
||||
<template v-slot:append>
|
||||
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl"/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<input type="file" id="file" ref="file" @change="loadFile" style='display: none;'/>
|
||||
<el-button size="mini" @click="loadFileClick">
|
||||
|
||||
<div class="q-my-sm"></div>
|
||||
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
|
||||
Загрузить файл с диска
|
||||
</el-button>
|
||||
<div class="space"></div>
|
||||
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openComments">Комментарии</span>
|
||||
</q-btn>
|
||||
|
||||
<div class="q-my-sm"></div>
|
||||
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
|
||||
Из буфера обмена
|
||||
</q-btn>
|
||||
|
||||
<div class="q-my-md"></div>
|
||||
<!--div v-if="mode == 'omnireader'">
|
||||
<div ref="yaShare2" class="ya-share2"
|
||||
data-services="collections,vkontakte,facebook,odnoklassniki,twitter,telegram"
|
||||
data-description="Чтение fb2-книг онлайн. Загрузка любой страницы интернета одним кликом, синхронизация между устройствами, удобное управление, регистрация не требуется."
|
||||
data-title="Omni Reader - браузерная онлайн-читалка"
|
||||
data-url="https://omnireader.ru">
|
||||
</div>
|
||||
</div>
|
||||
<div class="q-my-sm"></div-->
|
||||
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openComments">Отзывы о читалке</span>
|
||||
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openOldVersion">Старая версия</span>
|
||||
</div>
|
||||
|
||||
<div class="part bottom">
|
||||
<div class="col column justify-end items-center no-wrap overflow-hidden">
|
||||
<span class="bottom-span clickable" @click="openHelp">Справка</span>
|
||||
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
|
||||
<span class="bottom-span">{{ version }}</span>
|
||||
|
||||
<span v-if="version == clientVersion" class="bottom-span">v{{ version }}</span>
|
||||
<span v-else class="bottom-span">Версия сервера {{ version }}, версия клиента {{ clientVersion }}, необходимо обновить страницу</span>
|
||||
</div>
|
||||
|
||||
<PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -33,12 +60,21 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import GithubCorner from './GithubCorner/GithubCorner.vue';
|
||||
|
||||
import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
|
||||
import {versionHistory} from '../versionHistory';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
GithubCorner,
|
||||
PasteTextPage,
|
||||
},
|
||||
})
|
||||
class LoaderPage extends Vue {
|
||||
bookUrl = null;
|
||||
loadPercent = 0;
|
||||
pasteTextActive = false;
|
||||
|
||||
created() {
|
||||
this.commit = this.$store.commit;
|
||||
@@ -46,6 +82,8 @@ class LoaderPage extends Vue {
|
||||
|
||||
mounted() {
|
||||
this.progress = this.$refs.progress;
|
||||
/*if (this.mode == 'omnireader')
|
||||
Ya.share2(this.$refs.yaShare2);// eslint-disable-line no-undef*/
|
||||
}
|
||||
|
||||
activated() {
|
||||
@@ -53,8 +91,10 @@ class LoaderPage extends Vue {
|
||||
}
|
||||
|
||||
get title() {
|
||||
if (this.$store.state.config.mode == 'omnireader')
|
||||
if (this.mode == 'omnireader')
|
||||
return 'Omni Reader - браузерная онлайн-читалка.';
|
||||
if (this.mode == 'liberama.top')
|
||||
return 'Liberama Reader - браузерная онлайн-читалка.';
|
||||
return 'Универсальная читалка книг и ресурсов интернета.';
|
||||
|
||||
}
|
||||
@@ -64,16 +104,22 @@ class LoaderPage extends Vue {
|
||||
}
|
||||
|
||||
get version() {
|
||||
return `v${this.$store.state.config.version}`;
|
||||
return this.$store.state.config.version;
|
||||
}
|
||||
|
||||
get isExternalConverter() {
|
||||
return this.$store.state.config.useExternalBookConverter;
|
||||
}
|
||||
|
||||
get clientVersion() {
|
||||
let v = versionHistory[0].header;
|
||||
v = v.split(' ')[0];
|
||||
return v;
|
||||
}
|
||||
|
||||
submitUrl() {
|
||||
if (this.bookUrl) {
|
||||
this.$emit('load-book', {url: this.bookUrl});
|
||||
this.$emit('load-book', {url: this.bookUrl, force: true});
|
||||
this.bookUrl = '';
|
||||
}
|
||||
}
|
||||
@@ -83,98 +129,83 @@ class LoaderPage extends Vue {
|
||||
}
|
||||
|
||||
loadFile() {
|
||||
const file = this.$refs.file.files[0];
|
||||
const file = this.$refs.file.files[0];
|
||||
this.$refs.file.value = '';
|
||||
if (file)
|
||||
this.$emit('load-file', {file});
|
||||
}
|
||||
|
||||
openHelp() {
|
||||
this.$emit('help-toggle');
|
||||
loadBufferClick() {
|
||||
this.pasteTextToggle();
|
||||
}
|
||||
|
||||
loadBuffer(opts) {
|
||||
if (opts.buffer.length) {
|
||||
const file = new File([opts.buffer], 'dummyName-PasteFromClipboard');
|
||||
this.$emit('load-file', {file});
|
||||
}
|
||||
}
|
||||
|
||||
pasteTextToggle() {
|
||||
this.pasteTextActive = !this.pasteTextActive;
|
||||
}
|
||||
|
||||
openHelp(event) {
|
||||
this.$emit('do-action', {action: 'help', event});
|
||||
}
|
||||
|
||||
openDonate() {
|
||||
this.$emit('donate-toggle');
|
||||
this.$emit('do-action', {action: 'donate'});
|
||||
}
|
||||
|
||||
openComments() {
|
||||
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
|
||||
}
|
||||
|
||||
openOldVersion() {
|
||||
window.open('http://old.omnireader.ru', '_blank');
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (this.pasteTextActive) {
|
||||
return this.$refs.pasteTextPage.keyHook(event);
|
||||
}
|
||||
|
||||
//недостатки сторонних ui
|
||||
const input = this.$refs.input.$refs.input;
|
||||
if (document.activeElement === input && event.type == 'keydown' && event.code == 'Enter') {
|
||||
if (document.activeElement === input && event.type == 'keydown' && event.key == 'Enter') {
|
||||
this.submitUrl();
|
||||
}
|
||||
|
||||
if (event.type == 'keydown' && (event.code == 'F1' || (document.activeElement !== input && event.code == 'KeyH'))) {
|
||||
this.$emit('help-toggle');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.type == 'keydown' && (document.activeElement !== input && event.code == 'KeyQ')) {
|
||||
this.$emit('tool-bar-toggle');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return true;
|
||||
if (event.type == 'keydown' && document.activeElement !== input) {
|
||||
const action = this.$root.readerActionByKeyEvent(event);
|
||||
switch (action) {
|
||||
case 'help':
|
||||
this.openHelp(event);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
<style scoped>
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 340px;
|
||||
}
|
||||
|
||||
.part {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.greeting {
|
||||
font-size: 120%;
|
||||
line-height: 160%;
|
||||
}
|
||||
|
||||
.bold-font {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.center {
|
||||
justify-content: flex-start;
|
||||
padding: 0 10px 0 10px;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.bottom-span {
|
||||
font-size: 70%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.space {
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
<span style="position: relative; top: -3px">
|
||||
Вставьте текст и нажмите
|
||||
<span class="clickable text-primary" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
|
||||
или F2
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<q-input class="q-px-sm" dense borderless v-model="bookTitle" placeholder="Введите название текста"/>
|
||||
<hr/>
|
||||
<textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
import Window from '../../../share/Window.vue';
|
||||
import _ from 'lodash';
|
||||
import * as utils from '../../../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
})
|
||||
class PasteTextPage extends Vue {
|
||||
bookTitle = '';
|
||||
|
||||
created() {
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.$refs.textArea.focus();
|
||||
}
|
||||
|
||||
getNonEmptyLine3words(text, count) {
|
||||
let result = '';
|
||||
const lines = text.split("\n");
|
||||
let i = 0;
|
||||
while (i < lines.length) {
|
||||
if (lines[i].trim() != '') {
|
||||
count--;
|
||||
if (count <= 0) {
|
||||
result = lines[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
result = result.trim().split(' ');
|
||||
return result.slice(0, 3).join(' ');
|
||||
}
|
||||
|
||||
calcTitle(event) {
|
||||
if (this.bookTitle == '') {
|
||||
let text = event.clipboardData.getData('text');
|
||||
this.bookTitle = `Из буфера обмена ${utils.formatDate(new Date(), 'noDate')}: ` + _.compact([
|
||||
this.getNonEmptyLine3words(text, 1),
|
||||
this.getNonEmptyLine3words(text, 2)
|
||||
]).join(' - ');
|
||||
}
|
||||
}
|
||||
|
||||
loadBuffer() {
|
||||
this.$emit('load-buffer', {buffer: `<buffer><cut-title>${utils.escapeXml(this.bookTitle)}</cut-title>${this.$refs.textArea.value}</buffer>`});
|
||||
this.close();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$emit('paste-text-toggle');
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (event.type == 'keydown') {
|
||||
switch (event.key) {
|
||||
case 'F2':
|
||||
this.loadBuffer();
|
||||
break;
|
||||
case 'Escape':
|
||||
this.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text {
|
||||
flex: 1;
|
||||
overflow-wrap: anywhere;
|
||||
overflow-y: auto;
|
||||
padding: 0 10px 0 10px;
|
||||
position: relative;
|
||||
font-size: 120%;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.text:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
color: blue;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,8 +1,24 @@
|
||||
<template>
|
||||
<div v-show="visible" class="main">
|
||||
<div class="center">
|
||||
<el-progress type="circle" :width="100" :stroke-width="6" color="#0F9900" :percentage="percentage"></el-progress>
|
||||
<p class="text">{{ text }}</p>
|
||||
<div v-show="visible" class="column justify-center items-center z-max" style="background-color: rgba(0, 0, 0, 0.8)">
|
||||
<div class="column justify-start items-center" style="height: 250px">
|
||||
<q-circular-progress
|
||||
show-value
|
||||
instant-feedback
|
||||
font-size="13px"
|
||||
:value="percentage"
|
||||
size="100px"
|
||||
:thickness="0.11"
|
||||
color="green-7"
|
||||
track-color="grey-4"
|
||||
class="q-ma-md"
|
||||
>
|
||||
<span class="text-yellow">{{ percentage }}%</span>
|
||||
</q-circular-progress>
|
||||
|
||||
<div>
|
||||
<span class="text-yellow">{{ text }}</span>
|
||||
<q-icon :style="iconStyle" color="yellow" name="la la-slash" size="20px"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -11,11 +27,13 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
const ruMessage = {
|
||||
'start': ' ',
|
||||
'finish': ' ',
|
||||
'error': ' ',
|
||||
'queue': 'очередь',
|
||||
'download': 'скачивание',
|
||||
'decompress': 'распаковка',
|
||||
'convert': 'конвертирование',
|
||||
@@ -32,68 +50,51 @@ class ProgressPage extends Vue {
|
||||
step = 1;
|
||||
progress = 0;
|
||||
visible = false;
|
||||
iconStyle = '';
|
||||
|
||||
show() {
|
||||
this.$el.style.width = this.$parent.$el.offsetWidth + 'px';
|
||||
this.$el.style.height = this.$parent.$el.offsetHeight + 'px';
|
||||
this.text = '';
|
||||
this.totalSteps = 1;
|
||||
this.step = 1;
|
||||
this.progress = 0;
|
||||
this.iconAngle = 0;
|
||||
this.ani = false;
|
||||
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.visible = false;
|
||||
this.text = '';
|
||||
this.iconAngle = 0;
|
||||
}
|
||||
|
||||
setState(state) {
|
||||
if (state.state)
|
||||
this.text = (ruMessage[state.state] ? ruMessage[state.state] : state.state);
|
||||
if (state.state) {
|
||||
if (state.state == 'queue') {
|
||||
this.text = (state.place ? 'Номер в очереди: ' + state.place : '');
|
||||
} else {
|
||||
this.text = (ruMessage[state.state] ? ruMessage[state.state] : state.state);
|
||||
}
|
||||
}
|
||||
this.step = (state.step ? state.step : this.step);
|
||||
this.totalSteps = (state.totalSteps > this.totalSteps ? state.totalSteps : this.totalSteps);
|
||||
this.progress = state.progress || 0;
|
||||
|
||||
if (!this.ani) {
|
||||
(async() => {
|
||||
this.ani = true;
|
||||
this.iconAngle += 30;
|
||||
this.iconStyle = `transform: rotate(${this.iconAngle}deg); transition: 150ms linear`;
|
||||
await utils.sleep(150);
|
||||
this.ani = false;
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
get percentage() {
|
||||
let circle = document.querySelector('path[class="el-progress-circle__path"]');
|
||||
if (circle)
|
||||
circle.style.transition = '';
|
||||
return Math.round(((this.step - 1)/this.totalSteps + this.progress/(100*this.totalSteps))*100);
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
<style scoped>
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
z-index: 100;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
|
||||
position: absolute;
|
||||
}
|
||||
.center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
color: white;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
.el-progress__text {
|
||||
color: lightgreen;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
250
client/components/Reader/ReaderDialogs/ReaderDialogs.vue
Normal file
250
client/components/Reader/ReaderDialogs/ReaderDialogs.vue
Normal file
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<div>
|
||||
<Dialog ref="dialog1" v-model="whatsNewVisible">
|
||||
<template slot="header">
|
||||
Что нового:
|
||||
</template>
|
||||
|
||||
<div style="line-height: 20px" v-html="whatsNewContent"></div>
|
||||
|
||||
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
|
||||
<span slot="footer">
|
||||
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn>
|
||||
</span>
|
||||
</Dialog>
|
||||
|
||||
<Dialog ref="dialog2" v-model="donationVisible">
|
||||
<template slot="header">
|
||||
Здравствуйте, уважаемые читатели!
|
||||
</template>
|
||||
|
||||
<div style="word-break: normal">
|
||||
Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br>
|
||||
|
||||
Для оплаты годового хостинга читалки, необходимо собрать около 2000 рублей.
|
||||
В настоящий момент у автора эта сумма есть в наличии. Однако будет справедливо, если каждый
|
||||
сможет проголосовать рублем за то, чтобы читалка так и оставалась:
|
||||
|
||||
<ul>
|
||||
<li>непрерывно улучшаемой</li>
|
||||
<li>без рекламы</li>
|
||||
<li>без регистрации</li>
|
||||
<li>Open Source</li>
|
||||
</ul>
|
||||
|
||||
Автор также обращается с просьбой о помощи в распространении
|
||||
<a href="https://omnireader.ru" target="_blank">ссылки</a>
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyLink('https://omnireader.ru')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
на читалку через тематические форумы, соцсети, мессенджеры и пр.
|
||||
Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
|
||||
|
||||
<br><br>
|
||||
Если соберется бóльшая сумма, то разработка децентрализованной библиотеки для свободного обмена книгами будет по возможности ускорена.
|
||||
<br><br>
|
||||
P.S. При необходимости можно воспользоваться подходящим обменником на <a href="https://www.bestchange.ru" target="_blank">bestchange.ru</a>
|
||||
|
||||
<br><br>
|
||||
<div class="row justify-center">
|
||||
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span slot="footer">
|
||||
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
|
||||
<br>
|
||||
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
|
||||
</span>
|
||||
</Dialog>
|
||||
|
||||
<Dialog ref="dialog3" v-model="liberamaTopVisible">
|
||||
<template slot="header">
|
||||
Здравствуйте, уважаемые читатели!
|
||||
</template>
|
||||
|
||||
<div style="word-break: normal">
|
||||
Создан новый ресурс:<br><br>
|
||||
|
||||
<a href="https://liberama.top" target="_blank">https://liberama.top</a>
|
||||
<br><br>
|
||||
Это клон читалки Omni Reader, но с некоторыми дополнениями, ориентированными в сторону более свободного обмена книгами:
|
||||
|
||||
<ul>
|
||||
<li>добавлено новое окно "Библиотека" для свободного доступа к Флибусте и другим ресурсам по желанию читателя</li>
|
||||
<li>планируется добавить возможность создания подборок книг и обмена ими между пользователями</li>
|
||||
</ul>
|
||||
|
||||
Легко мигрировать на новый сайт можно с помощью синхронизации с сервером.
|
||||
О багах и предложениях просьба сообщать на почту <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a><br><br>
|
||||
Спасибо, что вы с нами!
|
||||
<br><br>
|
||||
<div class="row justify-center">
|
||||
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span slot="footer">
|
||||
<q-btn class="q-px-sm" dense no-caps @click="liberamaTopDialogDisable">Больше не показывать</q-btn>
|
||||
</span>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
import Dialog from '../../share/Dialog.vue';
|
||||
import * as utils from '../../../share/utils';
|
||||
import {versionHistory} from '../versionHistory';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
Dialog
|
||||
},
|
||||
watch: {
|
||||
settings: function() {
|
||||
this.loadSettings();
|
||||
},
|
||||
},
|
||||
})
|
||||
class ReaderDialogs extends Vue {
|
||||
whatsNewVisible = false;
|
||||
whatsNewContent = '';
|
||||
donationVisible = false;
|
||||
liberamaTopVisible = false;
|
||||
|
||||
created() {
|
||||
this.commit = this.$store.commit;
|
||||
this.loadSettings();
|
||||
}
|
||||
|
||||
mounted() {
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.showWhatsNew();
|
||||
await this.showDonation();
|
||||
await this.showLiberamaTop();
|
||||
}
|
||||
|
||||
loadSettings() {
|
||||
const settings = this.settings;
|
||||
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
||||
this.showDonationDialog2020 = settings.showDonationDialog2020;
|
||||
this.showLiberamaTopDialog2020 = settings.showLiberamaTopDialog2020;
|
||||
}
|
||||
|
||||
async showWhatsNew() {
|
||||
const whatsNew = versionHistory[0];
|
||||
if (this.showWhatsNewDialog &&
|
||||
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
|
||||
whatsNew.header != this.whatsNewContentHash) {
|
||||
await utils.sleep(2000);
|
||||
this.whatsNewContent = 'Версия ' + whatsNew.header + whatsNew.content;
|
||||
this.whatsNewVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
async showDonation() {
|
||||
const today = utils.formatDate(new Date(), 'coDate');
|
||||
|
||||
if ((this.mode == 'omnireader' || this.mode == 'liberama.top') && today < '2020-03-01' && this.showDonationDialog2020 && this.donationRemindDate != today) {
|
||||
await utils.sleep(3000);
|
||||
this.donationVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
donationDialogDisable() {
|
||||
this.donationVisible = false;
|
||||
if (this.showDonationDialog2020) {
|
||||
this.commit('reader/setSettings', { showDonationDialog2020: false });
|
||||
}
|
||||
}
|
||||
|
||||
donationDialogRemind() {
|
||||
this.donationVisible = false;
|
||||
this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coDate'));
|
||||
}
|
||||
|
||||
openDonate() {
|
||||
this.donationVisible = false;
|
||||
this.liberamaTopVisible = false;
|
||||
this.$emit('donate-toggle');
|
||||
}
|
||||
|
||||
async copyLink(link) {
|
||||
const result = await utils.copyTextToClipboard(link);
|
||||
if (result)
|
||||
this.$root.notify.success(`Ссылка ${link} успешно скопирована в буфер обмена`);
|
||||
else
|
||||
this.$root.notify.error('Копирование не удалось');
|
||||
}
|
||||
|
||||
openVersionHistory() {
|
||||
this.whatsNewVisible = false;
|
||||
this.$emit('version-history-toggle');
|
||||
}
|
||||
|
||||
whatsNewDisable() {
|
||||
this.whatsNewVisible = false;
|
||||
const whatsNew = versionHistory[0];
|
||||
this.commit('reader/setWhatsNewContentHash', whatsNew.header);
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this.$store.state.config.mode;
|
||||
}
|
||||
|
||||
get settings() {
|
||||
return this.$store.state.reader.settings;
|
||||
}
|
||||
|
||||
get whatsNewContentHash() {
|
||||
return this.$store.state.reader.whatsNewContentHash;
|
||||
}
|
||||
|
||||
get donationRemindDate() {
|
||||
return this.$store.state.reader.donationRemindDate;
|
||||
}
|
||||
|
||||
async showLiberamaTop() {
|
||||
const today = utils.formatDate(new Date(), 'coDate');
|
||||
|
||||
if (this.mode == 'omnireader' && today < '2020-12-01' && this.showLiberamaTopDialog2020) {
|
||||
await utils.sleep(3000);
|
||||
this.liberamaTopVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
liberamaTopDialogDisable() {
|
||||
this.liberamaTopVisible = false;
|
||||
if (this.showLiberamaTopDialog2020) {
|
||||
this.commit('reader/setSettings', { showLiberamaTopDialog2020: false });
|
||||
}
|
||||
}
|
||||
|
||||
keyHook() {
|
||||
if (this.$refs.dialog1.active || this.$refs.dialog2.active || this.$refs.dialog3.active)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.clickable {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
cursor: pointer;
|
||||
font-size: 120%;
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
||||
366
client/components/Reader/RecentBooksPage/RecentBooksPage.vue
Normal file
366
client/components/Reader/RecentBooksPage/RecentBooksPage.vue
Normal file
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<Window width="600px" ref="window" @close="close">
|
||||
<template slot="header">
|
||||
<span v-show="!loading">{{ header }}</span>
|
||||
<span v-if="loading"><q-spinner class="q-mr-sm" color="lime-12" size="20px" :thickness="7"/>Список загружается</span>
|
||||
</template>
|
||||
|
||||
<a ref="download" style='display: none;' target="_blank"></a>
|
||||
|
||||
<q-table
|
||||
class="recent-books-table col"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
row-key="key"
|
||||
:pagination.sync="pagination"
|
||||
separator="cell"
|
||||
hide-bottom
|
||||
virtual-scroll
|
||||
dense
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th class="td-mp" style="width: 25px" key="num" :props="props"><span v-html="props.cols[0].label"></span></q-th>
|
||||
<q-th class="td-mp break-word" style="width: 77px" key="date" :props="props"><span v-html="props.cols[1].label"></span></q-th>
|
||||
<q-th class="td-mp" style="width: 332px" key="desc" :props="props" colspan="4">
|
||||
<q-input ref="input" outlined dense rounded style="position: absolute; top: 6px; left: 90px; width: 380px" bg-color="white"
|
||||
placeholder="Найти"
|
||||
v-model="search"
|
||||
@click.stop
|
||||
/>
|
||||
|
||||
<span v-html="props.cols[2].label"></span>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td key="num" :props="props" class="td-mp" auto-width>
|
||||
<div class="break-word" style="width: 25px">
|
||||
{{ props.row.num }}
|
||||
</div>
|
||||
</q-td>
|
||||
|
||||
<q-td key="date" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
|
||||
<div class="break-word" style="width: 68px">
|
||||
{{ props.row.touchDate }}<br>
|
||||
{{ props.row.touchTime }}
|
||||
</div>
|
||||
</q-td>
|
||||
|
||||
<q-td key="desc" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
|
||||
<div class="break-word" style="width: 332px; font-size: 90%">
|
||||
<div style="color: green">{{ props.row.desc.author }}</div>
|
||||
<div>{{ props.row.desc.title }}</div>
|
||||
</div>
|
||||
</q-td>
|
||||
|
||||
<q-td key="links" :props="props" class="td-mp" auto-width>
|
||||
<div class="break-word" style="width: 75px; font-size: 90%">
|
||||
<a v-show="isUrl(props.row.url)" :href="props.row.url" target="_blank">Оригинал</a><br>
|
||||
<a :href="props.row.path" @click.prevent="downloadBook(props.row.path, props.row.fullTitle)">Скачать FB2</a>
|
||||
</div>
|
||||
</q-td>
|
||||
|
||||
<q-td key="close" :props="props" class="td-mp" auto-width>
|
||||
<div style="width: 38px">
|
||||
<q-btn
|
||||
dense
|
||||
style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
|
||||
@click="handleDel(props.row.key)">
|
||||
<q-icon class="la la-times" size="14px" style="top: -6px"/>
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-td>
|
||||
<q-td key="last" :props="props" class="no-mp">
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import path from 'path';
|
||||
//import _ from 'lodash';
|
||||
|
||||
import * as utils from '../../../share/utils';
|
||||
import Window from '../../share/Window.vue';
|
||||
import bookManager from '../share/bookManager';
|
||||
import readerApi from '../../../api/reader';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
watch: {
|
||||
search: function() {
|
||||
this.updateTableData();
|
||||
}
|
||||
},
|
||||
})
|
||||
class RecentBooksPage extends Vue {
|
||||
loading = false;
|
||||
search = null;
|
||||
tableData = [];
|
||||
columns = [];
|
||||
pagination = {};
|
||||
|
||||
created() {
|
||||
this.firstInit = true;
|
||||
this.pagination = {rowsPerPage: 0};
|
||||
|
||||
this.columns = [
|
||||
{
|
||||
name: 'num',
|
||||
label: '#',
|
||||
align: 'center',
|
||||
sortable: true,
|
||||
field: 'num',
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: 'Время<br>просм.',
|
||||
align: 'left',
|
||||
field: 'touchDateTime',
|
||||
sortable: true,
|
||||
sort: (a, b, rowA, rowB) => rowA.touchDateTime - rowB.touchDateTime,
|
||||
},
|
||||
{
|
||||
name: 'desc',
|
||||
label: 'Название',
|
||||
align: 'left',
|
||||
field: 'descString',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'links',
|
||||
label: '',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
name: 'close',
|
||||
label: '',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
name: 'last',
|
||||
label: '',
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
init() {
|
||||
this.$refs.window.init();
|
||||
|
||||
this.$nextTick(() => {
|
||||
//this.$refs.input.focus();//плохо на планшетах
|
||||
});
|
||||
(async() => {//подгрузка списка
|
||||
if (this.initing)
|
||||
return;
|
||||
this.initing = true;
|
||||
|
||||
|
||||
if (this.firstInit) {//для отзывчивости
|
||||
await this.updateTableData(20);
|
||||
this.firstInit = false;
|
||||
}
|
||||
await utils.sleep(50);
|
||||
await this.updateTableData();
|
||||
this.initing = false;
|
||||
})();
|
||||
}
|
||||
|
||||
async updateTableData(limit) {
|
||||
while (this.updating) await utils.sleep(100);
|
||||
this.updating = true;
|
||||
let result = [];
|
||||
|
||||
this.loading = !!limit;
|
||||
const sorted = bookManager.getSortedRecent();
|
||||
|
||||
let num = 0;
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const book = sorted[i];
|
||||
if (book.deleted)
|
||||
continue;
|
||||
|
||||
num++;
|
||||
if (limit && result.length >= limit)
|
||||
break;
|
||||
|
||||
let d = new Date();
|
||||
d.setTime(book.touchTime);
|
||||
const t = utils.formatDate(d).split(' ');
|
||||
|
||||
let perc = '';
|
||||
let textLen = '';
|
||||
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
|
||||
if (book.textLength) {
|
||||
perc = ` [${((p/book.textLength)*100).toFixed(2)}%]`;
|
||||
textLen = ` ${Math.round(book.textLength/1000)}k`;
|
||||
}
|
||||
|
||||
const bt = utils.getBookTitle(book.fb2);
|
||||
|
||||
let title = bt.bookTitle;
|
||||
title = (title ? `"${title}"`: '');
|
||||
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : book.url));
|
||||
|
||||
result.push({
|
||||
num,
|
||||
touchDateTime: book.touchTime,
|
||||
touchDate: t[0],
|
||||
touchTime: t[1],
|
||||
desc: {
|
||||
author,
|
||||
title: `${title}${perc}${textLen}`,
|
||||
},
|
||||
descString: `${author}${title}${perc}${textLen}`,//для сортировки
|
||||
url: book.url,
|
||||
path: book.path,
|
||||
fullTitle: bt.fullTitle,
|
||||
key: book.key,
|
||||
});
|
||||
}
|
||||
|
||||
const search = this.search;
|
||||
result = result.filter(item => {
|
||||
return !search ||
|
||||
item.touchTime.includes(search) ||
|
||||
item.touchDate.includes(search) ||
|
||||
item.desc.title.toLowerCase().includes(search.toLowerCase()) ||
|
||||
item.desc.author.toLowerCase().includes(search.toLowerCase())
|
||||
});
|
||||
|
||||
this.tableData = result;
|
||||
this.updating = false;
|
||||
}
|
||||
|
||||
wordEnding(num) {
|
||||
const endings = ['', 'а', 'и', 'и', 'и', '', '', '', '', ''];
|
||||
const deci = num % 100;
|
||||
if (deci > 10 && deci < 20) {
|
||||
return '';
|
||||
} else {
|
||||
return endings[num % 10];
|
||||
}
|
||||
}
|
||||
|
||||
get header() {
|
||||
const len = (this.tableData ? this.tableData.length : 0);
|
||||
return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
|
||||
}
|
||||
|
||||
async downloadBook(fb2path, fullTitle) {
|
||||
try {
|
||||
await readerApi.checkCachedBook(fb2path);
|
||||
|
||||
const d = this.$refs.download;
|
||||
d.href = fb2path;
|
||||
try {
|
||||
const fn = utils.makeValidFilename(fullTitle);
|
||||
d.download = fn.substring(0, 100) + '.fb2';
|
||||
} catch(e) {
|
||||
d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
|
||||
}
|
||||
|
||||
d.click();
|
||||
} catch (e) {
|
||||
let errMes = e.message;
|
||||
if (errMes.indexOf('404') >= 0)
|
||||
errMes = 'Файл не найден на сервере (возможно был удален как устаревший)';
|
||||
this.$root.stdDialog.alert(errMes, 'Ошибка', {color: 'negative'});
|
||||
}
|
||||
}
|
||||
|
||||
async handleDel(key) {
|
||||
await bookManager.delRecentBook({key});
|
||||
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
|
||||
|
||||
if (!bookManager.mostRecentBook())
|
||||
this.close();
|
||||
}
|
||||
|
||||
loadBook(url) {
|
||||
this.$emit('load-book', {url});
|
||||
this.close();
|
||||
}
|
||||
|
||||
isUrl(url) {
|
||||
if (url)
|
||||
return (url.indexOf('disk://') != 0);
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$emit('recent-books-close');
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
|
||||
this.close();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.recent-books-table {
|
||||
width: 600px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.td-mp {
|
||||
margin: 0 !important;
|
||||
padding: 4px 4px 4px 4px !important;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.no-mp {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
border: 0;
|
||||
border-left: 1px solid #ddd !important;
|
||||
}
|
||||
|
||||
.break-word {
|
||||
line-height: 180%;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.recent-books-table .q-table__middle {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.recent-books-table thead tr:first-child th {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
background-color: #c1f4cd;
|
||||
}
|
||||
.recent-books-table tr:nth-child(even) {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
</style>
|
||||
@@ -1,28 +1,28 @@
|
||||
<template>
|
||||
<div ref="main" class="main" @click="close">
|
||||
<div class="mainWindow" @click.stop>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
{{ header }}
|
||||
</template>
|
||||
<Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
|
||||
<template slot="header">
|
||||
{{ header }}
|
||||
</template>
|
||||
|
||||
<div class="content">
|
||||
<span v-show="initStep">{{ initPercentage }}%</span>
|
||||
<div class="content">
|
||||
<span v-show="initStep">{{ initPercentage }}%</span>
|
||||
|
||||
<div v-show="!initStep" class="input">
|
||||
<input ref="input" class="el-input__inner"
|
||||
placeholder="что ищем"
|
||||
:value="needle" @input="needle = $event.target.value"/>
|
||||
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">{{ foundText }}</div>
|
||||
</div>
|
||||
<el-button-group v-show="!initStep" class="button-group">
|
||||
<el-button @click="showNext"><i class="el-icon-arrow-down"></i></el-button>
|
||||
<el-button @click="showPrev"><i class="el-icon-arrow-up"></i></el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</Window>
|
||||
<div v-show="!initStep" class="input">
|
||||
<!--input ref="input"
|
||||
placeholder="что ищем"
|
||||
:value="needle" @input="needle = $event.target.value"/-->
|
||||
<q-input ref="input" class="col" outlined dense
|
||||
placeholder="что ищем"
|
||||
v-model="needle" @keydown="inputKeyDown"
|
||||
/>
|
||||
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">{{ foundText }}</div>
|
||||
</div>
|
||||
<q-btn-group v-show="!initStep" class="button-group row no-wrap">
|
||||
<q-btn class="button" dense stretch @click="showNext"><q-icon style="top: -6px" name="la la-angle-down" dense size="22px"/></q-btn>
|
||||
<q-btn class="button" dense stretch @click="showPrev"><q-icon style="top: -4px" class="icon" name="la la-angle-up" dense size="22px"/></q-btn>
|
||||
</q-btn-group>
|
||||
</div>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -43,7 +43,10 @@ export default @Component({
|
||||
|
||||
},
|
||||
foundText: function(newValue) {
|
||||
this.$refs.input.style.paddingRight = (10 + newValue.length*12) + 'px';
|
||||
//недостатки сторонних ui
|
||||
const el = this.$refs.input.$el.querySelector('label div div div input');
|
||||
if (el)
|
||||
el.style.paddingRight = newValue.length*12 + 'px';
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -61,6 +64,8 @@ class SearchPage extends Vue {
|
||||
}
|
||||
|
||||
async init(parsed) {
|
||||
this.$refs.window.init();
|
||||
|
||||
if (this.parsed != parsed) {
|
||||
this.initStep = true;
|
||||
this.stopInit = false;
|
||||
@@ -159,16 +164,17 @@ class SearchPage extends Vue {
|
||||
|
||||
close() {
|
||||
this.stopInit = true;
|
||||
this.$emit('search-toggle');
|
||||
this.$emit('do-action', {action: 'search'});
|
||||
}
|
||||
|
||||
inputKeyDown(event) {
|
||||
if (event.key == 'Enter') {
|
||||
this.showNext();
|
||||
}
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
//недостатки сторонних ui
|
||||
if (document.activeElement === this.$refs.input && event.type == 'keydown' && event.key == 'Enter') {
|
||||
this.showNext();
|
||||
}
|
||||
|
||||
if (event.type == 'keydown' && (event.code == 'Escape')) {
|
||||
if (event.type == 'keydown' && event.key == 'Escape') {
|
||||
this.close();
|
||||
}
|
||||
return true;
|
||||
@@ -178,32 +184,13 @@ class SearchPage extends Vue {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mainWindow {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
height: 125px;
|
||||
display: flex;
|
||||
position: relative;
|
||||
top: -50px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
min-width: 430px;
|
||||
}
|
||||
|
||||
.input {
|
||||
@@ -215,16 +202,14 @@ class SearchPage extends Vue {
|
||||
}
|
||||
|
||||
.button-group {
|
||||
width: 150px;
|
||||
width: 100px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 37px;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
.button {
|
||||
padding: 9px 17px 9px 17px;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
width: 50px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div></div>
|
||||
<div class="hidden"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -13,7 +13,10 @@ import readerApi from '../../../api/reader';
|
||||
import * as utils from '../../../share/utils';
|
||||
import * as cryptoUtils from '../../../share/cryptoUtils';
|
||||
|
||||
const maxSetTries = 5;
|
||||
import localForage from 'localforage';
|
||||
const ssCacheStore = localForage.createInstance({
|
||||
name: 'ssCacheStore'
|
||||
});
|
||||
|
||||
export default @Component({
|
||||
watch: {
|
||||
@@ -32,11 +35,15 @@ export default @Component({
|
||||
currentProfile: function() {
|
||||
this.currentProfileChanged(true);
|
||||
},
|
||||
libs: function() {
|
||||
this.debouncedSaveLibs();
|
||||
},
|
||||
},
|
||||
})
|
||||
class ServerStorage extends Vue {
|
||||
created() {
|
||||
this.inited = false;
|
||||
this.keyInited = false;
|
||||
this.commit = this.$store.commit;
|
||||
this.prevServerStorageKey = null;
|
||||
this.$root.$on('generateNewServerStorageKey', () => {this.generateNewServerStorageKey()});
|
||||
@@ -45,27 +52,68 @@ class ServerStorage extends Vue {
|
||||
this.saveSettings();
|
||||
}, 500);
|
||||
|
||||
this.debouncedSaveLibs = _.debounce(() => {
|
||||
this.saveLibs();
|
||||
}, 500);
|
||||
|
||||
this.debouncedNotifySuccess = _.debounce(() => {
|
||||
this.success('Данные синхронизированы с сервером');
|
||||
}, 1000);
|
||||
|
||||
this.oldProfiles = {};
|
||||
this.oldSettings = {};
|
||||
this.oldRecent = {};
|
||||
this.oldRecentLast = {};
|
||||
this.oldLibs = {};
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
this.cachedRecent = await ssCacheStore.getItem('recent');
|
||||
if (!this.cachedRecent)
|
||||
await this.cleanCachedRecent('cachedRecent');
|
||||
|
||||
this.cachedRecentPatch = await ssCacheStore.getItem('recent-patch');
|
||||
if (!this.cachedRecentPatch)
|
||||
await this.cleanCachedRecent('cachedRecentPatch');
|
||||
|
||||
this.cachedRecentMod = await ssCacheStore.getItem('recent-mod');
|
||||
if (!this.cachedRecentMod)
|
||||
await this.cleanCachedRecent('cachedRecentMod');
|
||||
|
||||
if (!this.serverStorageKey) {
|
||||
//генерируем новый ключ
|
||||
await this.generateNewServerStorageKey();
|
||||
} else {
|
||||
await this.serverStorageKeyChanged();
|
||||
}
|
||||
this.oldRecent = _.cloneDeep(bookManager.recent);
|
||||
this.oldRecentLast = _.cloneDeep(bookManager.recentLast) || {};
|
||||
} finally {
|
||||
this.inited = true;
|
||||
}
|
||||
}
|
||||
|
||||
async setCachedRecent(value) {
|
||||
await ssCacheStore.setItem('recent', value);
|
||||
this.cachedRecent = value;
|
||||
}
|
||||
|
||||
async setCachedRecentPatch(value) {
|
||||
await ssCacheStore.setItem('recent-patch', value);
|
||||
this.cachedRecentPatch = value;
|
||||
}
|
||||
|
||||
async setCachedRecentMod(value) {
|
||||
await ssCacheStore.setItem('recent-mod', value);
|
||||
this.cachedRecentMod = value;
|
||||
}
|
||||
|
||||
async cleanCachedRecent(whatToClean) {
|
||||
if (whatToClean == 'cachedRecent' || whatToClean == 'all')
|
||||
await this.setCachedRecent({rev: 0, data: {}});
|
||||
if (whatToClean == 'cachedRecentPatch' || whatToClean == 'all')
|
||||
await this.setCachedRecentPatch({rev: 0, data: {}});
|
||||
if (whatToClean == 'cachedRecentMod' || whatToClean == 'all')
|
||||
await this.setCachedRecentMod({rev: 0, data: {}});
|
||||
}
|
||||
|
||||
async generateNewServerStorageKey() {
|
||||
const key = utils.toBase58(utils.randomArray(32));
|
||||
this.commit('reader/setServerStorageKey', key);
|
||||
@@ -75,7 +123,12 @@ class ServerStorage extends Vue {
|
||||
async serverSyncEnabledChanged() {
|
||||
if (this.serverSyncEnabled) {
|
||||
this.prevServerStorageKey = null;
|
||||
await this.serverStorageKeyChanged(true);
|
||||
if (!this.serverStorageKey) {
|
||||
//генерируем новый ключ
|
||||
await this.generateNewServerStorageKey();
|
||||
} else {
|
||||
await this.serverStorageKeyChanged(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,13 +136,19 @@ class ServerStorage extends Vue {
|
||||
if (this.prevServerStorageKey != this.serverStorageKey) {
|
||||
this.prevServerStorageKey = this.serverStorageKey;
|
||||
this.hashedStorageKey = utils.toBase58(cryptoUtils.sha256(this.serverStorageKey));
|
||||
this.keyInited = true;
|
||||
|
||||
await this.loadProfiles(force);
|
||||
this.checkCurrentProfile();
|
||||
await this.currentProfileChanged(force);
|
||||
await this.loadRecent(force);
|
||||
await this.loadLibs(force);
|
||||
|
||||
if (force)
|
||||
await this.cleanCachedRecent('all');
|
||||
const loadSuccess = await this.loadRecent();
|
||||
if (loadSuccess && force) {
|
||||
await this.saveRecent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,33 +191,37 @@ class ServerStorage extends Vue {
|
||||
return this.settings.showServerStorageMessages;
|
||||
}
|
||||
|
||||
get libs() {
|
||||
return this.$store.state.reader.libs;
|
||||
}
|
||||
|
||||
get libsRev() {
|
||||
return this.$store.state.reader.libsRev;
|
||||
}
|
||||
|
||||
checkCurrentProfile() {
|
||||
if (!this.profiles[this.currentProfile]) {
|
||||
this.commit('reader/setCurrentProfile', '');
|
||||
}
|
||||
}
|
||||
|
||||
notifySuccess() {
|
||||
this.success('Данные синхронизированы с сервером');
|
||||
}
|
||||
|
||||
success(message) {
|
||||
if (this.showServerStorageMessages)
|
||||
this.$notify.success({message});
|
||||
this.$root.notify.success(message);
|
||||
}
|
||||
|
||||
warning(message) {
|
||||
if (this.showServerStorageMessages)
|
||||
this.$notify.warning({message});
|
||||
if (this.showServerStorageMessages && !this.offlineModeActive)
|
||||
this.$root.notify.warning(message);
|
||||
}
|
||||
|
||||
error(message) {
|
||||
if (this.showServerStorageMessages)
|
||||
this.$notify.error({message});
|
||||
if (this.showServerStorageMessages && !this.offlineModeActive)
|
||||
this.$root.notify.error(message);
|
||||
}
|
||||
|
||||
async loadSettings(force) {
|
||||
if (!this.serverSyncEnabled || !this.currentProfile)
|
||||
async loadSettings(force = false, doNotifySuccess = true) {
|
||||
if (!this.keyInited || !this.serverSyncEnabled || !this.currentProfile)
|
||||
return;
|
||||
|
||||
const setsId = `settings-${this.currentProfile}`;
|
||||
@@ -194,14 +257,15 @@ class ServerStorage extends Vue {
|
||||
this.commit('reader/setSettings', sets.data);
|
||||
this.commit('reader/setSettingsRev', {[setsId]: sets.rev});
|
||||
|
||||
this.notifySuccess();
|
||||
if (doNotifySuccess)
|
||||
this.debouncedNotifySuccess();
|
||||
} else {
|
||||
this.warning(`Неверный ответ сервера: ${sets.state}`);
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
if (!this.serverSyncEnabled || !this.currentProfile || this.savingSettings)
|
||||
if (!this.keyInited || !this.serverSyncEnabled || !this.currentProfile || this.savingSettings)
|
||||
return;
|
||||
|
||||
const diff = utils.getObjDiff(this.oldSettings, this.settings);
|
||||
@@ -212,32 +276,18 @@ class ServerStorage extends Vue {
|
||||
try {
|
||||
const setsId = `settings-${this.currentProfile}`;
|
||||
let result = {state: ''};
|
||||
let tries = 0;
|
||||
while (result.state != 'success' && tries < maxSetTries) {
|
||||
const oldRev = this.settingsRev[setsId] || 0;
|
||||
try {
|
||||
result = await this.storageSet({[setsId]: {rev: oldRev + 1, data: this.settings}});
|
||||
} catch(e) {
|
||||
this.savingSettings = false;
|
||||
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.state == 'reject') {
|
||||
await this.loadSettings(true);
|
||||
const newSettings = utils.applyObjDiff(this.settings, diff);
|
||||
this.commit('reader/setSettings', newSettings);
|
||||
}
|
||||
|
||||
tries++;
|
||||
const oldRev = this.settingsRev[setsId] || 0;
|
||||
try {
|
||||
result = await this.storageSet({[setsId]: {rev: oldRev + 1, data: this.settings}});
|
||||
} catch(e) {
|
||||
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
||||
}
|
||||
|
||||
if (tries >= maxSetTries) {
|
||||
//отменять изменения не будем, просто предупредим
|
||||
//this.commit('reader/setSettings', this.oldSettings);
|
||||
console.error(result);
|
||||
this.error('Не удалось отправить настройки на сервер. Данные не сохранены и могут быть перезаписаны.');
|
||||
} else {
|
||||
if (result.state == 'reject') {
|
||||
await this.loadSettings(true, false);
|
||||
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||
} else if (result.state == 'success') {
|
||||
this.oldSettings = _.cloneDeep(this.settings);
|
||||
this.commit('reader/setSettingsRev', {[setsId]: this.settingsRev[setsId] + 1});
|
||||
}
|
||||
@@ -246,8 +296,8 @@ class ServerStorage extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async loadProfiles(force) {
|
||||
if (!this.serverSyncEnabled)
|
||||
async loadProfiles(force = false, doNotifySuccess = true) {
|
||||
if (!this.keyInited || !this.serverSyncEnabled)
|
||||
return;
|
||||
|
||||
const oldRev = this.profilesRev;
|
||||
@@ -281,22 +331,24 @@ class ServerStorage extends Vue {
|
||||
this.oldProfiles = _.cloneDeep(prof.data);
|
||||
this.commit('reader/setProfiles', prof.data);
|
||||
this.commit('reader/setProfilesRev', prof.rev);
|
||||
this.checkCurrentProfile();
|
||||
|
||||
this.notifySuccess();
|
||||
if (doNotifySuccess)
|
||||
this.debouncedNotifySuccess();
|
||||
} else {
|
||||
this.warning(`Неверный ответ сервера: ${prof.state}`);
|
||||
}
|
||||
}
|
||||
|
||||
async saveProfiles() {
|
||||
if (!this.serverSyncEnabled || this.savingProfiles)
|
||||
if (!this.keyInited || !this.serverSyncEnabled || this.savingProfiles)
|
||||
return;
|
||||
|
||||
const diff = utils.getObjDiff(this.oldProfiles, this.profiles);
|
||||
if (utils.isEmptyObjDiff(diff))
|
||||
return;
|
||||
|
||||
//обнуляются профили во время разработки, подстраховка
|
||||
//обнуляются профили во время разработки при hotReload, подстраховка
|
||||
if (!this.$store.state.reader.allowProfilesSave) {
|
||||
console.error('Сохранение профилей не санкционировано');
|
||||
return;
|
||||
@@ -305,54 +357,34 @@ class ServerStorage extends Vue {
|
||||
this.savingProfiles = true;
|
||||
try {
|
||||
let result = {state: ''};
|
||||
let tries = 0;
|
||||
while (result.state != 'success' && tries < maxSetTries) {
|
||||
try {
|
||||
result = await this.storageSet({profiles: {rev: this.profilesRev + 1, data: this.profiles}});
|
||||
} catch(e) {
|
||||
this.savingProfiles = false;
|
||||
this.commit('reader/setProfiles', this.oldProfiles);
|
||||
this.checkCurrentProfile();
|
||||
this.error(`Ошибка соединения с сервером: (${e.message}). Изменения отменены.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.state == 'reject') {
|
||||
await this.loadProfiles(true);
|
||||
const newProfiles = utils.applyObjDiff(this.profiles, diff);
|
||||
this.commit('reader/setProfiles', newProfiles);
|
||||
}
|
||||
|
||||
tries++;
|
||||
try {
|
||||
result = await this.storageSet({profiles: {rev: this.profilesRev + 1, data: this.profiles}});
|
||||
} catch(e) {
|
||||
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
||||
}
|
||||
|
||||
if (tries >= maxSetTries) {
|
||||
this.commit('reader/setProfiles', this.oldProfiles);
|
||||
this.checkCurrentProfile();
|
||||
console.error(result);
|
||||
this.error('Не удалось отправить данные на сервер. Изменения отменены.');
|
||||
} else {
|
||||
if (result.state == 'reject') {
|
||||
await this.loadProfiles(true, false);
|
||||
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||
} else if (result.state == 'success') {
|
||||
this.oldProfiles = _.cloneDeep(this.profiles);
|
||||
this.commit('reader/setProfilesRev', this.profilesRev + 1);
|
||||
this.commit('reader/setProfilesRev', this.profilesRev + 1);
|
||||
}
|
||||
} finally {
|
||||
this.savingProfiles = false;
|
||||
}
|
||||
}
|
||||
|
||||
async loadRecent(force) {
|
||||
if (!this.serverSyncEnabled)
|
||||
async loadLibs(force = false, doNotifySuccess = true) {
|
||||
if (!this.keyInited || !this.serverSyncEnabled)
|
||||
return;
|
||||
|
||||
const oldRev = bookManager.recentRev;
|
||||
const oldLastRev = bookManager.recentLastRev;
|
||||
const oldRev = this.libsRev;
|
||||
//проверим ревизию на сервере
|
||||
let revs = null;
|
||||
if (!force) {
|
||||
try {
|
||||
revs = await this.storageCheck({recent: {}, recentLast: {}});
|
||||
if (revs.state == 'success' && revs.items.recent.rev == oldRev &&
|
||||
revs.items.recentLast.rev == oldLastRev) {
|
||||
const revs = await this.storageCheck({libs: {}});
|
||||
if (revs.state == 'success' && revs.items.libs.rev == oldRev) {
|
||||
return;
|
||||
}
|
||||
} catch(e) {
|
||||
@@ -361,159 +393,247 @@ class ServerStorage extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
if (force || revs.items.recent.rev != oldRev) {
|
||||
let libs = null;
|
||||
try {
|
||||
libs = await this.storageGet({libs: {}});
|
||||
} catch(e) {
|
||||
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (libs.state == 'success') {
|
||||
libs = libs.items.libs;
|
||||
|
||||
if (libs.rev == 0)
|
||||
libs.data = {};
|
||||
|
||||
this.oldLibs = _.cloneDeep(libs.data);
|
||||
this.commit('reader/setLibs', libs.data);
|
||||
this.commit('reader/setLibsRev', libs.rev);
|
||||
|
||||
if (doNotifySuccess)
|
||||
this.debouncedNotifySuccess();
|
||||
} else {
|
||||
this.warning(`Неверный ответ сервера: ${libs.state}`);
|
||||
}
|
||||
}
|
||||
|
||||
async saveLibs() {
|
||||
if (!this.keyInited || !this.serverSyncEnabled || this.savingLibs)
|
||||
return;
|
||||
|
||||
const diff = utils.getObjDiff(this.oldLibs, this.libs);
|
||||
if (utils.isEmptyObjDiff(diff))
|
||||
return;
|
||||
|
||||
this.savingLibs = true;
|
||||
try {
|
||||
let result = {state: ''};
|
||||
try {
|
||||
result = await this.storageSet({libs: {rev: this.libsRev + 1, data: this.libs}});
|
||||
} catch(e) {
|
||||
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
||||
}
|
||||
|
||||
if (result.state == 'reject') {
|
||||
await this.loadLibs(true, false);
|
||||
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||
} else if (result.state == 'success') {
|
||||
this.oldLibs = _.cloneDeep(this.libs);
|
||||
this.commit('reader/setLibsRev', this.libsRev + 1);
|
||||
}
|
||||
} finally {
|
||||
this.savingLibs = false;
|
||||
}
|
||||
}
|
||||
|
||||
async loadRecent(skipRevCheck = false, doNotifySuccess = true) {
|
||||
if (!this.keyInited || !this.serverSyncEnabled || this.loadingRecent)
|
||||
return;
|
||||
this.loadingRecent = true;
|
||||
try {
|
||||
//проверим ревизию на сервере
|
||||
let query = {recent: {}, recentPatch: {}, recentMod: {}};
|
||||
let revs = null;
|
||||
if (!skipRevCheck) {
|
||||
try {
|
||||
revs = await this.storageCheck(query);
|
||||
if (revs.state == 'success') {
|
||||
if (revs.items.recent.rev != this.cachedRecent.rev) {
|
||||
//no changes
|
||||
} else if (revs.items.recentPatch.rev != this.cachedRecentPatch.rev) {
|
||||
query = {recentPatch: {}, recentMod: {}};
|
||||
} else if (revs.items.recentMod.rev != this.cachedRecentMod.rev) {
|
||||
query = {recentMod: {}};
|
||||
} else
|
||||
return true;
|
||||
}
|
||||
} catch(e) {
|
||||
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let recent = null;
|
||||
try {
|
||||
recent = await this.storageGet({recent: {}});
|
||||
recent = await this.storageGet(query);
|
||||
} catch(e) {
|
||||
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (recent.state == 'success') {
|
||||
recent = recent.items.recent;
|
||||
let newRecent = recent.items.recent;
|
||||
let newRecentPatch = recent.items.recentPatch;
|
||||
let newRecentMod = recent.items.recentMod;
|
||||
|
||||
if (recent.rev == 0)
|
||||
recent.data = {};
|
||||
if (!newRecent) {
|
||||
newRecent = _.cloneDeep(this.cachedRecent);
|
||||
}
|
||||
if (!newRecentPatch) {
|
||||
newRecentPatch = _.cloneDeep(this.cachedRecentPatch);
|
||||
}
|
||||
if (!newRecentMod) {
|
||||
newRecentMod = _.cloneDeep(this.cachedRecentMod);
|
||||
}
|
||||
|
||||
this.oldRecent = _.cloneDeep(recent.data);
|
||||
await bookManager.setRecent(recent.data);
|
||||
await bookManager.setRecentRev(recent.rev);
|
||||
if (newRecent.rev == 0) newRecent.data = {};
|
||||
if (newRecentPatch.rev == 0) newRecentPatch.data = {};
|
||||
if (newRecentMod.rev == 0) newRecentMod.data = {};
|
||||
|
||||
let result = Object.assign({}, newRecent.data, newRecentPatch.data);
|
||||
|
||||
const md = newRecentMod.data;
|
||||
if (md.key && result[md.key])
|
||||
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, {isAddChanged: true});
|
||||
|
||||
/*if (!bookManager.loaded) {
|
||||
this.warning('Ожидание загрузки списка книг перед синхронизацией');
|
||||
while (!bookManager.loaded) await utils.sleep(100);
|
||||
}*/
|
||||
|
||||
if (newRecent.rev != this.cachedRecent.rev)
|
||||
await this.setCachedRecent(newRecent);
|
||||
if (newRecentPatch.rev != this.cachedRecentPatch.rev)
|
||||
await this.setCachedRecentPatch(newRecentPatch);
|
||||
if (newRecentMod.rev != this.cachedRecentMod.rev)
|
||||
await this.setCachedRecentMod(newRecentMod);
|
||||
|
||||
await bookManager.setRecent(result);
|
||||
} else {
|
||||
this.warning(`Неверный ответ сервера: ${recent.state}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (force || revs.items.recentLast.rev != oldLastRev) {
|
||||
let recentLast = null;
|
||||
try {
|
||||
recentLast = await this.storageGet({recentLast: {}});
|
||||
} catch(e) {
|
||||
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (recentLast.state == 'success') {
|
||||
recentLast = recentLast.items.recentLast;
|
||||
|
||||
if (recentLast.rev == 0)
|
||||
recentLast.data = {};
|
||||
|
||||
this.oldRecentLast = _.cloneDeep(recentLast.data);
|
||||
await bookManager.setRecentLast(recentLast.data);
|
||||
await bookManager.setRecentLastRev(recentLast.rev);
|
||||
} else {
|
||||
this.warning(`Неверный ответ сервера: ${recentLast.state}`);
|
||||
}
|
||||
if (doNotifySuccess)
|
||||
this.debouncedNotifySuccess();
|
||||
} finally {
|
||||
this.loadingRecent = false;
|
||||
}
|
||||
|
||||
this.notifySuccess();
|
||||
return true;
|
||||
}
|
||||
|
||||
async saveRecent() {
|
||||
if (!this.serverSyncEnabled || this.savingRecent)
|
||||
return;
|
||||
async saveRecent(itemKey, recurse) {
|
||||
while (!this.inited || this.savingRecent)
|
||||
await utils.sleep(100);
|
||||
|
||||
const bm = bookManager;
|
||||
|
||||
const diff = utils.getObjDiff(this.oldRecent, bm.recent);
|
||||
if (utils.isEmptyObjDiff(diff))
|
||||
if (!this.keyInited || !this.serverSyncEnabled || this.savingRecent)
|
||||
return;
|
||||
|
||||
this.savingRecent = true;
|
||||
try {
|
||||
let result = {state: ''};
|
||||
let tries = 0;
|
||||
while (result.state != 'success' && tries < maxSetTries) {
|
||||
try {
|
||||
result = await this.storageSet({recent: {rev: bm.recentRev + 1, data: bm.recent}});
|
||||
} catch(e) {
|
||||
this.savingRecent = false;
|
||||
this.error(`Ошибка соединения с сервером: (${e.message}). Изменения не сохранены.`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const bm = bookManager;
|
||||
|
||||
if (result.state == 'reject') {
|
||||
await this.loadRecent(true);
|
||||
//похоже это лишнее
|
||||
/*const newRecent = utils.applyObjDiff(bm.recent, diff);
|
||||
await bm.setRecent(newRecent);*/
|
||||
}
|
||||
let needSaveRecent = false;
|
||||
let needSaveRecentPatch = false;
|
||||
let needSaveRecentMod = false;
|
||||
|
||||
tries++;
|
||||
//newRecentMod
|
||||
let newRecentMod = {};
|
||||
if (itemKey && this.cachedRecentPatch.data[itemKey] && this.prevItemKey == itemKey) {
|
||||
newRecentMod = _.cloneDeep(this.cachedRecentMod);
|
||||
newRecentMod.rev++;
|
||||
|
||||
newRecentMod.data.key = itemKey;
|
||||
newRecentMod.data.mod = utils.getObjDiff(this.cachedRecentPatch.data[itemKey], bm.recent[itemKey]);
|
||||
needSaveRecentMod = true;
|
||||
}
|
||||
this.prevItemKey = itemKey;
|
||||
|
||||
//newRecentPatch
|
||||
let newRecentPatch = {};
|
||||
if (itemKey && !needSaveRecentMod) {
|
||||
newRecentPatch = _.cloneDeep(this.cachedRecentPatch);
|
||||
newRecentPatch.rev++;
|
||||
newRecentPatch.data[itemKey] = _.cloneDeep(bm.recent[itemKey]);
|
||||
|
||||
let applyMod = this.cachedRecentMod.data;
|
||||
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
|
||||
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true});
|
||||
|
||||
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
|
||||
needSaveRecentPatch = true;
|
||||
needSaveRecentMod = true;
|
||||
}
|
||||
|
||||
if (tries >= maxSetTries) {
|
||||
console.error(result);
|
||||
this.error('Не удалось отправить данные на сервер. Данные не сохранены и могут быть перезаписаны.');
|
||||
//newRecent
|
||||
let newRecent = {};
|
||||
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
|
||||
//ждем весь bm.recent
|
||||
/*while (!bookManager.loaded)
|
||||
await utils.sleep(100);*/
|
||||
|
||||
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
|
||||
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
|
||||
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
|
||||
needSaveRecent = true;
|
||||
needSaveRecentPatch = true;
|
||||
needSaveRecentMod = true;
|
||||
}
|
||||
|
||||
//query
|
||||
let query = {};
|
||||
if (needSaveRecent) {
|
||||
query = {recent: newRecent, recentPatch: newRecentPatch, recentMod: newRecentMod};
|
||||
} else if (needSaveRecentPatch) {
|
||||
query = {recentPatch: newRecentPatch, recentMod: newRecentMod};
|
||||
} else {
|
||||
this.oldRecent = _.cloneDeep(bm.recent);
|
||||
await bm.setRecentRev(bm.recentRev + 1);
|
||||
await this.saveRecentLast(true);
|
||||
query = {recentMod: newRecentMod};
|
||||
}
|
||||
|
||||
//сохранение
|
||||
let result = {state: ''};
|
||||
|
||||
try {
|
||||
result = await this.storageSet(query);
|
||||
} catch(e) {
|
||||
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
||||
}
|
||||
|
||||
if (result.state == 'reject') {
|
||||
|
||||
const res = await this.loadRecent(false, false);
|
||||
|
||||
if (res)
|
||||
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||
if (!recurse && itemKey) {
|
||||
this.savingRecent = false;
|
||||
this.saveRecent(itemKey, true);
|
||||
return;
|
||||
}
|
||||
} else if (result.state == 'success') {
|
||||
if (needSaveRecent && newRecent.rev)
|
||||
await this.setCachedRecent(newRecent);
|
||||
if (needSaveRecentPatch && newRecentPatch.rev)
|
||||
await this.setCachedRecentPatch(newRecentPatch);
|
||||
if (needSaveRecentMod && newRecentMod.rev)
|
||||
await this.setCachedRecentMod(newRecentMod);
|
||||
}
|
||||
} finally {
|
||||
this.savingRecent = false;
|
||||
}
|
||||
}
|
||||
|
||||
async saveRecentLast(force = false) {
|
||||
if (!this.serverSyncEnabled || this.savingRecentLast)
|
||||
return;
|
||||
|
||||
const bm = bookManager;
|
||||
let recentLast = bm.recentLast;
|
||||
recentLast = (recentLast ? recentLast : {});
|
||||
let lastRev = bm.recentLastRev;
|
||||
|
||||
const diff = utils.getObjDiff(this.oldRecentLast, recentLast);
|
||||
if (utils.isEmptyObjDiff(diff))
|
||||
return;
|
||||
|
||||
this.savingRecentLast = true;
|
||||
try {
|
||||
let result = {state: ''};
|
||||
let tries = 0;
|
||||
while (result.state != 'success' && tries < maxSetTries) {
|
||||
if (force) {
|
||||
try {
|
||||
const revs = await this.storageCheck({recentLast: {}});
|
||||
if (revs.items.recentLast.rev)
|
||||
lastRev = revs.items.recentLast.rev;
|
||||
} catch(e) {
|
||||
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
result = await this.storageSet({recentLast: {rev: lastRev + 1, data: recentLast}}, force);
|
||||
} catch(e) {
|
||||
this.savingRecentLast = false;
|
||||
this.error(`Ошибка соединения с сервером: (${e.message}). Изменения не сохранены.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.state == 'reject') {
|
||||
await this.loadRecent(false);
|
||||
this.savingRecentLast = false;//!!!
|
||||
return;//!!!
|
||||
}
|
||||
|
||||
tries++;
|
||||
}
|
||||
|
||||
if (tries >= maxSetTries) {
|
||||
console.error(result);
|
||||
this.error('Не удалось отправить данные на сервер. Данные не сохранены и могут быть перезаписаны.');
|
||||
} else {
|
||||
this.oldRecentLast = _.cloneDeep(recentLast);
|
||||
await bm.setRecentLastRev(lastRev + 1);
|
||||
}
|
||||
} finally {
|
||||
this.savingRecentLast = false;
|
||||
}
|
||||
}
|
||||
|
||||
async storageCheck(items) {
|
||||
return await this.storageApi('check', items);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
<template>
|
||||
<div ref="main" class="main" @click="close">
|
||||
<div class="mainWindow" @click.stop>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
Установить позицию
|
||||
</template>
|
||||
<Window ref="window" height="140px" max-width="600px" :top-shift="-50" @close="close">
|
||||
<template slot="header">
|
||||
Установить позицию
|
||||
</template>
|
||||
|
||||
<div class="slider">
|
||||
<el-slider v-model="sliderValue" :max="sliderMax" :format-tooltip="formatTooltip"></el-slider>
|
||||
</div>
|
||||
</Window>
|
||||
<div id="set-position-slider" class="slider q-px-md">
|
||||
<q-slider
|
||||
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
|
||||
v-model="sliderValue"
|
||||
:max="sliderMax"
|
||||
label
|
||||
:label-value="(sliderMax ? (sliderValue/this.sliderMax*100).toFixed(2) + '%' : 0)"
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -43,25 +46,23 @@ class SetPositionPage extends Vue {
|
||||
}
|
||||
|
||||
init(sliderValue, sliderMax) {
|
||||
this.$refs.window.init();
|
||||
|
||||
this.sliderMax = sliderMax;
|
||||
this.sliderValue = sliderValue;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
formatTooltip(val) {
|
||||
if (this.sliderMax)
|
||||
return (val/this.sliderMax*100).toFixed(2) + '%';
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$emit('set-position-toggle');
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (event.type == 'keydown' && (event.code == 'Escape' || event.code == 'KeyP')) {
|
||||
this.close();
|
||||
if (event.type == 'keydown') {
|
||||
const action = this.$root.readerActionByKeyEvent(event);
|
||||
if (event.key == 'Escape' || action == 'setPosition') {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -70,34 +71,18 @@ class SetPositionPage extends Vue {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mainWindow {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
height: 140px;
|
||||
display: flex;
|
||||
position: relative;
|
||||
top: -50px;
|
||||
}
|
||||
|
||||
.slider {
|
||||
margin: 20px;
|
||||
background-color: #efefef;
|
||||
border-radius: 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
.el-slider {
|
||||
margin-right: 20px;
|
||||
margin-left: 20px;
|
||||
<style>
|
||||
#set-position-slider .q-slider__thumb path {
|
||||
fill: white !important;
|
||||
stroke: blue !important;
|
||||
stroke-width: 2 !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<div class="table col column no-wrap">
|
||||
<!-- header -->
|
||||
<div class="table-row row">
|
||||
<div class="desc q-pa-sm bg-blue-2">Команда</div>
|
||||
<div class="hotKeys col q-pa-sm bg-blue-2 row no-wrap">
|
||||
<div style="width: 80px">Сочетание клавиш</div>
|
||||
<q-input ref="input" class="q-ml-sm col"
|
||||
outlined dense rounded
|
||||
bg-color="grey-4"
|
||||
placeholder="Найти"
|
||||
v-model="search"
|
||||
@click.stop
|
||||
/>
|
||||
<div v-show="!readonly" class="q-ml-sm column justify-center">
|
||||
<q-btn class="bg-grey-4 text-grey-6" style="height: 35px; width: 35px" rounded flat icon="la la-broom" @click="defaultHotKeyAll">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Установить все сочетания по умолчанию
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- body -->
|
||||
<div class="table-row row" v-for="(action, index) in tableData" :key="index">
|
||||
<div class="desc q-pa-sm">{{ rstore.readerActions[action] }}</div>
|
||||
<div class="hotKeys col q-pa-sm">
|
||||
<q-chip
|
||||
:color="collisions[code] ? 'red' : 'grey-7'"
|
||||
:removable="!readonly" :clickable="collisions[code] ? true : false"
|
||||
text-color="white" v-for="(code, index) in value[action]" :key="index" @remove="removeCode(action, code)"
|
||||
@click="collisionWarning(code)"
|
||||
>
|
||||
{{ code }}
|
||||
</q-chip>
|
||||
</div>
|
||||
<div v-show="!readonly" class="column q-pa-xs">
|
||||
<q-icon
|
||||
name="la la-plus-circle"
|
||||
class="button bg-green-8 text-white"
|
||||
@click="addHotKey(action)"
|
||||
v-ripple
|
||||
:disabled="value[action].length >= maxCodesLength"
|
||||
>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Добавить сочетание клавиш
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon
|
||||
name="la la-broom"
|
||||
class="button text-grey-5"
|
||||
@click="defaultHotKey(action)"
|
||||
v-ripple
|
||||
>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
По умолчанию
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
import rstore from '../../../../store/modules/reader';
|
||||
//import * as utils from '../../share/utils';
|
||||
|
||||
const UserHotKeysProps = Vue.extend({
|
||||
props: {
|
||||
value: Object,
|
||||
readonly: Boolean,
|
||||
}
|
||||
});
|
||||
|
||||
export default @Component({
|
||||
watch: {
|
||||
search: function() {
|
||||
this.updateTableData();
|
||||
},
|
||||
value: function() {
|
||||
this.checkCollisions();
|
||||
this.updateTableData();
|
||||
}
|
||||
},
|
||||
})
|
||||
class UserHotKeys extends UserHotKeysProps {
|
||||
search = '';
|
||||
rstore = {};
|
||||
tableData = [];
|
||||
collisions = {};
|
||||
maxCodesLength = 10;
|
||||
|
||||
created() {
|
||||
this.rstore = rstore;
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.checkCollisions();
|
||||
this.updateTableData();
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this.$store.state.config.mode;
|
||||
}
|
||||
|
||||
updateTableData() {
|
||||
let result = rstore.hotKeys.map(hk => hk.name).filter(name => (this.mode == 'liberama.top' || name != 'libs'));
|
||||
|
||||
const search = this.search.toLowerCase();
|
||||
const codesIncludeSearch = (action) => {
|
||||
for (const code of this.value[action]) {
|
||||
if (code.toLowerCase().includes(search))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
result = result.filter(item => {
|
||||
return !search ||
|
||||
rstore.readerActions[item].toLowerCase().includes(search) ||
|
||||
codesIncludeSearch(item)
|
||||
});
|
||||
|
||||
this.tableData = result;
|
||||
}
|
||||
|
||||
checkCollisions() {
|
||||
const cols = {};
|
||||
for (const [action, codes] of Object.entries(this.value)) {
|
||||
codes.forEach(code => {
|
||||
if (!cols[code])
|
||||
cols[code] = [];
|
||||
if (cols[code].indexOf(action) < 0)
|
||||
cols[code].push(action);
|
||||
});
|
||||
}
|
||||
|
||||
const result = {};
|
||||
for (const [code, actions] of Object.entries(cols)) {
|
||||
if (actions.length > 1)
|
||||
result[code] = actions;
|
||||
}
|
||||
|
||||
this.collisions = result;
|
||||
}
|
||||
|
||||
collisionWarning(code) {
|
||||
if (this.collisions[code]) {
|
||||
const descs = this.collisions[code].map(action => `<b>${rstore.readerActions[action]}</b>`);
|
||||
this.$root.stdDialog.alert(`Сочетание '${code}' одновременно назначено<br>следующим командам:<br>${descs.join('<br>')}<br><br>
|
||||
Возможно неожиданное поведение.`, 'Предупреждение');
|
||||
}
|
||||
}
|
||||
|
||||
removeCode(action, code) {
|
||||
let codes = Array.from(this.value[action]);
|
||||
const index = codes.indexOf(code);
|
||||
if (index >= 0) {
|
||||
codes.splice(index, 1);
|
||||
const newValue = Object.assign({}, this.value, {[action]: codes});
|
||||
this.$emit('input', newValue);
|
||||
}
|
||||
}
|
||||
|
||||
async addHotKey(action) {
|
||||
if (this.value[action].length >= this.maxCodesLength)
|
||||
return;
|
||||
try {
|
||||
const result = await this.$root.stdDialog.getHotKey(`Добавить сочетание для:<br><b>${rstore.readerActions[action]}</b>`, '');
|
||||
if (result) {
|
||||
let codes = Array.from(this.value[action]);
|
||||
if (codes.indexOf(result) < 0) {
|
||||
codes.push(result);
|
||||
const newValue = Object.assign({}, this.value, {[action]: codes});
|
||||
this.$emit('input', newValue);
|
||||
this.$nextTick(() => {
|
||||
this.collisionWarning(result);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
async defaultHotKey(action) {
|
||||
try {
|
||||
if (await this.$root.stdDialog.confirm(`Подтвердите сброс сочетаний клавиш<br>в значения по умолчанию для команды:<br><b>${rstore.readerActions[action]}</b>`, ' ')) {
|
||||
const codes = Array.from(rstore.settingDefaults.userHotKeys[action]);
|
||||
const newValue = Object.assign({}, this.value, {[action]: codes});
|
||||
this.$emit('input', newValue);
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
async defaultHotKeyAll() {
|
||||
try {
|
||||
if (await this.$root.stdDialog.confirm('Подтвердите сброс сочетаний клавиш<br>для ВСЕХ команд в значения по умолчанию:', ' ')) {
|
||||
const newValue = Object.assign({}, rstore.settingDefaults.userHotKeys);
|
||||
this.$emit('input', newValue);
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.table {
|
||||
border-left: 1px solid grey;
|
||||
border-top: 1px solid grey;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
border-right: 1px solid grey;
|
||||
border-bottom: 1px solid grey;
|
||||
}
|
||||
|
||||
.table-row:nth-child(even) {
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.desc {
|
||||
width: 130px;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.hotKeys {
|
||||
border-left: 1px solid grey;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 25px;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
17
client/components/Reader/SettingsPage/defPalette.js
Normal file
17
client/components/Reader/SettingsPage/defPalette.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const defPalette = [
|
||||
'rgb(255,204,204)', 'rgb(255,230,204)', 'rgb(255,255,204)', 'rgb(204,255,204)', 'rgb(204,255,230)',
|
||||
'rgb(204,255,255)', 'rgb(204,230,255)', 'rgb(204,204,255)', 'rgb(230,204,255)', 'rgb(255,204,255)',
|
||||
'rgb(255,153,153)', 'rgb(255,204,153)', 'rgb(255,255,153)', 'rgb(153,255,153)', 'rgb(153,255,204)',
|
||||
'rgb(153,255,255)', 'rgb(153,204,255)', 'rgb(153,153,255)', 'rgb(204,153,255)', 'rgb(255,153,255)',
|
||||
'rgb(255,102,102)', 'rgb(255,179,102)', 'rgb(255,255,102)', 'rgb(102,255,102)', 'rgb(102,255,179)',
|
||||
'rgb(102,255,255)', 'rgb(102,179,255)', 'rgb(102,102,255)', 'rgb(179,102,255)', 'rgb(255,102,255)',
|
||||
'rgb(255,51,51)', 'rgb(255,153,51)', 'rgb(255,255,51)', 'rgb(51,255,51)', 'rgb(51,255,153)', 'rgb(51,255,255)', 'rgb(51,153,255)', 'rgb(51,51,255)', 'rgb(153,51,255)', 'rgb(255,51,255)',
|
||||
'rgb(255,0,0)', 'rgb(255,128,0)', 'rgb(255,255,0)', 'rgb(0,255,0)', 'rgb(0,255,128)', 'rgb(0,255,255)', 'rgb(0,128,255)', 'rgb(0,0,255)', 'rgb(128,0,255)', 'rgb(255,0,255)',
|
||||
'rgb(245,0,0)', 'rgb(245,123,0)', 'rgb(245,245,0)', 'rgb(0,245,0)', 'rgb(0,245,123)', 'rgb(0,245,245)', 'rgb(0,123,245)', 'rgb(0,0,245)', 'rgb(123,0,245)', 'rgb(245,0,245)',
|
||||
'rgb(214,0,0)', 'rgb(214,108,0)', 'rgb(214,214,0)', 'rgb(0,214,0)', 'rgb(0,214,108)', 'rgb(0,214,214)', 'rgb(0,108,214)', 'rgb(0,0,214)', 'rgb(108,0,214)', 'rgb(214,0,214)',
|
||||
'rgb(163,0,0)', 'rgb(163,82,0)', 'rgb(163,163,0)', 'rgb(0,163,0)', 'rgb(0,163,82)', 'rgb(0,163,163)', 'rgb(0,82,163)', 'rgb(0,0,163)', 'rgb(82,0,163)', 'rgb(163,0,163)',
|
||||
'rgb(92,0,0)', 'rgb(92,46,0)', 'rgb(92,92,0)', 'rgb(0,92,0)', 'rgb(0,92,46)', 'rgb(0,92,92)', 'rgb(0,46,92)', 'rgb(0,0,92)', 'rgb(46,0,92)', 'rgb(92,0,92)',
|
||||
'rgb(255,255,255)', 'rgb(205,205,205)', 'rgb(178,178,178)', 'rgb(153,153,153)', 'rgb(127,127,127)', 'rgb(102,102,102)', 'rgb(76,76,76)', 'rgb(51,51,51)', 'rgb(25,25,25)', 'rgb(0,0,0)'
|
||||
];
|
||||
|
||||
export default defPalette;
|
||||
10
client/components/Reader/SettingsPage/include/ButtonsTab.inc
Normal file
10
client/components/Reader/SettingsPage/include/ButtonsTab.inc
Normal file
@@ -0,0 +1,10 @@
|
||||
<div class="part-header">Показывать кнопки панели</div>
|
||||
|
||||
<div class="item row" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'">
|
||||
<div class="label-3"></div>
|
||||
<div class="col row">
|
||||
<q-checkbox size="xs" @input="changeShowToolButton(item.name)"
|
||||
:value="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
33
client/components/Reader/SettingsPage/include/KeysTab.inc
Normal file
33
client/components/Reader/SettingsPage/include/KeysTab.inc
Normal file
@@ -0,0 +1,33 @@
|
||||
<div class="bg-grey-3 row">
|
||||
<q-tabs
|
||||
v-model="selectedKeysTab"
|
||||
active-color="black"
|
||||
active-bg-color="white"
|
||||
indicator-color="white"
|
||||
dense
|
||||
no-caps
|
||||
class="no-mp bg-grey-4 text-grey-7"
|
||||
>
|
||||
<q-tab name="mouse" label="Мышь/тачскрин" />
|
||||
<q-tab name="keyboard" label="Клавиатура" />
|
||||
</q-tabs>
|
||||
</div>
|
||||
|
||||
<div class="q-mb-sm"/>
|
||||
|
||||
<div class="col tab-panel">
|
||||
<div v-if="selectedKeysTab == 'mouse'">
|
||||
<div class="item row">
|
||||
<div class="label-4"></div>
|
||||
<div class="col row">
|
||||
<q-checkbox size="xs" v-model="clickControl" label="Включить управление кликом" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedKeysTab == 'keyboard'">
|
||||
<div class="item row">
|
||||
<UserHotKeys v-model="userHotKeys" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
107
client/components/Reader/SettingsPage/include/OthersTab.inc
Normal file
107
client/components/Reader/SettingsPage/include/OthersTab.inc
Normal file
@@ -0,0 +1,107 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Подсказки, уведомления</div>
|
||||
|
||||
<div class="item row no-wrap">
|
||||
<div class="label-6">Подсказка</div>
|
||||
<q-checkbox size="xs" v-model="showClickMapPage" label="Показывать области управления кликом" :disable="!clickControl" >
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Показывать или нет подсказку при каждой загрузке книги
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Подсказка</div>
|
||||
<q-checkbox size="xs" v-model="blinkCachedLoad" label="Предупреждать о загрузке из кэша">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Мерцать сообщением в строке статуса и на кнопке<br>
|
||||
обновления при загрузке книги из кэша
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row no-wrap">
|
||||
<div class="label-6">Уведомление</div>
|
||||
<q-checkbox size="xs" v-model="showServerStorageMessages" label="Показывать сообщения синхронизации">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Показывать уведомления и ошибки от<br>
|
||||
синхронизатора данных с сервером
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Уведомление</div>
|
||||
<q-checkbox size="xs" v-model="showWhatsNewDialog">
|
||||
Показывать уведомление "Что нового"
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Показывать уведомления "Что нового"<br>
|
||||
при каждом выходе новой версии читалки
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Уведомление</div>
|
||||
<q-checkbox size="xs" v-model="showDonationDialog2020">
|
||||
Показывать "Оплатим хостинг вместе"
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Показывать уведомление "Оплатим хостинг вместе"
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Другое</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Обработка</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="enableSitesFilter" @input="needTextReload" size="xs" label="Включить html-фильтр для сайтов">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Html-фильтр вырезает лишние элементы со<br>
|
||||
страницы для определенных сайтов, таких как:<br>
|
||||
samlib.ru<br>
|
||||
www.fanfiction.net<br>
|
||||
archiveofourown.org<br>
|
||||
и других
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Обработка</div>
|
||||
<q-checkbox size="xs" v-model="lazyParseEnabled" label="Предварительная подготовка текста">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Включение этой опции позволяет делать предварительную<br>
|
||||
подготовку всего текста в ленивом режиме сразу после<br>
|
||||
загрузки книги. Это может повысить отзывчивость читалки,<br>
|
||||
но нагружает процессор каждый раз при открытии книги.
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Парам. в URL</div>
|
||||
<q-checkbox size="xs" v-model="allowUrlParamBookPos">
|
||||
Добавлять параметр "__p"
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Добавление параметра "__p" в строке браузера<br>
|
||||
позволяет передавать ссылку на книгу в читалке<br>
|
||||
без потери текущей позиции. Однако в этом случае<br>
|
||||
при листании забивается история браузера, т.к. на<br>
|
||||
каждое изменение позиции происходит смена URL.
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Копирование</div>
|
||||
<q-checkbox size="xs" v-model="copyFullText" label="Загружать весь текст">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Загружать весь текст в окно<br>
|
||||
копирования текста со страницы
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Анимация</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-5">Тип</div>
|
||||
<q-select class="col-left" v-model="pageChangeAnimation" :options="pageChangeAnimationOptions"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-5">Скорость</div>
|
||||
<NumInput class="col-left" v-model="pageChangeAnimationSpeed" :min="0" :max="100" :disable="pageChangeAnimation == ''"/>
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Другое</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-5">Страница</div>
|
||||
<q-checkbox v-model="keepLastToFirst" size="xs" label="Переносить последнюю строку">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Переносить последнюю строку страницы<br>
|
||||
в начало следующей при листании
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
101
client/components/Reader/SettingsPage/include/ProfilesTab.inc
Normal file
101
client/components/Reader/SettingsPage/include/ProfilesTab.inc
Normal file
@@ -0,0 +1,101 @@
|
||||
<div class="part-header">Управление синхронизацией данных</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<q-checkbox class="col" v-model="serverSyncEnabled" size="xs" label="Включить синхронизацию с сервером" />
|
||||
</div>
|
||||
|
||||
<div v-show="serverSyncEnabled">
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Профили устройств</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<div class="text col">
|
||||
Выберите или добавьте профиль устройства, чтобы начать синхронизацию настроек с сервером.
|
||||
<br>При выборе "Нет" синхронизация настроек (но не книг) отключается.
|
||||
</div>
|
||||
</div>
|
||||
<div class="item row">
|
||||
<div class="label-1">Устройство</div>
|
||||
<div class="col">
|
||||
<q-select v-model="currentProfile" :options="currentProfileOptions"
|
||||
style="width: 275px"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options display-value-sanitize options-sanitize
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<q-btn class="button" dense no-caps @click="addProfile">Добавить</q-btn>
|
||||
<q-btn class="button" dense no-caps @click="delProfile">Удалить</q-btn>
|
||||
<q-btn class="button" dense no-caps @click="delAllProfiles">Удалить все</q-btn>
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Ключ доступа</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<div class="text col">
|
||||
Ключ доступа позволяет восстановить профили с настройками и список читаемых книг.
|
||||
Для этого необходимо передать ключ на новое устройство через почту, мессенджер или другим способом.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<q-btn class="button" style="width: 250px" dense no-caps @click="showServerStorageKey">
|
||||
<span v-show="serverStorageKeyVisible">Скрыть</span>
|
||||
<span v-show="!serverStorageKeyVisible">Показать</span>
|
||||
ключ доступа
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<div v-if="!serverStorageKeyVisible" class="col">
|
||||
<hr/>
|
||||
<b>{{ partialStorageKey }}</b> (часть вашего ключа)
|
||||
<hr/>
|
||||
</div>
|
||||
<div v-else class="col" style="line-height: 100%">
|
||||
<hr/>
|
||||
<div style="width: 300px; padding-top: 5px; overflow-wrap: break-word;">
|
||||
<b>{{ serverStorageKey }}</b>
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyToClip(serverStorageKey, 'Ключ')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
<div v-if="mode == 'omnireader' || mode == 'liberama.top'">
|
||||
<br>Переход по ссылке позволит автоматически ввести ключ доступа:
|
||||
<br><div class="text-center" style="margin-top: 5px">
|
||||
<a :href="setStorageKeyLink" target="_blank">Ссылка для ввода ключа</a>
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyToClip(setStorageKeyLink, 'Ссылка')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<q-btn class="button" style="width: 250px" dense no-caps @click="enterServerStorageKey">Ввести ключ доступа</q-btn>
|
||||
</div>
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<q-btn class="button" style="width: 250px" dense no-caps @click="generateServerStorageKey">Сгенерировать новый ключ</q-btn>
|
||||
</div>
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<div class="text col">
|
||||
Рекомендуется сохранить ключ в надежном месте, чтобы всегда иметь возможность восстановить настройки,
|
||||
например, после переустановки ОС или чистки/смены браузера.<br>
|
||||
<b>ПРЕДУПРЕЖДЕНИЕ!</b> При утере ключа, НИКТО не сможет восстановить ваши данные, т.к. они сжимаются
|
||||
и шифруются ключом доступа перед отправкой на сервер.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="item row">
|
||||
<q-btn class="col q-ma-sm" dense no-caps @click="setDefaults">Установить по умолчанию</q-btn>
|
||||
</div>
|
||||
34
client/components/Reader/SettingsPage/include/ViewTab.inc
Normal file
34
client/components/Reader/SettingsPage/include/ViewTab.inc
Normal file
@@ -0,0 +1,34 @@
|
||||
<q-tabs
|
||||
v-model="selectedViewTab"
|
||||
active-color="black"
|
||||
active-bg-color="white"
|
||||
indicator-color="white"
|
||||
dense
|
||||
no-caps
|
||||
class="no-mp bg-grey-4 text-grey-7"
|
||||
>
|
||||
<q-tab name="color" label="Цвет" />
|
||||
<q-tab name="font" label="Шрифт" />
|
||||
<q-tab name="text" label="Текст" />
|
||||
<q-tab name="status" label="Строка статуса" />
|
||||
</q-tabs>
|
||||
|
||||
<div class="q-mb-sm"/>
|
||||
|
||||
<div class="col tab-panel">
|
||||
<div v-if="selectedViewTab == 'color'">
|
||||
@@include('./ViewTab/Color.inc');
|
||||
</div>
|
||||
|
||||
<div v-if="selectedViewTab == 'font'">
|
||||
@@include('./ViewTab/Font.inc');
|
||||
</div>
|
||||
|
||||
<div v-if="selectedViewTab == 'text'">
|
||||
@@include('./ViewTab/Text.inc');
|
||||
</div>
|
||||
|
||||
<div v-if="selectedViewTab == 'status'">
|
||||
@@include('./ViewTab/Status.inc');
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,58 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">Цвет</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Текст</div>
|
||||
<div class="col row">
|
||||
<q-input class="col-left no-mp"
|
||||
outlined dense
|
||||
v-model="textColorFiltered"
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('text')">
|
||||
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||
<div>
|
||||
<q-color v-model="textColor"
|
||||
no-header default-view="palette" :palette="predefineTextColors"
|
||||
/>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<span class="col" style="position: relative; top: 35px; left: 15px;">Обои:</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mt-md"/>
|
||||
<div class="item row">
|
||||
<div class="label-2">Фон</div>
|
||||
<div class="col row">
|
||||
<q-input class="col-left no-mp"
|
||||
outlined dense
|
||||
v-model="bgColorFiltered"
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
:disable="wallpaper != ''"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('bg')">
|
||||
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||
<div>
|
||||
<q-color v-model="backgroundColor" no-header default-view="palette" :palette="predefineBackgroundColors"/>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="q-px-sm"/>
|
||||
<q-select class="col" v-model="wallpaper" :options="wallpaperOptions"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,56 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">Шрифт</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Локальный/веб</div>
|
||||
<div class="col row">
|
||||
<q-select class="col-left" v-model="fontName" :options="fontsOptions" :disable="webFontName != ''"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
/>
|
||||
|
||||
<div class="q-px-sm"/>
|
||||
<q-select class="col" v-model="webFontName" :options="webFontsOptions"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Веб шрифты дают большое разнообразие,<br>
|
||||
однако есть шанс, что шрифт будет загружаться<br>
|
||||
очень медленно или вовсе не загрузится
|
||||
</q-tooltip>
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Размер</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="fontSize" :min="5" :max="200"/>
|
||||
|
||||
<div class="col q-pt-xs text-right">
|
||||
<a href="https://fonts.google.com/?subset=cyrillic" target="_blank">Примеры</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Сдвиг</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="vertShift" :min="-100" :max="100">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Сдвиг шрифта по вертикали в процентах от размера.<br>
|
||||
Отрицательное значение сдвигает вверх, положительное -<br>
|
||||
вниз. Значение зависит от метрики шрифта.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Стиль</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="fontBold" size="xs" label="Жирный" />
|
||||
<q-checkbox class="q-ml-sm" v-model="fontItalic" size="xs" label="Курсив" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">Строка статуса</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Статус</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="showStatusBar" size="xs" label="Показывать" />
|
||||
<q-checkbox class="q-ml-sm" v-model="statusBarTop" size="xs" :disable="!showStatusBar" label="Вверху/внизу" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Высота</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="statusBarHeight" :min="5" :max="100" :disable="!showStatusBar"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Прозрачность</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="statusBarColorAlpha" :min="0" :max="1" :digits="2" :step="0.1" :disable="!showStatusBar"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="statusBarClickOpen" size="xs" label="Открывать оригинал по клику">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
По клику на автора-название в строке статуса<br>
|
||||
открывать оригинал произведения в новой вкладке
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
144
client/components/Reader/SettingsPage/include/ViewTab/Text.inc
Normal file
144
client/components/Reader/SettingsPage/include/ViewTab/Text.inc
Normal file
@@ -0,0 +1,144 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">Текст</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Интервал</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="lineInterval" :min="0" :max="200"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Параграф</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="p" :min="0" :max="2000"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Отступ</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="indentLR" :min="0" :max="2000">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Слева/справа
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
<div class="q-px-sm"/>
|
||||
<NumInput class="col" v-model="indentTB" :min="0" :max="2000">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Сверху/снизу
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Сдвиг</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="textVertShift" :min="-100" :max="100">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Сдвиг текста по вертикали в процентах от размера шрифта.<br>
|
||||
Отрицательное значение сдвигает вверх, положительное -<br>
|
||||
вниз.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Скроллинг</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="scrollingDelay" :min="1" :max="10000">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Замедление скроллинга в миллисекундах.<br>
|
||||
Определяет время, за которое текст<br>
|
||||
прокручивается на одну строку.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
|
||||
<div class="q-px-sm"/>
|
||||
<q-select class="col" v-model="scrollingType" :options="['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out']"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Вид скроллинга: линейный,<br>
|
||||
ускорение-замедление и пр.
|
||||
</q-tooltip>
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Выравнивание</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="textAlignJustify" size="xs" label="По ширине" />
|
||||
<q-checkbox class="q-ml-sm" v-model="wordWrap" size="xs" label="Перенос по слогам" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col-left column justify-center text-right">
|
||||
Компактность
|
||||
</div>
|
||||
<div class="q-px-sm"/>
|
||||
<NumInput class="col" v-model="compactTextPerc" :min="0" :max="100">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Степень компактности текста в процентах.<br>
|
||||
Чем больше компактность, тем хуже выравнивание<br>
|
||||
по правому краю.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Обработка</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="cutEmptyParagraphs" size="xs" label="Убирать пустые строки" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col-left column justify-center text-right">
|
||||
Добавлять пустые
|
||||
</div>
|
||||
<div class="q-px-sm"/>
|
||||
<NumInput class="col" v-model="addEmptyParagraphs" :min="0" :max="2"/>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Изображения</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="showImages" size="xs" label="Показывать" />
|
||||
<q-checkbox class="q-ml-sm" v-model="showInlineImagesInCenter" @input="needReload" :disable="!showImages" size="xs" label="Инлайн в центр">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Выносить все изображения в центр экрана
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="imageFitWidth" :disable="!showImages" size="xs" label="Ширина не более размера экрана" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col-left column justify-center text-right">
|
||||
Высота не более
|
||||
</div>
|
||||
<div class="q-px-sm"/>
|
||||
<NumInput class="col" v-model="imageHeightLines" :min="1" :max="100" :disable="!showImages">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Определяет высоту изображения количеством строк.<br>
|
||||
В случае превышения высоты, изображение будет<br>
|
||||
уменьшено с сохранением пропорций так, чтобы<br>
|
||||
помещаться в указанное количество строк.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
@@ -159,7 +159,7 @@ export default class DrawHelper {
|
||||
const fh = h - 2*pad;
|
||||
const fh2 = fh/2;
|
||||
|
||||
const t1 = `${Math.floor((bookPos + 1)/1000)}k/${Math.floor(textLength/1000)}k`;
|
||||
const t1 = `${Math.floor((bookPos + 1)/1000)}/${Math.floor(textLength/1000)}`;
|
||||
const w1 = this.measureTextFont(t1, font) + fh2;
|
||||
const read = (bookPos + 1)/textLength;
|
||||
const t2 = `${(read*100).toFixed(2)}%`;
|
||||
@@ -317,4 +317,56 @@ export default class DrawHelper {
|
||||
await animation1Finish(duration);
|
||||
}
|
||||
}
|
||||
|
||||
async doPageAnimationRotate(page1, page2, duration, isDown, animation1Finish, animation2Finish) {
|
||||
if (isDown) {
|
||||
page1.style.transform = `rotateY(90deg)`;
|
||||
await sleep(30);
|
||||
|
||||
page2.style.transition = `${duration/2}ms ease-in`;
|
||||
page2.style.transform = `rotateY(-90deg)`;
|
||||
|
||||
await animation2Finish(duration/2);
|
||||
|
||||
page1.style.transition = `${duration/2}ms ease-out`;
|
||||
page1.style.transform = `rotateY(0deg)`;
|
||||
await animation1Finish(duration/2);
|
||||
} else {
|
||||
page1.style.transform = `rotateY(-90deg)`;
|
||||
await sleep(30);
|
||||
|
||||
page2.style.transition = `${duration/2}ms ease-in`;
|
||||
page2.style.transform = `rotateY(90deg)`;
|
||||
|
||||
await animation2Finish(duration/2);
|
||||
|
||||
page1.style.transition = `${duration/2}ms ease-out`;
|
||||
page1.style.transform = `rotateY(0deg)`;
|
||||
await animation1Finish(duration/2);
|
||||
}
|
||||
}
|
||||
|
||||
async doPageAnimationFlip(page1, page2, duration, isDown, animation1Finish, animation2Finish, backgroundColor) {
|
||||
page2.style.background = backgroundColor;
|
||||
|
||||
if (isDown) {
|
||||
page2.style.transformOrigin = '5%';
|
||||
await sleep(30);
|
||||
|
||||
page2.style.transition = `${duration}ms ease-in-out`;
|
||||
page2.style.transform = `rotateY(-120deg) translateX(${this.w/4}px)`;
|
||||
await animation2Finish(duration);
|
||||
} else {
|
||||
page2.style.transformOrigin = '95%';
|
||||
await sleep(30);
|
||||
|
||||
page2.style.transition = `${duration}ms ease-in-out`;
|
||||
page2.style.transform = `rotateY(120deg) translateX(-${this.w/4}px)`;
|
||||
await animation2Finish(duration);
|
||||
}
|
||||
|
||||
page2.style.transformOrigin = 'center';
|
||||
page2.style.background = '';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,16 +19,17 @@
|
||||
</div>
|
||||
<div v-show="clickControl" ref="layoutEvents" class="layout events" @mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
|
||||
@wheel.prevent.stop="onMouseWheel"
|
||||
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchcancel.prevent.stop="onTouchCancel"
|
||||
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
|
||||
oncontextmenu="return false;">
|
||||
<div v-show="showStatusBar" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||
<div v-show="showStatusBar && statusBarClickOpen" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||
@click.prevent.stop="onStatusBarClick"></div>
|
||||
<div v-show="fontsLoading" ref="fontsLoading"></div>
|
||||
</div>
|
||||
<div v-show="!clickControl && showStatusBar" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||
@click.prevent.stop="onStatusBarClick"></div>
|
||||
<div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||
@click.prevent.stop="onStatusBarClick">
|
||||
</div>
|
||||
<!-- невидимым делать нельзя, вовремя не подгружаютя шрифты -->
|
||||
<canvas ref="offscreenCanvas" class="layout" style="width: 0px; height: 0px"></canvas>
|
||||
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
|
||||
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -38,8 +39,8 @@ import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {loadCSS} from 'fg-loadcss';
|
||||
import _ from 'lodash';
|
||||
import {sleep} from '../../../share/utils';
|
||||
|
||||
import * as utils from '../../../share/utils';
|
||||
import bookManager from '../share/bookManager';
|
||||
import DrawHelper from './DrawHelper';
|
||||
import rstore from '../../../store/modules/reader';
|
||||
@@ -77,7 +78,6 @@ class TextPage extends Vue {
|
||||
page2 = null;
|
||||
statusBar = null;
|
||||
statusBarClickable = null;
|
||||
fontsLoading = null;
|
||||
|
||||
lastBook = null;
|
||||
bookPos = 0;
|
||||
@@ -132,8 +132,11 @@ class TextPage extends Vue {
|
||||
await this.doPageAnimation();
|
||||
}, 10);
|
||||
|
||||
this.$root.$on('resize', () => {this.$nextTick(this.onResize)});
|
||||
this.mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||
this.$root.$on('resize', async() => {
|
||||
this.$nextTick(this.onResize);
|
||||
await utils.sleep(500);
|
||||
this.$nextTick(this.onResize);
|
||||
});
|
||||
}
|
||||
|
||||
mounted() {
|
||||
@@ -146,6 +149,8 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
calcDrawProps() {
|
||||
const wideLetter = 'Щ';
|
||||
|
||||
//preloaded fonts
|
||||
this.fontList = [`12px ${this.fontName}`];
|
||||
|
||||
@@ -171,6 +176,12 @@ class TextPage extends Vue {
|
||||
this.fontShift = this.fontVertShift/100;
|
||||
this.textShift = this.textVertShift/100 + this.fontShift;
|
||||
|
||||
//statusBar
|
||||
this.$refs.statusBar.style.left = '0px';
|
||||
this.$refs.statusBar.style.top = (this.statusBarTop ? 1 : this.realHeight - this.statusBarHeight) + 'px';
|
||||
|
||||
this.statusBarColor = this.hex2rgba(this.textColor || '#000000', this.statusBarColorAlpha);
|
||||
|
||||
//drawHelper
|
||||
this.drawHelper.realWidth = this.realWidth;
|
||||
this.drawHelper.realHeight = this.realHeight;
|
||||
@@ -196,14 +207,24 @@ class TextPage extends Vue {
|
||||
this.drawHelper.lineHeight = this.lineHeight;
|
||||
this.drawHelper.context = this.context;
|
||||
|
||||
//сообщение "Загрузка шрифтов..."
|
||||
const flText = 'Загрузка шрифта...';
|
||||
this.$refs.fontsLoading.innerHTML = flText;
|
||||
const fontsLoadingStyle = this.$refs.fontsLoading.style;
|
||||
fontsLoadingStyle.position = 'absolute';
|
||||
fontsLoadingStyle.fontSize = this.fontSize + 'px';
|
||||
fontsLoadingStyle.top = (this.realHeight/2 - 2*this.fontSize) + 'px';
|
||||
fontsLoadingStyle.left = (this.realWidth - this.drawHelper.measureText(flText, {}))/2 + 'px';
|
||||
//альтернатива context.measureText
|
||||
if (!this.context.measureText(wideLetter).width) {
|
||||
const ctx = this.$refs.measureWidth;
|
||||
this.drawHelper.measureText = function(text, style) {
|
||||
ctx.innerText = text;
|
||||
ctx.style.font = this.fontByStyle(style);
|
||||
return ctx.clientWidth;
|
||||
};
|
||||
|
||||
this.drawHelper.measureTextFont = function(text, font) {
|
||||
ctx.innerText = text;
|
||||
ctx.style.font = font;
|
||||
return ctx.clientWidth;
|
||||
}
|
||||
}
|
||||
|
||||
//statusBar
|
||||
this.statusBarClickable = this.drawHelper.statusBarClickable(this.statusBarTop, this.statusBarHeight);
|
||||
|
||||
//parsed
|
||||
if (this.parsed) {
|
||||
@@ -214,8 +235,10 @@ class TextPage extends Vue {
|
||||
this.parsed.wordWrap = this.wordWrap;
|
||||
this.parsed.cutEmptyParagraphs = this.cutEmptyParagraphs;
|
||||
this.parsed.addEmptyParagraphs = this.addEmptyParagraphs;
|
||||
let t = '';
|
||||
while (this.drawHelper.measureText(t, {}) < this.w) t += 'Щ';
|
||||
let t = wideLetter;
|
||||
if (!this.drawHelper.measureText(t, {}))
|
||||
throw new Error('Ошибка measureText');
|
||||
while (this.drawHelper.measureText(t, {}) < this.w) t += wideLetter;
|
||||
this.parsed.maxWordLength = t.length - 1;
|
||||
this.parsed.measureText = this.drawHelper.measureText.bind(this.drawHelper);
|
||||
this.parsed.lineHeight = this.lineHeight;
|
||||
@@ -223,51 +246,50 @@ class TextPage extends Vue {
|
||||
this.parsed.showInlineImagesInCenter = this.showInlineImagesInCenter;
|
||||
this.parsed.imageHeightLines = this.imageHeightLines;
|
||||
this.parsed.imageFitWidth = this.imageFitWidth;
|
||||
this.parsed.compactTextPerc = this.compactTextPerc;
|
||||
|
||||
this.parsed.testText = 'Это тестовый текст. Его ширина выдается системой неверно некоторое время.';
|
||||
this.parsed.testWidth = this.drawHelper.measureText(this.parsed.testText, {});
|
||||
}
|
||||
|
||||
//statusBar
|
||||
this.$refs.statusBar.style.left = '0px';
|
||||
this.$refs.statusBar.style.top = (this.statusBarTop ? 1 : this.realHeight - this.statusBarHeight) + 'px';
|
||||
|
||||
this.statusBarColor = this.hex2rgba(this.textColor || '#000000', this.statusBarColorAlpha);
|
||||
this.statusBarClickable = this.drawHelper.statusBarClickable(this.statusBarTop, this.statusBarHeight);
|
||||
|
||||
//scrolling page
|
||||
const pageSpace = this.scrollHeight - this.pageLineCount*this.lineHeight;
|
||||
let y = pageSpace/2;
|
||||
let top = pageSpace/2;
|
||||
if (this.showStatusBar)
|
||||
y += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
|
||||
let page1 = this.$refs.scrollBox1;
|
||||
let page2 = this.$refs.scrollBox2;
|
||||
page1.style.width = this.w + this.indentLR + 'px';
|
||||
page2.style.width = this.w + this.indentLR + 'px';
|
||||
page1.style.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px';
|
||||
page2.style.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px';
|
||||
page1.style.top = y + 'px';
|
||||
page2.style.top = y + 'px';
|
||||
page1.style.left = this.indentLR + 'px';
|
||||
page2.style.left = this.indentLR + 'px';
|
||||
top += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
|
||||
let page1 = this.$refs.scrollBox1.style;
|
||||
let page2 = this.$refs.scrollBox2.style;
|
||||
|
||||
page1.perspective = page2.perspective = '3072px';
|
||||
|
||||
page1 = this.$refs.scrollingPage1;
|
||||
page2 = this.$refs.scrollingPage2;
|
||||
page1.style.width = this.w + this.indentLR + 'px';
|
||||
page2.style.width = this.w + this.indentLR + 'px';
|
||||
page1.style.height = this.scrollHeight + this.lineHeight + 'px';
|
||||
page2.style.height = this.scrollHeight + this.lineHeight + 'px';
|
||||
page1.width = page2.width = this.w + this.indentLR + 'px';
|
||||
page1.height = page2.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px';
|
||||
page1.top = page2.top = top + 'px';
|
||||
page1.left = page2.left = this.indentLR + 'px';
|
||||
|
||||
page1 = this.$refs.scrollingPage1.style;
|
||||
page2 = this.$refs.scrollingPage2.style;
|
||||
page1.width = page2.width = this.w + this.indentLR + 'px';
|
||||
page1.height = page2.height = this.scrollHeight + this.lineHeight + 'px';
|
||||
}
|
||||
|
||||
async checkLoadedFonts() {
|
||||
let loaded = await Promise.all(this.fontList.map(font => document.fonts.check(font)));
|
||||
if (loaded.some(r => !r)) {
|
||||
loaded = await Promise.all(this.fontList.map(font => document.fonts.load(font)));
|
||||
if (loaded.some(r => !r.length))
|
||||
throw new Error('some font not loaded');
|
||||
await Promise.all(this.fontList.map(font => document.fonts.load(font)));
|
||||
}
|
||||
}
|
||||
|
||||
async loadFonts() {
|
||||
this.fontsLoading = true;
|
||||
|
||||
let close = null;
|
||||
(async() => {
|
||||
await utils.sleep(500);
|
||||
if (this.fontsLoading)
|
||||
close = this.$root.notify.info('Загрузка шрифта <i class="la la-snowflake icon-rotate" style="font-size: 150%"></i>');
|
||||
})();
|
||||
|
||||
if (!this.fontsLoaded)
|
||||
this.fontsLoaded = {};
|
||||
//загрузка дин.шрифта
|
||||
@@ -277,27 +299,15 @@ class TextPage extends Vue {
|
||||
this.fontsLoaded[this.fontCssUrl] = 1;
|
||||
}
|
||||
|
||||
const waitingTime = 10*1000;
|
||||
const delay = 100;
|
||||
let i = 0;
|
||||
//ждем шрифты
|
||||
while (i < waitingTime/delay) {
|
||||
i++;
|
||||
try {
|
||||
await this.checkLoadedFonts();
|
||||
i = waitingTime;
|
||||
} catch (e) {
|
||||
await sleep(delay);
|
||||
}
|
||||
}
|
||||
if (i !== waitingTime) {
|
||||
this.$notify.error({
|
||||
title: 'Ошибка загрузки',
|
||||
message: 'Некоторые шрифты не удалось загрузить'
|
||||
});
|
||||
try {
|
||||
await this.checkLoadedFonts();
|
||||
} catch (e) {
|
||||
this.$root.notify.error('Некоторые шрифты не удалось загрузить', 'Ошибка загрузки');
|
||||
}
|
||||
|
||||
this.fontsLoading = false;
|
||||
if (close)
|
||||
close();
|
||||
}
|
||||
|
||||
getSettings() {
|
||||
@@ -325,13 +335,19 @@ class TextPage extends Vue {
|
||||
|
||||
this.draw();
|
||||
|
||||
// шрифты хрен знает когда подгружаются в div, поэтому
|
||||
const parsed = this.parsed;
|
||||
await sleep(5000);
|
||||
if (this.parsed === parsed) {
|
||||
parsed.force = true;
|
||||
this.draw();
|
||||
parsed.force = false;
|
||||
// ширина шрифта некоторое время выдается неверно, поэтому
|
||||
if (!omitLoadFonts) {
|
||||
const parsed = this.parsed;
|
||||
|
||||
let i = 0;
|
||||
const t = this.parsed.testText;
|
||||
while (i++ < 50 && this.parsed === parsed && this.drawHelper.measureText(t, {}) === this.parsed.testWidth)
|
||||
await utils.sleep(100);
|
||||
|
||||
if (this.parsed === parsed) {
|
||||
this.parsed.testWidth = this.drawHelper.measureText(t, {});
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,7 +366,6 @@ class TextPage extends Vue {
|
||||
this.updateLayout();
|
||||
this.book = null;
|
||||
this.meta = null;
|
||||
this.fb2 = null;
|
||||
this.parsed = null;
|
||||
|
||||
this.linesUp = null;
|
||||
@@ -364,47 +379,39 @@ class TextPage extends Vue {
|
||||
|
||||
if (this.lastBook) {
|
||||
(async() => {
|
||||
//подождем ленивый парсинг
|
||||
this.stopLazyParse = true;
|
||||
while (this.doingLazyParse) await sleep(10);
|
||||
try {
|
||||
//подождем ленивый парсинг
|
||||
this.stopLazyParse = true;
|
||||
while (this.doingLazyParse) await utils.sleep(10);
|
||||
|
||||
const isParsed = await bookManager.hasBookParsed(this.lastBook);
|
||||
if (!isParsed) {
|
||||
return;
|
||||
const isParsed = await bookManager.hasBookParsed(this.lastBook);
|
||||
if (!isParsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.book = await bookManager.getBook(this.lastBook);
|
||||
this.meta = bookManager.metaOnly(this.book);
|
||||
const bt = utils.getBookTitle(this.meta.fb2);
|
||||
|
||||
this.title = bt.fullTitle;
|
||||
|
||||
this.$root.$emit('set-app-title', this.title);
|
||||
|
||||
this.parsed = this.book.parsed;
|
||||
|
||||
this.page1 = null;
|
||||
this.page2 = null;
|
||||
this.statusBar = null;
|
||||
await this.stopTextScrolling();
|
||||
|
||||
await this.calcPropsAndLoadFonts();
|
||||
|
||||
this.refreshTime();
|
||||
if (this.lazyParseEnabled)
|
||||
this.lazyParsePara();
|
||||
} catch (e) {
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||
}
|
||||
|
||||
this.book = await bookManager.getBook(this.lastBook);
|
||||
this.meta = bookManager.metaOnly(this.book);
|
||||
this.fb2 = this.meta.fb2;
|
||||
|
||||
let authorNames = [];
|
||||
if (this.fb2.author) {
|
||||
authorNames = this.fb2.author.map(a => _.compact([
|
||||
a.lastName,
|
||||
a.firstName,
|
||||
a.middleName
|
||||
]).join(' '));
|
||||
}
|
||||
|
||||
this.title = _.compact([
|
||||
authorNames.join(', '),
|
||||
this.fb2.bookTitle
|
||||
]).join(' - ');
|
||||
|
||||
this.$root.$emit('set-app-title', this.title);
|
||||
|
||||
this.parsed = this.book.parsed;
|
||||
|
||||
this.page1 = null;
|
||||
this.page2 = null;
|
||||
this.statusBar = null;
|
||||
await this.stopTextScrolling();
|
||||
|
||||
this.calcPropsAndLoadFonts();
|
||||
|
||||
this.refreshTime();
|
||||
if (this.lazyParseEnabled)
|
||||
this.lazyParsePara();
|
||||
})();
|
||||
}
|
||||
}
|
||||
@@ -428,13 +435,13 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
async onResize() {
|
||||
/*this.page1 = null;
|
||||
this.page2 = null;
|
||||
this.statusBar = null;*/
|
||||
|
||||
this.calcDrawProps();
|
||||
this.setBackground();
|
||||
this.draw();
|
||||
try {
|
||||
this.calcDrawProps();
|
||||
this.setBackground();
|
||||
this.draw();
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
get settings() {
|
||||
@@ -468,15 +475,15 @@ class TextPage extends Vue {
|
||||
|
||||
generateWaitingFunc(waitingHandlerName, stopPropertyName) {
|
||||
const func = (timeout) => {
|
||||
return new Promise(async(resolve) => {
|
||||
return new Promise((resolve, reject) => { (async() => {
|
||||
this[waitingHandlerName] = resolve;
|
||||
let wait = (timeout + 201)/100;
|
||||
while (wait > 0 && !this[stopPropertyName]) {
|
||||
wait--;
|
||||
await sleep(100);
|
||||
await utils.sleep(100);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
})().catch(reject); });
|
||||
};
|
||||
return func;
|
||||
}
|
||||
@@ -484,12 +491,12 @@ class TextPage extends Vue {
|
||||
async startTextScrolling() {
|
||||
if (this.doingScrolling || !this.book || !this.parsed.textLength || !this.linesDown || this.pageLineCount < 1 ||
|
||||
this.linesDown.length <= this.pageLineCount) {
|
||||
this.$emit('stop-scrolling');
|
||||
this.doStopScrolling();
|
||||
return;
|
||||
}
|
||||
|
||||
//ждем анимацию
|
||||
while (this.inAnimation) await sleep(10);
|
||||
while (this.inAnimation) await utils.sleep(10);
|
||||
|
||||
this.stopScrolling = false;
|
||||
this.doingScrolling = true;
|
||||
@@ -500,7 +507,7 @@ class TextPage extends Vue {
|
||||
this.page1 = this.page2;
|
||||
this.toggleLayout = true;
|
||||
await this.$nextTick();
|
||||
await sleep(50);
|
||||
await utils.sleep(50);
|
||||
|
||||
this.cachedPos = -1;
|
||||
this.draw();
|
||||
@@ -525,7 +532,7 @@ class TextPage extends Vue {
|
||||
}
|
||||
this.resolveTransition1Finish = null;
|
||||
this.doingScrolling = false;
|
||||
this.$emit('stop-scrolling');
|
||||
this.doStopScrolling();
|
||||
this.draw();
|
||||
}
|
||||
|
||||
@@ -537,7 +544,7 @@ class TextPage extends Vue {
|
||||
page.style.transform = 'none';
|
||||
page.offsetHeight;
|
||||
|
||||
while (this.doingScrolling) await sleep(10);
|
||||
while (this.doingScrolling) await utils.sleep(10);
|
||||
}
|
||||
|
||||
draw() {
|
||||
@@ -625,7 +632,7 @@ class TextPage extends Vue {
|
||||
const animation1Finish = this.generateWaitingFunc('resolveAnimation1Finish', 'stopAnimation');
|
||||
const animation2Finish = this.generateWaitingFunc('resolveAnimation2Finish', 'stopAnimation');
|
||||
const transition1Finish = this.generateWaitingFunc('resolveTransition1Finish', 'stopAnimation');
|
||||
//const transition2Finish = this.generateWaitingFunc('resolveTransition2Finish', 'stopAnimation');
|
||||
const transition2Finish = this.generateWaitingFunc('resolveTransition2Finish', 'stopAnimation');
|
||||
|
||||
const duration = Math.round(3000*(1 - this.pageChangeAnimationSpeed/100));
|
||||
let page1 = this.$refs.scrollingPage1;
|
||||
@@ -654,6 +661,14 @@ class TextPage extends Vue {
|
||||
page1.style.height = this.scrollHeight + this.lineHeight + 'px';
|
||||
page2.style.height = this.scrollHeight + this.lineHeight + 'px';
|
||||
break;
|
||||
case 'rotate':
|
||||
await this.drawHelper.doPageAnimationRotate(page1, page2,
|
||||
duration, this.pageChangeDirectionDown, transition1Finish, transition2Finish);
|
||||
break;
|
||||
case 'flip':
|
||||
await this.drawHelper.doPageAnimationFlip(page1, page2,
|
||||
duration, this.pageChangeDirectionDown, transition1Finish, transition2Finish, this.backgroundColor);
|
||||
break;
|
||||
}
|
||||
|
||||
this.resolveAnimation1Finish = null;
|
||||
@@ -738,7 +753,7 @@ class TextPage extends Vue {
|
||||
for (let i = 0; i < this.parsed.para.length; i++) {
|
||||
j++;
|
||||
if (j > 1) {
|
||||
await sleep(1);
|
||||
await utils.sleep(1);
|
||||
j = 0;
|
||||
}
|
||||
if (this.stopLazyParse)
|
||||
@@ -760,7 +775,7 @@ class TextPage extends Vue {
|
||||
async refreshTime() {
|
||||
if (!this.timeRefreshing) {
|
||||
this.timeRefreshing = true;
|
||||
await sleep(60*1000);
|
||||
await utils.sleep(60*1000);
|
||||
|
||||
if (this.book && this.parsed.textLength) {
|
||||
this.debouncedDrawStatusBar();
|
||||
@@ -811,7 +826,7 @@ class TextPage extends Vue {
|
||||
let i = this.pageLineCount;
|
||||
if (this.keepLastToFirst)
|
||||
i--;
|
||||
if (i >= 0 && this.linesDown.length >= 2*i) {
|
||||
if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) {
|
||||
this.currentAnimation = this.pageChangeAnimation;
|
||||
this.pageChangeDirectionDown = true;
|
||||
this.bookPos = this.linesDown[i].begin;
|
||||
@@ -856,17 +871,28 @@ class TextPage extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
doToolBarToggle() {
|
||||
this.$emit('tool-bar-toggle');
|
||||
doToolBarToggle(event) {
|
||||
this.$emit('do-action', {action: 'switchToolbar', event});
|
||||
}
|
||||
|
||||
doScrollingToggle() {
|
||||
this.$emit('do-action', {action: 'scrolling', event});
|
||||
}
|
||||
|
||||
doFullScreenToggle() {
|
||||
this.$emit('do-action', {action: 'fullScreen', event});
|
||||
}
|
||||
|
||||
doStopScrolling() {
|
||||
this.$emit('do-action', {action: 'stopScrolling', event});
|
||||
}
|
||||
|
||||
async doFontSizeInc() {
|
||||
if (!this.settingsChanging) {
|
||||
this.settingsChanging = true;
|
||||
const newSize = (this.settings.fontSize + 1 < 100 ? this.settings.fontSize + 1 : 100);
|
||||
const newSettings = Object.assign({}, this.settings, {fontSize: newSize});
|
||||
this.commit('reader/setSettings', newSettings);
|
||||
await sleep(50);
|
||||
const newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100);
|
||||
this.commit('reader/setSettings', {fontSize: newSize});
|
||||
await utils.sleep(50);
|
||||
this.settingsChanging = false;
|
||||
}
|
||||
}
|
||||
@@ -875,9 +901,8 @@ class TextPage extends Vue {
|
||||
if (!this.settingsChanging) {
|
||||
this.settingsChanging = true;
|
||||
const newSize = (this.settings.fontSize - 1 > 5 ? this.settings.fontSize - 1 : 5);
|
||||
const newSettings = Object.assign({}, this.settings, {fontSize: newSize});
|
||||
this.commit('reader/setSettings', newSettings);
|
||||
await sleep(50);
|
||||
this.commit('reader/setSettings', {fontSize: newSize});
|
||||
await utils.sleep(50);
|
||||
this.settingsChanging = false;
|
||||
}
|
||||
}
|
||||
@@ -886,9 +911,8 @@ class TextPage extends Vue {
|
||||
if (!this.settingsChanging) {
|
||||
this.settingsChanging = true;
|
||||
const newDelay = (this.settings.scrollingDelay - 50 > 1 ? this.settings.scrollingDelay - 50 : 1);
|
||||
const newSettings = Object.assign({}, this.settings, {scrollingDelay: newDelay});
|
||||
this.commit('reader/setSettings', newSettings);
|
||||
await sleep(50);
|
||||
this.commit('reader/setSettings', {scrollingDelay: newDelay});
|
||||
await utils.sleep(50);
|
||||
this.settingsChanging = false;
|
||||
}
|
||||
}
|
||||
@@ -897,76 +921,12 @@ class TextPage extends Vue {
|
||||
if (!this.settingsChanging) {
|
||||
this.settingsChanging = true;
|
||||
const newDelay = (this.settings.scrollingDelay + 50 < 10000 ? this.settings.scrollingDelay + 50 : 10000);
|
||||
const newSettings = Object.assign({}, this.settings, {scrollingDelay: newDelay});
|
||||
this.commit('reader/setSettings', newSettings);
|
||||
await sleep(50);
|
||||
this.commit('reader/setSettings', {scrollingDelay: newDelay});
|
||||
await utils.sleep(50);
|
||||
this.settingsChanging = false;
|
||||
}
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
let result = false;
|
||||
if (event.type == 'keydown' && !event.ctrlKey && !event.altKey) {
|
||||
result = true;
|
||||
switch (event.code) {
|
||||
case 'ArrowDown':
|
||||
if (event.shiftKey)
|
||||
this.doScrollingSpeedUp();
|
||||
else
|
||||
this.doDown();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
if (event.shiftKey)
|
||||
this.doScrollingSpeedDown();
|
||||
else
|
||||
this.doUp();
|
||||
break;
|
||||
case 'PageDown':
|
||||
case 'ArrowRight':
|
||||
this.doPageDown();
|
||||
break;
|
||||
case 'Space':
|
||||
if (event.shiftKey)
|
||||
this.doPageUp();
|
||||
else
|
||||
this.doPageDown();
|
||||
break;
|
||||
case 'PageUp':
|
||||
case 'ArrowLeft':
|
||||
case 'Backspace':
|
||||
this.doPageUp();
|
||||
break;
|
||||
case 'Home':
|
||||
this.doHome();
|
||||
break;
|
||||
case 'End':
|
||||
this.doEnd();
|
||||
break;
|
||||
case 'KeyA':
|
||||
if (event.shiftKey)
|
||||
this.doFontSizeDec();
|
||||
else
|
||||
this.doFontSizeInc();
|
||||
break;
|
||||
case 'Enter':
|
||||
case 'Backquote'://`
|
||||
case 'KeyF':
|
||||
this.$emit('full-screen-toogle');
|
||||
break;
|
||||
case 'Tab':
|
||||
case 'KeyQ':
|
||||
this.doToolBarToggle();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
default:
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async startClickRepeat(pointX, pointY) {
|
||||
this.repX = pointX;
|
||||
this.repY = pointY;
|
||||
@@ -976,7 +936,7 @@ class TextPage extends Vue {
|
||||
let delay = 400;
|
||||
while (this.repDoing) {
|
||||
this.handleClick(pointX, pointY);
|
||||
await sleep(delay);
|
||||
await utils.sleep(delay);
|
||||
if (delay > 15)
|
||||
delay *= 0.8;
|
||||
}
|
||||
@@ -989,57 +949,100 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
onTouchStart(event) {
|
||||
if (!this.mobile)
|
||||
if (!this.$isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
|
||||
if (event.touches.length == 1) {
|
||||
const touch = event.touches[0];
|
||||
const rect = event.target.getBoundingClientRect();
|
||||
const x = touch.pageX - rect.left;
|
||||
const y = touch.pageY - rect.top;
|
||||
if (this.handleClick(x, y)) {
|
||||
this.repDoing = true;
|
||||
this.debouncedStartClickRepeat(x, y);
|
||||
const hc = this.handleClick(x, y, new Set(['Menu']));
|
||||
if (hc) {
|
||||
if (hc != 'Menu') {
|
||||
this.repDoing = true;
|
||||
this.debouncedStartClickRepeat(x, y);
|
||||
} else {
|
||||
this.startTouch = {x, y};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTouchEnd() {
|
||||
if (!this.mobile)
|
||||
onTouchMove(event) {
|
||||
if (this.startTouch) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
onTouchEnd(event) {
|
||||
if (!this.$isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
|
||||
if (event.changedTouches.length == 1) {
|
||||
const touch = event.changedTouches[0];
|
||||
const rect = event.target.getBoundingClientRect();
|
||||
const x = touch.pageX - rect.left;
|
||||
const y = touch.pageY - rect.top;
|
||||
if (this.startTouch) {
|
||||
const dy = this.startTouch.y - y;
|
||||
const dx = this.startTouch.x - x;
|
||||
const moveDelta = 30;
|
||||
const touchDelta = 15;
|
||||
if (dy > 0 && Math.abs(dy) >= moveDelta && Math.abs(dy) > Math.abs(dx)) {
|
||||
//движение вверх
|
||||
this.doFullScreenToggle();
|
||||
} else if (dy < 0 && Math.abs(dy) >= moveDelta && Math.abs(dy) > Math.abs(dx)) {
|
||||
//движение вниз
|
||||
this.doScrollingToggle();
|
||||
} else if (dx > 0 && Math.abs(dx) >= moveDelta && Math.abs(dy) < Math.abs(dx)) {
|
||||
//движение влево
|
||||
this.doScrollingSpeedDown();
|
||||
} else if (dx < 0 && Math.abs(dx) >= moveDelta && Math.abs(dy) < Math.abs(dx)) {
|
||||
//движение вправо
|
||||
this.doScrollingSpeedUp();
|
||||
} else if (Math.abs(dy) < touchDelta && Math.abs(dx) < touchDelta) {
|
||||
this.doToolBarToggle(event);
|
||||
}
|
||||
|
||||
this.startTouch = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTouchCancel() {
|
||||
if (!this.mobile)
|
||||
if (!this.$isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
}
|
||||
|
||||
onMouseDown(event) {
|
||||
if (this.mobile)
|
||||
if (this.$isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
if (event.button == 0) {
|
||||
if (this.handleClick(event.offsetX, event.offsetY)) {
|
||||
const hc = this.handleClick(event.offsetX, event.offsetY);
|
||||
if (hc && hc != 'Menu') {
|
||||
this.repDoing = true;
|
||||
this.debouncedStartClickRepeat(event.offsetX, event.offsetY);
|
||||
}
|
||||
} else if (event.button == 1) {
|
||||
this.$emit('scrolling-toggle');
|
||||
this.doScrollingToggle();
|
||||
} else if (event.button == 2) {
|
||||
this.doToolBarToggle();
|
||||
this.doToolBarToggle(event);
|
||||
}
|
||||
}
|
||||
|
||||
onMouseUp() {
|
||||
if (this.mobile)
|
||||
if (this.$isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
}
|
||||
|
||||
onMouseWheel(event) {
|
||||
if (this.mobile)
|
||||
if (this.$isMobileDevice)
|
||||
return;
|
||||
if (event.deltaY > 0) {
|
||||
this.doDown();
|
||||
@@ -1050,14 +1053,14 @@ class TextPage extends Vue {
|
||||
|
||||
onStatusBarClick() {
|
||||
const url = this.meta.url;
|
||||
if (url && url.indexOf('file://') != 0) {
|
||||
if (url && url.indexOf('disk://') != 0) {
|
||||
window.open(url, '_blank');
|
||||
} else {
|
||||
this.$alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска', '', {type: 'warning'});
|
||||
this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'});
|
||||
}
|
||||
}
|
||||
|
||||
handleClick(pointX, pointY) {
|
||||
getClickAction(pointX, pointY) {
|
||||
const w = pointX/this.realWidth*100;
|
||||
const h = pointY/this.realHeight*100;
|
||||
|
||||
@@ -1073,27 +1076,35 @@ class TextPage extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'Down' ://Down
|
||||
this.doDown();
|
||||
break;
|
||||
case 'Up' ://Up
|
||||
this.doUp();
|
||||
break;
|
||||
case 'PgDown' ://PgDown
|
||||
this.doPageDown();
|
||||
break;
|
||||
case 'PgUp' ://PgUp
|
||||
this.doPageUp();
|
||||
break;
|
||||
case 'Menu' :
|
||||
this.doToolBarToggle();
|
||||
break;
|
||||
default :
|
||||
// Nothing
|
||||
return action;
|
||||
}
|
||||
|
||||
handleClick(pointX, pointY, exclude) {
|
||||
const action = this.getClickAction(pointX, pointY);
|
||||
|
||||
if (!exclude || !exclude.has(action)) {
|
||||
switch (action) {
|
||||
case 'Down' ://Down
|
||||
this.doDown();
|
||||
break;
|
||||
case 'Up' ://Up
|
||||
this.doUp();
|
||||
break;
|
||||
case 'PgDown' ://PgDown
|
||||
this.doPageDown();
|
||||
break;
|
||||
case 'PgUp' ://PgUp
|
||||
this.doPageUp();
|
||||
break;
|
||||
case 'Menu' :
|
||||
this.doToolBarToggle();
|
||||
break;
|
||||
default :
|
||||
// Nothing
|
||||
}
|
||||
}
|
||||
|
||||
return (action && action != 'Menu');
|
||||
return action;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1120,6 +1131,10 @@ class TextPage extends Vue {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.on-top {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.back {
|
||||
z-index: 5;
|
||||
}
|
||||
@@ -1185,4 +1200,5 @@ class TextPage extends Vue {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import he from 'he';
|
||||
import sax from '../../../../server/core/BookConverter/sax';
|
||||
import {sleep} from '../../../share/utils';
|
||||
import sax from '../../../../server/core/sax';
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
const maxImageLineCount = 100;
|
||||
|
||||
@@ -32,9 +32,6 @@ export default class BookParser {
|
||||
|
||||
//defaults
|
||||
let fb2 = {
|
||||
firstName: '',
|
||||
middleName: '',
|
||||
lastName: '',
|
||||
bookTitle: '',
|
||||
};
|
||||
|
||||
@@ -49,11 +46,21 @@ export default class BookParser {
|
||||
let isFirstSection = true;
|
||||
let isFirstTitlePara = false;
|
||||
|
||||
//изображения
|
||||
this.binary = {};
|
||||
let binaryId = '';
|
||||
let binaryType = '';
|
||||
let dimPromises = [];
|
||||
|
||||
//оглавление
|
||||
this.contents = [];
|
||||
let curTitle = {paraIndex: -1, title: '', subtitles: []};
|
||||
let curSubtitle = {paraIndex: -1, title: ''};
|
||||
let inTitle = false;
|
||||
let inSubtitle = false;
|
||||
let sectionLevel = 0;
|
||||
let bodyIndex = 0;
|
||||
|
||||
let paraIndex = -1;
|
||||
let paraOffset = 0;
|
||||
let para = []; /*array of
|
||||
@@ -62,12 +69,11 @@ export default class BookParser {
|
||||
offset: Number, //сумма всех length до этого параграфа
|
||||
length: Number, //длина text без тегов
|
||||
text: String, //текст параграфа с вложенными тегами
|
||||
cut: Boolean, //параграф - кандидат на сокрытие (cutEmptyParagraphs)
|
||||
addIndex: Number, //индекс добавляемого пустого параграфа (addEmptyParagraphs)
|
||||
}
|
||||
*/
|
||||
const getImageDimensions = (binaryId, binaryType, data) => {
|
||||
return new Promise (async(resolve, reject) => {
|
||||
return new Promise ((resolve, reject) => { (async() => {
|
||||
const i = new Image();
|
||||
let resolved = false;
|
||||
i.onload = () => {
|
||||
@@ -81,19 +87,17 @@ export default class BookParser {
|
||||
resolve();
|
||||
};
|
||||
|
||||
i.onerror = (e) => {
|
||||
reject(e);
|
||||
};
|
||||
i.onerror = reject;
|
||||
|
||||
i.src = `data:${binaryType};base64,${data}`;
|
||||
await sleep(30*1000);
|
||||
await utils.sleep(30*1000);
|
||||
if (!resolved)
|
||||
reject('Не удалось получить размер изображения');
|
||||
});
|
||||
})().catch(reject); });
|
||||
};
|
||||
|
||||
const getExternalImageDimensions = (src) => {
|
||||
return new Promise (async(resolve, reject) => {
|
||||
return new Promise ((resolve, reject) => { (async() => {
|
||||
const i = new Image();
|
||||
let resolved = false;
|
||||
i.onload = () => {
|
||||
@@ -105,15 +109,13 @@ export default class BookParser {
|
||||
resolve();
|
||||
};
|
||||
|
||||
i.onerror = (e) => {
|
||||
reject(e);
|
||||
};
|
||||
i.onerror = reject;
|
||||
|
||||
i.src = src;
|
||||
await sleep(30*1000);
|
||||
await utils.sleep(30*1000);
|
||||
if (!resolved)
|
||||
reject('Не удалось получить размер изображения');
|
||||
});
|
||||
})().catch(reject); });
|
||||
};
|
||||
|
||||
const newParagraph = (text, len, addIndex) => {
|
||||
@@ -123,10 +125,15 @@ export default class BookParser {
|
||||
offset: paraOffset,
|
||||
length: len,
|
||||
text: text,
|
||||
cut: (!addIndex && (len == 1 && text[0] == ' ')),
|
||||
addIndex: (addIndex ? addIndex : 0),
|
||||
};
|
||||
|
||||
if (inSubtitle) {
|
||||
curSubtitle.title += '<p>';
|
||||
} else if (inTitle) {
|
||||
curTitle.title += '<p>';
|
||||
}
|
||||
|
||||
para[paraIndex] = p;
|
||||
paraOffset += p.length;
|
||||
};
|
||||
@@ -138,11 +145,12 @@ export default class BookParser {
|
||||
return;
|
||||
}
|
||||
|
||||
const prevParaIndex = paraIndex;
|
||||
let p = para[paraIndex];
|
||||
//добавление пустых (addEmptyParagraphs) параграфов
|
||||
paraOffset -= p.length;
|
||||
//добавление пустых (addEmptyParagraphs) параграфов перед текущим
|
||||
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
|
||||
paraIndex--;
|
||||
paraOffset -= p.length;
|
||||
for (let i = 0; i < 2; i++) {
|
||||
newParagraph(' ', 1, i + 1);
|
||||
}
|
||||
@@ -151,20 +159,27 @@ export default class BookParser {
|
||||
p.index = paraIndex;
|
||||
p.offset = paraOffset;
|
||||
para[paraIndex] = p;
|
||||
paraOffset += p.length;
|
||||
}
|
||||
|
||||
paraOffset -= p.length;
|
||||
//параграф оказался непустой
|
||||
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
|
||||
if (curTitle.paraIndex == prevParaIndex)
|
||||
curTitle.paraIndex = paraIndex;
|
||||
if (curSubtitle.paraIndex == prevParaIndex)
|
||||
curSubtitle.paraIndex = paraIndex;
|
||||
|
||||
//уберем начальный пробел
|
||||
p.length = 0;
|
||||
p.text = p.text.substr(1);
|
||||
p.cut = (len == 1 && text[0] == ' ');
|
||||
}
|
||||
|
||||
p.length += len;
|
||||
p.text += text;
|
||||
|
||||
|
||||
if (inSubtitle) {
|
||||
curSubtitle.title += text;
|
||||
} else if (inTitle) {
|
||||
curTitle.title += text;
|
||||
}
|
||||
|
||||
para[paraIndex] = p;
|
||||
paraOffset += p.length;
|
||||
};
|
||||
@@ -174,12 +189,12 @@ export default class BookParser {
|
||||
return;
|
||||
|
||||
tag = elemName;
|
||||
path += '/' + elemName;
|
||||
path += '/' + tag;
|
||||
|
||||
if (tag == 'binary') {
|
||||
let attrs = sax.getAttrsSync(tail);
|
||||
binaryType = (attrs['content-type'] && attrs['content-type'].value ? attrs['content-type'].value : '');
|
||||
if (binaryType == 'image/jpeg' || binaryType == 'image/png')
|
||||
if (binaryType == 'image/jpeg' || binaryType == 'image/png' || binaryType == 'application/octet-stream')
|
||||
binaryId = (attrs.id.value ? attrs.id.value : '');
|
||||
}
|
||||
|
||||
@@ -201,17 +216,55 @@ export default class BookParser {
|
||||
}
|
||||
}
|
||||
|
||||
if (elemName == 'author' && path.indexOf('/fictionbook/description/title-info/author') == 0) {
|
||||
if (path == '/fictionbook/description/title-info/author') {
|
||||
if (!fb2.author)
|
||||
fb2.author = [];
|
||||
|
||||
fb2.author.push({});
|
||||
}
|
||||
|
||||
const isPublishSequence = (path == '/fictionbook/description/publish-info/sequence');
|
||||
if (path == '/fictionbook/description/title-info/sequence' || isPublishSequence) {
|
||||
if (!fb2.sequence)
|
||||
fb2.sequence = [];
|
||||
|
||||
if (!isPublishSequence || !fb2.sequence.length) {
|
||||
const attrs = sax.getAttrsSync(tail);
|
||||
const seq = {};
|
||||
if (attrs.name && attrs.name.value) {
|
||||
seq.name = attrs.name.value;
|
||||
}
|
||||
if (attrs.number && attrs.number.value) {
|
||||
seq.number = attrs.number.value;
|
||||
}
|
||||
|
||||
fb2.sequence.push(seq);
|
||||
}
|
||||
}
|
||||
|
||||
if (path.indexOf('/fictionbook/body') == 0) {
|
||||
if (tag == 'body') {
|
||||
if (isFirstBody && fb2.annotation) {
|
||||
const ann = fb2.annotation.split('<p>').filter(v => v).map(v => utils.removeHtmlTags(v));
|
||||
ann.forEach(a => {
|
||||
newParagraph(`<emphasis><space w="1">${a}</space></emphasis>`, a.length);
|
||||
});
|
||||
if (ann.length)
|
||||
newParagraph(' ', 1);
|
||||
}
|
||||
|
||||
if (isFirstBody && fb2.sequence && fb2.sequence.length) {
|
||||
const bt = utils.getBookTitle(fb2);
|
||||
if (bt.sequence) {
|
||||
newParagraph(bt.sequence, bt.sequence.length);
|
||||
newParagraph(' ', 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isFirstBody)
|
||||
newParagraph(' ', 1);
|
||||
isFirstBody = false;
|
||||
bodyIndex++;
|
||||
}
|
||||
|
||||
if (tag == 'title') {
|
||||
@@ -219,12 +272,17 @@ export default class BookParser {
|
||||
isFirstTitlePara = true;
|
||||
bold = true;
|
||||
center = true;
|
||||
|
||||
inTitle = true;
|
||||
curTitle = {paraIndex, title: '', inset: sectionLevel, bodyIndex, subtitles: []};
|
||||
this.contents.push(curTitle);
|
||||
}
|
||||
|
||||
if (tag == 'section') {
|
||||
if (!isFirstSection)
|
||||
newParagraph(' ', 1);
|
||||
isFirstSection = false;
|
||||
sectionLevel++;
|
||||
}
|
||||
|
||||
if (tag == 'emphasis' || tag == 'strong') {
|
||||
@@ -244,9 +302,14 @@ export default class BookParser {
|
||||
newParagraph(' ', 1);
|
||||
isFirstTitlePara = true;
|
||||
bold = true;
|
||||
center = true;
|
||||
|
||||
inSubtitle = true;
|
||||
curSubtitle = {paraIndex, inset: sectionLevel, title: ''};
|
||||
curTitle.subtitles.push(curSubtitle);
|
||||
}
|
||||
|
||||
if (tag == 'epigraph') {
|
||||
if (tag == 'epigraph' || tag == 'annotation') {
|
||||
italic = true;
|
||||
space += 1;
|
||||
}
|
||||
@@ -273,6 +336,11 @@ export default class BookParser {
|
||||
isFirstTitlePara = false;
|
||||
bold = false;
|
||||
center = false;
|
||||
inTitle = false;
|
||||
}
|
||||
|
||||
if (tag == 'section') {
|
||||
sectionLevel--;
|
||||
}
|
||||
|
||||
if (tag == 'emphasis' || tag == 'strong') {
|
||||
@@ -286,11 +354,15 @@ export default class BookParser {
|
||||
if (tag == 'subtitle') {
|
||||
isFirstTitlePara = false;
|
||||
bold = false;
|
||||
center = false;
|
||||
inSubtitle = false;
|
||||
}
|
||||
|
||||
if (tag == 'epigraph') {
|
||||
if (tag == 'epigraph' || tag == 'annotation') {
|
||||
italic = false;
|
||||
space -= 1;
|
||||
if (tag == 'annotation')
|
||||
newParagraph(' ', 1);
|
||||
}
|
||||
|
||||
if (tag == 'stanza') {
|
||||
@@ -371,11 +443,10 @@ export default class BookParser {
|
||||
tClose += (bold ? '</strong>' : '');
|
||||
tClose += (center ? '</center>' : '');
|
||||
|
||||
if (path.indexOf('/fictionbook/body/title') == 0) {
|
||||
growParagraph(`${tOpen}${text}${tClose}`, text.length);
|
||||
}
|
||||
|
||||
if (path.indexOf('/fictionbook/body/section') == 0) {
|
||||
if (path.indexOf('/fictionbook/body/title') == 0 ||
|
||||
path.indexOf('/fictionbook/body/section') == 0 ||
|
||||
path.indexOf('/fictionbook/body/epigraph') == 0
|
||||
) {
|
||||
growParagraph(`${tOpen}${text}${tClose}`, text.length);
|
||||
}
|
||||
|
||||
@@ -385,7 +456,7 @@ export default class BookParser {
|
||||
};
|
||||
|
||||
const onProgress = async(prog) => {
|
||||
await sleep(1);
|
||||
await utils.sleep(1);
|
||||
callback(prog);
|
||||
};
|
||||
|
||||
@@ -407,7 +478,7 @@ export default class BookParser {
|
||||
this.textLength = paraOffset;
|
||||
|
||||
callback(100);
|
||||
await sleep(10);
|
||||
await utils.sleep(10);
|
||||
|
||||
return {fb2};
|
||||
}
|
||||
@@ -611,6 +682,7 @@ export default class BookParser {
|
||||
|
||||
if (!this.force &&
|
||||
para.parsed &&
|
||||
para.parsed.testWidth === this.testWidth &&
|
||||
para.parsed.w === this.w &&
|
||||
para.parsed.p === this.p &&
|
||||
para.parsed.wordWrap === this.wordWrap &&
|
||||
@@ -620,11 +692,13 @@ export default class BookParser {
|
||||
para.parsed.addEmptyParagraphs === this.addEmptyParagraphs &&
|
||||
para.parsed.showImages === this.showImages &&
|
||||
para.parsed.imageHeightLines === this.imageHeightLines &&
|
||||
para.parsed.imageFitWidth === this.imageFitWidth
|
||||
para.parsed.imageFitWidth === this.imageFitWidth &&
|
||||
para.parsed.compactTextPerc === this.compactTextPerc
|
||||
)
|
||||
return para.parsed;
|
||||
|
||||
const parsed = {
|
||||
testWidth: this.testWidth,
|
||||
w: this.w,
|
||||
p: this.p,
|
||||
wordWrap: this.wordWrap,
|
||||
@@ -635,10 +709,8 @@ export default class BookParser {
|
||||
showImages: this.showImages,
|
||||
imageHeightLines: this.imageHeightLines,
|
||||
imageFitWidth: this.imageFitWidth,
|
||||
visible: !(
|
||||
(this.cutEmptyParagraphs && para.cut) ||
|
||||
(para.addIndex > this.addEmptyParagraphs)
|
||||
)
|
||||
compactTextPerc: this.compactTextPerc,
|
||||
visible: true, //вычисляется позже
|
||||
};
|
||||
|
||||
|
||||
@@ -654,9 +726,12 @@ export default class BookParser {
|
||||
text: String,
|
||||
}
|
||||
}*/
|
||||
|
||||
let parts = this.splitToStyle(para.text);
|
||||
|
||||
//инициализация парсера
|
||||
let line = {begin: para.offset, parts: []};
|
||||
let paragraphText = '';//текст параграфа
|
||||
let partText = '';//накапливаемый кусок со стилем
|
||||
|
||||
let str = '';//измеряемая строка
|
||||
@@ -665,13 +740,16 @@ export default class BookParser {
|
||||
let style = {};
|
||||
let ofs = 0;//смещение от начала параграфа para.offset
|
||||
let imgW = 0;
|
||||
let imageInPara = false;
|
||||
const compactWidth = this.measureText('W', {})*this.compactTextPerc/100;
|
||||
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
|
||||
for (const part of parts) {
|
||||
style = part.style;
|
||||
paragraphText += part.text;
|
||||
|
||||
//изображения
|
||||
if (part.image.id && !part.image.inline) {
|
||||
parsed.visible = this.showImages;
|
||||
imageInPara = true;
|
||||
let bin = this.binary[part.image.id];
|
||||
if (!bin)
|
||||
bin = {h: 1, w: 1};
|
||||
@@ -749,7 +827,7 @@ export default class BookParser {
|
||||
p = (style.space ? p + parsed.p*style.space : p);
|
||||
let w = this.measureText(str, style) + p;
|
||||
let wordTail = word;
|
||||
if (w > parsed.w && prevStr != '') {
|
||||
if (w > parsed.w + compactWidth && prevStr != '') {
|
||||
if (parsed.wordWrap) {//по слогам
|
||||
let slogi = this.splitToSlogi(word);
|
||||
|
||||
@@ -762,7 +840,7 @@ export default class BookParser {
|
||||
for (let k = 0; k < slogiLen - 1; k++) {
|
||||
let slog = slogi[0];
|
||||
let ww = this.measureText(s + slog + (slog[slog.length - 1] == '-' ? '' : '-'), style) + p;
|
||||
if (ww <= parsed.w) {
|
||||
if (ww <= parsed.w + compactWidth) {
|
||||
s += slog;
|
||||
ss += slog;
|
||||
} else
|
||||
@@ -838,6 +916,16 @@ export default class BookParser {
|
||||
}
|
||||
}
|
||||
|
||||
//parsed.visible
|
||||
if (imageInPara) {
|
||||
parsed.visible = this.showImages;
|
||||
} else {
|
||||
parsed.visible = !(
|
||||
(para.addIndex > this.addEmptyParagraphs) ||
|
||||
(para.addIndex == 0 && this.cutEmptyParagraphs && paragraphText.trim() == '')
|
||||
);
|
||||
}
|
||||
|
||||
parsed.lines = lines;
|
||||
para.parsed = parsed;
|
||||
|
||||
|
||||
@@ -4,62 +4,121 @@ import _ from 'lodash';
|
||||
import * as utils from '../../../share/utils';
|
||||
import BookParser from './BookParser';
|
||||
|
||||
const maxDataSize = 300*1024*1024;//compressed bytes
|
||||
const maxDataSize = 500*1024*1024;//compressed bytes
|
||||
|
||||
//локальный кэш метаданных книг, ограничение maxDataSize
|
||||
const bmMetaStore = localForage.createInstance({
|
||||
name: 'bmMetaStore'
|
||||
});
|
||||
|
||||
//локальный кэш самих книг, ограничение maxDataSize
|
||||
const bmDataStore = localForage.createInstance({
|
||||
name: 'bmDataStore'
|
||||
});
|
||||
|
||||
const bmRecentStore = localForage.createInstance({
|
||||
//список недавно открытых книг
|
||||
const bmRecentStoreOld = localForage.createInstance({
|
||||
name: 'bmRecentStore'
|
||||
});
|
||||
|
||||
const bmCacheStore = localForage.createInstance({
|
||||
name: 'bmCacheStore'
|
||||
const bmRecentStoreNew = localForage.createInstance({
|
||||
name: 'bmRecentStoreNew'
|
||||
});
|
||||
|
||||
class BookManager {
|
||||
async init(settings) {
|
||||
this.loaded = false;
|
||||
this.settings = settings;
|
||||
|
||||
this.eventListeners = [];
|
||||
this.books = {};
|
||||
|
||||
//bmCacheStore нужен только для ускорения загрузки читалки
|
||||
this.booksCached = await bmCacheStore.getItem('books');
|
||||
if (!this.booksCached)
|
||||
this.booksCached = {};
|
||||
this.recent = await bmCacheStore.getItem('recent');
|
||||
this.recentLast = await bmCacheStore.getItem('recent-last');
|
||||
if (this.recentLast)
|
||||
this.recent[this.recentLast.key] = this.recentLast;
|
||||
this.recentRev = await bmRecentStore.getItem('recent-rev') || 0;
|
||||
this.recentLastRev = await bmRecentStore.getItem('recent-last-rev') || 0;
|
||||
this.books = Object.assign({}, this.booksCached);
|
||||
this.recent = {};
|
||||
this.saveRecent = _.debounce(() => {
|
||||
bmRecentStoreNew.setItem('recent', this.recent);
|
||||
}, 300, {maxWait: 800});
|
||||
|
||||
this.recentChanged2 = true;
|
||||
this.saveRecentItem = _.debounce(() => {
|
||||
bmRecentStoreNew.setItem('recent-item', this.recentItem);
|
||||
this.recentRev = (this.recentRev < 1000 ? this.recentRev + 1 : 1);
|
||||
bmRecentStoreNew.setItem('rev', this.recentRev);
|
||||
}, 200, {maxWait: 300});
|
||||
|
||||
if (!this.books || !this.recent) {
|
||||
this.books = {};
|
||||
this.recent = {};
|
||||
await this.loadMeta(true);
|
||||
} else {
|
||||
this.loadMeta(false);
|
||||
//загрузка bmRecentStore
|
||||
this.recentRev = await bmRecentStoreNew.getItem('rev') || 0;
|
||||
if (this.recentRev) {
|
||||
this.recent = await bmRecentStoreNew.getItem('recent');
|
||||
if (!this.recent)
|
||||
this.recent = {};
|
||||
|
||||
this.recentItem = await bmRecentStoreNew.getItem('recent-item');
|
||||
if (this.recentItem)
|
||||
this.recent[this.recentItem.key] = this.recentItem;
|
||||
|
||||
this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
|
||||
if (this.recentLastKey) {
|
||||
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
|
||||
if (_.isObject(meta)) {
|
||||
this.books[meta.key] = meta;
|
||||
}
|
||||
}
|
||||
|
||||
await this.cleanRecentBooks();
|
||||
|
||||
//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
|
||||
{
|
||||
await this.convertFileToDiskPrefix();
|
||||
if (this.recentRev > 10)
|
||||
await bmRecentStoreOld.clear();
|
||||
}
|
||||
} else {//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
|
||||
this.recentLast = await bmRecentStoreOld.getItem('recent-last');
|
||||
if (this.recentLast) {
|
||||
this.recent[this.recentLast.key] = this.recentLast;
|
||||
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
|
||||
if (_.isObject(meta)) {
|
||||
this.books[meta.key] = meta;
|
||||
}
|
||||
}
|
||||
|
||||
let key = null;
|
||||
const len = await bmRecentStoreOld.length();
|
||||
for (let i = len - 1; i >= 0; i--) {
|
||||
key = await bmRecentStoreOld.key(i);
|
||||
if (key) {
|
||||
let r = await bmRecentStoreOld.getItem(key);
|
||||
if (_.isObject(r) && r.key) {
|
||||
this.recent[r.key] = r;
|
||||
}
|
||||
} else {
|
||||
await bmRecentStoreOld.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
//размножение для дебага
|
||||
/*if (key) {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const k = this.keyFromUrl(i.toString());
|
||||
this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000, url: utils.randomHexString(300)});
|
||||
}
|
||||
}*/
|
||||
|
||||
await bmRecentStoreNew.setItem('recent', this.recent);
|
||||
this.recentRev = 1;
|
||||
await bmRecentStoreNew.setItem('rev', this.recentRev);
|
||||
}
|
||||
|
||||
this.recentChanged = true;
|
||||
|
||||
this.loadStored();//no await
|
||||
}
|
||||
|
||||
//долгая загрузка из хранилища,
|
||||
//хранение в отдельных записях дает относительно
|
||||
//нормальное поведение при нескольких вкладках с читалкой в браузере
|
||||
async loadMeta(immediate) {
|
||||
if (!immediate)
|
||||
await utils.sleep(2000);
|
||||
//Ленивая асинхронная загрузка bmMetaStore
|
||||
async loadStored() {
|
||||
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
|
||||
await utils.sleep(2000);
|
||||
|
||||
let len = await bmMetaStore.length();
|
||||
for (let i = 0; i < len; i++) {
|
||||
for (let i = len - 1; i >= 0; i--) {
|
||||
const key = await bmMetaStore.key(i);
|
||||
const keySplit = key.split('-');
|
||||
|
||||
@@ -67,6 +126,7 @@ class BookManager {
|
||||
let meta = await bmMetaStore.getItem(key);
|
||||
|
||||
if (_.isObject(meta)) {
|
||||
//уже может быть распарсена книга
|
||||
const oldBook = this.books[meta.key];
|
||||
this.books[meta.key] = meta;
|
||||
|
||||
@@ -79,39 +139,9 @@ class BookManager {
|
||||
}
|
||||
}
|
||||
|
||||
let key = null;
|
||||
len = await bmRecentStore.length();
|
||||
for (let i = 0; i < len; i++) {
|
||||
key = await bmRecentStore.key(i);
|
||||
if (key) {
|
||||
let r = await bmRecentStore.getItem(key);
|
||||
if (_.isObject(r) && r.key) {
|
||||
this.recent[r.key] = r;
|
||||
}
|
||||
} else {
|
||||
await bmRecentStore.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
//размножение для дебага
|
||||
/*if (key) {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const k = this.keyFromUrl(i.toString());
|
||||
this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000});
|
||||
}
|
||||
}*/
|
||||
await this.cleanBooks();
|
||||
|
||||
//очистка позже
|
||||
//await this.cleanRecentBooks();
|
||||
|
||||
this.booksCached = {};
|
||||
for (const key in this.books) {
|
||||
this.booksCached[key] = this.metaOnly(this.books[key]);
|
||||
}
|
||||
await bmCacheStore.setItem('books', this.booksCached);
|
||||
await bmCacheStore.setItem('recent', this.recent);
|
||||
this.emit('load-meta-finish');
|
||||
this.loaded = true;
|
||||
this.emit('load-stored-finish');
|
||||
}
|
||||
|
||||
async cleanBooks() {
|
||||
@@ -131,22 +161,93 @@ class BookManager {
|
||||
}
|
||||
|
||||
if (size > maxDataSize && toDel) {
|
||||
await this._delBook(toDel);
|
||||
await this.delBook(toDel);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async addBook(newBook, callback) {
|
||||
if (!this.books)
|
||||
await this.init();
|
||||
async deflateWithProgress(data, callback) {
|
||||
const chunkSize = 128*1024;
|
||||
const deflator = new utils.pako.Deflate({level: 5});
|
||||
|
||||
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
||||
let chunkNum = 0;
|
||||
let perc = 0;
|
||||
let prevPerc = 0;
|
||||
|
||||
for (var i = 0; i < data.length; i += chunkSize) {
|
||||
if ((i + chunkSize) >= data.length) {
|
||||
deflator.push(data.substring(i, i + chunkSize), true);
|
||||
} else {
|
||||
deflator.push(data.substring(i, i + chunkSize), false);
|
||||
}
|
||||
chunkNum++;
|
||||
|
||||
perc = Math.round(chunkNum/chunkTotal*100);
|
||||
if (perc != prevPerc) {
|
||||
callback(perc);
|
||||
await utils.sleep(1);
|
||||
prevPerc = perc;
|
||||
}
|
||||
}
|
||||
|
||||
if (deflator.err) {
|
||||
throw new Error(deflator.msg);
|
||||
}
|
||||
|
||||
callback(100);
|
||||
|
||||
return deflator.result;
|
||||
}
|
||||
|
||||
async inflateWithProgress(data, callback) {
|
||||
const chunkSize = 64*1024;
|
||||
const inflator = new utils.pako.Inflate({to: 'string'});
|
||||
|
||||
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
||||
let chunkNum = 0;
|
||||
let perc = 0;
|
||||
let prevPerc = 0;
|
||||
|
||||
for (var i = 0; i < data.length; i += chunkSize) {
|
||||
if ((i + chunkSize) >= data.length) {
|
||||
inflator.push(data.subarray(i, i + chunkSize), true);
|
||||
} else {
|
||||
inflator.push(data.subarray(i, i + chunkSize), false);
|
||||
}
|
||||
chunkNum++;
|
||||
|
||||
perc = Math.round(chunkNum/chunkTotal*100);
|
||||
if (perc != prevPerc) {
|
||||
callback(perc);
|
||||
await utils.sleep(1);
|
||||
prevPerc = perc;
|
||||
}
|
||||
}
|
||||
|
||||
if (inflator.err) {
|
||||
throw new Error(inflator.msg);
|
||||
}
|
||||
|
||||
callback(100);
|
||||
|
||||
return inflator.result;
|
||||
}
|
||||
|
||||
async addBook(newBook, callback) {
|
||||
let meta = {url: newBook.url, path: newBook.path};
|
||||
meta.key = this.keyFromUrl(meta.url);
|
||||
meta.addTime = Date.now();
|
||||
|
||||
const cb = (perc) => {
|
||||
const p = Math.round(80*perc/100);
|
||||
const p = Math.round(30*perc/100);
|
||||
callback(p);
|
||||
};
|
||||
|
||||
const cb2 = (perc) => {
|
||||
const p = Math.round(30 + 65*perc/100);
|
||||
callback(p);
|
||||
};
|
||||
|
||||
@@ -155,53 +256,80 @@ class BookManager {
|
||||
|
||||
let data = newBook.data;
|
||||
if (result.dataCompressed) {
|
||||
data = utils.pako.deflate(data, {level: 9});
|
||||
//data = utils.pako.deflate(data, {level: 5});
|
||||
data = await this.deflateWithProgress(data, cb2);
|
||||
result.dataCompressedLength = data.byteLength;
|
||||
}
|
||||
callback(90);
|
||||
callback(95);
|
||||
|
||||
this.books[meta.key] = result;
|
||||
this.booksCached[meta.key] = this.metaOnly(result);
|
||||
|
||||
await bmMetaStore.setItem(`bmMeta-${meta.key}`, this.metaOnly(result));
|
||||
await bmDataStore.setItem(`bmData-${meta.key}`, data);
|
||||
await bmCacheStore.setItem('books', this.booksCached);
|
||||
|
||||
callback(100);
|
||||
return result;
|
||||
}
|
||||
|
||||
hasBookParsed(meta) {
|
||||
async hasBookParsed(meta) {
|
||||
if (!this.books)
|
||||
return false;
|
||||
if (!meta.url)
|
||||
return false;
|
||||
if (!meta.key)
|
||||
meta.key = this.keyFromUrl(meta.url);
|
||||
|
||||
let book = this.books[meta.key];
|
||||
|
||||
if (!book && !this.loaded) {
|
||||
book = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
|
||||
if (book)
|
||||
this.books[meta.key] = book;
|
||||
}
|
||||
|
||||
return !!(book && book.parsed);
|
||||
}
|
||||
|
||||
async getBook(meta, callback) {
|
||||
if (!this.books)
|
||||
await this.init();
|
||||
let result = undefined;
|
||||
if (!meta.key)
|
||||
meta.key = this.keyFromUrl(meta.url);
|
||||
|
||||
result = this.books[meta.key];
|
||||
|
||||
if (!result) {
|
||||
result = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
|
||||
if (result)
|
||||
this.books[meta.key] = result;
|
||||
}
|
||||
|
||||
//Если файл на сервере изменился, считаем, что в кеше его нету
|
||||
if (meta.path && result && meta.path != result.path) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result && !result.parsed) {
|
||||
let data = await bmDataStore.getItem(`bmData-${meta.key}`);
|
||||
callback(10);
|
||||
callback(5);
|
||||
await utils.sleep(10);
|
||||
|
||||
let cb = (perc) => {
|
||||
const p = 5 + Math.round(15*perc/100);
|
||||
callback(p);
|
||||
};
|
||||
|
||||
if (result.dataCompressed) {
|
||||
data = utils.pako.inflate(data, {to: 'string'});
|
||||
try {
|
||||
//data = utils.pako.inflate(data, {to: 'string'});
|
||||
data = await this.inflateWithProgress(data, cb);
|
||||
} catch (e) {
|
||||
this.delBook(meta);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
callback(20);
|
||||
|
||||
const cb = (perc) => {
|
||||
cb = (perc) => {
|
||||
const p = 20 + Math.round(80*perc/100);
|
||||
callback(p);
|
||||
};
|
||||
@@ -213,27 +341,14 @@ class BookManager {
|
||||
return result;
|
||||
}
|
||||
|
||||
async _delBook(meta) {
|
||||
async delBook(meta) {
|
||||
await bmMetaStore.removeItem(`bmMeta-${meta.key}`);
|
||||
await bmDataStore.removeItem(`bmData-${meta.key}`);
|
||||
|
||||
delete this.books[meta.key];
|
||||
delete this.booksCached[meta.key];
|
||||
}
|
||||
|
||||
async delBook(meta) {
|
||||
if (!this.books)
|
||||
await this.init();
|
||||
|
||||
await this._delBook(meta);
|
||||
|
||||
await bmCacheStore.setItem('books', this.booksCached);
|
||||
}
|
||||
|
||||
async parseBook(meta, data, callback) {
|
||||
if (!this.books)
|
||||
await this.init();
|
||||
|
||||
const parsed = new BookParser(this.settings);
|
||||
|
||||
const parsedMeta = await parsed.parse(data, callback);
|
||||
@@ -248,7 +363,6 @@ class BookManager {
|
||||
|
||||
metaOnly(book) {
|
||||
let result = Object.assign({}, book);
|
||||
delete result.data;//можно будет убрать эту строку со временем
|
||||
delete result.parsed;
|
||||
return result;
|
||||
}
|
||||
@@ -257,9 +371,45 @@ class BookManager {
|
||||
return utils.stringToHex(url);
|
||||
}
|
||||
|
||||
//-- recent --------------------------------------------------------------
|
||||
async recentSetItem(item = null, skipCheck = false) {
|
||||
const rev = await bmRecentStoreNew.getItem('rev');
|
||||
if (rev != this.recentRev && !skipCheck) {
|
||||
const newRecent = await bmRecentStoreNew.getItem('recent');
|
||||
Object.assign(this.recent, newRecent);
|
||||
this.recentItem = await bmRecentStoreNew.getItem('recent-item');
|
||||
this.recentRev = rev;
|
||||
}
|
||||
|
||||
const prevKey = (this.recentItem ? this.recentItem.key : '');
|
||||
if (item) {
|
||||
this.recent[item.key] = item;
|
||||
this.recentItem = item;
|
||||
} else {
|
||||
this.recentItem = null;
|
||||
}
|
||||
|
||||
this.saveRecentItem();
|
||||
|
||||
if (!item || prevKey != item.key) {
|
||||
this.saveRecent();
|
||||
}
|
||||
|
||||
this.recentChanged = true;
|
||||
|
||||
if (item) {
|
||||
this.emit('recent-changed', item.key);
|
||||
} else {
|
||||
this.emit('recent-changed');
|
||||
}
|
||||
}
|
||||
|
||||
async recentSetLastKey(key) {
|
||||
this.recentLastKey = key;
|
||||
await bmRecentStoreNew.setItem('recent-last-key', this.recentLastKey);
|
||||
}
|
||||
|
||||
async setRecentBook(value) {
|
||||
if (!this.recent)
|
||||
await this.init();
|
||||
const result = this.metaOnly(value);
|
||||
result.touchTime = Date.now();
|
||||
result.deleted = 0;
|
||||
@@ -272,71 +422,75 @@ class BookManager {
|
||||
result.bookPosSeen = this.recent[result.key].bookPosSeen;
|
||||
}
|
||||
|
||||
this.recent[result.key] = result;
|
||||
|
||||
await bmRecentStore.setItem(result.key, result);
|
||||
|
||||
//кэшируем, аккуратно
|
||||
let saveRecent = false;
|
||||
if (!(this.recentLast && this.recentLast.key == result.key)) {
|
||||
await bmCacheStore.setItem('recent', this.recent);
|
||||
saveRecent = true;
|
||||
}
|
||||
this.recentLast = result;
|
||||
await bmCacheStore.setItem('recent-last', this.recentLast);
|
||||
|
||||
this.mostRecentCached = result;
|
||||
this.recentChanged2 = true;
|
||||
|
||||
if (saveRecent)
|
||||
this.emit('save-recent');
|
||||
this.emit('recent-changed');
|
||||
await this.recentSetLastKey(result.key);
|
||||
await this.recentSetItem(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async getRecentBook(value) {
|
||||
if (!this.recent)
|
||||
await this.init();
|
||||
return this.recent[value.key];
|
||||
}
|
||||
|
||||
async delRecentBook(value) {
|
||||
if (!this.recent)
|
||||
await this.init();
|
||||
const item = this.recent[value.key];
|
||||
item.deleted = 1;
|
||||
|
||||
this.recent[value.key].deleted = 1;
|
||||
await bmRecentStore.setItem(value.key, this.recent[value.key]);
|
||||
await bmCacheStore.setItem('recent', this.recent);
|
||||
if (this.recentLastKey == value.key) {
|
||||
await this.recentSetLastKey(null);
|
||||
}
|
||||
|
||||
this.mostRecentCached = null;
|
||||
this.recentChanged2 = true;
|
||||
|
||||
this.emit('save-recent');
|
||||
await this.recentSetItem(item);
|
||||
this.emit('recent-deleted', value.key);
|
||||
}
|
||||
|
||||
async cleanRecentBooks() {
|
||||
if (!this.recent)
|
||||
await this.init();
|
||||
|
||||
const sorted = this.getSortedRecent();
|
||||
|
||||
let isDel = false;
|
||||
for (let i = 1000; i < sorted.length; i++) {
|
||||
await bmRecentStore.removeItem(sorted[i].key);
|
||||
delete this.recent[sorted[i].key];
|
||||
isDel = true;
|
||||
}
|
||||
|
||||
this.sortedRecentCached = null;
|
||||
await bmCacheStore.setItem('recent', this.recent);
|
||||
|
||||
if (isDel)
|
||||
await this.recentSetItem();
|
||||
return isDel;
|
||||
}
|
||||
|
||||
mostRecentBook() {
|
||||
if (this.mostRecentCached) {
|
||||
return this.mostRecentCached;
|
||||
async convertFileToDiskPrefix() {
|
||||
let isConverted = false;
|
||||
|
||||
const newRecent = {};
|
||||
for (let key of Object.keys(this.recent)) {
|
||||
let newKey = key;
|
||||
let newUrl = this.recent[key].url;
|
||||
|
||||
if (newKey.indexOf('66696c65') == 0) {
|
||||
newKey = newKey.replace(/^66696c65/, '6469736b');
|
||||
if (newUrl)
|
||||
newUrl = newUrl.replace(/^file/, 'disk');
|
||||
isConverted = true;
|
||||
}
|
||||
|
||||
newRecent[newKey] = this.recent[key];
|
||||
newRecent[newKey].key = newKey;
|
||||
if (newUrl)
|
||||
newRecent[newKey].url = newUrl;
|
||||
}
|
||||
if (isConverted) {
|
||||
this.recent = newRecent;
|
||||
await this.recentSetItem(null, true);
|
||||
}
|
||||
return isConverted;
|
||||
}
|
||||
|
||||
mostRecentBook() {
|
||||
if (this.recentLastKey) {
|
||||
return this.recent[this.recentLastKey];
|
||||
}
|
||||
const oldKey = this.recentLastKey;
|
||||
|
||||
let max = 0;
|
||||
let result = null;
|
||||
@@ -347,12 +501,18 @@ class BookManager {
|
||||
result = book;
|
||||
}
|
||||
}
|
||||
this.mostRecentCached = result;
|
||||
|
||||
const newRecentLastKey = (result ? result.key : null);
|
||||
this.recentSetLastKey(newRecentLastKey);//no await
|
||||
|
||||
if (newRecentLastKey !== oldKey)
|
||||
this.emit('recent-changed');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getSortedRecent() {
|
||||
if (!this.recentChanged2 && this.sortedRecentCached) {
|
||||
if (!this.recentChanged && this.sortedRecentCached) {
|
||||
return this.sortedRecentCached;
|
||||
}
|
||||
|
||||
@@ -361,7 +521,7 @@ class BookManager {
|
||||
result.sort((a, b) => b.touchTime - a.touchTime);
|
||||
|
||||
this.sortedRecentCached = result;
|
||||
this.recentChanged2 = false;
|
||||
this.recentChanged = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -369,53 +529,24 @@ class BookManager {
|
||||
const mergedRecent = _.cloneDeep(this.recent);
|
||||
|
||||
Object.assign(mergedRecent, value);
|
||||
const newRecent = {};
|
||||
for (const rec of Object.values(mergedRecent)) {
|
||||
if (rec.key) {
|
||||
await bmRecentStore.setItem(rec.key, rec);
|
||||
newRecent[rec.key] = rec;
|
||||
}
|
||||
|
||||
//подстраховка от hotReload
|
||||
for (let i of Object.keys(mergedRecent)) {
|
||||
if (!mergedRecent[i].key || mergedRecent[i].key !== i)
|
||||
delete mergedRecent[i];
|
||||
}
|
||||
|
||||
this.recent = mergedRecent;
|
||||
|
||||
this.recent = newRecent;
|
||||
await bmCacheStore.setItem('recent', this.recent);
|
||||
await this.recentSetLastKey(null);
|
||||
await this.recentSetItem(null, true);
|
||||
|
||||
this.recentLast = null;
|
||||
await bmCacheStore.setItem('recent-last', this.recentLast);
|
||||
|
||||
this.mostRecentCached = null;
|
||||
this.emit('recent-changed');
|
||||
}
|
||||
|
||||
async setRecentRev(value) {
|
||||
await bmRecentStore.setItem('recent-rev', value);
|
||||
this.recentRev = value;
|
||||
}
|
||||
|
||||
async setRecentLast(value) {
|
||||
if (!value.key)
|
||||
value = null;
|
||||
|
||||
this.recentLast = value;
|
||||
await bmCacheStore.setItem('recent-last', this.recentLast);
|
||||
if (value && value.key) {
|
||||
this.recent[value.key] = value;
|
||||
await bmRecentStore.setItem(value.key, value);
|
||||
await bmCacheStore.setItem('recent', this.recent);
|
||||
}
|
||||
|
||||
this.mostRecentCached = null;
|
||||
this.emit('recent-changed');
|
||||
}
|
||||
|
||||
async setRecentLastRev(value) {
|
||||
bmRecentStore.setItem('recent-last-rev', value);
|
||||
this.recentLastRev = value;
|
||||
this.emit('set-recent');
|
||||
}
|
||||
|
||||
addEventListener(listener) {
|
||||
if (this.eventListeners.indexOf(listener) < 0)
|
||||
this.eventListeners.push(listener);
|
||||
this.eventListeners.push(listener);
|
||||
}
|
||||
|
||||
removeEventListener(listener) {
|
||||
@@ -425,8 +556,12 @@ class BookManager {
|
||||
}
|
||||
|
||||
emit(eventName, value) {
|
||||
for (const listener of this.eventListeners)
|
||||
listener(eventName, value);
|
||||
if (this.eventListeners) {
|
||||
for (const listener of this.eventListeners) {
|
||||
//console.log(eventName);
|
||||
listener(eventName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
420
client/components/Reader/versionHistory.js
Normal file
420
client/components/Reader/versionHistory.js
Normal file
@@ -0,0 +1,420 @@
|
||||
export const versionHistory = [
|
||||
{
|
||||
showUntil: '2020-12-10',
|
||||
header: '0.9.10 (2020-12-03)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлена частичная поддержка формата Djvu</li>
|
||||
<li>добавлена поддержка Rar-архивов</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-11-20',
|
||||
header: '0.9.9 (2020-11-21)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>оптимизации, исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-11-12',
|
||||
header: '0.9.8 (2020-11-13)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлено окно "Оглавление/закладки"</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-11-11',
|
||||
header: '0.9.7 (2020-11-12)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-11-05',
|
||||
header: '0.9.6 (2020-11-06)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>завершена работа над новым окном "Библиотека"</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-10-31',
|
||||
header: '0.9.5 (2020-11-01)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-10-28',
|
||||
header: '0.9.4 (2020-10-29)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>заработал новый сайт <a href="https://liberama.top">https://liberama.top</a>, где будет более свободный обмен книгами</li>
|
||||
<li>для liberama.top добавлено новое окно: "Библиотека"</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-05-20',
|
||||
header: '0.9.3 (2020-05-21)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-04-25',
|
||||
header: '0.9.2 (2020-03-15)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>в настройки добавлена возможность назначать сочетания клавиш на команды в читалке</li>
|
||||
<li>переход на Service Worker вместо AppCache для автономного режима работы</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-03-02',
|
||||
header: '0.9.1 (2020-03-03)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>улучшение работы серверной части</li>
|
||||
<li>незначительные изменения интерфейса</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-02-25',
|
||||
header: '0.9.0 (2020-02-26)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>переход на UI-фреймфорк Quasar</li>
|
||||
<li>незначительные изменения интерфейса</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-02-05',
|
||||
header: '0.8.4 (2020-02-06)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлен paypal-адрес для пожертвований</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-01-27',
|
||||
header: '0.8.3 (2020-01-28)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
|
||||
<li>внутренние оптимизации</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-01-19',
|
||||
header: '0.8.2 (2020-01-20)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>внутренние оптимизации</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-01-06',
|
||||
header: '0.8.1 (2020-01-07)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлена частичная поддержка формата FB3</li>
|
||||
<li>исправлен баг "Request path contains unescaped characters"</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-01-05',
|
||||
header: '0.8.0 (2020-01-02)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>окончательный переход на https</li>
|
||||
<li>код проекта теперь Open Source: <a href="https://github.com/bookpauk/liberama" target="_blank">https://github.com/bookpauk/liberama</a></li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-11-26',
|
||||
header: '0.7.9 (2019-11-27)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлен неубираемый баннер для http-версии о переходе на httpS</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-11-24',
|
||||
header: '0.7.8 (2019-11-25)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>улучшение html-фильтров для сайтов</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-11-10',
|
||||
header: '0.7.7 (2019-11-06)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлены следующие жесты для тачскрина (только при включенной опции "управление кликом"):</li>
|
||||
<ul>
|
||||
<li style="list-style-type: square">от центра вверх: на весь экран</li>
|
||||
<li style="list-style-type: square">от центра вниз: плавный скроллинг</li>
|
||||
<li style="list-style-type: square">от центра вправо: увеличить скорость скроллинга</li>
|
||||
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
|
||||
</ul>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-10-29',
|
||||
header: '0.7.6 (2019-10-30)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-10-21',
|
||||
header: '0.7.5 (2019-10-22)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-10-17',
|
||||
header: '0.7.3 (2019-10-18)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>внутренние переделки механизма синхронизации с сервером</li>
|
||||
<li>добавлен html-фильтр для сайтов www.fanfiction.net, archiveofourown.org</li>
|
||||
<li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-09-19',
|
||||
header: '0.7.1 (2019-09-20)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
<li>на панель управления добавлена кнопка "Автономный режим"</li>
|
||||
<li>актуализирована справка</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-10-01',
|
||||
header: '0.7.0 (2019-09-07)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>налажена работа https-версии сайта, рекомендуется плавный переход</li>
|
||||
<li>добавлена возможность загрузки и работы https-версии читалки в оффлайн-режиме (при отсутствии интернета)</li>
|
||||
<li>упрощение механизма серверной синхронизации с целью повышения надежности и избавления от багов</li>
|
||||
<li>окна теперь можно перемещать за заголовок</li>
|
||||
<li>немного улучшен внешний вид и управление на смартфонах</li>
|
||||
<li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-07-20',
|
||||
header: '0.6.10 (2019-07-21)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-06-22',
|
||||
header: '0.6.9 (2019-06-23)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправлен баг - падение сервера при распаковке битых архивов книг</li>
|
||||
<li>исправлен баг - не распознавались некоторые книги формата fb2 в кодировке utf8</li>
|
||||
<li>добавлены новые варианты анимации перелистывания</li>
|
||||
<li>на страницу загрузки добавлен блок "Поделиться"</li>
|
||||
<li>улучшены прогрессбары</li>
|
||||
<li>исправления недочетов, небольшие оптимизации</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-06-05',
|
||||
header: '0.6.7 (2019-05-30)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлен диалог "Что нового"</li>
|
||||
<li>в справку добавлена история версий проекта</li>
|
||||
<li>добавлена возможность настройки отображаемых кнопок на панели управления</li>
|
||||
<li>некоторые кнопки на панели управления были скрыты по умолчанию</li>
|
||||
<li>на страницу загрузки добавлена возможность загрузки книги из буфера обмена</li>
|
||||
<li>добавлен GET-параметр вида "/reader?__refresh=1&url=..." для принудительного обновления загружаемого текста</li>
|
||||
<li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li>
|
||||
<li>исправления багов и недочетов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-03-29',
|
||||
header: '0.6.6 (2019-03-29)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li>
|
||||
<li>оптимизации процесса синхронизации, внутренние переделки</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-03-24',
|
||||
header: '0.6.4 (2019-03-24)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов, оптимизации</li>
|
||||
<li>добавлена возможность синхронизации данных между устройствами</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-03-04',
|
||||
header: '0.5.4 (2019-03-04)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлена поддержка форматов pdf, epub, mobi</li>
|
||||
<li>(0.5.2) добавлена поддержка форматов rtf, doc, docx</li>
|
||||
<li>(0.4.2) фильтр для СИ больше не вырезает изображения</li>
|
||||
<li>(0.4.0) добавлено отображение картинок в fb2</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-02-17',
|
||||
header: '0.3.0 (2019-02-17)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>поправки багов</li>
|
||||
<li>улучшено распознавание текста</li>
|
||||
<li>изменена верстка страницы - убрано позиционирование каждого слова</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-02-14',
|
||||
header: '0.1.7 (2019-02-14)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>увеличены верхние границы отступов и др.размеров</li>
|
||||
<li>добавлена настройка для удаления/вставки пустых параграфов</li>
|
||||
<li>добавлена настройка включения/отключения управления кликом</li>
|
||||
<li>добавлена возможность сброса настроек</li>
|
||||
<li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-02-12',
|
||||
header: '0.1.0 (2019-02-12)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>первый деплой проекта, длительность разработки - 2 месяца</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
]
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Settings в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Sources в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
64
client/components/share/Dialog.vue
Normal file
64
client/components/share/Dialog.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<q-dialog v-model="active" no-route-dismiss>
|
||||
<div class="column bg-white no-wrap">
|
||||
<div class="header row">
|
||||
<div class="caption col row items-center q-ml-md">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col q-mx-md">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<div class="row justify-end q-pa-md">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
const DialogProps = Vue.extend({
|
||||
props: {
|
||||
value: Boolean,
|
||||
}
|
||||
});
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Dialog extends DialogProps {
|
||||
get active() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
set active(value) {
|
||||
this.$emit('input', value);
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.caption {
|
||||
font-size: 110%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
width: 50px;
|
||||
}
|
||||
</style>
|
||||
59
client/components/share/Notify.vue
Normal file
59
client/components/share/Notify.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="hidden"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Notify extends Vue {
|
||||
notify(opts) {
|
||||
let {
|
||||
caption = null,
|
||||
captionColor = 'black',
|
||||
color = 'positive',
|
||||
icon = '',
|
||||
iconColor = 'white',
|
||||
message = '',
|
||||
messageColor = 'black',
|
||||
position = 'top-right',
|
||||
} = opts;
|
||||
|
||||
caption = (caption ? `<div style="font-size: 120%; color: ${captionColor}"><b>${caption}</b></div><br>` : '');
|
||||
return this.$q.notify({
|
||||
position,
|
||||
color,
|
||||
textColor: iconColor,
|
||||
icon,
|
||||
actions: [{icon: 'la la-times notify-button-icon', color: 'black'}],
|
||||
html: true,
|
||||
|
||||
message:
|
||||
`<div style="max-width: 350px;">
|
||||
${caption}
|
||||
<div style="color: ${messageColor}; overflow-wrap: break-word; word-wrap: break-word;">${message}</div>
|
||||
</div>`
|
||||
});
|
||||
}
|
||||
|
||||
success(message, caption, options) {
|
||||
this.notify(Object.assign({color: 'positive', icon: 'la la-check-circle', message, caption}, options));
|
||||
}
|
||||
|
||||
warning(message, caption, options) {
|
||||
this.notify(Object.assign({color: 'warning', icon: 'la la-exclamation-circle', message, caption}, options));
|
||||
}
|
||||
|
||||
error(message, caption, options) {
|
||||
this.notify(Object.assign({color: 'negative', icon: 'la la-exclamation-circle', messageColor: 'yellow', captionColor: 'white', message, caption}, options));
|
||||
}
|
||||
|
||||
info(message, caption, options) {
|
||||
this.notify(Object.assign({color: 'info', icon: 'la la-bell', message, caption}, options));
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
185
client/components/share/NumInput.vue
Normal file
185
client/components/share/NumInput.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<q-input outlined dense
|
||||
v-model="filteredValue"
|
||||
input-style="text-align: center"
|
||||
class="no-mp"
|
||||
:class="(error ? 'error' : '')"
|
||||
:disable="disable"
|
||||
>
|
||||
<slot></slot>
|
||||
<template v-slot:prepend>
|
||||
<q-icon :class="(validate(value - step) ? '' : 'disable')"
|
||||
name="la la-minus-circle"
|
||||
class="button"
|
||||
v-ripple="validate(value - step)"
|
||||
@click="minus"
|
||||
@mousedown.prevent.stop="onMouseDown($event, 'minus')"
|
||||
@mouseup.prevent.stop="onMouseUp"
|
||||
@mouseout.prevent.stop="onMouseUp"
|
||||
@touchstart.stop="onTouchStart($event, 'minus')"
|
||||
@touchend.stop="onTouchEnd"
|
||||
@touchcancel.prevent.stop="onTouchEnd"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<q-icon :class="(validate(value + step) ? '' : 'disable')"
|
||||
name="la la-plus-circle"
|
||||
class="button"
|
||||
v-ripple="validate(value + step)"
|
||||
@click="plus"
|
||||
@mousedown.prevent.stop="onMouseDown($event, 'plus')"
|
||||
@mouseup.prevent.stop="onMouseUp"
|
||||
@mouseout.prevent.stop="onMouseUp"
|
||||
@touchstart.stop="onTouchStart($event, 'plus')"
|
||||
@touchend.stop="onTouchEnd"
|
||||
@touchcancel.prevent.stop="onTouchEnd"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
import * as utils from '../../share/utils';
|
||||
|
||||
const NumInputProps = Vue.extend({
|
||||
props: {
|
||||
value: Number,
|
||||
min: { type: Number, default: -Number.MAX_VALUE },
|
||||
max: { type: Number, default: Number.MAX_VALUE },
|
||||
step: { type: Number, default: 1 },
|
||||
digits: { type: Number, default: 0 },
|
||||
disable: Boolean
|
||||
}
|
||||
});
|
||||
|
||||
export default @Component({
|
||||
watch: {
|
||||
filteredValue: function(newValue) {
|
||||
if (this.validate(newValue)) {
|
||||
this.error = false;
|
||||
this.$emit('input', this.string2number(newValue));
|
||||
} else {
|
||||
this.error = true;
|
||||
}
|
||||
},
|
||||
value: function(newValue) {
|
||||
this.filteredValue = newValue;
|
||||
},
|
||||
}
|
||||
})
|
||||
class NumInput extends NumInputProps {
|
||||
filteredValue = 0;
|
||||
error = false;
|
||||
|
||||
created() {
|
||||
this.filteredValue = this.value;
|
||||
}
|
||||
|
||||
string2number(value) {
|
||||
return Number.parseFloat(Number.parseFloat(value).toFixed(this.digits));
|
||||
}
|
||||
|
||||
validate(value) {
|
||||
let n = this.string2number(value);
|
||||
if (isNaN(n))
|
||||
return false;
|
||||
if (n < this.min)
|
||||
return false;
|
||||
if (n > this.max)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
plus() {
|
||||
const newValue = this.value + this.step;
|
||||
if (this.validate(newValue))
|
||||
this.filteredValue = newValue;
|
||||
}
|
||||
|
||||
minus() {
|
||||
const newValue = this.value - this.step;
|
||||
if (this.validate(newValue))
|
||||
this.filteredValue = newValue;
|
||||
}
|
||||
|
||||
onMouseDown(event, way) {
|
||||
this.startClickRepeat = true;
|
||||
this.clickRepeat = false;
|
||||
|
||||
if (event.button == 0) {
|
||||
(async() => {
|
||||
await utils.sleep(300);
|
||||
if (this.startClickRepeat) {
|
||||
this.clickRepeat = true;
|
||||
while (this.clickRepeat) {
|
||||
if (way == 'plus') {
|
||||
this.plus();
|
||||
} else {
|
||||
this.minus();
|
||||
}
|
||||
await utils.sleep(50);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
onMouseUp() {
|
||||
if (this.inTouch)
|
||||
return;
|
||||
this.startClickRepeat = false;
|
||||
this.clickRepeat = false;
|
||||
}
|
||||
|
||||
onTouchStart(event, way) {
|
||||
if (!this.$isMobileDevice)
|
||||
return;
|
||||
if (event.touches.length == 1) {
|
||||
this.inTouch = true;
|
||||
this.onMouseDown({button: 0}, way);
|
||||
}
|
||||
}
|
||||
|
||||
onTouchEnd() {
|
||||
if (!this.$isMobileDevice)
|
||||
return;
|
||||
this.inTouch = false;
|
||||
this.onMouseUp();
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.no-mp {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 130%;
|
||||
border-radius: 20px;
|
||||
color: #bbb;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
color: #616161;
|
||||
background-color: #efebe9;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #ffabab;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.disable, .disable:hover {
|
||||
cursor: not-allowed;
|
||||
color: #bbb;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
342
client/components/share/StdDialog.vue
Normal file
342
client/components/share/StdDialog.vue
Normal file
@@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide" no-route-dismiss>
|
||||
<slot></slot>
|
||||
|
||||
<!--------------------------------------------------->
|
||||
<div v-show="type == 'alert'" class="bg-white no-wrap">
|
||||
<div class="header row">
|
||||
<div class="caption col row items-center q-ml-md">
|
||||
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
|
||||
<div v-html="caption"></div>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mx-md">
|
||||
<div v-html="message"></div>
|
||||
</div>
|
||||
|
||||
<div class="buttons row justify-end q-pa-md">
|
||||
<q-btn class="q-px-md" dense no-caps @click="okClick">OK</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--------------------------------------------------->
|
||||
<div v-show="type == 'confirm'" class="bg-white no-wrap">
|
||||
<div class="header row">
|
||||
<div class="caption col row items-center q-ml-md">
|
||||
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
|
||||
<div v-html="caption"></div>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mx-md">
|
||||
<div v-html="message"></div>
|
||||
</div>
|
||||
|
||||
<div class="buttons row justify-end q-pa-md">
|
||||
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">OK</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--------------------------------------------------->
|
||||
<div v-show="type == 'prompt'" class="bg-white no-wrap">
|
||||
<div class="header row">
|
||||
<div class="caption col row items-center q-ml-md">
|
||||
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
|
||||
<div v-html="caption"></div>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mx-md">
|
||||
<div v-html="message"></div>
|
||||
<q-input ref="input" class="q-mt-xs" outlined dense v-model="inputValue"/>
|
||||
<div class="error"><span v-show="error != ''">{{ error }}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="buttons row justify-end q-pa-md">
|
||||
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">OK</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--------------------------------------------------->
|
||||
<div v-show="type == 'hotKey'" class="bg-white no-wrap">
|
||||
<div class="header row">
|
||||
<div class="caption col row items-center q-ml-md">
|
||||
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
|
||||
<div v-html="caption"></div>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mx-md">
|
||||
<div v-html="message"></div>
|
||||
<div class="q-my-md text-center">
|
||||
<div v-show="hotKeyCode == ''" class="text-grey-5">Нет</div>
|
||||
<div>{{ hotKeyCode }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons row justify-end q-pa-md">
|
||||
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick" :disabled="hotKeyCode == ''">OK</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
import * as utils from '../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
watch: {
|
||||
inputValue: function(newValue) {
|
||||
this.validate(newValue);
|
||||
},
|
||||
}
|
||||
})
|
||||
class StdDialog extends Vue {
|
||||
caption = '';
|
||||
message = '';
|
||||
active = false;
|
||||
type = '';
|
||||
inputValue = '';
|
||||
error = '';
|
||||
iconColor = '';
|
||||
iconName = '';
|
||||
hotKeyCode = '';
|
||||
|
||||
created() {
|
||||
if (this.$root.addKeyHook) {
|
||||
this.$root.addKeyHook(this.keyHook);
|
||||
}
|
||||
}
|
||||
|
||||
init(message, caption, opts) {
|
||||
this.caption = caption;
|
||||
this.message = message;
|
||||
|
||||
this.ok = false;
|
||||
this.type = '';
|
||||
this.inputValidator = null;
|
||||
this.inputValue = '';
|
||||
this.error = '';
|
||||
this.showed = false;
|
||||
|
||||
this.iconColor = 'text-warning';
|
||||
if (opts && opts.color) {
|
||||
this.iconColor = `text-${opts.color}`;
|
||||
}
|
||||
|
||||
this.iconName = 'las la-exclamation-circle';
|
||||
if (opts && opts.iconName) {
|
||||
this.iconName = opts.iconName;
|
||||
}
|
||||
|
||||
this.hotKeyCode = '';
|
||||
if (opts && opts.hotKeyCode) {
|
||||
this.hotKeyCode = opts.hotKeyCode;
|
||||
}
|
||||
}
|
||||
|
||||
onHide() {
|
||||
if (this.hideTrigger) {
|
||||
this.hideTrigger();
|
||||
this.hideTrigger = null;
|
||||
}
|
||||
this.showed = false;
|
||||
}
|
||||
|
||||
onShow() {
|
||||
if (this.type == 'prompt') {
|
||||
this.enableValidator = true;
|
||||
if (this.inputValue)
|
||||
this.validate(this.inputValue);
|
||||
this.$refs.input.focus();
|
||||
}
|
||||
this.showed = true;
|
||||
}
|
||||
|
||||
validate(value) {
|
||||
if (!this.enableValidator)
|
||||
return false;
|
||||
|
||||
if (this.inputValidator) {
|
||||
const result = this.inputValidator(value);
|
||||
if (result !== true) {
|
||||
this.error = result;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.error = '';
|
||||
return true;
|
||||
}
|
||||
|
||||
okClick() {
|
||||
if (this.type == 'prompt' && !this.validate(this.inputValue)) {
|
||||
this.$refs.dialog.shake();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.type == 'hotKey' && this.hotKeyCode == '') {
|
||||
this.$refs.dialog.shake();
|
||||
return;
|
||||
}
|
||||
|
||||
this.ok = true;
|
||||
this.$refs.dialog.hide();
|
||||
}
|
||||
|
||||
alert(message, caption, opts) {
|
||||
return new Promise((resolve) => {
|
||||
this.init(message, caption, opts);
|
||||
|
||||
this.hideTrigger = () => {
|
||||
if (this.ok) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.type = 'alert';
|
||||
this.active = true;
|
||||
});
|
||||
}
|
||||
|
||||
confirm(message, caption, opts) {
|
||||
return new Promise((resolve) => {
|
||||
this.init(message, caption, opts);
|
||||
|
||||
this.hideTrigger = () => {
|
||||
if (this.ok) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.type = 'confirm';
|
||||
this.active = true;
|
||||
});
|
||||
}
|
||||
|
||||
prompt(message, caption, opts) {
|
||||
return new Promise((resolve) => {
|
||||
this.enableValidator = false;
|
||||
this.init(message, caption, opts);
|
||||
|
||||
this.hideTrigger = () => {
|
||||
if (this.ok) {
|
||||
resolve({value: this.inputValue});
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.type = 'prompt';
|
||||
if (opts) {
|
||||
this.inputValidator = opts.inputValidator || null;
|
||||
this.inputValue = opts.inputValue || '';
|
||||
}
|
||||
this.active = true;
|
||||
});
|
||||
}
|
||||
|
||||
getHotKey(message, caption, opts) {
|
||||
return new Promise((resolve) => {
|
||||
this.init(message, caption, opts);
|
||||
|
||||
this.hideTrigger = () => {
|
||||
if (this.ok) {
|
||||
resolve(this.hotKeyCode);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.type = 'hotKey';
|
||||
this.active = true;
|
||||
});
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (this.active && this.showed) {
|
||||
let handled = false;
|
||||
if (this.type == 'hotKey') {
|
||||
if (event.type == 'keydown') {
|
||||
this.hotKeyCode = utils.keyEventToCode(event);
|
||||
handled = true;
|
||||
}
|
||||
} else {
|
||||
if (event.key == 'Enter') {
|
||||
this.okClick();
|
||||
handled = true;
|
||||
}
|
||||
|
||||
if (event.key == 'Escape') {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.dialog.hide();
|
||||
});
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.caption {
|
||||
font-size: 110%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.error {
|
||||
height: 20px;
|
||||
font-size: 80%;
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,17 @@
|
||||
<template>
|
||||
<div class="window">
|
||||
<div class="header">
|
||||
<span class="header-text"><slot name="header"></slot></span>
|
||||
<span class="close-button" @click="close"><i class="el-icon-close"></i></span>
|
||||
<div ref="main" class="main xyfit absolute" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
|
||||
<div ref="windowBox" class="xyfit absolute flex no-wrap" @click.stop>
|
||||
<div ref="window" class="window flexfit column no-wrap">
|
||||
<div ref="header" class="header row justify-end" @mousedown.prevent.stop="onMouseDown"
|
||||
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove">
|
||||
<span class="header-text col"><slot name="header"></slot></span>
|
||||
<slot name="buttons"></slot>
|
||||
<span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px"/></span>
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -14,21 +21,124 @@ import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
export default @Component({
|
||||
props: {
|
||||
height: { type: String, default: '100%' },
|
||||
width: { type: String, default: '100%' },
|
||||
maxWidth: { type: String, default: '' },
|
||||
topShift: { type: Number, default: 0 },
|
||||
margin: '',
|
||||
}
|
||||
})
|
||||
class Window extends Vue {
|
||||
close() {
|
||||
this.$emit('close');
|
||||
init() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.main.style.top = 0;
|
||||
this.$refs.main.style.left = 0;
|
||||
|
||||
this.$refs.windowBox.style.height = this.height;
|
||||
this.$refs.windowBox.style.width = this.width;
|
||||
if (this.maxWidth)
|
||||
this.$refs.windowBox.style.maxWidth = this.maxWidth;
|
||||
|
||||
const left = (this.$refs.main.offsetWidth - this.$refs.windowBox.offsetWidth)/2;
|
||||
const top = (this.$refs.main.offsetHeight - this.$refs.windowBox.offsetHeight)/2 + this.topShift;
|
||||
this.$refs.windowBox.style.left = (left > 0 ? left : 0) + 'px';
|
||||
this.$refs.windowBox.style.top = (top > 0 ? top : 0) + 'px';
|
||||
|
||||
if (this.margin)
|
||||
this.$refs.window.style.margin = this.margin;
|
||||
});
|
||||
}
|
||||
|
||||
onMouseDown(event) {
|
||||
if (this.$isMobileDevice)
|
||||
return;
|
||||
if (event.button == 0) {
|
||||
this.$refs.header.style.cursor = 'move';
|
||||
this.startX = event.screenX;
|
||||
this.startY = event.screenY;
|
||||
this.moving = true;
|
||||
}
|
||||
}
|
||||
|
||||
onMouseUp(event) {
|
||||
if (event.button == 0) {
|
||||
this.$refs.header.style.cursor = 'default';
|
||||
this.moving = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMouseMove(event) {
|
||||
if (this.moving) {
|
||||
const deltaX = event.screenX - this.startX;
|
||||
const deltaY = event.screenY - this.startY;
|
||||
this.startX = event.screenX;
|
||||
this.startY = event.screenY;
|
||||
|
||||
this.$refs.windowBox.style.left = (this.$refs.windowBox.offsetLeft + deltaX) + 'px';
|
||||
this.$refs.windowBox.style.top = (this.$refs.windowBox.offsetTop + deltaY) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
onTouchStart(event) {
|
||||
if (!this.$isMobileDevice)
|
||||
return;
|
||||
if (event.touches.length == 1) {
|
||||
const touch = event.touches[0];
|
||||
this.$refs.header.style.cursor = 'move';
|
||||
this.startX = touch.screenX;
|
||||
this.startY = touch.screenY;
|
||||
this.moving = true;
|
||||
}
|
||||
}
|
||||
|
||||
onTouchMove(event) {
|
||||
if (!this.$isMobileDevice)
|
||||
return;
|
||||
if (event.touches.length == 1 && this.moving) {
|
||||
const touch = event.touches[0];
|
||||
const deltaX = touch.screenX - this.startX;
|
||||
const deltaY = touch.screenY - this.startY;
|
||||
this.startX = touch.screenX;
|
||||
this.startY = touch.screenY;
|
||||
|
||||
this.$refs.windowBox.style.left = (this.$refs.windowBox.offsetLeft + deltaX) + 'px';
|
||||
this.$refs.windowBox.style.top = (this.$refs.windowBox.offsetTop + deltaY) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
onTouchEnd() {
|
||||
if (!this.$isMobileDevice)
|
||||
return;
|
||||
this.$refs.header.style.cursor = 'default';
|
||||
this.moving = false;
|
||||
}
|
||||
|
||||
|
||||
close() {
|
||||
if (!this.moving)
|
||||
this.$emit('close');
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.window {
|
||||
.main {
|
||||
background-color: transparent !important;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.xyfit {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flexfit {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.window {
|
||||
margin: 10px;
|
||||
background-color: #ffffff;
|
||||
border: 3px double black;
|
||||
@@ -37,25 +147,28 @@ class Window extends Vue {
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
background-color: #e5e7ea;
|
||||
background: linear-gradient(to bottom right, green, #59B04F);
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
color: yellow;
|
||||
text-shadow: 2px 1px 5px black, 2px 2px 5px black;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background-color: #69C05F;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,128 +0,0 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
/*
|
||||
import ElementUI from 'element-ui';
|
||||
import './theme/index.css';
|
||||
import locale from 'element-ui/lib/locale/lang/ru-RU';
|
||||
|
||||
Vue.use(ElementUI, { locale });
|
||||
*/
|
||||
|
||||
//------------------------------------------------------
|
||||
//import './theme/index.css';
|
||||
|
||||
import './theme/icon.css';
|
||||
import './theme/tooltip.css';
|
||||
|
||||
import ElMenu from 'element-ui/lib/menu';
|
||||
import './theme/menu.css';
|
||||
|
||||
import ElMenuItem from 'element-ui/lib/menu-item';
|
||||
import './theme/menu-item.css';
|
||||
|
||||
import ElButton from 'element-ui/lib/button';
|
||||
import './theme/button.css';
|
||||
|
||||
import ElButtonGroup from 'element-ui/lib/button-group';
|
||||
import './theme/button-group.css';
|
||||
|
||||
import ElCheckbox from 'element-ui/lib/checkbox';
|
||||
import './theme/checkbox.css';
|
||||
|
||||
import ElTabs from 'element-ui/lib/tabs';
|
||||
import './theme/tabs.css';
|
||||
|
||||
import ElTabPane from 'element-ui/lib/tab-pane';
|
||||
import './theme/tab-pane.css';
|
||||
|
||||
import ElTooltip from 'element-ui/lib/tooltip';
|
||||
import './theme/tooltip.css';
|
||||
|
||||
import ElCol from 'element-ui/lib/col';
|
||||
import './theme/col.css';
|
||||
|
||||
import ElContainer from 'element-ui/lib/container';
|
||||
import './theme/container.css';
|
||||
|
||||
import ElAside from 'element-ui/lib/aside';
|
||||
import './theme/aside.css';
|
||||
|
||||
import ElHeader from 'element-ui/lib/header';
|
||||
import './theme/header.css';
|
||||
|
||||
import ElMain from 'element-ui/lib/main';
|
||||
import './theme/main.css';
|
||||
|
||||
import ElInput from 'element-ui/lib/input';
|
||||
import './theme/input.css';
|
||||
|
||||
import ElInputNumber from 'element-ui/lib/input-number';
|
||||
import './theme/input-number.css';
|
||||
|
||||
import ElSelect from 'element-ui/lib/select';
|
||||
import './theme/select.css';
|
||||
|
||||
import ElOption from 'element-ui/lib/option';
|
||||
import './theme/option.css';
|
||||
|
||||
import ElTable from 'element-ui/lib/table';
|
||||
import './theme/table.css';
|
||||
|
||||
import ElTableColumn from 'element-ui/lib/table-column';
|
||||
import './theme/table-column.css';
|
||||
|
||||
import ElProgress from 'element-ui/lib/progress';
|
||||
import './theme/progress.css';
|
||||
|
||||
import ElSlider from 'element-ui/lib/slider';
|
||||
import './theme/slider.css';
|
||||
|
||||
import ElForm from 'element-ui/lib/form';
|
||||
import './theme/form.css';
|
||||
|
||||
import ElFormItem from 'element-ui/lib/form-item';
|
||||
import './theme/form-item.css';
|
||||
|
||||
import ElColorPicker from 'element-ui/lib/color-picker';
|
||||
import './theme/color-picker.css';
|
||||
|
||||
//import ElDialog from 'element-ui/lib/dialog';
|
||||
//import './theme/dialog.css';
|
||||
|
||||
import Notification from 'element-ui/lib/notification';
|
||||
import './theme/notification.css';
|
||||
|
||||
import Loading from 'element-ui/lib/loading';
|
||||
import './theme/loading.css';
|
||||
|
||||
import MessageBox from 'element-ui/lib/message-box';
|
||||
import './theme/message-box.css';
|
||||
|
||||
//import Message from 'element-ui/lib/message';
|
||||
//import './theme/message.css';
|
||||
|
||||
const components = {
|
||||
ElMenu, ElMenuItem, ElButton, ElButtonGroup, ElCheckbox, ElTabs, ElTabPane, ElTooltip,
|
||||
ElCol, ElContainer, ElAside, ElMain, ElHeader,
|
||||
ElInput, ElInputNumber, ElSelect, ElOption, ElTable, ElTableColumn,
|
||||
ElProgress, ElSlider, ElForm, ElFormItem,
|
||||
ElColorPicker,
|
||||
};
|
||||
|
||||
for (let name in components) {
|
||||
Vue.component(name, components[name]);
|
||||
}
|
||||
|
||||
//Vue.use(Loading.directive);
|
||||
|
||||
Vue.prototype.$loading = Loading.service;
|
||||
Vue.prototype.$msgbox = MessageBox;
|
||||
Vue.prototype.$alert = MessageBox.alert;
|
||||
Vue.prototype.$confirm = MessageBox.confirm;
|
||||
Vue.prototype.$prompt = MessageBox.prompt;
|
||||
Vue.prototype.$notify = Notification;
|
||||
//Vue.prototype.$message = Message;
|
||||
|
||||
import lang from 'element-ui/lib/locale/lang/ru-RU';
|
||||
import locale from 'element-ui/lib/locale';
|
||||
locale.use(lang);
|
||||
@@ -1,11 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="description" content="браузерная онлайн-читалка книг из интернета и библиотека">
|
||||
<meta name="keywords" content="библиотека,онлайн,читалка,книги,читать,браузер,интернет">
|
||||
<title></title>
|
||||
<meta name="description" content="Браузерная онлайн-читалка книг. Поддерживаются форматы: fb2, html, txt, rtf, doc, docx, pdf, epub, mobi.">
|
||||
<meta name="keywords" content="онлайн,читалка,fb2,книги,читать,браузер,интернет">
|
||||
<script src="/sw-register.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import Vue from 'vue';
|
||||
import App from './components/App.vue';
|
||||
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import './element';
|
||||
import './quasar';
|
||||
|
||||
import vueSanitize from 'vue-sanitize';
|
||||
Vue.use(vueSanitize);
|
||||
|
||||
import App from './components/App.vue';
|
||||
//Vue.config.productionTip = false;
|
||||
Vue.prototype.$isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
|
||||
93
client/quasar.js
Normal file
93
client/quasar.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
import 'quasar/dist/quasar.css';
|
||||
import Quasar from 'quasar/src/vue-plugin.js'
|
||||
|
||||
//config
|
||||
const config = {};
|
||||
|
||||
//components
|
||||
//import {QLayout} from 'quasar/src/components/layout';
|
||||
//import {QPageContainer, QPage} from 'quasar/src/components/page';
|
||||
//import {QDrawer} from 'quasar/src/components/drawer';
|
||||
|
||||
import {QCircularProgress} from 'quasar/src/components/circular-progress';
|
||||
import {QInput} from 'quasar/src/components/input';
|
||||
import {QBtn} from 'quasar/src/components/btn';
|
||||
import {QBtnGroup} from 'quasar/src/components/btn-group';
|
||||
import {QBtnToggle} from 'quasar/src/components/btn-toggle';
|
||||
import {QIcon} from 'quasar/src/components/icon';
|
||||
import {QSlider} from 'quasar/src/components/slider';
|
||||
import {QTabs, QTab} from 'quasar/src/components/tabs';
|
||||
//import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels';
|
||||
import {QSeparator} from 'quasar/src/components/separator';
|
||||
//import {QList, QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
|
||||
import {QTooltip} from 'quasar/src/components/tooltip';
|
||||
import {QSpinner} from 'quasar/src/components/spinner';
|
||||
import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table';
|
||||
import {QCheckbox} from 'quasar/src/components/checkbox';
|
||||
import {QSelect} from 'quasar/src/components/select';
|
||||
import {QColor} from 'quasar/src/components/color';
|
||||
import {QPopupProxy} from 'quasar/src/components/popup-proxy';
|
||||
import {QDialog} from 'quasar/src/components/dialog';
|
||||
import {QChip} from 'quasar/src/components/chip';
|
||||
import {QTree} from 'quasar/src/components/tree';
|
||||
//import {QExpansionItem} from 'quasar/src/components/expansion-item';
|
||||
|
||||
const components = {
|
||||
//QLayout,
|
||||
//QPageContainer, QPage,
|
||||
//QDrawer,
|
||||
|
||||
QCircularProgress,
|
||||
QInput,
|
||||
QBtn,
|
||||
QBtnGroup,
|
||||
QBtnToggle,
|
||||
QIcon,
|
||||
QSlider,
|
||||
QTabs, QTab,
|
||||
//QTabPanels, QTabPanel,
|
||||
QSeparator,
|
||||
//QList, QItem, QItemSection, QItemLabel,
|
||||
QTooltip,
|
||||
QSpinner,
|
||||
QTable, QTh, QTr, QTd,
|
||||
QCheckbox,
|
||||
QSelect,
|
||||
QColor,
|
||||
QPopupProxy,
|
||||
QDialog,
|
||||
QChip,
|
||||
QTree,
|
||||
//QExpansionItem,
|
||||
};
|
||||
|
||||
//directives
|
||||
import Ripple from 'quasar/src/directives/Ripple';
|
||||
import ClosePopup from 'quasar/src/directives/ClosePopup';
|
||||
|
||||
const directives = {Ripple, ClosePopup};
|
||||
|
||||
//plugins
|
||||
import AppFullscreen from 'quasar/src/plugins/AppFullscreen';
|
||||
import Notify from 'quasar/src/plugins/Notify';
|
||||
|
||||
const plugins = {
|
||||
AppFullscreen,
|
||||
Notify,
|
||||
};
|
||||
|
||||
//use
|
||||
Vue.use(Quasar, { config, components, directives, plugins });
|
||||
|
||||
//icons
|
||||
//import '@quasar/extras/material-icons/material-icons.css';
|
||||
//import '@quasar/extras/material-icons-outlined/material-icons-outlined.css';
|
||||
//import '@quasar/extras/fontawesome-v5/fontawesome-v5.css';
|
||||
|
||||
import '@quasar/extras/line-awesome/line-awesome.css';
|
||||
|
||||
//import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js'
|
||||
import lineAwesome from 'quasar/icon-set/line-awesome.js'
|
||||
Quasar.iconSet.set(lineAwesome);
|
||||
@@ -2,16 +2,17 @@ import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import _ from 'lodash';
|
||||
|
||||
import App from './components/App.vue';
|
||||
|
||||
const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
|
||||
const Search = () => import('./components/CardIndex/Search/Search.vue');
|
||||
const Card = () => import('./components/CardIndex/Card/Card.vue');
|
||||
const Book = () => import('./components/CardIndex/Book/Book.vue');
|
||||
const History = () => import('./components/CardIndex/History/History.vue');
|
||||
|
||||
//немедленная загрузка
|
||||
//import Reader from './components/Reader/Reader.vue';
|
||||
const Reader = () => import('./components/Reader/Reader.vue');
|
||||
//const Forum = () => import('./components/Forum/Forum.vue');
|
||||
const ExternalLibs = () => import('./components/ExternalLibs/ExternalLibs.vue');
|
||||
|
||||
const Income = () => import('./components/Income/Income.vue');
|
||||
const Sources = () => import('./components/Sources/Sources.vue');
|
||||
const Settings = () => import('./components/Settings/Settings.vue');
|
||||
@@ -20,20 +21,22 @@ const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
|
||||
|
||||
const myRoutes = [
|
||||
['/', null, null, '/cardindex'],
|
||||
['/cardindex', CardIndex ],
|
||||
['/cardindex~search', Search ],
|
||||
['/cardindex~card', Card ],
|
||||
['/cardindex~card/:authorId', Card ],
|
||||
['/cardindex~book', Book ],
|
||||
['/cardindex~book/:bookId', Book ],
|
||||
['/cardindex~history', History ],
|
||||
['/cardindex', CardIndex],
|
||||
['/cardindex~search', Search],
|
||||
['/cardindex~card', Card],
|
||||
['/cardindex~card/:authorId', Card],
|
||||
['/cardindex~book', Book],
|
||||
['/cardindex~book/:bookId', Book],
|
||||
['/cardindex~history', History],
|
||||
|
||||
['/reader', Reader ],
|
||||
['/income', Income ],
|
||||
['/sources', Sources ],
|
||||
['/settings', Settings ],
|
||||
['/help', Help ],
|
||||
['*', null, null, '/cardindex' ],
|
||||
['/reader', Reader],
|
||||
['/external-libs', ExternalLibs],
|
||||
['/income', Income],
|
||||
['/sources', Sources],
|
||||
['/settings', Settings],
|
||||
['/help', Help],
|
||||
['/404', NotFound404],
|
||||
['*', null, null, '/cardindex'],
|
||||
];
|
||||
|
||||
let routes = {};
|
||||
|
||||
@@ -2,13 +2,12 @@ import _ from 'lodash';
|
||||
import baseX from 'base-x';
|
||||
import PAKO from 'pako';
|
||||
import {Buffer} from 'safe-buffer';
|
||||
import sjclWrapper from './sjclWrapper';
|
||||
|
||||
export const pako = PAKO;
|
||||
|
||||
const BASE58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||
const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
const bs58 = baseX(BASE58);
|
||||
const bs64 = baseX(BASE64);
|
||||
|
||||
export function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
@@ -40,6 +39,10 @@ export function formatDate(d, format) {
|
||||
case 'normal':
|
||||
return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()} ` +
|
||||
`${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
|
||||
case 'coDate':
|
||||
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
|
||||
case 'noDate':
|
||||
return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()}`;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -86,51 +89,121 @@ export function fromBase58(data) {
|
||||
return bs58.decode(data);
|
||||
}
|
||||
|
||||
//base-x слишком тормозит, используем sjcl
|
||||
export function toBase64(data) {
|
||||
return bs64.encode(Buffer.from(data));
|
||||
return sjclWrapper.codec.base64.fromBits(
|
||||
sjclWrapper.codec.bytes.toBits(Buffer.from(data))
|
||||
);
|
||||
}
|
||||
|
||||
//base-x слишком тормозит, используем sjcl
|
||||
export function fromBase64(data) {
|
||||
return bs64.decode(data);
|
||||
return Buffer.from(sjclWrapper.codec.bytes.fromBits(
|
||||
sjclWrapper.codec.base64.toBits(data)
|
||||
));
|
||||
}
|
||||
|
||||
export function getObjDiff(oldObj, newObj) {
|
||||
const result = {__isDiff: true, change: {}, add: {}, del: []};
|
||||
export function getObjDiff(oldObj, newObj, opts = {}) {
|
||||
const {
|
||||
exclude = [],
|
||||
excludeAdd = [],
|
||||
excludeDel = [],
|
||||
} = opts;
|
||||
|
||||
for (const key of Object.keys(oldObj)) {
|
||||
if (newObj.hasOwnProperty(key)) {
|
||||
if (!_.isEqual(oldObj[key], newObj[key])) {
|
||||
if (_.isObject(oldObj[key]) && _.isObject(newObj[key])) {
|
||||
result.change[key] = getObjDiff(oldObj[key], newObj[key]);
|
||||
} else {
|
||||
result.change[key] = _.cloneDeep(newObj[key]);
|
||||
const ex = new Set(exclude);
|
||||
const exAdd = new Set(excludeAdd);
|
||||
const exDel = new Set(excludeDel);
|
||||
|
||||
const makeObjDiff = (oldObj, newObj, keyPath) => {
|
||||
const result = {__isDiff: true, change: {}, add: {}, del: []};
|
||||
|
||||
keyPath = `${keyPath}${keyPath ? '/' : ''}`;
|
||||
|
||||
for (const key of Object.keys(oldObj)) {
|
||||
const kp = `${keyPath}${key}`;
|
||||
|
||||
if (newObj.hasOwnProperty(key)) {
|
||||
if (ex.has(kp))
|
||||
continue;
|
||||
|
||||
if (!_.isEqual(oldObj[key], newObj[key])) {
|
||||
if (_.isObject(oldObj[key]) && _.isObject(newObj[key])) {
|
||||
result.change[key] = makeObjDiff(oldObj[key], newObj[key], kp);
|
||||
} else {
|
||||
result.change[key] = _.cloneDeep(newObj[key]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (exDel.has(kp))
|
||||
continue;
|
||||
result.del.push(key);
|
||||
}
|
||||
} else {
|
||||
result.del.push(key);
|
||||
}
|
||||
|
||||
for (const key of Object.keys(newObj)) {
|
||||
const kp = `${keyPath}${key}`;
|
||||
if (exAdd.has(kp))
|
||||
continue;
|
||||
|
||||
if (!oldObj.hasOwnProperty(key)) {
|
||||
result.add[key] = _.cloneDeep(newObj[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(newObj)) {
|
||||
if (!oldObj.hasOwnProperty(key)) {
|
||||
result.add[key] = _.cloneDeep(newObj[key]);
|
||||
}
|
||||
}
|
||||
return makeObjDiff(oldObj, newObj, '');
|
||||
}
|
||||
|
||||
return result;
|
||||
export function isObjDiff(diff) {
|
||||
return (_.isObject(diff) && diff.__isDiff && diff.change && diff.add && diff.del);
|
||||
}
|
||||
|
||||
export function isEmptyObjDiff(diff) {
|
||||
return (!_.isObject(diff) || !diff.__isDiff ||
|
||||
(!Object.keys(diff.change).length &&
|
||||
!Object.keys(diff.add).length &&
|
||||
!diff.del.length
|
||||
return (!isObjDiff(diff) ||
|
||||
!(Object.keys(diff.change).length ||
|
||||
Object.keys(diff.add).length ||
|
||||
diff.del.length
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function applyObjDiff(obj, diff, isAddChanged) {
|
||||
const result = _.cloneDeep(obj);
|
||||
export function isEmptyObjDiffDeep(diff, opts = {}) {
|
||||
if (!isObjDiff(diff))
|
||||
return true;
|
||||
|
||||
const {
|
||||
isApplyChange = true,
|
||||
isApplyAdd = true,
|
||||
isApplyDel = true,
|
||||
} = opts;
|
||||
|
||||
let notEmptyDeep = false;
|
||||
const change = diff.change;
|
||||
for (const key of Object.keys(change)) {
|
||||
if (_.isObject(change[key]))
|
||||
notEmptyDeep |= !isEmptyObjDiffDeep(change[key], opts);
|
||||
else if (isApplyChange)
|
||||
notEmptyDeep = true;
|
||||
}
|
||||
|
||||
return !(
|
||||
notEmptyDeep ||
|
||||
(isApplyAdd && Object.keys(diff.add).length) ||
|
||||
(isApplyDel && diff.del.length)
|
||||
);
|
||||
}
|
||||
|
||||
export function applyObjDiff(obj, diff, opts = {}) {
|
||||
const {
|
||||
isAddChanged = false,
|
||||
isApplyChange = true,
|
||||
isApplyAdd = true,
|
||||
isApplyDel = true,
|
||||
} = opts;
|
||||
|
||||
let result = _.cloneDeep(obj);
|
||||
if (!diff.__isDiff)
|
||||
return result;
|
||||
|
||||
@@ -138,22 +211,148 @@ export function applyObjDiff(obj, diff, isAddChanged) {
|
||||
for (const key of Object.keys(change)) {
|
||||
if (result.hasOwnProperty(key)) {
|
||||
if (_.isObject(change[key])) {
|
||||
result[key] = applyObjDiff(result[key], change[key], isAddChanged);
|
||||
result[key] = applyObjDiff(result[key], change[key], opts);
|
||||
} else {
|
||||
result[key] = _.cloneDeep(change[key]);
|
||||
if (isApplyChange)
|
||||
result[key] = _.cloneDeep(change[key]);
|
||||
}
|
||||
} else if (isAddChanged) {
|
||||
result[key] = _.cloneDeep(change[key]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(diff.add)) {
|
||||
result[key] = _.cloneDeep(diff.add[key]);
|
||||
if (isApplyAdd) {
|
||||
for (const key of Object.keys(diff.add)) {
|
||||
result[key] = _.cloneDeep(diff.add[key]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of diff.del) {
|
||||
delete result[key];
|
||||
if (isApplyDel && diff.del.length) {
|
||||
for (const key of diff.del) {
|
||||
delete result[key];
|
||||
}
|
||||
if (_.isArray(result))
|
||||
result = result.filter(v => v);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseQuery(str) {
|
||||
if (typeof str != 'string' || str.length == 0)
|
||||
return {};
|
||||
let s = str.split('&');
|
||||
let s_length = s.length;
|
||||
let bit, query = {}, first, second;
|
||||
|
||||
for (let i = 0; i < s_length; i++) {
|
||||
bit = s[i].split('=');
|
||||
first = decodeURIComponent(bit[0]);
|
||||
if (first.length == 0)
|
||||
continue;
|
||||
second = decodeURIComponent(bit[1]);
|
||||
if (typeof query[first] == 'undefined')
|
||||
query[first] = second;
|
||||
else
|
||||
if (query[first] instanceof Array)
|
||||
query[first].push(second);
|
||||
else
|
||||
query[first] = [query[first], second];
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
export function escapeXml(str) {
|
||||
return str.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
;
|
||||
}
|
||||
|
||||
export function keyEventToCode(event) {
|
||||
let result = [];
|
||||
let code = event.code;
|
||||
|
||||
const modCode = code.substring(0, 3);
|
||||
if (event.metaKey && modCode != 'Met')
|
||||
result.push('Meta');
|
||||
if (event.ctrlKey && modCode != 'Con')
|
||||
result.push('Ctrl');
|
||||
if (event.shiftKey && modCode != 'Shi')
|
||||
result.push('Shift');
|
||||
if (event.altKey && modCode != 'Alt')
|
||||
result.push('Alt');
|
||||
|
||||
if (modCode == 'Dig') {
|
||||
code = code.substring(5, 6);
|
||||
} else if (modCode == 'Key') {
|
||||
code = code.substring(3, 4);
|
||||
}
|
||||
result.push(code);
|
||||
|
||||
return result.join('+');
|
||||
}
|
||||
|
||||
export function userHotKeysObjectSwap(userHotKeys) {
|
||||
let result = {};
|
||||
for (const [name, codes] of Object.entries(userHotKeys)) {
|
||||
for (const code of codes) {
|
||||
result[code] = name;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function removeHtmlTags(s) {
|
||||
return s.replace(/(<([^>]+)>)/ig, '');
|
||||
}
|
||||
|
||||
export function makeValidFilename(filename, repl = '_') {
|
||||
let f = filename.replace(/[\x00\\/:*"<>|]/g, repl); // eslint-disable-line no-control-regex
|
||||
f = f.trim();
|
||||
while (f.length && (f[f.length - 1] == '.' || f[f.length - 1] == '_')) {
|
||||
f = f.substring(0, f.length - 1);
|
||||
}
|
||||
|
||||
if (f)
|
||||
return f;
|
||||
else
|
||||
throw new Error('Invalid filename');
|
||||
}
|
||||
|
||||
export function getBookTitle(fb2) {
|
||||
fb2 = (fb2 ? fb2 : {});
|
||||
const result = {};
|
||||
|
||||
if (fb2.author) {
|
||||
const authorNames = fb2.author.map(a => _.compact([
|
||||
a.lastName,
|
||||
a.firstName,
|
||||
a.middleName
|
||||
]).join(' '));
|
||||
|
||||
result.author = authorNames.join(', ');
|
||||
}
|
||||
|
||||
if (fb2.sequence) {
|
||||
const seqs = fb2.sequence.map(s => _.compact([
|
||||
s.name,
|
||||
(s.number ? `#${s.number}` : null),
|
||||
]).join(' '));
|
||||
|
||||
result.sequence = seqs.join(', ');
|
||||
if (result.sequence)
|
||||
result.sequenceTitle = `(${result.sequence})`;
|
||||
}
|
||||
|
||||
result.bookTitle = _.compact([result.sequenceTitle, fb2.bookTitle]).join(' ');
|
||||
|
||||
result.fullTitle = _.compact([
|
||||
result.author,
|
||||
result.bookTitle
|
||||
]).join(' - ');
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -12,11 +12,11 @@ Vue.use(Vuex);
|
||||
const debug = process.env.NODE_ENV !== 'production';
|
||||
|
||||
export default new Vuex.Store(Object.assign({}, root, {
|
||||
modules: {
|
||||
uistate,
|
||||
config,
|
||||
reader,
|
||||
},
|
||||
strict: debug,
|
||||
plugins: [createPersistedState()]
|
||||
modules: {
|
||||
uistate,
|
||||
config,
|
||||
reader,
|
||||
},
|
||||
strict: debug,
|
||||
plugins: [createPersistedState()]
|
||||
}));
|
||||
|
||||
@@ -1,3 +1,86 @@
|
||||
import * as utils from '../../share/utils';
|
||||
|
||||
const readerActions = {
|
||||
'help': 'Вызвать cправку',
|
||||
'loader': 'На страницу загрузки',
|
||||
'settings': 'Настроить',
|
||||
'undoAction': 'Действие назад',
|
||||
'redoAction': 'Действие вперед',
|
||||
'fullScreen': 'На весь экран',
|
||||
'scrolling': 'Плавный скроллинг',
|
||||
'stopScrolling': '',
|
||||
'setPosition': 'Установить позицию',
|
||||
'search': 'Найти в тексте',
|
||||
'copyText': 'Скопировать текст со страницы',
|
||||
'splitToPara': 'Обновить с разбиением на параграфы',
|
||||
'refresh': 'Принудительно обновить книгу',
|
||||
'offlineMode': 'Автономный режим (без интернета)',
|
||||
'contents': 'Оглавление/закладки',
|
||||
'libs': 'Сетевая библиотека',
|
||||
'recentBooks': 'Открыть недавние',
|
||||
'switchToolbar': 'Показать/скрыть панель управления',
|
||||
'donate': '',
|
||||
'bookBegin': 'В начало книги',
|
||||
'bookEnd': 'В конец книги',
|
||||
'pageBack': 'Страницу назад',
|
||||
'pageForward': 'Страницу вперед',
|
||||
'lineBack': 'Строчку назад',
|
||||
'lineForward': 'Строчку вперед',
|
||||
'incFontSize': 'Увеличить размер шрифта',
|
||||
'decFontSize': 'Уменьшить размер шрифта',
|
||||
'scrollingSpeedUp': 'Увеличить скорость скроллинга',
|
||||
'scrollingSpeedDown': 'Уменьшить скорость скроллинга',
|
||||
};
|
||||
|
||||
//readerActions[name]
|
||||
const toolButtons = [
|
||||
{name: 'undoAction', show: true},
|
||||
{name: 'redoAction', show: true},
|
||||
{name: 'fullScreen', show: true},
|
||||
{name: 'scrolling', show: false},
|
||||
{name: 'setPosition', show: true},
|
||||
{name: 'search', show: true},
|
||||
{name: 'copyText', show: false},
|
||||
{name: 'splitToPara', show: false},
|
||||
{name: 'refresh', show: true},
|
||||
{name: 'contents', show: true},
|
||||
{name: 'libs', show: true},
|
||||
{name: 'recentBooks', show: true},
|
||||
{name: 'offlineMode', show: false},
|
||||
];
|
||||
|
||||
//readerActions[name]
|
||||
const hotKeys = [
|
||||
{name: 'help', codes: ['F1', 'H']},
|
||||
{name: 'loader', codes: ['Escape']},
|
||||
{name: 'settings', codes: ['S']},
|
||||
{name: 'undoAction', codes: ['Ctrl+BracketLeft']},
|
||||
{name: 'redoAction', codes: ['Ctrl+BracketRight']},
|
||||
{name: 'fullScreen', codes: ['Enter', 'Backquote', 'F']},
|
||||
{name: 'scrolling', codes: ['Z']},
|
||||
{name: 'setPosition', codes: ['P']},
|
||||
{name: 'search', codes: ['Ctrl+F']},
|
||||
{name: 'copyText', codes: ['Ctrl+C']},
|
||||
{name: 'splitToPara', codes: ['Shift+R']},
|
||||
{name: 'refresh', codes: ['R']},
|
||||
{name: 'contents', codes: ['C']},
|
||||
{name: 'libs', codes: ['L']},
|
||||
{name: 'recentBooks', codes: ['X']},
|
||||
{name: 'offlineMode', codes: ['O']},
|
||||
|
||||
{name: 'switchToolbar', codes: ['Tab', 'Q']},
|
||||
{name: 'bookBegin', codes: ['Home']},
|
||||
{name: 'bookEnd', codes: ['End']},
|
||||
{name: 'pageBack', codes: ['PageUp', 'ArrowLeft', 'Backspace', 'Shift+Space']},
|
||||
{name: 'pageForward', codes: ['PageDown', 'ArrowRight', 'Space']},
|
||||
{name: 'lineBack', codes: ['ArrowUp']},
|
||||
{name: 'lineForward', codes: ['ArrowDown']},
|
||||
{name: 'incFontSize', codes: ['A']},
|
||||
{name: 'decFontSize', codes: ['Shift+A']},
|
||||
{name: 'scrollingSpeedUp', codes: ['Shift+ArrowDown']},
|
||||
{name: 'scrollingSpeedDown', codes: ['Shift+ArrowUp']},
|
||||
];
|
||||
|
||||
const fonts = [
|
||||
{name: 'ReaderDefault', label: 'По-умолчанию', fontVertShift: 0},
|
||||
{name: 'GEO_1', label: 'BPG Arial', fontVertShift: 10},
|
||||
@@ -122,6 +205,7 @@ const webFonts = [
|
||||
|
||||
];
|
||||
|
||||
//----------------------------------------------------------------------------------------------------------
|
||||
const settingDefaults = {
|
||||
textColor: '#000000',
|
||||
backgroundColor: '#EBE2C9',
|
||||
@@ -132,7 +216,7 @@ const settingDefaults = {
|
||||
fontName: 'ReaderDefault',
|
||||
webFontName: '',
|
||||
fontVertShift: 0,
|
||||
textVertShift: -20,
|
||||
textVertShift: 0,
|
||||
|
||||
lineInterval: 3,// px, межстрочный интервал
|
||||
textAlignJustify: true,// выравнивание по ширине
|
||||
@@ -140,17 +224,18 @@ const settingDefaults = {
|
||||
indentLR: 15,// px, отступ всего текста слева и справа
|
||||
indentTB: 0,// px, отступ всего текста сверху и снизу
|
||||
wordWrap: true,//перенос по слогам
|
||||
keepLastToFirst: true,// перенос последней строки в первую при листании
|
||||
keepLastToFirst: false,// перенос последней строки в первую при листании
|
||||
|
||||
showStatusBar: true,
|
||||
statusBarTop: false,// top, bottom
|
||||
statusBarHeight: 19,// px
|
||||
statusBarColorAlpha: 0.4,
|
||||
statusBarClickOpen: true,
|
||||
|
||||
scrollingDelay: 3000,// замедление, ms
|
||||
scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out
|
||||
|
||||
pageChangeAnimation: 'blink',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание
|
||||
pageChangeAnimation: 'blink',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание, rotate - вращение, flip - листание
|
||||
pageChangeAnimationSpeed: 80, //0-100%
|
||||
|
||||
allowUrlParamBookPos: false,
|
||||
@@ -163,17 +248,69 @@ const settingDefaults = {
|
||||
blinkCachedLoad: true,
|
||||
showImages: true,
|
||||
showInlineImagesInCenter: true,
|
||||
compactTextPerc: 0,
|
||||
imageHeightLines: 100,
|
||||
imageFitWidth: true,
|
||||
showServerStorageMessages: true,
|
||||
showWhatsNewDialog: true,
|
||||
showDonationDialog2020: true,
|
||||
showLiberamaTopDialog2020: true,
|
||||
enableSitesFilter: true,
|
||||
|
||||
fontShifts: {},
|
||||
showToolButton: {},
|
||||
userHotKeys: {},
|
||||
};
|
||||
|
||||
for (const font of fonts)
|
||||
settingDefaults.fontShifts[font.name] = font.fontVertShift;
|
||||
for (const font of webFonts)
|
||||
settingDefaults.fontShifts[font.name] = font.fontVertShift;
|
||||
for (const button of toolButtons)
|
||||
settingDefaults.showToolButton[button.name] = button.show;
|
||||
for (const hotKey of hotKeys)
|
||||
settingDefaults.userHotKeys[hotKey.name] = hotKey.codes;
|
||||
|
||||
const excludeDiffHotKeys = [];
|
||||
for (const hotKey of hotKeys)
|
||||
excludeDiffHotKeys.push(`userHotKeys/${hotKey.name}`);
|
||||
|
||||
function addDefaultsToSettings(settings) {
|
||||
const diff = utils.getObjDiff(settings, settingDefaults, {exclude: excludeDiffHotKeys});
|
||||
if (!utils.isEmptyObjDiffDeep(diff, {isApplyChange: false})) {
|
||||
return utils.applyObjDiff(settings, diff, {isApplyChange: false});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const libsDefaults = {
|
||||
startLink: 'http://flibusta.is',
|
||||
comment: 'Флибуста | Книжное братство',
|
||||
closeAfterSubmit: false,
|
||||
openInFrameOnEnter: false,
|
||||
openInFrameOnAdd: false,
|
||||
groups: [
|
||||
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
|
||||
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
|
||||
]},
|
||||
{r: 'https://flibs.in', s: 'https://flibs.in', list: [
|
||||
{l: 'https://flibs.in', c: 'Flibs'},
|
||||
]},
|
||||
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
|
||||
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
|
||||
]},
|
||||
{r: 'http://samlib.ru', s: 'http://samlib.ru', list: [
|
||||
{l: 'http://samlib.ru', c: 'Журнал "Самиздат"'},
|
||||
]},
|
||||
{r: 'http://lib.ru', s: 'http://lib.ru', list: [
|
||||
{l: 'http://lib.ru', c: 'Библиотека Максима Мошкова'},
|
||||
]},
|
||||
{r: 'https://aldebaran.ru', s: 'https://aldebaran.ru', list: [
|
||||
{l: 'https://aldebaran.ru', c: 'АЛЬДЕБАРАН | Электронная библиотека книг'},
|
||||
]},
|
||||
]
|
||||
};
|
||||
|
||||
// initial state
|
||||
const state = {
|
||||
@@ -183,9 +320,13 @@ const state = {
|
||||
profiles: {},
|
||||
profilesRev: 0,
|
||||
allowProfilesSave: false,//подстраховка для разработки
|
||||
whatsNewContentHash: '',
|
||||
donationRemindDate: '',
|
||||
currentProfile: '',
|
||||
settings: Object.assign({}, settingDefaults),
|
||||
settingsRev: {},
|
||||
libs: Object.assign({}, libsDefaults),
|
||||
libsRev: 0,
|
||||
};
|
||||
|
||||
// getters
|
||||
@@ -214,21 +355,44 @@ const mutations = {
|
||||
setAllowProfilesSave(state, value) {
|
||||
state.allowProfilesSave = value;
|
||||
},
|
||||
setWhatsNewContentHash(state, value) {
|
||||
state.whatsNewContentHash = value;
|
||||
},
|
||||
setDonationRemindDate(state, value) {
|
||||
state.donationRemindDate = value;
|
||||
},
|
||||
setCurrentProfile(state, value) {
|
||||
state.currentProfile = value;
|
||||
},
|
||||
setSettings(state, value) {
|
||||
state.settings = Object.assign({}, state.settings, value);
|
||||
const newSettings = Object.assign({}, state.settings, value);
|
||||
const added = addDefaultsToSettings(newSettings);
|
||||
if (added) {
|
||||
state.settings = added;
|
||||
} else {
|
||||
state.settings = newSettings;
|
||||
}
|
||||
},
|
||||
setSettingsRev(state, value) {
|
||||
state.settingsRev = Object.assign({}, state.settingsRev, value);
|
||||
},
|
||||
setLibs(state, value) {
|
||||
state.libs = value;
|
||||
},
|
||||
setLibsRev(state, value) {
|
||||
state.libsRev = value;
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
readerActions,
|
||||
toolButtons,
|
||||
hotKeys,
|
||||
fonts,
|
||||
webFonts,
|
||||
settingDefaults,
|
||||
addDefaultsToSettings,
|
||||
libsDefaults,
|
||||
|
||||
namespaced: true,
|
||||
state,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
.el-alert{width:100%;padding:8px 16px;margin:0;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;position:relative;background-color:#fff;overflow:hidden;opacity:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-transition:opacity .2s;transition:opacity .2s}.el-alert.is-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-alert--success{background-color:#f0f9eb;color:#67c23a}.el-alert--success .el-alert__description{color:#67c23a}.el-alert--info{background-color:#f4f4f5;color:#909399}.el-alert--info .el-alert__description{color:#909399}.el-alert--warning{background-color:#fdf6ec;color:#e6a23c}.el-alert--warning .el-alert__description{color:#e6a23c}.el-alert--error{background-color:#fef0f0;color:#f56c6c}.el-alert--error .el-alert__description{color:#f56c6c}.el-alert__content{display:table-cell;padding:0 8px}.el-alert__icon{font-size:16px;width:16px}.el-alert__icon.is-big{font-size:28px;width:28px}.el-alert__title{font-size:13px;line-height:18px}.el-alert__title.is-bold{font-weight:700}.el-alert .el-alert__description{font-size:12px;margin:5px 0 0}.el-alert__closebtn{font-size:12px;color:#c0c4cc;opacity:1;position:absolute;top:12px;right:15px;cursor:pointer}.el-alert__closebtn.is-customed{font-style:normal;font-size:13px;top:9px}.el-alert-fade-enter,.el-alert-fade-leave-active{opacity:0}
|
||||
@@ -1 +0,0 @@
|
||||
.el-aside{overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.el-badge{position:relative;vertical-align:middle;display:inline-block}.el-badge__content{background-color:#f56c6c;border-radius:10px;color:#fff;display:inline-block;font-size:12px;height:18px;line-height:18px;padding:0 6px;text-align:center;white-space:nowrap;border:1px solid #fff}.el-badge__content.is-fixed{position:absolute;top:0;right:10px;-webkit-transform:translateY(-50%) translateX(100%);transform:translateY(-50%) translateX(100%)}.el-badge__content.is-fixed.is-dot{right:5px}.el-badge__content.is-dot{height:8px;width:8px;padding:0;right:0;border-radius:50%}.el-badge__content--primary{background-color:#00468F}.el-badge__content--success{background-color:#67c23a}.el-badge__content--warning{background-color:#e6a23c}.el-badge__content--info{background-color:#909399}.el-badge__content--danger{background-color:#f56c6c}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.el-breadcrumb{font-size:14px;line-height:1}.el-breadcrumb::after,.el-breadcrumb::before{display:table;content:""}.el-breadcrumb::after{clear:both}.el-breadcrumb__separator{margin:0 9px;font-weight:700;color:#c0c4cc}.el-breadcrumb__separator[class*=icon]{margin:0 6px;font-weight:400}.el-breadcrumb__item{float:left}.el-breadcrumb__inner{color:#606266}.el-breadcrumb__inner a,.el-breadcrumb__inner.is-link{font-weight:700;text-decoration:none;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1);color:#303133}.el-breadcrumb__inner a:hover,.el-breadcrumb__inner.is-link:hover{color:#00468F;cursor:pointer}.el-breadcrumb__item:last-child .el-breadcrumb__inner,.el-breadcrumb__item:last-child .el-breadcrumb__inner a,.el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover,.el-breadcrumb__item:last-child .el-breadcrumb__inner:hover{font-weight:400;color:#606266;cursor:text}.el-breadcrumb__item:last-child .el-breadcrumb__separator{display:none}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.el-card{border-radius:4px;border:1px solid #ebeef5;background-color:#fff;overflow:hidden;color:#303133;-webkit-transition:.3s;transition:.3s}.el-card.is-always-shadow,.el-card.is-hover-shadow:focus,.el-card.is-hover-shadow:hover{-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-card__header{padding:18px 20px;border-bottom:1px solid #ebeef5;-webkit-box-sizing:border-box;box-sizing:border-box}.el-card__body{padding:20px}
|
||||
@@ -1 +0,0 @@
|
||||
.el-carousel__item,.el-carousel__mask{position:absolute;height:100%;top:0;left:0}.el-carousel__item{width:100%;display:inline-block;overflow:hidden;z-index:0}.el-carousel__item.is-active{z-index:2}.el-carousel__item.is-animating{-webkit-transition:-webkit-transform .4s ease-in-out;transition:-webkit-transform .4s ease-in-out;transition:transform .4s ease-in-out;transition:transform .4s ease-in-out,-webkit-transform .4s ease-in-out}.el-carousel__item--card{width:50%;-webkit-transition:-webkit-transform .4s ease-in-out;transition:-webkit-transform .4s ease-in-out;transition:transform .4s ease-in-out;transition:transform .4s ease-in-out,-webkit-transform .4s ease-in-out}.el-carousel__item--card.is-in-stage{cursor:pointer;z-index:1}.el-carousel__item--card.is-in-stage.is-hover .el-carousel__mask,.el-carousel__item--card.is-in-stage:hover .el-carousel__mask{opacity:.12}.el-carousel__item--card.is-active{z-index:2}.el-carousel__mask{width:100%;background-color:#fff;opacity:.24;-webkit-transition:.2s;transition:.2s}
|
||||
@@ -1 +0,0 @@
|
||||
.el-carousel{overflow-x:hidden;position:relative}.el-carousel__container{position:relative;height:300px}.el-carousel__arrow{border:none;outline:0;padding:0;margin:0;height:36px;width:36px;cursor:pointer;-webkit-transition:.3s;transition:.3s;border-radius:50%;background-color:rgba(31,45,61,.11);color:#fff;position:absolute;top:50%;z-index:10;-webkit-transform:translateY(-50%);transform:translateY(-50%);text-align:center;font-size:12px}.el-carousel__arrow--left{left:16px}.el-carousel__arrow--right{right:16px}.el-carousel__arrow:hover{background-color:rgba(31,45,61,.23)}.el-carousel__arrow i{cursor:pointer}.el-carousel__indicators{position:absolute;list-style:none;bottom:0;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);margin:0;padding:0;z-index:2}.el-carousel__indicators--outside{bottom:26px;text-align:center;position:static;-webkit-transform:none;transform:none}.el-carousel__indicators--outside .el-carousel__indicator:hover button{opacity:.64}.el-carousel__indicators--outside button{background-color:#c0c4cc;opacity:.24}.el-carousel__indicators--labels{left:0;right:0;-webkit-transform:none;transform:none;text-align:center}.el-carousel__indicators--labels .el-carousel__button{height:auto;width:auto;padding:2px 18px;font-size:12px}.el-carousel__indicators--labels .el-carousel__indicator{padding:6px 4px}.el-carousel__indicator{display:inline-block;background-color:transparent;padding:12px 4px;cursor:pointer}.el-carousel__indicator:hover button{opacity:.72}.el-carousel__indicator.is-active button{opacity:1}.el-carousel__button{display:block;opacity:.48;width:30px;height:2px;background-color:#fff;border:none;outline:0;padding:0;margin:0;cursor:pointer;-webkit-transition:.3s;transition:.3s}.carousel-arrow-left-enter,.carousel-arrow-left-leave-active{-webkit-transform:translateY(-50%) translateX(-10px);transform:translateY(-50%) translateX(-10px);opacity:0}.carousel-arrow-right-enter,.carousel-arrow-right-leave-active{-webkit-transform:translateY(-50%) translateX(10px);transform:translateY(-50%) translateX(10px);opacity:0}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.el-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-preferred-size:auto;flex-basis:auto;-webkit-box-sizing:border-box;box-sizing:border-box;min-width:0}.el-container.is-vertical{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user