Compare commits
921 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
873a08fee1 | ||
|
|
7e89228803 | ||
|
|
fc630923a4 | ||
|
|
928f911d03 | ||
|
|
7ffcd3fe1b | ||
|
|
0efbaf643a | ||
|
|
f1bf8e54ae | ||
|
|
b4aa6ab6c8 | ||
|
|
72431f0202 | ||
|
|
04a326c0e4 | ||
|
|
931966f4f3 | ||
|
|
8808cc4779 | ||
|
|
988c959eba | ||
|
|
c0b658d9e6 | ||
|
|
3190246f34 | ||
|
|
d957b4a5f9 | ||
|
|
bef9e5705c | ||
|
|
eb2affa518 | ||
|
|
07b9a3c033 | ||
|
|
3ca14ae06a | ||
|
|
7caa0c2112 | ||
|
|
9c69f5bc01 | ||
|
|
125a2e0f17 | ||
|
|
1b4360b897 | ||
|
|
4775d6e47b | ||
|
|
33fc553c55 | ||
|
|
25cad81c50 | ||
|
|
02a2099c1f | ||
|
|
1cda186b1a | ||
|
|
f10291b6c6 | ||
|
|
26ab5d6765 | ||
|
|
5edeed0747 | ||
|
|
c878ce432f | ||
|
|
81798897c8 | ||
|
|
63840fadbc | ||
|
|
36aa057035 | ||
|
|
30afd2421c | ||
|
|
53a1d90bd8 | ||
|
|
2ecf6beef2 | ||
|
|
85910a20e9 | ||
|
|
66cf7790b3 | ||
|
|
4a9eb7e4bb | ||
|
|
07446696c1 | ||
|
|
a29f9d9a4b | ||
|
|
d49c9baec3 | ||
|
|
8c9d4a12ee | ||
|
|
fce69e4657 | ||
|
|
b387509f88 | ||
|
|
8dc8bdc0d6 | ||
|
|
00caae8363 | ||
|
|
2ead8570a7 | ||
|
|
408315466b | ||
|
|
c651836554 | ||
|
|
03a1e70fce | ||
|
|
ab5a11a24f | ||
|
|
8cd6ed472c | ||
|
|
055181b744 | ||
|
|
e331a3920b | ||
|
|
c62bccb470 | ||
|
|
ea351ea293 | ||
|
|
d806a07c60 | ||
|
|
c0ea096f1f | ||
|
|
011d4a1672 | ||
|
|
4836a737c6 | ||
|
|
5712b2ee17 | ||
|
|
32dd17694e | ||
|
|
3ebc932a6a | ||
|
|
8f351d9bef | ||
|
|
5ae3ea94e4 | ||
|
|
f203d453a4 | ||
|
|
0d5cba121b | ||
|
|
0cd6a48a46 | ||
|
|
4e07ce2b5c | ||
|
|
85a525e301 | ||
|
|
03e4a6d723 | ||
|
|
ab28af1abe | ||
|
|
7fceed5301 | ||
|
|
0077816afa | ||
|
|
cb01423147 | ||
|
|
61b0712d36 | ||
|
|
12d7843377 | ||
|
|
9293c0a0d4 | ||
|
|
bb9522197a | ||
|
|
450a2e0664 | ||
|
|
41e35f3ec8 | ||
|
|
a9bc98abe3 | ||
|
|
47bca03532 | ||
|
|
942021371c | ||
|
|
ea2f178730 | ||
|
|
4b5c8d9efe | ||
|
|
28ebf13c3a | ||
|
|
5d52e63dd9 | ||
|
|
1a0e024050 | ||
|
|
e627a0d970 | ||
|
|
48668d94ad | ||
|
|
e08c431dd9 | ||
|
|
5ee58ad6f0 | ||
|
|
ac0a4f0586 | ||
|
|
b6f4c153e5 | ||
|
|
4fdaf5f555 | ||
|
|
b4ee9d6c00 | ||
|
|
7c73c74730 | ||
|
|
c20aa089fa | ||
|
|
b0e15c22ea | ||
|
|
d58a2c065a | ||
|
|
53135e7ee8 | ||
|
|
5c48ca9e6c | ||
|
|
c4a280f3d8 | ||
|
|
ba2943c722 | ||
|
|
26f6ffc83a | ||
|
|
bcf075a72c | ||
|
|
02d458d192 | ||
|
|
a349d8af68 | ||
|
|
0dbaf32aac | ||
|
|
e8c41ef3a8 | ||
|
|
e43a44e986 | ||
|
|
f14b8ed277 | ||
|
|
bbfe8a64cb | ||
|
|
bcf3c2dab0 | ||
|
|
d5404fd260 | ||
|
|
54bc662e43 | ||
|
|
42546ca97e | ||
|
|
5c13cf0eb9 | ||
|
|
2a9d44ae9a | ||
|
|
38414ae7b6 | ||
|
|
3ecb3e80ac | ||
|
|
4968828488 | ||
|
|
4db3cd24df | ||
|
|
45c6d3da77 | ||
|
|
4aab1da3c6 | ||
|
|
bf5dfa1c15 | ||
|
|
7549bdd2b4 | ||
|
|
1bb2525ab2 | ||
|
|
22a556f612 | ||
|
|
056611e87c | ||
|
|
6debe24880 | ||
|
|
56559bddab | ||
|
|
9ec74eccb4 | ||
|
|
3d2f45c20d | ||
|
|
fb2eedd5ba | ||
|
|
e278b4a00e | ||
|
|
0beaa611f6 | ||
|
|
14ca2daa39 | ||
|
|
714eb3ae83 | ||
|
|
6286d663c9 | ||
|
|
b5db2079d2 | ||
|
|
b3b30b9bd9 | ||
|
|
0b6a726503 | ||
|
|
609334c5a6 | ||
|
|
4852c7aec3 | ||
|
|
b1e3d33694 | ||
|
|
2bfc557071 | ||
|
|
e1216109bc | ||
|
|
990b8f390c | ||
|
|
e6f6cd4ff3 | ||
|
|
7deb745651 | ||
|
|
70f3ca8067 | ||
|
|
bb8497a997 | ||
|
|
2127e2ec0a | ||
|
|
9be4011d54 | ||
|
|
c534edfeb5 | ||
|
|
adc8cd7243 | ||
|
|
522d2d3b9c | ||
|
|
046933a05e | ||
|
|
9143288de2 | ||
|
|
6053ca6c0e | ||
|
|
084197530e | ||
|
|
9f366ca811 | ||
|
|
7c07e6f004 | ||
|
|
3d4d7e0342 | ||
|
|
1a8f241aad | ||
|
|
33e938b76a | ||
|
|
e2db546066 | ||
|
|
def9ee52e2 | ||
|
|
1afe10be03 | ||
|
|
fa44641fa2 | ||
|
|
9a1ef85c93 | ||
|
|
b848cf5aa7 | ||
|
|
8057e18ebc | ||
|
|
76e09ef34e | ||
|
|
00cb2dc274 | ||
|
|
ed46e91432 | ||
|
|
88d75fb0d8 | ||
|
|
a1d7a73459 | ||
|
|
687f89729b | ||
|
|
6bf678e01f | ||
|
|
a18aec2f96 | ||
|
|
1c0cf303a0 | ||
|
|
5c7ae73982 | ||
|
|
4e9c69a1cf | ||
|
|
78375be8bf | ||
|
|
b684725094 | ||
|
|
ff52602c3a | ||
|
|
ce704c5e26 | ||
|
|
4503e4ed17 | ||
|
|
01c384c43a | ||
|
|
dda2de58a8 | ||
|
|
0365acbf7a | ||
|
|
bbf1ab7180 | ||
|
|
83bf1f1d3a | ||
|
|
fdf04fed0e | ||
|
|
acce32bfa7 | ||
|
|
614c45ac7d | ||
|
|
c4c0199a1b | ||
|
|
a53ebb9355 | ||
|
|
06e12930c7 | ||
|
|
0f7655773a | ||
|
|
26660461d4 | ||
|
|
b41ee91db5 | ||
|
|
746dd8d37a | ||
|
|
fb4a57027d | ||
|
|
c97660bed0 | ||
|
|
fd8c8812a3 | ||
|
|
0101392858 | ||
|
|
cc3f82d693 | ||
|
|
d21997c918 | ||
|
|
74fec12f5c | ||
|
|
59525f8fa7 | ||
|
|
3c6d3befb2 | ||
|
|
dfa72c80bc | ||
|
|
c6e534b9db | ||
|
|
032ab6a85d | ||
|
|
830c066ebf | ||
|
|
c432388515 | ||
|
|
476deba93a | ||
|
|
ffb4f2386d | ||
|
|
21716163cb | ||
|
|
ca924148a5 | ||
|
|
37aa9b84ae | ||
|
|
c7bd7d4d7d | ||
|
|
d81a50e696 | ||
|
|
dda9943dbe | ||
|
|
2b4b9f24a1 | ||
|
|
2af77f22d6 | ||
|
|
f142e5812d | ||
|
|
ed901fc181 | ||
|
|
87a068899a | ||
|
|
115f683128 | ||
|
|
111568fc2e | ||
|
|
825136b5ff | ||
|
|
eae34b1121 | ||
|
|
b9d7a6a3bb | ||
|
|
1e5375f8f9 | ||
|
|
f597c603bf | ||
|
|
b93dd0a59e | ||
|
|
a5740e4349 | ||
|
|
dacbd05911 | ||
|
|
65c66e0feb | ||
|
|
52f9131f99 | ||
|
|
cfc946ad12 | ||
|
|
a207a0554c | ||
|
|
675e898163 | ||
|
|
d2167d8605 | ||
|
|
de849d3447 | ||
|
|
6c20b0b83e | ||
|
|
a09b70a991 | ||
|
|
2427a3e08b | ||
|
|
1104f9b850 | ||
|
|
dc48700e9e | ||
|
|
f0b0c39328 | ||
|
|
aad74cf682 | ||
|
|
d449478204 | ||
|
|
d4f6536caa | ||
|
|
1eac00f71c | ||
|
|
ca1170a9f0 | ||
|
|
79dda03bac | ||
|
|
6c8e0b8573 | ||
|
|
17c14722fd | ||
|
|
48612ee118 | ||
|
|
205c676999 | ||
|
|
54e0dd0478 | ||
|
|
2de8d7515e | ||
|
|
a251d16432 | ||
|
|
599caba912 | ||
|
|
3477c43465 | ||
|
|
200dac7946 | ||
|
|
e60829946d | ||
|
|
ef12a84285 | ||
|
|
6a18ae3f27 | ||
|
|
a250e95950 | ||
|
|
b174ae452b | ||
|
|
0b63bce357 | ||
|
|
de0d10e792 | ||
|
|
b358b340b4 | ||
|
|
455aba7f4f | ||
|
|
fde0437157 | ||
|
|
480c95bd63 | ||
|
|
972f957685 | ||
|
|
40ff04e5dc | ||
|
|
b3c028bd7a | ||
|
|
51ec6a54fa | ||
|
|
7a29b16ee8 | ||
|
|
7af6fd8248 | ||
|
|
e1c93169b5 | ||
|
|
f4716d5a1e | ||
|
|
f5c06ce420 | ||
|
|
9492f85d80 | ||
|
|
b1303a3ba2 | ||
|
|
5c9cfe5e6f | ||
|
|
b89b5322b8 | ||
|
|
945feba6b2 | ||
|
|
c8af4b907b | ||
|
|
298e8928cf | ||
|
|
8cb67d2976 | ||
|
|
32b8382641 | ||
|
|
007e97463b | ||
|
|
e4f190698d | ||
|
|
b3be07b17e | ||
|
|
72f8977071 | ||
|
|
3dbf00344e | ||
|
|
ffdf0b12cd | ||
|
|
a51150c729 | ||
|
|
37e14b397c | ||
|
|
e48af7ee7d | ||
|
|
3eb3dd371a | ||
|
|
8ef6551560 | ||
|
|
b1f5f3dd28 | ||
|
|
6074c4b7bd | ||
|
|
9906dd43c7 | ||
|
|
17699f66f8 | ||
|
|
80a29e654d | ||
|
|
4184fda247 | ||
|
|
7460ff7055 | ||
|
|
3137b86cee | ||
|
|
b2ca84bb7e | ||
|
|
7d692dd730 | ||
|
|
8850a89aa7 | ||
|
|
57b01dd204 | ||
|
|
8aa1da36b6 | ||
|
|
2dbe29d632 | ||
|
|
7fa891b4fc | ||
|
|
6cb7412cf3 | ||
|
|
157322834b | ||
|
|
1a13a0fee1 | ||
|
|
37256255bf | ||
|
|
75e01c899e | ||
|
|
ef0d6eab89 | ||
|
|
5d54b1b0f4 | ||
|
|
522f953b4f | ||
|
|
15f02c7115 | ||
|
|
174c877eee | ||
|
|
fd9ec736d7 | ||
|
|
2c94025ba3 | ||
|
|
bfadf35c40 | ||
|
|
f3b69caa12 | ||
|
|
18a83a5b0b | ||
|
|
bd9669b782 | ||
|
|
e05713aa7f | ||
|
|
bc3e1f0a6f | ||
|
|
063d01b5ca | ||
|
|
81c38d7749 | ||
|
|
a29842b084 | ||
|
|
bb5adcdaf6 | ||
|
|
537e17a219 | ||
|
|
03ce50153e | ||
|
|
15d01ad7fc | ||
|
|
e2b29e2c2f | ||
|
|
ce7ae84e0f | ||
|
|
01eb545f15 | ||
|
|
706738c7f1 | ||
|
|
6afa78cde9 | ||
|
|
71f5710bba | ||
|
|
0d87043f91 | ||
|
|
e25375fb7a | ||
|
|
41822999c8 | ||
|
|
07444bc7c2 | ||
|
|
ec48e5b0b7 | ||
|
|
e8e2e9297f | ||
|
|
4f871dd5ca | ||
|
|
f5f07a591a | ||
|
|
4c11e6918f | ||
|
|
403b9c0508 | ||
|
|
ee8ba75371 | ||
|
|
a2773fb180 | ||
|
|
ca36d588fc | ||
|
|
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 | ||
|
|
a24eaaed50 | ||
|
|
26813c582f | ||
|
|
6067ac73e2 | ||
|
|
b1d94b67f4 | ||
|
|
452f4e69fd |
6
.babelrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"presets": [['@babel/preset-env', { "targets": { "esmodules": true } }]],
|
||||
"plugins": [
|
||||
["@babel/plugin-proposal-decorators", { "legacy": true }]
|
||||
]
|
||||
}
|
||||
19
.eslintrc
@@ -1,17 +1,18 @@
|
||||
{
|
||||
"parser": "vue-eslint-parser",
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
"parser": "@babel/eslint-parser",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/essential"
|
||||
"plugin:vue/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"vue",
|
||||
"html",
|
||||
"node"
|
||||
"@babel"
|
||||
],
|
||||
"env": {
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
@@ -24,6 +25,14 @@
|
||||
"LM_TOTAL": false
|
||||
},
|
||||
"rules": {
|
||||
"vue/html-indent": ["warn", 4, {
|
||||
"alignAttributesVertically": false
|
||||
}],
|
||||
"vue/max-attributes-per-line": "off",
|
||||
"vue/html-self-closing": "off",
|
||||
"vue/no-v-html": "off",
|
||||
"vue/no-v-model-argument": "off",
|
||||
|
||||
"strict": 0,
|
||||
"indent": [0, 4, {
|
||||
"SwitchCase": 1
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
Браузерная онлайн-читалка книг и децентрализованная библиотека.
|
||||
|
||||
Читалка [OmniReader](https://omnireader.ru) является частью данного проекта, размещенной на VPS:
|
||||
Читалка <img src="https://omnireader.ru/favicon.ico" width="14px"/>[OmniReader](https://omnireader.ru) является частью данного проекта, размещенной на VPS:
|
||||
|
||||

|
||||

|
||||
|
||||
## VPS
|
||||
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader](docs/omnireader/README.md)
|
||||
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
|
||||
|
||||
## Сборка проекта
|
||||
Необходима версия node.js не ниже 10.
|
||||
Необходима версия node.js не ниже 14.
|
||||
|
||||
```
|
||||
$ git clone https://github.com/bookpauk/liberama
|
||||
|
||||
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');
|
||||
}
|
||||
@@ -4,7 +4,7 @@ const util = require('util');
|
||||
const stream = require('stream');
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
|
||||
const got = require('got');
|
||||
const axios = require('axios');
|
||||
const FileDecompressor = require('../server/core/FileDecompressor');
|
||||
|
||||
const distDir = path.resolve(__dirname, '../dist');
|
||||
@@ -24,12 +24,13 @@ async function main() {
|
||||
await fs.ensureDir(tempDownloadDir);
|
||||
|
||||
//sqlite3
|
||||
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`;
|
||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-linux-x64.tar.gz';
|
||||
const sqliteDecompressedFilename = `${tempDownloadDir}/napi-v3-linux-x64/node_sqlite3.node`;
|
||||
|
||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
||||
await pipeline(got.stream(sqliteRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
|
||||
const res = await axios.get(sqliteRemoteUrl, {responseType: 'stream'})
|
||||
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
|
||||
console.log(`done downloading ${sqliteRemoteUrl}`);
|
||||
|
||||
//распаковываем
|
||||
@@ -46,7 +47,8 @@ async function main() {
|
||||
// Скачиваем ipfs
|
||||
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_linux-amd64.tar.gz';
|
||||
|
||||
await pipeline(got.stream(ipfsRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/ipfs.tar.gz`));
|
||||
const res = await axios.get(ipfsRemoteUrl, {responseType: 'stream'})
|
||||
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/ipfs.tar.gz`));
|
||||
console.log(`done downloading ${ipfsRemoteUrl}`);
|
||||
|
||||
//распаковываем
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
const path = require('path');
|
||||
//const webpack = require('webpack');
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin');
|
||||
const DefinePlugin = require('webpack').DefinePlugin;
|
||||
const { VueLoaderPlugin } = require('vue-loader');
|
||||
|
||||
const clientDir = path.resolve(__dirname, '../client');
|
||||
|
||||
module.exports = {
|
||||
resolve: {
|
||||
alias: {
|
||||
ws: false,
|
||||
//vue: '@vue/compat'
|
||||
}
|
||||
},
|
||||
entry: [`${clientDir}/main.js`],
|
||||
output: {
|
||||
publicPath: '/app/',
|
||||
@@ -14,53 +20,61 @@ module.exports = {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: "vue-loader"
|
||||
loader: 'vue-loader',
|
||||
/*options: {
|
||||
compilerOptions: {
|
||||
compatConfig: {
|
||||
MODE: 2
|
||||
}
|
||||
}
|
||||
}*/
|
||||
},
|
||||
{
|
||||
resourceQuery: /^\?vue/,
|
||||
use: path.resolve(__dirname, 'includer.js')
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
query: {
|
||||
options: {
|
||||
presets: [['@babel/preset-env', { targets: { esmodules: true } }]],
|
||||
plugins: [
|
||||
'syntax-dynamic-import',
|
||||
'transform-decorators-legacy',
|
||||
'transform-class-properties',
|
||||
// ["component", { "libraryName": "element-ui", "styleLibraryName": `~${clientDir}/theme` } ]
|
||||
['@babel/plugin-proposal-decorators', { legacy: true }]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.gif$/,
|
||||
loader: "url-loader",
|
||||
options: {
|
||||
name: "images/[name]-[hash:6].[ext]"
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.png$/,
|
||||
loader: "url-loader",
|
||||
options: {
|
||||
name: "images/[name]-[hash:6].[ext]"
|
||||
}
|
||||
test: /\.(gif|png)$/,
|
||||
type: 'asset/inline',
|
||||
},
|
||||
{
|
||||
test: /\.jpg$/,
|
||||
loader: "file-loader",
|
||||
options: {
|
||||
name: "images/[name]-[hash:6].[ext]"
|
||||
}
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'images/[name]-[hash:6][ext]'
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
test: /\.(ttf|eot|woff|woff2)$/,
|
||||
loader: "file-loader",
|
||||
options: {
|
||||
name: "fonts/[name]-[hash:6].[ext]"
|
||||
}
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'fonts/[name]-[hash:6][ext]'
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new DefinePlugin({
|
||||
__VUE_OPTIONS_API__: true,
|
||||
__VUE_PROD_DEVTOOLS__: false,
|
||||
__QUASAR_SSR__: false,
|
||||
__QUASAR_SSR_SERVER__: false,
|
||||
__QUASAR_SSR_CLIENT__: false,
|
||||
__QUASAR_VERSION__: false,
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
const merge = require('webpack-merge');
|
||||
const { merge } = require('webpack-merge');
|
||||
const baseWpConfig = require('./webpack.base.config');
|
||||
|
||||
baseWpConfig.entry.unshift('webpack-hot-middleware/client');
|
||||
@@ -13,7 +13,7 @@ const clientDir = path.resolve(__dirname, '../client');
|
||||
|
||||
module.exports = merge(baseWpConfig, {
|
||||
mode: 'development',
|
||||
devtool: "#inline-source-map",
|
||||
devtool: 'inline-source-map',
|
||||
output: {
|
||||
path: `${publicDir}/app`,
|
||||
filename: 'bundle.js'
|
||||
@@ -38,6 +38,6 @@ module.exports = merge(baseWpConfig, {
|
||||
template: `${clientDir}/index.html.template`,
|
||||
filename: `${publicDir}/index.html`
|
||||
}),
|
||||
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}])
|
||||
new CopyWebpackPlugin({patterns: [{from: `${clientDir}/assets/*`, to: `${publicDir}/`}]})
|
||||
]
|
||||
});
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
const path = require('path');
|
||||
//const webpack = require('webpack');
|
||||
|
||||
const merge = require('webpack-merge');
|
||||
const { merge } = require('webpack-merge');
|
||||
const baseWpConfig = require('./webpack.base.config');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
//const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const AppCachePlugin = require('appcache-webpack-plugin');
|
||||
const {GenerateSW} = require('workbox-webpack-plugin');
|
||||
|
||||
const publicDir = path.resolve(__dirname, '../dist/tmp/public');
|
||||
const clientDir = path.resolve(__dirname, '../client');
|
||||
@@ -34,19 +34,18 @@ module.exports = merge(baseWpConfig, {
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
terserOptions: {
|
||||
output: {
|
||||
format: {
|
||||
comments: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
new OptimizeCSSAssetsPlugin()
|
||||
new CssMinimizerWebpackPlugin()
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin([publicDir], {root: path.resolve(__dirname, '..')}),
|
||||
//new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: [`${publicDir}/**`] }),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "[name].[contenthash].css"
|
||||
}),
|
||||
@@ -54,7 +53,15 @@ module.exports = merge(baseWpConfig, {
|
||||
template: `${clientDir}/index.html.template`,
|
||||
filename: `${publicDir}/index.html`
|
||||
}),
|
||||
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}]),
|
||||
new AppCachePlugin({exclude: ['../index.html']})
|
||||
new CopyWebpackPlugin({patterns:
|
||||
[{from: `${clientDir}/assets/*`, to: `${publicDir}/`, context: `${clientDir}/assets` }]
|
||||
}),
|
||||
new GenerateSW({
|
||||
cacheId: 'liberama',
|
||||
swDest: `${publicDir}/service-worker.js`,
|
||||
navigateFallback: '/index.html',
|
||||
navigateFallbackDenylist: [new RegExp('^/api'), new RegExp('^/ws'), new RegExp('^/tmp'),],
|
||||
skipWaiting: true,
|
||||
}),
|
||||
]
|
||||
});
|
||||
|
||||
12
build/win.js
@@ -4,7 +4,7 @@ const util = require('util');
|
||||
const stream = require('stream');
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
|
||||
const got = require('got');
|
||||
const axios = require('axios');
|
||||
const FileDecompressor = require('../server/core/FileDecompressor');
|
||||
|
||||
const distDir = path.resolve(__dirname, '../dist');
|
||||
@@ -24,12 +24,13 @@ async function main() {
|
||||
await fs.ensureDir(tempDownloadDir);
|
||||
|
||||
//sqlite3
|
||||
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`;
|
||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-win32-x64.tar.gz';
|
||||
const sqliteDecompressedFilename = `${tempDownloadDir}/napi-v3-win32-x64/node_sqlite3.node`;
|
||||
|
||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
||||
await pipeline(got.stream(sqliteRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
|
||||
const res = await axios.get(sqliteRemoteUrl, {responseType: 'stream'})
|
||||
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
|
||||
console.log(`done downloading ${sqliteRemoteUrl}`);
|
||||
|
||||
//распаковываем
|
||||
@@ -46,7 +47,8 @@ async function main() {
|
||||
// Скачиваем ipfs
|
||||
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_windows-amd64.zip';
|
||||
|
||||
await pipeline(got.stream(ipfsRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/ipfs.zip`));
|
||||
const res = await axios.get(ipfsRemoteUrl, {responseType: 'stream'})
|
||||
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/ipfs.zip`));
|
||||
console.log(`done downloading ${ipfsRemoteUrl}`);
|
||||
|
||||
//распаковываем
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import wsc from './webSocketConnection';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api'
|
||||
@@ -6,9 +7,22 @@ const api = axios.create({
|
||||
|
||||
class Misc {
|
||||
async loadConfig() {
|
||||
const response = await api.post('/config', {params: [
|
||||
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch',
|
||||
]});
|
||||
|
||||
const query = {params: [
|
||||
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'branch',
|
||||
]};
|
||||
|
||||
try {
|
||||
const config = await wsc.message(await 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,6 +1,6 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import * as utils from '../share/utils';
|
||||
import wsc from './webSocketConnection';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api/reader'
|
||||
@@ -11,8 +11,71 @@ const workerApi = axios.create({
|
||||
});
|
||||
|
||||
class Reader {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
async getWorkerStateFinish(workerId, callback) {
|
||||
if (!callback) callback = () => {};
|
||||
|
||||
let response = {};
|
||||
try {
|
||||
const requestId = await wsc.send({action: 'worker-get-state-finish', workerId});
|
||||
|
||||
let prevResponse = false;
|
||||
while (1) {// eslint-disable-line no-constant-condition
|
||||
response = await wsc.message(requestId);
|
||||
|
||||
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
|
||||
const prevProgress = response.progress || 0;
|
||||
const prevState = response.state || 0;
|
||||
response = await workerApi.post('/get-state', {workerId});
|
||||
response = response.data;
|
||||
callback(response);
|
||||
|
||||
if (!response.state)
|
||||
throw new Error('Неверный ответ api');
|
||||
|
||||
if (response.state == 'finish' || response.state == 'error') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (i > 0)
|
||||
await utils.sleep(refreshPause);
|
||||
|
||||
i++;
|
||||
if (i > 180*1000/refreshPause) {//3 мин ждем телодвижений воркера
|
||||
throw new Error('Слишком долгое время ожидания');
|
||||
}
|
||||
//проверка воркера
|
||||
i = (prevProgress != response.progress || prevState != response.state ? 1 : i);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async loadBook(opts, callback) {
|
||||
const refreshPause = 300;
|
||||
if (!callback) callback = () => {};
|
||||
|
||||
let response = await api.post('/load-book', opts);
|
||||
@@ -22,62 +85,92 @@ class Reader {
|
||||
throw new Error('Неверный ответ api');
|
||||
|
||||
callback({totalSteps: 4});
|
||||
callback(response.data);
|
||||
|
||||
let i = 0;
|
||||
while (1) {// eslint-disable-line no-constant-condition
|
||||
callback(response.data);
|
||||
response = await this.getWorkerStateFinish(workerId, callback);
|
||||
|
||||
if (response.data.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
|
||||
if (response) {
|
||||
if (response.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
|
||||
callback({step: 4});
|
||||
const book = await this.loadCachedBook(response.data.path, callback);
|
||||
return Object.assign({}, response.data, {data: book.data});
|
||||
const book = await this.loadCachedBook(response.path, callback, response.size);
|
||||
return Object.assign({}, response, {data: book.data});
|
||||
}
|
||||
if (response.data.state == 'error') {
|
||||
let errMes = response.data.error;
|
||||
|
||||
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.data.url}`;
|
||||
errMes = `Ресурс не найден по адресу: ${response.url}`;
|
||||
throw new Error(errMes);
|
||||
}
|
||||
if (i > 0)
|
||||
await utils.sleep(refreshPause);
|
||||
} else {
|
||||
throw new Error('Пустой ответ сервера');
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
||||
throw new Error('Слишком долгое время ожидания');
|
||||
async checkCachedBook(url) {
|
||||
let estSize = -1;
|
||||
try {
|
||||
const response = await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
|
||||
|
||||
if (response.headers['content-length']) {
|
||||
estSize = response.headers['content-length'];
|
||||
}
|
||||
} catch (e) {
|
||||
//восстановим при необходимости файл на сервере из удаленного облака
|
||||
let response = null
|
||||
|
||||
try {
|
||||
response = await wsc.message(await 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;
|
||||
}
|
||||
//проверка воркера
|
||||
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);
|
||||
}
|
||||
|
||||
return estSize;
|
||||
}
|
||||
|
||||
async checkUrl(url) {
|
||||
return await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
|
||||
}
|
||||
|
||||
async loadCachedBook(url, callback) {
|
||||
const response = await axios.head(url);
|
||||
|
||||
let estSize = 1000000;
|
||||
if (response.headers['content-length']) {
|
||||
estSize = response.headers['content-length'];
|
||||
}
|
||||
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({progress: Math.round((progress.loaded*100)/estSize)});
|
||||
}
|
||||
}
|
||||
//загрузка
|
||||
|
||||
return await axios.get(url, options);
|
||||
}
|
||||
|
||||
@@ -86,9 +179,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: {
|
||||
@@ -114,13 +206,24 @@ class Reader {
|
||||
}
|
||||
|
||||
async storage(request) {
|
||||
let response = await api.post('/storage', request);
|
||||
let response = null;
|
||||
try {
|
||||
response = await wsc.message(await 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 (state == 'error') {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
client/api/webSocketConnection.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import WebSocketConnection from '../../server/core/WebSocketConnection';
|
||||
|
||||
export default new WebSocketConnection();
|
||||
|
Before Width: | Height: | Size: 227 B After Width: | Height: | Size: 1.3 KiB |
5
client/assets/sw-register.js
Normal file
@@ -0,0 +1,5 @@
|
||||
(function() {
|
||||
if('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/service-worker.js');
|
||||
}
|
||||
})();
|
||||
@@ -1,55 +1,32 @@
|
||||
<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>
|
||||
<div class="fit row">
|
||||
<Notify ref="notify" />
|
||||
<StdDialog ref="stdDialog" />
|
||||
|
||||
<el-main v-if="showMain" :style="{padding: (isReaderActive ? 0 : '5px')}">
|
||||
<keep-alive>
|
||||
<router-view></router-view>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive v-if="showPage">
|
||||
<component :is="Component" class="col" />
|
||||
</keep-alive>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from './vueComponent.js';
|
||||
|
||||
import Notify from './share/Notify.vue';
|
||||
import StdDialog from './share/StdDialog.vue';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
import miscApi from '../api/misc';
|
||||
import * as utils from '../share/utils';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Notify,
|
||||
StdDialog,
|
||||
},
|
||||
watch: {
|
||||
mode: function() {
|
||||
this.setAppTitle();
|
||||
@@ -57,8 +34,11 @@ export default @Component({
|
||||
}
|
||||
},
|
||||
|
||||
})
|
||||
class App extends Vue {
|
||||
};
|
||||
class App {
|
||||
_options = componentOptions;
|
||||
showPage = false;
|
||||
|
||||
itemRuText = {
|
||||
'/cardindex': 'Картотека',
|
||||
'/reader': 'Читалка',
|
||||
@@ -67,68 +47,117 @@ class App extends Vue {
|
||||
'/sources': 'Источники',
|
||||
'/settings': 'Параметры',
|
||||
'/help': 'Справка',
|
||||
}
|
||||
};
|
||||
|
||||
created() {
|
||||
this.commit = this.$store.commit;
|
||||
this.dispatch = this.$store.dispatch;
|
||||
this.state = this.$store.state;
|
||||
this.uistate = this.$store.state.uistate;
|
||||
this.config = this.$store.state.config;
|
||||
|
||||
// set-app-title
|
||||
this.$root.$on('set-app-title', this.setAppTitle);
|
||||
//root route
|
||||
let cachedRoute = '';
|
||||
let cachedPath = '';
|
||||
this.$root.getRootRoute = () => {
|
||||
if (this.$route.path != cachedPath) {
|
||||
cachedPath = this.$route.path;
|
||||
const m = cachedPath.match(/^(\/[^/]*).*$/i);
|
||||
cachedRoute = (m ? m[1] : this.$route.path);
|
||||
|
||||
//global keyHooks
|
||||
this.keyHooks = [];
|
||||
this.keyHook = (event) => {
|
||||
for (const hook of this.keyHooks)
|
||||
}
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
this.$root.isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||
|
||||
// setAppTitle
|
||||
this.$root.setAppTitle = this.setAppTitle;
|
||||
|
||||
//sanitize
|
||||
this.$root.sanitize = sanitizeHtml;
|
||||
|
||||
//global event hooks
|
||||
this.eventHooks = {};
|
||||
this.$root.eventHook = (hookName, event) => {
|
||||
if (!this.eventHooks[hookName])
|
||||
return;
|
||||
for (const hook of this.eventHooks[hookName])
|
||||
hook(event);
|
||||
}
|
||||
|
||||
this.$root.addKeyHook = (hook) => {
|
||||
if (this.keyHooks.indexOf(hook) < 0)
|
||||
this.keyHooks.push(hook);
|
||||
this.$root.addEventHook = (hookName, hook) => {
|
||||
if (!this.eventHooks[hookName])
|
||||
this.eventHooks[hookName] = [];
|
||||
if (this.eventHooks[hookName].indexOf(hook) < 0)
|
||||
this.eventHooks[hookName].push(hook);
|
||||
}
|
||||
|
||||
this.$root.removeKeyHook = (hook) => {
|
||||
const i = this.keyHooks.indexOf(hook);
|
||||
this.$root.removeEventHook = (hookName, hook) => {
|
||||
if (!this.eventHooks[hookName])
|
||||
return;
|
||||
const i = this.eventHooks[hookName].indexOf(hook);
|
||||
if (i >= 0)
|
||||
this.keyHooks.splice(i, 1);
|
||||
this.eventHooks[hookName].splice(i, 1);
|
||||
}
|
||||
|
||||
document.addEventListener('keyup', (event) => {
|
||||
this.keyHook(event);
|
||||
});
|
||||
this.$root.eventHook('key', event);
|
||||
});
|
||||
document.addEventListener('keypress', (event) => {
|
||||
this.$root.eventHook('key', event);
|
||||
});
|
||||
document.addEventListener('keydown', (event) => {
|
||||
this.keyHook(event);
|
||||
});
|
||||
window.addEventListener('resize', () => {
|
||||
this.$root.$emit('resize');
|
||||
this.$root.eventHook('key', event);
|
||||
});
|
||||
|
||||
window.addEventListener('resize', (event) => {
|
||||
this.$root.eventHook('resize', event);
|
||||
});
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.dispatch('config/loadConfig');
|
||||
this.$watch('apiError', function(newError) {
|
||||
if (newError) {
|
||||
let mes = newError.message;
|
||||
if (newError.response && newError.response.config)
|
||||
mes = newError.response.config.url + '<br>' + newError.response.statusText;
|
||||
this.$notify.error({
|
||||
title: 'Ошибка API',
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: mes
|
||||
});
|
||||
}
|
||||
});
|
||||
this.$root.notify = this.$refs.notify;
|
||||
this.$root.stdDialog = this.$refs.stdDialog;
|
||||
|
||||
this.setAppTitle();
|
||||
this.redirectIfNeeded();
|
||||
(async() => {
|
||||
//загрузим конфиг сревера
|
||||
try {
|
||||
const config = await miscApi.loadConfig();
|
||||
this.commit('config/setConfig', config);
|
||||
this.showPage = true;
|
||||
} catch(e) {
|
||||
//проверим, не получен ли конфиг ранее
|
||||
if (!this.mode) {
|
||||
this.$root.notify.error(e.message, 'Ошибка API');
|
||||
} else {
|
||||
//вероятно, работаем в оффлайне
|
||||
this.showPage = true;
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
//запросим persistent storage
|
||||
if (navigator.storage && navigator.storage.persist) {
|
||||
navigator.storage.persist();
|
||||
}
|
||||
await this.$router.isReady();
|
||||
this.redirectIfNeeded();
|
||||
})();
|
||||
}
|
||||
|
||||
toggleCollapse() {
|
||||
this.commit('uistate/setAsideBarCollapse', !this.uistate.asideBarCollapse);
|
||||
this.$root.$emit('resize');
|
||||
this.$root.eventHook('resize');
|
||||
}
|
||||
|
||||
get isCollapse() {
|
||||
@@ -137,9 +166,9 @@ class App extends Vue {
|
||||
|
||||
get asideWidth() {
|
||||
if (this.uistate.asideBarCollapse) {
|
||||
return '64px';
|
||||
return 64;
|
||||
} else {
|
||||
return '170px';
|
||||
return 170;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,18 +192,17 @@ 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.getRootRoute();
|
||||
}
|
||||
|
||||
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]}`;
|
||||
document.title = `${this.config.name} - ${this.itemRuText[this.rootRoute]}`;
|
||||
}
|
||||
} else {
|
||||
document.title = title;
|
||||
@@ -190,33 +218,37 @@ 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.isReaderActive)) {
|
||||
//старый url
|
||||
if ((this.mode == 'reader' || this.mode == 'omnireader' || this.mode == 'liberama.top')) {
|
||||
const search = window.location.search.substr(1);
|
||||
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 });
|
||||
//распознавание параметра 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(App);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -228,68 +260,36 @@ 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;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
} to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.notify-button-icon {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Book в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Book extends Vue {
|
||||
class Book {
|
||||
created() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(Book);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Card в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Card extends Vue {
|
||||
class Card {
|
||||
created() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(Card);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
<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>
|
||||
<div>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
<router-view></router-view>
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</el-tabs>
|
||||
</el-container>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
const rootRoute = '/cardindex';
|
||||
const selfRoute = '/cardindex';
|
||||
const tab2Route = [
|
||||
'/cardindex/search',
|
||||
'/cardindex/card',
|
||||
@@ -27,20 +22,32 @@ const tab2Route = [
|
||||
];
|
||||
let lastActiveTab = null;
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
watch: {
|
||||
selectedTab: function(newValue, oldValue) {
|
||||
selectedTab: function(newValue) {
|
||||
lastActiveTab = newValue;
|
||||
this.setRouteByTab(newValue);
|
||||
},
|
||||
curRoute: function(newValue, oldValue) {
|
||||
curRoute: function(newValue) {
|
||||
this.setTabByRoute(newValue);
|
||||
},
|
||||
},
|
||||
})
|
||||
class CardIndex extends Vue {
|
||||
};
|
||||
class CardIndex {
|
||||
_options = componentOptions;
|
||||
selectedTab = null;
|
||||
|
||||
created() {
|
||||
this.$watch(
|
||||
() => this.$route.path,
|
||||
(newValue) => {
|
||||
if (newValue == '/cardindex' && this.isReader) {
|
||||
this.$router.replace({ path: '/reader' });
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.setTabByRoute(this.curRoute);
|
||||
}
|
||||
@@ -51,7 +58,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);
|
||||
}
|
||||
}
|
||||
@@ -63,12 +70,22 @@ class CardIndex extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this.$store.state.config.mode;
|
||||
}
|
||||
|
||||
get curRoute() {
|
||||
const m = this.$route.path.match(/^(\/[^\/]*\/[^\/]*).*$/i);
|
||||
const m = this.$route.path.match(/^(\/[^/]*\/[^/]*).*$/i);
|
||||
return (m ? m[1] : this.$route.path);
|
||||
}
|
||||
|
||||
get isReader() {
|
||||
return (this.mode !== null && (this.mode == 'reader' || this.mode == 'omnireader' || this.mode == 'liberama.top'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(CardIndex);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел History в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class History extends Vue {
|
||||
class History {
|
||||
created() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(History);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Search в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Search extends Vue {
|
||||
class Search {
|
||||
created() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(Search);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<Window ref="window" width="600px" height="95%" @close="close">
|
||||
<template #header>
|
||||
Настроить закладки
|
||||
</template>
|
||||
|
||||
<div class="col column fit">
|
||||
<div class="row items-center top-panel bg-grey-3">
|
||||
<q-btn :disabled="!selected" class="q-mr-md" round dense color="blue" icon="la la-check" size="16px" @click.stop="openSelected">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Открыть выбранную закладку
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-input ref="search" v-model="search" class="col" outlined dense bg-color="white" placeholder="Найти">
|
||||
<template #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" size="14px" @click.stop="addBookmark">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Добавить закладку
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn :disabled="!ticked.length" class="q-mb-sm" round dense color="blue" icon="la la-minus" size="14px" @click.stop="delBookmark">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Удалить отмеченные закладки
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn :disabled="!selected || selected.indexOf('r-') == 0" class="q-mb-sm" round dense color="blue" icon="la la-edit" size="14px" @click.stop="editBookmark">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Редактировать закладку
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn :disabled="!ticked.length" class="q-mb-sm" round dense color="blue" icon="la la-arrow-up" size="14px" @click.stop="moveBookmark(false)">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Переместить отмеченные вверх
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn :disabled="!ticked.length" class="q-mb-sm" round dense color="blue" icon="la la-arrow-down" size="14px" @click.stop="moveBookmark(true)">
|
||||
<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" size="14px" @click.stop="setDefaultBookmarks">
|
||||
<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" size="36px" label="Выбрать все" @update:model-value="makeTickAll" />
|
||||
</div>
|
||||
<q-tree
|
||||
v-model:selected="selected"
|
||||
v-model:ticked="ticked"
|
||||
v-model:expanded="expanded"
|
||||
class="q-my-xs"
|
||||
:nodes="nodes"
|
||||
node-key="key"
|
||||
tick-strategy="leaf"
|
||||
selected-color="black"
|
||||
:filter="search"
|
||||
no-nodes-label="Закладок пока нет"
|
||||
no-results-label="Ничего не найдено"
|
||||
>
|
||||
<template #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 vueComponent from '../../vueComponent.js';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import Window from '../../share/Window.vue';
|
||||
import * as lu from '../linkUtils';
|
||||
import rstore from '../../../store/modules/reader';
|
||||
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
watch: {
|
||||
ticked() {
|
||||
this.checkAllTicked();
|
||||
},
|
||||
}
|
||||
};
|
||||
class BookmarkSettings {
|
||||
_options = componentOptions;
|
||||
_props = {
|
||||
libs: Object,
|
||||
addBookmarkVisible: Boolean,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(BookmarkSettings);
|
||||
//-----------------------------------------------------------------------------
|
||||
</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>
|
||||
913
client/components/ExternalLibs/ExternalLibs.vue
Normal file
@@ -0,0 +1,913 @@
|
||||
<template>
|
||||
<Window ref="window" margin="2px" @close="close">
|
||||
<template #header>
|
||||
{{ header }}
|
||||
</template>
|
||||
|
||||
<template #buttons>
|
||||
<span class="header-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="header-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="header-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="header-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
|
||||
ref="rootLink"
|
||||
v-model="rootLink"
|
||||
class="q-mr-sm"
|
||||
:options="rootLinkOptions"
|
||||
style="width: 230px"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options display-value-sanitize options-sanitize
|
||||
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-plus" size="12px" @click.stop="addBookmark">
|
||||
<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" size="12px" @click.stop="bookmarkSettings">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Настроить закладки
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
<template #selected>
|
||||
<div style="overflow: hidden; white-space: nowrap;">
|
||||
{{ rootLinkWithoutProtocol }}
|
||||
</div>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<q-select
|
||||
ref="selectedLink"
|
||||
v-model="selectedLink"
|
||||
class="q-mr-sm"
|
||||
:options="selectedLinkOptions"
|
||||
style="width: 50px"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
|
||||
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
||||
>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Закладки
|
||||
</q-tooltip>
|
||||
</q-select>
|
||||
|
||||
<q-input
|
||||
ref="input"
|
||||
v-model="bookUrl"
|
||||
class="col q-mr-sm"
|
||||
outlined dense
|
||||
bg-color="white"
|
||||
placeholder="Скопируйте сюда ссылку на книгу и нажмите 'Открыть'"
|
||||
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-home" size="12px" @click="goToLink(selectedLink)">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Вернуться на стартовую страницу
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn :disabled="!bookUrl" round dense color="blue" icon="la la-angle-double-down" size="12px" @click="openBookUrlInFrame">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Загрузить URL во фрейм
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
<template #append>
|
||||
<q-btn round dense color="blue" icon="la la-cog" size="12px" @click.stop="optionsVisible = true">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Опции
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-btn :disabled="!bookUrl" color="green-7" no-caps size="14px" @click="submitUrl">
|
||||
Открыть
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Открыть в читалке
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
|
||||
<div ref="frameBox" class="col fit" 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 #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"
|
||||
v-model="bookmarkLink"
|
||||
class="col q-mr-sm"
|
||||
outlined dense
|
||||
bg-color="white"
|
||||
placeholder="Ссылка для закладки" maxlength="2000" @focus="selectAllOnFocus" @keydown="bookmarkLinkKeyDown"
|
||||
>
|
||||
</q-input>
|
||||
|
||||
<q-select
|
||||
ref="defaultRootLink"
|
||||
v-model="defaultRootLink"
|
||||
class="q-mr-sm"
|
||||
:options="defaultRootLinkOptions"
|
||||
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
|
||||
ref="bookmarkDesc"
|
||||
v-model="bookmarkDesc"
|
||||
class="col q-mr-sm"
|
||||
outlined dense
|
||||
bg-color="white"
|
||||
placeholder="Описание" style="width: 400px" maxlength="100" @focus="selectAllOnFocus" @keydown="bookmarkDescKeyDown"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
|
||||
Отмена
|
||||
</q-btn>
|
||||
<q-btn :disabled="!bookmarkLink" class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okAddBookmark">
|
||||
OK
|
||||
</q-btn>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog ref="options" v-model="optionsVisible">
|
||||
<template #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 #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"
|
||||
:add-bookmark-visible="addBookmarkVisible"
|
||||
@do-action="doAction" @close="closeBookmarkSettings"
|
||||
>
|
||||
</BookmarkSettings>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import vueComponent from '../vueComponent.js';
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window,
|
||||
Dialog,
|
||||
BookmarkSettings
|
||||
},
|
||||
watch: {
|
||||
libs() {
|
||||
this.loadLibs();
|
||||
},
|
||||
defaultRootLink() {
|
||||
this.updateBookmarkLink();
|
||||
},
|
||||
bookUrl(newValue) {
|
||||
const value = lu.addProtocol(newValue);
|
||||
const subst = this.makeProxySubst(value, true);
|
||||
if (value != subst) {
|
||||
this.$nextTick(() => {
|
||||
this.bookUrl = subst;
|
||||
});
|
||||
}
|
||||
},
|
||||
bookmarkLink(newValue) {
|
||||
const value = lu.addProtocol(newValue);
|
||||
const subst = this.makeProxySubst(value, true);
|
||||
if (value != subst) {
|
||||
this.$nextTick(() => {
|
||||
this.bookmarkLink = subst;
|
||||
});
|
||||
}
|
||||
},
|
||||
closeAfterSubmit(newValue) {
|
||||
this.commitProp('closeAfterSubmit', newValue);
|
||||
},
|
||||
openInFrameOnEnter(newValue) {
|
||||
this.commitProp('openInFrameOnEnter', newValue);
|
||||
},
|
||||
openInFrameOnAdd(newValue) {
|
||||
this.commitProp('openInFrameOnAdd', newValue);
|
||||
},
|
||||
rootLink() {
|
||||
this.rootLinkInput();
|
||||
},
|
||||
selectedLink() {
|
||||
this.selectedLinkInput();
|
||||
},
|
||||
}
|
||||
};
|
||||
class ExternalLibs {
|
||||
_options = componentOptions;
|
||||
|
||||
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.addEventHook('key', this.keyHook);
|
||||
|
||||
this.$root.addEventHook('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() {
|
||||
(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.setAppTitle(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(async() => {
|
||||
await this.$refs.dialogAddBookmark.waitShown();
|
||||
this.$refs.bookmarkLink.focus();
|
||||
});
|
||||
}
|
||||
|
||||
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 = '';
|
||||
}
|
||||
}
|
||||
|
||||
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.$root.sanitize(item.c)}' на '${this.$root.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.getRootRoute() == '/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;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(ExternalLibs);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.separator {
|
||||
height: 1px;
|
||||
background-color: #A0A0A0;
|
||||
}
|
||||
|
||||
.header-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-button:hover {
|
||||
color: white;
|
||||
background-color: #39902F;
|
||||
}
|
||||
|
||||
.transparent-layout {
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
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,20 +1,19 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Help в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Help extends Vue {
|
||||
class Help {
|
||||
created() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(Help);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Income в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Income extends Vue {
|
||||
class Income {
|
||||
created() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(Income);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Страница не найдена
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class NotFound404 extends Vue {
|
||||
class NotFound404 {
|
||||
created() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(NotFound404);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -6,15 +6,12 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import {sleep} from '../../../share/utils';
|
||||
import {clickMap, clickMapText} from '../share/clickMap';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class ClickMapPage extends Vue {
|
||||
class ClickMapPage {
|
||||
fontSize = '200%';
|
||||
|
||||
created() {
|
||||
@@ -53,6 +50,8 @@ class ClickMapPage extends Vue {
|
||||
await sleep(5000);
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(ClickMapPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
543
client/components/Reader/ContentsPage/ContentsPage.vue
Normal file
@@ -0,0 +1,543 @@
|
||||
<template>
|
||||
<Window ref="window" width="600px" @close="close">
|
||||
<template #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="images" icon="la la-image" label="Изображения" />
|
||||
<q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" />
|
||||
</q-tabs>
|
||||
</div>
|
||||
|
||||
<div class="q-mb-sm" />
|
||||
|
||||
<div v-show="selectedTab == 'contents'" ref="tabPanelContents" class="tab-panel">
|
||||
<div>
|
||||
<div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
|
||||
<div :ref="`mainitem${item.key}`" class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
|
||||
<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="24px" />
|
||||
</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="24px" />
|
||||
</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="`subdiv${item.key}`" class="subitems-transition">
|
||||
<div
|
||||
v-for="subitem in item.list"
|
||||
:ref="`subitem${subitem.key}`"
|
||||
:key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}"
|
||||
>
|
||||
<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 v-show="selectedTab == 'images'" ref="tabPanelImages" class="tab-panel">
|
||||
<div>
|
||||
<div v-for="item in images" :key="item.key" class="column" style="width: 540px">
|
||||
<div :ref="`image${item.key}`" class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
|
||||
<div class="col row clickable" @click="setBookPos(item.offset)">
|
||||
<div class="image-thumb-box row justify-center items-center">
|
||||
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center">
|
||||
<i class="loading-img-icon la la-images"></i>
|
||||
</div>
|
||||
<img v-show="imageLoaded[item.id]" class="image-thumb" :src="imageSrc[item.id]" />
|
||||
</div>
|
||||
<div class="no-expand-button column justify-center items-center">
|
||||
<div class="image-num">
|
||||
{{ item.num }}
|
||||
</div>
|
||||
<div v-show="item.type == 'image/jpeg'" class="image-type it-jpg-color row justify-center">
|
||||
JPG
|
||||
</div>
|
||||
<div v-show="item.type == 'image/png'" class="image-type it-png-color row justify-center">
|
||||
PNG
|
||||
</div>
|
||||
<div v-show="!item.local" class="image-type it-net-color row justify-center">
|
||||
INET
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<div v-if="!images.length" class="column justify-center items-center" style="height: 100px">
|
||||
Изображения отсутствуют
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="selectedTab == 'bookmarks'" class="tab-panel">
|
||||
<div class="column justify-center items-center" style="height: 100px">
|
||||
Раздел находится в разработке
|
||||
</div>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
//import _ from 'lodash';
|
||||
|
||||
import Window from '../../share/Window.vue';
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
watch: {
|
||||
bookPos() {
|
||||
this.updateBookPosSelection();
|
||||
},
|
||||
selectedTab() {
|
||||
this.updateBookPosScrollTop();
|
||||
},
|
||||
},
|
||||
};
|
||||
class ContentsPage {
|
||||
_options = componentOptions;
|
||||
_props = {
|
||||
bookPos: Number,
|
||||
isVisible: Boolean,
|
||||
};
|
||||
|
||||
selectedTab = 'contents';
|
||||
contents = [];
|
||||
images = [];
|
||||
imageSrc = [];
|
||||
imageLoaded = [];
|
||||
|
||||
created() {
|
||||
}
|
||||
|
||||
async init(currentBook, parsed) {
|
||||
this.$refs.window.init();
|
||||
|
||||
//закладки
|
||||
|
||||
//проверим, надо ли обновлять списки
|
||||
if (this.parsed == parsed) {
|
||||
this.updateBookPosSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
//далее формирование оглавления
|
||||
this.parsed = parsed;
|
||||
this.contents = [];
|
||||
await this.$nextTick();
|
||||
|
||||
const pc = parsed.contents;
|
||||
const ims = parsed.images;
|
||||
const newpc = [];
|
||||
if (pc.length) {//если есть оглавление
|
||||
//преобразуем все, кроме первого, разделы 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);
|
||||
}
|
||||
}
|
||||
} else {//попробуем вытащить из images
|
||||
for (let i = 0; i < ims.length; i++) {
|
||||
const image = ims[i];
|
||||
|
||||
if (image.alt) {
|
||||
newpc.push({paraIndex: image.paraIndex, title: image.alt, inset: 1, bodyIndex: 0, subtitles: []});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
//формируем newImages
|
||||
const newImages = [];
|
||||
for (i = 0; i < ims.length; i++) {
|
||||
const image = ims[i];
|
||||
const bin = parsed.binary[image.id];
|
||||
const type = (bin ? bin.type : '');
|
||||
|
||||
const label = (image.alt ? image.alt : '<span style="font-size: 90%; color: #dddddd"><i>Без названия</i></span>');
|
||||
const indentStyle = getIndentStyle(1);
|
||||
const labelStyle = getLabelStyle(1);
|
||||
|
||||
const p = parsed.para[image.paraIndex];
|
||||
newImages.push({perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset,
|
||||
indentStyle, labelStyle, type, num: image.num, id: image.id, local: image.local});
|
||||
}
|
||||
|
||||
this.images = newImages;
|
||||
|
||||
if (this.selectedTab == 'contents' && !this.contents.length && this.images.length)
|
||||
this.selectedTab = 'images';
|
||||
|
||||
//выделим на bookPos
|
||||
this.updateBookPosSelection();
|
||||
|
||||
//асинхронная загрузка изображений
|
||||
this.imageSrc = [];
|
||||
this.imageLoaded = [];
|
||||
await utils.sleep(50);
|
||||
(async() => {
|
||||
for (i = 0; i < ims.length; i++) {
|
||||
const {id, local} = ims[i];
|
||||
const bin = this.parsed.binary[id];
|
||||
if (local)
|
||||
this.imageSrc[id] = (bin ? `data:${bin.type};base64,${bin.data}` : '');
|
||||
else
|
||||
this.imageSrc[id] = id;
|
||||
this.imageLoaded[id] = true;
|
||||
await utils.sleep(5);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
async updateBookPosSelection() {
|
||||
if (!this.isVisible)
|
||||
return;
|
||||
|
||||
await this.$nextTick();
|
||||
const bp = this.bookPos;
|
||||
|
||||
for (let i = 0; i < this.contents.length; i++) {
|
||||
const item = this.contents[i];
|
||||
const nextOffset = (i < this.contents.length - 1 ? this.contents[i + 1].offset : this.parsed.textLength);
|
||||
|
||||
if (bp >= item.offset && bp < nextOffset) {
|
||||
item.isBookPos = true;
|
||||
} else if (item.isBookPos) {
|
||||
item.isBookPos = false;
|
||||
}
|
||||
|
||||
for (let j = 0; j < item.list.length; j++) {
|
||||
const subitem = item.list[j];
|
||||
const nextSubOffset = (j < item.list.length - 1 ? item.list[j + 1].offset : nextOffset);
|
||||
|
||||
if (bp >= subitem.offset && bp < nextSubOffset) {
|
||||
subitem.isBookPos = true;
|
||||
this.updateBookPosScrollTop('contents', item, subitem, j);
|
||||
} else if (subitem.isBookPos) {
|
||||
subitem.isBookPos = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.images.length; i++) {
|
||||
const img = this.images[i];
|
||||
const nextOffset = (i < this.images.length - 1 ? this.images[i + 1].offset : this.parsed.textLength);
|
||||
|
||||
if (bp >= img.offset && bp < nextOffset) {
|
||||
this.images[i].isBookPos = true;
|
||||
} else if (img.isBookPos) {
|
||||
this.images[i].isBookPos = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateBookPosScrollTop();
|
||||
}
|
||||
|
||||
/*getOffsetTop(key) {
|
||||
let el = this.getFirstElem(this.$refs[`mainitem${key}`]);
|
||||
return (el ? el.offsetTop : 0);
|
||||
}*/
|
||||
|
||||
async updateBookPosScrollTop() {
|
||||
try {
|
||||
await this.$nextTick();
|
||||
|
||||
if (this.selectedTab == 'contents') {
|
||||
let item;
|
||||
let subitem;
|
||||
let i;
|
||||
|
||||
//ищем выделенные item
|
||||
for(const _item of this.contents) {
|
||||
if (_item.isBookPos) {
|
||||
item = _item;
|
||||
for (let ii = 0; ii < item.list.length; ii++) {
|
||||
const _subitem = item.list[ii];
|
||||
if (_subitem.isBookPos) {
|
||||
subitem = _subitem;
|
||||
i = ii;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
//вычисляем и смещаем tabPanel.scrollTop
|
||||
let el = this.getFirstElem(this.$refs[`mainitem${item.key}`]);
|
||||
let elShift = 0;
|
||||
if (subitem && item.expanded) {
|
||||
const subEl = this.getFirstElem(this.$refs[`subitem${subitem.key}`]);
|
||||
elShift = el.offsetHeight - subEl.offsetHeight*(i + 1);
|
||||
} else {
|
||||
elShift = el.offsetHeight;
|
||||
}
|
||||
|
||||
const tabPanel = this.$refs.tabPanelContents;
|
||||
const halfH = tabPanel.clientHeight/2;
|
||||
const newScrollTop = el.offsetTop - halfH - elShift;
|
||||
if (newScrollTop < 20 + tabPanel.scrollTop - halfH || newScrollTop > -20 + tabPanel.scrollTop + halfH)
|
||||
tabPanel.scrollTop = newScrollTop;
|
||||
}
|
||||
|
||||
if (this.selectedTab == 'images') {
|
||||
let item;
|
||||
|
||||
//ищем выделенные item
|
||||
for(const _item of this.images) {
|
||||
if (_item.isBookPos) {
|
||||
item = _item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
//вычисляем и смещаем tabPanel.scrollTop
|
||||
let el = this.getFirstElem(this.$refs[`image${item.key}`]);
|
||||
|
||||
const tabPanel = this.$refs.tabPanelImages;
|
||||
const halfH = tabPanel.clientHeight/2;
|
||||
const newScrollTop = el.offsetTop - halfH - el.offsetHeight/2;
|
||||
|
||||
if (newScrollTop < 20 + tabPanel.scrollTop - halfH || newScrollTop > -20 + tabPanel.scrollTop + halfH)
|
||||
tabPanel.scrollTop = newScrollTop;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
getFirstElem(items) {
|
||||
return (Array.isArray(items) ? items[0] : items);
|
||||
}
|
||||
|
||||
async expandClick(key) {
|
||||
const item = this.contents[key];
|
||||
const expanded = !item.expanded;
|
||||
|
||||
if (!expanded) {
|
||||
let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
|
||||
subdiv.style.height = '0';
|
||||
await utils.sleep(200);
|
||||
}
|
||||
|
||||
this.contents[key].expanded = expanded;
|
||||
|
||||
if (expanded) {
|
||||
await this.$nextTick();
|
||||
let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
|
||||
subdiv.style.height = subdiv.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;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(ContentsPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</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, .item-book-pos, .subitem-book-pos {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.item:hover, .subitem:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.item-book-pos {
|
||||
background-color: #b0f0b0;
|
||||
}
|
||||
|
||||
.subitem-book-pos {
|
||||
background-color: #d0f5d0;
|
||||
}
|
||||
|
||||
.item-book-pos:hover {
|
||||
background-color: #b0e0b0;
|
||||
}
|
||||
|
||||
.subitem-book-pos:hover {
|
||||
background-color: #d0f0d0;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.image-num {
|
||||
font-size: 120%;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
.image-type {
|
||||
border: 1px solid black;
|
||||
border-radius: 6px;
|
||||
font-size: 80%;
|
||||
padding: 2px 0 2px 0;
|
||||
width: 34px;
|
||||
}
|
||||
.it-jpg-color {
|
||||
background: linear-gradient(to right, #fabc3d, #ffec6d);
|
||||
}
|
||||
.it-png-color {
|
||||
background: linear-gradient(to right, #4bc4e5, #6bf4ff);
|
||||
}
|
||||
.it-net-color {
|
||||
background: linear-gradient(to right, #00c400, #00f400);
|
||||
}
|
||||
|
||||
.image-thumb-box {
|
||||
width: 120px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-thumb {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.loading-img-icon {
|
||||
font-size: 250%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
Скопировать текст
|
||||
</template>
|
||||
|
||||
@@ -12,18 +12,19 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import Window from '../../share/Window.vue';
|
||||
import {sleep} from '../../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
})
|
||||
class CopyTextPage extends Vue {
|
||||
};
|
||||
class CopyTextPage {
|
||||
_options = componentOptions;
|
||||
|
||||
text = null;
|
||||
initStep = null;
|
||||
initPercentage = 0;
|
||||
@@ -91,16 +92,18 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(CopyTextPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,12 +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>
|
||||
@@ -18,19 +18,24 @@
|
||||
<li>поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий</li>
|
||||
</ul>
|
||||
|
||||
<p>В качестве URL книги можно задавать html-страничку с книгой, либо прямую ссылку
|
||||
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").</p>
|
||||
<p>
|
||||
В качестве URL книги можно задавать html-страничку с книгой, либо прямую ссылку
|
||||
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").
|
||||
</p>
|
||||
<p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p>
|
||||
|
||||
<div v-show="mode == 'omnireader'">
|
||||
<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
||||
<br><strong>javascript:location.href='https://omnireader.ru/?url='+location.href;</strong>
|
||||
|
||||
<span class="clickable" @click="copyText('javascript:location.href=\'https://omnireader.ru/?url=\'+location.href;', 'Код для адреса закладки успешно скопирован в буфер обмена')">
|
||||
(скопировать)
|
||||
</span>
|
||||
<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="javascript:location.href='https://omnireader.ru/?url='+location.href;">Omni Reader</a>
|
||||
<br><a style="margin-left: 50px" :href="bookmarkText">{{ (mode == 'omnireader' ? 'Omni' : 'Liberama') }} Reader</a>
|
||||
<br>Тогда, активировав получившуюся закладку на любой странице интернета, вы автоматически загрузите эту страницу в Omni Reader.
|
||||
<br>В Chrome для Android можно вызывать такую закладку по имени прямо в адресной строке браузера (имя стоит сделать попроще).
|
||||
</p>
|
||||
@@ -41,14 +46,11 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../../vueComponent.js';
|
||||
|
||||
import {copyTextToClipboard} from '../../../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class CommonHelpPage extends Vue {
|
||||
class CommonHelpPage {
|
||||
created() {
|
||||
}
|
||||
|
||||
@@ -56,34 +58,36 @@ class CommonHelpPage extends Vue {
|
||||
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.$notify.success({message: msg});
|
||||
this.$root.notify.success(msg);
|
||||
else
|
||||
this.$notify.error({message: msg});
|
||||
this.$root.notify.error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(CommonHelpPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
.copy-icon {
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 120%;
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,30 +1,70 @@
|
||||
<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>
|
||||
<img class="logo" src="./assets/yoomoney.png">
|
||||
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYooMoney">
|
||||
Пожертвовать
|
||||
</q-btn><br>
|
||||
<div class="para">
|
||||
{{ yooAddress }}
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yooAddress, 'Кошелёк ЮMoney')">
|
||||
<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>
|
||||
@@ -32,14 +72,13 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../../vueComponent.js';
|
||||
|
||||
import {copyTextToClipboard} from '../../../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class DonateHelpPage extends Vue {
|
||||
yandexAddress = '410018702323056';
|
||||
class DonateHelpPage {
|
||||
yooAddress = '410018702323056';
|
||||
paypalAddress = 'bookpauk@gmail.com';
|
||||
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
|
||||
litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ';
|
||||
moneroAddress = '8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz';
|
||||
@@ -47,29 +86,29 @@ class DonateHelpPage extends Vue {
|
||||
created() {
|
||||
}
|
||||
|
||||
donateYandexMoney() {
|
||||
window.open(`https://money.yandex.ru/to/${this.yandexAddress}`, '_blank');
|
||||
donateYooMoney() {
|
||||
window.open(`https://yoomoney.ru/to/${this.yooAddress}`, '_blank');
|
||||
}
|
||||
|
||||
async copyAddress(address, prefix) {
|
||||
const result = await copyTextToClipboard(address);
|
||||
if (result)
|
||||
this.$notify.success({message: `${prefix}-адрес ${address} успешно скопирован в буфер обмена`});
|
||||
this.$root.notify.success(`${prefix} ${address} успешно скопирован в буфер обмена`);
|
||||
else
|
||||
this.$notify.error({message: 'Копирование не удалось'});
|
||||
this.$root.notify.error('Копирование не удалось');
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(DonateHelpPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.p {
|
||||
@@ -79,15 +118,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;
|
||||
@@ -97,13 +131,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>
|
||||
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
@@ -1,87 +1,103 @@
|
||||
<template>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
<template #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="releases">
|
||||
<VersionHistoryPage></VersionHistoryPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Помочь проекту" name="donate">
|
||||
<DonateHelpPage></DonateHelpPage>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="col column" style="min-width: 600px">
|
||||
<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="bg-grey-4 text-grey-7"
|
||||
>
|
||||
<q-tab v-for="btn in buttons" :key="btn.value" :name="btn.value" :label="btn.label" />
|
||||
</q-tabs>
|
||||
</div>
|
||||
|
||||
<keep-alive>
|
||||
<component :is="activePage" ref="page" class="col"></component>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
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 DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
||||
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
|
||||
//import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
Window,
|
||||
CommonHelpPage,
|
||||
HotkeysHelpPage,
|
||||
MouseHelpPage,
|
||||
DonateHelpPage,
|
||||
VersionHistoryPage,
|
||||
},
|
||||
})
|
||||
class HelpPage extends Vue {
|
||||
selectedTab = null;
|
||||
const pages = {
|
||||
'CommonHelpPage': CommonHelpPage,
|
||||
'HotkeysHelpPage': HotkeysHelpPage,
|
||||
'MouseHelpPage': MouseHelpPage,
|
||||
'VersionHistoryPage': VersionHistoryPage,
|
||||
//'DonateHelpPage': DonateHelpPage,
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
['CommonHelpPage', 'Общее'],
|
||||
['MouseHelpPage', 'Мышь/тачскрин'],
|
||||
['HotkeysHelpPage', 'Клавиатура'],
|
||||
['VersionHistoryPage', 'История версий'],
|
||||
//['DonateHelpPage', 'Помочь проекту'],
|
||||
];
|
||||
|
||||
const componentOptions = {
|
||||
components: Object.assign({ Window }, pages),
|
||||
};
|
||||
class HelpPage {
|
||||
_options = componentOptions;
|
||||
|
||||
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 = 'releases';
|
||||
this.selectedTab = 'VersionHistoryPage';
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (event.type == 'keydown' && (event.code == 'Escape')) {
|
||||
if (event.type == 'keydown' && event.key == 'Escape') {
|
||||
this.close();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(HelpPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-tabs {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-tab-pane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,55 +1,54 @@
|
||||
<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>O</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>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../../vueComponent.js';
|
||||
|
||||
import UserHotKeys from '../../SettingsPage/UserHotKeys/UserHotKeys.vue';
|
||||
|
||||
const componentOptions = {
|
||||
components: {
|
||||
UserHotKeys,
|
||||
},
|
||||
};
|
||||
class HotkeysHelpPage {
|
||||
_options = componentOptions;
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class HotkeysHelpPage extends Vue {
|
||||
created() {
|
||||
}
|
||||
|
||||
get userHotKeys() {
|
||||
return this.$store.state.reader.settings.userHotKeys;
|
||||
}
|
||||
|
||||
set userHotKeys(value) {
|
||||
//no setter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(HotkeysHelpPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
@@ -25,17 +32,18 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../../vueComponent.js';
|
||||
|
||||
import ClickMapPage from '../../ClickMapPage/ClickMapPage.vue';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
ClickMapPage,
|
||||
},
|
||||
})
|
||||
class MouseHelpPage extends Vue {
|
||||
};
|
||||
class MouseHelpPage {
|
||||
_options = componentOptions;
|
||||
|
||||
created() {
|
||||
}
|
||||
|
||||
@@ -44,22 +52,19 @@ class MouseHelpPage extends Vue {
|
||||
this.$refs.clickMapPage.$el.style.backgroundColor = '#478355';
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(MouseHelpPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<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;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<div id="versionHistoryPage" class="page">
|
||||
<span class="clickable" v-for="(item, index) in versionHeader" :key="index" @click="showRelease(item)">
|
||||
<span class="text-h6 text-bold">История версий:</span>
|
||||
<br><br>
|
||||
|
||||
<span v-for="(item, index) in versionHeader" :key="index" class="clickable" @click="showRelease(item)">
|
||||
<p>
|
||||
{{ item }}
|
||||
{{ item }}
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<br>
|
||||
<h4>История версий:</h4>
|
||||
<br>
|
||||
|
||||
<div v-for="item in versionContent" :id="item.key" :key="item.key">
|
||||
@@ -19,13 +20,11 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../../vueComponent.js';
|
||||
|
||||
import {versionHistory} from '../../versionHistory';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class VersionHistoryPage extends Vue {
|
||||
class VersionHistoryPage {
|
||||
versionHeader = [];
|
||||
versionContent = [];
|
||||
|
||||
@@ -34,14 +33,15 @@ class VersionHistoryPage extends Vue {
|
||||
|
||||
mounted() {
|
||||
let vh = [];
|
||||
for (const version of versionHistory) {
|
||||
vh.push(version.header);
|
||||
for (const v of versionHistory) {
|
||||
vh.push(`${v.version} (${v.releaseDate})`);
|
||||
}
|
||||
this.versionHeader = vh;
|
||||
|
||||
let vc = [];
|
||||
for (const version of versionHistory) {
|
||||
vc.push({key: version.header, content: 'Версия ' + version.header + version.content});
|
||||
for (const v of versionHistory) {
|
||||
let header = `${v.version} (${v.releaseDate})`;
|
||||
vc.push({key: header, content: 'Версия ' + header + v.content});
|
||||
}
|
||||
this.versionContent = vc;
|
||||
}
|
||||
@@ -53,20 +53,18 @@ class VersionHistoryPage extends Vue {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(VersionHistoryPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
135
client/components/Reader/LibsPage/LibsPage.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="hidden"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import Window from '../../share/Window.vue';
|
||||
import * as utils from '../../../share/utils';
|
||||
//import rstore from '../../../store/modules/reader';
|
||||
import _ from 'lodash';
|
||||
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window
|
||||
},
|
||||
watch: {
|
||||
libs: function() {
|
||||
this.sendLibs();
|
||||
},
|
||||
}
|
||||
};
|
||||
class LibsPage {
|
||||
_options = componentOptions;
|
||||
|
||||
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: _.cloneDeep(this.libs)});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$emit('libs-close');
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(LibsPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.separator {
|
||||
height: 1px;
|
||||
background-color: #A0A0A0;
|
||||
}
|
||||
</style>
|
||||
@@ -47,7 +47,7 @@
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
svgStyle: {
|
||||
fill: this.cornerColor,
|
||||
@@ -60,8 +60,13 @@
|
||||
},
|
||||
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'
|
||||
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: {
|
||||
@@ -99,7 +104,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
#github-corner .octo-arm {
|
||||
transform-origin: 130px 106px
|
||||
}
|
||||
@@ -122,7 +127,8 @@
|
||||
top: 0;
|
||||
border: 0;
|
||||
}
|
||||
#github-corner-svg, #github-corner-svg .octo-arm, #github-corner-svg .octo-body {
|
||||
transition: fill 1s ease;
|
||||
}
|
||||
|
||||
#github-corner-svg, #github-corner-svg .octo-arm, #github-corner-svg .octo-body {
|
||||
transition: fill 1s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,32 +1,47 @@
|
||||
<template>
|
||||
<div ref="main" class="main">
|
||||
<GithubCorner url="https://github.com/bookpauk/liberama" cornerColor="#1B695F"></GithubCorner>
|
||||
<div class="part top">
|
||||
<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" corner-color="#1B695F" git-color="#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>
|
||||
<input type="file" id="file" ref="file" @change="loadFile" style='display: none;'/>
|
||||
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
||||
<q-input
|
||||
ref="input" v-model="bookUrl" class="full-width q-px-sm" style="max-width: 700px"
|
||||
outlined dense bg-color="white" placeholder="Ссылка на книгу или веб-страницу" @keydown="onInputKeydown"
|
||||
>
|
||||
<template #append>
|
||||
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<el-button size="mini" @click="loadFileClick">
|
||||
<input
|
||||
id="file" ref="file" type="file"
|
||||
style="display: none;"
|
||||
:accept="acceptFileExt"
|
||||
@change="loadFile"
|
||||
/>
|
||||
|
||||
<div class="q-my-sm"></div>
|
||||
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
|
||||
<q-icon class="q-mr-xs" name="la la-caret-square-up" size="24px" />
|
||||
Загрузить файл с диска
|
||||
</el-button>
|
||||
<div class="space"></div>
|
||||
<el-button size="mini" @click="loadBufferClick">
|
||||
</q-btn>
|
||||
|
||||
<div class="q-my-sm"></div>
|
||||
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
|
||||
<q-icon class="q-mr-xs" name="la la-comment" size="24px" />
|
||||
Из буфера обмена
|
||||
</el-button>
|
||||
</q-btn>
|
||||
|
||||
<div class="space"></div>
|
||||
<div class="space"></div>
|
||||
<div v-if="mode == 'omnireader'">
|
||||
<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-книг онлайн. Загрузка любой страницы интернета одним кликом, синхронизация между устройствами, удобное управление, регистрация не требуется."
|
||||
@@ -34,42 +49,61 @@
|
||||
data-url="https://omnireader.ru">
|
||||
</div>
|
||||
</div>
|
||||
<div class="space"></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 v-if="mode == 'omnireader'" class="bottom-span clickable" @click="findBook">Найти книгу</span>
|
||||
<span class="bottom-span clickable" @click="openHelp">Справка</span>
|
||||
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
|
||||
<!--span class="bottom-span clickable" @click="openDonate">Помочь проекту</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>
|
||||
|
||||
<Dialog ref="dialog1" v-model="findBookVisible">
|
||||
<template #header>
|
||||
Подсказка ;-)
|
||||
</template>
|
||||
|
||||
<div style="word-break: normal">
|
||||
Если вы хотите найти определенную книгу, добро пожаловать в
|
||||
раздел "Сетевая библиотека" (кнопка <q-icon name="la la-sitemap" size="32px" />) на сайте читалки
|
||||
<a href="https://liberama.top" target="_blank">liberama.top</a>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import GithubCorner from './GithubCorner/GithubCorner.vue';
|
||||
|
||||
import Dialog from '../../share/Dialog.vue';
|
||||
import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
|
||||
import {versionHistory} from '../versionHistory';
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
GithubCorner,
|
||||
Dialog,
|
||||
PasteTextPage,
|
||||
},
|
||||
})
|
||||
class LoaderPage extends Vue {
|
||||
};
|
||||
class LoaderPage {
|
||||
_options = componentOptions;
|
||||
|
||||
bookUrl = null;
|
||||
loadPercent = 0;
|
||||
pasteTextActive = false;
|
||||
findBookVisible = false;
|
||||
|
||||
created() {
|
||||
this.commit = this.$store.commit;
|
||||
@@ -77,8 +111,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
|
||||
/*if (this.mode == 'omnireader')
|
||||
Ya.share2(this.$refs.yaShare2);// eslint-disable-line no-undef*/
|
||||
}
|
||||
|
||||
activated() {
|
||||
@@ -88,6 +122,8 @@ class LoaderPage extends Vue {
|
||||
get title() {
|
||||
if (this.mode == 'omnireader')
|
||||
return 'Omni Reader - браузерная онлайн-читалка.';
|
||||
if (this.mode == 'liberama.top')
|
||||
return 'Liberama Reader - браузерная онлайн-читалка.';
|
||||
return 'Универсальная читалка книг и ресурсов интернета.';
|
||||
|
||||
}
|
||||
@@ -100,19 +136,21 @@ class LoaderPage extends Vue {
|
||||
return this.$store.state.config.version;
|
||||
}
|
||||
|
||||
get acceptFileExt() {
|
||||
return this.$store.state.config.acceptFileExt;
|
||||
}
|
||||
|
||||
get isExternalConverter() {
|
||||
return this.$store.state.config.useExternalBookConverter;
|
||||
}
|
||||
|
||||
get clientVersion() {
|
||||
let v = versionHistory[0].header;
|
||||
v = v.split(' ')[0];
|
||||
return v;
|
||||
return versionHistory[0].version;
|
||||
}
|
||||
|
||||
submitUrl() {
|
||||
if (this.bookUrl) {
|
||||
this.$emit('load-book', {url: this.bookUrl});
|
||||
this.$emit('load-book', {url: this.bookUrl, force: true});
|
||||
this.bookUrl = '';
|
||||
}
|
||||
}
|
||||
@@ -129,7 +167,7 @@ class LoaderPage extends Vue {
|
||||
}
|
||||
|
||||
loadBufferClick() {
|
||||
this.pasteTextToggle();
|
||||
this.showPasteText();
|
||||
}
|
||||
|
||||
loadBuffer(opts) {
|
||||
@@ -139,18 +177,26 @@ class LoaderPage extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
showPasteText() {
|
||||
this.pasteTextActive = true;
|
||||
}
|
||||
|
||||
pasteTextToggle() {
|
||||
this.pasteTextActive = !this.pasteTextActive;
|
||||
}
|
||||
|
||||
openHelp() {
|
||||
this.$emit('help-toggle');
|
||||
openHelp(event) {
|
||||
this.$emit('do-action', {action: 'help', event});
|
||||
}
|
||||
|
||||
openDonate() {
|
||||
this.$emit('donate-toggle');
|
||||
this.$emit('do-action', {action: 'donate'});
|
||||
}
|
||||
|
||||
findBook() {
|
||||
this.findBookVisible = true;
|
||||
}
|
||||
|
||||
openComments() {
|
||||
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
|
||||
}
|
||||
@@ -159,89 +205,46 @@ class LoaderPage extends Vue {
|
||||
window.open('http://old.omnireader.ru', '_blank');
|
||||
}
|
||||
|
||||
async onInputKeydown(event) {
|
||||
if (event.key == 'Enter') {
|
||||
await utils.sleep(100);
|
||||
this.submitUrl();
|
||||
}
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (this.$refs.dialog1.active)
|
||||
return true;
|
||||
|
||||
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') {
|
||||
this.submitUrl();
|
||||
}
|
||||
|
||||
if (event.type == 'keydown' && (event.code == 'F1' || (document.activeElement !== input && event.code == 'KeyH'))) {
|
||||
this.$emit('help-toggle');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const input = this.$refs.input.getNativeElement();
|
||||
if (event.type == 'keydown' && (document.activeElement === input || event.code == 'Enter') && event.code != 'Escape')
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.type == 'keydown' && (document.activeElement !== input && event.code == 'KeyQ')) {
|
||||
this.$emit('tool-bar-toggle');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(LoaderPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
<style scoped>
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 480px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.top {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.center {
|
||||
justify-content: flex-start;
|
||||
padding: 0 10px 0 10px;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.bottom-span {
|
||||
font-size: 70%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.space {
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,36 +1,35 @@
|
||||
<template>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
<span style="position: relative; top: -3px">
|
||||
Вставьте текст и нажмите
|
||||
<span class="clickable" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
|
||||
<span class="clickable text-primary" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
|
||||
или F2
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<el-input placeholder="Введите название текста" class="input" v-model="bookTitle"></el-input>
|
||||
</div>
|
||||
<hr/>
|
||||
<q-input v-model="bookTitle" class="q-px-sm" dense borderless placeholder="Введите название текста" />
|
||||
<hr />
|
||||
<textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../../vueComponent.js';
|
||||
|
||||
import Window from '../../../share/Window.vue';
|
||||
import _ from 'lodash';
|
||||
import * as utils from '../../../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
})
|
||||
class PasteTextPage extends Vue {
|
||||
};
|
||||
class PasteTextPage {
|
||||
_options = componentOptions;
|
||||
|
||||
bookTitle = '';
|
||||
|
||||
created() {
|
||||
@@ -61,16 +60,20 @@ class PasteTextPage extends Vue {
|
||||
|
||||
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(' - ');
|
||||
this.bookTitle = `Из буфера обмена ${utils.formatDate(new Date(), 'noDate')}`;
|
||||
if (event) {
|
||||
let text = event.clipboardData.getData('text');
|
||||
this.bookTitle += ': ' + _.compact([
|
||||
this.getNonEmptyLine3words(text, 1),
|
||||
this.getNonEmptyLine3words(text, 2)
|
||||
]).join(' - ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadBuffer() {
|
||||
this.$emit('load-buffer', {buffer: `<cut-title>${this.bookTitle}</cut-title>${this.$refs.textArea.value}`});
|
||||
this.calcTitle();
|
||||
this.$emit('load-buffer', {buffer: `<buffer><fb2-title>${utils.escapeXml(this.bookTitle)}</fb2-title>${utils.escapeXml(this.$refs.textArea.value)}</buffer>`});
|
||||
this.close();
|
||||
}
|
||||
|
||||
@@ -80,7 +83,7 @@ class PasteTextPage extends Vue {
|
||||
|
||||
keyHook(event) {
|
||||
if (event.type == 'keydown') {
|
||||
switch (event.code) {
|
||||
switch (event.key) {
|
||||
case 'F2':
|
||||
this.loadBuffer();
|
||||
break;
|
||||
@@ -92,6 +95,8 @@ class PasteTextPage extends Vue {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(PasteTextPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,21 +1,39 @@
|
||||
<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" style="background-color: rgba(0, 0, 0, 0.8); z-index: 100;">
|
||||
<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>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
const ruMessage = {
|
||||
'start': ' ',
|
||||
'finish': ' ',
|
||||
'error': ' ',
|
||||
'queue': 'очередь',
|
||||
'download': 'скачивание',
|
||||
'decompress': 'распаковка',
|
||||
'convert': 'конвертирование',
|
||||
@@ -24,76 +42,59 @@ const ruMessage = {
|
||||
'upload': 'отправка',
|
||||
};
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class ProgressPage extends Vue {
|
||||
class ProgressPage {
|
||||
text = '';
|
||||
totalSteps = 1;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(ProgressPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</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 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
247
client/components/Reader/ReaderDialogs/ReaderDialogs.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<div>
|
||||
<Dialog ref="dialog1" v-model="whatsNewVisible">
|
||||
<template #header>
|
||||
Что нового:
|
||||
</template>
|
||||
|
||||
<div style="line-height: 20px; min-width: 300px">
|
||||
<div v-html="whatsNewContent"></div>
|
||||
</div>
|
||||
|
||||
<span class="clickable" style="font-size: 13px" @click="openVersionHistory">Посмотреть историю версий</span>
|
||||
|
||||
<template #footer>
|
||||
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">
|
||||
Больше не показывать
|
||||
</q-btn>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog ref="dialog2" v-model="donationVisible">
|
||||
<template #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 @click="openDonate">
|
||||
Помочь проекту
|
||||
</q-btn-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #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>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog ref="dialog3" v-model="urlHelpVisible">
|
||||
<template #header>
|
||||
Обнаружена невалидная ссылка в поле "URL книги".
|
||||
<br>
|
||||
</template>
|
||||
|
||||
<div style="word-break: normal">
|
||||
Если вы хотите найти определенную книгу и открыть в читалке, добро пожаловать в
|
||||
раздел "Сетевая библиотека" (кнопка <q-icon name="la la-sitemap" size="32px" />) на сайте
|
||||
<a href="https://liberama.top" target="_blank">liberama.top</a>
|
||||
|
||||
<br><br>
|
||||
Если же вы пытаетесь вставить текст в читалку из буфера обмена, пожалуйста воспользуйтесь кнопкой
|
||||
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
|
||||
<q-icon class="q-mr-xs" name="la la-comment" size="24px" />
|
||||
Из буфера обмена
|
||||
</q-btn>
|
||||
на странице загрузки.
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import Dialog from '../../share/Dialog.vue';
|
||||
import * as utils from '../../../share/utils';
|
||||
import {versionHistory} from '../versionHistory';
|
||||
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Dialog
|
||||
},
|
||||
watch: {
|
||||
settings: function() {
|
||||
this.loadSettings();
|
||||
},
|
||||
},
|
||||
};
|
||||
class ReaderDialogs {
|
||||
_options = componentOptions;
|
||||
|
||||
whatsNewVisible = false;
|
||||
whatsNewContent = '';
|
||||
donationVisible = false;
|
||||
urlHelpVisible = false;
|
||||
|
||||
created() {
|
||||
this.commit = this.$store.commit;
|
||||
this.loadSettings();
|
||||
}
|
||||
|
||||
mounted() {
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.showWhatsNew();
|
||||
await this.showDonation();
|
||||
}
|
||||
|
||||
loadSettings() {
|
||||
const settings = this.settings;
|
||||
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
||||
this.showDonationDialog2020 = settings.showDonationDialog2020;
|
||||
}
|
||||
|
||||
async showWhatsNew() {
|
||||
const whatsNew = versionHistory[0];
|
||||
if (this.showWhatsNewDialog &&
|
||||
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
|
||||
this.whatsNewHeader != this.whatsNewContentHash) {
|
||||
await utils.sleep(2000);
|
||||
this.whatsNewContent = 'Версия ' + this.whatsNewHeader + 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;
|
||||
}
|
||||
}
|
||||
|
||||
async showUrlHelp() {
|
||||
this.urlHelpVisible = true;
|
||||
}
|
||||
|
||||
loadBufferClick() {
|
||||
this.$emit('load-buffer-toggle');
|
||||
this.urlHelpVisible = false;
|
||||
}
|
||||
|
||||
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.$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;
|
||||
this.commit('reader/setWhatsNewContentHash', this.whatsNewHeader);
|
||||
}
|
||||
|
||||
get whatsNewHeader() {
|
||||
return `${versionHistory[0].version} (${versionHistory[0].releaseDate})`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
keyHook() {
|
||||
if (this.$refs.dialog1.active || this.$refs.dialog2.active || this.$refs.dialog3.active)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(ReaderDialogs);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.clickable {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
cursor: pointer;
|
||||
font-size: 120%;
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
||||
@@ -1,335 +1,667 @@
|
||||
<template>
|
||||
<Window width="600px" ref="window" @close="close">
|
||||
<template slot="header">
|
||||
<span v-show="!loading">Последние {{tableData ? tableData.length : 0}} открытых книг</span>
|
||||
<span v-show="loading"><i class="el-icon-loading" style="font-size: 25px"></i> <span style="position: relative; top: -4px">Список загружается</span></span>
|
||||
<Window ref="window" width="600px" @close="close">
|
||||
<template #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;'></a>
|
||||
<el-table
|
||||
:data="tableData"
|
||||
style="width: 570px"
|
||||
size="mini"
|
||||
height="1px"
|
||||
stripe
|
||||
border
|
||||
:default-sort = "{prop: 'touchDateTime', order: 'descending'}"
|
||||
:header-cell-style = "headerCellStyle"
|
||||
:row-key = "rowKey"
|
||||
<a ref="download" style="display: none;" target="_blank"></a>
|
||||
|
||||
<div id="vs-container" ref="vsContainer" class="recent-books-scroll col">
|
||||
<div ref="header" class="scroll-header row bg-blue-2">
|
||||
<q-btn class="tool-button" round @click="showSameBookClick">
|
||||
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': showSameBook}" color="green-8" size="24px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Показать/скрыть версии книг
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn class="tool-button" round @click="scrollToBegin">
|
||||
<q-icon name="la la-arrow-up" color="green-8" size="24px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
В начало списка
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn class="tool-button" round @click="scrollToEnd">
|
||||
<q-icon name="la la-arrow-down" color="green-8" size="24px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
В конец списка
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn class="tool-button" round @click="scrollToActiveBook">
|
||||
<q-icon name="la la-location-arrow" color="green-8" size="24px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
На текущую книгу
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-select
|
||||
ref="sortMethod"
|
||||
v-model="sortMethod"
|
||||
class="q-ml-md q-mt-xs"
|
||||
:options="sortMethodOptions"
|
||||
style="width: 180px"
|
||||
bg-color="white"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options display-value-sanitize options-sanitize
|
||||
options-html display-value-html
|
||||
|
||||
@update:model-value="sortMethodSelected"
|
||||
>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Метод сортировки
|
||||
</q-tooltip>
|
||||
|
||||
<template #selected-item="scope">
|
||||
<div style="height: 28px; padding-top: 2px; overflow: hidden" v-html="scope.opt.label" />
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<q-input
|
||||
ref="input"
|
||||
v-model="search"
|
||||
class="q-ml-sm q-mt-xs"
|
||||
outlined dense
|
||||
style="width: 180px"
|
||||
bg-color="white"
|
||||
placeholder="Найти"
|
||||
@click.stop
|
||||
>
|
||||
<template #append>
|
||||
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click.stop="resetSearch" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<q-virtual-scroll
|
||||
ref="virtualScroll"
|
||||
v-slot="{ item, index }"
|
||||
:items="tableData"
|
||||
scroll-target="#vs-container"
|
||||
virtual-scroll-item-size="80"
|
||||
@virtual-scroll="onScroll"
|
||||
>
|
||||
|
||||
<el-table-column
|
||||
type="index"
|
||||
width="35px"
|
||||
>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="touchDateTime"
|
||||
min-width="85px"
|
||||
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 class="table-row row" :class="{even: index % 2 > 0, 'active-book': item.active, 'active-parent-book': item.activeParent}">
|
||||
<div v-show="item.inGroup" class="row-part column justify-center items-center" style="width: 40px; border-right: 1px solid #cccccc">
|
||||
<q-icon name="la la-code-branch" size="24px" style="color: green" />
|
||||
</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; vertical-align: bottom; margin-top: 20px; padding: 0 10px 0 10px"
|
||||
:value="search" @input="search = $event.target.value"
|
||||
/>
|
||||
<div class="row-part column justify-center items-stretch" style="width: 80px">
|
||||
<div class="col row justify-center items-center clickable" @click="loadBook(item)">
|
||||
<q-icon name="la la-book" size="40px" style="color: #dddddd" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table-column
|
||||
min-width="280px"
|
||||
>
|
||||
<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 v-show="!showSameBook && item.group && item.group.length > 0" class="row justify-center" style="font-size: 70%">
|
||||
{{ (item.group ? item.group.length + 1 : 0) }} верси{{ wordEnding((item.group ? item.group.length + 1 : 0), 1) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</div>
|
||||
|
||||
<el-table-column
|
||||
min-width="90px"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<a v-show="isUrl(scope.row.url)" :href="scope.row.url" target="_blank">Оригинал</a><br>
|
||||
<a :href="scope.row.path" @click.prevent="downloadBook(scope.row.path)">Скачать FB2</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<div class="row-part column items-stretch clickable break-word" :style="{ 'width': (350 - 40*(+item.inGroup)) + 'px' }" style="font-size: 75%" @click="loadBook(item)">
|
||||
<div class="row" style="font-size: 80%">
|
||||
<div class="row justify-center row-info-top" style="width: 30px">
|
||||
{{ item.num }}
|
||||
</div>
|
||||
<div class="row justify-center row-info-top" style="width: 130px">
|
||||
Читался: {{ item.touchTime }}
|
||||
</div>
|
||||
<div class="row justify-center row-info-top" style="width: 138px">
|
||||
Загружен: {{ item.loadTime }}
|
||||
</div>
|
||||
<div class="row justify-center row-info-top" style="width: 1px">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div class="col q-mt-xs" :style="{ 'width': (340 - 40*(+item.inGroup)) + 'px' }">
|
||||
<div class="text-green-10" style="font-size: 105%">
|
||||
{{ item.desc.author }}
|
||||
</div>
|
||||
<div>{{ item.desc.title }}</div>
|
||||
<!--div>{{ item.path }}</div-->
|
||||
</div>
|
||||
|
||||
</el-table-column>
|
||||
<div class="row q-mt-xs" style="font-size: 80%">
|
||||
<div class="row justify-center row-info-bottom" style="width: 60px">
|
||||
{{ item.desc.textLen }}
|
||||
</div>
|
||||
<div class="row justify-center row-info-bottom" style="width: 60px">
|
||||
{{ item.desc.perc }}
|
||||
</div>
|
||||
<div class="row justify-center row-info-bottom" style="width: 1px">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</el-table>
|
||||
<div class="read-bar" :style="`width: ${(340 - 40*(+item.inGroup))*item.readPart}px`"></div>
|
||||
</div>
|
||||
|
||||
<div class="row-part column justify-center" style="width: 80px; font-size: 75%">
|
||||
<div>
|
||||
<a v-show="isUrl(item.url)" :href="item.url" target="_blank">Оригинал</a><br><br>
|
||||
<a :href="item.path" @click.prevent="downloadBook(item.path, item.fullTitle)">Скачать FB2</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row-part column justify-center">
|
||||
<q-btn
|
||||
dense
|
||||
style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
|
||||
@click="handleDel(item.key)"
|
||||
>
|
||||
<q-icon class="la la-times" size="14px" />
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-virtual-scroll>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import path from 'path';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import path from 'path-browserify';
|
||||
import _ from 'lodash';
|
||||
|
||||
import * as utils from '../../../share/utils';
|
||||
import LockQueue from '../../../share/LockQueue';
|
||||
import Window from '../../share/Window.vue';
|
||||
import bookManager from '../share/bookManager';
|
||||
import readerApi from '../../../api/reader';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
watch: {
|
||||
search: function() {
|
||||
search() {
|
||||
this.updateTableData();
|
||||
}
|
||||
},
|
||||
sortMethod() {
|
||||
this.updateTableData();
|
||||
},
|
||||
settings() {
|
||||
this.loadSettings();
|
||||
},
|
||||
},
|
||||
})
|
||||
class RecentBooksPage extends Vue {
|
||||
};
|
||||
class RecentBooksPage {
|
||||
_options = componentOptions;
|
||||
|
||||
loading = false;
|
||||
search = null;
|
||||
search = '';
|
||||
tableData = [];
|
||||
sortMethod = '';
|
||||
showSameBook = false;
|
||||
|
||||
created() {
|
||||
this.commit = this.$store.commit;
|
||||
|
||||
this.lastScrollTop1 = 0;
|
||||
this.lastScrollTop2 = 0;
|
||||
|
||||
this.lock = new LockQueue(100);
|
||||
|
||||
this.loadSettings();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.$refs.window.init();
|
||||
|
||||
this.$nextTick(() => {
|
||||
//this.$refs.input.focus();
|
||||
//this.$refs.input.focus();//плохо на планшетах
|
||||
});
|
||||
(async() => {//отбражение подгрузки списка, иначе тормозит
|
||||
if (this.initing)
|
||||
return;
|
||||
this.initing = true;
|
||||
|
||||
await this.updateTableData(3);
|
||||
await utils.sleep(200);
|
||||
this.inited = true;
|
||||
|
||||
if (bookManager.loaded) {
|
||||
const t = Date.now();
|
||||
await this.updateTableData(10);
|
||||
if (bookManager.getSortedRecent().length > 10)
|
||||
await utils.sleep(10*(Date.now() - t));
|
||||
} else {
|
||||
let i = 0;
|
||||
let j = 5;
|
||||
while (i < 500 && !bookManager.loaded) {
|
||||
if (i % j == 0) {
|
||||
bookManager.sortedRecentCached = null;
|
||||
await this.updateTableData(100);
|
||||
j *= 2;
|
||||
}
|
||||
|
||||
await utils.sleep(100);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
(async() => {
|
||||
this.showBar();
|
||||
await this.updateTableData();
|
||||
this.initing = false;
|
||||
await this.scrollToActiveBook();
|
||||
})();
|
||||
}
|
||||
|
||||
rowKey(row) {
|
||||
return row.key;
|
||||
loadSettings() {
|
||||
const settings = this.settings;
|
||||
this.showSameBook = settings.recentShowSameBook;
|
||||
this.sortMethod = settings.recentSortMethod || 'loadTimeDesc';
|
||||
}
|
||||
|
||||
async updateTableData(limit) {
|
||||
while (this.updating) await utils.sleep(100);
|
||||
this.updating = true;
|
||||
let result = [];
|
||||
|
||||
this.loading = !!limit;
|
||||
const sorted = bookManager.getSortedRecent();
|
||||
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const book = sorted[i];
|
||||
if (book.deleted)
|
||||
continue;
|
||||
|
||||
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 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 {//TODO: убрать в будущем
|
||||
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())
|
||||
});
|
||||
|
||||
/*for (let i = 0; i < result.length; i++) {
|
||||
if (!_.isEqual(this.tableData[i], result[i])) {
|
||||
this.$set(this.tableData, i, result[i]);
|
||||
await utils.sleep(10);
|
||||
}
|
||||
}
|
||||
if (this.tableData.length > result.length)
|
||||
this.tableData.splice(result.length);*/
|
||||
|
||||
this.tableData = result;
|
||||
this.updating = false;
|
||||
get settings() {
|
||||
return this.$store.state.reader.settings;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
async updateTableData() {
|
||||
if (!this.inited)
|
||||
return;
|
||||
|
||||
async downloadBook(fb2path) {
|
||||
await this.lock.get();
|
||||
try {
|
||||
await readerApi.checkUrl(fb2path);
|
||||
let result = [];
|
||||
|
||||
const sorted = bookManager.getSortedRecent();
|
||||
const activeBook = bookManager.mostRecentBook();
|
||||
|
||||
//подготовка полей
|
||||
for (const book of sorted) {
|
||||
if (book.deleted)
|
||||
continue;
|
||||
|
||||
let d = new Date();
|
||||
d.setTime(book.touchTime);
|
||||
const touchTime = utils.formatDate(d);
|
||||
const loadTimeRaw = (book.loadTime ? book.loadTime : 0);//book.addTime);
|
||||
d.setTime(loadTimeRaw);
|
||||
const loadTime = utils.formatDate(d);
|
||||
|
||||
let readPart = 0;
|
||||
let perc = '';
|
||||
let textLen = '';
|
||||
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
|
||||
if (book.textLength) {
|
||||
readPart = p/book.textLength;
|
||||
perc = `${(readPart*100).toFixed(2)}%`;
|
||||
textLen = `${Math.floor(readPart*book.textLength/1000)}/${Math.floor(book.textLength/1000)}`;
|
||||
}
|
||||
|
||||
const bt = utils.getBookTitle(book.fb2);
|
||||
|
||||
let title = bt.bookTitle;
|
||||
title = (title ? `"${title}"`: '');
|
||||
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : (book.uploadFileName ? book.uploadFileName : book.url)));
|
||||
|
||||
result.push({
|
||||
touchTime,
|
||||
loadTime,
|
||||
desc: {
|
||||
author,
|
||||
title,
|
||||
perc,
|
||||
textLen,
|
||||
},
|
||||
readPart,
|
||||
url: book.url,
|
||||
path: book.path,
|
||||
fullTitle: bt.fullTitle,
|
||||
key: book.key,
|
||||
sameBookKey: book.sameBookKey,
|
||||
active: (activeBook.key == book.key),
|
||||
activeParent: false,
|
||||
inGroup: false,
|
||||
|
||||
//для сортировки
|
||||
loadTimeRaw,
|
||||
touchTimeRaw: book.touchTime,
|
||||
});
|
||||
}
|
||||
|
||||
//нумерация
|
||||
let num = 0;
|
||||
|
||||
result.sort((a, b) => b.loadTimeRaw - a.loadTimeRaw);
|
||||
for (const book of result) {
|
||||
num++;
|
||||
book.num = num;
|
||||
}
|
||||
|
||||
//фильтрация
|
||||
const search = this.search;
|
||||
if (search) {
|
||||
result = result.filter(item => {
|
||||
return !search ||
|
||||
item.touchTime.includes(search) ||
|
||||
item.loadTime.includes(search) ||
|
||||
item.desc.title.toLowerCase().includes(search.toLowerCase()) ||
|
||||
item.desc.author.toLowerCase().includes(search.toLowerCase())
|
||||
});
|
||||
}
|
||||
|
||||
//сортировка
|
||||
switch (this.sortMethod) {
|
||||
case 'loadTimeDesc':
|
||||
result.sort((a, b) => b.loadTimeRaw - a.loadTimeRaw);
|
||||
break;
|
||||
case 'loadTimeAsc':
|
||||
result.sort((a, b) => a.loadTimeRaw - b.loadTimeRaw);
|
||||
break;
|
||||
case 'touchTimeDesc':
|
||||
result.sort((a, b) => b.touchTimeRaw - a.touchTimeRaw);
|
||||
break;
|
||||
case 'touchTimeAsc':
|
||||
result.sort((a, b) => a.touchTimeRaw - b.touchTimeRaw);
|
||||
break;
|
||||
case 'authorDesc':
|
||||
result.sort((a, b) => b.desc.author.localeCompare(a.desc.author));
|
||||
break;
|
||||
case 'authorAsc':
|
||||
result.sort((a, b) => a.desc.author.localeCompare(b.desc.author));
|
||||
break;
|
||||
case 'titleDesc':
|
||||
result.sort((a, b) => b.desc.title.localeCompare(a.desc.title));
|
||||
break;
|
||||
case 'titleAsc':
|
||||
result.sort((a, b) => a.desc.title.localeCompare(b.desc.title));
|
||||
break;
|
||||
}
|
||||
|
||||
//группировка
|
||||
const groups = {};
|
||||
const parents = {};
|
||||
let newResult = [];
|
||||
for (const book of result) {
|
||||
if (book.sameBookKey !== undefined) {
|
||||
if (!groups[book.sameBookKey]) {
|
||||
groups[book.sameBookKey] = [];
|
||||
parents[book.sameBookKey] = book;
|
||||
|
||||
book.group = groups[book.sameBookKey];
|
||||
newResult.push(book);
|
||||
} else {
|
||||
book.inGroup = true;
|
||||
if (book.active)
|
||||
parents[book.sameBookKey].activeParent = true;
|
||||
|
||||
groups[book.sameBookKey].push(book);
|
||||
}
|
||||
} else {
|
||||
newResult.push(book);
|
||||
}
|
||||
}
|
||||
result = newResult;
|
||||
|
||||
//showSameBook
|
||||
if (this.showSameBook) {
|
||||
newResult = [];
|
||||
for (const book of result) {
|
||||
newResult.push(book);
|
||||
if (book.group) {
|
||||
for (const sameBook of book.group) {
|
||||
newResult.push(sameBook);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = newResult;
|
||||
}
|
||||
|
||||
//другие стадии
|
||||
//.....
|
||||
|
||||
this.tableData = result;
|
||||
} finally {
|
||||
this.lock.ret();
|
||||
}
|
||||
}
|
||||
|
||||
resetSearch() {
|
||||
this.search = '';
|
||||
this.$refs.input.focus();
|
||||
}
|
||||
|
||||
wordEnding(num, type = 0) {
|
||||
const endings = [
|
||||
['ов', '', 'а', 'а', 'а', 'ов', 'ов', 'ов', 'ов', 'ов'],
|
||||
['й', 'я', 'и', 'и', 'и', 'й', 'й', 'й', 'й', 'й']
|
||||
];
|
||||
const deci = num % 100;
|
||||
if (deci > 10 && deci < 20) {
|
||||
return endings[type][0];
|
||||
} else {
|
||||
return endings[type][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;
|
||||
d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
|
||||
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.$alert(errMes, 'Ошибка', {type: 'error'});
|
||||
this.$root.stdDialog.alert(errMes, 'Ошибка', {color: 'negative'});
|
||||
}
|
||||
}
|
||||
|
||||
openOriginal(url) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
openFb2(path) {
|
||||
window.open(path, '_blank');
|
||||
}
|
||||
|
||||
async handleDel(key) {
|
||||
await bookManager.delRecentBook({key});
|
||||
this.updateTableData();
|
||||
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
|
||||
|
||||
if (!bookManager.mostRecentBook())
|
||||
this.close();
|
||||
}
|
||||
|
||||
loadBook(url) {
|
||||
this.$emit('load-book', {url});
|
||||
loadBook(row) {
|
||||
this.$emit('load-book', {url: row.url, path: row.path});
|
||||
this.close();
|
||||
}
|
||||
|
||||
isUrl(url) {
|
||||
if (url)
|
||||
return (url.indexOf('file://') != 0);
|
||||
return (url.indexOf('disk://') != 0);
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
showBar() {
|
||||
this.lastScrollTop1 = this.$refs.vsContainer.scrollTop;
|
||||
this.$refs.header.style.position = 'sticky';
|
||||
this.$refs.header.style.top = 0;
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
const curScrollTop = this.$refs.vsContainer.scrollTop;
|
||||
|
||||
if (this.lockScroll) {
|
||||
this.lastScrollTop1 = curScrollTop;
|
||||
return;
|
||||
}
|
||||
|
||||
if (curScrollTop - this.lastScrollTop1 > 100) {
|
||||
this.$refs.header.style.top = `-${this.$refs.header.offsetHeight}px`;
|
||||
this.$refs.header.style.transition = 'top 0.2s ease 0s';
|
||||
|
||||
this.lastScrollTop1 = curScrollTop;
|
||||
} else if (curScrollTop - this.lastScrollTop2 < 0) {
|
||||
this.$refs.header.style.position = 'sticky';
|
||||
this.$refs.header.style.top = 0;
|
||||
|
||||
this.lastScrollTop1 = curScrollTop;
|
||||
}
|
||||
|
||||
this.lastScrollTop2 = curScrollTop;
|
||||
}
|
||||
|
||||
showSameBookClick() {
|
||||
this.showSameBook = !this.showSameBook;
|
||||
|
||||
const newSettings = _.cloneDeep(this.settings);
|
||||
newSettings.recentShowSameBook = this.showSameBook;
|
||||
this.commit('reader/setSettings', newSettings);
|
||||
|
||||
this.updateTableData();
|
||||
}
|
||||
|
||||
sortMethodSelected() {
|
||||
const newSettings = _.cloneDeep(this.settings);
|
||||
newSettings.recentSortMethod = this.sortMethod;
|
||||
this.commit('reader/setSettings', newSettings);
|
||||
}
|
||||
|
||||
async scrollToActiveBook() {
|
||||
this.lockScroll = true;
|
||||
try {
|
||||
let activeIndex = -1;
|
||||
let activeParentIndex = -1;
|
||||
for (let i = 0; i < this.tableData.length; i++) {
|
||||
const book = this.tableData[i];
|
||||
if (book.active)
|
||||
activeIndex = i;
|
||||
if (book.activeParent)
|
||||
activeParentIndex = i;
|
||||
|
||||
if (activeIndex >= 0 && activeParentIndex >= 0)
|
||||
break;
|
||||
}
|
||||
|
||||
const index = (activeIndex >= 0 ? activeIndex : activeParentIndex);
|
||||
if (index >= 0) {
|
||||
this.$refs.virtualScroll.scrollTo(index, 'center');
|
||||
}
|
||||
} finally {
|
||||
await utils.sleep(100);
|
||||
this.lockScroll = false;
|
||||
}
|
||||
}
|
||||
|
||||
async scrollToBegin() {
|
||||
this.lockScroll = true;
|
||||
try {
|
||||
this.$refs.virtualScroll.scrollTo(0, 'center');
|
||||
} finally {
|
||||
await utils.sleep(100);
|
||||
this.lockScroll = false;
|
||||
}
|
||||
}
|
||||
|
||||
async scrollToEnd() {
|
||||
this.lockScroll = true;
|
||||
try {
|
||||
this.$refs.virtualScroll.scrollTo(this.tableData.length, 'center');
|
||||
} finally {
|
||||
await utils.sleep(100);
|
||||
this.lockScroll = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
get sortMethodOptions() {
|
||||
return [
|
||||
{label: '<span style="font-size: 150%">↑</span> Время загрузки', value: 'loadTimeDesc'},
|
||||
{label: '<span style="font-size: 150%">↓</span> Время загрузки', value: 'loadTimeAsc'},
|
||||
{label: '<span style="font-size: 150%">↑</span> Время чтения', value: 'touchTimeDesc'},
|
||||
{label: '<span style="font-size: 150%">↓</span> Время чтения', value: 'touchTimeAsc'},
|
||||
{label: '<span style="font-size: 150%">↑</span> Автор', value: 'authorDesc'},
|
||||
{label: '<span style="font-size: 150%">↓</span> Автор', value: 'authorAsc'},
|
||||
{label: '<span style="font-size: 150%">↑</span> Название', value: 'titleDesc'},
|
||||
{label: '<span style="font-size: 150%">↓</span> Название', value: 'titleAsc'},
|
||||
];
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$emit('recent-books-toggle');
|
||||
this.$emit('recent-books-close');
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (event.type == 'keydown' && event.code == 'Escape') {
|
||||
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
|
||||
this.close();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(RecentBooksPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.desc {
|
||||
.recent-books-scroll {
|
||||
width: 573px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.scroll-header {
|
||||
height: 50px;
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
border-bottom: 2px solid #aaaaaa;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
min-height: 80px;
|
||||
border-bottom: 1px solid #cccccc;
|
||||
}
|
||||
|
||||
.row-part {
|
||||
padding: 4px 4px 4px 4px;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
.break-word {
|
||||
line-height: 180%;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.read-bar {
|
||||
height: 3px;
|
||||
background-color: #aaaaaa;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.even {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
.active-book {
|
||||
background-color: #b0f0b0 !important;
|
||||
}
|
||||
|
||||
.active-parent-book {
|
||||
background-color: #ffbbbb !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.expanded-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.row-info-top {
|
||||
line-height: 110%;
|
||||
border-left: 1px solid #cccccc;
|
||||
border-bottom: 1px solid #cccccc;
|
||||
}
|
||||
|
||||
.row-info-bottom {
|
||||
line-height: 110%;
|
||||
border: 1px solid #cccccc;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.tool-button {
|
||||
min-width: 30px;
|
||||
width: 30px;
|
||||
min-height: 30px;
|
||||
height: 30px;
|
||||
margin: 10px 6px 0px 3px;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
{{ header }}
|
||||
</template>
|
||||
|
||||
@@ -8,28 +8,36 @@
|
||||
<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>
|
||||
<q-input
|
||||
ref="input" v-model="needle"
|
||||
class="col" outlined dense
|
||||
placeholder="Найти"
|
||||
@keydown="inputKeyDown"
|
||||
/>
|
||||
<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>
|
||||
<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>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import Window from '../../share/Window.vue';
|
||||
import {sleep} from '../../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
@@ -39,11 +47,16 @@ 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';
|
||||
},
|
||||
},
|
||||
})
|
||||
class SearchPage extends Vue {
|
||||
};
|
||||
class SearchPage {
|
||||
_options = componentOptions;
|
||||
|
||||
header = null;
|
||||
initStep = null;
|
||||
initPercentage = 0;
|
||||
@@ -93,7 +106,7 @@ class SearchPage extends Vue {
|
||||
this.parsed = parsed;
|
||||
}
|
||||
|
||||
this.header = 'Найти';
|
||||
this.header = 'Поиск в тексте';
|
||||
await this.$nextTick();
|
||||
this.$refs.input.focus();
|
||||
this.$refs.input.select();
|
||||
@@ -157,21 +170,24 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(SearchPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -194,17 +210,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;
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
width: 50px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div></div>
|
||||
<div class="hidden"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import bookManager from '../share/bookManager';
|
||||
@@ -18,7 +18,7 @@ const ssCacheStore = localForage.createInstance({
|
||||
name: 'ssCacheStore'
|
||||
});
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
watch: {
|
||||
serverSyncEnabled: function() {
|
||||
this.serverSyncEnabledChanged();
|
||||
@@ -35,41 +35,51 @@ export default @Component({
|
||||
currentProfile: function() {
|
||||
this.currentProfileChanged(true);
|
||||
},
|
||||
libs: function() {
|
||||
this.debouncedSaveLibs();
|
||||
},
|
||||
},
|
||||
})
|
||||
class ServerStorage extends Vue {
|
||||
};
|
||||
class ServerStorage {
|
||||
_options = componentOptions;
|
||||
|
||||
created() {
|
||||
this.inited = false;
|
||||
this.keyInited = false;
|
||||
this.commit = this.$store.commit;
|
||||
this.prevServerStorageKey = null;
|
||||
this.$root.$on('generateNewServerStorageKey', () => {this.generateNewServerStorageKey()});
|
||||
this.$root.generateNewServerStorageKey = () => {this.generateNewServerStorageKey()};
|
||||
|
||||
this.debouncedSaveSettings = _.debounce(() => {
|
||||
this.saveSettings();
|
||||
}, 500);
|
||||
|
||||
this.debouncedSaveLibs = _.debounce(() => {
|
||||
this.saveLibs();
|
||||
}, 500);
|
||||
|
||||
this.debouncedNotifySuccess = _.debounce(() => {
|
||||
this.success('Данные синхронизированы с сервером');
|
||||
}, 1000);
|
||||
|
||||
this.oldProfiles = {};
|
||||
this.oldSettings = {};
|
||||
this.oldLibs = {};
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
this.cachedRecent = await ssCacheStore.getItem('recent');
|
||||
if (!this.cachedRecent)
|
||||
await this.setCachedRecent({rev: 0, data: {}});
|
||||
await this.cleanCachedRecent('cachedRecent');
|
||||
|
||||
this.cachedRecentPatch = await ssCacheStore.getItem('recent-patch');
|
||||
if (!this.cachedRecentPatch)
|
||||
await this.setCachedRecentPatch({rev: 0, data: {}});
|
||||
await this.cleanCachedRecent('cachedRecentPatch');
|
||||
|
||||
this.cachedRecentMod = await ssCacheStore.getItem('recent-mod');
|
||||
if (!this.cachedRecentMod)
|
||||
await this.setCachedRecentMod({rev: 0, data: {}});
|
||||
await this.cleanCachedRecent('cachedRecentMod');
|
||||
|
||||
if (!this.serverStorageKey) {
|
||||
//генерируем новый ключ
|
||||
@@ -97,6 +107,15 @@ class ServerStorage extends Vue {
|
||||
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);
|
||||
@@ -124,9 +143,14 @@ class ServerStorage extends Vue {
|
||||
await this.loadProfiles(force);
|
||||
this.checkCurrentProfile();
|
||||
await this.currentProfileChanged(force);
|
||||
await this.loadLibs(force);
|
||||
|
||||
if (force)
|
||||
await this.cleanCachedRecent('all');
|
||||
const loadSuccess = await this.loadRecent();
|
||||
if (loadSuccess && force)
|
||||
if (loadSuccess && force) {
|
||||
await this.saveRecent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +193,14 @@ 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', '');
|
||||
@@ -177,17 +209,24 @@ class ServerStorage extends Vue {
|
||||
|
||||
success(message) {
|
||||
if (this.showServerStorageMessages)
|
||||
this.$notify.success({message});
|
||||
this.$root.notify.success(message);
|
||||
}
|
||||
|
||||
warning(message) {
|
||||
if (this.showServerStorageMessages && !this.offlineModeActive)
|
||||
this.$notify.warning({message});
|
||||
this.$root.notify.warning(message);
|
||||
}
|
||||
|
||||
error(message) {
|
||||
if (this.showServerStorageMessages && !this.offlineModeActive)
|
||||
this.$notify.error({message});
|
||||
if (this.showServerStorageMessages && !this.offlineModeActive) {
|
||||
this.errorMessageCounter = (this.errorMessageCounter ? this.errorMessageCounter + 1 : 1);
|
||||
const hint = (this.errorMessageCounter < 2 ? '' :
|
||||
'<div><br>Надоело это сообщение? Добавьте в настройках кнопку "Автономный режим" ' +
|
||||
'<i class="la la-unlink" style="font-size: 20px; color: white"></i> на панель инструментов и активируйте ее.</div>'
|
||||
);
|
||||
|
||||
this.$root.notify.error(message + hint);
|
||||
}
|
||||
}
|
||||
|
||||
async loadSettings(force = false, doNotifySuccess = true) {
|
||||
@@ -338,13 +377,85 @@ class ServerStorage extends Vue {
|
||||
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 loadLibs(force = false, doNotifySuccess = true) {
|
||||
if (!this.keyInited || !this.serverSyncEnabled)
|
||||
return;
|
||||
|
||||
const oldRev = this.libsRev;
|
||||
//проверим ревизию на сервере
|
||||
if (!force) {
|
||||
try {
|
||||
const revs = await this.storageCheck({libs: {}});
|
||||
if (revs.state == 'success' && revs.items.libs.rev == oldRev) {
|
||||
return;
|
||||
}
|
||||
} catch(e) {
|
||||
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -403,12 +514,12 @@ class ServerStorage extends Vue {
|
||||
|
||||
const md = newRecentMod.data;
|
||||
if (md.key && result[md.key])
|
||||
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, true);
|
||||
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, {isAddChanged: true});
|
||||
|
||||
if (!bookManager.loaded) {
|
||||
/*if (!bookManager.loaded) {
|
||||
this.warning('Ожидание загрузки списка книг перед синхронизацией');
|
||||
while (!bookManager.loaded) await utils.sleep(100);
|
||||
}
|
||||
}*/
|
||||
|
||||
if (newRecent.rev != this.cachedRecent.rev)
|
||||
await this.setCachedRecent(newRecent);
|
||||
@@ -465,9 +576,9 @@ class ServerStorage extends Vue {
|
||||
newRecentPatch.rev++;
|
||||
newRecentPatch.data[itemKey] = _.cloneDeep(bm.recent[itemKey]);
|
||||
|
||||
let applyMod = this.cachedRecentMod.data;
|
||||
const applyMod = this.cachedRecentMod.data;
|
||||
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
|
||||
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, true);
|
||||
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true});
|
||||
|
||||
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
|
||||
needSaveRecentPatch = true;
|
||||
@@ -478,8 +589,8 @@ class ServerStorage extends Vue {
|
||||
let newRecent = {};
|
||||
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
|
||||
//ждем весь bm.recent
|
||||
while (!bookManager.loaded)
|
||||
await utils.sleep(100);
|
||||
/*while (!bookManager.loaded)
|
||||
await utils.sleep(100);*/
|
||||
|
||||
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
|
||||
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
|
||||
@@ -516,7 +627,7 @@ class ServerStorage extends Vue {
|
||||
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||
if (!recurse && itemKey) {
|
||||
this.savingRecent = false;
|
||||
this.saveRecent(itemKey, true);
|
||||
await this.saveRecent(itemKey, true);
|
||||
return;
|
||||
}
|
||||
} else if (result.state == 'success') {
|
||||
@@ -617,13 +728,15 @@ class ServerStorage extends Vue {
|
||||
const ids = id.split('.');
|
||||
if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
|
||||
throw new Error(`decodeStorageItems: bad id - ${id}`);
|
||||
items[utils.fromBase58(ids[1])] = decoded;
|
||||
items[utils.fromBase58(ids[1]).toString()] = decoded;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
result.items = items;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(ServerStorage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
<template>
|
||||
<Window ref="window" height="140px" max-width="600px" :top-shift="-50" @close="close">
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
Установить позицию
|
||||
</template>
|
||||
|
||||
<div class="slider">
|
||||
<el-slider v-model="sliderValue" :max="sliderMax" :format-tooltip="formatTooltip"></el-slider>
|
||||
<div id="set-position-slider" class="slider q-px-md">
|
||||
<q-slider
|
||||
v-model="sliderValue"
|
||||
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"
|
||||
|
||||
:max="sliderMax"
|
||||
label
|
||||
:label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)"
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import Window from '../../share/Window.vue';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
@@ -27,8 +34,10 @@ export default @Component({
|
||||
this.$emit('book-pos-changed', {bookPos: newValue});
|
||||
},
|
||||
},
|
||||
})
|
||||
class SetPositionPage extends Vue {
|
||||
};
|
||||
class SetPositionPage {
|
||||
_options = componentOptions;
|
||||
|
||||
sliderValue = null;
|
||||
sliderMax = null;
|
||||
|
||||
@@ -46,24 +55,22 @@ class SetPositionPage extends Vue {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(SetPositionPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -73,9 +80,13 @@ class SetPositionPage extends Vue {
|
||||
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>
|
||||
87
client/components/Reader/SettingsPage/ConvertTab.inc
Normal file
@@ -0,0 +1,87 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="q-mt-sm column items-center">
|
||||
<span>Настройки конвертирования применяются ко всем</span>
|
||||
<span>вновь загружаемым или обновляемым файлам</span>
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">HTML, XML, TXT</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-7">Текст</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="splitToPara" size="xs" label="Попытаться разбить текст на параграфы">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Опция принудительно включает эвристику разбиения текста на<br>
|
||||
параграфы в случае, если формат файла определен как html,<br>
|
||||
xml или txt. Возможна нечитабельная разметка текста.
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-7">Сайты</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="enableSitesFilter" 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 v-if="isExternalConverter">
|
||||
<div class="part-header">PDF</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-7">Формат</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="pdfAsText" size="xs" label="Извлекать текст из PDF">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Пытается извлечь текст из pdf-файла и переразбить на параграфы.<br>
|
||||
Размер получаемого fb2-файла при этом относительно небольшой.<br>
|
||||
При отключении этой опции, pdf будет представлен как набор<br>
|
||||
изображений (аналогично ковертированию djvu).
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-7">Качество</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-5" v-model="pdfQuality" :min="10" :max="100" :disable="pdfAsText" >
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Качество конвертирования Pdf в Fb2. Чем значение выше, тем больше<br>
|
||||
размер итогового файла. Если сервер отказывается конвертировать<br>
|
||||
слишком большой файл, то попробуйте понизить качество.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div v-if="isExternalConverter">
|
||||
<div class="part-header">DJVU</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-7">Качество</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-5" v-model="djvuQuality" :min="10" :max="100">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Качество конвертирования Djvu в Fb2. Чем значение выше, тем больше<br>
|
||||
размер итогового файла. Если сервер отказывается конвертировать<br>
|
||||
слишком большой файл, то попробуйте понизить качество.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
33
client/components/Reader/SettingsPage/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>
|
||||
102
client/components/Reader/SettingsPage/OthersTab.inc
Normal file
@@ -0,0 +1,102 @@
|
||||
<!---------------------------------------------->
|
||||
<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="showNeedUpdateNotify">
|
||||
Показывать уведомление о новой версии
|
||||
<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>
|
||||
<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>
|
||||
28
client/components/Reader/SettingsPage/PageMoveTab.inc
Normal file
@@ -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/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>
|
||||
3
client/components/Reader/SettingsPage/ResetTab.inc
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="item row">
|
||||
<q-btn class="col q-ma-sm" dense no-caps @click="setDefaults">Установить по умолчанию</q-btn>
|
||||
</div>
|
||||
18
client/components/Reader/SettingsPage/ToolBarTab.inc
Normal file
@@ -0,0 +1,18 @@
|
||||
<div class="part-header">Отображение</div>
|
||||
|
||||
<div class="item row no-wrap">
|
||||
<div class="label-3"></div>
|
||||
<q-checkbox size="xs" v-model="toolBarHideOnScroll" label="Скрывать/показывать панель при прокрутке" >
|
||||
<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 no-wrap" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'">
|
||||
<div class="label-3"></div>
|
||||
<q-checkbox size="xs" v-model="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,261 @@
|
||||
<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"
|
||||
v-model="search"
|
||||
class="q-ml-sm col"
|
||||
outlined dense
|
||||
bg-color="grey-4"
|
||||
placeholder="Найти"
|
||||
@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 v-for="(action, index) in tableData" :key="index" class="table-row row">
|
||||
<div class="desc q-pa-sm">
|
||||
{{ rstore.readerActions[action] }}
|
||||
</div>
|
||||
<div class="hotKeys col q-pa-sm">
|
||||
<q-chip
|
||||
v-for="(code, index2) in modelValue[action]" :key="index2"
|
||||
:color="collisions[code] ? 'red' : 'grey-7'"
|
||||
:removable="!readonly" :clickable="collisions[code] ? true : false"
|
||||
text-color="white" @remove="removeCode(action, code)"
|
||||
@click="collisionWarning(code)"
|
||||
>
|
||||
{{ code }}
|
||||
</q-chip>
|
||||
</div>
|
||||
<div v-show="!readonly" class="column q-pa-xs">
|
||||
<q-icon
|
||||
v-ripple
|
||||
:disabled="(modelValue[action].length >= maxCodesLength) || null"
|
||||
name="la la-plus-circle"
|
||||
class="button bg-green-8 text-white"
|
||||
@click="addHotKey(action)"
|
||||
>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Добавить сочетание клавиш
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon
|
||||
v-ripple
|
||||
name="la la-broom"
|
||||
class="button text-grey-5"
|
||||
@click="defaultHotKey(action)"
|
||||
>
|
||||
<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 vueComponent from '../../../vueComponent.js';
|
||||
|
||||
import rstore from '../../../../store/modules/reader';
|
||||
//import * as utils from '../../share/utils';
|
||||
|
||||
const componentOptions = {
|
||||
watch: {
|
||||
search: function() {
|
||||
this.updateTableData();
|
||||
},
|
||||
modelValue: function() {
|
||||
this.checkCollisions();
|
||||
this.updateTableData();
|
||||
}
|
||||
},
|
||||
};
|
||||
class UserHotKeys {
|
||||
_options = componentOptions;
|
||||
_props = {
|
||||
modelValue: Object,
|
||||
readonly: Boolean,
|
||||
};
|
||||
|
||||
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.modelValue[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.modelValue)) {
|
||||
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.modelValue[action]);
|
||||
const index = codes.indexOf(code);
|
||||
if (index >= 0) {
|
||||
codes.splice(index, 1);
|
||||
const newValue = Object.assign({}, this.modelValue, {[action]: codes});
|
||||
this.$emit('update:modelValue', newValue);
|
||||
}
|
||||
}
|
||||
|
||||
async addHotKey(action) {
|
||||
if (this.modelValue[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.modelValue[action]);
|
||||
if (codes.indexOf(result) < 0) {
|
||||
codes.push(result);
|
||||
const newValue = Object.assign({}, this.modelValue, {[action]: codes});
|
||||
this.$emit('update:modelValue', 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.modelValue, {[action]: codes});
|
||||
this.$emit('update:modelValue', newValue);
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
async defaultHotKeyAll() {
|
||||
try {
|
||||
if (await this.$root.stdDialog.confirm('Подтвердите сброс сочетаний клавиш<br>для ВСЕХ команд в значения по умолчанию:', ' ')) {
|
||||
const newValue = Object.assign({}, rstore.settingDefaults.userHotKeys);
|
||||
this.$emit('update:modelValue', newValue);
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(UserHotKeys);
|
||||
//-----------------------------------------------------------------------------
|
||||
</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>
|
||||
116
client/components/Reader/SettingsPage/ViewTab/Color.inc
Normal file
@@ -0,0 +1,116 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">
|
||||
Цвет
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">
|
||||
Текст
|
||||
</div>
|
||||
<div class="col row">
|
||||
<q-input
|
||||
v-model="textColorFiltered"
|
||||
class="col-left no-mp"
|
||||
outlined dense
|
||||
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
>
|
||||
<template #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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mt-md" />
|
||||
<div class="item row">
|
||||
<div class="label-2">
|
||||
Фон
|
||||
</div>
|
||||
<div class="col row">
|
||||
<q-input
|
||||
v-model="bgColorFiltered"
|
||||
class="col-left no-mp"
|
||||
outlined dense
|
||||
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
>
|
||||
<template #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>
|
||||
</div>
|
||||
|
||||
<div class="q-mt-md" />
|
||||
<div class="item row">
|
||||
<div class="label-2">
|
||||
Обои
|
||||
</div>
|
||||
<div class="col row items-center">
|
||||
<q-select
|
||||
v-model="wallpaper"
|
||||
class="col-left no-mp"
|
||||
:options="wallpaperOptions"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
>
|
||||
<template #selected-item="scope">
|
||||
<div>
|
||||
{{ scope.opt.label }}
|
||||
</div>
|
||||
<div v-show="scope.opt.value" class="q-ml-sm" :class="scope.opt.value" style="width: 40px; height: 28px;"></div>
|
||||
</template>
|
||||
|
||||
<template #option="scope">
|
||||
<q-item
|
||||
v-bind="scope.itemProps"
|
||||
>
|
||||
<q-item-section style="min-width: 50px;">
|
||||
<q-item-label v-html="scope.opt.label" />
|
||||
</q-item-section>
|
||||
<q-item-section v-show="scope.opt.value" :class="scope.opt.value" style="min-width: 70px; min-height: 50px;" />
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<div class="q-px-xs" />
|
||||
<q-btn class="q-ml-sm" round dense color="blue" icon="la la-plus" @click.stop="loadWallpaperFileClick">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Добавить файл обоев
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn v-show="wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-minus" @click.stop="delWallpaper">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Удалить выбранные обои
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mt-sm" />
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col row items-center">
|
||||
<q-checkbox v-model="wallpaperIgnoreStatusBar" size="xs" label="Не включать строку статуса в обои" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input ref="file" type="file" style="display: none;" @change="loadWallpaperFile" />
|
||||
56
client/components/Reader/SettingsPage/ViewTab/Font.inc
Normal file
@@ -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>
|
||||
124
client/components/Reader/SettingsPage/ViewTab/Mode.inc
Normal file
@@ -0,0 +1,124 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">Режим</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="dualPageMode" size="xs" label="Двухстраничный режим" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="part-header">Страницы</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 v-show="dualPageMode" class="item row">
|
||||
<div class="label-2">Отступ внутри</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="dualIndentLR" :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 v-show="dualPageMode">
|
||||
<div class="part-header">Разделитель</div>
|
||||
|
||||
<div class="item row no-wrap">
|
||||
<div class="label-2">Цвет</div>
|
||||
<div class="col-left row">
|
||||
<q-input class="col-left no-mp"
|
||||
outlined dense
|
||||
v-model="dualDivColorFiltered"
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
:disable="dualDivColorAsText"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('div')">
|
||||
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||
<div>
|
||||
<q-color v-model="dualDivColor"
|
||||
no-header default-view="palette" :palette="predefineTextColors"
|
||||
/>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="q-px-xs"/>
|
||||
<q-checkbox v-model="dualDivColorAsText" size="xs" label="Как у текста" />
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Прозрачность</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="dualDivColorAlpha" :min="0" :max="1" :digits="2" :step="0.1"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Ширина (px)</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="dualDivWidth" :min="0" :max="100">
|
||||
<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="dualDivHeight" :min="0" :max="100">
|
||||
<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="dualDivStrokeFill" :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="dualDivStrokeGap" :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="dualDivShadowWidth" :min="0" :max="100"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
64
client/components/Reader/SettingsPage/ViewTab/Status.inc
Normal file
@@ -0,0 +1,64 @@
|
||||
<!---------------------------------------------->
|
||||
<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 v-show="showStatusBar" class="q-ml-sm" v-model="statusBarTop" size="xs" label="Вверху/внизу" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="showStatusBar" class="item row no-wrap">
|
||||
<div class="label-2">Цвет</div>
|
||||
<div class="col-left row">
|
||||
<q-input class="col-left no-mp"
|
||||
outlined dense
|
||||
v-model="statusBarColorFiltered"
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
:disable="statusBarColorAsText"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('statusbar')">
|
||||
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||
<div>
|
||||
<q-color v-model="statusBarColor"
|
||||
no-header default-view="palette" :palette="predefineTextColors"
|
||||
/>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="q-px-xs"/>
|
||||
<q-checkbox v-model="statusBarColorAsText" size="xs" label="Как у текста"/>
|
||||
</div>
|
||||
|
||||
<div v-show="showStatusBar" 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"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="showStatusBar" class="item row">
|
||||
<div class="label-2">Высота</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="statusBarHeight" :min="5" :max="100"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="showStatusBar" 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>
|
||||
127
client/components/Reader/SettingsPage/ViewTab/Text.inc
Normal file
@@ -0,0 +1,127 @@
|
||||
<!---------------------------------------------->
|
||||
<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="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" size="xs" label="Ширина не более размера страницы" :disable="!showImages || dualPageMode"/>
|
||||
</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>
|
||||
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;
|
||||
@@ -2,11 +2,11 @@ import {sleep} from '../../../share/utils';
|
||||
|
||||
export default class DrawHelper {
|
||||
fontBySize(size) {
|
||||
return `${size}px ${this.fontName}`;
|
||||
return `${size}px '${this.fontName}'`;
|
||||
}
|
||||
|
||||
fontByStyle(style) {
|
||||
return `${style.italic ? 'italic' : this.fontStyle} ${style.bold ? 'bold' : this.fontWeight} ${this.fontSize}px ${this.fontName}`;
|
||||
return `${style.italic ? 'italic' : this.fontStyle} ${style.bold ? 'bold' : this.fontWeight} ${this.fontSize}px '${this.fontName}'`;
|
||||
}
|
||||
|
||||
measureText(text, style) {// eslint-disable-line no-unused-vars
|
||||
@@ -19,6 +19,109 @@ export default class DrawHelper {
|
||||
return this.context.measureText(text).width;
|
||||
}
|
||||
|
||||
drawLine(line, lineIndex, baseLineIndex, sel, imageDrawn) {
|
||||
/* line:
|
||||
{
|
||||
begin: Number,
|
||||
end: Number,
|
||||
first: Boolean,
|
||||
last: Boolean,
|
||||
parts: array of {
|
||||
style: {bold: Boolean, italic: Boolean, center: Boolean},
|
||||
image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number},
|
||||
text: String,
|
||||
}
|
||||
}*/
|
||||
|
||||
let out = '<div>';
|
||||
|
||||
let lineText = '';
|
||||
let center = false;
|
||||
let space = 0;
|
||||
let j = 0;
|
||||
//формируем строку
|
||||
for (const part of line.parts) {
|
||||
let tOpen = '';
|
||||
tOpen += (part.style.bold ? '<b>' : '');
|
||||
tOpen += (part.style.italic ? '<i>' : '');
|
||||
tOpen += (part.style.sup ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: -0.3em">' : '');
|
||||
tOpen += (part.style.sub ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: 0.3em">' : '');
|
||||
let tClose = '';
|
||||
tClose += (part.style.sub ? '</span>' : '');
|
||||
tClose += (part.style.sup ? '</span>' : '');
|
||||
tClose += (part.style.italic ? '</i>' : '');
|
||||
tClose += (part.style.bold ? '</b>' : '');
|
||||
|
||||
let text = '';
|
||||
if (lineIndex == 0 && this.searching) {
|
||||
for (let k = 0; k < part.text.length; k++) {
|
||||
text += (sel.has(j) ? `<ins>${part.text[k]}</ins>` : part.text[k]);
|
||||
j++;
|
||||
}
|
||||
} else
|
||||
text = part.text;
|
||||
|
||||
if (text && text.trim() == '')
|
||||
text = `<span style="white-space: pre">${text}</span>`;
|
||||
|
||||
lineText += `${tOpen}${text}${tClose}`;
|
||||
|
||||
center = center || part.style.center;
|
||||
space = (part.style.space > space ? part.style.space : space);
|
||||
|
||||
//избражения
|
||||
//image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number, w: Number, h: Number},
|
||||
const img = part.image;
|
||||
if (img && img.id && !img.inline && !imageDrawn.has(img.paraIndex)) {
|
||||
const bin = this.parsed.binary[img.id];
|
||||
if (bin) {
|
||||
let resize = '';
|
||||
if (bin.h > img.h) {
|
||||
resize = `height: ${img.h}px`;
|
||||
}
|
||||
|
||||
const left = (this.w - img.w)/2;
|
||||
const top = ((img.lineCount*this.lineHeight - img.h)/2) + (lineIndex - baseLineIndex - img.imageLine)*this.lineHeight;
|
||||
if (img.local) {
|
||||
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
|
||||
} else {
|
||||
lineText += `<img src="${img.id}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
|
||||
}
|
||||
}
|
||||
imageDrawn.add(img.paraIndex);
|
||||
}
|
||||
|
||||
if (img && img.id && img.inline) {
|
||||
if (img.local) {
|
||||
const bin = this.parsed.binary[img.id];
|
||||
if (bin) {
|
||||
let resize = '';
|
||||
if (bin.h > this.fontSize) {
|
||||
resize = `height: ${this.fontSize - 3}px`;
|
||||
}
|
||||
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="${resize}"/>`;
|
||||
}
|
||||
} else {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const centerStyle = (center ? `text-align: center; text-align-last: center; width: ${this.w}px` : '')
|
||||
if ((line.first || space) && !center) {
|
||||
let p = (line.first ? this.p : 0);
|
||||
p = (space ? p + this.p*space : p);
|
||||
lineText = `<span style="display: inline-block; margin-left: ${p}px"></span>${lineText}`;
|
||||
}
|
||||
|
||||
if (line.last || center)
|
||||
lineText = `<span style="display: inline-block; ${centerStyle}">${lineText}</span>`;
|
||||
|
||||
out += lineText + '</div>';
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
drawPage(lines, isScrolling) {
|
||||
if (!this.lastBook || this.pageLineCount < 1 || !this.book || !lines || !this.parsed.textLength)
|
||||
return '';
|
||||
@@ -26,140 +129,78 @@ export default class DrawHelper {
|
||||
const font = this.fontByStyle({});
|
||||
const justify = (this.textAlignJustify ? 'text-align: justify; text-align-last: justify;' : '');
|
||||
|
||||
let out = `<div style="width: ${this.w}px; height: ${this.h + (isScrolling ? this.lineHeight : 0)}px;` +
|
||||
const boxH = this.h + (isScrolling ? this.lineHeight : 0);
|
||||
let out = `<div class="row no-wrap" style="width: ${this.boxW}px; height: ${boxH}px;` +
|
||||
` position: absolute; top: ${this.fontSize*this.textShift}px; color: ${this.textColor}; font: ${font}; ${justify}` +
|
||||
` line-height: ${this.lineHeight}px; white-space: nowrap;">`;
|
||||
|
||||
let imageDrawn = new Set();
|
||||
let imageDrawn1 = new Set();
|
||||
let imageDrawn2 = new Set();
|
||||
let len = lines.length;
|
||||
const lineCount = this.pageLineCount + (isScrolling ? 1 : 0);
|
||||
len = (len > lineCount ? lineCount : len);
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const line = lines[i];
|
||||
/* line:
|
||||
{
|
||||
begin: Number,
|
||||
end: Number,
|
||||
first: Boolean,
|
||||
last: Boolean,
|
||||
parts: array of {
|
||||
style: {bold: Boolean, italic: Boolean, center: Boolean},
|
||||
image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number},
|
||||
text: String,
|
||||
}
|
||||
}*/
|
||||
let sel = new Set();
|
||||
//поиск
|
||||
if (i == 0 && this.searching) {
|
||||
let pureText = '';
|
||||
for (const part of line.parts) {
|
||||
pureText += part.text;
|
||||
}
|
||||
|
||||
pureText = pureText.toLowerCase();
|
||||
let j = 0;
|
||||
while (1) {// eslint-disable-line no-constant-condition
|
||||
j = pureText.indexOf(this.needle, j);
|
||||
if (j >= 0) {
|
||||
for (let k = 0; k < this.needle.length; k++) {
|
||||
sel.add(j + k);
|
||||
}
|
||||
} else
|
||||
break;
|
||||
j++;
|
||||
}
|
||||
//поиск
|
||||
let sel = new Set();
|
||||
if (len > 0 && this.searching) {
|
||||
const line = lines[0];
|
||||
let pureText = '';
|
||||
for (const part of line.parts) {
|
||||
pureText += part.text;
|
||||
}
|
||||
|
||||
let lineText = '';
|
||||
let center = false;
|
||||
let space = 0;
|
||||
pureText = pureText.toLowerCase();
|
||||
let j = 0;
|
||||
//формируем строку
|
||||
for (const part of line.parts) {
|
||||
let tOpen = (part.style.bold ? '<b>' : '');
|
||||
tOpen += (part.style.italic ? '<i>' : '');
|
||||
let tClose = (part.style.italic ? '</i>' : '');
|
||||
tClose += (part.style.bold ? '</b>' : '');
|
||||
|
||||
let text = '';
|
||||
if (i == 0 && this.searching) {
|
||||
for (let k = 0; k < part.text.length; k++) {
|
||||
text += (sel.has(j) ? `<ins>${part.text[k]}</ins>` : part.text[k]);
|
||||
j++;
|
||||
while (1) {// eslint-disable-line no-constant-condition
|
||||
j = pureText.indexOf(this.needle, j);
|
||||
if (j >= 0) {
|
||||
for (let k = 0; k < this.needle.length; k++) {
|
||||
sel.add(j + k);
|
||||
}
|
||||
} else
|
||||
text = part.text;
|
||||
|
||||
if (text && text.trim() == '')
|
||||
text = `<span style="white-space: pre">${text}</span>`;
|
||||
|
||||
lineText += `${tOpen}${text}${tClose}`;
|
||||
|
||||
center = center || part.style.center;
|
||||
space = (part.style.space > space ? part.style.space : space);
|
||||
|
||||
//избражения
|
||||
//image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number, w: Number, h: Number},
|
||||
const img = part.image;
|
||||
if (img && img.id && !img.inline && !imageDrawn.has(img.paraIndex)) {
|
||||
const bin = this.parsed.binary[img.id];
|
||||
if (bin) {
|
||||
let resize = '';
|
||||
if (bin.h > img.h) {
|
||||
resize = `height: ${img.h}px`;
|
||||
}
|
||||
|
||||
const left = (this.w - img.w)/2;
|
||||
const top = ((img.lineCount*this.lineHeight - img.h)/2) + (i - img.imageLine)*this.lineHeight;
|
||||
if (img.local) {
|
||||
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
|
||||
} else {
|
||||
lineText += `<img src="${img.id}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
|
||||
}
|
||||
}
|
||||
imageDrawn.add(img.paraIndex);
|
||||
}
|
||||
|
||||
if (img && img.id && img.inline) {
|
||||
if (img.local) {
|
||||
const bin = this.parsed.binary[img.id];
|
||||
if (bin) {
|
||||
let resize = '';
|
||||
if (bin.h > this.fontSize) {
|
||||
resize = `height: ${this.fontSize - 3}px`;
|
||||
}
|
||||
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="${resize}"/>`;
|
||||
}
|
||||
} else {
|
||||
//
|
||||
}
|
||||
}
|
||||
break;
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
const centerStyle = (center ? `text-align: center; text-align-last: center; width: ${this.w}px` : '')
|
||||
if ((line.first || space) && !center) {
|
||||
let p = (line.first ? this.p : 0);
|
||||
p = (space ? p + this.p*space : p);
|
||||
lineText = `<span style="display: inline-block; margin-left: ${p}px"></span>${lineText}`;
|
||||
//отрисовка строк
|
||||
if (!this.dualPageMode) {
|
||||
out += `<div class="fit">`;
|
||||
for (let i = 0; i < len; i++) {
|
||||
out += this.drawLine(lines[i], i, 0, sel, imageDrawn1);
|
||||
}
|
||||
out += `</div>`;
|
||||
} else {
|
||||
//левая страница
|
||||
out += `<div style="width: ${this.w}px; margin-left: ${this.dualIndentLR}px; position: relative;">`;
|
||||
const l2 = (this.pageRowsCount > len ? len : this.pageRowsCount);
|
||||
for (let i = 0; i < l2; i++) {
|
||||
out += this.drawLine(lines[i], i, 0, sel, imageDrawn1);
|
||||
}
|
||||
out += '</div>';
|
||||
|
||||
if (line.last || center)
|
||||
lineText = `<span style="display: inline-block; ${centerStyle}">${lineText}</span>`;
|
||||
//разделитель
|
||||
out += `<div style="width: ${this.dualIndentLR*2}px;"></div>`;
|
||||
|
||||
out += (i > 0 ? '<br>' : '') + lineText;
|
||||
//правая страница
|
||||
out += `<div style="width: ${this.w}px; margin-right: ${this.dualIndentLR}px; position: relative;">`;
|
||||
for (let i = l2; i < len; i++) {
|
||||
out += this.drawLine(lines[i], i, l2, sel, imageDrawn2);
|
||||
}
|
||||
out += '</div>';
|
||||
}
|
||||
|
||||
out += '</div>';
|
||||
return out;
|
||||
}
|
||||
|
||||
drawPercentBar(x, y, w, h, font, fontSize, bookPos, textLength) {
|
||||
drawPercentBar(x, y, w, h, font, fontSize, bookPos, textLength, imageNum, imageLength) {
|
||||
const pad = 3;
|
||||
const fh = h - 2*pad;
|
||||
const fh2 = fh/2;
|
||||
|
||||
const t1 = `${Math.floor((bookPos + 1)/1000)}k/${Math.floor(textLength/1000)}k`;
|
||||
const tImg = (imageNum > 0 ? ` (${imageNum}/${imageLength})` : '');
|
||||
const t1 = `${Math.floor((bookPos + 1)/1000)}/${Math.floor(textLength/1000)}${tImg}`;
|
||||
const w1 = this.measureTextFont(t1, font) + fh2;
|
||||
const read = (bookPos + 1)/textLength;
|
||||
const t2 = `${(read*100).toFixed(2)}%`;
|
||||
@@ -172,8 +213,8 @@ export default class DrawHelper {
|
||||
|
||||
if (w1 + w2 + w3 <= w && w3 > (10 + fh2)) {
|
||||
const barWidth = w - w1 - w2 - fh2;
|
||||
out += this.strokeRect(x + w1, y + pad, barWidth, fh - 2, this.statusBarColor);
|
||||
out += this.fillRect(x + w1 + 2, y + pad + 2, (barWidth - 4)*read, fh - 6, this.statusBarColor);
|
||||
out += this.strokeRect(x + w1, y + pad, barWidth, fh - 2, this.statusBarRgbaColor);
|
||||
out += this.fillRect(x + w1 + 2, y + pad + 2, (barWidth - 4)*read, fh - 6, this.statusBarRgbaColor);
|
||||
}
|
||||
|
||||
if (w1 <= w)
|
||||
@@ -182,16 +223,16 @@ export default class DrawHelper {
|
||||
return out;
|
||||
}
|
||||
|
||||
drawStatusBar(statusBarTop, statusBarHeight, bookPos, textLength, title) {
|
||||
drawStatusBar(statusBarTop, statusBarHeight, bookPos, textLength, title, imageNum, imageLength) {
|
||||
|
||||
let out = `<div class="layout" style="` +
|
||||
`width: ${this.realWidth}px; height: ${statusBarHeight}px; ` +
|
||||
`color: ${this.statusBarColor}">`;
|
||||
`color: ${this.statusBarRgbaColor}">`;
|
||||
|
||||
const fontSize = statusBarHeight*0.75;
|
||||
const font = 'bold ' + this.fontBySize(fontSize);
|
||||
|
||||
out += this.fillRect(0, (statusBarTop ? statusBarHeight : 0), this.realWidth, 1, this.statusBarColor);
|
||||
out += this.fillRect(0, (statusBarTop ? statusBarHeight : 0), this.realWidth, 1, this.statusBarRgbaColor);
|
||||
|
||||
const date = new Date();
|
||||
const time = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||||
@@ -200,7 +241,7 @@ export default class DrawHelper {
|
||||
|
||||
out += this.fillTextShift(this.fittingString(title, this.realWidth/2 - fontSize - 3, font), fontSize, 2, font, fontSize);
|
||||
|
||||
out += this.drawPercentBar(this.realWidth/2, 2, this.realWidth/2 - timeW - 2*fontSize, statusBarHeight, font, fontSize, bookPos, textLength);
|
||||
out += this.drawPercentBar(this.realWidth/2 + fontSize, 2, this.realWidth/2 - timeW - 3*fontSize, statusBarHeight, font, fontSize, bookPos, textLength, imageNum, imageLength);
|
||||
|
||||
out += '</div>';
|
||||
return out;
|
||||
@@ -267,7 +308,7 @@ export default class DrawHelper {
|
||||
}
|
||||
|
||||
async doPageAnimationRightShift(page1, page2, duration, isDown, animation1Finish) {
|
||||
const s = this.w + this.fontSize;
|
||||
const s = this.boxW + this.fontSize;
|
||||
|
||||
if (isDown) {
|
||||
page1.style.transform = `translateX(${s}px)`;
|
||||
|
||||
93
client/components/Reader/TextPage/TextPage.css
Normal file
@@ -0,0 +1,93 @@
|
||||
@keyframes page1-animation-thaw {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes page2-animation-thaw {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.paper1 {
|
||||
background: url("images/paper1.jpg") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper2 {
|
||||
background: url("images/paper2.jpg") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper3 {
|
||||
background: url("images/paper3.jpg") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper4 {
|
||||
background: url("images/paper4.jpg") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper5 {
|
||||
background: url("images/paper5.jpg") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper6 {
|
||||
background: url("images/paper6.jpg") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper7 {
|
||||
background: url("images/paper7.jpg") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper8 {
|
||||
background: url("images/paper8.jpg") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper9 {
|
||||
background: url("images/paper9.jpg");
|
||||
}
|
||||
|
||||
.paper10 {
|
||||
background: url("images/paper10.png") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper11 {
|
||||
background: url("images/paper11.png") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper12 {
|
||||
background: url("images/paper12.png") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper13 {
|
||||
background: url("images/paper13.png") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper14 {
|
||||
background: url("images/paper14.png") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper15 {
|
||||
background: url("images/paper15.png") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper16 {
|
||||
background: url("images/paper16.png") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper17 {
|
||||
background: url("images/paper17.png") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
BIN
client/components/Reader/TextPage/images/paper10.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
client/components/Reader/TextPage/images/paper11.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
client/components/Reader/TextPage/images/paper12.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
client/components/Reader/TextPage/images/paper13.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
client/components/Reader/TextPage/images/paper14.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
client/components/Reader/TextPage/images/paper15.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
client/components/Reader/TextPage/images/paper16.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
client/components/Reader/TextPage/images/paper17.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
@@ -1,26 +1,58 @@
|
||||
import he from 'he';
|
||||
import sax from '../../../../server/core/sax';
|
||||
import {sleep} from '../../../share/utils';
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
const maxImageLineCount = 100;
|
||||
|
||||
// defaults
|
||||
const defaultSettings = {
|
||||
p: 30, //px, отступ параграфа
|
||||
w: 500, //px, ширина страницы
|
||||
|
||||
font: '', //css описание шрифта
|
||||
fontSize: 20, //px, размер шрифта
|
||||
wordWrap: false, //перенос по слогам
|
||||
cutEmptyParagraphs: false, //убирать пустые параграфы
|
||||
addEmptyParagraphs: 0, //добавлять n пустых параграфов перед непустым
|
||||
maxWordLength: 500, //px, максимальная длина слова без пробелов
|
||||
lineHeight: 26, //px, высота строки
|
||||
showImages: true, //показыввать изображения
|
||||
showInlineImagesInCenter: true, //выносить изображения в центр, работает на этапе первичного парсинга (parse)
|
||||
imageHeightLines: 100, //кол-во строк, максимальная высота изображения
|
||||
imageFitWidth: true, //ширина изображения не более ширины страницы
|
||||
dualPageMode: false, //двухстраничный режим
|
||||
compactTextPerc: 0, //проценты, степень компактности текста
|
||||
testWidth: 0, //ширина тестовой строки, пересчитывается извне при изменении шрифта браузером
|
||||
isTesting: false, //тестовый режим
|
||||
|
||||
//заглушка, измеритель ширины текста
|
||||
measureText: (text, style) => {// eslint-disable-line no-unused-vars
|
||||
return text.length*20;
|
||||
},
|
||||
};
|
||||
|
||||
//for splitToSlogi()
|
||||
const glas = new Set(['а', 'А', 'о', 'О', 'и', 'И', 'е', 'Е', 'ё', 'Ё', 'э', 'Э', 'ы', 'Ы', 'у', 'У', 'ю', 'Ю', 'я', 'Я']);
|
||||
const soglas = new Set([
|
||||
'б', 'в', 'г', 'д', 'ж', 'з', 'й', 'к', 'л', 'м', 'н', 'п', 'р', 'с', 'т', 'ф', 'х', 'ц', 'ч', 'ш', 'щ',
|
||||
'Б', 'В', 'Г', 'Д', 'Ж', 'З', 'Й', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х', 'Ч', 'Ц', 'Ш', 'Щ'
|
||||
]);
|
||||
const znak = new Set(['ь', 'Ь', 'ъ', 'Ъ', 'й', 'Й']);
|
||||
const alpha = new Set([...glas, ...soglas, ...znak]);
|
||||
|
||||
export default class BookParser {
|
||||
constructor(settings) {
|
||||
if (settings) {
|
||||
this.showInlineImagesInCenter = settings.showInlineImagesInCenter;
|
||||
}
|
||||
constructor(settings = {}) {
|
||||
this.sets = {};
|
||||
|
||||
// defaults
|
||||
this.p = 30;// px, отступ параграфа
|
||||
this.w = 300;// px, ширина страницы
|
||||
this.wordWrap = false;// перенос по слогам
|
||||
|
||||
//заглушка
|
||||
this.measureText = (text, style) => {// eslint-disable-line no-unused-vars
|
||||
return text.length*20;
|
||||
};
|
||||
this.setSettings(defaultSettings);
|
||||
this.setSettings(settings);
|
||||
}
|
||||
|
||||
setSettings(settings = {}) {
|
||||
this.sets = Object.assign({}, this.sets, settings);
|
||||
this.measureText = this.sets.measureText;
|
||||
}
|
||||
|
||||
async parse(data, callback) {
|
||||
if (!callback)
|
||||
callback = () => {};
|
||||
@@ -46,11 +78,23 @@ export default class BookParser {
|
||||
let isFirstSection = true;
|
||||
let isFirstTitlePara = false;
|
||||
|
||||
//изображения
|
||||
this.binary = {};
|
||||
let binaryId = '';
|
||||
let binaryType = '';
|
||||
let dimPromises = [];
|
||||
|
||||
//оглавление
|
||||
this.contents = [];
|
||||
this.images = [];
|
||||
let curTitle = {paraIndex: -1, title: '', subtitles: []};
|
||||
let curSubtitle = {paraIndex: -1, title: ''};
|
||||
let inTitle = false;
|
||||
let inSubtitle = false;
|
||||
let sectionLevel = 0;
|
||||
let bodyIndex = 0;
|
||||
let imageNum = 0;
|
||||
|
||||
let paraIndex = -1;
|
||||
let paraOffset = 0;
|
||||
let para = []; /*array of
|
||||
@@ -59,12 +103,12 @@ 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 ((resolve, reject) => { (async() => {
|
||||
data = data.replace(/[\n\r\s]/g, '');
|
||||
const i = new Image();
|
||||
let resolved = false;
|
||||
i.onload = () => {
|
||||
@@ -81,7 +125,7 @@ export default class BookParser {
|
||||
i.onerror = reject;
|
||||
|
||||
i.src = `data:${binaryType};base64,${data}`;
|
||||
await sleep(30*1000);
|
||||
await utils.sleep(30*1000);
|
||||
if (!resolved)
|
||||
reject('Не удалось получить размер изображения');
|
||||
})().catch(reject); });
|
||||
@@ -103,63 +147,102 @@ export default class BookParser {
|
||||
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) => {
|
||||
paraIndex++;
|
||||
let p = {
|
||||
index: paraIndex,
|
||||
offset: paraOffset,
|
||||
length: len,
|
||||
text: text,
|
||||
cut: (!addIndex && (len == 1 && text[0] == ' ')),
|
||||
addIndex: (addIndex ? addIndex : 0),
|
||||
};
|
||||
|
||||
para[paraIndex] = p;
|
||||
paraOffset += p.length;
|
||||
};
|
||||
|
||||
const growParagraph = (text, len) => {
|
||||
if (paraIndex < 0) {
|
||||
newParagraph(' ', 1);
|
||||
growParagraph(text, len);
|
||||
return;
|
||||
}
|
||||
|
||||
let p = para[paraIndex];
|
||||
//добавление пустых (addEmptyParagraphs) параграфов
|
||||
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
|
||||
paraIndex--;
|
||||
const correctCurrentPara = () => {
|
||||
//коррекция текущего параграфа
|
||||
if (paraIndex >= 0) {
|
||||
const prevParaIndex = paraIndex;
|
||||
let p = para[paraIndex];
|
||||
paraOffset -= p.length;
|
||||
for (let i = 0; i < 2; i++) {
|
||||
newParagraph(' ', 1, i + 1);
|
||||
|
||||
//уберем пробелы с концов параграфа, минимум 1 пробел должен быть у пустого параграфа
|
||||
let newParaText = p.text.trim();
|
||||
newParaText = (newParaText.length ? newParaText : ' ');
|
||||
const ldiff = p.text.length - newParaText.length;
|
||||
if (ldiff != 0) {
|
||||
p.text = newParaText;
|
||||
p.length -= ldiff;
|
||||
}
|
||||
|
||||
//удаление параграфов, которые содержат только разметку, такого не должно быть
|
||||
if (!p.length) {
|
||||
delete para[paraIndex];
|
||||
paraIndex--;
|
||||
return;
|
||||
}
|
||||
|
||||
//добавление пустых (не)видимых (addEmptyParagraphs) параграфов перед текущим непустым
|
||||
if (p.text.trim() != '') {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
para[paraIndex] = {
|
||||
index: paraIndex,
|
||||
offset: paraOffset,
|
||||
length: 1,
|
||||
text: ' ',
|
||||
addIndex: i + 1,
|
||||
};
|
||||
paraIndex++;
|
||||
paraOffset++;
|
||||
}
|
||||
|
||||
if (curTitle.paraIndex == prevParaIndex)
|
||||
curTitle.paraIndex = paraIndex;
|
||||
if (curSubtitle.paraIndex == prevParaIndex)
|
||||
curSubtitle.paraIndex = paraIndex;
|
||||
}
|
||||
|
||||
paraIndex++;
|
||||
p.index = paraIndex;
|
||||
p.offset = paraOffset;
|
||||
para[paraIndex] = p;
|
||||
paraOffset += p.length;
|
||||
}
|
||||
};
|
||||
|
||||
paraOffset -= p.length;
|
||||
//параграф оказался непустой
|
||||
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
|
||||
p.length = 0;
|
||||
p.text = p.text.substr(1);
|
||||
p.cut = (len == 1 && text[0] == ' ');
|
||||
const newParagraph = (text = '', len = 0) => {
|
||||
correctCurrentPara();
|
||||
|
||||
//новый параграф
|
||||
paraIndex++;
|
||||
let p = {
|
||||
index: paraIndex,
|
||||
offset: paraOffset,
|
||||
length: len,//длина текста внутри параграфа без учета длины разметки
|
||||
text: text,
|
||||
addIndex: 0,
|
||||
};
|
||||
|
||||
if (inSubtitle) {
|
||||
curSubtitle.title += '<p>';
|
||||
} else if (inTitle) {
|
||||
curTitle.title += '<p>';
|
||||
}
|
||||
|
||||
para[paraIndex] = p;
|
||||
paraOffset += len;
|
||||
};
|
||||
|
||||
const growParagraph = (text, len) => {
|
||||
if (paraIndex < 0) {
|
||||
newParagraph();
|
||||
growParagraph(text, len);
|
||||
return;
|
||||
}
|
||||
|
||||
if (inSubtitle) {
|
||||
curSubtitle.title += text;
|
||||
} else if (inTitle) {
|
||||
curTitle.title += text;
|
||||
}
|
||||
|
||||
const p = para[paraIndex];
|
||||
p.length += len;
|
||||
p.text += text;
|
||||
|
||||
para[paraIndex] = p;
|
||||
paraOffset += p.length;
|
||||
paraOffset += len;
|
||||
};
|
||||
|
||||
const onStartNode = (elemName, tail) => {// eslint-disable-line no-unused-vars
|
||||
@@ -167,12 +250,13 @@ 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' || binaryType == 'application/octet-stream')
|
||||
binaryType = (binaryType == 'image/jpg' || binaryType == 'application/octet-stream' ? 'image/jpeg' : binaryType);
|
||||
if (binaryType == 'image/jpeg' || binaryType == 'image/png')
|
||||
binaryId = (attrs.id.value ? attrs.id.value : '');
|
||||
}
|
||||
|
||||
@@ -180,53 +264,111 @@ export default class BookParser {
|
||||
let attrs = sax.getAttrsSync(tail);
|
||||
if (attrs.href && attrs.href.value) {
|
||||
const href = attrs.href.value;
|
||||
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
|
||||
const {id, local} = this.imageHrefToId(href);
|
||||
if (href[0] == '#') {//local
|
||||
if (inPara && !this.showInlineImagesInCenter && !center)
|
||||
growParagraph(`<image-inline href="${href}"></image-inline>`, 0);
|
||||
imageNum++;
|
||||
|
||||
if (inPara && !this.sets.showInlineImagesInCenter && !center)
|
||||
growParagraph(`<image-inline href="${href}" num="${imageNum}"></image-inline>`, 0);
|
||||
else
|
||||
newParagraph(`<image href="${href}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
||||
if (inPara && this.showInlineImagesInCenter)
|
||||
newParagraph(' ', 1);
|
||||
newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
||||
|
||||
this.images.push({paraIndex, num: imageNum, id, local, alt});
|
||||
|
||||
if (inPara && this.sets.showInlineImagesInCenter)
|
||||
newParagraph();
|
||||
} else {//external
|
||||
dimPromises.push(getExternalImageDimensions(href));
|
||||
newParagraph(`<image href="${href}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
||||
imageNum++;
|
||||
|
||||
if (!this.sets.isTesting) {
|
||||
dimPromises.push(getExternalImageDimensions(href));
|
||||
} else {
|
||||
dimPromises.push(this.sets.getExternalImageDimensions(this, href));
|
||||
}
|
||||
newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
||||
|
||||
this.images.push({paraIndex, num: imageNum, id, local, alt});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
if (isFirstBody && fb2.sequence && fb2.sequence.length) {
|
||||
const bt = utils.getBookTitle(fb2);
|
||||
if (bt.sequence) {
|
||||
newParagraph(bt.sequence, bt.sequence.length);
|
||||
newParagraph();
|
||||
}
|
||||
}
|
||||
|
||||
if (!isFirstBody)
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
isFirstBody = false;
|
||||
bodyIndex++;
|
||||
}
|
||||
|
||||
if (tag == 'title') {
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
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);
|
||||
newParagraph();
|
||||
isFirstSection = false;
|
||||
sectionLevel++;
|
||||
}
|
||||
|
||||
if (tag == 'emphasis' || tag == 'strong') {
|
||||
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
|
||||
growParagraph(`<${tag}>`, 0);
|
||||
}
|
||||
|
||||
if ((tag == 'p' || tag == 'empty-line' || tag == 'v')) {
|
||||
if (!(tag == 'p' && isFirstTitlePara))
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
if (tag == 'p') {
|
||||
inPara = true;
|
||||
isFirstTitlePara = false;
|
||||
@@ -234,23 +376,33 @@ export default class BookParser {
|
||||
}
|
||||
|
||||
if (tag == 'subtitle') {
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
isFirstTitlePara = true;
|
||||
bold = true;
|
||||
center = true;
|
||||
|
||||
if (curTitle.paraIndex < 0) {
|
||||
curTitle = {paraIndex, title: 'Оглавление', inset: sectionLevel, bodyIndex, subtitles: []};
|
||||
this.contents.push(curTitle);
|
||||
}
|
||||
|
||||
inSubtitle = true;
|
||||
curSubtitle = {paraIndex, inset: sectionLevel, title: ''};
|
||||
curTitle.subtitles.push(curSubtitle);
|
||||
}
|
||||
|
||||
if (tag == 'epigraph') {
|
||||
if (tag == 'epigraph' || tag == 'annotation') {
|
||||
italic = true;
|
||||
space += 1;
|
||||
}
|
||||
|
||||
if (tag == 'poem') {
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
}
|
||||
|
||||
if (tag == 'text-author') {
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
bold = true;
|
||||
space += 1;
|
||||
}
|
||||
}
|
||||
@@ -267,9 +419,14 @@ export default class BookParser {
|
||||
isFirstTitlePara = false;
|
||||
bold = false;
|
||||
center = false;
|
||||
inTitle = false;
|
||||
}
|
||||
|
||||
if (tag == 'emphasis' || tag == 'strong') {
|
||||
if (tag == 'section') {
|
||||
sectionLevel--;
|
||||
}
|
||||
|
||||
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
|
||||
growParagraph(`</${tag}>`, 0);
|
||||
}
|
||||
|
||||
@@ -281,18 +438,21 @@ export default class BookParser {
|
||||
isFirstTitlePara = false;
|
||||
bold = false;
|
||||
center = false;
|
||||
inSubtitle = false;
|
||||
}
|
||||
|
||||
if (tag == 'epigraph') {
|
||||
if (tag == 'epigraph' || tag == 'annotation') {
|
||||
italic = false;
|
||||
space -= 1;
|
||||
newParagraph();
|
||||
}
|
||||
|
||||
if (tag == 'stanza') {
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
}
|
||||
|
||||
if (tag == 'text-author') {
|
||||
bold = false;
|
||||
space -= 1;
|
||||
}
|
||||
}
|
||||
@@ -309,17 +469,14 @@ export default class BookParser {
|
||||
|
||||
const onTextNode = (text) => {// eslint-disable-line no-unused-vars
|
||||
text = he.decode(text);
|
||||
text = text.replace(/>/g, '>');
|
||||
text = text.replace(/</g, '<');
|
||||
text = text.replace(/>/g, '>').replace(/</g, '<').replace(/[\t\n\r\xa0]/g, ' ');
|
||||
|
||||
if (text && text.trim() == '')
|
||||
text = (text.indexOf(' ') >= 0 ? ' ' : '');
|
||||
text = ' ';
|
||||
|
||||
if (!text)
|
||||
return;
|
||||
|
||||
text = text.replace(/[\t\n\r\xa0]/g, ' ');
|
||||
|
||||
const authorLength = (fb2.author && fb2.author.length ? fb2.author.length : 0);
|
||||
switch (path) {
|
||||
case '/fictionbook/description/title-info/author/first-name':
|
||||
@@ -357,35 +514,43 @@ export default class BookParser {
|
||||
fb2.annotation += text;
|
||||
}
|
||||
|
||||
let tOpen = (center ? '<center>' : '');
|
||||
tOpen += (bold ? '<strong>' : '');
|
||||
tOpen += (italic ? '<emphasis>' : '');
|
||||
tOpen += (space ? `<space w="${space}">` : '');
|
||||
let tClose = (space ? '</space>' : '');
|
||||
tClose += (italic ? '</emphasis>' : '');
|
||||
tClose += (bold ? '</strong>' : '');
|
||||
tClose += (center ? '</center>' : '');
|
||||
if (binaryId) {
|
||||
if (!this.sets.isTesting) {
|
||||
dimPromises.push(getImageDimensions(binaryId, binaryType, text));
|
||||
} else {
|
||||
dimPromises.push(this.sets.getImageDimensions(this, binaryId, binaryType, text));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
let tOpen = (center ? '<center>' : '');
|
||||
tOpen += (bold ? '<strong>' : '');
|
||||
tOpen += (italic ? '<emphasis>' : '');
|
||||
tOpen += (space ? `<space w="${space}">` : '');
|
||||
let tClose = (space ? '</space>' : '');
|
||||
tClose += (italic ? '</emphasis>' : '');
|
||||
tClose += (bold ? '</strong>' : '');
|
||||
tClose += (center ? '</center>' : '');
|
||||
|
||||
if (binaryId) {
|
||||
dimPromises.push(getImageDimensions(binaryId, binaryType, text));
|
||||
if (text != ' ')
|
||||
growParagraph(`${tOpen}${text}${tClose}`, text.length);
|
||||
else
|
||||
growParagraph(' ', 1);
|
||||
}
|
||||
};
|
||||
|
||||
const onProgress = async(prog) => {
|
||||
await sleep(1);
|
||||
await utils.sleep(1);
|
||||
callback(prog);
|
||||
};
|
||||
|
||||
await sax.parse(data, {
|
||||
onStartNode, onEndNode, onTextNode, onProgress
|
||||
});
|
||||
correctCurrentPara();
|
||||
|
||||
if (dimPromises.length) {
|
||||
try {
|
||||
@@ -401,11 +566,20 @@ export default class BookParser {
|
||||
this.textLength = paraOffset;
|
||||
|
||||
callback(100);
|
||||
await sleep(10);
|
||||
await utils.sleep(10);
|
||||
|
||||
return {fb2};
|
||||
}
|
||||
|
||||
imageHrefToId(id) {
|
||||
let local = false;
|
||||
if (id[0] == '#') {
|
||||
id = id.substr(1);
|
||||
local = true;
|
||||
}
|
||||
return {id, local};
|
||||
}
|
||||
|
||||
findParaIndex(bookPos) {
|
||||
let result = undefined;
|
||||
//дихотомия
|
||||
@@ -430,16 +604,26 @@ export default class BookParser {
|
||||
|
||||
splitToStyle(s) {
|
||||
let result = [];/*array of {
|
||||
style: {bold: Boolean, italic: Boolean, center: Boolean, space: Number},
|
||||
style: {bold: Boolean, italic: Boolean, sup: Boolean, sub: Boolean, center: Boolean, space: Number},
|
||||
image: {local: Boolean, inline: Boolean, id: String},
|
||||
text: String,
|
||||
}*/
|
||||
let style = {};
|
||||
let image = {};
|
||||
|
||||
//оптимизация по памяти
|
||||
const copyStyle = (s) => {
|
||||
const r = {};
|
||||
for (const prop in s) {
|
||||
if (s[prop])
|
||||
r[prop] = s[prop];
|
||||
}
|
||||
return r;
|
||||
};
|
||||
|
||||
const onTextNode = async(text) => {// eslint-disable-line no-unused-vars
|
||||
result.push({
|
||||
style: Object.assign({}, style),
|
||||
style: copyStyle(style),
|
||||
image,
|
||||
text
|
||||
});
|
||||
@@ -453,6 +637,12 @@ export default class BookParser {
|
||||
case 'emphasis':
|
||||
style.italic = true;
|
||||
break;
|
||||
case 'sup':
|
||||
style.sup = true;
|
||||
break;
|
||||
case 'sub':
|
||||
style.sub = true;
|
||||
break;
|
||||
case 'center':
|
||||
style.center = true;
|
||||
break;
|
||||
@@ -465,28 +655,21 @@ export default class BookParser {
|
||||
case 'image': {
|
||||
let attrs = sax.getAttrsSync(tail);
|
||||
if (attrs.href && attrs.href.value) {
|
||||
let id = attrs.href.value;
|
||||
let local = false;
|
||||
if (id[0] == '#') {
|
||||
id = id.substr(1);
|
||||
local = true;
|
||||
}
|
||||
image = {local, inline: false, id};
|
||||
image = this.imageHrefToId(attrs.href.value);
|
||||
image.inline = false;
|
||||
image.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'image-inline': {
|
||||
let attrs = sax.getAttrsSync(tail);
|
||||
if (attrs.href && attrs.href.value) {
|
||||
let id = attrs.href.value;
|
||||
let local = false;
|
||||
if (id[0] == '#') {
|
||||
id = id.substr(1);
|
||||
local = true;
|
||||
}
|
||||
const img = this.imageHrefToId(attrs.href.value);
|
||||
img.inline = true;
|
||||
img.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
|
||||
result.push({
|
||||
style: Object.assign({}, style),
|
||||
image: {local, inline: true, id},
|
||||
style: copyStyle(style),
|
||||
image: img,
|
||||
text: ''
|
||||
});
|
||||
}
|
||||
@@ -503,6 +686,12 @@ export default class BookParser {
|
||||
case 'emphasis':
|
||||
style.italic = false;
|
||||
break;
|
||||
case 'sup':
|
||||
style.sup = false;
|
||||
break;
|
||||
case 'sub':
|
||||
style.sub = false;
|
||||
break;
|
||||
case 'center':
|
||||
style.center = false;
|
||||
break;
|
||||
@@ -522,7 +711,7 @@ export default class BookParser {
|
||||
});
|
||||
|
||||
//длинные слова (или белиберду без пробелов) тоже разобьем
|
||||
const maxWordLength = this.maxWordLength;
|
||||
const maxWordLength = this.sets.maxWordLength;
|
||||
const parts = result;
|
||||
result = [];
|
||||
for (const part of parts) {
|
||||
@@ -535,7 +724,7 @@ export default class BookParser {
|
||||
spaceIndex = i;
|
||||
|
||||
if (i - spaceIndex >= maxWordLength && i < p.text.length - 1 &&
|
||||
this.measureText(p.text.substr(spaceIndex + 1, i - spaceIndex), p.style) >= this.w - this.p) {
|
||||
this.measureText(p.text.substr(spaceIndex + 1, i - spaceIndex), p.style) >= this.sets.w - this.sets.p) {
|
||||
result.push({style: p.style, image: p.image, text: p.text.substr(0, i + 1)});
|
||||
p = {style: p.style, image: p.image, text: p.text.substr(i + 1)};
|
||||
spaceIndex = -1;
|
||||
@@ -553,88 +742,88 @@ export default class BookParser {
|
||||
splitToSlogi(word) {
|
||||
let result = [];
|
||||
|
||||
const glas = new Set(['а', 'А', 'о', 'О', 'и', 'И', 'е', 'Е', 'ё', 'Ё', 'э', 'Э', 'ы', 'Ы', 'у', 'У', 'ю', 'Ю', 'я', 'Я']);
|
||||
const soglas = new Set([
|
||||
'б', 'в', 'г', 'д', 'ж', 'з', 'й', 'к', 'л', 'м', 'н', 'п', 'р', 'с', 'т', 'ф', 'х', 'ц', 'ч', 'ш', 'щ',
|
||||
'Б', 'В', 'Г', 'Д', 'Ж', 'З', 'Й', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х', 'Ч', 'Ц', 'Ш', 'Щ'
|
||||
]);
|
||||
const znak = new Set(['ь', 'Ь', 'ъ', 'Ъ', 'й', 'Й']);
|
||||
const alpha = new Set([...glas, ...soglas, ...znak]);
|
||||
|
||||
let slog = '';
|
||||
let slogLen = 0;
|
||||
const len = word.length;
|
||||
word += ' ';
|
||||
for (let i = 0; i < len; i++) {
|
||||
slog += word[i];
|
||||
if (alpha.has(word[i]))
|
||||
slogLen++;
|
||||
if (len > 3) {
|
||||
let slog = '';
|
||||
let slogLen = 0;
|
||||
word += ' ';
|
||||
for (let i = 0; i < len; i++) {
|
||||
slog += word[i];
|
||||
if (alpha.has(word[i]))
|
||||
slogLen++;
|
||||
|
||||
if (slogLen > 1 && i < len - 2 && (
|
||||
//гласная, а следом не 2 согласные буквы
|
||||
(glas.has(word[i]) && !(soglas.has(word[i + 1]) &&
|
||||
soglas.has(word[i + 2])) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])
|
||||
) ||
|
||||
//предыдущая не согласная буква, текущая согласная, а следом согласная и согласная|гласная буквы
|
||||
(alpha.has(word[i - 1]) && !soglas.has(word[i - 1]) &&
|
||||
soglas.has(word[i]) && soglas.has(word[i + 1]) &&
|
||||
(glas.has(word[i + 2]) || soglas.has(word[i + 2])) &&
|
||||
alpha.has(word[i + 1]) && alpha.has(word[i + 2])
|
||||
) ||
|
||||
//мягкий или твердый знак или Й
|
||||
(znak.has(word[i]) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])) ||
|
||||
(word[i] == '-')
|
||||
) &&
|
||||
//нельзя оставлять окончания на ь, ъ, й
|
||||
!(znak.has(word[i + 2]) && !alpha.has(word[i + 3]))
|
||||
if (slogLen > 1 && i < len - 2 && (
|
||||
//гласная, а следом не 2 согласные буквы
|
||||
(glas.has(word[i]) && !( soglas.has(word[i + 1]) && soglas.has(word[i + 2]) ) &&
|
||||
alpha.has(word[i + 1]) && alpha.has(word[i + 2])
|
||||
) ||
|
||||
//предыдущая не согласная буква, текущая согласная, а следом согласная и согласная|гласная буквы
|
||||
(alpha.has(word[i - 1]) && !soglas.has(word[i - 1]) && soglas.has(word[i]) && soglas.has(word[i + 1]) &&
|
||||
( glas.has(word[i + 2]) || soglas.has(word[i + 2]) ) &&
|
||||
alpha.has(word[i + 1]) && alpha.has(word[i + 2])
|
||||
) ||
|
||||
//мягкий или твердый знак или Й
|
||||
(znak.has(word[i]) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])) ||
|
||||
(word[i] == '-')
|
||||
) &&
|
||||
//нельзя оставлять окончания на ь, ъ, й
|
||||
!(znak.has(word[i + 2]) && !alpha.has(word[i + 3]))
|
||||
|
||||
) {
|
||||
result.push(slog);
|
||||
slog = '';
|
||||
slogLen = 0;
|
||||
) {
|
||||
result.push(slog);
|
||||
slog = '';
|
||||
slogLen = 0;
|
||||
}
|
||||
}
|
||||
if (slog)
|
||||
result.push(slog);
|
||||
} else {
|
||||
result.push(word);
|
||||
}
|
||||
if (slog)
|
||||
result.push(slog);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
parsePara(paraIndex) {
|
||||
const para = this.para[paraIndex];
|
||||
const s = this.sets;
|
||||
|
||||
//перераспарсиваем только при изменении одного из параметров
|
||||
if (!this.force &&
|
||||
para.parsed &&
|
||||
para.parsed.w === this.w &&
|
||||
para.parsed.p === this.p &&
|
||||
para.parsed.wordWrap === this.wordWrap &&
|
||||
para.parsed.maxWordLength === this.maxWordLength &&
|
||||
para.parsed.font === this.font &&
|
||||
para.parsed.cutEmptyParagraphs === this.cutEmptyParagraphs &&
|
||||
para.parsed.addEmptyParagraphs === this.addEmptyParagraphs &&
|
||||
para.parsed.showImages === this.showImages &&
|
||||
para.parsed.imageHeightLines === this.imageHeightLines &&
|
||||
para.parsed.imageFitWidth === this.imageFitWidth &&
|
||||
para.parsed.compactTextPerc === this.compactTextPerc
|
||||
para.parsed.p === s.p &&
|
||||
para.parsed.w === s.w &&
|
||||
para.parsed.font === s.font &&
|
||||
para.parsed.fontSize === s.fontSize &&
|
||||
para.parsed.wordWrap === s.wordWrap &&
|
||||
para.parsed.cutEmptyParagraphs === s.cutEmptyParagraphs &&
|
||||
para.parsed.addEmptyParagraphs === s.addEmptyParagraphs &&
|
||||
para.parsed.maxWordLength === s.maxWordLength &&
|
||||
para.parsed.lineHeight === s.lineHeight &&
|
||||
para.parsed.showImages === s.showImages &&
|
||||
para.parsed.imageHeightLines === s.imageHeightLines &&
|
||||
para.parsed.imageFitWidth === (s.imageFitWidth || s.dualPageMode) &&
|
||||
para.parsed.compactTextPerc === s.compactTextPerc &&
|
||||
para.parsed.testWidth === s.testWidth
|
||||
)
|
||||
return para.parsed;
|
||||
|
||||
const parsed = {
|
||||
w: this.w,
|
||||
p: this.p,
|
||||
wordWrap: this.wordWrap,
|
||||
maxWordLength: this.maxWordLength,
|
||||
font: this.font,
|
||||
cutEmptyParagraphs: this.cutEmptyParagraphs,
|
||||
addEmptyParagraphs: this.addEmptyParagraphs,
|
||||
showImages: this.showImages,
|
||||
imageHeightLines: this.imageHeightLines,
|
||||
imageFitWidth: this.imageFitWidth,
|
||||
compactTextPerc: this.compactTextPerc,
|
||||
visible: !(
|
||||
(this.cutEmptyParagraphs && para.cut) ||
|
||||
(para.addIndex > this.addEmptyParagraphs)
|
||||
)
|
||||
p: s.p,
|
||||
w: s.w,
|
||||
font: s.font,
|
||||
fontSize: s.fontSize,
|
||||
wordWrap: s.wordWrap,
|
||||
cutEmptyParagraphs: s.cutEmptyParagraphs,
|
||||
addEmptyParagraphs: s.addEmptyParagraphs,
|
||||
maxWordLength: s.maxWordLength,
|
||||
lineHeight: s.lineHeight,
|
||||
showImages: s.showImages,
|
||||
imageHeightLines: s.imageHeightLines,
|
||||
imageFitWidth: (s.imageFitWidth || s.dualPageMode),
|
||||
compactTextPerc: s.compactTextPerc,
|
||||
testWidth: s.testWidth,
|
||||
visible: true, //вычисляется позже
|
||||
};
|
||||
|
||||
|
||||
@@ -650,9 +839,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 = '';//измеряемая строка
|
||||
@@ -661,26 +853,28 @@ export default class BookParser {
|
||||
let style = {};
|
||||
let ofs = 0;//смещение от начала параграфа para.offset
|
||||
let imgW = 0;
|
||||
const compactWidth = this.measureText('W', {})*this.compactTextPerc/100;
|
||||
let imageInPara = false;
|
||||
const compactWidth = this.measureText('W', {})*parsed.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};
|
||||
|
||||
let lineCount = this.imageHeightLines;
|
||||
let c = Math.ceil(bin.h/this.lineHeight);
|
||||
let lineCount = parsed.imageHeightLines;
|
||||
let c = Math.ceil(bin.h/parsed.lineHeight);
|
||||
|
||||
const maxH = lineCount*this.lineHeight;
|
||||
const maxH = lineCount*parsed.lineHeight;
|
||||
let maxH2 = maxH;
|
||||
if (this.imageFitWidth && bin.w > this.w) {
|
||||
maxH2 = bin.h*this.w/bin.w;
|
||||
c = Math.ceil(maxH2/this.lineHeight);
|
||||
if (parsed.imageFitWidth && bin.w > parsed.w) {
|
||||
maxH2 = bin.h*parsed.w/bin.w;
|
||||
c = Math.ceil(maxH2/parsed.lineHeight);
|
||||
}
|
||||
lineCount = (c < lineCount ? c : lineCount);
|
||||
|
||||
@@ -703,6 +897,7 @@ export default class BookParser {
|
||||
paraIndex,
|
||||
w: imageWidth,
|
||||
h: imageHeight,
|
||||
num: part.image.num
|
||||
}});
|
||||
lines.push(line);
|
||||
line = {begin: line.end + 1, parts: []};
|
||||
@@ -713,19 +908,19 @@ export default class BookParser {
|
||||
line.last = true;
|
||||
line.parts.push({style, text: ' ',
|
||||
image: {local: part.image.local, inline: false, id: part.image.id,
|
||||
imageLine: i, lineCount, paraIndex, w: imageWidth, h: imageHeight}
|
||||
imageLine: i, lineCount, paraIndex, w: imageWidth, h: imageHeight, num: part.image.num}
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part.image.id && part.image.inline && this.showImages) {
|
||||
if (part.image.id && part.image.inline && parsed.showImages) {
|
||||
const bin = this.binary[part.image.id];
|
||||
if (bin) {
|
||||
let imgH = (bin.h > this.fontSize ? this.fontSize : bin.h);
|
||||
let imgH = (bin.h > parsed.fontSize ? parsed.fontSize : bin.h);
|
||||
imgW += bin.w*imgH/bin.h;
|
||||
line.parts.push({style, text: '',
|
||||
image: {local: part.image.local, inline: true, id: part.image.id}});
|
||||
image: {local: part.image.local, inline: true, id: part.image.id, num: part.image.num}});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -835,6 +1030,16 @@ export default class BookParser {
|
||||
}
|
||||
}
|
||||
|
||||
//parsed.visible
|
||||
if (imageInPara) {
|
||||
parsed.visible = parsed.showImages;
|
||||
} else {
|
||||
parsed.visible = !(
|
||||
(para.addIndex > parsed.addEmptyParagraphs) ||
|
||||
(para.addIndex == 0 && parsed.cutEmptyParagraphs && paragraphText.trim() == '')
|
||||
);
|
||||
}
|
||||
|
||||
parsed.lines = lines;
|
||||
para.parsed = parsed;
|
||||
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import localForage from 'localforage';
|
||||
import path from 'path-browserify';
|
||||
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
|
||||
const maxRecentLength = 5000;
|
||||
|
||||
//локальный кэш метаданных книг, ограничение maxDataSize
|
||||
const bmMetaStore = localForage.createInstance({
|
||||
name: 'bmMetaStore'
|
||||
});
|
||||
|
||||
//локальный кэш самих книг, ограничение maxDataSize
|
||||
const bmDataStore = localForage.createInstance({
|
||||
name: 'bmDataStore'
|
||||
});
|
||||
|
||||
const bmRecentStore = localForage.createInstance({
|
||||
name: 'bmRecentStore'
|
||||
//список недавно открытых книг
|
||||
const bmRecentStoreNew = localForage.createInstance({
|
||||
name: 'bmRecentStoreNew'
|
||||
});
|
||||
|
||||
class BookManager {
|
||||
@@ -25,15 +30,41 @@ class BookManager {
|
||||
|
||||
this.eventListeners = [];
|
||||
this.books = {};
|
||||
this.recent = {};
|
||||
|
||||
this.recentLast = await bmRecentStore.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;
|
||||
this.recent = {};
|
||||
this.saveRecent = _.debounce(() => {
|
||||
bmRecentStoreNew.setItem('recent', this.recent);
|
||||
}, 300, {maxWait: 800});
|
||||
|
||||
this.saveRecentItem = _.debounce(() => {
|
||||
bmRecentStoreNew.setItem('recent-item', this.recentItem);
|
||||
this.recentRev = (this.recentRev < maxRecentLength ? this.recentRev + 1 : 1);
|
||||
bmRecentStoreNew.setItem('rev', this.recentRev);
|
||||
}, 200, {maxWait: 300});
|
||||
|
||||
//загрузка 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;
|
||||
|
||||
//конвертируем в новые ключи
|
||||
await this.convertRecent();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
this.recentChanged = true;
|
||||
@@ -41,9 +72,41 @@ class BookManager {
|
||||
this.loadStored();//no await
|
||||
}
|
||||
|
||||
//Долгая асинхронная загрузка из хранилища.
|
||||
//Хранение в отдельных записях дает относительно
|
||||
//нормальное поведение при нескольких вкладках с читалкой в браузере.
|
||||
//TODO: убрать в 2025г
|
||||
async convertRecent() {
|
||||
const converted = await bmRecentStoreNew.getItem('recent-converted');
|
||||
|
||||
if (converted)
|
||||
return;
|
||||
|
||||
const newRecent = {};
|
||||
for (const book of Object.values(this.recent)) {
|
||||
|
||||
if (!book.path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newKey = this.keyFromPath(book.path);
|
||||
|
||||
newRecent[newKey] = _.cloneDeep(book);
|
||||
newRecent[newKey].key = newKey;
|
||||
if (!newRecent[newKey].loadTime)
|
||||
newRecent[newKey].loadTime = newRecent[newKey].addTime;
|
||||
}
|
||||
|
||||
this.recent = newRecent;
|
||||
|
||||
//console.log(converted);
|
||||
(async() => {
|
||||
await utils.sleep(3000);
|
||||
this.saveRecent();
|
||||
this.emit('recent-changed');
|
||||
this.emit('set-recent');
|
||||
await bmRecentStoreNew.setItem('recent-converted', true);
|
||||
})();
|
||||
}
|
||||
|
||||
//Ленивая асинхронная загрузка bmMetaStore
|
||||
async loadStored() {
|
||||
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
|
||||
await utils.sleep(2000);
|
||||
@@ -70,32 +133,7 @@ class BookManager {
|
||||
}
|
||||
}
|
||||
|
||||
let key = null;
|
||||
len = await bmRecentStore.length();
|
||||
for (let i = len - 1; i >= 0; 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, url: utils.randomHexString(300)});
|
||||
}
|
||||
}*/
|
||||
|
||||
await this.cleanBooks();
|
||||
await this.cleanRecentBooks();
|
||||
|
||||
this.recentChanged = true;
|
||||
this.loaded = true;
|
||||
this.emit('load-stored-finish');
|
||||
}
|
||||
@@ -125,7 +163,7 @@ class BookManager {
|
||||
}
|
||||
|
||||
async deflateWithProgress(data, callback) {
|
||||
const chunkSize = 128*1024;
|
||||
const chunkSize = 512*1024;
|
||||
const deflator = new utils.pako.Deflate({level: 5});
|
||||
|
||||
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
||||
@@ -159,7 +197,7 @@ class BookManager {
|
||||
}
|
||||
|
||||
async inflateWithProgress(data, callback) {
|
||||
const chunkSize = 64*1024;
|
||||
const chunkSize = 512*1024;
|
||||
const inflator = new utils.pako.Inflate({to: 'string'});
|
||||
|
||||
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
||||
@@ -194,8 +232,8 @@ class BookManager {
|
||||
|
||||
async addBook(newBook, callback) {
|
||||
let meta = {url: newBook.url, path: newBook.path};
|
||||
meta.key = this.keyFromUrl(meta.url);
|
||||
meta.addTime = Date.now();
|
||||
meta.key = this.keyFromPath(meta.path);
|
||||
meta.addTime = Date.now();//время добавления в кеш
|
||||
|
||||
const cb = (perc) => {
|
||||
const p = Math.round(30*perc/100);
|
||||
@@ -230,15 +268,15 @@ class BookManager {
|
||||
async hasBookParsed(meta) {
|
||||
if (!this.books)
|
||||
return false;
|
||||
if (!meta.url)
|
||||
if (!meta.path)
|
||||
return false;
|
||||
if (!meta.key)
|
||||
meta.key = this.keyFromUrl(meta.url);
|
||||
meta.key = this.keyFromPath(meta.path);
|
||||
|
||||
let book = this.books[meta.key];
|
||||
|
||||
if (!book && !this.loaded) {
|
||||
book = await bmDataStore.getItem(`bmMeta-${meta.key}`);
|
||||
book = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
|
||||
if (book)
|
||||
this.books[meta.key] = book;
|
||||
}
|
||||
@@ -248,22 +286,21 @@ class BookManager {
|
||||
|
||||
async getBook(meta, callback) {
|
||||
let result = undefined;
|
||||
|
||||
if (!meta.path)
|
||||
return;
|
||||
|
||||
if (!meta.key)
|
||||
meta.key = this.keyFromUrl(meta.url);
|
||||
meta.key = this.keyFromPath(meta.path);
|
||||
|
||||
result = this.books[meta.key];
|
||||
|
||||
if (!result) {
|
||||
result = await bmDataStore.getItem(`bmMeta-${meta.key}`);
|
||||
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(5);
|
||||
@@ -323,95 +360,127 @@ class BookManager {
|
||||
return result;
|
||||
}
|
||||
|
||||
keyFromUrl(url) {
|
||||
/*keyFromUrl(url) {
|
||||
return utils.stringToHex(url);
|
||||
}*/
|
||||
|
||||
keyFromPath(bookPath) {
|
||||
return path.basename(bookPath);
|
||||
}
|
||||
|
||||
keysEqual(bookPath1, bookPath2) {
|
||||
if (bookPath1 === undefined || bookPath2 === undefined)
|
||||
return false;
|
||||
|
||||
return (this.keyFromPath(bookPath1) === this.keyFromPath(bookPath2));
|
||||
}
|
||||
//-- recent --------------------------------------------------------------
|
||||
async setRecentBook(value) {
|
||||
const result = this.metaOnly(value);
|
||||
result.touchTime = Date.now();
|
||||
result.deleted = 0;
|
||||
|
||||
if (this.recent[result.key] && this.recent[result.key].deleted) {
|
||||
//восстановим из небытия пользовательские данные
|
||||
if (!result.bookPos)
|
||||
result.bookPos = this.recent[result.key].bookPos;
|
||||
if (!result.bookPosSeen)
|
||||
result.bookPosSeen = this.recent[result.key].bookPosSeen;
|
||||
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;
|
||||
}
|
||||
|
||||
this.recent[result.key] = result;
|
||||
const prevKey = (this.recentItem ? this.recentItem.key : '');
|
||||
if (item) {
|
||||
this.recent[item.key] = item;
|
||||
this.recentItem = item;
|
||||
} else {
|
||||
this.recentItem = null;
|
||||
}
|
||||
|
||||
await bmRecentStore.setItem(result.key, result);
|
||||
this.saveRecentItem();
|
||||
|
||||
this.recentLast = result;
|
||||
await bmRecentStore.setItem('recent-last', this.recentLast);
|
||||
if (!item || prevKey != item.key) {
|
||||
this.saveRecent();
|
||||
}
|
||||
|
||||
this.recentChanged = true;
|
||||
this.emit('recent-changed', result.key);
|
||||
|
||||
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) {
|
||||
let result = this.metaOnly(value);
|
||||
result.touchTime = Date.now();//время последнего чтения
|
||||
if (!result.loadTime)
|
||||
result.loadTime = Date.now();//время загрузки файла
|
||||
|
||||
result.deleted = 0;
|
||||
|
||||
if (this.recent[result.key]) {
|
||||
result = Object.assign({}, this.recent[result.key], result);
|
||||
}
|
||||
|
||||
await this.recentSetLastKey(result.key);
|
||||
await this.recentSetItem(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async getRecentBook(value) {
|
||||
let result = this.recent[value.key];
|
||||
if (!result) {
|
||||
result = await bmRecentStore.getItem(value.key);
|
||||
if (result)
|
||||
this.recent[value.key] = result;
|
||||
}
|
||||
return result;
|
||||
return this.recent[value.key];
|
||||
}
|
||||
|
||||
async delRecentBook(value) {
|
||||
this.recent[value.key].deleted = 1;
|
||||
await bmRecentStore.setItem(value.key, this.recent[value.key]);
|
||||
const item = this.recent[value.key];
|
||||
item.deleted = 1;
|
||||
|
||||
if (this.recentLast.key == value.key) {
|
||||
this.recentLast = null;
|
||||
await bmRecentStore.setItem('recent-last', this.recentLast);
|
||||
if (this.recentLastKey == value.key) {
|
||||
await this.recentSetLastKey(null);
|
||||
}
|
||||
|
||||
await this.recentSetItem(item);
|
||||
this.emit('recent-deleted', value.key);
|
||||
this.emit('recent-changed', value.key);
|
||||
}
|
||||
|
||||
async cleanRecentBooks() {
|
||||
const sorted = this.getSortedRecent();
|
||||
|
||||
let isDel = false;
|
||||
for (let i = 1000; i < sorted.length; i++) {
|
||||
await bmRecentStore.removeItem(sorted[i].key);
|
||||
for (let i = maxRecentLength; i < sorted.length; i++) {
|
||||
delete this.recent[sorted[i].key];
|
||||
await bmRecentStore.removeItem(sorted[i].key);
|
||||
isDel = true;
|
||||
}
|
||||
|
||||
this.sortedRecentCached = null;
|
||||
|
||||
if (isDel)
|
||||
this.emit('recent-changed');
|
||||
await this.recentSetItem();
|
||||
return isDel;
|
||||
}
|
||||
|
||||
mostRecentBook() {
|
||||
if (this.recentLast) {
|
||||
return this.recentLast;
|
||||
if (this.recentLastKey) {
|
||||
return this.recent[this.recentLastKey];
|
||||
}
|
||||
const oldRecentLast = this.recentLast;
|
||||
const oldKey = this.recentLastKey;
|
||||
|
||||
let max = 0;
|
||||
let result = null;
|
||||
for (let key in this.recent) {
|
||||
for (const key in this.recent) {
|
||||
const book = this.recent[key];
|
||||
if (!book.deleted && book.touchTime > max) {
|
||||
max = book.touchTime;
|
||||
result = book;
|
||||
}
|
||||
}
|
||||
this.recentLast = result;
|
||||
bmRecentStore.setItem('recent-last', this.recentLast);//no await
|
||||
|
||||
const newRecentLastKey = (result ? result.key : null);
|
||||
this.recentSetLastKey(newRecentLastKey);//no await
|
||||
|
||||
if (this.recentLast !== oldRecentLast)
|
||||
if (newRecentLastKey !== oldKey)
|
||||
this.emit('recent-changed');
|
||||
|
||||
return result;
|
||||
@@ -431,6 +500,43 @@ class BookManager {
|
||||
return result;
|
||||
}
|
||||
|
||||
findRecentByUrlAndPath(url, bookPath) {
|
||||
if (bookPath) {
|
||||
const key = this.keyFromPath(bookPath);
|
||||
const book = this.recent[key];
|
||||
if (book && !book.deleted)
|
||||
return book;
|
||||
}
|
||||
|
||||
let max = 0;
|
||||
let result = null;
|
||||
|
||||
for (const key in this.recent) {
|
||||
const book = this.recent[key];
|
||||
if (!book.deleted && book.url == url && book.loadTime > max) {
|
||||
max = book.loadTime;
|
||||
result = book;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
findRecentBySameBookKey(sameKey) {
|
||||
let max = 0;
|
||||
let result = null;
|
||||
|
||||
for (const key in this.recent) {
|
||||
const book = this.recent[key];
|
||||
if (!book.deleted && book.sameBookKey == sameKey && book.loadTime > max) {
|
||||
max = book.loadTime;
|
||||
result = book;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async setRecent(value) {
|
||||
const mergedRecent = _.cloneDeep(this.recent);
|
||||
|
||||
@@ -442,29 +548,17 @@ class BookManager {
|
||||
delete mergedRecent[i];
|
||||
}
|
||||
|
||||
//"ленивое" обновление хранилища
|
||||
(async() => {
|
||||
for (const rec of Object.values(mergedRecent)) {
|
||||
if (rec.key) {
|
||||
await bmRecentStore.setItem(rec.key, rec);
|
||||
await utils.sleep(1);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
this.recent = mergedRecent;
|
||||
|
||||
this.recentLast = null;
|
||||
await bmRecentStore.setItem('recent-last', this.recentLast);
|
||||
await this.recentSetLastKey(null);
|
||||
await this.recentSetItem(null, true);
|
||||
|
||||
this.recentChanged = true;
|
||||
this.emit('set-recent');
|
||||
this.emit('recent-changed');
|
||||
}
|
||||
|
||||
addEventListener(listener) {
|
||||
if (this.eventListeners.indexOf(listener) < 0)
|
||||
this.eventListeners.push(listener);
|
||||
this.eventListeners.push(listener);
|
||||
}
|
||||
|
||||
removeEventListener(listener) {
|
||||
|
||||
40
client/components/Reader/share/wallpaperStorage.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import localForage from 'localforage';
|
||||
//import _ from 'lodash';
|
||||
|
||||
const wpStore = localForage.createInstance({
|
||||
name: 'wallpaperStorage'
|
||||
});
|
||||
|
||||
class WallpaperStorage {
|
||||
constructor() {
|
||||
this.cachedKeys = [];
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.cachedKeys = await wpStore.keys();
|
||||
}
|
||||
|
||||
async getLength() {
|
||||
return await wpStore.length();
|
||||
}
|
||||
|
||||
async setData(key, data) {
|
||||
await wpStore.setItem(key, data);
|
||||
this.cachedKeys = await wpStore.keys();
|
||||
}
|
||||
|
||||
async getData(key) {
|
||||
return await wpStore.getItem(key);
|
||||
}
|
||||
|
||||
async removeData(key) {
|
||||
await wpStore.removeItem(key);
|
||||
this.cachedKeys = await wpStore.keys();
|
||||
}
|
||||
|
||||
keyExists(key) {//не асинхронная
|
||||
return this.cachedKeys.includes(key);
|
||||
}
|
||||
}
|
||||
|
||||
export default new WallpaperStorage();
|
||||
@@ -1,55 +1,418 @@
|
||||
export const versionHistory = [
|
||||
{
|
||||
version: '0.11.7',
|
||||
releaseDate: '2022-07-12',
|
||||
showUntil: '2022-07-19',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлено автосокрытие панели управления при листании, отключается в настройках</li>
|
||||
<li>изменения в окне загруженных книг:</li>
|
||||
<ul>
|
||||
<li>добавлена группировка по версиям файла одной и той же книги</li>
|
||||
<li>группировка происходит по имени загружаемого файла, либо по URL книги</li>
|
||||
<li>добавлены различные методы сортировки списка загруженных книг</li>
|
||||
<li>нумерация всегда осуществляется по времени загрузки</li>
|
||||
</ul>
|
||||
<li>незначительные общие изменения интерфейса, приведение к единому стилю</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.11.6',
|
||||
releaseDate: '2022-07-02',
|
||||
showUntil: '2022-07-01',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>улучшено копирование текста прямо со страницы, для переводчиков</li>
|
||||
<li>актуализация используемых пакетов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.11.5',
|
||||
releaseDate: '2022-04-15',
|
||||
showUntil: '2022-04-14',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>небольшие дополнения интерфейса</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.11.1',
|
||||
releaseDate: '2021-12-03',
|
||||
showUntil: '2021-12-02',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>переход на JembaDb вместо SQLite</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.11.0',
|
||||
releaseDate: '2021-11-18',
|
||||
showUntil: '2021-11-17',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>переход на Vue 3</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.10.3',
|
||||
releaseDate: '2021-10-24',
|
||||
showUntil: '2021-10-23',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.10.2',
|
||||
releaseDate: '2021-10-19',
|
||||
showUntil: '2021-10-18',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>актуализация версий пакетов и стека используемых технологий</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.10.1',
|
||||
releaseDate: '2021-10-10',
|
||||
showUntil: '2021-10-09',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.10.0',
|
||||
releaseDate: '2021-02-09',
|
||||
showUntil: '2021-02-16',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлен двухстраничный режим</li>
|
||||
<li>в настройки добавлены все кириллические веб-шрифты от google</li>
|
||||
<li>в настройки добавлена возможность загрузки пользовательских обоев (пока без синхронизации)</li>
|
||||
<li>немного улучшен парсинг fb2</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.12',
|
||||
releaseDate: '2020-12-18',
|
||||
showUntil: '2020-12-17',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлена вкладка "Изображения" в окно оглавления</li>
|
||||
<li>настройки конвертирования вынесены в отдельную вкладку</li>
|
||||
<li>добавлена кнопка для быстрого доступа к настройкам конвертирования</li>
|
||||
<li>улучшения работы конвертеров</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.11',
|
||||
releaseDate: '2020-12-09',
|
||||
showUntil: '2020-12-08',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>оптимизации, улучшения работы конвертеров</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.10',
|
||||
releaseDate: '2020-12-03',
|
||||
showUntil: '2020-12-10',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлена частичная поддержка формата Djvu</li>
|
||||
<li>добавлена поддержка Rar-архивов</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.9',
|
||||
releaseDate: '2020-11-21',
|
||||
showUntil: '2020-11-20',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>оптимизации, исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.8',
|
||||
releaseDate: '2020-11-13',
|
||||
showUntil: '2020-11-12',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлено окно "Оглавление/закладки"</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.7',
|
||||
releaseDate: '2020-11-12',
|
||||
showUntil: '2020-11-11',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.6',
|
||||
releaseDate: '2020-11-06',
|
||||
showUntil: '2020-11-05',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>завершена работа над новым окном "Библиотека"</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.5',
|
||||
releaseDate: '2020-11-01',
|
||||
showUntil: '2020-10-31',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.4',
|
||||
releaseDate: '2020-10-29',
|
||||
showUntil: '2020-10-28',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>заработал новый сайт <a href="https://liberama.top">https://liberama.top</a>, где будет более свободный обмен книгами</li>
|
||||
<li>для liberama.top добавлено новое окно: "Библиотека"</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.3',
|
||||
releaseDate: '2020-05-21',
|
||||
showUntil: '2020-05-20',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.2',
|
||||
releaseDate: '2020-03-15',
|
||||
showUntil: '2020-04-25',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>в настройки добавлена возможность назначать сочетания клавиш на команды в читалке</li>
|
||||
<li>переход на Service Worker вместо AppCache для автономного режима работы</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.1',
|
||||
releaseDate: '2020-03-03',
|
||||
showUntil: '2020-03-02',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>улучшение работы серверной части</li>
|
||||
<li>незначительные изменения интерфейса</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.0',
|
||||
releaseDate: '2020-02-26',
|
||||
showUntil: '2020-02-25',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>переход на UI-фреймфорк Quasar</li>
|
||||
<li>незначительные изменения интерфейса</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.8.4',
|
||||
releaseDate: '2020-02-06',
|
||||
showUntil: '2020-02-05',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлен paypal-адрес для пожертвований</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.8.3',
|
||||
releaseDate: '2020-01-28',
|
||||
showUntil: '2020-01-27',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
|
||||
<li>внутренние оптимизации</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.8.2',
|
||||
releaseDate: '2020-01-20',
|
||||
showUntil: '2020-01-19',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>внутренние оптимизации</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.8.1',
|
||||
releaseDate: '2020-01-07',
|
||||
showUntil: '2020-01-06',
|
||||
header: '0.8.1 (2020-01-07)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлена частичная поддержка формата FB3</li>
|
||||
<li>исправлен баг "Request path contains unescaped characters"</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.8.0',
|
||||
releaseDate: '2020-01-02',
|
||||
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>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.9',
|
||||
releaseDate: '2019-11-27',
|
||||
showUntil: '2019-11-26',
|
||||
header: '0.7.9 (2019-11-27)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлен неубираемый баннер для http-версии о переходе на httpS</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.8',
|
||||
releaseDate: '2019-11-25',
|
||||
showUntil: '2019-11-24',
|
||||
header: '0.7.8 (2019-11-25)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>улучшение html-фильтров для сайтов</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.7',
|
||||
releaseDate: '2019-11-06',
|
||||
showUntil: '2019-11-10',
|
||||
header: '0.7.7 (2019-11-06)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -61,34 +424,40 @@ export const versionHistory = [
|
||||
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.6',
|
||||
releaseDate: '2019-10-30',
|
||||
showUntil: '2019-10-29',
|
||||
header: '0.7.6 (2019-10-30)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.5',
|
||||
releaseDate: '2019-10-22',
|
||||
showUntil: '2019-10-21',
|
||||
header: '0.7.5 (2019-10-22)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.3',
|
||||
releaseDate: '2019-10-18',
|
||||
showUntil: '2019-10-17',
|
||||
header: '0.7.3 (2019-10-18)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -97,12 +466,14 @@ export const versionHistory = [
|
||||
<li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.1',
|
||||
releaseDate: '2019-09-20',
|
||||
showUntil: '2019-09-19',
|
||||
header: '0.7.1 (2019-09-20)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -110,12 +481,14 @@ export const versionHistory = [
|
||||
<li>на панель управления добавлена кнопка "Автономный режим"</li>
|
||||
<li>актуализирована справка</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.0',
|
||||
releaseDate: '2019-09-07',
|
||||
showUntil: '2019-10-01',
|
||||
header: '0.7.0 (2019-09-07)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -126,23 +499,27 @@ export const versionHistory = [
|
||||
<li>немного улучшен внешний вид и управление на смартфонах</li>
|
||||
<li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.6.10',
|
||||
releaseDate: '2019-07-21',
|
||||
showUntil: '2019-07-20',
|
||||
header: '0.6.10 (2019-07-21)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.6.9',
|
||||
releaseDate: '2019-06-23',
|
||||
showUntil: '2019-06-22',
|
||||
header: '0.6.9 (2019-06-23)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -153,12 +530,14 @@ export const versionHistory = [
|
||||
<li>улучшены прогрессбары</li>
|
||||
<li>исправления недочетов, небольшие оптимизации</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.6.7',
|
||||
releaseDate: '2019-05-30',
|
||||
showUntil: '2019-06-05',
|
||||
header: '0.6.7 (2019-05-30)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -171,36 +550,42 @@ export const versionHistory = [
|
||||
<li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li>
|
||||
<li>исправления багов и недочетов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.6.6',
|
||||
releaseDate: '2019-03-29',
|
||||
showUntil: '2019-03-29',
|
||||
header: '0.6.6 (2019-03-29)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li>
|
||||
<li>оптимизации процесса синхронизации, внутренние переделки</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.6.4',
|
||||
releaseDate: '2019-03-24',
|
||||
showUntil: '2019-03-24',
|
||||
header: '0.6.4 (2019-03-24)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов, оптимизации</li>
|
||||
<li>добавлена возможность синхронизации данных между устройствами</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.5.4',
|
||||
releaseDate: '2019-03-04',
|
||||
showUntil: '2019-03-04',
|
||||
header: '0.5.4 (2019-03-04)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -209,12 +594,14 @@ export const versionHistory = [
|
||||
<li>(0.4.2) фильтр для СИ больше не вырезает изображения</li>
|
||||
<li>(0.4.0) добавлено отображение картинок в fb2</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.3.0',
|
||||
releaseDate: '2019-02-17',
|
||||
showUntil: '2019-02-17',
|
||||
header: '0.3.0 (2019-02-17)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -222,12 +609,14 @@ export const versionHistory = [
|
||||
<li>улучшено распознавание текста</li>
|
||||
<li>изменена верстка страницы - убрано позиционирование каждого слова</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.1.7',
|
||||
releaseDate: '2019-02-14',
|
||||
showUntil: '2019-02-14',
|
||||
header: '0.1.7 (2019-02-14)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -237,17 +626,20 @@ export const versionHistory = [
|
||||
<li>добавлена возможность сброса настроек</li>
|
||||
<li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.1.0',
|
||||
releaseDate: '2019-02-12',
|
||||
showUntil: '2019-02-12',
|
||||
header: '0.1.0 (2019-02-12)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>первый деплой проекта, длительность разработки - 2 месяца</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Settings в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Settings extends Vue {
|
||||
class Settings {
|
||||
created() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(Settings);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Sources в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Sources extends Vue {
|
||||
class Sources {
|
||||
created() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(Sources);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
80
client/components/share/Dialog.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<q-dialog v-model="active" no-route-dismiss @show="onShow" @hide="onHide">
|
||||
<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 v-close-popup flat round dense>
|
||||
<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 vueComponent from '../vueComponent.js';
|
||||
import * as utils from '../../share/utils';
|
||||
|
||||
class Dialog {
|
||||
_props = {
|
||||
modelValue: Boolean,
|
||||
};
|
||||
|
||||
shown = false;
|
||||
|
||||
get active() {
|
||||
return this.modelValue;
|
||||
}
|
||||
|
||||
set active(value) {
|
||||
this.$emit('update:modelValue', value);
|
||||
}
|
||||
|
||||
onShow() {
|
||||
this.shown = true;
|
||||
}
|
||||
|
||||
onHide() {
|
||||
this.shown = false;
|
||||
}
|
||||
|
||||
async waitShown() {
|
||||
let i = 100;
|
||||
while (!this.shown && i > 0) {
|
||||
await utils.sleep(10);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(Dialog);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.caption {
|
||||
font-size: 110%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
width: 50px;
|
||||
}
|
||||
</style>
|
||||
58
client/components/share/Notify.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="hidden"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import vueComponent from '../vueComponent.js';
|
||||
|
||||
class Notify {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(Notify);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
188
client/components/share/NumInput.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<q-input
|
||||
v-model="filteredValue"
|
||||
outlined dense
|
||||
input-style="text-align: center"
|
||||
class="no-mp"
|
||||
:class="(error ? 'error' : '')"
|
||||
:disable="disable"
|
||||
>
|
||||
<slot></slot>
|
||||
<template #prepend>
|
||||
<q-icon
|
||||
v-ripple="validate(modelValue - step)"
|
||||
:class="(validate(modelValue - step) ? '' : 'disable')"
|
||||
name="la la-minus-circle"
|
||||
class="button"
|
||||
@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 #append>
|
||||
<q-icon
|
||||
v-ripple="validate(modelValue + step)"
|
||||
:class="(validate(modelValue + step) ? '' : 'disable')"
|
||||
name="la la-plus-circle"
|
||||
class="button"
|
||||
@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 vueComponent from '../vueComponent.js';
|
||||
|
||||
import * as utils from '../../share/utils';
|
||||
|
||||
const componentOptions = {
|
||||
watch: {
|
||||
filteredValue: function(newValue) {
|
||||
if (this.validate(newValue)) {
|
||||
this.error = false;
|
||||
this.$emit('update:modelValue', this.string2number(newValue));
|
||||
} else {
|
||||
this.error = true;
|
||||
}
|
||||
},
|
||||
modelValue: function(newValue) {
|
||||
this.filteredValue = newValue;
|
||||
},
|
||||
}
|
||||
};
|
||||
class NumInput {
|
||||
_options = componentOptions;
|
||||
_props = {
|
||||
modelValue: 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
|
||||
};
|
||||
|
||||
filteredValue = 0;
|
||||
error = false;
|
||||
|
||||
created() {
|
||||
this.filteredValue = this.modelValue;
|
||||
}
|
||||
|
||||
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.modelValue + this.step;
|
||||
if (this.validate(newValue))
|
||||
this.filteredValue = newValue;
|
||||
}
|
||||
|
||||
minus() {
|
||||
const newValue = this.modelValue - 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.$root.isMobileDevice)
|
||||
return;
|
||||
if (event.touches.length == 1) {
|
||||
this.inTouch = true;
|
||||
this.onMouseDown({button: 0}, way);
|
||||
}
|
||||
}
|
||||
|
||||
onTouchEnd() {
|
||||
if (!this.$root.isMobileDevice)
|
||||
return;
|
||||
this.inTouch = false;
|
||||
this.onMouseUp();
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(NumInput);
|
||||
//-----------------------------------------------------------------------------
|
||||
</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>
|
||||
406
client/components/share/StdDialog.vue
Normal file
@@ -0,0 +1,406 @@
|
||||
<template>
|
||||
<q-dialog ref="dialog" v-model="active" no-route-dismiss @show="onShow" @hide="onHide">
|
||||
<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 v-close-popup flat round dense>
|
||||
<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 v-close-popup flat round dense>
|
||||
<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 v-close-popup class="q-px-md q-ml-sm" dense no-caps>
|
||||
Отмена
|
||||
</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 == 'askYesNo'" 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 v-close-popup flat round dense>
|
||||
<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 v-close-popup class="q-px-md q-ml-sm" dense no-caps>
|
||||
Нет
|
||||
</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
|
||||
Да
|
||||
</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 v-close-popup flat round dense>
|
||||
<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" v-model="inputValue" class="q-mt-xs" outlined dense />
|
||||
<div class="error">
|
||||
<span v-show="error != ''">{{ error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons row justify-end q-pa-md">
|
||||
<q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
|
||||
Отмена
|
||||
</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 v-close-popup flat round dense>
|
||||
<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 v-close-popup class="q-px-md q-ml-sm" dense no-caps>
|
||||
Отмена
|
||||
</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps :disabled="hotKeyCode == ''" @click="okClick">
|
||||
OK
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import vueComponent from '../vueComponent.js';
|
||||
import * as utils from '../../share/utils';
|
||||
|
||||
const componentOptions = {
|
||||
watch: {
|
||||
inputValue: function(newValue) {
|
||||
this.validate(newValue);
|
||||
},
|
||||
}
|
||||
};
|
||||
class StdDialog {
|
||||
_options = componentOptions;
|
||||
caption = '';
|
||||
message = '';
|
||||
active = false;
|
||||
type = '';
|
||||
inputValue = '';
|
||||
error = '';
|
||||
iconColor = '';
|
||||
iconName = '';
|
||||
hotKeyCode = '';
|
||||
|
||||
created() {
|
||||
if (this.$root.addEventHook) {
|
||||
this.$root.addEventHook('key', 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;
|
||||
});
|
||||
}
|
||||
|
||||
askYesNo(message, caption, opts) {
|
||||
return new Promise((resolve) => {
|
||||
this.init(message, caption, opts);
|
||||
|
||||
this.hideTrigger = () => {
|
||||
if (this.ok) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.type = 'askYesNo';
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(StdDialog);
|
||||
//-----------------------------------------------------------------------------
|
||||
</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,12 +1,20 @@
|
||||
<template>
|
||||
<div ref="main" class="main" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
|
||||
<div ref="windowBox" class="windowBox" @click.stop>
|
||||
<div class="window">
|
||||
<div ref="header" class="header" @mousedown.prevent.stop="onMouseDown"
|
||||
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove">
|
||||
<span class="header-text"><slot name="header"></slot></span>
|
||||
<span class="close-button" @mousedown.stop @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>
|
||||
@@ -15,20 +23,22 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
props: {
|
||||
class Window {
|
||||
_props = {
|
||||
height: { type: String, default: '100%' },
|
||||
width: { type: String, default: '100%' },
|
||||
maxWidth: { type: String, default: '' },
|
||||
topShift: { type: Number, default: 0 },
|
||||
}
|
||||
})
|
||||
class Window extends Vue {
|
||||
margin: '',
|
||||
};
|
||||
|
||||
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)
|
||||
@@ -38,11 +48,14 @@ class Window extends Vue {
|
||||
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)
|
||||
if (this.$root.isMobileDevice)
|
||||
return;
|
||||
if (event.button == 0) {
|
||||
this.$refs.header.style.cursor = 'move';
|
||||
@@ -72,7 +85,7 @@ class Window extends Vue {
|
||||
}
|
||||
|
||||
onTouchStart(event) {
|
||||
if (!this.$isMobileDevice)
|
||||
if (!this.$root.isMobileDevice)
|
||||
return;
|
||||
if (event.touches.length == 1) {
|
||||
const touch = event.touches[0];
|
||||
@@ -84,7 +97,7 @@ class Window extends Vue {
|
||||
}
|
||||
|
||||
onTouchMove(event) {
|
||||
if (!this.$isMobileDevice)
|
||||
if (!this.$root.isMobileDevice)
|
||||
return;
|
||||
if (event.touches.length == 1 && this.moving) {
|
||||
const touch = event.touches[0];
|
||||
@@ -99,7 +112,7 @@ class Window extends Vue {
|
||||
}
|
||||
|
||||
onTouchEnd() {
|
||||
if (!this.$isMobileDevice)
|
||||
if (!this.$root.isMobileDevice)
|
||||
return;
|
||||
this.$refs.header.style.cursor = 'default';
|
||||
this.moving = false;
|
||||
@@ -111,28 +124,27 @@ class Window extends Vue {
|
||||
this.$emit('close');
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(Window);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: transparent !important;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.windowBox {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
.xyfit {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.window {
|
||||
.flexfit {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.window {
|
||||
margin: 10px;
|
||||
background-color: #ffffff;
|
||||
border: 3px double black;
|
||||
@@ -141,29 +153,29 @@ class Window extends Vue {
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
background-color: #59B04F;
|
||||
background: linear-gradient(to bottom right, #007000, #59B04F);
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
color: #FFFFA0;
|
||||
text-shadow: 2px 2px 5px #005000, 2px 1px 5px #005000;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background-color: #69C05F;
|
||||
color: white;
|
||||
background-color: #FF3030;
|
||||
}
|
||||
</style>
|
||||
|
||||
</style>
|
||||
|
||||
52
client/components/vueComponent.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default function(componentClass) {
|
||||
const comp = {};
|
||||
const obj = new componentClass();
|
||||
|
||||
//data, options, props
|
||||
const data = {};
|
||||
for (const prop of Object.getOwnPropertyNames(obj)) {
|
||||
if (['_options', '_props'].includes(prop)) {//meta props
|
||||
if (prop === '_options') {
|
||||
const options = obj[prop];
|
||||
for (const optName of ['components', 'watch', 'emits']) {
|
||||
if (options[optName]) {
|
||||
comp[optName] = options[optName];
|
||||
}
|
||||
}
|
||||
} else if (prop === '_props') {
|
||||
comp['props'] = obj[prop];
|
||||
}
|
||||
} else {//usual prop
|
||||
data[prop] = obj[prop];
|
||||
}
|
||||
}
|
||||
comp.data = () => _.cloneDeep(data);
|
||||
|
||||
//methods
|
||||
const classProto = Object.getPrototypeOf(obj);
|
||||
const classMethods = Object.getOwnPropertyNames(classProto);
|
||||
const methods = {};
|
||||
const computed = {};
|
||||
for (const method of classMethods) {
|
||||
const desc = Object.getOwnPropertyDescriptor(classProto, method);
|
||||
if (desc.get) {//has getter, computed
|
||||
computed[method] = {get: desc.get};
|
||||
if (desc.set)
|
||||
computed[method].set = desc.set;
|
||||
} else if ( ['beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'activated',//life cycle hooks
|
||||
'deactivated', 'beforeUnmount', 'unmounted', 'errorCaptured', 'renderTracked', 'renderTriggered',//life cycle hooks
|
||||
'setup'].includes(method) ) {
|
||||
comp[method] = obj[method];
|
||||
} else if (method !== 'constructor') {//usual
|
||||
methods[method] = obj[method];
|
||||
}
|
||||
}
|
||||
comp.methods = methods;
|
||||
comp.computed = computed;
|
||||
|
||||
//console.log(comp);
|
||||
return defineComponent(comp);
|
||||
}
|
||||
@@ -1,68 +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 ElMenu from 'element-ui/lib/menu';
|
||||
import ElMenuItem from 'element-ui/lib/menu-item';
|
||||
import ElButton from 'element-ui/lib/button';
|
||||
import ElButtonGroup from 'element-ui/lib/button-group';
|
||||
import ElCheckbox from 'element-ui/lib/checkbox';
|
||||
import ElTabs from 'element-ui/lib/tabs';
|
||||
import ElTabPane from 'element-ui/lib/tab-pane';
|
||||
import ElTooltip from 'element-ui/lib/tooltip';
|
||||
import ElCol from 'element-ui/lib/col';
|
||||
import ElContainer from 'element-ui/lib/container';
|
||||
import ElAside from 'element-ui/lib/aside';
|
||||
import ElHeader from 'element-ui/lib/header';
|
||||
import ElMain from 'element-ui/lib/main';
|
||||
import ElInput from 'element-ui/lib/input';
|
||||
import ElInputNumber from 'element-ui/lib/input-number';
|
||||
import ElSelect from 'element-ui/lib/select';
|
||||
import ElOption from 'element-ui/lib/option';
|
||||
import ElTable from 'element-ui/lib/table';
|
||||
import ElTableColumn from 'element-ui/lib/table-column';
|
||||
import ElProgress from 'element-ui/lib/progress';
|
||||
import ElSlider from 'element-ui/lib/slider';
|
||||
import ElForm from 'element-ui/lib/form';
|
||||
import ElFormItem from 'element-ui/lib/form-item';
|
||||
import ElColorPicker from 'element-ui/lib/color-picker';
|
||||
import ElDialog from 'element-ui/lib/dialog';
|
||||
|
||||
import Notification from 'element-ui/lib/notification';
|
||||
import Loading from 'element-ui/lib/loading';
|
||||
import MessageBox from 'element-ui/lib/message-box';
|
||||
|
||||
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, ElDialog,
|
||||
};
|
||||
|
||||
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,14 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html manifest="/app/manifest.appcache">
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="description" content="Браузерная онлайн-читалка книг. Поддерживаются форматы: fb2, html, txt, rtf, doc, docx, pdf, epub, mobi.">
|
||||
<meta name="keywords" content="онлайн,читалка,fb2,книги,читать,браузер,интернет">
|
||||
<title></title>
|
||||
<script src="/sw-register.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="https://yastatic.net/share2/share.js" async="async"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import Vue from 'vue';
|
||||
import { createApp } from 'vue';
|
||||
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import './element';
|
||||
import q from './quasar';
|
||||
|
||||
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,
|
||||
store,
|
||||
render: h => h(App),
|
||||
}).$mount('#app');
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(router);
|
||||
app.use(store);
|
||||
app.use(q.quasar, q.options);
|
||||
q.init();
|
||||
|
||||
app.mount('#app');
|
||||
|
||||
98
client/quasar.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'quasar/dist/quasar.css';
|
||||
//import Quasar from 'quasar/dist/quasar.umd.prod.js';
|
||||
|
||||
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} from 'quasar/src/components/item';
|
||||
import {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 {QVirtualScroll} from 'quasar/src/components/virtual-scroll';
|
||||
|
||||
//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,
|
||||
QVirtualScroll,
|
||||
};
|
||||
|
||||
//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,
|
||||
};
|
||||
|
||||
//icons
|
||||
//import '@quasar/extras/fontawesome-v5/fontawesome-v5.css';
|
||||
//import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js'
|
||||
|
||||
import '@quasar/extras/line-awesome/line-awesome.css';
|
||||
import lineAwesome from 'quasar/icon-set/line-awesome.js'
|
||||
|
||||
export default {
|
||||
quasar: Quasar,
|
||||
options: { config, components, directives, plugins },
|
||||
init: () => {
|
||||
Quasar.iconSet.set(lineAwesome);
|
||||
}
|
||||
};
|
||||
@@ -1,43 +1,41 @@
|
||||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
import _ from 'lodash';
|
||||
|
||||
//немедленная загрузка
|
||||
import CardIndex from './components/CardIndex/CardIndex.vue';
|
||||
//const CardIndex = () => import('./components/CardIndex/CardIndex.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');
|
||||
|
||||
//немедленная загрузка
|
||||
//const Reader = () => import('./components/Reader/Reader.vue');
|
||||
import Reader from './components/Reader/Reader.vue';
|
||||
//import Reader from './components/Reader/Reader.vue';
|
||||
const Reader = () => import('./components/Reader/Reader.vue');
|
||||
const ExternalLibs = () => import('./components/ExternalLibs/ExternalLibs.vue');
|
||||
|
||||
//const Forum = () => import('./components/Forum/Forum.vue');
|
||||
const Income = () => import('./components/Income/Income.vue');
|
||||
const Sources = () => import('./components/Sources/Sources.vue');
|
||||
const Settings = () => import('./components/Settings/Settings.vue');
|
||||
const Help = () => import('./components/Help/Help.vue');
|
||||
//const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
|
||||
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],
|
||||
['/:pathMatch(.*)*', null, null, '/cardindex'],
|
||||
];
|
||||
|
||||
let routes = {};
|
||||
@@ -64,8 +62,7 @@ for (let route of myRoutes) {
|
||||
}
|
||||
routes = routes.children;
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
export default new VueRouter({
|
||||
export default createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
});
|
||||
|
||||
53
client/share/LockQueue.js
Normal file
@@ -0,0 +1,53 @@
|
||||
class LockQueue {
|
||||
constructor(queueSize) {
|
||||
this.queueSize = queueSize;
|
||||
this.freed = true;
|
||||
this.waitingQueue = [];
|
||||
}
|
||||
|
||||
//async
|
||||
get(take = true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.freed) {
|
||||
if (take)
|
||||
this.freed = false;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.waitingQueue.length < this.queueSize) {
|
||||
this.waitingQueue.push({resolve, reject});
|
||||
} else {
|
||||
reject(new Error('Lock queue is too long'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ret() {
|
||||
if (this.waitingQueue.length) {
|
||||
this.waitingQueue.shift().resolve();
|
||||
} else {
|
||||
this.freed = true;
|
||||
}
|
||||
}
|
||||
|
||||
//async
|
||||
wait() {
|
||||
return this.get(false);
|
||||
}
|
||||
|
||||
retAll() {
|
||||
while (this.waitingQueue.length) {
|
||||
this.waitingQueue.shift().resolve();
|
||||
}
|
||||
}
|
||||
|
||||
errAll(error = 'rejected') {
|
||||
while (this.waitingQueue.length) {
|
||||
this.waitingQueue.shift().reject(new Error(error));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default LockQueue;
|
||||
22
client/share/dynamicCss.js
Normal file
@@ -0,0 +1,22 @@
|
||||
class DynamicCss {
|
||||
constructor() {
|
||||
this.cssNodes = {};
|
||||
}
|
||||
|
||||
replace(name, cssText) {
|
||||
const style = document.createElement('style');
|
||||
style.type = 'text/css';
|
||||
style.innerHTML = cssText;
|
||||
|
||||
const parent = document.getElementsByTagName('head')[0];
|
||||
|
||||
if (this.cssNodes[name]) {
|
||||
parent.removeChild(this.cssNodes[name]);
|
||||
delete this.cssNodes[name];
|
||||
}
|
||||
|
||||
this.cssNodes[name] = parent.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
export default new DynamicCss();
|
||||
@@ -13,6 +13,10 @@ export function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function toHex(buf) {
|
||||
return Buffer.from(buf).toString('hex');
|
||||
}
|
||||
|
||||
export function stringToHex(str) {
|
||||
return Buffer.from(str).toString('hex');
|
||||
}
|
||||
@@ -86,7 +90,7 @@ export function toBase58(data) {
|
||||
}
|
||||
|
||||
export function fromBase58(data) {
|
||||
return bs58.decode(data);
|
||||
return Buffer.from(bs58.decode(data));
|
||||
}
|
||||
|
||||
//base-x слишком тормозит, используем sjcl
|
||||
@@ -103,69 +107,140 @@ export function fromBase64(data) {
|
||||
));
|
||||
}
|
||||
|
||||
export function getObjDiff(oldObj, newObj) {
|
||||
const result = {__isDiff: true, change: {}, add: {}, del: []};
|
||||
export function hasProp(obj, prop) {
|
||||
return Object.prototype.hasOwnProperty.call(obj, prop);
|
||||
}
|
||||
|
||||
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]);
|
||||
export function getObjDiff(oldObj, newObj, opts = {}) {
|
||||
const {
|
||||
exclude = [],
|
||||
excludeAdd = [],
|
||||
excludeDel = [],
|
||||
} = opts;
|
||||
|
||||
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 (Object.prototype.hasOwnProperty.call(newObj, 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 (!Object.prototype.hasOwnProperty.call(oldObj, 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 result;
|
||||
return makeObjDiff(oldObj, newObj, '');
|
||||
}
|
||||
|
||||
export function isObjDiff(diff) {
|
||||
return (_.isObject(diff) && diff.__isDiff);
|
||||
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;
|
||||
|
||||
const change = diff.change;
|
||||
for (const key of Object.keys(change)) {
|
||||
if (result.hasOwnProperty(key)) {
|
||||
if (Object.prototype.hasOwnProperty.call(result, 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;
|
||||
@@ -193,4 +268,99 @@ export function parseQuery(str) {
|
||||
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;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import { createStore } from 'vuex';
|
||||
import createPersistedState from 'vuex-persistedstate';
|
||||
|
||||
import root from './root.js';
|
||||
@@ -7,16 +6,14 @@ import uistate from './modules/uistate';
|
||||
import config from './modules/config';
|
||||
import reader from './modules/reader';
|
||||
|
||||
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()]
|
||||
export default createStore(Object.assign({}, root, {
|
||||
modules: {
|
||||
uistate,
|
||||
config,
|
||||
reader,
|
||||
},
|
||||
strict: debug,
|
||||
plugins: [createPersistedState()]
|
||||
}));
|
||||
|
||||
@@ -10,18 +10,7 @@ const state = {
|
||||
const getters = {};
|
||||
|
||||
// actions
|
||||
const actions = {
|
||||
async loadConfig({ commit, state }) {
|
||||
commit('setApiError', null, { root: true });
|
||||
commit('setConfig', {});
|
||||
try {
|
||||
const config = await miscApi.loadConfig();
|
||||
commit('setConfig', config);
|
||||
} catch (e) {
|
||||
commit('setApiError', e, { root: true });
|
||||
}
|
||||
},
|
||||
};
|
||||
const actions = {};
|
||||
|
||||
// mutations
|
||||
const mutations = {
|
||||
|
||||
1
client/store/modules/fonts/fonts.json
Normal file
@@ -0,0 +1 @@
|
||||
["Alegreya","Alegreya SC","Alegreya Sans","Alegreya Sans SC","Alice","Amatic SC","Andika","Anonymous Pro","Arimo","Arsenal","Bad Script","Balsamiq Sans","Bellota","Bellota Text","Bitter","Caveat","Comfortaa","Commissioner","Cormorant","Cormorant Garamond","Cormorant Infant","Cormorant SC","Cormorant Unicase","Cousine","Cuprum","Didact Gothic","EB Garamond","El Messiri","Exo 2","Fira Code","Fira Mono","Fira Sans","Fira Sans Condensed","Fira Sans Extra Condensed","Forum","Gabriela","Hachi Maru Pop","IBM Plex Mono","IBM Plex Sans","IBM Plex Serif","Inter","Istok Web","JetBrains Mono","Jost","Jura","Kelly Slab","Kosugi","Kosugi Maru","Kurale","Ledger","Literata","Lobster","Lora","M PLUS 1p","M PLUS Rounded 1c","Manrope","Marck Script","Marmelad","Merriweather","Montserrat","Montserrat Alternates","Neucha","Noto Sans","Noto Serif","Nunito","Old Standard TT","Open Sans","Open Sans Condensed","Oranienbaum","Oswald","PT Mono","PT Sans","PT Sans Caption","PT Sans Narrow","PT Serif","PT Serif Caption","Pacifico","Pangolin","Pattaya","Philosopher","Piazzolla","Play","Playfair Display","Playfair Display SC","Podkova","Poiret One","Prata","Press Start 2P","Prosto One","Raleway","Roboto","Roboto Condensed","Roboto Mono","Roboto Slab","Rubik","Rubik Mono One","Ruda","Ruslan Display","Russo One","Sawarabi Gothic","Scada","Seymour One","Source Code Pro","Source Sans Pro","Source Serif Pro","Spectral","Spectral SC","Stalinist One","Tenor Sans","Tinos","Ubuntu","Ubuntu Condensed","Ubuntu Mono","Underdog","Viaoda Libre","Vollkorn","Vollkorn SC","Yanone Kaffeesatz","Yeseva One"]
|
||||
13
client/store/modules/fonts/fonts2list.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const fs = require('fs-extra');
|
||||
|
||||
async function main() {
|
||||
const webfonts = await fs.readFile('webfonts.json');
|
||||
let fonts = JSON.parse(webfonts);
|
||||
|
||||
fonts = fonts.items.filter(item => item.subsets.includes('cyrillic'));
|
||||
fonts = fonts.map(item => item.family);
|
||||
fonts.sort();
|
||||
|
||||
await fs.writeFile('fonts.json', JSON.stringify(fonts));
|
||||
}
|
||||
main();
|
||||
@@ -1,15 +1,95 @@
|
||||
//занчение toolButtons.name не должно совпадать с settingDefaults-propertyName
|
||||
import * as utils from '../../share/utils';
|
||||
import googleFonts from './fonts/fonts.json';
|
||||
|
||||
const readerActions = {
|
||||
'loader': 'На страницу загрузки',
|
||||
'loadFile': 'Загрузить файл с диска',
|
||||
'loadBuffer': 'Загрузить из буфера обмена',
|
||||
'help': 'Вызвать cправку',
|
||||
'settings': 'Настроить',
|
||||
'undoAction': 'Действие назад',
|
||||
'redoAction': 'Действие вперед',
|
||||
'fullScreen': 'На весь экран',
|
||||
'scrolling': 'Плавный скроллинг',
|
||||
'stopScrolling': '',
|
||||
'setPosition': 'Установить позицию',
|
||||
'search': 'Найти в тексте',
|
||||
'copyText': 'Скопировать текст со страницы',
|
||||
'convOptions': 'Настроить конвертирование',
|
||||
'refresh': 'Принудительно обновить книгу',
|
||||
'clickControl': 'Управление кликом',
|
||||
'offlineMode': 'Автономный режим (без интернета)',
|
||||
'contents': 'Оглавление/закладки',
|
||||
'libs': 'Сетевая библиотека',
|
||||
'recentBooks': 'Показать загруженные',
|
||||
'switchToolbar': 'Показать/скрыть панель управления',
|
||||
'donate': '',
|
||||
'bookBegin': 'В начало книги',
|
||||
'bookEnd': 'В конец книги',
|
||||
'pageBack': 'Страницу назад',
|
||||
'pageForward': 'Страницу вперед',
|
||||
'lineBack': 'Строчку назад',
|
||||
'lineForward': 'Строчку вперед',
|
||||
'incFontSize': 'Увеличить размер шрифта',
|
||||
'decFontSize': 'Уменьшить размер шрифта',
|
||||
'scrollingSpeedUp': 'Увеличить скорость скроллинга',
|
||||
'scrollingSpeedDown': 'Уменьшить скорость скроллинга',
|
||||
};
|
||||
|
||||
//readerActions[name]
|
||||
const toolButtons = [
|
||||
{name: 'undoAction', show: true, text: 'Действие назад'},
|
||||
{name: 'redoAction', show: true, text: 'Действие вперед'},
|
||||
{name: 'fullScreen', show: true, text: 'На весь экран'},
|
||||
{name: 'scrolling', show: false, text: 'Плавный скроллинг'},
|
||||
{name: 'setPosition', show: true, text: 'На страницу'},
|
||||
{name: 'search', show: true, text: 'Найти в тексте'},
|
||||
{name: 'copyText', show: false, text: 'Скопировать текст со страницы'},
|
||||
{name: 'refresh', show: true, text: 'Принудительно обновить книгу'},
|
||||
{name: 'offlineMode', show: false, text: 'Автономный режим (без интернета)'},
|
||||
{name: 'recentBooks', show: true, text: 'Открыть недавние'},
|
||||
{name: 'loadFile', show: true},
|
||||
{name: 'loadBuffer', show: true},
|
||||
{name: 'help', show: true},
|
||||
{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: 'convOptions', show: true},
|
||||
{name: 'refresh', show: true},
|
||||
{name: 'contents', show: true},
|
||||
{name: 'libs', show: true},
|
||||
{name: 'recentBooks', show: true},
|
||||
{name: 'clickControl', show: false},
|
||||
{name: 'offlineMode', show: false},
|
||||
];
|
||||
|
||||
//readerActions[name]
|
||||
const hotKeys = [
|
||||
{name: 'loader', codes: ['Escape']},
|
||||
{name: 'loadFile', codes: ['F3']},
|
||||
{name: 'loadBuffer', codes: ['F4']},
|
||||
{name: 'help', codes: ['F1', 'H']},
|
||||
{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+Space']},
|
||||
{name: 'convOptions', codes: ['Ctrl+M']},
|
||||
{name: 'refresh', codes: ['R']},
|
||||
{name: 'contents', codes: ['C']},
|
||||
{name: 'libs', codes: ['L']},
|
||||
{name: 'recentBooks', codes: ['X']},
|
||||
{name: 'clickControl', codes: ['Ctrl+B']},
|
||||
{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 = [
|
||||
@@ -22,124 +102,22 @@ const fonts = [
|
||||
{name: 'Rubik', fontVertShift: 0},
|
||||
];
|
||||
|
||||
const webFonts = [
|
||||
{css: 'https://fonts.googleapis.com/css?family=Alegreya', name: 'Alegreya', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Alegreya+Sans', name: 'Alegreya Sans', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Alegreya+SC', name: 'Alegreya SC', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Alice', name: 'Alice', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Amatic+SC', name: 'Amatic SC', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Andika', name: 'Andika', fontVertShift: -35},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Anonymous+Pro', name: 'Anonymous Pro', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Arsenal', name: 'Arsenal', fontVertShift: 0},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Bad+Script', name: 'Bad Script', fontVertShift: -30},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Caveat', name: 'Caveat', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Comfortaa', name: 'Comfortaa', fontVertShift: 10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Cormorant', name: 'Cormorant', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Cormorant+Garamond', name: 'Cormorant Garamond', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Cormorant+Infant', name: 'Cormorant Infant', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Cormorant+Unicase', name: 'Cormorant Unicase', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Cousine', name: 'Cousine', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Cuprum', name: 'Cuprum', fontVertShift: 5},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Didact+Gothic', name: 'Didact Gothic', fontVertShift: -10},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=EB+Garamond', name: 'EB Garamond', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=El+Messiri', name: 'El Messiri', fontVertShift: -5},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Fira+Mono', name: 'Fira Mono', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Fira+Sans', name: 'Fira Sans', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Fira+Sans+Condensed', name: 'Fira Sans Condensed', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Fira+Sans+Extra+Condensed', name: 'Fira Sans Extra Condensed', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Forum', name: 'Forum', fontVertShift: 5},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Gabriela', name: 'Gabriela', fontVertShift: 5},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=IBM+Plex+Mono', name: 'IBM Plex Mono', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=IBM+Plex+Sans', name: 'IBM Plex Sans', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=IBM+Plex+Serif', name: 'IBM Plex Serif', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Istok+Web', name: 'Istok Web', fontVertShift: -5},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Jura', name: 'Jura', fontVertShift: 0},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Kelly+Slab', name: 'Kelly Slab', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Kosugi', name: 'Kosugi', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Kosugi+Maru', name: 'Kosugi Maru', fontVertShift: 10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Kurale', name: 'Kurale', fontVertShift: -15},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Ledger', name: 'Ledger', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Lobster', name: 'Lobster', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Lora', name: 'Lora', fontVertShift: 0},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Marck+Script', name: 'Marck Script', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Marmelad', name: 'Marmelad', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Merriweather', name: 'Merriweather', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Montserrat', name: 'Montserrat', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Montserrat+Alternates', name: 'Montserrat Alternates', fontVertShift: 0},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Neucha', name: 'Neucha', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Noto+Sans', name: 'Noto Sans', fontVertShift: -10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Noto+Sans+SC', name: 'Noto Sans SC', fontVertShift: -15},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Noto+Serif', name: 'Noto Serif', fontVertShift: -10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Noto+Serif+TC', name: 'Noto Serif TC', fontVertShift: -15},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Old+Standard+TT', name: 'Old Standard TT', fontVertShift: 15},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300', name: 'Open Sans Condensed', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Oranienbaum', name: 'Oranienbaum', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Oswald', name: 'Oswald', fontVertShift: -20},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Pacifico', name: 'Pacifico', fontVertShift: -35},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Pangolin', name: 'Pangolin', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Pattaya', name: 'Pattaya', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Philosopher', name: 'Philosopher', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Play', name: 'Play', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Playfair+Display', name: 'Playfair Display', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Playfair+Display+SC', name: 'Playfair Display SC', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Podkova', name: 'Podkova', fontVertShift: 10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Poiret+One', name: 'Poiret One', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Prata', name: 'Prata', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Prosto+One', name: 'Prosto One', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=PT+Mono', name: 'PT Mono', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=PT+Sans', name: 'PT Sans', fontVertShift: -10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=PT+Sans+Caption', name: 'PT Sans Caption', fontVertShift: -10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=PT+Sans+Narrow', name: 'PT Sans Narrow', fontVertShift: -10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=PT+Serif', name: 'PT Serif', fontVertShift: -10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=PT+Serif+Caption', name: 'PT Serif Caption', fontVertShift: -10},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Roboto+Condensed', name: 'Roboto Condensed', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Roboto+Mono', name: 'Roboto Mono', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Roboto+Slab', name: 'Roboto Slab', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Ruslan+Display', name: 'Ruslan Display', fontVertShift: 20},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Russo+One', name: 'Russo One', fontVertShift: 5},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Sawarabi+Gothic', name: 'Sawarabi Gothic', fontVertShift: -15},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Scada', name: 'Scada', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Seymour+One', name: 'Seymour One', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Source+Sans+Pro', name: 'Source Sans Pro', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Spectral', name: 'Spectral', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Stalinist+One', name: 'Stalinist One', fontVertShift: 0},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Tinos', name: 'Tinos', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Tenor+Sans', name: 'Tenor Sans', fontVertShift: 5},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Underdog', name: 'Underdog', fontVertShift: 10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Ubuntu+Mono', name: 'Ubuntu Mono', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Ubuntu+Condensed', name: 'Ubuntu Condensed', fontVertShift: -5},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Vollkorn', name: 'Vollkorn', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Vollkorn+SC', name: 'Vollkorn SC', fontVertShift: 0},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz', name: 'Yanone Kaffeesatz', fontVertShift: 20},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Yeseva+One', name: 'Yeseva One', fontVertShift: 10},
|
||||
|
||||
|
||||
];
|
||||
//webFonts: [{css: 'https://fonts.googleapis.com/css?family=Alegreya', name: 'Alegreya', fontVertShift: 0}, ...],
|
||||
const webFonts = [];
|
||||
for (const family of googleFonts) {
|
||||
webFonts.push({
|
||||
css: `https://fonts.googleapis.com/css?family=${family.replace(/\s/g, '+')}`,
|
||||
name: family,
|
||||
fontVertShift: 0,
|
||||
});
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------------------------------
|
||||
const settingDefaults = {
|
||||
textColor: '#000000',
|
||||
backgroundColor: '#EBE2C9',
|
||||
backgroundColor: '#ebe2c9',
|
||||
wallpaper: '',
|
||||
wallpaperIgnoreStatusBar: false,
|
||||
fontStyle: '',// 'italic'
|
||||
fontWeight: '',// 'bold'
|
||||
fontSize: 20,// px
|
||||
@@ -156,15 +134,29 @@ const settingDefaults = {
|
||||
wordWrap: true,//перенос по слогам
|
||||
keepLastToFirst: false,// перенос последней строки в первую при листании
|
||||
|
||||
dualPageMode: false,
|
||||
dualIndentLR: 10,// px, отступ слева и справа внутри страницы в двухстраничном режиме
|
||||
dualDivWidth: 2,// px, ширина разделителя
|
||||
dualDivHeight: 100,// процент, высота разделителя
|
||||
dualDivColorAsText: true,//цвет как у текста
|
||||
dualDivColor: '#000000',
|
||||
dualDivColorAlpha: 0.7,// прозрачность разделителя
|
||||
dualDivStrokeFill: 1,// px, заполнение пунктира
|
||||
dualDivStrokeGap: 1,// px, промежуток пунктира
|
||||
dualDivShadowWidth: 0,// px, ширина тени
|
||||
|
||||
showStatusBar: true,
|
||||
statusBarTop: false,// top, bottom
|
||||
statusBarHeight: 19,// px
|
||||
statusBarColorAsText: true,//цвет как у текста
|
||||
statusBarColor: '#000000',
|
||||
statusBarColorAlpha: 0.4,
|
||||
statusBarClickOpen: true,
|
||||
|
||||
scrollingDelay: 3000,// замедление, ms
|
||||
scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out
|
||||
|
||||
pageChangeAnimation: 'flip',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание, rotate - вращение, flip - листание
|
||||
pageChangeAnimation: 'blink',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание, rotate - вращение, flip - листание
|
||||
pageChangeAnimationSpeed: 80, //0-100%
|
||||
|
||||
allowUrlParamBookPos: false,
|
||||
@@ -180,12 +172,25 @@ const settingDefaults = {
|
||||
compactTextPerc: 0,
|
||||
imageHeightLines: 100,
|
||||
imageFitWidth: true,
|
||||
enableSitesFilter: true,
|
||||
splitToPara: false,
|
||||
djvuQuality: 20,
|
||||
pdfAsText: true,
|
||||
pdfQuality: 20,
|
||||
|
||||
showServerStorageMessages: true,
|
||||
showWhatsNewDialog: true,
|
||||
enableSitesFilter: true,
|
||||
showDonationDialog2020: true,
|
||||
showNeedUpdateNotify: true,
|
||||
|
||||
fontShifts: {},
|
||||
showToolButton: {},
|
||||
toolBarHideOnScroll: true,
|
||||
userHotKeys: {},
|
||||
userWallpapers: [],
|
||||
|
||||
recentShowSameBook: false,
|
||||
recentSortMethod: '',
|
||||
};
|
||||
|
||||
for (const font of fonts)
|
||||
@@ -194,6 +199,47 @@ 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 diffExclude = [];
|
||||
for (const hotKey of hotKeys)
|
||||
diffExclude.push(`userHotKeys/${hotKey.name}`);
|
||||
diffExclude.push('userWallpapers');
|
||||
|
||||
function addDefaultsToSettings(settings) {
|
||||
const diff = utils.getObjDiff(settings, settingDefaults, {exclude: diffExclude});
|
||||
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: '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 = {
|
||||
@@ -204,9 +250,12 @@ const state = {
|
||||
profilesRev: 0,
|
||||
allowProfilesSave: false,//подстраховка для разработки
|
||||
whatsNewContentHash: '',
|
||||
donationRemindDate: '',
|
||||
currentProfile: '',
|
||||
settings: Object.assign({}, settingDefaults),
|
||||
settingsRev: {},
|
||||
libs: Object.assign({}, libsDefaults),
|
||||
libsRev: 0,
|
||||
};
|
||||
|
||||
// getters
|
||||
@@ -238,22 +287,41 @@ const mutations = {
|
||||
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,
|
||||
|
||||