Compare commits
1564 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c86984ea1 | ||
|
|
834b3f6210 | ||
|
|
105b8d5042 | ||
|
|
7ca8fd9ca1 | ||
|
|
0067c2800a | ||
|
|
688c8796f4 | ||
|
|
56af65742b | ||
|
|
629ad26d40 | ||
|
|
4b0e499c10 | ||
|
|
4697b46cba | ||
|
|
7f17e7daed | ||
|
|
a1fcb7597b | ||
|
|
35e46d0685 | ||
|
|
e2c0f3658b | ||
|
|
a3541ec16a | ||
|
|
08d0d3e7f3 | ||
|
|
2c47b2bee3 | ||
|
|
e6008b5ec4 | ||
|
|
e214ddf8d5 | ||
|
|
52927c6188 | ||
|
|
92ca9dd983 | ||
|
|
ed8be34c12 | ||
|
|
93bddfd05e | ||
|
|
8c99101bb3 | ||
|
|
d874f9ded4 | ||
|
|
d7be4d3d94 | ||
|
|
a2fa312839 | ||
|
|
f7e1e09928 | ||
|
|
f0832b07cb | ||
|
|
7c253df291 | ||
|
|
bb7cd9cbde | ||
|
|
56c4182985 | ||
|
|
cb6c7536bf | ||
|
|
fbfe8cbda0 | ||
|
|
6129d2d7eb | ||
|
|
16b30c922a | ||
|
|
c42ad66be6 | ||
|
|
f36c13fea1 | ||
|
|
4fd9d579e0 | ||
|
|
e65a8a13ea | ||
|
|
6ddb97d43e | ||
|
|
89082603de | ||
|
|
a9a3227433 | ||
|
|
60cb3514b2 | ||
|
|
4aeaa05f0b | ||
|
|
9c06552278 | ||
|
|
000f8dde82 | ||
|
|
9ffc218002 | ||
|
|
68a188f099 | ||
|
|
8829bb3810 | ||
|
|
5164d2f536 | ||
|
|
451538fcf7 | ||
|
|
82a02ef339 | ||
|
|
b834d4951f | ||
|
|
edc3b669be | ||
|
|
522826311d | ||
|
|
e69b9951d5 | ||
|
|
c6300222ea | ||
|
|
5aa6ee899c | ||
|
|
4b76f97d2b | ||
|
|
5ccfe71c55 | ||
|
|
97fc902cdb | ||
|
|
7e935951d7 | ||
|
|
810c6d68d2 | ||
|
|
003dc70f4f | ||
|
|
371ff64a95 | ||
|
|
b0de5adbf3 | ||
|
|
d1d2b07c33 | ||
|
|
d9b2444c1a | ||
|
|
e7fae27031 | ||
|
|
eb0c7b0a32 | ||
|
|
3d7ad0dd9a | ||
|
|
ae04feb311 | ||
|
|
7b59f911ef | ||
|
|
d3444da647 | ||
|
|
66738d0c9c | ||
|
|
7e187acd68 | ||
|
|
c751372a54 | ||
|
|
7fc98fc7da | ||
|
|
b56f45694e | ||
|
|
091ca521ef | ||
|
|
c7a17b0a76 | ||
|
|
26468b996a | ||
|
|
c4e240d87c | ||
|
|
04713f47c8 | ||
|
|
37ab3493db | ||
|
|
a4cb3c628e | ||
|
|
8492da8a13 | ||
|
|
98d7c64a56 | ||
|
|
25f121e5ed | ||
|
|
4c8797c99c | ||
|
|
1155aa285d | ||
|
|
239bbb8263 | ||
|
|
e6b9330108 | ||
|
|
935b767c2e | ||
|
|
8acf3295b5 | ||
|
|
48c3a12fa0 | ||
|
|
a1dea514b7 | ||
|
|
d4788439cb | ||
|
|
0a60ad354c | ||
|
|
c565a20344 | ||
|
|
735ee88f0b | ||
|
|
9405ce2cc0 | ||
|
|
115277d88a | ||
|
|
6925c11dbd | ||
|
|
984d835892 | ||
|
|
23353a4960 | ||
|
|
955bcda032 | ||
|
|
81ad5d7a2c | ||
|
|
dada7980ec | ||
|
|
511a308646 | ||
|
|
65c8f2cc81 | ||
|
|
238c18bc48 | ||
|
|
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 | ||
|
|
da34472a6f | ||
|
|
a24eaaed50 | ||
|
|
26813c582f | ||
|
|
6067ac73e2 | ||
|
|
b1d94b67f4 | ||
|
|
452f4e69fd | ||
|
|
e89b6e3ea0 | ||
|
|
977bab4745 | ||
|
|
26c73109fe | ||
|
|
65f911ad51 | ||
|
|
f8ed5ebd6a | ||
|
|
e4cb61bebe | ||
|
|
7d5310af42 | ||
|
|
f68c610c0d | ||
|
|
ccfb6a6d73 | ||
|
|
da55996e22 | ||
|
|
ecd8400a34 | ||
|
|
03914883bc | ||
|
|
9981e1f3bd | ||
|
|
4d1df66025 | ||
|
|
a0f64e188b | ||
|
|
08407a1094 | ||
|
|
445ea3bb2e | ||
|
|
0e0aab98b1 | ||
|
|
721d5eb0c1 | ||
|
|
6d99dbc3a7 | ||
|
|
2be31f649b | ||
|
|
828ac27c03 | ||
|
|
b3d614002f | ||
|
|
2b2000ca10 | ||
|
|
8d7428d099 | ||
|
|
57f8322f31 | ||
|
|
bee7bc4294 | ||
|
|
28702065bc | ||
|
|
c248057081 | ||
|
|
6186f5e138 | ||
|
|
2201d8176d | ||
|
|
2ba6819876 | ||
|
|
a393b2a370 | ||
|
|
59fe713df2 | ||
|
|
4b8efaca9a | ||
|
|
a26100a8d0 | ||
|
|
8c52f4718c | ||
|
|
85b5c3c4ec | ||
|
|
4fd559e4c7 | ||
|
|
a337d0ddc7 | ||
|
|
9e4cb7071e | ||
|
|
c3f1707343 | ||
|
|
1ed058a553 | ||
|
|
0500a8178d | ||
|
|
7d0059f573 | ||
|
|
4e3b882362 | ||
|
|
13cf47873e | ||
|
|
7ee23ec38f | ||
|
|
eebf17c42c | ||
|
|
f84536788b | ||
|
|
4bbfdc2cb2 | ||
|
|
211fec35e3 | ||
|
|
b8214a46ae | ||
|
|
549ef91c81 | ||
|
|
cede65313b | ||
|
|
d897a7400f | ||
|
|
47f059213f | ||
|
|
8af51bbf08 | ||
|
|
53d9f5ddc6 | ||
|
|
06fffdccc8 | ||
|
|
aa13dc68fc | ||
|
|
813876dd90 | ||
|
|
596c7d65c5 | ||
|
|
ce8dcb75bf | ||
|
|
1bd51b5565 | ||
|
|
1f9ec305b4 | ||
|
|
be0f6e57d7 | ||
|
|
b268e9ee74 | ||
|
|
e97774435b | ||
|
|
93586bc5bb | ||
|
|
fe23089714 | ||
|
|
e743986f38 | ||
|
|
a6c9b700ed | ||
|
|
afa3fcb524 | ||
|
|
b9aeb648d6 | ||
|
|
5f5df1e5b7 | ||
|
|
ad885679e4 | ||
|
|
e002bebfbe | ||
|
|
a8a41e2b3d | ||
|
|
31940caa84 | ||
|
|
880334054e | ||
|
|
5f03ad5597 | ||
|
|
1efa3f055d | ||
|
|
8ccf11278b | ||
|
|
8a9e7ab4c3 | ||
|
|
c0fa7c0c51 | ||
|
|
022dfd4709 | ||
|
|
71e08aacc3 | ||
|
|
337eca87f2 | ||
|
|
074aceff8f | ||
|
|
cdc6cf229a | ||
|
|
1f33513dc9 | ||
|
|
b095b91ff2 | ||
|
|
454a62dbb9 | ||
|
|
5f7cc12157 | ||
|
|
97ef1ee201 | ||
|
|
a318568b72 | ||
|
|
5bb9949440 | ||
|
|
c33e91d5d0 | ||
|
|
ca65ef3cb7 | ||
|
|
9ebdbc81d0 | ||
|
|
b64985349e | ||
|
|
625fd9d1a4 | ||
|
|
eac5fdcec0 | ||
|
|
970b4d5d97 | ||
|
|
f741bc818d | ||
|
|
5f04c24187 | ||
|
|
a382bef336 | ||
|
|
4ddf28f344 | ||
|
|
0dc650305a | ||
|
|
697093d1c9 | ||
|
|
622f7a4479 | ||
|
|
c4b607804b | ||
|
|
864f008679 | ||
|
|
25f309bcb0 | ||
|
|
1354361ad9 | ||
|
|
8136c7b072 | ||
|
|
c9243e7249 | ||
|
|
1a487da3d9 | ||
|
|
b52395751c | ||
|
|
cfa6cc9a83 | ||
|
|
f203384b00 | ||
|
|
9ac3be455c | ||
|
|
20b74a9dcd | ||
|
|
3b848a5a86 | ||
|
|
a9b5e865a5 | ||
|
|
ab46a1b99d | ||
|
|
4a08465f5b | ||
|
|
a7960d6cd6 | ||
|
|
3caea77dde | ||
|
|
fdaa3b7f93 | ||
|
|
4f433b4456 | ||
|
|
309a9ad4fb | ||
|
|
b0e7431e72 | ||
|
|
158118d183 | ||
|
|
382e37fc5a | ||
|
|
3390676847 | ||
|
|
544a995312 | ||
|
|
f209d49bb5 | ||
|
|
42ed691fdc | ||
|
|
98d2e9d266 | ||
|
|
6111158896 | ||
|
|
3267fc653c | ||
|
|
7250608767 | ||
|
|
e82063e435 | ||
|
|
6d4c44bc25 | ||
|
|
2bc94d8792 | ||
|
|
4ca3edd789 | ||
|
|
d6859efde2 | ||
|
|
3f8cbfa259 | ||
|
|
5d18c9371d | ||
|
|
631990e3bb | ||
|
|
4ae7338f94 | ||
|
|
0d1e51cb21 | ||
|
|
475fb833ea | ||
|
|
580b030ee4 | ||
|
|
6a7cbc70d6 | ||
|
|
d76f60639c | ||
|
|
e2bea407ee | ||
|
|
558fed31aa | ||
|
|
f6513d40c8 | ||
|
|
259f9baa59 | ||
|
|
db5650e276 | ||
|
|
51ebbbc569 | ||
|
|
5184661652 | ||
|
|
7853a14ce6 | ||
|
|
a01e78ace9 | ||
|
|
f7eb576d0d | ||
|
|
34f1ad8fae | ||
|
|
c60f0991df | ||
|
|
d505fd0795 | ||
|
|
93cf506535 | ||
|
|
bfb37e55d4 | ||
|
|
92afc5cb33 | ||
|
|
75cb611701 | ||
|
|
2ec1dd58a5 | ||
|
|
7d59af54de | ||
|
|
2b5f47b3de | ||
|
|
16eebfb9a4 | ||
|
|
9025218671 | ||
|
|
6bccb546bb | ||
|
|
29d49046a0 | ||
|
|
717af9ffaf | ||
|
|
00060c9f43 | ||
|
|
759ff46c92 | ||
|
|
41957cdceb | ||
|
|
d418e3a1c9 | ||
|
|
f650124428 | ||
|
|
795d109c76 | ||
|
|
6868b3effc | ||
|
|
26747b7013 | ||
|
|
5198f8aa60 | ||
|
|
552da48a32 | ||
|
|
db8a688620 | ||
|
|
3088028d05 | ||
|
|
fd62ef865d | ||
|
|
ed74ed00ed | ||
|
|
741317aaaf | ||
|
|
9b6ecd4e6b | ||
|
|
7863b3358e | ||
|
|
e1be68ec3d | ||
|
|
a054186d4b | ||
|
|
2d5c549c83 | ||
|
|
9f6072dfe1 | ||
|
|
69c44fe1ab | ||
|
|
4fa7b2443e | ||
|
|
25a69592bb | ||
|
|
44e0b26990 | ||
|
|
c4496f8dc8 | ||
|
|
9e296231d9 | ||
|
|
49b3f05d65 | ||
|
|
f124b9c050 | ||
|
|
63a86f7c06 | ||
|
|
fd0f523c64 | ||
|
|
487e605520 | ||
|
|
9e169e1f4b | ||
|
|
9612e7ebcd | ||
|
|
f66162efe7 | ||
|
|
656642697b | ||
|
|
feb70f85f8 | ||
|
|
ab1981559b | ||
|
|
c8852d9a8e | ||
|
|
9ac8dc7fd1 | ||
|
|
c9419d99e6 | ||
|
|
a1f4a83e72 | ||
|
|
a8abd5d427 | ||
|
|
629d1b0630 | ||
|
|
97c368f63a | ||
|
|
3266a444d0 | ||
|
|
1c246f71f8 | ||
|
|
96945dfc4a | ||
|
|
30eb3001ef | ||
|
|
bdd8636390 | ||
|
|
f762d2a271 | ||
|
|
cf2efc2b92 | ||
|
|
7670da4cba | ||
|
|
d87f9f2a21 | ||
|
|
6e690f3fea | ||
|
|
6321002617 | ||
|
|
15ec362428 | ||
|
|
454004e705 | ||
|
|
e14b414fc1 | ||
|
|
c4b47a5915 | ||
|
|
957c252cd7 | ||
|
|
d6a6c21762 | ||
|
|
834580cfdf | ||
|
|
de13cfb555 | ||
|
|
4f87508834 | ||
|
|
682a044f32 | ||
|
|
bdb5d90b1d | ||
|
|
01880f4456 | ||
|
|
39f78ce7e8 | ||
|
|
755c6b92da | ||
|
|
2eab9c2837 | ||
|
|
63861789de | ||
|
|
086c353eff | ||
|
|
4fe5b44655 | ||
|
|
036547e260 | ||
|
|
696f434c90 | ||
|
|
0c654d9346 | ||
|
|
a2c393b06b | ||
|
|
eae2c2b102 | ||
|
|
d9e49e3484 | ||
|
|
a28d4c2f1c | ||
|
|
9af055ec54 | ||
|
|
0d41171e9d | ||
|
|
08af826ae9 | ||
|
|
4fd577d7c5 | ||
|
|
2c8efebe98 | ||
|
|
93c9fb53ac | ||
|
|
5a4d249cf9 | ||
|
|
4cc7bdee37 | ||
|
|
a6af568411 | ||
|
|
576a6a094a | ||
|
|
e671e4b6f5 | ||
|
|
a66b2a4c70 | ||
|
|
f1ae409535 | ||
|
|
a4b56b477d | ||
|
|
d9c389812a | ||
|
|
074ef3645f | ||
|
|
cc3aa413e8 | ||
|
|
7f90c09227 | ||
|
|
f6f4d8ccc9 | ||
|
|
31afce8304 | ||
|
|
2c4ff856cd | ||
|
|
f59974e310 | ||
|
|
70e2c12a6b | ||
|
|
11f3c6ce6f | ||
|
|
e213c4640b | ||
|
|
959c5eaa59 | ||
|
|
66fa510b26 | ||
|
|
f26a3b31ac | ||
|
|
724fbf579e | ||
|
|
f192f8e3cd | ||
|
|
f13c3d19fb | ||
|
|
b51a09efcc | ||
|
|
6004043782 | ||
|
|
f9fd0dc2c3 | ||
|
|
eb5411cd20 | ||
|
|
da3c7a02f0 | ||
|
|
e67d05007f | ||
|
|
b0a9a6a08e | ||
|
|
d848ea35f4 | ||
|
|
350f20effe | ||
|
|
b6dc8f98fe | ||
|
|
1b762ee48d | ||
|
|
cc3d7f1eac | ||
|
|
4107282fbf | ||
|
|
c29ffc3fcd | ||
|
|
f648bcda13 | ||
|
|
aa0044eed2 | ||
|
|
2312a721ae | ||
|
|
b93fc39b00 | ||
|
|
2dc2cd700f | ||
|
|
d69e534f8b | ||
|
|
1de9ddd394 | ||
|
|
77c68d4e11 | ||
|
|
2a0d1dcfce | ||
|
|
5a19cca407 | ||
|
|
4e8773ecde | ||
|
|
4c7dada809 | ||
|
|
65690b15da | ||
|
|
8ba07812ce | ||
|
|
2dd8f35001 | ||
|
|
2d15aa88d4 | ||
|
|
e4257e50f0 | ||
|
|
33ebc07915 | ||
|
|
bc07299626 | ||
|
|
25e8aeef53 | ||
|
|
a2ed34abf3 | ||
|
|
36a7b7b91a | ||
|
|
b4e8b7375f | ||
|
|
153b635bdb | ||
|
|
80af72465e | ||
|
|
a91a8f9993 | ||
|
|
a0ccc7fe07 | ||
|
|
c162c9ae0e | ||
|
|
25542cdff3 | ||
|
|
16d0ae60c1 | ||
|
|
b1937eb8c0 | ||
|
|
3f6b468021 | ||
|
|
92d929b704 | ||
|
|
737ae75c28 | ||
|
|
79ced4eca4 | ||
|
|
329ac44c11 | ||
|
|
f65a91dfed | ||
|
|
2a79207427 | ||
|
|
70be3d10d0 | ||
|
|
3500a40599 | ||
|
|
090ffa9921 | ||
|
|
b12198fdcf | ||
|
|
826ee18666 | ||
|
|
f9d8b37b1a | ||
|
|
e626cb6b40 | ||
|
|
20697ad9e4 | ||
|
|
0800385b96 | ||
|
|
d6e326e8be | ||
|
|
8b969a6d36 | ||
|
|
d520e13c88 | ||
|
|
ae4081001c | ||
|
|
5a48b597b9 | ||
|
|
c8a953db7c | ||
|
|
d20ec144ff | ||
|
|
0147a82b0a | ||
|
|
8732a78d01 | ||
|
|
015254ae40 | ||
|
|
712bf405bb | ||
|
|
3a46a157f9 | ||
|
|
2a4ff926ae | ||
|
|
58941116c8 | ||
|
|
a13146d722 | ||
|
|
02e6f392b4 | ||
|
|
d4515bd643 | ||
|
|
a73555b7ca | ||
|
|
983d9ee1b9 | ||
|
|
e800dfe796 | ||
|
|
b0c59be340 | ||
|
|
dca12b6467 | ||
|
|
5a0d98cbd0 | ||
|
|
9cbaf22270 | ||
|
|
a64687f64f | ||
|
|
d229aab8c9 | ||
|
|
2ff94c1458 | ||
|
|
3b9f3ea81d | ||
|
|
23f12ad3cf | ||
|
|
01e7c1f183 | ||
|
|
37d60bc9b9 | ||
|
|
cd5d3903fe | ||
|
|
6904cfd224 | ||
|
|
c430e2c8f4 | ||
|
|
0cf8a94b24 | ||
|
|
ff3674aca7 | ||
|
|
b50498fa46 | ||
|
|
571f71c7f0 | ||
|
|
091c50ec84 | ||
|
|
e473dc8843 | ||
|
|
886af11d3a | ||
|
|
c72fd7ee9c | ||
|
|
7dc76b4222 | ||
|
|
5011e23050 | ||
|
|
89d9a90901 | ||
|
|
05128b12a8 | ||
|
|
c287ca9ea8 | ||
|
|
5122cda6db | ||
|
|
a39626f867 | ||
|
|
c7abae10b7 | ||
|
|
9a8f35fd8a | ||
|
|
0341cc1630 | ||
|
|
d307d233f0 | ||
|
|
5931b9625b | ||
|
|
fb837f5b97 | ||
|
|
8cfe95b3cf | ||
|
|
5fd73ac1e1 | ||
|
|
b51a574038 | ||
|
|
51b39f0775 | ||
|
|
17c4f96c94 | ||
|
|
89bf907613 | ||
|
|
641d0e45fd | ||
|
|
b3e579d8b7 | ||
|
|
fcb61c89d5 | ||
|
|
3483d78c2c | ||
|
|
36b14d0b3a | ||
|
|
2f8b68ec62 | ||
|
|
cb65cac333 | ||
|
|
d12ffc3d0d | ||
|
|
921744167e | ||
|
|
ebd96c4759 | ||
|
|
dd9876fc43 | ||
|
|
e0de614f30 | ||
|
|
30260883fb | ||
|
|
91c331e5f3 | ||
|
|
db803bcd23 | ||
|
|
cd482ea890 | ||
|
|
a2497c939a | ||
|
|
2e5249d30b | ||
|
|
b1d60c19d5 | ||
|
|
d28a82b33a | ||
|
|
787821f64b | ||
|
|
612b15fecc | ||
|
|
d88d5a1352 | ||
|
|
8584ddd00e | ||
|
|
4f572b5a10 | ||
|
|
90a0882c59 | ||
|
|
759344bb34 | ||
|
|
c9b65a3c43 | ||
|
|
b06e600946 | ||
|
|
2777751e54 | ||
|
|
b4493b2e8d | ||
|
|
55d5f6524d | ||
|
|
c7d376adf2 | ||
|
|
56bf69a770 | ||
|
|
22f9287d8b | ||
|
|
ca47d9272c | ||
|
|
0d61f5523a | ||
|
|
863ea9089a | ||
|
|
ad2af95ebd | ||
|
|
d65092c203 | ||
|
|
7982698880 | ||
|
|
afbdff8a88 | ||
|
|
cec07208ac | ||
|
|
c1b82d0fd2 | ||
|
|
c69e9d4b69 | ||
|
|
cc66c7f5ce | ||
|
|
7d3a689577 | ||
|
|
7b5915cdf7 | ||
|
|
5cc99366ef | ||
|
|
73289543e7 | ||
|
|
716b8b5b9a | ||
|
|
c4f6c9383c | ||
|
|
7d77d478c1 | ||
|
|
572c0d0717 | ||
|
|
b786b7b2d6 | ||
|
|
3253858c7f | ||
|
|
a5fe61078d | ||
|
|
528adae3d0 | ||
|
|
d3ff0edbff | ||
|
|
61cfee222f | ||
|
|
a96eb50784 | ||
|
|
8219e19c1b | ||
|
|
3d56a6915f | ||
|
|
4d6502f5e2 | ||
|
|
f0f245884f | ||
|
|
815f9178bf | ||
|
|
2df88f4280 | ||
|
|
156a1b4aa8 | ||
|
|
962eda7860 | ||
|
|
954ce9e85c | ||
|
|
ff27ddd442 | ||
|
|
f9cc2ad70a | ||
|
|
4bcd45a795 | ||
|
|
9e7ccd6e20 | ||
|
|
631c5930e9 | ||
|
|
0ddb182642 | ||
|
|
de2f0e74c8 | ||
|
|
ad4ee6ccc9 | ||
|
|
1670df02db | ||
|
|
0268e647cd | ||
|
|
4b9315c13c | ||
|
|
14a9948dd2 | ||
|
|
060ec98b0c | ||
|
|
9c1efe381e | ||
|
|
6a1b052d16 | ||
|
|
d6151d541e | ||
|
|
3e90277e1e | ||
|
|
641cbdfe85 | ||
|
|
c36e9b36d8 | ||
|
|
ecc3acce93 | ||
|
|
33a2ca55f0 | ||
|
|
9d0bbec4b3 | ||
|
|
4de0b3cffd | ||
|
|
06221a474b | ||
|
|
e99a42b7af | ||
|
|
37822e8409 | ||
|
|
2e477e6c99 | ||
|
|
360ee98d8d | ||
|
|
69afd7720a | ||
|
|
a75590c493 | ||
|
|
2acb65f6b3 | ||
|
|
1e1a58b58c | ||
|
|
aeadb5aeb8 | ||
|
|
3e2f01d56d | ||
|
|
cad97e639a | ||
|
|
e2632f1802 | ||
|
|
9aa0bb2bde | ||
|
|
f015d5f7ed | ||
|
|
74f8f7f9a4 | ||
|
|
2598538de9 | ||
|
|
a78a00df2b | ||
|
|
92f6beb64e |
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": {
|
"parserOptions": {
|
||||||
"parser": "babel-eslint"
|
"parser": "@babel/eslint-parser",
|
||||||
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:vue/essential"
|
"plugin:vue/recommended"
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"vue",
|
"@babel"
|
||||||
"html",
|
|
||||||
"node"
|
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
|
"es6": true,
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
@@ -24,6 +25,14 @@
|
|||||||
"LM_TOTAL": false
|
"LM_TOTAL": false
|
||||||
},
|
},
|
||||||
"rules": {
|
"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,
|
"strict": 0,
|
||||||
"indent": [0, 4, {
|
"indent": [0, 4, {
|
||||||
"SwitchCase": 1
|
"SwitchCase": 1
|
||||||
|
|||||||
106
LICENSE.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# CC0 1.0 Universal
|
||||||
|
|
||||||
|
## Statement of Purpose
|
||||||
|
|
||||||
|
The laws of most jurisdictions throughout the world automatically confer
|
||||||
|
exclusive Copyright and Related Rights (defined below) upon the creator and
|
||||||
|
subsequent owner(s) (each and all, an “owner”) of an original work of
|
||||||
|
authorship and/or a database (each, a “Work”).
|
||||||
|
|
||||||
|
Certain owners wish to permanently relinquish those rights to a Work for the
|
||||||
|
purpose of contributing to a commons of creative, cultural and scientific works
|
||||||
|
(“Commons”) that the public can reliably and without fear of later claims of
|
||||||
|
infringement build upon, modify, incorporate in other works, reuse and
|
||||||
|
redistribute as freely as possible in any form whatsoever and for any purposes,
|
||||||
|
including without limitation commercial purposes. These owners may contribute
|
||||||
|
to the Commons to promote the ideal of a free culture and the further
|
||||||
|
production of creative, cultural and scientific works, or to gain reputation or
|
||||||
|
greater distribution for their Work in part through the use and efforts of
|
||||||
|
others.
|
||||||
|
|
||||||
|
For these and/or other purposes and motivations, and without any expectation of
|
||||||
|
additional consideration or compensation, the person associating CC0 with a
|
||||||
|
Work (the “Affirmer”), to the extent that he or she is an owner of Copyright
|
||||||
|
and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and
|
||||||
|
publicly distribute the Work under its terms, with knowledge of his or her
|
||||||
|
Copyright and Related Rights in the Work and the meaning and intended legal
|
||||||
|
effect of CC0 on those rights.
|
||||||
|
|
||||||
|
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||||
|
protected by copyright and related or neighboring rights (“Copyright and
|
||||||
|
Related Rights”). Copyright and Related Rights include, but are not limited
|
||||||
|
to, the following:
|
||||||
|
1. the right to reproduce, adapt, distribute, perform, display,
|
||||||
|
communicate, and translate a Work;
|
||||||
|
2. moral rights retained by the original author(s) and/or performer(s);
|
||||||
|
3. publicity and privacy rights pertaining to a person’s image or likeness
|
||||||
|
depicted in a Work;
|
||||||
|
4. rights protecting against unfair competition in regards to a Work,
|
||||||
|
subject to the limitations in paragraph 4(i), below;
|
||||||
|
5. rights protecting the extraction, dissemination, use and reuse of data
|
||||||
|
in a Work;
|
||||||
|
6. database rights (such as those arising under Directive 96/9/EC of the
|
||||||
|
European Parliament and of the Council of 11 March 1996 on the legal
|
||||||
|
protection of databases, and under any national implementation thereof,
|
||||||
|
including any amended or successor version of such directive); and
|
||||||
|
7. other similar, equivalent or corresponding rights throughout the world
|
||||||
|
based on applicable law or treaty, and any national implementations
|
||||||
|
thereof.
|
||||||
|
|
||||||
|
2. Waiver. To the greatest extent permitted by, but not in contravention of,
|
||||||
|
applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
|
||||||
|
unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright
|
||||||
|
and Related Rights and associated claims and causes of action, whether now
|
||||||
|
known or unknown (including existing as well as future claims and causes of
|
||||||
|
action), in the Work (i) in all territories worldwide, (ii) for the maximum
|
||||||
|
duration provided by applicable law or treaty (including future time
|
||||||
|
extensions), (iii) in any current or future medium and for any number of
|
||||||
|
copies, and (iv) for any purpose whatsoever, including without limitation
|
||||||
|
commercial, advertising or promotional purposes (the “Waiver”). Affirmer makes
|
||||||
|
the Waiver for the benefit of each member of the public at large and to the
|
||||||
|
detriment of Affirmer’s heirs and successors, fully intending that such Waiver
|
||||||
|
shall not be subject to revocation, rescission, cancellation, termination, or
|
||||||
|
any other legal or equitable action to disrupt the quiet enjoyment of the Work
|
||||||
|
by the public as contemplated by Affirmer’s express Statement of Purpose.
|
||||||
|
|
||||||
|
3. Public License Fallback. Should any part of the Waiver for any reason be
|
||||||
|
judged legally invalid or ineffective under applicable law, then the Waiver
|
||||||
|
shall be preserved to the maximum extent permitted taking into account
|
||||||
|
Affirmer’s express Statement of Purpose. In addition, to the extent the Waiver
|
||||||
|
is so judged Affirmer hereby grants to each affected person a royalty-free, non
|
||||||
|
transferable, non sublicensable, non exclusive, irrevocable and unconditional
|
||||||
|
license to exercise Affirmer’s Copyright and Related Rights in the Work (i) in
|
||||||
|
all territories worldwide, (ii) for the maximum duration provided by applicable
|
||||||
|
law or treaty (including future time extensions), (iii) in any current or
|
||||||
|
future medium and for any number of copies, and (iv) for any purpose
|
||||||
|
whatsoever, including without limitation commercial, advertising or promotional
|
||||||
|
purposes (the “License”). The License shall be deemed effective as of the date
|
||||||
|
CC0 was applied by Affirmer to the Work. Should any part of the License for any
|
||||||
|
reason be judged legally invalid or ineffective under applicable law, such
|
||||||
|
partial invalidity or ineffectiveness shall not invalidate the remainder of the
|
||||||
|
License, and in such case Affirmer hereby affirms that he or she will not (i)
|
||||||
|
exercise any of his or her remaining Copyright and Related Rights in the Work
|
||||||
|
or (ii) assert any associated claims and causes of action with respect to the
|
||||||
|
Work, in either case contrary to Affirmer’s express Statement of Purpose.
|
||||||
|
|
||||||
|
4. Limitations and Disclaimers.
|
||||||
|
1. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||||
|
surrendered, licensed or otherwise affected by this document.
|
||||||
|
2. Affirmer offers the Work as-is and makes no representations or
|
||||||
|
warranties of any kind concerning the Work, express, implied, statutory
|
||||||
|
or otherwise, including without limitation warranties of title,
|
||||||
|
merchantability, fitness for a particular purpose, non infringement, or
|
||||||
|
the absence of latent or other defects, accuracy, or the present or
|
||||||
|
absence of errors, whether or not discoverable, all to the greatest
|
||||||
|
extent permissible under applicable law.
|
||||||
|
3. Affirmer disclaims responsibility for clearing rights of other persons
|
||||||
|
that may apply to the Work or any use thereof, including without
|
||||||
|
limitation any person’s Copyright and Related Rights in the Work.
|
||||||
|
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||||
|
consents, permissions or other rights required for any use of the Work.
|
||||||
|
4. Affirmer understands and acknowledges that Creative Commons is not a
|
||||||
|
party to this document and has no duty or obligation with respect to
|
||||||
|
this CC0 or use of the Work.
|
||||||
|
|
||||||
|
For more information, please see
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/.
|
||||||
42
README.md
@@ -1,3 +1,43 @@
|
|||||||
# Liberama
|
# Liberama
|
||||||
|
|
||||||
Свободный обмен книгами в формате fb2
|
Браузерная онлайн-читалка книг и децентрализованная библиотека.
|
||||||
|
|
||||||
|
Читалка <img src="https://omnireader.ru/favicon.ico" width="14px"/>[OmniReader](https://omnireader.ru) является частью данного проекта, размещенной на VPS:
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## VPS
|
||||||
|
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
|
||||||
|
|
||||||
|
## Сборка проекта
|
||||||
|
Необходима версия node.js не ниже 14.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git clone https://github.com/bookpauk/liberama
|
||||||
|
$ cd liberama
|
||||||
|
$ npm i
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
```
|
||||||
|
$ npm run build:win
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
```
|
||||||
|
$ npm run build:linux
|
||||||
|
```
|
||||||
|
|
||||||
|
Результат сборки будет доступен в каталоге `dist/linux|win` в виде исполнимого (standalone) файла
|
||||||
|
|
||||||
|
### Разработка
|
||||||
|
```
|
||||||
|
$ npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Помочь проекту
|
||||||
|
|
||||||
|
* bitcoin: 3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85
|
||||||
|
* litecoin: MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ
|
||||||
|
* monero: 8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz
|
||||||
|
|||||||
31
build/includer.js
Normal file
@@ -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,9 +4,8 @@ const util = require('util');
|
|||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
|
|
||||||
const got = require('got');
|
const axios = require('axios');
|
||||||
const decompress = require('decompress');
|
const FileDecompressor = require('../server/core/FileDecompressor');
|
||||||
const decompressTargz = require('decompress-targz');
|
|
||||||
|
|
||||||
const distDir = path.resolve(__dirname, '../dist');
|
const distDir = path.resolve(__dirname, '../dist');
|
||||||
const publicDir = `${distDir}/tmp/public`;
|
const publicDir = `${distDir}/tmp/public`;
|
||||||
@@ -15,6 +14,8 @@ const outDir = `${distDir}/linux`;
|
|||||||
const tempDownloadDir = `${distDir}/tmp/download`;
|
const tempDownloadDir = `${distDir}/tmp/download`;
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
const decomp = new FileDecompressor();
|
||||||
|
|
||||||
await fs.emptyDir(outDir);
|
await fs.emptyDir(outDir);
|
||||||
// перемещаем public на место
|
// перемещаем public на место
|
||||||
if (await fs.pathExists(publicDir))
|
if (await fs.pathExists(publicDir))
|
||||||
@@ -22,42 +23,18 @@ async function main() {
|
|||||||
|
|
||||||
await fs.ensureDir(tempDownloadDir);
|
await fs.ensureDir(tempDownloadDir);
|
||||||
|
|
||||||
//sqlite3
|
|
||||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.0.4/node-v64-linux-x64.tar.gz';
|
|
||||||
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v64-linux-x64/node_sqlite3.node`;
|
|
||||||
|
|
||||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
|
||||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
|
||||||
await pipeline(got.stream(sqliteRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
|
|
||||||
console.log(`done downloading ${sqliteRemoteUrl}`);
|
|
||||||
|
|
||||||
//распаковываем
|
|
||||||
await decompress(`${tempDownloadDir}/sqlite.tar.gz`, `${tempDownloadDir}`, {
|
|
||||||
plugins: [
|
|
||||||
decompressTargz()
|
|
||||||
]
|
|
||||||
});
|
|
||||||
console.log('files decompressed');
|
|
||||||
}
|
|
||||||
// копируем в дистрибутив
|
|
||||||
await fs.copy(sqliteDecompressedFilename, `${outDir}/node_sqlite3.node`);
|
|
||||||
console.log(`copied ${sqliteDecompressedFilename} to ${outDir}/node_sqlite3.node`);
|
|
||||||
|
|
||||||
//ipfs
|
//ipfs
|
||||||
const ipfsDecompressedFilename = `${tempDownloadDir}/go-ipfs/ipfs`;
|
const ipfsDecompressedFilename = `${tempDownloadDir}/go-ipfs/ipfs`;
|
||||||
if (!await fs.pathExists(ipfsDecompressedFilename)) {
|
if (!await fs.pathExists(ipfsDecompressedFilename)) {
|
||||||
// Скачиваем ipfs
|
// Скачиваем ipfs
|
||||||
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_linux-amd64.tar.gz';
|
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}`);
|
console.log(`done downloading ${ipfsRemoteUrl}`);
|
||||||
|
|
||||||
//распаковываем
|
//распаковываем
|
||||||
await decompress(`${tempDownloadDir}/ipfs.tar.gz`, `${tempDownloadDir}`, {
|
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/ipfs.tar.gz`, tempDownloadDir));
|
||||||
plugins: [
|
|
||||||
decompressTargz()
|
|
||||||
]
|
|
||||||
});
|
|
||||||
console.log('files decompressed');
|
console.log('files decompressed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
//const webpack = require('webpack');
|
const DefinePlugin = require('webpack').DefinePlugin;
|
||||||
const VueLoaderPlugin = require('vue-loader/lib/plugin');
|
const { VueLoaderPlugin } = require('vue-loader');
|
||||||
|
|
||||||
const clientDir = path.resolve(__dirname, '../client');
|
const clientDir = path.resolve(__dirname, '../client');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
ws: false,
|
||||||
|
//vue: '@vue/compat'
|
||||||
|
}
|
||||||
|
},
|
||||||
entry: [`${clientDir}/main.js`],
|
entry: [`${clientDir}/main.js`],
|
||||||
output: {
|
output: {
|
||||||
publicPath: '/app/',
|
publicPath: '/app/',
|
||||||
@@ -14,53 +20,61 @@ module.exports = {
|
|||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.vue$/,
|
test: /\.vue$/,
|
||||||
loader: "vue-loader"
|
loader: 'vue-loader',
|
||||||
|
/*options: {
|
||||||
|
compilerOptions: {
|
||||||
|
compatConfig: {
|
||||||
|
MODE: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resourceQuery: /^\?vue/,
|
||||||
|
use: path.resolve(__dirname, 'includer.js')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
query: {
|
options: {
|
||||||
|
presets: [['@babel/preset-env', { targets: { esmodules: true } }]],
|
||||||
plugins: [
|
plugins: [
|
||||||
'syntax-dynamic-import',
|
['@babel/plugin-proposal-decorators', { legacy: true }]
|
||||||
'transform-decorators-legacy',
|
|
||||||
'transform-class-properties',
|
|
||||||
// ["component", { "libraryName": "element-ui", "styleLibraryName": `~${clientDir}/theme` } ]
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.gif$/,
|
test: /\.(gif|png)$/,
|
||||||
loader: "url-loader",
|
type: 'asset/inline',
|
||||||
options: {
|
|
||||||
name: "images/[name]-[hash:6].[ext]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.png$/,
|
|
||||||
loader: "url-loader",
|
|
||||||
options: {
|
|
||||||
name: "images/[name]-[hash:6].[ext]"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.jpg$/,
|
test: /\.jpg$/,
|
||||||
loader: "file-loader",
|
type: 'asset/resource',
|
||||||
options: {
|
generator: {
|
||||||
name: "images/[name]-[hash:6].[ext]"
|
filename: 'images/[name]-[hash:6][ext]'
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
test: /\.(ttf|eot|woff|woff2)$/,
|
test: /\.(ttf|eot|woff|woff2)$/,
|
||||||
loader: "file-loader",
|
type: 'asset/resource',
|
||||||
options: {
|
generator: {
|
||||||
name: "fonts/[name]-[hash:6].[ext]"
|
filename: 'fonts/[name]-[hash:6][ext]'
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
plugins: [
|
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(),
|
new VueLoaderPlugin(),
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
|
|
||||||
const merge = require('webpack-merge');
|
const { merge } = require('webpack-merge');
|
||||||
const baseWpConfig = require('./webpack.base.config');
|
const baseWpConfig = require('./webpack.base.config');
|
||||||
|
|
||||||
baseWpConfig.entry.unshift('webpack-hot-middleware/client');
|
baseWpConfig.entry.unshift('webpack-hot-middleware/client');
|
||||||
@@ -13,7 +13,7 @@ const clientDir = path.resolve(__dirname, '../client');
|
|||||||
|
|
||||||
module.exports = merge(baseWpConfig, {
|
module.exports = merge(baseWpConfig, {
|
||||||
mode: 'development',
|
mode: 'development',
|
||||||
devtool: "#inline-source-map",
|
devtool: 'inline-source-map',
|
||||||
output: {
|
output: {
|
||||||
path: `${publicDir}/app`,
|
path: `${publicDir}/app`,
|
||||||
filename: 'bundle.js'
|
filename: 'bundle.js'
|
||||||
@@ -38,6 +38,6 @@ module.exports = merge(baseWpConfig, {
|
|||||||
template: `${clientDir}/index.html.template`,
|
template: `${clientDir}/index.html.template`,
|
||||||
filename: `${publicDir}/index.html`
|
filename: `${publicDir}/index.html`
|
||||||
}),
|
}),
|
||||||
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}])
|
new CopyWebpackPlugin({patterns: [{from: `${clientDir}/assets/*`, to: `${publicDir}/`}]})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
//const webpack = require('webpack');
|
//const webpack = require('webpack');
|
||||||
|
|
||||||
const merge = require('webpack-merge');
|
const { merge } = require('webpack-merge');
|
||||||
const baseWpConfig = require('./webpack.base.config');
|
const baseWpConfig = require('./webpack.base.config');
|
||||||
const TerserPlugin = require('terser-webpack-plugin');
|
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 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 HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
|
const {GenerateSW} = require('workbox-webpack-plugin');
|
||||||
|
|
||||||
const publicDir = path.resolve(__dirname, '../dist/tmp/public');
|
const publicDir = path.resolve(__dirname, '../dist/tmp/public');
|
||||||
const clientDir = path.resolve(__dirname, '../client');
|
const clientDir = path.resolve(__dirname, '../client');
|
||||||
@@ -32,12 +33,19 @@ module.exports = merge(baseWpConfig, {
|
|||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
minimizer: [
|
minimizer: [
|
||||||
new TerserPlugin(),
|
new TerserPlugin({
|
||||||
new OptimizeCSSAssetsPlugin()
|
parallel: true,
|
||||||
|
terserOptions: {
|
||||||
|
format: {
|
||||||
|
comments: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new CssMinimizerWebpackPlugin()
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new CleanWebpackPlugin([publicDir], {root: path.resolve(__dirname, '..')}),
|
//new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: [`${publicDir}/**`] }),
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: "[name].[contenthash].css"
|
filename: "[name].[contenthash].css"
|
||||||
}),
|
}),
|
||||||
@@ -45,6 +53,15 @@ module.exports = merge(baseWpConfig, {
|
|||||||
template: `${clientDir}/index.html.template`,
|
template: `${clientDir}/index.html.template`,
|
||||||
filename: `${publicDir}/index.html`
|
filename: `${publicDir}/index.html`
|
||||||
}),
|
}),
|
||||||
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}])
|
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,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
33
build/win.js
@@ -4,9 +4,8 @@ const util = require('util');
|
|||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
|
|
||||||
const got = require('got');
|
const axios = require('axios');
|
||||||
const decompress = require('decompress');
|
const FileDecompressor = require('../server/core/FileDecompressor');
|
||||||
const decompressTargz = require('decompress-targz');
|
|
||||||
|
|
||||||
const distDir = path.resolve(__dirname, '../dist');
|
const distDir = path.resolve(__dirname, '../dist');
|
||||||
const publicDir = `${distDir}/tmp/public`;
|
const publicDir = `${distDir}/tmp/public`;
|
||||||
@@ -15,6 +14,8 @@ const outDir = `${distDir}/win`;
|
|||||||
const tempDownloadDir = `${distDir}/tmp/download`;
|
const tempDownloadDir = `${distDir}/tmp/download`;
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
const decomp = new FileDecompressor();
|
||||||
|
|
||||||
await fs.emptyDir(outDir);
|
await fs.emptyDir(outDir);
|
||||||
// перемещаем public на место
|
// перемещаем public на место
|
||||||
if (await fs.pathExists(publicDir))
|
if (await fs.pathExists(publicDir))
|
||||||
@@ -22,38 +23,18 @@ async function main() {
|
|||||||
|
|
||||||
await fs.ensureDir(tempDownloadDir);
|
await fs.ensureDir(tempDownloadDir);
|
||||||
|
|
||||||
//sqlite3
|
|
||||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.0.4/node-v64-win32-x64.tar.gz';
|
|
||||||
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v64-win32-x64/node_sqlite3.node`;
|
|
||||||
|
|
||||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
|
||||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
|
||||||
await pipeline(got.stream(sqliteRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
|
|
||||||
console.log(`done downloading ${sqliteRemoteUrl}`);
|
|
||||||
|
|
||||||
//распаковываем
|
|
||||||
await decompress(`${tempDownloadDir}/sqlite.tar.gz`, `${tempDownloadDir}`, {
|
|
||||||
plugins: [
|
|
||||||
decompressTargz()
|
|
||||||
]
|
|
||||||
});
|
|
||||||
console.log('files decompressed');
|
|
||||||
}
|
|
||||||
// копируем в дистрибутив
|
|
||||||
await fs.copy(sqliteDecompressedFilename, `${outDir}/node_sqlite3.node`);
|
|
||||||
console.log(`copied ${sqliteDecompressedFilename} to ${outDir}/node_sqlite3.node`);
|
|
||||||
|
|
||||||
//ipfs
|
//ipfs
|
||||||
const ipfsDecompressedFilename = `${tempDownloadDir}/go-ipfs/ipfs.exe`;
|
const ipfsDecompressedFilename = `${tempDownloadDir}/go-ipfs/ipfs.exe`;
|
||||||
if (!await fs.pathExists(ipfsDecompressedFilename)) {
|
if (!await fs.pathExists(ipfsDecompressedFilename)) {
|
||||||
// Скачиваем ipfs
|
// Скачиваем ipfs
|
||||||
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_windows-amd64.zip';
|
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}`);
|
console.log(`done downloading ${ipfsRemoteUrl}`);
|
||||||
|
|
||||||
//распаковываем
|
//распаковываем
|
||||||
await decompress(`${tempDownloadDir}/ipfs.zip`, `${tempDownloadDir}`);
|
console.log(await decomp.unpack(`${tempDownloadDir}/ipfs.zip`, tempDownloadDir));
|
||||||
console.log('files decompressed');
|
console.log('files decompressed');
|
||||||
}
|
}
|
||||||
// копируем в дистрибутив
|
// копируем в дистрибутив
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import wsc from './webSocketConnection';
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: '/api'
|
baseURL: '/api'
|
||||||
@@ -6,7 +7,22 @@ const api = axios.create({
|
|||||||
|
|
||||||
class Misc {
|
class Misc {
|
||||||
async loadConfig() {
|
async loadConfig() {
|
||||||
const response = await api.post('/config', {params: ['name', 'version', 'mode', 'maxUploadFileSize', 'branch']});
|
|
||||||
|
const query = {params: [
|
||||||
|
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'bucEnabled', '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;
|
return response.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import {sleep} from '../share/utils';
|
import * as utils from '../share/utils';
|
||||||
|
import * as cryptoUtils from '../share/cryptoUtils';
|
||||||
|
import wsc from './webSocketConnection';
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: '/api/reader'
|
baseURL: '/api/reader'
|
||||||
@@ -10,78 +12,150 @@ const workerApi = axios.create({
|
|||||||
});
|
});
|
||||||
|
|
||||||
class Reader {
|
class Reader {
|
||||||
async loadBook(url, callback) {
|
constructor() {
|
||||||
const refreshPause = 200;
|
}
|
||||||
|
|
||||||
|
async getWorkerStateFinish(workerId, callback) {
|
||||||
if (!callback) callback = () => {};
|
if (!callback) callback = () => {};
|
||||||
|
|
||||||
let response = await api.post('/load-book', {type: 'url', url});
|
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) {
|
||||||
|
if (!callback) callback = () => {};
|
||||||
|
|
||||||
|
let response = await api.post('/load-book', opts);
|
||||||
|
|
||||||
const workerId = response.data.workerId;
|
const workerId = response.data.workerId;
|
||||||
if (!workerId)
|
if (!workerId)
|
||||||
throw new Error('Неверный ответ api');
|
throw new Error('Неверный ответ api');
|
||||||
|
|
||||||
callback({totalSteps: 4});
|
callback({totalSteps: 4});
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
while (1) {// eslint-disable-line no-constant-condition
|
|
||||||
callback(response.data);
|
callback(response.data);
|
||||||
|
|
||||||
if (response.data.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
|
response = await this.getWorkerStateFinish(workerId, callback);
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
if (response.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
|
||||||
callback({step: 4});
|
callback({step: 4});
|
||||||
const book = await this.loadCachedBook(response.data.path, callback);
|
const book = await this.loadCachedBook(response.path, callback, response.size);
|
||||||
return Object.assign({}, response.data, {data: book.data});
|
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 ||
|
if (errMes.indexOf('getaddrinfo') >= 0 ||
|
||||||
errMes.indexOf('ECONNRESET') >= 0 ||
|
errMes.indexOf('ECONNRESET') >= 0 ||
|
||||||
errMes.indexOf('EINVAL') >= 0 ||
|
errMes.indexOf('EINVAL') >= 0 ||
|
||||||
errMes.indexOf('404') >= 0)
|
errMes.indexOf('404') >= 0)
|
||||||
errMes = `Ресурс не найден по адресу: ${response.data.url}`;
|
errMes = `Ресурс не найден по адресу: ${response.url}`;
|
||||||
throw new Error(errMes);
|
throw new Error(errMes);
|
||||||
}
|
}
|
||||||
if (i > 0)
|
} else {
|
||||||
await sleep(refreshPause);
|
throw new Error('Пустой ответ сервера');
|
||||||
|
|
||||||
i++;
|
|
||||||
if (i > 30*1000/refreshPause) {//30 сек ждем телодвижений воркера
|
|
||||||
throw new Error('Слишком долгое время ожидания');
|
|
||||||
}
|
|
||||||
//проверка воркера
|
|
||||||
const prevProgress = response.data.progress;
|
|
||||||
response = await workerApi.post('/get-state', {workerId});
|
|
||||||
i = (prevProgress != response.data.progress ? 1 : i);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadCachedBook(url, callback){
|
async checkCachedBook(url) {
|
||||||
const response = await axios.head(url);
|
let estSize = -1;
|
||||||
|
try {
|
||||||
|
const response = await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
|
||||||
|
|
||||||
let estSize = 1000000;
|
|
||||||
if (response.headers['content-length']) {
|
if (response.headers['content-length']) {
|
||||||
estSize = response.headers['content-length'];
|
estSize = response.headers['content-length'];
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
return estSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadCachedBook(url, callback, estSize = -1) {
|
||||||
|
if (!callback) callback = () => {};
|
||||||
|
|
||||||
|
callback({state: 'loading', progress: 0});
|
||||||
|
|
||||||
|
//получение размера файла
|
||||||
|
if (estSize && estSize < 0) {
|
||||||
|
estSize = await this.checkCachedBook(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
//получение файла
|
||||||
|
estSize = (estSize > 0 ? estSize : 1000000);
|
||||||
const options = {
|
const options = {
|
||||||
onDownloadProgress: progress => {
|
onDownloadProgress: (progress) => {
|
||||||
while (progress.loaded > estSize) estSize *= 1.5;
|
while (progress.loaded > estSize) estSize *= 1.5;
|
||||||
|
|
||||||
if (callback)
|
if (callback)
|
||||||
callback({state: 'loading', progress: Math.round((progress.loaded*100)/estSize)});
|
callback({progress: Math.round((progress.loaded*100)/estSize)});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//загрузка
|
|
||||||
return await axios.get(url, options);
|
return await axios.get(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFile(file, maxUploadFileSize, callback) {
|
async uploadFile(file, maxUploadFileSize = 10*1024*1024, callback) {
|
||||||
if (!maxUploadFileSize)
|
|
||||||
maxUploadFileSize = 10*1024*1024;
|
|
||||||
if (file.size > maxUploadFileSize)
|
if (file.size > maxUploadFileSize)
|
||||||
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
|
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
|
||||||
|
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file, file.name);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -105,6 +179,67 @@ class Reader {
|
|||||||
|
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async 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.state;
|
||||||
|
if (!state)
|
||||||
|
throw new Error('Неверный ответ api');
|
||||||
|
if (state == 'error') {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeUrlFromBuf(buf) {
|
||||||
|
const key = utils.toHex(cryptoUtils.sha256(buf));
|
||||||
|
return `disk://${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFileBuf(buf, url) {
|
||||||
|
if (!url)
|
||||||
|
url = this.makeUrlFromBuf(buf);
|
||||||
|
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
await axios.head(url.replace('disk://', '/upload/'), {headers: {'Cache-Control': 'no-cache'}});
|
||||||
|
response = await wsc.message(await wsc.send({action: 'upload-file-touch', url}));
|
||||||
|
} catch (e) {
|
||||||
|
response = await wsc.message(await wsc.send({action: 'upload-file-buf', buf}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.error)
|
||||||
|
throw new Error(response.error);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUploadedFileBuf(url) {
|
||||||
|
url = url.replace('disk://', '/upload/');
|
||||||
|
return (await axios.get(url)).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkBuc(bookUrls) {
|
||||||
|
const response = await wsc.message(await wsc.send({action: 'check-buc', bookUrls}));
|
||||||
|
|
||||||
|
if (response.error)
|
||||||
|
throw new Error(response.error);
|
||||||
|
|
||||||
|
if (!response.data)
|
||||||
|
throw new Error(`response.data is empty`);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Reader();
|
export default new Reader();
|
||||||
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 |
@@ -1,3 +1,2 @@
|
|||||||
User-agent: *
|
User-agent: *
|
||||||
Disallow: /?*url=
|
Disallow: /?*url=
|
||||||
Disallow: /#/
|
|
||||||
5
client/assets/sw-register.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(function() {
|
||||||
|
if('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register('/service-worker.js');
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -1,66 +1,44 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div class="fit row">
|
||||||
<el-aside v-if="showAsideBar" :width="asideWidth">
|
<Notify ref="notify" />
|
||||||
<div class="app-name"><span v-html="appName"></span></div>
|
<StdDialog ref="stdDialog" />
|
||||||
<el-button class="el-button-collapse" @click="toggleCollapse" :icon="buttonCollapseIcon"></el-button>
|
|
||||||
<el-menu class="el-menu-vertical" :default-active="rootRoute" :collapse="isCollapse" router>
|
|
||||||
<el-menu-item index="/cardindex">
|
|
||||||
<i class="el-icon-search"></i>
|
|
||||||
<span :class="itemTitleClass('/cardindex')" slot="title">{{ this.itemRuText['/cardindex'] }}</span>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/reader">
|
|
||||||
<i class="el-icon-tickets"></i>
|
|
||||||
<span :class="itemTitleClass('/reader')" slot="title">{{ this.itemRuText['/reader'] }}</span>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/forum" disabled>
|
|
||||||
<i class="el-icon-message"></i>
|
|
||||||
<span :class="itemTitleClass('/forum')" slot="title">{{ this.itemRuText['/forum'] }}</span>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/income">
|
|
||||||
<i class="el-icon-upload"></i>
|
|
||||||
<span :class="itemTitleClass('/income')" slot="title">{{ this.itemRuText['/income'] }}</span>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/sources">
|
|
||||||
<i class="el-icon-menu"></i>
|
|
||||||
<span :class="itemTitleClass('/sources')" slot="title">{{ this.itemRuText['/sources'] }}</span>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/settings">
|
|
||||||
<i class="el-icon-setting"></i>
|
|
||||||
<span :class="itemTitleClass('/settings')" slot="title">{{ this.itemRuText['/settings'] }}</span>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/help">
|
|
||||||
<i class="el-icon-question"></i>
|
|
||||||
<span :class="itemTitleClass('/help')" slot="title">{{ this.itemRuText['/help'] }}</span>
|
|
||||||
</el-menu-item>
|
|
||||||
</el-menu>
|
|
||||||
</el-aside>
|
|
||||||
|
|
||||||
<el-main v-if="showMain" :style="{padding: (isReaderActive ? 0 : '5px')}">
|
<router-view v-slot="{ Component }">
|
||||||
<keep-alive>
|
<keep-alive v-if="showPage">
|
||||||
<router-view></router-view>
|
<component :is="Component" class="col" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</el-main>
|
</router-view>
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from './vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
|
||||||
export default @Component({
|
import Notify from './share/Notify.vue';
|
||||||
watch: {
|
import StdDialog from './share/StdDialog.vue';
|
||||||
rootRoute: function() {
|
import sanitizeHtml from 'sanitize-html';
|
||||||
this.setAppTitle();
|
|
||||||
this.redirectIfNeeded();
|
import miscApi from '../api/misc';
|
||||||
|
import * as utils from '../share/utils';
|
||||||
|
|
||||||
|
const componentOptions = {
|
||||||
|
components: {
|
||||||
|
Notify,
|
||||||
|
StdDialog,
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
mode: function() {
|
mode: function() {
|
||||||
|
this.setAppTitle();
|
||||||
this.redirectIfNeeded();
|
this.redirectIfNeeded();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
})
|
};
|
||||||
class App extends Vue {
|
class App {
|
||||||
|
_options = componentOptions;
|
||||||
|
showPage = false;
|
||||||
|
|
||||||
itemRuText = {
|
itemRuText = {
|
||||||
'/cardindex': 'Картотека',
|
'/cardindex': 'Картотека',
|
||||||
'/reader': 'Читалка',
|
'/reader': 'Читалка',
|
||||||
@@ -69,62 +47,117 @@ class App extends Vue {
|
|||||||
'/sources': 'Источники',
|
'/sources': 'Источники',
|
||||||
'/settings': 'Параметры',
|
'/settings': 'Параметры',
|
||||||
'/help': 'Справка',
|
'/help': 'Справка',
|
||||||
}
|
};
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
this.dispatch = this.$store.dispatch;
|
|
||||||
this.state = this.$store.state;
|
this.state = this.$store.state;
|
||||||
this.uistate = this.$store.state.uistate;
|
this.uistate = this.$store.state.uistate;
|
||||||
this.config = this.$store.state.config;
|
this.config = this.$store.state.config;
|
||||||
|
|
||||||
// set-app-title
|
//root route
|
||||||
this.$root.$on('set-app-title', this.setAppTitle);
|
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 = [];
|
return cachedRoute;
|
||||||
this.keyHook = (event) => {
|
}
|
||||||
for (const hook of this.keyHooks)
|
|
||||||
|
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);
|
hook(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$root.addKeyHook = (hook) => {
|
this.$root.addEventHook = (hookName, hook) => {
|
||||||
if (this.keyHooks.indexOf(hook) < 0)
|
if (!this.eventHooks[hookName])
|
||||||
this.keyHooks.push(hook);
|
this.eventHooks[hookName] = [];
|
||||||
|
if (this.eventHooks[hookName].indexOf(hook) < 0)
|
||||||
|
this.eventHooks[hookName].push(hook);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$root.removeKeyHook = (hook) => {
|
this.$root.removeEventHook = (hookName, hook) => {
|
||||||
const i = this.keyHooks.indexOf(hook);
|
if (!this.eventHooks[hookName])
|
||||||
|
return;
|
||||||
|
const i = this.eventHooks[hookName].indexOf(hook);
|
||||||
if (i >= 0)
|
if (i >= 0)
|
||||||
this.keyHooks.splice(i, 1);
|
this.eventHooks[hookName].splice(i, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('keyup', (event) => {
|
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) => {
|
document.addEventListener('keydown', (event) => {
|
||||||
this.keyHook(event);
|
this.$root.eventHook('key', event);
|
||||||
});
|
});
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
this.$root.$emit('resize');
|
window.addEventListener('resize', (event) => {
|
||||||
|
this.$root.eventHook('resize', event);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.dispatch('config/loadConfig');
|
this.$root.notify = this.$refs.notify;
|
||||||
this.$watch('apiError', function(newError) {
|
this.$root.stdDialog = this.$refs.stdDialog;
|
||||||
if (newError) {
|
|
||||||
this.$notify.error({
|
this.setAppTitle();
|
||||||
title: 'Ошибка API',
|
(async() => {
|
||||||
dangerouslyUseHTMLString: true,
|
//загрузим конфиг сревера
|
||||||
message: newError.response.config.url + '<br>' + newError.response.statusText
|
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() {
|
toggleCollapse() {
|
||||||
this.commit('uistate/setAsideBarCollapse', !this.uistate.asideBarCollapse);
|
this.commit('uistate/setAsideBarCollapse', !this.uistate.asideBarCollapse);
|
||||||
this.$root.$emit('resize');
|
this.$root.eventHook('resize');
|
||||||
}
|
}
|
||||||
|
|
||||||
get isCollapse() {
|
get isCollapse() {
|
||||||
@@ -133,9 +166,9 @@ class App extends Vue {
|
|||||||
|
|
||||||
get asideWidth() {
|
get asideWidth() {
|
||||||
if (this.uistate.asideBarCollapse) {
|
if (this.uistate.asideBarCollapse) {
|
||||||
return '64px';
|
return 64;
|
||||||
} else {
|
} else {
|
||||||
return '170px';
|
return 170;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,18 +192,17 @@ class App extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get rootRoute() {
|
get rootRoute() {
|
||||||
const m = this.$route.path.match(/^(\/[^/]*).*$/i);
|
return this.$root.getRootRoute();
|
||||||
this.$root.rootRoute = (m ? m[1] : this.$route.path);
|
|
||||||
|
|
||||||
return this.$root.rootRoute;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setAppTitle(title) {
|
setAppTitle(title) {
|
||||||
if (!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 - всегда с вами`;
|
document.title = `Omni Reader - всегда с вами`;
|
||||||
} else if (this.config && this.mode !== null) {
|
} 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 {
|
} else {
|
||||||
document.title = title;
|
document.title = title;
|
||||||
@@ -186,46 +218,37 @@ class App extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get showAsideBar() {
|
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() {
|
get isReaderActive() {
|
||||||
return this.rootRoute == '/reader';
|
return (this.rootRoute == '/reader' || this.rootRoute == '/external-libs');
|
||||||
}
|
|
||||||
|
|
||||||
get showMain() {
|
|
||||||
return (this.showAsideBar || this.isReaderActive);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectIfNeeded() {
|
redirectIfNeeded() {
|
||||||
if ((this.mode == 'reader' || this.mode == 'omnireader') && (this.rootRoute != '/reader')) {
|
if ((this.mode == 'reader' || this.mode == 'omnireader' || this.mode == 'liberama.top')) {
|
||||||
//старый url
|
|
||||||
const search = window.location.search.substr(1);
|
const search = window.location.search.substr(1);
|
||||||
const url = search.split('url=')[1] || '';
|
|
||||||
|
//распознавание параметра url вида "?url=<link>" и редирект при необходимости
|
||||||
|
if (!this.isReaderActive) {
|
||||||
|
const s = search.split('url=');
|
||||||
|
const url = s[1] || '';
|
||||||
|
const q = utils.parseQuery(s[0] || '');
|
||||||
if (url) {
|
if (url) {
|
||||||
window.location = `/#/reader?url=${url}`;
|
q.url = decodeURIComponent(url);
|
||||||
} else {
|
|
||||||
this.$router.replace('/reader');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//yandex-метрика для omnireader
|
window.history.replaceState({}, '', '/');
|
||||||
if (this.config.branch == 'production' && this.mode == 'omnireader' && !this.yaMetricsDone) {
|
this.$router.replace({ path: '/reader', query: q });
|
||||||
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
}
|
||||||
m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
|
|
||||||
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");// eslint-disable-line no-unexpected-multiline
|
|
||||||
|
|
||||||
ym(52347334, "init", {// eslint-disable-line no-undef
|
|
||||||
id:52347334,
|
|
||||||
clickmap:true,
|
|
||||||
trackLinks:true,
|
|
||||||
accurateTrackBounce:true
|
|
||||||
});
|
|
||||||
|
|
||||||
this.yaMetricsDone = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(App);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -237,68 +260,36 @@ class App extends Vue {
|
|||||||
line-height: 140%;
|
line-height: 140%;
|
||||||
font-weight: bold;
|
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>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body, html, #app {
|
body, html, #app {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font: normal 12pt ReaderDefault;
|
font: normal 12pt ReaderDefault;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-tabs__content {
|
.dborder {
|
||||||
flex: 1;
|
border: 2px solid magenta !important;
|
||||||
padding: 0 !important;
|
}
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
.icon-rotate {
|
||||||
overflow: hidden;
|
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 {
|
@font-face {
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div>
|
||||||
Раздел Book в разработке
|
Раздел Book в разработке
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
|
||||||
export default @Component({
|
class Book {
|
||||||
})
|
|
||||||
class Book extends Vue {
|
|
||||||
created() {
|
created() {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(Book);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div>
|
||||||
Раздел Card в разработке
|
Раздел Card в разработке
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
|
||||||
export default @Component({
|
class Card {
|
||||||
})
|
|
||||||
class Card extends Vue {
|
|
||||||
created() {
|
created() {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(Card);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,24 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container direction="vertical">
|
<div>
|
||||||
<el-tabs type="border-card" style="height: 100%;" v-model="selectedTab">
|
<router-view v-slot="{ Component }">
|
||||||
<el-tab-pane label="Поиск"></el-tab-pane>
|
|
||||||
<el-tab-pane label="Автор"></el-tab-pane>
|
|
||||||
<el-tab-pane label="Книга"></el-tab-pane>
|
|
||||||
<el-tab-pane label="История"></el-tab-pane>
|
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<router-view></router-view>
|
<component :is="Component" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</el-tabs>
|
</router-view>
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
const rootRoute = '/cardindex';
|
const selfRoute = '/cardindex';
|
||||||
const tab2Route = [
|
const tab2Route = [
|
||||||
'/cardindex/search',
|
'/cardindex/search',
|
||||||
'/cardindex/card',
|
'/cardindex/card',
|
||||||
@@ -27,20 +22,32 @@ const tab2Route = [
|
|||||||
];
|
];
|
||||||
let lastActiveTab = null;
|
let lastActiveTab = null;
|
||||||
|
|
||||||
export default @Component({
|
const componentOptions = {
|
||||||
watch: {
|
watch: {
|
||||||
selectedTab: function(newValue, oldValue) {
|
selectedTab: function(newValue) {
|
||||||
lastActiveTab = newValue;
|
lastActiveTab = newValue;
|
||||||
this.setRouteByTab(newValue);
|
this.setRouteByTab(newValue);
|
||||||
},
|
},
|
||||||
curRoute: function(newValue, oldValue) {
|
curRoute: function(newValue) {
|
||||||
this.setTabByRoute(newValue);
|
this.setTabByRoute(newValue);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
};
|
||||||
class CardIndex extends Vue {
|
class CardIndex {
|
||||||
|
_options = componentOptions;
|
||||||
selectedTab = null;
|
selectedTab = null;
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.$watch(
|
||||||
|
() => this.$route.path,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue == '/cardindex' && this.isReader) {
|
||||||
|
this.$router.replace({ path: '/reader' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.setTabByRoute(this.curRoute);
|
this.setTabByRoute(this.curRoute);
|
||||||
}
|
}
|
||||||
@@ -51,7 +58,7 @@ class CardIndex extends Vue {
|
|||||||
if (t !== this.selectedTab)
|
if (t !== this.selectedTab)
|
||||||
this.selectedTab = t.toString();
|
this.selectedTab = t.toString();
|
||||||
} else {
|
} else {
|
||||||
if (route == rootRoute && lastActiveTab !== null)
|
if (route == selfRoute && lastActiveTab !== null)
|
||||||
this.setRouteByTab(lastActiveTab);
|
this.setRouteByTab(lastActiveTab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,12 +70,22 @@ class CardIndex extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get mode() {
|
||||||
|
return this.$store.state.config.mode;
|
||||||
|
}
|
||||||
|
|
||||||
get curRoute() {
|
get curRoute() {
|
||||||
const m = this.$route.path.match(/^(\/[^\/]*\/[^\/]*).*$/i);
|
const m = this.$route.path.match(/^(\/[^/]*\/[^/]*).*$/i);
|
||||||
return (m ? m[1] : this.$route.path);
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div>
|
||||||
Раздел History в разработке
|
Раздел History в разработке
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
|
||||||
export default @Component({
|
class History {
|
||||||
})
|
|
||||||
class History extends Vue {
|
|
||||||
created() {
|
created() {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(History);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div>
|
||||||
Раздел Search в разработке
|
Раздел Search в разработке
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
|
||||||
export default @Component({
|
class Search {
|
||||||
})
|
|
||||||
class Search extends Vue {
|
|
||||||
created() {
|
created() {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(Search);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</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>
|
<template>
|
||||||
<el-container>
|
<div>
|
||||||
Раздел Help в разработке
|
Раздел Help в разработке
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
|
||||||
export default @Component({
|
class Help {
|
||||||
})
|
|
||||||
class Help extends Vue {
|
|
||||||
created() {
|
created() {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(Help);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div>
|
||||||
Раздел Income в разработке
|
Раздел Income в разработке
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
|
||||||
export default @Component({
|
class Income {
|
||||||
})
|
|
||||||
class Income extends Vue {
|
|
||||||
created() {
|
created() {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(Income);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div>
|
||||||
Страница не найдена
|
Страница не найдена
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
|
||||||
export default @Component({
|
class NotFound404 {
|
||||||
})
|
|
||||||
class NotFound404 extends Vue {
|
|
||||||
created() {
|
created() {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(NotFound404);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,15 +6,12 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
|
||||||
import {sleep} from '../../../share/utils';
|
import {sleep} from '../../../share/utils';
|
||||||
import {clickMap, clickMapText} from '../share/clickMap';
|
import {clickMap, clickMapText} from '../share/clickMap';
|
||||||
|
|
||||||
export default @Component({
|
class ClickMapPage {
|
||||||
})
|
|
||||||
class ClickMapPage extends Vue {
|
|
||||||
fontSize = '200%';
|
fontSize = '200%';
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
@@ -53,6 +50,8 @@ class ClickMapPage extends Vue {
|
|||||||
await sleep(5000);
|
await sleep(5000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(ClickMapPage);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</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,8 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="main" class="main" @click="close">
|
|
||||||
<div class="mainWindow" @click.stop>
|
|
||||||
<Window @close="close">
|
<Window @close="close">
|
||||||
<template slot="header">
|
<template #header>
|
||||||
Скопировать текст
|
Скопировать текст
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -10,24 +8,23 @@
|
|||||||
<div v-html="text"></div>
|
<div v-html="text"></div>
|
||||||
</div>
|
</div>
|
||||||
</Window>
|
</Window>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
|
||||||
import Window from '../../share/Window.vue';
|
import Window from '../../share/Window.vue';
|
||||||
import {sleep} from '../../../share/utils';
|
import {sleep} from '../../../share/utils';
|
||||||
|
|
||||||
export default @Component({
|
const componentOptions = {
|
||||||
components: {
|
components: {
|
||||||
Window,
|
Window,
|
||||||
},
|
},
|
||||||
})
|
};
|
||||||
class CopyTextPage extends Vue {
|
class CopyTextPage {
|
||||||
|
_options = componentOptions;
|
||||||
|
|
||||||
text = null;
|
text = null;
|
||||||
initStep = null;
|
initStep = null;
|
||||||
initPercentage = 0;
|
initPercentage = 0;
|
||||||
@@ -95,37 +92,22 @@ class CopyTextPage extends Vue {
|
|||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.stopInit = true;
|
this.stopInit = true;
|
||||||
this.$emit('copy-text-toggle');
|
this.$emit('do-action', {action: 'copyText'});
|
||||||
}
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
if (event.type == 'keydown' && (event.code == 'Escape')) {
|
if (event.type == 'keydown' && event.key == 'Escape') {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(CopyTextPage);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 40;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainWindow {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<h4>Возможности читалки:</h4>
|
<span class="text-h6 text-bold">Возможности читалки:</span>
|
||||||
<ul>
|
<ul>
|
||||||
<li>загрузка любой страницы интернета</li>
|
<li>загрузка любой страницы интернета</li>
|
||||||
|
<li>синхронизация данных (настроек и читаемых книг) между различными устройствами</li>
|
||||||
|
<li>работа в автономном режиме (без связи)</li>
|
||||||
<li>изменение цвета фона, текста, размер и тип шрифта и прочее</li>
|
<li>изменение цвета фона, текста, размер и тип шрифта и прочее</li>
|
||||||
<li>установка и запоминание текущей позиции и настроек в браузере (в будущем планируется сохранение и на сервер)</li>
|
<li>установка и запоминание текущей позиции и настроек в браузере и на сервере</li>
|
||||||
<li>кэширование файлов книг на клиенте и на сервере</li>
|
<li>кэширование файлов книг на клиенте и на сервере</li>
|
||||||
<li>открытие книг с локального диска</li>
|
<li>открытие книг с локального диска</li>
|
||||||
<li>плавный скроллинг текста</li>
|
<li>плавный скроллинг текста</li>
|
||||||
@@ -12,55 +14,80 @@
|
|||||||
<li>поиск по тексту и копирование фрагмента</li>
|
<li>поиск по тексту и копирование фрагмента</li>
|
||||||
<li>запоминание недавних книг, скачивание книги из читалки в формате fb2</li>
|
<li>запоминание недавних книг, скачивание книги из читалки в формате fb2</li>
|
||||||
<li>управление кликом и с клавиатуры</li>
|
<li>управление кликом и с клавиатуры</li>
|
||||||
<li>подключение к интернету не обязательно для чтения книги после ее загрузки</li>
|
|
||||||
<li>регистрация не требуется</li>
|
<li>регистрация не требуется</li>
|
||||||
<li>поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий</li>
|
<li>поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>В качестве URL можно задавать html-страничку с книгой, либо прямую ссылку
|
<p>
|
||||||
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").</p>
|
В качестве URL книги можно задавать html-страничку с книгой, либо прямую ссылку
|
||||||
<p>Поддерживаемые форматы: <strong>html, txt, fb2, fb2.zip</strong></p>
|
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").
|
||||||
|
</p>
|
||||||
|
<p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p>
|
||||||
|
|
||||||
<div v-html="automationHtml"></div>
|
<div v-show="mode == 'omnireader' || mode == 'liberama.top'">
|
||||||
|
<p>
|
||||||
|
Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
||||||
|
<br><strong>{{ bookmarkText }}</strong>
|
||||||
|
<q-icon class="copy-icon" name="la la-copy" @click="copyText(bookmarkText, 'Код для адреса закладки успешно скопирован в буфер обмена')">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
||||||
|
Скопировать
|
||||||
|
</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
|
||||||
|
<br>или перетащив на панель закладок следующую ссылку:
|
||||||
|
<br><a style="margin-left: 50px" :href="bookmarkText">{{ (mode == 'omnireader' ? 'Omni' : 'Liberama') }} Reader</a>
|
||||||
|
<br>Тогда, активировав получившуюся закладку на любой странице интернета, вы автоматически загрузите эту страницу в Omni Reader.
|
||||||
|
<br>В Chrome для Android можно вызывать такую закладку по имени прямо в адресной строке браузера (имя стоит сделать попроще).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<p>Связаться с разработчиком: <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a></p>
|
<p>Связаться с разработчиком: <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a></p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../../../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
|
||||||
export default @Component({
|
import {copyTextToClipboard} from '../../../../share/utils';
|
||||||
})
|
|
||||||
class CommonHelpPage extends Vue {
|
class CommonHelpPage {
|
||||||
created() {
|
created() {
|
||||||
this.config = this.$store.state.config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get automationHtml() {
|
get mode() {
|
||||||
if (this.config.mode == 'omnireader') {
|
return this.$store.state.config.mode;
|
||||||
return `<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
|
||||||
<br><strong>javascript:location.href='http://omnireader.ru/?url='+location.href;</strong>
|
|
||||||
<br>Тогда, нажав на получившуюся кнопку на любой странице интернета, вы автоматически откроете ее в Omni Reader.</p>`;
|
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get bookmarkText() {
|
||||||
|
return `javascript:location.href='https://${window.location.host}/?url='+location.href;`
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyText(text, mes) {
|
||||||
|
const result = await copyTextToClipboard(text);
|
||||||
|
const msg = (result ? mes : 'Копирование не удалось');
|
||||||
|
if (result)
|
||||||
|
this.$root.notify.success(msg);
|
||||||
|
else
|
||||||
|
this.$root.notify.error(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(CommonHelpPage);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.page {
|
||||||
flex: 1;
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
line-height: 130%;
|
line-height: 130%;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
.copy-icon {
|
||||||
margin: 0;
|
margin-left: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 120%;
|
||||||
|
color: blue;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,30 +1,70 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<p class="p">Проект существует исключительно на личном энтузиазме.</p>
|
<p class="p">
|
||||||
<p class="p">Чтобы энтузиазма было побольше, вы можете пожертвовать на развитие проекта любую сумму:</p>
|
Вы можете пожертвовать на развитие проекта любую сумму:
|
||||||
|
</p>
|
||||||
<div class="address">
|
<div class="address">
|
||||||
<img class="logo" src="./assets/yandex.png">
|
<img class="logo" src="./assets/yoomoney.png">
|
||||||
<el-button class="button" @click="donateYandexMoney">Пожертвовать</el-button><br>
|
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYooMoney">
|
||||||
<div class="para">{{ yandexAddress }}</div>
|
Пожертвовать
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!--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">
|
<div class="address">
|
||||||
<img class="logo" src="./assets/bitcoin.png">
|
<img class="logo" src="./assets/bitcoin.png">
|
||||||
<el-button class="button" @click="copyAddress(bitcoinAddress, 'Bitcoin')">Скопировать</el-button><br>
|
<div class="para">
|
||||||
<div class="para">{{ bitcoinAddress }}</div>
|
{{ 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>
|
||||||
|
|
||||||
<div class="address">
|
<div class="address">
|
||||||
<img class="logo" src="./assets/litecoin.png">
|
<img class="logo" src="./assets/litecoin.png">
|
||||||
<el-button class="button" @click="copyAddress(litecoinAddress, 'Litecoin')">Скопировать</el-button><br>
|
<div class="para">
|
||||||
<div class="para">{{ litecoinAddress }}</div>
|
{{ 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>
|
||||||
|
|
||||||
<div class="address">
|
<div class="address">
|
||||||
<img class="logo" src="./assets/monero.png">
|
<img class="logo" src="./assets/monero.png">
|
||||||
<el-button class="button" @click="copyAddress(moneroAddress, 'Monero')">Скопировать</el-button><br>
|
<div class="para">
|
||||||
<div class="para">{{ moneroAddress }}</div>
|
{{ 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,14 +72,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../../../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
import {copyTextToClipboard} from '../../../../share/utils';
|
import {copyTextToClipboard} from '../../../../share/utils';
|
||||||
|
|
||||||
export default @Component({
|
class DonateHelpPage {
|
||||||
})
|
yooAddress = '410018702323056';
|
||||||
class DonateHelpPage extends Vue {
|
paypalAddress = 'bookpauk@gmail.com';
|
||||||
yandexAddress = '410018702323056';
|
|
||||||
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
|
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
|
||||||
litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ';
|
litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ';
|
||||||
moneroAddress = '8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz';
|
moneroAddress = '8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz';
|
||||||
@@ -47,30 +86,29 @@ class DonateHelpPage extends Vue {
|
|||||||
created() {
|
created() {
|
||||||
}
|
}
|
||||||
|
|
||||||
donateYandexMoney() {
|
donateYooMoney() {
|
||||||
window.open(`https://money.yandex.ru/to/${this.yandexAddress}`, '_blank');
|
window.open(`https://yoomoney.ru/to/${this.yooAddress}`, '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
async copyAddress(address, prefix) {
|
async copyAddress(address, prefix) {
|
||||||
const result = await copyTextToClipboard(address);
|
const result = await copyTextToClipboard(address);
|
||||||
const msg = (result ? `${prefix}-адрес ${address} успешно скопирован в буфер обмена` : 'Копирование не удалось');
|
|
||||||
if (result)
|
if (result)
|
||||||
this.$notify.success({message: msg});
|
this.$root.notify.success(`${prefix} ${address} успешно скопирован в буфер обмена`);
|
||||||
else
|
else
|
||||||
this.$notify.error({message: msg});
|
this.$root.notify.error('Копирование не удалось');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(DonateHelpPage);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.page {
|
||||||
flex: 1;
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
line-height: 130%;
|
line-height: 130%;
|
||||||
display: flex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.p {
|
.p {
|
||||||
@@ -80,15 +118,10 @@ class DonateHelpPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
.box {
|
||||||
flex: 1;
|
|
||||||
max-width: 550px;
|
max-width: 550px;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
h5 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.address {
|
.address {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
@@ -98,13 +131,16 @@ h5 {
|
|||||||
margin: 10px 10px 10px 40px;
|
margin: 10px 10px 10px 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
width: 130px;
|
width: 130px;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.copy-icon {
|
||||||
|
margin-left: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 120%;
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
@@ -1,100 +1,103 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="main" class="main" @click="close">
|
|
||||||
<div class="mainWindow" @click.stop>
|
|
||||||
<Window @close="close">
|
<Window @close="close">
|
||||||
<template slot="header">
|
<template #header>
|
||||||
Справка
|
Справка
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-tabs type="border-card" v-model="selectedTab">
|
<div class="col column" style="min-width: 600px">
|
||||||
<el-tab-pane class="tab" label="Общее">
|
<div class="bg-grey-3 row">
|
||||||
<CommonHelpPage></CommonHelpPage>
|
<q-tabs
|
||||||
</el-tab-pane>
|
v-model="selectedTab"
|
||||||
<el-tab-pane label="Клавиатура">
|
active-color="black"
|
||||||
<HotkeysHelpPage></HotkeysHelpPage>
|
active-bg-color="white"
|
||||||
</el-tab-pane>
|
indicator-color="white"
|
||||||
<el-tab-pane label="Мышь/тачпад">
|
dense
|
||||||
<MouseHelpPage></MouseHelpPage>
|
no-caps
|
||||||
</el-tab-pane>
|
inline-label
|
||||||
<el-tab-pane label="Помочь проекту" name="donate">
|
class="bg-grey-4 text-grey-7"
|
||||||
<DonateHelpPage></DonateHelpPage>
|
>
|
||||||
</el-tab-pane>
|
<q-tab v-for="btn in buttons" :key="btn.value" :name="btn.value" :label="btn.label" />
|
||||||
|
</q-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
</el-tabs>
|
<keep-alive>
|
||||||
|
<component :is="activePage" ref="page" class="col"></component>
|
||||||
|
</keep-alive>
|
||||||
|
</div>
|
||||||
</Window>
|
</Window>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
|
||||||
import Window from '../../share/Window.vue';
|
import Window from '../../share/Window.vue';
|
||||||
import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
|
import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
|
||||||
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
|
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
|
||||||
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.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({
|
const pages = {
|
||||||
components: {
|
'CommonHelpPage': CommonHelpPage,
|
||||||
Window,
|
'HotkeysHelpPage': HotkeysHelpPage,
|
||||||
CommonHelpPage,
|
'MouseHelpPage': MouseHelpPage,
|
||||||
HotkeysHelpPage,
|
'VersionHistoryPage': VersionHistoryPage,
|
||||||
MouseHelpPage,
|
//'DonateHelpPage': DonateHelpPage,
|
||||||
DonateHelpPage,
|
};
|
||||||
},
|
|
||||||
})
|
const tabs = [
|
||||||
class HelpPage extends Vue {
|
['CommonHelpPage', 'Общее'],
|
||||||
selectedTab = null;
|
['MouseHelpPage', 'Мышь/тачскрин'],
|
||||||
|
['HotkeysHelpPage', 'Клавиатура'],
|
||||||
|
['VersionHistoryPage', 'История версий'],
|
||||||
|
//['DonateHelpPage', 'Помочь проекту'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const componentOptions = {
|
||||||
|
components: Object.assign({ Window }, pages),
|
||||||
|
};
|
||||||
|
class HelpPage {
|
||||||
|
_options = componentOptions;
|
||||||
|
|
||||||
|
selectedTab = 'CommonHelpPage';
|
||||||
|
|
||||||
close() {
|
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() {
|
activateDonateHelpPage() {
|
||||||
this.selectedTab = 'donate';
|
//this.selectedTab = 'DonateHelpPage';
|
||||||
|
}
|
||||||
|
|
||||||
|
activateVersionHistoryHelpPage() {
|
||||||
|
this.selectedTab = 'VersionHistoryPage';
|
||||||
}
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
if (event.type == 'keydown' && (event.code == 'Escape')) {
|
if (event.type == 'keydown' && event.key == 'Escape') {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(HelpPage);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 40;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainWindow {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-tabs {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-tab-pane {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,54 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<h4>Управление с помощью горячих клавиш:</h4>
|
<div style="font-size: 120%">
|
||||||
<ul>
|
<div class="text-h6 text-bold">
|
||||||
<li><b>F1, H</b> - открыть справку</li>
|
Доступны следующие клавиатурные команды:
|
||||||
<li><b>Escape</b> - показать/скрыть страницу загрузки</li>
|
</div>
|
||||||
<li><b>Tab, Q</b> - показать/скрыть панель управления</li>
|
<br>
|
||||||
<li><b>PageUp, Left, Shift+Space, Backspace</b> - страницу назад</li>
|
</div>
|
||||||
<li><b>PageDown, Right, Space</b> - страницу вперед</li>
|
<div class="q-mb-md" style="width: 550px">
|
||||||
<li><b>Home</b> - в начало книги</li>
|
<div class="text-right text-italic" style="font-size: 80%">
|
||||||
<li><b>End</b> - в конец книги</li>
|
* Изменить сочетания клавиш можно в настройках
|
||||||
<li><b>Up</b> - строчку назад</li>
|
</div>
|
||||||
<li><b>Down</b> - строчку вперёд</li>
|
<UserHotKeys v-model="userHotKeys" readonly />
|
||||||
<li><b>A, Shift+A</b> - изменить размер шрифта</li>
|
</div>
|
||||||
<li><b>Enter, F, F11, ` (апостроф)</b> - вкл./выкл. полный экран</li>
|
|
||||||
<li><b>Z</b> - вкл./выкл. плавный скроллинг текста</li>
|
|
||||||
<li><b>Shift+Down/Shift+Up</b> - увеличить/уменьшить скорость скроллинга
|
|
||||||
<li><b>P</b> - установить страницу</li>
|
|
||||||
<li><b>Ctrl+F</b> - найти в тексте</li>
|
|
||||||
<li><b>Ctrl+C</b> - скопировать текст со страницы</li>
|
|
||||||
<li><b>R</b> - принудительно обновить книгу в обход кэша</li>
|
|
||||||
<li><b>X</b> - открыть недавние</li>
|
|
||||||
<li><b>S</b> - открыть окно настроек</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../../../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
import UserHotKeys from '../../SettingsPage/UserHotKeys/UserHotKeys.vue';
|
||||||
|
|
||||||
|
const componentOptions = {
|
||||||
|
components: {
|
||||||
|
UserHotKeys,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
class HotkeysHelpPage {
|
||||||
|
_options = componentOptions;
|
||||||
|
|
||||||
export default @Component({
|
|
||||||
})
|
|
||||||
class HotkeysHelpPage extends Vue {
|
|
||||||
created() {
|
created() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get userHotKeys() {
|
||||||
|
return this.$store.state.reader.settings.userHotKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
set userHotKeys(value) {
|
||||||
|
//no setter
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(HotkeysHelpPage);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.page {
|
||||||
flex: 1;
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-size: 120%;
|
|
||||||
line-height: 130%;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,31 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<h4>Управление с помощью мыши/тачпада:</h4>
|
<span class="text-h6 text-bold">Управление с помощью мыши/тачскрина:</span>
|
||||||
<ul>
|
<ul>
|
||||||
<li><b>ЛКМ/ТАЧ</b> по экрану в одну из областей - активация действия:</li>
|
<li><b>ЛКМ/ТАЧ</b> по экрану в одну из областей - активация действия:</li>
|
||||||
<div class="click-map-page">
|
<div class="click-map-page">
|
||||||
<ClickMapPage ref="clickMapPage"></ClickMapPage>
|
<ClickMapPage ref="clickMapPage"></ClickMapPage>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<li><b>ПКМ</b> - показать/скрыть панель управления</li>
|
<li><b>ПКМ</b> - показать/скрыть панель управления</li>
|
||||||
<li><b>СКМ</b> - вкл./выкл. плавный скроллинг текста</li>
|
<li><b>СКМ</b> - вкл./выкл. плавный скроллинг текста</li>
|
||||||
|
<br>
|
||||||
|
<li>Жесты для тачскрина:</li>
|
||||||
|
<ul>
|
||||||
|
<li style="list-style-type: square">
|
||||||
|
от центра вверх: на весь экран
|
||||||
|
</li>
|
||||||
|
<li style="list-style-type: square">
|
||||||
|
от центра вниз: плавный скроллинг
|
||||||
|
</li>
|
||||||
|
<li style="list-style-type: square">
|
||||||
|
от центра вправо: увеличить скорость скроллинга
|
||||||
|
</li>
|
||||||
|
<li style="list-style-type: square">
|
||||||
|
от центра влево: уменьшить скорость скроллинга
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
* Для управления с помощью мыши/тачпада необходимо установить галочку "Включить управление кликом" в настройках
|
</ul>
|
||||||
|
* Для управления с помощью мыши/тачскрина необходимо установить галочку "Включить управление кликом" в настройках
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../../../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
|
||||||
import ClickMapPage from '../../ClickMapPage/ClickMapPage.vue';
|
import ClickMapPage from '../../ClickMapPage/ClickMapPage.vue';
|
||||||
|
|
||||||
export default @Component({
|
const componentOptions = {
|
||||||
components: {
|
components: {
|
||||||
ClickMapPage,
|
ClickMapPage,
|
||||||
},
|
},
|
||||||
})
|
};
|
||||||
class MouseHelpPage extends Vue {
|
class MouseHelpPage {
|
||||||
|
_options = componentOptions;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,22 +52,19 @@ class MouseHelpPage extends Vue {
|
|||||||
this.$refs.clickMapPage.$el.style.backgroundColor = '#478355';
|
this.$refs.clickMapPage.$el.style.backgroundColor = '#478355';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(MouseHelpPage);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.page {
|
||||||
flex: 1;
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
line-height: 130%;
|
line-height: 130%;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-map-page {
|
.click-map-page {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<div id="versionHistoryPage" class="page">
|
||||||
|
<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 }}
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div v-for="item in versionContent" :id="item.key" :key="item.key">
|
||||||
|
<span v-html="item.content"></span>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import vueComponent from '../../../vueComponent.js';
|
||||||
|
|
||||||
|
import {versionHistory} from '../../versionHistory';
|
||||||
|
|
||||||
|
class VersionHistoryPage {
|
||||||
|
versionHeader = [];
|
||||||
|
versionContent = [];
|
||||||
|
|
||||||
|
created() {
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
let vh = [];
|
||||||
|
for (const v of versionHistory) {
|
||||||
|
vh.push(`${v.version} (${v.releaseDate})`);
|
||||||
|
}
|
||||||
|
this.versionHeader = vh;
|
||||||
|
|
||||||
|
let vc = [];
|
||||||
|
for (const v of versionHistory) {
|
||||||
|
let header = `${v.version} (${v.releaseDate})`;
|
||||||
|
vc.push({key: header, content: 'Версия ' + header + v.content});
|
||||||
|
}
|
||||||
|
this.versionContent = vc;
|
||||||
|
}
|
||||||
|
|
||||||
|
showRelease(id) {
|
||||||
|
let el = document.getElementById(id);
|
||||||
|
if (el) {
|
||||||
|
document.getElementById('versionHistoryPage').scrollTop = el.offsetTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default vueComponent(VersionHistoryPage);
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
padding: 15px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 120%;
|
||||||
|
line-height: 130%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
color: blue;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div ref="main" class="main" @click="close">
|
|
||||||
<div class="mainWindow" @click.stop>
|
|
||||||
<Window @close="close">
|
|
||||||
<template slot="header">
|
|
||||||
Последние 100 открытых книг
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<el-table
|
|
||||||
:data="tableData"
|
|
||||||
style="width: 100%"
|
|
||||||
size="mini"
|
|
||||||
height="1px"
|
|
||||||
stripe
|
|
||||||
border
|
|
||||||
:default-sort = "{prop: 'touchDateTime', order: 'descending'}"
|
|
||||||
:header-cell-style = "headerCellStyle"
|
|
||||||
>
|
|
||||||
|
|
||||||
<el-table-column
|
|
||||||
prop="touchDateTime"
|
|
||||||
min-width="90px"
|
|
||||||
sortable
|
|
||||||
>
|
|
||||||
<template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
|
|
||||||
<span style="font-size: 90%">Время<br>просм.</span>
|
|
||||||
</template>
|
|
||||||
<template slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
|
|
||||||
<div class="desc" @click="loadBook(scope.row.url)">
|
|
||||||
{{ scope.row.touchDate }}<br>
|
|
||||||
{{ scope.row.touchTime }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column
|
|
||||||
>
|
|
||||||
<template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
|
|
||||||
<!--el-input ref="input"
|
|
||||||
:value="search" @input="search = $event"
|
|
||||||
size="mini"
|
|
||||||
style="margin: 0; padding: 0; vertical-align: bottom; margin-top: 10px"
|
|
||||||
placeholder="Найти"/-->
|
|
||||||
<div class="el-input el-input--mini">
|
|
||||||
<input class="el-input__inner"
|
|
||||||
placeholder="Найти"
|
|
||||||
style="margin: 0; padding: 0; vertical-align: bottom; margin-top: 20px; padding: 0 10px 0 10px"
|
|
||||||
:value="search" @input="search = $event.target.value"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<el-table-column
|
|
||||||
min-width="300px"
|
|
||||||
>
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<div class="desc" @click="loadBook(scope.row.url)">
|
|
||||||
<span style="color: green">{{ scope.row.desc.author }}</span><br>
|
|
||||||
<span>{{ scope.row.desc.title }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column
|
|
||||||
min-width="100px"
|
|
||||||
>
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<a v-show="isUrl(scope.row.url)" :href="scope.row.url" target="_blank">Оригинал</a><br>
|
|
||||||
<a :href="scope.row.path" :download="getFileNameFromPath(scope.row.path)">Скачать FB2</a>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column
|
|
||||||
width="60px"
|
|
||||||
>
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-button
|
|
||||||
size="mini"
|
|
||||||
style="width: 30px; padding: 7px 0 7px 0; margin-left: 4px"
|
|
||||||
@click="handleDel(scope.row.key)"><i class="el-icon-close"></i>
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
</el-table>
|
|
||||||
</Window>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
import Vue from 'vue';
|
|
||||||
import Component from 'vue-class-component';
|
|
||||||
import path from 'path';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
import {formatDate} from '../../../share/utils';
|
|
||||||
import Window from '../../share/Window.vue';
|
|
||||||
import bookManager from '../share/bookManager';
|
|
||||||
|
|
||||||
export default @Component({
|
|
||||||
components: {
|
|
||||||
Window,
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
search: function() {
|
|
||||||
this.updateTableData();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
class HistoryPage extends Vue {
|
|
||||||
search = null;
|
|
||||||
tableData = null;
|
|
||||||
|
|
||||||
created() {
|
|
||||||
}
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.updateTableData();
|
|
||||||
this.mostRecentBook = bookManager.mostRecentBook();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTableData() {
|
|
||||||
let result = [];
|
|
||||||
|
|
||||||
const sorted = bookManager.getSortedRecent();
|
|
||||||
const len = (sorted.length < 100 ? sorted.length : 100);
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
const book = sorted[i];
|
|
||||||
let d = new Date();
|
|
||||||
d.setTime(book.touchTime);
|
|
||||||
const t = formatDate(d).split(' ');
|
|
||||||
|
|
||||||
let perc = '';
|
|
||||||
let textLen = '';
|
|
||||||
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
|
|
||||||
if (book.textLength) {
|
|
||||||
perc = ` [${((p/book.textLength)*100).toFixed(2)}%]`;
|
|
||||||
textLen = ` ${Math.round(book.textLength/1000)}k`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fb2 = (book.fb2 ? book.fb2 : {});
|
|
||||||
|
|
||||||
let title = fb2.bookTitle;
|
|
||||||
if (title)
|
|
||||||
title = `"${title}"`;
|
|
||||||
else
|
|
||||||
title = '';
|
|
||||||
|
|
||||||
let author = _.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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const search = this.search;
|
|
||||||
this.tableData = 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())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
headerCellStyle(cell) {
|
|
||||||
let result = {margin: 0, padding: 0};
|
|
||||||
if (cell.columnIndex > 0) {
|
|
||||||
result['border-bottom'] = 0;
|
|
||||||
}
|
|
||||||
if (cell.rowIndex > 0) {
|
|
||||||
result.height = '0px';
|
|
||||||
result['border-right'] = 0;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFileNameFromPath(fb2Path) {
|
|
||||||
return path.basename(fb2Path).substr(0, 10) + '.fb2';
|
|
||||||
}
|
|
||||||
|
|
||||||
openOriginal(url) {
|
|
||||||
window.open(url, '_blank');
|
|
||||||
}
|
|
||||||
|
|
||||||
openFb2(path) {
|
|
||||||
window.open(path, '_blank');
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleDel(key) {
|
|
||||||
await bookManager.delRecentBook({key});
|
|
||||||
this.updateTableData();
|
|
||||||
|
|
||||||
const newRecent = bookManager.mostRecentBook();
|
|
||||||
if (this.mostRecentBook != newRecent)
|
|
||||||
this.$emit('load-book', newRecent);
|
|
||||||
|
|
||||||
this.mostRecentBook = newRecent;
|
|
||||||
if (!this.mostRecentBook)
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadBook(url) {
|
|
||||||
this.$emit('load-book', {url});
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
isUrl(url) {
|
|
||||||
return (url.indexOf('file://') != 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.$emit('history-toggle');
|
|
||||||
}
|
|
||||||
|
|
||||||
keyHook(event) {
|
|
||||||
if (event.type == 'keydown' && event.code == 'Escape') {
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.main {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 50;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainWindow {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desc {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
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>
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<template>
|
||||||
|
<div id="vue-github-corner">
|
||||||
|
<a :href="url" id="github-corner" target="_blank" aria-label="View source on Github" >
|
||||||
|
<svg id="github-corner-svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 250 250"
|
||||||
|
:width="size" :height="size"
|
||||||
|
:style="svgStyle" >
|
||||||
|
<path :d="svgPath1" @mouseenter="flipColor" @mouseleave="flipColor"></path>
|
||||||
|
<path :d="svgPath2" :style="gitStyle" class="octo-arm"></path>
|
||||||
|
<path :d="svgPath3" :style="gitStyle" class="octo-body"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'GithubCorner',
|
||||||
|
props: {
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
default: '/'
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 80
|
||||||
|
},
|
||||||
|
colorScheme: {
|
||||||
|
type: String,
|
||||||
|
default: 'auto'
|
||||||
|
},
|
||||||
|
cornerColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#625D5D'
|
||||||
|
},
|
||||||
|
gitColor: {
|
||||||
|
type: String,
|
||||||
|
default: 'PeachPuff'
|
||||||
|
},
|
||||||
|
leftCorner: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
flipOnHover: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
svgStyle: {
|
||||||
|
fill: this.cornerColor,
|
||||||
|
right: (this.leftCorner ? 'auto' : '0'),
|
||||||
|
left: (this.leftCorner ? '0' : 'auto'),
|
||||||
|
transform: (this.leftCorner ? 'scale(-1, 1)' : 'none')
|
||||||
|
},
|
||||||
|
gitStyle: {
|
||||||
|
fill: this.gitColor
|
||||||
|
},
|
||||||
|
flipped: false,
|
||||||
|
svgPath1: 'M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z',
|
||||||
|
svgPath2: 'M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 ' +
|
||||||
|
'123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2',
|
||||||
|
svgPath3: 'M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 ' +
|
||||||
|
'C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 ' +
|
||||||
|
'176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 ' +
|
||||||
|
'216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 ' +
|
||||||
|
'C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
flipColor: function() {
|
||||||
|
if (this.flipOnHover) {
|
||||||
|
let holdSvgFill = this.svgStyle.fill
|
||||||
|
this.svgStyle.fill = this.gitStyle.fill
|
||||||
|
this.gitStyle.fill = holdSvgFill
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeMount: function() {
|
||||||
|
if (this.colorScheme != 'auto') {
|
||||||
|
let sch = this.colorScheme
|
||||||
|
this.gitStyle.fill = '#fff'
|
||||||
|
|
||||||
|
if (sch.toLowerCase() == 'black') {
|
||||||
|
this.svgStyle.fill = '#151513'
|
||||||
|
}
|
||||||
|
if (sch.toLowerCase() == 'green') {
|
||||||
|
this.svgStyle.fill = '#64CEAA'
|
||||||
|
}
|
||||||
|
if (sch.toLowerCase() == 'red') {
|
||||||
|
this.svgStyle.fill = '#FD6C6C'
|
||||||
|
}
|
||||||
|
if (sch.toLowerCase() == 'blue') {
|
||||||
|
this.svgStyle.fill = '#70B7FD'
|
||||||
|
}
|
||||||
|
if (sch.toLowerCase() == 'white') {
|
||||||
|
this.svgStyle.fill = '#fff'
|
||||||
|
this.gitStyle.fill = '#151513'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#github-corner .octo-arm {
|
||||||
|
transform-origin: 130px 106px
|
||||||
|
}
|
||||||
|
#github-corner:hover .octo-arm {
|
||||||
|
animation: octocat-wave 560ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes octocat-wave {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
20% { transform: rotate(-25deg); }
|
||||||
|
40% { transform: rotate(10deg); }
|
||||||
|
60% { transform: rotate(-25deg); }
|
||||||
|
80% { transform: rotate(10deg); }
|
||||||
|
100% { transform: rotate(0deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#github-corner-svg {
|
||||||
|
color: #fff;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#github-corner-svg, #github-corner-svg .octo-arm, #github-corner-svg .octo-body {
|
||||||
|
transition: fill 1s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,48 +1,118 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="main" class="main">
|
<div ref="main" class="column no-wrap" style="min-height: 500px">
|
||||||
<div class="part">
|
<div v-if="mode != 'liberama.top'" class="relative-position">
|
||||||
<span class="greeting bold-font">{{ title }}</span>
|
<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">Добро пожаловать!</span>
|
||||||
<span class="greeting">Поддерживаются форматы: fb2, fb2.zip, html, txt</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>
|
||||||
<div class="part center">
|
|
||||||
<el-input ref="input" placeholder="URL книги" v-model="bookUrl">
|
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
||||||
<el-button slot="append" icon="el-icon-check" @click="submitUrl"></el-button>
|
<q-input
|
||||||
</el-input>
|
ref="input" v-model="bookUrl" class="full-width q-px-sm" style="max-width: 700px"
|
||||||
<div class="space"></div>
|
outlined dense bg-color="white" placeholder="Ссылка на книгу или веб-страницу" @keydown="onInputKeydown"
|
||||||
<input type="file" id="file" ref="file" @change="loadFile" style='display: none;'/>
|
>
|
||||||
<el-button size="mini" @click="loadFileClick">
|
<template #append>
|
||||||
|
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<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>
|
</q-btn>
|
||||||
<div class="space"></div>
|
|
||||||
<span v-if="config.mode == 'omnireader'" class="bottom-span clickable" @click="openComments">Комментарии</span>
|
<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" />
|
||||||
|
Из буфера обмена
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<div class="q-my-md"></div>
|
||||||
|
<!--div v-if="mode == 'omnireader'">
|
||||||
|
<div ref="yaShare2" class="ya-share2"
|
||||||
|
data-services="collections,vkontakte,facebook,odnoklassniki,twitter,telegram"
|
||||||
|
data-description="Чтение fb2-книг онлайн. Загрузка любой страницы интернета одним кликом, синхронизация между устройствами, удобное управление, регистрация не требуется."
|
||||||
|
data-title="Omni Reader - браузерная онлайн-читалка"
|
||||||
|
data-url="https://omnireader.ru">
|
||||||
</div>
|
</div>
|
||||||
<div class="part bottom">
|
</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="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="openHelp">Справка</span>
|
||||||
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
|
<!--span class="bottom-span clickable" @click="openDonate">Помочь проекту</span-->
|
||||||
<span class="bottom-span">{{ version }}</span>
|
|
||||||
|
<span v-if="version == clientVersion" class="bottom-span">v{{ version }}</span>
|
||||||
|
<span v-else class="bottom-span">Версия сервера {{ version }}, версия клиента {{ clientVersion }}, необходимо обновить страницу</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
const componentOptions = {
|
||||||
|
components: {
|
||||||
|
GithubCorner,
|
||||||
|
Dialog,
|
||||||
|
PasteTextPage,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
class LoaderPage {
|
||||||
|
_options = componentOptions;
|
||||||
|
|
||||||
export default @Component({
|
|
||||||
})
|
|
||||||
class LoaderPage extends Vue {
|
|
||||||
bookUrl = null;
|
bookUrl = null;
|
||||||
loadPercent = 0;
|
loadPercent = 0;
|
||||||
|
pasteTextActive = false;
|
||||||
|
findBookVisible = false;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
this.config = this.$store.state.config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.progress = this.$refs.progress;
|
this.progress = this.$refs.progress;
|
||||||
|
/*if (this.mode == 'omnireader')
|
||||||
|
Ya.share2(this.$refs.yaShare2);// eslint-disable-line no-undef*/
|
||||||
}
|
}
|
||||||
|
|
||||||
activated() {
|
activated() {
|
||||||
@@ -50,19 +120,37 @@ class LoaderPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
if (this.config.mode == 'omnireader')
|
if (this.mode == 'omnireader')
|
||||||
return 'Omni Reader - браузерная онлайн-читалка.';
|
return 'Omni Reader - браузерная онлайн-читалка.';
|
||||||
|
if (this.mode == 'liberama.top')
|
||||||
|
return 'Liberama Reader - браузерная онлайн-читалка.';
|
||||||
return 'Универсальная читалка книг и ресурсов интернета.';
|
return 'Универсальная читалка книг и ресурсов интернета.';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get mode() {
|
||||||
|
return this.$store.state.config.mode;
|
||||||
|
}
|
||||||
|
|
||||||
get version() {
|
get version() {
|
||||||
return `v${this.config.version}`;
|
return this.$store.state.config.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
get acceptFileExt() {
|
||||||
|
return this.$store.state.config.acceptFileExt;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isExternalConverter() {
|
||||||
|
return this.$store.state.config.useExternalBookConverter;
|
||||||
|
}
|
||||||
|
|
||||||
|
get clientVersion() {
|
||||||
|
return versionHistory[0].version;
|
||||||
}
|
}
|
||||||
|
|
||||||
submitUrl() {
|
submitUrl() {
|
||||||
if (this.bookUrl) {
|
if (this.bookUrl) {
|
||||||
this.$emit('load-book', {url: this.bookUrl});
|
this.$emit('load-book', {url: this.bookUrl, force: true});
|
||||||
this.bookUrl = '';
|
this.bookUrl = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,64 +166,75 @@ class LoaderPage extends Vue {
|
|||||||
this.$emit('load-file', {file});
|
this.$emit('load-file', {file});
|
||||||
}
|
}
|
||||||
|
|
||||||
openHelp() {
|
loadBufferClick() {
|
||||||
this.$emit('help-toggle');
|
this.showPasteText();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBuffer(opts) {
|
||||||
|
if (opts.buffer.length) {
|
||||||
|
const file = new File([opts.buffer], 'dummyName-PasteFromClipboard');
|
||||||
|
this.$emit('load-file', {file});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showPasteText() {
|
||||||
|
this.pasteTextActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pasteTextToggle() {
|
||||||
|
this.pasteTextActive = !this.pasteTextActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
openHelp(event) {
|
||||||
|
this.$emit('do-action', {action: 'help', event});
|
||||||
}
|
}
|
||||||
|
|
||||||
openDonate() {
|
openDonate() {
|
||||||
this.$emit('donate-toggle');
|
this.$emit('do-action', {action: 'donate'});
|
||||||
|
}
|
||||||
|
|
||||||
|
findBook() {
|
||||||
|
this.findBookVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
openComments() {
|
openComments() {
|
||||||
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
|
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
openOldVersion() {
|
||||||
//недостатки сторонних ui
|
window.open('http://old.omnireader.ru', '_blank');
|
||||||
const input = this.$refs.input.$refs.input;
|
}
|
||||||
if (document.activeElement === input && event.type == 'keydown' && event.code == 'Enter') {
|
|
||||||
|
async onInputKeydown(event) {
|
||||||
|
if (event.key == 'Enter') {
|
||||||
|
await utils.sleep(100);
|
||||||
this.submitUrl();
|
this.submitUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type == 'keydown' && (event.code == 'F1' || (document.activeElement !== input && event.code == 'KeyH'))) {
|
|
||||||
this.$emit('help-toggle');
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type == 'keydown' && (document.activeElement !== input && event.code == 'KeyQ')) {
|
keyHook(event) {
|
||||||
this.$emit('tool-bar-toggle');
|
if (this.$refs.dialog1.active)
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
if (this.pasteTextActive) {
|
||||||
|
return this.$refs.pasteTextPage.keyHook(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const input = this.$refs.input.getNativeElement();
|
||||||
|
if (event.type == 'keydown' && (document.activeElement === input || event.code == 'Enter') && event.code != 'Escape')
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(LoaderPage);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.part {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greeting {
|
.greeting {
|
||||||
font-size: 130%;
|
font-size: 120%;
|
||||||
line-height: 170%;
|
line-height: 160%;
|
||||||
}
|
|
||||||
|
|
||||||
.bold-font {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.clickable {
|
.clickable {
|
||||||
@@ -144,25 +243,8 @@ class LoaderPage extends Vue {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.center {
|
|
||||||
justify-content: flex-start;
|
|
||||||
padding: 0 10px 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-span {
|
.bottom-span {
|
||||||
font-size: 70%;
|
font-size: 70%;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-input {
|
|
||||||
max-width: 700px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.space {
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<Window @close="close">
|
||||||
|
<template #header>
|
||||||
|
<span style="position: relative; top: -3px">
|
||||||
|
Вставьте текст и нажмите
|
||||||
|
<span class="clickable text-primary" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
|
||||||
|
или F2
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<q-input v-model="bookTitle" class="q-px-sm" dense borderless placeholder="Введите название текста" />
|
||||||
|
<hr />
|
||||||
|
<textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
|
||||||
|
</Window>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import vueComponent from '../../../vueComponent.js';
|
||||||
|
|
||||||
|
import Window from '../../../share/Window.vue';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import * as utils from '../../../../share/utils';
|
||||||
|
|
||||||
|
const componentOptions = {
|
||||||
|
components: {
|
||||||
|
Window,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
class PasteTextPage {
|
||||||
|
_options = componentOptions;
|
||||||
|
|
||||||
|
bookTitle = '';
|
||||||
|
|
||||||
|
created() {
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$refs.textArea.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
getNonEmptyLine3words(text, count) {
|
||||||
|
let result = '';
|
||||||
|
const lines = text.split("\n");
|
||||||
|
let i = 0;
|
||||||
|
while (i < lines.length) {
|
||||||
|
if (lines[i].trim() != '') {
|
||||||
|
count--;
|
||||||
|
if (count <= 0) {
|
||||||
|
result = lines[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = result.trim().split(' ');
|
||||||
|
return result.slice(0, 3).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
calcTitle(event) {
|
||||||
|
if (this.bookTitle == '') {
|
||||||
|
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.calcTitle();
|
||||||
|
this.$emit('load-buffer', {buffer: `<buffer><fb2-title>${utils.escapeXml(this.bookTitle)}</fb2-title>${utils.escapeXml(this.$refs.textArea.value)}</buffer>`});
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.$emit('paste-text-toggle');
|
||||||
|
}
|
||||||
|
|
||||||
|
keyHook(event) {
|
||||||
|
if (event.type == 'keydown') {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'F2':
|
||||||
|
this.loadBuffer();
|
||||||
|
break;
|
||||||
|
case 'Escape':
|
||||||
|
this.close();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default vueComponent(PasteTextPage);
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.text {
|
||||||
|
flex: 1;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 10px 0 10px;
|
||||||
|
position: relative;
|
||||||
|
font-size: 120%;
|
||||||
|
min-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
color: blue;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,21 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-show="visible" class="main">
|
<div v-show="visible" class="column justify-center items-center" style="background-color: rgba(0, 0, 0, 0.8); z-index: 100;">
|
||||||
<div class="center">
|
<div class="column justify-start items-center" style="height: 250px">
|
||||||
<el-progress type="circle" :width="100" :stroke-width="6" color="#0F9900" :percentage="percentage"></el-progress>
|
<q-circular-progress
|
||||||
<p class="text">{{ text }}</p>
|
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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
import * as utils from '../../../share/utils';
|
||||||
|
|
||||||
const ruMessage = {
|
const ruMessage = {
|
||||||
'start': ' ',
|
'start': ' ',
|
||||||
'finish': ' ',
|
'finish': ' ',
|
||||||
'error': ' ',
|
'error': ' ',
|
||||||
|
'queue': 'очередь',
|
||||||
'download': 'скачивание',
|
'download': 'скачивание',
|
||||||
'decompress': 'распаковка',
|
'decompress': 'распаковка',
|
||||||
'convert': 'конвертирование',
|
'convert': 'конвертирование',
|
||||||
@@ -24,76 +42,59 @@ const ruMessage = {
|
|||||||
'upload': 'отправка',
|
'upload': 'отправка',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default @Component({
|
class ProgressPage {
|
||||||
})
|
|
||||||
class ProgressPage extends Vue {
|
|
||||||
text = '';
|
text = '';
|
||||||
totalSteps = 1;
|
totalSteps = 1;
|
||||||
step = 1;
|
step = 1;
|
||||||
progress = 0;
|
progress = 0;
|
||||||
visible = false;
|
visible = false;
|
||||||
|
iconStyle = '';
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
this.$el.style.width = this.$parent.$el.offsetWidth + 'px';
|
|
||||||
this.$el.style.height = this.$parent.$el.offsetHeight + 'px';
|
|
||||||
this.text = '';
|
this.text = '';
|
||||||
this.totalSteps = 1;
|
this.totalSteps = 1;
|
||||||
this.step = 1;
|
this.step = 1;
|
||||||
this.progress = 0;
|
this.progress = 0;
|
||||||
|
this.iconAngle = 0;
|
||||||
|
this.ani = false;
|
||||||
|
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
|
this.text = '';
|
||||||
|
this.iconAngle = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(state) {
|
setState(state) {
|
||||||
if (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.text = (ruMessage[state.state] ? ruMessage[state.state] : state.state);
|
||||||
|
}
|
||||||
|
}
|
||||||
this.step = (state.step ? state.step : this.step);
|
this.step = (state.step ? state.step : this.step);
|
||||||
this.totalSteps = (state.totalSteps > this.totalSteps ? state.totalSteps : this.totalSteps);
|
this.totalSteps = (state.totalSteps > this.totalSteps ? state.totalSteps : this.totalSteps);
|
||||||
this.progress = state.progress || 0;
|
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() {
|
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);
|
return Math.round(((this.step - 1)/this.totalSteps + this.progress/(100*this.totalSteps))*100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(ProgressPage);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
|
||||||
.main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
z-index: 100;
|
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
.center {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
color: white;
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
color: yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
<style>
|
|
||||||
.el-progress__text {
|
|
||||||
color: lightgreen;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
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>
|
||||||
968
client/components/Reader/RecentBooksPage/RecentBooksPage.vue
Normal file
@@ -0,0 +1,968 @@
|
|||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<template #buttons>
|
||||||
|
<div
|
||||||
|
v-show="needBookUpdateCount > 0"
|
||||||
|
class="row justify-center items-center"
|
||||||
|
:class="{'header-button-update': !showNeedBookUpdateOnly, 'header-button-update-pressed': showNeedBookUpdateOnly}"
|
||||||
|
@mousedown.stop @click="showNeedBookUpdateOnlyToggle"
|
||||||
|
>
|
||||||
|
<span style="font-size: 90%">{{ needBookUpdateCount }} обновлен{{ wordEnding(needBookUpdateCount, 3) }}</span>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
|
{{ (needBookUpdateCount ? 'Скрыть обновления' : 'Показать обновления') }}
|
||||||
|
</q-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="row justify-center items-center"
|
||||||
|
:class="{'header-button': !showArchive, 'header-button-pressed': showArchive}"
|
||||||
|
@mousedown.stop @click="showArchiveToggle"
|
||||||
|
>
|
||||||
|
<q-icon class="q-mr-xs" name="la la-archive" size="20px" />
|
||||||
|
<span style="font-size: 90%">Архив</span>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
|
{{ (showArchive ? 'Скрыть архивные' : 'Показать архивные') }}
|
||||||
|
</q-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<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-input
|
||||||
|
ref="input"
|
||||||
|
v-model="search"
|
||||||
|
class="q-ml-sm q-mt-xs"
|
||||||
|
outlined dense
|
||||||
|
style="width: 185px"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
ref="sortMethod"
|
||||||
|
v-model="sortMethod"
|
||||||
|
class="q-ml-sm 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-virtual-scroll
|
||||||
|
ref="virtualScroll"
|
||||||
|
v-slot="{ item, index }"
|
||||||
|
:items="tableData"
|
||||||
|
scroll-target="#vs-container"
|
||||||
|
virtual-scroll-item-size="80"
|
||||||
|
@virtual-scroll="onScroll"
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
<q-icon name="la la-code-branch" size="24px" style="color: green" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row-part column justify-center items-stretch" style="width: 80px">
|
||||||
|
<div class="col row justify-center items-center clickable" style="padding: 0 2px 0 2px" @click="loadBook(item, bothBucEnabled && item.needBookUpdate)">
|
||||||
|
<div v-show="isLoadedCover(item.coverPageUrl)" style="height: 80px" v-html="getCoverHtml(item.coverPageUrl)" />
|
||||||
|
<q-icon v-show="!isLoadedCover(item.coverPageUrl)" name="la la-book" size="40px" style="color: #dddddd" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-show="bothBucEnabled && item.needBookUpdate"
|
||||||
|
class="column justify-center"
|
||||||
|
style="position: absolute; background-color: rgba(255, 255, 255, 0.5); border-radius: 40px;"
|
||||||
|
>
|
||||||
|
<q-icon name="la la-sync" size="60px" style="color: blue" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row-part column items-stretch clickable break-word" @click="loadBook(item)">
|
||||||
|
<div
|
||||||
|
class="col" style="border: 1px solid #cccccc; border-bottom: 0; padding: 4px; line-height: 140%;"
|
||||||
|
:style="{ 'width': (380 - 40*(+item.inGroup)) + 'px' }"
|
||||||
|
>
|
||||||
|
<div class="text-green-10" style="font-size: 80%">
|
||||||
|
{{ item.desc.author }}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 75%">
|
||||||
|
{{ item.desc.title }}
|
||||||
|
</div>
|
||||||
|
<div v-show="bothBucEnabled && item.needBookUpdate" style="font-size: 75%; color: blue;">
|
||||||
|
Размер: {{ item.downloadSize }} → {{ item.bucSize }},
|
||||||
|
{{ item.bucSize - item.downloadSize > 0 ? '+' : '' }}{{ item.bucSize - item.downloadSize }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" style="font-size: 10px">
|
||||||
|
<div class="row justify-center items-center row-info-top" style="width: 60px">
|
||||||
|
{{ item.desc.textLen }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row items-center row-info-top" :style="`width: ${(260 - 40*(+item.inGroup))}px; padding: 1px`">
|
||||||
|
<div class="read-bar" :style="`width: ${100*item.readPart}%`"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-center items-center row-info-top" style="width: 59px">
|
||||||
|
{{ item.desc.perc }}
|
||||||
|
</div>
|
||||||
|
<div class="row-info-top" style="width: 1px">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" style="font-size: 10px" :style="{ 'width': (380 - 40*(+item.inGroup)) + 'px' }">
|
||||||
|
<div class="row justify-center items-center row-info-bottom" style="width: 30px">
|
||||||
|
{{ item.num }}
|
||||||
|
</div>
|
||||||
|
<div class="col row">
|
||||||
|
<div class="row justify-center items-center row-info-bottom time-info" style="width: 50%">
|
||||||
|
Загружен: {{ item.loadTime }}
|
||||||
|
</div>
|
||||||
|
<div class="row justify-center items-center row-info-bottom time-info" style="width: 50%">
|
||||||
|
Читался: {{ item.touchTime }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row-info-bottom" style="width: 1px">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="row-part column"
|
||||||
|
style="width: 90px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col column justify-center"
|
||||||
|
style="font-size: 75%; padding-left: 6px; border: 1px solid #cccccc; border-left: 0;"
|
||||||
|
>
|
||||||
|
<div style="margin: 25px 0 0 5px">
|
||||||
|
<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="del-button self-end row justify-center items-center clickable"
|
||||||
|
@click="handleDel(item.key)"
|
||||||
|
>
|
||||||
|
<q-icon class="la la-times" size="12px" />
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
|
{{ (showArchive ? 'Удалить окончательно' : 'Перенести в архив') }}
|
||||||
|
</q-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-show="showArchive"
|
||||||
|
class="restore-button self-start row justify-center items-center clickable"
|
||||||
|
@click="handleRestore(item.key)"
|
||||||
|
>
|
||||||
|
<q-icon class="la la-arrow-left" size="14px" />
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
|
Восстановить из архива
|
||||||
|
</q-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-show="bothBucEnabled && item.showCheckBuc"
|
||||||
|
class="buc-checkbox self-start"
|
||||||
|
>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="item.checkBuc"
|
||||||
|
size="xs"
|
||||||
|
style="position: relative; top: -3px; left: -3px;"
|
||||||
|
@update:model-value="checkBucChange(item)"
|
||||||
|
>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
|
Проверять обновления
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-virtual-scroll>
|
||||||
|
</div>
|
||||||
|
</Window>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
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';
|
||||||
|
import coversStorage from '../share/coversStorage';
|
||||||
|
|
||||||
|
const componentOptions = {
|
||||||
|
components: {
|
||||||
|
Window,
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
search() {
|
||||||
|
this.updateTableData();
|
||||||
|
},
|
||||||
|
sortMethod() {
|
||||||
|
this.updateTableData();
|
||||||
|
},
|
||||||
|
settings() {
|
||||||
|
this.loadSettings();
|
||||||
|
},
|
||||||
|
needBookUpdateCount() {
|
||||||
|
if (this.needBookUpdateCount == 0)
|
||||||
|
this.showNeedBookUpdateOnly = false;
|
||||||
|
|
||||||
|
this.$emit('update-count-changed', {needBookUpdateCount: this.needBookUpdateCount});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
class RecentBooksPage {
|
||||||
|
_options = componentOptions;
|
||||||
|
|
||||||
|
loading = false;
|
||||||
|
search = '';
|
||||||
|
tableData = [];
|
||||||
|
sortMethod = '';
|
||||||
|
showSameBook = false;
|
||||||
|
bucEnabled = false;
|
||||||
|
bucSizeDiff = 0;
|
||||||
|
bucSetOnNew = false;
|
||||||
|
needBookUpdateCount = 0;
|
||||||
|
|
||||||
|
showArchive = false;
|
||||||
|
showNeedBookUpdateOnly = false;
|
||||||
|
|
||||||
|
covers = {};
|
||||||
|
coversLoadFunc = {};
|
||||||
|
|
||||||
|
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.inited = true;
|
||||||
|
|
||||||
|
(async() => {
|
||||||
|
this.showBar();
|
||||||
|
await this.updateTableData();
|
||||||
|
await this.scrollToActiveBook();
|
||||||
|
//await this.scrollRefresh();
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSettings() {
|
||||||
|
const settings = this.settings;
|
||||||
|
this.showSameBook = settings.recentShowSameBook;
|
||||||
|
this.sortMethod = settings.recentSortMethod || 'loadTimeDesc';
|
||||||
|
this.bucEnabled = settings.bucEnabled;
|
||||||
|
this.bucSizeDiff = settings.bucSizeDiff;
|
||||||
|
this.bucSetOnNew = settings.bucSetOnNew;
|
||||||
|
}
|
||||||
|
|
||||||
|
get settings() {
|
||||||
|
return this.$store.state.reader.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
get bothBucEnabled() {
|
||||||
|
return this.$store.state.config.bucEnabled && this.bucEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTableData() {
|
||||||
|
if (!this.inited)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await this.lock.get();
|
||||||
|
try {
|
||||||
|
let result = [];
|
||||||
|
|
||||||
|
const sorted = bookManager.getSortedRecent();
|
||||||
|
const activeBook = bookManager.mostRecentBook();
|
||||||
|
|
||||||
|
//подготовка полей
|
||||||
|
for (const book of sorted) {
|
||||||
|
if ((!this.showArchive && book.deleted) || (this.showArchive && book.deleted != 1))
|
||||||
|
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({
|
||||||
|
key: book.key,
|
||||||
|
url: book.url,
|
||||||
|
path: book.path,
|
||||||
|
deleted: book.deleted,
|
||||||
|
|
||||||
|
touchTime,
|
||||||
|
loadTime,
|
||||||
|
desc: {
|
||||||
|
author,
|
||||||
|
title,
|
||||||
|
perc,
|
||||||
|
textLen,
|
||||||
|
},
|
||||||
|
readPart,
|
||||||
|
fullTitle: bt.fullTitle,
|
||||||
|
sameBookKey: book.sameBookKey,
|
||||||
|
active: (activeBook.key == book.key),
|
||||||
|
activeParent: false,
|
||||||
|
inGroup: false,
|
||||||
|
coverPageUrl: book.coverPageUrl,
|
||||||
|
|
||||||
|
showCheckBuc: !this.showArchive && utils.hasProp(book, 'downloadSize'),
|
||||||
|
checkBuc: !!book.checkBuc,
|
||||||
|
needBookUpdate: (
|
||||||
|
!this.showArchive
|
||||||
|
&& book.checkBuc
|
||||||
|
&& book.bucSize
|
||||||
|
&& utils.hasProp(book, 'downloadSize')
|
||||||
|
&& book.bucSize !== book.downloadSize
|
||||||
|
&& (book.bucSize - book.downloadSize >= this.bucSizeDiff)
|
||||||
|
),
|
||||||
|
bucSize: book.bucSize,
|
||||||
|
downloadSize: book.downloadSize,
|
||||||
|
|
||||||
|
//для сортировки
|
||||||
|
loadTimeRaw,
|
||||||
|
touchTimeRaw: book.touchTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//нумерация
|
||||||
|
result.sort((a, b) => b.loadTimeRaw - a.loadTimeRaw);
|
||||||
|
let num = 0;
|
||||||
|
for (let i = result.length - 1; i >= 0; i--) {
|
||||||
|
num++;
|
||||||
|
result[i].num = num;
|
||||||
|
}
|
||||||
|
|
||||||
|
//фильтрация
|
||||||
|
const search = this.search;
|
||||||
|
if (search) {
|
||||||
|
const lowerSearch = search.toLowerCase();
|
||||||
|
|
||||||
|
result = result.filter(item => {
|
||||||
|
return !search
|
||||||
|
|| item.touchTime.includes(search)
|
||||||
|
|| item.loadTime.includes(search)
|
||||||
|
|| item.desc.title.toLowerCase().includes(lowerSearch)
|
||||||
|
|| item.desc.author.toLowerCase().includes(lowerSearch)
|
||||||
|
;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//сортировка
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
//группировка
|
||||||
|
let nbuCount = 0;
|
||||||
|
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;
|
||||||
|
|
||||||
|
book.showCheckBuc = false;
|
||||||
|
book.needBookUpdate = false;
|
||||||
|
|
||||||
|
groups[book.sameBookKey].push(book);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newResult.push(book);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (book.needBookUpdate)
|
||||||
|
nbuCount++;
|
||||||
|
}
|
||||||
|
result = newResult;
|
||||||
|
this.needBookUpdateCount = nbuCount;
|
||||||
|
|
||||||
|
//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;
|
||||||
|
}
|
||||||
|
|
||||||
|
//showNeedBookUpdateOnly
|
||||||
|
if (this.showNeedBookUpdateOnly) {
|
||||||
|
result = result.filter(item => item.needBookUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
//другие стадии
|
||||||
|
//.....
|
||||||
|
|
||||||
|
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 || this.showNeedBookUpdateOnly ? `Найден${this.wordEnding(len, 2)}` : 'Всего')} ${len} файл${this.wordEnding(len)}${this.showArchive ? ' в архиве' : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadBook(fb2path, fullTitle) {
|
||||||
|
try {
|
||||||
|
await readerApi.checkCachedBook(fb2path);
|
||||||
|
|
||||||
|
const d = this.$refs.download;
|
||||||
|
d.href = fb2path;
|
||||||
|
try {
|
||||||
|
const fn = utils.makeValidFilename(fullTitle);
|
||||||
|
d.download = fn.substring(0, 100) + '.fb2';
|
||||||
|
} catch(e) {
|
||||||
|
d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
|
||||||
|
}
|
||||||
|
|
||||||
|
d.click();
|
||||||
|
} catch (e) {
|
||||||
|
let errMes = e.message;
|
||||||
|
if (errMes.indexOf('404') >= 0)
|
||||||
|
errMes = 'Файл не найден на сервере (возможно был удален как устаревший)';
|
||||||
|
this.$root.stdDialog.alert(errMes, 'Ошибка', {color: 'negative'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDel(key) {
|
||||||
|
if (!this.showArchive) {
|
||||||
|
await bookManager.delRecentBook({key});
|
||||||
|
this.$root.notify.info('Перенесено в архив');
|
||||||
|
} else {
|
||||||
|
if (await this.$root.stdDialog.confirm('Подтвердите удаление из архива:', ' ')) {
|
||||||
|
await bookManager.delRecentBook({key}, 2);
|
||||||
|
this.$root.notify.info('Удалено безвозвратно');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleRestore(key) {
|
||||||
|
await bookManager.restoreRecentBook({key});
|
||||||
|
this.$root.notify.info('Восстановлено из архива');
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadBook(item, force = false) {
|
||||||
|
if (item.deleted)
|
||||||
|
await this.handleRestore(item.key);
|
||||||
|
|
||||||
|
this.$emit('load-book', {url: item.url, path: item.path, force});
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
isUrl(url) {
|
||||||
|
if (url)
|
||||||
|
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() {
|
||||||
|
await this.$nextTick();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async scrollRefresh() {
|
||||||
|
this.lockScroll = true;
|
||||||
|
await utils.sleep(100);
|
||||||
|
try {
|
||||||
|
this.$refs.virtualScroll.refresh();
|
||||||
|
} 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'},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
showArchiveToggle() {
|
||||||
|
this.showArchive = !this.showArchive;
|
||||||
|
this.showNeedBookUpdateOnly = false;
|
||||||
|
|
||||||
|
this.updateTableData();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.$emit('recent-books-close');
|
||||||
|
}
|
||||||
|
|
||||||
|
keyHook(event) {
|
||||||
|
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeCoverHtml(data) {
|
||||||
|
return `<img src="${data}" style="height: 100%; width: 100%; object-fit: contain" />`;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadedCover(coverPageUrl) {
|
||||||
|
if (!coverPageUrl)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
let loadedCover = this.covers[coverPageUrl];
|
||||||
|
|
||||||
|
if (loadedCover == 'error')
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!loadedCover) {
|
||||||
|
(async() => {
|
||||||
|
if (this.coversLoadFunc[coverPageUrl])
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.coversLoadFunc[coverPageUrl] = (async() => {
|
||||||
|
//сначала заглянем в storage
|
||||||
|
let data = await coversStorage.getData(coverPageUrl);
|
||||||
|
if (data) {
|
||||||
|
this.covers[coverPageUrl] = this.makeCoverHtml(data);
|
||||||
|
} else {//иначе идем на сервер
|
||||||
|
try {
|
||||||
|
data = await readerApi.getUploadedFileBuf(coverPageUrl);
|
||||||
|
await coversStorage.setData(coverPageUrl, data);
|
||||||
|
this.covers[coverPageUrl] = this.makeCoverHtml(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.covers[coverPageUrl] = 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.coversLoadFunc[coverPageUrl]();
|
||||||
|
} finally {
|
||||||
|
this.coversLoadFunc[coverPageUrl] = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (loadedCover != undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCoverHtml(coverPageUrl) {
|
||||||
|
if (coverPageUrl && this.covers[coverPageUrl])
|
||||||
|
return this.covers[coverPageUrl];
|
||||||
|
else
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkBucChange(item) {
|
||||||
|
const book = await bookManager.getRecentBook(item);
|
||||||
|
if (book) {
|
||||||
|
await bookManager.setCheckBuc(book, item.checkBuc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showNeedBookUpdateOnlyToggle() {
|
||||||
|
this.showNeedBookUpdateOnly = !this.showNeedBookUpdateOnly;
|
||||||
|
this.showArchive = false;
|
||||||
|
|
||||||
|
this.updateTableData();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default vueComponent(RecentBooksPage);
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-part {
|
||||||
|
padding: 4px 0px 4px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.break-word {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-button {
|
||||||
|
min-width: 30px;
|
||||||
|
width: 30px;
|
||||||
|
min-height: 30px;
|
||||||
|
height: 30px;
|
||||||
|
margin: 10px 6px 0px 3px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-info-bottom {
|
||||||
|
line-height: 110%;
|
||||||
|
border-left: 1px solid #cccccc;
|
||||||
|
border-bottom: 1px solid #cccccc;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-info-top {
|
||||||
|
line-height: 110%;
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
border-right: 0;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-info, .row-info-top {
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-bar {
|
||||||
|
height: 6px;
|
||||||
|
background-color: #b8b8b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-button {
|
||||||
|
width: 25px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
border-left: 1px solid #cccccc;
|
||||||
|
border-bottom: 1px solid #cccccc;
|
||||||
|
border-radius: 0 0 0 10px;
|
||||||
|
margin: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-button:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: #FF3030;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restore-button {
|
||||||
|
width: 25px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
border-right: 1px solid #cccccc;
|
||||||
|
border-bottom: 1px solid #cccccc;
|
||||||
|
border-radius: 0 0 10px 0;
|
||||||
|
margin: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restore-button:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: #00bb00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-button, .header-button-pressed {
|
||||||
|
width: 80px;
|
||||||
|
height: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #555555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-button-update, .header-button-update-pressed {
|
||||||
|
width: 120px;
|
||||||
|
height: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-button:hover, .header-button-update:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: #39902F;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-button-pressed, .header-button-update-pressed {
|
||||||
|
color: black;
|
||||||
|
background-color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buc-checkbox {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="main" class="main" @click="close">
|
<Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
|
||||||
<div class="mainWindow" @click.stop>
|
<template #header>
|
||||||
<Window @close="close">
|
|
||||||
<template slot="header">
|
|
||||||
{{ header }}
|
{{ header }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -10,30 +8,36 @@
|
|||||||
<span v-show="initStep">{{ initPercentage }}%</span>
|
<span v-show="initStep">{{ initPercentage }}%</span>
|
||||||
|
|
||||||
<div v-show="!initStep" class="input">
|
<div v-show="!initStep" class="input">
|
||||||
<input ref="input" class="el-input__inner"
|
<q-input
|
||||||
placeholder="что ищем"
|
ref="input" v-model="needle"
|
||||||
:value="needle" @input="needle = $event.target.value"/>
|
class="col" outlined dense
|
||||||
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">{{ foundText }}</div>
|
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">
|
</div>
|
||||||
<el-button @click="showNext"><i class="el-icon-arrow-down"></i></el-button>
|
<q-btn-group v-show="!initStep" class="button-group row no-wrap">
|
||||||
<el-button @click="showPrev"><i class="el-icon-arrow-up"></i></el-button>
|
<q-btn class="button" dense stretch @click="showNext">
|
||||||
</el-button-group>
|
<q-icon style="top: -6px" name="la la-angle-down" dense size="22px" />
|
||||||
|
</q-btn>
|
||||||
|
<q-btn class="button" dense stretch @click="showPrev">
|
||||||
|
<q-icon style="top: -4px" class="icon" name="la la-angle-up" dense size="22px" />
|
||||||
|
</q-btn>
|
||||||
|
</q-btn-group>
|
||||||
</div>
|
</div>
|
||||||
</Window>
|
</Window>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
|
||||||
import Window from '../../share/Window.vue';
|
import Window from '../../share/Window.vue';
|
||||||
import {sleep} from '../../../share/utils';
|
import {sleep} from '../../../share/utils';
|
||||||
|
|
||||||
export default @Component({
|
const componentOptions = {
|
||||||
components: {
|
components: {
|
||||||
Window,
|
Window,
|
||||||
},
|
},
|
||||||
@@ -43,11 +47,16 @@ export default @Component({
|
|||||||
|
|
||||||
},
|
},
|
||||||
foundText: function(newValue) {
|
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;
|
header = null;
|
||||||
initStep = null;
|
initStep = null;
|
||||||
initPercentage = 0;
|
initPercentage = 0;
|
||||||
@@ -61,6 +70,8 @@ class SearchPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init(parsed) {
|
async init(parsed) {
|
||||||
|
this.$refs.window.init();
|
||||||
|
|
||||||
if (this.parsed != parsed) {
|
if (this.parsed != parsed) {
|
||||||
this.initStep = true;
|
this.initStep = true;
|
||||||
this.stopInit = false;
|
this.stopInit = false;
|
||||||
@@ -95,7 +106,7 @@ class SearchPage extends Vue {
|
|||||||
this.parsed = parsed;
|
this.parsed = parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.header = 'Найти';
|
this.header = 'Поиск в тексте';
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
this.$refs.input.focus();
|
this.$refs.input.focus();
|
||||||
this.$refs.input.select();
|
this.$refs.input.select();
|
||||||
@@ -159,51 +170,35 @@ class SearchPage extends Vue {
|
|||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.stopInit = true;
|
this.stopInit = true;
|
||||||
this.$emit('search-toggle');
|
this.$emit('do-action', {action: 'search'});
|
||||||
|
}
|
||||||
|
|
||||||
|
inputKeyDown(event) {
|
||||||
|
if (event.key == 'Enter') {
|
||||||
|
this.showNext();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
//недостатки сторонних ui
|
if (event.type == 'keydown' && event.key == 'Escape') {
|
||||||
if (document.activeElement === this.$refs.input && event.type == 'keydown' && event.key == 'Enter') {
|
|
||||||
this.showNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type == 'keydown' && (event.code == 'Escape')) {
|
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(SearchPage);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 40;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainWindow {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
height: 125px;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
top: -50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
min-width: 430px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
@@ -215,16 +210,14 @@ class SearchPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
width: 150px;
|
width: 100px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
height: 37px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-button {
|
.button {
|
||||||
padding: 9px 17px 9px 17px;
|
padding: 9px 17px 9px 17px;
|
||||||
}
|
width: 50px;
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
751
client/components/Reader/ServerStorage/ServerStorage.vue
Normal file
@@ -0,0 +1,751 @@
|
|||||||
|
<template>
|
||||||
|
<div class="hidden"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import vueComponent from '../../vueComponent.js';
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import bookManager from '../share/bookManager';
|
||||||
|
import readerApi from '../../../api/reader';
|
||||||
|
import * as utils from '../../../share/utils';
|
||||||
|
import * as cryptoUtils from '../../../share/cryptoUtils';
|
||||||
|
import LockQueue from '../../../share/LockQueue';
|
||||||
|
|
||||||
|
import localForage from 'localforage';
|
||||||
|
const ssCacheStore = localForage.createInstance({
|
||||||
|
name: 'ssCacheStore'
|
||||||
|
});
|
||||||
|
|
||||||
|
const componentOptions = {
|
||||||
|
watch: {
|
||||||
|
serverSyncEnabled: function() {
|
||||||
|
this.serverSyncEnabledChanged();
|
||||||
|
},
|
||||||
|
serverStorageKey: function() {
|
||||||
|
this.serverStorageKeyChanged(true);
|
||||||
|
},
|
||||||
|
settings: function() {
|
||||||
|
this.debouncedSaveSettings();
|
||||||
|
},
|
||||||
|
profiles: function() {
|
||||||
|
this.saveProfiles();
|
||||||
|
},
|
||||||
|
currentProfile: function() {
|
||||||
|
this.currentProfileChanged(true);
|
||||||
|
},
|
||||||
|
libs: function() {
|
||||||
|
this.debouncedSaveLibs();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
class ServerStorage {
|
||||||
|
_options = componentOptions;
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.inited = false;
|
||||||
|
this.keyInited = false;
|
||||||
|
this.commit = this.$store.commit;
|
||||||
|
this.prevServerStorageKey = null;
|
||||||
|
this.lock = new LockQueue(100);
|
||||||
|
|
||||||
|
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.cleanCachedRecent('cachedRecent');
|
||||||
|
|
||||||
|
this.cachedRecentPatch = await ssCacheStore.getItem('recent-patch');
|
||||||
|
if (!this.cachedRecentPatch)
|
||||||
|
await this.cleanCachedRecent('cachedRecentPatch');
|
||||||
|
|
||||||
|
this.cachedRecentMod = await ssCacheStore.getItem('recent-mod');
|
||||||
|
if (!this.cachedRecentMod)
|
||||||
|
await this.cleanCachedRecent('cachedRecentMod');
|
||||||
|
|
||||||
|
if (!this.serverStorageKey) {
|
||||||
|
//генерируем новый ключ
|
||||||
|
await this.generateNewServerStorageKey();
|
||||||
|
} else {
|
||||||
|
await this.serverStorageKeyChanged();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.inited = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCachedRecent(value) {
|
||||||
|
await ssCacheStore.setItem('recent', value);
|
||||||
|
this.cachedRecent = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCachedRecentPatch(value) {
|
||||||
|
await ssCacheStore.setItem('recent-patch', value);
|
||||||
|
this.cachedRecentPatch = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCachedRecentMod(value) {
|
||||||
|
await ssCacheStore.setItem('recent-mod', value);
|
||||||
|
this.cachedRecentMod = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanCachedRecent(whatToClean) {
|
||||||
|
if (whatToClean == 'cachedRecent' || whatToClean == 'all')
|
||||||
|
await this.setCachedRecent({rev: 0, data: {}});
|
||||||
|
if (whatToClean == 'cachedRecentPatch' || whatToClean == 'all')
|
||||||
|
await this.setCachedRecentPatch({rev: 0, data: {}});
|
||||||
|
if (whatToClean == 'cachedRecentMod' || whatToClean == 'all')
|
||||||
|
await this.setCachedRecentMod({rev: 0, data: {}});
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateNewServerStorageKey() {
|
||||||
|
const key = utils.toBase58(utils.randomArray(32));
|
||||||
|
this.commit('reader/setServerStorageKey', key);
|
||||||
|
await this.serverStorageKeyChanged(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async serverSyncEnabledChanged() {
|
||||||
|
if (this.serverSyncEnabled) {
|
||||||
|
this.prevServerStorageKey = null;
|
||||||
|
if (!this.serverStorageKey) {
|
||||||
|
//генерируем новый ключ
|
||||||
|
await this.generateNewServerStorageKey();
|
||||||
|
} else {
|
||||||
|
await this.serverStorageKeyChanged(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async serverStorageKeyChanged(force) {
|
||||||
|
if (this.prevServerStorageKey != this.serverStorageKey) {
|
||||||
|
this.prevServerStorageKey = this.serverStorageKey;
|
||||||
|
this.hashedStorageKey = utils.toBase58(cryptoUtils.sha256(this.serverStorageKey));
|
||||||
|
this.keyInited = true;
|
||||||
|
|
||||||
|
await this.loadProfiles(force);
|
||||||
|
this.checkCurrentProfile();
|
||||||
|
await this.currentProfileChanged(force);
|
||||||
|
await this.loadLibs(force);
|
||||||
|
|
||||||
|
if (force)
|
||||||
|
await this.cleanCachedRecent('all');
|
||||||
|
const loadSuccess = await this.loadRecent();
|
||||||
|
if (loadSuccess && force) {
|
||||||
|
await this.saveRecent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async currentProfileChanged(force) {
|
||||||
|
if (!this.currentProfile)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await this.loadSettings(force);
|
||||||
|
}
|
||||||
|
|
||||||
|
get serverSyncEnabled() {
|
||||||
|
return this.$store.state.reader.serverSyncEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
get settings() {
|
||||||
|
return this.$store.state.reader.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
get settingsRev() {
|
||||||
|
return this.$store.state.reader.settingsRev;
|
||||||
|
}
|
||||||
|
|
||||||
|
get serverStorageKey() {
|
||||||
|
return this.$store.state.reader.serverStorageKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
get profiles() {
|
||||||
|
return this.$store.state.reader.profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
get profilesRev() {
|
||||||
|
return this.$store.state.reader.profilesRev;
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentProfile() {
|
||||||
|
return this.$store.state.reader.currentProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showServerStorageMessages() {
|
||||||
|
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', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
success(message) {
|
||||||
|
if (this.showServerStorageMessages)
|
||||||
|
this.$root.notify.success(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
warning(message) {
|
||||||
|
if (this.showServerStorageMessages && !this.offlineModeActive)
|
||||||
|
this.$root.notify.warning(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (!this.keyInited || !this.serverSyncEnabled || !this.currentProfile)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const setsId = `settings-${this.currentProfile}`;
|
||||||
|
const oldRev = this.settingsRev[setsId] || 0;
|
||||||
|
//проверим ревизию на сервере
|
||||||
|
if (!force) {
|
||||||
|
try {
|
||||||
|
const revs = await this.storageCheck({[setsId]: {}});
|
||||||
|
if (revs.state == 'success' && revs.items[setsId].rev == oldRev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sets = null;
|
||||||
|
try {
|
||||||
|
sets = await this.storageGet({[setsId]: {}});
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sets.state == 'success') {
|
||||||
|
sets = sets.items[setsId];
|
||||||
|
|
||||||
|
if (sets.rev == 0)
|
||||||
|
sets.data = {};
|
||||||
|
|
||||||
|
this.oldSettings = _.cloneDeep(sets.data);
|
||||||
|
this.commit('reader/setSettings', sets.data);
|
||||||
|
this.commit('reader/setSettingsRev', {[setsId]: sets.rev});
|
||||||
|
|
||||||
|
if (doNotifySuccess)
|
||||||
|
this.debouncedNotifySuccess();
|
||||||
|
} else {
|
||||||
|
this.warning(`Неверный ответ сервера: ${sets.state}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSettings() {
|
||||||
|
if (!this.keyInited || !this.serverSyncEnabled || !this.currentProfile || this.savingSettings)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const diff = utils.getObjDiff(this.oldSettings, this.settings);
|
||||||
|
if (utils.isEmptyObjDiff(diff))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.savingSettings = true;
|
||||||
|
try {
|
||||||
|
const setsId = `settings-${this.currentProfile}`;
|
||||||
|
let result = {state: ''};
|
||||||
|
|
||||||
|
const oldRev = this.settingsRev[setsId] || 0;
|
||||||
|
try {
|
||||||
|
result = await this.storageSet({[setsId]: {rev: oldRev + 1, data: this.settings}});
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.state == 'reject') {
|
||||||
|
await this.loadSettings(true, false);
|
||||||
|
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||||
|
} else if (result.state == 'success') {
|
||||||
|
this.oldSettings = _.cloneDeep(this.settings);
|
||||||
|
this.commit('reader/setSettingsRev', {[setsId]: this.settingsRev[setsId] + 1});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.savingSettings = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadProfiles(force = false, doNotifySuccess = true) {
|
||||||
|
if (!this.keyInited || !this.serverSyncEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const oldRev = this.profilesRev;
|
||||||
|
//проверим ревизию на сервере
|
||||||
|
if (!force) {
|
||||||
|
try {
|
||||||
|
const revs = await this.storageCheck({profiles: {}});
|
||||||
|
if (revs.state == 'success' && revs.items.profiles.rev == oldRev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prof = null;
|
||||||
|
try {
|
||||||
|
prof = await this.storageGet({profiles: {}});
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prof.state == 'success') {
|
||||||
|
prof = prof.items.profiles;
|
||||||
|
|
||||||
|
if (prof.rev == 0)
|
||||||
|
prof.data = {};
|
||||||
|
|
||||||
|
this.oldProfiles = _.cloneDeep(prof.data);
|
||||||
|
this.commit('reader/setProfiles', prof.data);
|
||||||
|
this.commit('reader/setProfilesRev', prof.rev);
|
||||||
|
this.checkCurrentProfile();
|
||||||
|
|
||||||
|
if (doNotifySuccess)
|
||||||
|
this.debouncedNotifySuccess();
|
||||||
|
} else {
|
||||||
|
this.warning(`Неверный ответ сервера: ${prof.state}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveProfiles() {
|
||||||
|
if (!this.keyInited || !this.serverSyncEnabled || this.savingProfiles)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const diff = utils.getObjDiff(this.oldProfiles, this.profiles);
|
||||||
|
if (utils.isEmptyObjDiff(diff))
|
||||||
|
return;
|
||||||
|
|
||||||
|
//обнуляются профили во время разработки при hotReload, подстраховка
|
||||||
|
if (!this.$store.state.reader.allowProfilesSave) {
|
||||||
|
console.error('Сохранение профилей не санкционировано');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.savingProfiles = true;
|
||||||
|
try {
|
||||||
|
let result = {state: ''};
|
||||||
|
try {
|
||||||
|
result = await this.storageSet({profiles: {rev: this.profilesRev + 1, data: this.profiles}});
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.state == 'reject') {
|
||||||
|
await this.loadProfiles(true, false);
|
||||||
|
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||||
|
} else if (result.state == 'success') {
|
||||||
|
this.oldProfiles = _.cloneDeep(this.profiles);
|
||||||
|
this.commit('reader/setProfilesRev', this.profilesRev + 1);
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
this.loadingRecent = true;
|
||||||
|
try {
|
||||||
|
//проверим ревизию на сервере
|
||||||
|
let query = {recent: {}, recentPatch: {}, recentMod: {}};
|
||||||
|
let revs = null;
|
||||||
|
if (!skipRevCheck) {
|
||||||
|
try {
|
||||||
|
revs = await this.storageCheck(query);
|
||||||
|
if (revs.state == 'success') {
|
||||||
|
if (revs.items.recent.rev != this.cachedRecent.rev) {
|
||||||
|
//no changes
|
||||||
|
} else if (revs.items.recentPatch.rev != this.cachedRecentPatch.rev) {
|
||||||
|
query = {recentPatch: {}, recentMod: {}};
|
||||||
|
} else if (revs.items.recentMod.rev != this.cachedRecentMod.rev) {
|
||||||
|
query = {recentMod: {}};
|
||||||
|
} else
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let recent = null;
|
||||||
|
try {
|
||||||
|
recent = await this.storageGet(query);
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recent.state == 'success') {
|
||||||
|
let newRecent = recent.items.recent;
|
||||||
|
let newRecentPatch = recent.items.recentPatch;
|
||||||
|
let newRecentMod = recent.items.recentMod;
|
||||||
|
|
||||||
|
if (!newRecent) {
|
||||||
|
newRecent = _.cloneDeep(this.cachedRecent);
|
||||||
|
}
|
||||||
|
if (!newRecentPatch) {
|
||||||
|
newRecentPatch = _.cloneDeep(this.cachedRecentPatch);
|
||||||
|
}
|
||||||
|
if (!newRecentMod) {
|
||||||
|
newRecentMod = _.cloneDeep(this.cachedRecentMod);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newRecent.rev == 0) newRecent.data = {};
|
||||||
|
if (newRecentPatch.rev == 0) newRecentPatch.data = {};
|
||||||
|
if (newRecentMod.rev == 0) newRecentMod.data = {};
|
||||||
|
|
||||||
|
let result = Object.assign({}, newRecent.data, newRecentPatch.data);
|
||||||
|
|
||||||
|
const md = newRecentMod.data;
|
||||||
|
if (md.key && result[md.key])
|
||||||
|
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, {isAddChanged: true});
|
||||||
|
|
||||||
|
/*if (!bookManager.loaded) {
|
||||||
|
this.warning('Ожидание загрузки списка книг перед синхронизацией');
|
||||||
|
while (!bookManager.loaded) await utils.sleep(100);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
if (newRecent.rev != this.cachedRecent.rev)
|
||||||
|
await this.setCachedRecent(newRecent);
|
||||||
|
if (newRecentPatch.rev != this.cachedRecentPatch.rev)
|
||||||
|
await this.setCachedRecentPatch(newRecentPatch);
|
||||||
|
if (newRecentMod.rev != this.cachedRecentMod.rev)
|
||||||
|
await this.setCachedRecentMod(newRecentMod);
|
||||||
|
|
||||||
|
await bookManager.setRecent(result);
|
||||||
|
} else {
|
||||||
|
this.warning(`Неверный ответ сервера: ${recent.state}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doNotifySuccess)
|
||||||
|
this.debouncedNotifySuccess();
|
||||||
|
} finally {
|
||||||
|
this.loadingRecent = false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveRecent(itemKeys, recurse) {
|
||||||
|
while (!this.inited)
|
||||||
|
await utils.sleep(100);
|
||||||
|
|
||||||
|
if (!this.keyInited || !this.serverSyncEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let needRecurseCall = false;
|
||||||
|
|
||||||
|
await this.lock.get();
|
||||||
|
try {
|
||||||
|
const bm = bookManager;
|
||||||
|
|
||||||
|
let needSaveRecent = false;
|
||||||
|
let needSaveRecentPatch = false;
|
||||||
|
let needSaveRecentMod = false;
|
||||||
|
|
||||||
|
//newRecentMod
|
||||||
|
let newRecentMod = {};
|
||||||
|
let oneItemKey = null;
|
||||||
|
if (itemKeys && itemKeys.length == 1)
|
||||||
|
oneItemKey = itemKeys[0];
|
||||||
|
|
||||||
|
if (oneItemKey && this.cachedRecentPatch.data[oneItemKey] && this.prevItemKey == oneItemKey) {
|
||||||
|
newRecentMod = _.cloneDeep(this.cachedRecentMod);
|
||||||
|
newRecentMod.rev++;
|
||||||
|
|
||||||
|
newRecentMod.data.key = oneItemKey;
|
||||||
|
newRecentMod.data.mod = utils.getObjDiff(this.cachedRecentPatch.data[oneItemKey], bm.recent[oneItemKey]);
|
||||||
|
needSaveRecentMod = true;
|
||||||
|
}
|
||||||
|
this.prevItemKey = oneItemKey;
|
||||||
|
|
||||||
|
//newRecentPatch
|
||||||
|
let newRecentPatch = {};
|
||||||
|
if (itemKeys && !needSaveRecentMod) {
|
||||||
|
newRecentPatch = _.cloneDeep(this.cachedRecentPatch);
|
||||||
|
newRecentPatch.rev++;
|
||||||
|
|
||||||
|
for (const key of itemKeys) {
|
||||||
|
newRecentPatch.data[key] = _.cloneDeep(bm.recent[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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, {isAddChanged: true});
|
||||||
|
|
||||||
|
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
|
||||||
|
needSaveRecentPatch = true;
|
||||||
|
needSaveRecentMod = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//newRecent
|
||||||
|
let newRecent = {};
|
||||||
|
if (!itemKeys || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
|
||||||
|
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
|
||||||
|
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
|
||||||
|
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
|
||||||
|
needSaveRecent = true;
|
||||||
|
needSaveRecentPatch = true;
|
||||||
|
needSaveRecentMod = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//query
|
||||||
|
let query = {};
|
||||||
|
if (needSaveRecent) {
|
||||||
|
query = {recent: newRecent, recentPatch: newRecentPatch, recentMod: newRecentMod};
|
||||||
|
} else if (needSaveRecentPatch) {
|
||||||
|
query = {recentPatch: newRecentPatch, recentMod: newRecentMod};
|
||||||
|
} else {
|
||||||
|
query = {recentMod: newRecentMod};
|
||||||
|
}
|
||||||
|
|
||||||
|
//сохранение
|
||||||
|
let result = {state: ''};
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await this.storageSet(query);
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.state == 'reject') {
|
||||||
|
|
||||||
|
const res = await this.loadRecent(false, false);
|
||||||
|
|
||||||
|
if (res)
|
||||||
|
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||||
|
if (!recurse && itemKeys) {
|
||||||
|
needRecurseCall = true;
|
||||||
|
}
|
||||||
|
} else if (result.state == 'success') {
|
||||||
|
if (needSaveRecent && newRecent.rev)
|
||||||
|
await this.setCachedRecent(newRecent);
|
||||||
|
if (needSaveRecentPatch && newRecentPatch.rev)
|
||||||
|
await this.setCachedRecentPatch(newRecentPatch);
|
||||||
|
if (needSaveRecentMod && newRecentMod.rev)
|
||||||
|
await this.setCachedRecentMod(newRecentMod);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.lock.ret();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needRecurseCall)
|
||||||
|
await this.saveRecent(itemKeys, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async storageCheck(items) {
|
||||||
|
return await this.storageApi('check', items);
|
||||||
|
}
|
||||||
|
|
||||||
|
async storageGet(items) {
|
||||||
|
return await this.storageApi('get', items);
|
||||||
|
}
|
||||||
|
|
||||||
|
async storageSet(items, force) {
|
||||||
|
return await this.storageApi('set', items, force);
|
||||||
|
}
|
||||||
|
|
||||||
|
async storageApi(action, items, force) {
|
||||||
|
const request = {action, items};
|
||||||
|
if (force)
|
||||||
|
request.force = true;
|
||||||
|
const encodedRequest = await this.encodeStorageItems(request);
|
||||||
|
return await this.decodeStorageItems(await readerApi.storage(encodedRequest));
|
||||||
|
}
|
||||||
|
|
||||||
|
async encodeStorageItems(request) {
|
||||||
|
if (!this.hashedStorageKey)
|
||||||
|
throw new Error('hashedStorageKey is empty');
|
||||||
|
|
||||||
|
if (!_.isObject(request.items))
|
||||||
|
throw new Error('items is not an object');
|
||||||
|
|
||||||
|
let result = Object.assign({}, request);
|
||||||
|
let items = {};
|
||||||
|
for (const id of Object.keys(request.items)) {
|
||||||
|
const item = request.items[id];
|
||||||
|
if (request.action == 'set' && !_.isObject(item.data))
|
||||||
|
throw new Error('encodeStorageItems: data is not an object');
|
||||||
|
|
||||||
|
let encoded = Object.assign({}, item);
|
||||||
|
|
||||||
|
if (item.data) {
|
||||||
|
const comp = utils.pako.deflate(JSON.stringify(item.data), {level: 1});
|
||||||
|
let encrypted = null;
|
||||||
|
try {
|
||||||
|
encrypted = cryptoUtils.aesEncrypt(comp, this.serverStorageKey);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('encrypt failed');
|
||||||
|
}
|
||||||
|
encoded.data = '1' + utils.toBase64(encrypted);
|
||||||
|
}
|
||||||
|
items[`${this.hashedStorageKey}.${utils.toBase58(id)}`] = encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.items = items;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async decodeStorageItems(response) {
|
||||||
|
if (!this.hashedStorageKey)
|
||||||
|
throw new Error('hashedStorageKey is empty');
|
||||||
|
|
||||||
|
let result = Object.assign({}, response);
|
||||||
|
let items = {};
|
||||||
|
if (response.items) {
|
||||||
|
if (!_.isObject(response.items))
|
||||||
|
throw new Error('items is not an object');
|
||||||
|
|
||||||
|
for (const id of Object.keys(response.items)) {
|
||||||
|
const item = response.items[id];
|
||||||
|
let decoded = Object.assign({}, item);
|
||||||
|
if (item.data) {
|
||||||
|
if (!_.isString(item.data) || !item.data.length)
|
||||||
|
throw new Error('decodeStorageItems: data is not a string');
|
||||||
|
if (item.data[0] !== '1')
|
||||||
|
throw new Error('decodeStorageItems: unknown data format');
|
||||||
|
|
||||||
|
const a = utils.fromBase64(item.data.substr(1));
|
||||||
|
let decrypted = null;
|
||||||
|
try {
|
||||||
|
decrypted = cryptoUtils.aesDecrypt(a, this.serverStorageKey);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('decrypt failed');
|
||||||
|
}
|
||||||
|
decoded.data = JSON.parse(utils.pako.inflate(decrypted, {to: 'string'}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = id.split('.');
|
||||||
|
if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
|
||||||
|
throw new Error(`decodeStorageItems: bad id - ${id}`);
|
||||||
|
items[utils.fromBase58(ids[1]).toString()] = decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.items = items;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default vueComponent(ServerStorage);
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
@@ -1,51 +1,60 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="main" class="main" @click="close">
|
<Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
|
||||||
<div class="mainWindow" @click.stop>
|
<template #header>
|
||||||
<Window @close="close">
|
|
||||||
<template slot="header">
|
|
||||||
Установить позицию
|
Установить позицию
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="slider">
|
<div class="col column justify-center">
|
||||||
<el-slider v-model="sliderValue" :max="sliderMax" :format-tooltip="formatTooltip"></el-slider>
|
<div id="set-position-slider" class="slider q-px-md column justify-center">
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</Window>
|
</Window>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
import Window from '../../share/Window.vue';
|
import Window from '../../share/Window.vue';
|
||||||
|
|
||||||
export default @Component({
|
const componentOptions = {
|
||||||
components: {
|
components: {
|
||||||
Window,
|
Window,
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
sliderValue: function(newValue) {
|
sliderValue: function(newValue) {
|
||||||
|
if (this.initialized)
|
||||||
this.$emit('book-pos-changed', {bookPos: newValue});
|
this.$emit('book-pos-changed', {bookPos: newValue});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
};
|
||||||
class SetPositionPage extends Vue {
|
class SetPositionPage {
|
||||||
|
_options = componentOptions;
|
||||||
|
|
||||||
sliderValue = null;
|
sliderValue = null;
|
||||||
sliderMax = null;
|
sliderMax = null;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
this.reader = this.$store.state.reader;
|
this.reader = this.$store.state.reader;
|
||||||
|
this.initialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
formatTooltip(val) {
|
init(sliderValue, sliderMax) {
|
||||||
if (this.sliderMax)
|
this.$refs.window.init();
|
||||||
return (val/this.sliderMax*100).toFixed(2) + '%';
|
|
||||||
else
|
this.sliderMax = sliderMax;
|
||||||
return 0;
|
this.sliderValue = sliderValue;
|
||||||
|
this.initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
@@ -53,44 +62,34 @@ class SetPositionPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
if (event.type == 'keydown' && (event.code == 'Escape' || event.code == 'KeyP')) {
|
if (event.type == 'keydown') {
|
||||||
|
const action = this.$root.readerActionByKeyEvent(event);
|
||||||
|
if (event.key == 'Escape' || action == 'setPosition') {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(SetPositionPage);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 40;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainWindow {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 600px;
|
|
||||||
height: 140px;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
top: -50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
margin: 20px;
|
margin: 0 20px 0 20px;
|
||||||
|
height: 35px;
|
||||||
background-color: #efefef;
|
background-color: #efefef;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
.el-slider {
|
|
||||||
margin-right: 20px;
|
<style>
|
||||||
margin-left: 20px;
|
#set-position-slider .q-slider__thumb path {
|
||||||
}
|
fill: white !important;
|
||||||
|
stroke: blue !important;
|
||||||
|
stroke-width: 2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</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>
|
||||||
91
client/components/Reader/SettingsPage/OthersTab.inc
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="part-header">Подсказки, уведомления</div>
|
||||||
|
|
||||||
|
<div class="item row no-wrap">
|
||||||
|
<div class="label-6">Подсказка</div>
|
||||||
|
<q-checkbox size="xs" v-model="showClickMapPage" label="Показывать области управления кликом" :disable="!clickControl" >
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Показывать или нет подсказку при каждой загрузке книги
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-6">Подсказка</div>
|
||||||
|
<q-checkbox size="xs" v-model="blinkCachedLoad" label="Предупреждать о загрузке из кэша">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Мерцать сообщением в строке статуса и на кнопке<br>
|
||||||
|
обновления при загрузке книги из кэша
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row no-wrap">
|
||||||
|
<div class="label-6">Уведомление</div>
|
||||||
|
<q-checkbox size="xs" v-model="showServerStorageMessages" label="Показывать сообщения синхронизации">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Показывать уведомления и ошибки от<br>
|
||||||
|
синхронизатора данных с сервером
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-6">Уведомление</div>
|
||||||
|
<q-checkbox size="xs" v-model="showWhatsNewDialog">
|
||||||
|
Показывать уведомление "Что нового"
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Показывать уведомления "Что нового"<br>
|
||||||
|
при появлении новой версии читалки
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--div class="item row">
|
||||||
|
<div class="label-6">Уведомление</div>
|
||||||
|
<q-checkbox size="xs" v-model="showDonationDialog2020">
|
||||||
|
Показывать "Оплатим хостинг вместе"
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Показывать уведомление "Оплатим хостинг вместе"
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div-->
|
||||||
|
|
||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="part-header">Другое</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-6">Обработка</div>
|
||||||
|
<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>
|
||||||
50
client/components/Reader/SettingsPage/UpdateTab.inc
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="part-header">Обновление читалки</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="part-header">Обновление книг</div>
|
||||||
|
<div v-show="!configBucEnabled" class="item row">
|
||||||
|
<div class="label-6"></div>
|
||||||
|
<div>Сервер обновлений временно не работает</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="configBucEnabled" class="item row">
|
||||||
|
<div class="label-6"></div>
|
||||||
|
<q-checkbox size="xs" v-model="bucEnabled">
|
||||||
|
Проверять обновления книг
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="configBucEnabled && bucEnabled" class="item row">
|
||||||
|
<div class="label-6"></div>
|
||||||
|
<q-checkbox size="xs" v-model="bucSetOnNew">
|
||||||
|
Автопроверка для вновь загружаемых
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Автоматически устанавливать флаг проверки<br>
|
||||||
|
обновлений для всех вновь загружаемых книг
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="configBucEnabled && bucEnabled" class="item row">
|
||||||
|
<div class="label-6">Разница размеров</div>
|
||||||
|
<div class="col row">
|
||||||
|
<NumInput class="col-left" v-model="bucSizeDiff" />
|
||||||
|
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Уведомлять о наличии обновления книги в списке загруженных<br>
|
||||||
|
при указанной разнице в размерах старого и нового файлов.<br>
|
||||||
|
Разница указывается в байтах и может быть отрицательной.
|
||||||
|
</q-tooltip>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
121
client/components/Reader/SettingsPage/ViewTab/Color.inc
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<!---------------------------------------------->
|
||||||
|
<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>
|
||||||
|
<q-btn v-show="wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-file-download" @click.stop="downloadWallpaper">
|
||||||
|
<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 {
|
export default class DrawHelper {
|
||||||
fontBySize(size) {
|
fontBySize(size) {
|
||||||
return `${size}px ${this.fontName}`;
|
return `${size}px '${this.fontName}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
fontByStyle(style) {
|
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
|
measureText(text, style) {// eslint-disable-line no-unused-vars
|
||||||
@@ -19,24 +19,7 @@ export default class DrawHelper {
|
|||||||
return this.context.measureText(text).width;
|
return this.context.measureText(text).width;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawPage(lines, isScrolling) {
|
drawLine(line, lineIndex, baseLineIndex, sel, imageDrawn) {
|
||||||
if (!this.lastBook || this.pageLineCount < 1 || !this.book || !lines || !this.parsed.textLength)
|
|
||||||
return '';
|
|
||||||
|
|
||||||
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}px;` +
|
|
||||||
` position: absolute; top: ${this.fontSize*this.textShift}px; color: ${this.textColor}; font: ${font}; ${justify}` +
|
|
||||||
` line-height: ${this.lineHeight}px;">`;
|
|
||||||
|
|
||||||
let imageDrawn = 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:
|
/* line:
|
||||||
{
|
{
|
||||||
begin: Number,
|
begin: Number,
|
||||||
@@ -49,27 +32,8 @@ export default class DrawHelper {
|
|||||||
text: String,
|
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 out = '<div>';
|
||||||
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 lineText = '';
|
let lineText = '';
|
||||||
let center = false;
|
let center = false;
|
||||||
@@ -77,13 +41,19 @@ export default class DrawHelper {
|
|||||||
let j = 0;
|
let j = 0;
|
||||||
//формируем строку
|
//формируем строку
|
||||||
for (const part of line.parts) {
|
for (const part of line.parts) {
|
||||||
let tOpen = (part.style.bold ? '<b>' : '');
|
let tOpen = '';
|
||||||
|
tOpen += (part.style.bold ? '<b>' : '');
|
||||||
tOpen += (part.style.italic ? '<i>' : '');
|
tOpen += (part.style.italic ? '<i>' : '');
|
||||||
let tClose = (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>' : '');
|
tClose += (part.style.bold ? '</b>' : '');
|
||||||
|
|
||||||
let text = '';
|
let text = '';
|
||||||
if (i == 0 && this.searching) {
|
if (lineIndex == 0 && this.searching) {
|
||||||
for (let k = 0; k < part.text.length; k++) {
|
for (let k = 0; k < part.text.length; k++) {
|
||||||
text += (sel.has(j) ? `<ins>${part.text[k]}</ins>` : part.text[k]);
|
text += (sel.has(j) ? `<ins>${part.text[k]}</ins>` : part.text[k]);
|
||||||
j++;
|
j++;
|
||||||
@@ -91,32 +61,27 @@ export default class DrawHelper {
|
|||||||
} else
|
} else
|
||||||
text = part.text;
|
text = part.text;
|
||||||
|
|
||||||
if (text.trim() == '')
|
if (text && text.trim() == '')
|
||||||
text = `<span style="white-space: pre">${text}</span>`;
|
text = `<span style="white-space: pre">${text}</span>`;
|
||||||
|
|
||||||
lineText += `${tOpen}${text}${tClose}`;
|
lineText += `${tOpen}${text}${tClose}`;
|
||||||
|
|
||||||
center = center || part.style.center;
|
center = center || part.style.center;
|
||||||
space = (part.style.space > 0 ? part.style.space : space);
|
space = (part.style.space > space ? part.style.space : space);
|
||||||
|
|
||||||
//избражения
|
//избражения
|
||||||
//image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number},
|
//image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number, w: Number, h: Number},
|
||||||
const img = part.image;
|
const img = part.image;
|
||||||
if (img && img.id && !img.inline && !imageDrawn.has(img.paraIndex)) {
|
if (img && img.id && !img.inline && !imageDrawn.has(img.paraIndex)) {
|
||||||
const bin = this.parsed.binary[img.id];
|
const bin = this.parsed.binary[img.id];
|
||||||
if (bin) {
|
if (bin) {
|
||||||
let imgH = img.lineCount*this.lineHeight;
|
|
||||||
imgH = (imgH <= bin.h ? imgH : bin.h);
|
|
||||||
let imgW = bin.w;
|
|
||||||
|
|
||||||
let resize = '';
|
let resize = '';
|
||||||
if (bin.h > imgH) {
|
if (bin.h > img.h) {
|
||||||
resize = `height: ${imgH}px`;
|
resize = `height: ${img.h}px`;
|
||||||
imgW = imgW*imgH/bin.h;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const left = (this.w - imgW)/2;
|
const left = (this.w - img.w)/2;
|
||||||
const top = ((img.lineCount*this.lineHeight - imgH)/2) + (i - img.imageLine)*this.lineHeight;
|
const top = ((img.lineCount*this.lineHeight - img.h)/2) + (lineIndex - baseLineIndex - img.imageLine)*this.lineHeight;
|
||||||
if (img.local) {
|
if (img.local) {
|
||||||
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
|
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
|
||||||
} else {
|
} else {
|
||||||
@@ -152,19 +117,90 @@ export default class DrawHelper {
|
|||||||
if (line.last || center)
|
if (line.last || center)
|
||||||
lineText = `<span style="display: inline-block; ${centerStyle}">${lineText}</span>`;
|
lineText = `<span style="display: inline-block; ${centerStyle}">${lineText}</span>`;
|
||||||
|
|
||||||
out += (i > 0 ? '<br>' : '') + lineText;
|
out += lineText + '</div>';
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawPage(lines, isScrolling) {
|
||||||
|
if (!this.lastBook || this.pageLineCount < 1 || !this.book || !lines || !this.parsed.textLength)
|
||||||
|
return '';
|
||||||
|
|
||||||
|
const font = this.fontByStyle({});
|
||||||
|
const justify = (this.textAlignJustify ? 'text-align: justify; text-align-last: justify;' : '');
|
||||||
|
|
||||||
|
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 imageDrawn1 = new Set();
|
||||||
|
let imageDrawn2 = new Set();
|
||||||
|
let len = lines.length;
|
||||||
|
const lineCount = this.pageLineCount + (isScrolling ? 1 : 0);
|
||||||
|
len = (len > lineCount ? lineCount : len);
|
||||||
|
|
||||||
|
//поиск
|
||||||
|
let sel = new Set();
|
||||||
|
if (len > 0 && this.searching) {
|
||||||
|
const line = lines[0];
|
||||||
|
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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//отрисовка строк
|
||||||
|
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>';
|
||||||
|
|
||||||
|
//разделитель
|
||||||
|
out += `<div style="width: ${this.dualIndentLR*2}px;"></div>`;
|
||||||
|
|
||||||
|
//правая страница
|
||||||
|
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>';
|
out += '</div>';
|
||||||
return out;
|
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 pad = 3;
|
||||||
const fh = h - 2*pad;
|
const fh = h - 2*pad;
|
||||||
const fh2 = fh/2;
|
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 w1 = this.measureTextFont(t1, font) + fh2;
|
||||||
const read = (bookPos + 1)/textLength;
|
const read = (bookPos + 1)/textLength;
|
||||||
const t2 = `${(read*100).toFixed(2)}%`;
|
const t2 = `${(read*100).toFixed(2)}%`;
|
||||||
@@ -177,8 +213,8 @@ export default class DrawHelper {
|
|||||||
|
|
||||||
if (w1 + w2 + w3 <= w && w3 > (10 + fh2)) {
|
if (w1 + w2 + w3 <= w && w3 > (10 + fh2)) {
|
||||||
const barWidth = w - w1 - w2 - fh2;
|
const barWidth = w - w1 - w2 - fh2;
|
||||||
out += this.strokeRect(x + w1, y + pad, barWidth, fh - 2, 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.statusBarColor);
|
out += this.fillRect(x + w1 + 2, y + pad + 2, (barWidth - 4)*read, fh - 6, this.statusBarRgbaColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (w1 <= w)
|
if (w1 <= w)
|
||||||
@@ -187,16 +223,16 @@ export default class DrawHelper {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawStatusBar(statusBarTop, statusBarHeight, bookPos, textLength, title) {
|
drawStatusBar(statusBarTop, statusBarHeight, bookPos, textLength, title, imageNum, imageLength) {
|
||||||
|
|
||||||
let out = `<div class="layout" style="` +
|
let out = `<div class="layout" style="` +
|
||||||
`width: ${this.realWidth}px; height: ${statusBarHeight}px; ` +
|
`width: ${this.realWidth}px; height: ${statusBarHeight}px; ` +
|
||||||
`color: ${this.statusBarColor}">`;
|
`color: ${this.statusBarRgbaColor}">`;
|
||||||
|
|
||||||
const fontSize = statusBarHeight*0.75;
|
const fontSize = statusBarHeight*0.75;
|
||||||
const font = 'bold ' + this.fontBySize(fontSize);
|
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 date = new Date();
|
||||||
const time = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
const time = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||||||
@@ -205,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.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>';
|
out += '</div>';
|
||||||
return out;
|
return out;
|
||||||
@@ -272,7 +308,7 @@ export default class DrawHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async doPageAnimationRightShift(page1, page2, duration, isDown, animation1Finish) {
|
async doPageAnimationRightShift(page1, page2, duration, isDown, animation1Finish) {
|
||||||
const s = this.w + this.fontSize;
|
const s = this.boxW + this.fontSize;
|
||||||
|
|
||||||
if (isDown) {
|
if (isDown) {
|
||||||
page1.style.transform = `translateX(${s}px)`;
|
page1.style.transform = `translateX(${s}px)`;
|
||||||
@@ -322,4 +358,56 @@ export default class DrawHelper {
|
|||||||
await animation1Finish(duration);
|
await animation1Finish(duration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async doPageAnimationRotate(page1, page2, duration, isDown, animation1Finish, animation2Finish) {
|
||||||
|
if (isDown) {
|
||||||
|
page1.style.transform = `rotateY(90deg)`;
|
||||||
|
await sleep(30);
|
||||||
|
|
||||||
|
page2.style.transition = `${duration/2}ms ease-in`;
|
||||||
|
page2.style.transform = `rotateY(-90deg)`;
|
||||||
|
|
||||||
|
await animation2Finish(duration/2);
|
||||||
|
|
||||||
|
page1.style.transition = `${duration/2}ms ease-out`;
|
||||||
|
page1.style.transform = `rotateY(0deg)`;
|
||||||
|
await animation1Finish(duration/2);
|
||||||
|
} else {
|
||||||
|
page1.style.transform = `rotateY(-90deg)`;
|
||||||
|
await sleep(30);
|
||||||
|
|
||||||
|
page2.style.transition = `${duration/2}ms ease-in`;
|
||||||
|
page2.style.transform = `rotateY(90deg)`;
|
||||||
|
|
||||||
|
await animation2Finish(duration/2);
|
||||||
|
|
||||||
|
page1.style.transition = `${duration/2}ms ease-out`;
|
||||||
|
page1.style.transform = `rotateY(0deg)`;
|
||||||
|
await animation1Finish(duration/2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async doPageAnimationFlip(page1, page2, duration, isDown, animation1Finish, animation2Finish, backgroundColor) {
|
||||||
|
page2.style.background = backgroundColor;
|
||||||
|
|
||||||
|
if (isDown) {
|
||||||
|
page2.style.transformOrigin = '5%';
|
||||||
|
await sleep(30);
|
||||||
|
|
||||||
|
page2.style.transition = `${duration}ms ease-in-out`;
|
||||||
|
page2.style.transform = `rotateY(-120deg) translateX(${this.w/4}px)`;
|
||||||
|
await animation2Finish(duration);
|
||||||
|
} else {
|
||||||
|
page2.style.transformOrigin = '95%';
|
||||||
|
await sleep(30);
|
||||||
|
|
||||||
|
page2.style.transition = `${duration}ms ease-in-out`;
|
||||||
|
page2.style.transform = `rotateY(120deg) translateX(-${this.w/4}px)`;
|
||||||
|
await animation2Finish(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
page2.style.transformOrigin = 'center';
|
||||||
|
page2.style.background = '';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
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,24 +1,58 @@
|
|||||||
import he from 'he';
|
import he from 'he';
|
||||||
import sax from '../../../../server/core/BookConverter/sax';
|
import sax from '../../../../server/core/sax';
|
||||||
import {sleep} from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
|
|
||||||
const maxImageLineCount = 100;
|
const maxImageLineCount = 100;
|
||||||
|
const maxParaLength = 10000;
|
||||||
|
const maxParaTextLength = 10000;
|
||||||
|
|
||||||
|
// 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 {
|
export default class BookParser {
|
||||||
constructor(settings) {
|
constructor(settings = {}) {
|
||||||
if (settings) {
|
this.sets = {};
|
||||||
this.showInlineImagesInCenter = settings.showInlineImagesInCenter;
|
|
||||||
|
this.setSettings(defaultSettings);
|
||||||
|
this.setSettings(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaults
|
setSettings(settings = {}) {
|
||||||
this.p = 30;// px, отступ параграфа
|
this.sets = Object.assign({}, this.sets, settings);
|
||||||
this.w = 300;// px, ширина страницы
|
this.measureText = this.sets.measureText;
|
||||||
this.wordWrap = false;// перенос по слогам
|
|
||||||
|
|
||||||
//заглушка
|
|
||||||
this.measureText = (text, style) => {// eslint-disable-line no-unused-vars
|
|
||||||
return text.length*20;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async parse(data, callback) {
|
async parse(data, callback) {
|
||||||
@@ -32,9 +66,6 @@ export default class BookParser {
|
|||||||
|
|
||||||
//defaults
|
//defaults
|
||||||
let fb2 = {
|
let fb2 = {
|
||||||
firstName: '',
|
|
||||||
middleName: '',
|
|
||||||
lastName: '',
|
|
||||||
bookTitle: '',
|
bookTitle: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,11 +76,27 @@ export default class BookParser {
|
|||||||
let italic = false;
|
let italic = false;
|
||||||
let space = 0;
|
let space = 0;
|
||||||
let inPara = false;
|
let inPara = false;
|
||||||
|
let isFirstBody = true;
|
||||||
|
let isFirstSection = true;
|
||||||
|
let isFirstTitlePara = false;
|
||||||
|
|
||||||
|
//изображения
|
||||||
this.binary = {};
|
this.binary = {};
|
||||||
let binaryId = '';
|
let binaryId = '';
|
||||||
let binaryType = '';
|
let binaryType = '';
|
||||||
let dimPromises = [];
|
let dimPromises = [];
|
||||||
|
this.coverPageId = '';
|
||||||
|
|
||||||
|
//оглавление
|
||||||
|
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 paraIndex = -1;
|
||||||
let paraOffset = 0;
|
let paraOffset = 0;
|
||||||
@@ -59,12 +106,12 @@ export default class BookParser {
|
|||||||
offset: Number, //сумма всех length до этого параграфа
|
offset: Number, //сумма всех length до этого параграфа
|
||||||
length: Number, //длина text без тегов
|
length: Number, //длина text без тегов
|
||||||
text: String, //текст параграфа с вложенными тегами
|
text: String, //текст параграфа с вложенными тегами
|
||||||
cut: Boolean, //параграф - кандидат на сокрытие (cutEmptyParagraphs)
|
|
||||||
addIndex: Number, //индекс добавляемого пустого параграфа (addEmptyParagraphs)
|
addIndex: Number, //индекс добавляемого пустого параграфа (addEmptyParagraphs)
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
const getImageDimensions = (binaryId, binaryType, data) => {
|
const getImageDimensions = (binaryId, binaryType, data) => {
|
||||||
return new Promise (async(resolve, reject) => {
|
return new Promise ((resolve, reject) => { (async() => {
|
||||||
|
data = data.replace(/[\n\r\s]/g, '');
|
||||||
const i = new Image();
|
const i = new Image();
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
i.onload = () => {
|
i.onload = () => {
|
||||||
@@ -78,19 +125,17 @@ export default class BookParser {
|
|||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
i.onerror = (e) => {
|
i.onerror = reject;
|
||||||
reject(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
i.src = `data:${binaryType};base64,${data}`;
|
i.src = `data:${binaryType};base64,${data}`;
|
||||||
await sleep(30*1000);
|
await utils.sleep(30*1000);
|
||||||
if (!resolved)
|
if (!resolved)
|
||||||
reject('Не удалось получить размер изображения');
|
reject('Не удалось получить размер изображения');
|
||||||
});
|
})().catch(reject); });
|
||||||
};
|
};
|
||||||
|
|
||||||
const getExternalImageDimensions = (src) => {
|
const getExternalImageDimensions = (src) => {
|
||||||
return new Promise (async(resolve, reject) => {
|
return new Promise ((resolve, reject) => { (async() => {
|
||||||
const i = new Image();
|
const i = new Image();
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
i.onload = () => {
|
i.onload = () => {
|
||||||
@@ -102,68 +147,126 @@ export default class BookParser {
|
|||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
i.onerror = (e) => {
|
i.onerror = reject;
|
||||||
reject(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
i.src = src;
|
i.src = src;
|
||||||
await sleep(30*1000);
|
await utils.sleep(30*1000);
|
||||||
if (!resolved)
|
if (!resolved)
|
||||||
reject('Не удалось получить размер изображения');
|
reject('Не удалось получить размер изображения');
|
||||||
});
|
})().catch(reject); });
|
||||||
};
|
};
|
||||||
|
|
||||||
const newParagraph = (text, len, addIndex) => {
|
const correctCurrentPara = () => {
|
||||||
paraIndex++;
|
//коррекция текущего параграфа
|
||||||
let p = {
|
if (paraIndex >= 0) {
|
||||||
index: paraIndex,
|
const prevParaIndex = paraIndex;
|
||||||
offset: paraOffset,
|
let p = para[paraIndex];
|
||||||
length: len,
|
paraOffset -= p.length;
|
||||||
text: text,
|
|
||||||
cut: (!addIndex && (len == 1 && text[0] == ' ')),
|
|
||||||
addIndex: (addIndex ? addIndex : 0),
|
|
||||||
};
|
|
||||||
|
|
||||||
para[paraIndex] = p;
|
//уберем пробелы с концов параграфа, минимум 1 пробел должен быть у пустого параграфа
|
||||||
paraOffset += p.length;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
const growParagraph = (text, len) => {
|
//удаление параграфов, которые содержат только разметку, такого не должно быть
|
||||||
if (paraIndex < 0) {
|
if (!p.length) {
|
||||||
newParagraph(' ', 1);
|
delete para[paraIndex];
|
||||||
growParagraph(text, len);
|
paraIndex--;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let p = para[paraIndex];
|
//добавление пустых (не)видимых (addEmptyParagraphs) параграфов перед текущим непустым
|
||||||
//добавление пустых (addEmptyParagraphs) параграфов
|
if (p.text.trim() != '') {
|
||||||
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
|
|
||||||
paraIndex--;
|
|
||||||
paraOffset -= p.length;
|
|
||||||
for (let i = 0; i < 2; i++) {
|
for (let i = 0; i < 2; i++) {
|
||||||
newParagraph(' ', 1, i + 1);
|
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.index = paraIndex;
|
||||||
p.offset = paraOffset;
|
p.offset = paraOffset;
|
||||||
para[paraIndex] = p;
|
para[paraIndex] = p;
|
||||||
paraOffset += p.length;
|
paraOffset += p.length;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
paraOffset -= p.length;
|
const newParagraph = (text = '', len = 0) => {
|
||||||
//параграф оказался непустой
|
correctCurrentPara();
|
||||||
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
|
|
||||||
p.length = 0;
|
//новый параграф
|
||||||
p.text = p.text.substr(1);
|
paraIndex++;
|
||||||
p.cut = (len == 1 && text[0] == ' ');
|
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, textRaw) => {
|
||||||
|
//начальный параграф
|
||||||
|
if (paraIndex < 0) {
|
||||||
|
newParagraph();
|
||||||
|
growParagraph(text, len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//ограничение на размер куска текста в параграфе
|
||||||
|
if (textRaw && textRaw.length > maxParaTextLength) {
|
||||||
|
while (textRaw.length > 0) {
|
||||||
|
const textPart = textRaw.substring(0, maxParaTextLength);
|
||||||
|
textRaw = textRaw.substring(maxParaTextLength);
|
||||||
|
|
||||||
|
newParagraph();
|
||||||
|
growParagraph(textPart, textPart.length);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inSubtitle) {
|
||||||
|
curSubtitle.title += text;
|
||||||
|
} else if (inTitle) {
|
||||||
|
curTitle.title += text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = para[paraIndex];
|
||||||
|
|
||||||
|
//ограничение на размер параграфа
|
||||||
|
if (p.length > maxParaLength) {
|
||||||
|
newParagraph();
|
||||||
|
growParagraph(text, len);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
p.length += len;
|
p.length += len;
|
||||||
p.text += text;
|
p.text += text;
|
||||||
|
paraOffset += len;
|
||||||
para[paraIndex] = p;
|
|
||||||
paraOffset += p.length;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onStartNode = (elemName, tail) => {// eslint-disable-line no-unused-vars
|
const onStartNode = (elemName, tail) => {// eslint-disable-line no-unused-vars
|
||||||
@@ -171,11 +274,12 @@ export default class BookParser {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
tag = elemName;
|
tag = elemName;
|
||||||
path += '/' + elemName;
|
path += '/' + tag;
|
||||||
|
|
||||||
if (tag == 'binary') {
|
if (tag == 'binary') {
|
||||||
let attrs = sax.getAttrsSync(tail);
|
let attrs = sax.getAttrsSync(tail);
|
||||||
binaryType = (attrs['content-type'] && attrs['content-type'].value ? attrs['content-type'].value : '');
|
binaryType = (attrs['content-type'] && attrs['content-type'].value ? attrs['content-type'].value : '');
|
||||||
|
binaryType = (binaryType == 'image/jpg' || binaryType == 'application/octet-stream' ? 'image/jpeg' : binaryType);
|
||||||
if (binaryType == 'image/jpeg' || binaryType == 'image/png')
|
if (binaryType == 'image/jpeg' || binaryType == 'image/png')
|
||||||
binaryId = (attrs.id.value ? attrs.id.value : '');
|
binaryId = (attrs.id.value ? attrs.id.value : '');
|
||||||
}
|
}
|
||||||
@@ -184,57 +288,150 @@ export default class BookParser {
|
|||||||
let attrs = sax.getAttrsSync(tail);
|
let attrs = sax.getAttrsSync(tail);
|
||||||
if (attrs.href && attrs.href.value) {
|
if (attrs.href && attrs.href.value) {
|
||||||
const href = attrs.href.value;
|
const href = attrs.href.value;
|
||||||
if (href[0] == '#') {//local
|
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
|
||||||
if (inPara && !this.showInlineImagesInCenter)
|
const {id, local} = this.imageHrefToId(href);
|
||||||
growParagraph(`<image-inline href="${href}"></image-inline>`, 0);
|
if (local) {//local
|
||||||
|
imageNum++;
|
||||||
|
|
||||||
|
if (inPara && !this.sets.showInlineImagesInCenter && !center)
|
||||||
|
growParagraph(`<image-inline href="${href}" num="${imageNum}"></image-inline>`, 0);
|
||||||
else
|
else
|
||||||
newParagraph(`<image href="${href}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
||||||
if (inPara && this.showInlineImagesInCenter)
|
|
||||||
newParagraph(' ', 1);
|
this.images.push({paraIndex, num: imageNum, id, local, alt});
|
||||||
} else {//external
|
|
||||||
dimPromises.push(getExternalImageDimensions(href));
|
if (inPara && this.sets.showInlineImagesInCenter)
|
||||||
newParagraph(`<image href="${href}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
newParagraph();
|
||||||
|
|
||||||
|
//coverpage
|
||||||
|
if (path == '/fictionbook/description/title-info/coverpage/image') {
|
||||||
|
this.coverPageId = id;
|
||||||
}
|
}
|
||||||
|
} else {//external
|
||||||
|
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 (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 (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();
|
||||||
|
isFirstBody = false;
|
||||||
|
bodyIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
if (tag == 'title') {
|
if (tag == 'title') {
|
||||||
newParagraph(' ', 1);
|
newParagraph();
|
||||||
|
isFirstTitlePara = true;
|
||||||
bold = true;
|
bold = true;
|
||||||
center = true;
|
center = true;
|
||||||
|
|
||||||
|
inTitle = true;
|
||||||
|
curTitle = {paraIndex, title: '', inset: sectionLevel, bodyIndex, subtitles: []};
|
||||||
|
this.contents.push(curTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'section') {
|
if (tag == 'section') {
|
||||||
newParagraph(' ', 1);
|
if (!isFirstSection)
|
||||||
|
newParagraph();
|
||||||
|
isFirstSection = false;
|
||||||
|
sectionLevel++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'emphasis' || tag == 'strong') {
|
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
|
||||||
growParagraph(`<${tag}>`, 0);
|
growParagraph(`<${tag}>`, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((tag == 'p' || tag == 'empty-line' || tag == 'v')) {
|
if ((tag == 'p' || tag == 'empty-line' || tag == 'v')) {
|
||||||
newParagraph(' ', 1);
|
if (!(tag == 'p' && isFirstTitlePara))
|
||||||
if (tag == 'p')
|
newParagraph();
|
||||||
|
if (tag == 'p') {
|
||||||
inPara = true;
|
inPara = true;
|
||||||
|
isFirstTitlePara = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'subtitle') {
|
if (tag == 'subtitle') {
|
||||||
newParagraph(' ', 1);
|
newParagraph();
|
||||||
|
isFirstTitlePara = true;
|
||||||
bold = true;
|
bold = true;
|
||||||
|
center = true;
|
||||||
|
|
||||||
|
if (curTitle.paraIndex < 0) {
|
||||||
|
curTitle = {paraIndex, title: 'Оглавление', inset: sectionLevel, bodyIndex, subtitles: []};
|
||||||
|
this.contents.push(curTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'epigraph') {
|
inSubtitle = true;
|
||||||
|
curSubtitle = {paraIndex, inset: sectionLevel, title: ''};
|
||||||
|
curTitle.subtitles.push(curSubtitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag == 'epigraph' || tag == 'annotation') {
|
||||||
italic = true;
|
italic = true;
|
||||||
space += 1;
|
space += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'poem') {
|
if (tag == 'poem') {
|
||||||
newParagraph(' ', 1);
|
newParagraph();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'text-author') {
|
if (tag == 'text-author') {
|
||||||
newParagraph(' ', 1);
|
newParagraph();
|
||||||
|
bold = true;
|
||||||
space += 1;
|
space += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,11 +445,17 @@ export default class BookParser {
|
|||||||
|
|
||||||
if (path.indexOf('/fictionbook/body') == 0) {
|
if (path.indexOf('/fictionbook/body') == 0) {
|
||||||
if (tag == 'title') {
|
if (tag == 'title') {
|
||||||
|
isFirstTitlePara = false;
|
||||||
bold = false;
|
bold = false;
|
||||||
center = 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);
|
growParagraph(`</${tag}>`, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,19 +464,24 @@ export default class BookParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'subtitle') {
|
if (tag == 'subtitle') {
|
||||||
|
isFirstTitlePara = false;
|
||||||
bold = false;
|
bold = false;
|
||||||
|
center = false;
|
||||||
|
inSubtitle = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'epigraph') {
|
if (tag == 'epigraph' || tag == 'annotation') {
|
||||||
italic = false;
|
italic = false;
|
||||||
space -= 1;
|
space -= 1;
|
||||||
|
newParagraph();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'stanza') {
|
if (tag == 'stanza') {
|
||||||
newParagraph(' ', 1);
|
newParagraph();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'text-author') {
|
if (tag == 'text-author') {
|
||||||
|
bold = false;
|
||||||
space -= 1;
|
space -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -290,26 +498,27 @@ export default class BookParser {
|
|||||||
|
|
||||||
const onTextNode = (text) => {// eslint-disable-line no-unused-vars
|
const onTextNode = (text) => {// eslint-disable-line no-unused-vars
|
||||||
text = he.decode(text);
|
text = he.decode(text);
|
||||||
text = text.replace(/>/g, '>');
|
text = text.replace(/>/g, '>').replace(/</g, '<').replace(/[\t\n\r\xa0]/g, ' ');
|
||||||
text = text.replace(/</g, '<');
|
|
||||||
|
|
||||||
if (text != ' ' && text.trim() == '')
|
if (text && text.trim() == '')
|
||||||
text = text.trim();
|
text = ' ';
|
||||||
|
|
||||||
if (text == '')
|
if (!text)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
text = text.replace(/[\t\n\r]/g, ' ');
|
const authorLength = (fb2.author && fb2.author.length ? fb2.author.length : 0);
|
||||||
|
|
||||||
switch (path) {
|
switch (path) {
|
||||||
case '/fictionbook/description/title-info/author/first-name':
|
case '/fictionbook/description/title-info/author/first-name':
|
||||||
fb2.firstName = text;
|
if (authorLength)
|
||||||
|
fb2.author[authorLength - 1].firstName = text;
|
||||||
break;
|
break;
|
||||||
case '/fictionbook/description/title-info/author/middle-name':
|
case '/fictionbook/description/title-info/author/middle-name':
|
||||||
fb2.middleName = text;
|
if (authorLength)
|
||||||
|
fb2.author[authorLength - 1].middleName = text;
|
||||||
break;
|
break;
|
||||||
case '/fictionbook/description/title-info/author/last-name':
|
case '/fictionbook/description/title-info/author/last-name':
|
||||||
fb2.lastName = text;
|
if (authorLength)
|
||||||
|
fb2.author[authorLength - 1].lastName = text;
|
||||||
break;
|
break;
|
||||||
case '/fictionbook/description/title-info/genre':
|
case '/fictionbook/description/title-info/genre':
|
||||||
fb2.genre = text;
|
fb2.genre = text;
|
||||||
@@ -334,6 +543,18 @@ export default class BookParser {
|
|||||||
fb2.annotation += text;
|
fb2.annotation += text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
) {
|
||||||
let tOpen = (center ? '<center>' : '');
|
let tOpen = (center ? '<center>' : '');
|
||||||
tOpen += (bold ? '<strong>' : '');
|
tOpen += (bold ? '<strong>' : '');
|
||||||
tOpen += (italic ? '<emphasis>' : '');
|
tOpen += (italic ? '<emphasis>' : '');
|
||||||
@@ -343,33 +564,22 @@ export default class BookParser {
|
|||||||
tClose += (bold ? '</strong>' : '');
|
tClose += (bold ? '</strong>' : '');
|
||||||
tClose += (center ? '</center>' : '');
|
tClose += (center ? '</center>' : '');
|
||||||
|
|
||||||
if (path.indexOf('/fictionbook/body/title') == 0) {
|
if (text != ' ')
|
||||||
growParagraph(`${tOpen}${text}${tClose}`, text.length);
|
growParagraph(`${tOpen}${text}${tClose}`, text.length, text);
|
||||||
}
|
else
|
||||||
|
growParagraph(' ', 1);
|
||||||
if (path.indexOf('/fictionbook/body/section') == 0) {
|
|
||||||
switch (tag) {
|
|
||||||
case 'p':
|
|
||||||
growParagraph(`${tOpen}${text}${tClose}`, text.length);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
growParagraph(`${tOpen}${text}${tClose}`, text.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (binaryId) {
|
|
||||||
dimPromises.push(getImageDimensions(binaryId, binaryType, text));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onProgress = async(prog) => {
|
const onProgress = async(prog) => {
|
||||||
await sleep(1);
|
await utils.sleep(1);
|
||||||
callback(prog);
|
callback(prog);
|
||||||
};
|
};
|
||||||
|
|
||||||
await sax.parse(data, {
|
await sax.parse(data, {
|
||||||
onStartNode, onEndNode, onTextNode, onProgress
|
onStartNode, onEndNode, onTextNode, onProgress
|
||||||
});
|
});
|
||||||
|
correctCurrentPara();
|
||||||
|
|
||||||
if (dimPromises.length) {
|
if (dimPromises.length) {
|
||||||
try {
|
try {
|
||||||
@@ -385,11 +595,20 @@ export default class BookParser {
|
|||||||
this.textLength = paraOffset;
|
this.textLength = paraOffset;
|
||||||
|
|
||||||
callback(100);
|
callback(100);
|
||||||
await sleep(10);
|
await utils.sleep(10);
|
||||||
|
|
||||||
return {fb2};
|
return {fb2};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
imageHrefToId(id) {
|
||||||
|
let local = false;
|
||||||
|
if (id[0] == '#') {
|
||||||
|
id = id.substr(1);
|
||||||
|
local = true;
|
||||||
|
}
|
||||||
|
return {id, local};
|
||||||
|
}
|
||||||
|
|
||||||
findParaIndex(bookPos) {
|
findParaIndex(bookPos) {
|
||||||
let result = undefined;
|
let result = undefined;
|
||||||
//дихотомия
|
//дихотомия
|
||||||
@@ -414,16 +633,26 @@ export default class BookParser {
|
|||||||
|
|
||||||
splitToStyle(s) {
|
splitToStyle(s) {
|
||||||
let result = [];/*array of {
|
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},
|
image: {local: Boolean, inline: Boolean, id: String},
|
||||||
text: String,
|
text: String,
|
||||||
}*/
|
}*/
|
||||||
let style = {};
|
let style = {};
|
||||||
let image = {};
|
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
|
const onTextNode = async(text) => {// eslint-disable-line no-unused-vars
|
||||||
result.push({
|
result.push({
|
||||||
style: Object.assign({}, style),
|
style: copyStyle(style),
|
||||||
image,
|
image,
|
||||||
text
|
text
|
||||||
});
|
});
|
||||||
@@ -437,6 +666,12 @@ export default class BookParser {
|
|||||||
case 'emphasis':
|
case 'emphasis':
|
||||||
style.italic = true;
|
style.italic = true;
|
||||||
break;
|
break;
|
||||||
|
case 'sup':
|
||||||
|
style.sup = true;
|
||||||
|
break;
|
||||||
|
case 'sub':
|
||||||
|
style.sub = true;
|
||||||
|
break;
|
||||||
case 'center':
|
case 'center':
|
||||||
style.center = true;
|
style.center = true;
|
||||||
break;
|
break;
|
||||||
@@ -449,28 +684,21 @@ export default class BookParser {
|
|||||||
case 'image': {
|
case 'image': {
|
||||||
let attrs = sax.getAttrsSync(tail);
|
let attrs = sax.getAttrsSync(tail);
|
||||||
if (attrs.href && attrs.href.value) {
|
if (attrs.href && attrs.href.value) {
|
||||||
let id = attrs.href.value;
|
image = this.imageHrefToId(attrs.href.value);
|
||||||
let local = false;
|
image.inline = false;
|
||||||
if (id[0] == '#') {
|
image.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
|
||||||
id = id.substr(1);
|
|
||||||
local = true;
|
|
||||||
}
|
|
||||||
image = {local, inline: false, id};
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'image-inline': {
|
case 'image-inline': {
|
||||||
let attrs = sax.getAttrsSync(tail);
|
let attrs = sax.getAttrsSync(tail);
|
||||||
if (attrs.href && attrs.href.value) {
|
if (attrs.href && attrs.href.value) {
|
||||||
let id = attrs.href.value;
|
const img = this.imageHrefToId(attrs.href.value);
|
||||||
let local = false;
|
img.inline = true;
|
||||||
if (id[0] == '#') {
|
img.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
|
||||||
id = id.substr(1);
|
|
||||||
local = true;
|
|
||||||
}
|
|
||||||
result.push({
|
result.push({
|
||||||
style: Object.assign({}, style),
|
style: copyStyle(style),
|
||||||
image: {local, inline: true, id},
|
image: img,
|
||||||
text: ''
|
text: ''
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -487,6 +715,12 @@ export default class BookParser {
|
|||||||
case 'emphasis':
|
case 'emphasis':
|
||||||
style.italic = false;
|
style.italic = false;
|
||||||
break;
|
break;
|
||||||
|
case 'sup':
|
||||||
|
style.sup = false;
|
||||||
|
break;
|
||||||
|
case 'sub':
|
||||||
|
style.sub = false;
|
||||||
|
break;
|
||||||
case 'center':
|
case 'center':
|
||||||
style.center = false;
|
style.center = false;
|
||||||
break;
|
break;
|
||||||
@@ -506,7 +740,7 @@ export default class BookParser {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//длинные слова (или белиберду без пробелов) тоже разобьем
|
//длинные слова (или белиберду без пробелов) тоже разобьем
|
||||||
const maxWordLength = this.maxWordLength;
|
const maxWordLength = this.sets.maxWordLength;
|
||||||
const parts = result;
|
const parts = result;
|
||||||
result = [];
|
result = [];
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
@@ -519,7 +753,7 @@ export default class BookParser {
|
|||||||
spaceIndex = i;
|
spaceIndex = i;
|
||||||
|
|
||||||
if (i - spaceIndex >= maxWordLength && i < p.text.length - 1 &&
|
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)});
|
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)};
|
p = {style: p.style, image: p.image, text: p.text.substr(i + 1)};
|
||||||
spaceIndex = -1;
|
spaceIndex = -1;
|
||||||
@@ -537,17 +771,10 @@ export default class BookParser {
|
|||||||
splitToSlogi(word) {
|
splitToSlogi(word) {
|
||||||
let result = [];
|
let result = [];
|
||||||
|
|
||||||
const glas = new Set(['а', 'А', 'о', 'О', 'и', 'И', 'е', 'Е', 'ё', 'Ё', 'э', 'Э', 'ы', 'Ы', 'у', 'У', 'ю', 'Ю', 'я', 'Я']);
|
const len = word.length;
|
||||||
const soglas = new Set([
|
if (len > 3) {
|
||||||
'б', 'в', 'г', 'д', 'ж', 'з', 'й', 'к', 'л', 'м', 'н', 'п', 'р', 'с', 'т', 'ф', 'х', 'ц', 'ч', 'ш', 'щ',
|
|
||||||
'Б', 'В', 'Г', 'Д', 'Ж', 'З', 'Й', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х', 'Ч', 'Ц', 'Ш', 'Щ'
|
|
||||||
]);
|
|
||||||
const znak = new Set(['ь', 'Ь', 'ъ', 'Ъ', 'й', 'Й']);
|
|
||||||
const alpha = new Set([...glas, ...soglas, ...znak]);
|
|
||||||
|
|
||||||
let slog = '';
|
let slog = '';
|
||||||
let slogLen = 0;
|
let slogLen = 0;
|
||||||
const len = word.length;
|
|
||||||
word += ' ';
|
word += ' ';
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
slog += word[i];
|
slog += word[i];
|
||||||
@@ -556,13 +783,12 @@ export default class BookParser {
|
|||||||
|
|
||||||
if (slogLen > 1 && i < len - 2 && (
|
if (slogLen > 1 && i < len - 2 && (
|
||||||
//гласная, а следом не 2 согласные буквы
|
//гласная, а следом не 2 согласные буквы
|
||||||
(glas.has(word[i]) && !(soglas.has(word[i + 1]) &&
|
(glas.has(word[i]) && !( soglas.has(word[i + 1]) && soglas.has(word[i + 2]) ) &&
|
||||||
soglas.has(word[i + 2])) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])
|
alpha.has(word[i + 1]) && alpha.has(word[i + 2])
|
||||||
) ||
|
) ||
|
||||||
//предыдущая не согласная буква, текущая согласная, а следом согласная и согласная|гласная буквы
|
//предыдущая не согласная буква, текущая согласная, а следом согласная и согласная|гласная буквы
|
||||||
(alpha.has(word[i - 1]) && !soglas.has(word[i - 1]) &&
|
(alpha.has(word[i - 1]) && !soglas.has(word[i - 1]) && soglas.has(word[i]) && soglas.has(word[i + 1]) &&
|
||||||
soglas.has(word[i]) && soglas.has(word[i + 1]) &&
|
( glas.has(word[i + 2]) || soglas.has(word[i + 2]) ) &&
|
||||||
(glas.has(word[i + 2]) || soglas.has(word[i + 2])) &&
|
|
||||||
alpha.has(word[i + 1]) && alpha.has(word[i + 2])
|
alpha.has(word[i + 1]) && alpha.has(word[i + 2])
|
||||||
) ||
|
) ||
|
||||||
//мягкий или твердый знак или Й
|
//мягкий или твердый знак или Й
|
||||||
@@ -580,41 +806,53 @@ export default class BookParser {
|
|||||||
}
|
}
|
||||||
if (slog)
|
if (slog)
|
||||||
result.push(slog);
|
result.push(slog);
|
||||||
|
} else {
|
||||||
|
result.push(word);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
parsePara(paraIndex) {
|
parsePara(paraIndex) {
|
||||||
const para = this.para[paraIndex];
|
const para = this.para[paraIndex];
|
||||||
|
const s = this.sets;
|
||||||
|
|
||||||
|
//перераспарсиваем только при изменении одного из параметров
|
||||||
if (!this.force &&
|
if (!this.force &&
|
||||||
para.parsed &&
|
para.parsed &&
|
||||||
para.parsed.w === this.w &&
|
para.parsed.p === s.p &&
|
||||||
para.parsed.p === this.p &&
|
para.parsed.w === s.w &&
|
||||||
para.parsed.wordWrap === this.wordWrap &&
|
para.parsed.font === s.font &&
|
||||||
para.parsed.maxWordLength === this.maxWordLength &&
|
para.parsed.fontSize === s.fontSize &&
|
||||||
para.parsed.font === this.font &&
|
para.parsed.wordWrap === s.wordWrap &&
|
||||||
para.parsed.cutEmptyParagraphs === this.cutEmptyParagraphs &&
|
para.parsed.cutEmptyParagraphs === s.cutEmptyParagraphs &&
|
||||||
para.parsed.addEmptyParagraphs === this.addEmptyParagraphs &&
|
para.parsed.addEmptyParagraphs === s.addEmptyParagraphs &&
|
||||||
para.parsed.showImages === this.showImages &&
|
para.parsed.maxWordLength === s.maxWordLength &&
|
||||||
para.parsed.imageHeightLines === this.imageHeightLines
|
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;
|
return para.parsed;
|
||||||
|
|
||||||
const parsed = {
|
const parsed = {
|
||||||
w: this.w,
|
p: s.p,
|
||||||
p: this.p,
|
w: s.w,
|
||||||
wordWrap: this.wordWrap,
|
font: s.font,
|
||||||
maxWordLength: this.maxWordLength,
|
fontSize: s.fontSize,
|
||||||
font: this.font,
|
wordWrap: s.wordWrap,
|
||||||
cutEmptyParagraphs: this.cutEmptyParagraphs,
|
cutEmptyParagraphs: s.cutEmptyParagraphs,
|
||||||
addEmptyParagraphs: this.addEmptyParagraphs,
|
addEmptyParagraphs: s.addEmptyParagraphs,
|
||||||
showImages: this.showImages,
|
maxWordLength: s.maxWordLength,
|
||||||
imageHeightLines: this.imageHeightLines,
|
lineHeight: s.lineHeight,
|
||||||
visible: !(
|
showImages: s.showImages,
|
||||||
(this.cutEmptyParagraphs && para.cut) ||
|
imageHeightLines: s.imageHeightLines,
|
||||||
(para.addIndex > this.addEmptyParagraphs)
|
imageFitWidth: (s.imageFitWidth || s.dualPageMode),
|
||||||
)
|
compactTextPerc: s.compactTextPerc,
|
||||||
|
testWidth: s.testWidth,
|
||||||
|
visible: true, //вычисляется позже
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -626,13 +864,16 @@ export default class BookParser {
|
|||||||
last: Boolean,
|
last: Boolean,
|
||||||
parts: array of {
|
parts: array of {
|
||||||
style: {bold: Boolean, italic: Boolean, center: Boolean},
|
style: {bold: Boolean, italic: Boolean, center: Boolean},
|
||||||
image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number},
|
image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number, w: Number, h: Number},
|
||||||
text: String,
|
text: String,
|
||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
let parts = this.splitToStyle(para.text);
|
let parts = this.splitToStyle(para.text);
|
||||||
|
|
||||||
|
//инициализация парсера
|
||||||
let line = {begin: para.offset, parts: []};
|
let line = {begin: para.offset, parts: []};
|
||||||
|
let paragraphText = '';//текст параграфа
|
||||||
let partText = '';//накапливаемый кусок со стилем
|
let partText = '';//накапливаемый кусок со стилем
|
||||||
|
|
||||||
let str = '';//измеряемая строка
|
let str = '';//измеряемая строка
|
||||||
@@ -641,21 +882,36 @@ export default class BookParser {
|
|||||||
let style = {};
|
let style = {};
|
||||||
let ofs = 0;//смещение от начала параграфа para.offset
|
let ofs = 0;//смещение от начала параграфа para.offset
|
||||||
let imgW = 0;
|
let imgW = 0;
|
||||||
|
let imageInPara = false;
|
||||||
|
const compactWidth = this.measureText('W', {})*parsed.compactTextPerc/100;
|
||||||
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
|
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
style = part.style;
|
style = part.style;
|
||||||
|
paragraphText += part.text;
|
||||||
|
|
||||||
//изображения
|
//изображения
|
||||||
if (part.image.id && !part.image.inline) {
|
if (part.image.id && !part.image.inline) {
|
||||||
parsed.visible = this.showImages;
|
imageInPara = true;
|
||||||
let bin = this.binary[part.image.id];
|
let bin = this.binary[part.image.id];
|
||||||
if (!bin)
|
if (!bin)
|
||||||
bin = {h: 0, w: 0};
|
bin = {h: 1, w: 1};
|
||||||
|
|
||||||
let lineCount = this.imageHeightLines;
|
let lineCount = parsed.imageHeightLines;
|
||||||
const c = Math.ceil(bin.h/this.lineHeight);
|
let c = Math.ceil(bin.h/parsed.lineHeight);
|
||||||
|
|
||||||
|
const maxH = lineCount*parsed.lineHeight;
|
||||||
|
let maxH2 = maxH;
|
||||||
|
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);
|
lineCount = (c < lineCount ? c : lineCount);
|
||||||
|
|
||||||
|
let imageHeight = (maxH2 < maxH ? maxH2 : maxH);
|
||||||
|
imageHeight = (imageHeight <= bin.h ? imageHeight : bin.h);
|
||||||
|
|
||||||
|
let imageWidth = (bin.h > imageHeight ? bin.w*imageHeight/bin.h : bin.w);
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (; i < lineCount - 1; i++) {
|
for (; i < lineCount - 1; i++) {
|
||||||
line.end = para.offset + ofs;
|
line.end = para.offset + ofs;
|
||||||
@@ -667,7 +923,10 @@ export default class BookParser {
|
|||||||
id: part.image.id,
|
id: part.image.id,
|
||||||
imageLine: i,
|
imageLine: i,
|
||||||
lineCount,
|
lineCount,
|
||||||
paraIndex
|
paraIndex,
|
||||||
|
w: imageWidth,
|
||||||
|
h: imageHeight,
|
||||||
|
num: part.image.num
|
||||||
}});
|
}});
|
||||||
lines.push(line);
|
lines.push(line);
|
||||||
line = {begin: line.end + 1, parts: []};
|
line = {begin: line.end + 1, parts: []};
|
||||||
@@ -677,18 +936,20 @@ export default class BookParser {
|
|||||||
line.first = (j == 0);
|
line.first = (j == 0);
|
||||||
line.last = true;
|
line.last = true;
|
||||||
line.parts.push({style, text: ' ',
|
line.parts.push({style, text: ' ',
|
||||||
image: {local: part.image.local, inline: false, id: part.image.id, imageLine: i, lineCount, paraIndex}});
|
image: {local: part.image.local, inline: false, id: part.image.id,
|
||||||
|
imageLine: i, lineCount, paraIndex, w: imageWidth, h: imageHeight, num: part.image.num}
|
||||||
|
});
|
||||||
|
|
||||||
continue;
|
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];
|
const bin = this.binary[part.image.id];
|
||||||
if (bin) {
|
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;
|
imgW += bin.w*imgH/bin.h;
|
||||||
line.parts.push({style, text: '',
|
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}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,7 +970,7 @@ export default class BookParser {
|
|||||||
p = (style.space ? p + parsed.p*style.space : p);
|
p = (style.space ? p + parsed.p*style.space : p);
|
||||||
let w = this.measureText(str, style) + p;
|
let w = this.measureText(str, style) + p;
|
||||||
let wordTail = word;
|
let wordTail = word;
|
||||||
if (w > parsed.w && prevStr != '') {
|
if (w > parsed.w + compactWidth && prevStr != '') {
|
||||||
if (parsed.wordWrap) {//по слогам
|
if (parsed.wordWrap) {//по слогам
|
||||||
let slogi = this.splitToSlogi(word);
|
let slogi = this.splitToSlogi(word);
|
||||||
|
|
||||||
@@ -722,7 +983,7 @@ export default class BookParser {
|
|||||||
for (let k = 0; k < slogiLen - 1; k++) {
|
for (let k = 0; k < slogiLen - 1; k++) {
|
||||||
let slog = slogi[0];
|
let slog = slogi[0];
|
||||||
let ww = this.measureText(s + slog + (slog[slog.length - 1] == '-' ? '' : '-'), style) + p;
|
let ww = this.measureText(s + slog + (slog[slog.length - 1] == '-' ? '' : '-'), style) + p;
|
||||||
if (ww <= parsed.w) {
|
if (ww <= parsed.w + compactWidth) {
|
||||||
s += slog;
|
s += slog;
|
||||||
ss += slog;
|
ss += slog;
|
||||||
} else
|
} else
|
||||||
@@ -798,6 +1059,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;
|
parsed.lines = lines;
|
||||||
para.parsed = parsed;
|
para.parsed = parsed;
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +1,143 @@
|
|||||||
import localForage from 'localforage';
|
import localForage from 'localforage';
|
||||||
|
import path from 'path-browserify';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
import * as utils from '../../../share/utils';
|
|
||||||
import BookParser from './BookParser';
|
import BookParser from './BookParser';
|
||||||
|
import readerApi from '../../../api/reader';
|
||||||
|
import coversStorage from './coversStorage';
|
||||||
|
import * as utils from '../../../share/utils';
|
||||||
|
|
||||||
const maxDataSize = 500*1024*1024;//chars, not bytes
|
const maxDataSize = 500*1024*1024;//compressed bytes
|
||||||
|
const maxRecentLength = 5000;
|
||||||
|
|
||||||
|
//локальный кэш метаданных книг, ограничение maxDataSize
|
||||||
const bmMetaStore = localForage.createInstance({
|
const bmMetaStore = localForage.createInstance({
|
||||||
name: 'bmMetaStore'
|
name: 'bmMetaStore'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//локальный кэш самих книг, ограничение maxDataSize
|
||||||
const bmDataStore = localForage.createInstance({
|
const bmDataStore = localForage.createInstance({
|
||||||
name: 'bmDataStore'
|
name: 'bmDataStore'
|
||||||
});
|
});
|
||||||
|
|
||||||
const bmRecentStore = localForage.createInstance({
|
//список недавно открытых книг
|
||||||
name: 'bmRecentStore'
|
const bmRecentStoreNew = localForage.createInstance({
|
||||||
|
name: 'bmRecentStoreNew'
|
||||||
});
|
});
|
||||||
|
|
||||||
class BookManager {
|
class BookManager {
|
||||||
async init(settings) {
|
async init(settings) {
|
||||||
|
this.loaded = false;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
|
|
||||||
|
this.eventListeners = [];
|
||||||
this.books = {};
|
this.books = {};
|
||||||
|
|
||||||
this.recent = {};
|
this.recent = {};
|
||||||
this.recentChanged1 = true;
|
this.saveRecent = _.debounce(() => {
|
||||||
this.recentChanged2 = true;
|
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;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
let len = await bmMetaStore.length();
|
let len = await bmMetaStore.length();
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = len - 1; i >= 0; i--) {
|
||||||
const key = await bmMetaStore.key(i);
|
const key = await bmMetaStore.key(i);
|
||||||
const keySplit = key.split('-');
|
const keySplit = key.split('-');
|
||||||
|
|
||||||
if (keySplit.length == 2 && keySplit[0] == 'bmMeta') {
|
if (keySplit.length == 2 && keySplit[0] == 'bmMeta') {
|
||||||
let meta = await bmMetaStore.getItem(key);
|
let meta = await bmMetaStore.getItem(key);
|
||||||
|
|
||||||
|
if (_.isObject(meta)) {
|
||||||
|
//уже может быть распарсена книга
|
||||||
|
const oldBook = this.books[meta.key];
|
||||||
this.books[meta.key] = meta;
|
this.books[meta.key] = meta;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
len = await bmRecentStore.length();
|
if (oldBook && oldBook.parsed) {
|
||||||
for (let i = 0; i < len; i++) {
|
this.books[meta.key].parsed = oldBook.parsed;
|
||||||
const key = await bmRecentStore.key(i);
|
}
|
||||||
let r = await bmRecentStore.getItem(key);
|
} else {
|
||||||
this.recent[r.key] = r;
|
await bmMetaStore.removeItem(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.cleanBooks();
|
await this.cleanBooks();
|
||||||
|
this.loaded = true;
|
||||||
|
this.emit('load-stored-finish');
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanBooks() {
|
async cleanBooks() {
|
||||||
@@ -54,7 +147,8 @@ class BookManager {
|
|||||||
let toDel = null;
|
let toDel = null;
|
||||||
for (let key in this.books) {
|
for (let key in this.books) {
|
||||||
let book = this.books[key];
|
let book = this.books[key];
|
||||||
size += (book.length ? book.length : 0);
|
const bookLength = (book.length ? book.length : 0);
|
||||||
|
size += (book.dataCompressedLength ? book.dataCompressedLength : bookLength);
|
||||||
|
|
||||||
if (book.addTime < min) {
|
if (book.addTime < min) {
|
||||||
toDel = book;
|
toDel = book;
|
||||||
@@ -70,49 +164,176 @@ class BookManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addBook(newBook, callback) {
|
async deflateWithProgress(data, callback) {
|
||||||
if (!this.books)
|
const chunkSize = 512*1024;
|
||||||
await this.init();
|
const deflator = new utils.pako.Deflate({level: 5});
|
||||||
let meta = {url: newBook.url, path: newBook.path};
|
|
||||||
meta.key = this.keyFromUrl(meta.url);
|
|
||||||
meta.addTime = Date.now();
|
|
||||||
|
|
||||||
const result = await this.parseBook(meta, newBook.data, callback);
|
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
||||||
|
let chunkNum = 0;
|
||||||
|
let perc = 0;
|
||||||
|
let prevPerc = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < data.length; i += chunkSize) {
|
||||||
|
if ((i + chunkSize) >= data.length) {
|
||||||
|
deflator.push(data.substring(i, i + chunkSize), true);
|
||||||
|
} else {
|
||||||
|
deflator.push(data.substring(i, i + chunkSize), false);
|
||||||
|
}
|
||||||
|
chunkNum++;
|
||||||
|
|
||||||
|
perc = Math.round(chunkNum/chunkTotal*100);
|
||||||
|
if (perc != prevPerc) {
|
||||||
|
callback(perc);
|
||||||
|
await utils.sleep(1);
|
||||||
|
prevPerc = perc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deflator.err) {
|
||||||
|
throw new Error(deflator.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(100);
|
||||||
|
|
||||||
|
return deflator.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async inflateWithProgress(data, callback) {
|
||||||
|
const chunkSize = 512*1024;
|
||||||
|
const inflator = new utils.pako.Inflate({to: 'string'});
|
||||||
|
|
||||||
|
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
||||||
|
let chunkNum = 0;
|
||||||
|
let perc = 0;
|
||||||
|
let prevPerc = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < data.length; i += chunkSize) {
|
||||||
|
if ((i + chunkSize) >= data.length) {
|
||||||
|
inflator.push(data.subarray(i, i + chunkSize), true);
|
||||||
|
} else {
|
||||||
|
inflator.push(data.subarray(i, i + chunkSize), false);
|
||||||
|
}
|
||||||
|
chunkNum++;
|
||||||
|
|
||||||
|
perc = Math.round(chunkNum/chunkTotal*100);
|
||||||
|
if (perc != prevPerc) {
|
||||||
|
callback(perc);
|
||||||
|
await utils.sleep(1);
|
||||||
|
prevPerc = perc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inflator.err) {
|
||||||
|
throw new Error(inflator.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(100);
|
||||||
|
|
||||||
|
return inflator.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addBook(newBook, callback) {
|
||||||
|
let meta = {url: newBook.url, path: newBook.path};
|
||||||
|
|
||||||
|
if (newBook.downloadSize !== undefined && newBook.downloadSize >= 0)
|
||||||
|
meta.downloadSize = newBook.downloadSize;
|
||||||
|
|
||||||
|
meta.key = this.keyFromPath(meta.path);
|
||||||
|
meta.addTime = Date.now();//время добавления в кеш
|
||||||
|
|
||||||
|
const cb = (perc) => {
|
||||||
|
const p = Math.round(30*perc/100);
|
||||||
|
callback(p);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cb2 = (perc) => {
|
||||||
|
const p = Math.round(30 + 65*perc/100);
|
||||||
|
callback(p);
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.parseBook(meta, newBook.data, cb);
|
||||||
|
result.dataCompressed = true;
|
||||||
|
|
||||||
|
let data = newBook.data;
|
||||||
|
if (result.dataCompressed) {
|
||||||
|
//data = utils.pako.deflate(data, {level: 5});
|
||||||
|
data = await this.deflateWithProgress(data, cb2);
|
||||||
|
result.dataCompressedLength = data.byteLength;
|
||||||
|
}
|
||||||
|
callback(95);
|
||||||
|
|
||||||
this.books[meta.key] = result;
|
this.books[meta.key] = result;
|
||||||
|
|
||||||
await bmMetaStore.setItem(`bmMeta-${meta.key}`, this.metaOnly(result));
|
await bmMetaStore.setItem(`bmMeta-${meta.key}`, this.metaOnly(result));
|
||||||
await bmDataStore.setItem(`bmData-${meta.key}`, result.data);
|
await bmDataStore.setItem(`bmData-${meta.key}`, data);
|
||||||
|
|
||||||
|
callback(100);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasBookParsed(meta) {
|
async hasBookParsed(meta) {
|
||||||
if (!this.books)
|
if (!this.books)
|
||||||
return false;
|
return false;
|
||||||
if (!meta.url)
|
if (!meta.path)
|
||||||
return false;
|
return false;
|
||||||
if (!meta.key)
|
if (!meta.key)
|
||||||
meta.key = this.keyFromUrl(meta.url);
|
meta.key = this.keyFromPath(meta.path);
|
||||||
|
|
||||||
let book = this.books[meta.key];
|
let book = this.books[meta.key];
|
||||||
|
|
||||||
|
if (!book && !this.loaded) {
|
||||||
|
book = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
|
||||||
|
if (book)
|
||||||
|
this.books[meta.key] = book;
|
||||||
|
}
|
||||||
|
|
||||||
return !!(book && book.parsed);
|
return !!(book && book.parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBook(meta, callback) {
|
async getBook(meta, callback) {
|
||||||
if (!this.books)
|
|
||||||
await this.init();
|
|
||||||
let result = undefined;
|
let result = undefined;
|
||||||
|
|
||||||
|
if (!meta.path)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!meta.key)
|
if (!meta.key)
|
||||||
meta.key = this.keyFromUrl(meta.url);
|
meta.key = this.keyFromPath(meta.path);
|
||||||
|
|
||||||
result = this.books[meta.key];
|
result = this.books[meta.key];
|
||||||
|
|
||||||
if (result && !result.data) {
|
if (!result) {
|
||||||
result.data = await bmDataStore.getItem(`bmData-${meta.key}`);
|
result = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
|
||||||
|
if (result)
|
||||||
this.books[meta.key] = result;
|
this.books[meta.key] = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result && !result.parsed) {
|
if (result && !result.parsed) {
|
||||||
result = await this.parseBook(result, result.data, callback);
|
let data = await bmDataStore.getItem(`bmData-${meta.key}`);
|
||||||
|
callback(5);
|
||||||
|
await utils.sleep(10);
|
||||||
|
|
||||||
|
let cb = (perc) => {
|
||||||
|
const p = 5 + Math.round(15*perc/100);
|
||||||
|
callback(p);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result.dataCompressed) {
|
||||||
|
try {
|
||||||
|
//data = utils.pako.inflate(data, {to: 'string'});
|
||||||
|
data = await this.inflateWithProgress(data, cb);
|
||||||
|
} catch (e) {
|
||||||
|
this.delBook(meta);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback(20);
|
||||||
|
|
||||||
|
cb = (perc) => {
|
||||||
|
const p = 20 + Math.round(80*perc/100);
|
||||||
|
callback(p);
|
||||||
|
};
|
||||||
|
|
||||||
|
result = await this.parseBook(result, data, cb);
|
||||||
this.books[meta.key] = result;
|
this.books[meta.key] = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,9 +341,6 @@ class BookManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delBook(meta) {
|
async delBook(meta) {
|
||||||
if (!this.books)
|
|
||||||
await this.init();
|
|
||||||
|
|
||||||
await bmMetaStore.removeItem(`bmMeta-${meta.key}`);
|
await bmMetaStore.removeItem(`bmMeta-${meta.key}`);
|
||||||
await bmDataStore.removeItem(`bmData-${meta.key}`);
|
await bmDataStore.removeItem(`bmData-${meta.key}`);
|
||||||
|
|
||||||
@@ -130,15 +348,41 @@ class BookManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async parseBook(meta, data, callback) {
|
async parseBook(meta, data, callback) {
|
||||||
if (!this.books)
|
|
||||||
await this.init();
|
|
||||||
const parsed = new BookParser(this.settings);
|
const parsed = new BookParser(this.settings);
|
||||||
|
|
||||||
const parsedMeta = await parsed.parse(data, callback);
|
const parsedMeta = await parsed.parse(data, callback);
|
||||||
|
|
||||||
|
//cover page
|
||||||
|
let coverPageUrl = '';
|
||||||
|
if (parsed.coverPageId && parsed.binary[parsed.coverPageId]) {
|
||||||
|
const bin = parsed.binary[parsed.coverPageId];
|
||||||
|
let dataUrl = `data:${bin.type};base64,${bin.data}`;
|
||||||
|
try {
|
||||||
|
dataUrl = await utils.resizeImage(dataUrl, 160, 160, 0.94);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
coverPageUrl = readerApi.makeUrlFromBuf(dataUrl);
|
||||||
|
|
||||||
|
//далее асинхронно
|
||||||
|
(async() => {
|
||||||
|
//отправим dataUrl на сервер в /upload
|
||||||
|
try {
|
||||||
|
await readerApi.uploadFileBuf(dataUrl, coverPageUrl);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
//сохраним в storage
|
||||||
|
await coversStorage.setData(coverPageUrl, dataUrl);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
const result = Object.assign({}, meta, parsedMeta, {
|
const result = Object.assign({}, meta, parsedMeta, {
|
||||||
length: data.length,
|
length: data.length,
|
||||||
textLength: parsed.textLength,
|
textLength: parsed.textLength,
|
||||||
data,
|
coverPageUrl,
|
||||||
parsed
|
parsed
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -147,94 +391,167 @@ class BookManager {
|
|||||||
|
|
||||||
metaOnly(book) {
|
metaOnly(book) {
|
||||||
let result = Object.assign({}, book);
|
let result = Object.assign({}, book);
|
||||||
delete result.data;
|
|
||||||
delete result.parsed;
|
delete result.parsed;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
keyFromUrl(url) {
|
/*keyFromUrl(url) {
|
||||||
return utils.stringToHex(url);
|
return utils.stringToHex(url);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
keyFromPath(bookPath) {
|
||||||
|
return path.basename(bookPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setRecentBook(value, noTouch) {
|
keysEqual(bookPath1, bookPath2) {
|
||||||
if (!this.recent)
|
if (bookPath1 === undefined || bookPath2 === undefined)
|
||||||
await this.init();
|
return false;
|
||||||
const result = Object.assign({}, value);
|
|
||||||
if (!noTouch)
|
|
||||||
Object.assign(result, {touchTime: Date.now()});
|
|
||||||
|
|
||||||
if (result.textLength && !result.bookPos && result.bookPosPercent)
|
return (this.keyFromPath(bookPath1) === this.keyFromPath(bookPath2));
|
||||||
result.bookPos = Math.round(result.bookPosPercent*result.textLength);
|
}
|
||||||
|
//-- recent --------------------------------------------------------------
|
||||||
|
async recentSetItem(item = null, skipCheck = false) {
|
||||||
|
const rev = await bmRecentStoreNew.getItem('rev');
|
||||||
|
if (rev != this.recentRev && !skipCheck) {//если изменение произошло в другой вкладке барузера
|
||||||
|
const newRecent = await bmRecentStoreNew.getItem('recent');
|
||||||
|
Object.assign(this.recent, newRecent);
|
||||||
|
this.recentItem = await bmRecentStoreNew.getItem('recent-item');
|
||||||
|
this.recentRev = rev;
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
||||||
await this.cleanRecentBooks();
|
|
||||||
|
|
||||||
this.recentChanged1 = true;
|
if (!item || prevKey != item.key) {
|
||||||
this.recentChanged2 = true;
|
this.saveRecent();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.recentChanged = true;
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
this.emit('recent-changed', item.key);
|
||||||
|
} else {
|
||||||
|
this.emit('recent-changed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async recentSetLastKey(key) {
|
||||||
|
this.recentLastKey = key;
|
||||||
|
await bmRecentStoreNew.setItem('recent-last-key', this.recentLastKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setRecentBook(value) {
|
||||||
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRecentBook(value) {
|
async getRecentBook(value) {
|
||||||
if (!this.recent)
|
|
||||||
await this.init();
|
|
||||||
return this.recent[value.key];
|
return this.recent[value.key];
|
||||||
}
|
}
|
||||||
|
|
||||||
async delRecentBook(value) {
|
async delRecentBook(value, delFlag = 1) {
|
||||||
if (!this.recent)
|
const item = this.recent[value.key];
|
||||||
await this.init();
|
item.deleted = delFlag;
|
||||||
|
|
||||||
await bmRecentStore.removeItem(value.key);
|
if (this.recentLastKey == value.key) {
|
||||||
delete this.recent[value.key];
|
await this.recentSetLastKey(null);
|
||||||
this.recentChanged1 = true;
|
}
|
||||||
this.recentChanged2 = true;
|
|
||||||
|
await this.recentSetItem(item);
|
||||||
|
this.emit('recent-deleted', value.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreRecentBook(value) {
|
||||||
|
const item = this.recent[value.key];
|
||||||
|
item.deleted = 0;
|
||||||
|
|
||||||
|
await this.recentSetItem(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCheckBuc(value, checkBuc = true) {
|
||||||
|
const item = this.recent[value.key];
|
||||||
|
|
||||||
|
const updateItems = [];
|
||||||
|
if (item) {
|
||||||
|
if (item.sameBookKey !== undefined) {
|
||||||
|
const sorted = this.getSortedRecent();
|
||||||
|
for (const book of sorted) {
|
||||||
|
if (book.sameBookKey === item.sameBookKey)
|
||||||
|
updateItems.push(book);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateItems.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const book of updateItems) {
|
||||||
|
book.checkBuc = checkBuc;
|
||||||
|
await this.recentSetItem(book);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanRecentBooks() {
|
async cleanRecentBooks() {
|
||||||
if (!this.recent)
|
const sorted = this.getSortedRecent();
|
||||||
await this.init();
|
|
||||||
|
|
||||||
if (Object.keys(this.recent).length > 1000) {
|
let isDel = false;
|
||||||
let min = Date.now();
|
for (let i = maxRecentLength; i < sorted.length; i++) {
|
||||||
let found = null;
|
delete this.recent[sorted[i].key];
|
||||||
for (let key in this.recent) {
|
isDel = true;
|
||||||
const book = this.recent[key];
|
|
||||||
if (book.touchTime < min) {
|
|
||||||
min = book.touchTime;
|
|
||||||
found = book;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (found) {
|
this.sortedRecentCached = null;
|
||||||
await this.delRecentBook(found);
|
|
||||||
await this.cleanRecentBooks();
|
if (isDel)
|
||||||
}
|
await this.recentSetItem();
|
||||||
}
|
return isDel;
|
||||||
}
|
}
|
||||||
|
|
||||||
mostRecentBook() {
|
mostRecentBook() {
|
||||||
if (!this.recentChanged1 && this.mostRecentCached) {
|
if (this.recentLastKey) {
|
||||||
return this.mostRecentCached;
|
return this.recent[this.recentLastKey];
|
||||||
}
|
}
|
||||||
|
const oldKey = this.recentLastKey;
|
||||||
|
|
||||||
let max = 0;
|
let max = 0;
|
||||||
let result = null;
|
let result = null;
|
||||||
for (let key in this.recent) {
|
for (const key in this.recent) {
|
||||||
const book = this.recent[key];
|
const book = this.recent[key];
|
||||||
if (book.touchTime > max) {
|
if (!book.deleted && book.touchTime > max) {
|
||||||
max = book.touchTime;
|
max = book.touchTime;
|
||||||
result = book;
|
result = book;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.mostRecentCached = result;
|
|
||||||
this.recentChanged1 = false;
|
const newRecentLastKey = (result ? result.key : null);
|
||||||
|
this.recentSetLastKey(newRecentLastKey);//no await
|
||||||
|
|
||||||
|
if (newRecentLastKey !== oldKey)
|
||||||
|
this.emit('recent-changed');
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortedRecent() {
|
getSortedRecent() {
|
||||||
if (!this.recentChanged2 && this.sortedRecentCached) {
|
if (!this.recentChanged && this.sortedRecentCached) {
|
||||||
return this.sortedRecentCached;
|
return this.sortedRecentCached;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,10 +560,85 @@ class BookManager {
|
|||||||
result.sort((a, b) => b.touchTime - a.touchTime);
|
result.sort((a, b) => b.touchTime - a.touchTime);
|
||||||
|
|
||||||
this.sortedRecentCached = result;
|
this.sortedRecentCached = result;
|
||||||
this.recentChanged2 = false;
|
this.recentChanged = false;
|
||||||
return result;
|
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);
|
||||||
|
|
||||||
|
Object.assign(mergedRecent, value);
|
||||||
|
|
||||||
|
//подстраховка от hotReload
|
||||||
|
for (let i of Object.keys(mergedRecent)) {
|
||||||
|
if (!mergedRecent[i].key || mergedRecent[i].key !== i)
|
||||||
|
delete mergedRecent[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.recent = mergedRecent;
|
||||||
|
|
||||||
|
await this.recentSetLastKey(null);
|
||||||
|
await this.recentSetItem(null, true);
|
||||||
|
|
||||||
|
this.emit('set-recent');
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(listener) {
|
||||||
|
if (this.eventListeners.indexOf(listener) < 0)
|
||||||
|
this.eventListeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEventListener(listener) {
|
||||||
|
const i = this.eventListeners.indexOf(listener);
|
||||||
|
if (i >= 0)
|
||||||
|
this.eventListeners.splice(i, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(eventName, value) {
|
||||||
|
if (this.eventListeners) {
|
||||||
|
for (const listener of this.eventListeners) {
|
||||||
|
//console.log(eventName);
|
||||||
|
listener(eventName, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
61
client/components/Reader/share/coversStorage.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import localForage from 'localforage';
|
||||||
|
//import _ from 'lodash';
|
||||||
|
import * as utils from '../../../share/utils';
|
||||||
|
|
||||||
|
const maxDataSize = 100*1024*1024;
|
||||||
|
|
||||||
|
const coversStore = localForage.createInstance({
|
||||||
|
name: 'coversStorage'
|
||||||
|
});
|
||||||
|
|
||||||
|
class CoversStorage {
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
this.cleanCovers(); //no await
|
||||||
|
}
|
||||||
|
|
||||||
|
async setData(key, data) {
|
||||||
|
await coversStore.setItem(key, {addTime: Date.now(), data});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getData(key) {
|
||||||
|
const item = await coversStore.getItem(key);
|
||||||
|
return (item ? item.data : undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeData(key) {
|
||||||
|
await coversStore.removeItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanCovers() {
|
||||||
|
await utils.sleep(10000);
|
||||||
|
|
||||||
|
while (1) {// eslint-disable-line no-constant-condition
|
||||||
|
let size = 0;
|
||||||
|
let min = Date.now();
|
||||||
|
let toDel = null;
|
||||||
|
for (const key of (await coversStore.keys())) {
|
||||||
|
const item = await coversStore.getItem(key);
|
||||||
|
|
||||||
|
size += item.data.length;
|
||||||
|
|
||||||
|
if (item.addTime < min) {
|
||||||
|
toDel = key;
|
||||||
|
min = item.addTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (size > maxDataSize && toDel) {
|
||||||
|
await this.removeData(toDel);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new CoversStorage();
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
export default async function restoreOldSettings(settings, bookManager, commit) {
|
|
||||||
const oldSets = localStorage['colorSetting'];
|
|
||||||
let isOld = false;
|
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
|
||||||
let key = unescape(localStorage.key(i));
|
|
||||||
if (key.indexOf('bpr-book-') == 0)
|
|
||||||
isOld = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOld || oldSets) {
|
|
||||||
let newSettings = null;
|
|
||||||
if (oldSets) {
|
|
||||||
const [textColor, backgroundColor, lineStep, , , statusBarHeight, scInt] = unescape(oldSets).split('|');
|
|
||||||
|
|
||||||
const fontSize = Math.round(lineStep*0.8);
|
|
||||||
const scrollingDelay = fontSize*scInt;
|
|
||||||
|
|
||||||
newSettings = Object.assign({}, settings, {
|
|
||||||
textColor,
|
|
||||||
backgroundColor,
|
|
||||||
fontSize,
|
|
||||||
statusBarHeight: statusBarHeight*1,
|
|
||||||
scrollingDelay,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
|
||||||
let key = localStorage.key(i);
|
|
||||||
if (key.indexOf('bpr-') == 0) {
|
|
||||||
let v = unescape(localStorage[key]);
|
|
||||||
key = unescape(key);
|
|
||||||
|
|
||||||
if (key.lastIndexOf('=timestamp') == key.length - 10) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key.indexOf('bpr-book-') == 0) {
|
|
||||||
const url = key.substr(9);
|
|
||||||
const [scrollTop, scrollHeight, ] = v.split('|');
|
|
||||||
|
|
||||||
const bookPosPercent = scrollTop*1/(scrollHeight*1 + 1);
|
|
||||||
const title = unescape(localStorage[`bpr-title-${escape(url)}`]);
|
|
||||||
const author = unescape(localStorage[`bpr-author-${escape(url)}`]);
|
|
||||||
const time = unescape(localStorage[`bpr-book-${escape(url)}=timestamp`]).split(';')[0];
|
|
||||||
const touchTime = Date.parse(time);
|
|
||||||
|
|
||||||
const bookKey = bookManager.keyFromUrl(url);
|
|
||||||
const recent = await bookManager.getRecentBook({key: bookKey});
|
|
||||||
|
|
||||||
if (!recent) {
|
|
||||||
await bookManager.setRecentBook({
|
|
||||||
key: bookKey,
|
|
||||||
touchTime,
|
|
||||||
bookPosPercent,
|
|
||||||
url,
|
|
||||||
fb2: {
|
|
||||||
bookTitle: title,
|
|
||||||
lastName: author,
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.clear();
|
|
||||||
if (oldSets)
|
|
||||||
commit('reader/setSettings', newSettings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
44
client/components/Reader/share/wallpaperStorage.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getKeys() {
|
||||||
|
return await wpStore.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
keyExists(key) {//не асинхронная
|
||||||
|
return this.cachedKeys.includes(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new WallpaperStorage();
|
||||||
677
client/components/Reader/versionHistory.js
Normal file
@@ -0,0 +1,677 @@
|
|||||||
|
export const versionHistory = [
|
||||||
|
{
|
||||||
|
version: '0.12.0',
|
||||||
|
releaseDate: '2022-07-27',
|
||||||
|
showUntil: '2022-08-03',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>запущен сервер проверки обновлений книг:</li>
|
||||||
|
<ul>
|
||||||
|
<li>проверка обновления той или иной книги настраивается в списке загруженных (чекбокс)</li>
|
||||||
|
<li>в настройках можно указать разницу размеров, при которой необходимо делать уведомление</li>
|
||||||
|
</ul>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.11.8',
|
||||||
|
releaseDate: '2022-07-14',
|
||||||
|
showUntil: '2022-07-13',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлено отображение и синхронизация обложек в окне загруженных книг</li>
|
||||||
|
<li>добавлена синхронизация обоев</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
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',
|
||||||
|
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',
|
||||||
|
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',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлен неубираемый баннер для http-версии о переходе на httpS</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.7.8',
|
||||||
|
releaseDate: '2019-11-25',
|
||||||
|
showUntil: '2019-11-24',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>улучшение html-фильтров для сайтов</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.7.7',
|
||||||
|
releaseDate: '2019-11-06',
|
||||||
|
showUntil: '2019-11-10',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлены следующие жесты для тачскрина (только при включенной опции "управление кликом"):</li>
|
||||||
|
<ul>
|
||||||
|
<li style="list-style-type: square">от центра вверх: на весь экран</li>
|
||||||
|
<li style="list-style-type: square">от центра вниз: плавный скроллинг</li>
|
||||||
|
<li style="list-style-type: square">от центра вправо: увеличить скорость скроллинга</li>
|
||||||
|
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
|
||||||
|
</ul>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.7.6',
|
||||||
|
releaseDate: '2019-10-30',
|
||||||
|
showUntil: '2019-10-29',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.7.5',
|
||||||
|
releaseDate: '2019-10-22',
|
||||||
|
showUntil: '2019-10-21',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.7.3',
|
||||||
|
releaseDate: '2019-10-18',
|
||||||
|
showUntil: '2019-10-17',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>внутренние переделки механизма синхронизации с сервером</li>
|
||||||
|
<li>добавлен html-фильтр для сайтов www.fanfiction.net, archiveofourown.org</li>
|
||||||
|
<li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.7.1',
|
||||||
|
releaseDate: '2019-09-20',
|
||||||
|
showUntil: '2019-09-19',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
<li>на панель управления добавлена кнопка "Автономный режим"</li>
|
||||||
|
<li>актуализирована справка</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.7.0',
|
||||||
|
releaseDate: '2019-09-07',
|
||||||
|
showUntil: '2019-10-01',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>налажена работа https-версии сайта, рекомендуется плавный переход</li>
|
||||||
|
<li>добавлена возможность загрузки и работы https-версии читалки в оффлайн-режиме (при отсутствии интернета)</li>
|
||||||
|
<li>упрощение механизма серверной синхронизации с целью повышения надежности и избавления от багов</li>
|
||||||
|
<li>окна теперь можно перемещать за заголовок</li>
|
||||||
|
<li>немного улучшен внешний вид и управление на смартфонах</li>
|
||||||
|
<li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.6.10',
|
||||||
|
releaseDate: '2019-07-21',
|
||||||
|
showUntil: '2019-07-20',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.6.9',
|
||||||
|
releaseDate: '2019-06-23',
|
||||||
|
showUntil: '2019-06-22',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>исправлен баг - падение сервера при распаковке битых архивов книг</li>
|
||||||
|
<li>исправлен баг - не распознавались некоторые книги формата fb2 в кодировке utf8</li>
|
||||||
|
<li>добавлены новые варианты анимации перелистывания</li>
|
||||||
|
<li>на страницу загрузки добавлен блок "Поделиться"</li>
|
||||||
|
<li>улучшены прогрессбары</li>
|
||||||
|
<li>исправления недочетов, небольшие оптимизации</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.6.7',
|
||||||
|
releaseDate: '2019-05-30',
|
||||||
|
showUntil: '2019-06-05',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлен диалог "Что нового"</li>
|
||||||
|
<li>в справку добавлена история версий проекта</li>
|
||||||
|
<li>добавлена возможность настройки отображаемых кнопок на панели управления</li>
|
||||||
|
<li>некоторые кнопки на панели управления были скрыты по умолчанию</li>
|
||||||
|
<li>на страницу загрузки добавлена возможность загрузки книги из буфера обмена</li>
|
||||||
|
<li>добавлен GET-параметр вида "/reader?__refresh=1&url=..." для принудительного обновления загружаемого текста</li>
|
||||||
|
<li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li>
|
||||||
|
<li>исправления багов и недочетов</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.6.6',
|
||||||
|
releaseDate: '2019-03-29',
|
||||||
|
showUntil: '2019-03-29',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li>
|
||||||
|
<li>оптимизации процесса синхронизации, внутренние переделки</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.6.4',
|
||||||
|
releaseDate: '2019-03-24',
|
||||||
|
showUntil: '2019-03-24',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>исправления багов, оптимизации</li>
|
||||||
|
<li>добавлена возможность синхронизации данных между устройствами</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.5.4',
|
||||||
|
releaseDate: '2019-03-04',
|
||||||
|
showUntil: '2019-03-04',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлена поддержка форматов pdf, epub, mobi</li>
|
||||||
|
<li>(0.5.2) добавлена поддержка форматов rtf, doc, docx</li>
|
||||||
|
<li>(0.4.2) фильтр для СИ больше не вырезает изображения</li>
|
||||||
|
<li>(0.4.0) добавлено отображение картинок в fb2</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.3.0',
|
||||||
|
releaseDate: '2019-02-17',
|
||||||
|
showUntil: '2019-02-17',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>поправки багов</li>
|
||||||
|
<li>улучшено распознавание текста</li>
|
||||||
|
<li>изменена верстка страницы - убрано позиционирование каждого слова</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.1.7',
|
||||||
|
releaseDate: '2019-02-14',
|
||||||
|
showUntil: '2019-02-14',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>увеличены верхние границы отступов и др.размеров</li>
|
||||||
|
<li>добавлена настройка для удаления/вставки пустых параграфов</li>
|
||||||
|
<li>добавлена настройка включения/отключения управления кликом</li>
|
||||||
|
<li>добавлена возможность сброса настроек</li>
|
||||||
|
<li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.1.0',
|
||||||
|
releaseDate: '2019-02-12',
|
||||||
|
showUntil: '2019-02-12',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>первый деплой проекта, длительность разработки - 2 месяца</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div>
|
||||||
Раздел Settings в разработке
|
Раздел Settings в разработке
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
|
||||||
export default @Component({
|
class Settings {
|
||||||
})
|
|
||||||
class Settings extends Vue {
|
|
||||||
created() {
|
created() {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(Settings);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div>
|
||||||
Раздел Sources в разработке
|
Раздел Sources в разработке
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
|
||||||
export default @Component({
|
class Sources {
|
||||||
})
|
|
||||||
class Sources extends Vue {
|
|
||||||
created() {
|
created() {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(Sources);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</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,34 +1,150 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="window">
|
<div ref="main" class="main xyfit absolute" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
|
||||||
<div class="header">
|
<div ref="windowBox" class="xyfit absolute flex no-wrap" @click.stop>
|
||||||
<span class="header-text"><slot name="header"></slot></span>
|
<div ref="window" class="window flexfit column no-wrap">
|
||||||
<span class="close-button" @click="close"><i class="el-icon-close"></i></span>
|
<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>
|
</div>
|
||||||
|
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import vueComponent from '../vueComponent.js';
|
||||||
import Component from 'vue-class-component';
|
|
||||||
|
|
||||||
export default @Component({
|
class Window {
|
||||||
})
|
_props = {
|
||||||
class Window extends Vue {
|
height: { type: String, default: '100%' },
|
||||||
close() {
|
width: { type: String, default: '100%' },
|
||||||
this.$emit('close');
|
maxWidth: { type: String, default: '' },
|
||||||
|
topShift: { type: Number, default: 0 },
|
||||||
|
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)
|
||||||
|
this.$refs.windowBox.style.maxWidth = this.maxWidth;
|
||||||
|
|
||||||
|
const left = (this.$refs.main.offsetWidth - this.$refs.windowBox.offsetWidth)/2;
|
||||||
|
const top = (this.$refs.main.offsetHeight - this.$refs.windowBox.offsetHeight)/2 + this.topShift;
|
||||||
|
this.$refs.windowBox.style.left = (left > 0 ? left : 0) + 'px';
|
||||||
|
this.$refs.windowBox.style.top = (top > 0 ? top : 0) + 'px';
|
||||||
|
|
||||||
|
if (this.margin)
|
||||||
|
this.$refs.window.style.margin = this.margin;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMouseDown(event) {
|
||||||
|
if (this.$root.isMobileDevice)
|
||||||
|
return;
|
||||||
|
if (event.button == 0) {
|
||||||
|
this.$refs.header.style.cursor = 'move';
|
||||||
|
this.startX = event.screenX;
|
||||||
|
this.startY = event.screenY;
|
||||||
|
this.moving = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseUp(event) {
|
||||||
|
if (event.button == 0) {
|
||||||
|
this.$refs.header.style.cursor = 'default';
|
||||||
|
this.moving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove(event) {
|
||||||
|
if (this.moving) {
|
||||||
|
const deltaX = event.screenX - this.startX;
|
||||||
|
const deltaY = event.screenY - this.startY;
|
||||||
|
this.startX = event.screenX;
|
||||||
|
this.startY = event.screenY;
|
||||||
|
|
||||||
|
this.$refs.windowBox.style.left = (this.$refs.windowBox.offsetLeft + deltaX) + 'px';
|
||||||
|
this.$refs.windowBox.style.top = (this.$refs.windowBox.offsetTop + deltaY) + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTouchStart(event) {
|
||||||
|
if (!this.$root.isMobileDevice)
|
||||||
|
return;
|
||||||
|
if (event.touches.length == 1) {
|
||||||
|
const touch = event.touches[0];
|
||||||
|
this.$refs.header.style.cursor = 'move';
|
||||||
|
this.startX = touch.screenX;
|
||||||
|
this.startY = touch.screenY;
|
||||||
|
this.moving = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTouchMove(event) {
|
||||||
|
if (!this.$root.isMobileDevice)
|
||||||
|
return;
|
||||||
|
if (event.touches.length == 1 && this.moving) {
|
||||||
|
const touch = event.touches[0];
|
||||||
|
const deltaX = touch.screenX - this.startX;
|
||||||
|
const deltaY = touch.screenY - this.startY;
|
||||||
|
this.startX = touch.screenX;
|
||||||
|
this.startY = touch.screenY;
|
||||||
|
|
||||||
|
this.$refs.windowBox.style.left = (this.$refs.windowBox.offsetLeft + deltaX) + 'px';
|
||||||
|
this.$refs.windowBox.style.top = (this.$refs.windowBox.offsetTop + deltaY) + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTouchEnd() {
|
||||||
|
if (!this.$root.isMobileDevice)
|
||||||
|
return;
|
||||||
|
this.$refs.header.style.cursor = 'default';
|
||||||
|
this.moving = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (!this.moving)
|
||||||
|
this.$emit('close');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default vueComponent(Window);
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.window {
|
.main {
|
||||||
|
background-color: transparent !important;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xyfit {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flexfit {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
}
|
||||||
flex-direction: column;
|
|
||||||
|
.window {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border: 3px double black;
|
border: 3px double black;
|
||||||
@@ -37,25 +153,29 @@ class Window extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
background: linear-gradient(to bottom right, #007000, #59B04F);
|
||||||
justify-content: flex-end;
|
|
||||||
background-color: #e5e7ea;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 40px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-text {
|
.header-text {
|
||||||
flex: 1;
|
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
color: #FFFFA0;
|
||||||
|
text-shadow: 2px 2px 5px #005000, 2px 1px 5px #005000;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-button {
|
.close-button {
|
||||||
display: flex;
|
width: 30px;
|
||||||
justify-content: center;
|
height: 30px;
|
||||||
align-items: center;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close-button:hover {
|
||||||
|
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,122 +0,0 @@
|
|||||||
import Vue from 'vue';
|
|
||||||
|
|
||||||
/*
|
|
||||||
import ElementUI from 'element-ui';
|
|
||||||
import './theme/index.css';
|
|
||||||
import locale from 'element-ui/lib/locale/lang/ru-RU';
|
|
||||||
|
|
||||||
Vue.use(ElementUI, { locale });
|
|
||||||
*/
|
|
||||||
|
|
||||||
//------------------------------------------------------
|
|
||||||
//import './theme/index.css';
|
|
||||||
|
|
||||||
import './theme/icon.css';
|
|
||||||
import './theme/tooltip.css';
|
|
||||||
|
|
||||||
import ElMenu from 'element-ui/lib/menu';
|
|
||||||
import './theme/menu.css';
|
|
||||||
|
|
||||||
import ElMenuItem from 'element-ui/lib/menu-item';
|
|
||||||
import './theme/menu-item.css';
|
|
||||||
|
|
||||||
import ElButton from 'element-ui/lib/button';
|
|
||||||
import './theme/button.css';
|
|
||||||
|
|
||||||
import ElButtonGroup from 'element-ui/lib/button-group';
|
|
||||||
import './theme/button-group.css';
|
|
||||||
|
|
||||||
import ElCheckbox from 'element-ui/lib/checkbox';
|
|
||||||
import './theme/checkbox.css';
|
|
||||||
|
|
||||||
import ElTabs from 'element-ui/lib/tabs';
|
|
||||||
import './theme/tabs.css';
|
|
||||||
|
|
||||||
import ElTabPane from 'element-ui/lib/tab-pane';
|
|
||||||
import './theme/tab-pane.css';
|
|
||||||
|
|
||||||
import ElTooltip from 'element-ui/lib/tooltip';
|
|
||||||
import './theme/tooltip.css';
|
|
||||||
|
|
||||||
import ElCol from 'element-ui/lib/col';
|
|
||||||
import './theme/col.css';
|
|
||||||
|
|
||||||
import ElContainer from 'element-ui/lib/container';
|
|
||||||
import './theme/container.css';
|
|
||||||
|
|
||||||
import ElAside from 'element-ui/lib/aside';
|
|
||||||
import './theme/aside.css';
|
|
||||||
|
|
||||||
import ElHeader from 'element-ui/lib/header';
|
|
||||||
import './theme/header.css';
|
|
||||||
|
|
||||||
import ElMain from 'element-ui/lib/main';
|
|
||||||
import './theme/main.css';
|
|
||||||
|
|
||||||
import ElInput from 'element-ui/lib/input';
|
|
||||||
import './theme/input.css';
|
|
||||||
|
|
||||||
import ElInputNumber from 'element-ui/lib/input-number';
|
|
||||||
import './theme/input-number.css';
|
|
||||||
|
|
||||||
import ElSelect from 'element-ui/lib/select';
|
|
||||||
import './theme/select.css';
|
|
||||||
|
|
||||||
import ElOption from 'element-ui/lib/option';
|
|
||||||
import './theme/option.css';
|
|
||||||
|
|
||||||
import ElTable from 'element-ui/lib/table';
|
|
||||||
import './theme/table.css';
|
|
||||||
|
|
||||||
import ElTableColumn from 'element-ui/lib/table-column';
|
|
||||||
import './theme/table-column.css';
|
|
||||||
|
|
||||||
import ElProgress from 'element-ui/lib/progress';
|
|
||||||
import './theme/progress.css';
|
|
||||||
|
|
||||||
import ElSlider from 'element-ui/lib/slider';
|
|
||||||
import './theme/slider.css';
|
|
||||||
|
|
||||||
import ElForm from 'element-ui/lib/form';
|
|
||||||
import './theme/form.css';
|
|
||||||
|
|
||||||
import ElFormItem from 'element-ui/lib/form-item';
|
|
||||||
import './theme/form-item.css';
|
|
||||||
|
|
||||||
import ElColorPicker from 'element-ui/lib/color-picker';
|
|
||||||
import './theme/color-picker.css';
|
|
||||||
|
|
||||||
import Notification from 'element-ui/lib/notification';
|
|
||||||
import './theme/notification.css';
|
|
||||||
|
|
||||||
import Loading from 'element-ui/lib/loading';
|
|
||||||
import './theme/loading.css';
|
|
||||||
|
|
||||||
import MessageBox from 'element-ui/lib/message-box';
|
|
||||||
import './theme/message-box.css';
|
|
||||||
|
|
||||||
const components = {
|
|
||||||
ElMenu, ElMenuItem, ElButton, ElButtonGroup, ElCheckbox, ElTabs, ElTabPane, ElTooltip,
|
|
||||||
ElCol, ElContainer, ElAside, ElMain, ElHeader,
|
|
||||||
ElInput, ElInputNumber, ElSelect, ElOption, ElTable, ElTableColumn,
|
|
||||||
ElProgress, ElSlider, ElForm, ElFormItem,
|
|
||||||
ElColorPicker,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let name in components) {
|
|
||||||
Vue.component(name, components[name]);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Vue.use(Loading.directive);
|
|
||||||
|
|
||||||
Vue.prototype.$loading = Loading.service;
|
|
||||||
Vue.prototype.$msgbox = MessageBox;
|
|
||||||
Vue.prototype.$alert = MessageBox.alert;
|
|
||||||
Vue.prototype.$confirm = MessageBox.confirm;
|
|
||||||
Vue.prototype.$prompt = MessageBox.prompt;
|
|
||||||
Vue.prototype.$notify = Notification;
|
|
||||||
//Vue.prototype.$message = Message;
|
|
||||||
|
|
||||||
import lang from 'element-ui/lib/locale/lang/ru-RU';
|
|
||||||
import locale from 'element-ui/lib/locale';
|
|
||||||
locale.use(lang);
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<title></title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<meta name="description" content="браузерная онлайн-читалка книг из интернета и библиотека">
|
<meta name="description" content="Браузерная онлайн-читалка книг. Поддерживаются форматы: fb2, html, txt, rtf, doc, docx, pdf, epub, mobi.">
|
||||||
<meta name="keywords" content="библиотека,онлайн,читалка,книги,читать,браузер,интернет">
|
<meta name="keywords" content="онлайн,читалка,fb2,книги,читать,браузер,интернет">
|
||||||
<title></title>
|
<script src="/sw-register.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import Vue from 'vue';
|
import { createApp } from 'vue';
|
||||||
import App from './components/App.vue';
|
|
||||||
|
|
||||||
import router from './router';
|
import router from './router';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import './element';
|
import q from './quasar';
|
||||||
|
|
||||||
//Vue.config.productionTip = false;
|
import App from './components/App.vue';
|
||||||
|
|
||||||
new Vue({
|
const app = createApp(App);
|
||||||
router,
|
|
||||||
store,
|
app.use(router);
|
||||||
render: h => h(App),
|
app.use(store);
|
||||||
}).$mount('#app');
|
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,17 +1,17 @@
|
|||||||
import Vue from 'vue';
|
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||||
import VueRouter from 'vue-router';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import App from './components/App.vue';
|
|
||||||
|
|
||||||
const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
|
const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
|
||||||
const Search = () => import('./components/CardIndex/Search/Search.vue');
|
const Search = () => import('./components/CardIndex/Search/Search.vue');
|
||||||
const Card = () => import('./components/CardIndex/Card/Card.vue');
|
const Card = () => import('./components/CardIndex/Card/Card.vue');
|
||||||
const Book = () => import('./components/CardIndex/Book/Book.vue');
|
const Book = () => import('./components/CardIndex/Book/Book.vue');
|
||||||
const History = () => import('./components/CardIndex/History/History.vue');
|
const History = () => import('./components/CardIndex/History/History.vue');
|
||||||
|
|
||||||
|
//немедленная загрузка
|
||||||
|
//import Reader from './components/Reader/Reader.vue';
|
||||||
const Reader = () => import('./components/Reader/Reader.vue');
|
const Reader = () => import('./components/Reader/Reader.vue');
|
||||||
//const Forum = () => import('./components/Forum/Forum.vue');
|
const ExternalLibs = () => import('./components/ExternalLibs/ExternalLibs.vue');
|
||||||
|
|
||||||
const Income = () => import('./components/Income/Income.vue');
|
const Income = () => import('./components/Income/Income.vue');
|
||||||
const Sources = () => import('./components/Sources/Sources.vue');
|
const Sources = () => import('./components/Sources/Sources.vue');
|
||||||
const Settings = () => import('./components/Settings/Settings.vue');
|
const Settings = () => import('./components/Settings/Settings.vue');
|
||||||
@@ -20,20 +20,22 @@ const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
|
|||||||
|
|
||||||
const myRoutes = [
|
const myRoutes = [
|
||||||
['/', null, null, '/cardindex'],
|
['/', null, null, '/cardindex'],
|
||||||
['/cardindex', CardIndex ],
|
['/cardindex', CardIndex],
|
||||||
['/cardindex~search', Search ],
|
['/cardindex~search', Search],
|
||||||
['/cardindex~card', Card ],
|
['/cardindex~card', Card],
|
||||||
['/cardindex~card/:authorId', Card ],
|
['/cardindex~card/:authorId', Card],
|
||||||
['/cardindex~book', Book ],
|
['/cardindex~book', Book],
|
||||||
['/cardindex~book/:bookId', Book ],
|
['/cardindex~book/:bookId', Book],
|
||||||
['/cardindex~history', History ],
|
['/cardindex~history', History],
|
||||||
|
|
||||||
['/reader', Reader ],
|
['/reader', Reader],
|
||||||
['/income', Income ],
|
['/external-libs', ExternalLibs],
|
||||||
['/sources', Sources ],
|
['/income', Income],
|
||||||
['/settings', Settings ],
|
['/sources', Sources],
|
||||||
['/help', Help ],
|
['/settings', Settings],
|
||||||
['*', null, null, '/cardindex' ],
|
['/help', Help],
|
||||||
|
['/404', NotFound404],
|
||||||
|
['/:pathMatch(.*)*', null, null, '/cardindex'],
|
||||||
];
|
];
|
||||||
|
|
||||||
let routes = {};
|
let routes = {};
|
||||||
@@ -60,8 +62,7 @@ for (let route of myRoutes) {
|
|||||||
}
|
}
|
||||||
routes = routes.children;
|
routes = routes.children;
|
||||||
|
|
||||||
Vue.use(VueRouter);
|
export default createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
export default new VueRouter({
|
|
||||||
routes
|
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;
|
||||||
26
client/share/cryptoUtils.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//WebCrypto API (crypto.subtle) не работает без https, поэтому приходится извращаться через sjcl
|
||||||
|
import sjclWrapper from './sjclWrapper';
|
||||||
|
|
||||||
|
//не менять
|
||||||
|
const iv = 'B6E2XejNh2dS';
|
||||||
|
const salt = 'Liberama project is awesome';
|
||||||
|
|
||||||
|
export function aesEncrypt(data, password) {
|
||||||
|
return sjclWrapper.codec.bytes.fromBits(
|
||||||
|
sjclWrapper.encryptArray(
|
||||||
|
password, sjclWrapper.codec.bytes.toBits(data), {iv, salt}
|
||||||
|
).ct
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function aesDecrypt(data, password) {
|
||||||
|
return sjclWrapper.codec.bytes.fromBits(
|
||||||
|
sjclWrapper.decryptArray(
|
||||||
|
password, {ct: sjclWrapper.codec.bytes.toBits(data)}, {iv, salt}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sha256(str) {
|
||||||
|
return sjclWrapper.codec.bytes.fromBits(sjclWrapper.hash.sha256.hash(str));
|
||||||
|
}
|
||||||